Skip to content

Commit fe46e0c

Browse files
authored
Replace Hugo shortcodes in OpenAPI output (#2088)
Signed-off-by: Kévin Commaille <[email protected]>
1 parent a8c3269 commit fe46e0c

File tree

3 files changed

+114
-10
lines changed

3 files changed

+114
-10
lines changed

.github/workflows/main.yml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ name: "Spec"
22

33
env:
44
HUGO_VERSION: 0.139.0
5+
PYTHON_VERSION: 3.13
56

67
on:
78
push:
@@ -40,7 +41,7 @@ jobs:
4041
- name: "➕ Setup Python"
4142
uses: actions/setup-python@v5
4243
with:
43-
python-version: '3.9'
44+
python-version: ${{ env.PYTHON_VERSION }}
4445
cache: 'pip'
4546
cache-dependency-path: scripts/requirements.txt
4647
- name: "➕ Install dependencies"
@@ -59,7 +60,7 @@ jobs:
5960
- name: "➕ Setup Python"
6061
uses: actions/setup-python@v5
6162
with:
62-
python-version: '3.9'
63+
python-version: ${{ env.PYTHON_VERSION }}
6364
cache: 'pip'
6465
cache-dependency-path: scripts/requirements.txt
6566
- name: "➕ Install dependencies"
@@ -78,7 +79,7 @@ jobs:
7879
- name: "➕ Setup Python"
7980
uses: actions/setup-python@v5
8081
with:
81-
python-version: '3.9'
82+
python-version: ${{ env.PYTHON_VERSION }}
8283
cache: 'pip'
8384
cache-dependency-path: scripts/requirements.txt
8485
- name: "➕ Install dependencies"
@@ -120,7 +121,7 @@ jobs:
120121
- name: "➕ Setup Python"
121122
uses: actions/setup-python@v5
122123
with:
123-
python-version: '3.9'
124+
python-version: ${{ env.PYTHON_VERSION }}
124125
cache: 'pip'
125126
cache-dependency-path: scripts/requirements.txt
126127
- name: "➕ Install dependencies"
@@ -172,7 +173,7 @@ jobs:
172173
- name: "➕ Setup Python"
173174
uses: actions/setup-python@v5
174175
with:
175-
python-version: '3.9'
176+
python-version: ${{ env.PYTHON_VERSION }}
176177
- name: "➕ Install towncrier"
177178
run: "pip install 'towncrier'"
178179
- name: "Generate changelog"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Replace Hugo shortcodes in OpenAPI output.

scripts/dump-openapi.py

Lines changed: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,35 @@
3232
scripts_dir = os.path.dirname(os.path.abspath(__file__))
3333
api_dir = os.path.join(os.path.dirname(scripts_dir), "data", "api")
3434

35+
# Finds a Hugo shortcode in a string.
36+
#
37+
# A shortcode is defined as (newlines and whitespaces for presentation purpose):
38+
#
39+
# {{%
40+
# <zero or more whitespaces>
41+
# <name of shortcode>
42+
# (optional <one or more whitespaces><list of parameters>)
43+
# <zero or more whitespaces>
44+
# %}}
45+
#
46+
# With:
47+
#
48+
# * <name of shortcode>: any word character and `-` and `/`. `re.ASCII` is used to only match
49+
# ASCII characters in the name.
50+
# * <list of parameters>: any character except `}`, must not start or end with a
51+
# whitespace.
52+
shortcode_regex = re.compile(r"""\{\{\% # {{%
53+
\s* # zero or more whitespaces
54+
(?P<name>[\w/-]+) # name of shortcode
55+
(?:\s+(?P<params>[^\s\}][^\}]+[^\s\}]))? # optional list of parameters
56+
\s* # zero or more whitespaces
57+
\%\}\} # %}}""", re.ASCII | re.VERBOSE)
58+
59+
# Parses the parameters of a Hugo shortcode.
60+
#
61+
# For simplicity, this currently only supports the `key="value"` format.
62+
shortcode_params_regex = re.compile(r"(?P<key>\w+)=\"(?P<value>[^\"]+)\"", re.ASCII)
63+
3564
def prefix_absolute_path_references(text, base_url):
3665
"""Adds base_url to absolute-path references.
3766
@@ -44,17 +73,90 @@ def prefix_absolute_path_references(text, base_url):
4473
"""
4574
return text.replace("](/", "]({}/".format(base_url))
4675

47-
def edit_links(node, base_url):
48-
"""Finds description nodes and makes any links in them absolute."""
76+
def replace_match(match, replacement):
77+
"""Replaces the regex match by the replacement in the text."""
78+
return match.string[:match.start()] + replacement + match.string[match.end():]
79+
80+
def replace_shortcode(shortcode):
81+
"""Replaces the shortcode by a Markdown fallback in the text.
82+
83+
The supported shortcodes are:
84+
85+
* boxes/note, boxes/rationale, boxes/warning
86+
* added-in, changed-in
87+
88+
All closing tags (`{{ /shortcode }}`) are replaced with the empty string.
89+
"""
90+
91+
if shortcode['name'].startswith("/"):
92+
# This is the end of the shortcode, just remove it.
93+
return replace_match(shortcode, "")
94+
95+
# Parse the parameters of the shortcode
96+
params = {}
97+
if shortcode['params']:
98+
for param in shortcode_params_regex.finditer(shortcode['params']):
99+
if param['key']:
100+
params[param['key']] = param['value']
101+
102+
match shortcode['name']:
103+
case "boxes/note":
104+
return replace_match(shortcode, "**NOTE:** ")
105+
case "boxes/rationale":
106+
return replace_match(shortcode, "**RATIONALE:** ")
107+
case "boxes/warning":
108+
return replace_match(shortcode, "**WARNING:** ")
109+
case "added-in":
110+
version = params['v']
111+
if not version:
112+
raise ValueError("Missing parameter `v` for `added-in` shortcode")
113+
114+
return replace_match(shortcode, f"**[Added in `v{version}`]** ")
115+
case "changed-in":
116+
version = params['v']
117+
if not version:
118+
raise ValueError("Missing parameter `v` for `changed-in` shortcode")
119+
120+
return replace_match(shortcode, f"**[Changed in `v{version}`]** ")
121+
case _:
122+
raise ValueError("Unknown shortcode", shortcode['name'])
123+
124+
125+
def find_and_replace_shortcodes(text):
126+
"""Finds Hugo shortcodes and replaces them by a Markdown fallback.
127+
128+
The supported shortcodes are:
129+
130+
* boxes/note, boxes/rationale, boxes/warning
131+
* added-in, changed-in
132+
"""
133+
# We use a `while` loop with `search` instead of a `for` loop with
134+
# `finditer`, because as soon as we start replacing text, the
135+
# indices of the match are invalid.
136+
while shortcode := shortcode_regex.search(text):
137+
text = replace_shortcode(shortcode)
138+
139+
return text
140+
141+
def edit_descriptions(node, base_url):
142+
"""Finds description nodes and apply fixes to them.
143+
144+
The fixes that are applied are:
145+
146+
* Make links absolute
147+
* Replace Hugo shortcodes
148+
"""
49149
if isinstance(node, dict):
50150
for key in node:
51151
if isinstance(node[key], str):
52152
node[key] = prefix_absolute_path_references(node[key], base_url)
153+
node[key] = find_and_replace_shortcodes(node[key])
53154
else:
54-
edit_links(node[key], base_url)
155+
edit_descriptions(node[key], base_url)
55156
elif isinstance(node, list):
56157
for item in node:
57-
edit_links(item, base_url)
158+
edit_descriptions(item, base_url)
159+
58160

59161
parser = argparse.ArgumentParser(
60162
"dump-openapi.py - assemble the OpenAPI specs into a single JSON file"
@@ -164,7 +266,7 @@ def edit_links(node, base_url):
164266
if untagged != 0:
165267
print("{} untagged operations, you may want to look into fixing that.".format(untagged))
166268

167-
edit_links(output, base_url)
269+
edit_descriptions(output, base_url)
168270

169271
print("Generating %s" % output_file)
170272

0 commit comments

Comments
 (0)