docyard 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 +4 -4
- data/CHANGELOG.md +19 -1
- data/LICENSE.vscode-icons +42 -0
- data/README.md +46 -5
- data/lib/docyard/asset_handler.rb +33 -0
- data/lib/docyard/components/base_processor.rb +24 -0
- data/lib/docyard/components/callout_processor.rb +121 -0
- data/lib/docyard/components/code_block_processor.rb +55 -0
- data/lib/docyard/components/code_detector.rb +59 -0
- data/lib/docyard/components/icon_detector.rb +57 -0
- data/lib/docyard/components/icon_processor.rb +51 -0
- data/lib/docyard/components/registry.rb +34 -0
- data/lib/docyard/components/tabs_parser.rb +60 -0
- data/lib/docyard/components/tabs_processor.rb +44 -0
- data/lib/docyard/config/validator.rb +171 -0
- data/lib/docyard/config.rb +133 -0
- data/lib/docyard/constants.rb +5 -0
- data/lib/docyard/icons/LICENSE.phosphor +21 -0
- data/lib/docyard/icons/file_types.rb +92 -0
- data/lib/docyard/icons/phosphor.rb +63 -0
- data/lib/docyard/icons.rb +40 -0
- data/lib/docyard/initializer.rb +20 -2
- data/lib/docyard/language_mapping.rb +52 -0
- data/lib/docyard/markdown.rb +14 -3
- data/lib/docyard/rack_application.rb +76 -7
- data/lib/docyard/renderer.rb +40 -7
- data/lib/docyard/server.rb +5 -2
- data/lib/docyard/sidebar_builder.rb +10 -2
- data/lib/docyard/templates/assets/css/code.css +150 -2
- data/lib/docyard/templates/assets/css/components/callout.css +169 -0
- data/lib/docyard/templates/assets/css/components/code-block.css +196 -0
- data/lib/docyard/templates/assets/css/components/icon.css +16 -0
- data/lib/docyard/templates/assets/css/components/logo.css +44 -0
- data/lib/docyard/templates/assets/css/{components.css → components/navigation.css} +47 -47
- data/lib/docyard/templates/assets/css/components/tabs.css +298 -0
- data/lib/docyard/templates/assets/css/components/theme-toggle.css +61 -0
- data/lib/docyard/templates/assets/css/layout.css +14 -4
- data/lib/docyard/templates/assets/css/markdown.css +9 -8
- data/lib/docyard/templates/assets/css/reset.css +4 -0
- data/lib/docyard/templates/assets/css/variables.css +94 -3
- data/lib/docyard/templates/assets/favicon.svg +16 -0
- data/lib/docyard/templates/assets/js/components/code-block.js +162 -0
- data/lib/docyard/templates/assets/js/components/tabs.js +338 -0
- data/lib/docyard/templates/assets/js/theme.js +16 -0
- data/lib/docyard/templates/assets/logo-dark.svg +4 -0
- data/lib/docyard/templates/assets/logo.svg +12 -0
- data/lib/docyard/templates/config/docyard.yml.erb +20 -0
- data/lib/docyard/templates/layouts/default.html.erb +31 -3
- data/lib/docyard/templates/markdown/components/callouts.md.erb +204 -0
- data/lib/docyard/templates/markdown/components/icons.md.erb +125 -0
- data/lib/docyard/templates/markdown/components/tabs.md.erb +686 -0
- data/lib/docyard/templates/markdown/configuration.md.erb +202 -0
- data/lib/docyard/templates/partials/_callout.html.erb +11 -0
- data/lib/docyard/templates/partials/_code_block.html.erb +6 -0
- data/lib/docyard/templates/partials/_icon.html.erb +1 -0
- data/lib/docyard/templates/partials/_icon_file_extension.html.erb +1 -0
- data/lib/docyard/templates/partials/_tabs.html.erb +40 -0
- data/lib/docyard/templates/partials/_theme_toggle.html.erb +13 -0
- data/lib/docyard/version.rb +1 -1
- metadata +41 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eab96fe7bde5b0471a5a60996f818f5909d159e34a96034506e5095730e64988
|
|
4
|
+
data.tar.gz: 319e4251a18b40c28960c2854079420feac035e472bf9f5f03845cccfc310879
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 25c8f7d91d19d3905141894e7cf8f2968b7d7d20343f9fda48e5f105f952ba3f627227063a888dd70b7c17187950ac83e649196afbbde215e643e5f723157c1f
|
|
7
|
+
data.tar.gz: 338baa09ab053ffba20f745263f6bed124276ce707d8b2adc4d96ebf0a18b0b195f7097edfba8ffcde9265abe7cda3eaefd6710f01e3bf71b3836b87c21eb429
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2025-01-09
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Configuration system with optional `docyard.yml` file (#20)
|
|
14
|
+
- Logo and favicon support with light/dark mode switching (#21)
|
|
15
|
+
- Dark mode with theme toggle and system preference detection (#14)
|
|
16
|
+
- Icon system with 24 Phosphor icons and `:icon:` syntax (#15)
|
|
17
|
+
- Callouts/Admonitions with 5 types (note, tip, important, warning, danger) (#16)
|
|
18
|
+
- Tabs component with keyboard navigation and icon auto-detection (#17, #18)
|
|
19
|
+
- Copy button for code blocks with visual feedback (#19)
|
|
20
|
+
- Component-based architecture with processors for extensibility
|
|
21
|
+
- Asset handler with dynamic concatenation of component files
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- CSS architecture now uses CSS variables for comprehensive theming
|
|
25
|
+
- Markdown processing enhanced with preprocessor/postprocessor pattern
|
|
26
|
+
|
|
10
27
|
## [0.2.0] - 2025-11-08
|
|
11
28
|
|
|
12
29
|
### Added
|
|
@@ -46,7 +63,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
46
63
|
- Initial gem structure
|
|
47
64
|
- Project scaffolding
|
|
48
65
|
|
|
49
|
-
[Unreleased]: https://github.com/sanifhimani/docyard/compare/v0.
|
|
66
|
+
[Unreleased]: https://github.com/sanifhimani/docyard/compare/v0.3.0...HEAD
|
|
67
|
+
[0.3.0]: https://github.com/sanifhimani/docyard/compare/v0.2.0...v0.3.0
|
|
50
68
|
[0.2.0]: https://github.com/sanifhimani/docyard/compare/v0.1.0...v0.2.0
|
|
51
69
|
[0.1.0]: https://github.com/sanifhimani/docyard/compare/v0.0.1...v0.1.0
|
|
52
70
|
[0.0.1]: https://github.com/sanifhimani/docyard/releases/tag/v0.0.1
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
File Type Icons from VSCode Icons
|
|
2
|
+
=====================================
|
|
3
|
+
|
|
4
|
+
The file type icons embedded in lib/docyard/icons/file_types.rb are from the VSCode Icons project:
|
|
5
|
+
https://github.com/vscode-icons/vscode-icons
|
|
6
|
+
|
|
7
|
+
License: Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)
|
|
8
|
+
https://creativecommons.org/licenses/by-sa/4.0/
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2016 Roberto Huertas
|
|
11
|
+
|
|
12
|
+
Icons included:
|
|
13
|
+
- JavaScript (.js)
|
|
14
|
+
- TypeScript (.ts)
|
|
15
|
+
- JSX (.jsx)
|
|
16
|
+
- TSX (.tsx)
|
|
17
|
+
- Python (.py)
|
|
18
|
+
- Ruby (.rb)
|
|
19
|
+
- HTML (.html)
|
|
20
|
+
- CSS (.css)
|
|
21
|
+
- JSON (.json)
|
|
22
|
+
- YAML (.yaml)
|
|
23
|
+
- TOML (.toml)
|
|
24
|
+
- Go (.go)
|
|
25
|
+
- Rust (.rs)
|
|
26
|
+
- PHP (.php)
|
|
27
|
+
- SQL (.sql)
|
|
28
|
+
- MySQL (.mysql)
|
|
29
|
+
- PostgreSQL (.pgsql)
|
|
30
|
+
- GraphQL (.graphql)
|
|
31
|
+
- Vue (.vue)
|
|
32
|
+
- Svelte (.svelte)
|
|
33
|
+
- Protobuf (.proto)
|
|
34
|
+
|
|
35
|
+
These icons are used under the terms of the CC BY-SA 4.0 license.
|
|
36
|
+
You are free to:
|
|
37
|
+
- Share — copy and redistribute the material in any medium or format
|
|
38
|
+
- Adapt — remix, transform, and build upon the material for any purpose, even commercially
|
|
39
|
+
|
|
40
|
+
Under the following terms:
|
|
41
|
+
- Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made
|
|
42
|
+
- ShareAlike — If you remix, transform, or build upon the material, you must distribute your contributions under the same license
|
data/README.md
CHANGED
|
@@ -5,14 +5,22 @@
|
|
|
5
5
|
|
|
6
6
|
> Documentation generator for Ruby
|
|
7
7
|
|
|
8
|
-
**Early development** - Core features work, but missing search and build command. See [roadmap](#roadmap).
|
|
8
|
+
**Early development** - Core features and components work, but missing search and build command. See [roadmap](#roadmap).
|
|
9
9
|
|
|
10
10
|
## Features
|
|
11
11
|
|
|
12
|
+
- **Configuration system** - Optional `docyard.yml` for site metadata, branding, and build settings
|
|
13
|
+
- **Dark mode** - Beautiful light/dark theme with system preference detection
|
|
12
14
|
- **Sidebar navigation** - Automatic sidebar with nested folders and collapsible sections
|
|
13
15
|
- **Hot reload** - Changes appear instantly while you write
|
|
14
16
|
- **GitHub Flavored Markdown** - Tables, task lists, strikethrough
|
|
15
17
|
- **Syntax highlighting** - 100+ languages via Rouge
|
|
18
|
+
- **Markdown components**:
|
|
19
|
+
- **Callouts** - 5 types (note, tip, important, warning, danger) with GitHub alerts syntax
|
|
20
|
+
- **Tabs** - Code blocks, package managers, and custom tabs with keyboard navigation
|
|
21
|
+
- **Icons** - 24 Phosphor icons with `:icon:` syntax
|
|
22
|
+
- **Code block enhancements** - Copy button with visual feedback
|
|
23
|
+
- **Custom branding** - Logo and favicon with light/dark mode support
|
|
16
24
|
- **YAML frontmatter** - Add metadata to your pages
|
|
17
25
|
- **Customizable error pages** - Make 404/500 pages your own
|
|
18
26
|
|
|
@@ -119,6 +127,33 @@ Write links with `.md` extension, they'll be automatically cleaned:
|
|
|
119
127
|
[Guide](./guide/index.md) → /guide
|
|
120
128
|
```
|
|
121
129
|
|
|
130
|
+
### Using Icons
|
|
131
|
+
|
|
132
|
+
Docyard includes 24 essential Phosphor icons that work out of the box. Just type `:icon-name:` in your markdown:
|
|
133
|
+
|
|
134
|
+
```markdown
|
|
135
|
+
:check: Zero configuration
|
|
136
|
+
:lightning: Hot reload
|
|
137
|
+
:rocket-launch: Fast and lightweight
|
|
138
|
+
|
|
139
|
+
Use different weights:
|
|
140
|
+
:heart: → regular weight (default)
|
|
141
|
+
:heart:bold: → bold weight
|
|
142
|
+
:heart:fill: → filled version
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Available icons: `heart`, `check`, `x`, `warning`, `info`, `question`, `arrow-right`, `arrow-left`, `arrow-up`, `arrow-down`, `code`, `terminal`, `package`, `rocket-launch`, `star`, `lightning`, `moon-stars`, `sun`, `link-external`, `copy`, `github`, `file`, `terminal-window`, `warning-circle`.
|
|
146
|
+
|
|
147
|
+
Weights: `regular` (default), `bold`, `fill`, `light`, `thin`, `duotone`
|
|
148
|
+
|
|
149
|
+
Icons automatically match your text size and color.
|
|
150
|
+
|
|
151
|
+
**Adding new icons:**
|
|
152
|
+
|
|
153
|
+
1. Get the SVG path from [phosphoricons.com](https://phosphoricons.com)
|
|
154
|
+
2. Add to `lib/docyard/icons/phosphor.rb` under the appropriate weight
|
|
155
|
+
3. Format: `"icon-name" => '<path d="..."/>',`
|
|
156
|
+
|
|
122
157
|
### Directory Structure
|
|
123
158
|
|
|
124
159
|
```
|
|
@@ -170,11 +205,17 @@ bundle exec rubocop
|
|
|
170
205
|
|
|
171
206
|
## Roadmap
|
|
172
207
|
|
|
173
|
-
**
|
|
208
|
+
**v0.3.0 - Recently shipped:**
|
|
209
|
+
- Configuration system (docyard.yml)
|
|
210
|
+
- Logo and favicon support
|
|
174
211
|
- Dark mode with theme toggle
|
|
175
|
-
-
|
|
176
|
-
-
|
|
177
|
-
-
|
|
212
|
+
- Icon system (24 Phosphor icons)
|
|
213
|
+
- Callouts/Admonitions
|
|
214
|
+
- Tabs component
|
|
215
|
+
- Copy button for code blocks
|
|
216
|
+
|
|
217
|
+
**Next up (v0.4.0):**
|
|
218
|
+
- Sidebar customization
|
|
178
219
|
- Static site generation (`docyard build`)
|
|
179
220
|
|
|
180
221
|
## Contributing
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module Docyard
|
|
4
4
|
class AssetHandler
|
|
5
5
|
ASSETS_PATH = File.join(__dir__, "templates", "assets")
|
|
6
|
+
USER_ASSETS_PATH = "docs/assets"
|
|
6
7
|
|
|
7
8
|
CONTENT_TYPES = {
|
|
8
9
|
".css" => "text/css; charset=utf-8",
|
|
@@ -21,6 +22,9 @@ module Docyard
|
|
|
21
22
|
|
|
22
23
|
return forbidden_response if directory_traversal?(asset_path)
|
|
23
24
|
|
|
25
|
+
return serve_components_css if asset_path == "css/components.css"
|
|
26
|
+
return serve_components_js if asset_path == "js/components.js"
|
|
27
|
+
|
|
24
28
|
file_path = build_file_path(asset_path)
|
|
25
29
|
return not_found_response unless File.file?(file_path)
|
|
26
30
|
|
|
@@ -38,6 +42,9 @@ module Docyard
|
|
|
38
42
|
end
|
|
39
43
|
|
|
40
44
|
def build_file_path(asset_path)
|
|
45
|
+
user_path = File.join(USER_ASSETS_PATH, asset_path)
|
|
46
|
+
return user_path if File.file?(user_path)
|
|
47
|
+
|
|
41
48
|
File.join(ASSETS_PATH, asset_path)
|
|
42
49
|
end
|
|
43
50
|
|
|
@@ -48,6 +55,32 @@ module Docyard
|
|
|
48
55
|
[200, { "Content-Type" => content_type }, [content]]
|
|
49
56
|
end
|
|
50
57
|
|
|
58
|
+
def serve_components_css
|
|
59
|
+
content = concatenate_component_css
|
|
60
|
+
[200, { "Content-Type" => "text/css; charset=utf-8" }, [content]]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def concatenate_component_css
|
|
64
|
+
components_dir = File.join(ASSETS_PATH, "css", "components")
|
|
65
|
+
return "" unless Dir.exist?(components_dir)
|
|
66
|
+
|
|
67
|
+
css_files = Dir.glob(File.join(components_dir, "*.css"))
|
|
68
|
+
css_files.map { |file| File.read(file) }.join("\n\n")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def serve_components_js
|
|
72
|
+
content = concatenate_component_js
|
|
73
|
+
[200, { "Content-Type" => "application/javascript; charset=utf-8" }, [content]]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def concatenate_component_js
|
|
77
|
+
components_dir = File.join(ASSETS_PATH, "js", "components")
|
|
78
|
+
return "" unless Dir.exist?(components_dir)
|
|
79
|
+
|
|
80
|
+
js_files = Dir.glob(File.join(components_dir, "*.js"))
|
|
81
|
+
js_files.map { |file| File.read(file) }.join("\n\n")
|
|
82
|
+
end
|
|
83
|
+
|
|
51
84
|
def detect_content_type(file_path)
|
|
52
85
|
extension = File.extname(file_path)
|
|
53
86
|
CONTENT_TYPES.fetch(extension, "application/octet-stream")
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
module Components
|
|
5
|
+
class BaseProcessor
|
|
6
|
+
class << self
|
|
7
|
+
attr_accessor :priority
|
|
8
|
+
|
|
9
|
+
def inherited(subclass)
|
|
10
|
+
super
|
|
11
|
+
Registry.register(subclass)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def preprocess(content)
|
|
16
|
+
content
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def postprocess(html)
|
|
20
|
+
html
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../icons"
|
|
4
|
+
require_relative "../renderer"
|
|
5
|
+
require_relative "base_processor"
|
|
6
|
+
require "kramdown"
|
|
7
|
+
require "kramdown-parser-gfm"
|
|
8
|
+
|
|
9
|
+
module Docyard
|
|
10
|
+
module Components
|
|
11
|
+
class CalloutProcessor < BaseProcessor
|
|
12
|
+
self.priority = 10
|
|
13
|
+
|
|
14
|
+
CALLOUT_TYPES = {
|
|
15
|
+
"note" => { title: "Note", icon: "info", color: "note" },
|
|
16
|
+
"tip" => { title: "Tip", icon: "lightbulb", color: "tip" },
|
|
17
|
+
"important" => { title: "Important", icon: "warning-circle", color: "important" },
|
|
18
|
+
"warning" => { title: "Warning", icon: "warning", color: "warning" },
|
|
19
|
+
"danger" => { title: "Danger", icon: "siren", color: "danger" }
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
GITHUB_ALERT_TYPES = {
|
|
23
|
+
"NOTE" => "note",
|
|
24
|
+
"TIP" => "tip",
|
|
25
|
+
"IMPORTANT" => "important",
|
|
26
|
+
"WARNING" => "warning",
|
|
27
|
+
"CAUTION" => "danger"
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
def preprocess(markdown)
|
|
31
|
+
process_container_syntax(markdown)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def postprocess(html)
|
|
35
|
+
process_github_alerts(html)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def process_container_syntax(markdown)
|
|
41
|
+
markdown.gsub(/^:::[ \t]*(\w+)(?:[ \t]+([^\n]+?))?[ \t]*\n(.*?)^:::[ \t]*$/m) do
|
|
42
|
+
process_callout_match(Regexp.last_match(0), Regexp.last_match(1), Regexp.last_match(2), Regexp.last_match(3))
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def process_callout_match(original_match, type_raw, custom_title, content_markdown)
|
|
47
|
+
type = type_raw.downcase
|
|
48
|
+
return original_match unless CALLOUT_TYPES.key?(type)
|
|
49
|
+
|
|
50
|
+
config = CALLOUT_TYPES[type]
|
|
51
|
+
title = determine_title(custom_title, config[:title])
|
|
52
|
+
content_html = render_markdown_content(content_markdown.strip)
|
|
53
|
+
|
|
54
|
+
wrap_in_nomarkdown(render_callout_html(type, title, content_html, config[:icon]))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def determine_title(custom_title, default_title)
|
|
58
|
+
title = custom_title&.strip
|
|
59
|
+
title.nil? || title.empty? ? default_title : title
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def render_markdown_content(content_markdown)
|
|
63
|
+
return "" if content_markdown.empty?
|
|
64
|
+
|
|
65
|
+
Kramdown::Document.new(
|
|
66
|
+
content_markdown,
|
|
67
|
+
input: "GFM",
|
|
68
|
+
hard_wrap: false,
|
|
69
|
+
syntax_highlighter: "rouge"
|
|
70
|
+
).to_html
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def wrap_in_nomarkdown(html)
|
|
74
|
+
"{::nomarkdown}\n#{html}\n{:/nomarkdown}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def process_github_alerts(html)
|
|
78
|
+
github_alert_regex = %r{
|
|
79
|
+
<blockquote>\s*
|
|
80
|
+
<p>\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*
|
|
81
|
+
(?:<br\s*/>)?\s*
|
|
82
|
+
(.*?)</p>
|
|
83
|
+
(.*?)
|
|
84
|
+
</blockquote>
|
|
85
|
+
}mx
|
|
86
|
+
|
|
87
|
+
html.gsub(github_alert_regex) do
|
|
88
|
+
process_github_alert_match(Regexp.last_match(1), Regexp.last_match(2), Regexp.last_match(3))
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def process_github_alert_match(alert_type, first_para, rest_content)
|
|
93
|
+
type = GITHUB_ALERT_TYPES[alert_type]
|
|
94
|
+
config = CALLOUT_TYPES[type]
|
|
95
|
+
content_html = combine_alert_content(first_para.strip, rest_content.strip)
|
|
96
|
+
|
|
97
|
+
render_callout_html(type, config[:title], content_html, config[:icon])
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def combine_alert_content(first_para, rest_content)
|
|
101
|
+
return "<p>#{first_para}</p>" if rest_content.empty?
|
|
102
|
+
|
|
103
|
+
"<p>#{first_para}</p>#{rest_content}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def render_callout_html(type, title, content_html, icon_name)
|
|
107
|
+
icon_svg = Icons.render(icon_name, "duotone") || ""
|
|
108
|
+
renderer = Renderer.new
|
|
109
|
+
|
|
110
|
+
renderer.render_partial(
|
|
111
|
+
"_callout", {
|
|
112
|
+
type: type,
|
|
113
|
+
title: title,
|
|
114
|
+
content_html: content_html,
|
|
115
|
+
icon_svg: icon_svg
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../icons"
|
|
4
|
+
require_relative "../renderer"
|
|
5
|
+
require_relative "base_processor"
|
|
6
|
+
|
|
7
|
+
module Docyard
|
|
8
|
+
module Components
|
|
9
|
+
class CodeBlockProcessor < BaseProcessor
|
|
10
|
+
self.priority = 20
|
|
11
|
+
|
|
12
|
+
def postprocess(html)
|
|
13
|
+
return html unless html.include?('<div class="highlight">')
|
|
14
|
+
|
|
15
|
+
html.gsub(%r{<div class="highlight">(.*?)</div>}m) do
|
|
16
|
+
process_code_block(Regexp.last_match(0), Regexp.last_match(1))
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def process_code_block(original_html, inner_html)
|
|
23
|
+
code_text = extract_code_text(inner_html)
|
|
24
|
+
|
|
25
|
+
render_code_block_with_copy(original_html, code_text)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def extract_code_text(html)
|
|
29
|
+
text = html.gsub(/<[^>]+>/, "")
|
|
30
|
+
text = CGI.unescapeHTML(text)
|
|
31
|
+
text.strip
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def render_code_block_with_copy(code_block_html, code_text)
|
|
35
|
+
copy_icon = Icons.render("copy", "regular") || ""
|
|
36
|
+
renderer = Renderer.new
|
|
37
|
+
|
|
38
|
+
renderer.render_partial(
|
|
39
|
+
"_code_block", {
|
|
40
|
+
code_block_html: code_block_html,
|
|
41
|
+
code_text: escape_html_attribute(code_text),
|
|
42
|
+
copy_icon: copy_icon
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def escape_html_attribute(text)
|
|
48
|
+
text.gsub('"', """)
|
|
49
|
+
.gsub("'", "'")
|
|
50
|
+
.gsub("<", "<")
|
|
51
|
+
.gsub(">", ">")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../language_mapping"
|
|
4
|
+
|
|
5
|
+
module Docyard
|
|
6
|
+
module Components
|
|
7
|
+
class CodeDetector
|
|
8
|
+
def self.detect(content)
|
|
9
|
+
new(content).detect
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(content)
|
|
13
|
+
@content = content
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def detect
|
|
17
|
+
return nil unless code_only?
|
|
18
|
+
|
|
19
|
+
language = extract_language
|
|
20
|
+
return nil unless language
|
|
21
|
+
|
|
22
|
+
icon_for_language(language)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
attr_reader :content
|
|
28
|
+
|
|
29
|
+
def code_only?
|
|
30
|
+
stripped = content.strip
|
|
31
|
+
return false unless stripped.start_with?("```") && stripped.end_with?("```")
|
|
32
|
+
|
|
33
|
+
parts = stripped.split("```")
|
|
34
|
+
parts.length == 2 && parts[0].empty?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def extract_language
|
|
38
|
+
parts = content.strip.split("```")
|
|
39
|
+
return nil unless parts[1]
|
|
40
|
+
|
|
41
|
+
lines = parts[1].split("\n", 2)
|
|
42
|
+
lang_line = lines[0].strip
|
|
43
|
+
return nil if lang_line.empty? || lang_line.include?(" ")
|
|
44
|
+
|
|
45
|
+
lang_line.downcase
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def icon_for_language(language)
|
|
49
|
+
if LanguageMapping.terminal_language?(language)
|
|
50
|
+
{ icon: "terminal-window", source: "phosphor" }
|
|
51
|
+
elsif (extension = LanguageMapping.extension_for(language))
|
|
52
|
+
{ icon: extension, source: "file-extension" }
|
|
53
|
+
else
|
|
54
|
+
{ icon: "file", source: "phosphor" }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "code_detector"
|
|
4
|
+
|
|
5
|
+
module Docyard
|
|
6
|
+
module Components
|
|
7
|
+
class IconDetector
|
|
8
|
+
MANUAL_ICON_PATTERN = /^:([a-z0-9-]+):\s*(.+)$/i
|
|
9
|
+
|
|
10
|
+
def self.detect(tab_name, tab_content)
|
|
11
|
+
new(tab_name, tab_content).detect
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(tab_name, tab_content)
|
|
15
|
+
@tab_name = tab_name
|
|
16
|
+
@tab_content = tab_content
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def detect
|
|
20
|
+
manual_icon || auto_detected_icon || no_icon
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
attr_reader :tab_name, :tab_content
|
|
26
|
+
|
|
27
|
+
def manual_icon
|
|
28
|
+
return nil unless tab_name.match(MANUAL_ICON_PATTERN)
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
name: Regexp.last_match(2).strip,
|
|
32
|
+
icon: Regexp.last_match(1),
|
|
33
|
+
icon_source: "phosphor"
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def auto_detected_icon
|
|
38
|
+
detected = CodeDetector.detect(tab_content)
|
|
39
|
+
return nil unless detected
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
name: tab_name,
|
|
43
|
+
icon: detected[:icon],
|
|
44
|
+
icon_source: detected[:source]
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def no_icon
|
|
49
|
+
{
|
|
50
|
+
name: tab_name,
|
|
51
|
+
icon: nil,
|
|
52
|
+
icon_source: nil
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../icons"
|
|
4
|
+
require_relative "base_processor"
|
|
5
|
+
|
|
6
|
+
module Docyard
|
|
7
|
+
module Components
|
|
8
|
+
class IconProcessor < BaseProcessor
|
|
9
|
+
self.priority = 20
|
|
10
|
+
|
|
11
|
+
ICON_PATTERN = /:([a-z][a-z0-9-]*):(?:([a-z]+):)?/i
|
|
12
|
+
|
|
13
|
+
def postprocess(html)
|
|
14
|
+
segments = split_preserving_code_blocks(html)
|
|
15
|
+
|
|
16
|
+
segments.map do |segment|
|
|
17
|
+
segment[:type] == :code ? segment[:content] : process_segment(segment[:content])
|
|
18
|
+
end.join
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def split_preserving_code_blocks(html)
|
|
24
|
+
segments = []
|
|
25
|
+
current_pos = 0
|
|
26
|
+
|
|
27
|
+
html.scan(%r{<(code|pre)[^>]*>.*?</\1>}m) do
|
|
28
|
+
match_start = Regexp.last_match.begin(0)
|
|
29
|
+
match_end = Regexp.last_match.end(0)
|
|
30
|
+
|
|
31
|
+
segments << { type: :text, content: html[current_pos...match_start] } if match_start > current_pos
|
|
32
|
+
segments << { type: :code, content: html[match_start...match_end] }
|
|
33
|
+
|
|
34
|
+
current_pos = match_end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
segments << { type: :text, content: html[current_pos..] } if current_pos < html.length
|
|
38
|
+
|
|
39
|
+
segments.empty? ? [{ type: :text, content: html }] : segments
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def process_segment(content)
|
|
43
|
+
content.gsub(ICON_PATTERN) do
|
|
44
|
+
icon_name = Regexp.last_match(1)
|
|
45
|
+
weight = Regexp.last_match(2) || "regular"
|
|
46
|
+
Icons.render(icon_name, weight) || Regexp.last_match(0)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
module Components
|
|
5
|
+
class Registry
|
|
6
|
+
@processors = []
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def register(processor_class)
|
|
10
|
+
@processors << processor_class
|
|
11
|
+
@processors.sort_by! { |p| p.priority || 100 }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def run_preprocessors(content)
|
|
15
|
+
@processors.reduce(content) do |processed_content, processor_class|
|
|
16
|
+
processor_class.new.preprocess(processed_content)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run_postprocessors(html)
|
|
21
|
+
@processors.reduce(html) do |processed_html, processor_class|
|
|
22
|
+
processor_class.new.postprocess(processed_html)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def reset!
|
|
27
|
+
@processors = []
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
attr_reader :processors
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "icon_detector"
|
|
4
|
+
require "kramdown"
|
|
5
|
+
require "kramdown-parser-gfm"
|
|
6
|
+
|
|
7
|
+
module Docyard
|
|
8
|
+
module Components
|
|
9
|
+
class TabsParser
|
|
10
|
+
def self.parse(content)
|
|
11
|
+
new(content).parse
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(content)
|
|
15
|
+
@content = content
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def parse
|
|
19
|
+
sections.filter_map { |section| parse_section(section) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :content
|
|
25
|
+
|
|
26
|
+
def sections
|
|
27
|
+
content.split(/^==[ \t]+/)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def parse_section(section)
|
|
31
|
+
return nil if section.strip.empty?
|
|
32
|
+
|
|
33
|
+
parts = section.split("\n", 2)
|
|
34
|
+
tab_name = parts[0]&.strip
|
|
35
|
+
return nil if tab_name.nil? || tab_name.empty?
|
|
36
|
+
|
|
37
|
+
tab_content = parts[1]&.strip || ""
|
|
38
|
+
icon_data = IconDetector.detect(tab_name, tab_content)
|
|
39
|
+
|
|
40
|
+
{
|
|
41
|
+
name: icon_data[:name],
|
|
42
|
+
content: render_markdown(tab_content),
|
|
43
|
+
icon: icon_data[:icon],
|
|
44
|
+
icon_source: icon_data[:icon_source]
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def render_markdown(markdown_content)
|
|
49
|
+
return "" if markdown_content.empty?
|
|
50
|
+
|
|
51
|
+
Kramdown::Document.new(
|
|
52
|
+
markdown_content,
|
|
53
|
+
input: "GFM",
|
|
54
|
+
hard_wrap: false,
|
|
55
|
+
syntax_highlighter: "rouge"
|
|
56
|
+
).to_html
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|