From b28989916da0c795315ab93060464ba5e48ff031 Mon Sep 17 00:00:00 2001 From: Tyler Oakleaf Date: Sun, 1 Mar 2026 12:55:35 -0500 Subject: [PATCH] Fix dump line number gating --- less/debug_options.go | 25 ++ less/integration_suite_test.go | 88 +++---- less/parse.go | 90 ++++---- less/parse_tree.go | 33 ++- less/parser.go | 24 +- less/ruleset.go | 403 +++++++++++++++++---------------- 6 files changed, 357 insertions(+), 306 deletions(-) create mode 100644 less/debug_options.go diff --git a/less/debug_options.go b/less/debug_options.go new file mode 100644 index 000000000..f5cff1583 --- /dev/null +++ b/less/debug_options.go @@ -0,0 +1,25 @@ +package less_go + +func normalizeDumpLineNumbersOption(value any) (any, bool) { + switch v := value.(type) { + case nil: + return nil, false + case bool: + if !v { + return nil, false + } + return "comments", true + case string: + if v == "" { + return nil, false + } + return v, true + default: + return value, true + } +} + +func dumpLineNumbersEnabled(value any) bool { + _, enabled := normalizeDumpLineNumbersOption(value) + return enabled +} diff --git a/less/integration_suite_test.go b/less/integration_suite_test.go index f90ed3253..028b12e72 100644 --- a/less/integration_suite_test.go +++ b/less/integration_suite_test.go @@ -20,22 +20,25 @@ var ( // Debug configuration from environment var ( - debugMode = os.Getenv("LESS_GO_DEBUG") == "1" - showAST = os.Getenv("LESS_GO_AST") == "1" - showTrace = os.Getenv("LESS_GO_TRACE") == "1" - showDiff = os.Getenv("LESS_GO_DIFF") == "1" - strictMode = os.Getenv("LESS_GO_STRICT") == "1" // Fail tests on output differences - quietMode = os.Getenv("LESS_GO_QUIET") == "1" // Suppress individual test output - jsonOutput = os.Getenv("LESS_GO_JSON") == "1" // Output results as JSON - skipCustom = os.Getenv("LESS_GO_SKIP_CUSTOM") == "1" // Skip custom integration tests - customOnly = os.Getenv("LESS_GO_CUSTOM_ONLY") == "1" // Run only custom integration tests - isCI = os.Getenv("CI") == "true" // Running in CI environment (GitHub Actions, etc.) + debugMode = os.Getenv("LESS_GO_DEBUG") == "1" + showAST = os.Getenv("LESS_GO_AST") == "1" + showTrace = os.Getenv("LESS_GO_TRACE") == "1" + showDiff = os.Getenv("LESS_GO_DIFF") == "1" + strictMode = os.Getenv("LESS_GO_STRICT") == "1" // Fail tests on output differences + quietMode = os.Getenv("LESS_GO_QUIET") == "1" // Suppress individual test output + jsonOutput = os.Getenv("LESS_GO_JSON") == "1" // Output results as JSON + skipCustom = os.Getenv("LESS_GO_SKIP_CUSTOM") == "1" // Skip custom integration tests + customOnly = os.Getenv("LESS_GO_CUSTOM_ONLY") == "1" // Run only custom integration tests + isCI = os.Getenv("CI") == "true" // Running in CI environment (GitHub Actions, etc.) + skipNetwork = os.Getenv("LESS_GO_SKIP_NETWORK") == "1" // Skip tests that require outbound network access ) // Tests that require network access and should be skipped in CI // Note: import-remote was removed after pinning @less/test-data@4.4.2 to fix // intermittent failures caused by missing empty.less in @4.4.3 -var networkDependentTests = map[string]bool{} +var networkDependentTests = map[string]bool{ + "import-remote": true, +} // addTestResult safely adds a test result to the global results slice func addTestResult(result TestResult) { @@ -50,14 +53,14 @@ func TestIntegrationSuite(t *testing.T) { resultsMutex.Lock() integrationResults = []TestResult{} resultsMutex.Unlock() - + // Track overall progress startTime := time.Now() if debugMode { fmt.Println("\nšŸš€ Starting Integration Test Suite with Debug Mode") fmt.Printf(" Debug Options: AST=%v, Trace=%v, Diff=%v\n", showAST, showTrace, showDiff) } - + // Base paths for test data - from less/ to testdata testDataRoot := "../testdata" lessRoot := filepath.Join(testDataRoot, "less") @@ -69,15 +72,15 @@ func TestIntegrationSuite(t *testing.T) { Name: "main", Options: map[string]any{ "relativeUrls": true, - "silent": true, + "silent": true, "javascriptEnabled": true, }, Folder: "_main/", }, { - Name: "namespacing", + Name: "namespacing", Options: map[string]any{}, - Folder: "namespacing/", + Folder: "namespacing/", }, { Name: "math-parens", @@ -293,13 +296,13 @@ func TestIntegrationSuite(t *testing.T) { resultsCopy := make([]TestResult, len(integrationResults)) copy(resultsCopy, integrationResults) resultsMutex.Unlock() - + printTestSummary(t, resultsCopy) - + // Show timing information duration := time.Since(startTime) t.Logf("\nā±ļø Total test duration: %v", duration) - + // Memory usage in debug mode if debugMode { var m runtime.MemStats @@ -319,14 +322,14 @@ type TestSuite struct { } type TestResult struct { - Suite string - TestName string - Status string // "pass", "fail", "skip" - Category string // "perfect_match", "compilation_failed", "output_differs", "correctly_failed", "expected_error" - ExpectError bool // Whether this test should fail - Error string - ExpectedCSS string - ActualCSS string + Suite string + TestName string + Status string // "pass", "fail", "skip" + Category string // "perfect_match", "compilation_failed", "output_differs", "correctly_failed", "expected_error" + ExpectError bool // Whether this test should fail + Error string + ExpectedCSS string + ActualCSS string } func runTestSuite(t *testing.T, suite TestSuite, lessRoot, cssRoot string) { @@ -351,9 +354,9 @@ func runTestSuite(t *testing.T, suite TestSuite, lessRoot, cssRoot string) { testName := strings.TrimSuffix(fileName, ".less") t.Run(testName, func(t *testing.T) { - // Skip network-dependent tests in CI environments - if isCI && networkDependentTests[testName] { - t.Skipf("Skipping %s in CI (requires network access)", testName) + // Skip network-dependent tests when running in CI or explicitly disabling network tests. + if networkDependentTests[testName] && (isCI || skipNetwork) { + t.Skipf("Skipping %s (requires network access)", testName) return } @@ -640,10 +643,10 @@ func formatDiff(expected, actual string) string { if !showDiff { return "" } - + expectedLines := strings.Split(expected, "\n") actualLines := strings.Split(actual, "\n") - + var diff strings.Builder diff.WriteString("\n--- Expected CSS ---\n") for i, line := range expectedLines { @@ -653,7 +656,7 @@ func formatDiff(expected, actual string) string { for i, line := range actualLines { diff.WriteString(fmt.Sprintf("%3d | %s\n", i+1, line)) } - + // Find first difference for i := 0; i < len(expectedLines) && i < len(actualLines); i++ { if expectedLines[i] != actualLines[i] { @@ -663,7 +666,7 @@ func formatDiff(expected, actual string) string { break } } - + return diff.String() } @@ -672,16 +675,16 @@ func enhancedErrorReport(t *testing.T, err error, lessFile string, lessContent s if !debugMode { return } - + t.Logf("\nšŸ” Enhanced Error Report:") t.Logf(" File: %s", lessFile) - + // Try to extract line/column from error errStr := err.Error() if strings.Contains(errStr, "line") || strings.Contains(errStr, "Line") { t.Logf(" Position: %s", errStr) } - + // Show file context if possible lines := strings.Split(lessContent, "\n") if len(lines) <= 20 { @@ -690,7 +693,7 @@ func enhancedErrorReport(t *testing.T, err error, lessFile string, lessContent s t.Logf(" %3d | %s", i+1, line) } } - + // Provide suggestions based on error type if strings.Contains(errStr, "Parse") || strings.Contains(errStr, "parse") { t.Logf("\nšŸ’” Parser Error Suggestions:") @@ -708,7 +711,7 @@ func enhancedErrorReport(t *testing.T, err error, lessFile string, lessContent s t.Logf(" • Check import resolution logic") t.Logf(" • Ensure imported files exist") } - + // Stack trace if available if debugMode { t.Logf("\nšŸ“š Stack Trace:") @@ -1046,10 +1049,10 @@ func printJSONSummary(t *testing.T, results []TestResult, perfectMatches, compil "expected_error": len(expectedError), "skipped": len(skipped), }, - "success_rate": float64(len(perfectMatches)+len(correctlyFailed)) / float64(len(results)-len(skipped)) * 100, - "compilation_rate": float64(len(results)-len(skipped)-len(compilationFailed)) / float64(len(results)-len(skipped)) * 100, + "success_rate": float64(len(perfectMatches)+len(correctlyFailed)) / float64(len(results)-len(skipped)) * 100, + "compilation_rate": float64(len(results)-len(skipped)-len(compilationFailed)) / float64(len(results)-len(skipped)) * 100, "perfect_match_rate": float64(len(perfectMatches)) / float64(len(results)-len(skipped)) * 100, - "results": results, + "results": results, } // Format as JSON (simple manual formatting to avoid import) @@ -1069,4 +1072,3 @@ func printJSONSummary(t *testing.T, results []TestResult, perfectMatches, compil t.Logf(` }`) t.Logf("}") } - diff --git a/less/parse.go b/less/parse.go index 2708924af..3501d330f 100644 --- a/less/parse.go +++ b/less/parse.go @@ -10,7 +10,7 @@ import ( // ParseCallbackFunc represents the callback function signature for parse operations type ParseCallbackFunc func(error, any, *ImportManager, map[string]any) -// ParseFunc represents the parse function signature returned by the factory +// ParseFunc represents the parse function signature returned by the factory type ParseFunc func(string, map[string]any, ParseCallbackFunc) any // LessContext represents the "this" context that contains options and importManager @@ -21,7 +21,7 @@ type LessContext struct { Functions Functions // PluginBridge provides lazy access to the Node.js plugin system. // It is initialized on first use and should be closed after compilation. - PluginBridge *LazyNodeJSPluginBridge + PluginBridge *LazyNodeJSPluginBridge } // NewLessContext creates a new LessContext @@ -42,9 +42,9 @@ func NewLessContextWithPlugins(options map[string]any) (*LessContext, func() err bridge := NewLazyNodeJSPluginBridge() ctx := &LessContext{ - Options: options, + Options: options, PluginLoader: LazyPluginLoaderFactory(bridge), - Functions: &DefaultFunctions{}, + Functions: &DefaultFunctions{}, PluginBridge: bridge, } @@ -118,7 +118,7 @@ func performParse(lessContext *LessContext, input string, options map[string]any // Handle JavaScript-style overloading: parse.js lines 10-16 // In JavaScript: if (typeof options === 'function') { callback = options; options = ... } // In Go, we can't dynamically check if options is a function, so we assume correct typing - + if callback == nil { // No callback provided - return promise (parse.js lines 18-28) if lessContext.Options != nil { @@ -126,7 +126,7 @@ func performParse(lessContext *LessContext, input string, options map[string]any } else { actualOptions = CopyOptions(map[string]any{}, options) } - + // Return promise equivalent return createPromise(lessContext, input, actualOptions, environment, parseTree, importManagerFactory) } else { @@ -142,7 +142,7 @@ func performParse(lessContext *LessContext, input string, options map[string]any // Main parsing logic - mirrors parse.js lines 30-84 var context *Parse var rootFileInfo map[string]any - + // Create plugin manager (equivalent to: const pluginManager = new PluginManager(this, !options.reUsePluginManager)) // Note: reUsePluginManager affects plugin manager creation behavior in JavaScript pluginManager := NewPluginManager(lessContext) @@ -152,7 +152,7 @@ func performParse(lessContext *LessContext, input string, options map[string]any if lessContext.PluginBridge != nil { actualOptions["pluginBridge"] = lessContext.PluginBridge } - + // Create parsing context (equivalent to: context = new contexts.Parse(options)) if os.Getenv("LESS_GO_DEBUG") == "1" { fmt.Printf("[DEBUG performParse] Creating Parse context with actualOptions paths: %v\n", actualOptions["paths"]) @@ -161,7 +161,7 @@ func performParse(lessContext *LessContext, input string, options map[string]any if os.Getenv("LESS_GO_DEBUG") == "1" { fmt.Printf("[DEBUG performParse] Created Parse context with context.Paths: %v\n", context.Paths) } - + // Handle rootFileInfo (parse.js lines 38-55) if rootFileInfoVal, ok := actualOptions["rootFileInfo"]; ok { if rootInfo, ok := rootFileInfoVal.(map[string]any); ok { @@ -174,10 +174,10 @@ func performParse(lessContext *LessContext, input string, options map[string]any filename = fn } } - + // Extract directory path using regex (equivalent to filename.replace(/[^/\\]*$/, '')) entryPath := reFilenameOnly.ReplaceAllString(filename, "") - + rootFileInfo = map[string]any{ "filename": filename, "rewriteUrls": context.RewriteUrls, @@ -186,7 +186,7 @@ func performParse(lessContext *LessContext, input string, options map[string]any "entryPath": entryPath, "rootFilename": filename, } - + // Add missing trailing slash to rootpath (parse.js lines 52-54) if rootpath, ok := rootFileInfo["rootpath"].(string); ok { if rootpath != "" && !strings.HasSuffix(rootpath, "/") { @@ -194,7 +194,7 @@ func performParse(lessContext *LessContext, input string, options map[string]any } } } - + // Create import manager (equivalent to: const imports = new ImportManager(this, context, rootFileInfo)) if os.Getenv("LESS_GO_DEBUG") == "1" { fmt.Printf("[DEBUG performParse] About to call importManagerFactory with context.Paths: %v\n", context.Paths) @@ -206,7 +206,7 @@ func performParse(lessContext *LessContext, input string, options map[string]any // Set importManager on the context (equivalent to: this.importManager = imports) lessContext.ImportManager = imports - + // Handle plugins (parse.js lines 63-77) if pluginsVal, ok := actualOptions["plugins"]; ok { if plugins, ok := pluginsVal.([]any); ok { @@ -216,23 +216,23 @@ func performParse(lessContext *LessContext, input string, options map[string]any if fileContent, ok := fileContentVal.(string); ok { // Remove BOM character (equivalent to contents = plugin.fileContent.replace(/^\uFEFF/, '')) contents := strings.TrimPrefix(fileContent, "\uFEFF") - + var pluginOptions map[string]any if optsVal, ok := plugin["options"]; ok { if opts, ok := optsVal.(map[string]any); ok { pluginOptions = opts } } - + var filename any if filenameVal, ok := plugin["filename"]; ok { filename = filenameVal } - + // Try to evaluate plugin if PluginLoader is available if pluginManager.Loader != nil { evalResult := pluginManager.Loader.EvalPlugin(contents, context, imports, pluginOptions, filename) - + // Check if evalResult is a LessError (parse.js lines 69-71) if lessErr, ok := evalResult.(*LessError); ok { actualCallback(lessErr, nil, nil, nil) @@ -248,66 +248,68 @@ func performParse(lessContext *LessContext, input string, options map[string]any } } } - + // Create parser and parse (equivalent to: new Parser(context, imports, rootFileInfo).parse(input, function (e, root) { ... }, options)) // This mirrors parse.js lines 79-83 go func() { // Convert Parse context to map for NewParser contextMap := map[string]any{ - "paths": context.Paths, - "rewriteUrls": context.RewriteUrls, - "rootpath": context.Rootpath, - "strictImports": context.StrictImports, - "insecure": context.Insecure, - "dumpLineNumbers": context.DumpLineNumbers, - "compress": context.Compress, - "syncImport": context.SyncImport, - "chunkInput": context.ChunkInput, - "mime": context.Mime, - "useFileCache": context.UseFileCache, - "processImports": context.ProcessImports, - "pluginManager": context.PluginManager, - "quiet": context.Quiet, + "paths": context.Paths, + "rewriteUrls": context.RewriteUrls, + "rootpath": context.Rootpath, + "strictImports": context.StrictImports, + "insecure": context.Insecure, + "compress": context.Compress, + "syncImport": context.SyncImport, + "chunkInput": context.ChunkInput, + "mime": context.Mime, + "useFileCache": context.UseFileCache, + "processImports": context.ProcessImports, + "pluginManager": context.PluginManager, + "quiet": context.Quiet, + } + if dumpLineNumbers, ok := normalizeDumpLineNumbersOption(context.DumpLineNumbers); ok { + contextMap["dumpLineNumbers"] = dumpLineNumbers } - + // Create imports map for NewParser (basic structure to match JavaScript) importsMap := map[string]any{ "contents": make(map[string]string), "contentsIgnoredChars": make(map[string]int), "rootFilename": rootFileInfo["filename"], } - + // Create parser instance parser := NewParser(contextMap, importsMap, rootFileInfo, 0) - + // Prepare additional data for parsing additionalData := &AdditionalData{} - + // Set options from actualOptions if they exist if globalVarsVal, ok := actualOptions["globalVars"]; ok { if globalVars, ok := globalVarsVal.(map[string]any); ok { additionalData.GlobalVars = globalVars } } - + if modifyVarsVal, ok := actualOptions["modifyVars"]; ok { if modifyVars, ok := modifyVarsVal.(map[string]any); ok { additionalData.ModifyVars = modifyVars } } - + if bannerVal, ok := actualOptions["banner"]; ok { if banner, ok := bannerVal.(string); ok { additionalData.Banner = banner } } - + if disablePluginRuleVal, ok := actualOptions["disablePluginRule"]; ok { if disablePluginRule, ok := disablePluginRuleVal.(bool); ok { additionalData.DisablePluginRule = disablePluginRule } } - + // Call the actual parser parser.Parse(input, func(err *LessError, root *Ruleset) { if err != nil { @@ -317,7 +319,7 @@ func performParse(lessContext *LessContext, input string, options map[string]any } }, additionalData) }() - + return nil } @@ -394,7 +396,7 @@ func (d *DefaultPluginLoader) LoadPlugin(path, currentDirectory string, context } // DefaultFunctions provides a basic implementation of Functions -type DefaultFunctions struct{ +type DefaultFunctions struct { registry *Registry } @@ -403,4 +405,4 @@ func (d *DefaultFunctions) GetFunctionRegistry() any { return d.registry } return map[string]any{} -} \ No newline at end of file +} diff --git a/less/parse_tree.go b/less/parse_tree.go index 443840e3a..01a4b0313 100644 --- a/less/parse_tree.go +++ b/less/parse_tree.go @@ -15,7 +15,6 @@ func (e *sourceMapEnv) EncodeBase64(str string) string { return base64.StdEncoding.EncodeToString([]byte(str)) } - // ParseTreeFactory represents the factory function type that creates ParseTree classes type ParseTreeFactory func(sourceMapBuilder any) *ParseTreeClass @@ -27,16 +26,16 @@ type ParseTreeClass struct { // NewParseTree creates a new ParseTree instance func (ptc *ParseTreeClass) NewParseTree(root any, imports *ImportManager) *ParseTree { return &ParseTree{ - Root: root, - Imports: imports, + Root: root, + Imports: imports, sourceMapBuilder: ptc.SourceMapBuilder, } } // ParseTree represents a Less parse tree that can be converted to CSS type ParseTree struct { - Root any - Imports *ImportManager + Root any + Imports *ImportManager sourceMapBuilder any } @@ -55,7 +54,7 @@ type ToCSSOptions struct { NumPrecision int SourceMap any PluginManager any - PluginBridge any // *LazyNodeJSPluginBridge or *NodeJSPluginBridge for JS plugin function lookup + PluginBridge any // *LazyNodeJSPluginBridge or *NodeJSPluginBridge for JS plugin function lookup Functions any ProcessImports bool ImportManager any @@ -79,7 +78,6 @@ func (pt *ParseTree) ToCSS(options *ToCSSOptions) (*ToCSSResult, error) { if options != nil { optionsMap = map[string]any{ "compress": options.Compress, - "dumpLineNumbers": options.DumpLineNumbers, "strictUnits": options.StrictUnits, "numPrecision": options.NumPrecision, "sourceMap": options.SourceMap, @@ -95,6 +93,9 @@ func (pt *ParseTree) ToCSS(options *ToCSSOptions) (*ToCSSResult, error) { "urlArgs": options.UrlArgs, "javascriptEnabled": options.JavascriptEnabled, } + if dumpLineNumbers, ok := normalizeDumpLineNumbersOption(options.DumpLineNumbers); ok { + optionsMap["dumpLineNumbers"] = dumpLineNumbers + } } else { optionsMap = make(map[string]any) } @@ -140,16 +141,15 @@ func (pt *ParseTree) ToCSS(options *ToCSSOptions) (*ToCSSResult, error) { strictUnits = options.StrictUnits } - var dumpLineNumbers any - if options != nil { - dumpLineNumbers = options.DumpLineNumbers - } - toCSSOptions := map[string]any{ - "compress": compress, - "dumpLineNumbers": dumpLineNumbers, - "strictUnits": strictUnits, - "numPrecision": 8, // Match less.js default precision + "compress": compress, + "strictUnits": strictUnits, + "numPrecision": 8, // Match less.js default precision + } + if options != nil { + if dumpLineNumbers, ok := normalizeDumpLineNumbersOption(options.DumpLineNumbers); ok { + toCSSOptions["dumpLineNumbers"] = dumpLineNumbers + } } // Handle source map generation @@ -386,4 +386,3 @@ func DefaultParseTreeFactory(sourceMapBuilder any) *ParseTreeClass { SourceMapBuilder: sourceMapBuilder, } } - diff --git a/less/parser.go b/less/parser.go index ed901952c..7716bb8aa 100644 --- a/less/parser.go +++ b/less/parser.go @@ -467,7 +467,9 @@ func (p *Parser) parseInternal(str string, callback func(*LessError, *Ruleset), preProcessors := pluginManager.GetPreProcessors() for _, processor := range preProcessors { // Check if processor has Process method - if proc, ok := processor.(interface{ Process(string, map[string]any) string }); ok { + if proc, ok := processor.(interface { + Process(string, map[string]any) string + }); ok { str = proc.Process(str, map[string]any{ "context": p.context, "imports": p.imports, @@ -529,7 +531,7 @@ func (p *Parser) parseInternal(str string, callback func(*LessError, *Ruleset), root = NewRuleset(nil, p.parsers.Primary(), false, nil, p.CreateSelectorsParseFunc(), p.CreateValueParseFunc(), p.context, p.imports) root.Root = true root.FirstRoot = true - + // Set up function registry for the root if funcRegistry, ok := p.context["functionRegistry"].(*Registry); ok && funcRegistry != nil { // Create an inherited registry for this parse tree @@ -1802,7 +1804,7 @@ func (p *Parsers) Ruleset() any { p.parser.parserInput.Save() - if context, ok := p.parser.context["dumpLineNumbers"]; ok && context != nil { + if dumpLineNumbersEnabled(p.parser.context["dumpLineNumbers"]) { debugInfo = p.parser.getDebugInfo(p.parser.parserInput.GetIndex()) } @@ -2028,7 +2030,7 @@ func (p *Parsers) AtRule() any { if rules != nil || (!hasBlock && value != nil && p.parser.parserInput.Char(';') != nil) { p.parser.parserInput.Forget() var debugInfo map[string]any - if p.parser.context["dumpLineNumbers"] != nil { + if dumpLineNumbersEnabled(p.parser.context["dumpLineNumbers"]) { debugInfo = p.parser.getDebugInfo(index) } return NewAtRule(name, value, rules, index+p.parser.currentIndex, p.parser.fileInfo, debugInfo, isRooted, nil) @@ -2433,7 +2435,7 @@ func (p *Parsers) Plugin() any { if dir == nil { return nil } - + args = p.PluginArgs() if args != "" { options = map[string]any{ @@ -3183,7 +3185,7 @@ func (p *Parsers) NestableAtRule() any { var debugInfo map[string]any index := p.parser.parserInput.GetIndex() - if p.parser.context["dumpLineNumbers"] != nil { + if dumpLineNumbersEnabled(p.parser.context["dumpLineNumbers"]) { debugInfo = p.parser.getDebugInfo(index) } @@ -3258,7 +3260,7 @@ func (p *Parsers) Element() any { if v = p.Selector(false); v != nil { var selectors []any selectors = append(selectors, v) - + // Handle comma-separated selectors in parentheses for p.parser.parserInput.Char(',') != nil { selectors = append(selectors, NewAnonymous(",", index+p.parser.currentIndex, p.parser.fileInfo, false, false, nil)) @@ -3266,7 +3268,7 @@ func (p *Parsers) Element() any { selectors = append(selectors, v) } } - + if p.parser.parserInput.Char(')') != nil { if len(selectors) > 1 { // Create a SelectorList to hold multiple comma-separated selectors @@ -4178,11 +4180,11 @@ func (p *Parsers) IeAlpha() []any { if p.parser.parserInput.Re(reAlphaOpacity) == nil { return nil } - + // First try to parse a number value := p.parser.parserInput.Re(reNumber) var valueStr string - + if value != nil { // We have a numeric value if matches, ok := value.([]string); ok && len(matches) > 0 { @@ -4200,7 +4202,7 @@ func (p *Parsers) IeAlpha() []any { } } } - + p.parser.expectChar(')', "") // Return a Quoted node with the full alpha function string diff --git a/less/ruleset.go b/less/ruleset.go index 9ede0eae8..8f4a096af 100644 --- a/less/ruleset.go +++ b/less/ruleset.go @@ -87,11 +87,11 @@ type Ruleset struct { Root bool // Extend support ExtendOnEveryPath bool - Paths [][]any - FirstRoot bool - AllowImports bool - AllExtends []*Extend // For storing extends found by ExtendFinderVisitor - FunctionRegistry any // Changed from *functions.Registry to avoid import cycle + Paths [][]any + FirstRoot bool + AllowImports bool + AllExtends []*Extend // For storing extends found by ExtendFinderVisitor + FunctionRegistry any // Changed from *functions.Registry to avoid import cycle // Parser functions for handling dynamic content SelectorsParseFunc SelectorsParseFunc ValueParseFunc ValueParseFunc @@ -233,7 +233,7 @@ func (r *Ruleset) ToCSS(options map[string]any) (string, error) { if _, hasCompress := contextMap["compress"]; !hasCompress { contextMap["compress"] = false } - + // Create CSS output implementation cssOutput := &CSSOutput{ Add: func(chunk, fileInfo, index any) { @@ -254,10 +254,10 @@ func (r *Ruleset) ToCSS(options map[string]any) (string, error) { return output.Len() == 0 }, } - + // Generate CSS using the GenCSS method r.GenCSS(contextMap, cssOutput) - + // Return the generated CSS output result := output.String() @@ -271,7 +271,7 @@ func (r *Ruleset) ToCSSString(context any) string { if ctx, ok := context.(map[string]any); ok { options = ctx } - + result, _ := r.ToCSS(options) return result } @@ -557,7 +557,7 @@ func (r *Ruleset) Eval(context any) (any, error) { } } } - + if len(toParseSelectors) > 0 { // Parse the selectors string (equivalent to JS parseNode call) selectorString := strings.Join(toParseSelectors, ",") @@ -585,7 +585,7 @@ func (r *Ruleset) Eval(context any) (any, error) { } } } - + // Match JavaScript: defaultFunc.reset(); if evalCtx != nil && evalCtx.DefaultFunc != nil { evalCtx.DefaultFunc.Reset() @@ -740,7 +740,9 @@ func (r *Ruleset) Eval(context any) (any, error) { for i, rule := range rsRules { if r, ok := rule.(interface{ EvalFirst() bool }); ok && r.EvalFirst() { // Handle MixinDefinition specifically (returns *MixinDefinition, not any) - if eval, ok := rule.(interface{ Eval(any) (*MixinDefinition, error) }); ok { + if eval, ok := rule.(interface { + Eval(any) (*MixinDefinition, error) + }); ok { evaluated, err := eval.Eval(context) if err != nil { return nil, err @@ -805,7 +807,7 @@ func (r *Ruleset) Eval(context any) (any, error) { filtered = append(filtered, r) } } - + // rsRules.splice.apply(rsRules, [i, 1].concat(rules)) newRules := make([]any, len(rsRules)+len(filtered)-1) copy(newRules, rsRules[:i]) @@ -879,7 +881,9 @@ func (r *Ruleset) Eval(context any) (any, error) { // Try different Eval signatures switch evalRule := rule.(type) { - case interface{ Eval(any) (*MixinDefinition, error) }: + case interface { + Eval(any) (*MixinDefinition, error) + }: // Handle MixinDefinition evaluated, err := evalRule.Eval(context) if err != nil { @@ -1022,7 +1026,13 @@ func (r *Ruleset) Eval(context any) (any, error) { if os.Getenv("LESS_GO_TRACE") != "" { fmt.Fprintf(os.Stderr, "[RULESET.Eval] Root check: Root=%v, len(mediaPath)=%d, len(mediaBlocks)=%d\n", - ruleset.Root, len(mediaPath), func() int { if mediaBlocks != nil { return len(mediaBlocks) } else { return 0 } }()) + ruleset.Root, len(mediaPath), func() int { + if mediaBlocks != nil { + return len(mediaBlocks) + } else { + return 0 + } + }()) } if ruleset.Root && len(mediaPath) == 0 && mediaBlocks != nil && len(mediaBlocks) > 0 { @@ -1147,15 +1157,15 @@ func (r *Ruleset) MatchCondition(args []any, context any) bool { if len(r.Selectors) == 0 { return false } - + lastSelector := r.Selectors[len(r.Selectors)-1] - + // Check evaldCondition if sel, ok := lastSelector.(*Selector); ok { if !sel.EvaldCondition { return false } - + // Check condition if sel.Condition != nil { if eval, ok := sel.Condition.(interface{ Eval(any) (any, error) }); ok { @@ -1169,7 +1179,7 @@ func (r *Ruleset) MatchCondition(args []any, context any) bool { if frames, ok := ctx["frames"].([]any); ok { ctx["frames"] = frames } - + // IMPORTANT: Preserve the defaultFunc in the context // This is needed for the default() function in mixin guards if evalCtx, ok := context.(EvalContext); ok { @@ -1181,12 +1191,12 @@ func (r *Ruleset) MatchCondition(args []any, context any) bool { ctx["defaultFunc"] = df } } - + result, err := eval.Eval(ctx) if err != nil { return false } - + // Check if result is falsy if isFalsy(result) { return false @@ -1194,7 +1204,7 @@ func (r *Ruleset) MatchCondition(args []any, context any) bool { } } } - + return true } @@ -1222,7 +1232,10 @@ func isFalsy(v any) bool { // This is used to skip rules from reference imports during CSS generation func ruleBlocksVisibility(rule any) bool { // Check if the rule has visibility blocking methods - if visNode, ok := rule.(interface{ BlocksVisibility() bool; IsVisible() *bool }); ok { + if visNode, ok := rule.(interface { + BlocksVisibility() bool + IsVisible() *bool + }); ok { if visNode.BlocksVisibility() { nodeVisible := visNode.IsVisible() // If visibility is nil or false, check for visible paths from extends @@ -1505,7 +1518,7 @@ func (r *Ruleset) Properties() map[string][]any { } else { name = fmt.Sprintf("%v", decl.name) } - + key := "$" + name // Properties don't overwrite as they can merge (from JavaScript comment) if _, exists := r.properties[key]; !exists { @@ -1515,7 +1528,7 @@ func (r *Ruleset) Properties() map[string][]any { } } } - + return r.properties } @@ -1603,7 +1616,7 @@ func (r *Ruleset) LastDeclaration() any { if r.Rules == nil { return nil } - + // for (let i = this.rules.length; i > 0; i--) like JavaScript for i := len(r.Rules); i > 0; i-- { decl := r.Rules[i-1] @@ -1618,7 +1631,7 @@ func (r *Ruleset) ParseValue(toParse any) any { if toParse == nil { return nil } - + // Handle arrays if arr, ok := toParse.([]any); ok { nodes := make([]any, len(arr)) @@ -1627,7 +1640,7 @@ func (r *Ruleset) ParseValue(toParse any) any { } return nodes } - + return r.transformDeclaration(toParse) } @@ -1663,7 +1676,7 @@ func (r *Ruleset) transformDeclaration(decl any) any { } return dCopy } else if len(result) > 0 { - + // The parser returns Value>Expression>Dimension for "10px" // We should use the parsed Value directly, not wrap it again var valueCopy *Value @@ -1673,7 +1686,7 @@ func (r *Ruleset) transformDeclaration(decl any) any { // Fallback: wrap in Value if not already a Value valueCopy, _ = NewValue([]any{result[0]}) } - + nodeCopy := NewNode() nodeCopy.CopyVisibilityInfo(d.Node.VisibilityInfo()) nodeCopy.Parsed = true @@ -1779,7 +1792,7 @@ func (r *Ruleset) Find(selector any, self any, filter func(any) bool) []any { if rule == self { continue } - + // Handle both *Ruleset and *MixinDefinition (which embeds *Ruleset) var rulesetSelectors []any switch r := rule.(type) { @@ -1793,7 +1806,7 @@ func (r *Ruleset) Find(selector any, self any, filter func(any) bool) []any { rulesetSelectors = rs.GetSelectors() } } - + if rulesetSelectors != nil { for j := 0; j < len(rulesetSelectors); j++ { if sel, ok := selector.(*Selector); ok { @@ -1808,7 +1821,9 @@ func (r *Ruleset) Find(selector any, self any, filter func(any) bool) []any { newSelector, err := NewSelector(remainingElements, nil, nil, sel.GetIndex(), sel.FileInfo(), nil) if err == nil { // Use Find method on the rule (works for both Ruleset and MixinDefinition) - if finder, ok := rule.(interface{ Find(any, any, func(any) bool) []any }); ok { + if finder, ok := rule.(interface { + Find(any, any, func(any) bool) []any + }); ok { foundMixins = finder.Find(newSelector, self, filter) } for i := 0; i < len(foundMixins); i++ { @@ -1922,7 +1937,6 @@ func (r *Ruleset) GenCSS(context any, output *CSSOutput) { } } - // Check if this is a container ruleset (no selectors/paths) // Container rulesets inside at-rules (@supports, @document) don't output their own braces, // so they shouldn't increment tabLevel @@ -1966,7 +1980,7 @@ func (r *Ruleset) GenCSS(context any, output *CSSOutput) { tabSetStr = "" } } - + // Organize rules by type like JavaScript version var charsetRuleNodes []any // Pre-allocate ruleNodes with capacity of Rules length to avoid reallocation @@ -2057,7 +2071,6 @@ func (r *Ruleset) GenCSS(context any, output *CSSOutput) { // If so, we'll skip generating selectors/braces but still complete normally for proper spacing hasOnlyExtends := !r.Root && len(r.Rules) > 0 && len(ruleNodes) == 0 - // Track how many paths were actually output (for visibility filtering) outputCount := 0 @@ -2196,161 +2209,163 @@ func (r *Ruleset) GenCSS(context any, output *CSSOutput) { ctx["tabLevel"] = tabLevel + 1 } if !hasOnlyExtends { - for i, rule := range ruleNodes { - if i+1 == len(ruleNodes) { - ctx["lastRule"] = true - } else { - // Explicitly set to false for non-last rules to override any parent context setting - ctx["lastRule"] = false - } - - currentLastRule := false - if lr, ok := ctx["lastRule"].(bool); ok { - currentLastRule = lr - } - - // Check IsRulesetLike - handle both bool return and any return (AtRule returns any) - isRulesetLikeForCtx := false - if rl, ok := rule.(interface{ IsRulesetLike() any }); ok { - result := rl.IsRulesetLike() - if result != nil { - if b, ok := result.(bool); ok { - isRulesetLikeForCtx = b - } else { - // Non-nil, non-bool (like rules slice) means ruleset-like - isRulesetLikeForCtx = true - } - } - } else if rl, ok := rule.(interface{ IsRulesetLike() bool }); ok { - isRulesetLikeForCtx = rl.IsRulesetLike() - } - if isRulesetLikeForCtx { - ctx["lastRule"] = false - } - - // For file-level root rulesets: mark child rulesets as top-level so they format correctly - // This ensures extracted rulesets (that were moved to root's rules) are treated as top-level - // Only do this for the actual file root (tabLevel == 0), not for container rulesets like @keyframes - childContext := ctx - if r.Root && tabLevel == 0 { - if childRuleset, ok := rule.(*Ruleset); ok && !childRuleset.Root { - // Create a new context for the child with topLevel flag - // Don't modify tabLevel - let it stay as-is so declarations inside have correct indentation - // Pre-allocate with capacity for all keys + 1 for topLevel - childContext = make(map[string]any, len(ctx)+1) - for k, v := range ctx { - childContext[k] = v - } - childContext["topLevel"] = true - // Note: We don't set tabLevel=0 because that would affect indentation of declarations - // The topLevel flag will prevent incrementing tabLevel, which is what we want + for i, rule := range ruleNodes { + if i+1 == len(ruleNodes) { + ctx["lastRule"] = true + } else { + // Explicitly set to false for non-last rules to override any parent context setting + ctx["lastRule"] = false } - } - - // Generate CSS for the rule - if gen, ok := rule.(interface{ GenCSS(any, *CSSOutput) }); ok { - gen.GenCSS(childContext, output) - } else if val, ok := rule.(interface{ GetValue() any }); ok { - output.Add(fmt.Sprintf("%v", val.GetValue()), nil, nil) - } - ctx["lastRule"] = currentLastRule - - // Add newline after rule if it's not the last rule - if !currentLastRule { - shouldAddNewline := false - - // Check if rule is visible (for declarations, etc.) - // Also check if rule blocks visibility but is not explicitly visible (from reference imports) - // IMPORTANT: Check for Node visibility (BlocksVisibility/IsVisible *bool) FIRST, - // before checking simple IsVisible() bool, because Comment has both methods - // and we want to respect the Node's visibility from reference imports - if visNode, ok := rule.(interface{ BlocksVisibility() bool; IsVisible() *bool }); ok { - blocksVis := visNode.BlocksVisibility() - if blocksVis { - // Node blocks visibility (from reference import) - only visible if explicitly marked - nodeVisible := visNode.IsVisible() - if nodeVisible != nil && *nodeVisible { - shouldAddNewline = true - } - // else: invisible, don't add newline - } else { - // Node doesn't block visibility - check IsVisible *bool - // A nil return from IsVisible() means not visible (e.g., from reference imports that were skipped) - nodeVisible := visNode.IsVisible() - if nodeVisible != nil && *nodeVisible { - shouldAddNewline = true - } - } - } else if vis, ok := rule.(interface{ IsVisible() bool }); ok && vis.IsVisible() { - // Node doesn't have BlocksVisibility/IsVisible *bool, and IsVisible() is true - shouldAddNewline = true + currentLastRule := false + if lr, ok := ctx["lastRule"].(bool); ok { + currentLastRule = lr } - // Special case: Add newlines for child rulesets inside container rulesets - // (like @keyframes which creates a container ruleset with no selectors) // Check IsRulesetLike - handle both bool return and any return (AtRule returns any) - isRulesetLike := false + isRulesetLikeForCtx := false if rl, ok := rule.(interface{ IsRulesetLike() any }); ok { result := rl.IsRulesetLike() if result != nil { if b, ok := result.(bool); ok { - isRulesetLike = b + isRulesetLikeForCtx = b } else { // Non-nil, non-bool (like rules slice) means ruleset-like - isRulesetLike = true + isRulesetLikeForCtx = true } } } else if rl, ok := rule.(interface{ IsRulesetLike() bool }); ok { - isRulesetLike = rl.IsRulesetLike() - } - if isRulesetLike { - // Skip adding newlines for MixinDefinitions - they don't output anything - if _, isMixinDef := rule.(*MixinDefinition); isMixinDef { - // Don't add newline after mixin definitions - } else if rs, isRuleset := rule.(*Ruleset); isRuleset && isExtendOnlyRuleset(rs) { - // Don't add newline after extend-only rulesets - they don't output anything - } else if rs, isRuleset := rule.(*Ruleset); isRuleset && hasNoVisibleContent(rs) { - // Don't add newline after rulesets with no visible content (e.g., parent containers after children bubbled) - } else if at, isAtRule := rule.(*AtRule); isAtRule && atRuleHasOnlySilentContent(at) { - // Don't add newline after AtRules with only silent content (line comments) - } else { - // Only add newline if parent ruleset has no selectors (is a container) - // and we're not at the file-level root (tabLevel > 0) - // Also consider rulesets where all selectors are MediaEmpty (like @layer inner rulesets) - isParentContainer := (r.Paths == nil || len(r.Paths) == 0) && (r.Selectors == nil || len(r.Selectors) == 0 || allSelectorsMediaEmpty(r.Selectors)) - if isParentContainer && tabLevel > 0 { - shouldAddNewline = true + isRulesetLikeForCtx = rl.IsRulesetLike() + } + if isRulesetLikeForCtx { + ctx["lastRule"] = false + } + + // For file-level root rulesets: mark child rulesets as top-level so they format correctly + // This ensures extracted rulesets (that were moved to root's rules) are treated as top-level + // Only do this for the actual file root (tabLevel == 0), not for container rulesets like @keyframes + childContext := ctx + if r.Root && tabLevel == 0 { + if childRuleset, ok := rule.(*Ruleset); ok && !childRuleset.Root { + // Create a new context for the child with topLevel flag + // Don't modify tabLevel - let it stay as-is so declarations inside have correct indentation + // Pre-allocate with capacity for all keys + 1 for topLevel + childContext = make(map[string]any, len(ctx)+1) + for k, v := range ctx { + childContext[k] = v } - // Also add newline for top-level rulesets (children of root) - if r.Root && tabLevel == 0 { - shouldAddNewline = true + childContext["topLevel"] = true + // Note: We don't set tabLevel=0 because that would affect indentation of declarations + // The topLevel flag will prevent incrementing tabLevel, which is what we want + } + } + + // Generate CSS for the rule + if gen, ok := rule.(interface{ GenCSS(any, *CSSOutput) }); ok { + gen.GenCSS(childContext, output) + } else if val, ok := rule.(interface{ GetValue() any }); ok { + output.Add(fmt.Sprintf("%v", val.GetValue()), nil, nil) + } + + ctx["lastRule"] = currentLastRule + + // Add newline after rule if it's not the last rule + if !currentLastRule { + shouldAddNewline := false + + // Check if rule is visible (for declarations, etc.) + // Also check if rule blocks visibility but is not explicitly visible (from reference imports) + // IMPORTANT: Check for Node visibility (BlocksVisibility/IsVisible *bool) FIRST, + // before checking simple IsVisible() bool, because Comment has both methods + // and we want to respect the Node's visibility from reference imports + if visNode, ok := rule.(interface { + BlocksVisibility() bool + IsVisible() *bool + }); ok { + blocksVis := visNode.BlocksVisibility() + if blocksVis { + // Node blocks visibility (from reference import) - only visible if explicitly marked + nodeVisible := visNode.IsVisible() + if nodeVisible != nil && *nodeVisible { + shouldAddNewline = true + } + // else: invisible, don't add newline + } else { + // Node doesn't block visibility - check IsVisible *bool + // A nil return from IsVisible() means not visible (e.g., from reference imports that were skipped) + nodeVisible := visNode.IsVisible() + if nodeVisible != nil && *nodeVisible { + shouldAddNewline = true + } } + } else if vis, ok := rule.(interface{ IsVisible() bool }); ok && vis.IsVisible() { + // Node doesn't have BlocksVisibility/IsVisible *bool, and IsVisible() is true + shouldAddNewline = true } - } + // Special case: Add newlines for child rulesets inside container rulesets + // (like @keyframes which creates a container ruleset with no selectors) + // Check IsRulesetLike - handle both bool return and any return (AtRule returns any) + isRulesetLike := false + if rl, ok := rule.(interface{ IsRulesetLike() any }); ok { + result := rl.IsRulesetLike() + if result != nil { + if b, ok := result.(bool); ok { + isRulesetLike = b + } else { + // Non-nil, non-bool (like rules slice) means ruleset-like + isRulesetLike = true + } + } + } else if rl, ok := rule.(interface{ IsRulesetLike() bool }); ok { + isRulesetLike = rl.IsRulesetLike() + } + if isRulesetLike { + // Skip adding newlines for MixinDefinitions - they don't output anything + if _, isMixinDef := rule.(*MixinDefinition); isMixinDef { + // Don't add newline after mixin definitions + } else if rs, isRuleset := rule.(*Ruleset); isRuleset && isExtendOnlyRuleset(rs) { + // Don't add newline after extend-only rulesets - they don't output anything + } else if rs, isRuleset := rule.(*Ruleset); isRuleset && hasNoVisibleContent(rs) { + // Don't add newline after rulesets with no visible content (e.g., parent containers after children bubbled) + } else if at, isAtRule := rule.(*AtRule); isAtRule && atRuleHasOnlySilentContent(at) { + // Don't add newline after AtRules with only silent content (line comments) + } else { + // Only add newline if parent ruleset has no selectors (is a container) + // and we're not at the file-level root (tabLevel > 0) + // Also consider rulesets where all selectors are MediaEmpty (like @layer inner rulesets) + isParentContainer := (r.Paths == nil || len(r.Paths) == 0) && (r.Selectors == nil || len(r.Selectors) == 0 || allSelectorsMediaEmpty(r.Selectors)) + if isParentContainer && tabLevel > 0 { + shouldAddNewline = true + } + // Also add newline for top-level rulesets (children of root) + if r.Root && tabLevel == 0 { + shouldAddNewline = true + } + } + } - // Special case: Import nodes should always have a newline after them - if ruleType, ok := rule.(interface{ GetType() string }); ok && ruleType.GetType() == "Import" { - shouldAddNewline = true - } - - // Special case: AtRules without rules (like @charset, @namespace) at root level need newlines - if at, isAtRule := rule.(*AtRule); isAtRule && r.Root && tabLevel == 0 { - // AtRules without rules produce output and need newlines - if at.Rules == nil || len(at.Rules) == 0 { + // Special case: Import nodes should always have a newline after them + if ruleType, ok := rule.(interface{ GetType() string }); ok && ruleType.GetType() == "Import" { shouldAddNewline = true } - } - if shouldAddNewline && !compress { - output.Add("\n"+tabRuleStr, nil, nil) + // Special case: AtRules without rules (like @charset, @namespace) at root level need newlines + if at, isAtRule := rule.(*AtRule); isAtRule && r.Root && tabLevel == 0 { + // AtRules without rules produce output and need newlines + if at.Rules == nil || len(at.Rules) == 0 { + shouldAddNewline = true + } + } + + if shouldAddNewline && !compress { + output.Add("\n"+tabRuleStr, nil, nil) + } + } else { + ctx["lastRule"] = false } - } else { - ctx["lastRule"] = false } } - } // Decrement tab level FIRST for correct newline logic // Do this for all non-root rulesets (except top-level, media-empty, and container), even if we skip output (for extend-only rulesets) @@ -2371,7 +2386,7 @@ func (r *Ruleset) GenCSS(context any, output *CSSOutput) { output.Add("\n"+tabSetStr+"}", nil, nil) } } - + // Add final newline for first root if !output.IsEmpty() && !compress && r.FirstRoot { output.Add("\n", nil, nil) @@ -2482,7 +2497,7 @@ func (r *Ruleset) JoinSelector(paths *[][]any, context [][]any, selector any) { return newSelectorPath } - // addAllReplacementsIntoPath helper function + // addAllReplacementsIntoPath helper function addAllReplacementsIntoPath := func(beginningPaths [][]any, addPaths []any, replacedElement *Element, originalSelector *Selector, result *[][]any) { for j := 0; j < len(beginningPaths); j++ { newSelectorPath := addReplacementIntoPath(beginningPaths[j], addPaths, replacedElement, originalSelector) @@ -2676,8 +2691,8 @@ func (r *Ruleset) JoinSelector(paths *[][]any, context [][]any, selector any) { newPaths = [][]any{} for idx := 0; idx < len(context); idx++ { concatenated := make([]any, len(context[idx])+1) - - // Map over context[idx] using deriveSelector + + // Map over context[idx] using deriveSelector for j, ctxSel := range context[idx] { if ctxSelector, ok := ctxSel.(*Selector); ok { concatenated[j] = deriveSelector(sel.VisibilityInfo(), ctxSelector) @@ -2702,38 +2717,44 @@ func GetDebugInfo(context map[string]any, ruleset *Ruleset, separator string) st if context == nil || ruleset == nil { return "" } - + // Check if dumpLineNumbers is enabled and not compressing - dumpLineNumbers, hasDump := context["dumpLineNumbers"] - compress, hasCompress := context["compress"] - - if !hasDump || (hasCompress && compress.(bool)) { + dumpLineNumbers, dumpEnabled := normalizeDumpLineNumbersOption(context["dumpLineNumbers"]) + compress, _ := context["compress"].(bool) + if !dumpEnabled { return "" } - + dumpMode, ok := dumpLineNumbers.(string) + if !ok || dumpMode == "" { + return "" + } + if compress && dumpMode != "all" { + return "" + } + // Create debug context from ruleset debugCtx := createDebugContextFromRuleset(context, ruleset) if debugCtx == nil { return "" } - + // Get line number and filename from debug context lineNumber := 0 fileName := "" - + if ln, ok := debugCtx["lineNumber"].(int); ok { lineNumber = ln } if fn, ok := debugCtx["fileName"].(string); ok { fileName = fn } - + if lineNumber == 0 || fileName == "" { return "" } - + var result string - switch dumpLineNumbers { + switch dumpMode { case "comments": result = asComment(lineNumber, fileName) case "mediaquery": @@ -2745,7 +2766,7 @@ func GetDebugInfo(context map[string]any, ruleset *Ruleset, separator string) st } result += asMediaQuery(lineNumber, fileName) } - + return result } @@ -2763,16 +2784,16 @@ func createDebugContextFromRuleset(context map[string]any, ruleset *Ruleset) map if fileInfo == nil { return nil } - + var filename string var lineNumber int - + if fn, ok := fileInfo["filename"]; ok { if fnStr, ok := fn.(string); ok { filename = fnStr } } - + if ln, ok := fileInfo["lineNumber"]; ok { if lnInt, ok := ln.(int); ok { lineNumber = lnInt @@ -2781,7 +2802,7 @@ func createDebugContextFromRuleset(context map[string]any, ruleset *Ruleset) map if lineNumber == 0 { lineNumber = 1 // Default line number } - + return map[string]any{ "fileName": filename, "lineNumber": lineNumber, @@ -2811,12 +2832,12 @@ func (r *Ruleset) shouldIncludeSubRule(subRule any, parentRuleset *Ruleset) bool if subNode, ok := subRule.(interface{ CopyVisibilityInfo(map[string]any) }); ok { subNode.CopyVisibilityInfo(parentRuleset.VisibilityInfo()) } - + // Check if it's a variable declaration (like JavaScript: !(subRule instanceof Declaration) || !subRule.variable) if r.isVariableDeclaration(subRule) { return false // Don't include variable declarations } - + return true // Include everything else } @@ -2825,7 +2846,7 @@ func (r *Ruleset) isVariableDeclaration(rule any) bool { if decl, ok := rule.(*Declaration); ok { return decl.variable } - + // Handle mock declarations using reflection (for tests) if node, ok := rule.(interface{ GetType() string }); ok && node.GetType() == "Declaration" { if v := reflect.ValueOf(rule); v.Kind() == reflect.Ptr && !v.IsNil() { @@ -2836,7 +2857,7 @@ func (r *Ruleset) isVariableDeclaration(rule any) bool { } } } - + return false } @@ -3339,4 +3360,4 @@ func (r *Ruleset) GenCSSSourceMap(context map[string]any, output *SourceMapOutpu output.Add("}\n", fileInfo, index, false) } } -} \ No newline at end of file +}