Files
dokku/docs/_build/hooks.py
Jose Diaz-Gonzalez 0c83356542 refactor: move docs rewrite into mkdocs hooks
This will ensure `mkdocs build` does not rewrite content, making contribution easier.
2024-01-30 06:53:14 -05:00

408 lines
12 KiB
Python

#!/usr/bin/env python
import posixpath
import re
from collections.abc import Callable
from re import Match
from mkdocs.config.defaults import MkDocsConfig
from mkdocs.structure.files import File, Files
from mkdocs.structure.pages import Page
def on_page_markdown(markdown: str, *, page: Page, config: MkDocsConfig, files: Files) -> str | None:
resp = re_on_page_markdown(markdown, page=page, config=config, files=files)
if not resp:
return resp
modifiers: list[Callable[[list[str], str], tuple[list[str], bool]]] = [
modify_content_noop,
modify_content_links,
modify_content_stripspace,
modify_content_admonition,
modify_content_terminal_example,
]
is_modified = False
lines = resp.split("\n")
for modifier in modifiers:
lines, modified = modifier(lines, page.file.src_path)
if modified:
is_modified = True
if is_modified:
resp = "\n".join(lines)
return resp
def modify_content_noop(lines: list[str], _) -> tuple[list[str], bool]:
"""
Simply returns the lines as is
"""
modified = False
updated_lines = []
for line in lines:
updated_lines.append(line)
return updated_lines, modified
def modify_content_links(lines: list[str], filename: str) -> tuple[list[str], bool]:
"""
Modifies links to be relative instead of absolute
"""
filename = filename.replace("/usr/src/source/docs/", "")
parts = filename.split("/")
parts.pop()
replacement = "](" + "/".join([".." for _ in parts]) + "/"
modified = False
updated_lines = []
for line in lines:
if "](/docs/" in line:
line = line.replace("](/docs/", replacement)
modified = True
updated_lines.append(line)
return updated_lines, modified
def modify_content_stripspace(lines: list[str], _) -> tuple[list[str], bool]:
"""
Removes trailing whitespace from each line
"""
modified = False
updated_lines = []
for line in lines:
line = line.rstrip()
updated_lines.append(line)
return updated_lines, modified
def is_github_new(line: str, next_line: str | None) -> bool:
"""
Checks if a given line is a github "new as of" admonition
"""
if not next_line:
return False
return line.startswith("> [!IMPORTANT]") and "new as of" in next_line.lower()
def is_github_note(line: str) -> bool:
"""
Checks if a given line is a github "note" admonition
"""
return line.startswith("> [!NOTE]")
def is_github_warning(line: str) -> bool:
"""
Checks if a given line is a github "warning" admonition
"""
return line.startswith("> [!WARNING]")
def is_info(line: str) -> bool:
"""
Checks if a given line is an "info" admonition
"""
return line.startswith("> ")
def is_new(line: str) -> bool:
"""
Checks if a given line is a "new as of" admonition
"""
return line.startswith("> ") and "new as of" in line.lower()
def is_note(line: str) -> bool:
"""
Checks if a given line is a "note" admonition
"""
return line.startswith("> Note:")
def is_warning(line: str) -> bool:
"""
Checks if a given line is a "warning" admonition
"""
return line.startswith("> Warning:")
def modify_content_admonition(lines: list[str], filename: str) -> tuple[list[str], bool]:
"""
Applies adminition info to each line in the output
"""
modified = False
updated_lines = []
admonition_lines = []
is_admonition = False
replace_new_in_next_line = False
for index, line in enumerate(lines):
next_line: str | None = None
if index + 1 < len(lines):
next_line = lines[index + 1]
if replace_new_in_next_line:
line = line.replace("New as of", "Introduced in")
line = line.replace("new as of", "introduced in")
replace_new_in_next_line = False
if is_github_new(line, next_line):
line = line.replace("> [!IMPORTANT]", '!!! tip "New"')
is_admonition = True
admonition_lines.append(line)
admonition_lines.append("")
replace_new_in_next_line = True
elif is_github_note(line):
line = line.replace("> [!NOTE]", '!!! note "Note"')
is_admonition = True
admonition_lines.append(line)
admonition_lines.append("")
elif is_github_warning(line):
line = line.replace("> [!WARNING]", '!!! warning "Warning"')
is_admonition = True
admonition_lines.append(line)
admonition_lines.append("")
elif is_new(line):
print("is_new")
line = line.replace("> ", '!!! tip "New"\n\n ')
line = line.replace("New as of", "Introduced in")
line = line.replace("new as of", "introduced in")
is_admonition = True
admonition_lines.append(line)
elif is_note(line):
print("is_note")
line = line.replace("> Note: ", '!!! note "Note"\n\n ')
is_admonition = True
admonition_lines.append(line)
elif is_warning(line):
print("is_warning")
line = line.replace("> Warning: ", '!!! warning "Warning"\n\n ')
is_admonition = True
admonition_lines.append(line)
elif is_info(line):
if not is_admonition:
line = line.replace("> ", '!!! info "Info"\n\n ')
elif line in [">", "> "]:
line = ""
else:
line = " " + line.removeprefix("> ")
is_admonition = True
admonition_lines.append(line)
elif is_admonition and line in [">", "> "]:
line = ""
admonition_lines.append(line)
elif is_admonition and line.startswith("> "):
line = " " + line.removeprefix("> ")
admonition_lines.append(line)
elif line == "":
is_admonition = False
if len(admonition_lines) > 0:
modified = True
updated_lines.extend(admonition_lines)
admonition_lines = []
updated_lines.append("")
else:
updated_lines.append(line)
return updated_lines, modified
def is_shell_codeblock_start(line: str) -> bool:
"""
Checks to see if a line starts a codeblock
"""
return line == "```shell"
def modify_content_terminal_example(lines: list[str], _) -> tuple[list[str], bool]:
"""
Modifies content so that terminal output is shown appropriately
"""
modified = False
updated_lines = []
command_block = []
example_block = []
in_command_block = False
in_example_block = False
previous_block = ""
next_line_must_be = None
for line in lines:
if is_shell_codeblock_start(line):
command_block.append(line)
modified = True
in_command_block = True
continue
elif in_command_block:
command_block.append(line)
if line == "```":
in_command_block = False
previous_block = "command_block"
next_line_must_be = ""
continue
elif line == "```":
if previous_block == "":
updated_lines.append(line)
continue
if previous_block == "command_block":
example_block.append(line)
if in_example_block:
previous_block = ""
in_example_block = False
updated_lines.append('=== "Shell"')
updated_lines.append("")
for command_line in command_block:
updated_lines.append(f" {command_line}")
command_block = []
updated_lines.append("")
updated_lines.append('=== "Output"')
updated_lines.append("")
for example_line in example_block:
updated_lines.append(f" {example_line}")
example_block = []
else:
in_example_block = True
continue
elif previous_block == "command_block":
if next_line_must_be is None:
if in_example_block:
example_block.append(line)
else:
updated_lines.extend(command_block)
updated_lines.append("")
updated_lines.append(line)
command_block = []
previous_block = ""
continue
if next_line_must_be == "":
if line == "":
next_line_must_be = None
continue
updated_lines.append(line)
if len(command_block) > 0:
updated_lines.extend(command_block)
return updated_lines, modified
def re_on_page_markdown(markdown: str, *, page: Page, config: MkDocsConfig, files: Files) -> str | None:
"""
This hook is called after the page's markdown is loaded from file and can be
"""
def replace(match: Match):
badge_type, args = match.groups()
args = args.strip()
if badge_type == "version":
return _badge_for_version(args, page, files)
if badge_type == "scheduler":
return _badge_for_scheduler(args, page, files)
if type == "flag":
return flag(args, page, files)
# Otherwise, raise an error
raise RuntimeError(f"Unknown shortcode: {badge_type}")
return re.sub(r"<!-- md:(\w+)(.*?) -->", replace, markdown, flags=re.I | re.M)
def _badge(icon: str, text: str = "", badge_type: str = ""):
"""
Create badge
"""
classes = f"mdx-badge mdx-badge--{badge_type}" if badge_type else "mdx-badge"
text = f"{text}{{ data-preview='' }}" if text.endswith(")") else text
return "".join(
[
f'<span class="{classes}">',
*([f'<span class="mdx-badge__icon">{icon}</span>'] if icon else []),
*([f'<span class="mdx-badge__text">{text}</span>'] if text else []),
"</span>",
]
)
def _badge_for_scheduler(text: str, page: Page, files: Files):
"""
Create badge for scheduler
"""
# Return badge
icon = "simple-docker"
if text in ["kubernetes", "k3s"]:
icon = "simple-kubernetes"
text = "k3s"
elif text == "nomad":
icon = "simple-nomad"
elif text in ["docker", "docker-local"]:
icon = "simple-docker"
text = "docker-local"
href = f"https://dokku.com/docs/deployment/schedulers/{text}/"
return _badge(
icon=f"[:{icon}:]({href} '{text}')",
text=f"[{text}]({href})",
)
def _badge_for_version(text: str, page: Page, files: Files):
"""
Create badge for version
"""
# Return badge
icon = "material-tag-outline"
href = f"https://github.com/dokku/dokku/releases/tag/v{text}"
return _badge(
icon=f"[:{icon}:]({href} 'Since {text}')",
text=f"[{text}]({href})",
)
def flag(args: str, page: Page, files: Files):
"""
Create flag
"""
flag_type, *_ = args.split(" ", 1)
if flag_type == "experimental":
return _badge_for_experimental(page, files)
raise RuntimeError(f"Unknown flag type: {flag_type}")
def _badge_for_experimental(page: Page, files: Files):
"""
Create badge for experimental
"""
icon = "material-flask-outline"
href = _resolve_path("conventions.md#experimental", page, files)
return _badge(icon=f"[:{icon}:]({href} 'Experimental')")
def _resolve_path(path: str, page: Page, files: Files):
"""
Resolve path of file relative to given page - the posixpath always includes
one additional level of `..` which we need to remove
"""
path, anchor, *_ = f"{path}#".split("#")
path = _resolve(files.get_file_from_path(path), page)
return "#".join([path, anchor]) if anchor else path
def _resolve(file: File, page: Page):
"""
Resolve path of file relative to given page - the posixpath always includes
one additional level of `..` which we need to remove
"""
path = posixpath.relpath(file.src_uri, page.file.src_uri)
return posixpath.sep.join(path.split(posixpath.sep)[1:])