markawesome 0.1.0 → 0.2.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 +7 -17
- data/README.md +57 -21
- data/lib/markawesome/transformer.rb +10 -1
- data/lib/markawesome/transformers/callout_transformer.rb +3 -2
- data/lib/markawesome/transformers/carousel_transformer.rb +127 -0
- data/lib/markawesome/transformers/dialog_transformer.rb +173 -0
- data/lib/markawesome/transformers/image_dialog_transformer.rb +169 -0
- data/lib/markawesome/transformers/tag_transformer.rb +19 -5
- data/lib/markawesome/transformers.rb +3 -0
- data/lib/markawesome/version.rb +1 -1
- data/lib/markawesome.rb +2 -1
- data/markawesome.gemspec +3 -2
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e7ee0dc5c92fb7e9774b22ff56152d96d47a5eaf16da5f5a0181eda5e61aae57
|
|
4
|
+
data.tar.gz: 321ab99ac9d4b1bc7355a898271bcaf14aa8edf7bbeeafebf3b307d2ce0dd041
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: de9cc698634f6e9d741317fa48a98d46619869949ca4641615a08c55504e253ecd4c292e3cc4a6b89ff7c394f277b5671c22c58b38e1f5839b751147a2cc6c5a
|
|
7
|
+
data.tar.gz: 736f49201d6323f34cc9c4ca198db98a8cd34ff0c4ec891649aafce06360f497ea797a697f623ee93f23807461334e05fa5df1fe76b76d39019fdc363c3b109d
|
data/CHANGELOG.md
CHANGED
|
@@ -2,24 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
|
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.2.0] - 2025-10-27
|
|
8
8
|
|
|
9
9
|
### Added
|
|
10
10
|
|
|
11
|
-
- Initial release
|
|
12
|
-
-
|
|
13
|
-
- Support for
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
- Callouts
|
|
17
|
-
- Cards
|
|
18
|
-
- Comparisons
|
|
19
|
-
- Copy Buttons
|
|
20
|
-
- Details
|
|
21
|
-
- Icons
|
|
22
|
-
- Tabs
|
|
23
|
-
- Tags
|
|
24
|
-
- Configuration system for customizing component behavior
|
|
25
|
-
- Framework-agnostic Markdown to Web Awesome transformation
|
|
11
|
+
- Initial release of Markawesome
|
|
12
|
+
- Framework-agnostic Markdown to Web Awesome component transformation
|
|
13
|
+
- Support for Badge, Button, Callout, Card, Carousel, Comparison, Copy Button, Details, Dialog, Icon, Image Dialog, Tabs, and Tag components
|
|
14
|
+
- Configuration system for callout icons and custom components
|
|
15
|
+
- Extracted from jekyll-webawesome gem for reusability across frameworks
|
data/README.md
CHANGED
|
@@ -1,8 +1,30 @@
|
|
|
1
1
|
# Markawesome
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A framework-agnostic Ruby library that transforms custom Markdown syntax into [Web Awesome](https://webawesome.com/) HTML components. Use it with Jekyll, Hugo, Middleman, or any static site generator that processes Markdown.
|
|
4
4
|
|
|
5
|
-
Used as the transformation engine
|
|
5
|
+
Used as the transformation engine for the [jekyll-webawesome](https://github.com/jannewaren/jekyll-webawesome) plugin.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- 🎯 **Framework Agnostic** - Works with any static site generator or Ruby application
|
|
10
|
+
- 🚀 **Simple Syntax** - Clean, intuitive Markdown extensions
|
|
11
|
+
- ⚙️ **Configurable** - Customize icons, variants, and component behavior
|
|
12
|
+
|
|
13
|
+
## Supported components
|
|
14
|
+
|
|
15
|
+
| Component | Primary Syntax | Alternative Syntax | HTML Output |
|
|
16
|
+
|-----------|----------------|-------------------|-------------|
|
|
17
|
+
| **Badge** | `!!!variant` | `:::wa-badge variant` | `<wa-badge variant="brand">content</wa-badge>` |
|
|
18
|
+
| **Button** | `%%%variant` | `:::wa-button variant` | `<wa-button variant="brand" href="url">text</wa-button>` or `<wa-button variant="brand">text</wa-button>` |
|
|
19
|
+
| **Callouts** | `:::info` | `:::wa-callout info` | `<wa-callout variant="brand"><wa-icon name="circle-info"></wa-icon>content</wa-callout>` |
|
|
20
|
+
| **Card** | `===` | `:::wa-card` | `<wa-card>content</wa-card>` |
|
|
21
|
+
| **Carousel** | `~~~~~~` | `:::wa-carousel` | `<wa-carousel>` with carousel items |
|
|
22
|
+
| **Comparison** | `\|\|\|` or `\|\|\|25` | `:::wa-comparison` or `:::wa-comparison 25` | `<wa-comparison>` with before/after slots |
|
|
23
|
+
| **Copy Button** | `<<<` | `:::wa-copy-button` | `<wa-copy-button value="content">content</wa-copy-button>` |
|
|
24
|
+
| **Details** | `^^^appearance? icon-placement?` | `:::wa-details appearance? icon-placement?` | `<wa-details appearance="..." icon-placement="...">content</wa-details>` |
|
|
25
|
+
| **Dialog** | `???params?` | `:::wa-dialog params?` | `<wa-dialog>` with trigger button and content |
|
|
26
|
+
| **Tab Group** | `++++++` | `:::wa-tabs` | `<wa-tab-group><wa-tab>content</wa-tab></wa-tab-group>` |
|
|
27
|
+
| **Tag** | `@@@brand` | `:::wa-tag brand` | `<wa-tag variant="brand">content</wa-tag>` |
|
|
6
28
|
|
|
7
29
|
## Installation
|
|
8
30
|
|
|
@@ -26,36 +48,31 @@ gem install markawesome
|
|
|
26
48
|
|
|
27
49
|
## Usage
|
|
28
50
|
|
|
51
|
+
### Basic Usage
|
|
52
|
+
|
|
29
53
|
```ruby
|
|
30
54
|
require 'markawesome'
|
|
31
55
|
|
|
32
|
-
|
|
56
|
+
markdown = <<~MD
|
|
57
|
+
!!!brand
|
|
58
|
+
New Feature
|
|
59
|
+
!!!
|
|
60
|
+
|
|
33
61
|
:::info
|
|
34
|
-
This is an
|
|
62
|
+
This is an informational callout
|
|
35
63
|
:::
|
|
36
|
-
|
|
64
|
+
MD
|
|
37
65
|
|
|
38
|
-
|
|
66
|
+
html = Markawesome::Transformer.process(markdown)
|
|
39
67
|
```
|
|
40
68
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
| Component | Primary Syntax | Alternative Syntax | HTML Output |
|
|
44
|
-
|-----------|----------------|-------------------|-------------|
|
|
45
|
-
| **Badge** | `!!!variant` | `:::wa-badge variant` | `<wa-badge variant="brand">content</wa-badge>` |
|
|
46
|
-
| **Button** | `%%%variant` | `:::wa-button variant` | `<wa-button variant="brand" href="url">text</wa-button>` or `<wa-button variant="brand">text</wa-button>` |
|
|
47
|
-
| **Callouts** | `:::info` | `:::wa-callout info` | `<wa-callout variant="brand"><wa-icon name="circle-info"></wa-icon>content</wa-callout>` |
|
|
48
|
-
| **Card** | `===` | `:::wa-card` | `<wa-card>content</wa-card>` |
|
|
49
|
-
| **Comparison** | `\|\|\|` or `\|\|\|25` | `:::wa-comparison` or `:::wa-comparison 25` | `<wa-comparison>` with before/after slots |
|
|
50
|
-
| **Copy Button** | `<<<` | `:::wa-copy-button` | `<wa-copy-button value="content">content</wa-copy-button>` |
|
|
51
|
-
| **Details** | `^^^appearance? icon-placement?` | `:::wa-details appearance? icon-placement?` | `<wa-details appearance="..." icon-placement="...">content</wa-details>` |
|
|
52
|
-
| **Tab Group** | `++++++` | `:::wa-tabs` | `<wa-tab-group><wa-tab>content</wa-tab></wa-tab-group>` |
|
|
53
|
-
| **Tag** | `@@@brand` | `:::wa-tag brand` | `<wa-tag variant="brand">content</wa-tag>` |
|
|
69
|
+
### Configuration
|
|
54
70
|
|
|
55
|
-
|
|
71
|
+
Configure Markawesome globally to customize icon names and add custom components:
|
|
56
72
|
|
|
57
73
|
```ruby
|
|
58
74
|
Markawesome.configure do |config|
|
|
75
|
+
# Customize callout icons
|
|
59
76
|
config.callout_icons = {
|
|
60
77
|
info: 'circle-info',
|
|
61
78
|
success: 'circle-check',
|
|
@@ -63,13 +80,32 @@ Markawesome.configure do |config|
|
|
|
63
80
|
warning: 'triangle-exclamation',
|
|
64
81
|
danger: 'circle-exclamation'
|
|
65
82
|
}
|
|
83
|
+
|
|
84
|
+
# Add custom components (for future extensibility)
|
|
85
|
+
config.custom_components = {
|
|
86
|
+
'my-component' => 'MyComponent'
|
|
87
|
+
}
|
|
66
88
|
end
|
|
67
89
|
```
|
|
68
90
|
|
|
91
|
+
### Image Dialog Feature
|
|
92
|
+
|
|
93
|
+
Transform images into clickable dialogs:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
markdown = ''
|
|
97
|
+
|
|
98
|
+
# Enable image dialog transformation
|
|
99
|
+
html = Markawesome::Transformer.process(markdown, image_dialog: true)
|
|
100
|
+
|
|
101
|
+
# Or with configuration
|
|
102
|
+
html = Markawesome::Transformer.process(markdown, image_dialog: { default_width: '80vw' })
|
|
103
|
+
```
|
|
104
|
+
|
|
69
105
|
## Contributing
|
|
70
106
|
|
|
71
107
|
Bug reports and pull requests are welcome on GitHub at https://github.com/jannewaren/markawesome.
|
|
72
108
|
|
|
73
109
|
## License
|
|
74
110
|
|
|
75
|
-
The gem is available as open source under the terms of the [MIT License](
|
|
111
|
+
The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
|
|
@@ -6,14 +6,23 @@ require_relative 'transformers'
|
|
|
6
6
|
module Markawesome
|
|
7
7
|
# Main transformer that orchestrates all component transformers
|
|
8
8
|
class Transformer
|
|
9
|
-
def self.process(content)
|
|
9
|
+
def self.process(content, options = {})
|
|
10
10
|
content = BadgeTransformer.transform(content)
|
|
11
11
|
content = ButtonTransformer.transform(content)
|
|
12
12
|
content = CalloutTransformer.transform(content)
|
|
13
13
|
content = CardTransformer.transform(content)
|
|
14
|
+
content = CarouselTransformer.transform(content)
|
|
14
15
|
content = ComparisonTransformer.transform(content)
|
|
15
16
|
content = CopyButtonTransformer.transform(content)
|
|
16
17
|
content = DetailsTransformer.transform(content)
|
|
18
|
+
|
|
19
|
+
# Apply image dialog transformer BEFORE dialog transformer if enabled
|
|
20
|
+
if options[:image_dialog]
|
|
21
|
+
config = options[:image_dialog].is_a?(Hash) ? options[:image_dialog] : {}
|
|
22
|
+
content = ImageDialogTransformer.transform(content, config)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
content = DialogTransformer.transform(content)
|
|
17
26
|
content = IconTransformer.transform(content)
|
|
18
27
|
content = TagTransformer.transform(content)
|
|
19
28
|
TabsTransformer.transform(content)
|
|
@@ -32,8 +32,9 @@ module Markawesome
|
|
|
32
32
|
private
|
|
33
33
|
|
|
34
34
|
def callout_attributes(variant)
|
|
35
|
+
# Get icon from configuration if available
|
|
35
36
|
config = Markawesome.configuration
|
|
36
|
-
icons = config&.callout_icons ||
|
|
37
|
+
icons = config&.callout_icons || default_icons
|
|
37
38
|
|
|
38
39
|
case variant
|
|
39
40
|
when 'info'
|
|
@@ -69,7 +70,7 @@ module Markawesome
|
|
|
69
70
|
end
|
|
70
71
|
end
|
|
71
72
|
|
|
72
|
-
def
|
|
73
|
+
def default_icons
|
|
73
74
|
{
|
|
74
75
|
info: 'circle-info',
|
|
75
76
|
success: 'circle-check',
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_transformer'
|
|
4
|
+
|
|
5
|
+
module Markawesome
|
|
6
|
+
# Transforms carousel syntax into wa-carousel elements
|
|
7
|
+
# Primary syntax: ~~~~~~params\n~~~ slide1\ncontent\n~~~\n~~~ slide2\ncontent\n~~~\n~~~~~~
|
|
8
|
+
# Alternative syntax: :::wa-carousel params\n~~~ slide1\ncontent\n~~~\n~~~ slide2\ncontent\n~~~\n:::
|
|
9
|
+
# Params can include: numbers (slides-per-page, slides-per-move), keywords (loop, navigation, pagination,
|
|
10
|
+
# autoplay, mouse-dragging, vertical), and CSS properties (scroll-hint:value, aspect-ratio:value, slide-gap:value)
|
|
11
|
+
class CarouselTransformer < BaseTransformer
|
|
12
|
+
def self.transform(content)
|
|
13
|
+
# Define both regex patterns
|
|
14
|
+
# Match: ~~~~~~params (optional)
|
|
15
|
+
# ~~~
|
|
16
|
+
# content (can be empty)
|
|
17
|
+
# ~~~
|
|
18
|
+
# (repeat slides)
|
|
19
|
+
# ~~~~~~
|
|
20
|
+
primary_regex = /^~{6}([^\n]*)\n((?:~~~\n(?:.*?\n)?~~~\n?)+)~{6}/m
|
|
21
|
+
alternative_regex = /^:::wa-carousel\s*([^\n]*)\n((?:~~~\n(?:.*?\n)?~~~\n?)+):::/m
|
|
22
|
+
|
|
23
|
+
# Define shared transformation logic
|
|
24
|
+
transform_proc = proc do |params, slides_block, _third_capture|
|
|
25
|
+
parsed_params = parse_params(params)
|
|
26
|
+
slides = extract_slides(slides_block)
|
|
27
|
+
|
|
28
|
+
build_carousel_html(slides, parsed_params)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Apply both patterns
|
|
32
|
+
patterns = dual_syntax_patterns(primary_regex, alternative_regex, transform_proc)
|
|
33
|
+
apply_multiple_patterns(content, patterns)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def parse_params(params)
|
|
40
|
+
return {} if params.nil? || params.strip.empty?
|
|
41
|
+
|
|
42
|
+
result = {
|
|
43
|
+
attributes: {},
|
|
44
|
+
css_vars: {}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
tokens = params.strip.split(/\s+/)
|
|
48
|
+
numeric_count = 0
|
|
49
|
+
|
|
50
|
+
tokens.each do |token|
|
|
51
|
+
# Check for CSS custom properties (key:value)
|
|
52
|
+
if token.include?(':')
|
|
53
|
+
key, value = token.split(':', 2)
|
|
54
|
+
case key
|
|
55
|
+
when 'scroll-hint'
|
|
56
|
+
result[:css_vars]['--scroll-hint'] = value
|
|
57
|
+
when 'aspect-ratio'
|
|
58
|
+
# Support 'auto', 'none', or 'unset' to remove the default aspect ratio
|
|
59
|
+
# This is useful for text content or variable-height slides
|
|
60
|
+
result[:css_vars]['--aspect-ratio'] = value
|
|
61
|
+
when 'slide-gap'
|
|
62
|
+
result[:css_vars]['--slide-gap'] = value
|
|
63
|
+
end
|
|
64
|
+
# Check for numeric values
|
|
65
|
+
elsif token.match?(/^\d+$/)
|
|
66
|
+
numeric_count += 1
|
|
67
|
+
if numeric_count == 1
|
|
68
|
+
result[:attributes]['slides-per-page'] = token
|
|
69
|
+
elsif numeric_count == 2
|
|
70
|
+
result[:attributes]['slides-per-move'] = token
|
|
71
|
+
end
|
|
72
|
+
# Check for boolean flags
|
|
73
|
+
elsif %w[loop navigation pagination autoplay mouse-dragging vertical].include?(token)
|
|
74
|
+
# For orientation, we need to handle it specially
|
|
75
|
+
if token == 'vertical'
|
|
76
|
+
result[:attributes]['orientation'] = 'vertical'
|
|
77
|
+
else
|
|
78
|
+
result[:attributes][token] = true
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
result
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def extract_slides(slides_block)
|
|
87
|
+
# Extract individual slides using ~~~ markers
|
|
88
|
+
# Handle both content and empty slides
|
|
89
|
+
slide_contents = slides_block.scan(/~~~\n(.*?)~~~(?:\n|$)/m)
|
|
90
|
+
slide_contents.map { |match| match[0].strip }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def build_carousel_html(slides, parsed_params)
|
|
94
|
+
attributes = build_attributes(parsed_params[:attributes] || {})
|
|
95
|
+
style = build_style(parsed_params[:css_vars] || {})
|
|
96
|
+
|
|
97
|
+
attr_string = attributes.empty? ? '' : " #{attributes.join(' ')}"
|
|
98
|
+
style_string = style.empty? ? '' : " style=\"#{style}\""
|
|
99
|
+
|
|
100
|
+
slide_items = slides.map do |slide_content|
|
|
101
|
+
slide_html = markdown_to_html(slide_content)
|
|
102
|
+
"<wa-carousel-item>#{slide_html}</wa-carousel-item>"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
"<wa-carousel#{attr_string}#{style_string}>#{slide_items.join}</wa-carousel>"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def build_attributes(attrs)
|
|
109
|
+
return [] if attrs.nil? || attrs.empty?
|
|
110
|
+
|
|
111
|
+
attrs.map do |key, value|
|
|
112
|
+
if value == true
|
|
113
|
+
key
|
|
114
|
+
else
|
|
115
|
+
"#{key}=\"#{value}\""
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def build_style(css_vars)
|
|
121
|
+
return '' if css_vars.nil? || css_vars.empty?
|
|
122
|
+
|
|
123
|
+
css_vars.map { |key, value| "#{key}: #{value}" }.join('; ')
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require_relative 'base_transformer'
|
|
5
|
+
|
|
6
|
+
module Markawesome
|
|
7
|
+
# Transforms dialog syntax into wa-dialog elements with trigger buttons
|
|
8
|
+
# Primary syntax: ???params\nbutton text\n>>>\ncontent\n???
|
|
9
|
+
# Alternative syntax: :::wa-dialog params\nbutton text\n>>>\ncontent\n:::
|
|
10
|
+
# Params: light-dismiss and optional width (e.g., 500px, 50vw, 40em)
|
|
11
|
+
# Note: Header with close X button is always enabled for accessibility
|
|
12
|
+
class DialogTransformer < BaseTransformer
|
|
13
|
+
def self.transform(content)
|
|
14
|
+
# Define both regex patterns - capture parameter string, button text, and content
|
|
15
|
+
# Params are on the same line as the opening delimiter
|
|
16
|
+
# Button text is on the next line(s) until >>>
|
|
17
|
+
# Content is everything after >>> until the closing delimiter
|
|
18
|
+
primary_regex = /^\?\?\?([^\n]*)$\n(.*?)\n^>>>$\n(.*?)\n^\?\?\?$/m
|
|
19
|
+
alternative_regex = /^:::wa-dialog([^\n]*)$\n(.*?)\n^>>>$\n(.*?)\n^:::$/m
|
|
20
|
+
|
|
21
|
+
# Define shared transformation logic
|
|
22
|
+
transform_proc = proc do |params_string, button_text, dialog_content|
|
|
23
|
+
button_text = button_text.strip
|
|
24
|
+
dialog_content = dialog_content.strip
|
|
25
|
+
|
|
26
|
+
# Parse parameters
|
|
27
|
+
light_dismiss, width = parse_parameters(params_string)
|
|
28
|
+
|
|
29
|
+
# Extract label from first heading or use button text
|
|
30
|
+
label, content_without_label = extract_label(dialog_content, button_text)
|
|
31
|
+
|
|
32
|
+
# Generate unique ID based on content
|
|
33
|
+
dialog_id = generate_dialog_id(button_text, dialog_content)
|
|
34
|
+
|
|
35
|
+
# Convert markdown to HTML
|
|
36
|
+
content_html = markdown_to_html(content_without_label)
|
|
37
|
+
|
|
38
|
+
# Build the dialog HTML
|
|
39
|
+
build_dialog_html(dialog_id, button_text, label, content_html,
|
|
40
|
+
light_dismiss, width)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Apply both patterns
|
|
44
|
+
patterns = dual_syntax_patterns(primary_regex, alternative_regex, transform_proc)
|
|
45
|
+
apply_multiple_patterns(content, patterns)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class << self
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# Parse parameters from the params string
|
|
52
|
+
def parse_parameters(params_string)
|
|
53
|
+
return [false, nil] if params_string.nil? || params_string.strip.empty?
|
|
54
|
+
|
|
55
|
+
tokens = params_string.strip.split(/\s+/)
|
|
56
|
+
|
|
57
|
+
light_dismiss = tokens.include?('light-dismiss')
|
|
58
|
+
|
|
59
|
+
# Look for width parameter (last token with CSS units)
|
|
60
|
+
width = nil
|
|
61
|
+
tokens.reverse_each do |token|
|
|
62
|
+
if token.match?(/^\d+(\.\d+)?(px|em|rem|vw|vh|%|ch)$/)
|
|
63
|
+
width = token
|
|
64
|
+
break
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
[light_dismiss, width]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Extract label from first heading in content
|
|
72
|
+
# Always returns a label - uses heading if available, otherwise default_label
|
|
73
|
+
def extract_label(content, default_label)
|
|
74
|
+
# Check if content starts with a heading
|
|
75
|
+
if content.match(/^#\s+(.+?)$/)
|
|
76
|
+
label = Regexp.last_match(1).strip
|
|
77
|
+
# Remove the heading from content
|
|
78
|
+
content_without_label = content.sub(/^#\s+.+?\n/, '').strip
|
|
79
|
+
[label, content_without_label]
|
|
80
|
+
else
|
|
81
|
+
# Use default label (button text) to ensure header is always shown
|
|
82
|
+
[default_label, content]
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Generate a unique ID for the dialog using MD5 hash
|
|
87
|
+
def generate_dialog_id(button_text, content)
|
|
88
|
+
hash_input = "#{button_text}#{content}"
|
|
89
|
+
hash = Digest::MD5.hexdigest(hash_input)
|
|
90
|
+
"dialog-#{hash[0..7]}" # Use first 8 characters of hash
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Build the complete dialog HTML with trigger button
|
|
94
|
+
# Header with X close button is always enabled for accessibility
|
|
95
|
+
def build_dialog_html(dialog_id, button_text, label, content_html,
|
|
96
|
+
light_dismiss, width)
|
|
97
|
+
# Build dialog attributes
|
|
98
|
+
dialog_attrs = ["id='#{dialog_id}'"]
|
|
99
|
+
# Escape both HTML and attribute characters for label
|
|
100
|
+
# Header is always shown to provide the X close button
|
|
101
|
+
dialog_attrs << "label='#{escape_attribute(escape_html(label))}'"
|
|
102
|
+
dialog_attrs << 'light-dismiss' if light_dismiss
|
|
103
|
+
|
|
104
|
+
# Build style attribute for width if specified
|
|
105
|
+
style_attr = width ? " style='--width: #{width}'" : ''
|
|
106
|
+
|
|
107
|
+
# Check if button contains an image (for image dialog support)
|
|
108
|
+
is_image_button = button_text.include?('<img')
|
|
109
|
+
|
|
110
|
+
# Build the HTML
|
|
111
|
+
html = []
|
|
112
|
+
|
|
113
|
+
# Add CSS Parts styling for image buttons to make them invisible
|
|
114
|
+
if is_image_button
|
|
115
|
+
button_id = "#{dialog_id}-btn"
|
|
116
|
+
html << '<style>'
|
|
117
|
+
html << " ##{button_id}::part(base) {"
|
|
118
|
+
html << ' padding: 0;'
|
|
119
|
+
html << ' margin: 0;'
|
|
120
|
+
html << ' border: none;'
|
|
121
|
+
html << ' background: transparent;'
|
|
122
|
+
html << ' box-shadow: none;'
|
|
123
|
+
html << ' color: inherit;'
|
|
124
|
+
html << ' min-width: 0;'
|
|
125
|
+
html << ' height: auto;'
|
|
126
|
+
html << ' }'
|
|
127
|
+
html << " ##{button_id}::part(base):hover {"
|
|
128
|
+
html << ' background: transparent;'
|
|
129
|
+
html << ' border-color: transparent;'
|
|
130
|
+
html << ' }'
|
|
131
|
+
html << " ##{button_id}::part(base):active {"
|
|
132
|
+
html << ' background: transparent;'
|
|
133
|
+
html << ' border-color: transparent;'
|
|
134
|
+
html << ' }'
|
|
135
|
+
html << '</style>'
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Trigger button
|
|
139
|
+
# Only allow HTML for image tags (for image dialog support), escape everything else for security
|
|
140
|
+
button_content = is_image_button ? button_text : escape_html(button_text)
|
|
141
|
+
button_id_attr = is_image_button ? " id='#{button_id}'" : ''
|
|
142
|
+
button_variant = is_image_button ? " variant='text'" : ''
|
|
143
|
+
html << "<wa-button#{button_id_attr}#{button_variant} data-dialog='open #{dialog_id}'>#{button_content}</wa-button>"
|
|
144
|
+
|
|
145
|
+
# Dialog element
|
|
146
|
+
html << "<wa-dialog #{dialog_attrs.join(' ')}#{style_attr}>"
|
|
147
|
+
html << content_html
|
|
148
|
+
|
|
149
|
+
# Footer with close button
|
|
150
|
+
html << "<wa-button slot='footer' variant='primary' data-dialog='close'>Close</wa-button>"
|
|
151
|
+
|
|
152
|
+
html << '</wa-dialog>'
|
|
153
|
+
|
|
154
|
+
html.join("\n")
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Escape HTML entities in text
|
|
158
|
+
def escape_html(text)
|
|
159
|
+
text.gsub('&', '&')
|
|
160
|
+
.gsub('<', '<')
|
|
161
|
+
.gsub('>', '>')
|
|
162
|
+
.gsub('"', '"')
|
|
163
|
+
.gsub("'", ''')
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Escape attribute values
|
|
167
|
+
def escape_attribute(text)
|
|
168
|
+
text.gsub("'", ''')
|
|
169
|
+
.gsub('"', '"')
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require_relative 'base_transformer'
|
|
5
|
+
|
|
6
|
+
module Markawesome
|
|
7
|
+
# Transforms standalone images into clickable images that open in dialogs
|
|
8
|
+
# Images can opt-out by adding "nodialog" to the title attribute
|
|
9
|
+
# Example: 
|
|
10
|
+
class ImageDialogTransformer < BaseTransformer
|
|
11
|
+
def self.transform(content, config = {})
|
|
12
|
+
# First, protect code blocks, inline code, and comparison blocks from transformation
|
|
13
|
+
protected_content, fenced_code_blocks = protect_fenced_code_blocks(content)
|
|
14
|
+
protected_content, inline_code_blocks = protect_inline_code(protected_content)
|
|
15
|
+
protected_content, comparison_blocks = protect_comparisons(protected_content)
|
|
16
|
+
|
|
17
|
+
# Match markdown images:  or 
|
|
18
|
+
# Capture alt text, URL, and optional title
|
|
19
|
+
# URL can contain spaces and special characters
|
|
20
|
+
image_regex = /!\[([^\]]*)\]\(([^)]+?)(?:\s+"([^"]*)")?\)/
|
|
21
|
+
|
|
22
|
+
result = protected_content.gsub(image_regex) do |match|
|
|
23
|
+
alt_text = Regexp.last_match(1)
|
|
24
|
+
image_url = Regexp.last_match(2).strip
|
|
25
|
+
title = Regexp.last_match(3)
|
|
26
|
+
|
|
27
|
+
# Skip transformation if title contains "nodialog"
|
|
28
|
+
if title&.include?('nodialog')
|
|
29
|
+
# Return original image without dialog
|
|
30
|
+
match
|
|
31
|
+
else
|
|
32
|
+
# Transform to clickable image with dialog
|
|
33
|
+
transform_to_dialog(alt_text, image_url, title, config)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Restore protected blocks in reverse order
|
|
38
|
+
result = restore_comparisons(result, comparison_blocks)
|
|
39
|
+
result = restore_inline_code(result, inline_code_blocks)
|
|
40
|
+
restore_fenced_code_blocks(result, fenced_code_blocks)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class << self
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# Protect fenced code blocks from transformation
|
|
47
|
+
def protect_fenced_code_blocks(content)
|
|
48
|
+
code_blocks = []
|
|
49
|
+
# Match both ``` and ~~~ style code blocks with optional language
|
|
50
|
+
protected = content.gsub(/^```.*?^```$|^~~~.*?^~~~$/m) do |match|
|
|
51
|
+
placeholder = "<!--IMAGE_DIALOG_FENCED_CODE_#{code_blocks.length}-->"
|
|
52
|
+
code_blocks << match
|
|
53
|
+
placeholder
|
|
54
|
+
end
|
|
55
|
+
[protected, code_blocks]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Restore protected fenced code blocks
|
|
59
|
+
def restore_fenced_code_blocks(content, code_blocks)
|
|
60
|
+
code_blocks.each_with_index do |code, index|
|
|
61
|
+
content = content.gsub("<!--IMAGE_DIALOG_FENCED_CODE_#{index}-->", code)
|
|
62
|
+
end
|
|
63
|
+
content
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Protect inline code from transformation
|
|
67
|
+
def protect_inline_code(content)
|
|
68
|
+
code_blocks = []
|
|
69
|
+
protected = content.gsub(/`[^`]+`/) do |match|
|
|
70
|
+
placeholder = "<!--IMAGE_DIALOG_INLINE_CODE_#{code_blocks.length}-->"
|
|
71
|
+
code_blocks << match
|
|
72
|
+
placeholder
|
|
73
|
+
end
|
|
74
|
+
[protected, code_blocks]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Restore protected inline code
|
|
78
|
+
def restore_inline_code(content, code_blocks)
|
|
79
|
+
code_blocks.each_with_index do |code, index|
|
|
80
|
+
content = content.gsub("<!--IMAGE_DIALOG_INLINE_CODE_#{index}-->", code)
|
|
81
|
+
end
|
|
82
|
+
content
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Protect comparison blocks from image transformation
|
|
86
|
+
# Must protect both markdown syntax (|||...|||) and already-transformed HTML
|
|
87
|
+
def protect_comparisons(content)
|
|
88
|
+
comparison_blocks = []
|
|
89
|
+
|
|
90
|
+
# First protect markdown comparison syntax: |||...|||
|
|
91
|
+
protected = content.gsub(/\|\|\|(\d+)?\n.*?\n\|\|\|/m) do |match|
|
|
92
|
+
placeholder = "<!--IMAGE_DIALOG_COMPARISON_#{comparison_blocks.length}-->"
|
|
93
|
+
comparison_blocks << match
|
|
94
|
+
placeholder
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Also protect already-transformed HTML comparison blocks: <wa-comparison ...>...</wa-comparison>
|
|
98
|
+
protected = protected.gsub(/<wa-comparison[^>]*>.*?<\/wa-comparison>/m) do |match|
|
|
99
|
+
placeholder = "<!--IMAGE_DIALOG_COMPARISON_#{comparison_blocks.length}-->"
|
|
100
|
+
comparison_blocks << match
|
|
101
|
+
placeholder
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
[protected, comparison_blocks]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Restore protected comparison blocks
|
|
108
|
+
def restore_comparisons(content, comparison_blocks)
|
|
109
|
+
comparison_blocks.each_with_index do |block, index|
|
|
110
|
+
content = content.gsub("<!--IMAGE_DIALOG_COMPARISON_#{index}-->", block)
|
|
111
|
+
end
|
|
112
|
+
content
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Transform image into our custom dialog syntax
|
|
116
|
+
# This will be processed by DialogTransformer to create the actual wa-dialog
|
|
117
|
+
def transform_to_dialog(alt_text, image_url, title, config = {})
|
|
118
|
+
# Parse width from title if specified (e.g., "50%", "800px", "60vw")
|
|
119
|
+
width = extract_width_from_title(title)
|
|
120
|
+
|
|
121
|
+
# Use default width from config if no width specified in title
|
|
122
|
+
width ||= config[:default_width] if config[:default_width]
|
|
123
|
+
|
|
124
|
+
# Build dialog parameters
|
|
125
|
+
# Always include header with X close button for accessibility
|
|
126
|
+
params = ['light-dismiss']
|
|
127
|
+
params << width if width
|
|
128
|
+
params_string = params.join(' ')
|
|
129
|
+
|
|
130
|
+
# Build the button content - a styled image that acts as the trigger
|
|
131
|
+
# Add title attribute if provided and doesn't contain "nodialog" or width
|
|
132
|
+
title_attr = title && !title.include?('nodialog') && !contains_width?(title) ? " title=\"#{title}\"" : ''
|
|
133
|
+
button_content = "<img src=\"#{image_url}\" alt=\"#{alt_text}\" style=\"cursor: zoom-in; display: block; width: 100%; height: auto;\"#{title_attr} />"
|
|
134
|
+
|
|
135
|
+
# Build the dialog content with alt text as heading for the label
|
|
136
|
+
# Use alt text for the label, or "Image" as fallback if alt is empty
|
|
137
|
+
label_text = alt_text.empty? ? 'Image' : alt_text
|
|
138
|
+
dialog_content = "# #{label_text}\n\n<img src=\"#{image_url}\" alt=\"#{alt_text}\" style=\"max-width: 100%; height: auto; display: block; margin: 0 auto;\" />"
|
|
139
|
+
|
|
140
|
+
# Use our custom dialog syntax that will be processed by DialogTransformer
|
|
141
|
+
# Format: ???params\nbutton_content\n>>>\ndialog_content\n???
|
|
142
|
+
result = []
|
|
143
|
+
result << "???#{params_string}"
|
|
144
|
+
result << button_content
|
|
145
|
+
result << '>>>'
|
|
146
|
+
result << dialog_content
|
|
147
|
+
result << '???'
|
|
148
|
+
|
|
149
|
+
result.join("\n")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Extract width parameter from title attribute
|
|
153
|
+
def extract_width_from_title(title)
|
|
154
|
+
return nil unless title
|
|
155
|
+
|
|
156
|
+
# Match CSS width units: px, em, rem, vw, vh, %, ch
|
|
157
|
+
match = title.match(/(\d+(?:\.\d+)?(?:px|em|rem|vw|vh|%|ch))/)
|
|
158
|
+
match ? match[1] : nil
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Check if title contains a width value
|
|
162
|
+
def contains_width?(title)
|
|
163
|
+
return false unless title
|
|
164
|
+
|
|
165
|
+
title.match?(/\d+(?:\.\d+)?(?:px|em|rem|vw|vh|%|ch)/)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -5,13 +5,18 @@ require_relative 'base_transformer'
|
|
|
5
5
|
module Markawesome
|
|
6
6
|
# Transforms tag syntax into wa-tag elements
|
|
7
7
|
# Primary syntax: @@@variant?\ncontent\n@@@
|
|
8
|
+
# Inline syntax: @@@ variant? content @@@
|
|
8
9
|
# Alternative syntax: :::wa-tag variant?\ncontent\n:::
|
|
9
10
|
# Variants: brand, success, neutral, warning, danger
|
|
10
11
|
class TagTransformer < BaseTransformer
|
|
11
12
|
def self.transform(content)
|
|
12
|
-
# Define
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
# Define regex patterns
|
|
14
|
+
# Block syntax (multiline with newlines) - supports both LF and CRLF
|
|
15
|
+
primary_regex = /^@@@(brand|success|neutral|warning|danger)?\r?\n(.*?)\r?\n@@@/m
|
|
16
|
+
alternative_regex = /^:::wa-tag\s*(brand|success|neutral|warning|danger)?\r?\n(.*?)\r?\n:::/m
|
|
17
|
+
|
|
18
|
+
# Inline syntax (same line with spaces)
|
|
19
|
+
inline_regex = /@@@\s*(brand|success|neutral|warning|danger)?\s+([^@\r\n]+?)\s+@@@/
|
|
15
20
|
|
|
16
21
|
# Define shared transformation logic
|
|
17
22
|
transform_proc = proc do |variant, tag_content|
|
|
@@ -20,8 +25,17 @@ module Markawesome
|
|
|
20
25
|
build_tag_html(tag_content, variant)
|
|
21
26
|
end
|
|
22
27
|
|
|
23
|
-
# Apply
|
|
24
|
-
patterns =
|
|
28
|
+
# Apply all patterns (inline first to avoid conflicts)
|
|
29
|
+
patterns = [
|
|
30
|
+
{
|
|
31
|
+
regex: inline_regex,
|
|
32
|
+
block: proc do |_match, matchdata|
|
|
33
|
+
captures = matchdata.captures
|
|
34
|
+
transform_proc.call(*captures)
|
|
35
|
+
end
|
|
36
|
+
},
|
|
37
|
+
*dual_syntax_patterns(primary_regex, alternative_regex, transform_proc)
|
|
38
|
+
]
|
|
25
39
|
apply_multiple_patterns(content, patterns)
|
|
26
40
|
end
|
|
27
41
|
|
|
@@ -8,9 +8,12 @@ require_relative 'transformers/badge_transformer'
|
|
|
8
8
|
require_relative 'transformers/button_transformer'
|
|
9
9
|
require_relative 'transformers/callout_transformer'
|
|
10
10
|
require_relative 'transformers/card_transformer'
|
|
11
|
+
require_relative 'transformers/carousel_transformer'
|
|
11
12
|
require_relative 'transformers/comparison_transformer'
|
|
12
13
|
require_relative 'transformers/copy_button_transformer'
|
|
13
14
|
require_relative 'transformers/details_transformer'
|
|
15
|
+
require_relative 'transformers/dialog_transformer'
|
|
14
16
|
require_relative 'transformers/icon_transformer'
|
|
17
|
+
require_relative 'transformers/image_dialog_transformer'
|
|
15
18
|
require_relative 'transformers/tabs_transformer'
|
|
16
19
|
require_relative 'transformers/tag_transformer'
|
data/lib/markawesome/version.rb
CHANGED
data/lib/markawesome.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative 'markawesome/version'
|
|
4
4
|
require_relative 'markawesome/transformer'
|
|
5
5
|
|
|
6
|
+
# Main module for Markawesome - framework-agnostic Markdown to Web Awesome component transformer
|
|
6
7
|
module Markawesome
|
|
7
8
|
class Error < StandardError; end
|
|
8
9
|
|
|
@@ -17,7 +18,7 @@ module Markawesome
|
|
|
17
18
|
end
|
|
18
19
|
end
|
|
19
20
|
|
|
20
|
-
# Configuration class for customizing
|
|
21
|
+
# Configuration class for customizing transformations
|
|
21
22
|
class Configuration
|
|
22
23
|
attr_accessor :callout_icons, :custom_components
|
|
23
24
|
|
data/markawesome.gemspec
CHANGED
|
@@ -8,8 +8,9 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
spec.authors = ['Janne Waren']
|
|
9
9
|
spec.email = ['janne.waren@iki.fi']
|
|
10
10
|
|
|
11
|
-
spec.summary = 'Markdown to Web Awesome transformer'
|
|
12
|
-
spec.description = 'A library that transforms custom Markdown syntax into Web Awesome components.'
|
|
11
|
+
spec.summary = 'Framework-agnostic Markdown to Web Awesome component transformer'
|
|
12
|
+
spec.description = 'A library that transforms custom Markdown syntax into Web Awesome components. ' \
|
|
13
|
+
'Framework-agnostic and can be used with Jekyll, Hugo, or any other static site generator.'
|
|
13
14
|
spec.homepage = 'https://github.com/jannewaren/markawesome'
|
|
14
15
|
spec.license = 'MIT'
|
|
15
16
|
spec.required_ruby_version = '>= 3.2'
|
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.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Janne Waren
|
|
@@ -80,6 +80,7 @@ dependencies:
|
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
81
|
version: '1.0'
|
|
82
82
|
description: A library that transforms custom Markdown syntax into Web Awesome components.
|
|
83
|
+
Framework-agnostic and can be used with Jekyll, Hugo, or any other static site generator.
|
|
83
84
|
email:
|
|
84
85
|
- janne.waren@iki.fi
|
|
85
86
|
executables: []
|
|
@@ -97,10 +98,13 @@ files:
|
|
|
97
98
|
- lib/markawesome/transformers/button_transformer.rb
|
|
98
99
|
- lib/markawesome/transformers/callout_transformer.rb
|
|
99
100
|
- lib/markawesome/transformers/card_transformer.rb
|
|
101
|
+
- lib/markawesome/transformers/carousel_transformer.rb
|
|
100
102
|
- lib/markawesome/transformers/comparison_transformer.rb
|
|
101
103
|
- lib/markawesome/transformers/copy_button_transformer.rb
|
|
102
104
|
- lib/markawesome/transformers/details_transformer.rb
|
|
105
|
+
- lib/markawesome/transformers/dialog_transformer.rb
|
|
103
106
|
- lib/markawesome/transformers/icon_transformer.rb
|
|
107
|
+
- lib/markawesome/transformers/image_dialog_transformer.rb
|
|
104
108
|
- lib/markawesome/transformers/tabs_transformer.rb
|
|
105
109
|
- lib/markawesome/transformers/tag_transformer.rb
|
|
106
110
|
- lib/markawesome/version.rb
|
|
@@ -128,5 +132,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
128
132
|
requirements: []
|
|
129
133
|
rubygems_version: 3.6.9
|
|
130
134
|
specification_version: 4
|
|
131
|
-
summary: Markdown to Web Awesome transformer
|
|
135
|
+
summary: Framework-agnostic Markdown to Web Awesome component transformer
|
|
132
136
|
test_files: []
|