Files
avorion-docgen/main.go

368 lines
10 KiB
Go

package main
import (
"flag"
"fmt"
"os"
"path/filepath"
"strings"
_ "embed"
logger "git.site.quack-lab.dev/dave/cylogger"
utils "git.site.quack-lab.dev/dave/cyutils"
"github.com/PuerkitoBio/goquery"
)
func main() {
outdir := flag.String("o", ".", "Output directory")
flag.Parse()
logger.InitFlag()
logger.Info("Starting...")
files := flag.Args()
if len(files) == 0 {
logger.Error("No files specified")
flag.Usage()
return
}
// Group files by their base class name (without [Client]/[Server])
classGroups := make(map[string][]string)
for _, file := range files {
// Extract base class name from filename
baseName := extractBaseClassName(file)
classGroups[baseName] = append(classGroups[baseName], file)
}
// Convert groups to work items for parallel processing
var workItems []WorkItem
for baseName, groupFiles := range classGroups {
workItems = append(workItems, WorkItem{
BaseName: baseName,
GroupFiles: groupFiles,
})
}
// Process each group in parallel
utils.WithWorkers(100, workItems, func(worker int, item WorkItem) {
// First check for enums in each file
for _, file := range item.GroupFiles {
hasEnums, err := processEnums(file, *outdir)
if err != nil {
logger.Error("Error processing enums in file %s: %v", file, err)
}
// If the file only contained enums, skip class parsing
if hasEnums && !strings.Contains(file, "[Client]") && !strings.Contains(file, "[Server]") {
return
}
}
// Then process classes
if len(item.GroupFiles) == 1 {
// Single file, process normally
class, err := ParseClass(item.GroupFiles[0])
if err != nil {
logger.Error("Error parsing file: %v", err)
return
}
class.Write(*outdir, classTemplate)
} else {
// Multiple files for same class, merge them
mergedClass, err := MergeClasses(item.GroupFiles)
if err != nil {
logger.Error("Error merging classes for %s: %v", item.BaseName, err)
return
}
mergedClass.Write(*outdir, classTemplate)
}
})
}
func processEnums(file string, outdir string) (bool, error) {
log := logger.Default.WithPrefix("processEnums")
log.Info("Processing enums from file: %s", file)
filehandle, err := os.Open(file)
if err != nil {
return false, fmt.Errorf("error opening file: %w", err)
}
defer filehandle.Close()
doc, err := goquery.NewDocumentFromReader(filehandle)
if err != nil {
return false, fmt.Errorf("error parsing file: %w", err)
}
// Find all code containers
codeblocks := doc.Find("div.codecontainer")
log.Trace("Found %d code blocks", codeblocks.Length())
foundEnums := false
codeblocks.Each(func(i int, s *goquery.Selection) {
// Check if this is an enum block
enumType := s.Find("span.type").First()
if !strings.HasPrefix(strings.TrimSpace(enumType.Text()), "enum") {
return
}
foundEnums = true
// Get enum name from the ID attribute
enumName := s.AttrOr("id", "")
if enumName == "" {
return
}
enum := &Enum{
Name: enumName,
Values: []string{},
}
// Get enum comment if any
comment := s.Find("p").Text()
if comment != "" {
enum.Comment = strings.TrimSpace(comment)
}
// Get enum values
s.Contents().Each(func(i int, s *goquery.Selection) {
if s.Is("br") {
return
}
text := strings.TrimSpace(s.Text())
if text == "" || strings.HasPrefix(text, "enum") {
return
}
enum.Values = append(enum.Values, text)
})
if len(enum.Values) > 0 {
log.Info("Writing enum %s with %d values", enum.Name, len(enum.Values))
if err := enum.Write(outdir, enumTemplate); err != nil {
log.Error("Error writing enum %s: %v", enum.Name, err)
}
}
})
return foundEnums, nil
}
func MapType(t string) string {
// Handle complex types like table<int, string>
if strings.Contains(t, "<") && strings.Contains(t, ">") {
// Extract the base type and inner types
openBracket := strings.Index(t, "<")
closeBracket := strings.LastIndex(t, ">")
if openBracket != -1 && closeBracket != -1 {
baseType := t[:openBracket]
innerTypes := t[openBracket+1 : closeBracket]
// Split inner types by comma, but be careful about nested brackets
var mappedInnerTypes []string
var current strings.Builder
bracketDepth := 0
for i := 0; i < len(innerTypes); i++ {
char := innerTypes[i]
if char == '<' {
bracketDepth++
current.WriteByte(char)
} else if char == '>' {
bracketDepth--
current.WriteByte(char)
} else if char == ',' && bracketDepth == 0 {
// Only split on commas that are not inside nested brackets
mappedInnerTypes = append(mappedInnerTypes, MapType(strings.TrimSpace(current.String())))
current.Reset()
} else {
current.WriteByte(char)
}
}
// Add the last inner type
if current.Len() > 0 {
mappedInnerTypes = append(mappedInnerTypes, MapType(strings.TrimSpace(current.String())))
}
// Reconstruct the complex type
return baseType + "<" + strings.Join(mappedInnerTypes, ", ") + ">"
}
}
// Handle simple types
switch t {
case "var":
return "any"
case "var...":
return "..."
case "int":
return "number"
case "int...":
return "number..."
case "unsigned int":
return "number"
case "float":
return "number"
case "double":
return "number"
case "bool":
return "boolean"
case "char":
return "string"
case "table_t":
return "table"
default:
return t
}
}
func IsReservedKeyword(t string) bool {
switch t {
case "and", "break", "do", "else", "elseif", "end", "false", "for", "function", "if", "in", "local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while", "any", "boolean", "number", "string", "table", "thread", "userdata", "var":
return true
}
return false
}
// extractBaseClassName extracts the base class name from a filename
// e.g., "Player [Client].html" -> "Player"
func extractBaseClassName(filename string) string {
// Extract filename without path
base := filepath.Base(filename)
// Remove .html extension
base = strings.TrimSuffix(base, ".html")
// Remove [Client] or [Server] suffix
base = strings.ReplaceAll(base, " [Client]", "")
base = strings.ReplaceAll(base, " [Server]", "")
return base
}
// MergeClasses merges multiple class files into a single class
func MergeClasses(files []string) (*Class, error) {
if len(files) == 0 {
return nil, fmt.Errorf("no files to merge")
}
// Parse all classes
var classes []*Class
var baseName string
var inheritance string
for _, file := range files {
class, err := ParseClass(file)
if err != nil {
return nil, fmt.Errorf("error parsing %s: %w", file, err)
}
classes = append(classes, class)
// Use the first class name as base
if baseName == "" {
baseName = class.ClassName
}
// Check for inheritance information in the original file
originalClassName := getOriginalClassName(file)
if strings.Contains(originalClassName, " : ") {
parts := strings.Split(originalClassName, " : ")
if len(parts) == 2 {
// Extract the inherited class name and clean it
inheritedClass := strings.TrimSpace(parts[1])
inheritedClass = strings.ReplaceAll(inheritedClass, "[Client]", "")
inheritedClass = strings.ReplaceAll(inheritedClass, "[Server]", "")
inheritedClass = strings.ReplaceAll(inheritedClass, "[", "")
inheritedClass = strings.ReplaceAll(inheritedClass, "]", "")
inheritedClass = strings.ReplaceAll(inheritedClass, "-", "_")
inheritedClass = strings.ReplaceAll(inheritedClass, ",", "")
inheritedClass = strings.ReplaceAll(inheritedClass, " ", "_")
// Clean up multiple underscores
for strings.Contains(inheritedClass, "__") {
inheritedClass = strings.ReplaceAll(inheritedClass, "__", "_")
}
inheritedClass = strings.Trim(inheritedClass, "_")
if inheritance == "" {
inheritance = inheritedClass
}
}
}
}
// Merge all classes into the first one
merged := classes[0]
// Set inheritance if found
if inheritance != "" {
merged.Inheritance = inheritance
}
// Create maps to track methods and fields by name
methodMap := make(map[string]*Method)
fieldMap := make(map[string]*Field)
// Add methods from all classes, handling duplicates
for _, class := range classes {
for _, method := range class.Methods {
if existing, exists := methodMap[method.Name]; exists {
// Method exists in multiple contexts, update availability
if !strings.Contains(existing.Comment, "[Client/Server]") {
if strings.Contains(existing.Comment, "[Client]") && strings.Contains(method.Comment, "[Server]") {
existing.Comment = "[Client/Server] " + strings.TrimPrefix(existing.Comment, "[Client] ")
} else if strings.Contains(existing.Comment, "[Server]") && strings.Contains(method.Comment, "[Client]") {
existing.Comment = "[Client/Server] " + strings.TrimPrefix(existing.Comment, "[Server] ")
}
}
} else {
// New method, add it
methodMap[method.Name] = &method
}
}
// Add fields from all classes, avoiding duplicates
for _, field := range class.Fields {
if _, exists := fieldMap[field.Name]; !exists {
fieldMap[field.Name] = &field
}
}
}
// Convert maps back to slices
merged.Methods = make([]Method, 0, len(methodMap))
for _, method := range methodMap {
merged.Methods = append(merged.Methods, *method)
}
merged.Fields = make([]Field, 0, len(fieldMap))
for _, field := range fieldMap {
merged.Fields = append(merged.Fields, *field)
}
return merged, nil
}
// getOriginalClassName extracts the original class name from the HTML file
func getOriginalClassName(file string) string {
filehandle, err := os.Open(file)
if err != nil {
return ""
}
defer filehandle.Close()
doc, err := goquery.NewDocumentFromReader(filehandle)
if err != nil {
return ""
}
class := doc.Find("div.floatright > h1")
if class.Length() == 0 {
return ""
}
return strings.TrimSpace(class.Text())
}
// WorkItem represents a group of files to be processed
type WorkItem struct {
BaseName string
GroupFiles []string
}