markawesome 0.16.0 → 0.17.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 +25 -0
- data/lib/markawesome/plain_markdown_renderer.rb +4 -0
- data/lib/markawesome/transformer.rb +2 -0
- data/lib/markawesome/transformers/date_transformer.rb +209 -0
- data/lib/markawesome/transformers/popover_transformer.rb +32 -33
- data/lib/markawesome/transformers/tabs_transformer.rb +20 -2
- data/lib/markawesome/transformers/tooltip_transformer.rb +18 -11
- data/lib/markawesome/transformers/video_transformer.rb +158 -0
- data/lib/markawesome/transformers.rb +2 -0
- data/lib/markawesome/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a8753b6516f4a04d147b50bd980b9dd2040dc2cba63a87746121b4e25bb8529e
|
|
4
|
+
data.tar.gz: 606d078978520bf6f53c96d4d91f2f1bb66cbdbc85e1d4236d2d3ca18f222c35
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a647d34d02688da9ce8b7a5511f8544123c4943c9f108f66598a9da8246bf6c7130d2138cde68392efc8b7603be42679e88843414847759c69d48d1cfc8297ca
|
|
7
|
+
data.tar.gz: fc0f43062d5ca3a7a2a12d80c8a77cbfc33a856270b1d02dbb5d9203200c591b6b74e768ea3ccc8c48b1aadb607bf9228dd5602acd0bcc81dbc8f8d865a920da
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@ 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
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.17.0] - 2026-06-26
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- New `VideoTransformer` producing Web Awesome's two media components — `<wa-video>` (a single embedded video with custom controls) and `<wa-video-playlist>` (a playlist wrapping multiple `<wa-video>` children). Both are Web Awesome **Pro** (experimental); the markup emitted is declarative and static-site-safe.
|
|
14
|
+
- **Single video** (primary): a `;;;<tokens>` fence, body, closing `;;;`. **Playlist** (primary): a `;;;;;;<tokens>` container wrapping bare `;;;` items (mirroring the carousel `~~~`/`~~~~~~` structure — the bare `;;;` item open keeps the closing `;;;;;;` from being mis-read as an item). Block alternatives: `:::wa-video <tokens>` and `:::wa-video-playlist <tokens>`. Runs in the pipeline immediately after `ComparisonTransformer`.
|
|
15
|
+
- **Body**: the first markdown link `[text](url)` supplies `title`/`src`; the first markdown image `` supplies `poster` (a negative lookbehind keeps the image's `[…]()` from being taken as the link). A block with no link (no `src`) is left untransformed.
|
|
16
|
+
- **Tokens**: `controls:none|standard|full` and `preload:auto|metadata|none` (enum-validated `key:value`; invalid values dropped) plus the boolean flags `autoplay`, `autoplay-muted`, `autoplay-on-visible`, `loop`, `muted` (whole-token matched, so `autoplay-muted` never triggers `autoplay`). The playlist's `controls` preset is forwarded to the container only — children omit it. Deterministic emission order: `src`, `poster`, `title`, `controls`, `preload`, then `autoplay`, `autoplay-muted`, `autoplay-on-visible`, `loop`, `muted`; `src`/`poster`/`title` are HTML-escaped.
|
|
17
|
+
- **Plain-markdown degradation** (`render_as_markdown`, used for `.md` endpoints / llms.txt): a single video degrades to `[title](src)`; a playlist degrades to a bulleted list of `- [title](src)`.
|
|
18
|
+
- Captions (`<track>`) and multi-format `<source>` children are documented v1 follow-ups — a single `src` attribute suffices today.
|
|
19
|
+
- New `DateTransformer` producing Web Awesome's two declarative timestamp components — `<wa-format-date>` (an absolute, locale-formatted date such as "June 26, 2026") and `<wa-relative-time>` ("3 days ago", optionally live-ticking). The date value is baked into the markup at build time; both components are pure declarative wrappers over the browser's `Intl.DateTimeFormat` / `Intl.RelativeTimeFormat`, with no data fetching — ideal for blog post dates, changelog stamps, and "last updated".
|
|
20
|
+
- **Inline syntax** (primary): `[[[ <date> <tokens> ]]]` — triple square brackets, single-line, transformed before Kramdown (runs right after `TooltipTransformer`). `[[[` collides with no other delimiter and survives Kramdown + Jekyll/Liquid.
|
|
21
|
+
- **Block alternative**: `:::wa-format-date <date> <tokens>` / `:::wa-relative-time <date> <tokens>` with an empty body closed by `:::`. The selector name chooses the mode directly.
|
|
22
|
+
- **Mode**: absolute (`<wa-format-date>`) is the default; a bare `relative` token in the inline form switches to `<wa-relative-time>`.
|
|
23
|
+
- **Date token**: the token matching ISO 8601 `YYYY-MM-DD` (optionally `THH:MM[:SS][.s][Z|±HH:MM]`) is passed verbatim (escaped) into `date="…"`. If omitted, `date` is dropped and the component shows the viewer's **current** time (runtime-now). Datetimes use the `T` separator (a space would break tokenization).
|
|
24
|
+
- **format-date formatting**: `style:short|medium|long|full` and `time:short|medium|long|full` presets expand to Web Awesome's granular attributes, with granular overrides (`weekday`, `era`, `year`, `month`, `day`, `hour`, `minute`, `second`, `hour-format`, `time-zone-name`, `time-zone`, `lang`/`locale` → `lang`) winning per key (rightmost-wins). Enum values are validated; invalid values and unknown tokens are silently dropped. A bare date with no style/time/granular field defaults to `style:long`. Deterministic emission order: `date weekday era year month day hour minute second time-zone-name time-zone hour-format lang`.
|
|
25
|
+
- **relative-time formatting**: `format` (`long|short|narrow`, default `long` omitted), `numeric` (`auto|always`, default `auto` omitted), `sync` (boolean live-update flag), and `lang`/`locale` → `lang`. Date/style tokens are ignored in relative mode. Deterministic emission order: `date format numeric sync lang`.
|
|
26
|
+
- **Plain-markdown degradation** (`render_as_markdown`, used for `.md` endpoints / llms.txt): each timestamp degrades to its raw date string (empty for a runtime-now timestamp), since plain text has no locale formatting.
|
|
27
|
+
- **Static-site caveat**: like `<wa-icon>`, both timestamp components render generated text into shadow DOM with no light-DOM fallback — with Web Awesome's JS disabled they show nothing. This matches our existing generated-content model (documented alongside the `<wa-tag with-remove>` caveat); no fallback text is emitted.
|
|
28
|
+
- `PopoverTransformer` and `TooltipTransformer` now accept **all twelve** Web Awesome placements — the four primary (`top`, `bottom`, `left`, `right`) plus the eight aligned variants (`top-start`, `top-end`, `right-start`, `right-end`, `bottom-start`, `bottom-end`, `left-start`, `left-end`) — matching `<wa-popover>`/`<wa-tooltip>`'s full `placement` surface. `AttributeParser` matches whole tokens, so `bottom-start` resolves without colliding with `bottom`.
|
|
29
|
+
- `PopoverTransformer` and `TooltipTransformer` gain a `skidding:N` token, mirroring `distance:N`. It emits `<wa-popover>`/`<wa-tooltip>`'s `skidding` attribute — the offset **along** the target (whereas `distance` is the offset **away** from it). Negative values are allowed (`skidding:-4`). Emission order is deterministic: `placement`, `without-arrow`, `distance`, `skidding` (popover) and `placement`, `distance`, `skidding` (tooltip).
|
|
30
|
+
- `TabsTransformer` supports a per-tab `disabled` flag as a **leading token** on the `+++ ` item header (e.g. `+++ disabled Coming soon`), mirroring the accordion item flags. It emits `<wa-tab disabled>` (the tab renders but cannot be selected) and strips the flag from the label; a non-leading occurrence of the word is left untouched. The `render_as_markdown` degradation strips the flag too (`### Coming soon`). Non-disabled tabs are byte-identical to before.
|
|
31
|
+
|
|
7
32
|
## [0.16.0] - 2026-06-25
|
|
8
33
|
|
|
9
34
|
### Added
|
|
@@ -46,12 +46,14 @@ module Markawesome
|
|
|
46
46
|
layout
|
|
47
47
|
popover
|
|
48
48
|
tooltip
|
|
49
|
+
date
|
|
49
50
|
badge
|
|
50
51
|
button
|
|
51
52
|
callout
|
|
52
53
|
card
|
|
53
54
|
carousel
|
|
54
55
|
comparison
|
|
56
|
+
video
|
|
55
57
|
copy_button
|
|
56
58
|
details
|
|
57
59
|
image_dialog
|
|
@@ -66,12 +68,14 @@ module Markawesome
|
|
|
66
68
|
layout: LayoutTransformer,
|
|
67
69
|
popover: PopoverTransformer,
|
|
68
70
|
tooltip: TooltipTransformer,
|
|
71
|
+
date: DateTransformer,
|
|
69
72
|
badge: BadgeTransformer,
|
|
70
73
|
button: ButtonTransformer,
|
|
71
74
|
callout: CalloutTransformer,
|
|
72
75
|
card: CardTransformer,
|
|
73
76
|
carousel: CarouselTransformer,
|
|
74
77
|
comparison: ComparisonTransformer,
|
|
78
|
+
video: VideoTransformer,
|
|
75
79
|
copy_button: CopyButtonTransformer,
|
|
76
80
|
details: DetailsTransformer,
|
|
77
81
|
image_dialog: ImageDialogTransformer,
|
|
@@ -13,12 +13,14 @@ module Markawesome
|
|
|
13
13
|
content = LayoutTransformer.transform(content)
|
|
14
14
|
content = PopoverTransformer.transform(content)
|
|
15
15
|
content = TooltipTransformer.transform(content)
|
|
16
|
+
content = DateTransformer.transform(content)
|
|
16
17
|
content = BadgeTransformer.transform(content)
|
|
17
18
|
content = ButtonTransformer.transform(content)
|
|
18
19
|
content = CalloutTransformer.transform(content)
|
|
19
20
|
content = CardTransformer.transform(content)
|
|
20
21
|
content = CarouselTransformer.transform(content)
|
|
21
22
|
content = ComparisonTransformer.transform(content)
|
|
23
|
+
content = VideoTransformer.transform(content)
|
|
22
24
|
content = CopyButtonTransformer.transform(content)
|
|
23
25
|
content = DetailsTransformer.transform(content)
|
|
24
26
|
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_transformer'
|
|
4
|
+
|
|
5
|
+
module Markawesome
|
|
6
|
+
# Transforms declarative timestamp syntax into Web Awesome's two timestamp
|
|
7
|
+
# components:
|
|
8
|
+
# <wa-format-date> — an absolute, locale-formatted date ("June 26, 2026")
|
|
9
|
+
# <wa-relative-time> — a relative phrase ("3 days ago"), optionally ticking
|
|
10
|
+
#
|
|
11
|
+
# These are pure declarative wrappers over the browser's Intl.DateTimeFormat /
|
|
12
|
+
# Intl.RelativeTimeFormat: the date value is baked into the markup at build
|
|
13
|
+
# time, with no data fetching. Great for blog dates, changelog stamps, and
|
|
14
|
+
# "last updated".
|
|
15
|
+
#
|
|
16
|
+
# Inline (primary): [[[ <date> <tokens> ]]]
|
|
17
|
+
# Block (alternative): :::wa-format-date <date> <tokens> / :::wa-relative-time …
|
|
18
|
+
# followed by a closing ::: (empty body)
|
|
19
|
+
#
|
|
20
|
+
# Mode: absolute (<wa-format-date>) is the default; a bare `relative` token in
|
|
21
|
+
# the inline form switches to <wa-relative-time>. The block selector name
|
|
22
|
+
# chooses the mode directly.
|
|
23
|
+
#
|
|
24
|
+
# Static-site caveat: both components render generated text into shadow DOM
|
|
25
|
+
# with no light-DOM fallback — with Web Awesome's JS disabled they show
|
|
26
|
+
# nothing. This is identical to <wa-icon> and the other generated-content
|
|
27
|
+
# components we already emit, so it is consistent with our model.
|
|
28
|
+
class DateTransformer < BaseTransformer
|
|
29
|
+
# Inline: content excludes `]`, non-greedy, multiple-per-line, single-line.
|
|
30
|
+
INLINE_REGEX = /\[\[\[[ \t]*([^\]\r\n]+?)[ \t]*\]\]\]/
|
|
31
|
+
# Block: selector name picks the mode; an empty body, closed by `:::`.
|
|
32
|
+
ALTERNATIVE_REGEX = /^:::wa-(format-date|relative-time)[ \t]*([^\n]*)\n:::$/
|
|
33
|
+
|
|
34
|
+
# A token is the date when it is an ISO 8601 date or datetime (datetimes use
|
|
35
|
+
# the `T` separator — a space would break whitespace tokenization).
|
|
36
|
+
DATE_TOKEN_REGEX =
|
|
37
|
+
/\A\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)?\z/
|
|
38
|
+
|
|
39
|
+
# style:/time: presets expand to Web Awesome's granular date/time attributes.
|
|
40
|
+
STYLE_PRESETS = {
|
|
41
|
+
'short' => { 'month' => 'numeric', 'day' => 'numeric', 'year' => '2-digit' },
|
|
42
|
+
'medium' => { 'month' => 'short', 'day' => 'numeric', 'year' => 'numeric' },
|
|
43
|
+
'long' => { 'month' => 'long', 'day' => 'numeric', 'year' => 'numeric' },
|
|
44
|
+
'full' => { 'weekday' => 'long', 'month' => 'long', 'day' => 'numeric', 'year' => 'numeric' }
|
|
45
|
+
}.freeze
|
|
46
|
+
|
|
47
|
+
TIME_PRESETS = {
|
|
48
|
+
'short' => { 'hour' => 'numeric', 'minute' => 'numeric' },
|
|
49
|
+
'medium' => { 'hour' => 'numeric', 'minute' => 'numeric', 'second' => 'numeric' },
|
|
50
|
+
'long' => { 'hour' => 'numeric', 'minute' => 'numeric', 'second' => 'numeric', 'time-zone-name' => 'short' },
|
|
51
|
+
'full' => { 'hour' => 'numeric', 'minute' => 'numeric', 'second' => 'numeric', 'time-zone-name' => 'long' }
|
|
52
|
+
}.freeze
|
|
53
|
+
|
|
54
|
+
# Granular key:value tokens that pass through to the same-named WA attribute,
|
|
55
|
+
# validated against an allowed enum (invalid values dropped).
|
|
56
|
+
GRANULAR_ENUMS = {
|
|
57
|
+
'weekday' => %w[narrow short long],
|
|
58
|
+
'era' => %w[narrow short long],
|
|
59
|
+
'year' => %w[numeric 2-digit],
|
|
60
|
+
'month' => %w[numeric 2-digit narrow short long],
|
|
61
|
+
'day' => %w[numeric 2-digit],
|
|
62
|
+
'hour' => %w[numeric 2-digit],
|
|
63
|
+
'minute' => %w[numeric 2-digit],
|
|
64
|
+
'second' => %w[numeric 2-digit],
|
|
65
|
+
'hour-format' => %w[auto 12 24],
|
|
66
|
+
'time-zone-name' => %w[short long]
|
|
67
|
+
}.freeze
|
|
68
|
+
|
|
69
|
+
# Granular keys that count as an explicit date/time field — their presence
|
|
70
|
+
# (or a style:/time: preset) suppresses the style:long default.
|
|
71
|
+
CONTENT_FIELDS = %w[weekday era year month day hour minute second time-zone-name].freeze
|
|
72
|
+
|
|
73
|
+
# Deterministic emission order (required for byte-for-byte parity).
|
|
74
|
+
FORMAT_DATE_ORDER = %w[date weekday era year month day hour minute second
|
|
75
|
+
time-zone-name time-zone hour-format lang].freeze
|
|
76
|
+
|
|
77
|
+
RELATIVE_FORMATS = %w[long short narrow].freeze
|
|
78
|
+
RELATIVE_NUMERICS = %w[auto always].freeze
|
|
79
|
+
|
|
80
|
+
def self.transform(content)
|
|
81
|
+
patterns = [
|
|
82
|
+
{ regex: INLINE_REGEX, block: proc { |_m, md| render_tokens(md[1], nil) } },
|
|
83
|
+
{ regex: ALTERNATIVE_REGEX,
|
|
84
|
+
block: proc { |_m, md| render_tokens(md[2], md[1] == 'relative-time' ? :relative : :absolute) } }
|
|
85
|
+
]
|
|
86
|
+
apply_multiple_patterns(content, patterns)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Plain-markdown degradation: there is no locale formatting in plain text,
|
|
90
|
+
# so each timestamp degrades to its raw date string (empty when omitted).
|
|
91
|
+
def self.render_as_markdown(content, _options = {})
|
|
92
|
+
patterns = [
|
|
93
|
+
{ regex: INLINE_REGEX, block: proc { |_m, md| extract_date(md[1]) } },
|
|
94
|
+
{ regex: ALTERNATIVE_REGEX, block: proc { |_m, md| extract_date(md[2]) } }
|
|
95
|
+
]
|
|
96
|
+
apply_multiple_patterns(content, patterns)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class << self
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def render_tokens(token_string, mode_override)
|
|
103
|
+
tokens = token_string.to_s.strip.split(/\s+/)
|
|
104
|
+
date, tokens = split_date(tokens)
|
|
105
|
+
mode = mode_override || (tokens.include?('relative') ? :relative : :absolute)
|
|
106
|
+
|
|
107
|
+
mode == :relative ? build_relative_time(date, tokens) : build_format_date(date, tokens)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Pull the first date/datetime token out, leaving the option tokens.
|
|
111
|
+
def split_date(tokens)
|
|
112
|
+
index = tokens.index { |token| token.match?(DATE_TOKEN_REGEX) }
|
|
113
|
+
return [nil, tokens] unless index
|
|
114
|
+
|
|
115
|
+
[tokens[index], tokens[0...index] + tokens[(index + 1)..]]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def extract_date(token_string)
|
|
119
|
+
tokens = token_string.to_s.strip.split(/\s+/)
|
|
120
|
+
tokens.find { |token| token.match?(DATE_TOKEN_REGEX) }.to_s
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def build_format_date(date, tokens)
|
|
124
|
+
attrs = {}
|
|
125
|
+
apply_presets(tokens, attrs)
|
|
126
|
+
apply_granular(tokens, attrs)
|
|
127
|
+
# A bare date (no style/time/granular field) defaults to style:long.
|
|
128
|
+
attrs.merge!(STYLE_PRESETS['long']) if (attrs.keys & CONTENT_FIELDS).empty?
|
|
129
|
+
attrs['date'] = date if date
|
|
130
|
+
|
|
131
|
+
parts = FORMAT_DATE_ORDER.filter_map do |key|
|
|
132
|
+
"#{key}=\"#{escape_html(attrs[key])}\"" if attrs.key?(key)
|
|
133
|
+
end
|
|
134
|
+
build_element('wa-format-date', parts)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Apply the rightmost valid style: and time: presets.
|
|
138
|
+
def apply_presets(tokens, attrs)
|
|
139
|
+
style = nil
|
|
140
|
+
time = nil
|
|
141
|
+
tokens.each do |token|
|
|
142
|
+
if (m = token.match(/\Astyle:(.+)\z/)) && STYLE_PRESETS.key?(m[1])
|
|
143
|
+
style = m[1]
|
|
144
|
+
elsif (m = token.match(/\Atime:(.+)\z/)) && TIME_PRESETS.key?(m[1])
|
|
145
|
+
time = m[1]
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
attrs.merge!(STYLE_PRESETS[style]) if style
|
|
149
|
+
attrs.merge!(TIME_PRESETS[time]) if time
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Apply granular enum keys (override presets) and free-string modifiers
|
|
153
|
+
# (time-zone, lang/locale). Later tokens win.
|
|
154
|
+
def apply_granular(tokens, attrs)
|
|
155
|
+
tokens.each do |token|
|
|
156
|
+
next unless (m = token.match(/\A([a-z-]+):(.+)\z/))
|
|
157
|
+
|
|
158
|
+
key = m[1]
|
|
159
|
+
value = m[2]
|
|
160
|
+
if GRANULAR_ENUMS[key]&.include?(value)
|
|
161
|
+
attrs[key] = value
|
|
162
|
+
elsif key == 'time-zone'
|
|
163
|
+
attrs['time-zone'] = value
|
|
164
|
+
elsif %w[lang locale].include?(key)
|
|
165
|
+
attrs['lang'] = value
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def build_relative_time(date, tokens)
|
|
171
|
+
opts = parse_relative_options(tokens)
|
|
172
|
+
parts = []
|
|
173
|
+
parts << "date=\"#{escape_html(date)}\"" if date
|
|
174
|
+
parts << "format=\"#{opts[:format]}\"" if opts[:format] && opts[:format] != 'long'
|
|
175
|
+
parts << "numeric=\"#{opts[:numeric]}\"" if opts[:numeric] && opts[:numeric] != 'auto'
|
|
176
|
+
parts << 'sync' if opts[:sync]
|
|
177
|
+
parts << "lang=\"#{escape_html(opts[:lang])}\"" if opts[:lang]
|
|
178
|
+
build_element('wa-relative-time', parts)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def parse_relative_options(tokens)
|
|
182
|
+
opts = { format: nil, numeric: nil, sync: false, lang: nil }
|
|
183
|
+
tokens.each do |token|
|
|
184
|
+
if token == 'sync'
|
|
185
|
+
opts[:sync] = true
|
|
186
|
+
elsif (m = token.match(/\Aformat:(.+)\z/)) && RELATIVE_FORMATS.include?(m[1])
|
|
187
|
+
opts[:format] = m[1]
|
|
188
|
+
elsif (m = token.match(/\Anumeric:(.+)\z/)) && RELATIVE_NUMERICS.include?(m[1])
|
|
189
|
+
opts[:numeric] = m[1]
|
|
190
|
+
elsif (m = token.match(/\A(?:lang|locale):(.+)\z/))
|
|
191
|
+
opts[:lang] = m[1]
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
opts
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def build_element(tag, parts)
|
|
198
|
+
return "<#{tag}></#{tag}>" if parts.empty?
|
|
199
|
+
|
|
200
|
+
"<#{tag} #{parts.join(' ')}></#{tag}>"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def escape_html(text)
|
|
204
|
+
text.gsub('&', '&').gsub('<', '<').gsub('>', '>')
|
|
205
|
+
.gsub('"', '"').gsub("'", ''')
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
@@ -11,12 +11,15 @@ module Markawesome
|
|
|
11
11
|
# Inline syntax: &&¶ms? trigger text >>> popover content&&&
|
|
12
12
|
#
|
|
13
13
|
# Params: space-separated tokens (order doesn't matter)
|
|
14
|
-
# Placement: top (default), bottom, left, right
|
|
14
|
+
# Placement: top (default), bottom, left, right, plus the eight aligned
|
|
15
|
+
# variants (top-start, top-end, right-start, …) — the full wa-popover surface
|
|
15
16
|
# Flags: without-arrow
|
|
16
|
-
# Distance: distance:N (e.g., distance:10)
|
|
17
|
+
# Distance: distance:N (e.g., distance:10) — offset away from the target
|
|
18
|
+
# Skidding: skidding:N (e.g., skidding:12, skidding:-4) — offset along the target
|
|
17
19
|
class PopoverTransformer < BaseTransformer
|
|
18
20
|
POPOVER_ATTRIBUTES = {
|
|
19
|
-
placement: %w[top
|
|
21
|
+
placement: %w[top top-start top-end right right-start right-end
|
|
22
|
+
bottom bottom-start bottom-end left left-start left-end],
|
|
20
23
|
without_arrow: %w[without-arrow],
|
|
21
24
|
trigger_style: %w[link]
|
|
22
25
|
}.freeze
|
|
@@ -42,13 +45,13 @@ module Markawesome
|
|
|
42
45
|
popover_content = matchdata[2].strip
|
|
43
46
|
|
|
44
47
|
params_string, trigger_text = parse_inline_trigger_and_params(combined)
|
|
45
|
-
placement, without_arrow, distance, _link_style = parse_parameters(params_string)
|
|
48
|
+
placement, without_arrow, distance, _link_style, skidding = parse_parameters(params_string)
|
|
46
49
|
|
|
47
50
|
popover_id = generate_popover_id(trigger_text, popover_content, seen_ids)
|
|
48
51
|
|
|
49
52
|
build_inline_popover_html(popover_id, trigger_text, popover_content,
|
|
50
53
|
{ placement: placement, without_arrow: without_arrow,
|
|
51
|
-
distance: distance })
|
|
54
|
+
distance: distance, skidding: skidding })
|
|
52
55
|
end
|
|
53
56
|
}
|
|
54
57
|
|
|
@@ -57,7 +60,7 @@ module Markawesome
|
|
|
57
60
|
trigger_text = trigger_text.strip
|
|
58
61
|
popover_content = popover_content.strip
|
|
59
62
|
|
|
60
|
-
placement, without_arrow, distance, link_style = parse_parameters(params_string)
|
|
63
|
+
placement, without_arrow, distance, link_style, skidding = parse_parameters(params_string)
|
|
61
64
|
|
|
62
65
|
popover_id = generate_popover_id(trigger_text, popover_content, seen_ids)
|
|
63
66
|
|
|
@@ -65,7 +68,7 @@ module Markawesome
|
|
|
65
68
|
|
|
66
69
|
build_popover_html(popover_id, trigger_text, content_html,
|
|
67
70
|
{ placement: placement, without_arrow: without_arrow,
|
|
68
|
-
distance: distance, link_style: link_style })
|
|
71
|
+
distance: distance, link_style: link_style, skidding: skidding })
|
|
69
72
|
end
|
|
70
73
|
|
|
71
74
|
# Inline patterns first to avoid conflicts with block patterns
|
|
@@ -106,19 +109,21 @@ module Markawesome
|
|
|
106
109
|
private
|
|
107
110
|
|
|
108
111
|
def parse_parameters(params_string)
|
|
109
|
-
return ['top', false, nil, false] if params_string.nil? || params_string.strip.empty?
|
|
112
|
+
return ['top', false, nil, false, nil] if params_string.nil? || params_string.strip.empty?
|
|
110
113
|
|
|
111
114
|
attributes = AttributeParser.parse(params_string, POPOVER_ATTRIBUTES)
|
|
112
115
|
placement = attributes[:placement] || 'top'
|
|
113
116
|
without_arrow = attributes[:without_arrow] == 'without-arrow'
|
|
114
117
|
link_style = attributes[:trigger_style] == 'link'
|
|
115
118
|
|
|
116
|
-
# Look for distance:N
|
|
119
|
+
# Look for distance:N and skidding:N parameters (skidding may be negative)
|
|
117
120
|
tokens = params_string.strip.split(/\s+/)
|
|
118
121
|
distance_token = tokens.find { |token| token.match?(/^distance:\d+$/) }
|
|
119
122
|
distance = distance_token&.sub('distance:', '')
|
|
123
|
+
skidding_token = tokens.find { |token| token.match?(/^skidding:-?\d+$/) }
|
|
124
|
+
skidding = skidding_token&.sub('skidding:', '')
|
|
120
125
|
|
|
121
|
-
[placement, without_arrow, distance, link_style]
|
|
126
|
+
[placement, without_arrow, distance, link_style, skidding]
|
|
122
127
|
end
|
|
123
128
|
|
|
124
129
|
def generate_popover_id(trigger_text, content, seen_ids)
|
|
@@ -156,42 +161,36 @@ module Markawesome
|
|
|
156
161
|
|
|
157
162
|
def popover_param?(token)
|
|
158
163
|
POPOVER_ATTRIBUTES.any? { |_attr, values| values.include?(token) } ||
|
|
159
|
-
token.match?(/^distance:\d+$/)
|
|
164
|
+
token.match?(/^distance:\d+$/) ||
|
|
165
|
+
token.match?(/^skidding:-?\d+$/)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Build the <wa-popover> attribute list. Emission order is fixed:
|
|
169
|
+
# for, placement, without-arrow, distance, skidding.
|
|
170
|
+
def popover_attributes(popover_id, options)
|
|
171
|
+
attrs = ["for='#{popover_id}'", "placement='#{options[:placement]}'"]
|
|
172
|
+
attrs << 'without-arrow' if options[:without_arrow]
|
|
173
|
+
attrs << "distance='#{options[:distance]}'" if options[:distance]
|
|
174
|
+
attrs << "skidding='#{options[:skidding]}'" if options[:skidding]
|
|
175
|
+
attrs
|
|
160
176
|
end
|
|
161
177
|
|
|
162
178
|
def build_inline_popover_html(popover_id, trigger_text, content_text, options)
|
|
163
179
|
trigger_content = escape_html(trigger_text)
|
|
164
|
-
content_escaped = escape_html(content_text)
|
|
165
|
-
content_escaped = content_escaped.gsub('\n', '<br>')
|
|
166
|
-
|
|
167
|
-
popover_attrs = ["for='#{popover_id}'"]
|
|
168
|
-
popover_attrs << "placement='#{options[:placement]}'"
|
|
169
|
-
popover_attrs << 'without-arrow' if options[:without_arrow]
|
|
170
|
-
popover_attrs << "distance='#{options[:distance]}'" if options[:distance]
|
|
180
|
+
content_escaped = escape_html(content_text).gsub('\n', '<br>')
|
|
171
181
|
|
|
182
|
+
attrs = popover_attributes(popover_id, options)
|
|
172
183
|
trigger = build_trigger(popover_id, trigger_content, true)
|
|
173
184
|
|
|
174
|
-
"#{trigger}<wa-popover #{
|
|
185
|
+
"#{trigger}<wa-popover #{attrs.join(' ')}>#{content_escaped}</wa-popover>"
|
|
175
186
|
end
|
|
176
187
|
|
|
177
188
|
def build_popover_html(popover_id, trigger_text, content_html, options)
|
|
178
|
-
# Escape trigger text for security
|
|
179
189
|
trigger_content = escape_html(trigger_text)
|
|
180
|
-
|
|
181
|
-
# Build popover attributes
|
|
182
|
-
popover_attrs = ["for='#{popover_id}'"]
|
|
183
|
-
popover_attrs << "placement='#{options[:placement]}'"
|
|
184
|
-
popover_attrs << 'without-arrow' if options[:without_arrow]
|
|
185
|
-
popover_attrs << "distance='#{options[:distance]}'" if options[:distance]
|
|
186
|
-
|
|
190
|
+
attrs = popover_attributes(popover_id, options)
|
|
187
191
|
trigger = build_trigger(popover_id, trigger_content, options[:link_style])
|
|
188
192
|
|
|
189
|
-
|
|
190
|
-
html << trigger
|
|
191
|
-
html << "<wa-popover #{popover_attrs.join(' ')}>"
|
|
192
|
-
html << content_html
|
|
193
|
-
html << '</wa-popover>'
|
|
194
|
-
html.join("\n")
|
|
193
|
+
[trigger, "<wa-popover #{attrs.join(' ')}>", content_html, '</wa-popover>'].join("\n")
|
|
195
194
|
end
|
|
196
195
|
|
|
197
196
|
def build_trigger(popover_id, trigger_content, link_style)
|
|
@@ -12,6 +12,8 @@ module Markawesome
|
|
|
12
12
|
# - activation: auto (default), manual
|
|
13
13
|
# - active: panel name to show initially (e.g., "general", "tab-2")
|
|
14
14
|
# - no-scroll-controls: disables scroll arrows
|
|
15
|
+
# Per-tab flag (leading token on the `+++ ` item header, mirroring accordion):
|
|
16
|
+
# - disabled: this tab renders but cannot be selected (e.g., "+++ disabled Coming soon")
|
|
15
17
|
class TabsTransformer < BaseTransformer
|
|
16
18
|
COMPONENT_ATTRIBUTES = {
|
|
17
19
|
placement: %w[top bottom start end],
|
|
@@ -66,7 +68,8 @@ module Markawesome
|
|
|
66
68
|
transform_proc = proc do |_params_string, tabs_block, _third|
|
|
67
69
|
tab_contents = tabs_block.scan(/^\+\+\+ ([^\n]+)\n(.*?)\n\+\+\+/m)
|
|
68
70
|
tab_contents.map do |title, panel_content|
|
|
69
|
-
|
|
71
|
+
label, = parse_tab_header(title)
|
|
72
|
+
"### #{label}\n\n#{panel_content.strip}"
|
|
70
73
|
end.join("\n\n")
|
|
71
74
|
end
|
|
72
75
|
|
|
@@ -85,7 +88,11 @@ module Markawesome
|
|
|
85
88
|
|
|
86
89
|
tab_contents.each_with_index do |(title, panel_content), index|
|
|
87
90
|
tab_id = "tab-#{index + 1}"
|
|
88
|
-
|
|
91
|
+
label, disabled = parse_tab_header(title)
|
|
92
|
+
|
|
93
|
+
tab_attrs = ["panel=\"#{tab_id}\""]
|
|
94
|
+
tab_attrs << 'disabled' if disabled
|
|
95
|
+
tabs << "<wa-tab #{tab_attrs.join(' ')}>#{label}</wa-tab>"
|
|
89
96
|
|
|
90
97
|
panel_html = markdown_to_html(panel_content.strip)
|
|
91
98
|
tab_panels << "<wa-tab-panel name=\"#{tab_id}\">#{panel_html}</wa-tab-panel>"
|
|
@@ -93,6 +100,17 @@ module Markawesome
|
|
|
93
100
|
|
|
94
101
|
[tabs, tab_panels]
|
|
95
102
|
end
|
|
103
|
+
|
|
104
|
+
# Parse a tab item header. A leading `disabled` token (case-sensitive,
|
|
105
|
+
# exactly `disabled` or `disabled `-prefixed) flags the tab as disabled and
|
|
106
|
+
# is stripped from the label; otherwise the label is the stripped title,
|
|
107
|
+
# unchanged. Mirrors accordion's leading item flags.
|
|
108
|
+
def parse_tab_header(title)
|
|
109
|
+
stripped = title.to_s.strip
|
|
110
|
+
return [stripped, false] unless stripped == 'disabled' || stripped.start_with?('disabled ')
|
|
111
|
+
|
|
112
|
+
[stripped.sub(/\Adisabled\s*/, ''), true]
|
|
113
|
+
end
|
|
96
114
|
end
|
|
97
115
|
end
|
|
98
116
|
end
|
|
@@ -13,15 +13,18 @@ module Markawesome
|
|
|
13
13
|
# Alternative block syntax: :::wa-tooltip params\nanchor\n>>>\ntip\n:::
|
|
14
14
|
#
|
|
15
15
|
# Params: space-separated tokens (order doesn't matter)
|
|
16
|
-
# Placement: top (default), bottom, left, right
|
|
17
|
-
#
|
|
16
|
+
# Placement: top (default), bottom, left, right, plus the eight aligned
|
|
17
|
+
# variants (top-start, top-end, right-start, …) — the full wa-tooltip surface
|
|
18
|
+
# Distance: distance:N (e.g., distance:10) — offset away from the target
|
|
19
|
+
# Skidding: skidding:N (e.g., skidding:12, skidding:-4) — offset along the target
|
|
18
20
|
#
|
|
19
21
|
# Tip content is plain text (HTML-escaped), with literal `\n` rendered as
|
|
20
22
|
# <br> — the same surface as the popover's inline form. Tooltips hold brief
|
|
21
23
|
# text, so there is no markdown body.
|
|
22
24
|
class TooltipTransformer < BaseTransformer
|
|
23
25
|
TOOLTIP_ATTRIBUTES = {
|
|
24
|
-
placement: %w[top
|
|
26
|
+
placement: %w[top top-start top-end right right-start right-end
|
|
27
|
+
bottom bottom-start bottom-end left left-start left-end]
|
|
25
28
|
}.freeze
|
|
26
29
|
|
|
27
30
|
# Inline regex (single-line, no newlines allowed): capture 1 = params+anchor,
|
|
@@ -43,12 +46,12 @@ module Markawesome
|
|
|
43
46
|
tip_text = matchdata[2].strip
|
|
44
47
|
|
|
45
48
|
params_string, anchor_text = parse_inline_anchor_and_params(combined)
|
|
46
|
-
placement, distance = parse_parameters(params_string)
|
|
49
|
+
placement, distance, skidding = parse_parameters(params_string)
|
|
47
50
|
|
|
48
51
|
tooltip_id = generate_tooltip_id(anchor_text, tip_text, seen_ids)
|
|
49
52
|
|
|
50
53
|
build_tooltip_html(tooltip_id, anchor_text, tip_text,
|
|
51
|
-
{ placement: placement, distance: distance })
|
|
54
|
+
{ placement: placement, distance: distance, skidding: skidding })
|
|
52
55
|
end
|
|
53
56
|
}
|
|
54
57
|
|
|
@@ -59,12 +62,12 @@ module Markawesome
|
|
|
59
62
|
anchor_text = matchdata[2].strip
|
|
60
63
|
tip_text = matchdata[3].strip
|
|
61
64
|
|
|
62
|
-
placement, distance = parse_parameters(params_string)
|
|
65
|
+
placement, distance, skidding = parse_parameters(params_string)
|
|
63
66
|
|
|
64
67
|
tooltip_id = generate_tooltip_id(anchor_text, tip_text, seen_ids)
|
|
65
68
|
|
|
66
69
|
build_tooltip_html(tooltip_id, anchor_text, tip_text,
|
|
67
|
-
{ placement: placement, distance: distance })
|
|
70
|
+
{ placement: placement, distance: distance, skidding: skidding })
|
|
68
71
|
end
|
|
69
72
|
}
|
|
70
73
|
|
|
@@ -99,17 +102,19 @@ module Markawesome
|
|
|
99
102
|
private
|
|
100
103
|
|
|
101
104
|
def parse_parameters(params_string)
|
|
102
|
-
return ['top', nil] if params_string.nil? || params_string.strip.empty?
|
|
105
|
+
return ['top', nil, nil] if params_string.nil? || params_string.strip.empty?
|
|
103
106
|
|
|
104
107
|
attributes = AttributeParser.parse(params_string, TOOLTIP_ATTRIBUTES)
|
|
105
108
|
placement = attributes[:placement] || 'top'
|
|
106
109
|
|
|
107
|
-
# Look for distance:N
|
|
110
|
+
# Look for distance:N / skidding:N parameters (rightmost-wins; skidding may be negative)
|
|
108
111
|
tokens = params_string.strip.split(/\s+/)
|
|
109
112
|
distance_token = tokens.reverse.find { |token| token.match?(/^distance:\d+$/) }
|
|
110
113
|
distance = distance_token&.sub('distance:', '')
|
|
114
|
+
skidding_token = tokens.reverse.find { |token| token.match?(/^skidding:-?\d+$/) }
|
|
115
|
+
skidding = skidding_token&.sub('skidding:', '')
|
|
111
116
|
|
|
112
|
-
[placement, distance]
|
|
117
|
+
[placement, distance, skidding]
|
|
113
118
|
end
|
|
114
119
|
|
|
115
120
|
def generate_tooltip_id(anchor_text, tip_text, seen_ids)
|
|
@@ -147,7 +152,8 @@ module Markawesome
|
|
|
147
152
|
|
|
148
153
|
def tooltip_param?(token)
|
|
149
154
|
TOOLTIP_ATTRIBUTES.any? { |_attr, values| values.include?(token) } ||
|
|
150
|
-
token.match?(/^distance:\d+$/)
|
|
155
|
+
token.match?(/^distance:\d+$/) ||
|
|
156
|
+
token.match?(/^skidding:-?\d+$/)
|
|
151
157
|
end
|
|
152
158
|
|
|
153
159
|
def build_tooltip_html(tooltip_id, anchor_text, tip_text, options)
|
|
@@ -157,6 +163,7 @@ module Markawesome
|
|
|
157
163
|
tooltip_attrs = ["for=\"#{tooltip_id}\""]
|
|
158
164
|
tooltip_attrs << "placement=\"#{options[:placement]}\""
|
|
159
165
|
tooltip_attrs << "distance=\"#{options[:distance]}\"" if options[:distance]
|
|
166
|
+
tooltip_attrs << "skidding=\"#{options[:skidding]}\"" if options[:skidding]
|
|
160
167
|
|
|
161
168
|
anchor = build_anchor(tooltip_id, anchor_content)
|
|
162
169
|
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_transformer'
|
|
4
|
+
require_relative '../attribute_parser'
|
|
5
|
+
|
|
6
|
+
module Markawesome
|
|
7
|
+
# Transforms video syntax into Web Awesome's two media components:
|
|
8
|
+
# <wa-video> — a single embedded video with custom controls
|
|
9
|
+
# <wa-video-playlist> — a playlist wrapping multiple <wa-video> children
|
|
10
|
+
#
|
|
11
|
+
# Single video
|
|
12
|
+
# Primary: ;;;tokens\n[Title](src)\n\n;;;
|
|
13
|
+
# Alternative: :::wa-video tokens\n…\n:::
|
|
14
|
+
#
|
|
15
|
+
# Playlist (a ;;;;;; container wrapping bare ;;; items, mirroring carousel)
|
|
16
|
+
# Primary: ;;;;;;tokens\n;;;\n[Title](src)\n;;;\n…\n;;;;;;
|
|
17
|
+
# Alternative: :::wa-video-playlist tokens\n;;;\n…\n;;;\n:::
|
|
18
|
+
#
|
|
19
|
+
# Body: the first markdown link `[text](url)` supplies `title`/`src`; the first
|
|
20
|
+
# markdown image `` supplies `poster`. A block with no link (no
|
|
21
|
+
# `src`) is left untransformed.
|
|
22
|
+
#
|
|
23
|
+
# Tokens: `controls:none|standard|full` and `preload:auto|metadata|none`
|
|
24
|
+
# (enum-validated, invalid values dropped) plus the boolean flags
|
|
25
|
+
# `autoplay`, `autoplay-muted`, `autoplay-on-visible`, `loop`, `muted`.
|
|
26
|
+
# Playlist children omit `controls` (the container forwards it to each child).
|
|
27
|
+
class VideoTransformer < BaseTransformer
|
|
28
|
+
# Boolean flags, matched as whole tokens so `autoplay-muted` never triggers
|
|
29
|
+
# `autoplay`. Parsed via AttributeParser (rightmost-wins, order-independent).
|
|
30
|
+
VIDEO_FLAGS = {
|
|
31
|
+
autoplay: %w[autoplay],
|
|
32
|
+
'autoplay-muted': %w[autoplay-muted],
|
|
33
|
+
'autoplay-on-visible': %w[autoplay-on-visible],
|
|
34
|
+
loop: %w[loop],
|
|
35
|
+
muted: %w[muted]
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
# Deterministic emission order for the boolean flags.
|
|
39
|
+
FLAG_ORDER = %w[autoplay autoplay-muted autoplay-on-visible loop muted].freeze
|
|
40
|
+
|
|
41
|
+
CONTROLS_VALUES = %w[none standard full].freeze
|
|
42
|
+
PRELOAD_VALUES = %w[auto metadata none].freeze
|
|
43
|
+
|
|
44
|
+
# Playlist consumes its inner ;;; items first; the bare `;;;\n` item open
|
|
45
|
+
# (no params) is load-bearing — it stops the closing `;;;;;;` from being
|
|
46
|
+
# mis-read as another item (mirrors carousel's `~~~`/`~~~~~~` trick).
|
|
47
|
+
PLAYLIST_PRIMARY = /^;{6}([^\n]*)\n((?:;;;\n(?:.*?\n)?;;;\n?)+);{6}/m
|
|
48
|
+
PLAYLIST_ALT = /^:::wa-video-playlist\s*([^\n]*)\n(.*?)\n:::/m
|
|
49
|
+
# `(?!;)` keeps the single open from matching a leftover `;;;;;;` fence.
|
|
50
|
+
SINGLE_PRIMARY = /^;;;(?!;)([^\n]*)\n(.*?)\n^;;;$/m
|
|
51
|
+
SINGLE_ALT = /^:::wa-video\s*([^\n]*)\n(.*?)\n:::/m
|
|
52
|
+
ITEM_REGEX = /;;;\n(.*?);;;(?:\n|$)/m
|
|
53
|
+
|
|
54
|
+
# First markdown link that is not the `![…]()` of an image (negative
|
|
55
|
+
# lookbehind on `!`) → title + src.
|
|
56
|
+
LINK_REGEX = /(?<!!)\[([^\]]+)\]\(([^)]+)\)/
|
|
57
|
+
# First markdown image → poster.
|
|
58
|
+
IMAGE_REGEX = /!\[([^\]]*)\]\(([^)]+)\)/
|
|
59
|
+
|
|
60
|
+
def self.transform(content)
|
|
61
|
+
patterns = [
|
|
62
|
+
{ regex: PLAYLIST_PRIMARY, block: proc { |_m, md| build_playlist(md[1], md[2]) } },
|
|
63
|
+
{ regex: PLAYLIST_ALT, block: proc { |_m, md| build_playlist(md[1], md[2]) } },
|
|
64
|
+
{ regex: SINGLE_PRIMARY, block: proc { |m, md| build_single(md[1], md[2]) || m } },
|
|
65
|
+
{ regex: SINGLE_ALT, block: proc { |m, md| build_single(md[1], md[2]) || m } }
|
|
66
|
+
]
|
|
67
|
+
apply_multiple_patterns(content, patterns)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.render_as_markdown(content, _options = {})
|
|
71
|
+
patterns = [
|
|
72
|
+
{ regex: PLAYLIST_PRIMARY, block: proc { |_m, md| render_playlist_markdown(md[2]) } },
|
|
73
|
+
{ regex: PLAYLIST_ALT, block: proc { |_m, md| render_playlist_markdown(md[2]) } },
|
|
74
|
+
{ regex: SINGLE_PRIMARY, block: proc { |m, md| render_single_markdown(md[2]) || m } },
|
|
75
|
+
{ regex: SINGLE_ALT, block: proc { |m, md| render_single_markdown(md[2]) || m } }
|
|
76
|
+
]
|
|
77
|
+
apply_multiple_patterns(content, patterns)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
class << self
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def build_single(params, body)
|
|
84
|
+
build_video(params, body, suppress_controls: false)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def build_playlist(params, body)
|
|
88
|
+
controls = parse_tokens(params)[:controls]
|
|
89
|
+
controls_attr = controls ? " controls=\"#{controls}\"" : ''
|
|
90
|
+
children = extract_items(body).filter_map do |item_body|
|
|
91
|
+
build_video('', item_body, suppress_controls: true)
|
|
92
|
+
end
|
|
93
|
+
"<wa-video-playlist#{controls_attr}>#{children.join}</wa-video-playlist>"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Returns the <wa-video> HTML, or nil when the body has no link (no `src`),
|
|
97
|
+
# signalling the caller to leave the block untransformed.
|
|
98
|
+
def build_video(params, body, suppress_controls:)
|
|
99
|
+
tokens = parse_tokens(params)
|
|
100
|
+
title, src, poster = extract_link_and_image(body)
|
|
101
|
+
return nil unless src
|
|
102
|
+
|
|
103
|
+
parts = ["src=\"#{escape_html(src)}\""]
|
|
104
|
+
parts << "poster=\"#{escape_html(poster)}\"" if poster
|
|
105
|
+
parts << "title=\"#{escape_html(title)}\"" if title && !title.empty?
|
|
106
|
+
parts << "controls=\"#{tokens[:controls]}\"" if tokens[:controls] && !suppress_controls
|
|
107
|
+
parts << "preload=\"#{tokens[:preload]}\"" if tokens[:preload]
|
|
108
|
+
FLAG_ORDER.each { |flag| parts << flag if tokens[:flags].include?(flag) }
|
|
109
|
+
|
|
110
|
+
"<wa-video #{parts.join(' ')}></wa-video>"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def parse_tokens(params)
|
|
114
|
+
result = { controls: nil, preload: nil, flags: [] }
|
|
115
|
+
params.to_s.strip.split(/\s+/).each do |token|
|
|
116
|
+
if (m = token.match(/\Acontrols:(.+)\z/)) && CONTROLS_VALUES.include?(m[1])
|
|
117
|
+
result[:controls] = m[1]
|
|
118
|
+
elsif (m = token.match(/\Apreload:(.+)\z/)) && PRELOAD_VALUES.include?(m[1])
|
|
119
|
+
result[:preload] = m[1]
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
result[:flags] = AttributeParser.parse(params, VIDEO_FLAGS).keys.map(&:to_s)
|
|
123
|
+
result
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def extract_items(body)
|
|
127
|
+
body.scan(ITEM_REGEX).map { |match| match[0] }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def extract_link_and_image(body)
|
|
131
|
+
link = body.match(LINK_REGEX)
|
|
132
|
+
image = body.match(IMAGE_REGEX)
|
|
133
|
+
[link && link[1], link && link[2], image && image[2]]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def render_single_markdown(body)
|
|
137
|
+
title, src, = extract_link_and_image(body)
|
|
138
|
+
return nil unless src
|
|
139
|
+
|
|
140
|
+
"[#{title.to_s.empty? ? src : title}](#{src})"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def render_playlist_markdown(body)
|
|
144
|
+
extract_items(body).filter_map do |item_body|
|
|
145
|
+
title, src, = extract_link_and_image(item_body)
|
|
146
|
+
next unless src
|
|
147
|
+
|
|
148
|
+
"- [#{title.to_s.empty? ? src : title}](#{src})"
|
|
149
|
+
end.join("\n")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def escape_html(text)
|
|
153
|
+
text.to_s.gsub('&', '&').gsub('<', '<').gsub('>', '>')
|
|
154
|
+
.gsub('"', '"').gsub("'", ''')
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -12,6 +12,7 @@ require_relative 'transformers/card_transformer'
|
|
|
12
12
|
require_relative 'transformers/carousel_transformer'
|
|
13
13
|
require_relative 'transformers/comparison_transformer'
|
|
14
14
|
require_relative 'transformers/copy_button_transformer'
|
|
15
|
+
require_relative 'transformers/date_transformer'
|
|
15
16
|
require_relative 'transformers/details_transformer'
|
|
16
17
|
require_relative 'transformers/dialog_transformer'
|
|
17
18
|
require_relative 'transformers/icon_transformer'
|
|
@@ -21,3 +22,4 @@ require_relative 'transformers/popover_transformer'
|
|
|
21
22
|
require_relative 'transformers/tabs_transformer'
|
|
22
23
|
require_relative 'transformers/tag_transformer'
|
|
23
24
|
require_relative 'transformers/tooltip_transformer'
|
|
25
|
+
require_relative 'transformers/video_transformer'
|
data/lib/markawesome/version.rb
CHANGED
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.17.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Janne Waren
|
|
@@ -93,6 +93,7 @@ files:
|
|
|
93
93
|
- lib/markawesome/transformers/carousel_transformer.rb
|
|
94
94
|
- lib/markawesome/transformers/comparison_transformer.rb
|
|
95
95
|
- lib/markawesome/transformers/copy_button_transformer.rb
|
|
96
|
+
- lib/markawesome/transformers/date_transformer.rb
|
|
96
97
|
- lib/markawesome/transformers/details_transformer.rb
|
|
97
98
|
- lib/markawesome/transformers/dialog_transformer.rb
|
|
98
99
|
- lib/markawesome/transformers/icon_transformer.rb
|
|
@@ -102,6 +103,7 @@ files:
|
|
|
102
103
|
- lib/markawesome/transformers/tabs_transformer.rb
|
|
103
104
|
- lib/markawesome/transformers/tag_transformer.rb
|
|
104
105
|
- lib/markawesome/transformers/tooltip_transformer.rb
|
|
106
|
+
- lib/markawesome/transformers/video_transformer.rb
|
|
105
107
|
- lib/markawesome/version.rb
|
|
106
108
|
- markawesome.gemspec
|
|
107
109
|
homepage: https://github.com/jannewaren/markawesome
|