Skip to content

Commit 0a5a2c3

Browse files
committed
feat: building JavaScript pages with Python instead of Jekyll
1 parent 6375ea9 commit 0a5a2c3

File tree

10 files changed

+838
-36
lines changed

10 files changed

+838
-36
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
node_modules
22
_site
33

4+
__pycache__
5+
46
build/*
57
!build/plotcss.js
68
!build/README.md

docs/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
_data/plotschema.json
2+
tmp

docs/Makefile

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Manage plotly.js documentation.
22

33
JEKYLL=bundle exec jekyll
4+
RUN = uv run
45
SCHEMA_SRC=../test/plot-schema.json
56
SCHEMA_DST=_data/plotschema.json
67

@@ -14,6 +15,26 @@ build:
1415
cp ${SCHEMA_SRC} ${SCHEMA_DST}
1516
${JEKYLL} build
1617

18+
## examples: build example documentation in ./tmp
19+
examples:
20+
@rm -rf tmp
21+
@mkdir -p tmp
22+
${RUN} bin/example_pages.py --indir _posts/plotly_js --outdir tmp/javascript --jsversion 3.2.1 --verbose 1
23+
24+
## format: reformat Python code
25+
format:
26+
@ruff format bin
27+
28+
## lint: check code and project
29+
lint:
30+
@ruff check bin
31+
32+
## reference: build reference documentation in ./tmp
33+
reference:
34+
@rm -rf tmp
35+
@mkdir -p tmp
36+
${RUN} bin/reference_pages.py --schema ${SCHEMA_SRC} --outdir tmp/reference _posts/reference_pages/javascript/*.html
37+
1738
## serve: display documentation
1839
serve:
1940
@mkdir -p _data

docs/_posts/reference_pages/javascript/2020-07-20-heatmapgl.html

Lines changed: 0 additions & 18 deletions
This file was deleted.

docs/_posts/reference_pages/javascript/2020-07-20-pointcloud.html

Lines changed: 0 additions & 18 deletions
This file was deleted.

docs/bin/example_pages.py

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
"""Rebuild plotly.js example pages from Jekyll HTML."""
2+
3+
import argparse
4+
import frontmatter
5+
from html import escape
6+
import json
7+
from markdown import markdown
8+
from pathlib import Path
9+
import re
10+
11+
from utils import _log
12+
13+
14+
HTML_TAG_RE = re.compile(r"<[^>]*>")
15+
SUITE_RE = re.compile(r'where:"suite","(.+?)"')
16+
17+
18+
def main():
19+
"""Main driver."""
20+
args = _parse_args()
21+
index_files, example_files = _get_source_files(args)
22+
for path, record in index_files.items():
23+
_process(args, path, record, example_files)
24+
25+
26+
def _get_source_files(args):
27+
"""Load and classify source files."""
28+
index_files = {}
29+
example_files = {}
30+
for filepath in args.indir.glob("**/*.html"):
31+
page = frontmatter.load(filepath)
32+
record = {"header": page.metadata, "content": page.content}
33+
if not str(filepath).endswith("index.html"):
34+
example_files[filepath] = record
35+
elif "posts/auto_examples.html" in page.content:
36+
index_files[filepath] = record
37+
return index_files, example_files
38+
39+
40+
def _get_suite(path, content):
41+
"""Get suite specification from index file."""
42+
m = SUITE_RE.search(content)
43+
if _log(not m, f"cannot find 'suite' in index file {path}"):
44+
return None
45+
return m.group(1)
46+
47+
48+
def _make_html(args, examples):
49+
"""Build HTML page full of examples."""
50+
accum = []
51+
for counter, (path, record) in enumerate(examples):
52+
header = record["header"]
53+
content = record["content"]
54+
accum.append('<div class="section">\n')
55+
accum.append(' <div class="row auto-eg-padding">\n')
56+
57+
_make_html_name(accum, path, header)
58+
59+
accum.append(' <div class="row">\n')
60+
_make_html_text(accum, path, header, content)
61+
if _make_plot_url(accum, path, header, content):
62+
pass
63+
elif _make_mydiv(args, accum, path, header, content, counter):
64+
pass
65+
accum.append(" </div>\n")
66+
67+
accum.append(" </div>\n")
68+
accum.append("</div>\n\n")
69+
70+
return "".join(accum)
71+
72+
73+
HTML_NAME = """\
74+
<div class=" row twelve columns">
75+
<h3 id="{name}">
76+
<a class="no_underline plot-blue" href="#{name}">{name}</a>
77+
</h3>
78+
</div>
79+
"""
80+
81+
82+
def _make_html_name(accum, path, header):
83+
"""Make example name block."""
84+
name = header["name"] if header["name"] else ""
85+
_log(not name, f"{path} does not have name")
86+
name = _strip_html(name.replace(" ", "-").replace(",", "").lower())
87+
accum.append(HTML_NAME.format(name=name))
88+
89+
90+
HTML_TEXT = """\
91+
<div class="{columns} columns">
92+
{markdown_content}
93+
{page_content}
94+
{description}
95+
</div>
96+
"""
97+
98+
HTML_TEXT_PAGE_CONTENT = """\
99+
<div class="z-depth-1">
100+
<pre><code class="javascript">{text}</code></pre>
101+
</div>
102+
"""
103+
104+
HTML_TEXT_DESCRIPTION = """\
105+
<blockquote>
106+
{text}
107+
</blockquote>
108+
"""
109+
110+
111+
def _make_html_text(accum, path, header, content):
112+
"""Make text of example."""
113+
columns = "twelve" if "horizontal" in header.get("arrangement", "") else "six"
114+
markdown_content = markdown(header.get("markdown_content", ""))
115+
page_content = (
116+
HTML_TEXT_PAGE_CONTENT.format(text=escape(content)) if content else ""
117+
)
118+
description = header.get("description", "")
119+
description = HTML_TEXT_DESCRIPTION.format(text=description) if description else ""
120+
accum.append(
121+
HTML_TEXT.format(
122+
columns=columns,
123+
markdown_content=markdown_content,
124+
page_content=page_content,
125+
description=description,
126+
)
127+
)
128+
129+
130+
MYDIV_D3 = "\n\t&lt;script src='https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js'>&lt;/script&gt;"
131+
MYDIV_MATHJAX = "\n\t&lt;script src='//cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML'>&lt;/script&gt;"
132+
133+
MYDIV = """\
134+
<div class="{columns} columns">
135+
<form style="margin-bottom: 35px; font-weight: 'Open Sans', sans-serif;"
136+
action="https://codepen.io/pen/define" method="POST" target="_blank">
137+
<input type="hidden" name="data"
138+
value="{{&quot;title&quot;:&quot;Plotly.js {name}&quot;,&quot;html&quot;:&quot;&lt;head&gt;\n\t&lt;!-- Load plotly.js into the DOM --&gt;{mathjax}\n\t&lt;script src='https://cdn.plot.ly/plotly-{jsversion}.min.js'>&lt;/script&gt;{d3}\n&lt;/head>\n\n&lt;body&gt;\n\t&lt;div id='myDiv'&gt;&lt;!-- Plotly chart will be drawn inside this DIV --&gt;&lt;/div&gt;\n&lt;/body&gt;&quot;,&quot;js&quot;:{content_json}}}">
139+
<input style=" float: right; border-radius: 4px;" class="codepen-submit" type="submit" value="Try It On CodePen!">
140+
</form>
141+
<div style="max-width: 100%; margin: auto" id="{unique_mydiv}"></div>
142+
<script>
143+
{content_mydiv}
144+
</script>
145+
</div>
146+
"""
147+
148+
149+
def _make_mydiv(args, accum, path, header, content, counter):
150+
"""Handle myDiv case."""
151+
if ("'myDiv'" not in content) and ('"myDiv"' not in content):
152+
return False
153+
154+
d3 = MYDIV_D3 if "d3." in content else ""
155+
mathjax = MYDIV_MATHJAX if "remember to load MathJax.js" in content else ""
156+
columns = "twelve" if "horizontal" in header.get("arrangement", "") else "six"
157+
name = header["name"]
158+
unique_mydiv = f"myDiv_{counter}"
159+
content_mydiv = content.replace("myDiv", unique_mydiv)
160+
content_json = escape(json.dumps(content))
161+
162+
accum.append(
163+
MYDIV.format(
164+
d3=d3,
165+
mathjax=mathjax,
166+
columns=columns,
167+
name=name,
168+
unique_mydiv=unique_mydiv,
169+
content_mydiv=content_mydiv,
170+
content_json=content_json,
171+
jsversion=args.jsversion,
172+
)
173+
)
174+
175+
return True
176+
177+
178+
PLOT_URL = """\
179+
<div class="{columns} columns">
180+
{plot_url_img}
181+
{plot_url_embed}
182+
</div>
183+
"""
184+
185+
PLOT_URL_IMG = """\
186+
<img src="{plot_url}" />
187+
"""
188+
189+
PLOT_URL_EMBED = """\
190+
<iframe id="auto-examples" src="{plot_url}{embed_class}"
191+
style="width: {width} height: {height} border: none;"></iframe>
192+
"""
193+
194+
195+
def _make_plot_url(accum, path, header, content):
196+
"""Handle specified plot URL."""
197+
plot_url = header.get("plot_url")
198+
if not plot_url:
199+
return False
200+
columns = "twelve" if "horizontal" in header.get("arrangement", "") else "six"
201+
202+
plot_url_img = ""
203+
plot_url_embed = ""
204+
if (".gif" in plot_url) or (".png" in plot_url):
205+
plot_url_img = PLOT_URL_IMG.format(plot_url=plot_url)
206+
else:
207+
embed_class = ".embed" if "plot.ly" in plot_url else ""
208+
width = f"{header.get('width', '550')}px"
209+
height = f"{header.get('height', '550')}px"
210+
plot_url_embed = PLOT_URL_EMBED.format(
211+
plot_url=plot_url,
212+
embed_class=embed_class,
213+
width=width,
214+
height=height,
215+
)
216+
accum.append(
217+
PLOT_URL.format(
218+
columns=columns,
219+
plot_url=plot_url,
220+
plot_url_img=plot_url_img,
221+
plot_url_embed=plot_url_embed,
222+
)
223+
)
224+
225+
return True
226+
227+
228+
def _parse_args():
229+
"""Parse command-line arguments."""
230+
parser = argparse.ArgumentParser(description="Generate HTML example documentation")
231+
parser.add_argument("--indir", type=Path, help="Input directory")
232+
parser.add_argument("--jsversion", help="Plotly JS version")
233+
parser.add_argument("--schema", type=Path, help="Path to plot schema JSON file")
234+
parser.add_argument("--outdir", type=Path, help="Output directory")
235+
parser.add_argument(
236+
"--verbose", type=int, default=0, help="Integer verbosity level"
237+
)
238+
return parser.parse_args()
239+
240+
241+
def _process(args, path, record, example_files):
242+
"""Process a section."""
243+
if (suite := _get_suite(path, record["content"])) is None:
244+
return
245+
246+
examples = [
247+
(p, r)
248+
for p, r in example_files.items()
249+
if r["header"].get("suite", None) == suite
250+
]
251+
examples.sort(
252+
key=lambda pair: (example_files[pair[0]]["header"]["order"], str(pair[0]))
253+
)
254+
255+
section = record["header"]["permalink"].strip("/").split("/")[-1]
256+
_log(args.verbose > 0, f"...{section}: {len(examples)}")
257+
258+
html = _make_html(args, examples)
259+
260+
output_path = args.outdir / section / "index.html"
261+
output_path.parent.mkdir(parents=True, exist_ok=True)
262+
output_path.write_text(html)
263+
264+
265+
def _strip_html(text):
266+
"""Remove HTML tags from text."""
267+
return HTML_TAG_RE.sub("", text)
268+
269+
270+
if __name__ == "__main__":
271+
main()

0 commit comments

Comments
 (0)