Module: Html::Merge

Defined in:
lib/html/merge.rb,
lib/html/merge/version.rb,
lib/html/merge/crispr_adapter.rb,
sig/html/merge.rbs

Defined Under Namespace

Modules: Version Classes: CrisprAdapter, Error, ParseError

Constant Summary collapse

PACKAGE_NAME =
"html-merge"
BACKEND_REFERENCE =
TreeHaver::KREUZBERG_LANGUAGE_PACK_BACKEND
BACKEND_REGISTRY =
Struct.new(:registered, :mutex).new(false, Mutex.new)
VERSION =

Current gem version exposed at the traditional constant location.

Returns:

  • (String)
Version::VERSION

Class Method Summary collapse

Class Method Details

.available_html_backendsArray[untyped]

Returns:

  • (Array[untyped])


43
44
45
# File 'lib/html/merge.rb', line 43

def available_html_backends
  html_backend_available_for_analysis?(BACKEND_REFERENCE.id) ? [BACKEND_REFERENCE] : []
end

.document_context(content:, source_label: "source") ⇒ Object

Parameters:

  • content: (String)
  • source_label: (String) (defaults to: "source")

Returns:

  • (Object)


207
208
209
210
211
212
213
214
215
# File 'lib/html/merge.rb', line 207

def document_context(content:, source_label: "source")
  require "ast/crispr"

  Ast::Crispr::DocumentContext.new(
    content: content,
    source_label: source_label,
    adapter: CrisprAdapter.new
  )
end

.element_text_selector(text:, id: nil) ⇒ Object

Parameters:

  • text: (String)
  • id: (String, nil) (defaults to: nil)

Returns:

  • (Object)


217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/html/merge.rb', line 217

def element_text_selector(text:, id: nil)
  require "ast/crispr"

  Ast::Crispr::Selectors.owner_filter(
    id: id || "html_element_text:#{text}",
    adapter: CrisprAdapter.new,
    owner_scope: :html_element
  ) do |_context, owner|
    owner.type == "element" &&
      owner.text.include?(text.to_s) &&
      !html_descendant_element_text?(owner.node, text.to_s)
  end
end

.ensure_yard_content_wrapper(source) ⇒ String

Parameters:

  • source (String)

Returns:

  • (String)


240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/html/merge.rb', line 240

def ensure_yard_content_wrapper(source)
  require "ast/crispr"

  replacement = %(<div id="content"><h1 class="noborder title">Documentation by YARD</h1></div>\n)
  target = element_text_selector(text: "Documentation by YARD", id: "yard_title_element")
  actor = Ast::Crispr::Replace.result(content: source.to_s, target: target, replacement: replacement)
  if actor.failure?
    raise Error, actor.respond_to?(:error) ? actor.error.to_s : "HTML CRISPR replacement failed"
  end

  actor.updated_content
end

.html_backend_available_for_analysis?(backend_id) ⇒ Boolean

Returns:

  • (Boolean)


183
184
185
186
187
188
189
# File 'lib/html/merge.rb', line 183

def html_backend_available_for_analysis?(backend_id)
  requested = requested_html_backend_id(backend_id)
  register_backend!
  TreeHaver.with_backend(requested) { TreeHaver.parser_for(:html).respond_to?(:parse) }
rescue TreeHaver::Error, StandardError
  false
end

.html_backend_feature_profile(backend: nil) ⇒ Hash[Symbol, untyped]

Parameters:

  • backend: (Object) (defaults to: nil)

Returns:

  • (Hash[Symbol, untyped])


47
48
49
50
51
52
53
54
55
56
57
# File 'lib/html/merge.rb', line 47

def html_backend_feature_profile(backend: nil)
  requested = requested_html_backend_id(backend)
  unless available_html_backends.any? { |backend_ref| backend_ref.id == requested }
    return unsupported_feature_result("Unsupported HTML backend #{requested}.")
  end

  html_feature_profile.merge(
    backend: requested,
    backend_ref: BACKEND_REFERENCE.to_h
  )
end

.html_descendant_element_text?(node, text) ⇒ Boolean

Returns:

  • (Boolean)


231
232
233
234
235
236
237
238
# File 'lib/html/merge.rb', line 231

def html_descendant_element_text?(node, text)
  node.child_count.times.any? do |index|
    child = node.child(index)
    next false unless child

    (child.type == "element" && child.text.include?(text)) || html_descendant_element_text?(child, text)
  end
end

.html_feature_profileHash[Symbol, untyped]

Returns:

  • (Hash[Symbol, untyped])


35
36
37
38
39
40
41
# File 'lib/html/merge.rb', line 35

def html_feature_profile
  {
    family: "html",
    supported_dialects: ["html"],
    supported_policies: []
  }
end

.html_node_span(node) ⇒ Object



152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/html/merge.rb', line 152

def html_node_span(node)
  start_point = html_point(html_raw_point(node, :start))
  end_point = html_point(html_raw_point(node, :end))
  {
    start_byte: node.start_byte,
    end_byte: node.end_byte,
    start_line: start_point.fetch(:row) + 1,
    end_line: end_point.fetch(:row) + 1,
    start_column: start_point.fetch(:column),
    end_column: end_point.fetch(:column)
  }
end

.html_node_summaries(root) ⇒ Object



122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/html/merge.rb', line 122

def html_node_summaries(root)
  nodes = []
  visit_html_node(root) do |node|
    next unless node.structural?

    nodes << {
      type: node.type,
      span: html_node_span(node),
      text: node.text.to_s.strip[0, 120]
    }
  end
  nodes
end

.html_parse_error_nodes(root) ⇒ Object



136
137
138
139
140
141
142
# File 'lib/html/merge.rb', line 136

def html_parse_error_nodes(root)
  errors = []
  visit_html_node(root) do |node|
    errors << node if node.has_error? || node.type == "ERROR"
  end
  errors
end

.html_plan_context(backend: nil) ⇒ Hash[Symbol, untyped]

Parameters:

  • backend: (Object) (defaults to: nil)

Returns:

  • (Hash[Symbol, untyped])


59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/html/merge.rb', line 59

def html_plan_context(backend: nil)
  profile = html_backend_feature_profile(backend: backend)
  return profile if profile[:ok] == false

  {
    family_profile: html_feature_profile,
    feature_profile: {
      backend: profile[:backend],
      supports_dialects: false,
      supported_policies: profile[:supported_policies]
    }
  }
end

.html_point(point) ⇒ Object

Raises:



175
176
177
178
179
180
181
# File 'lib/html/merge.rb', line 175

def html_point(point)
  return { row: point.row, column: point.column } if point.respond_to?(:row) && point.respond_to?(:column)

  return { row: point[:row], column: point[:column] } if point.respond_to?(:[])

  raise Error, "Unsupported TreeHaver point #{point.inspect}"
end

.html_raw_point(node, boundary) ⇒ Object



165
166
167
168
169
170
171
172
173
# File 'lib/html/merge.rb', line 165

def html_raw_point(node, boundary)
  raw_node = node.respond_to?(:inner_node) ? node.inner_node : node
  methods = boundary == :start ? %i[start_point start_position] : %i[end_point end_position]
  methods.each do |method_name|
    return raw_node.public_send(method_name) if raw_node.respond_to?(method_name)
  end

  node.public_send(methods.first)
end

.html_root_node(source, backend: nil) ⇒ Object



116
117
118
119
120
# File 'lib/html/merge.rb', line 116

def html_root_node(source, backend: nil)
  requested = requested_html_backend_id(backend)
  register_backend!
  TreeHaver.with_backend(requested) { TreeHaver.parser_for(:html).parse(source.to_s).root_node }
end

.parse_html(source, dialect = "html", backend: nil) ⇒ Hash[Symbol, untyped]

Parameters:

  • source (String)
  • dialect (String) (defaults to: "html")
  • backend: (Object) (defaults to: nil)

Returns:

  • (Hash[Symbol, untyped])


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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/html/merge.rb', line 73

def parse_html(source, dialect = "html", backend: nil)
  return unsupported_feature_result("Unsupported HTML dialect #{dialect}.") unless dialect.to_s == "html"

  requested = requested_html_backend_id(backend)
  unless available_html_backends.any? { |backend_ref| backend_ref.id == requested }
    return unsupported_feature_result("Unsupported HTML backend #{requested}.")
  end

  register_backend!
  tree = TreeHaver.with_backend(requested) { TreeHaver.parser_for(:html).parse(source.to_s) }
  root = tree.root_node
  diagnostics = html_parse_error_nodes(root).map do |node|
    {
      severity: "error",
      category: "parse_error",
      message: "HTML parse error in #{node.type}",
      span: html_node_span(node)
    }
  end
  return { ok: false, diagnostics: diagnostics, policies: [] } unless diagnostics.empty?

  {
    ok: true,
    diagnostics: [],
    analysis: {
      dialect: "html",
      root_kind: root.type,
      nodes: html_node_summaries(root)
    },
    policies: []
  }
rescue TreeHaver::Error, StandardError => e
  {
    ok: false,
    diagnostics: [{
      severity: "error",
      category: "parse_error",
      message: e.message
    }],
    policies: []
  }
end

.register_backend!nil

Returns:

  • (nil)


22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/html/merge.rb', line 22

def register_backend!
  BACKEND_REGISTRY.mutex.synchronize do
    return if BACKEND_REGISTRY.registered

    TreeHaver::BackendRegistry.register(BACKEND_REFERENCE)

    grammar_finder = TreeHaver::GrammarFinder.new(:html)
    grammar_finder.register! if grammar_finder.available?

    BACKEND_REGISTRY.registered = true
  end
end

.requested_html_backend_id(backend) ⇒ Object



191
192
193
# File 'lib/html/merge.rb', line 191

def requested_html_backend_id(backend)
  backend.to_s.empty? ? BACKEND_REFERENCE.id : backend.to_s
end

.unsupported_feature_result(message) ⇒ Object



195
196
197
198
199
200
201
202
203
204
205
# File 'lib/html/merge.rb', line 195

def unsupported_feature_result(message)
  {
    ok: false,
    diagnostics: [{
      severity: "error",
      category: "unsupported_feature",
      message: message
    }],
    policies: []
  }
end

.visit_html_node(node) {|node| ... } ⇒ Object

Yields:

  • (node)


144
145
146
147
148
149
150
# File 'lib/html/merge.rb', line 144

def visit_html_node(node, &block)
  yield node
  node.child_count.times do |index|
    child = node.child(index)
    visit_html_node(child, &block) if child
  end
end