From 53546ffc197254771674283f071fd0bdc94f2f03 Mon Sep 17 00:00:00 2001 From: Mike Edmunds Date: Mon, 19 Feb 2024 14:39:53 -0800 Subject: [PATCH] Docs: simplify editing ESP feature matrix Move the big ESP feature matrix table into a CSV file for easier maintenance. Remove the doc8 line-length exception the old table needed. Docutils csv-table directive doesn't support colspan on the subheadings like the old table did. Add some JS that replicates the old behavior. (The new table is still readable even with JS disabled.) --- docs/_static/table-formatting.js | 40 +++++++++++++++++++++++ docs/conf.py | 1 + docs/esps/esp-feature-matrix.csv | 19 +++++++++++ docs/esps/index.rst | 55 +++++++------------------------- pyproject.toml | 2 -- 5 files changed, 72 insertions(+), 45 deletions(-) create mode 100644 docs/_static/table-formatting.js create mode 100644 docs/esps/esp-feature-matrix.csv diff --git a/docs/_static/table-formatting.js b/docs/_static/table-formatting.js new file mode 100644 index 0000000..c3f225f --- /dev/null +++ b/docs/_static/table-formatting.js @@ -0,0 +1,40 @@ +/** + * Return the first sibling of el that matches CSS selector, or null if no matches. + * @param {HTMLElement} el + * @param {string} selector + * @returns {HTMLElement|null} + */ +function nextSiblingMatching(el, selector) { + while (el && el.nextElementSibling) { + el = el.nextElementSibling; + if (el.matches(selector)) { + return el; + } + } + return null; +} + +/** + * Convert runs of empty elements to a colspan on the first . + */ +function collapseEmptyTableCells() { + document.querySelectorAll(".rst-content tr:has(td:empty)").forEach((tr) => { + for ( + let spanStart = tr.querySelector("td"); + spanStart; + spanStart = nextSiblingMatching(spanStart, "td") + ) { + let emptyCell; + while ((emptyCell = nextSiblingMatching(spanStart, "td:empty"))) { + emptyCell.remove(); + spanStart.colSpan++; + } + } + }); +} + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", collapseEmptyTableCells); +} else { + collapseEmptyTableCells(); +} diff --git a/docs/conf.py b/docs/conf.py index 820249d..e1d575e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -279,6 +279,7 @@ def setup(app): anymail_config_js = (DOCS_PATH / "_static/anymail-config.js").read_text() app.add_js_file(None, body=anymail_config_js) app.add_js_file("version-alert.js", **{"async": "async"}) + app.add_js_file("table-formatting.js", **{"async": "async"}) app.add_js_file("https://unpkg.com/rate-the-docs", **{"async": "async"}) # Django-specific roles, from diff --git a/docs/esps/esp-feature-matrix.csv b/docs/esps/esp-feature-matrix.csv new file mode 100644 index 0000000..affd044 --- /dev/null +++ b/docs/esps/esp-feature-matrix.csv @@ -0,0 +1,19 @@ +Email Service Provider,:ref:`amazon-ses-backend`,:ref:`brevo-backend`,:ref:`mailersend-backend`,:ref:`mailgun-backend`,:ref:`mailjet-backend`,:ref:`mandrill-backend`,:ref:`postal-backend`,:ref:`postmark-backend`,:ref:`resend-backend`,:ref:`sendgrid-backend`,:ref:`sparkpost-backend` +.. rubric:: :ref:`Anymail send options `,,,,,,,,,,, +:attr:`~AnymailMessage.envelope_sender`,Yes,No,No,Domain only,Yes,Domain only,Yes,No,No,No,Yes +:attr:`~AnymailMessage.metadata`,Yes,Yes,No,Yes,Yes,Yes,No,Yes,Yes,Yes,Yes +:attr:`~AnymailMessage.merge_metadata`,No,No,No,Yes,Yes,Yes,No,Yes,No,Yes,Yes +:attr:`~AnymailMessage.send_at`,No,Yes,Yes,Yes,No,Yes,No,No,No,Yes,Yes +:attr:`~AnymailMessage.tags`,Yes,Yes,Yes,Yes,Max 1 tag,Yes,Max 1 tag,Max 1 tag,Yes,Yes,Max 1 tag +:attr:`~AnymailMessage.track_clicks`,No,No,Yes,Yes,Yes,Yes,No,Yes,No,Yes,Yes +:attr:`~AnymailMessage.track_opens`,No,No,Yes,Yes,Yes,Yes,No,Yes,No,Yes,Yes +:ref:`amp-email`,Yes,No,No,Yes,No,No,No,No,No,Yes,Yes +.. rubric:: :ref:`templates-and-merge`,,,,,,,,,,, +:attr:`~AnymailMessage.template_id`,Yes,Yes,Yes,Yes,Yes,Yes,No,Yes,No,Yes,Yes +:attr:`~AnymailMessage.merge_data`,Yes,No,Yes,Yes,Yes,Yes,No,Yes,No,Yes,Yes +:attr:`~AnymailMessage.merge_global_data`,Yes,Yes,(emulated),(emulated),Yes,Yes,No,Yes,No,Yes,Yes +.. rubric:: :ref:`Status ` and :ref:`event tracking `,,,,,,,,,,, +:attr:`~AnymailMessage.anymail_status`,Yes,Yes,Yes,Yes,Yes,Yes,Yes,Yes,Yes,Yes,Yes +:class:`~anymail.signals.AnymailTrackingEvent` from webhooks,Yes,Yes,Yes,Yes,Yes,Yes,Yes,Yes,Yes,Yes,Yes +.. rubric:: :ref:`Inbound handling `,,,,,,,,,,, +:class:`~anymail.signals.AnymailInboundEvent` from webhooks,Yes,Yes,Yes,Yes,Yes,Yes,Yes,Yes,No,Yes,Yes diff --git a/docs/esps/index.rst b/docs/esps/index.rst index 06f8ae9..19a54c6 100644 --- a/docs/esps/index.rst +++ b/docs/esps/index.rst @@ -33,56 +33,25 @@ The table below summarizes the Anymail features supported for each ESP. .. currentmodule:: anymail.message -.. rst-class:: sticky-left - -============================================ ============ ======= ============ =========== ========== =========== ========== ========== ======== ========== =========== -Email Service Provider |Amazon SES| |Brevo| |MailerSend| |Mailgun| |Mailjet| |Mandrill| |Postal| |Postmark| |Resend| |SendGrid| |SparkPost| -============================================ ============ ======= ============ =========== ========== =========== ========== ========== ======== ========== =========== -.. rubric:: :ref:`Anymail send options ` ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -:attr:`~AnymailMessage.envelope_sender` Yes No No Domain only Yes Domain only Yes No No No Yes -:attr:`~AnymailMessage.metadata` Yes Yes No Yes Yes Yes No Yes Yes Yes Yes -:attr:`~AnymailMessage.merge_metadata` No No No Yes Yes Yes No Yes No Yes Yes -:attr:`~AnymailMessage.send_at` No Yes Yes Yes No Yes No No No Yes Yes -:attr:`~AnymailMessage.tags` Yes Yes Yes Yes Max 1 tag Yes Max 1 tag Max 1 tag Yes Yes Max 1 tag -:attr:`~AnymailMessage.track_clicks` No No Yes Yes Yes Yes No Yes No Yes Yes -:attr:`~AnymailMessage.track_opens` No No Yes Yes Yes Yes No Yes No Yes Yes -:ref:`amp-email` Yes No No Yes No No No No No Yes Yes - -.. rubric:: :ref:`templates-and-merge` ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -:attr:`~AnymailMessage.template_id` Yes Yes Yes Yes Yes Yes No Yes No Yes Yes -:attr:`~AnymailMessage.merge_data` Yes No Yes Yes Yes Yes No Yes No Yes Yes -:attr:`~AnymailMessage.merge_global_data` Yes Yes (emulated) (emulated) Yes Yes No Yes No Yes Yes -.. rubric:: :ref:`Status ` and :ref:`event tracking ` ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -:attr:`~AnymailMessage.anymail_status` Yes Yes Yes Yes Yes Yes Yes Yes Yes Yes Yes -|AnymailTrackingEvent| from webhooks Yes Yes Yes Yes Yes Yes Yes Yes Yes Yes Yes -.. rubric:: :ref:`Inbound handling ` ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -|AnymailInboundEvent| from webhooks Yes Yes Yes Yes Yes Yes Yes Yes No Yes Yes -============================================ ============ ======= ============ =========== ========== =========== ========== ========== ======== ========== =========== +.. It's much easier to edit esp-feature-matrix.csv with a CSV-aware editor, such as: +.. PyCharm (Pro has native CSV support; use a CSV editor plugin with Community) +.. VSCode with a CSV editor extension +.. Excel (watch out for charset issues), Apple Numbers, or Google Sheets +.. Every row must have the same number of columns. If you add a column, you must +.. also add a comma to each sub-heading row. (A CSV editor should handle this for you.) +.. Please keep columns sorted alphabetically by ESP name. +.. csv-table:: + :file: esp-feature-matrix.csv + :header-rows: 1 + :widths: auto + :class: sticky-left Trying to choose an ESP? Please **don't** start with this table. It's far more important to consider things like an ESP's deliverability stats, latency, uptime, and support for developers. The *number* of extra features an ESP offers is almost meaningless. (And even specific features don't matter if you don't plan to use them.) -.. |Amazon SES| replace:: :ref:`amazon-ses-backend` -.. |Brevo| replace:: :ref:`brevo-backend` -.. |MailerSend| replace:: :ref:`mailersend-backend` -.. |Mailgun| replace:: :ref:`mailgun-backend` -.. |Mailjet| replace:: :ref:`mailjet-backend` -.. |Mandrill| replace:: :ref:`mandrill-backend` -.. |Postal| replace:: :ref:`postal-backend` -.. |Postmark| replace:: :ref:`postmark-backend` -.. |Resend| replace:: :ref:`resend-backend` -.. |SendGrid| replace:: :ref:`sendgrid-backend` -.. |SparkPost| replace:: :ref:`sparkpost-backend` -.. |AnymailTrackingEvent| replace:: :class:`~anymail.signals.AnymailTrackingEvent` -.. |AnymailInboundEvent| replace:: :class:`~anymail.signals.AnymailInboundEvent` - Other ESPs ---------- diff --git a/pyproject.toml b/pyproject.toml index 1d75d5e..ee1fb7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,8 +109,6 @@ max-line-length = 88 target-version = ["py37"] [tool.doc8] -# ignore very long lines in ESP support table: -ignore-path-errors = ["docs/esps/index.rst;D001"] # for now, Anymail allows longer lines in docs source: max-line-length = 120