website-build-tools 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- website_build_tools-0.1.0/LICENSE +20 -0
- website_build_tools-0.1.0/PKG-INFO +61 -0
- website_build_tools-0.1.0/README.md +19 -0
- website_build_tools-0.1.0/pyproject.toml +33 -0
- website_build_tools-0.1.0/setup.cfg +4 -0
- website_build_tools-0.1.0/test/test_import.py +2 -0
- website_build_tools-0.1.0/test/test_markup.py +9 -0
- website_build_tools-0.1.0/website_build_tools.egg-info/PKG-INFO +61 -0
- website_build_tools-0.1.0/website_build_tools.egg-info/SOURCES.txt +18 -0
- website_build_tools-0.1.0/website_build_tools.egg-info/dependency_links.txt +1 -0
- website_build_tools-0.1.0/website_build_tools.egg-info/requires.txt +13 -0
- website_build_tools-0.1.0/website_build_tools.egg-info/top_level.txt +1 -0
- website_build_tools-0.1.0/webtools/__init__.py +1 -0
- website_build_tools-0.1.0/webtools/citations.py +150 -0
- website_build_tools-0.1.0/webtools/code_markup.py +152 -0
- website_build_tools-0.1.0/webtools/html.py +33 -0
- website_build_tools-0.1.0/webtools/markup.py +534 -0
- website_build_tools-0.1.0/webtools/py.typed +0 -0
- website_build_tools-0.1.0/webtools/settings.py +19 -0
- website_build_tools-0.1.0/webtools/tools.py +75 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
Copyright (c) 2025 Matthew Scroggs & other contributors to DefElement
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: website-build-tools
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: tools for building websites, used by defelement.org and quadraturerules.org
|
|
5
|
+
Author-email: Matthew Scroggs <defelement@mscroggs.co.uk>
|
|
6
|
+
License: MIT License
|
|
7
|
+
Copyright (c) 2025 Matthew Scroggs & other contributors to DefElement
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Project-URL: homepage, https://github.com/DefElement/website-build-tools
|
|
28
|
+
Project-URL: repository, https://github.com/DefElement/website-build-tools
|
|
29
|
+
Requires-Python: >=3.8.0
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Requires-Dist: PyGithub
|
|
33
|
+
Requires-Dist: pytz
|
|
34
|
+
Requires-Dist: pyyaml
|
|
35
|
+
Provides-Extra: style
|
|
36
|
+
Requires-Dist: ruff; extra == "style"
|
|
37
|
+
Requires-Dist: mypy; extra == "style"
|
|
38
|
+
Provides-Extra: test
|
|
39
|
+
Requires-Dist: pytest; extra == "test"
|
|
40
|
+
Provides-Extra: ci
|
|
41
|
+
Requires-Dist: website-build-tools[style,test]; extra == "ci"
|
|
42
|
+
|
|
43
|
+
# Website build tools
|
|
44
|
+
This repo contains code for building encyclopedia websites that is used by
|
|
45
|
+
[DefElement](https://defelement.org)
|
|
46
|
+
and [the online encyclopedia of quadrature rules](https://quadraturerules.org).
|
|
47
|
+
|
|
48
|
+
## Installing
|
|
49
|
+
|
|
50
|
+
To install the latest release from PyPI, run:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install website-build-tools
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
To install the latest code from GitHub, run:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install git+https://github.com/DefElement/website-build-tools.git
|
|
60
|
+
```
|
|
61
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Website build tools
|
|
2
|
+
This repo contains code for building encyclopedia websites that is used by
|
|
3
|
+
[DefElement](https://defelement.org)
|
|
4
|
+
and [the online encyclopedia of quadrature rules](https://quadraturerules.org).
|
|
5
|
+
|
|
6
|
+
## Installing
|
|
7
|
+
|
|
8
|
+
To install the latest release from PyPI, run:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install website-build-tools
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
To install the latest code from GitHub, run:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install git+https://github.com/DefElement/website-build-tools.git
|
|
18
|
+
```
|
|
19
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "website-build-tools"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "tools for building websites, used by defelement.org and quadraturerules.org"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.8.0"
|
|
7
|
+
license = { file = "LICENSE" }
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Matthew Scroggs", email = "defelement@mscroggs.co.uk" }
|
|
10
|
+
]
|
|
11
|
+
dependencies = ["PyGithub", "pytz", "pyyaml"]
|
|
12
|
+
|
|
13
|
+
[project.urls]
|
|
14
|
+
homepage = "https://github.com/DefElement/website-build-tools"
|
|
15
|
+
repository = "https://github.com/DefElement/website-build-tools"
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
style = ["ruff", "mypy"]
|
|
19
|
+
test = ["pytest"]
|
|
20
|
+
ci = ["website-build-tools[style,test]"]
|
|
21
|
+
|
|
22
|
+
[tool.ruff]
|
|
23
|
+
line-length = 100
|
|
24
|
+
indent-width = 4
|
|
25
|
+
|
|
26
|
+
[tool.ruff.lint.per-file-ignores]
|
|
27
|
+
"__init__.py" = ["F401"]
|
|
28
|
+
|
|
29
|
+
[tool.ruff.lint.pydocstyle]
|
|
30
|
+
convention = "google"
|
|
31
|
+
|
|
32
|
+
[tool.mypy]
|
|
33
|
+
ignore_missing_imports = true
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: website-build-tools
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: tools for building websites, used by defelement.org and quadraturerules.org
|
|
5
|
+
Author-email: Matthew Scroggs <defelement@mscroggs.co.uk>
|
|
6
|
+
License: MIT License
|
|
7
|
+
Copyright (c) 2025 Matthew Scroggs & other contributors to DefElement
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Project-URL: homepage, https://github.com/DefElement/website-build-tools
|
|
28
|
+
Project-URL: repository, https://github.com/DefElement/website-build-tools
|
|
29
|
+
Requires-Python: >=3.8.0
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Requires-Dist: PyGithub
|
|
33
|
+
Requires-Dist: pytz
|
|
34
|
+
Requires-Dist: pyyaml
|
|
35
|
+
Provides-Extra: style
|
|
36
|
+
Requires-Dist: ruff; extra == "style"
|
|
37
|
+
Requires-Dist: mypy; extra == "style"
|
|
38
|
+
Provides-Extra: test
|
|
39
|
+
Requires-Dist: pytest; extra == "test"
|
|
40
|
+
Provides-Extra: ci
|
|
41
|
+
Requires-Dist: website-build-tools[style,test]; extra == "ci"
|
|
42
|
+
|
|
43
|
+
# Website build tools
|
|
44
|
+
This repo contains code for building encyclopedia websites that is used by
|
|
45
|
+
[DefElement](https://defelement.org)
|
|
46
|
+
and [the online encyclopedia of quadrature rules](https://quadraturerules.org).
|
|
47
|
+
|
|
48
|
+
## Installing
|
|
49
|
+
|
|
50
|
+
To install the latest release from PyPI, run:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install website-build-tools
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
To install the latest code from GitHub, run:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install git+https://github.com/DefElement/website-build-tools.git
|
|
60
|
+
```
|
|
61
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
test/test_import.py
|
|
5
|
+
test/test_markup.py
|
|
6
|
+
website_build_tools.egg-info/PKG-INFO
|
|
7
|
+
website_build_tools.egg-info/SOURCES.txt
|
|
8
|
+
website_build_tools.egg-info/dependency_links.txt
|
|
9
|
+
website_build_tools.egg-info/requires.txt
|
|
10
|
+
website_build_tools.egg-info/top_level.txt
|
|
11
|
+
webtools/__init__.py
|
|
12
|
+
webtools/citations.py
|
|
13
|
+
webtools/code_markup.py
|
|
14
|
+
webtools/html.py
|
|
15
|
+
webtools/markup.py
|
|
16
|
+
webtools/py.typed
|
|
17
|
+
webtools/settings.py
|
|
18
|
+
webtools/tools.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
webtools
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Website building tools."""
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Citations."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import typing
|
|
5
|
+
|
|
6
|
+
from webtools.tools import comma_and_join
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def markup_authors(a: typing.Union[str, typing.List[str]]) -> str:
|
|
10
|
+
"""Markup authors.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
a: Authors
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
Formatted list of authors
|
|
17
|
+
"""
|
|
18
|
+
if isinstance(a, str):
|
|
19
|
+
return a
|
|
20
|
+
else:
|
|
21
|
+
return comma_and_join(a)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def markup_citation(r: typing.Dict[str, typing.Any]) -> str:
|
|
25
|
+
"""Markup citations.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
r: Citation
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Formatted citation
|
|
32
|
+
"""
|
|
33
|
+
out = ""
|
|
34
|
+
if "author" in r:
|
|
35
|
+
out += markup_authors(r["author"])
|
|
36
|
+
else:
|
|
37
|
+
out += "<i>(unknown author)</i>"
|
|
38
|
+
if out[-1] != ".":
|
|
39
|
+
out += "."
|
|
40
|
+
out += f" {r['title']}"
|
|
41
|
+
if "journal" in r:
|
|
42
|
+
out += f", <em>{r['journal']}</em>"
|
|
43
|
+
if "volume" in r:
|
|
44
|
+
out += f" {r['volume']}"
|
|
45
|
+
if "issue" in r:
|
|
46
|
+
out += f"({r['issue']})"
|
|
47
|
+
if "pagestart" in r and "pageend" in r:
|
|
48
|
+
out += f", {r['pagestart']}–{r['pageend']}"
|
|
49
|
+
elif "arxiv" in r:
|
|
50
|
+
out += f", arΧiv: <a href='https://arxiv.org/abs/{r['arxiv']}'>{r['arxiv']}</a>"
|
|
51
|
+
if "booktitle" in r:
|
|
52
|
+
out += f", in <em>{r['booktitle']}</em>"
|
|
53
|
+
if "editor" in r:
|
|
54
|
+
out += f" (eds: {markup_authors(r['editor'])})"
|
|
55
|
+
if "year" in r:
|
|
56
|
+
out += f", {r['year']}"
|
|
57
|
+
out += "."
|
|
58
|
+
if "doi" in r:
|
|
59
|
+
out += f" [DOI: <a href='https://doi.org/{r['doi']}'>{r['doi']}</a>]"
|
|
60
|
+
if "url" in r:
|
|
61
|
+
out += f" [<a href='{r['url']}'>{r['url'].split('://')[1]}</a>]"
|
|
62
|
+
return out
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def wrap_caps(txt: str) -> str:
|
|
66
|
+
"""Wrap capitials in curly braces.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
txt: Input string
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
String with capitals wrapped in curly braces
|
|
73
|
+
"""
|
|
74
|
+
out = ""
|
|
75
|
+
for word in txt.split():
|
|
76
|
+
if out != "":
|
|
77
|
+
out += " "
|
|
78
|
+
if re.match(r".[A-Z]", word) or (out != "" and re.match(r"[A-Z]", word)):
|
|
79
|
+
out += f"{{{word}}}"
|
|
80
|
+
else:
|
|
81
|
+
out += word
|
|
82
|
+
return out
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def html_to_tex(txt: str) -> str:
|
|
86
|
+
"""Convert html to TeX.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
txt: HTML
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
TeX
|
|
93
|
+
"""
|
|
94
|
+
txt = re.sub(r"&([A-Za-z])acute;", r"\\'\1", txt)
|
|
95
|
+
txt = re.sub(r"&([A-Za-z])grave;", r"\\`\1", txt)
|
|
96
|
+
txt = re.sub(r"&([A-Za-z])caron;", r"\\v{\1}", txt)
|
|
97
|
+
txt = re.sub(r"&([A-Za-z])uml;", r"\\\"\1", txt)
|
|
98
|
+
txt = re.sub(r"&([A-Za-z])cedil;", r"\\c{\1}", txt)
|
|
99
|
+
txt = re.sub(r"&([A-Za-z])circ;", r"\\^\1", txt)
|
|
100
|
+
txt = re.sub(r"&([A-Za-z])tilde;", r"\\~\1", txt)
|
|
101
|
+
txt = txt.replace("ø", "{\\o}")
|
|
102
|
+
txt = txt.replace("–", "--")
|
|
103
|
+
txt = txt.replace("—", "---")
|
|
104
|
+
return txt
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def make_bibtex(id: str, r: typing.Dict[str, typing.Any]) -> str:
|
|
108
|
+
"""Make BibTex.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
id: Unique identifier
|
|
112
|
+
r: A citation
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
The citation in BibTeX format
|
|
116
|
+
"""
|
|
117
|
+
if "type" not in r:
|
|
118
|
+
r["type"] = "article"
|
|
119
|
+
out = f"@{r['type']}{{{id},\n"
|
|
120
|
+
|
|
121
|
+
# Author-type fields
|
|
122
|
+
for i, j in [("AUTHOR", "author"), ("EDITOR", "editor")]:
|
|
123
|
+
if j in r:
|
|
124
|
+
out += " " * (10 - len(i)) + f"{i} = {{"
|
|
125
|
+
if isinstance(r[j], str):
|
|
126
|
+
out += html_to_tex(r[j])
|
|
127
|
+
else:
|
|
128
|
+
out += " and ".join([html_to_tex(k) for k in r[j]])
|
|
129
|
+
out += "},\n"
|
|
130
|
+
|
|
131
|
+
# Fields with caps that need wrapping
|
|
132
|
+
for i, j in [("TITLE", "title"), ("BOOKTITLE", "booktitle")]:
|
|
133
|
+
if j in r:
|
|
134
|
+
out += " " * (10 - len(i)) + f"{i} = {{{wrap_caps(html_to_tex(r[j]))}}},\n"
|
|
135
|
+
|
|
136
|
+
# Text fields
|
|
137
|
+
for i, j in [("JOURNAL", "journal")]:
|
|
138
|
+
if j in r:
|
|
139
|
+
out += " " * (10 - len(i)) + f"{i} = {{{html_to_tex(r[j])}}},\n"
|
|
140
|
+
|
|
141
|
+
# Numerical fields
|
|
142
|
+
for i, j in [("VOLUME", "volume"), ("NUMBER", "issue"), ("YEAR", "year"), ("DOI", "doi")]:
|
|
143
|
+
if j in r:
|
|
144
|
+
out += " " * (10 - len(i)) + f"{i} = {{{r[j]}}},\n"
|
|
145
|
+
|
|
146
|
+
# Page numbers
|
|
147
|
+
if "pagestart" in r and "pageend" in r:
|
|
148
|
+
out += f" PAGES = {{{{{r['pagestart']}--{r['pageend']}}}}},\n"
|
|
149
|
+
out += "}"
|
|
150
|
+
return out
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Highlighting for code snippets."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import typing
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _highlight(txt: str, comment_start: str, keywords: typing.List[str]) -> str:
|
|
8
|
+
"""General highlight function."""
|
|
9
|
+
out = []
|
|
10
|
+
for line in txt.split("\n"):
|
|
11
|
+
comment = ""
|
|
12
|
+
if comment_start in line:
|
|
13
|
+
lsp = line.split(comment_start, 1)
|
|
14
|
+
line = lsp[0]
|
|
15
|
+
comment = f"<span style='color:#FF8800'>{comment_start}{lsp[1]}</span>"
|
|
16
|
+
|
|
17
|
+
lsp = line.split('"')
|
|
18
|
+
line = lsp[0]
|
|
19
|
+
|
|
20
|
+
for i, j in enumerate(lsp[1:]):
|
|
21
|
+
if i % 2 == 0:
|
|
22
|
+
line += f"<span style='color:#DD2299'>\"{j}"
|
|
23
|
+
else:
|
|
24
|
+
line += f'"</span>{j}'
|
|
25
|
+
|
|
26
|
+
for keyword in keywords:
|
|
27
|
+
line = re.sub(
|
|
28
|
+
rf"( |^)({keyword})( |$)",
|
|
29
|
+
r"\1<span style='color:#FF8800'>\2</span>\3",
|
|
30
|
+
line,
|
|
31
|
+
)
|
|
32
|
+
out.append(line + comment)
|
|
33
|
+
|
|
34
|
+
return "<br />".join(out)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def python_highlight(txt: str) -> str:
|
|
38
|
+
"""Apply syntax highlighting to Python snippet.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
txt: Python snippet
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Snippet with syntax highlighting
|
|
45
|
+
"""
|
|
46
|
+
return _highlight(
|
|
47
|
+
txt,
|
|
48
|
+
"#",
|
|
49
|
+
[
|
|
50
|
+
"for",
|
|
51
|
+
"while",
|
|
52
|
+
"from",
|
|
53
|
+
"import",
|
|
54
|
+
"return",
|
|
55
|
+
"if",
|
|
56
|
+
"elif",
|
|
57
|
+
"else",
|
|
58
|
+
"def",
|
|
59
|
+
"in",
|
|
60
|
+
"global",
|
|
61
|
+
"assert",
|
|
62
|
+
],
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def rust_highlight(txt: str) -> str:
|
|
67
|
+
"""Apply syntax highlighting to Rust snippet.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
txt: Python snippet
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Snippet with syntax highlighting
|
|
74
|
+
"""
|
|
75
|
+
return _highlight(
|
|
76
|
+
txt,
|
|
77
|
+
"//",
|
|
78
|
+
[
|
|
79
|
+
"use",
|
|
80
|
+
"while",
|
|
81
|
+
"for",
|
|
82
|
+
"return",
|
|
83
|
+
"if",
|
|
84
|
+
"else",
|
|
85
|
+
"function",
|
|
86
|
+
"let",
|
|
87
|
+
],
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def cpp_highlight(txt: str) -> str:
|
|
92
|
+
"""Apply syntax highlighting to C++ snippet.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
txt: Python snippet
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Snippet with syntax highlighting
|
|
99
|
+
"""
|
|
100
|
+
return _highlight(
|
|
101
|
+
txt,
|
|
102
|
+
"//",
|
|
103
|
+
[
|
|
104
|
+
"#include",
|
|
105
|
+
"auto",
|
|
106
|
+
"using",
|
|
107
|
+
"for",
|
|
108
|
+
"if",
|
|
109
|
+
"else",
|
|
110
|
+
"function",
|
|
111
|
+
"while",
|
|
112
|
+
],
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def bash_highlight(txt: str) -> str:
|
|
117
|
+
"""Apply syntax highlighting to Bash snippet.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
txt: Bash snippet
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Snippet with syntax highlighting
|
|
124
|
+
"""
|
|
125
|
+
txt = re.sub(
|
|
126
|
+
r"(python3?(?: -m .+?)? )", r"<span style='color:#FF8800'>\1</span>", txt
|
|
127
|
+
)
|
|
128
|
+
for keyword in ["wget", "mkdir", "tar", "cd", "cmake", "make", "ls", "cargo"]:
|
|
129
|
+
txt = re.sub(
|
|
130
|
+
rf"( |^)({keyword})( |$)",
|
|
131
|
+
r"\1<span style='color:#FF8800'>\2</span>\3",
|
|
132
|
+
txt,
|
|
133
|
+
)
|
|
134
|
+
return "<br />".join(txt.split("\n"))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def code_highlight(txt: str, lang: typing.Optional[str] = None):
|
|
138
|
+
for a, b in [
|
|
139
|
+
(" ", " "),
|
|
140
|
+
("<", "<"),
|
|
141
|
+
(">", ">"),
|
|
142
|
+
]:
|
|
143
|
+
txt = txt.replace(a, b)
|
|
144
|
+
if lang == "python":
|
|
145
|
+
return python_highlight(txt)
|
|
146
|
+
if lang == "rust":
|
|
147
|
+
return rust_highlight(txt)
|
|
148
|
+
if lang == "cpp":
|
|
149
|
+
return cpp_highlight(txt)
|
|
150
|
+
if lang == "bash":
|
|
151
|
+
return bash_highlight(txt)
|
|
152
|
+
return txt
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""HTML tools."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import typing
|
|
5
|
+
|
|
6
|
+
from webtools import settings
|
|
7
|
+
from webtools.markup import insert_dates
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def make_html_page(content: str, pagetitle: typing.Optional[str] = None) -> str:
|
|
11
|
+
"""Make a HTML page.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
content: Page content
|
|
15
|
+
pagetitle: Page title
|
|
16
|
+
|
|
17
|
+
Return:
|
|
18
|
+
Formatted HTML page
|
|
19
|
+
"""
|
|
20
|
+
assert settings.template_path is not None
|
|
21
|
+
out = ""
|
|
22
|
+
with open(os.path.join(settings.template_path, "intro.html")) as f:
|
|
23
|
+
out += insert_dates(f.read())
|
|
24
|
+
if pagetitle is None:
|
|
25
|
+
out = out.replace("{{: pagetitle}}", "")
|
|
26
|
+
out = out.replace("{{pagetitle | }}", "")
|
|
27
|
+
else:
|
|
28
|
+
out = out.replace("{{: pagetitle}}", f": {pagetitle}")
|
|
29
|
+
out = out.replace("{{pagetitle | }}", f"{pagetitle} | ")
|
|
30
|
+
out += content
|
|
31
|
+
with open(os.path.join(settings.template_path, "outro.html")) as f:
|
|
32
|
+
out += insert_dates(f.read())
|
|
33
|
+
return out
|
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
"""Markup."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import shlex
|
|
6
|
+
import typing
|
|
7
|
+
from github import Github
|
|
8
|
+
import warnings
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from urllib.parse import quote_plus
|
|
11
|
+
|
|
12
|
+
from webtools import settings
|
|
13
|
+
from webtools.code_markup import code_highlight
|
|
14
|
+
from webtools.tools import comma_and_join
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
page_references: typing.List[str] = []
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def cap_first(txt: str) -> str:
|
|
21
|
+
"""Captialise first letter.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
txt: Input text
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
text with capitalised first letter
|
|
28
|
+
"""
|
|
29
|
+
return txt[:1].upper() + txt[1:]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def heading(hx: str, content: str, style: typing.Optional[str] = None) -> str:
|
|
33
|
+
"""Create heading.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
hx: HTML tag
|
|
37
|
+
content: Heading content
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Heading with self reference
|
|
41
|
+
"""
|
|
42
|
+
out = f"<{hx}"
|
|
43
|
+
if style is not None:
|
|
44
|
+
out += f' style="{style}"'
|
|
45
|
+
out += f">{content}</{hx}>\n"
|
|
46
|
+
return out
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def heading_with_self_ref(hx: str, content: str, style: typing.Optional[str] = None) -> str:
|
|
50
|
+
"""Create heading with self reference.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
hx: HTML tag
|
|
54
|
+
content: Heading content
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Heading with self reference
|
|
58
|
+
"""
|
|
59
|
+
id = quote_plus(content)
|
|
60
|
+
out = f'<{hx} id="{id}"'
|
|
61
|
+
if style is not None:
|
|
62
|
+
out += f' style="{style}"'
|
|
63
|
+
out += f'><a href="#{id}">{content}</a></{hx}>\n'
|
|
64
|
+
return out
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def format_names(names: typing.List[str], format: str) -> str:
|
|
68
|
+
"""Format names.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
names: List of names
|
|
72
|
+
format: Format. `bibtex`, `html`, and `citation` are allowed
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Formatted names
|
|
76
|
+
"""
|
|
77
|
+
if format == "bibtex":
|
|
78
|
+
return ("\n" + " " * 17 + "and ").join(names)
|
|
79
|
+
else:
|
|
80
|
+
formatted_names = []
|
|
81
|
+
for n in names:
|
|
82
|
+
if n == "et al":
|
|
83
|
+
formatted_names.append("et al")
|
|
84
|
+
else:
|
|
85
|
+
nsp = n.split(", ")
|
|
86
|
+
name = ""
|
|
87
|
+
for i in nsp[:0:-1]:
|
|
88
|
+
for j in i.split(" "):
|
|
89
|
+
name += f"{j[0]}. "
|
|
90
|
+
name += nsp[0]
|
|
91
|
+
formatted_names.append(name)
|
|
92
|
+
if names[-1] == "et al":
|
|
93
|
+
if len(formatted_names) <= 2:
|
|
94
|
+
return " ".join(formatted_names)
|
|
95
|
+
else:
|
|
96
|
+
return ", ".join([", ".join(formatted_names[:-1]), formatted_names[-1]])
|
|
97
|
+
else:
|
|
98
|
+
return comma_and_join(formatted_names)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def person_sort_key(p: typing.Dict):
|
|
102
|
+
"""Key used to sort people for the contributors and citation lists."""
|
|
103
|
+
if "github" in p:
|
|
104
|
+
if p["github"] in settings.owners:
|
|
105
|
+
return "AAA" + p["name"]
|
|
106
|
+
if p["github"] in settings.editors:
|
|
107
|
+
return "AAB" + p["name"]
|
|
108
|
+
return p["name"]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def list_contributors(format: str = "html") -> str:
|
|
112
|
+
"""Get list of contributors.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
format: Format. `bibtex`, `html`, and `citation` are allowed
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Contributor list
|
|
119
|
+
"""
|
|
120
|
+
if format not in ["html", "bibtex", "citation"]:
|
|
121
|
+
raise ValueError(f"Unsupported format: {format}")
|
|
122
|
+
|
|
123
|
+
people = settings.contributors
|
|
124
|
+
people.sort(key=person_sort_key)
|
|
125
|
+
editors = settings.editors
|
|
126
|
+
|
|
127
|
+
if format == "html":
|
|
128
|
+
included = []
|
|
129
|
+
out = ""
|
|
130
|
+
|
|
131
|
+
editors_out = ""
|
|
132
|
+
contributors_out = ""
|
|
133
|
+
for info in people:
|
|
134
|
+
person_out = ""
|
|
135
|
+
if "img" in info:
|
|
136
|
+
person_out += f"<img src='/img/people/{info['img']}' class='person'>"
|
|
137
|
+
person_out += heading_with_self_ref("h2", " ".join(info["name"].split(", ")[::-1]))
|
|
138
|
+
if "desc" in info:
|
|
139
|
+
person_out += f"<p>{markup(info['desc'])}</p>"
|
|
140
|
+
if "website" in info:
|
|
141
|
+
website_name = info["website"].split("//")[1].strip("/")
|
|
142
|
+
person_out += (
|
|
143
|
+
f"<div class='social'><a href='{info['website']}'>"
|
|
144
|
+
"<i class='fa-brands fa-internet-explorer' aria-hidden='true'></i>"
|
|
145
|
+
f" {website_name}</a></div>"
|
|
146
|
+
)
|
|
147
|
+
if "email" in info:
|
|
148
|
+
person_out += (
|
|
149
|
+
f"<div class='social'><a href='mailto:{info['email']}'>"
|
|
150
|
+
"<i class='fa-regular fa-envelope' aria-hidden='true'></i>"
|
|
151
|
+
f" {info['email']}</a></div>"
|
|
152
|
+
)
|
|
153
|
+
if "github" in info:
|
|
154
|
+
person_out += (
|
|
155
|
+
f"<div class='social'><a href='https://github.com/{info['github']}'>"
|
|
156
|
+
"<i class='fa-brands fa-github' aria-hidden='true'></i>"
|
|
157
|
+
f" {info['github']}</a></div>"
|
|
158
|
+
)
|
|
159
|
+
included.append(info["github"])
|
|
160
|
+
if "twitter" in info:
|
|
161
|
+
person_out += (
|
|
162
|
+
f"<div class='social'><a href='https://twitter.com/{info['twitter']}'>"
|
|
163
|
+
"<i class='fa-brands fa-twitter' aria-hidden='true'></i>"
|
|
164
|
+
f" @{info['twitter']}</a></div>"
|
|
165
|
+
)
|
|
166
|
+
if "bluesky" in info:
|
|
167
|
+
person_out += (
|
|
168
|
+
f"<div class='social'><a href='https://bsky.app/profile/{info['bluesky']}'>"
|
|
169
|
+
"<i class='fa-brands fa-bluesky' aria-hidden='true'></i>"
|
|
170
|
+
f" @{info['bluesky']}</a></div>"
|
|
171
|
+
)
|
|
172
|
+
if "mastodon" in info:
|
|
173
|
+
handle, url = info["mastodon"].split("@")
|
|
174
|
+
person_out += (
|
|
175
|
+
f"<div class='social'><a href='https://{url}/@{handle}'>"
|
|
176
|
+
"<i class='fa-brands fa-mastodon' aria-hidden='true'></i>"
|
|
177
|
+
f" @{handle}@{url}</a></div>"
|
|
178
|
+
)
|
|
179
|
+
person_out += "<br style='clear:both' />"
|
|
180
|
+
if "github" in info and info["github"] in editors:
|
|
181
|
+
editors_out += person_out
|
|
182
|
+
else:
|
|
183
|
+
contributors_out += person_out
|
|
184
|
+
|
|
185
|
+
if editors != "":
|
|
186
|
+
out += heading_with_self_ref("h1", "Editors", "margin-top:50px")
|
|
187
|
+
out += (
|
|
188
|
+
"<p>The contributors listed in this section are responsible for reviewing "
|
|
189
|
+
f"contributions to {settings.website_name[1]}.</p>\n{editors_out}"
|
|
190
|
+
)
|
|
191
|
+
if contributors_out != "":
|
|
192
|
+
out += heading_with_self_ref("h1", "Contributors", "margin-top:50px")
|
|
193
|
+
out += contributors_out
|
|
194
|
+
|
|
195
|
+
if settings.github_token is None or settings.repo is None:
|
|
196
|
+
warnings.warn("Building without GitHub token. Skipping search for GitHub contributors.")
|
|
197
|
+
else:
|
|
198
|
+
g = Github(settings.github_token)
|
|
199
|
+
repo = g.get_repo(settings.repo)
|
|
200
|
+
pages = repo.get_contributors()
|
|
201
|
+
i = 0
|
|
202
|
+
extras = []
|
|
203
|
+
while True:
|
|
204
|
+
page = pages.get_page(i)
|
|
205
|
+
if len(page) == 0:
|
|
206
|
+
break
|
|
207
|
+
for user in page:
|
|
208
|
+
if user.login not in included:
|
|
209
|
+
extras.append((user.login, user.name))
|
|
210
|
+
i += 1
|
|
211
|
+
if len(extras) > 0:
|
|
212
|
+
out += heading_with_self_ref("h2", "Additional contributors")
|
|
213
|
+
out += (
|
|
214
|
+
f"<p>The following people have contributed to {settings.website_name[1]}"
|
|
215
|
+
" but are yet to add details about themselves to this page:</p>\n<ul>\n"
|
|
216
|
+
)
|
|
217
|
+
for u in extras:
|
|
218
|
+
out += "<li>"
|
|
219
|
+
if u[1] is not None:
|
|
220
|
+
out += f"{u[1]} ("
|
|
221
|
+
out += (
|
|
222
|
+
f"<a href='https://github.com/{u[0]}'>"
|
|
223
|
+
"<i class='fa-brands fa-github' aria-hidden='true'></i>"
|
|
224
|
+
f" {u[0]}</a>"
|
|
225
|
+
)
|
|
226
|
+
if u[1] is not None:
|
|
227
|
+
out += ")"
|
|
228
|
+
out += "</li>\n"
|
|
229
|
+
out += "</ul>"
|
|
230
|
+
out += (
|
|
231
|
+
"<p>If you're listed here, you can find instructions for how to add "
|
|
232
|
+
"information about yourself on the [contributing page](contributing.md"
|
|
233
|
+
"#Adding+yourself+to+the+contributors+list).</p>"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
return out
|
|
237
|
+
else:
|
|
238
|
+
names = []
|
|
239
|
+
for info in people:
|
|
240
|
+
names.append(info["name"])
|
|
241
|
+
|
|
242
|
+
if settings.github_token is None or settings.repo is None:
|
|
243
|
+
warnings.warn("Building without GitHub token. Skipping search for GitHub contributors.")
|
|
244
|
+
else:
|
|
245
|
+
included = [info["github"] for info in people if "github" in info]
|
|
246
|
+
g = Github(settings.github_token)
|
|
247
|
+
repo = g.get_repo(settings.repo)
|
|
248
|
+
pages = repo.get_contributors()
|
|
249
|
+
i = 0
|
|
250
|
+
while True:
|
|
251
|
+
page = pages.get_page(i)
|
|
252
|
+
if len(page) == 0:
|
|
253
|
+
break
|
|
254
|
+
for user in page:
|
|
255
|
+
if user.login not in included:
|
|
256
|
+
if format == "bibtex":
|
|
257
|
+
names.append("others")
|
|
258
|
+
else:
|
|
259
|
+
names.append("et al")
|
|
260
|
+
break
|
|
261
|
+
else:
|
|
262
|
+
i += 1
|
|
263
|
+
continue
|
|
264
|
+
break
|
|
265
|
+
|
|
266
|
+
return format_names(names, format)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def preprocess(content: str) -> str:
|
|
270
|
+
"""Preprocess content.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
content: Content
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Preprocessed content
|
|
277
|
+
"""
|
|
278
|
+
assert settings.dir_path is not None
|
|
279
|
+
for file in os.listdir(settings.dir_path):
|
|
280
|
+
if file.endswith(".md"):
|
|
281
|
+
if f"{{{{{file}}}}}" in content:
|
|
282
|
+
with open(os.path.join(settings.dir_path, file)) as f:
|
|
283
|
+
content = content.replace(
|
|
284
|
+
f"{{{{{file}}}}}", f.read().replace(f"]({settings.url}", "](")
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
if "{{list contributors}}" in content:
|
|
288
|
+
content = content.replace("{{list contributors}}", list_contributors())
|
|
289
|
+
if "{{list contributors|" in content:
|
|
290
|
+
content = re.sub(
|
|
291
|
+
"{{list contributors\\|([^}]+)}}",
|
|
292
|
+
lambda matches: list_contributors(matches[1]),
|
|
293
|
+
content,
|
|
294
|
+
)
|
|
295
|
+
content = re.sub(r"{{author-info::([^}]+)}}", author_info, content)
|
|
296
|
+
|
|
297
|
+
return content
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def insert_links(txt: str, root_dir: str = "") -> str:
|
|
301
|
+
"""Insert links.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
txt: text
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Text with links
|
|
308
|
+
"""
|
|
309
|
+
txt = re.sub(r"\(\/([^\)]+)\.md\)", r"(\1.html)", txt)
|
|
310
|
+
txt = re.sub(r"\(\/([^\)]+)\.md#([^\)]+)\)", r"(\1.html#\2)", txt)
|
|
311
|
+
if root_dir == "":
|
|
312
|
+
txt = re.sub(r"\(([^\)]+)\.md\)", r"(/\1.html)", txt)
|
|
313
|
+
txt = re.sub(r"\(([^\)]+)\.md#([^\)]+)\)", r"(/\1.html#\2)", txt)
|
|
314
|
+
else:
|
|
315
|
+
txt = re.sub(r"\(([^\)]+)\.md\)", rf"(/{root_dir}/\1.html)", txt)
|
|
316
|
+
txt = re.sub(r"\(([^\)]+)\.md#([^\)]+)\)", rf"(/{root_dir}/\1.html#\2)", txt)
|
|
317
|
+
txt = re.sub(r"\[([^\]]+)\]\(([^\)]+)\)", r"<a href='\2'>\1</a>", txt)
|
|
318
|
+
return txt
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def insert_dates(txt: str) -> str:
|
|
322
|
+
"""Insert dates.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
txt: Text
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Text with dates inserted
|
|
329
|
+
"""
|
|
330
|
+
now = datetime.now()
|
|
331
|
+
txt = txt.replace("{{date:Y}}", now.strftime("%Y"))
|
|
332
|
+
txt = txt.replace("{{date:D-M-Y}}", now.strftime("%d-%B-%Y"))
|
|
333
|
+
|
|
334
|
+
return txt
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def markup(content: str, root_dir: str = "") -> str:
|
|
338
|
+
"""Markup content.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
content: Content
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
Content with markup replaced by HTML
|
|
345
|
+
"""
|
|
346
|
+
global page_references
|
|
347
|
+
|
|
348
|
+
content = preprocess(content)
|
|
349
|
+
content = content.replace("\\vec", "\\mathbf")
|
|
350
|
+
|
|
351
|
+
out = ""
|
|
352
|
+
popen = False
|
|
353
|
+
ulopen = False
|
|
354
|
+
liopen = False
|
|
355
|
+
code = False
|
|
356
|
+
lang: typing.Optional[str] = None
|
|
357
|
+
|
|
358
|
+
for line in content.split("\n"):
|
|
359
|
+
if line.startswith("```"):
|
|
360
|
+
code = not code
|
|
361
|
+
lang = line[3:].strip()
|
|
362
|
+
elif not code and line.startswith("#"):
|
|
363
|
+
if popen:
|
|
364
|
+
out += "</p>\n"
|
|
365
|
+
popen = False
|
|
366
|
+
if ulopen:
|
|
367
|
+
if liopen:
|
|
368
|
+
out += "</li>"
|
|
369
|
+
liopen = False
|
|
370
|
+
out += "</ul>\n"
|
|
371
|
+
ulopen = False
|
|
372
|
+
i = 0
|
|
373
|
+
while line.startswith("#"):
|
|
374
|
+
line = line[1:]
|
|
375
|
+
i += 1
|
|
376
|
+
out += heading_with_self_ref(f"h{i}", line.strip())
|
|
377
|
+
elif not code and line.startswith("* "):
|
|
378
|
+
if popen:
|
|
379
|
+
out += "</p>\n"
|
|
380
|
+
popen = False
|
|
381
|
+
if not ulopen:
|
|
382
|
+
out += "<ul>"
|
|
383
|
+
ulopen = True
|
|
384
|
+
if liopen:
|
|
385
|
+
out += "</li>"
|
|
386
|
+
liopen = False
|
|
387
|
+
out += "<li>"
|
|
388
|
+
liopen = True
|
|
389
|
+
out += line[2:].strip()
|
|
390
|
+
elif line == "":
|
|
391
|
+
if popen:
|
|
392
|
+
out += "</p>\n"
|
|
393
|
+
popen = False
|
|
394
|
+
if ulopen:
|
|
395
|
+
if liopen:
|
|
396
|
+
out += "</li>"
|
|
397
|
+
liopen = False
|
|
398
|
+
out += "</ul>\n"
|
|
399
|
+
ulopen = False
|
|
400
|
+
else:
|
|
401
|
+
if not ulopen and not popen and not line.startswith("<") and not line.startswith("\\["):
|
|
402
|
+
if code:
|
|
403
|
+
out += "<p class='pcode'>"
|
|
404
|
+
else:
|
|
405
|
+
out += "<p>"
|
|
406
|
+
popen = True
|
|
407
|
+
if code:
|
|
408
|
+
out += code_highlight(line, lang)
|
|
409
|
+
out += "<br />"
|
|
410
|
+
else:
|
|
411
|
+
out += line
|
|
412
|
+
out += " "
|
|
413
|
+
|
|
414
|
+
page_references = []
|
|
415
|
+
|
|
416
|
+
out = out.replace("(CODE_OF_CONDUCT.md)", "(code-of-conduct.md)")
|
|
417
|
+
|
|
418
|
+
out = re.sub(r" *<ref ([^>]+)>", add_citation, out)
|
|
419
|
+
|
|
420
|
+
if settings.insert_links is None:
|
|
421
|
+
out = insert_links(out, root_dir)
|
|
422
|
+
else:
|
|
423
|
+
out = settings.insert_links(out, root_dir)
|
|
424
|
+
out = re.sub(r"{{code-include::([^}]+)}}", code_include, out)
|
|
425
|
+
|
|
426
|
+
for a, b in settings.re_extras:
|
|
427
|
+
out = re.sub(a, b, out)
|
|
428
|
+
for c, d in settings.str_extras:
|
|
429
|
+
out = out.replace(c, d)
|
|
430
|
+
|
|
431
|
+
out = re.sub(r"`([^`]+)`", r"<span style='font-family:monospace'>\1</span>", out)
|
|
432
|
+
|
|
433
|
+
out = re.sub(r"\*\*([^\n]+)\*\*", r"<strong>\1</strong>", out)
|
|
434
|
+
out = re.sub(r"\*([^\n]+)\*", r"<em>\1</em>", out)
|
|
435
|
+
|
|
436
|
+
out = out.replace("{{tick}}", "<span style='color:#008800'>✔</span>")
|
|
437
|
+
|
|
438
|
+
if len(page_references) > 0:
|
|
439
|
+
out += heading_with_self_ref("h2", "References")
|
|
440
|
+
out += "<ul class='citations'>"
|
|
441
|
+
out += "".join(
|
|
442
|
+
[
|
|
443
|
+
f"<li><a class='refid' id='ref{i + 1}'>[{i + 1}]</a> {j}</li>"
|
|
444
|
+
for i, j in enumerate(page_references)
|
|
445
|
+
]
|
|
446
|
+
)
|
|
447
|
+
out += "</ul>"
|
|
448
|
+
|
|
449
|
+
return insert_dates(out)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def code_include(matches: typing.Match[str]) -> str:
|
|
453
|
+
"""Format code snippet.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
matches: Code snippets
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
HTML
|
|
460
|
+
"""
|
|
461
|
+
assert settings.dir_path is not None
|
|
462
|
+
out = "<p class='pcode'>"
|
|
463
|
+
with open(os.path.join(settings.dir_path, matches[1])) as f:
|
|
464
|
+
out += "<br />".join(line.replace(" ", " ") for line in f)
|
|
465
|
+
out += "</p>"
|
|
466
|
+
return out
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def to_tex(txt: str) -> str:
|
|
470
|
+
"""Convert to TeX."""
|
|
471
|
+
for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
|
|
472
|
+
txt = txt.replace(letter, f"{{{letter}}}")
|
|
473
|
+
return txt
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def author_info(matches: typing.Match[str]) -> str:
|
|
477
|
+
"""Format author info.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
matches: author info
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
HTML
|
|
484
|
+
"""
|
|
485
|
+
assert settings.website_name[0] is not None
|
|
486
|
+
authors, title, url = matches[1].split("|")
|
|
487
|
+
authors = authors.split(";")
|
|
488
|
+
out = "<div class='authors'>Written by "
|
|
489
|
+
out += " ".join(" ".join(i.split(", ")[::-1]) for i in authors)
|
|
490
|
+
out += (
|
|
491
|
+
"</div>\n"
|
|
492
|
+
"<a class='show_eg_link' href='javascript:show_author_cite_info()' id='showcitelink' "
|
|
493
|
+
"style='display:block'>↓ Cite this page ↓</a>"
|
|
494
|
+
"<div id='authorcite' style='display:none'>"
|
|
495
|
+
"<a class='show_eg_link' href='javascript:hide_author_cite_info()' id='showcitelink' "
|
|
496
|
+
"style='display:block'>↑ Hide citation info ↑</a>"
|
|
497
|
+
"You can cite this page using the following BibTeX:\n\n"
|
|
498
|
+
"```\n"
|
|
499
|
+
f"@misc{{{settings.website_name[0].lower()},\n"
|
|
500
|
+
f" AUTHOR = {{{format_names(authors, 'bibtex')}}},\n"
|
|
501
|
+
f" TITLE = {{{to_tex(settings.website_name[0])}: {title}}},\n"
|
|
502
|
+
" YEAR = {{{{date:Y}}}},\n"
|
|
503
|
+
f" HOWPUBLISHED = {{\\url{{{settings.url}/{url}}}}},\n"
|
|
504
|
+
" NOTE = {[Online; accessed {{date:D-M-Y}}]}\n"
|
|
505
|
+
"}\n"
|
|
506
|
+
"```\n\n"
|
|
507
|
+
"This will create a reference along the lines of:\n\n"
|
|
508
|
+
"<ul class='citations'>"
|
|
509
|
+
f"<li>{format_names(authors, 'citation')}. <i>{settings.website_name[0]}: {title}</i>, "
|
|
510
|
+
f"{{{{date:Y}}}}, <a href='{settings.url}/{url}'>{settings.url}/{url}</a> "
|
|
511
|
+
"[Online; accessed: {{date:D-M-Y}}]</li>\n"
|
|
512
|
+
"</ul></div>"
|
|
513
|
+
)
|
|
514
|
+
return out
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def add_citation(matches: typing.Match[str]) -> str:
|
|
518
|
+
"""Add citation.
|
|
519
|
+
|
|
520
|
+
Args:
|
|
521
|
+
matches: Citation info
|
|
522
|
+
|
|
523
|
+
Returns:
|
|
524
|
+
HTML
|
|
525
|
+
"""
|
|
526
|
+
global page_references
|
|
527
|
+
from webtools.citations import markup_citation
|
|
528
|
+
|
|
529
|
+
ref = {}
|
|
530
|
+
for i in shlex.split(matches[1]):
|
|
531
|
+
a, b = i.split("=")
|
|
532
|
+
ref[a] = b
|
|
533
|
+
page_references.append(markup_citation(ref))
|
|
534
|
+
return f"<sup><a href='#ref{len(page_references)}'>[{len(page_references)}]</a></sup>"
|
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Settings."""
|
|
2
|
+
|
|
3
|
+
import typing as _typing
|
|
4
|
+
|
|
5
|
+
dir_path: _typing.Optional[str] = None
|
|
6
|
+
html_path: _typing.Optional[str] = None
|
|
7
|
+
template_path: _typing.Optional[str] = None
|
|
8
|
+
github_token: _typing.Optional[str] = None
|
|
9
|
+
|
|
10
|
+
owners: _typing.List[str] = []
|
|
11
|
+
editors: _typing.List[str] = []
|
|
12
|
+
contributors: _typing.List[_typing.Dict[str, str]] = []
|
|
13
|
+
url: _typing.Optional[str] = None
|
|
14
|
+
website_name: _typing.List[_typing.Optional[str]] = [None, None]
|
|
15
|
+
repo: _typing.Optional[str] = None
|
|
16
|
+
|
|
17
|
+
re_extras: _typing.List[_typing.Tuple[str, _typing.Callable]] = []
|
|
18
|
+
str_extras: _typing.List[_typing.Tuple[str, str]] = []
|
|
19
|
+
insert_links: _typing.Optional[_typing.Callable] = None
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Tools."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import typing
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
from webtools import settings
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def join(*folders):
|
|
11
|
+
"""Join multiple folders with os.path.join."""
|
|
12
|
+
if len(folders) == 1:
|
|
13
|
+
return folders[0]
|
|
14
|
+
|
|
15
|
+
return join(os.path.join(*folders[:2]), *folders[2:])
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def parse_metadata(content: str) -> typing.Tuple[typing.Dict[str, typing.Any], str]:
|
|
19
|
+
"""Parse metadata.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
content: Raw data
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Parsed metadata and content without metadata
|
|
26
|
+
"""
|
|
27
|
+
from webtools.markup import preprocess
|
|
28
|
+
|
|
29
|
+
metadata: typing.Dict[str, typing.Any] = {"title": None}
|
|
30
|
+
if content.startswith("--\n"):
|
|
31
|
+
metadata_in, content = content[3:].split("\n--\n", 1)
|
|
32
|
+
metadata.update(yaml.load(metadata_in, Loader=yaml.FullLoader))
|
|
33
|
+
content = preprocess(content.strip())
|
|
34
|
+
if metadata["title"] is None and content.startswith("# "):
|
|
35
|
+
metadata["title"] = content[2:].split("\n", 1)[0].strip()
|
|
36
|
+
return metadata, content
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def html_local(path: str) -> str:
|
|
40
|
+
"""Get the local HTML path of a absolute path.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
path: The absolute path
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Local HTML path
|
|
47
|
+
"""
|
|
48
|
+
assert settings.html_path is not None
|
|
49
|
+
assert path.startswith(settings.html_path)
|
|
50
|
+
return path[len(settings.html_path) :]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def comma_and_join(ls: typing.List[str], oxford_comma: bool = True) -> str:
|
|
54
|
+
"""Join a list with commas and an and between the last two items."""
|
|
55
|
+
if len(ls) == 1:
|
|
56
|
+
return ls[0]
|
|
57
|
+
if len(ls) == 2:
|
|
58
|
+
return f"{ls[0]} and {ls[1]}"
|
|
59
|
+
return ", ".join(ls[:-1]) + ("," if oxford_comma else "") + " and " + ls[-1]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def insert_author_info(content: str, authors: typing.List[str], url: str) -> str:
|
|
63
|
+
"""Insert author info into content.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
content: The content
|
|
67
|
+
authors: List of authors
|
|
68
|
+
url: A URL
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Content with authrso inserted
|
|
72
|
+
"""
|
|
73
|
+
assert content.startswith("# ")
|
|
74
|
+
title, content = content.split("\n", 1)
|
|
75
|
+
return f"{title}\n{{{{author-info::{';'.join(authors)}|{title[1:].strip()}|{url}}}}}\n{content}"
|