Module: Jekyll::DiceTray::HtmlRewriter

Defined in:
lib/jekyll/dice_tray/html_rewriter.rb

Constant Summary collapse

DICE_RE =
/
  (?<![A-Za-z0-9_])
  (?:
    (?:\d{1,3})?d\d{1,4}
    (?:\s*[+-]\s*\d{1,5})?
    (?:
      \s*[+-]\s*(?:\d{1,3})?d\d{1,4}
      (?:\s*[+-]\s*\d{1,5})?
    )?
  )
  (?![A-Za-z0-9_])
/x
THAC0_WORD_RE =
/THAC0/i
BRACKET_INNER_RE =
/
  [+-]?
  \d{1,5}
  (?:\s*[+-]\s*\d{1,5})*
/x
THAC0_INLINE_RE =

“THAC0 18 [+1]” in one run of text — link the inner “+1”, roll d20+1.

/
  THAC0\s*:?\s*
  \d{1,2}
  \s*
  \[
  \s*
  (?<mod>#{BRACKET_INNER_RE})
  \s*
  \]
/ix
THAC0_VALUE_BRACKET_RE =

“18 [+1]” in a THAC0 table row or under a THAC0 column header.

/
  (?<!\d)
  \d{1,2}
  \s*
  \[
  \s*
  (?<mod>#{BRACKET_INNER_RE})
  \s*
  \]
/x
SKIP_ANCESTORS =
%w[pre code a script style textarea].freeze

Class Method Summary collapse

Class Method Details

.bracket_mod_hit(text, m) ⇒ Object



82
83
84
85
86
87
88
89
90
91
92
# File 'lib/jekyll/dice_tray/html_rewriter.rb', line 82

def self.bracket_mod_hit(text, m)
  {
    start: m.begin(0),
    end: m.end(0),
    expr: bracket_mod_to_roll_expr(m[:mod]),
    label_start: m.begin(:mod),
    label_end: m.end(:mod),
    prefix: text[m.begin(0)...m.begin(:mod)],
    suffix: text[m.end(:mod)...m.end(0)],
  }
end

.bracket_mod_to_roll_expr(inner) ⇒ Object



51
52
53
54
55
56
57
58
# File 'lib/jekyll/dice_tray/html_rewriter.rb', line 51

def self.bracket_mod_to_roll_expr(inner)
  compact = inner.gsub(/\s+/, "")
  mod = 0
  compact.scan(/[+-]?\d+/) { |term| mod += term.to_i }
  return "d20" if mod.zero?

  mod.positive? ? "d20+#{mod}" : "d20#{mod}"
end

.collect_roll_matches(text, node) ⇒ Object



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/jekyll/dice_tray/html_rewriter.rb', line 94

def self.collect_roll_matches(text, node)
  matches = []

  text.to_enum(:scan, DICE_RE).each do
    m = Regexp.last_match
    matches << {
      start: m.begin(0),
      end: m.end(0),
      expr: m[0],
      label_start: m.begin(0),
      label_end: m.end(0),
    }
  end

  text.to_enum(:scan, THAC0_INLINE_RE).each do
    matches << bracket_mod_hit(text, Regexp.last_match)
  end

  if thac0_bracket_context?(node)
    text.to_enum(:scan, THAC0_VALUE_BRACKET_RE).each do
      matches << bracket_mod_hit(text, Regexp.last_match)
    end
  end

  matches.sort_by! { |hit| hit[:start] }
  accepted = []
  matches.each do |hit|
    next if accepted.any? { |prev| hit[:start] < prev[:end] && hit[:end] > prev[:start] }

    accepted << hit
  end
  accepted
end

.inject_tray(html, assets_path:) ⇒ Object



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/jekyll/dice_tray/html_rewriter.rb', line 180

def self.inject_tray(html, assets_path:)
  return html if html.include?('data-dice-tray-root="true"')

  tray = <<~HTML
    <div id="jekyll-dice-tray" data-dice-tray-root="true" aria-live="polite">
      <div class="jdt-header">
        <button type="button" class="jdt-toggle" aria-expanded="false" title="Toggle dice tray">Dice</button>
      </div>
      <div class="jdt-body" hidden>
        <div class="jdt-clue">Type <code>1d20+5</code> or <code>/help</code>, then press Enter.</div>
        <div class="jdt-log" role="log" aria-label="Dice roll log"></div>
        <input class="jdt-input" type="text" inputmode="text" autocomplete="off" spellcheck="false"
          placeholder="Roll: 1d6, d4, 2d8+1, /help" />
      </div>
    </div>
  HTML

  tags = <<~HTML
    <link rel="stylesheet" href="#{assets_path}/dice_tray.css" />
    <script defer src="#{assets_path}/dice_tray.js"></script>
  HTML

  if html.include?("</body>")
    html.sub("</body>", "#{tray}\n#{tags}\n</body>")
  else
    "#{html}\n#{tray}\n#{tags}\n"
  end
end

.rewrite(html) ⇒ Object



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/jekyll/dice_tray/html_rewriter.rb', line 128

def self.rewrite(html)
  frag = Nokogiri::HTML::DocumentFragment.parse(html)

  frag.traverse do |node|
    next unless node.text?
    next if node.content.nil? || node.content.empty?
    next if node.ancestors.any? { |a| SKIP_ANCESTORS.include?(a.name) }

    text = node.content
    hits = collect_roll_matches(text, node)
    next if hits.empty?

    new_nodes = []
    last = 0

    hits.each do |hit|
      start_idx = hit[:start]
      end_idx = hit[:end]

      new_nodes << Nokogiri::XML::Text.new(text[last...start_idx], frag.document) if start_idx > last

      if hit[:prefix]
        new_nodes << Nokogiri::XML::Text.new(hit[:prefix], frag.document) if !hit[:prefix].empty?

        a = Nokogiri::XML::Node.new("a", frag.document)
        a["href"] = "#"
        a["class"] = "dice-tray-roll"
        a["data-dice"] = hit[:expr]
        a.content = text[hit[:label_start]...hit[:label_end]]
        new_nodes << a

        new_nodes << Nokogiri::XML::Text.new(hit[:suffix], frag.document) if !hit[:suffix].empty?
      else
        a = Nokogiri::XML::Node.new("a", frag.document)
        a["href"] = "#"
        a["class"] = "dice-tray-roll"
        a["data-dice"] = hit[:expr]
        a.content = text[hit[:label_start]...hit[:label_end]]
        new_nodes << a
      end

      last = end_idx
    end

    new_nodes << Nokogiri::XML::Text.new(text[last..], frag.document) if last < text.length

    node.replace(new_nodes.map(&:to_html).join)
  end

  frag.to_html
end

.thac0_bracket_context?(node) ⇒ Boolean

Returns:

  • (Boolean)


78
79
80
# File 'lib/jekyll/dice_tray/html_rewriter.rb', line 78

def self.thac0_bracket_context?(node)
  thac0_row_context?(node) || thac0_table_context?(node)
end

.thac0_row_context?(node) ⇒ Boolean

Returns:

  • (Boolean)


60
61
62
63
# File 'lib/jekyll/dice_tray/html_rewriter.rb', line 60

def self.thac0_row_context?(node)
  tr = node.ancestors.find { |a| a.element? && a.name == "tr" }
  tr && tr.text.match?(THAC0_WORD_RE)
end

.thac0_table_context?(node) ⇒ Boolean

Returns:

  • (Boolean)


65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/jekyll/dice_tray/html_rewriter.rb', line 65

def self.thac0_table_context?(node)
  return false unless node.ancestors.any? { |a| a.element? && a.name == "td" }

  table = node.ancestors.find { |a| a.element? && a.name == "table" }
  return false unless table

  caption = table.at_css("caption")
  return true if caption&.text&.match?(THAC0_WORD_RE)

  first_row = table.at_css("tr")
  first_row && first_row.text.match?(THAC0_WORD_RE)
end