Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace Hugo shortcodes in OpenAPI output #2088

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ name: "Spec"

env:
HUGO_VERSION: 0.139.0
PYTHON_VERSION: 3.13

on:
push:
Expand Down Expand Up @@ -40,7 +41,7 @@ jobs:
- name: "➕ Setup Python"
uses: actions/setup-python@v5
with:
python-version: '3.9'
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
cache-dependency-path: scripts/requirements.txt
- name: "➕ Install dependencies"
Expand All @@ -59,7 +60,7 @@ jobs:
- name: "➕ Setup Python"
uses: actions/setup-python@v5
with:
python-version: '3.9'
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
cache-dependency-path: scripts/requirements.txt
- name: "➕ Install dependencies"
Expand All @@ -78,7 +79,7 @@ jobs:
- name: "➕ Setup Python"
uses: actions/setup-python@v5
with:
python-version: '3.9'
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
cache-dependency-path: scripts/requirements.txt
- name: "➕ Install dependencies"
Expand Down Expand Up @@ -120,7 +121,7 @@ jobs:
- name: "➕ Setup Python"
uses: actions/setup-python@v5
with:
python-version: '3.9'
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
cache-dependency-path: scripts/requirements.txt
- name: "➕ Install dependencies"
Expand Down Expand Up @@ -172,7 +173,7 @@ jobs:
- name: "➕ Setup Python"
uses: actions/setup-python@v5
with:
python-version: '3.9'
python-version: ${{ env.PYTHON_VERSION }}
- name: "➕ Install towncrier"
run: "pip install 'towncrier'"
- name: "Generate changelog"
Expand Down
1 change: 1 addition & 0 deletions changelogs/internal/newsfragments/2088.clarification
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Replace Hugo shortcodes in OpenAPI output.
111 changes: 106 additions & 5 deletions scripts/dump-openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,29 @@
scripts_dir = os.path.dirname(os.path.abspath(__file__))
api_dir = os.path.join(os.path.dirname(scripts_dir), "data", "api")

# Finds a Hugo shortcode in a string.
#
# A shortcode is defined as (newlines and whitespaces for presentation purpose):
#
# {{%
# <one or more whitespaces>
# <name of shortcode>
# <one or more whitespaces>
# (optional <list of parameters><one or more whitespaces>)
# %}}
#
# With:
#
# * <name of shortcode>: any word character and `-` and `/`.
# * <list of parameters>: any character except `}`, must not start or end with a
# whitespace.
shortcode_regex = re.compile(r"\{\{\%\s+(?P<name>[\w\/-]+)\s+(?:(?P<params>[^\s\}][^\}]+[^\s\}])\s+)?\%\}\}", re.ASCII)

# Parses the parameters of a Hugo shortcode.
#
# For simplicity, this currently only supports the `key="value"` format.
shortcode_params_regex = re.compile(r"(?P<key>\w+)=\"(?P<value>[^\"]+)\"", re.ASCII)

def prefix_absolute_path_references(text, base_url):
"""Adds base_url to absolute-path references.

Expand All @@ -44,17 +67,95 @@ def prefix_absolute_path_references(text, base_url):
"""
return text.replace("](/", "]({}/".format(base_url))

def edit_links(node, base_url):
"""Finds description nodes and makes any links in them absolute."""
def replace_match(text, match, replacement):
"""Replaces the regex match by the replacement in the text."""
return text[:match.start()] + replacement + text[match.end():]

def replace_shortcode(text, shortcode):
"""Replaces the shortcode by a Markdown fallback in the text.

The supported shortcodes are:

* boxes/note, boxes/rationale, boxes/warning
* added-in, changed-in
"""

if shortcode['name'].startswith("/"):
# This is the end of the shortcode, just remove it.
return replace_match(text, shortcode['match'], "")

match shortcode['name']:
case "boxes/note":
text = replace_match(text, shortcode['match'], "**NOTE:** ")
case "boxes/rationale":
text = replace_match(text, shortcode['match'], "**RATIONALE:** ")
case "boxes/warning":
text = replace_match(text, shortcode['match'], "**WARNING:** ")
case "added-in":
version = shortcode['params']['v']
if not version:
raise ValueError("Missing parameter `v` for `added-in` shortcode")

text = replace_match(text, shortcode['match'], f"**[Added in `v{version}`]** ")
case "changed-in":
version = shortcode['params']['v']
if not version:
raise ValueError("Missing parameter `v` for `changed-in` shortcode")

text = replace_match(text, shortcode['match'], f"**[Changed in `v{version}`]** ")
case _:
raise ValueError("Unknown shortcode", shortcode['name'])

return text


def find_and_replace_shortcodes(text):
"""Finds Hugo shortcodes and replaces them by a Markdown fallback.

The supported shortcodes are:

* boxes/note, boxes/rationale, boxes/warning
* added-in, changed-in
"""
# We use a `while` loop with `search` instead of a `for` loop with
# `finditer`, because as soon as we start replacing text, the
# indices of the match are invalid.
while match := shortcode_regex.search(text):
# Parse the parameters of the shortcode
params = {}
if match['params']:
for param in shortcode_params_regex.finditer(match['params']):
if param['key']:
params[param['key']] = param['value']

shortcode = {
'name': match['name'],
'params': params,
'match': match,
}
text = replace_shortcode(text, shortcode)

return text

def edit_descriptions(node, base_url):
"""Finds description nodes and apply fixes to them.

The fixes that are applied are:

* Make links absolute
* Replace shortcodes
"""
if isinstance(node, dict):
for key in node:
if isinstance(node[key], str):
node[key] = prefix_absolute_path_references(node[key], base_url)
node[key] = find_and_replace_shortcodes(node[key])
else:
edit_links(node[key], base_url)
edit_descriptions(node[key], base_url)
elif isinstance(node, list):
for item in node:
edit_links(item, base_url)
edit_descriptions(item, base_url)


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

edit_links(output, base_url)
edit_descriptions(output, base_url)

print("Generating %s" % output_file)

Expand Down
Loading