Skip to content

Instantly share code, notes, and snippets.

@Ezcha
Created April 3, 2024 21:27
Show Gist options
  • Save Ezcha/164f3aaf0ac96471bdf0d33fb57b6e58 to your computer and use it in GitHub Desktop.
Save Ezcha/164f3aaf0ac96471bdf0d33fb57b6e58 to your computer and use it in GitHub Desktop.
// USAGE:
// project_directory docs_version=stable
//
// https://ezcha.net
const fs = require('node:fs');
const path = require('node:path');
const PATTERN_EXTENDS = /^extends(?:\s*)(?<name>[a-zA-Z0-9_]*)/;
const PATTERN_CLASS_NAME = /^class_name(?:\s*)(?<name>[a-zA-Z0-9_]*)/;
const PATTERN_PROPERTY = /^(?:@?onready\s*)?(?:var|const)(?:\s*)(?<name>[a-zA-Z0-9_]*)(?:\s*:\s*(?<type>[a-zA-Z0-9_\[\].]*))?(?:\s*)=(?:\s*)(?<value>(?:".*")|\S*)/;
const PATTERN_ARRAY_SUBTYPE = /Array\[(?<subtype>[a-zA-Z0-9_.]*)\]/;
const PATTERN_METHOD = /^func\s*(?<name>[a-zA-Z0-9_]*)(?<arguments>\(.*\))(?:\s*->\s*(?<type>\S*))?:/;
const PATTERN_SIGNAL = /^signal\s*(?<name>[a-zA-Z0-9_]*)(?<arguments>\(.*\))?/;
const PATTERN_ARGUMENTS = /\(?(?<name>[a-zA-Z0-9_]*)(?:(?:\s*:\s*)(?<type>[a-zA-Z0-9_.]*))?(?:(?:\s*=\s*)(?<value>\S*))?(?:\)|,)/g;
const PATTERN_ENUM_GROUP_START = /^enum.*{/;
const PATTERN_ENUM_GROUP = /^enum\s*(?<name>[a-zA-Z0-9_]*)\s*{(?<keys>(.|\n)*)}/m;
const PATTERN_ENUM_GROUP_KEYS = /^\s*(?<name>[a-zA-Z0-9_]*)(?:\s*=\s*(?<value>[0-9]*))/mg;
(() => {
const baseDir = process.argv[2];
if (!baseDir) {
console.log('Please specify a directory.');
return;
}
if (!fs.existsSync(baseDir)) {
console.log('Invalid directory provided.');
return;
}
const version = process.argv[3] || 'stable';
const getBuiltInUrl = (type) => {
const lower = type.toLowerCase();
if (lower === 'void') return undefined;
return `https://docs.godotengine.org/en/${version}/classes/class_${lower}.html`;
}
// Parse data from gdscript files
const parseGd = (filePath) => {
let classData = {
name: '',
extends: '',
brief: '',
description: '',
properties: [],
methods: [],
signals: [],
enums: []
}
try {
const gd = fs.readFileSync(filePath, 'utf8');
const lines = gd.split('\n');
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
const currentLine = lines[lineIndex];
if (!currentLine || currentLine.startsWith(' ') || currentLine.startsWith('\t')) continue;
// Comment helper
const getCommentBlock = (direction) => {
let dist = 0;
let commentLines = [];
while(true) { // ew
const offsetIndex = lineIndex + dist + direction;
if (offsetIndex < 0 || offsetIndex >= lines.length) break;
const checkLine = lines[offsetIndex];
if (!checkLine.startsWith('##')) break;
commentLines.push(checkLine.substring(2).trim());
dist += direction;
}
if (direction > 0) lineIndex += dist;
return commentLines;
}
// Extends
if (!classData.extends) {
const extendsMatch = PATTERN_EXTENDS.exec(currentLine);
if (extendsMatch !== null) {
classData.extends = extendsMatch.groups.name;
continue;
}
}
// Class name
if (!classData.name) {
const classNameMatch = PATTERN_CLASS_NAME.exec(currentLine);
if (classNameMatch !== null) {
classData.name = classNameMatch.groups.name;
// Class brief and description
const block = getCommentBlock(1);
let target = 'brief';
for (const comment of block) {
if (!comment && target !== 'description') {
target = 'description';
continue;
}
if (classData[target]) classData[target] += ' ';
classData[target] += comment;
}
continue;
}
}
// Properties
const propertyMatch = PATTERN_PROPERTY.exec(currentLine);
if (propertyMatch !== null) {
if (propertyMatch.groups.name.startsWith('_')) continue;
const block = getCommentBlock(-1);
const description = (block.length > 0) ? block.join(' ') : '';
classData.properties.push({ ...propertyMatch.groups, description });
continue;
}
// Methods
const methodMatch = PATTERN_METHOD.exec(currentLine);
if (methodMatch !== null) {
if (methodMatch.groups.name.startsWith('_')) continue;
const block = getCommentBlock(-1);
const description = (block.length > 0) ? block.join(' ') : '';
const argumentStr = methodMatch.groups.arguments;
const arguments = [];
if (argumentStr !== '()') {
const argumentMatches = argumentStr.matchAll(PATTERN_ARGUMENTS);
for (const match of argumentMatches) arguments.push({...match.groups});
}
classData.methods.push({ name: methodMatch.groups.name, type: methodMatch.groups.type, arguments, description });
continue;
}
// Signals
const signalMatch = PATTERN_SIGNAL.exec(currentLine);
if (signalMatch !== null) {
if (signalMatch.groups.name.startsWith('_')) continue;
const block = getCommentBlock(-1);
const description = (block.length > 0) ? block.join(' ') : '';
const argumentStr = signalMatch.groups.arguments || '()';
const arguments = [];
if (argumentStr !== '()') {
const argumentMatches = argumentStr.matchAll(PATTERN_ARGUMENTS);
for (const match of argumentMatches) arguments.push({...match.groups});
}
classData.signals.push({ name: signalMatch.groups.name, arguments, description });
continue;
}
// Enumerations
if (currentLine.match(PATTERN_ENUM_GROUP_START)) {
const remainingGd = lines.slice(lineIndex).join('\n');
const enumGroupMatch = PATTERN_ENUM_GROUP.exec(remainingGd);
if (enumGroupMatch === null) continue;
const enumKeysMatch = enumGroupMatch.groups.keys.matchAll(PATTERN_ENUM_GROUP_KEYS);
const keys = [];
for (const match of enumKeysMatch) {
keys.push({ name: match.groups.name, value: parseInt(match.groups.value) });
}
classData.enums.push({ name: enumGroupMatch.groups.name, keys });
continue;
}
}
} catch (err) {
console.error(err);
}
return classData;
}
// Crawl directories
const crawl = (directoryPath) => {
var classList = [];
const contents = fs.readdirSync(directoryPath).map(fileName => path.join(directoryPath, fileName));
contents.sort((a, b) => {
const aIsFile = fs.lstatSync(a).isFile();
const bIsFile = fs.lstatSync(b).isFile()
if (aIsFile && !bIsFile) return -1;
if (a < b) return -1;
return 1;
});
for (const contentPath of contents) {
if (!fs.lstatSync(contentPath).isFile()) {
classList = [...classList, ...crawl(contentPath)];
continue;
}
if (!contentPath.endsWith('.gd')) continue;
classList.push(parseGd(contentPath));
}
return classList;
}
console.log('Parsing gdscript files...');
const classList = crawl(baseDir);
const customClassNames = classList.filter((c) => c.name).map((c) => c.name);
// Prepare to generate markdown
console.log('Generating markdown...');
let markdown = '';
const getUrl = (type) => {
const split = type.split('.');
return customClassNames.includes(split[0]) ? `#${split[0]}` : getBuiltInUrl(split[0]);
}
const formatType = (type) => {
if (type === undefined) type = 'Variant';
const subtypeMatch = PATTERN_ARRAY_SUBTYPE.exec(type);
if (subtypeMatch) {
const subtype = subtypeMatch.groups.subtype;
const subtypeUrl = getUrl(subtype);
return subtypeUrl ? `[Array](${getBuiltInUrl('array')}) [ [${subtype}](${subtypeUrl}) ]` : `[Array](${getBuiltInUrl('array')}) [ ${subtype} ]`;
}
const typeUrl = getUrl(type);
return typeUrl ? `[${type}](${typeUrl})` : type;
}
const formatArguments = (arguments) => {
if (arguments.length === 0) return '( )';
const parts = [];
for (const arg of arguments) {
let str = '';
const type = arg.type || 'Variant';
const typeUrl = getUrl(type);
str += typeUrl ? `[${type}](${typeUrl}) ` : `${type} `;
str += arg.name;
if (arg.value) str += `=${arg.value}`;
parts.push(str);
}
return `( ${parts.join(', ')} )`;
}
// Class Index
markdown += '# Class Index\n';
for (const classData of classList) {
if (!classData.name) continue;
markdown += `\n* [${classData.name}](#${classData.name})`;
}
// Class Documentation
markdown += '\n\n# Class Documentation';
for (const classData of classList) {
// Name
if (!classData.name) continue;
markdown += `\n\n<a name="${classData.name}"></a>\n## ${classData.name}`;
// Inherits
if (classData.extends) {
const extendsUrl = getUrl(classData.extends);
markdown += extendsUrl ? `\n\n**Inherits:** [${classData.extends}](${extendsUrl})` : `\n\n**Inherits:** ${classData.extends}`;
}
// Short description
if (classData.brief) {
markdown += `\n\n${classData.brief}`;
}
// Full description
if (classData.description) {
markdown += `\n\n### Description`;
markdown += `\n\n${classData.description}`;
}
// Properties table
if (classData.properties.length > 0) {
markdown += '\n\n### Properties\n\n';
markdown += '|Type|Name|Default|\n|-|-|-|';
for (const property of classData.properties) {
markdown += `\n|${formatType(property.type)}|`;
if (property.description) {
markdown += `[${property.name}](#${classData.name}-property-${property.name})|`;
} else {
markdown += `${property.name}|`;
}
markdown += `${property.value}|`;
}
}
// Methods table
if (classData.methods.length > 0) {
markdown += '\n\n### Methods';
markdown += '\n\n|Returns|Name|\n|-|-|';
for (const method of classData.methods) {
markdown += `\n|${formatType(method.type)}|`;
if (method.description) {
markdown += `[${method.name}](#${classData.name}-method-${method.name}) ${formatArguments(method.arguments)}`;
} else {
markdown += `${method.name} ${formatArguments(method.arguments)}`;
}
}
}
// Signals list
if (classData.signals.length > 0) {
markdown += '\n\n### Signals';
for (const signal of classData.signals) {
markdown += `\n\n**${signal.name}** ${formatArguments(signal.arguments)}`;
markdown += `\n\n${signal.description}`;
}
}
// Enum list
if (classData.enums.length > 0) {
markdown += '\n\n### Enumerations';
for (const en of classData.enums) {
markdown += `\n\nenum **${en.name}**:\n`;
for (const key of en.keys) {
markdown += `\n* ${en.name} **${key.name}** = ${key.value}`;
}
}
}
// Property Descriptions
const descProps = classData.properties.filter((p) => p.description);
if (descProps.length > 0) {
markdown += '\n\n### Property Descriptions';
for (const property of descProps) {
markdown += `\n\n<a name="${classData.name}-property-${property.name}"></a>`;
markdown += `\n${formatType(property.type)} **${property.name}**`;
if (property.value) {
markdown += ` = ${property.value}`;
}
markdown += `\n\n${property.description}`;
}
}
// Method Descriptions
const descMethods = classData.methods.filter((m) => m.description);
if (descMethods.length > 0) {
markdown += '\n\n### Method Descriptions';
for (const method of descMethods) {
markdown += `\n\n<a name="${classData.name}-method-${method.name}"></a>`;
markdown += `\n${formatType(method.type)} **${method.name}** ${formatArguments(method.arguments)}`;
markdown += `\n\n${method.description}`;
}
}
}
markdown += '\n';
// Save markdown to file
try {
fs.writeFileSync(path.join(__dirname, 'docs.md'), markdown);
} catch (err) {
console.error(err);
}
console.log('Done!');
})();
@Ezcha
Copy link
Author

Ezcha commented Apr 3, 2024

Current limitations:

  • Does not support inline doc comments
  • Custom enum links break
  • Multi-line dictionary/array default values do not display properly

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment