jekyll-webawesome 0.6.1 → 0.7.1

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.
@@ -1,175 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'digest'
4
- require_relative 'base_transformer'
5
-
6
- module Jekyll
7
- module WebAwesome
8
- # Transforms dialog syntax into wa-dialog elements with trigger buttons
9
- # Primary syntax: ???params\nbutton text\n>>>\ncontent\n???
10
- # Alternative syntax: :::wa-dialog params\nbutton text\n>>>\ncontent\n:::
11
- # Params: light-dismiss and optional width (e.g., 500px, 50vw, 40em)
12
- # Note: Header with close X button is always enabled for accessibility
13
- class DialogTransformer < BaseTransformer
14
- def self.transform(content)
15
- # Define both regex patterns - capture parameter string, button text, and content
16
- # Params are on the same line as the opening delimiter
17
- # Button text is on the next line(s) until >>>
18
- # Content is everything after >>> until the closing delimiter
19
- primary_regex = /^\?\?\?([^\n]*)$\n(.*?)\n^>>>$\n(.*?)\n^\?\?\?$/m
20
- alternative_regex = /^:::wa-dialog([^\n]*)$\n(.*?)\n^>>>$\n(.*?)\n^:::$/m
21
-
22
- # Define shared transformation logic
23
- transform_proc = proc do |params_string, button_text, dialog_content|
24
- button_text = button_text.strip
25
- dialog_content = dialog_content.strip
26
-
27
- # Parse parameters
28
- light_dismiss, width = parse_parameters(params_string)
29
-
30
- # Extract label from first heading or use button text
31
- label, content_without_label = extract_label(dialog_content, button_text)
32
-
33
- # Generate unique ID based on content
34
- dialog_id = generate_dialog_id(button_text, dialog_content)
35
-
36
- # Convert markdown to HTML
37
- content_html = markdown_to_html(content_without_label)
38
-
39
- # Build the dialog HTML
40
- build_dialog_html(dialog_id, button_text, label, content_html,
41
- light_dismiss, width)
42
- end
43
-
44
- # Apply both patterns
45
- patterns = dual_syntax_patterns(primary_regex, alternative_regex, transform_proc)
46
- apply_multiple_patterns(content, patterns)
47
- end
48
-
49
- class << self
50
- private
51
-
52
- # Parse parameters from the params string
53
- def parse_parameters(params_string)
54
- return [false, nil] if params_string.nil? || params_string.strip.empty?
55
-
56
- tokens = params_string.strip.split(/\s+/)
57
-
58
- light_dismiss = tokens.include?('light-dismiss')
59
-
60
- # Look for width parameter (last token with CSS units)
61
- width = nil
62
- tokens.reverse_each do |token|
63
- if token.match?(/^\d+(\.\d+)?(px|em|rem|vw|vh|%|ch)$/)
64
- width = token
65
- break
66
- end
67
- end
68
-
69
- [light_dismiss, width]
70
- end
71
-
72
- # Extract label from first heading in content
73
- # Always returns a label - uses heading if available, otherwise default_label
74
- def extract_label(content, default_label)
75
- # Check if content starts with a heading
76
- if content.match(/^#\s+(.+?)$/)
77
- label = Regexp.last_match(1).strip
78
- # Remove the heading from content
79
- content_without_label = content.sub(/^#\s+.+?\n/, '').strip
80
- [label, content_without_label]
81
- else
82
- # Use default label (button text) to ensure header is always shown
83
- [default_label, content]
84
- end
85
- end
86
-
87
- # Generate a unique ID for the dialog using MD5 hash
88
- def generate_dialog_id(button_text, content)
89
- hash_input = "#{button_text}#{content}"
90
- hash = Digest::MD5.hexdigest(hash_input)
91
- "dialog-#{hash[0..7]}" # Use first 8 characters of hash
92
- end
93
-
94
- # Build the complete dialog HTML with trigger button
95
- # Header with X close button is always enabled for accessibility
96
- def build_dialog_html(dialog_id, button_text, label, content_html,
97
- light_dismiss, width)
98
- # Build dialog attributes
99
- dialog_attrs = ["id='#{dialog_id}'"]
100
- # Escape both HTML and attribute characters for label
101
- # Header is always shown to provide the X close button
102
- dialog_attrs << "label='#{escape_attribute(escape_html(label))}'"
103
- dialog_attrs << 'light-dismiss' if light_dismiss
104
-
105
- # Build style attribute for width if specified
106
- style_attr = width ? " style='--width: #{width}'" : ''
107
-
108
- # Check if button contains an image (for image dialog support)
109
- is_image_button = button_text.include?('<img')
110
-
111
- # Build the HTML
112
- html = []
113
-
114
- # Add CSS Parts styling for image buttons to make them invisible
115
- if is_image_button
116
- button_id = "#{dialog_id}-btn"
117
- html << '<style>'
118
- html << " ##{button_id}::part(base) {"
119
- html << ' padding: 0;'
120
- html << ' margin: 0;'
121
- html << ' border: none;'
122
- html << ' background: transparent;'
123
- html << ' box-shadow: none;'
124
- html << ' color: inherit;'
125
- html << ' min-width: 0;'
126
- html << ' height: auto;'
127
- html << ' }'
128
- html << " ##{button_id}::part(base):hover {"
129
- html << ' background: transparent;'
130
- html << ' border-color: transparent;'
131
- html << ' }'
132
- html << " ##{button_id}::part(base):active {"
133
- html << ' background: transparent;'
134
- html << ' border-color: transparent;'
135
- html << ' }'
136
- html << '</style>'
137
- end
138
-
139
- # Trigger button
140
- # Only allow HTML for image tags (for image dialog support), escape everything else for security
141
- button_content = is_image_button ? button_text : escape_html(button_text)
142
- button_id_attr = is_image_button ? " id='#{button_id}'" : ''
143
- button_variant = is_image_button ? " variant='text'" : ''
144
- html << "<wa-button#{button_id_attr}#{button_variant} data-dialog='open #{dialog_id}'>#{button_content}</wa-button>"
145
-
146
- # Dialog element
147
- html << "<wa-dialog #{dialog_attrs.join(' ')}#{style_attr}>"
148
- html << content_html
149
-
150
- # Footer with close button
151
- html << "<wa-button slot='footer' variant='primary' data-dialog='close'>Close</wa-button>"
152
-
153
- html << '</wa-dialog>'
154
-
155
- html.join("\n")
156
- end
157
-
158
- # Escape HTML entities in text
159
- def escape_html(text)
160
- text.gsub('&', '&amp;')
161
- .gsub('<', '&lt;')
162
- .gsub('>', '&gt;')
163
- .gsub('"', '&quot;')
164
- .gsub("'", '&#39;')
165
- end
166
-
167
- # Escape attribute values
168
- def escape_attribute(text)
169
- text.gsub("'", '&apos;')
170
- .gsub('"', '&quot;')
171
- end
172
- end
173
- end
174
- end
175
- end
@@ -1,82 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'base_transformer'
4
-
5
- module Jekyll
6
- module WebAwesome
7
- # Transforms icon syntax into wa-icon elements
8
- # Primary syntax: $$$icon-name
9
- # Alternative syntax: :::wa-icon icon-name
10
- #
11
- # Examples:
12
- # $$$settings -> <wa-icon name="settings"></wa-icon>
13
- # $$$home -> <wa-icon name="home"></wa-icon>
14
- # $$$user-circle -> <wa-icon name="user-circle"></wa-icon>
15
- class IconTransformer < BaseTransformer
16
- def self.transform(content)
17
- # Protect code blocks first
18
- protected_content, code_blocks = protect_code_blocks(content)
19
-
20
- # Apply primary syntax transformation
21
- # Only block patterns that look like incomplete icon names:
22
- # $$$icon name (where 'icon name' could be intended as one identifier)
23
- result = protected_content.gsub(/\$\$\$([a-zA-Z0-9\-_]+)(?![a-zA-Z0-9\-_]|\s+name\b)/) do
24
- icon_name = ::Regexp.last_match(1)
25
- build_icon_html(icon_name)
26
- end
27
-
28
- # Apply alternative syntax transformation
29
- result = result.gsub(/:::wa-icon\s+([a-zA-Z0-9\-_]+)\s*\n:::/m) do
30
- icon_name = ::Regexp.last_match(1)
31
- build_icon_html(icon_name)
32
- end
33
-
34
- # Restore code blocks
35
- restore_code_blocks(result, code_blocks)
36
- end
37
-
38
- class << self
39
- private
40
-
41
- def build_icon_html(icon_name)
42
- # Clean and validate icon name
43
- clean_name = icon_name.strip
44
-
45
- # Return the wa-icon element
46
- "<wa-icon name=\"#{clean_name}\"></wa-icon>"
47
- end
48
-
49
- def protect_code_blocks(content)
50
- code_blocks = {}
51
- counter = 0
52
-
53
- # Protect fenced code blocks
54
- protected = content.gsub(/```.*?```/m) do |match|
55
- placeholder = "<!--ICON_PROTECTED_CODE_BLOCK_#{counter}-->"
56
- code_blocks[placeholder] = match
57
- counter += 1
58
- placeholder
59
- end
60
-
61
- # Protect inline code
62
- protected = protected.gsub(/`[^`]+`/) do |match|
63
- placeholder = "<!--ICON_PROTECTED_INLINE_CODE_#{counter}-->"
64
- code_blocks[placeholder] = match
65
- counter += 1
66
- placeholder
67
- end
68
-
69
- [protected, code_blocks]
70
- end
71
-
72
- def restore_code_blocks(content, code_blocks)
73
- result = content
74
- code_blocks.each do |placeholder, original|
75
- result = result.gsub(placeholder, original)
76
- end
77
- result
78
- end
79
- end
80
- end
81
- end
82
- end
@@ -1,174 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'digest'
4
- require_relative 'base_transformer'
5
-
6
- module Jekyll
7
- module WebAwesome
8
- # Transforms standalone images into clickable images that open in dialogs
9
- # Images can opt-out by adding "nodialog" to the title attribute
10
- # Example: ![Alt text](image.png "nodialog")
11
- class ImageDialogTransformer < BaseTransformer
12
- def self.transform(content, site = nil)
13
- # Get configuration including default width
14
- config = site ? Plugin.image_dialog_config(site) : {}
15
-
16
- # First, protect code blocks, inline code, and comparison blocks from transformation
17
- protected_content, fenced_code_blocks = protect_fenced_code_blocks(content)
18
- protected_content, inline_code_blocks = protect_inline_code(protected_content)
19
- protected_content, comparison_blocks = protect_comparisons(protected_content)
20
-
21
- # Match markdown images: ![alt](url) or ![alt](url "title")
22
- # Capture alt text, URL, and optional title
23
- # URL can contain spaces and special characters
24
- image_regex = /!\[([^\]]*)\]\(([^)]+?)(?:\s+"([^"]*)")?\)/
25
-
26
- result = protected_content.gsub(image_regex) do |match|
27
- alt_text = Regexp.last_match(1)
28
- image_url = Regexp.last_match(2).strip
29
- title = Regexp.last_match(3)
30
-
31
- # Skip transformation if title contains "nodialog"
32
- if title&.include?('nodialog')
33
- # Return original image without dialog
34
- match
35
- else
36
- # Transform to clickable image with dialog
37
- transform_to_dialog(alt_text, image_url, title, config)
38
- end
39
- end
40
-
41
- # Restore protected blocks in reverse order
42
- result = restore_comparisons(result, comparison_blocks)
43
- result = restore_inline_code(result, inline_code_blocks)
44
- restore_fenced_code_blocks(result, fenced_code_blocks)
45
- end
46
-
47
- class << self
48
- private
49
-
50
- # Protect fenced code blocks from transformation
51
- def protect_fenced_code_blocks(content)
52
- code_blocks = []
53
- # Match both ``` and ~~~ style code blocks with optional language
54
- protected = content.gsub(/^```.*?^```$|^~~~.*?^~~~$/m) do |match|
55
- placeholder = "<!--IMAGE_DIALOG_FENCED_CODE_#{code_blocks.length}-->"
56
- code_blocks << match
57
- placeholder
58
- end
59
- [protected, code_blocks]
60
- end
61
-
62
- # Restore protected fenced code blocks
63
- def restore_fenced_code_blocks(content, code_blocks)
64
- code_blocks.each_with_index do |code, index|
65
- content = content.gsub("<!--IMAGE_DIALOG_FENCED_CODE_#{index}-->", code)
66
- end
67
- content
68
- end
69
-
70
- # Protect inline code from transformation
71
- def protect_inline_code(content)
72
- code_blocks = []
73
- protected = content.gsub(/`[^`]+`/) do |match|
74
- placeholder = "<!--IMAGE_DIALOG_INLINE_CODE_#{code_blocks.length}-->"
75
- code_blocks << match
76
- placeholder
77
- end
78
- [protected, code_blocks]
79
- end
80
-
81
- # Restore protected inline code
82
- def restore_inline_code(content, code_blocks)
83
- code_blocks.each_with_index do |code, index|
84
- content = content.gsub("<!--IMAGE_DIALOG_INLINE_CODE_#{index}-->", code)
85
- end
86
- content
87
- end
88
-
89
- # Protect comparison blocks from image transformation
90
- # Must protect both markdown syntax (|||...|||) and already-transformed HTML
91
- def protect_comparisons(content)
92
- comparison_blocks = []
93
-
94
- # First protect markdown comparison syntax: |||...|||
95
- protected = content.gsub(/\|\|\|(\d+)?\n.*?\n\|\|\|/m) do |match|
96
- placeholder = "<!--IMAGE_DIALOG_COMPARISON_#{comparison_blocks.length}-->"
97
- comparison_blocks << match
98
- placeholder
99
- end
100
-
101
- # Also protect already-transformed HTML comparison blocks: <wa-comparison ...>...</wa-comparison>
102
- protected = protected.gsub(/<wa-comparison[^>]*>.*?<\/wa-comparison>/m) do |match|
103
- placeholder = "<!--IMAGE_DIALOG_COMPARISON_#{comparison_blocks.length}-->"
104
- comparison_blocks << match
105
- placeholder
106
- end
107
-
108
- [protected, comparison_blocks]
109
- end
110
-
111
- # Restore protected comparison blocks
112
- def restore_comparisons(content, comparison_blocks)
113
- comparison_blocks.each_with_index do |block, index|
114
- content = content.gsub("<!--IMAGE_DIALOG_COMPARISON_#{index}-->", block)
115
- end
116
- content
117
- end
118
-
119
- # Transform image into our custom dialog syntax
120
- # This will be processed by DialogTransformer to create the actual wa-dialog
121
- def transform_to_dialog(alt_text, image_url, title, config = {})
122
- # Parse width from title if specified (e.g., "50%", "800px", "60vw")
123
- width = extract_width_from_title(title)
124
-
125
- # Use default width from config if no width specified in title
126
- width ||= config[:default_width] if config[:default_width]
127
-
128
- # Build dialog parameters
129
- # Always include header with X close button for accessibility
130
- params = ['light-dismiss']
131
- params << width if width
132
- params_string = params.join(' ')
133
-
134
- # Build the button content - a styled image that acts as the trigger
135
- # Add title attribute if provided and doesn't contain "nodialog" or width
136
- title_attr = title && !title.include?('nodialog') && !contains_width?(title) ? " title=\"#{title}\"" : ''
137
- button_content = "<img src=\"#{image_url}\" alt=\"#{alt_text}\" style=\"cursor: zoom-in; display: block; width: 100%; height: auto;\"#{title_attr} />"
138
-
139
- # Build the dialog content with alt text as heading for the label
140
- # Use alt text for the label, or "Image" as fallback if alt is empty
141
- label_text = alt_text.empty? ? 'Image' : alt_text
142
- dialog_content = "# #{label_text}\n\n<img src=\"#{image_url}\" alt=\"#{alt_text}\" style=\"max-width: 100%; height: auto; display: block; margin: 0 auto;\" />"
143
-
144
- # Use our custom dialog syntax that will be processed by DialogTransformer
145
- # Format: ???params\nbutton_content\n>>>\ndialog_content\n???
146
- result = []
147
- result << "???#{params_string}"
148
- result << button_content
149
- result << '>>>'
150
- result << dialog_content
151
- result << '???'
152
-
153
- result.join("\n")
154
- end
155
-
156
- # Extract width parameter from title attribute
157
- def extract_width_from_title(title)
158
- return nil unless title
159
-
160
- # Match CSS width units: px, em, rem, vw, vh, %, ch
161
- match = title.match(/(\d+(?:\.\d+)?(?:px|em|rem|vw|vh|%|ch))/)
162
- match ? match[1] : nil
163
- end
164
-
165
- # Check if title contains a width value
166
- def contains_width?(title)
167
- return false unless title
168
-
169
- title.match?(/\d+(?:\.\d+)?(?:px|em|rem|vw|vh|%|ch)/)
170
- end
171
- end
172
- end
173
- end
174
- end
@@ -1,53 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'base_transformer'
4
-
5
- module Jekyll
6
- module WebAwesome
7
- # Transforms tabs syntax into wa-tab-group elements
8
- # Primary syntax: ++++++placement?\n+++tab1\ncontent\n+++\n+++tab2\ncontent\n+++\n++++++
9
- # Alternative syntax: :::wa-tabs placement?\n+++tab1\ncontent\n+++\n+++tab2\ncontent\n+++\n:::
10
- # Placements: top (default), bottom, start, end
11
- class TabsTransformer < BaseTransformer
12
- def self.transform(content)
13
- # Define both regex patterns
14
- primary_regex = /^\+{6}(top|bottom|start|end)?\n((\+\+\+ [^\n]+\n.*?\n\+\+\+\n?)+)\+{6}/m
15
- alternative_regex = /^:::wa-tabs\s*(top|bottom|start|end)?\n((\+\+\+ [^\n]+\n.*?\n\+\+\+\n?)+):::/m
16
-
17
- # Define shared transformation logic
18
- transform_proc = proc do |placement, tabs_block, _third_capture|
19
- placement ||= 'top'
20
-
21
- tabs, tab_panels = extract_tabs_and_panels(tabs_block)
22
-
23
- "<wa-tab-group placement=\"#{placement}\">#{tabs.join}#{tab_panels.join}</wa-tab-group>"
24
- end
25
-
26
- # Apply both patterns
27
- patterns = dual_syntax_patterns(primary_regex, alternative_regex, transform_proc)
28
- apply_multiple_patterns(content, patterns)
29
- end
30
-
31
- class << self
32
- private
33
-
34
- def extract_tabs_and_panels(tabs_block)
35
- # Extract individual tabs
36
- tab_contents = tabs_block.scan(/^\+\+\+ ([^\n]+)\n(.*?)\n\+\+\+/m)
37
- tabs = []
38
- tab_panels = []
39
-
40
- tab_contents.each_with_index do |(title, panel_content), index|
41
- tab_id = "tab-#{index + 1}"
42
- tabs << "<wa-tab panel=\"#{tab_id}\">#{title.strip}</wa-tab>"
43
-
44
- panel_html = markdown_to_html(panel_content.strip)
45
- tab_panels << "<wa-tab-panel name=\"#{tab_id}\">#{panel_html}</wa-tab-panel>"
46
- end
47
-
48
- [tabs, tab_panels]
49
- end
50
- end
51
- end
52
- end
53
- end
@@ -1,58 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'base_transformer'
4
-
5
- module Jekyll
6
- module WebAwesome
7
- # Transforms tag syntax into wa-tag elements
8
- # Primary syntax: @@@variant?\ncontent\n@@@
9
- # Inline syntax: @@@ variant? content @@@
10
- # Alternative syntax: :::wa-tag variant?\ncontent\n:::
11
- # Variants: brand, success, neutral, warning, danger
12
- class TagTransformer < BaseTransformer
13
- def self.transform(content)
14
- # Define regex patterns
15
- # Block syntax (multiline with newlines) - supports both LF and CRLF
16
- primary_regex = /^@@@(brand|success|neutral|warning|danger)?\r?\n(.*?)\r?\n@@@/m
17
- alternative_regex = /^:::wa-tag\s*(brand|success|neutral|warning|danger)?\r?\n(.*?)\r?\n:::/m
18
-
19
- # Inline syntax (same line with spaces)
20
- inline_regex = /@@@\s*(brand|success|neutral|warning|danger)?\s+([^@\r\n]+?)\s+@@@/
21
-
22
- # Define shared transformation logic
23
- transform_proc = proc do |variant, tag_content|
24
- tag_content = tag_content.strip
25
-
26
- build_tag_html(tag_content, variant)
27
- end
28
-
29
- # Apply all patterns (inline first to avoid conflicts)
30
- patterns = [
31
- {
32
- regex: inline_regex,
33
- block: proc do |_match, matchdata|
34
- captures = matchdata.captures
35
- transform_proc.call(*captures)
36
- end
37
- },
38
- *dual_syntax_patterns(primary_regex, alternative_regex, transform_proc)
39
- ]
40
- apply_multiple_patterns(content, patterns)
41
- end
42
-
43
- class << self
44
- private
45
-
46
- def build_tag_html(content, variant)
47
- variant_attr = variant ? " variant=\"#{variant}\"" : ''
48
- tag_html = markdown_to_html(content).strip
49
-
50
- # Remove paragraph tags if the content is just text
51
- tag_html = tag_html.gsub(%r{^<p>(.*)</p>$}m, '\1')
52
-
53
- "<wa-tag#{variant_attr}>#{tag_html}</wa-tag>"
54
- end
55
- end
56
- end
57
- end
58
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Index file for all Web Awesome transformers
4
- # This file makes it easy to require all transformers at once
5
-
6
- require_relative 'transformers/base_transformer'
7
- require_relative 'transformers/badge_transformer'
8
- require_relative 'transformers/button_transformer'
9
- require_relative 'transformers/callout_transformer'
10
- require_relative 'transformers/card_transformer'
11
- require_relative 'transformers/carousel_transformer'
12
- require_relative 'transformers/comparison_transformer'
13
- require_relative 'transformers/copy_button_transformer'
14
- require_relative 'transformers/details_transformer'
15
- require_relative 'transformers/dialog_transformer'
16
- require_relative 'transformers/icon_transformer'
17
- require_relative 'transformers/image_dialog_transformer'
18
- require_relative 'transformers/tabs_transformer'
19
- require_relative 'transformers/tag_transformer'