pax_global_header 0000666 0000000 0000000 00000000064 14355111041 0014505 g ustar 00root root 0000000 0000000 52 comment=e009ab92fa75c6eb77363344385bac8feb87be44
sphinxext-opengraph-0.7.5/ 0000775 0000000 0000000 00000000000 14355111041 0015531 5 ustar 00root root 0000000 0000000 sphinxext-opengraph-0.7.5/.github/ 0000775 0000000 0000000 00000000000 14355111041 0017071 5 ustar 00root root 0000000 0000000 sphinxext-opengraph-0.7.5/.github/workflows/ 0000775 0000000 0000000 00000000000 14355111041 0021126 5 ustar 00root root 0000000 0000000 sphinxext-opengraph-0.7.5/.github/workflows/workflow.yml 0000664 0000000 0000000 00000007713 14355111041 0023533 0 ustar 00root root 0000000 0000000 name: Test and Deploy
on:
pull_request:
branches:
- main
push:
env:
FORCE_COLOR: 1
jobs:
check:
runs-on: ubuntu-latest
# We want to run on external PRs, but not on our own internal PRs as they'll be run
# by the push to the branch.
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.x'
cache: pip
cache-dependency-path: .github/workflows/workflow.yml
- name: Black
run: |
pip install black
black --check --exclude /docs --diff .
build-wheel:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.7"
cache: pip
cache-dependency-path: |
.github/workflows/workflow.yml
dev-requirements.txt
- name: Install dependencies
run: |
set -xe
python -VV
python -m site
python -m pip install --upgrade pip setuptools wheel
python -m pip install -r dev-requirements.txt
- name: Install package
run: |
python -m pip install .
- name: Build wheel
run: |
python -m pip install build
python -m build
- name: Upload sdist and wheel artifacts
uses: actions/upload-artifact@v3
with:
name: my-dist
path: dist/*
test:
needs: build-wheel
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', 'pypy3.8']
sphinx-version: ['>=4,<5', '>=5,<6', '>=6a0,<7']
os: [windows-latest, macos-latest, ubuntu-latest]
exclude:
# Sphinx 6 supports 3.8+
- { python-version: '3.7', sphinx-version: '>=6a0,<7' }
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: pip
cache-dependency-path: |
.github/workflows/workflow.yml
dev-requirements.txt
- name: Install dependencies
run: |
python -VV
python -m site
python -m pip install --upgrade pip setuptools wheel
python -m pip install -r dev-requirements.txt
python -m pip install "sphinx${{ matrix.sphinx-version }}"
- name: Download sdist and wheel artifacts
uses: actions/download-artifact@v3
with:
name: my-dist
path: dist
- name: Install downloaded wheel
run: |
python -m pip install --only-binary=:all: --no-index --find-links=dist sphinxext-opengraph
- name: Run tests for ${{ matrix.python-version }}
run: |
python -m pytest -vv
build-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
cache: pip
cache-dependency-path: docs/requirements.txt
- name: Install dependencies
run: |
pip install -r docs/requirements.txt
- name: Build documentation
run: |
cd docs
make html
pypi-release:
needs: test
runs-on: ubuntu-latest
if: contains(github.ref, 'refs/tags/') && github.repository_owner == 'wpilibsuite'
steps:
- name: Download sdist and wheel artifacts
uses: actions/download-artifact@v3
with:
name: my-dist
path: dist
- name: Publish a Python distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.pypi_password }}
sphinxext-opengraph-0.7.5/.gitignore 0000664 0000000 0000000 00000011673 14355111041 0017531 0 ustar 00root root 0000000 0000000
# Created by https://www.toptal.com/developers/gitignore/api/windows,linux,python,pycharm,visualstudiocode
# Edit at https://www.toptal.com/developers/gitignore?templates=windows,linux,python,pycharm,visualstudiocode
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### PyCharm ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### PyCharm Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr
# Sonarlint plugin
.idea/**/sonarlint/
# SonarQube Plugin
.idea/**/sonarIssues.xml
# Markdown Navigator plugin
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/**/markdown-navigator/
# Cache file creation bug
# See https://youtrack.jetbrains.com/issue/JBR-2257
.idea/$CACHE_FILE$
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
docs/build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/windows,linux,python,pycharm,visualstudiocode sphinxext-opengraph-0.7.5/.readthedocs.yml 0000664 0000000 0000000 00000000331 14355111041 0020614 0 ustar 00root root 0000000 0000000 version: 2
python:
version: 3
install:
- method: pip
path: .
extra_requirements:
- rtd
- requirements: docs/requirements.txt
sphinx:
builder: html
fail_on_warning: true
sphinxext-opengraph-0.7.5/LICENSE.md 0000664 0000000 0000000 00000002705 14355111041 0017141 0 ustar 00root root 0000000 0000000 Copyright (c) 2020 FIRST
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the FIRST nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY FIRST AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY NONINFRINGEMENT AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL FIRST OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. sphinxext-opengraph-0.7.5/README.md 0000664 0000000 0000000 00000011561 14355111041 0017014 0 ustar 00root root 0000000 0000000 # sphinxext-opengraph
[](https://github.com/wpilibsuite/sphinxext-opengraph/actions)
[](https://github.com/psf/black)
Sphinx extension to generate [Open Graph metadata](https://ogp.me/).
## Installation
```sh
python -m pip install sphinxext-opengraph
```
## Usage
Just add `sphinxext.opengraph` to your extensions list in your `conf.py`
```python
extensions = [
"sphinxext.opengraph",
]
```
## Options
These values are placed in the `conf.py` of your Sphinx project.
Users hosting documentation on Read The Docs *do not* need to set any of the following unless custom configuration is wanted. The extension will automatically retrieve your site URL.
* `ogp_site_url`
* This config option is very important, set it to the URL the site is being hosted on.
* `ogp_description_length`
* Configure the amount of characters taken from a page. The default of 200 is probably good for most people. If something other than a number is used, it defaults back to 200.
* `ogp_site_name`
* This is not required. Name of the site. This is displayed above the title. Defaults to the Sphinx [`project`](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-project) config value. Set to `False` to unset and use no default.
* `ogp_image`
* This is not required. Link to image to show. Note that all relative paths are converted to be relative to the root of the html output as defined by `ogp_site_url`.
* `ogp_image_alt`
* This is not required. Alt text for image. Defaults to using `ogp_site_name` or the document's title as alt text, if available. Set to `False` if you want to turn off alt text completely.
* `ogp_use_first_image`
* This is not required. Set to `True` to use each page's first image, if available. If set to `True` but no image is found, Sphinx will use `ogp_image` instead.
* `ogp_type`
* This sets the ogp type attribute, for more information on the types available please take a look at [https://ogp.me/#types](https://ogp.me/#types). By default it is set to `website`, which should be fine for most use cases.
* `ogp_custom_meta_tags`
* This is not required. List of custom html snippets to insert.
* `ogp_enable_meta_description`
* This is not required. When `True`, generates `` from the page.
## Example Config
### Simple Config
```python
ogp_site_url = "http://example.org/"
ogp_image = "http://example.org/image.png"
```
### Advanced Config
```python
ogp_site_url = "http://example.org/"
ogp_image = "http://example.org/image.png"
ogp_description_length = 300
ogp_type = "article"
ogp_custom_meta_tags = [
'',
]
ogp_enable_meta_description = True
```
## Per Page Overrides
[Field lists](https://www.sphinx-doc.org/en/master/usage/restructuredtext/field-lists.html) are used to allow you to override certain settings on each page and set unsupported arbitrary Open Graph tags.
Make sure you place the fields at the very start of the document such that Sphinx will pick them up and also won't build them into the html.
### Overrides
These are some overrides that can be used on individual pages, you can actually override any tag and field lists will always take priority.
* `:ogp_description_length:`
* Configure the amount of characters to grab for the description of the page. If the value isn't a number it will fall back to `ogp_description_length`. Note the slightly different syntax because this isn't directly an Open Graph tag.
* `:og:description:`
* Lets you override the description of the page.
* `:description:` or `.. meta::\n :description:`
* Sets the `` description.
* `:og:title:`
* Lets you override the title of the page.
* `:og:type:`
* Override the type of the page, for the list of available types take a look at https://ogp.me/#types.
* `:og:image:`
* Set the image for the page.[^1]
* `:og:image:alt:`
* Sets the alt text. Will be ignored if there is no image set.
### Example
Remember that the fields **must** be placed at the very start of the file. You can verify Sphinx has picked up the fields if they aren't shown in the final html file.
```rst
:og:description: New description
:og:image: http://example.org/image.png
:og:image:alt: Example Image
Page contents
=============
```
### Arbitrary Tags[^1]
Additionally, you can use field lists to add any arbitrary Open Graph tag not supported by the extension. The syntax for arbitrary tags is the same with `:og:tag: content`. For example:
```rst
:og:video: http://example.org/video.mp4
Page contents
=============
```
[^1]: Note: Relative file paths for images, videos and audio are currently **not** supported when using field lists. Please use an absolute path instead.
sphinxext-opengraph-0.7.5/dev-requirements.txt 0000664 0000000 0000000 00000000114 14355111041 0021565 0 ustar 00root root 0000000 0000000 sphinx
wheel==0.37.1
pytest==7.1.3
beautifulsoup4==4.11.1
setuptools==65.4.1 sphinxext-opengraph-0.7.5/docs/ 0000775 0000000 0000000 00000000000 14355111041 0016461 5 ustar 00root root 0000000 0000000 sphinxext-opengraph-0.7.5/docs/Makefile 0000664 0000000 0000000 00000001176 14355111041 0020126 0 ustar 00root root 0000000 0000000 # Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
sphinxext-opengraph-0.7.5/docs/make.bat 0000664 0000000 0000000 00000001374 14355111041 0020073 0 ustar 00root root 0000000 0000000 @ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd
sphinxext-opengraph-0.7.5/docs/requirements.txt 0000664 0000000 0000000 00000000065 14355111041 0021746 0 ustar 00root root 0000000 0000000 myst-parser==0.18.1
furo==2022.9.29
sphinx==5.2.3
./
sphinxext-opengraph-0.7.5/docs/source/ 0000775 0000000 0000000 00000000000 14355111041 0017761 5 ustar 00root root 0000000 0000000 sphinxext-opengraph-0.7.5/docs/source/conf.py 0000664 0000000 0000000 00000003201 14355111041 0021254 0 ustar 00root root 0000000 0000000 # Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
sys.path.insert(0, os.path.abspath("../.."))
# -- Project information -----------------------------------------------------
project = "sphinxext-opengraph"
copyright = "2020, FIRST"
author = "WPILib"
# The full version, including alpha/beta/rc tags
release = "1.0"
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"myst_parser",
"sphinxext.opengraph",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = "furo"
sphinxext-opengraph-0.7.5/docs/source/index.md 0000664 0000000 0000000 00000000063 14355111041 0021411 0 ustar 00root root 0000000 0000000 ```{include} ../../README.md
:relative-images:
```
sphinxext-opengraph-0.7.5/setup.py 0000664 0000000 0000000 00000002734 14355111041 0017251 0 ustar 00root root 0000000 0000000 import setuptools
with open("README.md", encoding="utf-8") as readme:
long_description = readme.read()
setuptools.setup(
name="sphinxext-opengraph",
use_scm_version=True,
setup_requires=["setuptools_scm"],
author="Itay Ziv",
author_email="itay220204@gmail.com",
description="Sphinx Extension to enable OGP support",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/wpilibsuite/sphinxext-opengraph",
license="LICENSE.md",
install_requires=["sphinx>=4.0"],
packages=["sphinxext/opengraph"],
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Plugins",
"Environment :: Web Environment",
"Framework :: Sphinx :: Extension",
"Intended Audience :: Developers",
"License :: OSI Approved :: BSD License",
"Natural Language :: English",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python",
"Topic :: Documentation :: Sphinx",
"Topic :: Documentation",
"Topic :: Software Development :: Documentation",
"Topic :: Text Processing",
"Topic :: Utilities",
],
python_requires=">=3.7",
)
sphinxext-opengraph-0.7.5/sphinxext/ 0000775 0000000 0000000 00000000000 14355111041 0017563 5 ustar 00root root 0000000 0000000 sphinxext-opengraph-0.7.5/sphinxext/opengraph/ 0000775 0000000 0000000 00000000000 14355111041 0021546 5 ustar 00root root 0000000 0000000 sphinxext-opengraph-0.7.5/sphinxext/opengraph/__init__.py 0000664 0000000 0000000 00000015106 14355111041 0023662 0 ustar 00root root 0000000 0000000 from typing import Any, Dict
from urllib.parse import urljoin, urlparse, urlunparse
from pathlib import Path
import docutils.nodes as nodes
from sphinx.application import Sphinx
from .descriptionparser import get_description
from .metaparser import get_meta_description
from .titleparser import get_title
import os
DEFAULT_DESCRIPTION_LENGTH = 200
# A selection from https://www.iana.org/assignments/media-types/media-types.xhtml#image
IMAGE_MIME_TYPES = {
"gif": "image/gif",
"apng": "image/apng",
"webp": "image/webp",
"jpeg": "image/jpeg",
"jpg": "image/jpeg",
"png": "image/png",
"bmp": "image/bmp",
"heic": "image/heic",
"heif": "image/heif",
"tiff": "image/tiff",
}
def make_tag(property: str, content: str, type_: str = "property") -> str:
# Parse quotation, so they won't break html tags if smart quotes are disabled
content = content.replace('"', """)
return f''
def get_tags(
app: Sphinx,
context: Dict[str, Any],
doctree: nodes.document,
config: Dict[str, Any],
) -> str:
# Get field lists for per-page overrides
fields = context["meta"]
if fields is None:
fields = {}
tags = {}
meta_tags = {} # For non-og meta tags
# Set length of description
try:
desc_len = int(
fields.get("ogp_description_length", config["ogp_description_length"])
)
except ValueError:
desc_len = DEFAULT_DESCRIPTION_LENGTH
# Get the title and parse any html in it
title = get_title(context["title"], skip_html_tags=False)
title_excluding_html = get_title(context["title"], skip_html_tags=True)
# Parse/walk doctree for metadata (tag/description)
description = get_description(doctree, desc_len, [title, title_excluding_html])
# title tag
tags["og:title"] = title
# type tag
tags["og:type"] = config["ogp_type"]
if os.getenv("READTHEDOCS") and not config["ogp_site_url"]:
# readthedocs uses html_baseurl for sphinx > 1.8
parse_result = urlparse(config["html_baseurl"])
if config["html_baseurl"] is None:
raise OSError("ReadTheDocs did not provide a valid canonical URL!")
# Grab root url from canonical url
config["ogp_site_url"] = urlunparse(
(
parse_result.scheme,
parse_result.netloc,
parse_result.path,
"",
"",
"",
)
)
# url tag
# Get the URL of the specific page
page_url = urljoin(
config["ogp_site_url"], app.builder.get_target_uri(context["pagename"])
)
tags["og:url"] = page_url
# site name tag, False disables, default to project if ogp_site_name not
# set.
if config["ogp_site_name"] is False:
site_name = None
elif config["ogp_site_name"] is None:
site_name = config["project"]
else:
site_name = config["ogp_site_name"]
if site_name:
tags["og:site_name"] = site_name
# description tag
if description:
tags["og:description"] = description
if config["ogp_enable_meta_description"] and not get_meta_description(
context["metatags"]
):
meta_tags["description"] = description
# image tag
# Get basic values from config
if "og:image" in fields:
image_url = fields["og:image"]
ogp_use_first_image = False
ogp_image_alt = fields.get("og:image:alt")
fields.pop("og:image", None)
else:
image_url = config["ogp_image"]
ogp_use_first_image = config["ogp_use_first_image"]
ogp_image_alt = fields.get("og:image:alt", config["ogp_image_alt"])
fields.pop("og:image:alt", None)
first_image = None
if ogp_use_first_image:
first_image = doctree.next_node(nodes.image)
if (
first_image
and Path(first_image.get("uri", "")).suffix[1:].lower() in IMAGE_MIME_TYPES
):
image_url = first_image["uri"]
ogp_image_alt = first_image.get("alt", None)
else:
first_image = None
if image_url:
# temporarily disable relative image paths with field lists
if "og:image" not in fields:
image_url_parsed = urlparse(image_url)
if not image_url_parsed.scheme:
# Relative image path detected, relative to the source. Make absolute.
if first_image:
root = page_url
else: # ogp_image is set
# ogp_image is defined as being relative to the site root.
# This workaround is to keep that functionality from breaking.
root = config["ogp_site_url"]
image_url = urljoin(root, image_url_parsed.path)
tags["og:image"] = image_url
# Add image alt text (either provided by config or from site_name)
if isinstance(ogp_image_alt, str):
tags["og:image:alt"] = ogp_image_alt
elif ogp_image_alt is None and site_name:
tags["og:image:alt"] = site_name
elif ogp_image_alt is None and title:
tags["og:image:alt"] = title
# arbitrary tags and overrides
tags.update({k: v for k, v in fields.items() if k.startswith("og:")})
return (
"\n".join(
[make_tag(p, c) for p, c in tags.items()]
+ [make_tag(p, c, "name") for p, c in meta_tags.items()]
+ config["ogp_custom_meta_tags"]
)
+ "\n"
)
def html_page_context(
app: Sphinx,
pagename: str,
templatename: str,
context: Dict[str, Any],
doctree: nodes.document,
) -> None:
if doctree:
context["metatags"] += get_tags(app, context, doctree, app.config)
def setup(app: Sphinx) -> Dict[str, Any]:
# ogp_site_url="" allows relative by default, even though it's not
# officially supported by OGP.
app.add_config_value("ogp_site_url", "", "html")
app.add_config_value("ogp_description_length", DEFAULT_DESCRIPTION_LENGTH, "html")
app.add_config_value("ogp_image", None, "html")
app.add_config_value("ogp_image_alt", None, "html")
app.add_config_value("ogp_use_first_image", False, "html")
app.add_config_value("ogp_type", "website", "html")
app.add_config_value("ogp_site_name", None, "html")
app.add_config_value("ogp_custom_meta_tags", [], "html")
app.add_config_value("ogp_enable_meta_description", True, "html")
app.connect("html-page-context", html_page_context)
return {
"parallel_read_safe": True,
"parallel_write_safe": True,
}
sphinxext-opengraph-0.7.5/sphinxext/opengraph/descriptionparser.py 0000664 0000000 0000000 00000007512 14355111041 0025665 0 ustar 00root root 0000000 0000000 import string
from typing import Iterable
import docutils.nodes as nodes
class DescriptionParser(nodes.NodeVisitor):
"""
Finds the title and creates a description from a doctree
"""
def __init__(
self,
desc_len: int,
known_titles: Iterable[str] = None,
document: nodes.document = None,
):
# Hack to prevent requirement for the doctree to be passed in.
# It's only used by doctree.walk(...) to print debug messages.
if document is None:
class document_cls:
class reporter:
@staticmethod
def debug(*args, **kwaargs):
pass
document = document_cls()
if known_titles == None:
known_titles = []
super().__init__(document)
self.description = ""
self.desc_len = desc_len
self.list_level = 0
self.known_titles = known_titles
self.first_title_found = False
# Exceptions can't be raised from dispatch_departure()
# This is used to loop the stop call back to the next dispatch_visit()
self.stop = False
def dispatch_visit(self, node: nodes.Element) -> None:
if self.stop:
raise nodes.StopTraversal
# Skip comments
if isinstance(node, nodes.Invisible):
raise nodes.SkipNode
# Skip all admonitions
if isinstance(node, nodes.Admonition):
raise nodes.SkipNode
# Mark start of nested lists
if isinstance(node, nodes.Sequential):
self.list_level += 1
if self.list_level > 1:
self.description += "-"
# Skip the first title if it's the title of the page
if not self.first_title_found and isinstance(node, nodes.title):
self.first_title_found = True
if node.astext() in self.known_titles:
raise nodes.SkipNode
if isinstance(node, nodes.raw) or isinstance(node.parent, nodes.literal_block):
raise nodes.SkipNode
# Only include leaf nodes in the description
if len(node.children) == 0:
text = node.astext().replace("\r", "").replace("\n", " ").strip()
# Remove double spaces
while text.find(" ") != -1:
text = text.replace(" ", " ")
# Put a space between elements if one does not already exist.
if (
len(self.description) > 0
and len(text) > 0
and self.description[-1] not in string.whitespace
and text[0] not in string.whitespace + string.punctuation
):
self.description += " "
self.description += text
def dispatch_departure(self, node: nodes.Element) -> None:
# Separate title from text
if isinstance(node, nodes.title):
self.description += ":"
# Separate list elements
if isinstance(node, nodes.Part):
self.description += ","
# Separate end of list from text
if isinstance(node, nodes.Sequential):
if self.description and self.description[-1] == ",":
self.description = self.description[:-1]
self.description += "."
self.list_level -= 1
# Check for length
if len(self.description) > self.desc_len:
self.description = self.description[: self.desc_len]
if self.desc_len >= 3:
self.description = self.description[:-3] + "..."
self.stop = True
def get_description(
doctree: nodes.document,
description_length: int,
known_titles: Iterable[str] = None,
document: nodes.document = None,
):
mcv = DescriptionParser(description_length, known_titles, document)
doctree.walkabout(mcv)
return mcv.description
sphinxext-opengraph-0.7.5/sphinxext/opengraph/metaparser.py 0000664 0000000 0000000 00000001354 14355111041 0024266 0 ustar 00root root 0000000 0000000 from html.parser import HTMLParser
class HTMLTextParser(HTMLParser):
"""
Parse HTML into text
"""
def __init__(self):
super().__init__()
self.meta_description = None
def handle_starttag(self, tag, attrs) -> None:
# For example:
# attrs = [("content", "My manual description"), ("name", "description")]
if ("name", "description") in attrs:
self.meta_description = True
for name, value in attrs:
if name == "content":
self.meta_description = value
break
def get_meta_description(meta_tags: str) -> bool:
htp = HTMLTextParser()
htp.feed(meta_tags)
htp.close()
return htp.meta_description
sphinxext-opengraph-0.7.5/sphinxext/opengraph/titleparser.py 0000664 0000000 0000000 00000001453 14355111041 0024461 0 ustar 00root root 0000000 0000000 from html.parser import HTMLParser
class HTMLTextParser(HTMLParser):
"""
Parse HTML into text
"""
def __init__(self):
super().__init__()
# All text found
self.text = ""
# Only text outside of html tags
self.text_outside_tags = ""
self.level = 0
def handle_starttag(self, tag, attrs) -> None:
self.level += 1
def handle_endtag(self, tag) -> None:
self.level -= 1
def handle_data(self, data) -> None:
self.text += data
if self.level == 0:
self.text_outside_tags += data
def get_title(title: str, skip_html_tags: bool = False):
htp = HTMLTextParser()
htp.feed(title)
htp.close()
if skip_html_tags:
return htp.text_outside_tags
else:
return htp.text
sphinxext-opengraph-0.7.5/tests/ 0000775 0000000 0000000 00000000000 14355111041 0016673 5 ustar 00root root 0000000 0000000 sphinxext-opengraph-0.7.5/tests/conftest.py 0000664 0000000 0000000 00000002302 14355111041 0021067 0 ustar 00root root 0000000 0000000 import pytest
from bs4 import BeautifulSoup
from sphinx.testing.path import path
from sphinx.application import Sphinx
pytest_plugins = "sphinx.testing.fixtures"
@pytest.fixture(scope="session")
def rootdir():
return path(__file__).parent.abspath() / "roots"
@pytest.fixture()
def content(app):
app.build()
yield app
def _meta_tags(content, subdir=None):
if subdir is None:
c = (content.outdir / "index.html").read_text()
else:
c = (content.outdir / subdir / "index.html").read_text()
return BeautifulSoup(c, "html.parser").find_all("meta")
def _og_meta_tags(content):
return [
tag for tag in _meta_tags(content) if tag.get("property", "").startswith("og:")
]
@pytest.fixture()
def meta_tags(content):
return _meta_tags(content)
@pytest.fixture()
def og_meta_tags(content):
return [
tag for tag in _meta_tags(content) if tag.get("property", "").startswith("og:")
]
@pytest.fixture()
def og_meta_tags_sub(content):
return [
tag
for tag in _meta_tags(content, "sub")
if tag.get("property", "").startswith("og:")
]
def pytest_configure(config):
config.addinivalue_line("markers", "sphinx")
sphinxext-opengraph-0.7.5/tests/roots/ 0000775 0000000 0000000 00000000000 14355111041 0020041 5 ustar 00root root 0000000 0000000 sphinxext-opengraph-0.7.5/tests/roots/test-arbitrary-tags/ 0000775 0000000 0000000 00000000000 14355111041 0023751 5 ustar 00root root 0000000 0000000 sphinxext-opengraph-0.7.5/tests/roots/test-arbitrary-tags/conf.py 0000664 0000000 0000000 00000000237 14355111041 0025252 0 ustar 00root root 0000000 0000000 extensions = ["sphinxext.opengraph"]
master_doc = "index"
exclude_patterns = ["_build"]
html_theme = "basic"
ogp_site_url = "http://example.org/en/latest/"
sphinxext-opengraph-0.7.5/tests/roots/test-arbitrary-tags/index.rst 0000664 0000000 0000000 00000000512 14355111041 0025610 0 ustar 00root root 0000000 0000000 :og:video: http://example.org/en/latest/video.mp4
:og:video:type: video/mp4
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse at lorem ornare, fringilla massa nec, venenatis mi. Donec erat sapien, tincidunt nec rhoncus nec, scelerisque id diam. Orci varius natoque penatibus et magnis dis parturient mauris. sphinxext-opengraph-0.7.5/tests/roots/test-custom-tags/ 0000775 0000000 0000000 00000000000 14355111041 0023264 5 ustar 00root root 0000000 0000000 sphinxext-opengraph-0.7.5/tests/roots/test-custom-tags/conf.py 0000664 0000000 0000000 00000000371 14355111041 0024564 0 ustar 00root root 0000000 0000000 extensions = ["sphinxext.opengraph"]
master_doc = "index"
exclude_patterns = ["_build"]
html_theme = "basic"
ogp_site_url = "http://example.org/en/latest/"
ogp_custom_meta_tags = [
'',
]
sphinxext-opengraph-0.7.5/tests/roots/test-custom-tags/index.rst 0000664 0000000 0000000 00000000372 14355111041 0025127 0 ustar 00root root 0000000 0000000 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse at lorem ornare, fringilla massa nec, venenatis mi. Donec erat sapien, tincidunt nec rhoncus nec, scelerisque id diam. Orci varius natoque penatibus et magnis dis parturient mauris. sphinxext-opengraph-0.7.5/tests/roots/test-description-length/ 0000775 0000000 0000000 00000000000 14355111041 0024620 5 ustar 00root root 0000000 0000000 sphinxext-opengraph-0.7.5/tests/roots/test-description-length/conf.py 0000664 0000000 0000000 00000000273 14355111041 0026121 0 ustar 00root root 0000000 0000000 extensions = ["sphinxext.opengraph"]
master_doc = "index"
exclude_patterns = ["_build"]
html_theme = "basic"
ogp_site_url = "http://example.org/en/latest/"
ogp_description_length = 50
sphinxext-opengraph-0.7.5/tests/roots/test-description-length/index.rst 0000664 0000000 0000000 00000000372 14355111041 0026463 0 ustar 00root root 0000000 0000000 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse at lorem ornare, fringilla massa nec, venenatis mi. Donec erat sapien, tincidunt nec rhoncus nec, scelerisque id diam. Orci varius natoque penatibus et magnis dis parturient mauris. sphinxext-opengraph-0.7.5/tests/roots/test-double-spacing/ 0000775 0000000 0000000 00000000000 14355111041 0023712 5 ustar 00root root 0000000 0000000 sphinxext-opengraph-0.7.5/tests/roots/test-double-spacing/conf.py 0000664 0000000 0000000 00000000237 14355111041 0025213 0 ustar 00root root 0000000 0000000 extensions = ["sphinxext.opengraph"]
master_doc = "index"
exclude_patterns = ["_build"]
html_theme = "basic"
ogp_site_url = "http://example.org/en/latest/"
sphinxext-opengraph-0.7.5/tests/roots/test-double-spacing/index.rst 0000664 0000000 0000000 00000000050 14355111041 0025546 0 ustar 00root root 0000000 0000000 Example sentence 1. Example sentence 2. sphinxext-opengraph-0.7.5/tests/roots/test-first-image-no-image/ 0000775 0000000 0000000 00000000000 14355111041 0024717 5 ustar 00root root 0000000 0000000 sphinxext-opengraph-0.7.5/tests/roots/test-first-image-no-image/conf.py 0000664 0000000 0000000 00000000452 14355111041 0026217 0 ustar 00root root 0000000 0000000 extensions = ["sphinxext.opengraph"]
master_doc = "index"
exclude_patterns = ["_build"]
html_theme = "basic"
ogp_site_name = "Example's Docs!"
ogp_site_url = "http://example.org/en/latest/"
ogp_image = "http://example.org/en/latest/image33.png"
ogp_image_alt = "TEST"
ogp_use_first_image = True
sphinxext-opengraph-0.7.5/tests/roots/test-first-image-no-image/index.rst 0000664 0000000 0000000 00000000012 14355111041 0026551 0 ustar 00root root 0000000 0000000 Title Only sphinxext-opengraph-0.7.5/tests/roots/test-first-image/ 0000775 0000000 0000000 00000000000 14355111041 0023225 5 ustar 00root root 0000000 0000000 sphinxext-opengraph-0.7.5/tests/roots/test-first-image/conf.py 0000664 0000000 0000000 00000000452 14355111041 0024525 0 ustar 00root root 0000000 0000000 extensions = ["sphinxext.opengraph"]
master_doc = "index"
exclude_patterns = ["_build"]
html_theme = "basic"
ogp_site_name = "Example's Docs!"
ogp_site_url = "http://example.org/en/latest/"
ogp_image = "http://example.org/en/latest/image33.png"
ogp_image_alt = "TEST"
ogp_use_first_image = True
sphinxext-opengraph-0.7.5/tests/roots/test-first-image/index.rst 0000664 0000000 0000000 00000000117 14355111041 0025065 0 ustar 00root root 0000000 0000000 .. image:: http://example.org/en/latest/image2.png
:alt: Test image alt text sphinxext-opengraph-0.7.5/tests/roots/test-image-rel-paths/ 0000775 0000000 0000000 00000000000 14355111041 0023775 5 ustar 00root root 0000000 0000000 sphinxext-opengraph-0.7.5/tests/roots/test-image-rel-paths/conf.py 0000664 0000000 0000000 00000000376 14355111041 0025302 0 ustar 00root root 0000000 0000000 extensions = ["sphinxext.opengraph"]
master_doc = "index"
exclude_patterns = ["_build"]
html_theme = "basic"
ogp_site_name = "Example's Docs!"
ogp_site_url = "http://example.org/en/latest/"
ogp_image = "_static/image33.png"
ogp_use_first_image = True
sphinxext-opengraph-0.7.5/tests/roots/test-image-rel-paths/img/ 0000775 0000000 0000000 00000000000 14355111041 0024551 5 ustar 00root root 0000000 0000000 sphinxext-opengraph-0.7.5/tests/roots/test-image-rel-paths/img/sample.jpg 0000664 0000000 0000000 00000020271 14355111041 0026536 0 ustar 00root root 0000000 0000000 ÿØÿà JFIF À À ÿÛ C
ÿÛ CÿÀ È È" ÿÄ
ÿÄ µ } !1AQa"q2‘¡#B±ÁRÑð$3br‚
%&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚáâãäåæçèéêñòóôõö÷øùúÿÄ
ÿÄ µ w !1AQaq"2B‘¡±Á #3RðbrÑ
$4á%ñ&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz‚ƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚâãäåæçèéêòóôõö÷øùúÿÚ ? ¯ÿ Cÿ ‚!ü)ÿ ‚”þÊ^ ñ׎¼AñJÕ´¯\è0à ßYÁlÐGge:³,Ö²¶ý× ñIûþ!9ýèsøÕÿ ƒ}3ÿ •ôÁ§?òޝÿ ÙG¾ÿ Óf—_§Õçá°Ô¥J2”u?Zã^5Ï0™æ'
†ÄÊ0Œ¬’Ù#óþ!9ýèsøÕÿ ƒ}3ÿ •ôÄ'?³¯ý¿ðo¦ò¾¿O¨þ©GùO–ÿ ˆƒÄô?ÃüÌø„çöuÿ ¡ÏãWþ
ôÏþWÑÿ œþοô9üjÿ Á¾™ÿ Êúý>¢ªQþPÿ ˆƒÄô?ÃüÌø„çöuÿ ¡ÏãWþ
ôÏþWÑÿ œþοô9üjÿ Á¾™ÿ Êúý>¢ªQþPÿ ˆƒÄô?ÃüÌø„çöuÿ ¡ÏãWþ
ôÏþWÑÿ œþοô9üjÿ Á¾™ÿ Êúý>¢ªQþPÿ ˆƒÄô?ÃüÌø„çöuÿ ¡ÏãWþ
ôÏþWÑÿ œþοô9üjÿ Á¾™ÿ Êúý>¢ªQþPÿ ˆƒÄô?ÃüÌø„çöuÿ ¡ÏãWþ
ôÏþWÑÿ œþοô9üjÿ Á¾™ÿ Êúý>¢ªQþPÿ ˆƒÄô?ÃüÌø„çöuÿ ¡ÏãWþ
ôÏþWÑÿ œþοô9üjÿ Á¾™ÿ Êúý>¢ªQþPÿ ˆƒÄô?ÃüÌø„çöuÿ ¡ÏãWþ
ôÏþWÑÿ œþοô9üjÿ Á¾™ÿ Êúý>¢ªQþPÿ ˆƒÄô?ÃüÌø„çöuÿ ¡ÏãWþ
ôÏþWÑÿ œþοô9üjÿ Á¾™ÿ Êúý>¢ªQþPÿ ˆƒÄô?ÃüÌø„çöuÿ ¡ÏãWþ
ôÏþWÑÿ œþοô9üjÿ Á¾™ÿ Êúý>¢ªQþPÿ ˆƒÄô?ÃüÌø„çöuÿ ¡ÏãWþ
ôÏþWÑÿ œþοô9üjÿ Á¾™ÿ Êúý>¢ªQþPÿ ˆƒÄô?Ãüçïþyÿ CøSÿ Öý”¼?ã¯øƒâ««j¾,¶Ðf‡^¾³žÙ`’ÎöveXmbmûÐXŒã$WØßðv7ü£«ÁŸöQìôÙªQ^&:…^X«#úSÃÓ˜dqÄãfç>i+¿ ÿ ƒNå^3ÿ ²}ÿ ¦Í.¿O«óþ
9ÿ ”uxÏþÊ=÷þ›4ºý>¯oüŸÍ~ ÿ ÉG‹ÿ 肊(®“り( Š( Š( Š( Š( Š( Š( Š( Š( Š( Ìø;þQÕàÏû(ö?úlÕ(£þÆÿ ”ux3þÊ=þ›5J+ç³ãÖ¾É9ñÏóø4çþQÕã?û(÷ßúlÒëôú¿0àÓŸùGWŒÿ ì£ßé³K¯Óêö0Á‰ü÷âü”x¿ñþˆ(¢Šé>8(¢Š (¢Š (¢Š (¢Š (¢Š (¢Š (¢Š (¢Š (¢Š (¢Š üÁÿ ƒ±¿å^ÿ ²cÿ ¦ÍRŠ?àìoùGWƒ?ì£Øÿ é³T¢¾{1þ1ýkà÷ü“‘ÿ ÿ 0ÿ ƒNå^3ÿ ²}ÿ ¦Í.¿O«óþ
9ÿ ”uxÏþÊ=÷þ›4ºý>¯cüŸÏ~ ÿ ÉG‹ÿ 肊(®“り( Š( Š( Š( Š( Š( Š( Š( Š( Š( Ìø;þQÕàÏû(ö?úlÕ(£þÆÿ ”ux3þÊ=þ›5J+ç³ãÖ¾É9ñÏóø4çþQÕã?û(÷ßúlÒëôú¿0àÓŸùGWŒÿ ì£ßé³K¯Óêö0Á‰ü÷âü”x¿ñþˆ(¢Šé>8(¢Š ü¨ÿ ‚Éÿ Áǯì9ûKÛüøðÐüWøÂðE%âL—ÚtÒ¢Ë
¬v–ÃÏ»™¡;Ø#Æ2rä²§Ìþ ÿ ƒ®hïÙ‡â¶aû[~ÌÒxÂþ!˜ª]YøwVðåüP©Q$ðè<¢ëËÜ"²}áó׫ÿ ‚Ýÿ Á!ÿ i߇_ðQ‡ý±¿dÛË[Ä×ñ>£§X‹i5]&x¬þÈòCopWpK(1aäÞíˆÙy_žþJø‡¢x›Gøû|~ɾñÖ‰c$wqÁ®ø,éÚÅ”›]ðYjJöò¾ÖÚ<´·à·ÍÎ+<¯¹µÏ}T´OÉy[®Ÿ}Ù®1jÔ¹efµw¶·¿ŸM};ÿ G^ñ^ãÏ
išæwþ“¬ÚE}cuù.`•Ç"û2°#ëZ5ð×ü;á‰?oßø%v‡ìÅñ›Ãü#«évš¥¶ª‘>™cs¢|Çd.`em>ß²6;c1ºŸÁ¿ø)Ÿì¡û;ÿ Á0¼à~Í?µ®±ã¿I©"kÒxÄ–÷q*4.ÒÝ[Ïcû8*©ŠiäfŽ»XÖ•§u§§¤¢–ò×´Ûg¶Ï¡•(Êta8´Û‹}£t¯k¾ý7ù½ë&Šüÿ ƒ›>/jÿ ´üoöGñψ$Yõß^hºÎ£*¢ –æãAžY[j€£.Ìp <