Module: JekyllMermaidPrebuild::EmojiCompensator

Defined in:
lib/jekyll-mermaid-prebuild/emoji_compensator.rb

Overview

Stateless module: compensates for headless Chromium emoji width undermeasurement by appending   padding to emoji-containing node labels before mmdc renders.

Constant Summary collapse

EMOJI_RE =

Match a single Extended_Pictographic codepoint (emoji).

/\p{Extended_Pictographic}/
BR_RE =
%r{(<br\s*/?>)}i
NBSP =
"&nbsp;"

Class Method Summary collapse

Class Method Details

.compensate(mermaid_source, diagram_type) ⇒ String

If diagram_type is enabled for compensation, pad emoji-containing labels with non-breaking spaces (2 per emoji). Otherwise return source unchanged.

Parameters:

  • mermaid_source (String)

    Mermaid diagram source

  • diagram_type (String)

    result of detect_diagram_type

Returns:

  • (String)

    possibly modified source



44
45
46
47
48
# File 'lib/jekyll-mermaid-prebuild/emoji_compensator.rb', line 44

def self.compensate(mermaid_source, diagram_type)
  return compensate_flowchart_labels(mermaid_source) if diagram_type == "flowchart"

  mermaid_source
end

.compensate_flowchart_labels(source) ⇒ Object



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/jekyll-mermaid-prebuild/emoji_compensator.rb', line 93

def self.compensate_flowchart_labels(source)
  result = source

  [
    [/\["(.+?)"\]/m, '["', '"]'],
    [/\['(.+?)'\]/m, "['", "']"],
    [/\("(.+?)"\)/m, '("', '")'],
    [/\{"(.+?)"\}/m, '{"', '"}']
  ].each do |regex, open_str, close_str|
    result = result.gsub(regex) do
      "#{open_str}#{pad_label_content(Regexp.last_match(1))}#{close_str}"
    end
  end

  result.gsub(%r{\[/"((?:[^"\\]|\\.)+?)"/\]}) do
    "[/\"#{pad_label_content(Regexp.last_match(1))}\"/]"
  end
end

.count_emoji(text) ⇒ Integer

Count Extended_Pictographic codepoints in string.

Parameters:

  • text (String)

Returns:

  • (Integer)


57
58
59
# File 'lib/jekyll-mermaid-prebuild/emoji_compensator.rb', line 57

def self.count_emoji(text)
  text.scan(EMOJI_RE).length
end

.detect_diagram_type(mermaid_source) ⇒ String?

Detect diagram type from Mermaid source. Skips frontmatter (---), %% comments, and blank lines. Returns the first token of the first content line; normalizes "graph" to "flowchart". Returns nil if no diagram type line found.

Parameters:

  • mermaid_source (String)

    raw Mermaid diagram source

Returns:

  • (String, nil)

    diagram type keyword (e.g. "flowchart", "sequenceDiagram") or nil



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/jekyll-mermaid-prebuild/emoji_compensator.rb', line 16

def self.detect_diagram_type(mermaid_source)
  return nil unless mermaid_source

  in_frontmatter = false

  mermaid_source.each_line do |line|
    stripped = line.strip
    if stripped == "---"
      in_frontmatter = !in_frontmatter
      next
    end
    next if in_frontmatter
    next if stripped.empty?
    next if stripped.start_with?("%%")

    token = stripped[/\A\S+/]
    return token == "graph" ? "flowchart" : token
  end

  nil
end

.pad_label_content(content) ⇒ String

Pad the longest line in a label if it contains emoji. Splits on
variants, finds the visually longest line, and only pads that line (shorter lines center naturally in the wider container). When no padding is applied, returns the same String object (identity), not merely an equal copy.

Parameters:

  • content (String)

    raw label text (may contain
    line breaks)

Returns:

  • (String)

    possibly padded label text



78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/jekyll-mermaid-prebuild/emoji_compensator.rb', line 78

def self.pad_label_content(content)
  segments = content.split(BR_RE)
  # Even indices are text lines; odd indices are the captured <br> delimiters.
  line_pairs = segments.each_with_index.select { |_segment, index| index.even? }
  return content if line_pairs.empty?

  # max_by keeps the first of equal-length ties — pad that one index only.
  longest_line, longest_idx = line_pairs.max_by { |segment, _index| visual_length(segment) }
  emoji_count = count_emoji(longest_line)
  return content unless emoji_count.positive?

  segments[longest_idx] = "#{longest_line}#{NBSP * (emoji_count * 2)}"
  segments.join
end

.visual_length(text) ⇒ Integer

Visual length for longest-line comparison. Each emoji counts as 2 because emoji glyphs render roughly double the width of a regular character.

Parameters:

  • text (String)

Returns:

  • (Integer)


66
67
68
# File 'lib/jekyll-mermaid-prebuild/emoji_compensator.rb', line 66

def self.visual_length(text)
  text.length + count_emoji(text)
end