├── .gitattributes
├── .gitignore
├── .travis.yml
├── CONTRIBUTING.md
├── LICENSE.md
├── MANIFEST.in
├── README.md
├── docs
├── .gitignore
├── docs
│ ├── index.md
│ └── options.md
├── mkdocs.yml
└── theme-handler
│ └── cinder.py
├── mkdocs_pdf_export_plugin
├── __init__.py
├── plugin.py
├── preprocessor
│ ├── __init__.py
│ ├── links
│ │ ├── __init__.py
│ │ ├── transform.py
│ │ └── util.py
│ └── prep.py
├── renderer.py
└── themes
│ ├── __init__.py
│ ├── cinder.py
│ ├── generic.py
│ └── material.py
├── requirements.txt
└── setup.py
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea/
2 | /venv/
3 | __pycache__/
4 | *.egg-info/
5 | /build/
6 | /dist/
7 |
8 | !.gitkeep
9 |
10 | .DS_Store
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | matrix:
3 | include:
4 | - python: 3.5
5 | - python: 3.6
6 | - python: 3.7
7 | sudo: required
8 | dist: xenial
9 |
10 | script: true
11 |
12 | branches:
13 | only:
14 | - master
15 | - /^v\d+\.\d+(\.\d+)?(-\S*)?$/ # version tags
16 |
17 | notifications:
18 | email:
19 | on_success: never
20 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contribution Guidelines
2 |
3 | Thank you for considering to contribute to this project. These guidelines will help you get going with development and outline the most important rules to follow when submitting pull requests for this project.
4 |
5 | ## Submitting Changes
6 |
7 | To get changes merged, create a pull request. Here are a few things to pay attention to when doing so:
8 |
9 | #### Commit Messages
10 |
11 | The summary of a commit should be concise and worded in an imperative mood.
12 | ...a *what* mood? This should clear things up: *[How to Write a Git Commit Message][git-commit-message]*
13 |
14 | #### Code Style
15 |
16 | Make sure your code follows [PEP-8](https://www.python.org/dev/peps/pep-0008/) and keeps things consistent with the rest of the code.
17 |
18 | [git-commit-message]: https://chris.beams.io/posts/git-commit/
19 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | Copyright (c) 2018 Stephan Hauser
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.md
2 | include LICENSE.md
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MkDocs PDF Export Plugin [![Build Status][travis-status]][travis-link]
2 |
3 | *An MkDocs plugin to export content pages as PDF files*
4 |
5 | The pdf-export plugin will export all markdown pages in your MkDocs repository as PDF files using [WeasyPrint](http://weasyprint.org/). The exported documents support many advanced features missing in most other PDF exports, such as a PDF Index and support for [CSS paged media module](https://drafts.csswg.org/css-page-3/).
6 |
7 | ## Requirements
8 |
9 | 1. This package requires MkDocs version 1.0 or higher (0.17 works as well)
10 | 2. Python ~3.4~ 3.5 or higher
11 | 3. WeasyPrint depends on cairo, Pango and GDK-PixBuf which need to be installed separately. Please follow the installation instructions for your platform carefully:
12 | - [Linux][weasyprint-linux]
13 | - [MacOS][weasyprint-macos]
14 | - [Windows][weasyprint-windows]
15 | 4. Explicit support for your mkdocs theme is probably required. As of now, the only supported theme is [mkdocs-material][mkdocs-material]. A generic version will just generate the PDF files and put the download link into a `` tag.
16 |
17 | If you want to add a new theme, see [adding support for new themes](#adding-support-for-new-themes) for more information.
18 |
19 | ## Installation
20 |
21 | Install the package with pip:
22 |
23 | ```bash
24 | pip install mkdocs-pdf-export-plugin
25 | ```
26 |
27 | Enable the plugin in your `mkdocs.yml`:
28 |
29 | ```yaml
30 | plugins:
31 | - search
32 | - pdf-export
33 | ```
34 |
35 | > **Note:** If you have no `plugins` entry in your config file yet, you'll likely also want to add the `search` plugin. MkDocs enables it by default if there is no `plugins` entry set, but now you have to enable it explicitly.
36 |
37 | More information about plugins in the [MkDocs documentation][mkdocs-plugins].
38 |
39 | ## Testing
40 |
41 | When building your repository with `mkdocs build`, you should now see the following message at the end of your build output:
42 |
43 | > Converting 17 files to PDF took 15.6s
44 |
45 | In your `site_dir` you should now have a PDF file for every markdown page.
46 |
47 | ## Options
48 |
49 | You may customize the plugin by passing options in `mkdocs.yml`:
50 |
51 | ```yaml
52 | plugins:
53 | - pdf-export:
54 | verbose: true
55 | media_type: print
56 | enabled_if_env: ENABLE_PDF_EXPORT
57 | ```
58 |
59 | ### `verbose`
60 |
61 | Setting this to `true` will show all WeasyPrint debug messages during the build. Default is `false`.
62 |
63 | ### `media_type`
64 |
65 | This option allows you to use a different CSS media type (or a custom one like `pdf-export`) for the PDF export. Default is `print`.
66 |
67 | ### `enabled_if_env`
68 |
69 | Setting this option will enable the build only if there is an environment variable set to 1. This is useful to disable building the PDF files during development, since it can take a long time to export all files. Default is not set.
70 |
71 | ### `combined`
72 |
73 | Setting this to `true` will combine all pages into a single PDF file. All download links will point to this file. Default is `false`.
74 |
75 | ### `combined_output_path`
76 |
77 | This option allows you to use a different destination for the combined PDF file. Has no effect when `combined` is set to `false`. Default is `pdf/combined.pdf`.
78 |
79 | ### `theme_handler_path`
80 |
81 | This option allows you to specify a custom theme handler module. This path must be **relative to your project root** (See example below). Default is not set.
82 |
83 | `mkdocs.yml`:
84 | ```yaml
85 | plugins:
86 | - pdf-export:
87 | theme_handler_path: theme-handler.py
88 | ```
89 | ```bash
90 | project-root
91 | ├── theme-handler.py
92 | ├── docs
93 | ├── mkdocs.yml
94 | ├── site
95 | .
96 | .
97 | ```
98 |
99 | ## Adjusting the output
100 |
101 | The resulting PDF can be customized easily by adding a custom stylesheet such as the following:
102 |
103 | ```
104 | @page {
105 | size: a4 portrait;
106 | margin: 25mm 10mm 25mm 10mm;
107 | counter-increment: page;
108 | font-family: "Roboto","Helvetica Neue",Helvetica,Arial,sans-serif;
109 | white-space: pre;
110 | color: grey;
111 | @top-left {
112 | content: '© 2018 My Company';
113 | }
114 | @top-center {
115 | content: string(chapter);
116 | }
117 | @top-right {
118 | content: 'Page ' counter(page);
119 | }
120 | }
121 | ```
122 | For this to take effect, use the `extra_css` directive in mkdocs.yml, as described in the [MkDocs user guide][extra-css].
123 |
124 | ## Adding support for new themes
125 |
126 | If you use a mkdocs theme which is currently not supported, check out the `themes/material.py` file and adjust it according to your requirements. You will have to implement two methods to support a theme:
127 |
128 | 1. `get_stylesheet` should return a CSS which gets applied to fix issues with weasyprint
129 | 2. `modify_html` should add a link to the PDF download before writing it to disk
130 |
131 | If there is no explicit support for your theme, the generic version will just add a `` tag in the head pointing to the generated PDF.
132 |
133 | ## Contributing
134 |
135 | From reporting a bug to submitting a pull request: every contribution is appreciated and welcome. Report bugs, ask questions and request features using [Github issues][github-issues].
136 | If you want to contribute to the code of this project, please read the [Contribution Guidelines][contributing].
137 |
138 | ## Special thanks
139 |
140 | Special thanks go to [Stephan Hauser][shauser] for the original development of this plugin.
141 |
142 | Special thanks go to [Lukas Geiter][lukasgeiter] for developing the [mkdocs-awesome-pages-plugin][awesome-pages-plugin] which was used as a base and for convincing [Stephan Hauser][shauser] to write a plugin for this.
143 |
144 | [travis-status]: https://travis-ci.org/zhaoterryy/mkdocs-pdf-export-plugin.svg?branch=master
145 | [travis-link]: https://travis-ci.org/zhaoterryy/mkdocs-pdf-export-plugin
146 | [weasyprint-linux]: https://weasyprint.readthedocs.io/en/latest/install.html#linux
147 | [weasyprint-macos]: https://weasyprint.readthedocs.io/en/latest/install.html#os-x
148 | [weasyprint-windows]: https://weasyprint.readthedocs.io/en/latest/install.html#windows
149 | [mkdocs-plugins]: http://www.mkdocs.org/user-guide/plugins/
150 | [mkdocs-material]: https://github.com/squidfunk/mkdocs-material
151 | [github-issues]: https://github.com/zhaoterryy/mkdocs-pdf-export-plugin/issues
152 | [contributing]: CONTRIBUTING.md
153 | [lukasgeiter]: https://github.com/lukasgeiter
154 | [shauser]: https://github.com/shauser
155 | [awesome-pages-plugin]: https://github.com/lukasgeiter/mkdocs-awesome-pages-plugin
156 | [extra-css]: https://www.mkdocs.org/user-guide/configuration/#extra_css
157 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | site/
--------------------------------------------------------------------------------
/docs/docs/index.md:
--------------------------------------------------------------------------------
1 | # MkDocs PDF Export Plugin [](https://travis-ci.org/zhaoterryy/mkdocs-pdf-export-plugin)
2 |
3 | The pdf-export plugin will export all markdown pages in your MkDocs repository as PDF files using [WeasyPrint](http://weasyprint.org/). The exported documents support many advanced features missing in most other PDF exports, such as a PDF Index and support for [CSS paged media module](https://developer.mozilla.org/en-US/docs/Web/CSS/@page).
4 |
5 | - MkDocs >= 1.0
6 | - Python >= 3.4
7 | - WeasyPrint >= 44
8 |
9 | ## Installation
10 |
11 | Install the package with pip:
12 |
13 | ```bash
14 | pip install mkdocs-pdf-export-plugin
15 | ```
16 |
17 | Enable the plugin in your `mkdocs.yml`:
18 |
19 | ```yaml
20 | plugins:
21 | - search
22 | - pdf-export
23 | ```
24 |
25 | > **Note:** If you have no `plugins` entry in your config file yet, you'll likely also want to add the `search` plugin. MkDocs enables it by default if there is no `plugins` entry set, but now you have to enable it explicitly.
26 |
27 | More information about plugins in the [MkDocs documentation](http://www.mkdocs.org/user-guide/plugins/).
28 |
29 | ## Contributing
30 |
31 | From reporting a bug to submitting a pull request: every contribution is appreciated and welcome. Report bugs, ask questions and request features using [Github issues][github-issues].
32 |
33 | If you want to contribute to the code of this project, please read the [Contribution Guidelines][contributing].
34 |
35 | #### **Special thanks**
36 |
37 | Special thanks go to [Stephan Hauser][shauser] for the original development of this plugin.
38 |
39 | Special thanks go to [Lukas Geiter][lukasgeiter] for developing the [mkdocs-awesome-pages-plugin][awesome-pages-plugin] which was used as a base and for convincing [Stephan Hauser][shauser] to write a plugin for this.
40 |
41 | [github-issues]: https://github.com/zhaoterryy/mkdocs-pdf-export-plugin/issues
42 | [contributing]: https://github.com/zhaoterryy/mkdocs-pdf-export-plugin/blob/master/CONTRIBUTING.md
43 | [lukasgeiter]: https://github.com/lukasgeiter
44 | [shauser]: https://github.com/shauser
45 | [awesome-pages-plugin]: https://github.com/lukasgeiter/mkdocs-awesome-pages-plugin
--------------------------------------------------------------------------------
/docs/docs/options.md:
--------------------------------------------------------------------------------
1 | # Options
2 |
3 | Pass in options through `mkdocs.yml`:
4 |
5 | ```yaml
6 | plugins:
7 | - pdf-export:
8 | verbose: true
9 | media_type: print
10 | enabled_if_env: ENABLE_PDF_EXPORT
11 | ```
12 |
13 | ### `verbose`
14 |
15 | *default: false*
16 |
17 | Setting this to `true` will show all WeasyPrint debug messages during the build.
18 |
19 | ### `media_type`
20 |
21 | *default: print*
22 |
23 | This option allows you to use a different CSS media type (or a custom one like `pdf-export`) for the PDF export.
24 |
25 | ### `enabled_if_env`
26 |
27 | *default: not set*
28 |
29 | Setting this option will enable the build only if there is an environment variable set to 1. This is useful to disable building the PDF files during development, since it can take a long time to export all files.
30 |
31 | ### `combined`
32 |
33 | *default: false*
34 |
35 | Setting this to `true` will combine all pages into a single PDF file. All download links will point to this file.
36 |
37 | ### `combined_output_path`
38 |
39 | *default: pdf/combined.pdf*
40 |
41 | This option allows you to use a different destination for the combined PDF file. Has no effect when `combined` is set to `false`.
42 |
43 | ### `theme_handler_path`
44 |
45 | *default: not set*
46 |
47 | This option allows you to specify a custom theme handler module. This path must be ***relative to your project root*** (See example below).
48 |
49 | `mkdocs.yml`:
50 | ```yaml
51 | plugins:
52 | - pdf-export:
53 | theme_handler_path: theme-handler.py
54 |
55 | ```
56 |
57 | ```bash
58 |
59 | project-root
60 |
61 | ├── theme-handler.py
62 |
63 | ├── docs
64 |
65 | ├── mkdocs.yml
66 |
67 | ├── site
68 |
69 | .
70 |
71 | .
72 |
73 | ```
--------------------------------------------------------------------------------
/docs/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: mkdocs-pdf-export-plugin
2 | theme:
3 | name: cinder
4 | use_directory_urls: false
5 | plugins:
6 | - search
7 | - pdf-export:
8 | combined: true
9 | theme_handler_path: theme-handler/cinder.py
10 |
11 | nav:
12 | - Intro: index.md
13 | - Options: options.md
14 |
15 | repo_url: https://github.com/zhaoterryy/mkdocs-pdf-export-plugin
16 | edit_uri: edit/master/docs/
17 | remote_name: git@github.com:zhaoterryy/mkdocs-pdf-export-plugin.git
--------------------------------------------------------------------------------
/docs/theme-handler/cinder.py:
--------------------------------------------------------------------------------
1 | from bs4 import BeautifulSoup
2 |
3 | def get_stylesheet() -> str:
4 | return """
5 | body > .container {
6 | margin-top: -100px;
7 | }
8 |
9 | @page {
10 | @bottom-left {
11 | content: counter(__pgnum__);
12 | }
13 | size: letter;
14 | }
15 |
16 | @media print {
17 | .noprint {
18 | display: none;
19 | }
20 | }
21 | """
22 |
23 | def modify_html(html: str, href: str) -> str:
24 | soup = BeautifulSoup(html, 'html.parser')
25 | sm_wrapper = soup.new_tag('small')
26 |
27 | a = soup.new_tag('a', href=href, title='PDF Export', download=None)
28 | a['class'] = 'pdf-download'
29 | a.string = 'Download PDF'
30 |
31 | sm_wrapper.append(a)
32 | soup.body.footer.insert(0, sm_wrapper)
33 |
34 | return str(soup)
--------------------------------------------------------------------------------
/mkdocs_pdf_export_plugin/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaoterryy/mkdocs-pdf-export-plugin/5d3d96cabe40f94b837060b7757a3790dfdde854/mkdocs_pdf_export_plugin/__init__.py
--------------------------------------------------------------------------------
/mkdocs_pdf_export_plugin/plugin.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | from timeit import default_timer as timer
4 |
5 | from mkdocs.config import config_options
6 | from mkdocs.plugins import BasePlugin
7 |
8 | class PdfExportPlugin(BasePlugin):
9 |
10 | DEFAULT_MEDIA_TYPE = 'print'
11 |
12 | config_scheme = (
13 | ('media_type', config_options.Type(str, default=DEFAULT_MEDIA_TYPE)),
14 | ('verbose', config_options.Type(bool, default=False)),
15 | ('enabled_if_env', config_options.Type(str)),
16 | ('combined', config_options.Type(bool, default=False)),
17 | ('combined_output_path', config_options.Type(str, default="pdf/combined.pdf")),
18 | ('theme_handler_path', config_options.Type(str))
19 | )
20 |
21 | def __init__(self):
22 | self.renderer = None
23 | self.enabled = True
24 | self.combined = False
25 | self.num_files = 0
26 | self.num_errors = 0
27 | self.total_time = 0
28 |
29 | def on_config(self, config):
30 | if 'enabled_if_env' in self.config:
31 | env_name = self.config['enabled_if_env']
32 | if env_name:
33 | self.enabled = os.environ.get(env_name) == '1'
34 | if not self.enabled:
35 | print('PDF export is disabled (set environment variable {} to 1 to enable)'.format(env_name))
36 | return
37 |
38 | self.combined = self.config['combined']
39 | if self.combined:
40 | print('Combined PDF export is enabled')
41 |
42 | from weasyprint.logger import LOGGER
43 | import logging
44 |
45 | if self.config['verbose']:
46 | LOGGER.setLevel(logging.DEBUG)
47 | else:
48 | LOGGER.setLevel(logging.ERROR)
49 |
50 | handler = logging.StreamHandler()
51 | handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
52 | LOGGER.addHandler(handler)
53 | return config
54 |
55 | def on_nav(self, nav, config, files):
56 | if not self.enabled:
57 | return nav
58 |
59 | from .renderer import Renderer
60 | self.renderer = Renderer(self.combined, config['theme'].name, self.config['theme_handler_path'])
61 |
62 | self.renderer.pages = [None] * len(nav.pages)
63 | for page in nav.pages:
64 | self.renderer.page_order.append(page.file.url)
65 |
66 | return nav
67 |
68 | def on_post_page(self, output_content, page, config):
69 | if not self.enabled:
70 | return output_content
71 |
72 | start = timer()
73 |
74 | self.num_files += 1
75 |
76 | try:
77 | abs_dest_path = page.file.abs_dest_path
78 | src_path = page.file.src_path
79 | except AttributeError:
80 | # Support for mkdocs <1.0
81 | abs_dest_path = page.abs_output_path
82 | src_path = page.input_path
83 |
84 | path = os.path.dirname(abs_dest_path)
85 | os.makedirs(path, exist_ok=True)
86 |
87 | filename = os.path.splitext(os.path.basename(src_path))[0]
88 |
89 | from weasyprint import urls
90 | base_url = urls.path2url(os.path.join(path, filename))
91 | pdf_file = filename + '.pdf'
92 |
93 | try:
94 | if self.combined:
95 | self.renderer.add_doc(output_content, base_url, page.file.url)
96 | pdf_path = self.get_path_to_pdf_from(page.file.dest_path)
97 | output_content = self.renderer.add_link(output_content, pdf_path)
98 | else:
99 | self.renderer.write_pdf(output_content, base_url, os.path.join(path, pdf_file))
100 | output_content = self.renderer.add_link(output_content, pdf_file)
101 | except Exception as e:
102 | print('Error converting {} to PDF: {}'.format(src_path, e), file=sys.stderr)
103 | self.num_errors += 1
104 |
105 | end = timer()
106 | self.total_time += (end - start)
107 |
108 | return output_content
109 |
110 | def on_post_build(self, config):
111 | if not self.enabled:
112 | return
113 |
114 | if self.combined:
115 | start = timer()
116 |
117 | abs_pdf_path = os.path.join(config['site_dir'], self.config['combined_output_path'])
118 | os.makedirs(os.path.dirname(abs_pdf_path), exist_ok=True)
119 | self.renderer.write_combined_pdf(abs_pdf_path)
120 |
121 | end = timer()
122 | self.total_time += (end - start)
123 |
124 | print('Converting {} files to PDF took {:.1f}s'.format(self.num_files, self.total_time))
125 | if self.num_errors > 0:
126 | print('{} conversion errors occurred (see above)'.format(self.num_errors))
127 |
128 | def get_path_to_pdf_from(self, start):
129 | pdf_split = os.path.split(self.config['combined_output_path'])
130 | start_dir = os.path.split(start)[0]
131 | pdf_dir = pdf_split[0] if pdf_split[0] else '.'
132 | return os.path.join(os.path.relpath(pdf_dir, start_dir), pdf_split[1])
133 |
--------------------------------------------------------------------------------
/mkdocs_pdf_export_plugin/preprocessor/__init__.py:
--------------------------------------------------------------------------------
1 | from .prep import get_combined, get_separate
--------------------------------------------------------------------------------
/mkdocs_pdf_export_plugin/preprocessor/links/__init__.py:
--------------------------------------------------------------------------------
1 | from .transform import transform_href, transform_id
2 | from .util import get_body_id, replace_asset_hrefs, rel_pdf_href
--------------------------------------------------------------------------------
/mkdocs_pdf_export_plugin/preprocessor/links/transform.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from .util import is_doc, normalize_href
4 |
5 | # normalize href to #foo/bar/section:id
6 | def transform_href(href: str, rel_url: str):
7 | head, tail = os.path.split(href)
8 |
9 | num_hashtags = tail.count('#')
10 |
11 | if tail.startswith('#'):
12 | head, section = os.path.split(rel_url)
13 | section = os.path.splitext(section)[0]
14 | id = tail[1:]
15 | elif num_hashtags is 1:
16 | section, ext = tuple(os.path.splitext(tail))
17 | id = str.split(ext, '#')[1]
18 |
19 | if head == '..':
20 | href = normalize_href(href, rel_url)
21 | return '#{}:{}'.format(href, id)
22 |
23 | elif num_hashtags is 0:
24 | if not is_doc(href):
25 | return href
26 |
27 | href = normalize_href(href, rel_url)
28 | return '#{}:'.format(href)
29 |
30 | if head != '':
31 | head += '/'
32 |
33 | return '#{}{}:{}'.format(head, section, id)
34 |
35 | # normalize id to foo/bar/section:id
36 | def transform_id(id: str, rel_url: str):
37 | head, tail = os.path.split(rel_url)
38 | section, _ = os.path.splitext(tail)
39 |
40 | if len(head) > 0:
41 | head += '/'
42 |
43 | return '{}{}:{}'.format(head, section, id)
--------------------------------------------------------------------------------
/mkdocs_pdf_export_plugin/preprocessor/links/util.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from weasyprint import urls
4 | from bs4 import BeautifulSoup
5 |
6 | # check if href is relative --
7 | # if it is relative it *should* be an html that generates a PDF doc
8 | def is_doc(href: str):
9 | tail = os.path.basename(href)
10 | _, ext = os.path.splitext(tail)
11 |
12 | absurl = urls.url_is_absolute(href)
13 | abspath = os.path.isabs(href)
14 | htmlfile = ext.startswith('.html')
15 | if absurl or abspath or not htmlfile:
16 | return False
17 |
18 | return True
19 |
20 | def rel_pdf_href(href: str):
21 | head, tail = os.path.split(href)
22 | filename, _ = os.path.splitext(tail)
23 |
24 | internal = href.startswith('#')
25 | if not is_doc(href) or internal:
26 | return href
27 |
28 | return urls.iri_to_uri(os.path.join(head, filename + '.pdf'))
29 |
30 | def abs_asset_href(href: str, base_url: str):
31 | if urls.url_is_absolute(href) or os.path.isabs(href):
32 | return href
33 |
34 | return urls.iri_to_uri(urls.urljoin(base_url, href))
35 |
36 | # makes all relative asset links absolute
37 | def replace_asset_hrefs(soup: BeautifulSoup, base_url: str):
38 | for link in soup.find_all('link', href=True):
39 | link['href'] = abs_asset_href(link['href'], base_url)
40 |
41 | for asset in soup.find_all(src=True):
42 | asset['src'] = abs_asset_href(asset['src'], base_url)
43 |
44 | return soup
45 |
46 | # normalize href to site root
47 | def normalize_href(href: str, rel_url: str):
48 | # foo/bar/baz/../../index.html -> foo/index.html
49 | def reduce_rel(x):
50 | try:
51 | i = x.index('..')
52 | if i is 0:
53 | return x
54 |
55 | del x[i]
56 | del x[i - 1]
57 | return reduce_rel(x)
58 | except ValueError:
59 | return x
60 |
61 | rel_dir = os.path.dirname(rel_url)
62 | href = str.split(os.path.join(rel_dir, href), '/')
63 | href = reduce_rel(href)
64 | href[-1], _ = os.path.splitext(href[-1])
65 |
66 | return os.path.join(*href)
67 |
68 | def get_body_id(url: str):
69 | section, _ = os.path.splitext(url)
70 | return '{}:'.format(section)
--------------------------------------------------------------------------------
/mkdocs_pdf_export_plugin/preprocessor/prep.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from .links import transform_href, transform_id, get_body_id, replace_asset_hrefs, rel_pdf_href
4 |
5 | from weasyprint import urls
6 | from bs4 import BeautifulSoup
7 |
8 | def get_combined(soup: BeautifulSoup, base_url: str, rel_url: str):
9 | for id in soup.find_all(id=True):
10 | id['id'] = transform_id(id['id'], rel_url)
11 |
12 | for a in soup.find_all('a', href=True):
13 | if urls.url_is_absolute(a['href']) or os.path.isabs(a['href']):
14 | continue
15 |
16 | a['href'] = transform_href(a['href'], rel_url)
17 |
18 | soup.body['id'] = get_body_id(rel_url)
19 | soup = replace_asset_hrefs(soup, base_url)
20 | return soup
21 |
22 | def get_separate(soup: BeautifulSoup, base_url: str):
23 | # transforms all relative hrefs pointing to other html docs
24 | # into relative pdf hrefs
25 | for a in soup.find_all('a', href=True):
26 | a['href'] = rel_pdf_href(a['href'])
27 |
28 | soup = replace_asset_hrefs(soup, base_url)
29 | return soup
--------------------------------------------------------------------------------
/mkdocs_pdf_export_plugin/renderer.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import os
3 |
4 | from importlib import import_module
5 | from importlib.util import spec_from_file_location, module_from_spec
6 | from weasyprint import HTML
7 | from bs4 import BeautifulSoup
8 |
9 | from .themes import generic as generic_theme
10 | from .preprocessor import get_separate as prep_separate, get_combined as prep_combined
11 |
12 | class Renderer(object):
13 | def __init__(self, combined: bool, theme: str, theme_handler_path: str=None):
14 | self.theme = self._load_theme_handler(theme, theme_handler_path)
15 | self.combined = combined
16 | self.page_order = []
17 | self.pgnum = 0
18 | self.pages = []
19 |
20 | def write_pdf(self, content: str, base_url: str, filename: str):
21 | self.render_doc(content, base_url).write_pdf(filename)
22 |
23 | def render_doc(self, content: str, base_url: str, rel_url: str = None):
24 | soup = BeautifulSoup(content, 'html.parser')
25 |
26 | self.inject_pgnum(soup)
27 |
28 | stylesheet = self.theme.get_stylesheet()
29 | if stylesheet:
30 | style_tag = soup.new_tag('style')
31 | style_tag.string = stylesheet
32 |
33 | soup.head.append(style_tag)
34 |
35 |
36 | if self.combined:
37 | soup = prep_combined(soup, base_url, rel_url)
38 | else:
39 | soup = prep_separate(soup, base_url)
40 |
41 | html = HTML(string=str(soup))
42 | return html.render()
43 |
44 | def add_doc(self, content: str, base_url: str, rel_url: str):
45 | pos = self.page_order.index(rel_url)
46 | self.pages[pos] = (content, base_url, rel_url)
47 |
48 | def write_combined_pdf(self, output_path: str):
49 | rendered_pages = []
50 | for p in self.pages:
51 | if p is None:
52 | print('Unexpected error - not all pages were rendered properly')
53 | continue
54 |
55 | render = self.render_doc(p[0], p[1], p[2])
56 | self.pgnum += len(render.pages)
57 | rendered_pages.append(render)
58 |
59 | flatten = lambda l: [item for sublist in l for item in sublist]
60 | all_pages = flatten([p.pages for p in rendered_pages if p != None])
61 |
62 | rendered_pages[0].copy(all_pages).write_pdf(output_path)
63 |
64 | def add_link(self, content: str, filename: str):
65 | return self.theme.modify_html(content, filename)
66 |
67 | def inject_pgnum(self, soup):
68 | pgnum_counter = soup.new_tag('style')
69 | pgnum_counter.string = '''
70 | @page :first {{
71 | counter-reset: __pgnum__ {};
72 | }}
73 | @page {{
74 | counter-increment: __pgnum__;
75 | }}
76 | '''.format(self.pgnum)
77 |
78 | soup.head.append(pgnum_counter)
79 |
80 | @staticmethod
81 | def _load_theme_handler(theme: str, custom_handler_path: str = None):
82 | module_name = '.' + (theme or 'generic').replace('-', '_')
83 |
84 | if custom_handler_path:
85 | try:
86 | spec = spec_from_file_location(module_name, os.path.join(os.getcwd(), custom_handler_path))
87 | mod = module_from_spec(spec)
88 | spec.loader.exec_module(mod)
89 | return mod
90 | except FileNotFoundError as e:
91 | print('Could not load theme handler {} from custom directory "{}": {}'.format(theme, custom_handler_path, e), file=sys.stderr)
92 | pass
93 |
94 | try:
95 | return import_module(module_name, 'mkdocs_pdf_export_plugin.themes')
96 | except ImportError as e:
97 | print('Could not load theme handler {}: {}'.format(theme, e), file=sys.stderr)
98 | return generic_theme
--------------------------------------------------------------------------------
/mkdocs_pdf_export_plugin/themes/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaoterryy/mkdocs-pdf-export-plugin/5d3d96cabe40f94b837060b7757a3790dfdde854/mkdocs_pdf_export_plugin/themes/__init__.py
--------------------------------------------------------------------------------
/mkdocs_pdf_export_plugin/themes/cinder.py:
--------------------------------------------------------------------------------
1 | from bs4 import BeautifulSoup
2 |
3 | def get_stylesheet() -> str:
4 | return None
5 |
6 |
7 | def modify_html(html: str, href: str) -> str:
8 | soup = BeautifulSoup(html, 'html.parser')
9 |
10 | sm_wrapper = soup.new_tag('small')
11 |
12 | a = soup.new_tag('a', href=href, title='PDF Export', download=None)
13 | a['class'] = 'pdf-download'
14 | a.string = 'Download PDF'
15 |
16 | sm_wrapper.append(a)
17 | soup.body.footer.insert(0, sm_wrapper)
18 |
19 | return str(soup)
20 |
--------------------------------------------------------------------------------
/mkdocs_pdf_export_plugin/themes/generic.py:
--------------------------------------------------------------------------------
1 | from bs4 import BeautifulSoup
2 |
3 |
4 | def get_stylesheet() -> str:
5 | return None
6 |
7 |
8 | def modify_html(html: str, href: str) -> str:
9 | soup = BeautifulSoup(html, 'html.parser')
10 |
11 | if soup.head:
12 | link = soup.new_tag('link', href=href, rel='alternate', title='PDF Export', type='application/pdf')
13 | soup.head.append(link)
14 |
15 | return str(soup)
16 |
--------------------------------------------------------------------------------
/mkdocs_pdf_export_plugin/themes/material.py:
--------------------------------------------------------------------------------
1 | from bs4 import BeautifulSoup
2 |
3 |
4 | def get_stylesheet() -> str:
5 | return """
6 | h1, h2, h3 {
7 | string-set: chapter content();
8 | }
9 |
10 | .md-container {
11 | display: block;
12 | padding-top: 0;
13 | }
14 |
15 | .md-main {
16 | display: block;
17 | height: inherit;
18 | }
19 |
20 | .md-main__inner {
21 | height: inherit;
22 | padding-top: 0;
23 | }
24 |
25 | .md-typeset .codehilitetable .linenos {
26 | display: none;
27 | }
28 |
29 | .md-typeset .footnote-ref {
30 | display: inline-block;
31 | }
32 |
33 | .md-typeset a.footnote-backref {
34 | transform: translateX(0);
35 | opacity: 1;
36 | }
37 |
38 | .md-typeset .admonition {
39 | display: block;
40 | border-top: .1rem solid rgba(0,0,0,.07);
41 | border-right: .1rem solid rgba(0,0,0,.07);
42 | border-bottom: .1rem solid rgba(0,0,0,.07);
43 | page-break-inside: avoid;
44 | }
45 |
46 | .md-typeset a::after {
47 | color: inherit;
48 | content: none;
49 | }
50 |
51 | .md-typeset table:not([class]) th {
52 | min-width: 0;
53 | }
54 |
55 | .md-typeset table {
56 | border: .1rem solid rgba(0,0,0,.07);
57 | }
58 | """
59 |
60 |
61 | def modify_html(html: str, href: str) -> str:
62 |
63 | # SVG 'file-download' size 2x from fontawesome: https://fontawesome.com/icons/file-download?style=solid
64 | # resized to 1.2rem width * height
65 | a_tag = "" % href
66 | icon = ''
67 | button_tag = a_tag + icon + ""
68 |
69 | # insert into HTML
70 | insert_point = ""
71 | html = html.replace(insert_point, insert_point + button_tag)
72 |
73 | return html
74 |
75 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | beautifulsoup4==4.6.3
2 | cairocffi==0.9.0
3 | CairoSVG==2.1.3
4 | cffi==1.11.5
5 | click==6.7
6 | cssselect2==0.2.1
7 | defusedxml==0.5.0
8 | html5lib==1.0.1
9 | Jinja2==2.11.2
10 | livereload==2.5.2
11 | Markdown==2.6.11
12 | MarkupSafe==1.1.1
13 | mkdocs==1.0.3
14 | pdfrw==0.4
15 | Pillow==6.2.2
16 | pycparser==2.18
17 | Pyphen==0.9.5
18 | PyYAML>=4.2b1
19 | six==1.11.0
20 | tinycss2==0.6.1
21 | tornado==5.1
22 | WeasyPrint==44
23 | webencodings==0.5.1
24 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 |
4 | setup(
5 | name='mkdocs-pdf-export-plugin',
6 | version='0.5.10',
7 | description='An MkDocs plugin to export content pages as PDF files',
8 | long_description='The pdf-export plugin will export all markdown pages in your MkDocs repository as PDF files'
9 | 'using WeasyPrint. The exported documents support many advanced features missing in most other'
10 | 'PDF exports, such as a PDF Index and support for CSS paged media module.',
11 | keywords='mkdocs pdf export weasyprint',
12 | url='https://github.com/zhaoterryy/mkdocs-pdf-export-plugin/',
13 | author='Terry Zhao',
14 | author_email='zhao.terryy@gmail.com',
15 | license='MIT',
16 | python_requires='>=3.5',
17 | install_requires=[
18 | 'mkdocs>=0.17',
19 | 'weasyprint>=0.44',
20 | 'beautifulsoup4>=4.6.3'
21 | ],
22 | classifiers=[
23 | 'Development Status :: 4 - Beta',
24 | 'Intended Audience :: Developers',
25 | 'Intended Audience :: Information Technology',
26 | 'License :: OSI Approved :: MIT License',
27 | 'Programming Language :: Python',
28 | 'Programming Language :: Python :: 3 :: Only',
29 | 'Programming Language :: Python :: 3.4',
30 | 'Programming Language :: Python :: 3.5',
31 | 'Programming Language :: Python :: 3.6',
32 | 'Programming Language :: Python :: 3.7'
33 | ],
34 | packages=find_packages(),
35 | entry_points={
36 | 'mkdocs.plugins': [
37 | 'pdf-export = mkdocs_pdf_export_plugin.plugin:PdfExportPlugin'
38 | ]
39 | }
40 | )
41 |
--------------------------------------------------------------------------------