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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,2 @@
1
+ def test_import():
2
+ import webtools # noqa: F401
@@ -0,0 +1,9 @@
1
+ """Test markup."""
2
+
3
+ from webtools.code_markup import code_highlight
4
+ from webtools.markup import markup
5
+
6
+
7
+ def test_code_highlight_cpp():
8
+ assert "#&lt;" in code_highlight("#<include>", "cpp")
9
+ assert "#&lt;" in markup("```cpp\n#<include>\n```\n")
@@ -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,13 @@
1
+ PyGithub
2
+ pytz
3
+ pyyaml
4
+
5
+ [ci]
6
+ website-build-tools[style,test]
7
+
8
+ [style]
9
+ ruff
10
+ mypy
11
+
12
+ [test]
13
+ pytest
@@ -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']}&ndash;{r['pageend']}"
49
+ elif "arxiv" in r:
50
+ out += f", ar&Chi;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:&nbsp;<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("&oslash;", "{\\o}")
102
+ txt = txt.replace("&ndash;", "--")
103
+ txt = txt.replace("&mdash;", "---")
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"(&nbsp;|^)({keyword})(&nbsp;|$)",
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?(?:&nbsp;-m&nbsp;.+?)?&nbsp;)", 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"(&nbsp;|^)({keyword})(&nbsp;|$)",
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
+ (" ", "&nbsp;"),
140
+ ("<", "&lt;"),
141
+ (">", "&gt;"),
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"&nbsp;{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"&nbsp;{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"&nbsp;{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"&nbsp;@{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"&nbsp;@{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"&nbsp;@{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"&nbsp;{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'>&#10004;</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(" ", "&nbsp;") 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'>&darr; Cite this page &darr;</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'>&uarr; Hide citation info &uarr;</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}"