Module: Ibex::Codegen::HTML

Defined in:
lib/ibex/codegen/html.rb,
sig/ibex/codegen/html.rbs

Overview

Renders a self-contained navigable automaton report.

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.conflict_sections(automaton, grammar, labels) ⇒ Object



164
165
166
167
168
169
170
171
172
173
174
# File 'lib/ibex/codegen/html.rb', line 164

def conflict_sections(automaton, grammar, labels)
  conflicts = automaton.states.flat_map do |state|
    state.conflicts.map do |conflict|
      link = "<a href=\"#state-#{state.id}\">state #{state.id}</a>"
      symbol = grammar.symbol(conflict[:symbol])
      displayed = symbol ? conflict.merge(symbol: symbol_name(labels, symbol.id)) : conflict
      "<li class=\"conflict\">#{link}: #{escape(displayed.inspect)}</li>"
    end
  end
  conflicts.empty? ? "<p>None</p>" : "<ul>#{conflicts.join}</ul>"
end

.controls(automaton) ⇒ Object



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/ibex/codegen/html.rb', line 99

def controls(automaton)
  options = automaton.states.filter_map do |state|
    next if state.conflicts.empty?

    %(<option value="#{state.id}">State #{state.id}</option>)
  end.join
  <<~HTML
    <div class="controls">
    <label>Search states <input id="state-search" type="search" aria-controls="states"></label>
    <label><input id="conflict-only" type="checkbox"> Conflicts only</label>
    <label>Conflict neighborhood
    <select id="conflict-neighborhood"><option value="">All states</option>#{options}</select></label>
    </div>
  HTML
end

.escape(value) ⇒ Object



177
178
179
# File 'lib/ibex/codegen/html.rb', line 177

def escape(value)
  value.to_s.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;").gsub('"', "&quot;")
end

.interaction_scriptObject



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
# File 'lib/ibex/codegen/html.rb', line 72

def interaction_script
  <<~HTML
    <script>
    (() => {
      const search = document.getElementById("state-search");
      const conflictOnly = document.getElementById("conflict-only");
      const neighborhood = document.getElementById("conflict-neighborhood");
      const states = Array.from(document.querySelectorAll(".state"));
      const update = () => {
        const query = search.value.trim().toLowerCase();
        const focus = neighborhood.value;
        states.forEach((state) => {
          const matchesText = !query || state.textContent.toLowerCase().includes(query);
          const matchesConflict = !conflictOnly.checked || state.classList.contains("conflict-state");
          const nearby = !focus || state.dataset.neighbors.split(" ").includes(focus);
          state.hidden = !(matchesText && matchesConflict && nearby);
        });
      };
      search.addEventListener("input", update);
      conflictOnly.addEventListener("change", update);
      neighborhood.addEventListener("change", update);
    })();
    </script>
  HTML
end

.item_html(item, grammar, labels) ⇒ Object



143
144
145
146
147
148
149
150
151
# File 'lib/ibex/codegen/html.rb', line 143

def item_html(item, grammar, labels)
  return "<li><code>$accept</code></li>" if item.production.negative?

  production = grammar.productions.fetch(item.production)
  lhs = symbol_name(labels, production.lhs)
  rhs = production.rhs.map { |id| symbol_name(labels, id) }.insert(item.dot, "").join(" ")
  link = "<a href=\"#rule-#{production.id}\">rule #{production.id}</a>"
  "<li>#{link} <code>#{escape(lhs)}#{escape(rhs)}</code></li>"
end

.one_hop_neighbors(automaton, state_id) ⇒ Object



134
135
136
137
138
139
140
# File 'lib/ibex/codegen/html.rb', line 134

def one_hop_neighbors(automaton, state_id)
  outgoing = automaton.states.fetch(state_id).transitions.values
  incoming = automaton.states.filter_map do |state|
    state.id if state.transitions.value?(state_id)
  end
  ([state_id] + outgoing + incoming).uniq.sort
end

.render(automaton) ⇒ Object

RBS:

  • (IR::Automaton automaton) -> String



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/ibex/codegen/html.rb', line 37

def render(automaton)
  grammar = automaton.grammar
  labels = SymbolLabels.build(grammar)
  <<~HTML
    <!doctype html>
    <html lang="en"><head><meta charset="utf-8"><title>Ibex automaton</title>
    #{styles}
    </head><body><h1>Ibex #{escape(automaton.algorithm)} automaton</h1>
    <nav><a href="#rules">Rules</a> · <a href="#conflicts">Conflicts</a></nav>
    #{controls(automaton)}
    <main id="states">#{state_sections(automaton, grammar, labels)}</main>
    <h2 id="rules">Rules</h2>#{rule_sections(grammar, labels)}
    <h2 id="conflicts">Conflicts</h2>#{conflict_sections(automaton, grammar, labels)}
    #{interaction_script}
    </body></html>
  HTML
end

.rule_sections(grammar, labels) ⇒ Object



154
155
156
157
158
159
160
161
# File 'lib/ibex/codegen/html.rb', line 154

def rule_sections(grammar, labels)
  grammar.productions.map do |production|
    lhs = symbol_name(labels, production.lhs)
    rhs = production.rhs.map { |id| symbol_name(labels, id) }.join(" ")
    number = "<strong>#{production.id}</strong>"
    "<p id=\"rule-#{production.id}\">#{number} <code>#{escape(lhs)}#{escape(rhs)}</code></p>"
  end.join
end

.state_sections(automaton, grammar, labels) ⇒ Object



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/ibex/codegen/html.rb', line 116

def state_sections(automaton, grammar, labels)
  automaton.states.map do |state|
    transitions = state.transitions.map do |symbol_id, target|
      symbol = escape(symbol_name(labels, symbol_id))
      "<li>#{symbol} → <a href=\"#state-#{target}\">state #{target}</a></li>"
    end.join
    items = state.items.map { |item| item_html(item, grammar, labels) }.join
    class_name = state.conflicts.empty? ? "state" : "state conflict-state"
    neighbors = one_hop_neighbors(automaton, state.id).join(" ")
    <<~HTML
      <section class="#{class_name}" id="state-#{state.id}" data-state-id="#{state.id}" data-neighbors="#{neighbors}">
      <h2>State #{state.id}</h2>
      <ul>#{transitions}</ul><ul>#{items}</ul></section>
    HTML
  end.join
end

.stylesObject



60
61
62
63
64
65
66
67
68
69
# File 'lib/ibex/codegen/html.rb', line 60

def styles
  <<~HTML
    <style>
    body{font:14px system-ui;margin:2rem;max-width:90rem}code{white-space:pre-wrap}
    .controls{align-items:center;display:flex;flex-wrap:wrap;gap:1rem;margin:1rem 0}
    .state{border:1px solid #bbb;padding:1rem;margin:1rem 0}.state[hidden]{display:none}
    .conflict-state{background:#fff7f7;border:2px solid #b91c1c}.conflict{color:#a00}a{color:#075985}
    </style>
  HTML
end

.symbol_name(labels, id) ⇒ Object



182
183
184
# File 'lib/ibex/codegen/html.rb', line 182

def symbol_name(labels, id)
  labels.fetch(id) { raise Ibex::Error, "missing grammar symbol id #{id}" }
end

Instance Method Details

#self?.renderString

RBS:

  • (IR::Automaton automaton) -> String

Parameters:

Returns:

  • (String)


48
# File 'sig/ibex/codegen/html.rbs', line 48

def self?.render: (IR::Automaton automaton) -> String