Skip to content

Commit e26766d

Browse files
authored
Merge pull request #1 from domWalters/pre-release-v0.9.4
Pre Release v0.9.4
2 parents ccf20e5 + 01e1707 commit e26766d

File tree

9 files changed

+188
-24
lines changed

9 files changed

+188
-24
lines changed

README.md

+19-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ This plugin is inspired by [MkDocs PDF Export Plugin][mkdocs-pdf-export-plugin].
1111
## Features
1212

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

@@ -181,12 +181,12 @@ plugins:
181181
182182
* `toc_level`
183183
184-
Set the level of _Table of Content_. This value is enabled in the range of from `1` to `3`.
184+
Set the level of _Table of Content_. This value is enabled in the range of from `1` to `6`.
185185
**default**: `3`
186186
187187
* `ordered_chapter_level`
188188
189-
Set the level of heading number addition. This value is enabled in the range of from `1` to `3`.
189+
Set the level of heading number addition. This value is enabled in the range of from `1` to `6`.
190190
**default**: `3`
191191
192192
* `excludes_children`
@@ -245,6 +245,22 @@ plugins:
245245
> <ANY_SITE_URL(eg. 'https://google.com')>
246246
> ```
247247
248+
* `relaxedjs_path`
249+
250+
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.
251+
Require "ReLaXed" Javascript PDF renderer to be installed on your system. See: '[ReLaXed](https://github.com/RelaxedJS/ReLaXed)'.
252+
253+
Please use 'theme_handler_path' option to specify custom JS sources and CSS Stylesheets which covers your needs. E.g. for Material
254+
theme it would be **material.py**. See: **mkdocs-with-pdf/mkdocs_with_pdf/themes/material.py** for implementation details.
255+
**default**: `None`
256+
_**since**: `v0.7.0`_
257+
258+
> Install on your system:
259+
> ```
260+
> $ npm i -g relaxedjs
261+
> $ relaxed --version
262+
> ```
263+
248264
##### ... and more
249265
250266
* `output_path`

mkdocs_with_pdf/drivers/relaxedjs.py

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import os
2+
from logging import Logger
3+
from shutil import which
4+
from subprocess import PIPE, Popen
5+
from tempfile import TemporaryDirectory
6+
7+
8+
class RelaxedJSRenderer(object):
9+
10+
@classmethod
11+
def setup(self, program_path: str, logger: Logger):
12+
if not program_path:
13+
return None
14+
15+
if not which(program_path):
16+
raise RuntimeError(
17+
'No such `ReLaXed` program or not executable'
18+
+ f': "{program_path}".')
19+
20+
return self(program_path, logger)
21+
22+
def __init__(self, program_path: str, logger: Logger):
23+
self._program_path = program_path
24+
self._logger = logger
25+
26+
def write_pdf(self, html_string: str, output: str):
27+
self._logger.info(' Rendering with `ReLaXed JS`.')
28+
29+
with TemporaryDirectory() as work_dir:
30+
entry_point = os.path.join(work_dir, 'pdf_print.html')
31+
with open(entry_point, 'w+') as f:
32+
f.write(html_string)
33+
f.close()
34+
35+
self._logger.info(f" entry_point: {entry_point}")
36+
with Popen([self._program_path, entry_point, output,
37+
"--build-once"],
38+
stdout=PIPE) as proc:
39+
while True:
40+
log = proc.stdout.readline().decode().strip()
41+
if log:
42+
self._logger.info(f" {log}")
43+
if proc.poll() is not None:
44+
break

mkdocs_with_pdf/generator.py

+14-5
Original file line numberDiff line numberDiff line change
@@ -139,22 +139,31 @@ def add_stylesheet(stylesheet: str):
139139
self._options.two_columns_level,
140140
self._options.logger)
141141
self._normalize_link_anchors(soup)
142-
html_string = self._render_js(soup)
142+
143+
if self._options.relaxed_js:
144+
html_string = str(soup)
145+
else:
146+
html_string = self._render_js(soup)
143147

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

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

149153
self.logger.info("Rendering for PDF.")
150-
html = HTML(string=html_string)
151-
render = html.render()
152154

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

156158
self.logger.info(f'Output a PDF to "{abs_pdf_path}".')
157-
render.write_pdf(abs_pdf_path)
159+
160+
if self._options.relaxed_js:
161+
self._options.relaxed_js.write_pdf(
162+
html_string, abs_pdf_path)
163+
else:
164+
html = HTML(string=html_string)
165+
render = html.render()
166+
render.write_pdf(abs_pdf_path)
158167

159168
# ------------------------
160169
def _remove_empty_tags(self, soup: PageElement):
@@ -378,7 +387,7 @@ def _render_js(self, soup):
378387
body.append(script)
379388
if len(self._mixed_script) > 0:
380389
tag = soup.new_tag('script')
381-
tag.text = self._mixed_script
390+
tag.append(self._mixed_script)
382391
body.append(tag)
383392
for src in scripts:
384393
body.append(soup.new_tag('script', src=f'file://{src}'))

mkdocs_with_pdf/options.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from .drivers.event_hook import EventHookHandler
66
from .drivers.headless_chrome import HeadlessChromeDriver
7+
from .drivers.relaxedjs import RelaxedJSRenderer
78
from .templates.template import Template
89

910

@@ -42,7 +43,9 @@ class Options(object):
4243

4344
('render_js', config_options.Type(bool, default=False)),
4445
('headless_chrome_path',
45-
config_options.Type(str, default='chromium-browser'))
46+
config_options.Type(str, default='chromium-browser')),
47+
('relaxedjs_path',
48+
config_options.Type(str, default=None)),
4649
)
4750

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

5558
self.output_path = local_config.get('output_path', None)
59+
self.theme_handler_path = local_config.get('theme_handler_path', None)
5660

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

101+
self.relaxed_js = RelaxedJSRenderer.setup(
102+
local_config['relaxedjs_path'], logger)
103+
97104
# Theming
98105
self.theme_name = config['theme'].name
99106
self.theme_handler_path = local_config.get('theme_handler_path', None)

mkdocs_with_pdf/plugin.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def on_config(self, config):
6969
if env_name:
7070
self.enabled = os.environ.get(env_name) == '1'
7171
if not self.enabled:
72-
self._logger.warning(
72+
self._logger.info(
7373
'without generate PDF'
7474
f'(set environment variable {env_name} to 1 to enable)'
7575
)

mkdocs_with_pdf/templates/filters/url.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import sys
23
from urllib.parse import urlparse
34

45
from . import _FilterBase
@@ -29,7 +30,9 @@ def __call__(self, pathname: str) -> str:
2930
continue
3031
path = os.path.abspath(os.path.join(d, pathname))
3132
if os.path.isfile(path):
32-
return 'file://' + path
33+
return 'file:///' + path.replace("\\", "/") \
34+
if sys.platform == 'win32' \
35+
else 'file://' + path
3336

3437
# not found?
3538
return pathname

mkdocs_with_pdf/themes/material.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def get_script_sources() -> list:
2222
def inject_link(html: str, href: str) -> str:
2323
soup = BeautifulSoup(html, 'html.parser')
2424

25-
footer = soup.select('.md-footer-copyright')
25+
footer = soup.select('.md-copyright')
2626
if footer and footer[0]:
2727
container = footer[0]
2828

mkdocs_with_pdf/themes/material.scss

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,19 @@
1+
.md-typeset > ul {
2+
margin-left: 0rem !important;
3+
}
4+
.md-typeset ul li,
5+
.md-typeset ol li {
6+
margin-left: 1.5rem !important;
7+
}
8+
19
@media print {
10+
11+
// admonition icon
12+
.md-typeset :is(.admonition-title,summary):before {
13+
top: 0.6rem;
14+
left: 0.6rem;
15+
}
16+
217
.md-typeset {
318
// Tabbed code block container
419
.tabbed-set {
@@ -65,7 +80,8 @@
6580
padding-bottom: 0;
6681
}
6782
}
68-
&>ul {
83+
&>ul,
84+
&>ol {
6985
margin-left: 1.5rem;
7086
}
7187

mkdocs_with_pdf/toc.py

+80-11
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@ def make_indexes(soup: PageElement, options: Options) -> None:
1717

1818
# Step 2: generate toc page
1919
level = options.toc_level
20-
if level < 1 or level > 3:
20+
if level < 1 or level > 6:
2121
return
2222

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

2626
h1li = None
27-
h2ul = h2li = h3ul = None
28-
exclude_lv2 = exclude_lv3 = False
27+
h2ul = h2li = h3ul = h4ul = h5ul = h6ul = None
28+
exclude_lv2 = exclude_lv3 = exclude_lv4 = exclude_lv5 = exclude_lv6 = False
2929

3030
def makeLink(h: Tag) -> Tag:
3131
li = soup.new_tag('li')
@@ -48,14 +48,14 @@ def makeLink(h: Tag) -> Tag:
4848
h1ul = soup.new_tag('ul')
4949
toc.append(h1ul)
5050

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

5454
if h.name == 'h1':
5555

5656
h1li = makeLink(h)
5757
h1ul.append(h1li)
58-
h2ul = h2li = h3ul = None
58+
h2ul = h2li = h3ul = h4ul = h5ul = h6ul = None
5959

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

@@ -80,6 +80,48 @@ def makeLink(h: Tag) -> Tag:
8080
h2li.append(h3ul)
8181
h3li = makeLink(h)
8282
h3ul.append(h3li)
83+
h4ul = None
84+
85+
exclude_lv4 = _is_exclude(h.get('id', None), options)
86+
87+
elif not exclude_lv3 and not exclude_lv4 \
88+
and h.name == 'h4' and level >= 4:
89+
90+
if not h3li:
91+
continue
92+
if not h4ul:
93+
h4ul = soup.new_tag('ul')
94+
h3li.append(h4ul)
95+
h4li = makeLink(h)
96+
h4ul.append(h4li)
97+
h5ul = None
98+
99+
exclude_lv5 = _is_exclude(h.get('id', None), options)
100+
101+
elif not exclude_lv4 and not exclude_lv5 \
102+
and h.name == 'h5' and level >= 5:
103+
104+
if not h4li:
105+
continue
106+
if not h5ul:
107+
h5ul = soup.new_tag('ul')
108+
h4li.append(h5ul)
109+
h5li = makeLink(h)
110+
h5ul.append(h5li)
111+
h6ul = None
112+
113+
exclude_lv6 = _is_exclude(h.get('id', None), options)
114+
115+
elif not exclude_lv5 and not exclude_lv6 \
116+
and h.name == 'h6' and level >= 6:
117+
118+
if not h5li:
119+
continue
120+
if not h6ul:
121+
h6ul = soup.new_tag('ul')
122+
h5li.append(h6ul)
123+
h6li = makeLink(h)
124+
h6ul.append(h6li)
83125

84126
else:
85127
continue
@@ -91,29 +133,29 @@ def makeLink(h: Tag) -> Tag:
91133
def _inject_heading_order(soup: Tag, options: Options):
92134

93135
level = options.ordered_chapter_level
94-
if level < 1 or level > 3:
136+
if level < 1 or level > 6:
95137
return
96138

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

99-
h1n = h2n = h3n = 0
100-
exclude_lv2 = exclude_lv3 = False
141+
h1n = h2n = h3n = h4n = h5n = h6n = 0
142+
exclude_lv2 = exclude_lv3 = exclude_lv4 = exclude_lv5 = exclude_lv6 = False
101143

102-
headings = soup.find_all(['h1', 'h2', 'h3'])
144+
headings = soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
103145
for h in headings:
104146

105147
if h.name == 'h1':
106148

107149
h1n += 1
108-
h2n = h3n = 0
150+
h2n = h3n = h4n = h5n = h6n = 0
109151
prefix = f'{h1n}. '
110152

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

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

115157
h2n += 1
116-
h3n = 0
158+
h3n = h4n = h5n = h6n = 0
117159
prefix = f'{h1n}.{h2n} '
118160

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

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

170+
exclude_lv4 = _is_exclude(h.get('id', None), options)
171+
172+
elif not exclude_lv3 and not exclude_lv4 \
173+
and h.name == 'h4' and level >= 4:
174+
175+
h4n += 1
176+
h5n = h6n = 0
177+
prefix = f'{h1n}.{h2n}.{h3n}.{h4n} '
178+
179+
exclude_lv5 = _is_exclude(h.get('id', None), options)
180+
181+
elif not exclude_lv4 and not exclude_lv5 \
182+
and h.name == 'h5' and level >= 5:
183+
184+
h6n = 0
185+
h5n += 1
186+
prefix = f'{h1n}.{h2n}.{h3n}.{h4n}.{h5n} '
187+
188+
exclude_lv6 = _is_exclude(h.get('id', None), options)
189+
190+
elif not exclude_lv5 and not exclude_lv6 \
191+
and h.name == 'h6' and level >= 6:
192+
193+
h6n += 1
194+
prefix = f'{h1n}.{h2n}.{h3n}.{h4n}.{h5n}.{h6n} '
195+
127196
else:
128197
continue
129198

0 commit comments

Comments
 (0)