Files
dokku/contrib/write-mkdocs

456 lines
14 KiB
Plaintext
Raw Normal View History

#!/usr/bin/env python
import os
import pathlib
import shutil
import sys
from collections.abc import Callable
import yaml
from bs4 import BeautifulSoup
def get_nav_from_selector(soup: BeautifulSoup, selector: str, docs_dir: str):
"""
Generates navigation from template file
"""
navigation = []
header_name = ""
for anchor in soup.select(selector):
classes = anchor.get("class", [])
if classes is None:
continue
if "disabled" in classes:
header_name = anchor.text
navigation.append({header_name: []})
href = anchor.get("href", "")
if href is None:
continue
href = str(href)
if href == "#":
continue
url = href.replace("{{NAME}}", "")
filename = url.replace("http://progrium.viewdocs.io/dokku/", "")
filename = filename.replace("http://dokku.viewdocs.io/dokku/", "")
filename = filename.strip("/") + ".md"
filename = filename.removeprefix("dokku/")
ignore_errors_for = [
"getting-started/installation.md",
"getting-started/upgrading.md",
]
if not os.path.exists(docs_dir + "/" + filename):
if filename not in ignore_errors_for:
print("error fetching markdown file:", filename)
continue
for nav in navigation:
if header_name in nav:
if filename == "getting-started/installation.md":
child_dir = "getting-started/install/"
children = os.listdir(docs_dir + "/" + child_dir)
children = [child_dir + c for c in children]
2022-09-10 16:11:16 -04:00
children.sort()
children.insert(0, "getting-started/advanced-installation.md")
if os.path.exists(docs_dir + "/" + filename):
children.insert(0, filename)
else:
children.insert(0, "getting-started/installation/index.md")
nav[header_name].append(
{
"Getting Started with Dokku": children,
}
)
continue
if filename == "getting-started/upgrading.md":
child_dir = "appendices/"
children = os.listdir(docs_dir + "/" + child_dir)
children.sort(
key=lambda x: list(map(int, x.split("-")[0].split("."))),
reverse=True,
)
children = [child_dir + c for c in children]
if os.path.exists(docs_dir + "/" + filename):
children.insert(0, filename)
else:
children.insert(0, "getting-started/upgrading/index.md")
nav[header_name].append(
{
"Upgrading": children,
}
)
continue
nav[header_name].append(filename)
return navigation
def generate_nav(src: str, dest: str) -> None:
"""
Writes out navigation information to the mkdocs yaml file
"""
navigation = []
repo_dir = pathlib.Path(__file__).parent.parent.resolve()
docs_dir = str(repo_dir) + "/docs"
with open(docs_dir + "/template.html", encoding="utf-8") as response:
soup = BeautifulSoup(response, "html.parser")
selectors = [
".container .row .list-group a",
".container-fluid .row .list-group a",
]
for selector in selectors:
navigation = get_nav_from_selector(soup, selector, docs_dir)
if len(navigation) > 0:
break
if len(navigation) == 0:
print("No navigation found")
sys.exit(1)
with open(src, encoding="utf-8") as handler:
data = yaml.unsafe_load(handler)
data["nav"] = [
{"Docs": navigation},
{"Pro": "https://pro.dokku.com/docs/getting-started/"},
{"Blog": "https://dokku.com/blog/"},
2022-12-05 05:53:10 -05:00
{"Tutorials": "https://dokku.com/tutorials/"},
{
"Purchase Dokku Pro": "https://dokku.dpdcart.com/cart/add?product_id=217344&method_id=236878"
},
]
with open(dest, mode="wt", encoding="utf-8") as handler:
yaml.dump(data, handler)
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 modify_content_inject_newlines(lines: list[str], _) -> tuple[list[str], bool]:
"""
Ensures every line has a trailing newline character
"""
modified = False
updated_lines = []
for line in lines:
2022-09-05 15:38:49 -04:00
updated_lines.append(line.rstrip() + "\n")
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], _) -> 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)
2022-09-05 15:38:49 -04:00
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 update_markdown(src):
"""
Updates all markdown files to be mkdocs compatible
"""
markdown_files = []
allowed_extensions = [".md"]
for subdir, _, files in os.walk(src):
for file in files:
ext = os.path.splitext(file)[-1].lower()
file_path = os.path.join(subdir, file)
if ext in allowed_extensions:
markdown_files.append(file_path)
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,
modify_content_inject_newlines,
]
for file in markdown_files:
is_modified = False
lines = []
with open(file, "r", encoding="utf-8") as handler:
lines = handler.readlines()
for modifier in modifiers:
lines, modified = modifier(lines, file)
if modified:
is_modified = True
if is_modified:
with open(file, "w", encoding="utf-8") as handler:
handler.writelines(lines)
def main():
"""
Main command that performs doc manipulation
"""
print("----> Copying mkdocs.yml")
print(" Generating navigation")
generate_nav("/usr/src/source/mkdocs.yml", "/usr/src/app/mkdocs.yml")
print("----> Copying docs folder")
update_markdown(
"/usr/src/source/docs",
)
if os.path.exists("/usr/src/app/docs"):
print(" Removing old docs folder")
shutil.rmtree("/usr/src/app/docs")
print(" Performing copy")
shutil.copytree("/usr/src/source/docs", "/usr/src/app/docs")
if __name__ == "__main__":
main()