Class: Herb::Engine::ValidationErrorOverlay

Inherits:
Object
  • Object
show all
Defined in:
lib/herb/engine/validation_error_overlay.rb,
sig/herb/engine/validation_error_overlay.rbs

Constant Summary collapse

CONTEXT_LINES =

Returns:

  • (::Integer)
2
VALIDATOR_BADGES =

Returns:

  • (Object)
{
  "SecurityValidator" => { label: "Security", color: "#dc2626" },
  "NestingValidator" => { label: "Nesting", color: "#f59e0b" },
  "AccessibilityValidator" => { label: "A11y", color: "#3b82f6" },
}.freeze
SEVERITY_COLORS =

Returns:

  • (Object)
{
  "error" => "#dc2626",
  "warning" => "#f59e0b",
  "info" => "#3b82f6",
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(source, error, filename: nil) ⇒ ValidationErrorOverlay

Returns a new instance of ValidationErrorOverlay.

Parameters:

  • source (Object)
  • error (Object)
  • filename: (Object) (defaults to: nil)


20
21
22
23
24
25
# File 'lib/herb/engine/validation_error_overlay.rb', line 20

def initialize(source, error, filename: nil)
  @source = source
  @error = error
  @filename = filename || "unknown"
  @lines = source.lines
end

Instance Method Details

#apply_token_style(token, text) ⇒ Object

Parameters:

  • token (Object)
  • text (Object)

Returns:

  • (Object)


147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/herb/engine/validation_error_overlay.rb', line 147

def apply_token_style(token, text)
  escaped_text = escape_html(text)

  case token.type
  when "TOKEN_ERB_START", "TOKEN_ERB_END"
    "<span class=\"herb-erb\">#{escaped_text}</span>"
  when "TOKEN_ERB_CONTENT"
    "<span class=\"herb-erb-content\">#{escaped_text}</span>"
  when "TOKEN_HTML_TAG_START", "TOKEN_HTML_TAG_START_CLOSE", "TOKEN_HTML_TAG_END", "TOKEN_HTML_TAG_SELF_CLOSE", "TOKEN_IDENTIFIER"
    "<span class=\"herb-tag\">#{escaped_text}</span>"
  when "TOKEN_HTML_ATTRIBUTE_NAME"
    "<span class=\"herb-attr\">#{escaped_text}</span>"
  when "TOKEN_QUOTE", "TOKEN_HTML_ATTRIBUTE_VALUE"
    "<span class=\"herb-value\">#{escaped_text}</span>"
  when "TOKEN_HTML_COMMENT_START", "TOKEN_HTML_COMMENT_END", "TOKEN_HTML_COMMENT_CONTENT"
    "<span class=\"herb-comment\">#{escaped_text}</span>"
  else
    escaped_text
  end
end

#escape_attr(text) ⇒ Object

Parameters:

  • text (Object)

Returns:

  • (Object)


177
178
179
# File 'lib/herb/engine/validation_error_overlay.rb', line 177

def escape_attr(text)
  escape_html(text).gsub("\n", "&#10;").gsub("\r", "&#13;")
end

#escape_html(text) ⇒ Object

Parameters:

  • text (Object)

Returns:

  • (Object)


168
169
170
171
172
173
174
175
# File 'lib/herb/engine/validation_error_overlay.rb', line 168

def escape_html(text)
  text.to_s
      .gsub("&", "&amp;")
      .gsub("<", "&lt;")
      .gsub(">", "&gt;")
      .gsub('"', "&quot;")
      .gsub("'", "&#39;")
end

#generate_code_snippet(line_num, col_num) ⇒ Object

Parameters:

  • line_num (Object)
  • col_num (Object)

Returns:

  • (Object)


58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/herb/engine/validation_error_overlay.rb', line 58

def generate_code_snippet(line_num, col_num)
  start_line = [line_num - CONTEXT_LINES, 1].max
  end_line = [line_num + CONTEXT_LINES, @lines.length].min

  code_lines = [] #: Array[String]
  (start_line..end_line).each do |line|
    line_content = @lines[line - 1] || ""
    is_error_line = line == line_num

    highlighted_content = syntax_highlight(line_content.chomp)

    if is_error_line
      code_lines << <<~HTML
        <div class="herb-code-line herb-error-line">
          <div class="herb-line-number">#{line}</div>
          <div class="herb-line-content">#{highlighted_content}</div>
        </div>
      HTML

      if col_num.positive?
        pointer = "#{" " * (col_num - 1)}^"
        code_lines << <<~HTML
          <div class="herb-error-pointer">#{escape_html(pointer)}</div>
        HTML
      end
    else
      code_lines << <<~HTML
        <div class="herb-code-line">
          <div class="herb-line-number">#{line}</div>
          <div class="herb-line-content">#{highlighted_content}</div>
        </div>
      HTML
    end
  end

  <<~HTML
    <div class="herb-code-snippet">
      #{code_lines.join}
    </div>
  HTML
end

#generate_fragmentObject

Returns:

  • (Object)


27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/herb/engine/validation_error_overlay.rb', line 27

def generate_fragment
  location = @error[:location]
  line_num = location&.start&.line || 1
  col_num = location&.start&.column || 1

  validator_info = VALIDATOR_BADGES[@error[:source]] || { label: @error[:source], color: "#6b7280" }
  severity_color = SEVERITY_COLORS[@error[:severity].to_s] || "#6b7280"

  code_snippet = generate_code_snippet(line_num, col_num)

  <<~HTML
    <div class="herb-validation-item" data-severity="#{escape_attr(@error[:severity].to_s)}">
      <div class="herb-validation-header">
        <span class="herb-validation-badge" style="background: #{validator_info[:color]}">
          #{escape_html(validator_info[:label])}
        </span>
        <span class="herb-validation-location">
          #{escape_html(@filename)}:#{line_num}:#{col_num}
        </span>
      </div>
      <div class="herb-validation-message" style="color: #{severity_color}">
        #{escape_html(@error[:message])}
      </div>
      #{code_snippet}
      #{generate_suggestion_html if @error[:suggestion]}
    </div>
  HTML
end

#generate_suggestion_htmlObject

Returns:

  • (Object)


100
101
102
103
104
105
106
107
# File 'lib/herb/engine/validation_error_overlay.rb', line 100

def generate_suggestion_html
  <<~HTML
    <div class="herb-validation-suggestion">
      <span class="herb-suggestion-icon">💡</span>
      #{escape_html(@error[:suggestion])}
    </div>
  HTML
end

#get_character_offset(_content, line, column) ⇒ Object

Parameters:

  • _content (Object)
  • line (Object)
  • column (Object)

Returns:

  • (Object)


141
142
143
144
145
# File 'lib/herb/engine/validation_error_overlay.rb', line 141

def get_character_offset(_content, line, column)
  return column - 1 if line == 1

  column - 1
end

#highlight_with_tokens(tokens, code) ⇒ Object

Parameters:

  • tokens (Object)
  • code (Object)

Returns:

  • (Object)


119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/herb/engine/validation_error_overlay.rb', line 119

def highlight_with_tokens(tokens, code)
  return escape_html(code) if tokens.nil? || tokens.empty?

  highlighted = ""
  last_end = 0

  tokens.each do |token|
    char_offset = get_character_offset(code, token.location.start.line, token.location.start.column)
    char_end = get_character_offset(code, token.location.end_point.line, token.location.end_point.column)

    highlighted += escape_html(code[last_end...char_offset]) if char_offset > last_end

    token_text = code[char_offset...char_end]
    highlighted += apply_token_style(token, token_text)
    last_end = char_end
  end

  highlighted += escape_html(code[last_end..]) if last_end < code.length

  highlighted
end

#syntax_highlight(code) ⇒ Object

Parameters:

  • code (Object)

Returns:

  • (Object)


109
110
111
112
113
114
115
116
117
# File 'lib/herb/engine/validation_error_overlay.rb', line 109

def syntax_highlight(code)
  lex_result = ::Herb.lex(code)
  return escape_html(code) if lex_result.errors.any?

  tokens = lex_result.value
  highlight_with_tokens(tokens, code)
rescue StandardError
  escape_html(code)
end