jekyll-mermaid-prebuild 0.3.1 → 0.3.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b0f97de5928066d6b512188d2aba57a8762048d1158f70fcaa256ace7fc7e662
4
- data.tar.gz: 4cb3056e57fa86d73e2a9ff0d12336fa8edda8fa7c3fa3aeddbc51506b6ffd3d
3
+ metadata.gz: 774a2f269fa0af19b0e9dd2008518271984d1a3e812037fa0eb9c27d54b08ed9
4
+ data.tar.gz: ce18c8d08f2fb59ebbffdd02c87e32dccd021bb839650aebbc065fa5778cae0e
5
5
  SHA512:
6
- metadata.gz: dd4085acef77b82dfc0f261a70c018be7de076362b3f9b31b1aa480c4503143d73798ff79fb9f978a6442ea2d45e505f89680b47e59a403e3ce9bae8b1e2bca3
7
- data.tar.gz: b9e4d6ce68bb02d9e144f290cee07466ce0411bef61be91469570d330e8f260ae945e2ddb8c5de11321201b8bd74fedb03847ffa626bdafcb2d03bdc0516256e
6
+ metadata.gz: bb13f85293b01d89745391543a28885ecc3ff05a9ca4448c5c8929df641cb72801c2efc65c6984d9960b36cea1a92716adeb6c3b66bc12d19013b6b23750dbd4
7
+ data.tar.gz: cec8771614670a55c05b0690db77d6914952a5354362aa402296d96caea841ee3647f4a3c64d39145e18bf0cb45bc6d6d82d0c52696a679c5e8e9b1ca4747a7f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.2](https://github.com/Texarkanine/jekyll-mermaid-prebuild/compare/v0.3.1...v0.3.2) (2026-03-22)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * block diagram node label centering and clipping w/ latest Mermaid ([#16](https://github.com/Texarkanine/jekyll-mermaid-prebuild/issues/16)) ([ae250c1](https://github.com/Texarkanine/jekyll-mermaid-prebuild/commit/ae250c14fd04d3f1826a70b8af339ced67bb988a))
9
+
10
+ ## [Unreleased]
11
+
12
+ ### Features
13
+
14
+ * Optional `block_edge_label_padding` widens block-diagram edge-label `<foreignObject>` widths after mmdc to prevent clipping caused by cross-browser text measurement differences on non-Mac build hosts.
15
+
16
+ ### Bug Fixes
17
+
18
+ * Inject `foreignObject > div { display: block !important; text-align: center }` into every generated SVG so that label text centers correctly when the viewing browser's font metrics differ from the generating headless Chromium. Fixes left-shifted labels visible when mmdc runs on a different OS than the viewer.
19
+
3
20
  ## [0.3.1](https://github.com/Texarkanine/jekyll-mermaid-prebuild/compare/v0.3.0...v0.3.1) (2026-03-13)
4
21
 
5
22
 
data/README.md CHANGED
@@ -94,6 +94,7 @@ Add to your `_config.yml`:
94
94
  mermaid_prebuild:
95
95
  enabled: true # default: true
96
96
  output_dir: assets/svg # default: assets/svg
97
+ block_edge_label_padding: 0 # optional; see Block diagrams below
97
98
  emoji_width_compensation: # optional, see below
98
99
  flowchart: true
99
100
  ```
@@ -104,8 +105,23 @@ mermaid_prebuild:
104
105
  |--------|---------|-------------|
105
106
  | `enabled` | `true` | Enable/disable the plugin |
106
107
  | `output_dir` | `assets/svg` | Directory for generated SVG files |
108
+ | `block_edge_label_padding` | `0` | Extra SVG user units added to **block** diagram edge-label `<foreignObject>` widths after mmdc (off when `0`, `false`, or omitted). See [Cross-browser text rendering fixes](#cross-browser-text-rendering-fixes). |
107
109
  | `emoji_width_compensation` | `{}` | Map of diagram types to booleans; see [Emoji width compensation](#emoji-width-compensation) below. |
108
110
 
111
+ ### Cross-browser text rendering fixes
112
+
113
+ When mmdc renders a diagram, headless Chromium measures text with `getBoundingClientRect()` and sets each `<foreignObject>` to exactly that width. If the viewing browser (different OS, different fonts) renders the same text at a different width, labels can clip or shift. The plugin applies two automatic fixes to every generated SVG:
114
+
115
+ 1. **Text centering** (always on, no config needed): Mermaid's CSS sets `text-align: center` on SVG `<g>` elements, but that has no effect on HTML inside `<foreignObject>`. The plugin injects a CSS rule (`foreignObject > div { display: block !important; text-align: center }`) so that label text centers within its container regardless of font metric differences. This is idempotent — if upstream Mermaid fixes this, the rule becomes redundant but harmless.
116
+
117
+ 2. **Block edge label padding** (opt-in via `block_edge_label_padding`): Block diagram edge labels have zero padding between the `<foreignObject>` boundary and the text. If the viewing browser renders text wider than headless Chromium measured, the last character(s) clip. This option widens only **edge** label `<foreignObject>` elements (not node labels) in SVGs whose root has `aria-roledescription="block"`. Flowcharts and other diagram types are unchanged.
118
+
119
+ - **When to enable:** If block diagram edge text clips in generated SVGs on your build host.
120
+ - **Starting value:** Try `4`-`8` (SVG user units); increase only if needed.
121
+ - **Caching:** The cache key includes this padding for block diagrams only, so changing the value invalidates cached block SVGs without affecting flowcharts.
122
+
123
+ > **CI tip:** If your CI pipeline sets `PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true` and `PUPPETEER_EXECUTABLE_PATH` to the runner's system Chrome, remove those overrides. Puppeteer's bundled Chromium produces measurements closer to common viewing browsers than the GitHub Actions runner's system Chrome, which can measure text 10-16% narrower.
124
+
109
125
  ### Emoji width compensation
110
126
 
111
127
  Headless Chromium (used by mermaid-cli/mmdc) [undermeasures emoji glyph widths](https://stackoverflow.com/q/42016125) on non-Mac platforms. That can make node labels containing emoji clip in the generated SVG. This option tells the plugin to **append invisible `&nbsp;` padding** to emoji-containing node labels *before* passing the source to mmdc, so Puppeteer allocates correct widths.
@@ -144,12 +160,13 @@ mermaid_prebuild:
144
160
 
145
161
  ## Caching
146
162
 
147
- Generated SVGs are cached in `.jekyll-cache/jekyll-mermaid-prebuild/`. The cache key is based on the diagram content (and, when emoji compensation is enabled for that diagram type, the compensated source), so:
163
+ Generated SVGs are cached in `.jekyll-cache/jekyll-mermaid-prebuild/`. The cache key is based on the diagram content (and, when emoji compensation is enabled for that diagram type, the compensated source; when `block_edge_label_padding` is positive, block diagrams also mix in that value), so:
148
164
 
149
165
  - Unchanged diagrams are served from cache (fast rebuilds)
150
166
  - Modified diagrams are automatically regenerated
151
167
  - Different diagrams with different content get different cache keys
152
168
  - Enabling or disabling emoji width compensation for a diagram type invalidates cache for that content (keys include compensated source when applicable)
169
+ - Changing `block_edge_label_padding` invalidates cache keys for **block** diagrams only
153
170
 
154
171
  To clear the cache:
155
172
 
@@ -6,7 +6,7 @@ module JekyllMermaidPrebuild
6
6
  DEFAULT_OUTPUT_DIR = "assets/svg"
7
7
  CACHE_DIR = ".jekyll-cache/jekyll-mermaid-prebuild"
8
8
 
9
- attr_reader :output_dir, :emoji_width_compensation
9
+ attr_reader :output_dir, :emoji_width_compensation, :block_edge_label_padding
10
10
 
11
11
  # Initialize configuration from Jekyll site
12
12
  #
@@ -16,6 +16,7 @@ module JekyllMermaidPrebuild
16
16
  @output_dir = parse_output_dir(config["output_dir"])
17
17
  @enabled = config.fetch("enabled", true)
18
18
  @emoji_width_compensation = parse_emoji_width_compensation(config["emoji_width_compensation"])
19
+ @block_edge_label_padding = parse_block_edge_label_padding(config["block_edge_label_padding"])
19
20
  end
20
21
 
21
22
  # Check if the plugin is enabled
@@ -54,5 +55,16 @@ module JekyllMermaidPrebuild
54
55
  # Strip leading/trailing slashes for consistency
55
56
  dir.gsub(%r{^/+|/+$}, "")
56
57
  end
58
+
59
+ # @param value [Object] raw config (numeric or off)
60
+ # @return [Numeric] non-negative padding in SVG user units; 0 means disabled
61
+ def parse_block_edge_label_padding(value)
62
+ return 0 if value.nil? || value == false
63
+
64
+ num = value.is_a?(Numeric) ? value : nil
65
+ return 0 unless num
66
+
67
+ num.negative? ? 0 : num
68
+ end
57
69
  end
58
70
  end
@@ -18,8 +18,9 @@ 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")
21
22
  # @return [String, nil] path to cached SVG file or nil on failure
22
- def generate(mermaid_source, cache_key)
23
+ def generate(mermaid_source, cache_key, diagram_type: nil)
23
24
  cache_path = File.join(@config.cache_dir, "#{cache_key}.svg")
24
25
 
25
26
  # Return cached file if it exists
@@ -32,6 +33,8 @@ module JekyllMermaidPrebuild
32
33
  success = MmdcWrapper.render(mermaid_source, cache_path)
33
34
  return nil unless success
34
35
 
36
+ post_process_svg(cache_path, diagram_type)
37
+
35
38
  cache_path
36
39
  end
37
40
 
@@ -54,5 +57,17 @@ module JekyllMermaidPrebuild
54
57
  </figure>
55
58
  HTML
56
59
  end
60
+
61
+ private
62
+
63
+ def post_process_svg(cache_path, diagram_type)
64
+ raw = File.read(cache_path)
65
+ svg = SvgPostProcessor.ensure_text_centering(raw)
66
+
67
+ pad = @config.block_edge_label_padding
68
+ svg = SvgPostProcessor.apply(svg, padding: pad) if diagram_type == "block" && pad.is_a?(Numeric) && pad.positive?
69
+
70
+ File.write(cache_path, svg) if svg != raw
71
+ end
57
72
  end
58
73
  end
@@ -46,6 +46,16 @@ module JekyllMermaidPrebuild
46
46
 
47
47
  private
48
48
 
49
+ # @param source [String] mermaid passed to mmdc (after optional emoji compensation)
50
+ # @param diagram_type [String, nil]
51
+ # @return [String] input to MD5 for cache key
52
+ def digest_string_for_cache(source, diagram_type)
53
+ pad = @config.block_edge_label_padding
54
+ return "#{source}\0block_edge_pad=#{pad}" if diagram_type == "block" && pad.is_a?(Numeric) && pad.positive?
55
+
56
+ source
57
+ end
58
+
49
59
  # Convert a single mermaid block to SVG
50
60
  #
51
61
  # @param block [Hash] block info with :content key
@@ -58,8 +68,9 @@ module JekyllMermaidPrebuild
58
68
  else
59
69
  mermaid_source
60
70
  end
61
- cache_key = DigestCalculator.content_digest(source_for_render)
62
- cached_path = @generator.generate(source_for_render, cache_key)
71
+ digest_input = digest_string_for_cache(source_for_render, diagram_type)
72
+ cache_key = DigestCalculator.content_digest(digest_input)
73
+ cached_path = @generator.generate(source_for_render, cache_key, diagram_type: diagram_type)
63
74
 
64
75
  return nil unless cached_path
65
76
 
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JekyllMermaidPrebuild
4
+ # Post-processes mmdc-generated SVGs to fix cross-browser rendering issues.
5
+ #
6
+ # Two independent fixes:
7
+ # 1. Text centering: Mermaid's CSS `text-align: center` targets SVG `<g>` elements where it
8
+ # has no effect on HTML inside `<foreignObject>`. We inject a CSS rule so that foreignObject
9
+ # content centers correctly regardless of text measurement differences between the generating
10
+ # and viewing browsers. Always applied, idempotent.
11
+ # 2. Block edge label padding: Widens block-diagram edge-label `<foreignObject>` widths to
12
+ # prevent clipping when the viewing browser renders text wider than headless Chromium measured.
13
+ # Opt-in via `block_edge_label_padding` config.
14
+ module SvgPostProcessor
15
+ module_function
16
+
17
+ # Opening sequence produced by mmdc for block edge labels (deterministic minified output).
18
+ EDGE_LABEL_FOREIGN_OBJECT_RE = /
19
+ (<g\sclass="edgeLabel"[^>]*><g\sclass="label"[^>]*><foreignObject)
20
+ (\s[^>]+)
21
+ (>)
22
+ /x
23
+
24
+ BLOCK_ROOT_MARKER = 'aria-roledescription="block"'
25
+
26
+ # @param svg_string [String] full SVG document from mmdc
27
+ # @param padding [Numeric] user units to add to each matching foreignObject width (must be positive)
28
+ # @return [String] possibly widened SVG, or the original string on no-op / error
29
+ def apply(svg_string, padding:)
30
+ return svg_string unless svg_string.is_a?(String)
31
+ return svg_string unless padding.is_a?(Numeric) && padding.positive?
32
+ return svg_string unless svg_string.include?(BLOCK_ROOT_MARKER)
33
+
34
+ apply_edge_label_padding(svg_string, padding)
35
+ rescue StandardError
36
+ svg_string
37
+ end
38
+
39
+ CENTERING_RULE = "foreignObject > div{display:block !important;text-align:center;}"
40
+
41
+ # Injects a CSS rule into the SVG <style> block that centers text inside foreignObject divs.
42
+ # Mermaid's own `.node .label { text-align: center }` targets SVG <g> elements where
43
+ # text-align has no effect; this rule targets the HTML div directly.
44
+ # Idempotent: no visual effect when foreignObject width matches text width.
45
+ #
46
+ # @param svg_string [String] full SVG document from mmdc
47
+ # @return [String] SVG with centering rule injected, or original on no-op / error
48
+ def ensure_text_centering(svg_string)
49
+ return svg_string unless svg_string.is_a?(String)
50
+ return svg_string if svg_string.include?(CENTERING_RULE)
51
+ return svg_string unless svg_string.include?("</style>")
52
+
53
+ svg_string.sub("</style>", "#{CENTERING_RULE}</style>")
54
+ rescue StandardError
55
+ svg_string
56
+ end
57
+
58
+ def apply_edge_label_padding(svg_string, padding)
59
+ svg_string.gsub(EDGE_LABEL_FOREIGN_OBJECT_RE) do
60
+ prefix = Regexp.last_match(1)
61
+ attrs = Regexp.last_match(2)
62
+ suffix = Regexp.last_match(3)
63
+ new_attrs = attrs.sub(/\swidth="(\d+(?:\.\d+)?)"/) do
64
+ new_w = Regexp.last_match(1).to_f + padding
65
+ %( width="#{format("%g", new_w)}")
66
+ end
67
+ prefix + new_attrs + suffix
68
+ end
69
+ end
70
+ private_class_method :apply_edge_label_padding
71
+ end
72
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JekyllMermaidPrebuild
4
- VERSION = "0.3.1"
4
+ VERSION = "0.3.2"
5
5
  end
@@ -6,6 +6,7 @@ require_relative "jekyll-mermaid-prebuild/version"
6
6
  require_relative "jekyll-mermaid-prebuild/configuration"
7
7
  require_relative "jekyll-mermaid-prebuild/digest_calculator"
8
8
  require_relative "jekyll-mermaid-prebuild/emoji_compensator"
9
+ require_relative "jekyll-mermaid-prebuild/svg_post_processor"
9
10
  require_relative "jekyll-mermaid-prebuild/mmdc_wrapper"
10
11
  require_relative "jekyll-mermaid-prebuild/generator"
11
12
  require_relative "jekyll-mermaid-prebuild/processor"
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.3.1
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Texarkanine
@@ -147,6 +147,7 @@ files:
147
147
  - lib/jekyll-mermaid-prebuild/hooks.rb
148
148
  - lib/jekyll-mermaid-prebuild/mmdc_wrapper.rb
149
149
  - lib/jekyll-mermaid-prebuild/processor.rb
150
+ - lib/jekyll-mermaid-prebuild/svg_post_processor.rb
150
151
  - lib/jekyll-mermaid-prebuild/version.rb
151
152
  homepage: https://github.com/Texarkanine/jekyll-mermaid-prebuild
152
153
  licenses: