forked from LeenkxTeam/LNXSDK
292 lines
11 KiB
Python
292 lines
11 KiB
Python
"""
|
|
Generates the logic nodes reference page for Leenkx3D's wiki:
|
|
https://github.com/leenkx3d/leenkx/wiki/reference
|
|
|
|
USAGE:
|
|
First, generate the node screenshots (1). After that, open a
|
|
terminal in the folder of this script and execute the following
|
|
command (Blender must have the Leenkx add-on activated of course):
|
|
|
|
path/to/blender.exe -b -P make_node_reference.py"
|
|
|
|
This will create markdown files containing the reference in the
|
|
`/output` folder relative to this script. You can copy the content
|
|
from those files into the logic node reference articles. DO NOT
|
|
commit the generated files to the leenkx_tools repo!
|
|
|
|
Todo: Create a GitHub action to automatically update the reference
|
|
for each release.
|
|
|
|
(1) https://github.com/leenkx3d/leenkx_wiki_images/blob/master/logic_nodes/make_screenshots.py
|
|
Please also read the usage notes in that file!
|
|
"""
|
|
import ensurepip
|
|
import itertools
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from typing import List
|
|
|
|
import bpy
|
|
from nodeitems_utils import NodeItem
|
|
|
|
from lnx.logicnode import lnx_nodes
|
|
import lnx.props
|
|
|
|
ensurepip.bootstrap()
|
|
os.environ.pop("PIP_REQ_TRACKER", None)
|
|
subprocess.check_output([sys.executable, '-m', 'pip', 'install', '--upgrade', 'markdownmaker'])
|
|
|
|
# If pip wants an update, toggle this flag before execution
|
|
UPDATE_PIP = False
|
|
if UPDATE_PIP:
|
|
subprocess.check_output([sys.executable, '-m', 'pip', 'install', '--upgrade', 'pip'])
|
|
|
|
from markdownmaker.document import Document
|
|
from markdownmaker.markdownmaker import *
|
|
|
|
PY_NODE_DIR = "https://github.com/leenkx3d/leenkx/blob/main/blender/lnx.logicnode/"
|
|
HX_NODE_DIR = "https://github.com/leenkx3d/leenkx/blob/main/Sources/leenkx/logicnode/"
|
|
IMG_DIR = "https://github.com/leenkx3d/leenkx_wiki_images/raw/master/logic_nodes/"
|
|
|
|
OUTPUT_DIR = os.path.abspath(__file__)
|
|
OUTPUT_DIR = os.path.join(os.path.dirname(OUTPUT_DIR), "output")
|
|
|
|
|
|
def get_anchor(text: str) -> str:
|
|
"""Gets the GitHub anchor id for a link."""
|
|
return "#" + text.lower().replace(" ", "-")
|
|
|
|
|
|
def make_node_link(nodename: str) -> str:
|
|
"""Create a link to a node given by the name of the node"""
|
|
return Link(label=InlineCode(nodename), url=get_anchor(nodename))
|
|
|
|
|
|
def get_nodetype(typename: str):
|
|
"""Convert the type name to the actual type."""
|
|
return bpy.types.bpy_struct.bl_rna_get_subclass_py(typename)
|
|
|
|
|
|
def format_desc(description_text: str, *, indented=False) -> str:
|
|
"""Format the raw description string."""
|
|
out = ""
|
|
|
|
# Indentation for list items is 2 spaces per markdown standard in
|
|
# this case. For now, sub-lists are indented one level only, more
|
|
# is not required currently
|
|
line_start = " " if indented else ""
|
|
|
|
# Whether the last line was empty (ignore multiple empty lines)
|
|
last_empty = False
|
|
|
|
for line in description_text.splitlines():
|
|
line = line.strip()
|
|
|
|
# List item. Explicitly check for space after "-", might be a negative number else
|
|
if line.startswith("- "):
|
|
out += "\n" + line_start + line
|
|
last_empty = False
|
|
|
|
elif line == "":
|
|
if last_empty:
|
|
continue
|
|
out += "\n"
|
|
last_empty = True
|
|
|
|
else:
|
|
if last_empty:
|
|
out += "\n" + line_start + line # Create a full empty line above the start of a paragraph
|
|
else:
|
|
out += " " + line # Combine one paragraph to a single line
|
|
last_empty = False
|
|
|
|
# Remove any left-over whitespace at the beginning/end
|
|
return out.strip()
|
|
|
|
|
|
def generate_node_documentation(doc: Document, nodeitem: NodeItem, category: lnx_nodes.LnxNodeCategory):
|
|
nodetype = get_nodetype(nodeitem.nodetype)
|
|
docstring: str = nodetype.__doc__
|
|
if docstring is not None:
|
|
doc_parts = docstring.split("@")
|
|
|
|
# Show docstring until the first "@"
|
|
node_description = doc_parts[0].rstrip("\n")
|
|
node_description = format_desc(node_description)
|
|
|
|
deprecation_note = Optional()
|
|
doc.add(deprecation_note)
|
|
doc.add(Paragraph(node_description))
|
|
|
|
has_see = False
|
|
has_inputs = False
|
|
has_outputs = False
|
|
has_options = False
|
|
see_list = []
|
|
input_list = []
|
|
output_list = []
|
|
option_list = []
|
|
for part in doc_parts:
|
|
# Reference to other logic nodes
|
|
if part.startswith("seeNode "):
|
|
if not has_see:
|
|
has_see = True
|
|
doc.add(Paragraph(Bold("See also:")))
|
|
doc.add(UnorderedList(see_list))
|
|
|
|
see_list.append(Italic(make_node_link(part[8:].rstrip())))
|
|
|
|
# General references
|
|
elif part.startswith("see "):
|
|
if not has_see:
|
|
has_see = True
|
|
doc.add(Paragraph(Bold("See also:")))
|
|
doc.add(UnorderedList(see_list))
|
|
|
|
see_list.append(Italic(part[4:].rstrip()))
|
|
|
|
# Add node screenshot
|
|
image_file = IMG_DIR + category.name.lower() + "/" + nodeitem.nodetype + ".jpg"
|
|
doc.add(Image(url=image_file, alt_text=nodeitem.label + " node"))
|
|
|
|
for part in doc_parts:
|
|
# Input sockets
|
|
if part.startswith("input "):
|
|
if not has_inputs:
|
|
has_inputs = True
|
|
doc.add(Paragraph(Bold("Inputs:")))
|
|
doc.add(UnorderedList(input_list))
|
|
|
|
socket_name, description = part[6:].split(":", 1)
|
|
description = format_desc(description, indented=True)
|
|
input_list.append(f"{InlineCode(socket_name)}: {description}")
|
|
|
|
# Output sockets
|
|
elif part.startswith("output "):
|
|
if not has_outputs:
|
|
has_outputs = True
|
|
doc.add(Paragraph(Bold("Outputs:")))
|
|
doc.add(UnorderedList(output_list))
|
|
|
|
socket_name, description = part[7:].split(":", 1)
|
|
description = format_desc(description, indented=True)
|
|
output_list.append(f"{InlineCode(socket_name)}: {description}")
|
|
|
|
# Other UI options
|
|
elif part.startswith("option "):
|
|
if not has_options:
|
|
has_options = True
|
|
doc.add(Paragraph(Bold("Options:")))
|
|
doc.add(UnorderedList(option_list))
|
|
|
|
option_name, description = part[7:].split(":", 1)
|
|
description = format_desc(description, indented=True)
|
|
option_list.append(f"{InlineCode(option_name)}: {description}")
|
|
|
|
elif part.startswith("deprecated "):
|
|
alternatives, message = part[11:].split(":", 1)
|
|
|
|
message = " ".join(message.split()).replace("\n", "")
|
|
if not message.endswith(".") and not message == "":
|
|
message += "."
|
|
|
|
links = []
|
|
for alternative in alternatives.split(","):
|
|
if alternative == "":
|
|
continue
|
|
links.append(str(make_node_link(alternative)))
|
|
|
|
if len(links) > 0:
|
|
alternatives = f"Please use the following node(s) instead: {', '.join(links)}."
|
|
message = alternatives + " " + message
|
|
|
|
deprecation_note.content = Quote(f"{Bold('DEPRECATED.')} This node is deprecated and will be removed in future versions of Leenkx. {message}")
|
|
|
|
# Link to sources
|
|
node_file_py = "/".join(nodetype.__module__.split(".")[2:]) + ".py"
|
|
node_file_hx = nodetype.bl_idname[2:] + ".hx" # Discard LN prefix
|
|
|
|
pylink = Link(label="Python", url=PY_NODE_DIR + node_file_py)
|
|
hxlink = Link(label="Haxe", url=HX_NODE_DIR + node_file_hx)
|
|
|
|
doc.add(Paragraph(f"{Bold('Sources:')} {pylink} | {hxlink}"))
|
|
|
|
|
|
def build_page(section_name: str = ""):
|
|
is_mainpage = section_name == ""
|
|
|
|
doc = Document()
|
|
|
|
doc.add(Header("Logic Nodes Reference" + ("" if is_mainpage else f": {Italic(section_name.capitalize())} nodes")))
|
|
|
|
doc.add(Paragraph(Italic(
|
|
"This reference was generated automatically. Please do not edit the"
|
|
" page directly, instead change the docstrings of the nodes in their"
|
|
f" {Link(label='Python files', url='https://github.com/leenkx3d/leenkx/tree/main/blender/lnx.logicnode')}"
|
|
f" or the {Link(label='generator script', url='https://github.com/leenkx3d/leenkx_tools/blob/main/mkdocs/make_node_reference.py')}"
|
|
f" and {Link(label='open a pull request', url='https://github.com/leenkx3d/leenkx/wiki/contribute#creating-a-pull-request')}."
|
|
" Thank you for contributing!")))
|
|
doc.add(Paragraph(Italic(f"This reference was built for {Bold(f'Leenkx {lnx.props.lnx_version}')}.")))
|
|
|
|
doc.add(HorizontalRule())
|
|
|
|
with HeaderSubLevel(doc):
|
|
|
|
# Table of contents
|
|
doc.add(Header("Node Categories"))
|
|
|
|
category_items: List[Node] = []
|
|
for section, section_categories in lnx_nodes.category_items.items():
|
|
# Ignore empty sections ("default" e.g)
|
|
if len(section_categories) > 0:
|
|
section_title = Bold(section.capitalize())
|
|
if section_name == section:
|
|
# Highlight current page
|
|
section_title = Italic(section_title)
|
|
category_items.append(section_title)
|
|
url = f"https://github.com/leenkx3d/leenkx/wiki/reference_{section}"
|
|
category_items.append(UnorderedList([Link(c.name, url + get_anchor(c.name)) for c in section_categories]))
|
|
|
|
doc.add(UnorderedList(category_items))
|
|
|
|
# Page content
|
|
if not is_mainpage:
|
|
for category in lnx_nodes.category_items[section_name]:
|
|
doc.add(Header(category.name))
|
|
|
|
if category.description != "":
|
|
doc.add(Paragraph(category.description))
|
|
|
|
with HeaderSubLevel(doc):
|
|
# Sort nodes alphabetically and discard section order
|
|
iterator = itertools.chain(category.get_all_nodes(), category.deprecated_nodes)
|
|
for nodeitem in sorted(iterator, key=lambda n: n.label):
|
|
doc.add(Header(nodeitem.label))
|
|
|
|
generate_node_documentation(doc, nodeitem, category)
|
|
|
|
filename = "reference.md" if is_mainpage else f"reference_{section_name}.md"
|
|
with open(os.path.join(OUTPUT_DIR, filename), "w") as out_file:
|
|
out_file.write(doc.write())
|
|
|
|
|
|
def run():
|
|
print("Generating documentation...")
|
|
|
|
if not os.path.exists(OUTPUT_DIR):
|
|
os.mkdir(OUTPUT_DIR)
|
|
|
|
# Main page
|
|
build_page()
|
|
|
|
# Section sub-pages
|
|
for section_name in lnx_nodes.category_items.keys():
|
|
if section_name == 'default':
|
|
continue
|
|
build_page(section_name)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
run()
|