|
|
@@ -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("```") |