Skip to content

Instantly share code, notes, and snippets.

@almereyda
Forked from mofosyne/autodoc-justfile.py
Created February 7, 2025 17:18
Show Gist options
  • Select an option

  • Save almereyda/816f35994c0c46c32c0b7ba2899ce93f to your computer and use it in GitHub Desktop.

Select an option

Save almereyda/816f35994c0c46c32c0b7ba2899ce93f to your computer and use it in GitHub Desktop.

Revisions

  1. @mofosyne mofosyne created this gist Aug 10, 2024.
    216 changes: 216 additions & 0 deletions autodoc-justfile.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,216 @@
    #!/usr/bin/env python3
    # Autogenerated Documentation For Justfiles
    # This was created to support this issue ticket https://github.com/casey/just/issues/2033#issuecomment-2278336973
    import json
    import subprocess
    from typing import Any

    # just --dump --dump-format json --unstable | jq > test.json
    json_output = subprocess.run(
    ["just", "--dump", "--dump-format", "json", "--unstable"],
    check=True,
    capture_output=True,
    ).stdout

    def markdown_table_with_alignment_support(header_map: list[dict[str, str]], data: list[dict[str, Any]]):
    # JSON to Markdown table formatting: https://stackoverflow.com/a/72983854/2850957

    # Alignment Utility Function
    def strAlign(padding: int, alignMode: str | None, strVal: str):
    if alignMode == 'center':
    return strVal.center(padding)
    elif alignMode == 'right':
    return strVal.rjust(padding - 1) + ' '
    elif alignMode == 'left':
    return ' ' + strVal.ljust(padding - 1)
    else: # default left
    return ' ' + strVal.ljust(padding - 1)

    def dashAlign(padding: int, alignMode: str | None):
    if alignMode == 'center':
    return ':' + '-' * (padding - 2) + ':'
    elif alignMode == 'right':
    return '-' * (padding - 1) + ':'
    elif alignMode == 'left':
    return ':' + '-' * (padding - 1)
    else: # default left
    return '-' * (padding)

    # Calculate Padding For Each Column Based On Header and Data Length
    rowsPadding = {}
    for index, columnEntry in enumerate(header_map):
    padCount = max([len(str(v)) for d in data for k, v in d.items() if k == columnEntry['key_name']], default=0) + 2
    headerPadCount = len(columnEntry['header_name']) + 2
    rowsPadding[index] = headerPadCount if padCount <= headerPadCount else padCount

    # Render Markdown Header
    rows = []
    rows.append('|'.join(strAlign(rowsPadding[index], columnEntry.get('align'), str(columnEntry['header_name'])) for index, columnEntry in enumerate(header_map)))
    rows.append('|'.join(dashAlign(rowsPadding[index], columnEntry.get('align')) for index, columnEntry in enumerate(header_map)))

    # Render Tabular Data
    for item in data:
    rows.append('|'.join(strAlign(rowsPadding[index], columnEntry.get('align'), str(item[columnEntry['key_name']])) for index, columnEntry in enumerate(header_map)))

    # Convert Tabular String Rows Into String
    tableString = ""
    for row in rows:
    tableString += f'|{row}|\n'

    return tableString

    def recipe_one_line_short(stuff):
    command_args = stuff["name"]

    if len(stuff["parameters"]) > 0:
    for parameter_entry in stuff["parameters"]:
    parameter_name = parameter_entry["name"].upper()
    if parameter_entry["kind"] == "singular":
    if parameter_entry["default"] is not None:
    # Singular Argument. Default value used if missing
    command_args += f" {{{parameter_name}}}"
    else:
    # Singular Argument. Must provide value
    command_args += f" <{parameter_name}>"
    elif parameter_entry["kind"] == "plus":
    # One or more arguments
    command_args += f" [{parameter_name}... 1 or more]"
    elif parameter_entry["kind"] == "star":
    # Zero or more arguments (optional option)
    command_args += f" [{parameter_name}... 0 or more]"

    return command_args


    def captialise_sentences(text: str):
    lines = text.split('. ')
    for index, line in enumerate(lines):
    if len(line) > 1:
    lines[index] = line[0].upper() + line[1:]
    return '. '.join(lines)


    data = json.loads(json_output)
    recipes = data["recipes"]

    print("# Justfile Autogenerated Documentation")
    print("")

    print("## Recipe List")
    print("- Legend")
    print(" - `{ARG}` : Singular Argument. Default Value Avaliable If Missing")
    print(" - `<ARG>` : Singular Argument. Must Provide Value")
    print(" - `[ARG... 1 or more]` : Varidict Argument. Must Provide At Least One Value")
    print(" - `[ARG... 0 or more]` : Varidict Argument. Optionally Provide Multiple Values")

    # Main Recipes
    print("")
    print("### Main Recipe")
    recipe_list_table: list[dict[str, str | int]] = []

    for recipe_name, stuff in recipes.items():
    if recipe_name[0] == "_":
    continue
    recipe_list_table.append({"recipe_name":f"[`{recipe_one_line_short(stuff)}`](#{recipe_name})", "doc":captialise_sentences(stuff.get('doc') or "")})

    recipe_list_header_map = [
    {'key_name':'recipe_name', 'header_name':'Recipe', 'align':'left'},
    {'key_name':'doc', 'header_name':'Description', 'align':'left'}
    ]
    print(markdown_table_with_alignment_support(recipe_list_header_map, recipe_list_table))

    # Internal Recipes
    print("")
    print("### Internal Recipe")
    recipe_list_table: list[dict[str, str | int]] = []

    for recipe_name, stuff in recipes.items():
    if recipe_name[0] != "_":
    continue
    recipe_list_table.append({"recipe_name":f"[`{recipe_one_line_short(stuff)}`](#{recipe_name})", "doc":stuff.get('doc') or ""})

    recipe_list_header_map = [
    {'key_name':'recipe_name', 'header_name':'Recipe', 'align':'left'},
    {'key_name':'doc', 'header_name':'Description', 'align':'left'}
    ]

    print(markdown_table_with_alignment_support(recipe_list_header_map, recipe_list_table))

    print("")
    print("---")
    print("")
    print("## Recipe Details")

    for recipe_name, stuff in recipes.items():
    print("")
    print(f"## <a id=\"{recipe_name}\"> {recipe_name} </a>")
    print(f"- Command: `{recipe_one_line_short(stuff)}`")
    if len(stuff["parameters"]) > 0:
    print(f"- Arguments:")
    for parameter_entry in stuff["parameters"]:
    print(f' - `{parameter_entry["name"].upper()}`')

    if parameter_entry["kind"] == "plus":
    print(f' - One or more arguments')
    elif parameter_entry["kind"] == "star":
    print(f' - Zero or more arguments')

    if parameter_entry["default"] is not None:
    if isinstance(parameter_entry["default"], str):
    print(f' - Default Value: {parameter_entry["default"]}')
    elif isinstance(parameter_entry["default"], list):
    if parameter_entry["default"][0] == "evaluate":
    print(f' - Default Value: evaluate(`{parameter_entry["default"][1]}`)')
    else:
    print(f' - Default Value: ??(`{parameter_entry["default"][1]}`)')
    else:
    print(f' - Default Value: `{parameter_entry["default"]}`)')

    if len(stuff["dependencies"]) > 0:
    print("- Dependencies:")
    for dependent_recipe_name in [d["recipe"] for d in recipes.get(recipe_name)["dependencies"]]:
    print(f' - [{dependent_recipe_name}](#{dependent_recipe_name})')

    # Print description
    if stuff["doc"] is not None:
    print("")
    print(captialise_sentences(stuff.get("doc", "")))
    elif (stuff["body"] is None or len(stuff["body"]) <= 0) and (len(stuff["dependencies"]) > 0):
    # No code but has dependencies, likely a meta command
    print("")
    print(f"Run all {recipe_name} recipies")

    if stuff["body"] is not None and len(stuff["body"]) > 0:
    print("")
    print("```bash")
    for body_line in stuff["body"]:
    line = ""
    for piece in body_line:
    if isinstance(piece, str):
    line += piece
    elif piece[0][0] == "variable":
    line += f"{{{{{piece[0][1]}}}}}"
    elif piece[0][0] == "call":
    line += f"{{{{{piece[0][1]}()}}}}"
    else:
    line += f"{piece}"
    print(line)
    print("```")

    if len(stuff["dependencies"]) > 0:
    print("")
    print("```mermaid")
    print("graph LR")

    def call_graph_gen(recipe_name):
    if recipe_name not in recipes:
    return
    stuff = recipes.get(recipe_name)
    dependencies = [d["recipe"] for d in stuff["dependencies"]]
    for d in dependencies:
    print(f' {recipe_name} --> {d}')
    print(f' click {d} "#{d}"')
    call_graph_gen(d)

    call_graph_gen(recipe_name)
    print("```")