Skip to content

Commit aa49ade

Browse files
ningitnarudocap
andcommitted
Initial contents
Co-authored-by: Paco Durán <narudocap@gmail.com>
0 parents  commit aa49ade

25 files changed

+1709
-0
lines changed

.github/workflows/publish.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Publish
2+
3+
permissions:
4+
contents: read
5+
pages: write
6+
id-token: write
7+
8+
on:
9+
push:
10+
branches: [ 'main' ]
11+
12+
jobs:
13+
build:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Check out repository code
17+
uses: actions/checkout@v3
18+
19+
- name: Build website
20+
run: |
21+
# wget https://github.com/maude-lang/maude-lang.github.io/releases/download/maude/old-maude-website.tar.xz
22+
mkdir output
23+
# tar -xf old-maude-website.tar.xz -C output
24+
pip install -r requirements.txt
25+
python generate.py --prefix /maudeweb/ --no-ext
26+
touch output/.nojekyll
27+
28+
- name: Upload static files as artifact
29+
uses: actions/upload-pages-artifact@v3
30+
with:
31+
path: output
32+
33+
deploy:
34+
environment:
35+
name: github-pages
36+
url: ${{ steps.deployment.outputs.page_url }}
37+
runs-on: ubuntu-latest
38+
needs: build
39+
steps:
40+
- name: Deploy to GitHub pages
41+
id: development
42+
uses: actions/deploy-pages@v4

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
## Maude website source
2+
3+
This is the source code of the Maude website. Every page is a Markdown file in the `pages` directory, `static` includes the static files (images, stylesheets, etc.), and general settings are written in `site.toml`. The website is built by `generate.py` using the HTML template in `templates` and some external libraries.
4+
```console
5+
$ python generate.py
6+
```
7+
The Maude 1 website and some tool pages are downloaded as a compressed bundle from the releases of this repository and merged with the new generated page. Other static resources are also stored in the releases and linked from the Markdown pages with paths of the form `:tag/name`.

generate.py

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
#
2+
# Maude web generator
3+
#
4+
5+
# Possible improvements:
6+
# - Include the update date based on the last modification in Git
7+
8+
import asyncio
9+
from datetime import datetime
10+
from pathlib import Path
11+
import re
12+
import shutil
13+
import sys
14+
import tomllib
15+
16+
from jinja2 import Environment, PackageLoader, select_autoescape
17+
from latex2mathml.converter import convert as latex2mathml
18+
from markdown_it import MarkdownIt
19+
from mdit_py_plugins.anchors import anchors_plugin
20+
from mdit_py_plugins.attrs import attrs_block_plugin, attrs_plugin
21+
from mdit_py_plugins.dollarmath import dollarmath_plugin
22+
from mdit_py_plugins.front_matter import front_matter_plugin
23+
import yaml
24+
# from pybtex.database import parse_file as parse_bibtex_file
25+
26+
# Source directory for the website pages in Markdown
27+
SOURCE = Path('pages')
28+
# Extension for HTML files
29+
HTML_EXTENSION = '.html'
30+
# Regex for citations in Pandoc format
31+
CITE_REGEX = re.compile(r'\[@([A-Za-z0-9_-]+)\]')
32+
33+
34+
def sync_to(origin: Path, destination: Path):
35+
"""Move all files from origin to destination"""
36+
37+
for root, _, files in origin.walk():
38+
other_root = destination / root.relative_to(origin)
39+
40+
if not other_root.exists():
41+
other_root.mkdir()
42+
43+
for elem in files:
44+
# Original file and its copy in the destination directory
45+
original = root / elem
46+
other = other_root / elem
47+
48+
if not other.exists() or original.stat().st_mtime > other.stat().st_mtime:
49+
shutil.copy(original, other)
50+
51+
52+
def citation_rule(state, silent):
53+
"""Rule to parse citations"""
54+
55+
# Look for [@name]
56+
if (m := CITE_REGEX.match(state.src[state.pos:])) is None:
57+
return False
58+
59+
name = m.group(1)
60+
token = state.push('cite', '', 0)
61+
token.content = name
62+
63+
state.pos += len(m.group(0))
64+
return True
65+
66+
67+
def render_citation(self, tokens, idx, options, env):
68+
"""Render a citation"""
69+
70+
name = tokens[idx].content
71+
return f'[<cite>{name}</cite>]'
72+
73+
74+
def render_link(self, tokens, idx, options, env):
75+
"""Render a link"""
76+
77+
token = tokens[idx]
78+
target = token.attrs['href']
79+
80+
class_attr = ''
81+
82+
# External link
83+
if target.startswith('http'):
84+
class_attr = ' class="external-link"'
85+
86+
# Markdown pages
87+
elif target.endswith('.md'):
88+
# Make broken links visible
89+
if not (SOURCE / target).exists():
90+
class_attr = ' class="broken-link"'
91+
92+
target = env['generator'].make_page_link(target[:-3])
93+
94+
# Resource
95+
elif target.startswith(':') and '/' in target:
96+
prefix, name = target.split('/', maxsplit=1)
97+
target = env['generator'].make_resource_link(prefix[1:], name)
98+
99+
return f'<a href="{target}"{class_attr}>'
100+
101+
102+
def render_math_inline(self, tokens, idx, options, env):
103+
"""Render math"""
104+
105+
content = tokens[idx].content
106+
return latex2mathml(content)
107+
108+
109+
class SiteGenerator:
110+
"""Generator of the Maude website"""
111+
112+
def __init__(self, args, config):
113+
# Save the configuration and arguments
114+
self.config = config
115+
self.output_path = args.o
116+
self.prefix = args.prefix
117+
self.current_prefix = args.prefix
118+
self.use_extension = args.ext
119+
120+
# Generate templates for this
121+
self.env = Environment(
122+
loader=PackageLoader('generate'),
123+
autoescape=select_autoescape()
124+
)
125+
126+
# Load the general template
127+
self.site_tmpl = self.env.get_template('site.htm')
128+
129+
# Prepare the Markdown renderer
130+
self.md = (
131+
MarkdownIt('commonmark', {'html': True})
132+
.enable('table')
133+
.use(dollarmath_plugin)
134+
.use(anchors_plugin)
135+
.use(attrs_plugin)
136+
.use(attrs_block_plugin)
137+
.use(front_matter_plugin)
138+
)
139+
140+
self.md.inline.ruler.push('cite', citation_rule)
141+
self.md.add_render_rule('link_open', render_link)
142+
self.md.add_render_rule('math_inline', render_math_inline)
143+
self.md.add_render_rule('cite', render_citation)
144+
145+
# Bibliography
146+
self.bibliography = {}
147+
148+
def make_page_link(self, path):
149+
"""Make an internal page link"""
150+
151+
# Make index page links point to the root
152+
if not self.use_extension and path == 'index':
153+
return self.current_prefix
154+
155+
return f'{self.current_prefix}{path}{HTML_EXTENSION if self.use_extension else ""}'
156+
157+
def make_resource_link(self, ns, name):
158+
"""Make a link to a resource"""
159+
160+
return self.config.get('release-path', 'missing').format(ns=ns, name=name)
161+
162+
async def index_site(self, generated_files):
163+
"""Index the website for search"""
164+
165+
from pagefind.index import PagefindIndex, IndexConfig
166+
167+
index_config = IndexConfig(output_path=str(self.output_path / 'pagefind'))
168+
169+
# Pagefind seems to add the prefix, so we do not do it again
170+
real_prefix, self.current_prefix = self.current_prefix, ''
171+
172+
async with PagefindIndex(config=index_config) as index:
173+
# Index each generated file
174+
for file in generated_files:
175+
content = file.read_text()
176+
url = file.relative_to(self.output_path)
177+
178+
await index.add_html_file(
179+
content=content,
180+
url=self.make_page_link(url.with_suffix('')),
181+
source_path=str(url),
182+
)
183+
184+
self.current_prefix = real_prefix
185+
186+
def generate(self):
187+
"""Generate the site"""
188+
189+
# Copy static files
190+
sync_to(Path('static'), self.output_path)
191+
192+
# Load bibliography
193+
# for source in self.config.get('bibliography', ()):
194+
# self.bibliography[source] = parse_bibtex_file(source)
195+
196+
now = datetime.now()
197+
toc = self.config['toc']
198+
generated_files = []
199+
200+
for page in SOURCE.rglob('*.md'):
201+
# Parse the input file
202+
page_tree = self.md.parse(page.read_text())
203+
204+
# Page the page metadata
205+
metadata = {}
206+
207+
if page_tree and page_tree[0].type == 'front_matter':
208+
metadata = yaml.safe_load(page_tree[0].content)
209+
210+
# Look for the title header
211+
title = metadata.get('title', page.name.title())
212+
213+
# Render content to HTML5
214+
html = self.md.renderer.render(page_tree, self.md.options, env={'generator': self})
215+
216+
# Adjust prefix
217+
if self.prefix is None:
218+
self.current_prefix = f'{SOURCE.relative_to(page.parent, walk_up=True)}/' if page.parent != SOURCE else ''
219+
220+
# Render the full page with Jinja
221+
source_path = page.relative_to(SOURCE)
222+
target_path = self.output_path / source_path.with_suffix(HTML_EXTENSION)
223+
target_path.parent.mkdir(exist_ok=True)
224+
225+
self.site_tmpl.stream(
226+
current=str(source_path.with_suffix('')), # current page path
227+
content=html, # page content
228+
toc=toc, # table of contents
229+
year=now.year, # current year
230+
title=title, # page title
231+
prefix=self.current_prefix, # prefix of the site
232+
make_page_link=self.make_page_link, # page link builder
233+
).dump(str(target_path))
234+
235+
generated_files.append(target_path)
236+
237+
# Generate the index
238+
asyncio.run(self.index_site(generated_files))
239+
240+
241+
def main():
242+
import argparse
243+
244+
parser = argparse.ArgumentParser(description='Maude website generator')
245+
parser.add_argument('-o', help='Output path for the generated site', type=Path, default=Path('output'))
246+
parser.add_argument('--prefix', '-p', help='Web server prefix')
247+
parser.add_argument('--no-ext', help='Omit extensions for internal links', action='store_false', dest='ext')
248+
249+
args = parser.parse_args()
250+
251+
# Create the output directory
252+
args.o.mkdir(exist_ok=True)
253+
254+
# Load the site settings
255+
with open('site.toml', 'rb') as tomlf:
256+
config = tomllib.load(tomlf)
257+
258+
SiteGenerator(args, config).generate()
259+
260+
261+
if __name__ == '__main__':
262+
sys.exit(main())

0 commit comments

Comments
 (0)