Created
April 3, 2024 21:27
-
-
Save Ezcha/164f3aaf0ac96471bdf0d33fb57b6e58 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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!'); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Current limitations: