markawesome 0.18.1 → 0.20.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: 6c2c5ebb3f9f231f78078d8ff504c309a0a20a2b786835db6763b79ec02c2ecb
4
- data.tar.gz: c2828be808439565b2ff460feaf052ed97640fd9605a973897656d86b71160e6
3
+ metadata.gz: '09defacfcfa437cfd7c5406b0705161597633880fac312488a7fa29c3055754d'
4
+ data.tar.gz: 369cd9e0d91dc6a423501367f98e2e54391a04aaebc4cf9da9c79d1c3a4267aa
5
5
  SHA512:
6
- metadata.gz: 43d62e1980cd64700193e51dfef75007b4a64382ee70b4623283e24d29bd9dc6b0043834411bad243131abf57328587008704c858da90a4dc2d33a3410db9faf
7
- data.tar.gz: 2668f52f02046b64911bbc7cb979fb4d9cd6c061eb811f3f06a7f21d0a46eb859adf300d836c30b1e544f2b03fabc39617a384c83c12ae70f1159db7b1424c02
6
+ metadata.gz: fa42c7657b0abac3554309763a38a75a8c6bc08d39b8a28e85b9fa0b57ea931207705efa0ff194c097582996986871a382b09872508dad2cfe325fc957350efa
7
+ data.tar.gz: 4a509a216fd0ce3aea3238e85dbf8cfea8fc60c7d55e9209e7e9915862b933708afd896a9582cc8c630bbbaf41caf759285f8687a5bff8df45c386d7a058617a
data/CHANGELOG.md CHANGED
@@ -6,6 +6,35 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.20.0] - 2026-10-09
10
+
11
+ ### Added
12
+
13
+ - **Labeled divider (`<wa-divider>` label, Web Awesome 3.13.0+).** New `DividerTransformer` with a mirrored one-line shorthand, `--- label ---`, and a `:::wa-divider params?` alternative whose body is the label (empty for a bare divider). Leading tokens are consumed from the left: `start`/`center`/`end` → `label-placement`, `horizontal`/`vertical` → `orientation`, `icon:name` → a `<wa-icon>` before the label. The rest is the label, rendered as inline Markdown. Output: `<wa-divider orientation="…" label-placement="…"><wa-icon name="…"></wa-icon> Label</wa-divider>`, attributes only when given. Plain `---` and `--- ---` are untouched and stay `<hr>`, and so is any line made only of `|`, `:`, `-` and whitespace, which keeps pipe-less GFM table separators such as `--- | ---` intact. The plain-Markdown renderer degrades it to `---` followed by the label as its own paragraph (icon dropped), with a leading blank line so the rule can't turn a preceding line into a setext heading. Known limitation, shared with every transformer: only backtick code fences are protected, so a `--- x ---` line inside a `~~~` fence is transformed.
14
+ - **Display-only stepper (`<wa-stepper>` / `<wa-step>`, Web Awesome 3.14.0, experimental).** New `StepperTransformer`: a `>>>>>> params?` fence (or `:::wa-stepper params?`) wrapping a Markdown task list. Each unindented list line is a step: `[x]`/`[X]` → `completed`; leading `active`, `loading`, `disabled`, a variant (`neutral`/`brand`/`success`/`warning`/`danger`), an attention (`none`/`pulse`/`bounce`) and `icon:name` (→ `slot="icon"`) are flags; the rest is the label as inline Markdown. Lines indented two or more spaces under a step are joined into `<span slot="description">`. Steps are named `step-1…N` and the container's `active` is the step flagged `active` (last wins), else the first incomplete step, else the last. Container params: `horizontal`/`vertical`/`auto` → `orientation`, `label:"…"` → `label` (HTML-escaped). Interactivity (`clickable`, `linear`, step invokers) is intentionally omitted — the steps have no content panels to navigate between. The `>>>>>>` fence never collides with the `^>>>$` separator of details/dialog/popover/tooltip/random-content. Plain-Markdown degrade: an ordered task list keeping `[x]`/`[ ]`, the current step in bold, and descriptions as indented continuation lines.
15
+ - Both run **first** in the pipeline (right after code-block protection, before layouts), so a divider or stepper can sit inside layouts, cards, callouts, tabs, accordion and dialog bodies — e.g. `--- vertical vs ---` between two buttons in a `::::cluster`. Same position in `PlainMarkdownRenderer::PIPELINE` (new `:divider` / `:stepper` override keys).
16
+ - Manifest coverage now also guards `wa-divider` `orientation`/`label-placement`, `wa-stepper` `orientation` and `wa-step` `variant`/`attention` (`DIVIDER_ATTRIBUTES`, `STEPPER_ATTRIBUTES`, `STEP_ATTRIBUTES`).
17
+
18
+ ### Internal
19
+
20
+ - **Refreshed the Web Awesome manifest fixture to 3.14.0** (September 24th, 2026; covers 3.13.0 too). `rake wa:manifest[3.14.0]` regenerated `spec/fixtures/webawesome-enums.json` and `WA_VERSION` moved to `3.14.0`. The diff is **purely additive** — three new tags (`wa-step`, `wa-stepper`, `wa-tag-input`) and a `label-placement` enum on `wa-divider` — with **zero DRIFT and zero GAP** against the enums we accept, so no transformer constant changed. `<wa-accordion>` and `<wa-random-content>` are now **stable** in Web Awesome, so the "experimental" wording was dropped from their transformer comments. Fixture remains byte-identical to `markawesome-js`'s copy. Example-site CDN pins bumped to 3.14.0 in `jekyll-webawesome` and `eleventy-plugin-webawesome`.
21
+
22
+ ## [0.19.0] - 2026-08-26
23
+
24
+ ### Fixed
25
+
26
+ - **Repeated dialogs no longer share a DOM id.** The id is an MD5 of the trigger text plus body, so two dialogs differing only by a param (e.g. width) collided — and byte-identical ones always did. Both triggers then emitted the same `data-dialog='open …'`, so `getElementById` resolved every one of them to the first dialog and the others were unreachable. Repeats now get a `-2`, `-3`, … suffix, the scheme `generate_popover_id`/`generate_tooltip_id` already used. The first occurrence keeps its bare id, so existing single-dialog output is unchanged.
27
+
28
+ ### Changed
29
+
30
+ - **Image-dialog trigger buttons now style both `::part(base)` and `::part(button)`.** Web Awesome 3.11.0 gives every component a CSS part named after itself and softly deprecates the generic `base` part — for `<wa-button>` the wrapper part is now `button`, and WA's own manifest marks `base` as *"Deprecated. Use the `button` part instead."* The chrome-stripping `<style>` block emitted for an image trigger (`![alt](src)` under `image_dialog: true`) now lists both selectors in each of its three rules, so the trigger stays chrome-less on 3.10-pinned sites (which have no `button` part) **and** on 3.11+ sites once `base` is eventually removed. Purely additive — no existing selector was dropped. The style block moved into a private `image_button_style` helper; output is byte-identical to `markawesome-js` and now locked by specs in `spec/dialog_transformer_spec.rb` (the emitted selectors were previously untested in either engine).
31
+
32
+ ### Internal
33
+
34
+ - **Refreshed the Web Awesome manifest fixture to 3.12.0** (August 2026). `rake wa:manifest[3.12.0]` regenerated `spec/fixtures/webawesome-enums.json` and `WA_VERSION` moved to `3.12.0`. The enum diff is a **single additive line** — `wa-dropdown-item` gained a `target` enum (`_blank`/`_parent`/`_self`/`_top`) alongside its new `href`/`rel`/`download` navigation attributes — on a component that is [explicitly out of scope](ROADMAP.md) for a static-Markdown model, so there is **zero DRIFT and zero GAP** against the enums we accept and no transformer constant changed. Nothing else in WA 3.12.0 touches a component we emit: the other additions are `<wa-data-grid>`'s `filterOptions` (JS-supplied data) and the Figma kit, and every fix/change is internal to Web Awesome's own shadow DOM. Two of those fixes land on our output for free — `<wa-button>` now keeps its accessible name and gains `aria-busy` while loading (we already accept a bare `loading` token in `BUTTON_ATTRIBUTES`), and duplicate landmarks were removed from `<wa-card>`, `<wa-dialog>` and `<wa-drawer>`. Fixture remains byte-identical to `markawesome-js`'s copy. Example-site CDN pins bumped to 3.12.0 in `jekyll-webawesome` and `eleventy-plugin-webawesome`.
35
+ - **Refreshed the Web Awesome manifest fixture to 3.11.0** (July 30th, 2026). `rake wa:manifest[3.11.0]` regenerated `spec/fixtures/webawesome-enums.json` and `WA_VERSION` moved to `3.11.0`. The diff was **purely additive** — four new tags (`wa-otp-input`, `wa-pagination`, and `wa-toast`/`wa-toast-item`, which moved from Pro to Core) — with **zero DRIFT and zero GAP** against the enums we accept, so no transformer constant changed. `IconAttributes::SCHEMA` was separately verified against WA 3.11.0's `IconAnimation` (18 values) and `IconCanvas` types and still matches exactly, as do every slot our transformers emit (`footer`, `icon`, `summary`). Fixture remains byte-identical to `markawesome-js`'s copy.
36
+ - **Web Awesome manifest coverage spec** (`spec/webawesome_manifest_coverage_spec.rb`) — a deterministic guard that checks every enum our transformers accept against a vendored slice of Web Awesome's machine-readable Custom Elements Manifest, pinned to **WA 3.11.0**. For 39 `(constant → wa-tag/attribute)` mappings across 15 components it fails on **DRIFT** (a value we accept that WA no longer lists — exempting the deliberate `callout` `info`→`brand` input alias) and **GAP** (a WA value we don't expose, gated by an `INTENTIONALLY_OMITTED` map that starts empty, so a new WA value turns the build red until adopted or recorded with a reason). 6 entries WA can't describe as an inline enum (`wa-icon` family/variant/animation/canvas are freeform/opaque types; `wa-video` is absent from WA core's manifest) are surfaced as `skip`s, not silently dropped. The fixture `spec/fixtures/webawesome-enums.json` is a **derived artifact** regenerated with `rake wa:manifest[X.Y.Z]` (`script/update_wa_manifest.rb`) — never hand-edited, and byte-identical to `markawesome-js`'s copy. Dev-only (not packaged in the gem); no version bump. See CLAUDE.md → "Web Awesome manifest coverage" for the WA-bump workflow.
37
+
9
38
  ## [0.18.1] - 2026-07-04
10
39
 
11
40
  ### Fixed
data/README.md CHANGED
@@ -4,6 +4,8 @@ A framework-agnostic Ruby library that transforms custom Markdown syntax into [W
4
4
 
5
5
  Used as the transformation engine for the [jekyll-webawesome](https://github.com/jannewaren/jekyll-webawesome) plugin.
6
6
 
7
+ > **🔗 See it live — [hosted examples site](https://jannewaren.github.io/jekyll-webawesome/).** Every supported component shown side by side: the Markawesome markdown on the left, the live Web Awesome component it renders on the right. The clearest way to see what this project does.
8
+
7
9
  ## Features
8
10
 
9
11
  - 🎯 **Framework Agnostic** - Works with any static site generator or Ruby application
@@ -23,9 +25,11 @@ Used as the transformation engine for the [jekyll-webawesome](https://github.com
23
25
  | **Copy Button** | `<<<placement? tooltip:full\|copy\|none?` | `:::wa-copy-button placement? tooltip:full\|copy\|none?` | `<wa-copy-button value="content" tooltip-placement="top" tooltip="copy"></wa-copy-button>` |
24
26
  | **Details** | `^^^appearance? icon-placement?` | `:::wa-details appearance? icon-placement?` | `<wa-details>content</wa-details>` |
25
27
  | **Dialog** | `???params?` | `:::wa-dialog params?` | `<wa-dialog>` with trigger button and content |
28
+ | **Divider** | `--- label ---` (e.g. `--- start icon:star New ---`) | `:::wa-divider params?` (body is the label) | `<wa-divider label-placement="start"><wa-icon name="star"></wa-icon> New</wa-divider>` |
26
29
  | **Icon** | `$$$icon-name` | `:::wa-icon icon-name` | `<wa-icon name="icon-name"></wa-icon>` |
27
30
  | **Image Dialog** | `![alt](url)` | — | Wraps images in clickable `<wa-dialog>` overlays |
28
31
  | **Popover** | `&&&params?` or inline `&&&trigger >>> content&&&` | `:::wa-popover params?` | `<wa-popover>` with trigger button and content |
32
+ | **Stepper** | `>>>>>> params?` wrapping a task list | `:::wa-stepper params?` | `<wa-stepper active="step-2"><wa-step name="step-1" completed>…</wa-step>…</wa-stepper>` |
29
33
  | **Tab Group** | `++++++` | `:::wa-tabs` | `<wa-tab-group><wa-tab>content</wa-tab></wa-tab-group>` |
30
34
  | **Tag** | `@@@brand` | `:::wa-tag brand` | `<wa-tag variant="brand">content</wa-tag>` |
31
35
 
@@ -36,6 +40,22 @@ Used as the transformation engine for the [jekyll-webawesome](https://github.com
36
40
  > listener (e.g. a single global handler that removes `event.target`). The markup
37
41
  > we emit is valid either way.
38
42
 
43
+ > **Labeled divider:** `--- label ---` needs text between the mirrored dashes,
44
+ > so a plain `---` stays an `<hr>`. Leading `start`/`center`/`end`
45
+ > (label placement), `horizontal`/`vertical` (orientation) and `icon:name`
46
+ > tokens are consumed from the left; the rest is the label, as inline Markdown.
47
+ > A line made only of `|`, `:` and `-` (a pipe-less table separator) is left
48
+ > alone. Use `:::wa-divider` when a label must start with one of those words.
49
+
50
+ > **Stepper (display-only):** each unindented list line is a step. `[x]` marks
51
+ > it completed; leading `active`, `loading`, `disabled`, a variant, an attention
52
+ > (`pulse`/`bounce`/`none`) and `icon:name` are flags; lines indented under a
53
+ > step become its description. The active step is the one flagged `active`,
54
+ > else the first incomplete step, else the last. Container params are
55
+ > `horizontal`/`vertical`/`auto` and `label:"…"`. `wa-stepper` is experimental
56
+ > in Web Awesome 3.14.0, and its interactive features (`clickable`, `linear`)
57
+ > are deliberately not exposed.
58
+
39
59
  ## Layout Utilities
40
60
 
41
61
  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.
@@ -53,7 +73,7 @@ Layout utilities use `::::` (quadruple colon) syntax to wrap content in CSS layo
53
73
 
54
74
  All layouts support these key:value attributes:
55
75
 
56
- - `gap:SIZE` — Sets spacing (`0`, `3xs`, `2xs`, `xs`, `s`, `m`, `l`, `xl`, `2xl`, `3xl`)
76
+ - `gap:SIZE` — Sets spacing (`0`, `3xs`, `2xs`, `xs`, `s`, `m`, `l`, `xl`, `2xl`, `3xl`, `4xl`, `5xl`)
57
77
  - `align:VALUE` — Align items (`start`, `end`, `center`, `stretch`, `baseline`)
58
78
  - `justify:VALUE` — Justify content (`start`, `end`, `center`, `space-between`, `space-around`, `space-evenly`)
59
79
 
@@ -25,9 +25,9 @@ module Markawesome
25
25
  # component without forking the gem.
26
26
  #
27
27
  # @param component [Symbol] one of :accordion, :callout, :badge, :button,
28
- # :card, :carousel, :comparison, :copy_button, :details, :dialog, :icon,
29
- # :image_dialog, :layout, :popover, :random_content, :tabs, :tag,
30
- # :tooltip.
28
+ # :card, :carousel, :comparison, :copy_button, :details, :dialog,
29
+ # :divider, :icon, :image_dialog, :layout, :popover, :random_content,
30
+ # :stepper, :tabs, :tag, :tooltip, :tree.
31
31
  # @yield [content, options] Proc that receives the full source content
32
32
  # and the renderer options; returns the content with that component
33
33
  # syntax replaced.
@@ -44,6 +44,8 @@ module Markawesome
44
44
  end
45
45
 
46
46
  PIPELINE = %i[
47
+ divider
48
+ stepper
47
49
  layout
48
50
  popover
49
51
  tooltip
@@ -68,6 +70,8 @@ module Markawesome
68
70
  ].freeze
69
71
 
70
72
  TRANSFORMER_MAP = {
73
+ divider: DividerTransformer,
74
+ stepper: StepperTransformer,
71
75
  layout: LayoutTransformer,
72
76
  popover: PopoverTransformer,
73
77
  tooltip: TooltipTransformer,
@@ -10,6 +10,13 @@ module Markawesome
10
10
  def self.process(content, options = {})
11
11
  content, tokens = CodeBlockProtector.protect(content)
12
12
 
13
+ # Divider and stepper run FIRST: their content is inline-only (a label,
14
+ # a list of one-line steps), so emitting them before any container lets
15
+ # them sit inside layouts, cards, callouts, tabs, accordion and dialog
16
+ # bodies — e.g. `--- vertical vs ---` inside a `::::cluster`.
17
+ content = DividerTransformer.transform(content)
18
+ content = StepperTransformer.transform(content)
19
+
13
20
  content = LayoutTransformer.transform(content)
14
21
  content = PopoverTransformer.transform(content)
15
22
  content = TooltipTransformer.transform(content)
@@ -23,7 +23,7 @@ module Markawesome
23
23
  # - expanded -> expanded; disabled -> disabled
24
24
  # - icon:name -> <wa-icon slot="icon" name="name"></wa-icon> as first child
25
25
  #
26
- # wa-accordion is experimental in Web Awesome (added 3.7). expanded/disabled
26
+ # wa-accordion was added in Web Awesome 3.7 (stable since 3.13). expanded/disabled
27
27
  # and the mode/appearance/icon-placement attributes are all static-safe.
28
28
  class AccordionTransformer < BaseTransformer
29
29
  CONTAINER_ATTRIBUTES = {
@@ -25,6 +25,10 @@ module Markawesome
25
25
  primary_regex = /^\?\?\?([^\n]*)$\n(.*?)\n^>>>$\n(.*?)\n^\?\?\?$/m
26
26
  alternative_regex = /^:::wa-dialog([^\n]*)$\n(.*?)\n^>>>$\n(.*?)\n^:::$/m
27
27
 
28
+ # Two dialogs with the same trigger and body hash alike, so suffix repeats
29
+ # (-2, -3, …) to keep ids unique — same scheme as popover/tooltip.
30
+ seen_ids = Hash.new(0)
31
+
28
32
  # Define shared transformation logic
29
33
  transform_proc = proc do |params_string, button_text, dialog_content|
30
34
  button_text = button_text.strip
@@ -37,7 +41,7 @@ module Markawesome
37
41
  label, content_without_label = extract_label(dialog_content, button_text)
38
42
 
39
43
  # Generate unique ID based on content
40
- dialog_id = generate_dialog_id(button_text, dialog_content)
44
+ dialog_id = generate_dialog_id(button_text, dialog_content, seen_ids)
41
45
 
42
46
  # Convert markdown to HTML
43
47
  content_html = markdown_to_html(content_without_label)
@@ -104,11 +108,17 @@ module Markawesome
104
108
  end
105
109
  end
106
110
 
107
- # Generate a unique ID for the dialog using MD5 hash
108
- def generate_dialog_id(button_text, content)
111
+ # Generate a unique ID for the dialog using MD5 hash.
112
+ # The hash covers the trigger and body only, so two dialogs differing solely
113
+ # by a param (e.g. width) would collide — and identical ones always do. The
114
+ # occurrence counter disambiguates: the first keeps the bare id, later ones
115
+ # get -2, -3, … Mirrors generate_popover_id / generate_tooltip_id.
116
+ def generate_dialog_id(button_text, content, seen_ids)
109
117
  hash_input = "#{button_text}#{content}"
110
118
  hash = Digest::MD5.hexdigest(hash_input)
111
- "dialog-#{hash[0..7]}" # Use first 8 characters of hash
119
+ base = "dialog-#{hash[0..7]}" # Use first 8 characters of hash
120
+ occurrence = seen_ids[base] += 1
121
+ occurrence == 1 ? base : "#{base}-#{occurrence}"
112
122
  end
113
123
 
114
124
  # Build the complete dialog HTML with trigger button
@@ -134,26 +144,7 @@ module Markawesome
134
144
  # Add CSS Parts styling for image buttons to make them invisible
135
145
  if is_image_button
136
146
  button_id = "#{dialog_id}-btn"
137
- html << '<style>'
138
- html << " ##{button_id}::part(base) {"
139
- html << ' padding: 0;'
140
- html << ' margin: 0;'
141
- html << ' border: none;'
142
- html << ' background: transparent;'
143
- html << ' box-shadow: none;'
144
- html << ' color: inherit;'
145
- html << ' min-width: 0;'
146
- html << ' height: auto;'
147
- html << ' }'
148
- html << " ##{button_id}::part(base):hover {"
149
- html << ' background: transparent;'
150
- html << ' border-color: transparent;'
151
- html << ' }'
152
- html << " ##{button_id}::part(base):active {"
153
- html << ' background: transparent;'
154
- html << ' border-color: transparent;'
155
- html << ' }'
156
- html << '</style>'
147
+ html.concat(image_button_style(button_id))
157
148
  end
158
149
 
159
150
  # Trigger button
@@ -175,6 +166,27 @@ module Markawesome
175
166
  html.join("\n")
176
167
  end
177
168
 
169
+ # Strip the trigger button's chrome so an image trigger reads as a bare image.
170
+ #
171
+ # WA 3.11 deprecated wa-button's generic `base` part in favour of a part named after
172
+ # the component (`button`). `base` still works but is on its way out, so target both —
173
+ # the output then styles correctly on 3.10-pinned and 3.11+ sites.
174
+ def image_button_style(button_id)
175
+ wrapper = lambda do |state|
176
+ " ##{button_id}::part(base)#{state}, ##{button_id}::part(button)#{state} {"
177
+ end
178
+
179
+ ['<style>', wrapper.call(''),
180
+ ' padding: 0;', ' margin: 0;', ' border: none;',
181
+ ' background: transparent;', ' box-shadow: none;', ' color: inherit;',
182
+ ' min-width: 0;', ' height: auto;', ' }',
183
+ wrapper.call(':hover'),
184
+ ' background: transparent;', ' border-color: transparent;', ' }',
185
+ wrapper.call(':active'),
186
+ ' background: transparent;', ' border-color: transparent;', ' }',
187
+ '</style>']
188
+ end
189
+
178
190
  # Escape HTML entities in text
179
191
  def escape_html(text)
180
192
  text.gsub('&', '&amp;')
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_transformer'
4
+ require_relative '../attribute_parser'
5
+ require_relative '../icon_slot_parser'
6
+
7
+ module Markawesome
8
+ # Transforms labeled-divider syntax into <wa-divider> elements with a label in
9
+ # the default slot (Web Awesome 3.13.0+).
10
+ #
11
+ # Primary syntax (one line, mirrored dashes):
12
+ # --- or ---
13
+ # --- start Release notes ---
14
+ # --- end icon:star New ---
15
+ # --- icon:quote-left ---
16
+ # --- vertical vs ---
17
+ # Alternative syntax (the body is the label, and may be empty):
18
+ # :::wa-divider start icon:star
19
+ # Release **notes**
20
+ # :::
21
+ #
22
+ # Leading tokens (consumed from the left until the first non-token word):
23
+ # - start, center, end -> label-placement
24
+ # - horizontal, vertical -> orientation
25
+ # - icon:name -> <wa-icon name="name"> before the label
26
+ # The rest of the line is the label, rendered as inline Markdown. To start a
27
+ # label with one of those words, use the alternative syntax.
28
+ #
29
+ # A plain `---` (and `--- ---`) is left alone and stays an <hr>. So is a line
30
+ # made only of `|`, `:`, `-` and whitespace (e.g. a pipe-less GFM table
31
+ # separator such as `--- | ---`).
32
+ class DividerTransformer < BaseTransformer
33
+ DIVIDER_ATTRIBUTES = {
34
+ orientation: %w[horizontal vertical],
35
+ label_placement: %w[start center end]
36
+ }.freeze
37
+
38
+ # Content slot: emits <wa-icon name="..."> with no slot attribute.
39
+ ICON_SLOTS = {
40
+ default: 'content',
41
+ slots: %w[content],
42
+ slot_map: { 'content' => 'content' }
43
+ }.freeze
44
+
45
+ PRIMARY_REGEX = /^---[ \t]+(\S[^\n]*?)[ \t]+---[ \t]*$/
46
+ ALTERNATIVE_REGEX = /^:::wa-divider[ \t]*([^\n]*)\n(?:(.*?)\n)?:::[ \t]*$/m
47
+ TABLE_SEPARATOR_REGEX = /\A[|:\-\s]*\z/
48
+
49
+ def self.transform(content)
50
+ content = content.gsub(PRIMARY_REGEX) do |match|
51
+ inner = Regexp.last_match(1)
52
+ next match if inner.match?(TABLE_SEPARATOR_REGEX)
53
+
54
+ lead, label = split_leading_tokens(inner)
55
+ build_html(lead, label)
56
+ end
57
+
58
+ content.gsub(ALTERNATIVE_REGEX) do
59
+ params_string = Regexp.last_match(1)
60
+ body = Regexp.last_match(2).to_s
61
+ build_html(params_string, body.strip)
62
+ end
63
+ end
64
+
65
+ # Degrade to a thematic break followed by the label as its own paragraph
66
+ # (icon dropped). The leading blank line stops `---` from turning a
67
+ # preceding paragraph line into a setext heading.
68
+ def self.render_as_markdown(content, _options = {})
69
+ content = content.gsub(PRIMARY_REGEX) do |match|
70
+ inner = Regexp.last_match(1)
71
+ next match if inner.match?(TABLE_SEPARATOR_REGEX)
72
+
73
+ _lead, label = split_leading_tokens(inner)
74
+ markdown_break(label)
75
+ end
76
+
77
+ content.gsub(ALTERNATIVE_REGEX) do
78
+ markdown_break(Regexp.last_match(2).to_s.strip)
79
+ end
80
+ end
81
+
82
+ class << self
83
+ private
84
+
85
+ # Consume leading placement/orientation/icon tokens; the rest is the label.
86
+ def split_leading_tokens(inner)
87
+ tokens = inner.strip.split(/\s+/)
88
+ lead = []
89
+ lead << tokens.shift while tokens.any? && leading_token?(tokens.first)
90
+ [lead.join(' '), tokens.join(' ')]
91
+ end
92
+
93
+ def leading_token?(token)
94
+ token.start_with?('icon:') || DIVIDER_ATTRIBUTES.values.any? { |values| values.include?(token) }
95
+ end
96
+
97
+ def build_html(params_string, label)
98
+ attributes = AttributeParser.parse(params_string, DIVIDER_ATTRIBUTES)
99
+ icon_result = IconSlotParser.parse(params_string, ICON_SLOTS)
100
+
101
+ attr_parts = []
102
+ attr_parts << "orientation=\"#{attributes[:orientation]}\"" if attributes[:orientation]
103
+ attr_parts << "label-placement=\"#{attributes[:label_placement]}\"" if attributes[:label_placement]
104
+ attrs_string = attr_parts.empty? ? '' : " #{attr_parts.join(' ')}"
105
+
106
+ icon_html = IconSlotParser.to_html(icon_result[:icons], ICON_SLOTS[:slot_map])
107
+ label_html = render_inline(label)
108
+ separator = !icon_html.empty? && !label_html.empty? ? ' ' : ''
109
+
110
+ "<wa-divider#{attrs_string}>#{icon_html}#{separator}#{label_html}</wa-divider>"
111
+ end
112
+
113
+ # Inline Markdown: render, then drop the wrapping <p> (the badge idiom).
114
+ def render_inline(text)
115
+ return '' if text.empty?
116
+
117
+ markdown_to_html(text).strip.sub(%r{\A<p>(.*)</p>\z}m, '\1')
118
+ end
119
+
120
+ def markdown_break(label)
121
+ label.empty? ? "\n---" : "\n---\n\n#{label}\n"
122
+ end
123
+ end
124
+ end
125
+ end
@@ -4,7 +4,7 @@ require_relative 'base_transformer'
4
4
  require_relative '../attribute_parser'
5
5
 
6
6
  module Markawesome
7
- # Transforms random-content syntax into Web Awesome's experimental
7
+ # Transforms random-content syntax into Web Awesome's
8
8
  # <wa-random-content> element, which shows one or more of its direct element
9
9
  # children at random (optionally rotating them) and hides the rest — all in
10
10
  # WA's own runtime, so the author writes zero JavaScript. Ideal for a "little
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_transformer'
4
+ require_relative '../attribute_parser'
5
+ require_relative '../icon_slot_parser'
6
+
7
+ module Markawesome
8
+ # Transforms stepper syntax into a display-only <wa-stepper> / <wa-step>
9
+ # progress indicator, written as a Markdown task list.
10
+ #
11
+ # Primary syntax:
12
+ # >>>>>> vertical label:"Checkout progress"
13
+ # - [x] Cart
14
+ # - [x] Shipping
15
+ # Standard, 3–5 days
16
+ # - [ ] pulse icon:credit-card Payment
17
+ # - [ ] Review
18
+ # >>>>>>
19
+ # Alternative syntax: :::wa-stepper params? ...same list... :::
20
+ #
21
+ # Container params:
22
+ # - horizontal, vertical, auto -> orientation
23
+ # - label:"text" -> label (accessible name)
24
+ # Steps are unindented list lines (`-`, `*` or `+`):
25
+ # - [x] / [X] marks the step completed; [ ] or no box means not completed
26
+ # - leading flags: active, loading, disabled, a variant (neutral, brand,
27
+ # success, warning, danger), an attention (none, pulse, bounce), and
28
+ # icon:name (slotted into the step's icon slot)
29
+ # - the rest of the line is the label, rendered as inline Markdown
30
+ # - lines indented by two or more spaces under a step are joined into its
31
+ # description slot, rendered as inline Markdown
32
+ #
33
+ # Steps are named step-1..N and the container's `active` points at the step
34
+ # flagged `active` (last one wins), else the first incomplete step, else the
35
+ # last step. Interactivity (`clickable`, `linear`, step invokers) is omitted
36
+ # on purpose: these steps have no content panels, so there is nothing to
37
+ # navigate between without app JavaScript.
38
+ class StepperTransformer < BaseTransformer
39
+ STEPPER_ATTRIBUTES = {
40
+ orientation: %w[horizontal vertical auto]
41
+ }.freeze
42
+
43
+ STEP_ATTRIBUTES = {
44
+ variant: %w[neutral brand success warning danger],
45
+ attention: %w[none pulse bounce]
46
+ }.freeze
47
+
48
+ STEP_FLAGS = %w[active loading disabled].freeze
49
+
50
+ ICON_SLOTS = {
51
+ default: 'icon',
52
+ slots: %w[icon],
53
+ slot_map: { 'icon' => 'icon' }
54
+ }.freeze
55
+
56
+ PRIMARY_REGEX = /^>{6}([^\n]*)\n(.*?)\n>{6}[ \t]*$/m
57
+ ALTERNATIVE_REGEX = /^:::wa-stepper[ \t]*([^\n]*)\n(.*?)\n:::[ \t]*$/m
58
+ LABEL_REGEX = /label:(?:"([^"]*)"|'([^']*)')/
59
+ STEP_LINE_REGEX = /\A[-*+][ \t]+(.*)\z/
60
+ CONTINUATION_REGEX = /\A[ \t]{2,}(\S.*)\z/
61
+ CHECKBOX_REGEX = /\A\[([ xX])\](?:[ \t]+(.*))?\z/
62
+
63
+ def self.transform(content)
64
+ transform_proc = proc do |params_string, body|
65
+ label, params_string = extract_label(params_string.to_s)
66
+ attributes = AttributeParser.parse(params_string, STEPPER_ATTRIBUTES)
67
+ steps = parse_steps(body)
68
+ active = active_index(steps)
69
+
70
+ attr_parts = []
71
+ attr_parts << "orientation=\"#{attributes[:orientation]}\"" if attributes[:orientation]
72
+ attr_parts << "label=\"#{escape_html(label)}\"" unless label.empty?
73
+ attr_parts << "active=\"step-#{active + 1}\"" if active
74
+ attrs_string = attr_parts.empty? ? '' : " #{attr_parts.join(' ')}"
75
+
76
+ steps_html = steps.each_with_index.map { |step, index| build_step(step, index) }.join
77
+ "<wa-stepper#{attrs_string}>#{steps_html}</wa-stepper>"
78
+ end
79
+
80
+ patterns = dual_syntax_patterns(PRIMARY_REGEX, ALTERNATIVE_REGEX, transform_proc)
81
+ apply_multiple_patterns(content, patterns)
82
+ end
83
+
84
+ # Degrade to an ordered task list: completed steps keep [x], the current
85
+ # step is bold, and a description becomes an indented continuation line.
86
+ def self.render_as_markdown(content, _options = {})
87
+ transform_proc = proc do |_params_string, body|
88
+ steps = parse_steps(body)
89
+ active = active_index(steps)
90
+
91
+ steps.each_with_index.map do |step, index|
92
+ marker = "#{index + 1}. "
93
+ label = index == active && !step[:label].empty? ? "**#{step[:label]}**" : step[:label]
94
+ line = "#{marker}[#{step[:completed] ? 'x' : ' '}]"
95
+ line += " #{label}" unless label.empty?
96
+ line += "\n#{' ' * marker.length}#{step[:description]}" unless step[:description].empty?
97
+ line
98
+ end.join("\n")
99
+ end
100
+
101
+ patterns = dual_syntax_patterns(PRIMARY_REGEX, ALTERNATIVE_REGEX, transform_proc)
102
+ apply_multiple_patterns(content, patterns)
103
+ end
104
+
105
+ class << self
106
+ private
107
+
108
+ # Pull out label:"…" (or label:'…') before the bare tokens are parsed, so
109
+ # words inside the label can't be read as orientation tokens.
110
+ def extract_label(params_string)
111
+ match = LABEL_REGEX.match(params_string)
112
+ return ['', params_string] unless match
113
+
114
+ [(match[1] || match[2]).strip, params_string.sub(LABEL_REGEX, ' ')]
115
+ end
116
+
117
+ def parse_steps(body)
118
+ steps = []
119
+ body.to_s.each_line do |line|
120
+ line = line.chomp
121
+ if (match = STEP_LINE_REGEX.match(line))
122
+ steps << parse_step(match[1])
123
+ elsif (match = CONTINUATION_REGEX.match(line)) && steps.any?
124
+ steps.last[:description_lines] << match[1].strip
125
+ end
126
+ end
127
+ steps.each { |step| step[:description] = step.delete(:description_lines).join(' ') }
128
+ end
129
+
130
+ def parse_step(raw)
131
+ completed = false
132
+ if (match = CHECKBOX_REGEX.match(raw.strip))
133
+ completed = match[1] != ' '
134
+ raw = match[2].to_s
135
+ end
136
+
137
+ tokens = raw.strip.split(/\s+/)
138
+ lead = []
139
+ lead << tokens.shift while tokens.any? && leading_token?(tokens.first)
140
+ lead_string = lead.join(' ')
141
+
142
+ {
143
+ completed: completed,
144
+ flags: lead & STEP_FLAGS,
145
+ attributes: AttributeParser.parse(lead_string, STEP_ATTRIBUTES),
146
+ icons: IconSlotParser.parse(lead_string, ICON_SLOTS)[:icons],
147
+ label: tokens.join(' '),
148
+ description_lines: []
149
+ }
150
+ end
151
+
152
+ def leading_token?(token)
153
+ token.start_with?('icon:') ||
154
+ STEP_FLAGS.include?(token) ||
155
+ STEP_ATTRIBUTES.values.any? { |values| values.include?(token) }
156
+ end
157
+
158
+ # The explicitly active step (last wins), else the first incomplete one,
159
+ # else the last step; nil when there are no steps.
160
+ def active_index(steps)
161
+ return nil if steps.empty?
162
+
163
+ explicit = steps.rindex { |step| step[:flags].include?('active') }
164
+ return explicit if explicit
165
+
166
+ steps.index { |step| !step[:completed] } || (steps.length - 1)
167
+ end
168
+
169
+ def build_step(step, index)
170
+ attr_parts = ["name=\"step-#{index + 1}\""]
171
+ attr_parts << 'completed' if step[:completed]
172
+ attr_parts << 'loading' if step[:flags].include?('loading')
173
+ attr_parts << 'disabled' if step[:flags].include?('disabled')
174
+ attr_parts << "variant=\"#{step[:attributes][:variant]}\"" if step[:attributes][:variant]
175
+ attr_parts << "attention=\"#{step[:attributes][:attention]}\"" if step[:attributes][:attention]
176
+
177
+ icon_html = IconSlotParser.to_html(step[:icons], ICON_SLOTS[:slot_map])
178
+ description = render_inline(step[:description])
179
+ description_html = description.empty? ? '' : "<span slot=\"description\">#{description}</span>"
180
+
181
+ "<wa-step #{attr_parts.join(' ')}>#{icon_html}#{render_inline(step[:label])}#{description_html}</wa-step>"
182
+ end
183
+
184
+ # Inline Markdown: render, then drop the wrapping <p> (the badge idiom).
185
+ def render_inline(text)
186
+ return '' if text.empty?
187
+
188
+ markdown_to_html(text).strip.sub(%r{\A<p>(.*)</p>\z}m, '\1')
189
+ end
190
+
191
+ def escape_html(text)
192
+ text.to_s
193
+ .gsub('&', '&amp;')
194
+ .gsub('<', '&lt;')
195
+ .gsub('>', '&gt;')
196
+ .gsub('"', '&quot;')
197
+ .gsub("'", '&#39;')
198
+ end
199
+ end
200
+ end
201
+ end
@@ -15,11 +15,13 @@ require_relative 'transformers/copy_button_transformer'
15
15
  require_relative 'transformers/date_transformer'
16
16
  require_relative 'transformers/details_transformer'
17
17
  require_relative 'transformers/dialog_transformer'
18
+ require_relative 'transformers/divider_transformer'
18
19
  require_relative 'transformers/icon_transformer'
19
20
  require_relative 'transformers/image_dialog_transformer'
20
21
  require_relative 'transformers/layout_transformer'
21
22
  require_relative 'transformers/popover_transformer'
22
23
  require_relative 'transformers/random_content_transformer'
24
+ require_relative 'transformers/stepper_transformer'
23
25
  require_relative 'transformers/tabs_transformer'
24
26
  require_relative 'transformers/tag_transformer'
25
27
  require_relative 'transformers/tooltip_transformer'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Markawesome
4
- VERSION = '0.18.1'
4
+ VERSION = '0.20.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.18.1
4
+ version: 0.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Janne Waren
@@ -96,11 +96,13 @@ files:
96
96
  - lib/markawesome/transformers/date_transformer.rb
97
97
  - lib/markawesome/transformers/details_transformer.rb
98
98
  - lib/markawesome/transformers/dialog_transformer.rb
99
+ - lib/markawesome/transformers/divider_transformer.rb
99
100
  - lib/markawesome/transformers/icon_transformer.rb
100
101
  - lib/markawesome/transformers/image_dialog_transformer.rb
101
102
  - lib/markawesome/transformers/layout_transformer.rb
102
103
  - lib/markawesome/transformers/popover_transformer.rb
103
104
  - lib/markawesome/transformers/random_content_transformer.rb
105
+ - lib/markawesome/transformers/stepper_transformer.rb
104
106
  - lib/markawesome/transformers/tabs_transformer.rb
105
107
  - lib/markawesome/transformers/tag_transformer.rb
106
108
  - lib/markawesome/transformers/tooltip_transformer.rb
@@ -130,7 +132,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
130
132
  - !ruby/object:Gem::Version
131
133
  version: '0'
132
134
  requirements: []
133
- rubygems_version: 4.0.10
135
+ rubygems_version: 4.0.20
134
136
  specification_version: 4
135
137
  summary: Framework-agnostic Markdown to Web Awesome component transformer
136
138
  test_files: []