Class: JekyllMermaidPrebuild::Processor

Inherits:
Object
  • Object
show all
Defined in:
lib/jekyll-mermaid-prebuild/processor.rb

Overview

Processes document/page content, replacing mermaid blocks with SVG references

Constant Summary collapse

FENCE_OPENER =

Pattern to detect any fence opener (captures fence chars and optional language)

/^(`{3,}|~{3,})(\w*)/

Instance Method Summary collapse

Constructor Details

#initialize(config, generator) ⇒ Processor

Initialize processor

Parameters:



13
14
15
16
# File 'lib/jekyll-mermaid-prebuild/processor.rb', line 13

def initialize(config, generator)
  @config = config
  @generator = generator
end

Instance Method Details

#convert_block(block) ⇒ Hash?

Convert a single mermaid block to SVG

Parameters:

  • block (Hash)

    block info with :content key

Returns:

  • (Hash, nil)

    :html or nil if failed



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/jekyll-mermaid-prebuild/processor.rb', line 67

def convert_block(block)
  mermaid_source = block[:content]
  diagram_type = EmojiCompensator.detect_diagram_type(mermaid_source)
  source_for_render = if @config.emoji_width_compensation[diagram_type]
                        EmojiCompensator.compensate(mermaid_source, diagram_type)
                      else
                        mermaid_source
                      end
  digest_input = digest_string_for_cache(source_for_render)
  cache_key = DigestCalculator.content_digest(digest_input)
  paths = @generator.generate(source_for_render, cache_key)

  return nil if paths.nil? || paths.empty?

  light_url = @generator.build_svg_url(cache_key)
  html = if @config.prefers_color_scheme == :auto
           dark_url = @generator.build_svg_url("#{cache_key}-dark")
           @generator.build_figure_html(light_url, dark_url: dark_url)
         else
           @generator.build_figure_html(light_url)
         end

  { svgs: paths, html: html }
end

#digest_string_for_cache(source) ⇒ String

Returns input to MD5 for cache key.

Parameters:

  • source (String)

    mermaid passed to mmdc (after optional emoji compensation)

Returns:

  • (String)

    input to MD5 for cache key



51
52
53
54
55
56
57
58
59
60
61
# File 'lib/jekyll-mermaid-prebuild/processor.rb', line 51

def digest_string_for_cache(source)
  parts = [source]
  parts << "pcs=#{@config.prefers_color_scheme}"
  parts << "bgL=#{@config.chart_background_light}"
  parts << "bgD=#{@config.chart_background_dark}"
  parts << "tc=#{@config.text_centering}" unless @config.text_centering
  parts << "op=#{@config.overflow_protection}" unless @config.overflow_protection
  pad = @config.edge_label_padding
  parts << "edge_pad=#{pad}" if pad.is_a?(Numeric) && pad.positive?
  parts.join("\0")
end

#find_top_level_mermaid_blocks(content) ⇒ Array<Hash>

Find all top-level mermaid code blocks (not nested inside other fences)

Parameters:

  • content (String)

    markdown content

Returns:

  • (Array<Hash>)

    array of end:, content: for each top-level mermaid block



96
97
98
99
100
101
102
103
104
105
# File 'lib/jekyll-mermaid-prebuild/processor.rb', line 96

def find_top_level_mermaid_blocks(content)
  blocks = []
  state = { blocks: blocks, fence_stack: [], position: 0 }

  content.lines do |line|
    process_line(line, state)
  end

  blocks
end

#handle_fence_line(line, line_start, match, state) ⇒ Object

Handle a line that matches a fence pattern



122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/jekyll-mermaid-prebuild/processor.rb', line 122

def handle_fence_line(line, line_start, match, state)
  fence_chars = match[1]
  language = match[2]
  fence_type = fence_chars.include?("~") ? "~" : "`"
  fence_length = fence_chars.length

  if state[:current_mermaid]
    handle_line_in_mermaid(line, fence_chars, fence_type, fence_length, state)
  elsif state[:fence_stack].empty?
    handle_line_at_top_level(line_start, language, fence_type, fence_length, state)
  else
    handle_line_in_nested_fence(line, fence_type, fence_length, state)
  end
end

#handle_line_at_top_level(line_start, language, fence_type, fence_length, state) ⇒ Object

Handle fence line at top level (not inside any fence)



152
153
154
155
156
157
158
159
# File 'lib/jekyll-mermaid-prebuild/processor.rb', line 152

def handle_line_at_top_level(line_start, language, fence_type, fence_length, state)
  if language == "mermaid"
    state[:current_mermaid] = { start: line_start, fence_type: fence_type,
                                fence_length: fence_length, content_lines: [] }
  else
    state[:fence_stack].push([fence_length, fence_type])
  end
end

#handle_line_in_mermaid(line, fence_chars, fence_type, fence_length, state) ⇒ Object

Handle fence line while inside a mermaid block



138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/jekyll-mermaid-prebuild/processor.rb', line 138

def handle_line_in_mermaid(line, fence_chars, fence_type, fence_length, state)
  cm = state[:current_mermaid]
  blocks = state[:blocks]
  position = state[:position]
  content_lines = cm[:content_lines]
  if fence_type == cm[:fence_type] && fence_length == cm[:fence_length] && line.strip == fence_chars
    blocks << { start: cm[:start], end: position, content: content_lines.join }
    state[:current_mermaid] = nil
  else
    content_lines << line
  end
end

#handle_line_in_nested_fence(line, fence_type, fence_length, state) ⇒ Object

Handle fence line while inside a non-mermaid fence



162
163
164
165
166
167
168
169
170
171
# File 'lib/jekyll-mermaid-prebuild/processor.rb', line 162

def handle_line_in_nested_fence(line, fence_type, fence_length, state)
  fence_stack = state[:fence_stack]
  top_fence_length, top_fence_type = fence_stack.last

  if fence_type == top_fence_type && fence_length >= top_fence_length && line.strip.match?(/\A[`~]+\z/)
    fence_stack.pop
  else
    fence_stack.push([fence_length, fence_type])
  end
end

#process_content(content, _site = nil) ⇒ Array<String, Integer, Hash>

Process content, replacing mermaid code blocks with figure HTML Only processes top-level mermaid blocks (not nested inside other fences)

Parameters:

  • content (String)

    markdown content

  • _site (Jekyll::Site) (defaults to: nil)

    the Jekyll site (unused, kept for API compatibility)

Returns:

  • (Array<String, Integer, Hash>)

    [processed_content, count, svgs_to_copy]



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/jekyll-mermaid-prebuild/processor.rb', line 24

def process_content(content, _site = nil)
  return [nil, 0, {}] unless content

  converted_count = 0
  svgs_to_copy = {}

  # Find top-level mermaid blocks respecting fence nesting
  top_level_blocks = find_top_level_mermaid_blocks(content)

  # Process blocks in reverse order to preserve string positions
  processed = content.dup
  top_level_blocks.reverse_each do |block|
    result = convert_block(block)
    next unless result

    result => { svgs:, html: }
    block => { start:, end: block_end }
    converted_count += 1
    svgs_to_copy.merge!(svgs)
    processed[start...block_end] = html
  end

  [processed, converted_count, svgs_to_copy]
end

#process_line(line, state) ⇒ Object

Process a single line for fence detection



108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/jekyll-mermaid-prebuild/processor.rb', line 108

def process_line(line, state)
  line_start = state[:position]
  state[:position] += line.length

  match = line.match(FENCE_OPENER)
  if match
    handle_fence_line(line, line_start, match, state)
  elsif (current_mermaid = state[:current_mermaid])
    content_lines = current_mermaid[:content_lines]
    content_lines << line
  end
end