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

📝 Update markdown includes to use the new simpler format #1054

Merged
merged 4 commits into from
Nov 18, 2024

Conversation

tiangolo
Copy link
Member

@tiangolo tiangolo commented Nov 18, 2024

📝 Update markdown includes to use the new simpler format from markdown-include-variants.

This was automated with this script, copying the process from FastAPI:

import re
from dataclasses import dataclass
from pathlib import Path
from typing import Optional, Tuple

import typer


class InvalidFormatError(Exception):
    """
    Invalid format
    """


def get_hl_lines(old_first_line: str) -> Optional[str]:
    """
    Takes lines such as:
    ```Python hl_lines="3"
    and extracts the hl_ines part and returns the new hlines string "3" in this example.
    For groups of likes such as
    ```Python hl_lines="5-10"
    it needs to convert the - to a : so in this example it would be "5:10".
    For multiple lines it needs to seperate them by comma, such as:
    ```Python hl_lines="3   5-10   12"
    would be "3,5:10,12".
    For examples with no hlighted lines such as
    ```Python
    it should return None.
    """
    old_hl = re.search(r"hl_lines=\"(.*)\"", old_first_line)
    if old_hl is None:
        return None
    old_hl = old_hl.group(1)
    hl_lines = []
    for line in old_hl.split():
        if "-" in line:
            hl_lines.append(replace_hypen_with_colon(line))
        else:
            hl_lines.append(line)
    return ",".join(hl_lines)


def replace_hypen_with_colon(line: str) -> str:
    start, end = line.split("-")
    return f"{start}:{end}"


def convert_file_reference(reference_line: str) -> Tuple[str, Optional[str]]:
    """
    This takes a line such as:
        {!> ../../docs_src/security/tutorial006_an_py39.py!}
    and extracts the file reference such as ../../docs_src/security/tutorial006_an_py39.py.
    This can also accept file references with a line reference such as
    {!> ../../docs_src/separate_openapi_schemas/tutorial001_py310.py[ln:1-7]!}
    in this case it will return the file reference ../../docs_src/separate_openapi_schemas/tutorial001_py310.py and the line reference 1:7 (where we have replaced - with a :).
    """
    file_ref = re.search(r"../(.*).py", reference_line)
    if file_ref is None:
        raise InvalidFormatError("Invalid file reference.")
    file_ref = file_ref.group(0)
    line_ref = re.search(r"\[ln:(.*)\]", reference_line)
    if line_ref is None:
        return file_ref, None
    line_ref = line_ref.group(1)
    if "-" not in line_ref:
        return file_ref, line_ref
    return file_ref, replace_hypen_with_colon(line_ref)


PYTHON_BLOCK = "```Python"
END_BLOCK = "```"
PYTHON_TAB = ("//// tab | Python", "//// tab | 🐍", "//// tab | 파이썬")
TAB_END = "////"


@dataclass
class FencedPython:
    first_line: str
    body: list[str]

    def __str__(self) -> str:
        formatted_body_lines = format_fenced_python(self)
        body = "\n".join(formatted_body_lines)
        return body


@dataclass
class Tab:
    first_line: str
    body: list[str]

    def __str__(self) -> str:
        body = "\n".join(self.body).strip()
        return f"{self.first_line}\n\n{body}\n\n{TAB_END}"


@dataclass
class TabGroup:
    tabs: list[Tab]

    def __str__(self) -> str:
        return format_tab_group(self)


def format_fenced_python(block: FencedPython) -> list[str]:
    if block.body[0].startswith("{!"):
        hlines = get_hl_lines(block.first_line)
        file_ref, line_ref = convert_file_reference(block.body[0])
        file_ref = file_ref.replace("../", "")
        return [
            "{* "
            + f"{file_ref} {f'ln[{line_ref}] ' if line_ref else ''}{f'hl[{hlines}] ' if hlines else ''}"
            + "*}"
        ]
    return [block.first_line] + block.body + [END_BLOCK]


def format_tab_group(group: TabGroup) -> str:
    first_tab = group.tabs[0]
    fenced_block: FencedPython | None = None
    for line in first_tab.body:
        if fenced_block:
            if line == END_BLOCK:
                if fenced_block.body[0].startswith("{!"):
                    return str(fenced_block)
                return "\n\n".join(str(tab) for tab in group.tabs)
            fenced_block.body.append(line)
            continue
        if line and not line.startswith(PYTHON_BLOCK):
            return "\n\n".join(str(tab) for tab in group.tabs)
        if line.startswith(PYTHON_BLOCK):
            fenced_block = FencedPython(first_line=line, body=[])
    raise RuntimeError("Invalid tab group")


def process_lines(lines: list[str]) -> list[str | FencedPython | Tab]:
    new_sections: list[str | FencedPython | Tab] = []
    fenced_block: FencedPython | None = None
    tab: Tab | None = None

    for line in lines:
        if not fenced_block and not tab:
            if line.startswith(PYTHON_BLOCK):
                fenced_block = FencedPython(first_line=line, body=[])
                continue
            if line.startswith(PYTHON_TAB):
                tab = Tab(first_line=line, body=[])
                continue
            new_sections.append(line)
            continue
        if fenced_block:
            if line == END_BLOCK:
                new_sections.append(fenced_block)
                fenced_block = None
                continue
            fenced_block.body.append(line)
            continue
        if tab:
            if line == TAB_END:
                new_sections.append(tab)
                tab = None
                continue
            tab.body.append(line)
            continue
    return new_sections


def process_fragments(
    fragments: list[str | FencedPython | Tab],
) -> list[str | FencedPython | TabGroup]:
    new_fragments: list[str | FencedPython | TabGroup] = []
    tab_group: TabGroup | None = None
    for fragment in fragments:
        if not tab_group:
            if isinstance(fragment, Tab):
                tab_group = TabGroup(tabs=[fragment])
                continue
            new_fragments.append(fragment)
            continue
        if tab_group:
            if isinstance(fragment, Tab):
                tab_group.tabs.append(fragment)
                continue
            if fragment == "":
                continue
            new_fragments.append(tab_group)
            new_fragments.append("")
            tab_group = None
            new_fragments.append(fragment)
            continue
    if tab_group:
        new_fragments.append(tab_group)
    return new_fragments


def process_file(file_path: Path) -> None:
    lines = file_path.read_text().splitlines()
    sections = process_lines(lines)
    groups = process_fragments(sections)
    group_str = [str(group) for group in groups]
    file_path.write_text("\n".join(group_str).strip() + "\n")


skip_file_names = []


def main(file_path: Path | None = None) -> None:
    if file_path:
        process_file(file_path)
        return
    files_with_errors = []
    for f in Path("docs/").glob("**/*.md"):
        if f.name in skip_file_names:
            continue
        try:
            process_file(f)
        except Exception as e:
            print(f"An error occurred in file {f}: {e}")
            files_with_errors.append(f)
    print("Files with errors:")
    for f in files_with_errors:
        print(f)


if __name__ == "__main__":
    typer.run(main)

@github-actions github-actions bot added the docs Improvements or additions to documentation label Nov 18, 2024

This comment was marked as resolved.

Copy link

📝 Docs preview for commit 06f3324 at: https://3778589b.typertiangolo.pages.dev

Modified Pages

@tiangolo tiangolo marked this pull request as ready for review November 18, 2024 22:23
@tiangolo tiangolo merged commit 12459d2 into master Nov 18, 2024
23 checks passed
@tiangolo tiangolo deleted the markdown-includes branch November 18, 2024 22:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
docs Improvements or additions to documentation
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant