diff --git a/README.md b/README.md index 1dae107..def3ccc 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ A highly-configurable plugin for [*MkDocs*](https://github.com/mkdocs/mkdocs) th ## Prerequisites - Python `>= 3.7` -- MkDocs `>= 1.1` +- MkDocs `>= 1.4` ## Installation diff --git a/docs/.pages b/docs/.pages index 877c0bd..f1f2eff 100644 --- a/docs/.pages +++ b/docs/.pages @@ -1,4 +1,3 @@ nav: - getting-started.md - setup - - reference.md diff --git a/docs/getting-started.md b/docs/getting-started.md index 72ec95c..d0359c7 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,12 +1,21 @@ +--- +hide: + - navigation +--- + # Getting started +## Introduction + [MkDocs Exporter](/) is a plugin for [MkDocs](https://www.mkdocs.org/), it allows you to export your documentation to various formats such as PDF. If you're familiar with Python, you can install the plugin with `pip` (or your favourite package manager). ???+ tip "Did you know?" This documentation website features the plugin, meaning that you can download this page as a PDF document and read it offline! - Try this out by clicking the download button at the top of this page (or you can directly head [here](/getting-started/getting-started.pdf){:target="_blank"}). + Try this out by clicking the download button at the top of this page (or you can directly head [here](./getting-started.pdf){:target="_blank"}). + + ## Installation @@ -16,9 +25,11 @@ You can start by installing the plugin with the package manager of your choice: pip install mkdocs-exporter ``` -If you plan on using this plugin to generate PDF documents, you'll also need to install a browser and its dependencies. -As this project uses [Playwright](https://github.com/microsoft/playwright) under the hood, the installation is a breeze: +You can now register the plugin in your configuration file: +```yaml +plugins: + - mkdocs/exporter ``` -playwright install --with-deps -``` + +Check out the [setup guides](/setup) for more details about how to use and configure the plugin. diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/reference.md b/docs/reference.md deleted file mode 100644 index bb52dea..0000000 --- a/docs/reference.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -covers: - front: resources/templates/covers/front.alternate.html.j2 ---- - -# Reference diff --git a/docs/setup/.pages b/docs/setup/.pages new file mode 100644 index 0000000..608b39b --- /dev/null +++ b/docs/setup/.pages @@ -0,0 +1,4 @@ +nav: + - setting-up-documents.md + - setting-up-buttons.md + - ... diff --git a/docs/setup/index.md b/docs/setup/index.md deleted file mode 100644 index feae8cb..0000000 --- a/docs/setup/index.md +++ /dev/null @@ -1 +0,0 @@ -# Setup diff --git a/docs/setup/setting-up-buttons.md b/docs/setup/setting-up-buttons.md index 5fae0c5..fdd15f7 100644 --- a/docs/setup/setting-up-buttons.md +++ b/docs/setup/setting-up-buttons.md @@ -1 +1,91 @@ +--- +buttons: + - title: I'm Feeling Lucky + href: https://www.youtube.com/watch?v=dQw4w9WgXcQ + icon: material-star-outline + target: _blank +--- + # Setting up buttons + +You can define custom buttons at the top of your pages. + +## Configuration + +As this feature is provided by the `mkdocs/exporter/extras` plugin, you'll need to add it to your list of plugins: + +```yaml +plugins: + - mkdocs/exporter + - mkdocs/exporter/extras +``` + +## Usage + +### Adding a download button + +This example will add a download button at the top of all pages that have a corresponding PDF document: + +```yaml +plugins: + - mkdocs/exporter/extras: + buttons: + - title: Download as PDF + icon: material-file-download-outline + enabled: !!python/name:mkdocs_exporter.plugins.pdf.button.enabled + href: !!python/name:mkdocs_exporter.plugins.pdf.button.href + download: !!python/name:mkdocs_exporter.plugins.pdf.button.download +``` + +The functions referenced in this configuration are provided by the **MkDocs Exporter** plugin. + +### Defining a dynamic button + +As you've seen in the previous example, you can use Python functions to resolve the attributes of a button dynamically. +Let's write a button that when clicked, it starts a search on Google with the current page's title as query. + +First of all, let's write the function that will resolve to the `href` attribute's of the button: + +```python +from urllib.parse import urlencode +from mkdocs_exporter.page import Page + +def href(page: Page) -> str: + """The button's 'href' attribute.""" + + return 'https://google.com/search' + urlencode({q: page.title}) +``` + +Then, we can define a button and specify the previously defined function (assuming it has been saved to `my_module/button.py`): + +```yaml +plugins: + - mkdocs/exporter/extras: + buttons: + - title: Search on Google + icon: material-google + href: !!python/name:my_module.button.href +``` + +Rinse and repeat, you can use this method for any property of a button. + +### Adding a button on a page + +You can also use `meta` tags to define buttons on a per-page basis. +Here's the configuration used by this page: + +```yaml +--- +{% set button = page.meta.buttons[0] -%} + +buttons: + - title: {{ button.title }} + href: {{ button.href }} + icon: {{ button.icon }} + target: {{ button.target }} +--- + +# {{ page.title }} + +[...] +``` diff --git a/docs/setup/setting-up-cover-pages.md b/docs/setup/setting-up-cover-pages.md deleted file mode 100644 index f596230..0000000 --- a/docs/setup/setting-up-cover-pages.md +++ /dev/null @@ -1 +0,0 @@ -# Setting up cover pages diff --git a/docs/setup/setting-up-documents.md b/docs/setup/setting-up-documents.md new file mode 100644 index 0000000..a45944b --- /dev/null +++ b/docs/setup/setting-up-documents.md @@ -0,0 +1,86 @@ +# Setting up documents + +## Configuration + +First of all, you'll need to register the `mkdocs/exporter/pdf` plugin (**after** the `mkdocs/exporter` one) to your configuration: + +```yaml +plugins: + - mkdocs/exporter + - mkdocs/exporter/pdf +``` + +???+ question "Why an additional plugin?" + + **MkDocs Exporter** comes with various plugins in a single package. + + This architecture was chosen to reduce code duplication and maintain a generic base that can be used + to export your pages to formats other than PDF (although this is the only format currently supported). + + Basically, the `mkdocs/exporter` must always be registered first as it provides a common ground for + other plugins to use. + +## Usage + +### Toggle documents generation + +The documents generation can be enabled or disabled at any time. +This feature is especially useful during your development process, when you don't want to slow down your iterations because of document generation. + +```yaml +plugins: + - mkdocs/exporter/pdf: + enabled: ![MKDOCS_EXPORTER_ENABLED, true] +``` + +You can now use the `MKDOCS_EXPORTER_ENABLED` environment variable to toggle the PDF generation. + +### Increase concurrency + +PDF are, by default, generated concurrently which greatly reduces build time. +You may want to override the default value of **4**, based on your current hardware. + +```yaml +plugins: + - mkdocs/exporter/pdf: + concurrency: 16 +``` + +With this configuration, up to **16** PDF documents can be generated concurrently. +As you've guessed, a value of **1** will build PDF documents sequentially. + +### Exclude some pages + +Sometimes, you may want to prevent a page from being converted to a PDF document. +You can use the `pdf` meta tag on your page to do so: + +```yaml +--- +pdf: false +--- + +# Lorem ipsum dolor sit amet + +[...] +``` + +If you exclude more pages than you include, you can take the problem the other way around and explicitly define the pages for which PDF documents should be generated. +We call that the `explicit` mode, it can be enabled in your configuration file: + +```yaml +plugins: + - mkdocs/exporter/pdf: + explicit: true +``` + +Only pages with a truthy value in the `pdf` meta tag will have a PDF document generated. + +```yaml +--- +pdf: true +--- + +# Lorem ipsum dolor sit amet + +[...] +``` diff --git a/mkdocs.yml b/mkdocs.yml index 06f5b63..00a5ef4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -48,8 +48,8 @@ plugins: - mkdocs/exporter/extras: buttons: - title: Download as PDF + icon: material-file-download-outline enabled: !!python/name:mkdocs_exporter.plugins.pdf.button.enabled - icon: !!python/name:mkdocs_exporter.plugins.pdf.button.icon href: !!python/name:mkdocs_exporter.plugins.pdf.button.href download: !!python/name:mkdocs_exporter.plugins.pdf.button.download - search: diff --git a/mkdocs_exporter/plugins/extras/config.py b/mkdocs_exporter/plugins/extras/config.py index c0a24d3..b82c8b5 100644 --- a/mkdocs_exporter/plugins/extras/config.py +++ b/mkdocs_exporter/plugins/extras/config.py @@ -9,6 +9,9 @@ class ButtonConfig(BaseConfig): enabled = c.Type((bool, Callable), default=True) """Is the button enabled?""" + id = c.Optional(c.Type(str, Callable)) + """The button's identifier.""" + title = c.Type((str, Callable)) """The button's title.""" diff --git a/mkdocs_exporter/plugins/extras/icon.py b/mkdocs_exporter/plugins/extras/icon.py new file mode 100644 index 0000000..623c22d --- /dev/null +++ b/mkdocs_exporter/plugins/extras/icon.py @@ -0,0 +1,40 @@ +from materialx.emoji import twemoji, to_svg + + +class HTMLStash: + """Markdown HTML stash stub.""" + + def store(self, str): + return str + + +class MarkdownEmoji: + """Markdown Emojij stub.""" + + emoji_index = twemoji({}, None) + + +class Markdown: + """Markdown stub.""" + + htmlStash = HTMLStash() + inlinePatterns = { + 'emoji': MarkdownEmoji() + } + + +def get_svg_icon(name): + """Gets an icon by its name.""" + + if not name.startswith(':'): + name = ':' + name + if not name.endswith(':'): + name = name + ':' + + try: + icon = to_svg('twemoji', name, None, None, None, None, None, {}, Markdown()) + + if icon is not None: + return icon.text + except KeyError: + return None diff --git a/mkdocs_exporter/plugins/extras/plugin.py b/mkdocs_exporter/plugins/extras/plugin.py index 21497da..bbf429e 100644 --- a/mkdocs_exporter/plugins/extras/plugin.py +++ b/mkdocs_exporter/plugins/extras/plugin.py @@ -2,8 +2,8 @@ from mkdocs.plugins import BasePlugin from mkdocs_exporter.page import Page from mkdocs.plugins import event_priority -from mkdocs_exporter.preprocessor import Preprocessor from mkdocs_exporter.plugins.extras.config import Config +from mkdocs_exporter.plugins.extras.preprocessor import Preprocessor class Plugin(BasePlugin[Config]): @@ -15,17 +15,14 @@ def on_post_page(self, html: str, page: Page, **kwargs) -> Optional[str]: """Invoked after a page has been built.""" def resolve(value): - if callable(value): - return value(page) - - return value + return value(page) if callable(value) else value preprocessor = Preprocessor() preprocessor.preprocess(html) - for button in self.config.buttons: - if resolve(button['enabled']): + for button in [*self.config.buttons, *page.meta.get('buttons', [])]: + if 'enabled' not in button or resolve(button['enabled']): preprocessor.button(**{k: resolve(v) for k, v in button.items()}) return preprocessor.done() diff --git a/mkdocs_exporter/plugins/extras/preprocessor.py b/mkdocs_exporter/plugins/extras/preprocessor.py new file mode 100644 index 0000000..21320ba --- /dev/null +++ b/mkdocs_exporter/plugins/extras/preprocessor.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from bs4 import BeautifulSoup +from mkdocs_exporter.plugins.extras.icon import get_svg_icon +from mkdocs_exporter.preprocessor import Preprocessor as BasePreprocessor + + +class Preprocessor(BasePreprocessor): + """An extended preprocessor.""" + + + def button(self, title: str, href: str, icon: str, **kwargs) -> Preprocessor: + """Adds a button at the top of the page.""" + + tags = self.html.find('nav', {'class': 'md-tags'}) + button = self.html.new_tag('a', title=title, href=href, **kwargs, attrs={'class': 'md-content__button md-icon'}) + svg = BeautifulSoup(get_svg_icon(icon) or get_svg_icon('material-progress-question'), 'lxml') + + button.append(svg) + + if tags: + tags.insert_after(button) + else: + self.html.find('article', {'class': 'md-content__inner'}).insert(0, button) + + return self diff --git a/mkdocs_exporter/plugins/pdf/button.py b/mkdocs_exporter/plugins/pdf/button.py index b49ec89..07a2777 100644 --- a/mkdocs_exporter/plugins/pdf/button.py +++ b/mkdocs_exporter/plugins/pdf/button.py @@ -1,6 +1,5 @@ import os -from textwrap import dedent from mkdocs_exporter.page import Page @@ -25,8 +24,4 @@ def download(page: Page) -> str: def icon(page: Page) -> str: """The button's icon.""" - return dedent(''' - - - - ''') + return page.meta.get('pdf-icon', 'material-file-download-outline') diff --git a/mkdocs_exporter/plugins/pdf/plugin.py b/mkdocs_exporter/plugins/pdf/plugin.py index 24a49fc..09d12c3 100644 --- a/mkdocs_exporter/plugins/pdf/plugin.py +++ b/mkdocs_exporter/plugins/pdf/plugin.py @@ -84,11 +84,13 @@ def on_pre_build(self, **kwargs) -> None: def on_pre_page(self, page: Page, config: dict, **kwargs): """Invoked before building the page.""" + if not hasattr(page, 'html'): + raise Exception('Missing `mkdocs/exporter` plugin or your plugins are not ordered properly!') if not self._enabled(): return directory = os.path.dirname(page.file.abs_dest_path) - filename = os.path.splitext(os.path.basename(page.file.abs_src_path))[0] + '.pdf' + filename = os.path.splitext(os.path.basename(page.file.abs_dest_path))[0] + '.pdf' fullpath = os.path.join(directory, filename) page.formats['pdf'] = os.path.relpath(fullpath, config['site_dir']) diff --git a/mkdocs_exporter/preprocessor.py b/mkdocs_exporter/preprocessor.py index 06be3fc..8064d6e 100644 --- a/mkdocs_exporter/preprocessor.py +++ b/mkdocs_exporter/preprocessor.py @@ -26,23 +26,6 @@ def preprocess(self, html: str) -> Preprocessor: return self - def button(self, title: str, href: str, icon: str, **kwargs) -> Preprocessor: - """Adds a button at the top of the page.""" - - tags = self.html.find('nav', {'class': 'md-tags'}) - button = self.html.new_tag('a', title=title, href=href, **kwargs, attrs={'class': 'md-content__button md-icon'}) - svg = BeautifulSoup(icon, 'lxml') - - button.append(svg) - - if tags: - tags.insert_after(button) - else: - self.html.find('article', {'class': 'md-content__inner'}).insert(0, button) - - return self - - def teleport(self) -> Preprocessor: """Teleport elements to their destination.""" diff --git a/poetry.lock b/poetry.lock index 6674486..3a71795 100644 --- a/poetry.lock +++ b/poetry.lock @@ -729,14 +729,14 @@ test = ["mkdocs-include-markdown-plugin", "mkdocs-macros-test", "mkdocs-material [[package]] name = "mkdocs-material" -version = "9.1.11" +version = "9.1.12" description = "Documentation that simply works" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "mkdocs_material-9.1.11-py3-none-any.whl", hash = "sha256:fbc86d50ec2cf34d40d5c4365780f290ceedde23f1a0704323b34e7f16b0c0dd"}, - {file = "mkdocs_material-9.1.11.tar.gz", hash = "sha256:f5d473eb79d6640a5e668d4b2ab5b9de5e76ae0a0e2d864112df0cfe9016dc1d"}, + {file = "mkdocs_material-9.1.12-py3-none-any.whl", hash = "sha256:68c57d95d10104179c8c3ce9a88ee9d2322a5145b3d0f1f38ff686253fb5ec98"}, + {file = "mkdocs_material-9.1.12.tar.gz", hash = "sha256:d4ebe9b5031ce63a265c19fb5eab4d27ea4edadb05de206372e831b2b7570fb5"}, ] [package.dependencies] @@ -754,7 +754,7 @@ requests = ">=2.26" name = "mkdocs-material-extensions" version = "1.1.1" description = "Extension pack for Python Markdown and MkDocs Material." -category = "dev" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1286,4 +1286,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = ">=3.7" -content-hash = "6fa3a0b936718ab81a3145172dab3d1d16a71dfcf19376102d069153ceba5926" +content-hash = "74f067bbdbe4d97e00e9f74577019786c1ef1bdd07fa09274e54179b9394f8b6" diff --git a/pyproject.toml b/pyproject.toml index c20f02e..798fac3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ lxml = ">=4.9" libsass = ">=0.22.0" importlib-resources = ">=5.0" importlib-metadata = "<5.0" +mkdocs-material-extensions = "^1.1.1" [tool.poetry.plugins."mkdocs.plugins"] "mkdocs/exporter" = "mkdocs_exporter.plugin:Plugin" diff --git a/resources/stylesheets/pdf.scss b/resources/stylesheets/pdf.scss index 60ceaf2..a301454 100644 --- a/resources/stylesheets/pdf.scss +++ b/resources/stylesheets/pdf.scss @@ -10,6 +10,14 @@ margin: 1.20cm; } +hr:has(+ div.md-source-file), .md-source-file { + display: none !important; +} + +summary::after { + display: none; +} + .front-cover { margin: var(--margin); height: calc(var(--height) - var(--offset));