hotdocs 0.1.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 +4 -4
- data/README.md +3 -3
- data/app/assets/javascript/controllers/fetcher_controller.js +29 -0
- data/app/assets/javascript/controllers/search_controller.js +162 -0
- data/app/assets/stylesheets/hotdocs/application.css +173 -13
- data/app/helpers/hotdocs/application_helper.rb +12 -1
- data/app/views/layouts/hotdocs/application.html.erb +129 -73
- data/config/importmap.rb +1 -1
- data/lib/hotdocs/engine.rb +1 -2
- data/lib/hotdocs/kramdown_alerts.rb +92 -0
- data/lib/hotdocs/markdown.rb +21 -25
- data/lib/hotdocs/version.rb +1 -1
- data/lib/install/install.rb +189 -173
- data/lib/tasks/hotdocs_tasks.rake +88 -4
- metadata +48 -4
- data/lib/hotdocs/markdown.mjs +0 -177
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a04eac67f8afb072c5b6ef87e98bac8316659fe4da338d64b1fdb85b66334fd8
|
4
|
+
data.tar.gz: 542ea09084b1e53da0657368a3a8b4c559dbf9e0a6305be13f1ae2726ddf04f7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7b8fea60fa67ca77c4b4655e928429483b0a77779843c64863fce8e8f49dfaba102896e54c1b36151a19ad107bf5540fab6b5663727d791e71db27081acefedd
|
7
|
+
data.tar.gz: fcbfa4cf654c14cb2eb023c3905399479bf784c1dd349f99ff2357e3d7687f342a230b61eceb5b37c8f8ecf444b8b460608be5af07affeede0186c6707e3a95b
|
data/README.md
CHANGED
@@ -20,9 +20,9 @@ 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 |
|
25
|
-
| Search |
|
23
|
+
| Markdown (with syntax highlight & themes) | ✅ | ✅ | ✅ |
|
24
|
+
| Static export | ✅ | ✅ | ✅ |
|
25
|
+
| Search | ✅ | 🔌 | 🔌 |
|
26
26
|
| Light / Dark | 🔜 ✅ | 🔌 | ✅ |
|
27
27
|
| Open source | ✅ | ✅ | ✅ |
|
28
28
|
| Free | ✅ | ✅ | ✅ |
|
@@ -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
|
+
}
|
@@ -0,0 +1,162 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
import lunr from "lunr";
|
3
|
+
|
4
|
+
export default class extends Controller {
|
5
|
+
static targets = ["search", "dialog", "results", "resultTemplate", "data"];
|
6
|
+
|
7
|
+
connect() {
|
8
|
+
this._allowOpening();
|
9
|
+
}
|
10
|
+
|
11
|
+
disconnect() {
|
12
|
+
document.removeEventListener("keydown", this.keydownOpen);
|
13
|
+
document.removeEventListener("click", this._clickClose);
|
14
|
+
}
|
15
|
+
|
16
|
+
open() {
|
17
|
+
if (this.searchTarget.open) return;
|
18
|
+
this._allowClosing();
|
19
|
+
this._initSearch();
|
20
|
+
this.searchTarget.showModal();
|
21
|
+
}
|
22
|
+
|
23
|
+
close() {
|
24
|
+
this.searchTarget.close();
|
25
|
+
document.removeEventListener("click", this._clickClose);
|
26
|
+
}
|
27
|
+
|
28
|
+
search = debounce(this._search, 200);
|
29
|
+
|
30
|
+
_allowOpening() {
|
31
|
+
this.keydownOpen = (event) => {
|
32
|
+
if (this.searchTarget.open || event.key !== "/") return;
|
33
|
+
event.preventDefault();
|
34
|
+
this.open();
|
35
|
+
};
|
36
|
+
|
37
|
+
document.addEventListener("keydown", this.keydownOpen);
|
38
|
+
}
|
39
|
+
|
40
|
+
_clickClose = (event) => {
|
41
|
+
if (!this.searchTarget.open) return;
|
42
|
+
if (this.dialogTarget.contains(event.target)) return;
|
43
|
+
this.close();
|
44
|
+
};
|
45
|
+
|
46
|
+
_allowClosing() {
|
47
|
+
document.removeEventListener("click", this._clickClose);
|
48
|
+
document.addEventListener("click", this._clickClose);
|
49
|
+
}
|
50
|
+
|
51
|
+
_initSearch() {
|
52
|
+
if (this.documents) {
|
53
|
+
this.searchTarget.classList.add("loaded");
|
54
|
+
return;
|
55
|
+
}
|
56
|
+
this._createSearchIndex();
|
57
|
+
this.searchTarget.classList.add("loaded");
|
58
|
+
}
|
59
|
+
|
60
|
+
_createSearchIndex() {
|
61
|
+
const documents = this._getDocuments();
|
62
|
+
if (documents.length === 0) return;
|
63
|
+
this.documents = documents;
|
64
|
+
this.searchIndex = lunr(function () {
|
65
|
+
this.ref("title");
|
66
|
+
this.field("title", { boost: 5 });
|
67
|
+
this.field("text");
|
68
|
+
this.metadataWhitelist = ["position"];
|
69
|
+
documents.forEach(function (doc) {
|
70
|
+
this.add(doc);
|
71
|
+
}, this);
|
72
|
+
});
|
73
|
+
}
|
74
|
+
|
75
|
+
_getDocuments() {
|
76
|
+
const searchData = JSON.parse(this.dataTarget.textContent);
|
77
|
+
if (searchData.length === 0) {
|
78
|
+
console.warn(
|
79
|
+
[
|
80
|
+
"The search data is not present in the HTML.",
|
81
|
+
"If you are in development, run `bundle exec rails hotdocs:index`.",
|
82
|
+
"If you are in production, assets compilation should have taken care of it.",
|
83
|
+
].join(" ")
|
84
|
+
);
|
85
|
+
}
|
86
|
+
return searchData.map((data) => {
|
87
|
+
const div = document.createElement("div");
|
88
|
+
div.innerHTML = data.html;
|
89
|
+
return { ...data, text: div.innerText };
|
90
|
+
});
|
91
|
+
}
|
92
|
+
|
93
|
+
_search(event) {
|
94
|
+
if (!this.searchIndex) return;
|
95
|
+
const query = event.target.value;
|
96
|
+
const results = this.searchIndex.search(query).slice(0, 10);
|
97
|
+
this._displayResults(results);
|
98
|
+
}
|
99
|
+
|
100
|
+
_displayResults(results) {
|
101
|
+
this.resultsTarget.innerHTML = null;
|
102
|
+
|
103
|
+
results.forEach((result) => {
|
104
|
+
const matches = Object.keys(result.matchData.metadata);
|
105
|
+
const excerpt = this._withExcerpt(matches, result)[0];
|
106
|
+
if (!excerpt) return;
|
107
|
+
this.resultsTarget.appendChild(this._createResultElement(excerpt));
|
108
|
+
});
|
109
|
+
}
|
110
|
+
|
111
|
+
_withExcerpt(matches, result) {
|
112
|
+
return matches.flatMap((match) => {
|
113
|
+
return Object.keys(result.matchData.metadata[match]).map((key) => {
|
114
|
+
const position = result.matchData.metadata[match][key].position[0];
|
115
|
+
const [sliceStart, sliceLength] = key === "text" ? position : [0, 0];
|
116
|
+
const doc = this.documents.find((doc) => doc.title === result.ref);
|
117
|
+
const excerpt = this._excerpt(doc.text, sliceStart, sliceLength);
|
118
|
+
return { ...doc, excerpt };
|
119
|
+
});
|
120
|
+
});
|
121
|
+
}
|
122
|
+
|
123
|
+
_excerpt(doc, sliceStart, sliceLength) {
|
124
|
+
const startPos = Math.max(sliceStart - 80, 0);
|
125
|
+
const endPos = Math.min(sliceStart + sliceLength + 80, doc.length);
|
126
|
+
return [
|
127
|
+
startPos > 0 ? "..." : "",
|
128
|
+
doc.slice(startPos, sliceStart),
|
129
|
+
"<strong>" +
|
130
|
+
escapeHtmlEntities(doc.slice(sliceStart, sliceStart + sliceLength)) +
|
131
|
+
"</strong>",
|
132
|
+
doc.slice(sliceStart + sliceLength, endPos),
|
133
|
+
endPos < doc.length ? "..." : "",
|
134
|
+
].join("");
|
135
|
+
}
|
136
|
+
|
137
|
+
_createResultElement(excerpt) {
|
138
|
+
const clone = this.resultTemplateTarget.content.cloneNode(true);
|
139
|
+
const li = clone.querySelector("li");
|
140
|
+
li.querySelector("h1").innerHTML = `${excerpt.parent} > ${excerpt.title}`;
|
141
|
+
li.querySelector("a").innerHTML = excerpt.excerpt;
|
142
|
+
li.querySelector("a").href = excerpt.url;
|
143
|
+
return clone;
|
144
|
+
}
|
145
|
+
}
|
146
|
+
|
147
|
+
function debounce(func, wait) {
|
148
|
+
let timeoutId;
|
149
|
+
|
150
|
+
return function (...args) {
|
151
|
+
clearTimeout(timeoutId);
|
152
|
+
timeoutId = setTimeout(() => func.apply(this, args), wait);
|
153
|
+
};
|
154
|
+
}
|
155
|
+
|
156
|
+
function escapeHtmlEntities(string) {
|
157
|
+
return String(string)
|
158
|
+
.replace(/&/g, "&")
|
159
|
+
.replace(/</g, "<")
|
160
|
+
.replace(/>/g, ">")
|
161
|
+
.replace(/"/g, """);
|
162
|
+
}
|
@@ -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:
|
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;
|
@@ -246,6 +246,163 @@ body {
|
|
246
246
|
font-weight: bold;
|
247
247
|
}
|
248
248
|
|
249
|
+
/* CSS: SEARCH */
|
250
|
+
|
251
|
+
:root {
|
252
|
+
--search-background-color: #f5f6f7;
|
253
|
+
--search-button-background-color: #e9e9e9;
|
254
|
+
--search-excerpt-background-color: white;
|
255
|
+
--search-excerpt-border-color: #d7d7d7;
|
256
|
+
--search-text-color: var(--text-color);
|
257
|
+
}
|
258
|
+
|
259
|
+
[data-theme=dark]:root {
|
260
|
+
--search-background-color: #242526;
|
261
|
+
--search-button-background-color: #1b1b1b;
|
262
|
+
--search-excerpt-background-color: #1b1b1b;
|
263
|
+
--search-excerpt-border-color: #535353;
|
264
|
+
}
|
265
|
+
|
266
|
+
.search-button {
|
267
|
+
align-items: center;
|
268
|
+
background-color: var(--search-background-color);
|
269
|
+
border: solid 1px transparent;
|
270
|
+
border-radius: 99999px;
|
271
|
+
display: flex;
|
272
|
+
gap: 0.5ch;
|
273
|
+
padding: 0.5rem 0.5rem;
|
274
|
+
|
275
|
+
@media (min-width: 40rem) {
|
276
|
+
padding: 0.25rem 0.5rem;
|
277
|
+
}
|
278
|
+
|
279
|
+
&:hover {
|
280
|
+
background: none;
|
281
|
+
border: solid 1px var(--nav-link-color);
|
282
|
+
}
|
283
|
+
}
|
284
|
+
|
285
|
+
.search-button__icon {
|
286
|
+
height: 1.2rem;
|
287
|
+
width: 1.2rem;
|
288
|
+
}
|
289
|
+
|
290
|
+
.search-button__label {
|
291
|
+
display: none;
|
292
|
+
|
293
|
+
@media (min-width: 40rem) {
|
294
|
+
display: initial;
|
295
|
+
}
|
296
|
+
}
|
297
|
+
|
298
|
+
body:has(.search:open), body:has(.search[open]) {
|
299
|
+
overflow: hidden;
|
300
|
+
}
|
301
|
+
|
302
|
+
.search {
|
303
|
+
background-color: #000000dd;
|
304
|
+
bottom: 0;
|
305
|
+
color: var(--search-text-color);
|
306
|
+
height: 100vh;
|
307
|
+
height: 100dvh;
|
308
|
+
left: 0;
|
309
|
+
max-height: 100vh;
|
310
|
+
height: 100dvh;
|
311
|
+
max-width: 100vw;
|
312
|
+
max-width: 100dvw;
|
313
|
+
position: fixed;
|
314
|
+
right: 0;
|
315
|
+
top: 0;
|
316
|
+
width: 100vw;
|
317
|
+
width: 100dvw;
|
318
|
+
|
319
|
+
@media (min-width: 64rem) {
|
320
|
+
padding-inline: 1rem;
|
321
|
+
}
|
322
|
+
}
|
323
|
+
|
324
|
+
::backdrop {
|
325
|
+
display: none;
|
326
|
+
}
|
327
|
+
|
328
|
+
.search__dialog {
|
329
|
+
background-color: var(--search-background-color);
|
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;
|
337
|
+
padding: 1rem;
|
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;
|
356
|
+
}
|
357
|
+
|
358
|
+
.search__input {
|
359
|
+
background-color: var(--search-excerpt-background-color);
|
360
|
+
border: 1px solid #808080;
|
361
|
+
border-radius: 0.2rem;
|
362
|
+
flex: 1 0 auto;
|
363
|
+
padding: 0.3rem 0.5rem;
|
364
|
+
|
365
|
+
&:focus-visible {
|
366
|
+
outline: solid 2px #0077ff;
|
367
|
+
}
|
368
|
+
}
|
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
|
+
|
380
|
+
.search__result {
|
381
|
+
margin-top: 1.5rem;
|
382
|
+
|
383
|
+
&:first-child {
|
384
|
+
margin-top: 1rem;
|
385
|
+
}
|
386
|
+
}
|
387
|
+
|
388
|
+
.search.loaded .search__result--loading {
|
389
|
+
display: none;
|
390
|
+
}
|
391
|
+
|
392
|
+
.search__result-excerpt {
|
393
|
+
background-color: var(--search-excerpt-background-color);
|
394
|
+
border: 1px solid var(--search-excerpt-border-color);
|
395
|
+
border-radius: 0.2rem;
|
396
|
+
box-shadow: 0 1px 3px 0 #0000001a;
|
397
|
+
display: block;
|
398
|
+
margin-top: 0.2rem;
|
399
|
+
padding: 0.3rem 0.5rem;
|
400
|
+
|
401
|
+
&:hover {
|
402
|
+
outline: solid 2px #0077ff;
|
403
|
+
}
|
404
|
+
}
|
405
|
+
|
249
406
|
/* CSS: MENU */
|
250
407
|
|
251
408
|
:root {
|
@@ -363,8 +520,10 @@ body {
|
|
363
520
|
.main {
|
364
521
|
display: flex;
|
365
522
|
flex-grow: 1;
|
366
|
-
|
523
|
+
margin-inline: auto;
|
524
|
+
max-width: 82rem;
|
367
525
|
padding-bottom: var(--content-padding-bottom);
|
526
|
+
width: 100%;
|
368
527
|
|
369
528
|
@media (min-width: 64rem) {
|
370
529
|
width: calc(100% - 20rem);
|
@@ -565,7 +724,7 @@ body {
|
|
565
724
|
.footer {
|
566
725
|
background-color: var(--footer-background-color);
|
567
726
|
flex: 0 0 auto;
|
568
|
-
padding: 4rem
|
727
|
+
padding: 4rem var(--nav-padding-horizontal);
|
569
728
|
}
|
570
729
|
|
571
730
|
.footer__sections {
|
@@ -603,6 +762,7 @@ body {
|
|
603
762
|
}
|
604
763
|
|
605
764
|
.credits {
|
765
|
+
padding-block: 1rem;
|
606
766
|
text-align: center;
|
607
767
|
}
|
608
768
|
|
@@ -644,9 +804,9 @@ body {
|
|
644
804
|
width: 1rem;
|
645
805
|
}
|
646
806
|
|
647
|
-
/* CSS:
|
807
|
+
/* CSS: ALERTS */
|
648
808
|
|
649
|
-
.
|
809
|
+
.alert {
|
650
810
|
border: 1px solid;
|
651
811
|
border-left: 0.5rem solid;
|
652
812
|
border-radius: 0.375rem;
|
@@ -654,44 +814,44 @@ body {
|
|
654
814
|
padding: 1rem;
|
655
815
|
}
|
656
816
|
|
657
|
-
.
|
817
|
+
.alert--tip {
|
658
818
|
background: #00940011;
|
659
819
|
border-color: #009400;
|
660
820
|
}
|
661
821
|
|
662
|
-
.
|
822
|
+
.alert--info {
|
663
823
|
background: #87cef911;
|
664
824
|
border-color: #87cef9;
|
665
825
|
}
|
666
826
|
|
667
|
-
.
|
827
|
+
.alert--warning {
|
668
828
|
background: #fea50011;
|
669
829
|
border-color: #fea500;
|
670
830
|
}
|
671
831
|
|
672
|
-
.
|
832
|
+
.alert--danger {
|
673
833
|
background: #db153b11;
|
674
834
|
border-color: #db153b;
|
675
835
|
}
|
676
836
|
|
677
|
-
.
|
837
|
+
.alert__header {
|
678
838
|
align-items: center;
|
679
839
|
display: flex;
|
680
840
|
gap: 0.5ch;
|
681
841
|
margin-bottom: 1rem;
|
682
842
|
}
|
683
843
|
|
684
|
-
.
|
844
|
+
.alert__icon {
|
685
845
|
height: 1rem;
|
686
846
|
width: 1rem;
|
687
847
|
}
|
688
848
|
|
689
|
-
.
|
849
|
+
.alert__label {
|
690
850
|
font-weight: bold;
|
691
851
|
margin: 0;
|
692
852
|
text-transform: uppercase;
|
693
853
|
}
|
694
854
|
|
695
|
-
.
|
855
|
+
.alert__content > :last-of-type {
|
696
856
|
margin-bottom: 0;
|
697
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)
|