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) { 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 MapType(t string) string { // Handle complex types like table 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 }