Merge branch 'getpelican:master' into Chinese-translation
|
|
@ -1,3 +1,2 @@
|
|||
[report]
|
||||
omit = pelican/tests/*
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ insert_final_newline = true
|
|||
trim_trailing_whitespace = true
|
||||
|
||||
[*.py]
|
||||
max_line_length = 79
|
||||
max_line_length = 88
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
|
|
|||
7
.git-blame-ignore-revs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# .git-blame-ignore-revs
|
||||
# Apply code style to project via: ruff format .
|
||||
cabdb26cee66e1173cf16cb31d3fe5f9fa4392e7
|
||||
# Upgrade code base for Python 3.8 and above
|
||||
ecd598f293161a52564aa6e8dfdcc8284dc93970
|
||||
# Apply Ruff and pyupgrade to Jinja templates
|
||||
db241feaa445375dc05e189e69287000ffe5fa8e
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -1,7 +1,8 @@
|
|||
---
|
||||
# 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
|
||||
- name: '💬 Pelican IRC Channel'
|
||||
url: https://web.libera.chat/?#pelican
|
||||
about: |
|
||||
Chat with the community, ask questions, and learn about best practices.
|
||||
|
|
|
|||
65
.github/workflows/github_pages.yml
vendored
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
name: Deploy to GitHub Pages
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
settings:
|
||||
required: true
|
||||
description: "The path to your Pelican settings file (`pelican`'s `--settings` option), for example: 'publishconf.py'"
|
||||
type: string
|
||||
requirements:
|
||||
required: false
|
||||
default: "pelican"
|
||||
description: "The Python requirements to install, for example to enable markdown and typogrify use: 'pelican[markdown] typogrify' or if you have a requirements file use: '-r requirements.txt'"
|
||||
type: string
|
||||
output-path:
|
||||
required: false
|
||||
default: "output/"
|
||||
description: "Where to output the generated files (`pelican`'s `--output` option)"
|
||||
type: string
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- name: Configure GitHub Pages
|
||||
id: pages
|
||||
uses: actions/configure-pages@v3
|
||||
- name: Install requirements
|
||||
run: pip install ${{ inputs.requirements }}
|
||||
- name: Build Pelican site
|
||||
run: |
|
||||
pelican \
|
||||
--settings "${{ inputs.settings }}" \
|
||||
--extra-settings SITEURL='"${{ steps.pages.outputs.base_url }}"' \
|
||||
--output "${{ inputs.output-path }}"
|
||||
- name: Fix permissions
|
||||
run: |
|
||||
chmod -c -R +rX "${{ inputs.output-path }}" | while read line; do
|
||||
echo "::warning title=Invalid file permissions automatically fixed::$line"
|
||||
done
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v2
|
||||
with:
|
||||
path: ${{ inputs.output-path }}
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v2
|
||||
73
.github/workflows/main.yml
vendored
|
|
@ -9,35 +9,25 @@ env:
|
|||
|
||||
jobs:
|
||||
test:
|
||||
name: Test - ${{ matrix.config.python }} - ${{ matrix.config.os }}
|
||||
runs-on: ${{ matrix.config.os }}-latest
|
||||
name: Test - ${{ matrix.python }} - ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
config:
|
||||
- os: ubuntu
|
||||
python: "3.7"
|
||||
os: [ubuntu, macos, windows]
|
||||
python: ["3.10", "3.11", "3.12"]
|
||||
include:
|
||||
- os: ubuntu
|
||||
python: "3.8"
|
||||
- os: ubuntu
|
||||
python: "3.9"
|
||||
- os: ubuntu
|
||||
python: "3.10"
|
||||
- os: ubuntu
|
||||
python: "3.11"
|
||||
- os: ubuntu
|
||||
python: "3.12"
|
||||
- os: macos
|
||||
python: "3.7"
|
||||
- os: windows
|
||||
python: "3.7"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.config.python }}
|
||||
- name: Set up Python ${{ matrix.python }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.config.python }}
|
||||
python-version: ${{ matrix.python }}
|
||||
cache: "pip"
|
||||
cache-dependency-path: "**/requirements/*"
|
||||
- name: Install locale (Linux)
|
||||
|
|
@ -56,24 +46,42 @@ jobs:
|
|||
echo "===== PANDOC ====="
|
||||
pandoc --version | head -2
|
||||
- name: Run tests
|
||||
run: tox -e py${{ matrix.config.python }}
|
||||
run: tox -e py${{ matrix.python }}
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
- uses: pdm-project/setup-pdm@v3
|
||||
with:
|
||||
python-version: "3.9"
|
||||
cache: "pip"
|
||||
cache-dependency-path: "**/requirements/*"
|
||||
- name: Install tox
|
||||
run: python -m pip install -U pip tox
|
||||
- name: Check
|
||||
run: tox -e flake8
|
||||
python-version: 3.9
|
||||
cache: true
|
||||
cache-dependency-path: ./pyproject.toml
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pdm install --no-default --dev
|
||||
- name: Run linters
|
||||
run: pdm lint --diff
|
||||
- name: Run pre-commit checks on all files
|
||||
uses: pre-commit/action@v3.0.0
|
||||
|
||||
build:
|
||||
name: Test build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: pdm-project/setup-pdm@v3
|
||||
with:
|
||||
python-version: 3.9
|
||||
cache: true
|
||||
cache-dependency-path: ./pyproject.toml
|
||||
- name: Install dependencies
|
||||
run: pdm install --dev
|
||||
- name: Build package
|
||||
run: pdm build
|
||||
- name: Test build
|
||||
run: pdm run pytest --check-build=dist pelican/tests/build_test
|
||||
|
||||
docs:
|
||||
name: Build docs
|
||||
|
|
@ -91,13 +99,18 @@ jobs:
|
|||
run: python -m pip install -U pip tox
|
||||
- name: Check
|
||||
run: tox -e docs
|
||||
- name: cache the docs for inspection
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: docs
|
||||
path: docs/_build/html/
|
||||
|
||||
deploy:
|
||||
name: Deploy
|
||||
environment: Deployment
|
||||
needs: [test, lint, docs]
|
||||
needs: [test, lint, docs, build]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref=='refs/heads/master' && github.event_name!='pull_request'
|
||||
if: github.ref=='refs/heads/master' && github.event_name!='pull_request' && github.repository == 'getpelican/pelican'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
|
|
|||
4
.gitignore
vendored
|
|
@ -15,4 +15,6 @@ htmlcov
|
|||
venv
|
||||
samples/output
|
||||
*.pem
|
||||
poetry.lock
|
||||
*.lock
|
||||
.pdm-python
|
||||
.venv
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# See https://pre-commit.com/hooks.html for info on hooks
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
rev: v4.5.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: check-ast
|
||||
|
|
@ -13,13 +13,11 @@ repos:
|
|||
- id: end-of-file-fixer
|
||||
- id: forbid-new-submodules
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 3.9.2
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.1.5
|
||||
hooks:
|
||||
- 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']
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
args: ["--check"]
|
||||
|
||||
exclude: ^pelican/tests/output/
|
||||
|
|
|
|||
|
|
@ -79,6 +79,10 @@ 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 ':pelican-doc:`plugins` — you can `ask for help`_ to make that determination.
|
||||
|
||||
Also, if you intend to submit a pull request to address something for which there
|
||||
is no existing issue, there is no need to create a new issue and then immediately
|
||||
submit a pull request that closes it. You can submit the pull request by itself.
|
||||
|
||||
Using Git and GitHub
|
||||
--------------------
|
||||
|
||||
|
|
@ -87,7 +91,8 @@ Using Git and GitHub
|
|||
* **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.
|
||||
request** for the bugfix. Similarly, any proposed changes to code style
|
||||
formatting should be in a completely separate pull request.
|
||||
* 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::
|
||||
|
|
@ -106,15 +111,8 @@ Using Git and GitHub
|
|||
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 <https://github.com/github/hub/#installation>`_ and then run
|
||||
`hub pull-request -i [ISSUE] <https://hub.github.com/hub-pull-request.1.html>`_
|
||||
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
|
||||
will run the test suite on all supported Python versions and check for code style
|
||||
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.)
|
||||
|
|
@ -122,13 +120,7 @@ Using Git and GitHub
|
|||
Contribution quality standards
|
||||
------------------------------
|
||||
|
||||
* Adhere to `PEP8 coding standards`_. This can be eased via the `pycodestyle
|
||||
<https://pypi.org/project/pycodestyle>`_ or `flake8
|
||||
<https://pypi.org/project/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.
|
||||
* Adhere to the project's code style standards. See: `Development Environment`_
|
||||
* 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.
|
||||
|
|
@ -142,6 +134,6 @@ need assistance or have any questions about these guidelines.
|
|||
.. _`Create a new 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
|
||||
.. _`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
|
||||
.. _`Development Environment`: https://docs.getpelican.com/en/latest/contribute.html#setting-up-the-development-environment
|
||||
.. _`officially-supported Python releases`: https://devguide.python.org/versions/#versions
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
include *.rst
|
||||
recursive-include pelican *.html *.css *png *.rst *.markdown *.md *.mkd *.xml *.py *.jinja2
|
||||
include LICENSE THANKS docs/changelog.rst pyproject.toml
|
||||
graft samples
|
||||
global-exclude __pycache__
|
||||
global-exclude *.py[co]
|
||||
2
docs/_static/pelican-logo.svg
vendored
|
|
@ -1 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><svg id="svg33" width="64" height="64" style="clip-rule:evenodd;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.4142" version="1.1" viewBox="0 0 64 64" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g id="g864" transform="matrix(.10228 0 0 .10228 2.441 6.0098e-5)"><g id="g4" transform="matrix(4.1667 0 0 4.1667 -301.27 -2392.2)"><path id="path2" d="m210.35 607.22c-.34-2.106-.842-4.303-1.491-6.591-1.537-5.441-4.918-10.074-9.506-13.854-1.205-1.001-2.503-1.946-3.88-2.823-5.293-3.381-11.692-5.851-18.522-7.32-4.588-.99-9.367-1.525-14.139-1.593-34.662-.774-56.234.387-90.373-.911.012.023.012.046.022.068 1.56 1.264 3.154 2.471 4.782 3.643 3.573 2.584 7.297 4.952 11.155 7.127 7.184 4.04 14.845 7.342 22.859 9.801.956.295 1.912.58 2.87.842 5.6.603 10.631 1.206 14.648 3.074 1.015.455 1.959 1.001 2.835 1.639 2.87 2.106 6.057 6.124 8.152 8.936 4.497 5.999 3.551 10.928 8.88 13.887.557.308 1.182.604 1.889.866 1.696.638 4.119 1.491 5.225-.91.16-.342.283-.764.387-1.264-.446-1.434-1.981-2.675-2.905-3.29-1.638-1.07-2.287-1.719-3.47-2.937-2.186-2.243-2.333-6.056-3.871-8.708 1.935-.82 12.146-2.186 14.287-1.89 4.576.204 8.185.557 10.939 3.392 1.08.854 1.672 1.594 2.652 2.334.069.057.125.114.194.159 4.338 3.153 8.343 4.28 11.894 5.362.936.284 1.822.558 2.69.876 1.332.478 2.582 1.048 3.754 1.81 1.39.922 3.748 3.336 3.849 5.419-3.496-1.116-1.185.296-6.342-.102-2.515-.285-5.087-.456-7.671-.638-4.018-.284-8.038-.581-11.805-1.297-.627-.115-1.254-.251-1.867-.399-.479-.102-.946-.227-1.401-.353-.011.193-.021.376-.021.546-.104 3.939 2.674 5.908-3.678 13.399-.057.08-.137.159-.205.25-1.686 1.97-10.449 5.715-13.182 6.432-11.634 2.334-20.502-5.237-34.515-1.423-4.929 1.833-8.549 9.824-10.815 15.8-3.016 7.936-5.406 17.576-8.139 27.06 5.329-.797 10.53-1.936 15.585-3.427 11.167-3.279 21.651-8.185 31.168-14.445.911-1.231 1.912-2.29 2.994-3.108.284-.217.58-.422.877-.603.215-.137.956-.286 2.127-.502 10.861-1.924 58.5-8.377 61.597-42.962.319-3.494.172-7.285-.513-11.372zm-106.94 18.59c-6.375-1.924-8.003-2.243-12.055-5.385.067.33.17.695.307 1.081 10.779 6.068 22.608 10.462 35.141 12.842-3.893-9.051-8.502-7.445-23.393-8.538zm29.518-4.099c-2.779-6.738-10.313-10.575-16.813-12.464-8.721-3.12-15.061-.125-33.458-8.811.147.239.284.467.432.694 3.575 2.584 7.297 4.963 11.157 7.126 7.184 4.041 14.844 7.343 22.857 9.802 4.167.489 8.175 1.184 11.863 2.96 1.639.773 3.21 1.764 4.702 3.039-.183-.82-.434-1.605-.74-2.346z" style="fill-rule:nonzero;fill:url(#_Linear1)"/></g><g id="g8" transform="matrix(4.1667 0 0 4.1667 -301.27 -2392.2)"><path id="path6" d="m114.13 595.61c-.958-.262-1.914-.547-2.87-.842-8.014-2.459-15.675-5.761-22.859-9.801-3.858-2.175-7.582-4.543-11.155-7.127-1.628-1.172-3.222-2.379-4.782-3.643 2.14 6.603 11.634 13.57 18.078 16.313 8.218 3.495 16.381 4.303 23.588 5.1z" style="fill:#90d4d1"/></g><g id="g12" transform="matrix(4.1667 0 0 4.1667 -301.27 -2392.2)"><path id="path10" d="m94.253 608.25c-3.86-2.163-7.582-4.542-11.157-7.126 10.006 15.823 22.575 15.584 34.014 16.928-8.013-2.459-15.673-5.761-22.857-9.802z" style="fill:#90d4d1"/></g><g id="g16" transform="matrix(4.1667 0 0 4.1667 -301.27 -2392.2)"><path id="path14" d="m126.81 634.34c-12.533-2.38-24.362-6.774-35.141-12.842 1.376 3.973 6.351 10.257 12.943 11.658 2.858 1.024 2.094.762 6.967.614 7.137-.364 10.552-.592 15.608 1.469-.126-.308-.251-.604-.377-.899z" style="fill:#90d4d1"/></g><g id="g20" transform="matrix(4.1667 0 0 4.1667 -301.27 -2392.2)"><path id="path18" d="m143.27 665.76c-.081.101-.159.204-.239.318-13.844 14.093-31.179 24.69-50.59 30.393 1.492-4.132 2.824-8.468 4.076-12.839 5.329-.797 10.53-1.936 15.585-3.427 11.167-3.279 21.651-8.185 31.168-14.445z" style="fill:#90d4d1"/></g><g id="g24" transform="matrix(4.1667 0 0 4.1667 -301.27 -2392.2)"><path id="path22" d="m143.03 666.08c-6.046 8.287-9.118 24.122-12.659 33.274-5.144 13.342-12.294 22.95-27.958 24.317-3.928.351-27.582 1.24-30.11-.035.159-1.344 4.098-2.961 5.123-3.747 6.852-4.847 11.416-13.5 15.014-23.416 19.411-5.703 36.746-16.3 50.59-30.393z" style="fill:#14a0c4"/></g></g><defs id="defs31"><linearGradient id="_Linear1" x2="1" gradientTransform="matrix(138.58 0 0 138.58 72.442 628.88)" gradientUnits="userSpaceOnUse"><stop id="stop26" style="stop-color:rgb(84,196,198)" offset="0"/><stop id="stop28" style="stop-color:rgb(18,186,213)" offset="1"/></linearGradient></defs></svg>
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="svg33" width="64" height="64" style="clip-rule:evenodd;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.4142" version="1.1" viewBox="0 0 64 64" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g id="g864" transform="matrix(.10228 0 0 .10228 2.441 6.0098e-5)"><g id="g4" transform="matrix(4.1667 0 0 4.1667 -301.27 -2392.2)"><path id="path2" d="m210.35 607.22c-.34-2.106-.842-4.303-1.491-6.591-1.537-5.441-4.918-10.074-9.506-13.854-1.205-1.001-2.503-1.946-3.88-2.823-5.293-3.381-11.692-5.851-18.522-7.32-4.588-.99-9.367-1.525-14.139-1.593-34.662-.774-56.234.387-90.373-.911.012.023.012.046.022.068 1.56 1.264 3.154 2.471 4.782 3.643 3.573 2.584 7.297 4.952 11.155 7.127 7.184 4.04 14.845 7.342 22.859 9.801.956.295 1.912.58 2.87.842 5.6.603 10.631 1.206 14.648 3.074 1.015.455 1.959 1.001 2.835 1.639 2.87 2.106 6.057 6.124 8.152 8.936 4.497 5.999 3.551 10.928 8.88 13.887.557.308 1.182.604 1.889.866 1.696.638 4.119 1.491 5.225-.91.16-.342.283-.764.387-1.264-.446-1.434-1.981-2.675-2.905-3.29-1.638-1.07-2.287-1.719-3.47-2.937-2.186-2.243-2.333-6.056-3.871-8.708 1.935-.82 12.146-2.186 14.287-1.89 4.576.204 8.185.557 10.939 3.392 1.08.854 1.672 1.594 2.652 2.334.069.057.125.114.194.159 4.338 3.153 8.343 4.28 11.894 5.362.936.284 1.822.558 2.69.876 1.332.478 2.582 1.048 3.754 1.81 1.39.922 3.748 3.336 3.849 5.419-3.496-1.116-1.185.296-6.342-.102-2.515-.285-5.087-.456-7.671-.638-4.018-.284-8.038-.581-11.805-1.297-.627-.115-1.254-.251-1.867-.399-.479-.102-.946-.227-1.401-.353-.011.193-.021.376-.021.546-.104 3.939 2.674 5.908-3.678 13.399-.057.08-.137.159-.205.25-1.686 1.97-10.449 5.715-13.182 6.432-11.634 2.334-20.502-5.237-34.515-1.423-4.929 1.833-8.549 9.824-10.815 15.8-3.016 7.936-5.406 17.576-8.139 27.06 5.329-.797 10.53-1.936 15.585-3.427 11.167-3.279 21.651-8.185 31.168-14.445.911-1.231 1.912-2.29 2.994-3.108.284-.217.58-.422.877-.603.215-.137.956-.286 2.127-.502 10.861-1.924 58.5-8.377 61.597-42.962.319-3.494.172-7.285-.513-11.372zm-106.94 18.59c-6.375-1.924-8.003-2.243-12.055-5.385.067.33.17.695.307 1.081 10.779 6.068 22.608 10.462 35.141 12.842-3.893-9.051-8.502-7.445-23.393-8.538zm29.518-4.099c-2.779-6.738-10.313-10.575-16.813-12.464-8.721-3.12-15.061-.125-33.458-8.811.147.239.284.467.432.694 3.575 2.584 7.297 4.963 11.157 7.126 7.184 4.041 14.844 7.343 22.857 9.802 4.167.489 8.175 1.184 11.863 2.96 1.639.773 3.21 1.764 4.702 3.039-.183-.82-.434-1.605-.74-2.346z" style="fill-rule:nonzero;fill:url(#_Linear1)"/></g><g id="g8" transform="matrix(4.1667 0 0 4.1667 -301.27 -2392.2)"><path id="path6" d="m114.13 595.61c-.958-.262-1.914-.547-2.87-.842-8.014-2.459-15.675-5.761-22.859-9.801-3.858-2.175-7.582-4.543-11.155-7.127-1.628-1.172-3.222-2.379-4.782-3.643 2.14 6.603 11.634 13.57 18.078 16.313 8.218 3.495 16.381 4.303 23.588 5.1z" style="fill:#90d4d1"/></g><g id="g12" transform="matrix(4.1667 0 0 4.1667 -301.27 -2392.2)"><path id="path10" d="m94.253 608.25c-3.86-2.163-7.582-4.542-11.157-7.126 10.006 15.823 22.575 15.584 34.014 16.928-8.013-2.459-15.673-5.761-22.857-9.802z" style="fill:#90d4d1"/></g><g id="g16" transform="matrix(4.1667 0 0 4.1667 -301.27 -2392.2)"><path id="path14" d="m126.81 634.34c-12.533-2.38-24.362-6.774-35.141-12.842 1.376 3.973 6.351 10.257 12.943 11.658 2.858 1.024 2.094.762 6.967.614 7.137-.364 10.552-.592 15.608 1.469-.126-.308-.251-.604-.377-.899z" style="fill:#90d4d1"/></g><g id="g20" transform="matrix(4.1667 0 0 4.1667 -301.27 -2392.2)"><path id="path18" d="m143.27 665.76c-.081.101-.159.204-.239.318-13.844 14.093-31.179 24.69-50.59 30.393 1.492-4.132 2.824-8.468 4.076-12.839 5.329-.797 10.53-1.936 15.585-3.427 11.167-3.279 21.651-8.185 31.168-14.445z" style="fill:#90d4d1"/></g><g id="g24" transform="matrix(4.1667 0 0 4.1667 -301.27 -2392.2)"><path id="path22" d="m143.03 666.08c-6.046 8.287-9.118 24.122-12.659 33.274-5.144 13.342-12.294 22.95-27.958 24.317-3.928.351-27.582 1.24-30.11-.035.159-1.344 4.098-2.961 5.123-3.747 6.852-4.847 11.416-13.5 15.014-23.416 19.411-5.703 36.746-16.3 50.59-30.393z" style="fill:#14a0c4"/></g></g><defs id="defs31"><linearGradient id="_Linear1" x2="1" gradientTransform="matrix(138.58 0 0 138.58 72.442 628.88)" gradientUnits="userSpaceOnUse"><stop id="stop26" style="stop-color:rgb(84,196,198)" offset="0"/><stop id="stop28" style="stop-color:rgb(18,186,213)" offset="1"/></linearGradient></defs></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
1
docs/_static/theme_overrides.css
vendored
|
|
@ -9,4 +9,3 @@
|
|||
.wy-table-responsive {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,34 @@
|
|||
Release history
|
||||
###############
|
||||
|
||||
4.9.1 - 2023-11-15
|
||||
==================
|
||||
|
||||
* Ensure ``tzdata`` dependency is installed on Windows
|
||||
|
||||
4.9.0 - 2023-11-12
|
||||
==================
|
||||
|
||||
* Upgrade code to new minimum supported Python version: 3.8
|
||||
* Settings support for ``pathlib.Path`` `(#2758) <https://github.com/getpelican/pelican/pull/2758>`_
|
||||
* Various improvements to Simple theme (`#2976 <https://github.com/getpelican/pelican/pull/2976>`_ & `#3234 <https://github.com/getpelican/pelican/pull/3234>`_)
|
||||
* Use Furo as Sphinx documentation theme `(#3023) <https://github.com/getpelican/pelican/pull/3023>`_
|
||||
* Default to 100 articles maximum in feeds `(#3127) <https://github.com/getpelican/pelican/pull/3127>`_
|
||||
* Add ``period_archives common context`` variable `(#3148) <https://github.com/getpelican/pelican/pull/3148>`_
|
||||
* Use ``watchfiles`` as the file-watching backend `(#3151) <https://github.com/getpelican/pelican/pull/3151>`_
|
||||
* Add GitHub Actions workflow for GitHub Pages `(#3189) <https://github.com/getpelican/pelican/pull/3189>`_
|
||||
* Allow dataclasses in settings `(#3204) <https://github.com/getpelican/pelican/pull/3204>`_
|
||||
* Switch build tool to PDM instead of Setuptools/Poetry `(#3220) <https://github.com/getpelican/pelican/pull/3220>`_
|
||||
* Provide a ``plugin_enabled`` Jinja test for themes `(#3235) <https://github.com/getpelican/pelican/pull/3235>`_
|
||||
* Preserve connection order in Blinker `(#3238) <https://github.com/getpelican/pelican/pull/3238>`_
|
||||
* Remove social icons from default ``notmyidea`` theme `(#3240) <https://github.com/getpelican/pelican/pull/3240>`_
|
||||
* Remove unreliable ``WRITE_SELECTED`` feature `(#3243) <https://github.com/getpelican/pelican/pull/3243>`_
|
||||
* Importer: Report broken embedded video links when importing from Tumblr `(#3177) <https://github.com/getpelican/pelican/issues/3177>`_
|
||||
* Importer: Remove newline addition when iterating Photo post types `(#3178) <https://github.com/getpelican/pelican/issues/3178>`_
|
||||
* Importer: Force timestamp conversion in Tumblr importer to be UTC with offset `(#3221) <https://github.com/getpelican/pelican/pull/3221>`_
|
||||
* Importer: Use tempfile for intermediate HTML file for Pandoc `(#3221) <https://github.com/getpelican/pelican/pull/3221>`_
|
||||
* Switch linters to Ruff `(#3223) <https://github.com/getpelican/pelican/pull/3223>`_
|
||||
|
||||
4.8.0 - 2022-07-11
|
||||
==================
|
||||
|
||||
|
|
|
|||
95
docs/conf.py
|
|
@ -2,45 +2,58 @@ import datetime
|
|||
import os
|
||||
import sys
|
||||
|
||||
from pelican import __version__
|
||||
if sys.version_info >= (3, 11):
|
||||
import tomllib
|
||||
else:
|
||||
import tomli as tomllib
|
||||
|
||||
|
||||
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'
|
||||
year = datetime.datetime.now().date().year
|
||||
copyright = f'2010–{year}'
|
||||
exclude_patterns = ['_build']
|
||||
release = __version__
|
||||
version = '.'.join(release.split('.')[:1])
|
||||
last_stable = __version__
|
||||
rst_prolog = '''
|
||||
.. |last_stable| replace:: :pelican-doc:`{}`
|
||||
'''.format(last_stable)
|
||||
|
||||
extlinks = {
|
||||
'pelican-doc': ('https://docs.getpelican.com/en/latest/%s.html', '%s')
|
||||
}
|
||||
with open("../pyproject.toml", "rb") as f:
|
||||
project_data = tomllib.load(f).get("project")
|
||||
if project_data is None:
|
||||
raise KeyError("project data is not found")
|
||||
|
||||
|
||||
# -- General configuration ----------------------------------------------------
|
||||
templates_path = ["_templates"]
|
||||
extensions = [
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.ifconfig",
|
||||
"sphinx.ext.extlinks",
|
||||
"sphinxext.opengraph",
|
||||
]
|
||||
source_suffix = ".rst"
|
||||
master_doc = "index"
|
||||
project = project_data.get("name").upper()
|
||||
year = datetime.datetime.now().date().year
|
||||
copyright = f"2010–{year}"
|
||||
exclude_patterns = ["_build"]
|
||||
release = project_data.get("version")
|
||||
version = ".".join(release.split(".")[:1])
|
||||
last_stable = project_data.get("version")
|
||||
rst_prolog = f"""
|
||||
.. |last_stable| replace:: :pelican-doc:`{last_stable}`
|
||||
.. |min_python| replace:: {project_data.get('requires-python').split(",")[0]}
|
||||
"""
|
||||
|
||||
extlinks = {"pelican-doc": ("https://docs.getpelican.com/en/latest/%s.html", "%s")}
|
||||
|
||||
# -- Options for HTML output --------------------------------------------------
|
||||
|
||||
html_theme = 'furo'
|
||||
html_title = f'<strong>{project}</strong> <i>{release}</i>'
|
||||
html_static_path = ['_static']
|
||||
html_theme = "furo"
|
||||
html_title = f"<strong>{project}</strong> <i>{release}</i>"
|
||||
html_static_path = ["_static"]
|
||||
html_theme_options = {
|
||||
'light_logo': 'pelican-logo.svg',
|
||||
'dark_logo': 'pelican-logo.svg',
|
||||
'navigation_with_keys': True,
|
||||
"light_logo": "pelican-logo.svg",
|
||||
"dark_logo": "pelican-logo.svg",
|
||||
"navigation_with_keys": True,
|
||||
}
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'Pelicandoc'
|
||||
htmlhelp_basename = "Pelicandoc"
|
||||
|
||||
html_use_smartypants = True
|
||||
|
||||
|
|
@ -56,21 +69,29 @@ 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
|
||||
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'),
|
||||
("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)
|
||||
("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,
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -15,16 +15,16 @@ 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
|
||||
instructions will utilize Pip_ and PDM_. 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.7+ is required for Pelican development.
|
||||
Please note that Python |min_python| is required for Pelican development.
|
||||
|
||||
*(Optional)* If you prefer to `install Poetry <https://python-poetry.org/docs/master/#installation>`_ once for use with multiple projects,
|
||||
*(Optional)* If you prefer to `install PDM <https://pdm.fming.dev/latest/#installation>`_ once for use with multiple projects,
|
||||
you can install it via::
|
||||
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
curl -sSL https://pdm.fming.dev/install-pdm.py | python3 -
|
||||
|
||||
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
|
||||
|
|
@ -35,7 +35,7 @@ as a Git remote::
|
|||
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
|
||||
While PDM can dynamically create and manage virtual environments, we're going
|
||||
to manually create and activate a virtual environment::
|
||||
|
||||
mkdir ~/virtualenvs && cd ~/virtualenvs
|
||||
|
|
@ -46,12 +46,11 @@ 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://python-poetry.org/
|
||||
.. _PDM: https://pdm.fming.dev/latest/
|
||||
.. _Pelican repository: https://github.com/getpelican/pelican
|
||||
|
||||
Development
|
||||
|
|
@ -75,6 +74,9 @@ via::
|
|||
|
||||
invoke tests
|
||||
|
||||
(For more on Invoke, see ``invoke -l`` to list tasks, or
|
||||
https://pyinvoke.org for documentation.)
|
||||
|
||||
In addition to running the test suite, it is important to also ensure that any
|
||||
lines you changed conform to code style guidelines. You can check that via::
|
||||
|
||||
|
|
@ -156,8 +158,7 @@ check for code style compliance via::
|
|||
|
||||
If style violations are found, many of them can be addressed automatically via::
|
||||
|
||||
invoke black
|
||||
invoke isort
|
||||
invoke format
|
||||
|
||||
If style violations are found even after running the above auto-formatters,
|
||||
you will need to make additional manual changes until ``invoke lint`` no longer
|
||||
|
|
|
|||
|
|
@ -217,10 +217,6 @@ 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?
|
||||
=============================================
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ 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
|
||||
|
|
@ -48,16 +47,15 @@ Usage
|
|||
|
||||
::
|
||||
|
||||
pelican-import [-h] [--blogger] [--dotclear] [--posterous] [--tumblr] [--wpfile] [--feed]
|
||||
pelican-import [-h] [--blogger] [--dotclear] [--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
|
||||
[--wp-attach] [--disable-slugs] [-b BLOGNAME]
|
||||
input|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
|
||||
============= ============================================================================
|
||||
|
||||
|
|
@ -67,7 +65,6 @@ 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)
|
||||
|
|
@ -101,10 +98,6 @@ Optional arguments
|
|||
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
|
||||
|
||||
|
|
@ -120,13 +113,9 @@ For Dotclear::
|
|||
|
||||
$ pelican-import --dotclear -o ~/output ~/backup.txt
|
||||
|
||||
for Posterous::
|
||||
|
||||
$ pelican-import --posterous -o ~/output --email=<email_address> --password=<password> <api_token>
|
||||
|
||||
For Tumblr::
|
||||
|
||||
$ pelican-import --tumblr -o ~/output --blogname=<blogname> <api_token>
|
||||
$ pelican-import --tumblr -o ~/output --blogname=<blogname> <api_key>
|
||||
|
||||
For WordPress::
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
Installing Pelican
|
||||
##################
|
||||
|
||||
Pelican currently runs best on 3.7+; earlier versions of Python are not supported.
|
||||
Pelican currently runs best on |min_python|; earlier versions of Python are not supported.
|
||||
|
||||
You can install Pelican via several different methods. The simplest is via Pip_::
|
||||
|
||||
|
|
|
|||
|
|
@ -94,9 +94,12 @@ which you map the signals to your plugin logic. Let's take a simple example::
|
|||
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.
|
||||
If multiple plugins connect to the same signal, plugins will be executed in the
|
||||
order they are connected. With ``PLUGINS`` setting, order will be as defined in
|
||||
the setting. If you rely on auto-discovered namespace plugins, no ``PLUGINS``
|
||||
setting, they will be connected in the same order they are discovered (same
|
||||
order as ``pelican-plugins`` output). If you want to specify the order
|
||||
explicitly, disable auto-discovery by defining ``PLUGINS`` in the desired order.
|
||||
|
||||
Namespace plugin structure
|
||||
--------------------------
|
||||
|
|
@ -341,4 +344,3 @@ custom article, using the ``article_generator_pretaxonomy`` signal::
|
|||
|
||||
.. _Pip: https://pip.pypa.io/
|
||||
.. _pelican-plugins bug #314: https://github.com/getpelican/pelican-plugins/issues/314
|
||||
.. _Blinker: https://pythonhosted.org/blinker/
|
||||
|
|
|
|||
|
|
@ -18,18 +18,6 @@ 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
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ Installation
|
|||
------------
|
||||
|
||||
Install Pelican (and optionally Markdown if you intend to use it) on Python
|
||||
3.7+ by running the following command in your preferred terminal, prefixing
|
||||
|min_python| by running the following command in your preferred terminal, prefixing
|
||||
with ``sudo`` if permissions warrant::
|
||||
|
||||
python -m pip install "pelican[markdown]"
|
||||
|
|
|
|||
|
|
@ -362,13 +362,6 @@ Basic settings
|
|||
|
||||
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
|
||||
|
|
@ -564,44 +557,47 @@ written over time.
|
|||
Example usage::
|
||||
|
||||
YEAR_ARCHIVE_SAVE_AS = 'posts/{date:%Y}/index.html'
|
||||
YEAR_ARCHIVE_URL = 'posts/{date:%Y}/'
|
||||
MONTH_ARCHIVE_SAVE_AS = 'posts/{date:%Y}/{date:%b}/index.html'
|
||||
MONTH_ARCHIVE_URL = 'posts/{date:%Y}/{date:%b}/'
|
||||
|
||||
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``.
|
||||
posts for the month at ``posts/2011/Aug/index.html``. These can be accessed
|
||||
through the URLs ``posts/2011/`` and ``posts/2011/Aug/``, respectively.
|
||||
|
||||
.. 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 = ''
|
||||
.. data:: YEAR_ARCHIVE_URL = ''
|
||||
|
||||
The URL to use for per-month archives of your posts. Used only if you have
|
||||
the ``{url}`` placeholder in ``PAGINATION_PATTERNS``.
|
||||
The URL to use for per-year archives of your posts. You should set this if
|
||||
you enable per-year archives.
|
||||
|
||||
.. data:: MONTH_ARCHIVE_SAVE_AS = ''
|
||||
|
||||
The location to save per-month archives of your posts.
|
||||
|
||||
.. data:: DAY_ARCHIVE_URL = ''
|
||||
.. data:: MONTH_ARCHIVE_URL = ''
|
||||
|
||||
The URL to use for per-day archives of your posts. Used only if you have the
|
||||
``{url}`` placeholder in ``PAGINATION_PATTERNS``.
|
||||
The URL to use for per-month archives of your posts. You should set this if
|
||||
you enable per-month archives.
|
||||
|
||||
.. data:: DAY_ARCHIVE_SAVE_AS = ''
|
||||
|
||||
The location to save per-day archives of your posts.
|
||||
|
||||
.. data:: DAY_ARCHIVE_URL = ''
|
||||
|
||||
The URL to use for per-day archives of your posts. You should set this if
|
||||
you enable per-day archives.
|
||||
|
||||
``DIRECT_TEMPLATES`` work a bit differently than noted above. Only the
|
||||
``_SAVE_AS`` settings are available, but it is available for any direct
|
||||
template.
|
||||
|
|
@ -1001,10 +997,10 @@ the ``TAG_FEED_ATOM`` and ``TAG_FEED_RSS`` settings:
|
|||
placeholder. If not set, ``TAG_FEED_RSS`` is used both for save location and
|
||||
URL.
|
||||
|
||||
.. data:: FEED_MAX_ITEMS
|
||||
.. data:: FEED_MAX_ITEMS = 100
|
||||
|
||||
Maximum number of items allowed in a feed. Feed item quantity is
|
||||
unrestricted by default.
|
||||
Maximum number of items allowed in a feed. Setting to ``None`` will cause the
|
||||
feed to contains every article. 100 if not specified.
|
||||
|
||||
.. data:: RSS_FEED_SUMMARY_ONLY = True
|
||||
|
||||
|
|
@ -1228,6 +1224,12 @@ Following are example ways to specify your preferred theme::
|
|||
# Specify a customized theme, via absolute path
|
||||
THEME = "/home/myuser/projects/mysite/themes/mycustomtheme"
|
||||
|
||||
The built-in ``simple`` theme can be customized using the following settings.
|
||||
|
||||
.. data:: STYLESHEET_URL
|
||||
|
||||
The URL of the stylesheet to use.
|
||||
|
||||
The built-in ``notmyidea`` theme can make good use of the following settings.
|
||||
Feel free to use them in your themes as well.
|
||||
|
||||
|
|
@ -1391,21 +1393,6 @@ 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
|
||||
================
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@ 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:
|
||||
|
||||
Common Variables
|
||||
----------------
|
||||
|
||||
|
|
@ -92,6 +94,10 @@ dates The same list of articles, but ordered by date,
|
|||
ascending.
|
||||
hidden_articles The list of hidden articles
|
||||
drafts The list of draft articles
|
||||
period_archives A dictionary containing elements related to
|
||||
time-period archives (if enabled). See the section
|
||||
:ref:`Listing and Linking to Period Archives
|
||||
<period_archives_variable>` for details.
|
||||
authors A list of (author, articles) tuples, containing all
|
||||
the authors and corresponding articles (values)
|
||||
categories A list of (category, articles) tuples, containing
|
||||
|
|
@ -134,6 +140,23 @@ your date according to the locale given in your settings::
|
|||
.. _datetime: https://docs.python.org/3/library/datetime.html#datetime-objects
|
||||
.. _strftime: https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior
|
||||
|
||||
Checking Loaded Plugins
|
||||
-----------------------
|
||||
|
||||
Pelican provides a ``plugin_enabled`` Jinja test for checking if a certain plugin
|
||||
is enabled. This test accepts a plugin name as a string and will return a Boolean.
|
||||
Namespace plugins can be specified by full name (``pelican.plugins.plugin_name``)
|
||||
or short name (``plugin_name``). The following example uses the ``webassets`` plugin
|
||||
to minify CSS if the plugin is enabled and otherwise falls back to regular CSS::
|
||||
|
||||
{% if "webassets" is plugin_enabled %}
|
||||
{% assets filters="cssmin", output="css/style.min.css", "css/style.scss" %}
|
||||
<link rel="stylesheet" href="{{SITEURL}}/{{ASSET_URL}}">
|
||||
{% endassets %}
|
||||
{% else %}
|
||||
<link rel="stylesheet" href="{{SITEURL}}/css/style.css}">
|
||||
{% endif %}
|
||||
|
||||
|
||||
index.html
|
||||
----------
|
||||
|
|
@ -348,6 +371,63 @@ period_archives.html template
|
|||
<https://github.com/getpelican/pelican/blob/master/pelican/themes/simple/templates/period_archives.html>`_.
|
||||
|
||||
|
||||
.. _period_archives_variable:
|
||||
|
||||
Listing and Linking to Period Archives
|
||||
""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
The ``period_archives`` variable can be used to generate a list of links to
|
||||
the set of period archives that Pelican generates. As a :ref:`common variable
|
||||
<common_variables>`, it is available for use in any template, so you
|
||||
can implement such an index in a custom direct template, or in a sidebar
|
||||
visible across different site pages.
|
||||
|
||||
``period_archives`` is a dict that may contain ``year``, ``month``, and/or
|
||||
``day`` keys, depending on which ``*_ARCHIVE_SAVE_AS`` settings are enabled.
|
||||
The corresponding value is a list of dicts, where each dict in turn represents
|
||||
a time period (ordered according to the ``NEWEST_FIRST_ARCHIVES`` setting)
|
||||
with the following keys and values:
|
||||
|
||||
=================== ===================================================
|
||||
Key Value
|
||||
=================== ===================================================
|
||||
period The same tuple as described in
|
||||
``period_archives.html``, e.g.
|
||||
``(2023, 'June', 18)``.
|
||||
period_num The same tuple as described in
|
||||
``period_archives.html``, e.g. ``(2023, 6, 18)``.
|
||||
url The URL to the period archive page, e.g.
|
||||
``posts/2023/06/18/``. This is controlled by the
|
||||
corresponding ``*_ARCHIVE_URL`` setting.
|
||||
save_as The path to the save location of the period archive
|
||||
page file, e.g. ``posts/2023/06/18/index.html``.
|
||||
This is used internally by Pelican and is usually
|
||||
not relevant to themes.
|
||||
articles A list of :ref:`Article <object-article>` objects
|
||||
that fall under the time period.
|
||||
dates Same list as ``articles``, but ordered according
|
||||
to the ``NEWEST_FIRST_ARCHIVES`` setting.
|
||||
=================== ===================================================
|
||||
|
||||
Here is an example of how ``period_archives`` can be used in a template:
|
||||
|
||||
.. code-block:: html+jinja
|
||||
|
||||
<ul>
|
||||
{% for archive in period_archives.month %}
|
||||
<li>
|
||||
<a href="{{ SITEURL }}/{{ archive.url }}">
|
||||
{{ archive.period | reverse | join(' ') }} ({{ archive.articles|count }})
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
You can change ``period_archives.month`` in the ``for`` statement to
|
||||
``period_archives.year`` or ``period_archives.day`` as appropriate, depending
|
||||
on the time period granularity desired.
|
||||
|
||||
|
||||
Objects
|
||||
=======
|
||||
|
||||
|
|
|
|||
116
docs/tips.rst
|
|
@ -34,17 +34,30 @@ settings on your AWS console. From there::
|
|||
|
||||
Error Document: 404.html
|
||||
|
||||
Publishing to GitHub
|
||||
====================
|
||||
Publishing to GitHub Pages
|
||||
==========================
|
||||
|
||||
`GitHub Pages <https://help.github.com/categories/20/articles>`_ offer an easy
|
||||
and convenient way to publish Pelican sites. There are `two types of GitHub
|
||||
Pages <https://help.github.com/articles/user-organization-and-project-pages>`_:
|
||||
*Project Pages* and *User Pages*. Pelican sites can be published as both
|
||||
Project Pages and User Pages.
|
||||
If you use `GitHub <https://github.com/>`_ for your Pelican site you can
|
||||
publish your site to `GitHub Pages <https://pages.github.com/>`_ for free.
|
||||
Your site will be published to ``https://<username>.github.io`` if it's a user or
|
||||
organization site or to ``https://<username>.github.io/<repository>`` if it's a
|
||||
project site. It's also possible to `use a custom domain with GitHub Pages <https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site>`_.
|
||||
|
||||
Project Pages
|
||||
-------------
|
||||
There are `two ways to publish a site to GitHub Pages <https://docs.github.com/en/pages/getting-started-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site>`_:
|
||||
|
||||
1. **Publishing from a branch:** run ``pelican`` locally and push the output
|
||||
directory to a special branch of your GitHub repo. GitHub will then publish
|
||||
the contents of this branch to your GitHub Pages site.
|
||||
2. **Publishing with a custom GitHub Actions workflow:** just push the source
|
||||
files of your Pelican site to your GitHub repo's default branch and have a
|
||||
custom GitHub Actions workflow run ``pelican`` for you to generate the
|
||||
output directory and publish it to your GitHub Pages site. This way you
|
||||
don't need to run ``pelican`` locally. You can even edit your site's source
|
||||
files using GitHub's web interface and any changes that you commit will be
|
||||
published.
|
||||
|
||||
Publishing a Project Site to GitHub Pages from a Branch
|
||||
-------------------------------------------------------
|
||||
|
||||
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
|
||||
|
|
@ -72,8 +85,8 @@ already exist). The ``git push origin gh-pages`` command updates the remote
|
|||
``tasks.py``) created by the ``pelican-quickstart`` command publishes the
|
||||
Pelican site as Project Pages, as described above.
|
||||
|
||||
User Pages
|
||||
----------
|
||||
Publishing a User Site to GitHub Pages from a Branch
|
||||
----------------------------------------------------
|
||||
|
||||
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
|
||||
|
|
@ -110,6 +123,87 @@ branch of your GitHub repository::
|
|||
|
||||
(assuming origin is set to your remote repository).
|
||||
|
||||
Publishing to GitHub Pages Using a Custom GitHub Actions Workflow
|
||||
-----------------------------------------------------------------
|
||||
|
||||
Pelican comes with a `custom workflow <https://github.com/getpelican/pelican/blob/master/.github/workflows/github_pages.yml>`_
|
||||
for publishing a Pelican site. To use it:
|
||||
|
||||
1. Enable GitHub Pages in your repo: go to **Settings → Pages** and choose
|
||||
**GitHub Actions** for the **Source** setting.
|
||||
|
||||
2. Commit a ``.github/workflows/pelican.yml`` file to your repo with these contents:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
name: Deploy to GitHub Pages
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
deploy:
|
||||
uses: "getpelican/pelican/.github/workflows/github_pages.yml@master"
|
||||
permissions:
|
||||
contents: "read"
|
||||
pages: "write"
|
||||
id-token: "write"
|
||||
with:
|
||||
settings: "publishconf.py"
|
||||
|
||||
3. Go to the **Actions** tab in your repo
|
||||
(``https://github.com/<username>/<repository>/actions``) and you should see a
|
||||
**Deploy to GitHub Pages** action running.
|
||||
|
||||
4. Once the action completes you should see your Pelican site deployed at your
|
||||
repo's GitHub Pages URL: ``https://<username>.github.io`` for a user or
|
||||
organization site or ``https://<username>.github.io/<repository>>`` for a
|
||||
project site.
|
||||
|
||||
Notes:
|
||||
|
||||
* You don't need to set ``SITEURL`` in your Pelican settings: the workflow will
|
||||
set it for you
|
||||
|
||||
* You don't need to commit your ``--output`` / ``OUTPUT_PATH`` directory
|
||||
(``output/``) to git: the workflow will run ``pelican`` to build the output
|
||||
directory for you on GitHub Actions
|
||||
|
||||
See `GitHub's docs about reusable workflows <https://docs.github.com/en/actions/using-workflows/reusing-workflows>`_
|
||||
for more information.
|
||||
|
||||
A number of optional inputs can be added to the ``with:`` block when calling
|
||||
the workflow:
|
||||
|
||||
+--------------+----------+-----------------------------------+--------+---------------+
|
||||
| Name | Required | Description | Type | Default |
|
||||
+==============+==========+===================================+========+===============+
|
||||
| settings | Yes | The path to your Pelican settings | string | |
|
||||
| | | file (``pelican``'s | | |
|
||||
| | | ``--settings`` option), | | |
|
||||
| | | for example: ``"publishconf.py"`` | | |
|
||||
+--------------+----------+-----------------------------------+--------+---------------+
|
||||
| requirements | No | The Python requirements to | string | ``"pelican"`` |
|
||||
| | | install, for example to enable | | |
|
||||
| | | markdown and typogrify use: | | |
|
||||
| | | ``"pelican[markdown] typogrify"`` | | |
|
||||
| | | or if you have a requirements | | |
|
||||
| | | file: ``"-r requirements.txt"`` | | |
|
||||
+--------------+----------+-----------------------------------+--------+---------------+
|
||||
| output-path | No | Where to output the generated | string | ``"output/"`` |
|
||||
| | | files (``pelican``'s ``--output`` | | |
|
||||
| | | option) | | |
|
||||
+--------------+----------+-----------------------------------+--------+---------------+
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
with:
|
||||
settings: "publishconf.py"
|
||||
requirements: "pelican[markdown] typogrify"
|
||||
output-path: "__output__/"
|
||||
|
||||
Custom 404 Pages
|
||||
----------------
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import argparse
|
||||
import importlib.metadata
|
||||
import json
|
||||
import logging
|
||||
import multiprocessing
|
||||
|
|
@ -8,39 +9,43 @@ import sys
|
|||
import time
|
||||
import traceback
|
||||
from collections.abc import Iterable
|
||||
|
||||
# Combines all paths to `pelican` package accessible from `sys.path`
|
||||
# Makes it possible to install `pelican` and namespace plugins into different
|
||||
# locations in the file system (e.g. pip with `-e` or `--user`)
|
||||
from pkgutil import extend_path
|
||||
|
||||
__path__ = extend_path(__path__, __name__)
|
||||
|
||||
# pelican.log has to be the first pelican module to be loaded
|
||||
# because logging.setLoggerClass has to be called before logging.getLogger
|
||||
from pelican.log import console
|
||||
from pelican.log import init as init_logging
|
||||
from pelican.generators import (ArticlesGenerator, # noqa: I100
|
||||
PagesGenerator, SourceFileGenerator,
|
||||
StaticGenerator, TemplatePagesGenerator)
|
||||
from pelican.generators import (
|
||||
ArticlesGenerator, # noqa: I100
|
||||
PagesGenerator,
|
||||
SourceFileGenerator,
|
||||
StaticGenerator,
|
||||
TemplatePagesGenerator,
|
||||
)
|
||||
from pelican.plugins import signals
|
||||
from pelican.plugins._utils import get_plugin_name, load_plugins
|
||||
from pelican.readers import Readers
|
||||
from pelican.server import ComplexHTTPRequestHandler, RootedHTTPServer
|
||||
from pelican.settings import read_settings
|
||||
from pelican.utils import (FileSystemWatcher, clean_output_dir, maybe_pluralize)
|
||||
from pelican.utils import clean_output_dir, maybe_pluralize, wait_for_changes
|
||||
from pelican.writers import Writer
|
||||
|
||||
try:
|
||||
__version__ = __import__('pkg_resources') \
|
||||
.get_distribution('pelican').version
|
||||
__version__ = importlib.metadata.version("pelican")
|
||||
except Exception:
|
||||
__version__ = "unknown"
|
||||
|
||||
DEFAULT_CONFIG_NAME = 'pelicanconf.py'
|
||||
DEFAULT_CONFIG_NAME = "pelicanconf.py"
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Pelican:
|
||||
|
||||
def __init__(self, settings):
|
||||
"""Pelican initialization
|
||||
|
||||
|
|
@ -50,35 +55,34 @@ class Pelican:
|
|||
# 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.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]):
|
||||
if not any(p in sys.path for p in ["", os.curdir]):
|
||||
logger.debug("Adding current directory to system path")
|
||||
sys.path.insert(0, '')
|
||||
sys.path.insert(0, "")
|
||||
|
||||
def init_plugins(self):
|
||||
self.plugins = []
|
||||
for plugin in load_plugins(self.settings):
|
||||
name = get_plugin_name(plugin)
|
||||
logger.debug('Registering plugin `%s`', name)
|
||||
logger.debug("Registering plugin `%s`", name)
|
||||
try:
|
||||
plugin.register()
|
||||
self.plugins.append(plugin)
|
||||
except Exception as e:
|
||||
logger.error('Cannot register plugin `%s`\n%s',
|
||||
name, e)
|
||||
logger.error("Cannot register plugin `%s`\n%s", name, e)
|
||||
|
||||
self.settings['PLUGINS'] = [get_plugin_name(p) for p in self.plugins]
|
||||
self.settings["PLUGINS"] = [get_plugin_name(p) for p in self.plugins]
|
||||
|
||||
def run(self):
|
||||
"""Run the generators and return"""
|
||||
|
|
@ -87,10 +91,10 @@ class Pelican:
|
|||
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']
|
||||
context["generated_content"] = {}
|
||||
context["static_links"] = set()
|
||||
context["static_content"] = {}
|
||||
context["localsiteurl"] = self.settings["SITEURL"]
|
||||
|
||||
generators = [
|
||||
cls(
|
||||
|
|
@ -99,23 +103,25 @@ class Pelican:
|
|||
path=self.path,
|
||||
theme=self.theme,
|
||||
output_path=self.output_path,
|
||||
) for cls in self._get_generator_classes()
|
||||
)
|
||||
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)])):
|
||||
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'):
|
||||
if hasattr(p, "generate_context"):
|
||||
p.generate_context()
|
||||
|
||||
for p in generators:
|
||||
if hasattr(p, 'refresh_metadata_intersite_links'):
|
||||
if hasattr(p, "refresh_metadata_intersite_links"):
|
||||
p.refresh_metadata_intersite_links()
|
||||
|
||||
signals.all_generators_finalized.send(generators)
|
||||
|
|
@ -123,61 +129,75 @@ class Pelican:
|
|||
writer = self._get_writer()
|
||||
|
||||
for p in generators:
|
||||
if hasattr(p, 'generate_output'):
|
||||
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))
|
||||
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')
|
||||
(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')
|
||||
(
|
||||
len(articles_generator.drafts)
|
||||
+ len(articles_generator.drafts_translations)
|
||||
),
|
||||
"draft",
|
||||
"drafts",
|
||||
)
|
||||
pluralized_hidden_articles = maybe_pluralize(
|
||||
(len(articles_generator.hidden_articles) +
|
||||
len(articles_generator.hidden_translations)),
|
||||
'hidden article',
|
||||
'hidden articles')
|
||||
(
|
||||
len(articles_generator.hidden_articles)
|
||||
+ len(articles_generator.hidden_translations)
|
||||
),
|
||||
"hidden article",
|
||||
"hidden articles",
|
||||
)
|
||||
pluralized_pages = maybe_pluralize(
|
||||
(len(pages_generator.pages) +
|
||||
len(pages_generator.translations)),
|
||||
'page',
|
||||
'pages')
|
||||
(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')
|
||||
(
|
||||
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')
|
||||
(
|
||||
len(pages_generator.draft_pages)
|
||||
+ len(pages_generator.draft_translations)
|
||||
),
|
||||
"draft page",
|
||||
"draft pages",
|
||||
)
|
||||
|
||||
console.print('Done: Processed {}, {}, {}, {}, {} and {} in {:.2f} seconds.'
|
||||
.format(
|
||||
pluralized_articles,
|
||||
pluralized_drafts,
|
||||
pluralized_hidden_articles,
|
||||
pluralized_pages,
|
||||
pluralized_hidden_pages,
|
||||
pluralized_draft_pages,
|
||||
time.time() - start_time))
|
||||
console.print(
|
||||
"Done: Processed {}, {}, {}, {}, {} and {} in {:.2f} seconds.".format(
|
||||
pluralized_articles,
|
||||
pluralized_drafts,
|
||||
pluralized_hidden_articles,
|
||||
pluralized_pages,
|
||||
pluralized_hidden_pages,
|
||||
pluralized_draft_pages,
|
||||
time.time() - start_time,
|
||||
)
|
||||
)
|
||||
|
||||
def _get_generator_classes(self):
|
||||
discovered_generators = [
|
||||
(ArticlesGenerator, "internal"),
|
||||
(PagesGenerator, "internal")
|
||||
(PagesGenerator, "internal"),
|
||||
]
|
||||
|
||||
if self.settings["TEMPLATE_PAGES"]:
|
||||
|
|
@ -236,7 +256,7 @@ class PrintSettings(argparse.Action):
|
|||
except Exception as e:
|
||||
logger.critical("%s: %s", e.__class__.__name__, e)
|
||||
console.print_exception()
|
||||
sys.exit(getattr(e, 'exitcode', 1))
|
||||
sys.exit(getattr(e, "exitcode", 1))
|
||||
|
||||
if values:
|
||||
# One or more arguments provided, so only print those settings
|
||||
|
|
@ -244,14 +264,16 @@ class PrintSettings(argparse.Action):
|
|||
if setting in settings:
|
||||
# Only add newline between setting name and value if dict
|
||||
if isinstance(settings[setting], (dict, tuple, list)):
|
||||
setting_format = '\n{}:\n{}'
|
||||
setting_format = "\n{}:\n{}"
|
||||
else:
|
||||
setting_format = '\n{}: {}'
|
||||
console.print(setting_format.format(
|
||||
setting,
|
||||
pprint.pformat(settings[setting])))
|
||||
setting_format = "\n{}: {}"
|
||||
console.print(
|
||||
setting_format.format(
|
||||
setting, pprint.pformat(settings[setting])
|
||||
)
|
||||
)
|
||||
else:
|
||||
console.print('\n{} is not a recognized setting.'.format(setting))
|
||||
console.print(f"\n{setting} is not a recognized setting.")
|
||||
break
|
||||
else:
|
||||
# No argument was given to --print-settings, so print all settings
|
||||
|
|
@ -268,170 +290,247 @@ class ParseOverrides(argparse.Action):
|
|||
k, v = item.split("=", 1)
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
'Extra settings must be specified as KEY=VALUE pairs '
|
||||
f'but you specified {item}'
|
||||
"Extra settings must be specified as KEY=VALUE pairs "
|
||||
f"but you specified {item}"
|
||||
)
|
||||
try:
|
||||
overrides[k] = json.loads(v)
|
||||
except json.decoder.JSONDecodeError:
|
||||
raise ValueError(
|
||||
f'Invalid JSON value: {v}. '
|
||||
'Values specified via -e / --extra-settings flags '
|
||||
'must be in JSON notation. '
|
||||
'Use -e KEY=\'"string"\' to specify a string value; '
|
||||
'-e KEY=null to specify None; '
|
||||
'-e KEY=false (or true) to specify False (or True).'
|
||||
f"Invalid JSON value: {v}. "
|
||||
"Values specified via -e / --extra-settings flags "
|
||||
"must be in JSON notation. "
|
||||
"Use -e KEY='\"string\"' to specify a string value; "
|
||||
"-e KEY=null to specify None; "
|
||||
"-e KEY=false (or true) to specify False (or True)."
|
||||
)
|
||||
setattr(namespace, self.dest, overrides)
|
||||
|
||||
|
||||
def parse_arguments(argv=None):
|
||||
parser = argparse.ArgumentParser(
|
||||
description='A tool to generate a static blog, '
|
||||
' with restructured text input files.',
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
||||
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(
|
||||
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(
|
||||
"-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(
|
||||
"-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(
|
||||
"-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(
|
||||
"-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(
|
||||
"-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(
|
||||
"-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(
|
||||
"-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(
|
||||
"--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(
|
||||
"-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(
|
||||
"--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(
|
||||
"--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(
|
||||
"--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(
|
||||
"--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('--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('--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('-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('-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('-b', '--bind', dest='bind',
|
||||
help='IP to bind to when serving files via HTTP '
|
||||
'(default: 127.0.0.1)')
|
||||
|
||||
parser.add_argument('-e', '--extra-settings', dest='overrides',
|
||||
help='Specify one or more SETTING=VALUE pairs to '
|
||||
'override settings. VALUE must be in JSON notation: '
|
||||
'specify string values as SETTING=\'"some string"\'; '
|
||||
'booleans as SETTING=true or SETTING=false; '
|
||||
'None as SETTING=null.',
|
||||
nargs='*',
|
||||
action=ParseOverrides,
|
||||
default={})
|
||||
parser.add_argument(
|
||||
"-e",
|
||||
"--extra-settings",
|
||||
dest="overrides",
|
||||
help="Specify one or more SETTING=VALUE pairs to "
|
||||
"override settings. VALUE must be in JSON notation: "
|
||||
"specify string values as SETTING='\"some string\"'; "
|
||||
"booleans as SETTING=true or SETTING=false; "
|
||||
"None as SETTING=null.",
|
||||
nargs="*",
|
||||
action=ParseOverrides,
|
||||
default={},
|
||||
)
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.port is not None and not args.listen:
|
||||
logger.warning('--port without --listen has no effect')
|
||||
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')
|
||||
logger.warning("--bind without --listen has no effect")
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def get_config(args):
|
||||
"""Builds a config dictionary based on supplied `args`.
|
||||
"""
|
||||
"""Builds a config dictionary based on supplied `args`."""
|
||||
config = {}
|
||||
if args.path:
|
||||
config['PATH'] = os.path.abspath(os.path.expanduser(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))
|
||||
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
|
||||
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
|
||||
config["DELETE_OUTPUT_DIRECTORY"] = args.delete_outputdir
|
||||
if args.ignore_cache:
|
||||
config['LOAD_CONTENT_CACHE'] = False
|
||||
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(',')
|
||||
config["CACHE_PATH"] = args.cache_path
|
||||
if args.relative_paths:
|
||||
config['RELATIVE_URLS'] = args.relative_paths
|
||||
config["RELATIVE_URLS"] = args.relative_paths
|
||||
if args.port is not None:
|
||||
config['PORT'] = args.port
|
||||
config["PORT"] = args.port
|
||||
if args.bind is not None:
|
||||
config['BIND'] = args.bind
|
||||
config['DEBUG'] = args.verbosity == logging.DEBUG
|
||||
config["BIND"] = args.bind
|
||||
config["DEBUG"] = args.verbosity == logging.DEBUG
|
||||
config.update(args.overrides)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def get_instance(args):
|
||||
|
||||
config_file = args.settings
|
||||
if config_file is None and os.path.isfile(DEFAULT_CONFIG_NAME):
|
||||
config_file = DEFAULT_CONFIG_NAME
|
||||
|
|
@ -439,9 +538,9 @@ def get_instance(args):
|
|||
|
||||
settings = read_settings(config_file, override=get_config(args))
|
||||
|
||||
cls = settings['PELICAN_CLASS']
|
||||
cls = settings["PELICAN_CLASS"]
|
||||
if isinstance(cls, str):
|
||||
module, cls_name = cls.rsplit('.', 1)
|
||||
module, cls_name = cls.rsplit(".", 1)
|
||||
module = __import__(module)
|
||||
cls = getattr(module, cls_name)
|
||||
|
||||
|
|
@ -449,29 +548,25 @@ def get_instance(args):
|
|||
|
||||
|
||||
def autoreload(args, excqueue=None):
|
||||
console.print(' --- AutoReload Mode: Monitoring `content`, `theme` and'
|
||||
' `settings` for changes. ---')
|
||||
console.print(
|
||||
" --- AutoReload Mode: Monitoring `content`, `theme` and"
|
||||
" `settings` for changes. ---"
|
||||
)
|
||||
pelican, settings = get_instance(args)
|
||||
watcher = FileSystemWatcher(args.settings, Readers, settings)
|
||||
sleep = False
|
||||
settings_file = os.path.abspath(args.settings)
|
||||
while True:
|
||||
try:
|
||||
# Don't sleep first time, but sleep afterwards to reduce cpu load
|
||||
if sleep:
|
||||
time.sleep(0.5)
|
||||
else:
|
||||
sleep = True
|
||||
pelican.run()
|
||||
|
||||
modified = watcher.check()
|
||||
changed_files = wait_for_changes(args.settings, Readers, settings)
|
||||
changed_files = {c[1] for c in changed_files}
|
||||
|
||||
if modified['settings']:
|
||||
if settings_file in changed_files:
|
||||
pelican, settings = get_instance(args)
|
||||
watcher.update_watchers(settings)
|
||||
|
||||
if any(modified.values()):
|
||||
console.print('\n-> Modified: {}. re-generating...'.format(
|
||||
', '.join(k for k, v in modified.items() if v)))
|
||||
pelican.run()
|
||||
console.print(
|
||||
"\n-> Modified: {}. re-generating...".format(", ".join(changed_files))
|
||||
)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
if excqueue is not None:
|
||||
|
|
@ -480,15 +575,14 @@ def autoreload(args, excqueue=None):
|
|||
raise
|
||||
|
||||
except Exception as e:
|
||||
if (args.verbosity == logging.DEBUG):
|
||||
if args.verbosity == logging.DEBUG:
|
||||
if excqueue is not None:
|
||||
excqueue.put(
|
||||
traceback.format_exception_only(type(e), e)[-1])
|
||||
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))
|
||||
'Caught exception:\n"%s".', e, exc_info=settings.get("DEBUG", False)
|
||||
)
|
||||
|
||||
|
||||
def listen(server, port, output, excqueue=None):
|
||||
|
|
@ -498,8 +592,7 @@ def listen(server, port, output, excqueue=None):
|
|||
|
||||
RootedHTTPServer.allow_reuse_address = True
|
||||
try:
|
||||
httpd = RootedHTTPServer(
|
||||
output, (server, port), ComplexHTTPRequestHandler)
|
||||
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:
|
||||
|
|
@ -507,8 +600,7 @@ def listen(server, port, output, excqueue=None):
|
|||
return
|
||||
|
||||
try:
|
||||
console.print("Serving site at: http://{}:{} - Tap CTRL-C to stop".format(
|
||||
server, port))
|
||||
console.print(f"Serving site at: http://{server}:{port} - Tap CTRL-C to stop")
|
||||
httpd.serve_forever()
|
||||
except Exception as e:
|
||||
if excqueue is not None:
|
||||
|
|
@ -525,24 +617,31 @@ def listen(server, port, output, excqueue=None):
|
|||
def main(argv=None):
|
||||
args = parse_arguments(argv)
|
||||
logs_dedup_min_level = getattr(logging, args.logs_dedup_min_level)
|
||||
init_logging(level=args.verbosity, fatal=args.fatal,
|
||||
name=__name__, logs_dedup_min_level=logs_dedup_min_level)
|
||||
init_logging(
|
||||
level=args.verbosity,
|
||||
fatal=args.fatal,
|
||||
name=__name__,
|
||||
logs_dedup_min_level=logs_dedup_min_level,
|
||||
)
|
||||
|
||||
logger.debug('Pelican version: %s', __version__)
|
||||
logger.debug('Python version: %s', sys.version.split()[0])
|
||||
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))
|
||||
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))
|
||||
args=(
|
||||
settings.get("BIND"),
|
||||
settings.get("PORT"),
|
||||
settings.get("OUTPUT_PATH"),
|
||||
excqueue,
|
||||
),
|
||||
)
|
||||
try:
|
||||
p1.start()
|
||||
p2.start()
|
||||
|
|
@ -555,18 +654,17 @@ def main(argv=None):
|
|||
elif args.autoreload:
|
||||
autoreload(args)
|
||||
elif args.listen:
|
||||
listen(settings.get('BIND'), settings.get('PORT'),
|
||||
settings.get("OUTPUT_PATH"))
|
||||
listen(
|
||||
settings.get("BIND"), settings.get("PORT"), settings.get("OUTPUT_PATH")
|
||||
)
|
||||
else:
|
||||
watcher = FileSystemWatcher(args.settings, Readers, settings)
|
||||
watcher.check()
|
||||
with console.status("Generating..."):
|
||||
pelican.run()
|
||||
except KeyboardInterrupt:
|
||||
logger.warning('Keyboard interrupt received. Exiting.')
|
||||
logger.warning("Keyboard interrupt received. Exiting.")
|
||||
except Exception as e:
|
||||
logger.critical("%s: %s", e.__class__.__name__, e)
|
||||
|
||||
if args.verbosity == logging.DEBUG:
|
||||
console.print_exception()
|
||||
sys.exit(getattr(e, 'exitcode', 1))
|
||||
sys.exit(getattr(e, "exitcode", 1))
|
||||
|
|
|
|||
|
|
@ -5,5 +5,5 @@ python -m pelican module entry point to run via python -m
|
|||
from . import main
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -19,29 +19,35 @@ class FileDataCacher:
|
|||
Sets caching policy according to *caching_policy*.
|
||||
"""
|
||||
self.settings = settings
|
||||
self._cache_path = os.path.join(self.settings['CACHE_PATH'],
|
||||
cache_name)
|
||||
self._cache_path = os.path.join(self.settings["CACHE_PATH"], cache_name)
|
||||
self._cache_data_policy = caching_policy
|
||||
if self.settings['GZIP_CACHE']:
|
||||
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:
|
||||
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)
|
||||
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)
|
||||
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 = {}
|
||||
|
|
@ -62,12 +68,13 @@ class FileDataCacher:
|
|||
"""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:
|
||||
mkdir_p(self.settings["CACHE_PATH"])
|
||||
with self._cache_open(self._cache_path, "wb") as fhandle:
|
||||
pickle.dump(self._cache, fhandle)
|
||||
except (OSError, pickle.PicklingError, TypeError) as err:
|
||||
logger.warning('Could not save cache %s\n ... %s',
|
||||
self._cache_path, err)
|
||||
logger.warning(
|
||||
"Could not save cache %s\n ... %s", self._cache_path, err
|
||||
)
|
||||
|
||||
|
||||
class FileStampDataCacher(FileDataCacher):
|
||||
|
|
@ -80,8 +87,8 @@ class FileStampDataCacher(FileDataCacher):
|
|||
|
||||
super().__init__(settings, cache_name, caching_policy, load_policy)
|
||||
|
||||
method = self.settings['CHECK_MODIFIED_METHOD']
|
||||
if method == 'mtime':
|
||||
method = self.settings["CHECK_MODIFIED_METHOD"]
|
||||
if method == "mtime":
|
||||
self._filestamp_func = os.path.getmtime
|
||||
else:
|
||||
try:
|
||||
|
|
@ -89,12 +96,12 @@ class FileStampDataCacher(FileDataCacher):
|
|||
|
||||
def filestamp_func(filename):
|
||||
"""return hash of file contents"""
|
||||
with open(filename, 'rb') as fhandle:
|
||||
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)
|
||||
logger.warning("Could not get hashing function\n\t%s", err)
|
||||
self._filestamp_func = None
|
||||
|
||||
def cache_data(self, filename, data):
|
||||
|
|
@ -115,9 +122,8 @@ class FileStampDataCacher(FileDataCacher):
|
|||
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 ''
|
||||
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
|
||||
|
|
|
|||
|
|
@ -16,12 +16,19 @@ except ModuleNotFoundError:
|
|||
|
||||
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)
|
||||
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 available when you import from pelican.contents.
|
||||
from pelican.urlwrappers import (Author, Category, Tag, URLWrapper) # NOQA
|
||||
from pelican.urlwrappers import Author, Category, Tag, URLWrapper # NOQA
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -36,12 +43,14 @@ class Content:
|
|||
:param context: The shared context between generators.
|
||||
|
||||
"""
|
||||
@deprecated_attribute(old='filename', new='source_path', since=(3, 2, 0))
|
||||
|
||||
@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):
|
||||
def __init__(
|
||||
self, content, metadata=None, settings=None, source_path=None, context=None
|
||||
):
|
||||
if metadata is None:
|
||||
metadata = {}
|
||||
if settings is None:
|
||||
|
|
@ -59,8 +68,8 @@ class Content:
|
|||
|
||||
# set metadata as attributes
|
||||
for key, value in local_metadata.items():
|
||||
if key in ('save_as', 'url'):
|
||||
key = 'override_' + key
|
||||
if key in ("save_as", "url"):
|
||||
key = "override_" + key
|
||||
setattr(self, key.lower(), value)
|
||||
|
||||
# also keep track of the metadata attributes available
|
||||
|
|
@ -71,53 +80,52 @@ class Content:
|
|||
|
||||
# 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'):
|
||||
if not hasattr(self, "author"):
|
||||
if hasattr(self, "authors"):
|
||||
self.author = self.authors[0]
|
||||
elif 'AUTHOR' in settings:
|
||||
self.author = Author(settings['AUTHOR'], settings)
|
||||
elif "AUTHOR" in settings:
|
||||
self.author = Author(settings["AUTHOR"], settings)
|
||||
|
||||
if not hasattr(self, 'authors') and hasattr(self, 'author'):
|
||||
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'):
|
||||
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)
|
||||
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')):
|
||||
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):
|
||||
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))
|
||||
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]
|
||||
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']
|
||||
self.date_format = settings["DEFAULT_DATE_FORMAT"]
|
||||
|
||||
if isinstance(self.date_format, tuple):
|
||||
locale_string = self.date_format[0]
|
||||
|
|
@ -129,22 +137,22 @@ class Content:
|
|||
timezone = getattr(self, "timezone", default_timezone)
|
||||
self.timezone = ZoneInfo(timezone)
|
||||
|
||||
if hasattr(self, 'date'):
|
||||
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'):
|
||||
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'):
|
||||
if not hasattr(self, "status"):
|
||||
# Previous default of None broke comment plugins and perhaps others
|
||||
self.status = getattr(self, 'default_status', '')
|
||||
self.status = getattr(self, "default_status", "")
|
||||
|
||||
# store the summary metadata if it is set
|
||||
if 'summary' in metadata:
|
||||
self._summary = metadata['summary']
|
||||
if "summary" in metadata:
|
||||
self._summary = metadata["summary"]
|
||||
|
||||
signals.content_object_init.send(self)
|
||||
|
||||
|
|
@ -156,8 +164,8 @@ class Content:
|
|||
for prop in self.mandatory_properties:
|
||||
if not hasattr(self, prop):
|
||||
logger.error(
|
||||
"Skipping %s: could not find information about '%s'",
|
||||
self, prop)
|
||||
"Skipping %s: could not find information about '%s'", self, prop
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
|
@ -183,12 +191,13 @@ class Content:
|
|||
return True
|
||||
|
||||
def _has_valid_status(self):
|
||||
if hasattr(self, 'allowed_statuses'):
|
||||
if hasattr(self, "allowed_statuses"):
|
||||
if self.status not in self.allowed_statuses:
|
||||
logger.error(
|
||||
"Unknown status '%s' for file %s, skipping it. (Not in %s)",
|
||||
self.status,
|
||||
self, self.allowed_statuses
|
||||
self,
|
||||
self.allowed_statuses,
|
||||
)
|
||||
return False
|
||||
|
||||
|
|
@ -198,42 +207,48 @@ class Content:
|
|||
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()])
|
||||
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 ''
|
||||
})
|
||||
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)
|
||||
fq_key = (f"{klass}_{key}").upper()
|
||||
return str(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
|
||||
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'))
|
||||
what = m.group("what")
|
||||
value = urlparse(m.group("value"))
|
||||
path = value.path
|
||||
origin = m.group('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
|
||||
|
|
@ -241,7 +256,7 @@ class Content:
|
|||
# `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']:
|
||||
if self.settings["RELATIVE_URLS"]:
|
||||
joiner = os.path.join
|
||||
else:
|
||||
joiner = urljoin
|
||||
|
|
@ -251,16 +266,17 @@ class Content:
|
|||
# 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 += '/'
|
||||
if not siteurl.endswith("/"):
|
||||
siteurl += "/"
|
||||
|
||||
# XXX Put this in a different location.
|
||||
if what in {'filename', 'static', 'attach'}:
|
||||
if what in {"filename", "static", "attach"}:
|
||||
|
||||
def _get_linked_content(key, url):
|
||||
nonlocal value
|
||||
|
||||
def _find_path(path):
|
||||
if path.startswith('/'):
|
||||
if path.startswith("/"):
|
||||
path = path[1:]
|
||||
else:
|
||||
# relative to the source path of this content
|
||||
|
|
@ -287,59 +303,64 @@ class Content:
|
|||
return result
|
||||
|
||||
# check if a static file is linked with {filename}
|
||||
if what == 'filename' and key == 'generated_content':
|
||||
linked_content = _get_linked_content('static_content', value)
|
||||
if what == "filename" and key == "generated_content":
|
||||
linked_content = _get_linked_content("static_content", value)
|
||||
if linked_content:
|
||||
logger.warning(
|
||||
'{filename} used for linking to static'
|
||||
' content %s in %s. Use {static} instead',
|
||||
"{filename} used for linking to static"
|
||||
" content %s in %s. Use {static} instead",
|
||||
value.path,
|
||||
self.get_relative_source_path())
|
||||
self.get_relative_source_path(),
|
||||
)
|
||||
return linked_content
|
||||
|
||||
return None
|
||||
|
||||
if what == 'filename':
|
||||
key = 'generated_content'
|
||||
if what == "filename":
|
||||
key = "generated_content"
|
||||
else:
|
||||
key = 'static_content'
|
||||
key = "static_content"
|
||||
|
||||
linked_content = _get_linked_content(key, value)
|
||||
if linked_content:
|
||||
if what == 'attach':
|
||||
if what == "attach":
|
||||
linked_content.attach_to(self)
|
||||
origin = joiner(siteurl, linked_content.url)
|
||||
origin = origin.replace('\\', '/') # for Windows paths.
|
||||
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':
|
||||
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':
|
||||
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':
|
||||
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)
|
||||
"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')))
|
||||
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']
|
||||
intrasite_link_regex = self.settings["INTRASITE_LINK_REGEX"]
|
||||
regex = r"""
|
||||
(?P<markup><[^\>]+ # match tag with all url-value attributes
|
||||
(?:href|src|poster|data|cite|formaction|action|content)\s*=\s*)
|
||||
|
|
@ -369,28 +390,28 @@ class Content:
|
|||
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'))
|
||||
what = m.group("what")
|
||||
value = urlparse(m.group("value"))
|
||||
path = value.path
|
||||
if what not in {'static', 'attach'}:
|
||||
if what not in {"static", "attach"}:
|
||||
continue
|
||||
if path.startswith('/'):
|
||||
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', ' ')
|
||||
path = path.replace("%20", " ")
|
||||
static_links.add(path)
|
||||
return static_links
|
||||
|
||||
def get_siteurl(self):
|
||||
return self._context.get('localsiteurl', '')
|
||||
return self._context.get("localsiteurl", "")
|
||||
|
||||
@memoized
|
||||
def get_content(self, siteurl):
|
||||
if hasattr(self, '_get_content'):
|
||||
if hasattr(self, "_get_content"):
|
||||
content = self._get_content()
|
||||
else:
|
||||
content = self._content
|
||||
|
|
@ -407,15 +428,17 @@ class Content:
|
|||
This is based on the summary metadata if set, otherwise truncate the
|
||||
content.
|
||||
"""
|
||||
if 'summary' in self.metadata:
|
||||
return self.metadata['summary']
|
||||
if "summary" in self.metadata:
|
||||
return self.metadata["summary"]
|
||||
|
||||
if self.settings['SUMMARY_MAX_LENGTH'] is None:
|
||||
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'])
|
||||
return truncate_html_words(
|
||||
self.content,
|
||||
self.settings["SUMMARY_MAX_LENGTH"],
|
||||
self.settings["SUMMARY_END_SUFFIX"],
|
||||
)
|
||||
|
||||
@property
|
||||
def summary(self):
|
||||
|
|
@ -424,8 +447,10 @@ class Content:
|
|||
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')
|
||||
logger.warning(
|
||||
"_get_summary() has been deprecated since 3.6.4. "
|
||||
"Use the summary decorator instead"
|
||||
)
|
||||
return self.summary
|
||||
|
||||
@summary.setter
|
||||
|
|
@ -444,14 +469,14 @@ class Content:
|
|||
|
||||
@property
|
||||
def url(self):
|
||||
return self.get_url_setting('url')
|
||||
return self.get_url_setting("url")
|
||||
|
||||
@property
|
||||
def save_as(self):
|
||||
return self.get_url_setting('save_as')
|
||||
return self.get_url_setting("save_as")
|
||||
|
||||
def _get_template(self):
|
||||
if hasattr(self, 'template') and self.template is not None:
|
||||
if hasattr(self, "template") and self.template is not None:
|
||||
return self.template
|
||||
else:
|
||||
return self.default_template
|
||||
|
|
@ -470,11 +495,10 @@ class Content:
|
|||
|
||||
return posixize_path(
|
||||
os.path.relpath(
|
||||
os.path.abspath(os.path.join(
|
||||
self.settings['PATH'],
|
||||
source_path)),
|
||||
os.path.abspath(self.settings['PATH'])
|
||||
))
|
||||
os.path.abspath(os.path.join(self.settings["PATH"], source_path)),
|
||||
os.path.abspath(self.settings["PATH"]),
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def relative_dir(self):
|
||||
|
|
@ -482,85 +506,84 @@ class Content:
|
|||
os.path.dirname(
|
||||
os.path.relpath(
|
||||
os.path.abspath(self.source_path),
|
||||
os.path.abspath(self.settings['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()
|
||||
)
|
||||
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
|
||||
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'
|
||||
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
|
||||
klass = "draft_page" if self.status == "draft" else None
|
||||
return super()._expand_settings(key, klass)
|
||||
|
||||
|
||||
class Article(Content):
|
||||
mandatory_properties = ('title', 'date', 'category')
|
||||
allowed_statuses = ('published', 'hidden', 'draft')
|
||||
default_status = 'published'
|
||||
default_template = 'article'
|
||||
mandatory_properties = ("title", "date", "category")
|
||||
allowed_statuses = ("published", "hidden", "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 not self.settings["WITH_FUTURE_DATES"] and hasattr(self, "date"):
|
||||
if self.date.tzinfo is None:
|
||||
now = datetime.datetime.now()
|
||||
else:
|
||||
now = datetime.datetime.utcnow().replace(tzinfo=timezone.utc)
|
||||
if self.date > now:
|
||||
self.status = 'draft'
|
||||
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':
|
||||
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'
|
||||
klass = "draft" if self.status == "draft" else "article"
|
||||
return super()._expand_settings(key, klass)
|
||||
|
||||
|
||||
class Static(Content):
|
||||
mandatory_properties = ('title',)
|
||||
default_status = 'published'
|
||||
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))
|
||||
@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))
|
||||
@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))
|
||||
@deprecated_attribute(old="dst", new="save_as", since=(3, 2, 0))
|
||||
def dst():
|
||||
return None
|
||||
|
||||
|
|
@ -577,8 +600,7 @@ class Static(Content):
|
|||
return super().save_as
|
||||
|
||||
def attach_to(self, content):
|
||||
"""Override our output directory with that of the given content object.
|
||||
"""
|
||||
"""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
|
||||
|
|
@ -589,8 +611,7 @@ class Static(Content):
|
|||
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)
|
||||
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
|
||||
|
|
@ -609,12 +630,14 @@ class Static(Content):
|
|||
"%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."})
|
||||
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 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
|
||||
|
|
|
|||
|
|
@ -4,9 +4,7 @@ from collections import defaultdict
|
|||
from rich.console import Console
|
||||
from rich.logging import RichHandler
|
||||
|
||||
__all__ = [
|
||||
'init'
|
||||
]
|
||||
__all__ = ["init"]
|
||||
|
||||
console = Console()
|
||||
|
||||
|
|
@ -34,8 +32,8 @@ class LimitFilter(logging.Filter):
|
|||
return True
|
||||
|
||||
# extract group
|
||||
group = record.__dict__.get('limit_msg', None)
|
||||
group_args = record.__dict__.get('limit_args', ())
|
||||
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())
|
||||
|
|
@ -50,7 +48,7 @@ class LimitFilter(logging.Filter):
|
|||
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):
|
||||
if template_key in self._ignore or message_key in self._ignore:
|
||||
return False
|
||||
|
||||
# check if we went over threshold
|
||||
|
|
@ -90,12 +88,12 @@ class FatalLogger(LimitLogger):
|
|||
def warning(self, *args, **kwargs):
|
||||
super().warning(*args, **kwargs)
|
||||
if FatalLogger.warnings_fatal:
|
||||
raise RuntimeError('Warning encountered')
|
||||
raise RuntimeError("Warning encountered")
|
||||
|
||||
def error(self, *args, **kwargs):
|
||||
super().error(*args, **kwargs)
|
||||
if FatalLogger.errors_fatal:
|
||||
raise RuntimeError('Error encountered')
|
||||
raise RuntimeError("Error encountered")
|
||||
|
||||
|
||||
logging.setLoggerClass(FatalLogger)
|
||||
|
|
@ -103,17 +101,19 @@ logging.setLoggerClass(FatalLogger)
|
|||
logging.getLogger().__class__ = FatalLogger
|
||||
|
||||
|
||||
def init(level=None, fatal='', handler=RichHandler(console=console), name=None,
|
||||
logs_dedup_min_level=None):
|
||||
FatalLogger.warnings_fatal = fatal.startswith('warning')
|
||||
def init(
|
||||
level=None,
|
||||
fatal="",
|
||||
handler=RichHandler(console=console),
|
||||
name=None,
|
||||
logs_dedup_min_level=None,
|
||||
):
|
||||
FatalLogger.warnings_fatal = fatal.startswith("warning")
|
||||
FatalLogger.errors_fatal = bool(fatal)
|
||||
|
||||
LOG_FORMAT = "%(message)s"
|
||||
logging.basicConfig(
|
||||
level=level,
|
||||
format=LOG_FORMAT,
|
||||
datefmt="[%H:%M:%S]",
|
||||
handlers=[handler]
|
||||
level=level, format=LOG_FORMAT, datefmt="[%H:%M:%S]", handlers=[handler]
|
||||
)
|
||||
|
||||
logger = logging.getLogger(name)
|
||||
|
|
@ -126,17 +126,18 @@ def init(level=None, fatal='', handler=RichHandler(console=console), name=None,
|
|||
|
||||
def log_warnings():
|
||||
import warnings
|
||||
|
||||
logging.captureWarnings(True)
|
||||
warnings.simplefilter("default", DeprecationWarning)
|
||||
init(logging.DEBUG, name='py.warnings')
|
||||
init(logging.DEBUG, name="py.warnings")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
init(level=logging.DEBUG, name=__name__)
|
||||
|
||||
root_logger = logging.getLogger(__name__)
|
||||
root_logger.debug('debug')
|
||||
root_logger.info('info')
|
||||
root_logger.warning('warning')
|
||||
root_logger.error('error')
|
||||
root_logger.critical('critical')
|
||||
root_logger.debug("debug")
|
||||
root_logger.info("info")
|
||||
root_logger.warning("warning")
|
||||
root_logger.error("error")
|
||||
root_logger.critical("critical")
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ from math import ceil
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
PaginationRule = namedtuple(
|
||||
'PaginationRule',
|
||||
'min_page URL SAVE_AS',
|
||||
"PaginationRule",
|
||||
"min_page URL SAVE_AS",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ class Paginator:
|
|||
self.settings = settings
|
||||
if per_page:
|
||||
self.per_page = per_page
|
||||
self.orphans = settings['DEFAULT_ORPHANS']
|
||||
self.orphans = settings["DEFAULT_ORPHANS"]
|
||||
else:
|
||||
self.per_page = len(object_list)
|
||||
self.orphans = 0
|
||||
|
|
@ -32,14 +32,21 @@ class Paginator:
|
|||
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)
|
||||
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):
|
||||
|
|
@ -48,6 +55,7 @@ class Paginator:
|
|||
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):
|
||||
|
|
@ -56,6 +64,7 @@ class Paginator:
|
|||
a template for loop.
|
||||
"""
|
||||
return list(range(1, self.num_pages + 1))
|
||||
|
||||
page_range = property(_get_page_range)
|
||||
|
||||
|
||||
|
|
@ -64,7 +73,7 @@ class Page:
|
|||
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_name = dn if fn in ("index.htm", "index.html") else self.name
|
||||
self.base_url = url
|
||||
self.object_list = object_list
|
||||
self.number = number
|
||||
|
|
@ -72,7 +81,7 @@ class Page:
|
|||
self.settings = settings
|
||||
|
||||
def __repr__(self):
|
||||
return '<Page {} of {}>'.format(self.number, self.paginator.num_pages)
|
||||
return f"<Page {self.number} of {self.paginator.num_pages}>"
|
||||
|
||||
def has_next(self):
|
||||
return self.number < self.paginator.num_pages
|
||||
|
|
@ -117,7 +126,7 @@ class Page:
|
|||
rule = None
|
||||
|
||||
# find the last matching pagination rule
|
||||
for p in self.settings['PAGINATION_PATTERNS']:
|
||||
for p in self.settings["PAGINATION_PATTERNS"]:
|
||||
if p.min_page == -1:
|
||||
if not self.has_next():
|
||||
rule = p
|
||||
|
|
@ -127,22 +136,22 @@ class Page:
|
|||
rule = p
|
||||
|
||||
if not rule:
|
||||
return ''
|
||||
return ""
|
||||
|
||||
prop_value = getattr(rule, key)
|
||||
|
||||
if not isinstance(prop_value, str):
|
||||
logger.warning('%s is set to %s', key, prop_value)
|
||||
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,
|
||||
"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)
|
||||
|
|
@ -155,9 +164,9 @@ class Page:
|
|||
# changed to lstrip() because that would remove all leading slashes and
|
||||
# thus make the workaround impossible. See
|
||||
# test_custom_pagination_pattern() for a verification of this.
|
||||
if ret.startswith('/'):
|
||||
if ret.startswith("/"):
|
||||
ret = ret[1:]
|
||||
return ret
|
||||
|
||||
url = property(functools.partial(_from_settings, key='URL'))
|
||||
save_as = property(functools.partial(_from_settings, key='SAVE_AS'))
|
||||
url = property(functools.partial(_from_settings, key="URL"))
|
||||
save_as = property(functools.partial(_from_settings, key="SAVE_AS"))
|
||||
|
|
|
|||
|
|
@ -24,26 +24,42 @@ def get_namespace_plugins(ns_pkg=None):
|
|||
|
||||
return {
|
||||
name: importlib.import_module(name)
|
||||
for finder, name, ispkg
|
||||
in iter_namespace(ns_pkg)
|
||||
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))
|
||||
logger.info("Plugins found:\n" + "\n".join(ns_plugins))
|
||||
else:
|
||||
logger.info('No plugins are installed')
|
||||
logger.info("No plugins are installed")
|
||||
|
||||
|
||||
def plugin_enabled(name, plugin_list=None):
|
||||
if plugin_list is None or not plugin_list:
|
||||
# no plugins are loaded
|
||||
return False
|
||||
|
||||
if name in plugin_list:
|
||||
# search name as is
|
||||
return True
|
||||
|
||||
if f"pelican.plugins.{name}" in plugin_list:
|
||||
# check if short name is a namespace plugin
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def load_legacy_plugin(plugin, plugin_paths):
|
||||
if '.' in plugin:
|
||||
if "." in plugin:
|
||||
# it is in a package, try to resolve package first
|
||||
package, _, _ = plugin.rpartition('.')
|
||||
package, _, _ = plugin.rpartition(".")
|
||||
load_legacy_plugin(package, plugin_paths)
|
||||
|
||||
# Try to find plugin in PLUGIN_PATHS
|
||||
|
|
@ -52,7 +68,7 @@ def load_legacy_plugin(plugin, plugin_paths):
|
|||
# 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))
|
||||
raise ImportError(f"Cannot import plugin `{plugin}`")
|
||||
else:
|
||||
# Avoid loading the same plugin twice
|
||||
if spec.name in sys.modules:
|
||||
|
|
@ -78,30 +94,28 @@ def load_legacy_plugin(plugin, plugin_paths):
|
|||
|
||||
|
||||
def load_plugins(settings):
|
||||
logger.debug('Finding namespace plugins')
|
||||
logger.debug("Finding namespace plugins")
|
||||
namespace_plugins = get_namespace_plugins()
|
||||
if namespace_plugins:
|
||||
logger.debug('Namespace plugins found:\n' +
|
||||
'\n'.join(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 settings.get("PLUGINS") is not None:
|
||||
for plugin in settings["PLUGINS"]:
|
||||
if isinstance(plugin, str):
|
||||
logger.debug('Loading plugin `%s`', plugin)
|
||||
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)]
|
||||
elif f"pelican.plugins.{plugin}" in namespace_plugins:
|
||||
plugin = namespace_plugins[f"pelican.plugins.{plugin}"]
|
||||
# try to import it
|
||||
else:
|
||||
try:
|
||||
plugin = load_legacy_plugin(
|
||||
plugin,
|
||||
settings.get('PLUGIN_PATHS', []))
|
||||
plugin, settings.get("PLUGIN_PATHS", [])
|
||||
)
|
||||
except ImportError as e:
|
||||
logger.error('Cannot load plugin `%s`\n%s', plugin, e)
|
||||
logger.error("Cannot load plugin `%s`\n%s", plugin, e)
|
||||
continue
|
||||
plugins.append(plugin)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -1,49 +1,53 @@
|
|||
from blinker import signal
|
||||
from blinker import signal, Signal
|
||||
from ordered_set import OrderedSet
|
||||
|
||||
# Signals will call functions in the order of connection, i.e. plugin order
|
||||
Signal.set_class = OrderedSet
|
||||
|
||||
# Run-level signals:
|
||||
|
||||
initialized = signal('pelican_initialized')
|
||||
get_generators = signal('get_generators')
|
||||
all_generators_finalized = signal('all_generators_finalized')
|
||||
get_writer = signal('get_writer')
|
||||
finalized = signal('pelican_finalized')
|
||||
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')
|
||||
readers_init = signal("readers_init")
|
||||
|
||||
# Generator-level signals
|
||||
|
||||
generator_init = signal('generator_init')
|
||||
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')
|
||||
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')
|
||||
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')
|
||||
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')
|
||||
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')
|
||||
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')
|
||||
static_generator_preread = signal("static_generator_preread")
|
||||
static_generator_context = signal("static_generator_context")
|
||||
|
||||
content_object_init = signal('content_object_init')
|
||||
content_object_init = signal("content_object_init")
|
||||
|
||||
# Writers signals
|
||||
content_written = signal('content_written')
|
||||
feed_generated = signal('feed_generated')
|
||||
feed_written = signal('feed_written')
|
||||
content_written = signal("content_written")
|
||||
feed_generated = signal("feed_generated")
|
||||
feed_written = signal("feed_written")
|
||||
|
|
|
|||
|
|
@ -31,33 +31,29 @@ except ImportError:
|
|||
_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
|
||||
"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,
|
||||
"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__)
|
||||
|
|
@ -65,25 +61,23 @@ 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.
|
||||
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.
|
||||
Regardless, all list items undergo .strip() before returning, and
|
||||
empty items are discarded.
|
||||
"""
|
||||
if isinstance(text, str):
|
||||
if ';' in text:
|
||||
text = text.split(';')
|
||||
if ";" in text:
|
||||
text = text.split(";")
|
||||
else:
|
||||
text = text.split(',')
|
||||
text = text.split(",")
|
||||
|
||||
return list(OrderedDict.fromkeys(
|
||||
[v for v in (w.strip() for w in text) if v]
|
||||
))
|
||||
return list(OrderedDict.fromkeys([v for v in (w.strip() for w in text) if v]))
|
||||
|
||||
|
||||
def _process_if_nonempty(processor, name, settings):
|
||||
|
|
@ -112,8 +106,9 @@ class BaseReader:
|
|||
Markdown).
|
||||
|
||||
"""
|
||||
|
||||
enabled = True
|
||||
file_extensions = ['static']
|
||||
file_extensions = ["static"]
|
||||
extensions = None
|
||||
|
||||
def __init__(self, settings):
|
||||
|
|
@ -132,13 +127,12 @@ class BaseReader:
|
|||
|
||||
|
||||
class _FieldBodyTranslator(HTMLTranslator):
|
||||
|
||||
def __init__(self, document):
|
||||
super().__init__(document)
|
||||
self.compact_p = None
|
||||
|
||||
def astext(self):
|
||||
return ''.join(self.body)
|
||||
return "".join(self.body)
|
||||
|
||||
def visit_field_body(self, node):
|
||||
pass
|
||||
|
|
@ -154,27 +148,25 @@ def render_node_to_html(document, node, field_body_translator_class):
|
|||
|
||||
|
||||
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))
|
||||
if node.hasattr("explanation"):
|
||||
attrs["title"] = node["explanation"]
|
||||
self.body.append(self.starttag(node, "abbr", "", **attrs))
|
||||
|
||||
def depart_abbreviation(self, node):
|
||||
self.body.append('</abbr>')
|
||||
self.body.append("</abbr>")
|
||||
|
||||
def visit_image(self, node):
|
||||
# set an empty alt if alt is not specified
|
||||
# avoids that alt is taken from src
|
||||
node['alt'] = node.get('alt', '')
|
||||
node["alt"] = node.get("alt", "")
|
||||
return HTMLTranslator.visit_image(self, node)
|
||||
|
||||
|
||||
|
|
@ -194,7 +186,7 @@ class RstReader(BaseReader):
|
|||
"""
|
||||
|
||||
enabled = bool(docutils)
|
||||
file_extensions = ['rst']
|
||||
file_extensions = ["rst"]
|
||||
|
||||
writer_class = PelicanHTMLWriter
|
||||
field_body_translator_class = _FieldBodyTranslator
|
||||
|
|
@ -202,25 +194,28 @@ class RstReader(BaseReader):
|
|||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
lang_code = self.settings.get('DEFAULT_LANG', 'en')
|
||||
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'
|
||||
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']
|
||||
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)
|
||||
"Document title missing in file %s: "
|
||||
"Ensure exactly one top level section",
|
||||
source_path,
|
||||
)
|
||||
|
||||
try:
|
||||
# docutils 0.18.1+
|
||||
|
|
@ -231,16 +226,16 @@ class RstReader(BaseReader):
|
|||
|
||||
for docinfo in nodes:
|
||||
for element in docinfo.children:
|
||||
if element.tagname == 'field': # custom fields (e.g. summary)
|
||||
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)
|
||||
document, body_elem, self.field_body_translator_class
|
||||
)
|
||||
else:
|
||||
value = body_elem.astext()
|
||||
elif element.tagname == 'authors': # author list
|
||||
elif element.tagname == "authors": # author list
|
||||
name = element.tagname
|
||||
value = [element.astext() for element in element.children]
|
||||
else: # standard fields (e.g. address)
|
||||
|
|
@ -252,22 +247,24 @@ class RstReader(BaseReader):
|
|||
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')
|
||||
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')
|
||||
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()
|
||||
|
|
@ -277,10 +274,10 @@ class RstReader(BaseReader):
|
|||
"""Parses restructured text"""
|
||||
pub = self._get_publisher(source_path)
|
||||
parts = pub.writer.parts
|
||||
content = parts.get('body')
|
||||
content = parts.get("body")
|
||||
|
||||
metadata = self._parse_metadata(pub.document, source_path)
|
||||
metadata.setdefault('title', parts.get('title'))
|
||||
metadata.setdefault("title", parts.get("title"))
|
||||
|
||||
return content, metadata
|
||||
|
||||
|
|
@ -289,26 +286,26 @@ class MarkdownReader(BaseReader):
|
|||
"""Reader for Markdown files"""
|
||||
|
||||
enabled = bool(Markdown)
|
||||
file_extensions = ['md', 'markdown', 'mkd', 'mdown']
|
||||
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')
|
||||
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']
|
||||
formatted_fields = self.settings["FORMATTED_FIELDS"]
|
||||
|
||||
# prevent metadata extraction in fields
|
||||
self._md.preprocessors.deregister('meta')
|
||||
self._md.preprocessors.deregister("meta")
|
||||
|
||||
output = {}
|
||||
for name, value in meta.items():
|
||||
|
|
@ -323,9 +320,10 @@ class MarkdownReader(BaseReader):
|
|||
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)
|
||||
"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
|
||||
|
|
@ -339,11 +337,11 @@ class MarkdownReader(BaseReader):
|
|||
"""Parse content and metadata of markdown files"""
|
||||
|
||||
self._source_path = source_path
|
||||
self._md = Markdown(**self.settings['MARKDOWN'])
|
||||
self._md = Markdown(**self.settings["MARKDOWN"])
|
||||
with pelican_open(source_path) as text:
|
||||
content = self._md.convert(text)
|
||||
|
||||
if hasattr(self._md, 'Meta'):
|
||||
if hasattr(self._md, "Meta"):
|
||||
metadata = self._parse_metadata(self._md.Meta)
|
||||
else:
|
||||
metadata = {}
|
||||
|
|
@ -353,17 +351,17 @@ class MarkdownReader(BaseReader):
|
|||
class HTMLReader(BaseReader):
|
||||
"""Parses HTML files as input, looking for meta, title, and body tags"""
|
||||
|
||||
file_extensions = ['htm', 'html']
|
||||
file_extensions = ["htm", "html"]
|
||||
enabled = True
|
||||
|
||||
class _HTMLParser(HTMLParser):
|
||||
def __init__(self, settings, filename):
|
||||
super().__init__(convert_charrefs=False)
|
||||
self.body = ''
|
||||
self.body = ""
|
||||
self.metadata = {}
|
||||
self.settings = settings
|
||||
|
||||
self._data_buffer = ''
|
||||
self._data_buffer = ""
|
||||
|
||||
self._filename = filename
|
||||
|
||||
|
|
@ -374,94 +372,100 @@ class HTMLReader(BaseReader):
|
|||
self._in_tags = False
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag == 'head' and self._in_top_level:
|
||||
if tag == "head" and self._in_top_level:
|
||||
self._in_top_level = False
|
||||
self._in_head = True
|
||||
elif tag == 'title' and self._in_head:
|
||||
elif tag == "title" and self._in_head:
|
||||
self._in_title = True
|
||||
self._data_buffer = ''
|
||||
elif tag == 'body' and self._in_top_level:
|
||||
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._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 tag == "head":
|
||||
if self._in_head:
|
||||
self._in_head = False
|
||||
self._in_top_level = True
|
||||
elif self._in_head and tag == 'title':
|
||||
elif self._in_head and tag == "title":
|
||||
self._in_title = False
|
||||
self.metadata['title'] = self._data_buffer
|
||||
elif tag == 'body':
|
||||
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))
|
||||
self._data_buffer += f"</{escape(tag)}>"
|
||||
|
||||
def handle_startendtag(self, tag, attrs):
|
||||
if tag == 'meta' and self._in_head:
|
||||
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)
|
||||
self._data_buffer += f"<!--{data}-->"
|
||||
|
||||
def handle_data(self, data):
|
||||
self._data_buffer += data
|
||||
|
||||
def handle_entityref(self, data):
|
||||
self._data_buffer += '&{};'.format(data)
|
||||
self._data_buffer += f"&{data};"
|
||||
|
||||
def handle_charref(self, data):
|
||||
self._data_buffer += '&#{};'.format(data)
|
||||
self._data_buffer += f"&#{data};"
|
||||
|
||||
def build_tag(self, tag, attrs, close_tag):
|
||||
result = '<{}'.format(escape(tag))
|
||||
result = f"<{escape(tag)}"
|
||||
for k, v in attrs:
|
||||
result += ' ' + escape(k)
|
||||
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))
|
||||
result += f"='{escape(v, quote=False)}'"
|
||||
else:
|
||||
result += '="{}"'.format(escape(v, quote=False))
|
||||
result += f'="{escape(v, quote=False)}"'
|
||||
if close_tag:
|
||||
return result + ' />'
|
||||
return result + '>'
|
||||
return result + " />"
|
||||
return result + ">"
|
||||
|
||||
def _handle_meta_tag(self, attrs):
|
||||
name = self._attr_value(attrs, 'name')
|
||||
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)
|
||||
attr_list = [f'{k}="{v}"' for k, v in attrs]
|
||||
attr_serialized = ", ".join(attr_list)
|
||||
logger.warning(
|
||||
"Meta tag in file %s does not have a 'name' "
|
||||
"attribute, skipping. Attributes: %s",
|
||||
self._filename,
|
||||
attr_serialized,
|
||||
)
|
||||
return
|
||||
name = name.lower()
|
||||
contents = self._attr_value(attrs, 'content', '')
|
||||
contents = self._attr_value(attrs, "content", "")
|
||||
if not contents:
|
||||
contents = self._attr_value(attrs, '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'"})
|
||||
extra={
|
||||
"limit_msg": "Other files have meta tag "
|
||||
"attribute 'contents' that should "
|
||||
"be changed to 'content'"
|
||||
},
|
||||
)
|
||||
|
||||
if name == 'keywords':
|
||||
name = 'tags'
|
||||
if name == "keywords":
|
||||
name = "tags"
|
||||
|
||||
if name in self.metadata:
|
||||
# if this metadata already exists (i.e. a previous tag with the
|
||||
|
|
@ -501,22 +505,23 @@ class Readers(FileStampDataCacher):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self, settings=None, cache_name=''):
|
||||
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))
|
||||
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'])
|
||||
if self.settings["READERS"]:
|
||||
self.reader_classes.update(self.settings["READERS"])
|
||||
|
||||
signals.readers_init.send(self)
|
||||
|
||||
|
|
@ -527,53 +532,67 @@ class Readers(FileStampDataCacher):
|
|||
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']
|
||||
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):
|
||||
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__)
|
||||
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)
|
||||
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)
|
||||
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)))
|
||||
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()
|
||||
metadata["reader"] = reader_name.replace("Reader", "").lower()
|
||||
|
||||
content, reader_metadata = self.get_cached_data(path, (None, None))
|
||||
if content is None:
|
||||
|
|
@ -587,14 +606,14 @@ class Readers(FileStampDataCacher):
|
|||
find_empty_alt(content, path)
|
||||
|
||||
# eventually filter the content with typogrify if asked so
|
||||
if self.settings['TYPOGRIFY']:
|
||||
if self.settings["TYPOGRIFY"]:
|
||||
from typogrify.filters import typogrify
|
||||
import smartypants
|
||||
|
||||
typogrify_dashes = self.settings['TYPOGRIFY_DASHES']
|
||||
if typogrify_dashes == 'oldschool':
|
||||
typogrify_dashes = self.settings["TYPOGRIFY_DASHES"]
|
||||
if typogrify_dashes == "oldschool":
|
||||
smartypants.Attr.default = smartypants.Attr.set2
|
||||
elif typogrify_dashes == 'oldschool_inverted':
|
||||
elif typogrify_dashes == "oldschool_inverted":
|
||||
smartypants.Attr.default = smartypants.Attr.set3
|
||||
else:
|
||||
smartypants.Attr.default = smartypants.Attr.set1
|
||||
|
|
@ -608,31 +627,32 @@ class Readers(FileStampDataCacher):
|
|||
def typogrify_wrapper(text):
|
||||
"""Ensures ignore_tags feature is backward compatible"""
|
||||
try:
|
||||
return typogrify(
|
||||
text,
|
||||
self.settings['TYPOGRIFY_IGNORE_TAGS'])
|
||||
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 "title" in metadata:
|
||||
metadata["title"] = typogrify_wrapper(metadata["title"])
|
||||
|
||||
if 'summary' in metadata:
|
||||
metadata['summary'] = typogrify_wrapper(metadata['summary'])
|
||||
if "summary" in metadata:
|
||||
metadata["summary"] = typogrify_wrapper(metadata["summary"])
|
||||
|
||||
if context_signal:
|
||||
logger.debug(
|
||||
'Signal %s.send(%s, <metadata>)',
|
||||
context_signal.name,
|
||||
context_sender)
|
||||
"Signal %s.send(%s, <metadata>)", 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)
|
||||
return content_class(
|
||||
content=content,
|
||||
metadata=metadata,
|
||||
settings=self.settings,
|
||||
source_path=path,
|
||||
context=context,
|
||||
)
|
||||
|
||||
|
||||
def find_empty_alt(content, path):
|
||||
|
|
@ -642,7 +662,8 @@ def find_empty_alt(content, path):
|
|||
as they are really likely to be accessibility flaws.
|
||||
|
||||
"""
|
||||
imgs = re.compile(r"""
|
||||
imgs = re.compile(
|
||||
r"""
|
||||
(?:
|
||||
# src before alt
|
||||
<img
|
||||
|
|
@ -658,53 +679,57 @@ def find_empty_alt(content, path):
|
|||
[^\>]*
|
||||
src=(['"])(.*?)\5
|
||||
)
|
||||
""", re.X)
|
||||
""",
|
||||
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'})
|
||||
"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():
|
||||
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 "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'])
|
||||
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'])
|
||||
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']
|
||||
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', {})
|
||||
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, ''))
|
||||
dirpath = posixize_path(os.path.join(path, ""))
|
||||
if source_path == path or source_path.startswith(dirpath):
|
||||
metadata.update(meta)
|
||||
|
||||
|
|
@ -736,11 +761,10 @@ def parse_path_metadata(source_path, settings=None, process=None):
|
|||
subdir = os.path.basename(dirname)
|
||||
if settings:
|
||||
checks = []
|
||||
for key, data in [('FILENAME_METADATA', base),
|
||||
('PATH_METADATA', source_path)]:
|
||||
for key, data in [("FILENAME_METADATA", base), ("PATH_METADATA", source_path)]:
|
||||
checks.append((settings.get(key, None), data))
|
||||
if settings.get('USE_FOLDER_AS_CATEGORY', None):
|
||||
checks.append(('(?P<category>.*)', subdir))
|
||||
if settings.get("USE_FOLDER_AS_CATEGORY", None):
|
||||
checks.append(("(?P<category>.*)", subdir))
|
||||
for regexp, data in checks:
|
||||
if regexp and data:
|
||||
match = re.match(regexp, data)
|
||||
|
|
|
|||
|
|
@ -11,26 +11,26 @@ import pelican.settings as pys
|
|||
|
||||
|
||||
class Pygments(Directive):
|
||||
""" Source code syntax highlighting.
|
||||
"""
|
||||
"""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,
|
||||
"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
|
||||
|
||||
|
|
@ -49,28 +49,30 @@ class Pygments(Directive):
|
|||
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')
|
||||
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'
|
||||
self.options["linenos"] = "table"
|
||||
|
||||
for flag in ('nowrap', 'nobackground', 'anchorlinenos'):
|
||||
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')]
|
||||
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)
|
||||
directives.register_directive("code-block", Pygments)
|
||||
directives.register_directive("sourcecode", Pygments)
|
||||
|
||||
|
||||
_abbr_re = re.compile(r'\((.*)\)$', re.DOTALL)
|
||||
_abbr_re = re.compile(r"\((.*)\)$", re.DOTALL)
|
||||
|
||||
|
||||
class abbreviation(nodes.Inline, nodes.TextElement):
|
||||
|
|
@ -82,9 +84,9 @@ def abbr_role(typ, rawtext, text, lineno, inliner, options={}, content=[]):
|
|||
m = _abbr_re.search(text)
|
||||
if m is None:
|
||||
return [abbreviation(text, text)], []
|
||||
abbr = text[:m.start()].strip()
|
||||
abbr = text[: m.start()].strip()
|
||||
expl = m.group(1)
|
||||
return [abbreviation(abbr, abbr, explanation=expl)], []
|
||||
|
||||
|
||||
roles.register_local_role('abbr', abbr_role)
|
||||
roles.register_local_role("abbr", abbr_role)
|
||||
|
|
|
|||
|
|
@ -14,38 +14,47 @@ except ImportError:
|
|||
|
||||
from pelican.log import console # noqa: F401
|
||||
from pelican.log import init as init_logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def parse_arguments():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Pelican Development Server',
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
||||
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",
|
||||
)
|
||||
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', '/', '']
|
||||
SUFFIXES = [".html", "/index.html", "/", ""]
|
||||
|
||||
extensions_map = {
|
||||
**server.SimpleHTTPRequestHandler.extensions_map,
|
||||
** {
|
||||
**{
|
||||
# web fonts
|
||||
".oft": "font/oft",
|
||||
".sfnt": "font/sfnt",
|
||||
|
|
@ -57,13 +66,13 @@ class ComplexHTTPRequestHandler(server.SimpleHTTPRequestHandler):
|
|||
|
||||
def translate_path(self, path):
|
||||
# abandon query parameters
|
||||
path = path.split('?', 1)[0]
|
||||
path = path.split('#', 1)[0]
|
||||
path = path.split("?", 1)[0]
|
||||
path = path.split("#", 1)[0]
|
||||
# Don't forget explicit trailing slash when normalizing. Issue17324
|
||||
trailing_slash = path.rstrip().endswith('/')
|
||||
trailing_slash = path.rstrip().endswith("/")
|
||||
path = urllib.parse.unquote(path)
|
||||
path = posixpath.normpath(path)
|
||||
words = path.split('/')
|
||||
words = path.split("/")
|
||||
words = filter(None, words)
|
||||
path = self.base_path
|
||||
for word in words:
|
||||
|
|
@ -72,12 +81,12 @@ class ComplexHTTPRequestHandler(server.SimpleHTTPRequestHandler):
|
|||
continue
|
||||
path = os.path.join(path, word)
|
||||
if trailing_slash:
|
||||
path += '/'
|
||||
path += "/"
|
||||
return path
|
||||
|
||||
def do_GET(self):
|
||||
# cut off a query string
|
||||
original_path = self.path.split('?', 1)[0]
|
||||
original_path = self.path.split("?", 1)[0]
|
||||
# try to find file
|
||||
self.path = self.get_path_that_exists(original_path)
|
||||
|
||||
|
|
@ -88,12 +97,12 @@ class ComplexHTTPRequestHandler(server.SimpleHTTPRequestHandler):
|
|||
|
||||
def get_path_that_exists(self, original_path):
|
||||
# Try to strip trailing slash
|
||||
trailing_slash = original_path.endswith('/')
|
||||
original_path = original_path.rstrip('/')
|
||||
trailing_slash = original_path.endswith("/")
|
||||
original_path = original_path.rstrip("/")
|
||||
# Try to detect file by applying various suffixes
|
||||
tries = []
|
||||
for suffix in self.SUFFIXES:
|
||||
if not trailing_slash and suffix == '/':
|
||||
if not trailing_slash and suffix == "/":
|
||||
# if original request does not have trailing slash, skip the '/' suffix
|
||||
# so that base class can redirect if needed
|
||||
continue
|
||||
|
|
@ -101,18 +110,17 @@ class ComplexHTTPRequestHandler(server.SimpleHTTPRequestHandler):
|
|||
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))
|
||||
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.
|
||||
"""
|
||||
"""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:
|
||||
if mimetype == "application/octet-stream" and magic_from_file:
|
||||
mimetype = magic_from_file(path, mime=True)
|
||||
|
||||
return mimetype
|
||||
|
|
@ -127,31 +135,33 @@ class RootedHTTPServer(server.HTTPServer):
|
|||
self.RequestHandlerClass.base_path = base_path
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
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.")
|
||||
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)
|
||||
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)
|
||||
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.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)
|
||||
logger.info("Serving at port %s, server %s.", args.port, args.server)
|
||||
try:
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
raise ImportError(
|
||||
'Importing from `pelican.signals` is deprecated. '
|
||||
'Use `from pelican import signals` or `import pelican.plugins.signals` instead.'
|
||||
"Importing from `pelican.signals` is deprecated. "
|
||||
"Use `from pelican import signals` or `import pelican.plugins.signals` instead."
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,4 +9,4 @@ Used for pelican test
|
|||
|
||||
The quick brown fox .
|
||||
|
||||
This page is a draft
|
||||
This page is a draft
|
||||
|
|
|
|||
7
pelican/tests/build_test/conftest.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
def pytest_addoption(parser):
|
||||
parser.addoption(
|
||||
"--check-build",
|
||||
action="store",
|
||||
default=False,
|
||||
help="Check wheel contents.",
|
||||
)
|
||||
66
pelican/tests/build_test/test_build_files.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
from re import match
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
from zipfile import ZipFile
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
"not config.getoption('--check-build')",
|
||||
reason="Only run when --check-build is given",
|
||||
)
|
||||
def test_wheel_contents(pytestconfig):
|
||||
"""
|
||||
This test should test the contents of the wheel to make sure
|
||||
that everything that is needed is included in the final build
|
||||
"""
|
||||
dist_folder = pytestconfig.getoption("--check-build")
|
||||
wheels = Path(dist_folder).rglob("*.whl")
|
||||
for wheel_file in wheels:
|
||||
files_list = ZipFile(wheel_file).namelist()
|
||||
# Check if theme files are copied to wheel
|
||||
simple_theme = Path("./pelican/themes/simple/templates")
|
||||
for x in simple_theme.iterdir():
|
||||
assert str(x) in files_list
|
||||
|
||||
# Check if tool templates are copied to wheel
|
||||
tools = Path("./pelican/tools/templates")
|
||||
for x in tools.iterdir():
|
||||
assert str(x) in files_list
|
||||
|
||||
assert "pelican/tools/templates/tasks.py.jinja2" in files_list
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
"not config.getoption('--check-build')",
|
||||
reason="Only run when --check-build is given",
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"expected_file",
|
||||
[
|
||||
("THANKS"),
|
||||
("README.rst"),
|
||||
("CONTRIBUTING.rst"),
|
||||
("docs/changelog.rst"),
|
||||
("samples/"),
|
||||
],
|
||||
)
|
||||
def test_sdist_contents(pytestconfig, expected_file):
|
||||
"""
|
||||
This test should test the contents of the source distribution to make sure
|
||||
that everything that is needed is included in the final build.
|
||||
"""
|
||||
dist_folder = pytestconfig.getoption("--check-build")
|
||||
sdist_files = Path(dist_folder).rglob("*.tar.gz")
|
||||
for dist in sdist_files:
|
||||
files_list = tarfile.open(dist, "r:gz").getnames()
|
||||
dir_matcher = ""
|
||||
if expected_file.endswith("/"):
|
||||
dir_matcher = ".*"
|
||||
filtered_values = [
|
||||
path
|
||||
for path in files_list
|
||||
if match(rf"^pelican-\d\.\d\.\d/{expected_file}{dir_matcher}$", path)
|
||||
]
|
||||
assert len(filtered_values) > 0
|
||||
|
|
@ -3,4 +3,3 @@ author: Alexis Métaireau
|
|||
|
||||
Markdown with filename metadata
|
||||
===============================
|
||||
|
||||
|
|
|
|||
|
|
@ -5,4 +5,3 @@ Title: Test Markdown extensions
|
|||
## Level1
|
||||
|
||||
### Level2
|
||||
|
||||
|
|
|
|||
|
|
@ -3,4 +3,3 @@ This is a super article !
|
|||
#########################
|
||||
|
||||
:Category: Yeah
|
||||
|
||||
|
|
|
|||
|
|
@ -3,4 +3,3 @@ This is an article without category !
|
|||
#####################################
|
||||
|
||||
This article should be in the DEFAULT_CATEGORY.
|
||||
|
||||
|
|
|
|||
2
pelican/tests/content/bloggerexport.xml
vendored
|
|
@ -1064,4 +1064,4 @@
|
|||
<gd:extendedProperty name="blogger.itemClass" value="pid-944253050"/>
|
||||
<gd:extendedProperty name="blogger.displayTime" value="29 november 2010 om 12:35"/>
|
||||
</entry>
|
||||
</feed>
|
||||
</feed>
|
||||
|
|
|
|||
2
pelican/tests/content/empty_with_bom.md
vendored
|
|
@ -1 +1 @@
|
|||
|
||||
|
||||
|
|
|
|||
|
|
@ -52,4 +52,3 @@ 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.
|
||||
|
||||
|
|
|
|||
49
pelican/tests/content/wordpressexport.xml
vendored
|
|
@ -685,7 +685,52 @@ proident, sunt in culpa qui officia deserunt mollit anim id est laborum.]]></con
|
|||
<wp:meta_key>_edit_last</wp:meta_key>
|
||||
<wp:meta_value><![CDATA[3]]></wp:meta_value>
|
||||
</wp:postmeta>
|
||||
</item>
|
||||
</item>
|
||||
<item>
|
||||
<title>Caption on image</title>
|
||||
<link>http://thisisa.test/?p=176</link>
|
||||
<pubDate>Thu, 01 Jan 1970 00:00:00 +0000</pubDate>
|
||||
<dc:creator>bob</dc:creator>
|
||||
<guid isPermaLink="false">http://thisisa.test/?p=176</guid>
|
||||
<description></description>
|
||||
<content:encoded><![CDATA[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.
|
||||
|
||||
[caption attachment_id="42" align="aligncenter" width="300" caption="This is a pelican"]<img src="/theme/img/xpelican.png.pagespeed.ic.Rjep0025-y.png"/>[/caption]
|
||||
|
||||
[caption attachment_id="43" align="aligncenter" width="300"]<img src="/theme/img/xpelican-3.png.pagespeed.ic.m-NAIdRCOM.png" width="300" height="216" class="size-medium wp-image-1055" /> This also a pelican[/caption]
|
||||
|
||||
[caption attachment_id="44" align="aligncenter" width="300"]<a href="https://getpelican.com/"><img src="/theme/img/xpelican.png.pagespeed.ic.Rjep0025-y.png" alt=""/> Yet another pelican[/caption]
|
||||
|
||||
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.]]></content:encoded>
|
||||
<excerpt:encoded><![CDATA[]]></excerpt:encoded>
|
||||
<wp:post_id>176</wp:post_id>
|
||||
<wp:post_date>2012-02-16 15:52:55</wp:post_date>
|
||||
<wp:post_date_gmt>0000-00-00 00:00:00</wp:post_date_gmt>
|
||||
<wp:comment_status>open</wp:comment_status>
|
||||
<wp:ping_status>open</wp:ping_status>
|
||||
<wp:post_name>caption-on-image</wp:post_name>
|
||||
<wp:status>publish</wp:status>
|
||||
<wp:post_parent>0</wp:post_parent>
|
||||
<wp:menu_order>0</wp:menu_order>
|
||||
<wp:post_type>post</wp:post_type>
|
||||
<wp:post_password></wp:post_password>
|
||||
<wp:is_sticky>0</wp:is_sticky>
|
||||
<category domain="category" nicename="category-2"><![CDATA[Category 2]]></category>
|
||||
<wp:postmeta>
|
||||
<wp:meta_key>_edit_last</wp:meta_key>
|
||||
<wp:meta_value><![CDATA[3]]></wp:meta_value>
|
||||
</wp:postmeta>
|
||||
</item>
|
||||
<item>
|
||||
<title>A custom post in category 4</title>
|
||||
<link>http://thisisa.test/?p=175</link>
|
||||
|
|
@ -793,7 +838,7 @@ proident, sunt in culpa qui officia deserunt mollit anim id est laborum.]]></con
|
|||
<wp:meta_key>_edit_last</wp:meta_key>
|
||||
<wp:meta_value><![CDATA[3]]></wp:meta_value>
|
||||
</wp:postmeta>
|
||||
</item>
|
||||
</item>
|
||||
<item>
|
||||
<title>A 2nd custom post type also in category 5</title>
|
||||
<link>http://thisisa.test/?p=177</link>
|
||||
|
|
|
|||
|
|
@ -1,43 +1,47 @@
|
|||
AUTHOR = 'Alexis Métaireau'
|
||||
AUTHOR = "Alexis Métaireau"
|
||||
SITENAME = "Alexis' log"
|
||||
SITEURL = 'http://blog.notmyidea.org'
|
||||
TIMEZONE = 'UTC'
|
||||
SITEURL = "http://blog.notmyidea.org"
|
||||
TIMEZONE = "UTC"
|
||||
|
||||
GITHUB_URL = 'http://github.com/ametaireau/'
|
||||
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'
|
||||
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/"),)
|
||||
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'),)
|
||||
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'}
|
||||
DEFAULT_METADATA = {"yeah": "it is"}
|
||||
|
||||
# path-specific metadata
|
||||
EXTRA_PATH_METADATA = {
|
||||
'extra/robots.txt': {'path': 'robots.txt'},
|
||||
"extra/robots.txt": {"path": "robots.txt"},
|
||||
}
|
||||
|
||||
# static paths will be copied without parsing their contents
|
||||
STATIC_PATHS = [
|
||||
'pictures',
|
||||
'extra/robots.txt',
|
||||
"pictures",
|
||||
"extra/robots.txt",
|
||||
]
|
||||
|
||||
FORMATTED_FIELDS = ['summary', 'custom_formatted_field']
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
NAME = 'namespace plugin'
|
||||
NAME = "namespace plugin"
|
||||
|
||||
|
||||
def register():
|
||||
|
|
|
|||
|
|
@ -58,10 +58,10 @@
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
4
pelican/tests/output/basic/archives.html
vendored
|
|
@ -60,10 +60,10 @@
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
4
pelican/tests/output/basic/article-1.html
vendored
|
|
@ -57,10 +57,10 @@
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
4
pelican/tests/output/basic/article-2.html
vendored
|
|
@ -57,10 +57,10 @@
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
4
pelican/tests/output/basic/article-3.html
vendored
|
|
@ -57,10 +57,10 @@
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -102,10 +102,10 @@ YEAH !</p>
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
4
pelican/tests/output/basic/authors.html
vendored
|
|
@ -42,10 +42,10 @@
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
4
pelican/tests/output/basic/categories.html
vendored
|
|
@ -45,10 +45,10 @@
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
4
pelican/tests/output/basic/category/bar.html
vendored
|
|
@ -58,10 +58,10 @@ YEAH !</p>
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -115,10 +115,10 @@
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -126,10 +126,10 @@ pelican.conf, it will …</p></div>
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -66,10 +66,10 @@
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -58,10 +58,10 @@ listed anywhere else.</p>
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -58,10 +58,10 @@ listed anywhere else.</p>
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -57,10 +57,10 @@
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
4
pelican/tests/output/basic/index.html
vendored
|
|
@ -265,10 +265,10 @@ pelican.conf, it will …</p></div>
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
4
pelican/tests/output/basic/oh-yeah-fr.html
vendored
|
|
@ -61,10 +61,10 @@ Translations:
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
4
pelican/tests/output/basic/oh-yeah.html
vendored
|
|
@ -69,10 +69,10 @@ YEAH !</p>
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -41,10 +41,10 @@ at a custom location.</p>
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -41,10 +41,10 @@ Anyone can see this page but it's not linked to anywhere!</p>
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -42,10 +42,10 @@
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -61,10 +61,10 @@
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -61,10 +61,10 @@
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
4
pelican/tests/output/basic/tag/bar.html
vendored
|
|
@ -114,10 +114,10 @@ YEAH !</p>
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
4
pelican/tests/output/basic/tag/baz.html
vendored
|
|
@ -57,10 +57,10 @@
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
4
pelican/tests/output/basic/tag/foo.html
vendored
|
|
@ -84,10 +84,10 @@ as well as <strong>inline markup</strong>.</p>
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
4
pelican/tests/output/basic/tag/foobar.html
vendored
|
|
@ -66,10 +66,10 @@
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
4
pelican/tests/output/basic/tag/oh.html
vendored
|
|
@ -40,10 +40,10 @@
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
4
pelican/tests/output/basic/tag/yeah.html
vendored
|
|
@ -58,10 +58,10 @@ YEAH !</p>
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
4
pelican/tests/output/basic/tags.html
vendored
|
|
@ -47,10 +47,10 @@
|
|||
|
||||
<footer id="contentinfo" class="body">
|
||||
<address id="about" class="vcard body">
|
||||
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
|
||||
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
|
||||
</address><!-- /#about -->
|
||||
|
||||
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
|
||||
</footer><!-- /#contentinfo -->
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -322,39 +322,6 @@ div.figure p.caption, figure p.caption { /* margin provided by figure */
|
|||
max-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
|
||||
*****************/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
Copyright 2010 The Yanone Kaffeesatz Project Authors (https://github.com/alexeiva/yanone-kaffeesatz)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
Before Width: | Height: | Size: 411 B |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 827 B |
|
Before Width: | Height: | Size: 150 B |
|
Before Width: | Height: | Size: 606 B |
|
Before Width: | Height: | Size: 223 B |
|
Before Width: | Height: | Size: 402 B |
|
Before Width: | Height: | Size: 420 B |
|
Before Width: | Height: | Size: 511 B |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 840 B |
|
Before Width: | Height: | Size: 625 B |
|
Before Width: | Height: | Size: 458 B |
|
Before Width: | Height: | Size: 751 B |
|
Before Width: | Height: | Size: 435 B |
|
Before Width: | Height: | Size: 580 B |
|
Before Width: | Height: | Size: 414 B |