From a19f4edfb0abce23af99766e17d51c8c02e25bd3 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Tue, 4 Nov 2025 00:26:32 +0100 Subject: [PATCH 1/5] Ruby: Add colored CLI output for `parse` and `lex` --- .rubocop.yml | 1 + lib/herb.rb | 1 + lib/herb/ast/node.rb | 2 + lib/herb/colors.rb | 70 +++++++++++++++++++++++++++ lib/herb/location.rb | 6 ++- lib/herb/token.rb | 16 ++++++- templates/lib/herb/ast/nodes.rb.erb | 30 +++++++----- templates/lib/herb/errors.rb.erb | 24 ++++++---- test/colors_test.rb | 74 +++++++++++++++++++++++++++++ 9 files changed, 200 insertions(+), 24 deletions(-) create mode 100644 lib/herb/colors.rb create mode 100644 test/colors_test.rb diff --git a/.rubocop.yml b/.rubocop.yml index 793a7639e..549100c41 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -74,6 +74,7 @@ Metrics/AbcSize: - lib/herb/project.rb - lib/herb/engine.rb - lib/herb/engine/**/*.rb + - lib/herb/token.rb - templates/template.rb - test/fork_helper.rb - test/snapshot_utils.rb diff --git a/lib/herb.rb b/lib/herb.rb index 53c0df792..9840251d5 100644 --- a/lib/herb.rb +++ b/lib/herb.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true # typed: false +require_relative "herb/colors" require_relative "herb/range" require_relative "herb/position" require_relative "herb/location" diff --git a/lib/herb/ast/node.rb b/lib/herb/ast/node.rb index e1e5e4270..076cfe568 100644 --- a/lib/herb/ast/node.rb +++ b/lib/herb/ast/node.rb @@ -4,6 +4,8 @@ module Herb module AST class Node + using Colors + attr_reader :type #: String attr_reader :location #: Location attr_reader :errors #: Array[Herb::Errors::Error] diff --git a/lib/herb/colors.rb b/lib/herb/colors.rb new file mode 100644 index 000000000..fc5a1cbb2 --- /dev/null +++ b/lib/herb/colors.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true +# typed: true + +module Herb + module Colors + def self.enabled? + return false if ENV["NO_COLOR"] + return false if defined?(IRB) + return false if defined?(Minitest) + + $stdout.tty? + end + + refine String do + def white + return self unless Herb::Colors.enabled? + + "\e[37m#{self}\e[0m" + end + + def yellow + return self unless Herb::Colors.enabled? + + "\e[33m#{self}\e[0m" + end + + def green + return self unless Herb::Colors.enabled? + + "\e[32m#{self}\e[0m" + end + + def red + return self unless Herb::Colors.enabled? + + "\e[31m#{self}\e[0m" + end + + def magenta + return self unless Herb::Colors.enabled? + + "\e[35m#{self}\e[0m" + end + + def cyan + return self unless Herb::Colors.enabled? + + "\e[36m#{self}\e[0m" + end + + def bright_magenta + return self unless Herb::Colors.enabled? + + "\e[95m#{self}\e[0m" + end + + def dimmed + return self unless Herb::Colors.enabled? + + "\e[2m#{self}\e[0m" + end + + def bold + return self unless Herb::Colors.enabled? + + "\e[1m#{self}\e[0m" + end + end + end +end diff --git a/lib/herb/location.rb b/lib/herb/location.rb index e7c4a1a10..a167c653c 100644 --- a/lib/herb/location.rb +++ b/lib/herb/location.rb @@ -3,6 +3,8 @@ module Herb class Location + using Colors + attr_reader :start #: Position attr_reader :end #: Position @@ -40,12 +42,12 @@ def to_json(state = nil) #: () -> String def tree_inspect - %((location: #{start.tree_inspect}-#{self.end.tree_inspect})) + "#{start.tree_inspect}-#{self.end.tree_inspect}" end #: () -> String def inspect - %(#) + %(#) end end end diff --git a/lib/herb/token.rb b/lib/herb/token.rb index e12d3c4d4..0c4308615 100644 --- a/lib/herb/token.rb +++ b/lib/herb/token.rb @@ -3,6 +3,8 @@ module Herb class Token + using Colors + attr_reader :value #: String attr_reader :range #: Range attr_reader :location #: Location @@ -33,7 +35,7 @@ def to_json(state = nil) #: () -> String def tree_inspect - %("#{value.force_encoding("utf-8")}" #{location.tree_inspect}) + "#{"\"#{value.force_encoding("utf-8")}\"".green} #{"(location: #{location.tree_inspect})".dimmed}" end #: () -> String @@ -45,9 +47,19 @@ def value_inspect end end + #: () -> String + def colorize_range + "[".white + range.from.to_s.cyan + ",".white + " ".white + range.to.to_s.cyan + "]".white + end + + #: (Position) -> String + def colorize_position(pos) + "(".white + pos.line.to_s.cyan + ":".white + pos.column.to_s.cyan + ")".white + end + #: () -> String def inspect - %(#) + "#{"#<".white}#{"Herb::Token".yellow.bold} #{"type=".white}#{"\"#{type}\"".bright_magenta} #{"value=".white}#{value_inspect.green} #{"range=".white}#{colorize_range} #{"start=".white}#{colorize_position(location.start)} #{"end=".white}#{colorize_position(location.end)}#{">".white}" end end end diff --git a/templates/lib/herb/ast/nodes.rb.erb b/templates/lib/herb/ast/nodes.rb.erb index cb04b29ab..2f3149844 100644 --- a/templates/lib/herb/ast/nodes.rb.erb +++ b/templates/lib/herb/ast/nodes.rb.erb @@ -2,6 +2,8 @@ module Herb module AST <%- nodes.each do |node| -%> class <%= node.name -%> < Node + using Colors + <%- node.fields.each do |field| -%> attr_reader :<%= field.name %> #: <%= field.ruby_type %> <%- end -%> @@ -59,34 +61,32 @@ module Herb def tree_inspect(indent = 0) output = +"" - output += "@ #{node_name} " - output += location.tree_inspect + output += "@".white + " " + node_name.yellow.bold + " " + "(location: #{location.tree_inspect})".dimmed output += "\n" output += inspect_errors(prefix: "<%= node.fields.any? ? "│ " : " " %>") <%- node.fields.each do |field| -%> <%- symbol = node.fields.last == field ? "└──" : "├──" -%> - <%- name = "#{symbol} #{field.name}: " -%> <%- case field -%> <%- when Herb::Template::StringField -%> - output += %(<%= name %>#{<%= field.name %>.inspect}\n) + output += "<%= symbol %>".white + " " + "<%= field.name %>".white + ": " + <%= field.name %>.inspect.green + "\n" <%- when Herb::Template::TokenField -%> - output += "<%= name %>" - output += <%= field.name %> ? <%= field.name %>.tree_inspect : "∅" + output += "<%= symbol %>".white + " " + "<%= field.name %>".white + ": " + output += <%= field.name %> ? <%= field.name %>.tree_inspect : "∅".magenta output += "\n" <%- when Herb::Template::BooleanField -%> - output += "<%= name %>" - output += [true, false].include?(<%= field.name %>) ? <%= field.name %>.to_s : "∅" + output += "<%= symbol %>".white + " " + "<%= field.name %>".white + ": " + output += [true, false].include?(<%= field.name %>) ? <%= field.name %>.to_s.magenta.bold : "∅".magenta output += "\n" <%- when Herb::Template::ElementSourceField -%> - output += %(<%= name %>#{<%= field.name %>.inspect}\n) + output += "<%= symbol %>".white + " " + "<%= field.name %>".white + ": " + <%= field.name %>.inspect.green + "\n" <%- when Herb::Template::PrismNodeField -%> # no-op for <%= field.name %> <%- when Herb::Template::AnalyzedRubyField -%> # no-op for <%= field.name %> <%- when Herb::Template::NodeField -%> - output += "<%= name %>" + output += "<%= symbol %>".white + " " + "<%= field.name %>".white + ": " if <%= field.name %> <%- prefix = (node.fields.last == field) ? " " : "│ " -%> output += "\n" @@ -96,11 +96,17 @@ module Herb output += "│ └── " <%- end -%> output += <%= field.name %>.tree_inspect(indent).gsub(/^/, " " * (indent + 1)).lstrip.gsub(/^/, "<%= prefix %>").delete_prefix("<%= prefix %>") + <%- if false -%> + <%- unless node.fields.last == field -%> + output += "\n" + output += "<%= prefix %>".white + "\n" + <%- end -%> + <%- end -%> else - output += "∅\n" + output += "∅".magenta + "\n" end <%- when Herb::Template::ArrayField -%> - output += "<%= name %>" + output += "<%= symbol %>".white + " " + "<%= field.name %>".white + ": " output += inspect_array(<%= field.name %>, prefix: "<%= (node.fields.last == field) ? " " : "│ " %>") <%- else -%> <%- raise "Unhandled type: #{field.class}" -%> diff --git a/templates/lib/herb/errors.rb.erb b/templates/lib/herb/errors.rb.erb index cc750f2fa..0cfeb15a6 100644 --- a/templates/lib/herb/errors.rb.erb +++ b/templates/lib/herb/errors.rb.erb @@ -2,6 +2,8 @@ module Herb module Errors class Error + using Colors + <%- base_arguments.each do |argument, type| -%> attr_reader :<%= argument %> #: <%= type %> <%- end -%> @@ -45,6 +47,8 @@ module Herb <%- errors.each do |error| -%> class <%= error.name -%> < Error + using Colors + <%- error.fields.each do |field| -%> attr_reader :<%= field.name %> #: <%= field.ruby_type %> <%- end -%> @@ -76,23 +80,27 @@ module Herb def tree_inspect(indent = 0) output = +"" - output += %(@ #{error_name} #{location.tree_inspect}\n) + output += "@".white + " " + error_name.red.bold + " " + "(location: #{location.tree_inspect})".dimmed + output += "\n" <%- symbol = error.fields.none? ? "└──" : "├──" -%> - output += %(<%= symbol %> message: #{message.inspect}\n) + output += "<%= symbol %>".white + " " + "message".white + ": " + message.inspect.green + "\n" <%- error.fields.each do |field| -%> <%- symbol = error.fields.last == field ? "└──" : "├──" -%> - <%- name = "#{symbol} #{field.name}: " -%> <%- case field -%> <%- when Herb::Template::PositionField -%> - output += %(<%= name %>#{<%= field.name %> ? <%= field.name %>.tree_inspect : "∅"}\n) + output += "<%= symbol %>".white + " " + "<%= field.name %>".white + ": " + output += <%= field.name %> ? <%= field.name %>.tree_inspect : "∅".magenta + output += "\n" <%- when Herb::Template::TokenField -%> - output += %(<%= name %>#{<%= field.name %> ? <%= field.name %>.tree_inspect : "∅"}\n) + output += "<%= symbol %>".white + " " + "<%= field.name %>".white + ": " + output += <%= field.name %> ? <%= field.name %>.tree_inspect : "∅".magenta + output += "\n" <%- when Herb::Template::TokenTypeField -%> - output += %(<%= name %>#{<%= field.name %>.inspect}\n) + output += "<%= symbol %>".white + " " + "<%= field.name %>".white + ": " + <%= field.name %>.inspect.green + "\n" <%- when Herb::Template::StringField -%> - output += %(<%= name %>#{<%= field.name %>.inspect}\n) + output += "<%= symbol %>".white + " " + "<%= field.name %>".white + ": " + <%= field.name %>.inspect.green + "\n" <%- else -%> - output += "<%= name %>'#{<%= field.name %>.class}'\n" + output += "<%= symbol %>".white + " " + "<%= field.name %>".white + ": '#{<%= field.name %>.class}'\n" <%- end -%> <%- end -%> output += %(\n) diff --git a/test/colors_test.rb b/test/colors_test.rb new file mode 100644 index 000000000..5e99fa21f --- /dev/null +++ b/test/colors_test.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +class ColorsTest < Minitest::Spec + test "colors are disabled in test environment" do + assert defined?(Minitest), "Minitest should be defined in test environment" + assert_equal false, Herb::Colors.enabled?, "Expected colors to be disabled when Minitest is loaded" + end + + test "token inspect does not contain ANSI codes in tests" do + start_position = Herb::Position.new(1, 0) + end_position = Herb::Position.new(1, 5) + location = Herb::Location.new(start_position, end_position) + range = Herb::Range.new(0, 5) + token = Herb::Token.new("
", range, location, "TOKEN_HTML_TAG_START") + + output = token.inspect + + refute_match(/\e\[/, output, "Token inspect should not contain ANSI codes in tests") + + assert_equal '#', output + end + + test "AST node tree_inspect does not contain ANSI codes in tests" do + result = Herb.parse("
hello
") + output = result.value.tree_inspect + + refute_match(/\e\[/, output, "AST tree_inspect should not contain ANSI codes in tests") + end + + test "NO_COLOR environment variable disables colors" do + original = ENV["NO_COLOR"] + + begin + ENV["NO_COLOR"] = "1" + + start_position = Herb::Position.new(1, 0) + end_position = Herb::Position.new(1, 1) + location = Herb::Location.new(start_position, end_position) + range = Herb::Range.new(0, 1) + token = Herb::Token.new("<", range, location, "TOKEN_HTML_TAG_START") + + output = token.inspect + + refute_match(/\e\[/, output, "Should not colorize when NO_COLOR=1") + ensure + if original + ENV["NO_COLOR"] = original + else + ENV.delete("NO_COLOR") + end + end + end + + test "colors are disabled inside IRB" do + skip "IRB is already defined" if defined?(::IRB) + + ::IRB = Module.new + + start_position = Herb::Position.new(1, 0) + end_position = Herb::Position.new(1, 1) + location = Herb::Location.new(start_position, end_position) + range = Herb::Range.new(0, 1) + token = Herb::Token.new("<", range, location, "TOKEN_HTML_TAG_START") + + output = token.inspect + + refute_match(/\e\[/, output, "Should not colorize inside IRB") + assert_equal false, Herb::Colors.enabled?, "Colors should be disabled when IRB is defined" + ensure + Object.send(:remove_const, :IRB) if defined?(::IRB) && ::IRB.is_a?(Module) && ::IRB.name.nil? + end +end From f17958020bed4573ff539f784523985a771990e1 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Tue, 4 Nov 2025 00:44:44 +0100 Subject: [PATCH 2/5] Refactor Refinements to methods since Steep doesn't support it --- lib/herb/ast/node.rb | 2 - lib/herb/colors.rb | 76 ++++++++++++++--------------- lib/herb/location.rb | 2 +- lib/herb/token.rb | 10 ++-- sig/herb/ast/nodes.rbs | 62 +++++++++++++++++++++++ sig/herb/colors.rbs | 25 ++++++++++ sig/herb/errors.rbs | 18 +++++++ sig/herb/location.rbs | 2 + sig/herb/token.rbs | 8 +++ templates/lib/herb/ast/nodes.rb.erb | 24 ++++----- templates/lib/herb/errors.rb.erb | 22 ++++----- 11 files changed, 181 insertions(+), 70 deletions(-) create mode 100644 sig/herb/colors.rbs diff --git a/lib/herb/ast/node.rb b/lib/herb/ast/node.rb index 076cfe568..e1e5e4270 100644 --- a/lib/herb/ast/node.rb +++ b/lib/herb/ast/node.rb @@ -4,8 +4,6 @@ module Herb module AST class Node - using Colors - attr_reader :type #: String attr_reader :location #: Location attr_reader :errors #: Array[Herb::Errors::Error] diff --git a/lib/herb/colors.rb b/lib/herb/colors.rb index fc5a1cbb2..9a26925e3 100644 --- a/lib/herb/colors.rb +++ b/lib/herb/colors.rb @@ -3,7 +3,9 @@ module Herb module Colors - def self.enabled? + module_function + + def enabled? return false if ENV["NO_COLOR"] return false if defined?(IRB) return false if defined?(Minitest) @@ -11,60 +13,58 @@ def self.enabled? $stdout.tty? end - refine String do - def white - return self unless Herb::Colors.enabled? + def white(string) + return string unless enabled? - "\e[37m#{self}\e[0m" - end + "\e[37m#{string}\e[0m" + end - def yellow - return self unless Herb::Colors.enabled? + def yellow(string) + return string unless enabled? - "\e[33m#{self}\e[0m" - end + "\e[33m#{string}\e[0m" + end - def green - return self unless Herb::Colors.enabled? + def green(string) + return string unless enabled? - "\e[32m#{self}\e[0m" - end + "\e[32m#{string}\e[0m" + end - def red - return self unless Herb::Colors.enabled? + def red(string) + return string unless enabled? - "\e[31m#{self}\e[0m" - end + "\e[31m#{string}\e[0m" + end - def magenta - return self unless Herb::Colors.enabled? + def magenta(string) + return string unless enabled? - "\e[35m#{self}\e[0m" - end + "\e[35m#{string}\e[0m" + end - def cyan - return self unless Herb::Colors.enabled? + def cyan(string) + return string unless enabled? - "\e[36m#{self}\e[0m" - end + "\e[36m#{string}\e[0m" + end - def bright_magenta - return self unless Herb::Colors.enabled? + def bright_magenta(string) + return string unless enabled? - "\e[95m#{self}\e[0m" - end + "\e[95m#{string}\e[0m" + end - def dimmed - return self unless Herb::Colors.enabled? + def dimmed(string) + return string unless enabled? - "\e[2m#{self}\e[0m" - end + "\e[2m#{string}\e[0m" + end - def bold - return self unless Herb::Colors.enabled? + def bold(string) + return string unless enabled? - "\e[1m#{self}\e[0m" - end + "\e[1m#{string}\e[0m" end end end diff --git a/lib/herb/location.rb b/lib/herb/location.rb index a167c653c..cb8c20b0f 100644 --- a/lib/herb/location.rb +++ b/lib/herb/location.rb @@ -3,7 +3,7 @@ module Herb class Location - using Colors + include Colors attr_reader :start #: Position attr_reader :end #: Position diff --git a/lib/herb/token.rb b/lib/herb/token.rb index 0c4308615..13c15b322 100644 --- a/lib/herb/token.rb +++ b/lib/herb/token.rb @@ -3,7 +3,7 @@ module Herb class Token - using Colors + include Colors attr_reader :value #: String attr_reader :range #: Range @@ -35,7 +35,7 @@ def to_json(state = nil) #: () -> String def tree_inspect - "#{"\"#{value.force_encoding("utf-8")}\"".green} #{"(location: #{location.tree_inspect})".dimmed}" + green("\"#{value.force_encoding("utf-8")}\"") + " " + dimmed("(location: #{location.tree_inspect})") end #: () -> String @@ -49,17 +49,17 @@ def value_inspect #: () -> String def colorize_range - "[".white + range.from.to_s.cyan + ",".white + " ".white + range.to.to_s.cyan + "]".white + white("[") + cyan(range.from.to_s) + white(",") + white(" ") + cyan(range.to.to_s) + white("]") end #: (Position) -> String def colorize_position(pos) - "(".white + pos.line.to_s.cyan + ":".white + pos.column.to_s.cyan + ")".white + white("(") + cyan(pos.line.to_s) + white(":") + cyan(pos.column.to_s) + white(")") end #: () -> String def inspect - "#{"#<".white}#{"Herb::Token".yellow.bold} #{"type=".white}#{"\"#{type}\"".bright_magenta} #{"value=".white}#{value_inspect.green} #{"range=".white}#{colorize_range} #{"start=".white}#{colorize_position(location.start)} #{"end=".white}#{colorize_position(location.end)}#{">".white}" + "#{white("#<")}#{bold(yellow("Herb::Token"))} #{white("type=")}#{bright_magenta("\"#{type}\"")} #{white("value=")}#{green(value_inspect)} #{white("range=")}#{colorize_range} #{white("start=")}#{colorize_position(location.start)} #{white("end=")}#{colorize_position(location.end)}#{white(">")}" end end end diff --git a/sig/herb/ast/nodes.rbs b/sig/herb/ast/nodes.rbs index bc41595ad..47ad6767b 100644 --- a/sig/herb/ast/nodes.rbs +++ b/sig/herb/ast/nodes.rbs @@ -3,6 +3,8 @@ module Herb module AST class DocumentNode < Node + include Colors + attr_reader children: Array[Herb::AST::Node] # : (String, Location, Array[Herb::Errors::Error], Array[Herb::AST::Node]) -> void @@ -28,6 +30,8 @@ module Herb end class LiteralNode < Node + include Colors + attr_reader content: String # : (String, Location, Array[Herb::Errors::Error], String) -> void @@ -53,6 +57,8 @@ module Herb end class HTMLOpenTagNode < Node + include Colors + attr_reader tag_opening: Herb::Token attr_reader tag_name: Herb::Token @@ -86,6 +92,8 @@ module Herb end class HTMLCloseTagNode < Node + include Colors + attr_reader tag_opening: Herb::Token attr_reader tag_name: Herb::Token @@ -117,6 +125,8 @@ module Herb end class HTMLElementNode < Node + include Colors + attr_reader open_tag: Herb::AST::HTMLOpenTagNode attr_reader tag_name: Herb::Token @@ -152,6 +162,8 @@ module Herb end class HTMLAttributeValueNode < Node + include Colors + attr_reader open_quote: Herb::Token attr_reader children: Array[Herb::AST::Node] @@ -183,6 +195,8 @@ module Herb end class HTMLAttributeNameNode < Node + include Colors + attr_reader children: Array[Herb::AST::Node] # : (String, Location, Array[Herb::Errors::Error], Array[Herb::AST::Node]) -> void @@ -208,6 +222,8 @@ module Herb end class HTMLAttributeNode < Node + include Colors + attr_reader name: Herb::AST::HTMLAttributeNameNode attr_reader equals: Herb::Token @@ -237,6 +253,8 @@ module Herb end class HTMLTextNode < Node + include Colors + attr_reader content: String # : (String, Location, Array[Herb::Errors::Error], String) -> void @@ -262,6 +280,8 @@ module Herb end class HTMLCommentNode < Node + include Colors + attr_reader comment_start: Herb::Token attr_reader children: Array[Herb::AST::Node] @@ -291,6 +311,8 @@ module Herb end class HTMLDoctypeNode < Node + include Colors + attr_reader tag_opening: Herb::Token attr_reader children: Array[Herb::AST::Node] @@ -320,6 +342,8 @@ module Herb end class XMLDeclarationNode < Node + include Colors + attr_reader tag_opening: Herb::Token attr_reader children: Array[Herb::AST::Node] @@ -349,6 +373,8 @@ module Herb end class CDATANode < Node + include Colors + attr_reader tag_opening: Herb::Token attr_reader children: Array[Herb::AST::Node] @@ -378,6 +404,8 @@ module Herb end class WhitespaceNode < Node + include Colors + attr_reader value: Herb::Token # : (String, Location, Array[Herb::Errors::Error], Herb::Token) -> void @@ -403,6 +431,8 @@ module Herb end class ERBContentNode < Node + include Colors + attr_reader tag_opening: Herb::Token attr_reader content: Herb::Token @@ -438,6 +468,8 @@ module Herb end class ERBEndNode < Node + include Colors + attr_reader tag_opening: Herb::Token attr_reader content: Herb::Token @@ -467,6 +499,8 @@ module Herb end class ERBElseNode < Node + include Colors + attr_reader tag_opening: Herb::Token attr_reader content: Herb::Token @@ -498,6 +532,8 @@ module Herb end class ERBIfNode < Node + include Colors + attr_reader tag_opening: Herb::Token attr_reader content: Herb::Token @@ -533,6 +569,8 @@ module Herb end class ERBBlockNode < Node + include Colors + attr_reader tag_opening: Herb::Token attr_reader content: Herb::Token @@ -566,6 +604,8 @@ module Herb end class ERBWhenNode < Node + include Colors + attr_reader tag_opening: Herb::Token attr_reader content: Herb::Token @@ -597,6 +637,8 @@ module Herb end class ERBCaseNode < Node + include Colors + attr_reader tag_opening: Herb::Token attr_reader content: Herb::Token @@ -634,6 +676,8 @@ module Herb end class ERBCaseMatchNode < Node + include Colors + attr_reader tag_opening: Herb::Token attr_reader content: Herb::Token @@ -671,6 +715,8 @@ module Herb end class ERBWhileNode < Node + include Colors + attr_reader tag_opening: Herb::Token attr_reader content: Herb::Token @@ -704,6 +750,8 @@ module Herb end class ERBUntilNode < Node + include Colors + attr_reader tag_opening: Herb::Token attr_reader content: Herb::Token @@ -737,6 +785,8 @@ module Herb end class ERBForNode < Node + include Colors + attr_reader tag_opening: Herb::Token attr_reader content: Herb::Token @@ -770,6 +820,8 @@ module Herb end class ERBRescueNode < Node + include Colors + attr_reader tag_opening: Herb::Token attr_reader content: Herb::Token @@ -803,6 +855,8 @@ module Herb end class ERBEnsureNode < Node + include Colors + attr_reader tag_opening: Herb::Token attr_reader content: Herb::Token @@ -834,6 +888,8 @@ module Herb end class ERBBeginNode < Node + include Colors + attr_reader tag_opening: Herb::Token attr_reader content: Herb::Token @@ -873,6 +929,8 @@ module Herb end class ERBUnlessNode < Node + include Colors + attr_reader tag_opening: Herb::Token attr_reader content: Herb::Token @@ -908,6 +966,8 @@ module Herb end class ERBYieldNode < Node + include Colors + attr_reader tag_opening: Herb::Token attr_reader content: Herb::Token @@ -937,6 +997,8 @@ module Herb end class ERBInNode < Node + include Colors + attr_reader tag_opening: Herb::Token attr_reader content: Herb::Token diff --git a/sig/herb/colors.rbs b/sig/herb/colors.rbs new file mode 100644 index 000000000..7e7569747 --- /dev/null +++ b/sig/herb/colors.rbs @@ -0,0 +1,25 @@ +# Generated from lib/herb/colors.rb with RBS::Inline + +module Herb + module Colors + def self?.enabled?: () -> untyped + + def self?.white: (untyped string) -> untyped + + def self?.yellow: (untyped string) -> untyped + + def self?.green: (untyped string) -> untyped + + def self?.red: (untyped string) -> untyped + + def self?.magenta: (untyped string) -> untyped + + def self?.cyan: (untyped string) -> untyped + + def self?.bright_magenta: (untyped string) -> untyped + + def self?.dimmed: (untyped string) -> untyped + + def self?.bold: (untyped string) -> untyped + end +end diff --git a/sig/herb/errors.rbs b/sig/herb/errors.rbs index ec1b47a57..6ae9679f6 100644 --- a/sig/herb/errors.rbs +++ b/sig/herb/errors.rbs @@ -29,6 +29,8 @@ module Herb end class UnexpectedError < Error + include Colors + attr_reader description: String attr_reader expected: String @@ -49,6 +51,8 @@ module Herb end class UnexpectedTokenError < Error + include Colors + attr_reader expected_type: String attr_reader found: Herb::Token @@ -67,6 +71,8 @@ module Herb end class MissingOpeningTagError < Error + include Colors + attr_reader closing_tag: Herb::Token # : (String, Location, String, Herb::Token) -> void @@ -83,6 +89,8 @@ module Herb end class MissingClosingTagError < Error + include Colors + attr_reader opening_tag: Herb::Token # : (String, Location, String, Herb::Token) -> void @@ -99,6 +107,8 @@ module Herb end class TagNamesMismatchError < Error + include Colors + attr_reader opening_tag: Herb::Token attr_reader closing_tag: Herb::Token @@ -117,6 +127,8 @@ module Herb end class QuotesMismatchError < Error + include Colors + attr_reader opening_quote: Herb::Token attr_reader closing_quote: Herb::Token @@ -135,6 +147,8 @@ module Herb end class VoidElementClosingTagError < Error + include Colors + attr_reader tag_name: Herb::Token attr_reader expected: String @@ -155,6 +169,8 @@ module Herb end class UnclosedElementError < Error + include Colors + attr_reader opening_tag: Herb::Token # : (String, Location, String, Herb::Token) -> void @@ -171,6 +187,8 @@ module Herb end class RubyParseError < Error + include Colors + attr_reader error_message: String attr_reader diagnostic_id: String diff --git a/sig/herb/location.rbs b/sig/herb/location.rbs index b10799f1d..2966f9b85 100644 --- a/sig/herb/location.rbs +++ b/sig/herb/location.rbs @@ -2,6 +2,8 @@ module Herb class Location + include Colors + attr_reader start: Position attr_reader end: Position diff --git a/sig/herb/token.rbs b/sig/herb/token.rbs index d29c75c06..ea8d71937 100644 --- a/sig/herb/token.rbs +++ b/sig/herb/token.rbs @@ -2,6 +2,8 @@ module Herb class Token + include Colors + attr_reader value: String attr_reader range: Range @@ -25,6 +27,12 @@ module Herb # : () -> String def value_inspect: () -> String + # : () -> String + def colorize_range: () -> String + + # : (Position) -> String + def colorize_position: (Position) -> String + # : () -> String def inspect: () -> String end diff --git a/templates/lib/herb/ast/nodes.rb.erb b/templates/lib/herb/ast/nodes.rb.erb index 2f3149844..9d05cc002 100644 --- a/templates/lib/herb/ast/nodes.rb.erb +++ b/templates/lib/herb/ast/nodes.rb.erb @@ -2,7 +2,7 @@ module Herb module AST <%- nodes.each do |node| -%> class <%= node.name -%> < Node - using Colors + include Colors <%- node.fields.each do |field| -%> attr_reader :<%= field.name %> #: <%= field.ruby_type %> @@ -61,7 +61,7 @@ module Herb def tree_inspect(indent = 0) output = +"" - output += "@".white + " " + node_name.yellow.bold + " " + "(location: #{location.tree_inspect})".dimmed + output += white("@") + " " + bold(yellow(node_name)) + " " + dimmed("(location: #{location.tree_inspect})") output += "\n" output += inspect_errors(prefix: "<%= node.fields.any? ? "│ " : " " %>") @@ -70,23 +70,23 @@ module Herb <%- symbol = node.fields.last == field ? "└──" : "├──" -%> <%- case field -%> <%- when Herb::Template::StringField -%> - output += "<%= symbol %>".white + " " + "<%= field.name %>".white + ": " + <%= field.name %>.inspect.green + "\n" + output += white("<%= symbol %>") + " " + white("<%= field.name %>") + ": " + green(<%= field.name %>.inspect) + "\n" <%- when Herb::Template::TokenField -%> - output += "<%= symbol %>".white + " " + "<%= field.name %>".white + ": " - output += <%= field.name %> ? <%= field.name %>.tree_inspect : "∅".magenta + output += white("<%= symbol %>") + " " + white("<%= field.name %>") + ": " + output += <%= field.name %> ? <%= field.name %>.tree_inspect : magenta("∅") output += "\n" <%- when Herb::Template::BooleanField -%> - output += "<%= symbol %>".white + " " + "<%= field.name %>".white + ": " - output += [true, false].include?(<%= field.name %>) ? <%= field.name %>.to_s.magenta.bold : "∅".magenta + output += white("<%= symbol %>") + " " + white("<%= field.name %>") + ": " + output += [true, false].include?(<%= field.name %>) ? bold(magenta(<%= field.name %>.to_s)) : magenta("∅") output += "\n" <%- when Herb::Template::ElementSourceField -%> - output += "<%= symbol %>".white + " " + "<%= field.name %>".white + ": " + <%= field.name %>.inspect.green + "\n" + output += white("<%= symbol %>") + " " + white("<%= field.name %>") + ": " + green(<%= field.name %>.inspect) + "\n" <%- when Herb::Template::PrismNodeField -%> # no-op for <%= field.name %> <%- when Herb::Template::AnalyzedRubyField -%> # no-op for <%= field.name %> <%- when Herb::Template::NodeField -%> - output += "<%= symbol %>".white + " " + "<%= field.name %>".white + ": " + output += white("<%= symbol %>") + " " + white("<%= field.name %>") + ": " if <%= field.name %> <%- prefix = (node.fields.last == field) ? " " : "│ " -%> output += "\n" @@ -99,14 +99,14 @@ module Herb <%- if false -%> <%- unless node.fields.last == field -%> output += "\n" - output += "<%= prefix %>".white + "\n" + output += white("<%= prefix %>") + "\n" <%- end -%> <%- end -%> else - output += "∅".magenta + "\n" + output += magenta("∅") + "\n" end <%- when Herb::Template::ArrayField -%> - output += "<%= symbol %>".white + " " + "<%= field.name %>".white + ": " + output += white("<%= symbol %>") + " " + white("<%= field.name %>") + ": " output += inspect_array(<%= field.name %>, prefix: "<%= (node.fields.last == field) ? " " : "│ " %>") <%- else -%> <%- raise "Unhandled type: #{field.class}" -%> diff --git a/templates/lib/herb/errors.rb.erb b/templates/lib/herb/errors.rb.erb index 0cfeb15a6..fff19e5db 100644 --- a/templates/lib/herb/errors.rb.erb +++ b/templates/lib/herb/errors.rb.erb @@ -2,8 +2,6 @@ module Herb module Errors class Error - using Colors - <%- base_arguments.each do |argument, type| -%> attr_reader :<%= argument %> #: <%= type %> <%- end -%> @@ -47,7 +45,7 @@ module Herb <%- errors.each do |error| -%> class <%= error.name -%> < Error - using Colors + include Colors <%- error.fields.each do |field| -%> attr_reader :<%= field.name %> #: <%= field.ruby_type %> @@ -80,27 +78,27 @@ module Herb def tree_inspect(indent = 0) output = +"" - output += "@".white + " " + error_name.red.bold + " " + "(location: #{location.tree_inspect})".dimmed + output += white("@") + " " + bold(red(error_name)) + " " + dimmed("(location: #{location.tree_inspect})") output += "\n" <%- symbol = error.fields.none? ? "└──" : "├──" -%> - output += "<%= symbol %>".white + " " + "message".white + ": " + message.inspect.green + "\n" + output += white("<%= symbol %>") + " " + white("message") + ": " + green(message.inspect) + "\n" <%- error.fields.each do |field| -%> <%- symbol = error.fields.last == field ? "└──" : "├──" -%> <%- case field -%> <%- when Herb::Template::PositionField -%> - output += "<%= symbol %>".white + " " + "<%= field.name %>".white + ": " - output += <%= field.name %> ? <%= field.name %>.tree_inspect : "∅".magenta + output += white("<%= symbol %>") + " " + white("<%= field.name %>") + ": " + output += <%= field.name %> ? <%= field.name %>.tree_inspect : magenta("∅") output += "\n" <%- when Herb::Template::TokenField -%> - output += "<%= symbol %>".white + " " + "<%= field.name %>".white + ": " - output += <%= field.name %> ? <%= field.name %>.tree_inspect : "∅".magenta + output += white("<%= symbol %>") + " " + white("<%= field.name %>") + ": " + output += <%= field.name %> ? <%= field.name %>.tree_inspect : magenta("∅") output += "\n" <%- when Herb::Template::TokenTypeField -%> - output += "<%= symbol %>".white + " " + "<%= field.name %>".white + ": " + <%= field.name %>.inspect.green + "\n" + output += white("<%= symbol %>") + " " + white("<%= field.name %>") + ": " + green(<%= field.name %>.inspect) + "\n" <%- when Herb::Template::StringField -%> - output += "<%= symbol %>".white + " " + "<%= field.name %>".white + ": " + <%= field.name %>.inspect.green + "\n" + output += white("<%= symbol %>") + " " + white("<%= field.name %>") + ": " + green(<%= field.name %>.inspect) + "\n" <%- else -%> - output += "<%= symbol %>".white + " " + "<%= field.name %>".white + ": '#{<%= field.name %>.class}'\n" + output += white("<%= symbol %>") + " " + white("<%= field.name %>") + ": '#{<%= field.name %>.class}'\n" <%- end -%> <%- end -%> output += %(\n) From 52afe95ead9ef8a0df83fe5a9b4bfcaf8ad6f4c0 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Tue, 4 Nov 2025 01:06:58 +0100 Subject: [PATCH 3/5] RuboCop --- lib/herb/location.rb | 2 -- lib/herb/token.rb | 8 ++++---- sig/herb/location.rbs | 2 -- templates/lib/herb/ast/nodes.rb.erb | 18 +++++++++--------- templates/lib/herb/errors.rb.erb | 15 +++++++-------- 5 files changed, 20 insertions(+), 25 deletions(-) diff --git a/lib/herb/location.rb b/lib/herb/location.rb index cb8c20b0f..276ce61aa 100644 --- a/lib/herb/location.rb +++ b/lib/herb/location.rb @@ -3,8 +3,6 @@ module Herb class Location - include Colors - attr_reader :start #: Position attr_reader :end #: Position diff --git a/lib/herb/token.rb b/lib/herb/token.rb index 13c15b322..33742fe5a 100644 --- a/lib/herb/token.rb +++ b/lib/herb/token.rb @@ -35,7 +35,7 @@ def to_json(state = nil) #: () -> String def tree_inspect - green("\"#{value.force_encoding("utf-8")}\"") + " " + dimmed("(location: #{location.tree_inspect})") + "#{green("\"#{value.force_encoding("utf-8")}\"")} #{dimmed("(location: #{location.tree_inspect})")}" end #: () -> String @@ -49,12 +49,12 @@ def value_inspect #: () -> String def colorize_range - white("[") + cyan(range.from.to_s) + white(",") + white(" ") + cyan(range.to.to_s) + white("]") + white("[") + cyan(range.from.to_s) + white(", ") + cyan(range.to.to_s) + white("]") end #: (Position) -> String - def colorize_position(pos) - white("(") + cyan(pos.line.to_s) + white(":") + cyan(pos.column.to_s) + white(")") + def colorize_position(position) + white("(") + cyan(position.line.to_s) + white(":") + cyan(position.column.to_s) + white(")") end #: () -> String diff --git a/sig/herb/location.rbs b/sig/herb/location.rbs index 2966f9b85..b10799f1d 100644 --- a/sig/herb/location.rbs +++ b/sig/herb/location.rbs @@ -2,8 +2,6 @@ module Herb class Location - include Colors - attr_reader start: Position attr_reader end: Position diff --git a/templates/lib/herb/ast/nodes.rb.erb b/templates/lib/herb/ast/nodes.rb.erb index 9d05cc002..f02c14ea1 100644 --- a/templates/lib/herb/ast/nodes.rb.erb +++ b/templates/lib/herb/ast/nodes.rb.erb @@ -61,7 +61,7 @@ module Herb def tree_inspect(indent = 0) output = +"" - output += white("@") + " " + bold(yellow(node_name)) + " " + dimmed("(location: #{location.tree_inspect})") + output += white("@ #{bold(yellow(node_name.to_s))} #{dimmed("(location: #{location.tree_inspect})")}") output += "\n" output += inspect_errors(prefix: "<%= node.fields.any? ? "│ " : " " %>") @@ -70,23 +70,23 @@ module Herb <%- symbol = node.fields.last == field ? "└──" : "├──" -%> <%- case field -%> <%- when Herb::Template::StringField -%> - output += white("<%= symbol %>") + " " + white("<%= field.name %>") + ": " + green(<%= field.name %>.inspect) + "\n" + output += white("<%= symbol %> <%= field.name %>: ") + green("#{<%= field.name %>.inspect}\n") <%- when Herb::Template::TokenField -%> - output += white("<%= symbol %>") + " " + white("<%= field.name %>") + ": " + output += white("<%= symbol %> <%= field.name %>: ") output += <%= field.name %> ? <%= field.name %>.tree_inspect : magenta("∅") output += "\n" <%- when Herb::Template::BooleanField -%> - output += white("<%= symbol %>") + " " + white("<%= field.name %>") + ": " + output += white("<%= symbol %> <%= field.name %>: ") output += [true, false].include?(<%= field.name %>) ? bold(magenta(<%= field.name %>.to_s)) : magenta("∅") output += "\n" <%- when Herb::Template::ElementSourceField -%> - output += white("<%= symbol %>") + " " + white("<%= field.name %>") + ": " + green(<%= field.name %>.inspect) + "\n" + output += white("<%= symbol %> <%= field.name %>: #{green(<%= field.name %>.inspect)}\n") <%- when Herb::Template::PrismNodeField -%> # no-op for <%= field.name %> <%- when Herb::Template::AnalyzedRubyField -%> # no-op for <%= field.name %> <%- when Herb::Template::NodeField -%> - output += white("<%= symbol %>") + " " + white("<%= field.name %>") + ": " + output += white("<%= symbol %> <%= field.name %>: ") if <%= field.name %> <%- prefix = (node.fields.last == field) ? " " : "│ " -%> output += "\n" @@ -99,14 +99,14 @@ module Herb <%- if false -%> <%- unless node.fields.last == field -%> output += "\n" - output += white("<%= prefix %>") + "\n" + output += white("<%= prefix %>\n") <%- end -%> <%- end -%> else - output += magenta("∅") + "\n" + output += magenta("∅\n") end <%- when Herb::Template::ArrayField -%> - output += white("<%= symbol %>") + " " + white("<%= field.name %>") + ": " + output += white("<%= symbol %> <%= field.name %>: ") output += inspect_array(<%= field.name %>, prefix: "<%= (node.fields.last == field) ? " " : "│ " %>") <%- else -%> <%- raise "Unhandled type: #{field.class}" -%> diff --git a/templates/lib/herb/errors.rb.erb b/templates/lib/herb/errors.rb.erb index fff19e5db..8fe497990 100644 --- a/templates/lib/herb/errors.rb.erb +++ b/templates/lib/herb/errors.rb.erb @@ -78,27 +78,26 @@ module Herb def tree_inspect(indent = 0) output = +"" - output += white("@") + " " + bold(red(error_name)) + " " + dimmed("(location: #{location.tree_inspect})") - output += "\n" + output += white("@ #{bold(red(error_name))} #{dimmed("(location: #{location.tree_inspect})\n")}") <%- symbol = error.fields.none? ? "└──" : "├──" -%> - output += white("<%= symbol %>") + " " + white("message") + ": " + green(message.inspect) + "\n" + output += white("<%= symbol %> message: #{green(message.inspect)}\n") <%- error.fields.each do |field| -%> <%- symbol = error.fields.last == field ? "└──" : "├──" -%> <%- case field -%> <%- when Herb::Template::PositionField -%> - output += white("<%= symbol %>") + " " + white("<%= field.name %>") + ": " + output += white("<%= symbol %> <%= field.name %>: ") output += <%= field.name %> ? <%= field.name %>.tree_inspect : magenta("∅") output += "\n" <%- when Herb::Template::TokenField -%> - output += white("<%= symbol %>") + " " + white("<%= field.name %>") + ": " + output += white("<%= symbol %> <%= field.name %>: ") output += <%= field.name %> ? <%= field.name %>.tree_inspect : magenta("∅") output += "\n" <%- when Herb::Template::TokenTypeField -%> - output += white("<%= symbol %>") + " " + white("<%= field.name %>") + ": " + green(<%= field.name %>.inspect) + "\n" + output += white("<%= symbol %> <%= field.name %>: #{green(<%= field.name %>.inspect)}\n") <%- when Herb::Template::StringField -%> - output += white("<%= symbol %>") + " " + white("<%= field.name %>") + ": " + green(<%= field.name %>.inspect) + "\n" + output += white("<%= symbol %> <%= field.name %>: #{green(<%= field.name %>.inspect)}\n") <%- else -%> - output += white("<%= symbol %>") + " " + white("<%= field.name %>") + ": '#{<%= field.name %>.class}'\n" + output += white("<%= symbol %> <%= field.name %>: ") + '#{<%= field.name %>.class}'\n" <%- end -%> <%- end -%> output += %(\n) From 0e58ee25d9f249db58fbc98e055f0ac4c390ea36 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Tue, 4 Nov 2025 01:09:19 +0100 Subject: [PATCH 4/5] Add rbs types for colors --- lib/herb/colors.rb | 12 ++++++++++++ sig/herb/colors.rbs | 30 ++++++++++++++++++++---------- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/lib/herb/colors.rb b/lib/herb/colors.rb index 9a26925e3..6edcbf82f 100644 --- a/lib/herb/colors.rb +++ b/lib/herb/colors.rb @@ -1,10 +1,13 @@ # frozen_string_literal: true # typed: true +# rbs_inline: enabled + module Herb module Colors module_function + #: () -> bool def enabled? return false if ENV["NO_COLOR"] return false if defined?(IRB) @@ -13,54 +16,63 @@ def enabled? $stdout.tty? end + #: (String) -> String def white(string) return string unless enabled? "\e[37m#{string}\e[0m" end + #: (String) -> String def yellow(string) return string unless enabled? "\e[33m#{string}\e[0m" end + #: (String) -> String def green(string) return string unless enabled? "\e[32m#{string}\e[0m" end + #: (String) -> String def red(string) return string unless enabled? "\e[31m#{string}\e[0m" end + #: (String) -> String def magenta(string) return string unless enabled? "\e[35m#{string}\e[0m" end + #: (String) -> String def cyan(string) return string unless enabled? "\e[36m#{string}\e[0m" end + #: (String) -> String def bright_magenta(string) return string unless enabled? "\e[95m#{string}\e[0m" end + #: (String) -> String def dimmed(string) return string unless enabled? "\e[2m#{string}\e[0m" end + #: (String) -> String def bold(string) return string unless enabled? diff --git a/sig/herb/colors.rbs b/sig/herb/colors.rbs index 7e7569747..50273d104 100644 --- a/sig/herb/colors.rbs +++ b/sig/herb/colors.rbs @@ -2,24 +2,34 @@ module Herb module Colors - def self?.enabled?: () -> untyped + # : () -> bool + def self?.enabled?: () -> bool - def self?.white: (untyped string) -> untyped + # : (String) -> String + def self?.white: (String) -> String - def self?.yellow: (untyped string) -> untyped + # : (String) -> String + def self?.yellow: (String) -> String - def self?.green: (untyped string) -> untyped + # : (String) -> String + def self?.green: (String) -> String - def self?.red: (untyped string) -> untyped + # : (String) -> String + def self?.red: (String) -> String - def self?.magenta: (untyped string) -> untyped + # : (String) -> String + def self?.magenta: (String) -> String - def self?.cyan: (untyped string) -> untyped + # : (String) -> String + def self?.cyan: (String) -> String - def self?.bright_magenta: (untyped string) -> untyped + # : (String) -> String + def self?.bright_magenta: (String) -> String - def self?.dimmed: (untyped string) -> untyped + # : (String) -> String + def self?.dimmed: (String) -> String - def self?.bold: (untyped string) -> untyped + # : (String) -> String + def self?.bold: (String) -> String end end From 37d0ef9bd9451d37b22801ed7faccff4fd0c363e Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Tue, 4 Nov 2025 01:13:52 +0100 Subject: [PATCH 5/5] RuboCop --- test/colors_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/colors_test.rb b/test/colors_test.rb index 5e99fa21f..166a11d23 100644 --- a/test/colors_test.rb +++ b/test/colors_test.rb @@ -30,7 +30,7 @@ class ColorsTest < Minitest::Spec end test "NO_COLOR environment variable disables colors" do - original = ENV["NO_COLOR"] + original = ENV.fetch("NO_COLOR", nil) begin ENV["NO_COLOR"] = "1"