Skip to content

Commit

Permalink
feat: revised project structure
Browse files Browse the repository at this point in the history
- Plugins are now called "formats". From now, only the 'exporter' plugin needs to be registered.
  - Formats are still based on plugins, but are loaded dynamically by the 'exporter' one.
  • Loading branch information
adrienbrignon committed Jun 2, 2024
1 parent 78c5c72 commit 7f49ac6
Show file tree
Hide file tree
Showing 23 changed files with 261 additions and 233 deletions.
52 changes: 28 additions & 24 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,30 +41,6 @@ extra:
link: https://pypi.org/project/mkdocs-exporter

plugins:
- exporter:
- exporter-pdf:
concurrency: 16
enabled: !ENV [MKDOCS_EXPORTER_PDF, true]
stylesheets:
- resources/stylesheets/pdf.scss
covers:
front: resources/templates/covers/front.html.j2
back: resources/templates/covers/back.html.j2
browser:
debug: false
headless: true
- exporter-aggregator:
enabled: true
output: .well-known/document.pdf
covers: limits
- exporter-extras:
buttons:
- title: Download as PDF
icon: material-file-download-outline
enabled: !!python/name:mkdocs_exporter.plugins.pdf.button.enabled
attributes:
href: !!python/name:mkdocs_exporter.plugins.pdf.button.href
download: !!python/name:mkdocs_exporter.plugins.pdf.button.download
- search:
lang: en
- awesome-pages
Expand All @@ -82,6 +58,34 @@ plugins:
- social:
cards_layout_options:
background_color: '#EA2027'
- exporter:
formats:
pdf:
concurrency: 16
enabled: !ENV [MKDOCS_EXPORTER_PDF, true]
stylesheets:
- resources/stylesheets/pdf.scss
covers:
front: resources/templates/covers/front.html.j2
back: resources/templates/covers/back.html.j2
browser:
debug: false
headless: true
aggregator:
enabled: true
output: .well-known/document.pdf
covers: all
buttons:
- title: View as PDF
icon: material-file-move-outline
enabled: !!python/name:mkdocs_exporter.formats.pdf.buttons.download.enabled
attributes:
target: _blank
href: !!python/name:mkdocs_exporter.formats.pdf.buttons.download.href
- title: Download as PDF
icon: material-file-download-outline
enabled: !!python/name:mkdocs_exporter.formats.pdf.buttons.download.enabled
attributes: !!python/name:mkdocs_exporter.formats.pdf.buttons.download.attributes

markdown_extensions:
- admonition
Expand Down
32 changes: 32 additions & 0 deletions mkdocs_exporter/config.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,41 @@
from typing import Callable

from mkdocs.config import config_options as c
from mkdocs.config.base import Config as BaseConfig

from mkdocs_exporter.formats.pdf.config import Config as PDFFormatConfig


class FormatsConfig(BaseConfig):

"""The PDF format configuration."""
pdf = c.SubConfig(PDFFormatConfig)


class ButtonConfig(BaseConfig):
"""The configuration of a button."""

enabled = c.Type((bool, Callable), default=True)
"""Is the button enabled?"""

title = c.Type((str, Callable))
"""The button's title."""

icon = c.Type((str, Callable))
"""The button's icon (typically, an SVG element)."""

attributes = c.Type((dict, Callable), default={})
"""Some extra attributes to add to the button."""


class Config(BaseConfig):
"""The plugin's configuration."""

theme = c.Optional(c.Theme(default=None))
"""Override the theme used by your MkDocs instance."""

formats = c.SubConfig(FormatsConfig)
"""The formats to generate."""

buttons = c.ListOfItems(c.SubConfig(ButtonConfig))
"""The buttons to add."""
File renamed without changes.
File renamed without changes.
58 changes: 58 additions & 0 deletions mkdocs_exporter/formats/pdf/aggregator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from __future__ import annotations

import os

from pypdf import PdfReader, PdfWriter


class Aggregator:
"""Aggregates PDF documents together."""


def open(self, path: str) -> Aggregator:
"""Opens the aggregator."""

self.path = path
self.writer = PdfWriter()


def covers(self, mode: str) -> Aggregator:
"""Defines the way of handling cover pages."""

self._covers = mode


def aggregate(self, pages: list) -> Aggregator:
"""Aggregates pages together."""

for n, page in enumerate(pages):
if 'pdf' not in page.formats:
continue

bounds = None
pdf = page.formats['pdf']['path']
total = len(PdfReader(pdf).pages)

if 'covers' in page.formats['pdf']:
covers = page.formats['pdf']['covers']

if self._covers == 'none':
bounds = (1 if covers['front'] else 0, (total - 1) if covers['back'] else total)
if self._covers == 'limits':
bounds = (1 if n != 0 and covers['front'] else 0, (total - 1) if n != (len(pages) - 1) and covers['back'] else total)

self.writer.append(pdf, pages=bounds)


def save(self, metadata={}) -> Aggregator:
"""Saves the aggregated document."""

os.makedirs(os.path.dirname(self.path), exist_ok=True)

self.writer.add_metadata({'/Producer': 'MkDocs Exporter', **metadata})
self.writer.write(self.path)
self.writer.close()

self.writer = None

return self
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class Browser:
"""A web browser instance."""

args = [
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
'--allow-file-access-from-files'
]
"""The browser's arguments..."""
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,26 @@ def enabled(page: Page, **kwargs) -> bool:


def href(page: Page, **kwargs) -> str:
"""The button's 'href' attribute."""
"""The value of the 'href' attribute."""

return os.path.relpath(page.formats['pdf']['path'], page.url)
return os.path.relpath(page.formats['pdf']['url'], page.url)


def download(page: Page, **kwargs) -> str:
"""The button's 'download' attribute."""
"""The value of the 'download' attribute."""

return page.title + os.path.extsep + 'pdf'


def attributes(page: Page, **kwargs) -> dict:
"""The button's 'href' attribute."""

return {
'href': href(page, **kwargs),
'download': download(page, **kwargs),
}


def icon(page: Page, **kwargs) -> str:
"""The button's icon."""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@ class CoversConfig(BaseConfig):
"""The back cover template location."""


class AggregatorConfig(BaseConfig):

enabled = c.Type(bool, default=False)
"""Is the aggregator enabled?"""

output = c.Type(str, default='site.pdf')
"""The aggregated PDF document output file path."""

covers = c.Choice(['none', 'all', 'limits'], default='all')
"""The behaviour of cover pages."""

metadata = c.Type(dict, default={})
"""Some metadata to append to the PDF document."""


class Config(BaseConfig):
"""The plugin's configuration."""

Expand All @@ -51,3 +66,6 @@ class Config(BaseConfig):

url = c.Optional(c.Type(str))
"""The base URL that'll be prefixed to links with a relative path."""

aggregator = c.SubConfig(AggregatorConfig)
"""The aggregator's configuration."""
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@

from mkdocs_exporter.page import Page
from mkdocs_exporter.logging import logger
from mkdocs_exporter.plugins.pdf.config import Config
from mkdocs_exporter.plugins.pdf.renderer import Renderer
from mkdocs_exporter.formats.pdf.config import Config
from mkdocs_exporter.formats.pdf.renderer import Renderer
from mkdocs_exporter.formats.pdf.aggregator import Aggregator


class Plugin(BasePlugin[Config]):
Expand All @@ -25,14 +26,7 @@ def __init__(self):
self.watch: list[str] = []
self.renderer: Optional[Renderer] = None
self.tasks: list[types.CoroutineType] = []
self.loop: asyncio.AbstractEventLoopPolicy = asyncio.new_event_loop()


def on_startup(self, **kwargs) -> None:
"""Invoked when the plugin is starting..."""

nest_asyncio.apply(self.loop)
asyncio.set_event_loop(self.loop)
self.loop: Optional[asyncio.AbstractEventLoopPolicy] = None


def on_config(self, config: dict) -> None:
Expand All @@ -57,6 +51,7 @@ def on_serve(self, server: LiveReloadServer, **kwargs) -> LiveReloadServer:
return server


@event_priority(100)
def on_page_markdown(self, markdown: str, page: Page, config: Config, **kwargs) -> str:
"""Invoked when the page's markdown has been loaded."""

Expand Down Expand Up @@ -95,6 +90,11 @@ def on_pre_build(self, **kwargs) -> None:
if not self._enabled():
return

self.loop = asyncio.new_event_loop()

nest_asyncio.apply(self.loop)
asyncio.set_event_loop(self.loop)

self.renderer = Renderer(options=self.config)

for stylesheet in self.config.stylesheets:
Expand All @@ -103,6 +103,7 @@ def on_pre_build(self, **kwargs) -> None:
self.renderer.add_script(script)


@event_priority(90)
def on_pre_page(self, page: Page, config: dict, **kwargs):
"""Invoked before building the page."""

Expand All @@ -116,39 +117,38 @@ def on_pre_page(self, page: Page, config: dict, **kwargs):
fullpath = os.path.join(directory, filename)

page.formats['pdf'] = {
'path': os.path.relpath(fullpath, config['site_dir'])
'path': fullpath,
'url': os.path.relpath(fullpath, config['site_dir'])
}


@event_priority(-75)
def on_post_page(self, html: str, page: Page, config: dict) -> Optional[str]:
@event_priority(90)
def on_post_page(self, html: str, page: Page, **kwargs) -> Optional[str]:
"""Invoked after a page has been built."""

if not self._enabled(page) and 'pdf' in page.formats:
del page.formats['pdf']
if 'pdf' not in page.formats:
return html

page.html = html

async def render(page: Page) -> None:
logger.info("[mkdocs-exporter.pdf] Rendering '%s'...", page.file.src_path)
if 'pdf' in page.formats:
async def render(page: Page) -> None:
logger.info("[mkdocs-exporter.pdf] Rendering '%s'...", page.file.src_path)

html = self.renderer.preprocess(page)
pdf = await self.renderer.render(html)
html = self.renderer.preprocess(page)
pdf = await self.renderer.render(html)

page.html = None
with open(page.formats['pdf']['path'], 'wb+') as file:
file.write(pdf)
logger.info("[mkdocs-exporter.pdf] File written to '%s'!", file.name)

with open(os.path.join(config['site_dir'], page.formats['pdf']['path']), 'wb+') as file:
file.write(pdf)
logger.info("[mkdocs-exporter.pdf] File written to '%s'!", file.name)

self.tasks.append(render(page))
self.tasks.append(render(page))

return page.html


def on_post_build(self, **kwargs) -> None:
@event_priority(-100)
def on_post_build(self, config: dict, **kwargs) -> None:
"""Invoked after the build process."""

if not self._enabled():
Expand All @@ -167,8 +167,40 @@ async def limit(coroutine: Coroutine) -> Coroutine:
self.loop.run_until_complete(self.renderer.dispose())
self.tasks.clear()

self.loop = None
self.renderer = None

nest_asyncio.apply(self.loop)
asyncio.set_event_loop(self.loop)

if self.config.get('aggregator', {})['enabled']:
aggregator = Aggregator()
aggregator_config = self.config.get('aggregator', {})

aggregator.open(os.path.join(config['site_dir'], aggregator_config['output']))
aggregator.covers(aggregator_config['covers'])
aggregator.aggregate(self.pages)
aggregator.save(metadata=aggregator_config['metadata'])


@event_priority(-100)
def on_nav(self, nav, **kwargs):
"""Invoked when the navigation is ready."""

def flatten(items):
pages = []

for item in items:
if item.is_page:
pages.append(item)
if item.is_section:
pages = pages + flatten(item.children)

return pages

self.nav = nav
self.pages = flatten(nav)


def _enabled(self, page: Page = None) -> bool:
"""Is the plugin enabled for this page?"""
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

from mkdocs_exporter.page import Page
from mkdocs_exporter.resources import js
from mkdocs_exporter.plugins.pdf.browser import Browser
from mkdocs_exporter.formats.pdf.browser import Browser
from mkdocs_exporter.renderer import Renderer as BaseRenderer
from mkdocs_exporter.plugins.pdf.preprocessor import Preprocessor
from mkdocs_exporter.formats.pdf.preprocessor import Preprocessor


class Renderer(BaseRenderer):
Expand Down
Loading

0 comments on commit 7f49ac6

Please sign in to comment.