jekyll-mermaid-prebuild 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/jekyll-mermaid-prebuild.gemspec +4 -2
- data/lib/jekyll-mermaid-prebuild/configuration.rb +16 -37
- data/lib/jekyll-mermaid-prebuild/digest_calculator.rb +1 -3
- data/lib/jekyll-mermaid-prebuild/emoji_compensator.rb +31 -37
- data/lib/jekyll-mermaid-prebuild/generator.rb +12 -14
- data/lib/jekyll-mermaid-prebuild/hooks.rb +92 -94
- data/lib/jekyll-mermaid-prebuild/mmdc_wrapper.rb +31 -31
- data/lib/jekyll-mermaid-prebuild/processor.rb +27 -22
- data/lib/jekyll-mermaid-prebuild/svg_post_processor.rb +7 -15
- data/lib/jekyll-mermaid-prebuild/version.rb +1 -1
- metadata +33 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1a6f44826b0694bbba4cea5d5b0272301e8909003a9b182eb340042c80454ebb
|
|
4
|
+
data.tar.gz: 9f632d879d948c8030edc5365bc06bc75bd36809da017fd35c6f20ee5b252b0a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 88b628146a89abbc660bda67b254653a1e77b74b63d55c4ba882a2abe600804129fe29405df0a720457bfbbcbf19b14e7d7841fea322835274deffdcd21c3e67
|
|
7
|
+
data.tar.gz: 3ce40596559ad01778c875cf599b693f42ee1477c70ab178f0f94ff4e735bbe1e1392d68430f2c3985b61ca82b1d8851f9e292715a501a10faf1c640d9dd001e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.0](https://github.com/Texarkanine/jekyll-mermaid-prebuild/compare/v0.5.0...v0.6.0) (2026-07-19)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* **test:** add Mutant mutation testing with 100% kill ([#44](https://github.com/Texarkanine/jekyll-mermaid-prebuild/issues/44))
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* **test:** add Mutant mutation testing with 100% kill ([#44](https://github.com/Texarkanine/jekyll-mermaid-prebuild/issues/44)) ([e86a82a](https://github.com/Texarkanine/jekyll-mermaid-prebuild/commit/e86a82a6eba5dc0fc037ff0f3451d12708a4f629))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **deps-dev:** migrate SimpleCov add_filter to skip ([#41](https://github.com/Texarkanine/jekyll-mermaid-prebuild/issues/41)) ([#42](https://github.com/Texarkanine/jekyll-mermaid-prebuild/issues/42)) ([bc068f0](https://github.com/Texarkanine/jekyll-mermaid-prebuild/commit/bc068f0268e2d974a061a1474d0973306d860f8e))
|
|
18
|
+
|
|
3
19
|
## [0.5.0](https://github.com/Texarkanine/jekyll-mermaid-prebuild/compare/v0.4.0...v0.5.0) (2026-03-22)
|
|
4
20
|
|
|
5
21
|
|
|
@@ -35,11 +35,13 @@ Gem::Specification.new do |spec|
|
|
|
35
35
|
spec.add_dependency "jekyll", ">= 4.0", "< 5.0"
|
|
36
36
|
|
|
37
37
|
# Development dependencies
|
|
38
|
+
spec.add_development_dependency "mutant", "~> 0.16"
|
|
39
|
+
spec.add_development_dependency "mutant-rspec", "~> 0.16"
|
|
38
40
|
spec.add_development_dependency "rake", "~> 13.3"
|
|
39
41
|
spec.add_development_dependency "rspec", "~> 3.13"
|
|
40
42
|
spec.add_development_dependency "rubocop", "~> 1.81"
|
|
41
43
|
spec.add_development_dependency "rubocop-rake", "~> 0.7"
|
|
42
44
|
spec.add_development_dependency "rubocop-rspec", "~> 3.8"
|
|
43
|
-
spec.add_development_dependency "simplecov", "~> 0
|
|
44
|
-
spec.add_development_dependency "simplecov-cobertura", "~>
|
|
45
|
+
spec.add_development_dependency "simplecov", "~> 1.0"
|
|
46
|
+
spec.add_development_dependency "simplecov-cobertura", "~> 4.0"
|
|
45
47
|
end
|
|
@@ -50,8 +50,6 @@ module JekyllMermaidPrebuild
|
|
|
50
50
|
CACHE_DIR
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
-
private
|
|
54
|
-
|
|
55
53
|
# Parse the prefers-color-scheme block (see PREFERS_COLOR_SCHEME_YAML_KEY) from a Hash only
|
|
56
54
|
# (mode + optional background-color map). Non-Hash values fall back to :light and default
|
|
57
55
|
# backgrounds with a warning.
|
|
@@ -59,40 +57,27 @@ module JekyllMermaidPrebuild
|
|
|
59
57
|
# @param value [Object] raw site config value
|
|
60
58
|
# @return [void]
|
|
61
59
|
def parse_prefers_color_scheme(value)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"MermaidPrebuild:",
|
|
69
|
-
"Invalid #{PREFERS_COLOR_SCHEME_YAML_KEY} (expected a Hash); " \
|
|
70
|
-
"using light mode and default backgrounds"
|
|
71
|
-
)
|
|
72
|
-
end
|
|
73
|
-
return
|
|
60
|
+
if !value.nil? && !value.is_a?(Hash)
|
|
61
|
+
Jekyll.logger.warn(
|
|
62
|
+
"MermaidPrebuild:",
|
|
63
|
+
"Invalid #{PREFERS_COLOR_SCHEME_YAML_KEY} (expected a Hash); " \
|
|
64
|
+
"using light mode and default backgrounds"
|
|
65
|
+
)
|
|
74
66
|
end
|
|
75
67
|
|
|
76
68
|
mode_raw = config_hash_fetch(value, "mode")
|
|
77
69
|
@prefers_color_scheme = normalize_prefers_mode(mode_raw)
|
|
78
70
|
|
|
79
71
|
bg_container = config_hash_fetch(value, BACKGROUND_COLOR_YAML_KEY)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
@chart_background_dark = coerce_chart_background(dark_raw, DEFAULT_CHART_BG_DARK, "dark")
|
|
85
|
-
else
|
|
86
|
-
@chart_background_light = finalize_background(DEFAULT_CHART_BG_LIGHT)
|
|
87
|
-
@chart_background_dark = finalize_background(DEFAULT_CHART_BG_DARK)
|
|
88
|
-
end
|
|
72
|
+
light_raw = config_hash_fetch(bg_container, "light")
|
|
73
|
+
dark_raw = config_hash_fetch(bg_container, "dark")
|
|
74
|
+
@chart_background_light = coerce_chart_background(light_raw, DEFAULT_CHART_BG_LIGHT, "light")
|
|
75
|
+
@chart_background_dark = coerce_chart_background(dark_raw, DEFAULT_CHART_BG_DARK, "dark")
|
|
89
76
|
end
|
|
90
77
|
|
|
91
78
|
# @param raw [Object]
|
|
92
79
|
# @return [Symbol] :light, :dark, or :auto
|
|
93
80
|
def normalize_prefers_mode(raw)
|
|
94
|
-
return DEFAULT_PREFERS_COLOR_SCHEME_MODE if raw.nil?
|
|
95
|
-
|
|
96
81
|
s = raw.to_s.strip.downcase
|
|
97
82
|
return DEFAULT_PREFERS_COLOR_SCHEME_MODE if s.empty?
|
|
98
83
|
|
|
@@ -117,10 +102,7 @@ module JekyllMermaidPrebuild
|
|
|
117
102
|
def config_hash_fetch(hash, key)
|
|
118
103
|
return nil unless hash.is_a?(Hash)
|
|
119
104
|
|
|
120
|
-
|
|
121
|
-
return hash[key.to_sym] if hash.key?(key.to_sym)
|
|
122
|
-
|
|
123
|
-
nil
|
|
105
|
+
hash.fetch(key) { hash.fetch(key.to_sym, nil) }
|
|
124
106
|
end
|
|
125
107
|
|
|
126
108
|
# @param value [Object] raw color string or nil (use default)
|
|
@@ -159,7 +141,7 @@ module JekyllMermaidPrebuild
|
|
|
159
141
|
# @param value [String]
|
|
160
142
|
# @return [String] frozen copy
|
|
161
143
|
def finalize_background(value)
|
|
162
|
-
value.to_s.freeze
|
|
144
|
+
value.to_s.dup.freeze
|
|
163
145
|
end
|
|
164
146
|
|
|
165
147
|
# Returns a frozen Hash of diagram type (string) => boolean. Non-hash values are rejected → {}.
|
|
@@ -169,7 +151,7 @@ module JekyllMermaidPrebuild
|
|
|
169
151
|
def parse_emoji_width_compensation(value)
|
|
170
152
|
return {}.freeze unless value.is_a?(Hash)
|
|
171
153
|
|
|
172
|
-
result = value.transform_keys(&:to_s).transform_values { |v|
|
|
154
|
+
result = value.transform_keys(&:to_s).transform_values { |v| v == true }
|
|
173
155
|
result.freeze
|
|
174
156
|
end
|
|
175
157
|
|
|
@@ -179,18 +161,15 @@ module JekyllMermaidPrebuild
|
|
|
179
161
|
dir = dir.strip
|
|
180
162
|
return DEFAULT_OUTPUT_DIR if dir.empty?
|
|
181
163
|
|
|
182
|
-
dir.
|
|
164
|
+
dir.split("/").reject(&:empty?).join("/")
|
|
183
165
|
end
|
|
184
166
|
|
|
185
167
|
# @param value [Object] raw config (numeric or off)
|
|
186
168
|
# @return [Numeric] non-negative padding in SVG user units; 0 means disabled
|
|
187
169
|
def parse_edge_label_padding(value)
|
|
188
|
-
return 0
|
|
189
|
-
|
|
190
|
-
num = value.is_a?(Numeric) ? value : nil
|
|
191
|
-
return 0 unless num
|
|
170
|
+
return 0 unless value.is_a?(Numeric)
|
|
192
171
|
|
|
193
|
-
|
|
172
|
+
value.negative? ? 0 : value
|
|
194
173
|
end
|
|
195
174
|
end
|
|
196
175
|
end
|
|
@@ -5,13 +5,11 @@ require "digest"
|
|
|
5
5
|
module JekyllMermaidPrebuild
|
|
6
6
|
# Computes content-based digests for caching
|
|
7
7
|
module DigestCalculator
|
|
8
|
-
module_function
|
|
9
|
-
|
|
10
8
|
# Generate a short digest for mermaid content
|
|
11
9
|
#
|
|
12
10
|
# @param content [String] mermaid diagram source
|
|
13
11
|
# @return [String] 8-character hex digest
|
|
14
|
-
def content_digest(content)
|
|
12
|
+
def self.content_digest(content)
|
|
15
13
|
Digest::MD5.hexdigest(content)[0, 8]
|
|
16
14
|
end
|
|
17
15
|
end
|
|
@@ -4,8 +4,6 @@ module JekyllMermaidPrebuild
|
|
|
4
4
|
# Stateless module: compensates for headless Chromium emoji width undermeasurement
|
|
5
5
|
# by appending padding to emoji-containing node labels before mmdc renders.
|
|
6
6
|
module EmojiCompensator
|
|
7
|
-
module_function
|
|
8
|
-
|
|
9
7
|
# Match a single Extended_Pictographic codepoint (emoji).
|
|
10
8
|
EMOJI_RE = /\p{Extended_Pictographic}/
|
|
11
9
|
|
|
@@ -15,28 +13,22 @@ module JekyllMermaidPrebuild
|
|
|
15
13
|
#
|
|
16
14
|
# @param mermaid_source [String] raw Mermaid diagram source
|
|
17
15
|
# @return [String, nil] diagram type keyword (e.g. "flowchart", "sequenceDiagram") or nil
|
|
18
|
-
def detect_diagram_type(mermaid_source)
|
|
19
|
-
return nil
|
|
16
|
+
def self.detect_diagram_type(mermaid_source)
|
|
17
|
+
return nil unless mermaid_source
|
|
20
18
|
|
|
21
19
|
in_frontmatter = false
|
|
22
|
-
frontmatter_delim_count = 0
|
|
23
20
|
|
|
24
21
|
mermaid_source.each_line do |line|
|
|
25
22
|
stripped = line.strip
|
|
26
|
-
# Track YAML frontmatter
|
|
27
23
|
if stripped == "---"
|
|
28
|
-
|
|
29
|
-
in_frontmatter = frontmatter_delim_count.odd?
|
|
24
|
+
in_frontmatter = !in_frontmatter
|
|
30
25
|
next
|
|
31
26
|
end
|
|
32
27
|
next if in_frontmatter
|
|
33
28
|
next if stripped.empty?
|
|
34
29
|
next if stripped.start_with?("%%")
|
|
35
30
|
|
|
36
|
-
|
|
37
|
-
token = stripped.split(/\s+/, 2).first
|
|
38
|
-
return nil if token.nil? || token.empty?
|
|
39
|
-
|
|
31
|
+
token = stripped[/\A\S+/]
|
|
40
32
|
return token == "graph" ? "flowchart" : token
|
|
41
33
|
end
|
|
42
34
|
|
|
@@ -49,10 +41,10 @@ module JekyllMermaidPrebuild
|
|
|
49
41
|
# @param mermaid_source [String] Mermaid diagram source
|
|
50
42
|
# @param diagram_type [String] result of detect_diagram_type
|
|
51
43
|
# @return [String] possibly modified source
|
|
52
|
-
def compensate(mermaid_source, diagram_type)
|
|
53
|
-
return mermaid_source if diagram_type
|
|
44
|
+
def self.compensate(mermaid_source, diagram_type)
|
|
45
|
+
return compensate_flowchart_labels(mermaid_source) if diagram_type == "flowchart"
|
|
54
46
|
|
|
55
|
-
|
|
47
|
+
mermaid_source
|
|
56
48
|
end
|
|
57
49
|
|
|
58
50
|
BR_RE = %r{(<br\s*/?>)}i
|
|
@@ -62,7 +54,7 @@ module JekyllMermaidPrebuild
|
|
|
62
54
|
#
|
|
63
55
|
# @param text [String]
|
|
64
56
|
# @return [Integer]
|
|
65
|
-
def count_emoji(text)
|
|
57
|
+
def self.count_emoji(text)
|
|
66
58
|
text.scan(EMOJI_RE).length
|
|
67
59
|
end
|
|
68
60
|
|
|
@@ -71,46 +63,48 @@ module JekyllMermaidPrebuild
|
|
|
71
63
|
#
|
|
72
64
|
# @param text [String]
|
|
73
65
|
# @return [Integer]
|
|
74
|
-
def visual_length(text)
|
|
66
|
+
def self.visual_length(text)
|
|
75
67
|
text.length + count_emoji(text)
|
|
76
68
|
end
|
|
77
69
|
|
|
78
70
|
# Pad the longest line in a label if it contains emoji.
|
|
79
71
|
# Splits on <br/> variants, finds the visually longest line, and only
|
|
80
72
|
# pads that line (shorter lines center naturally in the wider container).
|
|
81
|
-
#
|
|
73
|
+
# When no padding is applied, returns the same String object (identity),
|
|
74
|
+
# not merely an equal copy.
|
|
82
75
|
#
|
|
83
76
|
# @param content [String] raw label text (may contain <br/> line breaks)
|
|
84
77
|
# @return [String] possibly padded label text
|
|
85
|
-
def pad_label_content(content)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
78
|
+
def self.pad_label_content(content)
|
|
79
|
+
segments = content.split(BR_RE)
|
|
80
|
+
# Even indices are text lines; odd indices are the captured <br> delimiters.
|
|
81
|
+
line_pairs = segments.each_with_index.select { |_segment, index| index.even? }
|
|
82
|
+
return content if line_pairs.empty?
|
|
83
|
+
|
|
84
|
+
# max_by keeps the first of equal-length ties — pad that one index only.
|
|
85
|
+
longest_line, longest_idx = line_pairs.max_by { |segment, _index| visual_length(segment) }
|
|
86
|
+
emoji_count = count_emoji(longest_line)
|
|
87
|
+
return content unless emoji_count.positive?
|
|
88
|
+
|
|
89
|
+
segments[longest_idx] = "#{longest_line}#{NBSP * (emoji_count * 2)}"
|
|
90
|
+
segments.join
|
|
97
91
|
end
|
|
98
92
|
|
|
99
|
-
def compensate_flowchart_labels(source)
|
|
100
|
-
result = source
|
|
93
|
+
def self.compensate_flowchart_labels(source)
|
|
94
|
+
result = source
|
|
101
95
|
|
|
102
96
|
[
|
|
103
|
-
[/\["(
|
|
104
|
-
[/\['(
|
|
105
|
-
[/\("(
|
|
106
|
-
[/\{"(
|
|
97
|
+
[/\["(.+?)"\]/m, '["', '"]'],
|
|
98
|
+
[/\['(.+?)'\]/m, "['", "']"],
|
|
99
|
+
[/\("(.+?)"\)/m, '("', '")'],
|
|
100
|
+
[/\{"(.+?)"\}/m, '{"', '"}']
|
|
107
101
|
].each do |regex, open_str, close_str|
|
|
108
102
|
result = result.gsub(regex) do
|
|
109
103
|
"#{open_str}#{pad_label_content(Regexp.last_match(1))}#{close_str}"
|
|
110
104
|
end
|
|
111
105
|
end
|
|
112
106
|
|
|
113
|
-
result.gsub(%r{\[/"((?:[^"\\]|\\.)
|
|
107
|
+
result.gsub(%r{\[/"((?:[^"\\]|\\.)+?)"/\]}) do
|
|
114
108
|
"[/\"#{pad_label_content(Regexp.last_match(1))}\"/]"
|
|
115
109
|
end
|
|
116
110
|
end
|
|
@@ -18,16 +18,15 @@ module JekyllMermaidPrebuild
|
|
|
18
18
|
#
|
|
19
19
|
# @param mermaid_source [String] mermaid diagram definition
|
|
20
20
|
# @param cache_key [String] digest for caching
|
|
21
|
-
# @param diagram_type [String, nil] from EmojiCompensator.detect_diagram_type (e.g. "block")
|
|
22
21
|
# @return [Hash{String => String}, nil] stem (cache filename without .svg) => absolute cache path, or nil on failure
|
|
23
|
-
def generate(mermaid_source, cache_key
|
|
22
|
+
def generate(mermaid_source, cache_key)
|
|
24
23
|
case @config.prefers_color_scheme
|
|
25
24
|
when :light
|
|
26
|
-
generate_one(mermaid_source, cache_key,
|
|
25
|
+
generate_one(mermaid_source, cache_key, theme: :default)
|
|
27
26
|
when :dark
|
|
28
|
-
generate_one(mermaid_source, cache_key,
|
|
27
|
+
generate_one(mermaid_source, cache_key, theme: :dark)
|
|
29
28
|
when :auto
|
|
30
|
-
generate_auto(mermaid_source, cache_key
|
|
29
|
+
generate_auto(mermaid_source, cache_key)
|
|
31
30
|
end
|
|
32
31
|
end
|
|
33
32
|
|
|
@@ -72,7 +71,7 @@ module JekyllMermaidPrebuild
|
|
|
72
71
|
private
|
|
73
72
|
|
|
74
73
|
# @return [Hash{String => String}, nil]
|
|
75
|
-
def generate_one(mermaid_source, stem,
|
|
74
|
+
def generate_one(mermaid_source, stem, theme:)
|
|
76
75
|
cache_path = File.join(@config.cache_dir, "#{stem}.svg")
|
|
77
76
|
return { stem => cache_path } if File.exist?(cache_path)
|
|
78
77
|
|
|
@@ -80,12 +79,12 @@ module JekyllMermaidPrebuild
|
|
|
80
79
|
return nil unless MmdcWrapper.render(mermaid_source, cache_path, theme: theme)
|
|
81
80
|
|
|
82
81
|
bg = theme == :dark ? @config.chart_background_dark : @config.chart_background_light
|
|
83
|
-
post_process_svg(cache_path,
|
|
82
|
+
post_process_svg(cache_path, root_background: bg)
|
|
84
83
|
{ stem => cache_path }
|
|
85
84
|
end
|
|
86
85
|
|
|
87
86
|
# @return [Hash{String => String}, nil]
|
|
88
|
-
def generate_auto(mermaid_source, cache_key
|
|
87
|
+
def generate_auto(mermaid_source, cache_key)
|
|
89
88
|
light_stem = cache_key
|
|
90
89
|
dark_stem = "#{cache_key}-dark"
|
|
91
90
|
light_path = File.join(@config.cache_dir, "#{light_stem}.svg")
|
|
@@ -96,19 +95,19 @@ module JekyllMermaidPrebuild
|
|
|
96
95
|
unless File.exist?(light_path)
|
|
97
96
|
return nil unless MmdcWrapper.render(mermaid_source, light_path, theme: :default)
|
|
98
97
|
|
|
99
|
-
post_process_svg(light_path,
|
|
98
|
+
post_process_svg(light_path, root_background: @config.chart_background_light)
|
|
100
99
|
end
|
|
101
100
|
|
|
102
101
|
unless File.exist?(dark_path)
|
|
103
102
|
return nil unless MmdcWrapper.render(mermaid_source, dark_path, theme: :dark)
|
|
104
103
|
|
|
105
|
-
post_process_svg(dark_path,
|
|
104
|
+
post_process_svg(dark_path, root_background: @config.chart_background_dark)
|
|
106
105
|
end
|
|
107
106
|
|
|
108
107
|
{ light_stem => light_path, dark_stem => dark_path }
|
|
109
108
|
end
|
|
110
109
|
|
|
111
|
-
def post_process_svg(cache_path,
|
|
110
|
+
def post_process_svg(cache_path, root_background:)
|
|
112
111
|
raw = File.read(cache_path)
|
|
113
112
|
svg = raw
|
|
114
113
|
|
|
@@ -116,10 +115,9 @@ module JekyllMermaidPrebuild
|
|
|
116
115
|
svg = SvgPostProcessor.ensure_foreignobject_overflow(svg) if @config.overflow_protection
|
|
117
116
|
svg = SvgPostProcessor.apply_root_svg_background(svg, root_background)
|
|
118
117
|
|
|
119
|
-
|
|
120
|
-
svg = SvgPostProcessor.apply(svg, padding: pad) if pad.is_a?(Numeric) && pad.positive?
|
|
118
|
+
svg = SvgPostProcessor.apply(svg, padding: @config.edge_label_padding)
|
|
121
119
|
|
|
122
|
-
File.write(cache_path, svg)
|
|
120
|
+
File.write(cache_path, svg) unless svg.eql?(raw)
|
|
123
121
|
end
|
|
124
122
|
end
|
|
125
123
|
end
|
|
@@ -5,15 +5,13 @@ require "fileutils"
|
|
|
5
5
|
module JekyllMermaidPrebuild
|
|
6
6
|
# Jekyll hook integration
|
|
7
7
|
module Hooks
|
|
8
|
-
module_function
|
|
9
|
-
|
|
10
8
|
# Copy generated SVGs to _site directory
|
|
11
9
|
#
|
|
12
10
|
# @param site [Jekyll::Site] the Jekyll site
|
|
13
11
|
# @param config [Configuration] plugin configuration
|
|
14
12
|
# @param svgs [Hash] cache_key => cached_path mapping
|
|
15
|
-
def copy_svgs_to_site(site, config, svgs)
|
|
16
|
-
return
|
|
13
|
+
def self.copy_svgs_to_site(site, config, svgs)
|
|
14
|
+
return if svgs.nil? || svgs.empty?
|
|
17
15
|
|
|
18
16
|
dest_dir = File.join(site.dest, config.output_dir)
|
|
19
17
|
FileUtils.mkdir_p(dest_dir)
|
|
@@ -34,110 +32,110 @@ module JekyllMermaidPrebuild
|
|
|
34
32
|
end
|
|
35
33
|
|
|
36
34
|
# Log helpful error message for Puppeteer issues
|
|
37
|
-
def log_puppeteer_error
|
|
38
|
-
Jekyll.logger.error "MermaidPrebuild:", "=" * 60
|
|
35
|
+
def self.log_puppeteer_error
|
|
39
36
|
Jekyll.logger.error "MermaidPrebuild:", "mmdc failed: Puppeteer cannot launch headless Chrome"
|
|
40
|
-
Jekyll.logger.error "MermaidPrebuild:", ""
|
|
41
|
-
Jekyll.logger.error "MermaidPrebuild:", "This usually means missing system libraries."
|
|
42
|
-
Jekyll.logger.error "MermaidPrebuild:", "On Debian/Ubuntu/WSL, install with:"
|
|
43
|
-
Jekyll.logger.error "MermaidPrebuild:", ""
|
|
44
|
-
Jekyll.logger.error "MermaidPrebuild:", " sudo apt-get update"
|
|
45
|
-
Jekyll.logger.error "MermaidPrebuild:", " sudo apt-get install -y libgbm1 libasound2 libatk1.0-0 \\"
|
|
46
|
-
Jekyll.logger.error "MermaidPrebuild:", " libatk-bridge2.0-0 libcups2 libdrm2 libxcomposite1 \\"
|
|
47
|
-
Jekyll.logger.error "MermaidPrebuild:", " libxdamage1 libxfixes3 libxrandr2 libxkbcommon0 \\"
|
|
48
|
-
Jekyll.logger.error "MermaidPrebuild:", " libpango-1.0-0 libcairo2 libnss3 libnspr4"
|
|
49
|
-
Jekyll.logger.error "MermaidPrebuild:", ""
|
|
50
|
-
Jekyll.logger.error "MermaidPrebuild:", "See: https://pptr.dev/troubleshooting"
|
|
51
|
-
Jekyll.logger.error "MermaidPrebuild:", "=" * 60
|
|
37
|
+
Jekyll.logger.error "MermaidPrebuild:", "Install system libraries (see https://pptr.dev/troubleshooting)"
|
|
52
38
|
end
|
|
53
39
|
|
|
54
|
-
#
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
40
|
+
# Initialize plugin state after site data is loaded (:post_read)
|
|
41
|
+
#
|
|
42
|
+
# @param site [Jekyll::Site] the Jekyll site
|
|
43
|
+
def self.initialize_system(site)
|
|
44
|
+
config = Configuration.new(site)
|
|
45
|
+
site.data["mermaid_prebuild_config"] = config
|
|
46
|
+
|
|
47
|
+
unless config.enabled?
|
|
48
|
+
Jekyll.logger.info "MermaidPrebuild:", "Disabled in configuration"
|
|
49
|
+
site.data["mermaid_prebuild_enabled"] = false
|
|
50
|
+
return
|
|
51
|
+
end
|
|
60
52
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
53
|
+
status = MmdcWrapper.check_status
|
|
54
|
+
|
|
55
|
+
case status
|
|
56
|
+
when :ok
|
|
57
|
+
Jekyll.logger.info "MermaidPrebuild:", "Initialized (mmdc #{MmdcWrapper.version || "unknown version"})"
|
|
58
|
+
Jekyll.logger.info "MermaidPrebuild:", "Output directory: #{config.output_dir}"
|
|
59
|
+
site.data["mermaid_prebuild_enabled"] = true
|
|
60
|
+
generator = Generator.new(config)
|
|
61
|
+
site.data["mermaid_prebuild_generator"] = generator
|
|
62
|
+
site.data["mermaid_prebuild_processor"] = Processor.new(config, generator)
|
|
63
|
+
site.data["mermaid_prebuild_svgs"] = {}
|
|
64
|
+
when :not_found
|
|
65
|
+
Jekyll.logger.warn "MermaidPrebuild:", "mmdc not found - mermaid diagrams will not be converted"
|
|
66
|
+
Jekyll.logger.warn "MermaidPrebuild:", "Install with: npm install -g @mermaid-js/mermaid-cli"
|
|
67
|
+
site.data["mermaid_prebuild_enabled"] = false
|
|
68
|
+
when :puppeteer_error
|
|
69
|
+
log_puppeteer_error
|
|
70
|
+
site.data["mermaid_prebuild_enabled"] = false
|
|
71
|
+
else
|
|
72
|
+
Jekyll.logger.warn "MermaidPrebuild:", "mmdc check failed with unknown error"
|
|
73
|
+
site.data["mermaid_prebuild_enabled"] = false
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Process documents/pages before rendering (:pre_render)
|
|
78
|
+
#
|
|
79
|
+
# @param site [Jekyll::Site] the Jekyll site
|
|
80
|
+
def self.process_site(site)
|
|
81
|
+
return unless site.data["mermaid_prebuild_enabled"]
|
|
82
|
+
|
|
83
|
+
processor = site.data["mermaid_prebuild_processor"]
|
|
84
|
+
total_count = 0
|
|
85
|
+
|
|
86
|
+
site.documents.each do |document|
|
|
87
|
+
next unless document.content
|
|
66
88
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
Jekyll.logger.info "MermaidPrebuild:", "
|
|
72
|
-
|
|
73
|
-
site.data["mermaid_prebuild_enabled"] = true
|
|
74
|
-
site.data["mermaid_prebuild_generator"] = Generator.new(config)
|
|
75
|
-
site.data["mermaid_prebuild_processor"] = Processor.new(config, site.data["mermaid_prebuild_generator"])
|
|
76
|
-
site.data["mermaid_prebuild_svgs"] = {}
|
|
77
|
-
when :not_found
|
|
78
|
-
Jekyll.logger.warn "MermaidPrebuild:", "mmdc not found - mermaid diagrams will not be converted"
|
|
79
|
-
Jekyll.logger.warn "MermaidPrebuild:", "Install with: npm install -g @mermaid-js/mermaid-cli"
|
|
80
|
-
site.data["mermaid_prebuild_enabled"] = false
|
|
81
|
-
when :puppeteer_error
|
|
82
|
-
log_puppeteer_error
|
|
83
|
-
site.data["mermaid_prebuild_enabled"] = false
|
|
84
|
-
else
|
|
85
|
-
Jekyll.logger.warn "MermaidPrebuild:", "mmdc check failed with unknown error"
|
|
86
|
-
site.data["mermaid_prebuild_enabled"] = false
|
|
89
|
+
processed, count, svgs = processor.process_content(document.content, site)
|
|
90
|
+
if count.positive?
|
|
91
|
+
document.content = processed
|
|
92
|
+
site.data["mermaid_prebuild_svgs"].merge!(svgs)
|
|
93
|
+
Jekyll.logger.info "MermaidPrebuild:", "Converted #{count} diagram(s) in #{document.relative_path}"
|
|
94
|
+
total_count += count
|
|
87
95
|
end
|
|
96
|
+
rescue StandardError => e
|
|
97
|
+
Jekyll.logger.error "MermaidPrebuild:", "Error processing #{document.relative_path}: #{e.message}"
|
|
88
98
|
end
|
|
89
99
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
next unless site.data["mermaid_prebuild_enabled"]
|
|
93
|
-
|
|
94
|
-
processor = site.data["mermaid_prebuild_processor"]
|
|
95
|
-
total_count = 0
|
|
96
|
-
|
|
97
|
-
# Process all documents
|
|
98
|
-
site.documents.each do |document|
|
|
99
|
-
next unless document.content
|
|
100
|
-
|
|
101
|
-
processed, count, svgs = processor.process_content(document.content, site)
|
|
102
|
-
if count.positive?
|
|
103
|
-
document.content = processed
|
|
104
|
-
site.data["mermaid_prebuild_svgs"].merge!(svgs)
|
|
105
|
-
Jekyll.logger.info "MermaidPrebuild:", "Converted #{count} diagram(s) in #{document.relative_path}"
|
|
106
|
-
total_count += count
|
|
107
|
-
end
|
|
108
|
-
rescue StandardError => e
|
|
109
|
-
Jekyll.logger.error "MermaidPrebuild:", "Error processing #{document.relative_path}: #{e.message}"
|
|
110
|
-
end
|
|
100
|
+
site.pages.each do |page|
|
|
101
|
+
next unless page.content
|
|
111
102
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
page.content = processed
|
|
119
|
-
site.data["mermaid_prebuild_svgs"].merge!(svgs)
|
|
120
|
-
Jekyll.logger.info "MermaidPrebuild:", "Converted #{count} diagram(s) in #{page.relative_path}"
|
|
121
|
-
total_count += count
|
|
122
|
-
end
|
|
123
|
-
rescue StandardError => e
|
|
124
|
-
Jekyll.logger.error "MermaidPrebuild:", "Error processing page: #{e.message}"
|
|
103
|
+
processed, count, svgs = processor.process_content(page.content, site)
|
|
104
|
+
if count.positive?
|
|
105
|
+
page.content = processed
|
|
106
|
+
site.data["mermaid_prebuild_svgs"].merge!(svgs)
|
|
107
|
+
Jekyll.logger.info "MermaidPrebuild:", "Converted #{count} diagram(s) in #{page.relative_path}"
|
|
108
|
+
total_count += count
|
|
125
109
|
end
|
|
126
|
-
|
|
127
|
-
Jekyll.logger.
|
|
110
|
+
rescue StandardError => e
|
|
111
|
+
Jekyll.logger.error "MermaidPrebuild:", "Error processing #{page.relative_path}: #{e.message}"
|
|
128
112
|
end
|
|
129
113
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
next unless site.data["mermaid_prebuild_enabled"]
|
|
114
|
+
Jekyll.logger.info "MermaidPrebuild:", "Total: #{total_count} diagram(s) converted" if total_count.positive?
|
|
115
|
+
end
|
|
133
116
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
117
|
+
# Copy SVGs into _site after write (:post_write)
|
|
118
|
+
#
|
|
119
|
+
# @param site [Jekyll::Site] the Jekyll site
|
|
120
|
+
def self.copy_generated_svgs(site)
|
|
121
|
+
return unless site.data["mermaid_prebuild_enabled"]
|
|
122
|
+
|
|
123
|
+
config = site.data["mermaid_prebuild_config"]
|
|
124
|
+
svgs = site.data["mermaid_prebuild_svgs"]
|
|
125
|
+
copy_svgs_to_site(site, config, svgs)
|
|
138
126
|
end
|
|
139
127
|
end
|
|
140
128
|
end
|
|
141
129
|
|
|
142
|
-
# Register hooks when loaded
|
|
143
|
-
|
|
130
|
+
# Register Jekyll hooks when loaded
|
|
131
|
+
Jekyll::Hooks.register :site, :post_read do |site|
|
|
132
|
+
JekyllMermaidPrebuild::Hooks.initialize_system(site)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
Jekyll::Hooks.register :site, :pre_render do |site|
|
|
136
|
+
JekyllMermaidPrebuild::Hooks.process_site(site)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
Jekyll::Hooks.register :site, :post_write do |site|
|
|
140
|
+
JekyllMermaidPrebuild::Hooks.copy_generated_svgs(site)
|
|
141
|
+
end
|
|
@@ -6,39 +6,41 @@ require "tempfile"
|
|
|
6
6
|
module JekyllMermaidPrebuild
|
|
7
7
|
# Wrapper for mmdc (mermaid CLI) commands
|
|
8
8
|
module MmdcWrapper
|
|
9
|
-
module_function
|
|
10
|
-
|
|
11
9
|
# Check if mmdc is available in PATH
|
|
12
10
|
#
|
|
13
11
|
# @return [Boolean] true if mmdc is available
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
MMDC_COMMAND = "mmdc"
|
|
13
|
+
|
|
14
|
+
def self.available?
|
|
15
|
+
return @available if instance_variable_defined?(:@available)
|
|
16
16
|
|
|
17
|
-
@available = command_exists?(
|
|
17
|
+
@available = command_exists?(MMDC_COMMAND)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
# Check if a command exists in PATH (cross-platform)
|
|
21
21
|
#
|
|
22
22
|
# @param cmd [String] command name
|
|
23
23
|
# @return [Boolean] true if command found
|
|
24
|
-
def command_exists?(cmd)
|
|
24
|
+
def self.command_exists?(cmd)
|
|
25
|
+
return false if cmd.nil? || cmd.empty?
|
|
26
|
+
|
|
25
27
|
cmd_name = Gem.win_platform? ? "#{cmd}.exe" : cmd
|
|
26
28
|
path_dirs = ENV.fetch("PATH", "").split(File::PATH_SEPARATOR)
|
|
27
29
|
|
|
28
30
|
path_dirs.any? do |dir|
|
|
29
31
|
executable = File.join(dir, cmd_name)
|
|
30
|
-
File.executable?(executable)
|
|
32
|
+
File.file?(executable) && File.executable?(executable)
|
|
31
33
|
end
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
# Get mmdc version
|
|
35
37
|
#
|
|
36
38
|
# @return [String, nil] version string or nil
|
|
37
|
-
def version
|
|
38
|
-
return @version if
|
|
39
|
+
def self.version
|
|
40
|
+
return @version if instance_variable_defined?(:@version)
|
|
39
41
|
|
|
40
|
-
output, status = Open3.capture2e(
|
|
41
|
-
@version =
|
|
42
|
+
output, status = Open3.capture2e(MMDC_COMMAND, "--version")
|
|
43
|
+
@version = output.strip if status.success?
|
|
42
44
|
rescue StandardError
|
|
43
45
|
@version = nil
|
|
44
46
|
end
|
|
@@ -46,8 +48,8 @@ module JekyllMermaidPrebuild
|
|
|
46
48
|
# Check mmdc status (availability and Puppeteer)
|
|
47
49
|
#
|
|
48
50
|
# @return [Symbol] :ok, :not_found, :puppeteer_error, or :unknown_error
|
|
49
|
-
def check_status
|
|
50
|
-
return @check_status if
|
|
51
|
+
def self.check_status
|
|
52
|
+
return @check_status if instance_variable_defined?(:@check_status)
|
|
51
53
|
|
|
52
54
|
unless available?
|
|
53
55
|
@check_status = :not_found
|
|
@@ -58,25 +60,26 @@ module JekyllMermaidPrebuild
|
|
|
58
60
|
end
|
|
59
61
|
|
|
60
62
|
# Reset cached status (useful for testing)
|
|
61
|
-
def reset_cache!
|
|
62
|
-
remove_instance_variable(:@available) if
|
|
63
|
-
remove_instance_variable(:@version) if
|
|
64
|
-
remove_instance_variable(:@check_status) if
|
|
63
|
+
def self.reset_cache!
|
|
64
|
+
remove_instance_variable(:@available) if instance_variable_defined?(:@available)
|
|
65
|
+
remove_instance_variable(:@version) if instance_variable_defined?(:@version)
|
|
66
|
+
remove_instance_variable(:@check_status) if instance_variable_defined?(:@check_status)
|
|
65
67
|
end
|
|
66
68
|
|
|
67
69
|
# Test mmdc with a minimal diagram to verify Puppeteer works
|
|
68
70
|
#
|
|
69
71
|
# @return [Symbol] :ok, :puppeteer_error, or :unknown_error
|
|
70
|
-
def test_render
|
|
71
|
-
input = Tempfile.new(["
|
|
72
|
-
output = Tempfile.new(["
|
|
73
|
-
|
|
72
|
+
def self.test_render
|
|
73
|
+
input = Tempfile.new(["", ".mmd"])
|
|
74
|
+
output = Tempfile.new(["", ".svg"])
|
|
74
75
|
begin
|
|
75
76
|
input.write("graph TD\nA-->B")
|
|
76
77
|
input.close
|
|
77
78
|
output.close # Close before mmdc writes (Windows file locking)
|
|
78
79
|
|
|
79
|
-
_stdout, stderr, status = Open3.capture3(
|
|
80
|
+
_stdout, stderr, status = Open3.capture3(
|
|
81
|
+
MMDC_COMMAND, "-i", input.path, "-o", output.path, "-e", "svg"
|
|
82
|
+
)
|
|
80
83
|
|
|
81
84
|
if status.success?
|
|
82
85
|
:ok
|
|
@@ -86,8 +89,8 @@ module JekyllMermaidPrebuild
|
|
|
86
89
|
:unknown_error
|
|
87
90
|
end
|
|
88
91
|
ensure
|
|
89
|
-
input.
|
|
90
|
-
output.
|
|
92
|
+
input.unlink
|
|
93
|
+
output.unlink
|
|
91
94
|
end
|
|
92
95
|
end
|
|
93
96
|
|
|
@@ -100,11 +103,8 @@ module JekyllMermaidPrebuild
|
|
|
100
103
|
# @param theme [Symbol] :default (mermaid default theme) or :dark (mmdc -t dark)
|
|
101
104
|
# @return [Boolean] true if successful
|
|
102
105
|
# @raise [ArgumentError] if theme is not supported
|
|
103
|
-
def render(mermaid_source, output_path, theme: :default)
|
|
104
|
-
unless ALLOWED_RENDER_THEMES.include?(theme)
|
|
105
|
-
raise ArgumentError,
|
|
106
|
-
"unsupported mmdc theme #{theme.inspect} (allowed: #{ALLOWED_RENDER_THEMES.map(&:inspect).join(", ")})"
|
|
107
|
-
end
|
|
106
|
+
def self.render(mermaid_source, output_path, theme: :default)
|
|
107
|
+
raise ArgumentError, "unsupported mmdc theme #{theme.inspect}" unless ALLOWED_RENDER_THEMES.include?(theme)
|
|
108
108
|
|
|
109
109
|
input_file = Tempfile.new(["mermaid", ".mmd"])
|
|
110
110
|
|
|
@@ -112,7 +112,7 @@ module JekyllMermaidPrebuild
|
|
|
112
112
|
input_file.write(mermaid_source)
|
|
113
113
|
input_file.close
|
|
114
114
|
|
|
115
|
-
cmd = [
|
|
115
|
+
cmd = [MMDC_COMMAND, "-i", input_file.path, "-o", output_path, "-e", "svg"]
|
|
116
116
|
cmd += ["-t", "dark"] if theme == :dark
|
|
117
117
|
_stdout, _stderr, status = Open3.capture3(*cmd)
|
|
118
118
|
|
|
@@ -126,7 +126,7 @@ module JekyllMermaidPrebuild
|
|
|
126
126
|
# Supports both backtick (```) and tilde (~~~) fences with 3+ characters
|
|
127
127
|
#
|
|
128
128
|
# @return [Regexp] pattern matching mermaid code blocks
|
|
129
|
-
def mermaid_fence_pattern
|
|
129
|
+
def self.mermaid_fence_pattern
|
|
130
130
|
/
|
|
131
131
|
^(`{3,}|~{3,})mermaid\s*\n # Opening fence: 3+ backticks or tildes, then 'mermaid'
|
|
132
132
|
(.*?) # Content (captured, non-greedy)
|
|
@@ -22,7 +22,7 @@ module JekyllMermaidPrebuild
|
|
|
22
22
|
# @param _site [Jekyll::Site] the Jekyll site (unused, kept for API compatibility)
|
|
23
23
|
# @return [Array<String, Integer, Hash>] [processed_content, count, svgs_to_copy]
|
|
24
24
|
def process_content(content, _site = nil)
|
|
25
|
-
return [
|
|
25
|
+
return [nil, 0, {}] unless content
|
|
26
26
|
|
|
27
27
|
converted_count = 0
|
|
28
28
|
svgs_to_copy = {}
|
|
@@ -36,20 +36,19 @@ module JekyllMermaidPrebuild
|
|
|
36
36
|
result = convert_block(block)
|
|
37
37
|
next unless result
|
|
38
38
|
|
|
39
|
+
result => { svgs:, html: }
|
|
40
|
+
block => { start:, end: block_end }
|
|
39
41
|
converted_count += 1
|
|
40
|
-
svgs_to_copy.merge!(
|
|
41
|
-
processed[
|
|
42
|
+
svgs_to_copy.merge!(svgs)
|
|
43
|
+
processed[start...block_end] = html
|
|
42
44
|
end
|
|
43
45
|
|
|
44
46
|
[processed, converted_count, svgs_to_copy]
|
|
45
47
|
end
|
|
46
48
|
|
|
47
|
-
private
|
|
48
|
-
|
|
49
49
|
# @param source [String] mermaid passed to mmdc (after optional emoji compensation)
|
|
50
|
-
# @param diagram_type [String, nil]
|
|
51
50
|
# @return [String] input to MD5 for cache key
|
|
52
|
-
def digest_string_for_cache(source
|
|
51
|
+
def digest_string_for_cache(source)
|
|
53
52
|
parts = [source]
|
|
54
53
|
parts << "pcs=#{@config.prefers_color_scheme}"
|
|
55
54
|
parts << "bgL=#{@config.chart_background_light}"
|
|
@@ -68,14 +67,14 @@ module JekyllMermaidPrebuild
|
|
|
68
67
|
def convert_block(block)
|
|
69
68
|
mermaid_source = block[:content]
|
|
70
69
|
diagram_type = EmojiCompensator.detect_diagram_type(mermaid_source)
|
|
71
|
-
source_for_render = if
|
|
70
|
+
source_for_render = if @config.emoji_width_compensation[diagram_type]
|
|
72
71
|
EmojiCompensator.compensate(mermaid_source, diagram_type)
|
|
73
72
|
else
|
|
74
73
|
mermaid_source
|
|
75
74
|
end
|
|
76
|
-
digest_input = digest_string_for_cache(source_for_render
|
|
75
|
+
digest_input = digest_string_for_cache(source_for_render)
|
|
77
76
|
cache_key = DigestCalculator.content_digest(digest_input)
|
|
78
|
-
paths = @generator.generate(source_for_render, cache_key
|
|
77
|
+
paths = @generator.generate(source_for_render, cache_key)
|
|
79
78
|
|
|
80
79
|
return nil if paths.nil? || paths.empty?
|
|
81
80
|
|
|
@@ -95,13 +94,14 @@ module JekyllMermaidPrebuild
|
|
|
95
94
|
# @param content [String] markdown content
|
|
96
95
|
# @return [Array<Hash>] array of {start:, end:, content:} for each top-level mermaid block
|
|
97
96
|
def find_top_level_mermaid_blocks(content)
|
|
98
|
-
|
|
97
|
+
blocks = []
|
|
98
|
+
state = { blocks: blocks, fence_stack: [], position: 0 }
|
|
99
99
|
|
|
100
|
-
content.lines
|
|
100
|
+
content.lines do |line|
|
|
101
101
|
process_line(line, state)
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
blocks
|
|
105
105
|
end
|
|
106
106
|
|
|
107
107
|
# Process a single line for fence detection
|
|
@@ -112,8 +112,9 @@ module JekyllMermaidPrebuild
|
|
|
112
112
|
match = line.match(FENCE_OPENER)
|
|
113
113
|
if match
|
|
114
114
|
handle_fence_line(line, line_start, match, state)
|
|
115
|
-
elsif state[:current_mermaid]
|
|
116
|
-
|
|
115
|
+
elsif (current_mermaid = state[:current_mermaid])
|
|
116
|
+
content_lines = current_mermaid[:content_lines]
|
|
117
|
+
content_lines << line
|
|
117
118
|
end
|
|
118
119
|
end
|
|
119
120
|
|
|
@@ -121,7 +122,7 @@ module JekyllMermaidPrebuild
|
|
|
121
122
|
def handle_fence_line(line, line_start, match, state)
|
|
122
123
|
fence_chars = match[1]
|
|
123
124
|
language = match[2]
|
|
124
|
-
fence_type = fence_chars
|
|
125
|
+
fence_type = fence_chars.include?("~") ? "~" : "`"
|
|
125
126
|
fence_length = fence_chars.length
|
|
126
127
|
|
|
127
128
|
if state[:current_mermaid]
|
|
@@ -136,11 +137,14 @@ module JekyllMermaidPrebuild
|
|
|
136
137
|
# Handle fence line while inside a mermaid block
|
|
137
138
|
def handle_line_in_mermaid(line, fence_chars, fence_type, fence_length, state)
|
|
138
139
|
cm = state[:current_mermaid]
|
|
140
|
+
blocks = state[:blocks]
|
|
141
|
+
position = state[:position]
|
|
142
|
+
content_lines = cm[:content_lines]
|
|
139
143
|
if fence_type == cm[:fence_type] && fence_length == cm[:fence_length] && line.strip == fence_chars
|
|
140
|
-
|
|
144
|
+
blocks << { start: cm[:start], end: position, content: content_lines.join }
|
|
141
145
|
state[:current_mermaid] = nil
|
|
142
146
|
else
|
|
143
|
-
|
|
147
|
+
content_lines << line
|
|
144
148
|
end
|
|
145
149
|
end
|
|
146
150
|
|
|
@@ -156,12 +160,13 @@ module JekyllMermaidPrebuild
|
|
|
156
160
|
|
|
157
161
|
# Handle fence line while inside a non-mermaid fence
|
|
158
162
|
def handle_line_in_nested_fence(line, fence_type, fence_length, state)
|
|
159
|
-
|
|
163
|
+
fence_stack = state[:fence_stack]
|
|
164
|
+
top_fence_length, top_fence_type = fence_stack.last
|
|
160
165
|
|
|
161
|
-
if fence_type == top_fence_type && fence_length >= top_fence_length && line.strip.match?(
|
|
162
|
-
|
|
166
|
+
if fence_type == top_fence_type && fence_length >= top_fence_length && line.strip.match?(/\A[`~]+\z/)
|
|
167
|
+
fence_stack.pop
|
|
163
168
|
else
|
|
164
|
-
|
|
169
|
+
fence_stack.push([fence_length, fence_type])
|
|
165
170
|
end
|
|
166
171
|
end
|
|
167
172
|
end
|
|
@@ -18,8 +18,6 @@ module JekyllMermaidPrebuild
|
|
|
18
18
|
# of theme. The plugin replaces that token with configurable CSS color values for light and
|
|
19
19
|
# dark variants (defaults white / black) so charts match page background in both modes.
|
|
20
20
|
module SvgPostProcessor
|
|
21
|
-
module_function
|
|
22
|
-
|
|
23
21
|
# Opening sequence produced by mmdc for block edge labels (deterministic minified output).
|
|
24
22
|
EDGE_LABEL_FOREIGN_OBJECT_RE = /
|
|
25
23
|
(<g\sclass="edgeLabel"[^>]*><g\sclass="label"[^>]*><foreignObject)
|
|
@@ -30,9 +28,8 @@ module JekyllMermaidPrebuild
|
|
|
30
28
|
# @param svg_string [String] full SVG document from mmdc
|
|
31
29
|
# @param padding [Numeric] user units to add to each matching foreignObject width (must be positive)
|
|
32
30
|
# @return [String] possibly widened SVG, or the original string on no-op / error
|
|
33
|
-
def apply(svg_string, padding:)
|
|
34
|
-
return svg_string unless
|
|
35
|
-
return svg_string unless padding.is_a?(Numeric) && padding.positive?
|
|
31
|
+
def self.apply(svg_string, padding:)
|
|
32
|
+
return svg_string unless padding.positive?
|
|
36
33
|
|
|
37
34
|
apply_edge_label_padding(svg_string, padding)
|
|
38
35
|
rescue StandardError
|
|
@@ -49,10 +46,8 @@ module JekyllMermaidPrebuild
|
|
|
49
46
|
#
|
|
50
47
|
# @param svg_string [String] full SVG document from mmdc
|
|
51
48
|
# @return [String] SVG with overflow rule injected, or original on no-op / error
|
|
52
|
-
def ensure_foreignobject_overflow(svg_string)
|
|
53
|
-
return svg_string unless svg_string.is_a?(String)
|
|
49
|
+
def self.ensure_foreignobject_overflow(svg_string)
|
|
54
50
|
return svg_string if svg_string.include?(OVERFLOW_RULE)
|
|
55
|
-
return svg_string unless svg_string.include?("</style>")
|
|
56
51
|
|
|
57
52
|
svg_string.sub("</style>", "#{OVERFLOW_RULE}</style>")
|
|
58
53
|
rescue StandardError
|
|
@@ -68,10 +63,8 @@ module JekyllMermaidPrebuild
|
|
|
68
63
|
#
|
|
69
64
|
# @param svg_string [String] full SVG document from mmdc
|
|
70
65
|
# @return [String] SVG with centering rule injected, or original on no-op / error
|
|
71
|
-
def ensure_text_centering(svg_string)
|
|
72
|
-
return svg_string unless svg_string.is_a?(String)
|
|
66
|
+
def self.ensure_text_centering(svg_string)
|
|
73
67
|
return svg_string if svg_string.include?(CENTERING_RULE)
|
|
74
|
-
return svg_string unless svg_string.include?("</style>")
|
|
75
68
|
|
|
76
69
|
svg_string.sub("</style>", "#{CENTERING_RULE}</style>")
|
|
77
70
|
rescue StandardError
|
|
@@ -85,19 +78,18 @@ module JekyllMermaidPrebuild
|
|
|
85
78
|
# @param svg_string [String] full SVG document from mmdc
|
|
86
79
|
# @param css_background [String] literal after `background-color:` (e.g. "black", "#fff0aa")
|
|
87
80
|
# @return [String] SVG with updated root background, or original on no-op / error
|
|
88
|
-
def apply_root_svg_background(svg_string, css_background)
|
|
89
|
-
return svg_string unless svg_string.is_a?(String)
|
|
81
|
+
def self.apply_root_svg_background(svg_string, css_background)
|
|
90
82
|
return svg_string unless css_background.is_a?(String) && !css_background.empty?
|
|
91
83
|
|
|
92
84
|
svg_string.sub(
|
|
93
|
-
/(<svg\b[^>]
|
|
85
|
+
/(<svg\b[^>]+\bstyle="[^"]*?)background-color:\s*white;?/,
|
|
94
86
|
"\\1background-color: #{css_background};"
|
|
95
87
|
)
|
|
96
88
|
rescue StandardError
|
|
97
89
|
svg_string
|
|
98
90
|
end
|
|
99
91
|
|
|
100
|
-
def apply_edge_label_padding(svg_string, padding)
|
|
92
|
+
def self.apply_edge_label_padding(svg_string, padding)
|
|
101
93
|
svg_string.gsub(EDGE_LABEL_FOREIGN_OBJECT_RE) do
|
|
102
94
|
prefix = Regexp.last_match(1)
|
|
103
95
|
attrs = Regexp.last_match(2)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jekyll-mermaid-prebuild
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Texarkanine
|
|
@@ -29,6 +29,34 @@ dependencies:
|
|
|
29
29
|
- - "<"
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
31
|
version: '5.0'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: mutant
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - "~>"
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '0.16'
|
|
39
|
+
type: :development
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - "~>"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '0.16'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: mutant-rspec
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - "~>"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '0.16'
|
|
53
|
+
type: :development
|
|
54
|
+
prerelease: false
|
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - "~>"
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '0.16'
|
|
32
60
|
- !ruby/object:Gem::Dependency
|
|
33
61
|
name: rake
|
|
34
62
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -105,28 +133,28 @@ dependencies:
|
|
|
105
133
|
requirements:
|
|
106
134
|
- - "~>"
|
|
107
135
|
- !ruby/object:Gem::Version
|
|
108
|
-
version: '0
|
|
136
|
+
version: '1.0'
|
|
109
137
|
type: :development
|
|
110
138
|
prerelease: false
|
|
111
139
|
version_requirements: !ruby/object:Gem::Requirement
|
|
112
140
|
requirements:
|
|
113
141
|
- - "~>"
|
|
114
142
|
- !ruby/object:Gem::Version
|
|
115
|
-
version: '0
|
|
143
|
+
version: '1.0'
|
|
116
144
|
- !ruby/object:Gem::Dependency
|
|
117
145
|
name: simplecov-cobertura
|
|
118
146
|
requirement: !ruby/object:Gem::Requirement
|
|
119
147
|
requirements:
|
|
120
148
|
- - "~>"
|
|
121
149
|
- !ruby/object:Gem::Version
|
|
122
|
-
version: '
|
|
150
|
+
version: '4.0'
|
|
123
151
|
type: :development
|
|
124
152
|
prerelease: false
|
|
125
153
|
version_requirements: !ruby/object:Gem::Requirement
|
|
126
154
|
requirements:
|
|
127
155
|
- - "~>"
|
|
128
156
|
- !ruby/object:Gem::Version
|
|
129
|
-
version: '
|
|
157
|
+
version: '4.0'
|
|
130
158
|
description: Jekyll plugin that converts mermaid diagram code blocks to SVG files
|
|
131
159
|
at build time using the mmdc CLI. Eliminates the need for client-side mermaid.js
|
|
132
160
|
(~2MB) by generating static SVG files with intelligent caching.
|