markawesome 0.7.0 → 0.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bd4d3d594adb8f57319bfbb9b353d9e1557cf9949417a4da62bd2e8e5f736ed3
4
- data.tar.gz: e1018086c23e3c38f24312fb5a16c5194260083253e17100c7d591a9c93de25f
3
+ metadata.gz: aad59927898fc25ed0af9ae66614cf194c64e89ae06d9ac08dc1b82d810f9c07
4
+ data.tar.gz: 0e54e43e57692c1041fe898e972b703030e09dadb569f5c8cc57a9c385599e79
5
5
  SHA512:
6
- metadata.gz: f3934636dd055cd7e418b276a1eae04f06f4c5a7527213ea6e59cdfd907cd8e9db0874a71790255891a559b907360889202205b36c15d6fa6b2a8c9454ab9c73
7
- data.tar.gz: 83627f19b8439b91f2e2ac241cab1061546b2d50b267aa1096c2a3b96b4ee7b561d3bb53628cbeed2f285a07205331653fd1b6200d746cc0c2362bcfa7333bef
6
+ metadata.gz: ad7fccbc59895042fd63920d75dc6c4cd7505852226736af79dc9b5cf94feb2fca824e5973f68487d41494dd27d22193f2d35b84e3170222bd2e4dcae8fbdc6b
7
+ data.tar.gz: 47dab2727e5dc838ad25ead4a7f882e6ee8245e6e31ac8d5454654ec1b0d24f5b176747ba6d9673b043a2daf53f9e79c28ceff1e1dd90f298a1ea3b4467bd2fd
data/CHANGELOG.md CHANGED
@@ -4,19 +4,42 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [0.7.0] - 2026-02-16
7
+ ## [0.9.0] - 2026-03-12
8
8
 
9
9
  ### Added
10
10
 
11
- - **IconTransformer: Size support** — Append a size token to control icon font-size via inline style. Primary syntax uses `:` separator (`$$$settings:lg`), alternative syntax uses space (`:::wa-icon settings lg`). Available sizes: `xs` (0.75em), `sm` (0.875em), `lg` (1.25em), `xl` (1.5em), `2x` (2em), `3x` (3em), `4x` (4em)
12
- - **IconTransformer: Custom SVG icons** — Use a file path starting with `/` or `./` to render `<wa-icon src="...">` instead of `<wa-icon name="...">`. Supports sizes too (`$$$/assets/icons/shoe.svg:2x`)
11
+ - **PopoverTransformer**: Inline syntax `&&&trigger text >>> popover content&&&` for use within sentences
12
+ - Always renders as link-styled trigger (underlined text button)
13
+ - Supports all parameters: placement, `without-arrow`, `distance:N`
14
+ - Parameters placed before trigger text, separated by spaces
15
+ - Plain text content (HTML-escaped, no markdown processing)
16
+ - Multiple inline popovers supported on the same line
13
17
 
14
- ## [0.6.0] - 2026-02-14
18
+ ## [0.8.0] - 2026-03-12
19
+
20
+ ### Added
21
+
22
+ - **PopoverTransformer**: New `&&&` syntax for `wa-popover` floating content component
23
+ - Trigger text and popover content separated by `>>>` (same pattern as dialog/details)
24
+ - Placement options: `top` (default), `bottom`, `left`, `right`
25
+ - `without-arrow` flag to hide the popover arrow
26
+ - `distance:N` parameter for custom trigger-to-popover distance in pixels
27
+ - `link` flag to render trigger as a link-styled element instead of a button
28
+ - Unique anchor IDs generated via MD5 hash for trigger/popover pairing
29
+ - Alternative `:::wa-popover` syntax supported
30
+ - Markdown content support in popover body via Kramdown
31
+ - HTML escaping for trigger text (XSS prevention)
32
+
33
+ ## [0.7.0]
15
34
 
16
35
  ### Changed
17
36
 
18
37
  - **BREAKING: Card header syntax** — Card headers now use `**bold text**` instead of `# heading`. The first bold-only line inside a card block becomes the header slot. This avoids markdownlint warnings about multiple top-level headings (MD025) and incorrect heading levels (MD001), and better reflects that card titles are not semantic document headings.
19
38
 
39
+ ## [0.6.0] - 2026-02-14
40
+
41
+ _(Version bump included LayoutTransformer and icon slot syntax — see 0.5.0 and 0.4.0 entries below.)_
42
+
20
43
  ## [0.5.0] - 2026-02-10
21
44
 
22
45
  ### Added
data/README.md CHANGED
@@ -23,50 +23,12 @@ Used as the transformation engine for the [jekyll-webawesome](https://github.com
23
23
  | **Copy Button** | `<<<` | `:::wa-copy-button` | `<wa-copy-button value="content">content</wa-copy-button>` |
24
24
  | **Details** | `^^^appearance? icon-placement?` | `:::wa-details appearance? icon-placement?` | `<wa-details>content</wa-details>` |
25
25
  | **Dialog** | `???params?` | `:::wa-dialog params?` | `<wa-dialog>` with trigger button and content |
26
- | **Icon** | `$$$icon-name` or `$$$icon-name:size` | `:::wa-icon icon-name [size]` | `<wa-icon name="icon-name"></wa-icon>` |
26
+ | **Icon** | `$$$icon-name` | `:::wa-icon icon-name` | `<wa-icon name="icon-name"></wa-icon>` |
27
27
  | **Image Dialog** | `![alt](url)` | — | Wraps images in clickable `<wa-dialog>` overlays |
28
+ | **Popover** | `&&&params?` or inline `&&&trigger >>> content&&&` | `:::wa-popover params?` | `<wa-popover>` with trigger button and content |
28
29
  | **Tab Group** | `++++++` | `:::wa-tabs` | `<wa-tab-group><wa-tab>content</wa-tab></wa-tab-group>` |
29
30
  | **Tag** | `@@@brand` | `:::wa-tag brand` | `<wa-tag variant="brand">content</wa-tag>` |
30
31
 
31
- ## Icon
32
-
33
- ### Named icons
34
-
35
- Use a Web Awesome icon name:
36
-
37
- ```markdown
38
- Click $$$settings to configure.
39
- A large $$$home:lg icon.
40
- ```
41
-
42
- ### Custom SVG icons
43
-
44
- Point to a local SVG file by starting the path with `/` or `./`:
45
-
46
- ```markdown
47
- $$$/assets/icons/shoe.svg
48
- $$$/assets/icons/shoe.svg:2x
49
- $$$./icons/logo.svg:xl
50
- ```
51
-
52
- This outputs `<wa-icon src="/assets/icons/shoe.svg"></wa-icon>` instead of using the `name` attribute.
53
-
54
- ### Sizes
55
-
56
- Append a size token after `:` (primary syntax) or as a space-separated parameter (alternative syntax):
57
-
58
- | Token | CSS `font-size` |
59
- |-------|----------------|
60
- | `xs` | 0.75em |
61
- | `sm` | 0.875em |
62
- | `lg` | 1.25em |
63
- | `xl` | 1.5em |
64
- | `2x` | 2em |
65
- | `3x` | 3em |
66
- | `4x` | 4em |
67
-
68
- Without a size token, the icon inherits the surrounding font size.
69
-
70
32
  ## Layout Utilities
71
33
 
72
34
  Layout utilities use `::::` (quadruple colon) syntax to wrap content in CSS layout containers. Inner content is not markdown-converted, so component `:::` syntax inside layouts works normally.
@@ -24,6 +24,7 @@ module Markawesome
24
24
  end
25
25
 
26
26
  content = DialogTransformer.transform(content)
27
+ content = PopoverTransformer.transform(content)
27
28
  content = IconTransformer.transform(content)
28
29
  content = TagTransformer.transform(content)
29
30
  TabsTransformer.transform(content)
@@ -4,46 +4,30 @@ require_relative 'base_transformer'
4
4
 
5
5
  module Markawesome
6
6
  # Transforms icon syntax into wa-icon elements
7
- # Primary syntax: $$$icon-name or $$$icon-name:size
8
- # Custom SVG syntax: $$$/path/to/icon.svg or $$$/path/to/icon.svg:size
9
- # Alternative syntax: :::wa-icon icon-name [size] or :::wa-icon /path/to/icon.svg [size]
7
+ # Primary syntax: $$$icon-name
8
+ # Alternative syntax: :::wa-icon icon-name
10
9
  #
11
10
  # Examples:
12
11
  # $$$settings -> <wa-icon name="settings"></wa-icon>
13
- # $$$settings:lg -> <wa-icon name="settings" style="font-size: 1.25em;"></wa-icon>
14
- # $$$/assets/icons/shoe.svg -> <wa-icon src="/assets/icons/shoe.svg"></wa-icon>
15
- # $$$/assets/icons/shoe.svg:2x -> <wa-icon src="/assets/icons/shoe.svg" style="font-size: 2em;"></wa-icon>
16
- #
17
- # Available sizes: xs (0.75em), sm (0.875em), lg (1.25em), xl (1.5em), 2x (2em), 3x (3em), 4x (4em)
12
+ # $$$home -> <wa-icon name="home"></wa-icon>
13
+ # $$$user-circle -> <wa-icon name="user-circle"></wa-icon>
18
14
  class IconTransformer < BaseTransformer
19
- SIZES = {
20
- 'xs' => '0.75em',
21
- 'sm' => '0.875em',
22
- 'lg' => '1.25em',
23
- 'xl' => '1.5em',
24
- '2x' => '2em',
25
- '3x' => '3em',
26
- '4x' => '4em'
27
- }.freeze
28
-
29
15
  def self.transform(content)
30
16
  # Protect code blocks first
31
17
  protected_content, code_blocks = protect_code_blocks(content)
32
18
 
33
19
  # Apply primary syntax transformation
34
- # Supports named icons ($$$settings:lg) and custom SVG paths ($$$/path/to/icon.svg:2x)
35
- result = protected_content.gsub(%r{\$\$\$(\.?/[a-zA-Z0-9\-_./]+|[a-zA-Z0-9\-_]+)(?::([a-zA-Z0-9]+))?(?![a-zA-Z0-9\-_]|\s+name\b)}) do
36
- icon_ref = ::Regexp.last_match(1)
37
- size = ::Regexp.last_match(2)
38
- build_icon_html(icon_ref, size)
20
+ # Only block patterns that look like incomplete icon names:
21
+ # $$$icon name (where 'icon name' could be intended as one identifier)
22
+ result = protected_content.gsub(/\$\$\$([a-zA-Z0-9\-_]+)(?![a-zA-Z0-9\-_]|\s+name\b)/) do
23
+ icon_name = ::Regexp.last_match(1)
24
+ build_icon_html(icon_name)
39
25
  end
40
26
 
41
27
  # Apply alternative syntax transformation
42
- # Supports named icons and custom SVG paths with optional size
43
- result = result.gsub(%r{:::wa-icon\s+(\.?/[a-zA-Z0-9\-_./]+|[a-zA-Z0-9\-_]+)(?:\s+([a-zA-Z0-9]+))?\s*\n:::}m) do
44
- icon_ref = ::Regexp.last_match(1)
45
- size = ::Regexp.last_match(2)
46
- build_icon_html(icon_ref, size)
28
+ result = result.gsub(/:::wa-icon\s+([a-zA-Z0-9\-_]+)\s*\n:::/m) do
29
+ icon_name = ::Regexp.last_match(1)
30
+ build_icon_html(icon_name)
47
31
  end
48
32
 
49
33
  # Restore code blocks
@@ -53,20 +37,12 @@ module Markawesome
53
37
  class << self
54
38
  private
55
39
 
56
- def build_icon_html(icon_ref, size = nil)
57
- clean_ref = icon_ref.strip
58
-
59
- # Paths (starting with / or ./) use src attribute, otherwise use name
60
- attr = if clean_ref.include?('/')
61
- "src=\"#{clean_ref}\""
62
- else
63
- "name=\"#{clean_ref}\""
64
- end
65
-
66
- # Build style attribute for size
67
- style = SIZES[size] ? " style=\"font-size: #{SIZES[size]};\"" : ''
40
+ def build_icon_html(icon_name)
41
+ # Clean and validate icon name
42
+ clean_name = icon_name.strip
68
43
 
69
- "<wa-icon #{attr}#{style}></wa-icon>"
44
+ # Return the wa-icon element
45
+ "<wa-icon name=\"#{clean_name}\"></wa-icon>"
70
46
  end
71
47
 
72
48
  def protect_code_blocks(content)
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require_relative 'base_transformer'
5
+ require_relative '../attribute_parser'
6
+
7
+ module Markawesome
8
+ # Transforms popover syntax into wa-popover elements with trigger buttons
9
+ # Primary syntax: &&&params\ntrigger text\n>>>\ncontent\n&&&
10
+ # Alternative syntax: :::wa-popover params\ntrigger text\n>>>\ncontent\n:::
11
+ # Inline syntax: &&&params? trigger text >>> popover content&&&
12
+ #
13
+ # Params: space-separated tokens (order doesn't matter)
14
+ # Placement: top (default), bottom, left, right
15
+ # Flags: without-arrow
16
+ # Distance: distance:N (e.g., distance:10)
17
+ class PopoverTransformer < BaseTransformer
18
+ POPOVER_ATTRIBUTES = {
19
+ placement: %w[top bottom left right],
20
+ without_arrow: %w[without-arrow],
21
+ trigger_style: %w[link]
22
+ }.freeze
23
+
24
+ def self.transform(content)
25
+ # Inline regex (single-line, no newlines allowed)
26
+ inline_regex = /&&&[ \t]*([^\r\n]*?)[ \t]*>>>[ \t]*([^\r\n]+?)[ \t]*&&&/
27
+
28
+ # Block regexes (multiline)
29
+ primary_regex = /^&&&([^\n]*)$\n(.*?)\n^>>>$\n(.*?)\n^&&&$/m
30
+ alternative_regex = /^:::wa-popover([^\n]*)$\n(.*?)\n^>>>$\n(.*?)\n^:::$/m
31
+
32
+ # Inline transformation (always link style, plain text content)
33
+ inline_transform = {
34
+ regex: inline_regex,
35
+ block: proc do |_match, matchdata|
36
+ combined = matchdata[1]
37
+ popover_content = matchdata[2].strip
38
+
39
+ params_string, trigger_text = parse_inline_trigger_and_params(combined)
40
+ placement, without_arrow, distance, _link_style = parse_parameters(params_string)
41
+
42
+ popover_id = generate_popover_id(trigger_text, popover_content)
43
+
44
+ build_inline_popover_html(popover_id, trigger_text, popover_content,
45
+ { placement: placement, without_arrow: without_arrow,
46
+ distance: distance })
47
+ end
48
+ }
49
+
50
+ # Block transformation (existing)
51
+ transform_proc = proc do |params_string, trigger_text, popover_content|
52
+ trigger_text = trigger_text.strip
53
+ popover_content = popover_content.strip
54
+
55
+ placement, without_arrow, distance, link_style = parse_parameters(params_string)
56
+
57
+ popover_id = generate_popover_id(trigger_text, popover_content)
58
+
59
+ content_html = markdown_to_html(popover_content)
60
+
61
+ build_popover_html(popover_id, trigger_text, content_html,
62
+ { placement: placement, without_arrow: without_arrow,
63
+ distance: distance, link_style: link_style })
64
+ end
65
+
66
+ # Inline patterns first to avoid conflicts with block patterns
67
+ patterns = [
68
+ inline_transform,
69
+ *dual_syntax_patterns(primary_regex, alternative_regex, transform_proc)
70
+ ]
71
+ apply_multiple_patterns(content, patterns)
72
+ end
73
+
74
+ class << self
75
+ private
76
+
77
+ def parse_parameters(params_string)
78
+ return ['top', false, nil, false] if params_string.nil? || params_string.strip.empty?
79
+
80
+ attributes = AttributeParser.parse(params_string, POPOVER_ATTRIBUTES)
81
+ placement = attributes[:placement] || 'top'
82
+ without_arrow = attributes[:without_arrow] == 'without-arrow'
83
+ link_style = attributes[:trigger_style] == 'link'
84
+
85
+ # Look for distance:N parameter
86
+ tokens = params_string.strip.split(/\s+/)
87
+ distance_token = tokens.find { |token| token.match?(/^distance:\d+$/) }
88
+ distance = distance_token&.sub('distance:', '')
89
+
90
+ [placement, without_arrow, distance, link_style]
91
+ end
92
+
93
+ def generate_popover_id(trigger_text, content)
94
+ hash_input = "#{trigger_text}#{content}"
95
+ hash = Digest::MD5.hexdigest(hash_input)
96
+ "popover-#{hash[0..7]}"
97
+ end
98
+
99
+ def parse_inline_trigger_and_params(combined_string)
100
+ tokens = combined_string.strip.split(/\s+/)
101
+ param_tokens = []
102
+ trigger_tokens = []
103
+ found_content = false
104
+
105
+ tokens.each do |token|
106
+ if !found_content && popover_param?(token)
107
+ param_tokens << token
108
+ else
109
+ found_content = true
110
+ trigger_tokens << token
111
+ end
112
+ end
113
+
114
+ trigger_text = trigger_tokens.join(' ')
115
+
116
+ # If no trigger text remains, treat entire string as trigger (no params)
117
+ if trigger_text.empty?
118
+ ['', combined_string.strip]
119
+ else
120
+ [param_tokens.join(' '), trigger_text]
121
+ end
122
+ end
123
+
124
+ def popover_param?(token)
125
+ POPOVER_ATTRIBUTES.any? { |_attr, values| values.include?(token) } ||
126
+ token.match?(/^distance:\d+$/)
127
+ end
128
+
129
+ def build_inline_popover_html(popover_id, trigger_text, content_text, options)
130
+ trigger_content = escape_html(trigger_text)
131
+ content_escaped = escape_html(content_text)
132
+
133
+ popover_attrs = ["for='#{popover_id}'"]
134
+ popover_attrs << "placement='#{options[:placement]}'"
135
+ popover_attrs << 'without-arrow' if options[:without_arrow]
136
+ popover_attrs << "distance='#{options[:distance]}'" if options[:distance]
137
+
138
+ trigger = build_trigger(popover_id, trigger_content, true)
139
+
140
+ "#{trigger}<wa-popover #{popover_attrs.join(' ')}>#{content_escaped}</wa-popover>"
141
+ end
142
+
143
+ def build_popover_html(popover_id, trigger_text, content_html, options)
144
+ # Escape trigger text for security
145
+ trigger_content = escape_html(trigger_text)
146
+
147
+ # Build popover attributes
148
+ popover_attrs = ["for='#{popover_id}'"]
149
+ popover_attrs << "placement='#{options[:placement]}'"
150
+ popover_attrs << 'without-arrow' if options[:without_arrow]
151
+ popover_attrs << "distance='#{options[:distance]}'" if options[:distance]
152
+
153
+ trigger = build_trigger(popover_id, trigger_content, options[:link_style])
154
+
155
+ html = []
156
+ html << trigger
157
+ html << "<wa-popover #{popover_attrs.join(' ')}>"
158
+ html << content_html
159
+ html << '</wa-popover>'
160
+ html.join("\n")
161
+ end
162
+
163
+ def build_trigger(popover_id, trigger_content, link_style)
164
+ if link_style
165
+ link_style_attr = 'background: none; border: none; padding: 0; ' \
166
+ 'color: inherit; text-decoration: underline; ' \
167
+ 'cursor: pointer; font: inherit;'
168
+ "<button id='#{popover_id}' style='#{link_style_attr}'>#{trigger_content}</button>"
169
+ else
170
+ "<wa-button id='#{popover_id}' variant='text'>#{trigger_content}</wa-button>"
171
+ end
172
+ end
173
+
174
+ def escape_html(text)
175
+ text.gsub('&', '&amp;')
176
+ .gsub('<', '&lt;')
177
+ .gsub('>', '&gt;')
178
+ .gsub('"', '&quot;')
179
+ .gsub("'", '&#39;')
180
+ end
181
+ end
182
+ end
183
+ end
@@ -16,5 +16,6 @@ require_relative 'transformers/dialog_transformer'
16
16
  require_relative 'transformers/icon_transformer'
17
17
  require_relative 'transformers/image_dialog_transformer'
18
18
  require_relative 'transformers/layout_transformer'
19
+ require_relative 'transformers/popover_transformer'
19
20
  require_relative 'transformers/tabs_transformer'
20
21
  require_relative 'transformers/tag_transformer'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Markawesome
4
- VERSION = '0.7.0'
4
+ VERSION = '0.9.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: markawesome
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Janne Waren
@@ -108,6 +108,7 @@ files:
108
108
  - lib/markawesome/transformers/icon_transformer.rb
109
109
  - lib/markawesome/transformers/image_dialog_transformer.rb
110
110
  - lib/markawesome/transformers/layout_transformer.rb
111
+ - lib/markawesome/transformers/popover_transformer.rb
111
112
  - lib/markawesome/transformers/tabs_transformer.rb
112
113
  - lib/markawesome/transformers/tag_transformer.rb
113
114
  - lib/markawesome/version.rb