hotdocs 0.2.0 → 0.3.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: e9062e9c27d95f790d4a77b9508b3ac57b518d41573896f358e508d03eb5b676
4
- data.tar.gz: 29d4da1687c46d5bf2821aae114a96d30191c94204ab6936fd82892f0ed117ce
3
+ metadata.gz: a04eac67f8afb072c5b6ef87e98bac8316659fe4da338d64b1fdb85b66334fd8
4
+ data.tar.gz: 542ea09084b1e53da0657368a3a8b4c559dbf9e0a6305be13f1ae2726ddf04f7
5
5
  SHA512:
6
- metadata.gz: 568702e50ee8305c0858c46e2acad029e3cf9c3c6f45517fe566fa73980025b67e389316694e8d0ac878f5bea4da98c7cd19923deccaa340192384bb26a66a7c
7
- data.tar.gz: 9acea8151aa28cb1a6d89d21a45553cba06b86b093ff20aa87f6632a95d9250b09b698bf6c160cfa3f14f5c0905f5472e259e9a2aab7d23b6a9e2d8d4aff176a
6
+ metadata.gz: 7b8fea60fa67ca77c4b4655e928429483b0a77779843c64863fce8e8f49dfaba102896e54c1b36151a19ad107bf5540fab6b5663727d791e71db27081acefedd
7
+ data.tar.gz: fcbfa4cf654c14cb2eb023c3905399479bf784c1dd349f99ff2357e3d7687f342a230b61eceb5b37c8f8ecf444b8b460608be5af07affeede0186c6707e3a95b
data/README.md CHANGED
@@ -20,8 +20,8 @@ HotDocs is a set of optimized Rails components & tools for writing docs:
20
20
  | Embed docs in an existing Rails app | ✅ | ❌ | ❌ |
21
21
  | Standalone docs | ✅ | ✅ | ✅ |
22
22
  | Styled components you can customize | ✅ | ✅ | ✅ |
23
- | Markdown (with syntax highlight & themes) | 🚀 | 👍 | 🚀 |
24
- | Static export | 🔜 🚀 | 👍 | 🚀 |
23
+ | Markdown (with syntax highlight & themes) | | | |
24
+ | Static export | | | |
25
25
  | Search | ✅ | 🔌 | 🔌 |
26
26
  | Light / Dark | 🔜 ✅ | 🔌 | ✅ |
27
27
  | Open source | ✅ | ✅ | ✅ |
@@ -0,0 +1,29 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static values = {
5
+ host: String,
6
+ path: String,
7
+ fallback: String,
8
+ };
9
+
10
+ connect() {
11
+ if (!this.element.id) {
12
+ throw new Error("Element must have an id.");
13
+ }
14
+ this.element.innerHTML = this.fallbackValue || "Loading...";
15
+ this.element.style.visibility = "";
16
+ this.fetchElement();
17
+ }
18
+
19
+ fetchElement() {
20
+ fetch(`${this.hostValue}${this.pathValue}`)
21
+ .then((response) => response.text())
22
+ .then((text) => {
23
+ const parser = new DOMParser();
24
+ const doc = parser.parseFromString(text, "text/html");
25
+ const fetchedElement = doc.querySelector(`#${this.element.id}`);
26
+ this.element.innerHTML = fetchedElement.innerHTML;
27
+ });
28
+ }
29
+ }
@@ -10,7 +10,7 @@ export default class extends Controller {
10
10
 
11
11
  disconnect() {
12
12
  document.removeEventListener("keydown", this.keydownOpen);
13
- document.removeEventListener("click", this.clickClose, { once: true });
13
+ document.removeEventListener("click", this._clickClose);
14
14
  }
15
15
 
16
16
  open() {
@@ -20,6 +20,11 @@ export default class extends Controller {
20
20
  this.searchTarget.showModal();
21
21
  }
22
22
 
23
+ close() {
24
+ this.searchTarget.close();
25
+ document.removeEventListener("click", this._clickClose);
26
+ }
27
+
23
28
  search = debounce(this._search, 200);
24
29
 
25
30
  _allowOpening() {
@@ -32,13 +37,15 @@ export default class extends Controller {
32
37
  document.addEventListener("keydown", this.keydownOpen);
33
38
  }
34
39
 
35
- _allowClosing() {
36
- this.clickClose = (event) => {
37
- if (this.dialogTarget.contains(event.target)) return;
38
- this.searchTarget.close();
39
- };
40
+ _clickClose = (event) => {
41
+ if (!this.searchTarget.open) return;
42
+ if (this.dialogTarget.contains(event.target)) return;
43
+ this.close();
44
+ };
40
45
 
41
- document.addEventListener("click", this.clickClose, { once: true });
46
+ _allowClosing() {
47
+ document.removeEventListener("click", this._clickClose);
48
+ document.addEventListener("click", this._clickClose);
42
49
  }
43
50
 
44
51
  _initSearch() {
@@ -73,7 +73,7 @@ body {
73
73
  --nav-background-color: white;
74
74
  --nav-link-active-color: var(--link-active-color);
75
75
  --nav-link-color: var(--link-color);
76
- --nav-padding-horizontal: 1rem;
76
+ --nav-padding-horizontal: 1.5rem;
77
77
  --nav-padding-vertical: 0.5rem;
78
78
  --nav-shadow: 0 1px 2px 0 #0000001a;
79
79
  --nav-title-color: #1c1e21;
@@ -304,14 +304,21 @@ body:has(.search:open), body:has(.search[open]) {
304
304
  bottom: 0;
305
305
  color: var(--search-text-color);
306
306
  height: 100vh;
307
+ height: 100dvh;
307
308
  left: 0;
308
309
  max-height: 100vh;
310
+ height: 100dvh;
309
311
  max-width: 100vw;
310
- padding-inline: 1rem;
312
+ max-width: 100dvw;
311
313
  position: fixed;
312
314
  right: 0;
313
315
  top: 0;
314
316
  width: 100vw;
317
+ width: 100dvw;
318
+
319
+ @media (min-width: 64rem) {
320
+ padding-inline: 1rem;
321
+ }
315
322
  }
316
323
 
317
324
  ::backdrop {
@@ -319,28 +326,57 @@ body:has(.search:open), body:has(.search[open]) {
319
326
  }
320
327
 
321
328
  .search__dialog {
322
- overflow: auto;
323
329
  background-color: var(--search-background-color);
324
- border-radius: 0.375rem;
325
- max-height: calc(100vh - 120px);
326
- margin: 60px auto auto;
327
- max-width: 560px;
330
+ height: 100vh;
331
+ height: 100dvh;
332
+ max-height: 100vh;
333
+ max-height: 100dvh;
334
+ max-width: 100vw;
335
+ max-width: 100dvw;
336
+ overflow: auto;
328
337
  padding: 1rem;
329
- width: auto;
338
+ width: 100vw;
339
+ width: 100dvw;
340
+
341
+ @media (min-width: 64rem) {
342
+ border-radius: 0.375rem;
343
+ height: auto;
344
+ margin: 60px auto auto;
345
+ max-height: calc(100vh - 120px);
346
+ max-height: calc(100dvh - 120px);
347
+ max-width: 800px;
348
+ width: auto;
349
+ }
350
+ }
351
+
352
+ .search__header {
353
+ align-items: center;
354
+ display: flex;
355
+ gap: 1rem;
330
356
  }
331
357
 
332
358
  .search__input {
333
359
  background-color: var(--search-excerpt-background-color);
334
360
  border: 1px solid #808080;
335
361
  border-radius: 0.2rem;
362
+ flex: 1 0 auto;
336
363
  padding: 0.3rem 0.5rem;
337
- width: 100%;
338
364
 
339
365
  &:focus-visible {
340
366
  outline: solid 2px #0077ff;
341
367
  }
342
368
  }
343
369
 
370
+ .search__dismiss {
371
+ color: var(--search-text-color);
372
+ height: 1rem;
373
+ width: 1rem;
374
+
375
+ @media (min-width: 64rem) {
376
+ display: none;
377
+ }
378
+ }
379
+
344
380
  .search__result {
345
381
  margin-top: 1.5rem;
346
382
 
@@ -688,7 +724,7 @@ body:has(.search:open), body:has(.search[open]) {
688
724
  .footer {
689
725
  background-color: var(--footer-background-color);
690
726
  flex: 0 0 auto;
691
- padding: 4rem 1rem;
727
+ padding: 4rem var(--nav-padding-horizontal);
692
728
  }
693
729
 
694
730
  .footer__sections {
@@ -726,6 +762,7 @@ body:has(.search:open), body:has(.search[open]) {
726
762
  }
727
763
 
728
764
  .credits {
765
+ padding-block: 1rem;
729
766
  text-align: center;
730
767
  }
731
768
 
@@ -767,9 +804,9 @@ body:has(.search:open), body:has(.search[open]) {
767
804
  width: 1rem;
768
805
  }
769
806
 
770
- /* CSS: ADMONITIONS */
807
+ /* CSS: ALERTS */
771
808
 
772
- .admonition {
809
+ .alert {
773
810
  border: 1px solid;
774
811
  border-left: 0.5rem solid;
775
812
  border-radius: 0.375rem;
@@ -777,44 +814,44 @@ body:has(.search:open), body:has(.search[open]) {
777
814
  padding: 1rem;
778
815
  }
779
816
 
780
- .admonition--tip {
817
+ .alert--tip {
781
818
  background: #00940011;
782
819
  border-color: #009400;
783
820
  }
784
821
 
785
- .admonition--info {
822
+ .alert--info {
786
823
  background: #87cef911;
787
824
  border-color: #87cef9;
788
825
  }
789
826
 
790
- .admonition--warning {
827
+ .alert--warning {
791
828
  background: #fea50011;
792
829
  border-color: #fea500;
793
830
  }
794
831
 
795
- .admonition--danger {
832
+ .alert--danger {
796
833
  background: #db153b11;
797
834
  border-color: #db153b;
798
835
  }
799
836
 
800
- .admonition__header {
837
+ .alert__header {
801
838
  align-items: center;
802
839
  display: flex;
803
840
  gap: 0.5ch;
804
841
  margin-bottom: 1rem;
805
842
  }
806
843
 
807
- .admonition__icon {
844
+ .alert__icon {
808
845
  height: 1rem;
809
846
  width: 1rem;
810
847
  }
811
848
 
812
- .admonition__label {
849
+ .alert__label {
813
850
  font-weight: bold;
814
851
  margin: 0;
815
852
  text-transform: uppercase;
816
853
  }
817
854
 
818
- .admonition__content > :last-of-type {
855
+ .alert__content > :last-of-type {
819
856
  margin-bottom: 0;
820
857
  }
@@ -24,10 +24,11 @@ module Hotdocs
24
24
  [ *Array(old), *Array(new) ].join(" ")
25
25
  end
26
26
 
27
+ # Needs to be on one line otherwise kramdown chokes
27
28
  link_to(options, html_options) do
28
29
  concat(content_tag(:span, name))
29
30
 
30
- concat(<<~SVG.html_safe)
31
+ concat(<<~SVG.gsub(/\n/, "").html_safe)
31
32
  <svg aria-hidden="true" viewBox="0 0 24 24" class="external-link__icon">
32
33
  <path fill="currentColor" d="M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"></path>
33
34
  </svg>
@@ -70,6 +71,16 @@ module Hotdocs
70
71
  end
71
72
  end
72
73
 
74
+ def fetcher(id:, path:, fallback: nil, &)
75
+ data = {
76
+ controller: "fetcher",
77
+ "fetcher-host-value": fetcher_host,
78
+ "fetcher-path-value": path,
79
+ "fetcher-fallback-value": fallback
80
+ }
81
+ content_tag(:div, id: id, data: data, style: "visibility: hidden;", &)
82
+ end
83
+
73
84
  private
74
85
 
75
86
  def active_link?(url)
@@ -1,5 +1,5 @@
1
1
  <!DOCTYPE html>
2
- <html data-theme="light">
2
+ <html data-theme="light" lang="<%= I18n.locale %>">
3
3
  <head>
4
4
  <title><%= content_for(:title) %></title>
5
5
 
@@ -12,7 +12,16 @@
12
12
  <body data-controller="search">
13
13
  <dialog data-search-target="search" class="search">
14
14
  <div data-search-target="dialog" class="search__dialog">
15
- <input autofocus data-action="input->search#search" type="text" class="search__input"></input>
15
+ <div class="search__header">
16
+ <input autofocus data-action="input->search#search" type="text" class="search__input"></input>
17
+ <button aria-label="Close search dialog" type="button" class="search__dismiss" data-action="click->search#close">
18
+ <svg viewBox="0 0 15 15">
19
+ <g stroke="currentColor" stroke-width="1.2">
20
+ <path d="M.75.75l13.5 13.5M14.25.75L.75 14.25"></path>
21
+ </g>
22
+ </svg>
23
+ </button>
24
+ </div>
16
25
 
17
26
  <template data-search-target="resultTemplate">
18
27
  <li class="search__result">
@@ -33,88 +42,97 @@
33
42
  </script>
34
43
  </dialog>
35
44
 
36
- <nav class="nav" data-controller="sidenav" data-sidenav-open-class-value="sidenav--open" data-sidenav-main-menu-class-value="sidenav__sections--main">
37
- <div class="nav__section">
38
- <button class="nav__toggle" type="button" aria-label="Toggle navigation" aria-expanded="false" data-action="click->sidenav#open">
39
- <svg viewBox="0 0 30 30" aria-hidden="true">
40
- <path stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2" d="M4 7h22M4 15h22M4 23h22"></path>
41
- </svg>
42
- </button>
43
-
44
- <%= link_to root_path, class: "nav__brand" do %>
45
- <div class="nav__logo-wrapper">
46
- <img class="nav__logo" src="<%= logo.src %>" alt="<%= logo.alt %>" height="32" width="32" />
47
- </div>
45
+ <% if content_for?(:hotdocs_nav) %>
46
+ <%= content_for(:hotdocs_nav) %>
48
47
 
49
- <span class="nav__title">HotDocs</span>
50
- <% end %>
48
+ <% else %>
49
+ <nav class="nav" data-controller="sidenav" data-sidenav-open-class-value="sidenav--open" data-sidenav-main-menu-class-value="sidenav__sections--main">
50
+ <div class="nav__section">
51
+ <button class="nav__toggle" type="button" aria-label="Toggle navigation" aria-expanded="false" data-action="click->sidenav#open">
52
+ <svg viewBox="0 0 30 30" aria-hidden="true">
53
+ <path stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2" d="M4 7h22M4 15h22M4 23h22"></path>
54
+ </svg>
55
+ </button>
51
56
 
52
- <div class="nav__links">
53
- <% nav_left_items("nav__link").each do |item| %>
54
- <%= item %>
55
- <% end %>
56
- </div>
57
- </div>
57
+ <%= link_to root_path, class: "nav__brand" do %>
58
+ <% unless logo.nil? %>
59
+ <div class="nav__logo-wrapper">
60
+ <img class="nav__logo" src="<%= logo.src %>" alt="<%= logo.alt %>" height="32" width="32" />
61
+ </div>
62
+ <% end %>
58
63
 
59
- <div class="nav__section">
60
- <div class="nav__links">
61
- <% nav_right_items("nav__link").each do |item| %>
62
- <%= item %>
64
+ <span class="nav__title"><%= title %></span>
63
65
  <% end %>
64
- </div>
65
-
66
- <button type="button" data-action="click->search#open:stop" class="search-button">
67
- <svg class="search-button__icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
68
- <path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
69
- </svg>
70
66
 
71
- <span class="search-button__label">Type / to search</span>
72
- </button>
73
- </div>
74
-
75
- <div class="sidenav-backdrop"></div>
76
- <div class="sidenav">
77
- <div class="sidenav__header">
78
- <%= link_to root_path, class: "nav__brand" do %>
79
- <div class="nav__logo-wrapper">
80
- <img class="nav__logo" src="<%= logo.src %>" alt="<%= logo.alt %>" height="32" width="32" />
81
- </div>
67
+ <div class="nav__links">
68
+ <% nav_left_items("nav__link").each do |item| %>
69
+ <%= item %>
70
+ <% end %>
71
+ </div>
72
+ </div>
82
73
 
83
- <span class="nav__title">HotDocs</span>
84
- <% end %>
74
+ <div class="nav__section">
75
+ <div class="nav__links">
76
+ <% nav_right_items("nav__link").each do |item| %>
77
+ <%= item %>
78
+ <% end %>
79
+ </div>
85
80
 
86
- <button aria-label="Close navigation" class="sidenav__toggle" type="button" data-action="click->sidenav#close">
87
- <svg viewBox="0 0 15 15">
88
- <g stroke="currentColor" stroke-width="1.2">
89
- <path d="M.75.75l13.5 13.5M14.25.75L.75 14.25"></path>
90
- </g>
81
+ <button type="button" data-action="click->search#open:stop" class="search-button" aria-label="Open search dialog">
82
+ <svg class="search-button__icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
83
+ <path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
91
84
  </svg>
85
+
86
+ <span class="search-button__label">Type / to search</span>
92
87
  </button>
93
88
  </div>
94
89
 
95
- <div class="sidenav__sections" data-sidenav-target="sections">
96
- <div class="sidenav__section">
97
- <ul class="menu__section">
98
- <% (nav_left_items("menu__link") + nav_right_items("menu__link")).each do |item| %>
99
- <li>
100
- <div class="menu__row">
101
- <%= item %>
102
- </div>
103
- </li>
90
+ <div class="sidenav-backdrop"></div>
91
+ <div class="sidenav">
92
+ <div class="sidenav__header">
93
+ <%= link_to root_path, class: "nav__brand" do %>
94
+ <% unless logo.nil? %>
95
+ <div class="nav__logo-wrapper">
96
+ <img class="nav__logo" src="<%= logo.src %>" alt="<%= logo.alt %>" height="32" width="32" />
97
+ </div>
104
98
  <% end %>
105
- </ul>
99
+
100
+ <span class="nav__title"><%= title %></span>
101
+ <% end %>
102
+
103
+ <button aria-label="Close navigation" class="sidenav__toggle" type="button" data-action="click->sidenav#close">
104
+ <svg viewBox="0 0 15 15">
105
+ <g stroke="currentColor" stroke-width="1.2">
106
+ <path d="M.75.75l13.5 13.5M14.25.75L.75 14.25"></path>
107
+ </g>
108
+ </svg>
109
+ </button>
106
110
  </div>
107
111
 
108
- <div class="sidenav__section">
109
- <div class="menu__section">
110
- <button type="button" class="sidenav__back-button" data-action="click->sidenav#back">← Back to main menu</button>
112
+ <div class="sidenav__sections" data-sidenav-target="sections">
113
+ <div class="sidenav__section">
114
+ <ul class="menu__section">
115
+ <% (nav_left_items("menu__link") + nav_right_items("menu__link")).each do |item| %>
116
+ <li>
117
+ <div class="menu__row">
118
+ <%= item %>
119
+ </div>
120
+ </li>
121
+ <% end %>
122
+ </ul>
111
123
  </div>
112
124
 
113
- <%= menu %>
125
+ <div class="sidenav__section">
126
+ <div class="menu__section">
127
+ <button type="button" class="sidenav__back-button" data-action="click->sidenav#back">← Back to main menu</button>
128
+ </div>
129
+
130
+ <%= menu %>
131
+ </div>
114
132
  </div>
115
133
  </div>
116
- </div>
117
- </nav>
134
+ </nav>
135
+ <% end %>
118
136
 
119
137
  <div class="content">
120
138
  <aside class="menu">
@@ -152,24 +170,31 @@
152
170
  </main>
153
171
  </div>
154
172
 
155
- <footer class="footer">
156
- <div class="footer__sections">
157
- <% footer_items.each do |footer_item| %>
158
- <div class="footer__section">
159
- <p class="footer__heading"><%= footer_item.fetch(:heading) %></p>
173
+ <% if content_for?(:hotdocs_footer) %>
174
+ <%= content_for(:hotdocs_footer) %>
175
+
176
+ <% else %>
177
+ <footer class="footer">
178
+ <div class="footer__sections">
179
+ <% footer_items.each do |footer_item| %>
180
+ <div class="footer__section">
181
+ <p class="footer__heading"><%= footer_item.fetch(:heading) %></p>
182
+
183
+ <ul>
184
+ <% footer_item.fetch(:items).each do |item| %>
185
+ <li><%= item %></li>
186
+ <% end %>
187
+ </ul>
188
+ </div>
189
+ <% end %>
190
+ </div>
160
191
 
161
- <ul>
162
- <% footer_item.fetch(:items).each do |item| %>
163
- <li><%= item %></li>
164
- <% end %>
165
- </ul>
166
- </div>
192
+ <% unless logo.nil? %>
193
+ <img class="footer__logo" src="<%= logo.src %>" alt="<%= logo.alt %>" height="150" width="150" />
167
194
  <% end %>
168
- </div>
169
-
170
- <img class="footer__logo" src="<%= logo.src %>" alt="<%= logo.alt %>" height="150" width="150" />
195
+ </footer>
196
+ <% end %>
171
197
 
172
- <p class="credits">Built with Rails &amp; <a class="credits__link" href="https://hotdocsrails.com">HotDocs<img class="credits__logo" src="<%= asset_path "hotdocs/icon.svg" %>" alt="A humanized and happy hot dog" height="32" width="32" /></a></p>
173
- </footer>
198
+ <p class="credits">Built with Rails &amp; <a class="credits__link" href="https://hotdocsrails.com">HotDocs<img class="credits__logo" src="<%= asset_path "hotdocs/icon.svg" %>" alt="A humanized and happy hot dog" height="32" width="32" /></a></p>
174
199
  </body>
175
200
  </html>
data/config/importmap.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  # Pin npm packages by running ./bin/importmap
2
2
 
3
- pin_all_from Hotdocs::Engine.root.join("app/assets/javascript/controllers"), under: "controllers"
3
+ pin_all_from Hotdocs::Engine.root.join("app/assets/javascript/controllers"), to: "controllers", under: "hotdocs/controllers", preload: "hotdocs"
@@ -13,8 +13,7 @@ module Hotdocs
13
13
  end
14
14
 
15
15
  config.before_initialize do
16
- MarkdownHandler.prepare(self)
17
- ActionView::Template.register_template_handler :mderb, MarkdownHandler.new(self)
16
+ ActionView::Template.register_template_handler :mderb, MarkdownHandler.new
18
17
  end
19
18
  end
20
19
  end
@@ -0,0 +1,92 @@
1
+ module Kramdown
2
+ class Element
3
+ def to_h
4
+ {
5
+ children: children.map(&:to_h),
6
+ type:,
7
+ value:
8
+ }
9
+ end
10
+ end
11
+ end
12
+
13
+ module Alert
14
+ PATHS_BY_ALERT_TYPE = {
15
+ "danger" => [
16
+ "M15.362 5.214A8.252 8.252 0 0 1 12 21 8.25 8.25 0 0 1 6.038 7.047 8.287 8.287 0 0 0 9 9.601a8.983 8.983 0 0 1 3.361-6.867 8.21 8.21 0 0 0 3 2.48Z",
17
+ "M12 18a3.75 3.75 0 0 0 .495-7.468 5.99 5.99 0 0 0-1.925 3.547 5.975 5.975 0 0 1-2.133-1.001A3.75 3.75 0 0 0 12 18Z"
18
+ ],
19
+ "warning" => [
20
+ "M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"
21
+ ],
22
+ "tip" => [
23
+ "M12 18v-5.25m0 0a6.01 6.01 0 0 0 1.5-.189m-1.5.189a6.01 6.01 0 0 1-1.5-.189m3.75 7.478a12.06 12.06 0 0 1-4.5 0m3.75 2.383a14.406 14.406 0 0 1-3 0M14.25 18v-.192c0-.983.658-1.823 1.508-2.316a7.5 7.5 0 1 0-7.517 0c.85.493 1.509 1.333 1.509 2.316V18"
24
+ ],
25
+ "info" => [
26
+ "m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
27
+ ]
28
+ }
29
+
30
+ def convert_blockquote(el, indent)
31
+ child = el.children[0]
32
+
33
+ case child.to_h
34
+ in {
35
+ type: :p,
36
+ children: [
37
+ { type: :text, value: /\A\[!(INFO|TIP|WARNING|DANGER)\]\z/ },
38
+ { type: :br },
39
+ *
40
+ ]
41
+ }
42
+ alert_type = $+.downcase
43
+ child.children.slice!(0, 2) # remove :text & :br
44
+
45
+ svg = Kramdown::Element.new(
46
+ :html_element,
47
+ :svg,
48
+ {
49
+ class: "alert__icon",
50
+ xmlns: "http://www.w3.org/2000/svg",
51
+ fill: "none",
52
+ viewBox: "0 0 24 24",
53
+ "stroke-width": "1.5",
54
+ stroke: "currentColor"
55
+ }
56
+ )
57
+
58
+ PATHS_BY_ALERT_TYPE[alert_type].each do |path|
59
+ svg.children << Kramdown::Element.new(:html_element, :path, {
60
+ "stroke-linecap": "round",
61
+ "stroke-linejoin": "round",
62
+ d: path
63
+ })
64
+ end
65
+
66
+ label = Kramdown::Element.new(:html_element, :span, { class: "alert__label" })
67
+ label.children << Kramdown::Element.new(:text, alert_type.upcase)
68
+
69
+ header = Kramdown::Element.new(:html_element, :div, { class: "alert__header" })
70
+ header.children << svg
71
+ header.children << label
72
+
73
+ content = Kramdown::Element.new(:html_element, :div, { class: "alert__content" })
74
+ content.children.push(*el.children)
75
+
76
+ alert = Kramdown::Element.new(:html_element, :div, {})
77
+ alert.children << header
78
+ alert.children << content
79
+
80
+ format_as_block_html(
81
+ "div",
82
+ { class: "alert alert--#{alert_type}" },
83
+ format_as_indented_block_html("div", {}, inner(alert, indent), indent),
84
+ indent
85
+ )
86
+ else
87
+ super
88
+ end
89
+ end
90
+ end
91
+
92
+ Kramdown::Converter::Html.prepend(Alert)