hotdocs 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/app/assets/javascript/controllers/search_controller.js +155 -0
- data/app/assets/stylesheets/hotdocs/application.css +124 -1
- data/app/views/layouts/hotdocs/application.html.erb +32 -1
- data/lib/hotdocs/markdown.rb +1 -2
- data/lib/hotdocs/version.rb +1 -1
- data/lib/install/install.rb +47 -12
- data/lib/tasks/hotdocs_tasks.rake +86 -4
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e9062e9c27d95f790d4a77b9508b3ac57b518d41573896f358e508d03eb5b676
|
4
|
+
data.tar.gz: 29d4da1687c46d5bf2821aae114a96d30191c94204ab6936fd82892f0ed117ce
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 568702e50ee8305c0858c46e2acad029e3cf9c3c6f45517fe566fa73980025b67e389316694e8d0ac878f5bea4da98c7cd19923deccaa340192384bb26a66a7c
|
7
|
+
data.tar.gz: 9acea8151aa28cb1a6d89d21a45553cba06b86b093ff20aa87f6632a95d9250b09b698bf6c160cfa3f14f5c0905f5472e259e9a2aab7d23b6a9e2d8d4aff176a
|
data/README.md
CHANGED
@@ -22,7 +22,7 @@ HotDocs is a set of optimized Rails components & tools for writing docs:
|
|
22
22
|
| Styled components you can customize | ✅ | ✅ | ✅ |
|
23
23
|
| Markdown (with syntax highlight & themes) | 🚀 | 👍 | 🚀 |
|
24
24
|
| Static export | 🔜 🚀 | 👍 | 🚀 |
|
25
|
-
| Search |
|
25
|
+
| Search | ✅ | 🔌 | 🔌 |
|
26
26
|
| Light / Dark | 🔜 ✅ | 🔌 | ✅ |
|
27
27
|
| Open source | ✅ | ✅ | ✅ |
|
28
28
|
| Free | ✅ | ✅ | ✅ |
|
@@ -0,0 +1,155 @@
|
|
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, { once: true });
|
14
|
+
}
|
15
|
+
|
16
|
+
open() {
|
17
|
+
if (this.searchTarget.open) return;
|
18
|
+
this._allowClosing();
|
19
|
+
this._initSearch();
|
20
|
+
this.searchTarget.showModal();
|
21
|
+
}
|
22
|
+
|
23
|
+
search = debounce(this._search, 200);
|
24
|
+
|
25
|
+
_allowOpening() {
|
26
|
+
this.keydownOpen = (event) => {
|
27
|
+
if (this.searchTarget.open || event.key !== "/") return;
|
28
|
+
event.preventDefault();
|
29
|
+
this.open();
|
30
|
+
};
|
31
|
+
|
32
|
+
document.addEventListener("keydown", this.keydownOpen);
|
33
|
+
}
|
34
|
+
|
35
|
+
_allowClosing() {
|
36
|
+
this.clickClose = (event) => {
|
37
|
+
if (this.dialogTarget.contains(event.target)) return;
|
38
|
+
this.searchTarget.close();
|
39
|
+
};
|
40
|
+
|
41
|
+
document.addEventListener("click", this.clickClose, { once: true });
|
42
|
+
}
|
43
|
+
|
44
|
+
_initSearch() {
|
45
|
+
if (this.documents) {
|
46
|
+
this.searchTarget.classList.add("loaded");
|
47
|
+
return;
|
48
|
+
}
|
49
|
+
this._createSearchIndex();
|
50
|
+
this.searchTarget.classList.add("loaded");
|
51
|
+
}
|
52
|
+
|
53
|
+
_createSearchIndex() {
|
54
|
+
const documents = this._getDocuments();
|
55
|
+
if (documents.length === 0) return;
|
56
|
+
this.documents = documents;
|
57
|
+
this.searchIndex = lunr(function () {
|
58
|
+
this.ref("title");
|
59
|
+
this.field("title", { boost: 5 });
|
60
|
+
this.field("text");
|
61
|
+
this.metadataWhitelist = ["position"];
|
62
|
+
documents.forEach(function (doc) {
|
63
|
+
this.add(doc);
|
64
|
+
}, this);
|
65
|
+
});
|
66
|
+
}
|
67
|
+
|
68
|
+
_getDocuments() {
|
69
|
+
const searchData = JSON.parse(this.dataTarget.textContent);
|
70
|
+
if (searchData.length === 0) {
|
71
|
+
console.warn(
|
72
|
+
[
|
73
|
+
"The search data is not present in the HTML.",
|
74
|
+
"If you are in development, run `bundle exec rails hotdocs:index`.",
|
75
|
+
"If you are in production, assets compilation should have taken care of it.",
|
76
|
+
].join(" ")
|
77
|
+
);
|
78
|
+
}
|
79
|
+
return searchData.map((data) => {
|
80
|
+
const div = document.createElement("div");
|
81
|
+
div.innerHTML = data.html;
|
82
|
+
return { ...data, text: div.innerText };
|
83
|
+
});
|
84
|
+
}
|
85
|
+
|
86
|
+
_search(event) {
|
87
|
+
if (!this.searchIndex) return;
|
88
|
+
const query = event.target.value;
|
89
|
+
const results = this.searchIndex.search(query).slice(0, 10);
|
90
|
+
this._displayResults(results);
|
91
|
+
}
|
92
|
+
|
93
|
+
_displayResults(results) {
|
94
|
+
this.resultsTarget.innerHTML = null;
|
95
|
+
|
96
|
+
results.forEach((result) => {
|
97
|
+
const matches = Object.keys(result.matchData.metadata);
|
98
|
+
const excerpt = this._withExcerpt(matches, result)[0];
|
99
|
+
if (!excerpt) return;
|
100
|
+
this.resultsTarget.appendChild(this._createResultElement(excerpt));
|
101
|
+
});
|
102
|
+
}
|
103
|
+
|
104
|
+
_withExcerpt(matches, result) {
|
105
|
+
return matches.flatMap((match) => {
|
106
|
+
return Object.keys(result.matchData.metadata[match]).map((key) => {
|
107
|
+
const position = result.matchData.metadata[match][key].position[0];
|
108
|
+
const [sliceStart, sliceLength] = key === "text" ? position : [0, 0];
|
109
|
+
const doc = this.documents.find((doc) => doc.title === result.ref);
|
110
|
+
const excerpt = this._excerpt(doc.text, sliceStart, sliceLength);
|
111
|
+
return { ...doc, excerpt };
|
112
|
+
});
|
113
|
+
});
|
114
|
+
}
|
115
|
+
|
116
|
+
_excerpt(doc, sliceStart, sliceLength) {
|
117
|
+
const startPos = Math.max(sliceStart - 80, 0);
|
118
|
+
const endPos = Math.min(sliceStart + sliceLength + 80, doc.length);
|
119
|
+
return [
|
120
|
+
startPos > 0 ? "..." : "",
|
121
|
+
doc.slice(startPos, sliceStart),
|
122
|
+
"<strong>" +
|
123
|
+
escapeHtmlEntities(doc.slice(sliceStart, sliceStart + sliceLength)) +
|
124
|
+
"</strong>",
|
125
|
+
doc.slice(sliceStart + sliceLength, endPos),
|
126
|
+
endPos < doc.length ? "..." : "",
|
127
|
+
].join("");
|
128
|
+
}
|
129
|
+
|
130
|
+
_createResultElement(excerpt) {
|
131
|
+
const clone = this.resultTemplateTarget.content.cloneNode(true);
|
132
|
+
const li = clone.querySelector("li");
|
133
|
+
li.querySelector("h1").innerHTML = `${excerpt.parent} > ${excerpt.title}`;
|
134
|
+
li.querySelector("a").innerHTML = excerpt.excerpt;
|
135
|
+
li.querySelector("a").href = excerpt.url;
|
136
|
+
return clone;
|
137
|
+
}
|
138
|
+
}
|
139
|
+
|
140
|
+
function debounce(func, wait) {
|
141
|
+
let timeoutId;
|
142
|
+
|
143
|
+
return function (...args) {
|
144
|
+
clearTimeout(timeoutId);
|
145
|
+
timeoutId = setTimeout(() => func.apply(this, args), wait);
|
146
|
+
};
|
147
|
+
}
|
148
|
+
|
149
|
+
function escapeHtmlEntities(string) {
|
150
|
+
return String(string)
|
151
|
+
.replace(/&/g, "&")
|
152
|
+
.replace(/</g, "<")
|
153
|
+
.replace(/>/g, ">")
|
154
|
+
.replace(/"/g, """);
|
155
|
+
}
|
@@ -246,6 +246,127 @@ 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
|
+
left: 0;
|
308
|
+
max-height: 100vh;
|
309
|
+
max-width: 100vw;
|
310
|
+
padding-inline: 1rem;
|
311
|
+
position: fixed;
|
312
|
+
right: 0;
|
313
|
+
top: 0;
|
314
|
+
width: 100vw;
|
315
|
+
}
|
316
|
+
|
317
|
+
::backdrop {
|
318
|
+
display: none;
|
319
|
+
}
|
320
|
+
|
321
|
+
.search__dialog {
|
322
|
+
overflow: auto;
|
323
|
+
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;
|
328
|
+
padding: 1rem;
|
329
|
+
width: auto;
|
330
|
+
}
|
331
|
+
|
332
|
+
.search__input {
|
333
|
+
background-color: var(--search-excerpt-background-color);
|
334
|
+
border: 1px solid #808080;
|
335
|
+
border-radius: 0.2rem;
|
336
|
+
padding: 0.3rem 0.5rem;
|
337
|
+
width: 100%;
|
338
|
+
|
339
|
+
&:focus-visible {
|
340
|
+
outline: solid 2px #0077ff;
|
341
|
+
}
|
342
|
+
}
|
343
|
+
|
344
|
+
.search__result {
|
345
|
+
margin-top: 1.5rem;
|
346
|
+
|
347
|
+
&:first-child {
|
348
|
+
margin-top: 1rem;
|
349
|
+
}
|
350
|
+
}
|
351
|
+
|
352
|
+
.search.loaded .search__result--loading {
|
353
|
+
display: none;
|
354
|
+
}
|
355
|
+
|
356
|
+
.search__result-excerpt {
|
357
|
+
background-color: var(--search-excerpt-background-color);
|
358
|
+
border: 1px solid var(--search-excerpt-border-color);
|
359
|
+
border-radius: 0.2rem;
|
360
|
+
box-shadow: 0 1px 3px 0 #0000001a;
|
361
|
+
display: block;
|
362
|
+
margin-top: 0.2rem;
|
363
|
+
padding: 0.3rem 0.5rem;
|
364
|
+
|
365
|
+
&:hover {
|
366
|
+
outline: solid 2px #0077ff;
|
367
|
+
}
|
368
|
+
}
|
369
|
+
|
249
370
|
/* CSS: MENU */
|
250
371
|
|
251
372
|
:root {
|
@@ -363,8 +484,10 @@ body {
|
|
363
484
|
.main {
|
364
485
|
display: flex;
|
365
486
|
flex-grow: 1;
|
366
|
-
|
487
|
+
margin-inline: auto;
|
488
|
+
max-width: 82rem;
|
367
489
|
padding-bottom: var(--content-padding-bottom);
|
490
|
+
width: 100%;
|
368
491
|
|
369
492
|
@media (min-width: 64rem) {
|
370
493
|
width: calc(100% - 20rem);
|
@@ -9,7 +9,30 @@
|
|
9
9
|
<%= stylesheet_link_tag "hotdocs/application", media: "all", "data-turbo-track": "reload" %>
|
10
10
|
</head>
|
11
11
|
|
12
|
-
<body>
|
12
|
+
<body data-controller="search">
|
13
|
+
<dialog data-search-target="search" class="search">
|
14
|
+
<div data-search-target="dialog" class="search__dialog">
|
15
|
+
<input autofocus data-action="input->search#search" type="text" class="search__input"></input>
|
16
|
+
|
17
|
+
<template data-search-target="resultTemplate">
|
18
|
+
<li class="search__result">
|
19
|
+
<h1></h1>
|
20
|
+
<a href="#" class="search__result-excerpt"></a>
|
21
|
+
</li>
|
22
|
+
</template>
|
23
|
+
|
24
|
+
<ul data-search-target="results">
|
25
|
+
<li class="search__result search__result--loading">
|
26
|
+
Loading index...
|
27
|
+
</li>
|
28
|
+
</ul>
|
29
|
+
</div>
|
30
|
+
|
31
|
+
<script type="application/json" data-search-target="data">
|
32
|
+
<%= raw(Rails.application.assets.resolver.read("search_data.json")&.force_encoding("UTF-8") || [].to_json) %>
|
33
|
+
</script>
|
34
|
+
</dialog>
|
35
|
+
|
13
36
|
<nav class="nav" data-controller="sidenav" data-sidenav-open-class-value="sidenav--open" data-sidenav-main-menu-class-value="sidenav__sections--main">
|
14
37
|
<div class="nav__section">
|
15
38
|
<button class="nav__toggle" type="button" aria-label="Toggle navigation" aria-expanded="false" data-action="click->sidenav#open">
|
@@ -39,6 +62,14 @@
|
|
39
62
|
<%= item %>
|
40
63
|
<% end %>
|
41
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
|
+
|
71
|
+
<span class="search-button__label">Type / to search</span>
|
72
|
+
</button>
|
42
73
|
</div>
|
43
74
|
|
44
75
|
<div class="sidenav-backdrop"></div>
|
data/lib/hotdocs/markdown.rb
CHANGED
@@ -3,9 +3,8 @@ require "open3"
|
|
3
3
|
class MarkdownHandler
|
4
4
|
def self.prepare(engine)
|
5
5
|
# Install npm packages
|
6
|
+
# `capture3` raises if deno is not available
|
6
7
|
Open3.capture3("deno --allow-read --allow-env --node-modules-dir=auto #{engine.root.join("lib/hotdocs/markdown.mjs")}", stdin_data: "")
|
7
|
-
rescue
|
8
|
-
Rails.logger.info("deno not found: Could not install npm packages.")
|
9
8
|
end
|
10
9
|
|
11
10
|
def initialize(engine)
|
data/lib/hotdocs/version.rb
CHANGED
data/lib/install/install.rb
CHANGED
@@ -7,7 +7,7 @@ def gem?(name)
|
|
7
7
|
|
8
8
|
regex = /gem ["']#{name}["']/
|
9
9
|
if File.readlines(gemfile_path).grep(regex).any?
|
10
|
-
say "#{name} already
|
10
|
+
say "#{name} already bundled"
|
11
11
|
false
|
12
12
|
else
|
13
13
|
run("bundle add #{name}") || abort("Failed to add #{name} to the bundle")
|
@@ -15,6 +15,19 @@ def gem?(name)
|
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
|
+
def pin?(name)
|
19
|
+
importmap_path = Pathname(destination_root).join("config/importmap.rb")
|
20
|
+
|
21
|
+
regex = /pin ["']#{name}["']/
|
22
|
+
if File.readlines(importmap_path).grep(regex).any?
|
23
|
+
say "#{name} already pinned"
|
24
|
+
false
|
25
|
+
else
|
26
|
+
run("bin/importmap pin #{name}") || abort("Failed to pin #{name} to the importmap")
|
27
|
+
true
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
18
31
|
unless system("command -v deno > /dev/null 2>&1")
|
19
32
|
abort "Install deno before running this task. Read more: https://deno.com"
|
20
33
|
end
|
@@ -27,13 +40,14 @@ gem?("importmap-rails") && run("bin/rails importmap:install")
|
|
27
40
|
gem?("turbo-rails") && run("bin/rails turbo:install")
|
28
41
|
gem?("stimulus-rails") && run("bin/rails stimulus:install")
|
29
42
|
|
30
|
-
|
31
|
-
remove_file(Pathname(destination_root).join("app/views/hotdocs/index.html.erb"))
|
32
|
-
inject_into_class(Pathname(destination_root).join("app/controllers/hotdocs_controller.rb"), "HotdocsController", <<-LINES)
|
33
|
-
helper Hotdocs::Engine.helpers
|
34
|
-
layout "hotdocs"
|
43
|
+
pin?("lunr")
|
35
44
|
|
36
|
-
|
45
|
+
create_file(Pathname(destination_root).join("app/controllers/hotdocs_controller.rb"), <<~CONTROLLER)
|
46
|
+
class HotdocsController < ApplicationController
|
47
|
+
helper Hotdocs::Engine.helpers
|
48
|
+
layout "hotdocs"
|
49
|
+
end
|
50
|
+
CONTROLLER
|
37
51
|
|
38
52
|
create_file(Pathname(destination_root).join("app/views/layouts/hotdocs.html.erb"), <<~LAYOUT)
|
39
53
|
<% content_for :head do %>
|
@@ -127,6 +141,18 @@ create_file(Pathname(destination_root).join("app/helpers/hotdocs_helper.rb"), <<
|
|
127
141
|
HELPER
|
128
142
|
|
129
143
|
create_file(Pathname(destination_root).join("app/assets/stylesheets/prism.css"), <<~CSS)
|
144
|
+
/* Find more themes on: https://github.com/PrismJS/prism-themes */
|
145
|
+
|
146
|
+
/*
|
147
|
+
Darcula theme
|
148
|
+
|
149
|
+
Adapted from a theme based on:
|
150
|
+
IntelliJ Darcula Theme (https://github.com/bulenkov/Darcula)
|
151
|
+
|
152
|
+
@author Alexandre Paradis <service.paradis@gmail.com>
|
153
|
+
@version 1.0
|
154
|
+
*/
|
155
|
+
|
130
156
|
code[class*="language-"],
|
131
157
|
pre[class*="language-"] {
|
132
158
|
color: #a9b7c6;
|
@@ -278,10 +304,19 @@ create_file(Pathname(destination_root).join("app/assets/stylesheets/prism.css"),
|
|
278
304
|
}
|
279
305
|
CSS
|
280
306
|
|
307
|
+
empty_directory "app/assets/builds"
|
308
|
+
keep_file "app/assets/builds"
|
309
|
+
if Pathname(destination_root).join(".gitignore").exist?
|
310
|
+
append_to_file(".gitignore", %(\n/app/assets/builds/*\n!/app/assets/builds/.keep\n))
|
311
|
+
append_to_file(".gitignore", %(\n/node_modules/\n))
|
312
|
+
end
|
313
|
+
|
281
314
|
routes_path = Pathname(destination_root).join("config/routes.rb")
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
315
|
+
routes = File.readlines(routes_path)
|
316
|
+
unless routes.grep(/hotdocs#index/).any?
|
317
|
+
if routes.grep(/^\s*(?!#)root/).any?
|
318
|
+
route "get '/hotdocs', to: 'hotdocs#index'"
|
319
|
+
else
|
320
|
+
route "root to: 'hotdocs#index'"
|
321
|
+
end
|
287
322
|
end
|
@@ -1,9 +1,91 @@
|
|
1
1
|
namespace :hotdocs do
|
2
2
|
desc "Install HotDocs into the app"
|
3
3
|
task :install do
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
location = File.expand_path("../install/install.rb", __dir__)
|
5
|
+
system("#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{location}")
|
6
|
+
# Needed for hotdocs:index to find the generated ::HotdocsController
|
7
|
+
Rails.application.reloader.reload!
|
8
|
+
Rake::Task["hotdocs:index"].invoke
|
8
9
|
end
|
10
|
+
|
11
|
+
desc "Build search data"
|
12
|
+
task index: :environment do
|
13
|
+
path = Rails.root.join("app/assets/builds/search_data.json")
|
14
|
+
# Propshaft caches the `@load_path`s. Rendering data goes through Propshaft
|
15
|
+
# because of the assets, so the file must exist before rendering.
|
16
|
+
File.write(path, "")
|
17
|
+
data = render_search_data.call.to_json
|
18
|
+
File.write(path, data)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
if Rake::Task.task_defined?("assets:precompile")
|
23
|
+
Rake::Task["assets:precompile"].enhance([ "hotdocs:index" ])
|
24
|
+
end
|
25
|
+
|
26
|
+
if Rake::Task.task_defined?("test:prepare")
|
27
|
+
Rake::Task["test:prepare"].enhance([ "hotdocs:index" ])
|
28
|
+
elsif Rake::Task.task_defined?("spec:prepare")
|
29
|
+
Rake::Task["spec:prepare"].enhance([ "hotdocs:index" ])
|
30
|
+
elsif Rake::Task.task_defined?("db:test:prepare")
|
31
|
+
Rake::Task["db:test:prepare"].enhance([ "hotdocs:index" ])
|
32
|
+
end
|
33
|
+
|
34
|
+
def render_search_data
|
35
|
+
renderer = Class.new(::HotdocsController) do
|
36
|
+
include Hotdocs::ApplicationHelper
|
37
|
+
|
38
|
+
def call
|
39
|
+
with_no_view_annotations { render_search_data }
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def with_no_view_annotations(&)
|
45
|
+
annotate = Rails.application.config.action_view.annotate_rendered_view_with_filenames
|
46
|
+
Rails.application.config.action_view.annotate_rendered_view_with_filenames = false
|
47
|
+
yield
|
48
|
+
ensure
|
49
|
+
Rails.application.config.action_view.annotate_rendered_view_with_filename = annotate
|
50
|
+
end
|
51
|
+
|
52
|
+
def render_search_data
|
53
|
+
pages = pages_from(menu_items)
|
54
|
+
$stderr.puts "Indexing #{pages.size} pages:"
|
55
|
+
render_pages(pages).tap { $stderr.puts }
|
56
|
+
end
|
57
|
+
|
58
|
+
def render_pages(pages)
|
59
|
+
pages.filter_map do |page|
|
60
|
+
$stderr.putc "."
|
61
|
+
html = render_path(page.fetch(:url))
|
62
|
+
next unless html
|
63
|
+
{ **page, html: html }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def pages_from(menu_items, parent = "Docs")
|
68
|
+
menu_items
|
69
|
+
.filter { _1.fetch(:url).start_with?("/") }
|
70
|
+
.flat_map do |item|
|
71
|
+
current = { title: item.fetch(:label), parent: parent, url: item.fetch(:url) }
|
72
|
+
children = pages_from(item.fetch(:children, []), item.fetch(:label))
|
73
|
+
[ current ] + children
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def render_path(path)
|
78
|
+
controller, action = Rails.application.routes.recognize_path(path).values_at(:controller, :action)
|
79
|
+
render_to_string("#{controller}/#{action}", layout: false)
|
80
|
+
rescue ActionController::RoutingError => error
|
81
|
+
logger.info("Skipped building #{path}: #{error}")
|
82
|
+
nil
|
83
|
+
end
|
84
|
+
|
85
|
+
def request
|
86
|
+
ActionDispatch::TestRequest.create
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
renderer.new
|
9
91
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hotdocs
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- 3v0k4
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-04-07 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: rails
|
@@ -36,6 +36,7 @@ files:
|
|
36
36
|
- Rakefile
|
37
37
|
- app/assets/images/hotdocs/icon.svg
|
38
38
|
- app/assets/javascript/controllers/accordion_controller.js
|
39
|
+
- app/assets/javascript/controllers/search_controller.js
|
39
40
|
- app/assets/javascript/controllers/sidenav_controller.js
|
40
41
|
- app/assets/javascript/controllers/toc_controller.js
|
41
42
|
- app/assets/stylesheets/hotdocs/application.css
|