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()
 |