Skip to content

Pre Release v0.9.4 #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jan 28, 2024
Merged
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ This plugin is inspired by [MkDocs PDF Export Plugin][mkdocs-pdf-export-plugin].
## Features

* Cover and Table of Contents integrated in the PDF
* Automatically numbers on heading(h1-h3).
* Automatically numbers on heading (h1-h6).
* Shift down sub-page headings level.
* using [WeasyPrint][weasyprint].

Expand Down Expand Up @@ -181,12 +181,12 @@ plugins:

* `toc_level`

Set the level of _Table of Content_. This value is enabled in the range of from `1` to `3`.
Set the level of _Table of Content_. This value is enabled in the range of from `1` to `6`.
**default**: `3`

* `ordered_chapter_level`

Set the level of heading number addition. This value is enabled in the range of from `1` to `3`.
Set the level of heading number addition. This value is enabled in the range of from `1` to `6`.
**default**: `3`

* `excludes_children`
Expand Down Expand Up @@ -245,6 +245,22 @@ plugins:
> <ANY_SITE_URL(eg. 'https://google.com')>
> ```

* `relaxedjs_path`

Set the value to execute command of relaxed if you're using e.g. '[Mermaid](https://mermaid-js.github.io) diagrams and Headless Chrome is not working for you.
Require "ReLaXed" Javascript PDF renderer to be installed on your system. See: '[ReLaXed](https://github.com/RelaxedJS/ReLaXed)'.

Please use 'theme_handler_path' option to specify custom JS sources and CSS Stylesheets which covers your needs. E.g. for Material
theme it would be **material.py**. See: **mkdocs-with-pdf/mkdocs_with_pdf/themes/material.py** for implementation details.
**default**: `None`
_**since**: `v0.7.0`_

> Install on your system:
> ```
> $ npm i -g relaxedjs
> $ relaxed --version
> ```

##### ... and more

* `output_path`
Expand Down
44 changes: 44 additions & 0 deletions mkdocs_with_pdf/drivers/relaxedjs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import os
from logging import Logger
from shutil import which
from subprocess import PIPE, Popen
from tempfile import TemporaryDirectory


class RelaxedJSRenderer(object):

@classmethod
def setup(self, program_path: str, logger: Logger):
if not program_path:
return None

if not which(program_path):
raise RuntimeError(
'No such `ReLaXed` program or not executable'
+ f': "{program_path}".')

return self(program_path, logger)

def __init__(self, program_path: str, logger: Logger):
self._program_path = program_path
self._logger = logger

def write_pdf(self, html_string: str, output: str):
self._logger.info(' Rendering with `ReLaXed JS`.')

with TemporaryDirectory() as work_dir:
entry_point = os.path.join(work_dir, 'pdf_print.html')
with open(entry_point, 'w+') as f:
f.write(html_string)
f.close()

self._logger.info(f" entry_point: {entry_point}")
with Popen([self._program_path, entry_point, output,
"--build-once"],
stdout=PIPE) as proc:
while True:
log = proc.stdout.readline().decode().strip()
if log:
self._logger.info(f" {log}")
if proc.poll() is not None:
break
19 changes: 14 additions & 5 deletions mkdocs_with_pdf/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,22 +139,31 @@ def add_stylesheet(stylesheet: str):
self._options.two_columns_level,
self._options.logger)
self._normalize_link_anchors(soup)
html_string = self._render_js(soup)

if self._options.relaxed_js:
html_string = str(soup)
else:
html_string = self._render_js(soup)

html_string = self._options.hook.pre_pdf_render(html_string)

if self._options.debug_html:
print(f'{html_string}')

self.logger.info("Rendering for PDF.")
html = HTML(string=html_string)
render = html.render()

abs_pdf_path = os.path.join(config['site_dir'], output_path)
os.makedirs(os.path.dirname(abs_pdf_path), exist_ok=True)

self.logger.info(f'Output a PDF to "{abs_pdf_path}".')
render.write_pdf(abs_pdf_path)

if self._options.relaxed_js:
self._options.relaxed_js.write_pdf(
html_string, abs_pdf_path)
else:
html = HTML(string=html_string)
render = html.render()
render.write_pdf(abs_pdf_path)

# ------------------------
def _remove_empty_tags(self, soup: PageElement):
Expand Down Expand Up @@ -378,7 +387,7 @@ def _render_js(self, soup):
body.append(script)
if len(self._mixed_script) > 0:
tag = soup.new_tag('script')
tag.text = self._mixed_script
tag.append(self._mixed_script)
body.append(tag)
for src in scripts:
body.append(soup.new_tag('script', src=f'file://{src}'))
Expand Down
9 changes: 8 additions & 1 deletion mkdocs_with_pdf/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from .drivers.event_hook import EventHookHandler
from .drivers.headless_chrome import HeadlessChromeDriver
from .drivers.relaxedjs import RelaxedJSRenderer
from .templates.template import Template


Expand Down Expand Up @@ -42,7 +43,9 @@ class Options(object):

('render_js', config_options.Type(bool, default=False)),
('headless_chrome_path',
config_options.Type(str, default='chromium-browser'))
config_options.Type(str, default='chromium-browser')),
('relaxedjs_path',
config_options.Type(str, default=None)),
)

def __init__(self, local_config, config, logger: logging):
Expand All @@ -53,6 +56,7 @@ def __init__(self, local_config, config, logger: logging):
self.show_anchors = local_config['show_anchors']

self.output_path = local_config.get('output_path', None)
self.theme_handler_path = local_config.get('theme_handler_path', None)

# Author and Copyright
self._author = local_config['author']
Expand Down Expand Up @@ -94,6 +98,9 @@ def __init__(self, local_config, config, logger: logging):
self.js_renderer = HeadlessChromeDriver.setup(
local_config['headless_chrome_path'], logger)

self.relaxed_js = RelaxedJSRenderer.setup(
local_config['relaxedjs_path'], logger)

# Theming
self.theme_name = config['theme'].name
self.theme_handler_path = local_config.get('theme_handler_path', None)
Expand Down
2 changes: 1 addition & 1 deletion mkdocs_with_pdf/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def on_config(self, config):
if env_name:
self.enabled = os.environ.get(env_name) == '1'
if not self.enabled:
self._logger.warning(
self._logger.info(
'without generate PDF'
f'(set environment variable {env_name} to 1 to enable)'
)
Expand Down
5 changes: 4 additions & 1 deletion mkdocs_with_pdf/templates/filters/url.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import sys
from urllib.parse import urlparse

from . import _FilterBase
Expand Down Expand Up @@ -29,7 +30,9 @@ def __call__(self, pathname: str) -> str:
continue
path = os.path.abspath(os.path.join(d, pathname))
if os.path.isfile(path):
return 'file://' + path
return 'file:///' + path.replace("\\", "/") \
if sys.platform == 'win32' \
else 'file://' + path

# not found?
return pathname
2 changes: 1 addition & 1 deletion mkdocs_with_pdf/themes/material.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def get_script_sources() -> list:
def inject_link(html: str, href: str) -> str:
soup = BeautifulSoup(html, 'html.parser')

footer = soup.select('.md-footer-copyright')
footer = soup.select('.md-copyright')
if footer and footer[0]:
container = footer[0]

Expand Down
18 changes: 17 additions & 1 deletion mkdocs_with_pdf/themes/material.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
.md-typeset > ul {
margin-left: 0rem !important;
}
.md-typeset ul li,
.md-typeset ol li {
margin-left: 1.5rem !important;
}

@media print {

// admonition icon
.md-typeset :is(.admonition-title,summary):before {
top: 0.6rem;
left: 0.6rem;
}

.md-typeset {
// Tabbed code block container
.tabbed-set {
Expand Down Expand Up @@ -65,7 +80,8 @@
padding-bottom: 0;
}
}
&>ul {
&>ul,
&>ol {
margin-left: 1.5rem;
}

Expand Down
91 changes: 80 additions & 11 deletions mkdocs_with_pdf/toc.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ def make_indexes(soup: PageElement, options: Options) -> None:

# Step 2: generate toc page
level = options.toc_level
if level < 1 or level > 3:
if level < 1 or level > 6:
return

options.logger.info(
f'Generate a table of contents up to heading level {level}.')

h1li = None
h2ul = h2li = h3ul = None
exclude_lv2 = exclude_lv3 = False
h2ul = h2li = h3ul = h4ul = h5ul = h6ul = None
exclude_lv2 = exclude_lv3 = exclude_lv4 = exclude_lv5 = exclude_lv6 = False

def makeLink(h: Tag) -> Tag:
li = soup.new_tag('li')
Expand All @@ -48,14 +48,14 @@ def makeLink(h: Tag) -> Tag:
h1ul = soup.new_tag('ul')
toc.append(h1ul)

headings = soup.find_all(['h1', 'h2', 'h3'])
headings = soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
for h in headings:

if h.name == 'h1':

h1li = makeLink(h)
h1ul.append(h1li)
h2ul = h2li = h3ul = None
h2ul = h2li = h3ul = h4ul = h5ul = h6ul = None

exclude_lv2 = _is_exclude(h.get('id', None), options)

Expand All @@ -80,6 +80,48 @@ def makeLink(h: Tag) -> Tag:
h2li.append(h3ul)
h3li = makeLink(h)
h3ul.append(h3li)
h4ul = None

exclude_lv4 = _is_exclude(h.get('id', None), options)

elif not exclude_lv3 and not exclude_lv4 \
and h.name == 'h4' and level >= 4:

if not h3li:
continue
if not h4ul:
h4ul = soup.new_tag('ul')
h3li.append(h4ul)
h4li = makeLink(h)
h4ul.append(h4li)
h5ul = None

exclude_lv5 = _is_exclude(h.get('id', None), options)

elif not exclude_lv4 and not exclude_lv5 \
and h.name == 'h5' and level >= 5:

if not h4li:
continue
if not h5ul:
h5ul = soup.new_tag('ul')
h4li.append(h5ul)
h5li = makeLink(h)
h5ul.append(h5li)
h6ul = None

exclude_lv6 = _is_exclude(h.get('id', None), options)

elif not exclude_lv5 and not exclude_lv6 \
and h.name == 'h6' and level >= 6:

if not h5li:
continue
if not h6ul:
h6ul = soup.new_tag('ul')
h5li.append(h6ul)
h6li = makeLink(h)
h6ul.append(h6li)

else:
continue
Expand All @@ -91,29 +133,29 @@ def makeLink(h: Tag) -> Tag:
def _inject_heading_order(soup: Tag, options: Options):

level = options.ordered_chapter_level
if level < 1 or level > 3:
if level < 1 or level > 6:
return

options.logger.info(f'Number headings up to level {level}.')

h1n = h2n = h3n = 0
exclude_lv2 = exclude_lv3 = False
h1n = h2n = h3n = h4n = h5n = h6n = 0
exclude_lv2 = exclude_lv3 = exclude_lv4 = exclude_lv5 = exclude_lv6 = False

headings = soup.find_all(['h1', 'h2', 'h3'])
headings = soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
for h in headings:

if h.name == 'h1':

h1n += 1
h2n = h3n = 0
h2n = h3n = h4n = h5n = h6n = 0
prefix = f'{h1n}. '

exclude_lv2 = _is_exclude(h.get('id', None), options)

elif not exclude_lv2 and h.name == 'h2' and level >= 2:

h2n += 1
h3n = 0
h3n = h4n = h5n = h6n = 0
prefix = f'{h1n}.{h2n} '

exclude_lv3 = _is_exclude(h.get('id', None), options)
Expand All @@ -122,8 +164,35 @@ def _inject_heading_order(soup: Tag, options: Options):
and h.name == 'h3' and level >= 3:

h3n += 1
h4n = h5n = h6n = 0
prefix = f'{h1n}.{h2n}.{h3n} '

exclude_lv4 = _is_exclude(h.get('id', None), options)

elif not exclude_lv3 and not exclude_lv4 \
and h.name == 'h4' and level >= 4:

h4n += 1
h5n = h6n = 0
prefix = f'{h1n}.{h2n}.{h3n}.{h4n} '

exclude_lv5 = _is_exclude(h.get('id', None), options)

elif not exclude_lv4 and not exclude_lv5 \
and h.name == 'h5' and level >= 5:

h6n = 0
h5n += 1
prefix = f'{h1n}.{h2n}.{h3n}.{h4n}.{h5n} '

exclude_lv6 = _is_exclude(h.get('id', None), options)

elif not exclude_lv5 and not exclude_lv6 \
and h.name == 'h6' and level >= 6:

h6n += 1
prefix = f'{h1n}.{h2n}.{h3n}.{h4n}.{h5n}.{h6n} '

else:
continue

Expand Down