diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..862c1e1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.py] +max_line_length = 88 + +[*.yml] +indent_size = 2 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..c5b40b3 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,104 @@ +name: build + +on: [push, pull_request] + +env: + PYTEST_ADDOPTS: "--color=yes" + +jobs: + test: + name: Test - ${{ matrix.python-version }} + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.6, 3.7, 3.8, 3.9] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Set up Pip cache + uses: actions/cache@v2 + id: pip-cache + with: + path: ~/.cache/pip + key: pip-${{ hashFiles('**/pyproject.toml') }} + - name: Upgrade Pip + run: python -m pip install --upgrade pip + - name: Install Poetry + run: python -m pip install poetry + - name: Set up Poetry cache + uses: actions/cache@v2 + id: poetry-cache + with: + path: ~/.cache/pypoetry/virtualenvs + key: poetry-${{ hashFiles('**/poetry.lock') }} + - name: Install dependencies + run: | + poetry run pip install --upgrade pip + poetry install + - name: Run tests + run: poetry run invoke tests + + + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: "3.x" + - name: Set Poetry cache + uses: actions/cache@v2 + id: poetry-cache + with: + path: ~/.cache/pypoetry/virtualenvs + key: poetry-${{ hashFiles('**/poetry.lock') }} + - name: Upgrade Pip + run: python -m pip install --upgrade pip + - name: Install Poetry + run: python -m pip install poetry + - name: Install dependencies + run: | + poetry run pip install --upgrade pip + poetry install + - name: Run linters + run: poetry run invoke lint + + + deploy: + name: Deploy + environment: Deployment + needs: [test, lint] + runs-on: ubuntu-latest + if: ${{ github.ref=='refs/heads/main' && github.event_name!='pull_request' }} + + steps: + - uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: "3.x" + - name: Check release + id: check_release + run: | + python -m pip install --upgrade pip + python -m pip install poetry githubrelease httpx==0.16.1 autopub + echo "##[set-output name=release;]$(autopub check)" + - name: Publish + if: ${{ steps.check_release.outputs.release=='' }} + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + git remote set-url origin https://$GITHUB_TOKEN@github.com/${{ github.repository }} + autopub prepare + poetry build + autopub commit + autopub githubrelease + poetry publish -u __token__ -p $PYPI_PASSWORD diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c04bc49 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +poetry.lock diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..3338bab --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,31 @@ +# See https://pre-commit.com/hooks.html for info on hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.4.0 + hooks: + - id: check-added-large-files + - id: check-ast + - id: check-case-conflict + - id: check-toml + - id: check-yaml + - id: debug-statements + - id: detect-private-key + - id: end-of-file-fixer + - id: trailing-whitespace + + - repo: https://github.com/psf/black + rev: 19.10b0 + hooks: + - id: black + + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.9.0 + hooks: + - id: flake8 + args: [--max-line-length=88] + language_version: python3.7 + + - repo: https://github.com/PyCQA/isort + rev: 5.7.0 + hooks: + - id: isort diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f96daf4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,9 @@ +Contributing +============ + +Contributions are welcome and much appreciated. Every little bit helps. You can contribute by improving the documentation, adding missing features, and fixing bugs. You can also help out by reviewing and commenting on [existing issues][]. + +To start contributing to this plugin, review the [Contributing to Pelican][] documentation, beginning with the **Contributing Code** section. + +[existing issues]: https://github.com/pelican-plugins/share-post/issues +[Contributing to Pelican]: https://docs.getpelican.com/en/latest/contribute.html diff --git a/README.md b/README.md new file mode 100644 index 0000000..926e737 --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# Share Post: A Plugin for Pelican + +[![Build Status](https://img.shields.io/github/workflow/status/pelican-plugins/share-post/build)](https://github.com/pelican-plugins/share-post/actions) +[![PyPI Version](https://img.shields.io/pypi/v/pelican-share-post)](https://pypi.org/project/pelican-share-post/) +![License](https://img.shields.io/pypi/l/pelican-share-post?color=blue) + +Share Post is a Pelican plugin that creates share links in articles that allow site visitors to share the current article with others in a privacy-friendly manner. + +Many web sites have share widgets to let readers share posts on social networks. Most of these widgets are used by vendors for online tracking. These widgets can also be visually-distracting and negatively affect readers’ attention. + +Share Post creates old-school URLs for some popular sites which your theme can use. These links do not have the ability to track site visitors. They can also be unobtrusive depending on how Pelican theme uses them. + + +Installation +------------ + +This plugin can be installed via: + + python -m pip install pelican-share-post + +Usage +----- + +This plugin adds to each Pelican article a dictionary of URLs that, when followed, allows the reader to easily share the article via specific channels. When activated, the plugin adds the attribute `share_post` to each article with the following format: + +``` python +article.share_post = { + "facebook": "", + "email": "", + "twitter": "", + "diaspora": "", + "linkedin": "", + "hacker-news": "", + "reddit": "", +} +``` + +You can then access those variables in your template. For example: + +``` html+jinja +{% if article.share_post and article.status != 'draft' %} +
+

+ Share on: + Diaspora* + ❄ + Twitter + ❄ + Facebook + ❄ + LinkedIn + ❄ + HackerNews + ❄ + Email + ❄ + Reddit +

+
+{% endif %} +``` + +Contributing +------------ + +Contributions are welcome and much appreciated. Every little bit helps. You can contribute by improving the documentation, adding missing features, and fixing bugs. You can also help out by reviewing and commenting on [existing issues][]. + +To start contributing to this plugin, review the [Contributing to Pelican][] documentation, beginning with the **Contributing Code** section. + +[existing issues]: https://github.com/pelican-plugins/share-post/issues +[Contributing to Pelican]: https://docs.getpelican.com/en/latest/contribute.html + + +Contributors +------------ + +* [Talha Mansoor](https://www.oncrashreboot.com) - talha131@gmail.com +* [Jonathan DEKHTIAR](https://github.com/DEKHTIARJonathan) - contact@jonathandekhtiar.eu +* [Justin Mayer](https://justinmayer.com) +* [Leonardo Giordani](https://www.thedigitalcatonline.com) + + +License +------- + +This project is licensed under the MIT license. diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..9b81d15 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,3 @@ +Release type: major + +Initial release as namespace plugin diff --git a/pelican/plugins/share_post/__init__.py b/pelican/plugins/share_post/__init__.py new file mode 100644 index 0000000..4185ce8 --- /dev/null +++ b/pelican/plugins/share_post/__init__.py @@ -0,0 +1 @@ +from .share_post import * # noqa diff --git a/pelican/plugins/share_post/conftest.py b/pelican/plugins/share_post/conftest.py new file mode 100644 index 0000000..57affce --- /dev/null +++ b/pelican/plugins/share_post/conftest.py @@ -0,0 +1,9 @@ +import pytest + +from pelican.tests.support import temporary_folder + + +@pytest.fixture +def tmp_folder(): + with temporary_folder() as tf: + yield tf diff --git a/pelican/plugins/share_post/share_post.py b/pelican/plugins/share_post/share_post.py new file mode 100644 index 0000000..f30b088 --- /dev/null +++ b/pelican/plugins/share_post/share_post.py @@ -0,0 +1,125 @@ +""" +Share Post +========== + +This plugin was originally created by +Talha Mansoor + +This plugin adds social share URLs to each article. +""" + +# If you want to add a new link_processor please +# have a look at the create_link decorator and +# follow the example of the other functions + +from urllib.parse import quote + +from bs4 import BeautifulSoup + +from pelican import contents, signals +from pelican.generators import ArticlesGenerator, PagesGenerator + +_create_link_functions = [] + + +# Use this decorator to mark a function as +# a link creator. The function's prototype shall be +# create_link_NAME(title, url, content) +# where +# NAME is the name of the target, e.g. "dispora" or "facebook" +# title is the HTML-safe title of the content +# url is the content URL +# content is the full object, should you need to extract more data. +def create_link(f): + _create_link_functions.append(f) + return f + + +@create_link +def create_link_email(title, url, content): + return f"mailto:?subject={title}&body={url}" + + +@create_link +def create_link_hacker_news(title, url, content): + return f"https://news.ycombinator.com/submitlink?t={title}&u={url}" + + +@create_link +def create_link_diaspora(title, url, content): + return f"https://sharetodiaspora.github.io/?title={title}&url={url}" + + +@create_link +def create_link_facebook(title, url, content): + return f"https://www.facebook.com/sharer/sharer.php?u={url}" + + +@create_link +def create_link_twitter(title, url, content): + twitter_username = content.settings.get("TWITTER_USERNAME", "") + via = f"&via={twitter_username}" if twitter_username else "" + + tags = getattr(content, "tags", []) + tags = ",".join([tag.slug for tag in tags]) + hashtags = f"&hashtags={tags}" if tags else "" + + return f"https://twitter.com/intent/tweet?text={title}&url={url}{via}{hashtags}" + + +@create_link +def create_link_reddit(title, url, content): + return f"https://www.reddit.com/submit?url={url}&title={title}" + + +@create_link +def create_link_linkedin(title, url, content): + summary = quote( + BeautifulSoup(content.summary, "html.parser").get_text().strip().encode("utf-8") + ) + + return ( + f"https://www.linkedin.com/shareArticle?" + f"mini=true&url={url}&title={title}&" + f"summary={summary}&source={url}" + ) + + +def create_share_links(content): + if isinstance(content, contents.Static): + return + + main_title = BeautifulSoup(content.title, "html.parser").get_text().strip() + + try: + sub_title = ( + " " + BeautifulSoup(content.subtitle, "html.parser").get_text().strip() + ) + except AttributeError: + sub_title = "" + + title = quote(f"{main_title}{sub_title}".encode("utf-8")) + + site_url = content.settings["SITEURL"] + url = quote(f"{site_url}/{content.url}".encode("utf-8")) + + content.share_post = {} + for func in _create_link_functions: + key = func.__name__.replace("create_link_", "").replace("_", "-") + content.share_post[key] = func(title, url, content) + + +def run_plugin(generators): + for generator in generators: + if isinstance(generator, ArticlesGenerator): + for article in generator.articles: + create_share_links(article) + for translation in article.translations: + create_share_links(translation) + elif isinstance(generator, PagesGenerator): + for page in generator.pages: + create_share_links(page) + + +def register(): + signals.all_generators_finalized.connect(run_plugin) diff --git a/pelican/plugins/share_post/test_data/article.md b/pelican/plugins/share_post/test_data/article.md new file mode 100644 index 0000000..8c302b2 --- /dev/null +++ b/pelican/plugins/share_post/test_data/article.md @@ -0,0 +1,8 @@ +Title: Test post +Date: 2021-02-01 13:00:00 +Category: test +Tags: foo, bar, foobar +Summary: I have a lot to test +Series: test_series + +Content diff --git a/pelican/plugins/share_post/test_share_post.py b/pelican/plugins/share_post/test_share_post.py new file mode 100644 index 0000000..a35fc59 --- /dev/null +++ b/pelican/plugins/share_post/test_share_post.py @@ -0,0 +1,65 @@ +import os + +from share_post import run_plugin + +from pelican.generators import ArticlesGenerator +from pelican.tests.support import get_context, get_settings + +from . import share_post + + +def test_share_post(tmp_folder): + base_path = os.path.dirname(os.path.abspath(__file__)) + test_data_path = os.path.join(base_path, "test_data") + + share_post.register() + + settings = get_settings() + context = get_context(settings) + + generator = ArticlesGenerator( + context=context, + settings=settings, + path=test_data_path, + theme=settings["THEME"], + output_path=tmp_folder, + ) + generator.generate_context() + + run_plugin([generator]) + + share_links = generator.articles[0].share_post + + assert ( + share_links["diaspora"] + == "https://sharetodiaspora.github.io/?title=Test%20post&url=/test-post.html" + ) + + assert share_links["twitter"] == ( + "https://twitter.com/intent/tweet?text=Test%20post" + "&url=/test-post.html&hashtags=foo,bar,foobar" + ) + + assert ( + share_links["facebook"] + == "https://www.facebook.com/sharer/sharer.php?u=/test-post.html" + ) + assert share_links["linkedin"] == ( + "https://www.linkedin.com/shareArticle?" + "mini=true&url=/test-post.html&title=Test%20post&" + "summary=I%20have%20a%20lot%20to%20test&source=/test-post.html" + ) + + assert ( + share_links["hacker-news"] + == "https://news.ycombinator.com/submitlink?t=Test%20post&u=/test-post.html" + ) + + assert ( + share_links["email"] == "mailto:?subject=Test%20post&body=/test-post.html" + ) + + assert ( + share_links["reddit"] + == "https://www.reddit.com/submit?url=/test-post.html&title=Test%20post" + ) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4a11803 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,73 @@ +[tool.poetry] +name = "pelican-share-post" +version = "0.0.0" +description = "A Pelican plugin to create share URLs of article" +authors = ["Talha Mansoor "] +license = "MIT" +readme = "README.md" +keywords = ["pelican", "plugin", "social"] +repository = "https://github.com/pelican-plugins/share-post" +documentation = "https://docs.getpelican.com" +packages = [ + { include = "pelican" }, +] + +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Framework :: Pelican", + "Framework :: Pelican :: Plugins", + "Intended Audience :: End Users/Desktop", + "Operating System :: OS Independent", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Software Development :: Libraries :: Python Modules", +] + +[tool.poetry.urls] +"Funding" = "https://donate.getpelican.com/" +"Issue Tracker" = "https://github.com/pelican-plugins/share-post/issues" + +[tool.poetry.dependencies] +python = "^3.6" +pelican = "^4.5" +markdown = {version = "^3.2.2", optional = true} +beautifulsoup4 = "^4.9.3" + +[tool.poetry.dev-dependencies] +black = {version = "^19.10b0", allow-prereleases = true} +flake8 = "^3.9" +flake8-black = "^0.2.0" +invoke = "^1.3" +isort = "^5.4" +livereload = "^2.6" +markdown = "^3.2.2" +pytest = "^6.0" +pytest-cov = "^2.8" +pytest-pythonpath = "^0.7.3" +pytest-sugar = "^0.9.4" +Werkzeug = "^1.0" + +[tool.poetry.extras] +markdown = ["markdown"] + +[tool.autopub] +project-name = "Share Post" +git-username = "botpub" +git-email = "botpub@autopub.rocks" +append-github-contributor = true + +[tool.isort] +# Maintain compatibility with Black +profile = "black" +multi_line_output = 3 + +# Sort imports within their section independent of the import type +force_sort_within_sections = true + +# Designate "pelican" as separate import section +known_pelican = "pelican" +sections = "FUTURE,STDLIB,THIRDPARTY,PELICAN,FIRSTPARTY,LOCALFOLDER" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000..0377b73 --- /dev/null +++ b/tasks.py @@ -0,0 +1,79 @@ +import os +from pathlib import Path +from shutil import which + +from invoke import task + +PKG_NAME = "share_post" +PKG_PATH = Path(f"pelican/plugins/{PKG_NAME}") +ACTIVE_VENV = os.environ.get("VIRTUAL_ENV", None) +VENV_HOME = Path(os.environ.get("WORKON_HOME", "~/.local/share/virtualenvs")) +VENV_PATH = Path(ACTIVE_VENV) if ACTIVE_VENV else (VENV_HOME / PKG_NAME) +VENV = str(VENV_PATH.expanduser()) + +TOOLS = ["poetry", "pre-commit"] +POETRY = which("poetry") if which("poetry") else (VENV / Path("bin") / "poetry") +PRECOMMIT = ( + which("pre-commit") if which("pre-commit") else (VENV / Path("bin") / "pre-commit") +) + + +@task +def tests(c): + """Run the test suite""" + c.run(f"{VENV}/bin/pytest", pty=True) + + +@task +def black(c, check=False, diff=False): + """Run Black auto-formatter, optionally with --check or --diff""" + check_flag, diff_flag = "", "" + if check: + check_flag = "--check" + if diff: + diff_flag = "--diff" + c.run(f"{VENV}/bin/black {check_flag} {diff_flag} {PKG_PATH} tasks.py") + + +@task +def isort(c, check=False, diff=False): + check_flag, diff_flag = "", "" + if check: + check_flag = "-c" + if diff: + diff_flag = "--diff" + c.run(f"{VENV}/bin/isort {check_flag} {diff_flag} .") + + +@task +def flake8(c): + c.run(f"{VENV}/bin/flake8 {PKG_PATH} tasks.py") + + +@task +def lint(c): + isort(c, check=True) + black(c, check=True) + flake8(c) + + +@task +def tools(c): + """Install tools in the virtual environment if not already on PATH""" + for tool in TOOLS: + if not which(tool): + c.run(f"{VENV}/bin/pip install {tool}") + + +@task +def precommit(c): + """Install pre-commit hooks to .git/hooks/pre-commit""" + c.run(f"{PRECOMMIT} install") + + +@task +def setup(c): + c.run(f"{VENV}/bin/pip install -U pip") + tools(c) + c.run(f"{POETRY} install") + precommit(c) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..abbd0dc --- /dev/null +++ b/tox.ini @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 88 +ignore = E203, W503