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/colors.rb b/lib/herb/colors.rb new file mode 100644 index 000000000..6edcbf82f --- /dev/null +++ b/lib/herb/colors.rb @@ -0,0 +1,82 @@ +# 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) + return false if defined?(Minitest) + + $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? + + "\e[1m#{string}\e[0m" + end + end +end diff --git a/lib/herb/location.rb b/lib/herb/location.rb index e7c4a1a10..276ce61aa 100644 --- a/lib/herb/location.rb +++ b/lib/herb/location.rb @@ -40,12 +40,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..33742fe5a 100644 --- a/lib/herb/token.rb +++ b/lib/herb/token.rb @@ -3,6 +3,8 @@ module Herb class Token + include 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}) + "#{green("\"#{value.force_encoding("utf-8")}\"")} #{dimmed("(location: #{location.tree_inspect})")}" end #: () -> String @@ -45,9 +47,19 @@ def value_inspect end end + #: () -> String + def colorize_range + white("[") + cyan(range.from.to_s) + white(", ") + cyan(range.to.to_s) + white("]") + end + + #: (Position) -> String + def colorize_position(position) + white("(") + cyan(position.line.to_s) + white(":") + cyan(position.column.to_s) + white(")") + end + #: () -> String def inspect - %(#) + "#{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..50273d104 --- /dev/null +++ b/sig/herb/colors.rbs @@ -0,0 +1,35 @@ +# Generated from lib/herb/colors.rb with RBS::Inline + +module Herb + module Colors + # : () -> bool + def self?.enabled?: () -> bool + + # : (String) -> String + def self?.white: (String) -> String + + # : (String) -> String + def self?.yellow: (String) -> String + + # : (String) -> String + def self?.green: (String) -> String + + # : (String) -> String + def self?.red: (String) -> String + + # : (String) -> String + def self?.magenta: (String) -> String + + # : (String) -> String + def self?.cyan: (String) -> String + + # : (String) -> String + def self?.bright_magenta: (String) -> String + + # : (String) -> String + def self?.dimmed: (String) -> String + + # : (String) -> String + def self?.bold: (String) -> String + 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/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 cb04b29ab..f02c14ea1 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 + include 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("@ #{bold(yellow(node_name.to_s))} #{dimmed("(location: #{location.tree_inspect})")}") 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 += white("<%= symbol %> <%= field.name %>: ") + green("#{<%= field.name %>.inspect}\n") <%- when Herb::Template::TokenField -%> - output += "<%= name %>" - output += <%= field.name %> ? <%= field.name %>.tree_inspect : "∅" + output += white("<%= symbol %> <%= field.name %>: ") + 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 += white("<%= symbol %> <%= field.name %>: ") + output += [true, false].include?(<%= field.name %>) ? bold(magenta(<%= field.name %>.to_s)) : magenta("∅") output += "\n" <%- when Herb::Template::ElementSourceField -%> - output += %(<%= name %>#{<%= 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 += "<%= name %>" + output += white("<%= symbol %> <%= field.name %>: ") 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 += white("<%= prefix %>\n") + <%- end -%> + <%- end -%> else - output += "∅\n" + output += magenta("∅\n") end <%- when Herb::Template::ArrayField -%> - output += "<%= 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 cc750f2fa..8fe497990 100644 --- a/templates/lib/herb/errors.rb.erb +++ b/templates/lib/herb/errors.rb.erb @@ -45,6 +45,8 @@ module Herb <%- errors.each do |error| -%> class <%= error.name -%> < Error + include Colors + <%- error.fields.each do |field| -%> attr_reader :<%= field.name %> #: <%= field.ruby_type %> <%- end -%> @@ -76,23 +78,26 @@ module Herb def tree_inspect(indent = 0) output = +"" - output += %(@ #{error_name} #{location.tree_inspect}\n) + output += white("@ #{bold(red(error_name))} #{dimmed("(location: #{location.tree_inspect})\n")}") <%- symbol = error.fields.none? ? "└──" : "├──" -%> - output += %(<%= symbol %> message: #{message.inspect}\n) + output += white("<%= symbol %> message: #{green(message.inspect)}\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 += white("<%= symbol %> <%= field.name %>: ") + output += <%= field.name %> ? <%= field.name %>.tree_inspect : magenta("∅") + output += "\n" <%- when Herb::Template::TokenField -%> - output += %(<%= name %>#{<%= field.name %> ? <%= field.name %>.tree_inspect : "∅"}\n) + output += white("<%= symbol %> <%= field.name %>: ") + output += <%= field.name %> ? <%= field.name %>.tree_inspect : magenta("∅") + output += "\n" <%- when Herb::Template::TokenTypeField -%> - output += %(<%= name %>#{<%= field.name %>.inspect}\n) + output += white("<%= symbol %> <%= field.name %>: #{green(<%= field.name %>.inspect)}\n") <%- when Herb::Template::StringField -%> - output += %(<%= name %>#{<%= field.name %>.inspect}\n) + output += white("<%= symbol %> <%= field.name %>: #{green(<%= field.name %>.inspect)}\n") <%- else -%> - output += "<%= name %>'#{<%= field.name %>.class}'\n" + output += white("<%= symbol %> <%= field.name %>: ") + '#{<%= 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..166a11d23 --- /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.fetch("NO_COLOR", nil) + + 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