docyard 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -3
- data/README.md +37 -12
- data/lib/docyard/components/heading_anchor_processor.rb +34 -0
- data/lib/docyard/components/table_of_contents_processor.rb +64 -0
- data/lib/docyard/config.rb +11 -0
- data/lib/docyard/icons/phosphor.rb +2 -1
- data/lib/docyard/markdown.rb +6 -0
- data/lib/docyard/prev_next_builder.rb +159 -0
- data/lib/docyard/rack_application.rb +24 -2
- data/lib/docyard/renderer.rb +16 -5
- data/lib/docyard/templates/assets/css/components/heading-anchor.css +77 -0
- data/lib/docyard/templates/assets/css/components/prev-next.css +114 -0
- data/lib/docyard/templates/assets/css/components/table-of-contents.css +269 -0
- data/lib/docyard/templates/assets/css/layout.css +58 -1
- data/lib/docyard/templates/assets/css/variables.css +1 -0
- data/lib/docyard/templates/assets/js/components/heading-anchor.js +90 -0
- data/lib/docyard/templates/assets/js/components/navigation.js +6 -2
- data/lib/docyard/templates/assets/js/components/table-of-contents.js +301 -0
- data/lib/docyard/templates/layouts/default.html.erb +9 -1
- data/lib/docyard/templates/partials/_heading_anchor.html.erb +1 -0
- data/lib/docyard/templates/partials/_prev_next.html.erb +23 -0
- data/lib/docyard/templates/partials/_table_of_contents.html.erb +45 -0
- data/lib/docyard/templates/partials/_table_of_contents_toggle.html.erb +8 -0
- data/lib/docyard/version.rb +1 -1
- metadata +13 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 52a27eaf396879abae3d0b091e5a488629022bb777838fafc17c0bb07e15d65b
|
|
4
|
+
data.tar.gz: ec6a567e2e411800f67f96351a80f7e60823ac399df8cac7e82b7c3a9a1370b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6e0d9995254e35e291250db40c9572d3bde1a76d86433d0fbcfb6e2a8602d6e68ad6be910d7a03036814dcdc0fba64289d68c760d9d6f1336376e2f709ec35b8
|
|
7
|
+
data.tar.gz: 127131f44ea7140f227a8e95468b96e09256b5fced9ef557e37e5a1caefc888adb902a8b2d164474cb1f31ae0ddd4b76b1c4684b39e5125ddce17c84bbd754e7
|
data/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
## [0.
|
|
10
|
+
## [0.5.0] - 2025-11-18
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Table of Contents** - Auto-generated TOC from h2-h4 headings with clickable anchor links and smooth scrolling (#30)
|
|
14
|
+
- **Previous/Next Navigation** - Auto-detection from sidebar order with frontmatter override support and configurable labels (#31)
|
|
15
|
+
|
|
16
|
+
## [0.4.0] - 2025-11-16
|
|
11
17
|
|
|
12
18
|
### Added
|
|
13
19
|
- **Static site generation** - Build system with `docyard build` command (#27)
|
|
@@ -29,7 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
29
35
|
- Component CSS accessibility and performance improvements (#24)
|
|
30
36
|
- Table responsive styling with proper wrapper element (#23)
|
|
31
37
|
|
|
32
|
-
## [0.3.0] - 2025-
|
|
38
|
+
## [0.3.0] - 2025-11-09
|
|
33
39
|
|
|
34
40
|
### Added
|
|
35
41
|
- Configuration system with optional `docyard.yml` file (#20)
|
|
@@ -85,7 +91,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
85
91
|
- Initial gem structure
|
|
86
92
|
- Project scaffolding
|
|
87
93
|
|
|
88
|
-
[Unreleased]: https://github.com/sanifhimani/docyard/compare/v0.
|
|
94
|
+
[Unreleased]: https://github.com/sanifhimani/docyard/compare/v0.5.0...HEAD
|
|
95
|
+
[0.5.0]: https://github.com/sanifhimani/docyard/compare/v0.4.0...v0.5.0
|
|
89
96
|
[0.4.0]: https://github.com/sanifhimani/docyard/compare/v0.3.0...v0.4.0
|
|
90
97
|
[0.3.0]: https://github.com/sanifhimani/docyard/compare/v0.2.0...v0.3.0
|
|
91
98
|
[0.2.0]: https://github.com/sanifhimani/docyard/compare/v0.1.0...v0.2.0
|
data/README.md
CHANGED
|
@@ -20,6 +20,8 @@ Build beautiful documentation sites with hot reload, dark mode, and powerful mar
|
|
|
20
20
|
### Navigation
|
|
21
21
|
- **Sidebar navigation** - Automatic sidebar with nested folders and collapsible sections
|
|
22
22
|
- **Sidebar customization** - Custom ordering, icons, and external links via config
|
|
23
|
+
- **Table of Contents** - Auto-generated TOC with heading anchors and smooth scrolling
|
|
24
|
+
- **Previous/Next navigation** - Auto-detection from sidebar with frontmatter override support
|
|
23
25
|
- **Active page highlighting** - Always know where you are
|
|
24
26
|
|
|
25
27
|
### Markdown
|
|
@@ -139,6 +141,32 @@ description: Page description
|
|
|
139
141
|
|
|
140
142
|
Currently supported:
|
|
141
143
|
- `title` - Page title (shown in `<title>` tag)
|
|
144
|
+
- `prev` - Customize or disable previous link
|
|
145
|
+
- `next` - Customize or disable next link
|
|
146
|
+
|
|
147
|
+
### Customizing Navigation
|
|
148
|
+
|
|
149
|
+
Control previous/next links per page via frontmatter:
|
|
150
|
+
|
|
151
|
+
```yaml
|
|
152
|
+
---
|
|
153
|
+
title: My Page
|
|
154
|
+
prev: false # Disable previous link
|
|
155
|
+
next:
|
|
156
|
+
text: Custom Next Page
|
|
157
|
+
link: /custom-path
|
|
158
|
+
---
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Configure labels globally in `docyard.yml`:
|
|
162
|
+
|
|
163
|
+
```yaml
|
|
164
|
+
navigation:
|
|
165
|
+
footer:
|
|
166
|
+
enabled: true
|
|
167
|
+
prev_text: "← Back"
|
|
168
|
+
next_text: "Forward →"
|
|
169
|
+
```
|
|
142
170
|
|
|
143
171
|
### Linking Between Pages
|
|
144
172
|
|
|
@@ -227,18 +255,15 @@ bundle exec rubocop
|
|
|
227
255
|
|
|
228
256
|
## Roadmap
|
|
229
257
|
|
|
230
|
-
**v0.
|
|
231
|
-
-
|
|
232
|
-
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
-
|
|
236
|
-
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
- Search functionality
|
|
240
|
-
- Table of contents
|
|
241
|
-
- More markdown components
|
|
258
|
+
**v0.5.0 - Just shipped:**
|
|
259
|
+
- Table of Contents with heading anchors
|
|
260
|
+
- Previous/Next page navigation with auto-detection
|
|
261
|
+
|
|
262
|
+
**Next up (v0.6.0+):**
|
|
263
|
+
- Code block enhancements (line numbers, highlighting, diffs)
|
|
264
|
+
- Search functionality (client-side with Cmd/K)
|
|
265
|
+
- Details/collapsible blocks
|
|
266
|
+
- More markdown extensions
|
|
242
267
|
|
|
243
268
|
## Contributing
|
|
244
269
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
module Components
|
|
5
|
+
class HeadingAnchorProcessor < BaseProcessor
|
|
6
|
+
self.priority = 30
|
|
7
|
+
|
|
8
|
+
def postprocess(html)
|
|
9
|
+
add_anchor_links(html)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def add_anchor_links(html)
|
|
15
|
+
html.gsub(%r{<(h[2-6])\s+id="([^"]+)">(.*?)</\1>}m) do |_match|
|
|
16
|
+
tag = Regexp.last_match(1)
|
|
17
|
+
id = Regexp.last_match(2)
|
|
18
|
+
content = Regexp.last_match(3)
|
|
19
|
+
|
|
20
|
+
anchor_html = render_anchor_link(id)
|
|
21
|
+
|
|
22
|
+
"<#{tag} id=\"#{id}\">#{content}#{anchor_html}</#{tag}>"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def render_anchor_link(id)
|
|
27
|
+
renderer = Renderer.new
|
|
28
|
+
renderer.render_partial("_heading_anchor", {
|
|
29
|
+
id: id
|
|
30
|
+
})
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
module Components
|
|
5
|
+
class TableOfContentsProcessor < BaseProcessor
|
|
6
|
+
self.priority = 35
|
|
7
|
+
|
|
8
|
+
def postprocess(html)
|
|
9
|
+
headings = extract_headings(html)
|
|
10
|
+
Thread.current[:docyard_toc] = headings
|
|
11
|
+
html
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def extract_headings(html)
|
|
17
|
+
headings = []
|
|
18
|
+
|
|
19
|
+
html.scan(%r{<(h[2-4])\s+id="([^"]+)">(.*?)</\1>}m) do
|
|
20
|
+
level = Regexp.last_match(1)[1].to_i
|
|
21
|
+
id = Regexp.last_match(2)
|
|
22
|
+
text = strip_html(Regexp.last_match(3))
|
|
23
|
+
|
|
24
|
+
headings << {
|
|
25
|
+
level: level,
|
|
26
|
+
id: id,
|
|
27
|
+
text: text
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
build_hierarchy(headings)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def build_hierarchy(headings)
|
|
35
|
+
return [] if headings.empty?
|
|
36
|
+
|
|
37
|
+
root = []
|
|
38
|
+
stack = []
|
|
39
|
+
|
|
40
|
+
headings.each do |heading|
|
|
41
|
+
heading[:children] = []
|
|
42
|
+
|
|
43
|
+
stack.pop while stack.any? && stack.last[:level] >= heading[:level]
|
|
44
|
+
|
|
45
|
+
if stack.empty?
|
|
46
|
+
root << heading
|
|
47
|
+
else
|
|
48
|
+
stack.last[:children] << heading
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
stack << heading
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
root
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def strip_html(text)
|
|
58
|
+
text.gsub(%r{<a[^>]*class="heading-anchor"[^>]*>.*?</a>}, "")
|
|
59
|
+
.gsub(/<[^>]+>/, "")
|
|
60
|
+
.strip
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
data/lib/docyard/config.rb
CHANGED
|
@@ -27,6 +27,13 @@ module Docyard
|
|
|
27
27
|
},
|
|
28
28
|
"sidebar" => {
|
|
29
29
|
"items" => []
|
|
30
|
+
},
|
|
31
|
+
"navigation" => {
|
|
32
|
+
"footer" => {
|
|
33
|
+
"enabled" => true,
|
|
34
|
+
"prev_text" => "Previous",
|
|
35
|
+
"next_text" => "Next"
|
|
36
|
+
}
|
|
30
37
|
}
|
|
31
38
|
}.freeze
|
|
32
39
|
|
|
@@ -63,6 +70,10 @@ module Docyard
|
|
|
63
70
|
@sidebar ||= ConfigSection.new(data["sidebar"])
|
|
64
71
|
end
|
|
65
72
|
|
|
73
|
+
def navigation
|
|
74
|
+
@navigation ||= ConfigSection.new(data["navigation"])
|
|
75
|
+
end
|
|
76
|
+
|
|
66
77
|
private
|
|
67
78
|
|
|
68
79
|
def load_config_data
|
|
@@ -35,7 +35,8 @@ module Docyard
|
|
|
35
35
|
"warning-octagon" => '<path d="M120,136V80a8,8,0,0,1,16,0v56a8,8,0,0,1-16,0ZM232,91.55v72.9a15.86,15.86,0,0,1-4.69,11.31l-51.55,51.55A15.86,15.86,0,0,1,164.45,232H91.55a15.86,15.86,0,0,1-11.31-4.69L28.69,175.76A15.86,15.86,0,0,1,24,164.45V91.55a15.86,15.86,0,0,1,4.69-11.31L80.24,28.69A15.86,15.86,0,0,1,91.55,24h72.9a15.86,15.86,0,0,1,11.31,4.69l51.55,51.55A15.86,15.86,0,0,1,232,91.55Zm-16,0L164.45,40H91.55L40,91.55v72.9L91.55,216h72.9L216,164.45ZM128,160a12,12,0,1,0,12,12A12,12,0,0,0,128,160Z"/>',
|
|
36
36
|
"siren" => '<path d="M120,16V8a8,8,0,0,1,16,0v8a8,8,0,0,1-16,0Zm80,32a8,8,0,0,0,5.66-2.34l8-8a8,8,0,0,0-11.32-11.32l-8,8A8,8,0,0,0,200,48ZM50.34,45.66A8,8,0,0,0,61.66,34.34l-8-8A8,8,0,0,0,42.34,37.66Zm87,26.45a8,8,0,1,0-2.64,15.78C153.67,91.08,168,108.32,168,128a8,8,0,0,0,16,0C184,100.6,163.93,76.57,137.32,72.11ZM232,176v24a16,16,0,0,1-16,16H40a16,16,0,0,1-16-16V176a16,16,0,0,1,16-16V128a88,88,0,0,1,88.67-88c48.15.36,87.33,40.29,87.33,89v31A16,16,0,0,1,232,176ZM56,160H200V129c0-40-32.05-72.71-71.45-73H128a72,72,0,0,0-72,72Zm160,40V176H40v24H216Z"/>',
|
|
37
37
|
"file" => '<path d="M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40V216a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160ZM200,216H56V40h88V88a8,8,0,0,0,8,8h48V216Z"/>',
|
|
38
|
-
"terminal-window" => '<path d="M128,128a8,8,0,0,1-3,6.25l-40,32a8,8,0,1,1-10-12.5L107.19,128,75,102.25a8,8,0,1,1,10-12.5l40,32A8,8,0,0,1,128,128Zm48,24H136a8,8,0,0,0,0,16h40a8,8,0,0,0,0-16Zm56-96V200a16,16,0,0,1-16,16H40a16,16,0,0,1-16-16V56A16,16,0,0,1,40,40H216A16,16,0,0,1,232,56ZM216,200V56H40V200H216Z"/>'
|
|
38
|
+
"terminal-window" => '<path d="M128,128a8,8,0,0,1-3,6.25l-40,32a8,8,0,1,1-10-12.5L107.19,128,75,102.25a8,8,0,1,1,10-12.5l40,32A8,8,0,0,1,128,128Zm48,24H136a8,8,0,0,0,0,16h40a8,8,0,0,0,0-16Zm56-96V200a16,16,0,0,1-16,16H40a16,16,0,0,1-16-16V56A16,16,0,0,1,40,40H216A16,16,0,0,1,232,56ZM216,200V56H40V200H216Z"/>',
|
|
39
|
+
"list-dashes" => '<path d="M88,64a8,8,0,0,1,8-8H216a8,8,0,0,1,0,16H96A8,8,0,0,1,88,64Zm128,56H96a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16Zm0,64H96a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16ZM56,56H40a8,8,0,0,0,0,16H56a8,8,0,0,0,0-16Zm0,64H40a8,8,0,0,0,0,16H56a8,8,0,0,0,0-16Zm0,64H40a8,8,0,0,0,0,16H56a8,8,0,0,0,0-16Z"/>'
|
|
39
40
|
},
|
|
40
41
|
"bold" => {
|
|
41
42
|
"heart" => '<path d="M178,36c-20.09,0-37.92,7.93-50,21.56C115.92,43.93,98.09,36,78,36a66.08,66.08,0,0,0-66,66c0,72.34,105.81,130.14,110.31,132.57a12,12,0,0,0,11.38,0C138.19,232.14,244,174.34,244,102A66.08,66.08,0,0,0,178,36Zm-5.49,142.36A328.69,328.69,0,0,1,128,210.16a328.69,328.69,0,0,1-44.51-31.8C61.82,159.77,36,131.42,36,102A42,42,0,0,1,78,60c17.8,0,32.7,9.4,38.89,24.54a12,12,0,0,0,22.22,0C145.3,69.4,160.2,60,178,60a42,42,0,0,1,42,42C220,131.42,194.18,159.77,172.51,178.36Z"/>'
|
data/lib/docyard/markdown.rb
CHANGED
|
@@ -10,6 +10,8 @@ require_relative "components/tabs_processor"
|
|
|
10
10
|
require_relative "components/icon_processor"
|
|
11
11
|
require_relative "components/code_block_processor"
|
|
12
12
|
require_relative "components/table_wrapper_processor"
|
|
13
|
+
require_relative "components/heading_anchor_processor"
|
|
14
|
+
require_relative "components/table_of_contents_processor"
|
|
13
15
|
|
|
14
16
|
module Docyard
|
|
15
17
|
class Markdown
|
|
@@ -53,6 +55,10 @@ module Docyard
|
|
|
53
55
|
frontmatter.dig("sidebar", "collapsed")
|
|
54
56
|
end
|
|
55
57
|
|
|
58
|
+
def toc
|
|
59
|
+
@toc ||= Thread.current[:docyard_toc] || []
|
|
60
|
+
end
|
|
61
|
+
|
|
56
62
|
private
|
|
57
63
|
|
|
58
64
|
def parse_frontmatter
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "renderer"
|
|
4
|
+
require_relative "utils/path_resolver"
|
|
5
|
+
|
|
6
|
+
module Docyard
|
|
7
|
+
class PrevNextBuilder
|
|
8
|
+
attr_reader :sidebar_tree, :current_path, :frontmatter, :config
|
|
9
|
+
|
|
10
|
+
def initialize(sidebar_tree:, current_path:, frontmatter: {}, config: {})
|
|
11
|
+
@sidebar_tree = sidebar_tree
|
|
12
|
+
@current_path = Utils::PathResolver.normalize(current_path)
|
|
13
|
+
@frontmatter = frontmatter
|
|
14
|
+
@config = config
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def prev_next_links
|
|
18
|
+
return nil unless enabled?
|
|
19
|
+
|
|
20
|
+
{
|
|
21
|
+
prev: build_prev_link,
|
|
22
|
+
next: build_next_link
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_html
|
|
27
|
+
links = prev_next_links
|
|
28
|
+
return "" if links.nil? || (links[:prev].nil? && links[:next].nil?)
|
|
29
|
+
|
|
30
|
+
Renderer.new.render_partial(
|
|
31
|
+
"_prev_next", {
|
|
32
|
+
prev: links[:prev],
|
|
33
|
+
next: links[:next],
|
|
34
|
+
prev_text: config_prev_text,
|
|
35
|
+
next_text: config_next_text
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def enabled?
|
|
43
|
+
return false if config_disabled?
|
|
44
|
+
return false if frontmatter_disabled?
|
|
45
|
+
|
|
46
|
+
true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def config_disabled?
|
|
50
|
+
return false if config.nil? || config.empty?
|
|
51
|
+
|
|
52
|
+
config == false || config["enabled"] == false || config[:enabled] == false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def frontmatter_disabled?
|
|
56
|
+
frontmatter["prev"] == false && frontmatter["next"] == false
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def build_prev_link
|
|
60
|
+
return nil if frontmatter["prev"] == false
|
|
61
|
+
|
|
62
|
+
return build_frontmatter_link(frontmatter["prev"]) if frontmatter["prev"]
|
|
63
|
+
|
|
64
|
+
auto_prev_link
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def build_next_link
|
|
68
|
+
return nil if frontmatter["next"] == false
|
|
69
|
+
|
|
70
|
+
return build_frontmatter_link(frontmatter["next"]) if frontmatter["next"]
|
|
71
|
+
|
|
72
|
+
auto_next_link
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def build_frontmatter_link(value)
|
|
76
|
+
case value
|
|
77
|
+
when String
|
|
78
|
+
find_link_by_text(value)
|
|
79
|
+
when Hash
|
|
80
|
+
{
|
|
81
|
+
title: value["text"] || value[:text],
|
|
82
|
+
path: value["link"] || value[:link]
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def find_link_by_text(text)
|
|
88
|
+
flat_links.find { |link| link[:title].downcase == text.downcase }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def auto_prev_link
|
|
92
|
+
index = current_page_index
|
|
93
|
+
return nil unless index&.positive?
|
|
94
|
+
|
|
95
|
+
flat_links[index - 1]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def auto_next_link
|
|
99
|
+
index = current_page_index
|
|
100
|
+
return nil unless index && index < flat_links.length - 1
|
|
101
|
+
|
|
102
|
+
flat_links[index + 1]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def current_page_index
|
|
106
|
+
@current_page_index ||= flat_links.find_index do |link|
|
|
107
|
+
normalized_path(link[:path]) == normalized_path(current_path)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def flat_links
|
|
112
|
+
@flat_links ||= begin
|
|
113
|
+
links = []
|
|
114
|
+
flatten_tree(sidebar_tree, links)
|
|
115
|
+
links.uniq { |link| normalized_path(link[:path]) }
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def flatten_tree(items, links)
|
|
120
|
+
items.each do |item|
|
|
121
|
+
links << build_link(item) if valid_navigation_item?(item)
|
|
122
|
+
flatten_tree(item[:children], links) if item[:children]&.any?
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def valid_navigation_item?(item)
|
|
127
|
+
item[:type] == :file && item[:path] && !external_link?(item[:path])
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def build_link(item)
|
|
131
|
+
{
|
|
132
|
+
title: item[:footer_text] || item[:title],
|
|
133
|
+
path: item[:path]
|
|
134
|
+
}
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def external_link?(path)
|
|
138
|
+
path.start_with?("http://", "https://")
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def normalized_path(path)
|
|
142
|
+
return "" if path.nil?
|
|
143
|
+
|
|
144
|
+
path.gsub(/[?#].*$/, "")
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def config_prev_text
|
|
148
|
+
return "Previous" if config.nil? || config.empty?
|
|
149
|
+
|
|
150
|
+
config["prev_text"] || config[:prev_text] || "Previous"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def config_next_text
|
|
154
|
+
return "Next" if config.nil? || config.empty?
|
|
155
|
+
|
|
156
|
+
config["next_text"] || config[:next_text] || "Next"
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "rack"
|
|
5
5
|
require_relative "sidebar_builder"
|
|
6
|
+
require_relative "prev_next_builder"
|
|
6
7
|
require_relative "constants"
|
|
7
8
|
|
|
8
9
|
module Docyard
|
|
@@ -46,9 +47,12 @@ module Docyard
|
|
|
46
47
|
end
|
|
47
48
|
|
|
48
49
|
def render_documentation_page(file_path, current_path)
|
|
50
|
+
sidebar_builder = build_sidebar_instance(current_path)
|
|
51
|
+
|
|
49
52
|
html = renderer.render_file(
|
|
50
53
|
file_path,
|
|
51
|
-
sidebar_html:
|
|
54
|
+
sidebar_html: sidebar_builder.to_html,
|
|
55
|
+
prev_next_html: build_prev_next(sidebar_builder, current_path, file_path),
|
|
52
56
|
branding: branding_options
|
|
53
57
|
)
|
|
54
58
|
|
|
@@ -60,14 +64,32 @@ module Docyard
|
|
|
60
64
|
[Constants::STATUS_NOT_FOUND, { "Content-Type" => Constants::CONTENT_TYPE_HTML }, [html]]
|
|
61
65
|
end
|
|
62
66
|
|
|
63
|
-
def
|
|
67
|
+
def build_sidebar_instance(current_path)
|
|
64
68
|
SidebarBuilder.new(
|
|
65
69
|
docs_path: docs_path,
|
|
66
70
|
current_path: current_path,
|
|
67
71
|
config: config
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def build_prev_next(sidebar_builder, current_path, file_path)
|
|
76
|
+
markdown_content = File.read(file_path)
|
|
77
|
+
markdown = Markdown.new(markdown_content)
|
|
78
|
+
|
|
79
|
+
PrevNextBuilder.new(
|
|
80
|
+
sidebar_tree: sidebar_builder.tree,
|
|
81
|
+
current_path: current_path,
|
|
82
|
+
frontmatter: markdown.frontmatter,
|
|
83
|
+
config: navigation_config
|
|
68
84
|
).to_html
|
|
69
85
|
end
|
|
70
86
|
|
|
87
|
+
def navigation_config
|
|
88
|
+
return {} unless config
|
|
89
|
+
|
|
90
|
+
config.navigation&.footer || {}
|
|
91
|
+
end
|
|
92
|
+
|
|
71
93
|
def branding_options
|
|
72
94
|
return default_branding unless config
|
|
73
95
|
|
data/lib/docyard/renderer.rb
CHANGED
|
@@ -16,24 +16,33 @@ module Docyard
|
|
|
16
16
|
@base_url = normalize_base_url(base_url)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def render_file(file_path, sidebar_html: "", branding: {})
|
|
19
|
+
def render_file(file_path, sidebar_html: "", prev_next_html: "", branding: {})
|
|
20
20
|
markdown_content = File.read(file_path)
|
|
21
21
|
markdown = Markdown.new(markdown_content)
|
|
22
22
|
|
|
23
23
|
html_content = strip_md_from_links(markdown.html)
|
|
24
|
+
toc = markdown.toc
|
|
24
25
|
|
|
25
26
|
render(
|
|
26
27
|
content: html_content,
|
|
27
28
|
page_title: markdown.title || Constants::DEFAULT_SITE_TITLE,
|
|
28
|
-
|
|
29
|
+
navigation: {
|
|
30
|
+
sidebar_html: sidebar_html,
|
|
31
|
+
prev_next_html: prev_next_html,
|
|
32
|
+
toc: toc
|
|
33
|
+
},
|
|
29
34
|
branding: branding
|
|
30
35
|
)
|
|
31
36
|
end
|
|
32
37
|
|
|
33
|
-
def render(content:, page_title: Constants::DEFAULT_SITE_TITLE,
|
|
38
|
+
def render(content:, page_title: Constants::DEFAULT_SITE_TITLE, navigation: {}, branding: {})
|
|
34
39
|
template = File.read(layout_path)
|
|
35
40
|
|
|
36
|
-
|
|
41
|
+
sidebar_html = navigation[:sidebar_html] || ""
|
|
42
|
+
prev_next_html = navigation[:prev_next_html] || ""
|
|
43
|
+
toc = navigation[:toc] || []
|
|
44
|
+
|
|
45
|
+
assign_content_variables(content, page_title, sidebar_html, prev_next_html, toc)
|
|
37
46
|
assign_branding_variables(branding)
|
|
38
47
|
|
|
39
48
|
ERB.new(template).result(binding)
|
|
@@ -85,10 +94,12 @@ module Docyard
|
|
|
85
94
|
url.end_with?("/") ? url : "#{url}/"
|
|
86
95
|
end
|
|
87
96
|
|
|
88
|
-
def assign_content_variables(content, page_title, sidebar_html)
|
|
97
|
+
def assign_content_variables(content, page_title, sidebar_html, prev_next_html, toc)
|
|
89
98
|
@content = content
|
|
90
99
|
@page_title = page_title
|
|
91
100
|
@sidebar_html = sidebar_html
|
|
101
|
+
@prev_next_html = prev_next_html
|
|
102
|
+
@toc = toc
|
|
92
103
|
end
|
|
93
104
|
|
|
94
105
|
def assign_branding_variables(branding)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
.content h2[id],
|
|
2
|
+
.content h3[id],
|
|
3
|
+
.content h4[id],
|
|
4
|
+
.content h5[id],
|
|
5
|
+
.content h6[id] {
|
|
6
|
+
position: relative;
|
|
7
|
+
scroll-margin-top: 100px;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* Tablet: Account for both primary + secondary header */
|
|
11
|
+
@media (max-width: 1280px) and (min-width: 1025px) {
|
|
12
|
+
.content h2[id],
|
|
13
|
+
.content h3[id],
|
|
14
|
+
.content h4[id],
|
|
15
|
+
.content h5[id],
|
|
16
|
+
.content h6[id] {
|
|
17
|
+
scroll-margin-top: calc(var(--header-height) + 3rem + var(--space-4));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.heading-anchor {
|
|
22
|
+
float: left;
|
|
23
|
+
margin-left: -0.75em;
|
|
24
|
+
padding-right: 0.25em;
|
|
25
|
+
font-weight: var(--font-weight-normal);
|
|
26
|
+
color: var(--color-primary);
|
|
27
|
+
text-decoration: none !important;
|
|
28
|
+
opacity: 0;
|
|
29
|
+
transition: opacity var(--transition-fast);
|
|
30
|
+
cursor: pointer;
|
|
31
|
+
user-select: none;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.heading-anchor:hover,
|
|
35
|
+
.heading-anchor:focus {
|
|
36
|
+
opacity: 1;
|
|
37
|
+
text-decoration: none !important;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.heading-anchor:active,
|
|
41
|
+
.heading-anchor:visited {
|
|
42
|
+
text-decoration: none !important;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.heading-anchor:focus {
|
|
46
|
+
outline: 2px solid var(--color-primary);
|
|
47
|
+
outline-offset: 2px;
|
|
48
|
+
border-radius: 4px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.content h2[id]:hover .heading-anchor,
|
|
52
|
+
.content h3[id]:hover .heading-anchor,
|
|
53
|
+
.content h4[id]:hover .heading-anchor,
|
|
54
|
+
.content h5[id]:hover .heading-anchor,
|
|
55
|
+
.content h6[id]:hover .heading-anchor {
|
|
56
|
+
opacity: 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@media (max-width: 1024px) {
|
|
60
|
+
.heading-anchor {
|
|
61
|
+
position: static;
|
|
62
|
+
margin-left: var(--space-2);
|
|
63
|
+
float: none;
|
|
64
|
+
opacity: 0.6;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.heading-anchor:hover,
|
|
68
|
+
.heading-anchor:focus {
|
|
69
|
+
opacity: 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@media (prefers-reduced-motion: reduce) {
|
|
74
|
+
.heading-anchor {
|
|
75
|
+
transition: none;
|
|
76
|
+
}
|
|
77
|
+
}
|