From bcf6c367a92c9681dc0e220bd164fd0c26e90c0a Mon Sep 17 00:00:00 2001 From: florianvazelle Date: Sat, 30 Dec 2023 12:31:51 +0100 Subject: [PATCH] refactor: use template to generate documentation --- .codespellrc | 3 +- .env | 2 +- .gitattributes | 16 +- .github/workflows/release-packaging.yml | 17 +- .gitignore | 11 +- .gutconfig.json | 10 + .pre-commit-config.yaml | 11 +- .reuse/dep5 | 11 + CHANGELOG.md | 2 - Justfile | 26 +- README.md | 20 +- addons/gd-plug/plug.gd | 1015 +++++++++++++++++ addons/godot-autogen-docs/cli.gd | 112 ++ addons/godot-autogen-docs/collector.gd | 37 - addons/godot-autogen-docs/markdown.gd | 51 - addons/godot-autogen-docs/plugin.cfg | 6 - .../godot-autogen-docs/reference_collector.gd | 29 - .../reference_collector_cli.gd | 20 - addons/godot-autogen-docs/template_engine.gd | 223 ++++ .../templates/markdown.template.md | 23 + .../templates/readthedocs.template.md | 124 ++ docs/dev-guide/api-ref/Collector.md | 53 - docs/dev-guide/api-ref/Markdown.md | 11 - docs/dev-guide/api-ref/ReferenceCollector.md | 38 - .../api-ref/ReferenceCollectorCli.md | 27 - docs/dev-guide/api-ref/cli.gd.md | 271 +++++ docs/dev-guide/api-ref/collector.gd.md | 172 +++ docs/dev-guide/api-ref/template_engine.gd.md | 253 ++++ docs/dev-guide/index.md | 0 docs/index.md | 2 +- docs/user-guide/index.md | 44 +- plug.gd | 5 + project.godot | 6 +- test/.gitignore | 1 + test/collector.test.gd | 94 ++ test/template_engine.test.gd | 74 ++ 36 files changed, 2507 insertions(+), 313 deletions(-) create mode 100644 .gutconfig.json create mode 100644 addons/gd-plug/plug.gd create mode 100644 addons/godot-autogen-docs/cli.gd delete mode 100644 addons/godot-autogen-docs/markdown.gd delete mode 100644 addons/godot-autogen-docs/plugin.cfg delete mode 100644 addons/godot-autogen-docs/reference_collector.gd delete mode 100644 addons/godot-autogen-docs/reference_collector_cli.gd create mode 100644 addons/godot-autogen-docs/template_engine.gd create mode 100644 addons/godot-autogen-docs/templates/markdown.template.md create mode 100644 addons/godot-autogen-docs/templates/readthedocs.template.md delete mode 100644 docs/dev-guide/api-ref/Collector.md delete mode 100644 docs/dev-guide/api-ref/Markdown.md delete mode 100644 docs/dev-guide/api-ref/ReferenceCollector.md delete mode 100644 docs/dev-guide/api-ref/ReferenceCollectorCli.md create mode 100644 docs/dev-guide/api-ref/cli.gd.md create mode 100644 docs/dev-guide/api-ref/collector.gd.md create mode 100644 docs/dev-guide/api-ref/template_engine.gd.md delete mode 100644 docs/dev-guide/index.md create mode 100644 plug.gd create mode 100644 test/.gitignore create mode 100644 test/collector.test.gd create mode 100644 test/template_engine.test.gd diff --git a/.codespellrc b/.codespellrc index 3c19ea9..3fb8749 100644 --- a/.codespellrc +++ b/.codespellrc @@ -1,3 +1,2 @@ [codespell] -skip = assets/** -ignore-words-list = lod,LOD +skip = addons/gd-plug/**,addons/gut/** diff --git a/.env b/.env index 2a98ae7..e0079fc 100644 --- a/.env +++ b/.env @@ -2,7 +2,7 @@ # Godot -GODOT_VERSION=4.1.3 +GODOT_VERSION=4.2.1 # Addon diff --git a/.gitattributes b/.gitattributes index 4613087..bc70476 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,12 +1,18 @@ +# Properly detect languages on Github. +*.gd linguist-language=GDScript + # Normalize EOL for all files that Git considers text files. * text=auto eol=lf -*.gd linguist-language=GDScript + +# The above only works properly for Git 2.10+, so for older versions +# we need to manually list the binary files we don't want modified. +*.mp3 binary +*.png binary *.hdr binary # Exclude all top-level files and directories (except addons) from zip downloads. # This makes installing through the AssetLib easier, because no files and folders # need to be unchecked. - -/** export-ignore -/addons/godot-gherkin !export-ignore -/addons/godot-gherkin/** !export-ignore +/** export-ignore +/addons/godot-autogen-docs !export-ignore +/addons/godot-autogen-docs/** !export-ignore diff --git a/.github/workflows/release-packaging.yml b/.github/workflows/release-packaging.yml index 00cc3f6..2c3ee77 100644 --- a/.github/workflows/release-packaging.yml +++ b/.github/workflows/release-packaging.yml @@ -19,8 +19,13 @@ jobs: - name: Load dotenv run: just ci-load-dotenv + # Retry multiple times, sometimes in CI, gdlint raise "file exists" - name: Check - run: just fmt + uses: nick-fields/retry@v2 + with: + timeout_minutes: 1 + max_attempts: 3 + command: just fmt - name: Ensure version is equal to tag if: startsWith(github.ref, 'refs/tags/') @@ -31,8 +36,10 @@ jobs: runs-on: ubuntu-22.04 timeout-minutes: 30 - matrix: - - godot_version: ['4.0.4', '4.1.3'] + strategy: + fail-fast: true + matrix: + godot_version: ['4.1.3', '4.2.1'] steps: - uses: actions/checkout@v4 @@ -42,8 +49,8 @@ jobs: with: python-version: '3.10' - - name: Check - run: just doc + - name: Test + run: just test env: GODOT_VERSION: ${{ matrix.godot_version }} diff --git a/.gitignore b/.gitignore index 97b3542..ac0f04b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,14 +10,15 @@ gfxrecon_capture_* .mono/ data_*/ +# gd-plug +.plugged/ +addons/* +!addons/gd-plug/ +!addons/godot-autogen-docs/ + # Python-specific ignores venv/ -# Export output -dist/ -build/ -override.cfg - # Docs output reference.json site/ diff --git a/.gutconfig.json b/.gutconfig.json new file mode 100644 index 0000000..804f552 --- /dev/null +++ b/.gutconfig.json @@ -0,0 +1,10 @@ +{ + "dirs": [ + "res://test/" + ], + "prefix": "", + "suffix": ".test.gd", + "include_subdirs": true, + "log_level": 1, + "should_exit": true +} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f9553f7..cd9d97c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -46,6 +46,11 @@ repos: entry: gdformat language: system files: \.gd$ + exclude: | + (?x)^( + addons/gd-plug/| + plug.gd + ) - id: check-shaders name: check shaders entry: clang-format @@ -55,9 +60,13 @@ repos: - -i language: system files: \.gdshader$ - exclude: ^addons/ - id: lint-gdscript name: lint gdscript entry: gdlint language: system files: \.gd$ + exclude: | + (?x)^( + addons/gd-plug/| + plug.gd + ) diff --git a/.reuse/dep5 b/.reuse/dep5 index 6c14228..1e2d8ad 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -7,3 +7,14 @@ Files: * Copyright: 2022-present Florian Vazelle License: MIT +# Addons + +Files: addons/gd-plug/* +Copyright: 2021 Tan Jian Ping +License: MIT +Source: https://github.com/imjp94/gd-plug + +Files: addons/gut/* +Copyright: 2018 Tom "Butch" Wesley +License: MIT +Source: https://github.com/bitwes/Gut diff --git a/CHANGELOG.md b/CHANGELOG.md index 760fc3e..ddcb982 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,5 +9,3 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### Fixed ### Security ### Dependencies - -- Bump `actions/setup-python` from 4 to 5 ([#2](https://github.com/MechanicalFlower/godot-autogen-docs/pull/2)) diff --git a/Justfile b/Justfile index 59432c5..6a4f195 100644 --- a/Justfile +++ b/Justfile @@ -67,10 +67,17 @@ install-godot: install-addons: [ -f plug.gd ] && just godot --headless --script plug.gd install || true +# Workaround from https://github.com/godotengine/godot/pull/68461 +# Import game resources +import-resources: + just godot --headless --export-pack null /dev/null + # timeout 60 just godot --editor || true + # just godot --headless --quit --editor + # Updates the addon version @bump-version: - echo "Update version in the plugin.cfg" - sed -i "s,version=.*$,version=\"{{ addon_version }}\",g" ./addons/{{ addon_name }}/plugin.cfg + # echo "Update version in the plugin.cfg" + # sed -i "s,version=.*$,version=\"{{ addon_version }}\",g" ./addons/{{ addon_name }}/plugin.cfg # Godot binary wrapper @godot *ARGS: makedirs install-godot @@ -110,12 +117,19 @@ publish: gh release create "{{ addon_version }}" --title="v{{ addon_version }}" --generate-notes # TODO: Add a asset-lib publish step +cli *ARGS: + just godot --editor --headless --quit --script addons/godot-autogen-docs/cli.gd {{ ARGS }} + +# Generate documentation doc: - just godot --editor --headless --quit --script addons/godot-autogen-docs/reference_collector_cli.gd - just godot --headless --quit --script addons/godot-autogen-docs/markdown.gd + just godot --editor --headless --quit --script addons/godot-autogen-docs/cli.gd readthedocs -ddir=res://addons/godot-autogen-docs -doutdir=res://docs/dev-guide/api-ref/ just venv pip install mkdocs==1.5.3 mkdocs-literate-nav==0.6.1 just venv mkdocs build -serve: - just venv pip install mkdocs==1.5.3 mkdocs-literate-nav==0.6.1 +# Start serving the documentation +serve: doc just venv mkdocs serve + +# Run unit tests +test: install-addons import-resources + just godot --headless --script addons/gut/gut_cmdln.gd -gconfig=.gutconfig.json diff --git a/README.md b/README.md index 02a214b..bf12695 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # Godot Autogen Docs -![Godot Badge](https://img.shields.io/badge/godot-4.0|4.1-blue?logo=Godot-Engine&logoColor=white) +![Godot Badge](https://img.shields.io/badge/godot-4.1%20%7C%204.2-blue?logo=Godot-Engine&logoColor=white) ![license](https://img.shields.io/badge/license-MIT-green?logo=open-source-initiative&logoColor=white) ![reuse](./.reuse/REUSE-compliant.svg) @@ -16,14 +16,20 @@ Godot addon that automatically turns your code ### Usage -To generate a `reference.json`, run: -``` -godot --editor --headless --quit --script addons/godot-autogen-docs/reference_collector_cli.gd ``` +$ just godot --editor --headless --quit --script addons/godot-autogen-docs/cli.gd help -And to turn this JSON file into markdown, run: -``` -godot --headless --quit --script addons/godot-autogen-docs/markdown.gd +Usage: + godot --editor --headless --quit --script res://addons/godot-autogen-docs/cli.gd [ ...] + +Commands: + help Display this text + markdown Export documentation to markdown + readthedocs Export documentation to markdown compatible with the readthedocs theme + +Options: + -ddir Comma delimited list of directories to collect files from + -doutdir A directory for saving documentation files ``` ## Contributing diff --git a/addons/gd-plug/plug.gd b/addons/gd-plug/plug.gd new file mode 100644 index 0000000..c565a0e --- /dev/null +++ b/addons/gd-plug/plug.gd @@ -0,0 +1,1015 @@ +@tool +extends SceneTree + +signal updated(plugin) + +const VERSION = "0.2.5" +const DEFAULT_PLUGIN_URL = "https://git::@github.com/%s.git" +const DEFAULT_PLUG_DIR = "res://.plugged" +const DEFAULT_CONFIG_PATH = DEFAULT_PLUG_DIR + "/index.cfg" +const DEFAULT_USER_PLUG_SCRIPT_PATH = "res://plug.gd" +const DEFAULT_BASE_PLUG_SCRIPT_PATH = "res://addons/gd-plug/plug.gd" + +const ENV_PRODUCTION = "production" +const ENV_TEST = "test" +const ENV_FORCE = "force" +const ENV_KEEP_IMPORT_FILE = "keep_import_file" +const ENV_KEEP_IMPORT_RESOURCE_FILE = "keep_import_resource_file" + +const MSG_PLUG_START_ASSERTION = "_plug_start() must be called first" + +var project_dir +var installation_config = ConfigFile.new() +var logger = _Logger.new() + +var _installed_plugins +var _plugged_plugins = {} + +var _threads = [] +var _mutex = Mutex.new() +var _start_time = 0 +var threadpool = _ThreadPool.new(logger) + + +func _init(): + threadpool.connect("all_thread_finished", request_quit) + project_dir = DirAccess.open("res://") + +func _initialize(): + var args = OS.get_cmdline_args() + # Trim unwanted args passed to godot executable + for arg in Array(args): + args.remove_at(0) + if "plug.gd" in arg: + break + + for arg in args: + # NOTE: "--key" or "-key" will always be consumed by godot executable, see https://github.com/godotengine/godot/issues/8721 + var key = arg.to_lower() + match key: + "detail": + logger.log_format = _Logger.DEFAULT_LOG_FORMAT_DETAIL + "debug", "d": + logger.log_level = _Logger.LogLevel.DEBUG + "quiet", "q", "silent": + logger.log_level = _Logger.LogLevel.NONE + "production": + OS.set_environment(ENV_PRODUCTION, "true") + "test": + OS.set_environment(ENV_TEST, "true") + "force": + OS.set_environment(ENV_FORCE, "true") + "keep-import-file": + OS.set_environment(ENV_KEEP_IMPORT_FILE, "true") + "keep-import-resource-file": + OS.set_environment(ENV_KEEP_IMPORT_RESOURCE_FILE, "true") + + logger.debug("cmdline_args: %s" % args) + _start_time = Time.get_ticks_msec() + _plug_start() + if args.size() > 0: + _plugging() + match args[0]: + "init": + _plug_init() + "install", "update": + _plug_install() + "uninstall": + _plug_uninstall() + "clean": + _plug_clean() + "upgrade": + _plug_upgrade() + "status": + _plug_status() + "version": + logger.info(VERSION) + _: + logger.error("Unknown command %s" % args[0]) + # NOTE: Do no put anything after this line except request_quit(), as _plug_*() may call request_quit() + request_quit() + +func _process(delta): + threadpool.process(delta) + +func _finalize(): + _plug_end() + threadpool.stop() + logger.info("Finished, elapsed %.3fs" % ((Time.get_ticks_msec() - _start_time) / 1000.0)) + +func _on_updated(plugin): + pass + +func _plugging(): + pass + +func request_quit(exit_code=-1): + if threadpool.is_all_thread_finished() and threadpool.is_all_task_finished(): + quit(exit_code) + return true + logger.debug("Request quit declined, threadpool is still running") + return false + +# Index installed plugins, or create directory "plugged" if not exists +func _plug_start(): + logger.debug("Plug start") + if not project_dir.dir_exists_absolute(DEFAULT_PLUG_DIR): + if project_dir.make_dir(ProjectSettings.globalize_path(DEFAULT_PLUG_DIR)) == OK: + logger.debug("Make dir %s for plugin installation") + if installation_config.load(DEFAULT_CONFIG_PATH) == OK: + logger.debug("Installation config loaded") + else: + logger.debug("Installation config not found") + _installed_plugins = installation_config.get_value("plugin", "installed", {}) + +# Install plugin or uninstall plugin if unlisted +func _plug_end(): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + var test = !OS.get_environment(ENV_TEST).is_empty() + if not test: + installation_config.set_value("plugin", "installed", _installed_plugins) + if installation_config.save(DEFAULT_CONFIG_PATH) == OK: + logger.debug("Plugged config saved") + else: + logger.error("Failed to save plugged config") + else: + logger.warn("Skipped saving of plugged config in test mode") + _installed_plugins = null + +func _plug_init(): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + logger.info("Init gd-plug...") + if FileAccess.file_exists(DEFAULT_USER_PLUG_SCRIPT_PATH): + logger.warn("%s already exists!" % DEFAULT_USER_PLUG_SCRIPT_PATH) + else: + var file = FileAccess.open(DEFAULT_USER_PLUG_SCRIPT_PATH, FileAccess.WRITE) + file.store_string(INIT_PLUG_SCRIPT) + file.close() + logger.info("Created %s" % DEFAULT_USER_PLUG_SCRIPT_PATH) + +func _plug_install(): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + threadpool.active = false + logger.info("Installing...") + for plugin in _plugged_plugins.values(): + var installed = plugin.name in _installed_plugins + if installed: + var installed_plugin = get_installed_plugin(plugin.name) + if (installed_plugin.dev or plugin.dev) and OS.get_environment(ENV_PRODUCTION): + logger.info("Remove dev plugin for production: %s" % plugin.name) + threadpool.enqueue_task(uninstall_plugin.bind(installed_plugin)) + else: + threadpool.enqueue_task(update_plugin.bind(plugin)) + else: + threadpool.enqueue_task(install_plugin.bind(plugin)) + + var removed_plugins = [] + for plugin in _installed_plugins.values(): + var removed = not (plugin.name in _plugged_plugins) + if removed: + removed_plugins.append(plugin) + if removed_plugins: + threadpool.disconnect("all_thread_finished", request_quit) + if not threadpool.is_all_thread_finished(): + threadpool.active = true + await threadpool.all_thread_finished + threadpool.active = false + logger.debug("All installation finished! Ready to uninstall removed plugins...") + threadpool.connect("all_thread_finished", request_quit) + for plugin in removed_plugins: + threadpool.enqueue_task(uninstall_plugin.bind(plugin), Thread.PRIORITY_LOW) + threadpool.active = true + +func _plug_uninstall(): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + threadpool.active = false + logger.info("Uninstalling...") + for plugin in _installed_plugins.values(): + var installed_plugin = get_installed_plugin(plugin.name) + threadpool.enqueue_task(uninstall_plugin.bind(installed_plugin), Thread.PRIORITY_LOW) + threadpool.active = true + +func _plug_clean(): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + threadpool.active = false + logger.info("Cleaning...") + var plugged_dir = DirAccess.open(DEFAULT_PLUG_DIR) + plugged_dir.include_hidden = true + plugged_dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 + var file = plugged_dir.get_next() + while not file.is_empty(): + if plugged_dir.current_is_dir(): + if not (file in _installed_plugins): + logger.info("Remove %s" % file) + threadpool.enqueue_task(directory_delete_recursively.bind(plugged_dir.get_current_dir() + "/" + file)) + file = plugged_dir.get_next() + plugged_dir.list_dir_end() + threadpool.active = true + +func _plug_upgrade(): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + threadpool.active = false + logger.info("Upgrading gd-plug...") + plug("imjp94/gd-plug") + var gd_plug = _plugged_plugins["gd-plug"] + OS.set_environment(ENV_FORCE, "true") # Required to overwrite res://addons/gd-plug/plug.gd + threadpool.enqueue_task(install_plugin.bind(gd_plug)) + threadpool.disconnect("all_thread_finished", request_quit) + if not threadpool.is_all_thread_finished(): + threadpool.active = true + await threadpool.all_thread_finished + threadpool.active = false + logger.debug("All installation finished! Ready to uninstall removed plugins...") + threadpool.connect("all_thread_finished", request_quit) + threadpool.enqueue_task(directory_delete_recursively.bind(gd_plug.plug_dir)) + threadpool.active = true + +func _plug_status(): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + threadpool.active = false + logger.info("Installed %d plugin%s" % [_installed_plugins.size(), "s" if _installed_plugins.size() > 1 else ""]) + var new_plugins = _plugged_plugins.duplicate() + var has_checking_plugin = false + var removed_plugins = [] + for plugin in _installed_plugins.values(): + logger.info("- {name} - {url}".format(plugin)) + new_plugins.erase(plugin.name) + var removed = not (plugin.name in _plugged_plugins) + if removed: + removed_plugins.append(plugin) + else: + threadpool.enqueue_task(check_plugin.bind(_plugged_plugins[plugin.name])) + has_checking_plugin = true + if has_checking_plugin: + logger.info("\n", true) + threadpool.disconnect("all_thread_finished", request_quit) + threadpool.active = true + await threadpool.all_thread_finished + threadpool.active = false + threadpool.connect("all_thread_finished", request_quit) + logger.debug("Finished checking plugins, ready to proceed") + if new_plugins: + logger.info("\nPlugged %d plugin%s" % [new_plugins.size(), "s" if new_plugins.size() > 1 else ""]) + for plugin in new_plugins.values(): + var is_new = not (plugin.name in _installed_plugins) + if is_new: + logger.info("- {name} - {url}".format(plugin)) + if removed_plugins: + logger.info("\nUnplugged %d plugin%s" % [removed_plugins.size(), "s" if removed_plugins.size() > 1 else ""]) + for plugin in removed_plugins: + logger.info("- %s removed" % plugin.name) + var plug_directory = DirAccess.open(DEFAULT_PLUG_DIR) + var orphan_dirs = [] + if plug_directory.get_open_error() == OK: + plug_directory.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 + var file = plug_directory.get_next() + while not file.is_empty(): + if plug_directory.current_is_dir(): + if not (file in _installed_plugins): + orphan_dirs.append(file) + file = plug_directory.get_next() + plug_directory.list_dir_end() + if orphan_dirs: + logger.info("\nOrphan directory, %d found in %s, execute \"clean\" command to remove" % [orphan_dirs.size(), DEFAULT_PLUG_DIR]) + for dir in orphan_dirs: + logger.info("- %s" % dir) + threadpool.active = true + + if has_checking_plugin: + request_quit() + +# Index & validate plugin +func plug(repo, args={}): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + repo = repo.strip_edges() + var plugin_name = get_plugin_name_from_repo(repo) + if plugin_name in _plugged_plugins: + logger.info("Plugin already plugged: %s" % plugin_name) + return + var plugin = {} + plugin.name = plugin_name + plugin.url = "" + if ":" in repo: + plugin.url = repo + elif repo.find("/") == repo.rfind("/"): + plugin.url = DEFAULT_PLUGIN_URL % repo + else: + logger.error("Invalid repo: %s" % repo) + plugin.plug_dir = DEFAULT_PLUG_DIR + "/" + plugin.name + + var is_valid = true + plugin.include = args.get("include", []) + is_valid = is_valid and validate_var_type(plugin, "include", TYPE_ARRAY, "Array") + plugin.exclude = args.get("exclude", []) + is_valid = is_valid and validate_var_type(plugin, "exclude", TYPE_ARRAY, "Array") + plugin.branch = args.get("branch", "") + is_valid = is_valid and validate_var_type(plugin, "branch", TYPE_STRING, "String") + plugin.tag = args.get("tag", "") + is_valid = is_valid and validate_var_type(plugin, "tag", TYPE_STRING, "String") + plugin.commit = args.get("commit", "") + is_valid = is_valid and validate_var_type(plugin, "commit", TYPE_STRING, "String") + if not plugin.commit.is_empty(): + var is_valid_commit = plugin.commit.length() == 40 + if not is_valid_commit: + logger.error("Expected full length 40 digits commit-hash string, given %s" % plugin.commit) + is_valid = is_valid and is_valid_commit + plugin.dev = args.get("dev", false) + is_valid = is_valid and validate_var_type(plugin, "dev", TYPE_BOOL, "Boolean") + plugin.on_updated = args.get("on_updated", "") + is_valid = is_valid and validate_var_type(plugin, "on_updated", TYPE_STRING, "String") + plugin.install_root = args.get("install_root", "") + is_valid = is_valid and validate_var_type(plugin, "install_root", TYPE_STRING, "String") + + if is_valid: + _plugged_plugins[plugin.name] = plugin + logger.debug("Plug: %s" % plugin) + else: + logger.error("Failed to plug %s, validation error" % plugin.name) + +func install_plugin(plugin): + var test = !OS.get_environment(ENV_TEST).is_empty() + var can_install = OS.get_environment(ENV_PRODUCTION).is_empty() if plugin.dev else true + if can_install: + logger.info("Installing plugin %s..." % plugin.name) + var result = is_plugin_downloaded(plugin) + if result != OK: + result = download(plugin) + else: + logger.info("Plugin already downloaded") + + if result == OK: + install(plugin) + else: + logger.error("Failed to install plugin %s with error code %d" % [plugin.name, result]) + +func uninstall_plugin(plugin): + var test = !OS.get_environment(ENV_TEST).is_empty() + logger.info("Uninstalling plugin %s..." % plugin.name) + uninstall(plugin) + directory_delete_recursively(plugin.plug_dir, {"exclude": [DEFAULT_CONFIG_PATH], "test": test}) + +func update_plugin(plugin, checking=false): + if not (plugin.name in _installed_plugins): + logger.info("%s new plugin" % plugin.name) + return true + + var git = _GitExecutable.new(ProjectSettings.globalize_path(plugin.plug_dir), logger) + var installed_plugin = get_installed_plugin(plugin.name) + var changes = compare_plugins(plugin, installed_plugin) + var should_clone = false + var should_pull = false + var should_reinstall = false + + if plugin.tag or plugin.commit: + for rev in ["tag", "commit"]: + var freeze_at = plugin[rev] + if freeze_at: + logger.info("%s frozen at %s \"%s\"" % [plugin.name, rev, freeze_at]) + break + else: + var ahead_behind = [] + if git.fetch("origin " + plugin.branch if plugin.branch else "origin").exit == OK: + ahead_behind = git.get_commit_comparison("HEAD", "origin/" + plugin.branch if plugin.branch else "origin") + var is_commit_behind = !!ahead_behind[1] if ahead_behind.size() == 2 else false + if is_commit_behind: + logger.info("%s %d commits behind, update required" % [plugin.name, ahead_behind[1]]) + should_pull = true + else: + logger.info("%s up to date" % plugin.name) + + if changes: + logger.info("%s changed %s" % [plugin.name, changes]) + should_reinstall = true + if "url" in changes or "branch" in changes or "tag" in changes or "commit" in changes: + logger.info("%s repository setting changed, update required" % plugin.name) + should_clone = true + + if not checking: + if should_clone: + logger.info("%s cloning from %s..." % [plugin.name, plugin.url]) + var test = !OS.get_environment(ENV_TEST).is_empty() + uninstall(get_installed_plugin(plugin.name)) + directory_delete_recursively(plugin.plug_dir, {"exclude": [DEFAULT_CONFIG_PATH], "test": test}) + if download(plugin) == OK: + install(plugin) + elif should_pull: + logger.info("%s pulling updates from %s..." % [plugin.name, plugin.url]) + uninstall(get_installed_plugin(plugin.name)) + if git.pull().exit == OK: + install(plugin) + elif should_reinstall: + logger.info("%s reinstalling..." % plugin.name) + uninstall(get_installed_plugin(plugin.name)) + install(plugin) + +func check_plugin(plugin): + update_plugin(plugin, true) + +func download(plugin): + logger.info("Downloading %s from %s..." % [plugin.name, plugin.url]) + var test = !OS.get_environment(ENV_TEST).is_empty() + var global_dest_dir = ProjectSettings.globalize_path(plugin.plug_dir) + if project_dir.dir_exists_absolute(plugin.plug_dir): + directory_delete_recursively(plugin.plug_dir) + project_dir.make_dir(plugin.plug_dir) + var result = _GitExecutable.new(global_dest_dir, logger).clone(plugin.url, global_dest_dir, {"branch": plugin.branch, "tag": plugin.tag, "commit": plugin.commit}) + if result.exit == OK: + logger.info("Successfully download %s" % [plugin.name]) + else: + logger.info("Failed to download %s" % plugin.name) + # Make sure plug_dir is clean when failed + directory_delete_recursively(plugin.plug_dir, {"exclude": [DEFAULT_CONFIG_PATH], "test": test}) + project_dir.remove(plugin.plug_dir) # Remove empty directory + return result.exit + +func install(plugin): + var include = plugin.get("include", []) + if include.is_empty(): # Auto include "addons/" folder if not explicitly specified + include = ["addons/"] + if OS.get_environment(ENV_FORCE).is_empty() and OS.get_environment(ENV_TEST).is_empty(): + var is_exists = false + var dest_files = directory_copy_recursively(plugin.plug_dir, "res://" + plugin.install_root, {"include": include, "exclude": plugin.exclude, "test": true, "silent_test": true}) + for dest_file in dest_files: + if project_dir.file_exists(dest_file): + logger.warn("%s attempting to overwrite file %s" % [plugin.name, dest_file]) + is_exists = true + if is_exists: + logger.warn("Installation of %s terminated to avoid overwriting user files, you may disable safe mode with command \"force\"" % plugin.name) + return ERR_ALREADY_EXISTS + + logger.info("Installing files for %s..." % plugin.name) + var test = !OS.get_environment(ENV_TEST).is_empty() + var dest_files = directory_copy_recursively(plugin.plug_dir, "res://" + plugin.install_root, {"include": include, "exclude": plugin.exclude, "test": test}) + plugin.dest_files = dest_files + logger.info("Installed %d file%s for %s" % [dest_files.size(), "s" if dest_files.size() > 1 else "", plugin.name]) + if plugin.name != "gd-plug": + set_installed_plugin(plugin) + if plugin.on_updated: + if has_method(plugin.on_updated): + logger.info("Execute post-update function for %s" % plugin.name) + _on_updated(plugin) + call(plugin.on_updated, plugin.duplicate()) + emit_signal("updated", plugin) + return OK + +func uninstall(plugin): + var test = !OS.get_environment(ENV_TEST).is_empty() + var keep_import_file = !OS.get_environment(ENV_KEEP_IMPORT_FILE).is_empty() + var keep_import_resource_file = !OS.get_environment(ENV_KEEP_IMPORT_RESOURCE_FILE).is_empty() + var dest_files = plugin.get("dest_files", []) + logger.info("Uninstalling %d file%s for %s..." % [dest_files.size(), "s" if dest_files.size() > 1 else "",plugin.name]) + directory_remove_batch(dest_files, {"test": test, "keep_import_file": keep_import_file, "keep_import_resource_file": keep_import_resource_file}) + logger.info("Uninstalled %d file%s for %s" % [dest_files.size(), "s" if dest_files.size() > 1 else "",plugin.name]) + remove_installed_plugin(plugin.name) + +func is_plugin_downloaded(plugin): + if not project_dir.dir_exists_absolute(plugin.plug_dir + "/.git"): + return + + var git = _GitExecutable.new(ProjectSettings.globalize_path(plugin.plug_dir), logger) + return git.is_up_to_date(plugin) + +# Get installed plugin, thread safe +func get_installed_plugin(plugin_name): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + _mutex.lock() + var installed_plugin = _installed_plugins[plugin_name] + _mutex.unlock() + return installed_plugin + +# Set installed plugin, thread safe +func set_installed_plugin(plugin): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + _mutex.lock() + _installed_plugins[plugin.name] = plugin + _mutex.unlock() + +# Remove installed plugin, thread safe +func remove_installed_plugin(plugin_name): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + _mutex.lock() + var result = _installed_plugins.erase(plugin_name) + _mutex.unlock() + return result + +func directory_copy_recursively(from, to, args={}): + var include = args.get("include", []) + var exclude = args.get("exclude", []) + var test = args.get("test", false) + var silent_test = args.get("silent_test", false) + var dir = DirAccess.open(from) + dir.include_hidden = true + var dest_files = [] + if dir.get_open_error() == OK: + dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 + var file_name = dir.get_next() + while not file_name.is_empty(): + var source = dir.get_current_dir() + ("/" if dir.get_current_dir() != "res://" else "") + file_name + var dest = to + ("/" if to != "res://" else "") + file_name + + if dir.current_is_dir(): + dest_files += directory_copy_recursively(source, dest, args) + else: + for include_key in include: + if include_key in source: + var is_excluded = false + for exclude_key in exclude: + if exclude_key in source: + is_excluded = true + break + if not is_excluded: + if test: + if not silent_test: logger.warn("[TEST] Writing to %s" % dest) + else: + dir.make_dir_recursive_absolute(to) + if dir.copy(source, dest) == OK: + logger.debug("Copy from %s to %s" % [source, dest]) + dest_files.append(dest) + break + file_name = dir.get_next() + dir.list_dir_end() + else: + logger.error("Failed to access path: %s" % from) + + return dest_files + +func directory_delete_recursively(dir_path, args={}): + var remove_empty_directory = args.get("remove_empty_directory", true) + var exclude = args.get("exclude", []) + var test = args.get("test", false) + var silent_test = args.get("silent_test", false) + var dir = DirAccess.open(dir_path) + dir.include_hidden = true + if dir.get_open_error() == OK: + dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 + var file_name = dir.get_next() + while not file_name.is_empty(): + var source = dir.get_current_dir() + ("/" if dir.get_current_dir() != "res://" else "") + file_name + + if dir.current_is_dir(): + var sub_dir = directory_delete_recursively(source, args) + if remove_empty_directory: + if test: + if not silent_test: logger.warn("[TEST] Remove empty directory: %s" % sub_dir.get_current_dir()) + else: + if source.get_file() == ".git": + var empty_dir_path = ProjectSettings.globalize_path(source) + var exit = FAILED + match OS.get_name(): + "Windows": + empty_dir_path = "\"%s\"" % empty_dir_path + empty_dir_path = empty_dir_path.replace("/", "\\") + var cmd = "rd /s /q %s" % empty_dir_path + exit = OS.execute("cmd", ["/C", cmd]) + "X11", "OSX", "Server": + empty_dir_path = "\'%s\'" % empty_dir_path + var cmd = "rm -rf %s" % empty_dir_path + exit = OS.execute("bash", ["-c", cmd]) + # Hacks to remove .git, as git pack files stop it from being removed + # See https://stackoverflow.com/questions/1213430/how-to-fully-delete-a-git-repository-created-with-init + if exit == OK: + logger.debug("Remove empty directory: %s" % sub_dir.get_current_dir()) + else: + logger.debug("Failed to remove empty directory: %s" % sub_dir.get_current_dir()) + else: + if dir.remove(sub_dir.get_current_dir()) == OK: + logger.debug("Remove empty directory: %s" % sub_dir.get_current_dir()) + else: + var excluded = false + for exclude_key in exclude: + if source in exclude_key: + excluded = true + break + if not excluded: + if test: + if not silent_test: logger.warn("[TEST] Remove file: %s" % source) + else: + if dir.remove(file_name) == OK: + logger.debug("Remove file: %s" % source) + file_name = dir.get_next() + dir.list_dir_end() + else: + logger.error("Failed to access path: %s" % dir_path) + + if remove_empty_directory: + dir.remove(dir.get_current_dir()) + + return dir + +func directory_remove_batch(files, args={}): + var remove_empty_directory = args.get("remove_empty_directory", true) + var keep_import_file = args.get("keep_import_file", false) + var keep_import_resource_file = args.get("keep_import_resource_file", false) + var test = args.get("test", false) + var silent_test = args.get("silent_test", false) + var dirs = {} + for file in files: + var file_dir = file.get_base_dir() + var file_name =file.get_file() + var dir = dirs.get(file_dir) + + if not dir: + dir = DirAccess.open(file_dir) + dirs[file_dir] = dir + + if file.ends_with(".import"): + if not keep_import_file: + _remove_import_file(dir, file, keep_import_resource_file, test, silent_test) + else: + if test: + if not silent_test: logger.warn("[TEST] Remove file: " + file) + else: + if dir.remove(file_name) == OK: + logger.debug("Remove file: " + file) + if not keep_import_file: + _remove_import_file(dir, file + ".import", keep_import_resource_file, test, silent_test) + + for dir in dirs.values(): + var slash_count = dir.get_current_dir().count("/") - 2 # Deduct 2 slash from "res://" + if test: + if not silent_test: logger.warn("[TEST] Remove empty directory: %s" % dir.get_current_dir()) + else: + if dir.remove(dir.get_current_dir()) == OK: + logger.debug("Remove empty directory: %s" % dir.get_current_dir()) + # Dumb method to clean empty ancestor directories + logger.debug("Removing emoty ancestor directory for %s..." % dir.get_current_dir()) + var current_dir = dir.get_current_dir() + for i in slash_count: + current_dir = current_dir.get_base_dir() + var d = DirAccess.open(current_dir) + if d.get_open_error() == OK: + if test: + if not silent_test: logger.warn("[TEST] Remove empty ancestor directory: %s" % d.get_current_dir()) + else: + if d.remove(d.get_current_dir()) == OK: + logger.debug("Remove empty ancestor directory: %s" % d.get_current_dir()) + +func _remove_import_file(dir, file, keep_import_resource_file=false, test=false, silent_test=false): + if not dir.file_exists(file): + return + + if not keep_import_resource_file: + var import_config = ConfigFile.new() + if import_config.load(file) == OK: + var metadata = import_config.get_value("remap", "metadata", {}) + var imported_formats = metadata.get("imported_formats", []) + if imported_formats: + for format in imported_formats: + _remove_import_resource_file(dir, import_config, "." + format, test) + else: + _remove_import_resource_file(dir, import_config, "", test) + if test: + if not silent_test: logger.warn("[TEST] Remove import file: " + file) + else: + if dir.remove(file) == OK: + logger.debug("Remove import file: " + file) + else: + # TODO: Sometimes Directory.remove() unable to remove random .import file and return error code 1(Generic Error) + # Maybe enforce the removal from shell? + logger.warn("Failed to remove import file: " + file) + +func _remove_import_resource_file(dir, import_config, import_format="", test=false): + var import_resource_file = import_config.get_value("remap", "path" + import_format, "") + var checksum_file = import_resource_file.trim_suffix("." + import_resource_file.get_extension()) + ".md5" if import_resource_file else "" + if import_resource_file: + if dir.file_exists(import_resource_file): + if test: + logger.info("[IMPORT] Remove import resource file: " + import_resource_file) + else: + if dir.remove(import_resource_file) == OK: + logger.debug("Remove import resource file: " + import_resource_file) + if checksum_file: + checksum_file = checksum_file.replace(import_format, "") + if dir.file_exists(checksum_file): + if test: + logger.info("[IMPORT] Remove import checksum file: " + checksum_file) + else: + if dir.remove(checksum_file) == OK: + logger.debug("Remove import checksum file: " + checksum_file) + +func compare_plugins(p1, p2): + var changed_keys = [] + for key in p1.keys(): + var v1 = p1[key] + var v2 = p2[key] + if v1 != v2: + changed_keys.append(key) + return changed_keys + +func get_plugin_name_from_repo(repo): + repo = repo.replace(".git", "").trim_suffix("/") + return repo.get_file() + +func validate_var_type(obj, var_name, type, type_string): + var value = obj.get(var_name) + var is_valid = typeof(value) == type + if not is_valid: + logger.error("Expected variable \"%s\" to be %s, given %s" % [var_name, type_string, value]) + return is_valid + +const INIT_PLUG_SCRIPT = \ +"""extends "res://addons/gd-plug/plug.gd" + +func _plugging(): + # Declare plugins with plug(repo, args) + # For example, clone from github repo("user/repo_name") + # plug("imjp94/gd-YAFSM") # By default, gd-plug will only install anything from "addons/" directory + # Or you can explicitly specify which file/directory to include + # plug("imjp94/gd-YAFSM", {"include": ["addons/"]}) # By default, gd-plug will only install anything from "addons/" directory + pass +""" + +class _GitExecutable extends RefCounted: + var cwd = "" + var logger + + func _init(p_cwd, p_logger): + cwd = p_cwd + logger = p_logger + + func _execute(command, output=[], read_stderr=false): + var cmd = "cd '%s' && %s" % [cwd, command] + # NOTE: OS.execute() seems to ignore read_stderr + var exit = FAILED + match OS.get_name(): + "Windows": + cmd = cmd.replace("\'", "\"") # cmd doesn't accept single-quotes + cmd = cmd if read_stderr else "%s 2> nul" % cmd + logger.debug("Execute \"%s\"" % cmd) + exit = OS.execute("cmd", ["/C", cmd], output, read_stderr) + "macOS", "Linux", "FreeBSD", "NetBSD", "OpenBSD", "BSD": + cmd if read_stderr else "%s 2>/dev/null" % cmd + logger.debug("Execute \"%s\"" % cmd) + exit = OS.execute("bash", ["-c", cmd], output, read_stderr) + var unhandled_os: + logger.error("Unexpected OS: %s" % unhandled_os) + logger.debug("Execution ended(code:%d): %s" % [exit, output]) + return exit + + func init(): + logger.debug("Initializing git at %s..." % cwd) + var output = [] + var exit = _execute("git init", output) + logger.debug("Successfully init" if exit == OK else "Failed to init") + return {"exit": exit, "output": output} + + func clone(src, dest, args={}): + logger.debug("Cloning from %s to %s..." % [src, dest]) + var output = [] + var branch = args.get("branch", "") + var tag = args.get("tag", "") + var commit = args.get("commit", "") + var command = "git clone --depth=1 --progress '%s' '%s'" % [src, dest] + if branch or tag: + command = "git clone --depth=1 --single-branch --branch %s '%s' '%s'" % [branch if branch else tag, src, dest] + elif commit: + return clone_commit(src, dest, commit) + var exit = _execute(command, output) + logger.debug("Successfully cloned from %s" % src if exit == OK else "Failed to clone from %s" % src) + return {"exit": exit, "output": output} + + func clone_commit(src, dest, commit): + var output = [] + if commit.length() < 40: + logger.error("Expected full length 40 digits commit-hash to clone specific commit, given {%s}" % commit) + return {"exit": FAILED, "output": output} + + logger.debug("Cloning from %s to %s @ %s..." % [src, dest, commit]) + var result = init() + if result.exit == OK: + result = remote_add("origin", src) + if result.exit == OK: + result = fetch("%s %s" % ["origin", commit]) + if result.exit == OK: + result = reset("--hard", "FETCH_HEAD") + return result + + func fetch(rm="--all"): + logger.debug("Fetching %s..." % rm.replace("--", "")) + var output = [] + var exit = _execute("git fetch %s" % rm, output) + logger.debug("Successfully fetched" if exit == OK else "Failed to fetch") + return {"exit": exit, "output": output} + + func pull(): + logger.debug("Pulling...") + var output = [] + var exit = _execute("git pull --rebase", output) + logger.debug("Successfully pulled" if exit == OK else "Failed to pull") + return {"exit": exit, "output": output} + + func remote_add(name, src): + logger.debug("Adding remote %s@%s..." % [name, src]) + var output = [] + var exit = _execute("git remote add %s '%s'" % [name, src], output) + logger.debug("Successfully added remote" if exit == OK else "Failed to add remote") + return {"exit": exit, "output": output} + + func reset(mode, to): + logger.debug("Resetting %s %s..." % [mode, to]) + var output = [] + var exit = _execute("git reset %s %s" % [mode, to], output) + logger.debug("Successfully reset" if exit == OK else "Failed to reset") + return {"exit": exit, "output": output} + + func get_commit_comparison(branch_a, branch_b): + var output = [] + var exit = _execute("git rev-list --count --left-right %s...%s" % [branch_a, branch_b], output) + var raw_ahead_behind = output[0].split("\t") + var ahead_behind = [] + for msg in raw_ahead_behind: + ahead_behind.append(msg.to_int()) + return ahead_behind if exit == OK else [] + + func get_current_branch(): + var output = [] + var exit = _execute("git rev-parse --abbrev-ref HEAD", output) + return output[0] if exit == OK else "" + + func get_current_tag(): + var output = [] + var exit = _execute("git describe --tags --exact-match", output) + return output[0] if exit == OK else "" + + func get_current_commit(): + var output = [] + var exit = _execute("git rev-parse --short HEAD", output) + return output[0] if exit == OK else "" + + func is_detached_head(): + var output = [] + var exit = _execute("git rev-parse --short HEAD", output) + return (!!output[0]) if exit == OK else true + + func is_up_to_date(args={}): + if fetch().exit == OK: + var branch = args.get("branch", "") + var tag = args.get("tag", "") + var commit = args.get("commit", "") + + if branch: + if branch == get_current_branch(): + return FAILED if is_detached_head() else OK + elif tag: + if tag == get_current_tag(): + return OK + elif commit: + if commit == get_current_commit(): + return OK + + var ahead_behind = get_commit_comparison("HEAD", "origin") + var is_commit_behind = !!ahead_behind[1] if ahead_behind.size() == 2 else false + return FAILED if is_commit_behind else OK + return FAILED + +class _ThreadPool extends RefCounted: + signal all_thread_finished() + + var active = true + + var _threads = [] + var _finished_threads = [] + var _mutex = Mutex.new() + var _tasks = [] + var logger + + func _init(p_logger): + logger = p_logger + _threads.resize(OS.get_processor_count()) + + func _execute_task(task): + var thread = _get_thread() + var can_execute = thread + if can_execute: + task.thread = weakref(thread) + var callable = task.get("callable") + thread.start(_execute.bind(task), task.priority) + logger.debug("Execute task %s.%s() " % [callable.get_object(), callable.get_method()]) + return can_execute + + func _execute(args): + var callable = args.get("callable") + callable.call() + _mutex.lock() + var thread = args.thread.get_ref() + _threads[_threads.find(thread)] = null + _finished_threads.append(thread) + var all_finished = is_all_thread_finished() + _mutex.unlock() + + logger.debug("Execution finished %s.%s() " % [callable.get_object(), callable.get_method()]) + if all_finished: + logger.debug("All thread finished") + emit_signal("all_thread_finished") + + func _flush_tasks(): + if _tasks.size() == 0: + return + + var executed = true + while executed: + var task = _tasks.pop_front() + if task != null: + executed = _execute_task(task) + if not executed: + _tasks.push_front(task) + else: + executed = false + + func _flush_threads(): + for i in _finished_threads.size(): + var thread = _finished_threads.pop_front() + if not thread.is_alive(): + thread.wait_to_finish() + + func enqueue_task(callable, priority=1): + enqueue({"callable": callable, "priority": priority}) + + func enqueue(task): + var can_execute = false + if active: + can_execute = _execute_task(task) + if not can_execute: + _tasks.append(task) + + func process(delta): + if active: + _flush_tasks() + _flush_threads() + + func stop(): + _tasks.clear() + _flush_threads() + + func _get_thread(): + var thread + for i in OS.get_processor_count(): + var t = _threads[i] + if t: + if not t.is_started(): + thread = t + break + else: + thread = Thread.new() + _threads[i] = thread + break + return thread + + func is_all_thread_finished(): + for i in _threads.size(): + if _threads[i]: + return false + return true + + func is_all_task_finished(): + for i in _tasks.size(): + if _tasks[i]: + return false + return true + +class _Logger extends RefCounted: + enum LogLevel { + ALL, DEBUG, INFO, WARN, ERROR, NONE + } + const DEFAULT_LOG_FORMAT_DETAIL = "[{time}] [{level}] {msg}" + const DEFAULT_LOG_FORMAT_NORMAL = "{msg}" + + var log_level = LogLevel.INFO + var log_format = DEFAULT_LOG_FORMAT_NORMAL + var log_time_format = "{year}/{month}/{day} {hour}:{minute}:{second}" + + func debug(msg, raw=false): + _log(LogLevel.DEBUG, msg, raw) + + func info(msg, raw=false): + _log(LogLevel.INFO, msg, raw) + + func warn(msg, raw=false): + _log(LogLevel.WARN, msg, raw) + + func error(msg, raw=false): + _log(LogLevel.ERROR, msg, raw) + + func _log(level, msg, raw=false): + if log_level <= level: + if raw: + printraw(format_log(level, msg)) + else: + print(format_log(level, msg)) + + func format_log(level, msg): + return log_format.format({ + "time": log_time_format.format(get_formatted_datatime()), + "level": LogLevel.keys()[level], + "msg": msg + }) + + func get_formatted_datatime(): + var datetime = Time.get_datetime_dict_from_system() + datetime.year = "%04d" % datetime.year + datetime.month = "%02d" % datetime.month + datetime.day = "%02d" % datetime.day + datetime.hour = "%02d" % datetime.hour + datetime.minute = "%02d" % datetime.minute + datetime.second = "%02d" % datetime.second + return datetime diff --git a/addons/godot-autogen-docs/cli.gd b/addons/godot-autogen-docs/cli.gd new file mode 100644 index 0000000..6c4a9f3 --- /dev/null +++ b/addons/godot-autogen-docs/cli.gd @@ -0,0 +1,112 @@ +@tool +extends SceneTree + +const TEMPLATE_DIRECTORY := "res://addons/godot-autogen-docs/templates/" + +const TemplateEngine := preload("res://addons/godot-autogen-docs/template_engine.gd") +const Collector := preload("res://addons/godot-autogen-docs/collector.gd") + +# Default configuration + +## A list of directories to collect files from. +var directories := ["res://"] + +## If true, explore each directory recursively +var is_recursive := true + +## A list of patterns to filter files. +var patterns := ["*.gd"] + +## A directory for saving files. +var output_directory := "res://docs/" + + +func _process(_delta) -> bool: + var args = OS.get_cmdline_args() + + # Remove unwanted args + args.remove_at(args.find("--editor")) + args.remove_at(args.find("--script")) + args.remove_at(args.find("addons/godot-autogen-docs/cli.gd")) + + if args.size() > 0: + match args[0]: + "help": + print( + ( + """ +Usage: + godot --editor --headless --quit --script %s [ ...] + +Commands: + help Display this text + markdown Export documentation to markdown + readthedocs Export documentation to markdown compatible with the readthedocs theme + +Options: + -ddir Comma delimited list of directories to collect files from + -doutdir A directory for saving documentation files + +""" + % get_script().get_path() + ) + ) + "markdown", "readthedocs": + var template = args[0] + var template_path = TEMPLATE_DIRECTORY + "%s.template.md" % template + + if not FileAccess.file_exists(template_path): + print("Unable to find the %s template." % template) + + # Find options + for i in range(1, args.size()): + var option = args[i].split("=") + if option.size() == 2: + if option[0] == "-ddir": + directories = option[1].split(",") + elif option[0] == "-doutdir": + output_directory = option[1] + else: + print("Unknown option %s" % option[0]) + else: + print("Invalid argument %s" % args[i]) + + var reference := generate_reference() + render(reference, template_path, output_directory) + _: + print("Unknown command %s" % args[0]) + + return true + + +func generate_reference() -> Dictionary: + var collector := Collector.new() + var files := PackedStringArray() + for dirpath in directories: + files.append_array(collector.find_files(dirpath, patterns, is_recursive)) + return collector.get_reference(files) + + +func load_file(path: String) -> String: + var file := FileAccess.open(path, FileAccess.READ) + var str := file.get_as_text() + file.close() + return str + + +func save_file(content: String, path: String) -> void: + var file := FileAccess.open(path, FileAccess.WRITE) + file.store_string(content) + file.close() + + +func render(reference: Dictionary, template_path: String, output_dirpath: String) -> void: + # Load template + var template_str := load_file(template_path) + + for cls in reference["classes"]: + # Render the template + var content = TemplateEngine.render_template(template_str, cls) + + # Create the file + save_file(content, output_dirpath + cls["name"] + ".md") diff --git a/addons/godot-autogen-docs/collector.gd b/addons/godot-autogen-docs/collector.gd index cce8673..5c58d5e 100644 --- a/addons/godot-autogen-docs/collector.gd +++ b/addons/godot-autogen-docs/collector.gd @@ -1,6 +1,5 @@ ## Finds and generates a code reference from gdscript files. @tool -class_name Collector extends SceneTree var warnings_regex := RegEx.new() @@ -14,16 +13,6 @@ func _init() -> void: ## Returns a list of file paths found in the directory. -## -## **Arguments** -## -## - dirpath: path to the directory from which to search files. -## - patterns: an array of string match patterns, where "*" matches zero or more -## arbitrary characters and "?" matches any single character except a period -## ("."). You can use it to find files by extensions. To find only GDScript -## files, ["*.gd"] -## - is_recursive: if `true`, walks over subdirectories recursively, returning all -## files in the tree. func find_files( dirpath := "", patterns := PackedStringArray(), is_recursive := false, _do_skip_hidden := true ) -> PackedStringArray: @@ -99,31 +88,5 @@ func get_reference(files := PackedStringArray(), refresh_cache := false) -> Dict var symbols: Dictionary = workspace.generate_script_api(file) if symbols.has("name") and symbols["name"] == "": symbols["name"] = file.get_file() - remove_warning_comments(symbols) data["classes"].append(symbols) return data - - -## Directly removes 'warning-ignore', 'warning-ignore-all', and 'warning-disable' -## comments from all symbols in the `symbols` dictionary passed to the function. -func remove_warning_comments(symbols: Dictionary) -> void: - symbols["description"] = remove_warnings_from_description(symbols["description"]) - for meta in ["constants", "members", "signals", "methods", "static_functions"]: - for metadata in symbols[meta]: - metadata["description"] = remove_warnings_from_description(metadata["description"]) - - for sub_class in symbols["sub_classes"]: - remove_warning_comments(sub_class) - - -func remove_warnings_from_description(description: String) -> String: - var lines := description.strip_edges().split("\n") - var clean_lines := PackedStringArray() - for line in lines: - if not warnings_regex.search(line): - clean_lines.append(line) - return "\n".join(clean_lines) - - -func print_pretty_json(reference: Dictionary) -> String: - return JSON.stringify(reference, " ") diff --git a/addons/godot-autogen-docs/markdown.gd b/addons/godot-autogen-docs/markdown.gd deleted file mode 100644 index bbed2ba..0000000 --- a/addons/godot-autogen-docs/markdown.gd +++ /dev/null @@ -1,51 +0,0 @@ -@tool -class_name Markdown -extends SceneTree - - -# Convert data to Markdown -static func convert_to_markdown(cls: Dictionary) -> String: - var markdown_str: String = "" - - markdown_str += "# " + cls["name"] + "\n\n" - markdown_str += cls["description"] + "\n\n" - - # Members - if "members" in cls: - markdown_str += "## Members\n\n" - for member in cls["members"]: - markdown_str += "### " + member["name"] + "\n" - markdown_str += "- **Type:** " + member["data_type"] + "\n" - markdown_str += "- **Description:** " + member["description"] + "\n\n" - - # Methods - if "methods" in cls: - markdown_str += "## Methods\n\n" - for method in cls["methods"]: - markdown_str += "### " + method["name"] + "\n" - markdown_str += "- **Return Type:** " + method["return_type"] + "\n" - markdown_str += "- **Description:** " + method["description"] + "\n\n" - - return markdown_str - - -func _init(): - # Load JSON data from external file - var json_path := "res://reference.json" - var file := FileAccess.open(json_path, FileAccess.READ) - var json_str := file.get_as_text() - file.close() - - # Parse JSON data - var json := JSON.new() - var error := json.parse(json_str) - - for cls in json.data["classes"]: - # Convert Markdown - var markdown_output = convert_to_markdown(cls) - - # Create the markdown file - var markdown_path = "res://docs/dev-guide/api-ref/" + cls["name"] + ".md" - file = FileAccess.open(markdown_path, FileAccess.WRITE) - file.store_string(markdown_output) - file.close() diff --git a/addons/godot-autogen-docs/plugin.cfg b/addons/godot-autogen-docs/plugin.cfg deleted file mode 100644 index 3b93ccf..0000000 --- a/addons/godot-autogen-docs/plugin.cfg +++ /dev/null @@ -1,6 +0,0 @@ -[plugin] - -name="godot-autogen-docs" -description="" -author="florianvazelle" -version="0.1.0" diff --git a/addons/godot-autogen-docs/reference_collector.gd b/addons/godot-autogen-docs/reference_collector.gd deleted file mode 100644 index 4c53d09..0000000 --- a/addons/godot-autogen-docs/reference_collector.gd +++ /dev/null @@ -1,29 +0,0 @@ -## Finds and generates a code reference from gdscript files. -## -## To use this tool: -## -## - Place this script and Collector.gd in your Godot project folder. -## - Open the script in the script editor. -## - Modify the properties below to control the tool's behavior. -## - Go to File -> Run to run the script in the editor. -@tool -class_name ReferenceCollector -extends EditorScript - -var collector: SceneTree = load("res://addons/godot-autogen-docs/collector.gd").new() -## A list of directories to collect files from. -var directories := ["res://src"] -## If true, explore each directory recursively -var is_recursive := true -## A list of patterns to filter files. -var patterns := ["*.gd"] -## Output path to save the class reference. -var save_path := "res://reference.json" - - -func _run() -> void: - var files := PackedStringArray() - for dirpath in directories: - files.append_array(collector.find_files(dirpath, patterns, is_recursive)) - var json: String = collector.print_pretty_json(collector.get_reference(files)) - collector.save_text(save_path, json) diff --git a/addons/godot-autogen-docs/reference_collector_cli.gd b/addons/godot-autogen-docs/reference_collector_cli.gd deleted file mode 100644 index 255e3bf..0000000 --- a/addons/godot-autogen-docs/reference_collector_cli.gd +++ /dev/null @@ -1,20 +0,0 @@ -## Finds and generates a code reference from gdscript files. -@tool -class_name ReferenceCollectorCli -extends SceneTree - -var collector: SceneTree = load("res://addons/godot-autogen-docs/collector.gd").new() -## A list of directories to collect files from. -var directories := ["res://"] -## If true, explore each directory recursively -var is_recursive := true -## A list of patterns to filter files. -var patterns := ["*.gd"] - - -func _init() -> void: - var files := PackedStringArray() - for dirpath in directories: - files.append_array(collector.find_files(dirpath, patterns, is_recursive)) - var json: String = collector.print_pretty_json(collector.get_reference(files)) - collector.save_text("res://reference.json", json) diff --git a/addons/godot-autogen-docs/template_engine.gd b/addons/godot-autogen-docs/template_engine.gd new file mode 100644 index 0000000..1b39ddd --- /dev/null +++ b/addons/godot-autogen-docs/template_engine.gd @@ -0,0 +1,223 @@ +static func replace_variable(template: String, placeholder_key: String, value: Variant) -> String: + # Check if the value is an Array + # if value is Array: + # print("Attempt to replace with an array like a variable, use for-loops instead.") + # return "" + + # If the value is a Dictionary, delegate to render_dictionary + if value is Dictionary: + return render_dictionary(template, placeholder_key, value) + + # Replace the placeholder in the template with the string representation of the value + var placeholder = "{{ %s }}" % placeholder_key + return template.replace(placeholder, str(value)) + + +## Iterate through the keys in the dictionary and recursively replace placeholders +static func render_dictionary( + template: String, placeholder_key: String, data: Dictionary +) -> String: + for key in data.keys(): + var placeholder_subkey = "%s.%s" % [placeholder_key, key] + template = replace_variable(template, placeholder_subkey, data[key]) + return template + + +## Represents a for-loop block with relevant information +class ForLoopBlock: + var forloop_block: String + var data_key: String + var loop_variable: String + var loop_content: String + + func _init(flb: String, dk: String, lv: String, lc: String): + forloop_block = flb + data_key = dk + loop_variable = lv + loop_content = lc + + +class Token: + var text: String + + func _init(token: String): + text = token + + ## Convert a template and regex match to a Token instance + static func convert(template: String, regex_match: RegExMatch) -> Token: + var token := template.substr( + regex_match.get_start(), regex_match.get_end() - regex_match.get_start() + ) + + # Identify ForToken or EndForToken based on the token content + if token.begins_with("{% for"): + return ForToken.new(token) + if token == "{% endfor %}": + return EndForToken.new(token) + + return Token.new(token) + + +# Represents a {% for %} token +class ForToken: + extends Token + + +# Represents a {% endfor %} token +class EndForToken: + extends Token + + +## Tokenizes the template into individual elements for processing +static func tokenize_template(template: String, placeholder_key: String = "") -> Array: + var tokens = [] + + var regex = RegEx.new() + + var err + if placeholder_key == "": + err = regex.compile( + r"{% for (?\w+) in (?(\w|\.)+) %}|{% endfor %}" + ) + else: + err = regex.compile( + ( + r"{% for (?\w+) in " + + placeholder_key + + r"\.(?\w+) %}|{% endfor %}" + ) + ) + + if err != OK or not regex.is_valid(): + printerr("Failed to compile the template regex.") + return tokens + + var i := 0 + var regex_matchs := regex.search_all(template) + while i < regex_matchs.size(): + var regex_match = regex_matchs[i] + var token := Token.convert(template, regex_match) + + if token is ForToken: + # Find the corresponding endfor + var level = 1 + i += 1 + + var sub_regex_match: RegExMatch + while level > 0 and i < regex_matchs.size(): + sub_regex_match = regex_matchs[i] + var subtoken := Token.convert(template, sub_regex_match) + + if subtoken is ForToken: + level += 1 + elif subtoken is EndForToken: + level -= 1 + i += 1 + + assert(level == 0, "Missing endfor!") + + var forloop_block := template.substr( + regex_match.get_start(), sub_regex_match.get_end() - regex_match.get_start() + ) + + # Extract capture groups from the regex match + var loop_variable := regex_match.get_string("loop_variable") + var data_key := regex_match.get_string("data_key") + var loop_content := template.substr( + regex_match.get_end(), sub_regex_match.get_start() - regex_match.get_end() + ) + + ( + tokens + . append( + ( + ForLoopBlock + . new( + forloop_block, + data_key, + loop_variable, + loop_content, + ) + ) + ) + ) + + elif token is EndForToken: + assert(false, "endfor without forloop!") + + else: + printerr("Unknown token: %s" % token) + + return tokens + + +## Renders for-loops in the template with the provided data +static func render_forloops( + template: String, data: Dictionary, placeholder_key: String = "" +) -> String: + # Tokenize the template into individual elements + var tokens = tokenize_template(template, placeholder_key) + + var i := 0 + while i < tokens.size(): + var token = tokens[i] + + # Process the loop_content + template = _render_forloops( + template, + data, + token.forloop_block, + token.data_key, + token.loop_variable, + token.loop_content + ) + + i += 1 + + return template + + +static func _render_forloops( + template: String, + data: Dictionary, + forloops_block: String, + data_key: String, + loop_variable: String, + loop_content: String +) -> String: + var loop_result = "" + + # Check if the data dictionary has the specified key + if data.has(data_key): + # Iterate over each item in the data array for the specified key + for item in data[data_key]: + # Check if the item is a Dictionary or an object with a 'to_dict' method + var item_data = {} + if item is Dictionary: + item_data = item + elif item.has_method("to_dict"): + item_data = item.to_dict() + else: + printerr("Object doesn't have 'to_dict' method. Skipping in loop.") + + # Render the loop content with the data of the current item + loop_result += render_dictionary(loop_content, loop_variable, item_data) + + loop_result = render_forloops(loop_result, item_data, loop_variable) + + # Replace the entire loop block with the rendered result + template = template.replace(forloops_block, loop_result) + + return template + + +## Define a static function to render a template with data +static func render_template(template: String, data: Dictionary) -> String: + # Replace variables in the template with their corresponding values + for key in data.keys(): + template = replace_variable(template, key, data[key]) + + template = render_forloops(template, data) + + # Return the final result after processing variables and loops + return template diff --git a/addons/godot-autogen-docs/templates/markdown.template.md b/addons/godot-autogen-docs/templates/markdown.template.md new file mode 100644 index 0000000..20c500d --- /dev/null +++ b/addons/godot-autogen-docs/templates/markdown.template.md @@ -0,0 +1,23 @@ +# {{ name }} + +{{ description }} + +## Members +{% for member in members %} + +### {{ member.name }} + +- **Type:** {{ member.data_type }} +- **Description:** {{ member.description }} + +{% endfor %} + +## Methods +{% for method in methods %} + +### {{ method.name }} + +- **Return Type:** {{ method.return_type }} +- **Description:** {{ method.description }} + +{% endfor %} diff --git a/addons/godot-autogen-docs/templates/readthedocs.template.md b/addons/godot-autogen-docs/templates/readthedocs.template.md new file mode 100644 index 0000000..e3c0e6e --- /dev/null +++ b/addons/godot-autogen-docs/templates/readthedocs.template.md @@ -0,0 +1,124 @@ +
+
+ + class + + + + {{ name }} + + + + extends + + + + {{ extends_class }} + +
+
+

{{ description }}

+ {% for constant in constants %} +
+
+ + const + + + + {{ constant.name }} + +
+

{{ constant.description }}

+
+ {% endfor %} + {% for signal in signals %} +
+
+ + signal + + + + {{ signal.name }} + +
+

{{ signal.description }}

+
+ {% endfor %} + {% for member in members %} +
+
+ + var + + + + {{ member.name }} + +
+

{{ member.description }}

+
+ {% endfor %} + {% for method in methods %} +
+
+ + func + + + + {{ method.name }} + +
+

{{ method.description }}

+
+
Parameters:
+
+
    + {% for arg in method.arguments %} +
  • +

    {{ arg.name }} ({{ arg.type }})

    +
  • + {% endfor %} +
+
+
Return type:
+
+

{{ method.return_type }}

+
+
+
+ {% endfor %} + {% for static_function in static_functions %} +
+
+ + static func + + + + {{ static_function.name }} + +
+

{{ static_function.description }}

+
+
Parameters:
+
+
    + {% for arg in static_function.arguments %} +
  • +

    {{ arg.name }} ({{ arg.type }})

    +
  • + {% endfor %} +
+
+
Return type:
+
+

{{ static_function.return_type }}

+
+
+
+ {% endfor %} +
+
diff --git a/docs/dev-guide/api-ref/Collector.md b/docs/dev-guide/api-ref/Collector.md deleted file mode 100644 index ea11c06..0000000 --- a/docs/dev-guide/api-ref/Collector.md +++ /dev/null @@ -1,53 +0,0 @@ -# Collector - -Finds and generates a code reference from gdscript files. - -## Members - -### warnings_regex -- **Type:** RegEx -- **Description:** - -## Methods - -### _init -- **Return Type:** Collector -- **Description:** - -### find_files -- **Return Type:** PackedStringArray -- **Description:** Returns a list of file paths found in the directory. - - **Arguments** - - - dirpath: path to the directory from which to search files. - - patterns: an array of string match patterns, where "*" matches zero or more - arbitrary characters and "?" matches any single character except a period - ("."). You can use it to find files by extensions. To find only GDScript - files, ["*.gd"] - - is_recursive: if `true`, walks over subdirectories recursively, returning all - files in the tree. - -### save_text -- **Return Type:** null -- **Description:** Saves text to a file. - -### get_reference -- **Return Type:** Dictionary -- **Description:** Parses a list of GDScript files and returns a list of dictionaries with the - code reference data. - - If `refresh_cache` is true, will refresh Godot's cache and get fresh symbols. - -### remove_warning_comments -- **Return Type:** null -- **Description:** Directly removes 'warning-ignore', 'warning-ignore-all', and 'warning-disable' - comments from all symbols in the `symbols` dictionary passed to the function. - -### remove_warnings_from_description -- **Return Type:** String -- **Description:** - -### print_pretty_json -- **Return Type:** String -- **Description:** diff --git a/docs/dev-guide/api-ref/Markdown.md b/docs/dev-guide/api-ref/Markdown.md deleted file mode 100644 index 4835cb7..0000000 --- a/docs/dev-guide/api-ref/Markdown.md +++ /dev/null @@ -1,11 +0,0 @@ -# Markdown - - - -## Members - -## Methods - -### _init -- **Return Type:** Markdown -- **Description:** diff --git a/docs/dev-guide/api-ref/ReferenceCollector.md b/docs/dev-guide/api-ref/ReferenceCollector.md deleted file mode 100644 index e904502..0000000 --- a/docs/dev-guide/api-ref/ReferenceCollector.md +++ /dev/null @@ -1,38 +0,0 @@ -# ReferenceCollector - -Finds and generates a code reference from gdscript files. - - To use this tool: - - - Place this script and Collector.gd in your Godot project folder. - - Open the script in the script editor. - - Modify the properties below to control the tool's behavior. - - Go to File -> Run to run the script in the editor. - -## Members - -### collector -- **Type:** SceneTree -- **Description:** - -### directories -- **Type:** Array -- **Description:** A list of directories to collect files from. - -### is_recursive -- **Type:** bool -- **Description:** If true, explore each directory recursively - -### patterns -- **Type:** Array -- **Description:** A list of patterns to filter files. - -### save_path -- **Type:** String -- **Description:** Output path to save the class reference. - -## Methods - -### _run -- **Return Type:** null -- **Description:** diff --git a/docs/dev-guide/api-ref/ReferenceCollectorCli.md b/docs/dev-guide/api-ref/ReferenceCollectorCli.md deleted file mode 100644 index 7fe6e82..0000000 --- a/docs/dev-guide/api-ref/ReferenceCollectorCli.md +++ /dev/null @@ -1,27 +0,0 @@ -# ReferenceCollectorCli - -Finds and generates a code reference from gdscript files. - -## Members - -### collector -- **Type:** SceneTree -- **Description:** - -### directories -- **Type:** Array -- **Description:** A list of directories to collect files from. - -### is_recursive -- **Type:** bool -- **Description:** If true, explore each directory recursively - -### patterns -- **Type:** Array -- **Description:** A list of patterns to filter files. - -## Methods - -### _init -- **Return Type:** ReferenceCollectorCli -- **Description:** diff --git a/docs/dev-guide/api-ref/cli.gd.md b/docs/dev-guide/api-ref/cli.gd.md new file mode 100644 index 0000000..80181c5 --- /dev/null +++ b/docs/dev-guide/api-ref/cli.gd.md @@ -0,0 +1,271 @@ +
+
+ + class + + + + cli.gd + + + + extends + + + + ["SceneTree"] + +
+
+

+ +
+
+ + const + + + + TEMPLATE_DIRECTORY + +
+

+
+ +
+
+ + const + + + + TemplateEngine + +
+

+
+ +
+
+ + const + + + + Collector + +
+

+
+ + + +
+
+ + var + + + + directories + +
+

A list of directories to collect files from.

+
+ +
+
+ + var + + + + is_recursive + +
+

If true, explore each directory recursively

+
+ +
+
+ + var + + + + patterns + +
+

A list of patterns to filter files.

+
+ +
+
+ + var + + + + output_directory + +
+

A directory for saving files.

+
+ + +
+
+ + func + + + + _process + +
+

+
+
Parameters:
+
+
    + +
  • +

    _delta (Variant)

    +
  • + +
+
+
Return type:
+
+

bool

+
+
+
+ +
+
+ + func + + + + generate_reference + +
+

+
+
Parameters:
+
+
    + +
+
+
Return type:
+
+

Dictionary

+
+
+
+ +
+
+ + func + + + + load_file + +
+

+
+
Parameters:
+
+
    + +
  • +

    path (String)

    +
  • + +
+
+
Return type:
+
+

String

+
+
+
+ +
+
+ + func + + + + save_file + +
+

+
+
Parameters:
+
+
    + +
  • +

    content (String)

    +
  • + +
  • +

    path (String)

    +
  • + +
+
+
Return type:
+
+

null

+
+
+
+ +
+
+ + func + + + + render + +
+

+
+
Parameters:
+
+
    + +
  • +

    reference (Dictionary)

    +
  • + +
  • +

    template_path (String)

    +
  • + +
  • +

    output_dirpath (String)

    +
  • + +
+
+
Return type:
+
+

null

+
+
+
+ + +
+
diff --git a/docs/dev-guide/api-ref/collector.gd.md b/docs/dev-guide/api-ref/collector.gd.md new file mode 100644 index 0000000..347a97b --- /dev/null +++ b/docs/dev-guide/api-ref/collector.gd.md @@ -0,0 +1,172 @@ +
+
+ + class + + + + collector.gd + + + + extends + + + + ["SceneTree"] + +
+
+

+ + + +
+
+ + var + + + + warnings_regex + +
+

+
+ + +
+
+ + func + + + + _init + +
+

+
+
Parameters:
+
+
    + +
+
+
Return type:
+
+

res://addons/godot-autogen-docs/collector.gd

+
+
+
+ +
+
+ + func + + + + find_files + +
+

Returns a list of file paths found in the directory.

+
+
Parameters:
+
+
    + +
  • +

    dirpath (String)

    +
  • + +
  • +

    patterns (PackedStringArray)

    +
  • + +
  • +

    is_recursive (bool)

    +
  • + +
  • +

    _do_skip_hidden (bool)

    +
  • + +
+
+
Return type:
+
+

PackedStringArray

+
+
+
+ +
+
+ + func + + + + save_text + +
+

Saves text to a file.

+
+
Parameters:
+
+
    + +
  • +

    path (String)

    +
  • + +
  • +

    content (String)

    +
  • + +
+
+
Return type:
+
+

null

+
+
+
+ +
+
+ + func + + + + get_reference + +
+

Parses a list of GDScript files and returns a list of dictionaries with the code reference data. If `refresh_cache` is true, will refresh Godot's cache and get fresh symbols.

+
+
Parameters:
+
+
    + +
  • +

    files (PackedStringArray)

    +
  • + +
  • +

    refresh_cache (bool)

    +
  • + +
+
+
Return type:
+
+

Dictionary

+
+
+
+ + +
+
diff --git a/docs/dev-guide/api-ref/template_engine.gd.md b/docs/dev-guide/api-ref/template_engine.gd.md new file mode 100644 index 0000000..f8af880 --- /dev/null +++ b/docs/dev-guide/api-ref/template_engine.gd.md @@ -0,0 +1,253 @@ +
+
+ + class + + + + template_engine.gd + + + + extends + + + + [] + +
+
+

+ + + + + +
+
+ + static func + + + + replace_variable + +
+

+
+
Parameters:
+
+
    + +
  • +

    template (String)

    +
  • + +
  • +

    placeholder_key (String)

    +
  • + +
  • +

    value (Variant)

    +
  • + +
+
+
Return type:
+
+

String

+
+
+
+ +
+
+ + static func + + + + render_dictionary + +
+

Iterate through the keys in the dictionary and recursively replace placeholders

+
+
Parameters:
+
+
    + +
  • +

    template (String)

    +
  • + +
  • +

    placeholder_key (String)

    +
  • + +
  • +

    data (Dictionary)

    +
  • + +
+
+
Return type:
+
+

String

+
+
+
+ +
+
+ + static func + + + + tokenize_template + +
+

Tokenizes the template into individual elements for processing

+
+
Parameters:
+
+
    + +
  • +

    template (String)

    +
  • + +
  • +

    placeholder_key (String)

    +
  • + +
+
+
Return type:
+
+

Array

+
+
+
+ +
+
+ + static func + + + + render_forloops + +
+

Renders for-loops in the template with the provided data

+
+
Parameters:
+
+
    + +
  • +

    template (String)

    +
  • + +
  • +

    data (Dictionary)

    +
  • + +
  • +

    placeholder_key (String)

    +
  • + +
+
+
Return type:
+
+

String

+
+
+
+ +
+
+ + static func + + + + _render_forloops + +
+

+
+
Parameters:
+
+
    + +
  • +

    template (String)

    +
  • + +
  • +

    data (Dictionary)

    +
  • + +
  • +

    forloops_block (String)

    +
  • + +
  • +

    data_key (String)

    +
  • + +
  • +

    loop_variable (String)

    +
  • + +
  • +

    loop_content (String)

    +
  • + +
+
+
Return type:
+
+

String

+
+
+
+ +
+
+ + static func + + + + render_template + +
+

Define a static function to render a template with data

+
+
Parameters:
+
+
    + +
  • +

    template (String)

    +
  • + +
  • +

    data (Dictionary)

    +
  • + +
+
+
Return type:
+
+

String

+
+
+
+ +
+
diff --git a/docs/dev-guide/index.md b/docs/dev-guide/index.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/index.md b/docs/index.md index f14697f..dc58f8d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ # Godot Autogen Docs -![Godot Badge](https://img.shields.io/badge/godot-4.1-blue?logo=Godot-Engine&logoColor=white) +![Godot Badge](https://img.shields.io/badge/godot-4.0%20%7C%204.1%20%7C%204.2-blue?logo=Godot-Engine&logoColor=white) ![license](https://img.shields.io/badge/license-MIT-green?logo=open-source-initiative&logoColor=white) Godot addon that automatically turns your code diff --git a/docs/user-guide/index.md b/docs/user-guide/index.md index 51d0e02..7f9267b 100644 --- a/docs/user-guide/index.md +++ b/docs/user-guide/index.md @@ -1,11 +1,45 @@ -# Usage +## Command Line Interface -To generate a `reference.json`, run: +The Godot Autogen Docs tool provides a command-line interface for generating documentation from Godot Engine scripts. This guide outlines the available commands and options. + +### Usage + +```bash +$ godot --editor --headless --quit --script res://addons/godot-autogen-docs/cli.gd [