docyard 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/.rubocop.yml +3 -0
- data/CHANGELOG.md +41 -1
- data/LICENSE.vscode-icons +42 -0
- data/README.md +57 -8
- 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 +28 -0
- data/lib/docyard/errors.rb +54 -0
- data/lib/docyard/file_watcher.rb +2 -2
- 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 +27 -2
- data/lib/docyard/language_mapping.rb +52 -0
- data/lib/docyard/logging.rb +43 -0
- data/lib/docyard/markdown.rb +14 -3
- data/lib/docyard/rack_application.rb +100 -13
- data/lib/docyard/renderer.rb +40 -5
- data/lib/docyard/router.rb +13 -8
- data/lib/docyard/routing/resolution_result.rb +31 -0
- data/lib/docyard/server.rb +5 -2
- data/lib/docyard/sidebar/file_system_scanner.rb +77 -0
- data/lib/docyard/sidebar/renderer.rb +110 -0
- data/lib/docyard/sidebar/title_extractor.rb +25 -0
- data/lib/docyard/sidebar/tree_builder.rb +59 -0
- data/lib/docyard/sidebar_builder.rb +58 -0
- data/lib/docyard/templates/assets/css/code.css +362 -0
- 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/navigation.css +258 -0
- 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 +283 -0
- data/lib/docyard/templates/assets/css/main.css +10 -4
- data/lib/docyard/templates/assets/css/markdown.css +200 -0
- data/lib/docyard/templates/assets/css/reset.css +63 -0
- data/lib/docyard/templates/assets/css/typography.css +97 -0
- data/lib/docyard/templates/assets/css/variables.css +205 -0
- 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 +209 -1
- 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 +69 -19
- 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/markdown/core-concepts/file-structure.md.erb +61 -0
- data/lib/docyard/templates/markdown/core-concepts/markdown.md.erb +90 -0
- data/lib/docyard/templates/markdown/getting-started/installation.md.erb +43 -0
- data/lib/docyard/templates/markdown/getting-started/introduction.md.erb +30 -0
- data/lib/docyard/templates/markdown/getting-started/quick-start.md.erb +56 -0
- data/lib/docyard/templates/markdown/index.md.erb +78 -14
- 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/_icons.html.erb +11 -0
- data/lib/docyard/templates/partials/_nav_group.html.erb +7 -0
- data/lib/docyard/templates/partials/_nav_item.html.erb +3 -0
- data/lib/docyard/templates/partials/_nav_leaf.html.erb +1 -0
- data/lib/docyard/templates/partials/_nav_list.html.erb +3 -0
- data/lib/docyard/templates/partials/_nav_section.html.erb +6 -0
- data/lib/docyard/templates/partials/_sidebar.html.erb +6 -0
- data/lib/docyard/templates/partials/_sidebar_footer.html.erb +11 -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/utils/path_resolver.rb +30 -0
- data/lib/docyard/utils/text_formatter.rb +22 -0
- data/lib/docyard/version.rb +1 -1
- data/lib/docyard.rb +16 -4
- metadata +71 -3
- data/lib/docyard/templates/assets/css/syntax.css +0 -116
- data/lib/docyard/templates/markdown/getting-started.md.erb +0 -40
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/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,44 @@ 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
|
+
|
|
27
|
+
## [0.2.0] - 2025-11-08
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- Automatic sidebar navigation with nested folders
|
|
31
|
+
- Collapsible sidebar sections with active page highlighting
|
|
32
|
+
- Sidebar scroll position persistence
|
|
33
|
+
- Modern responsive theme with mobile hamburger menu
|
|
34
|
+
- Structured logging system with configurable levels
|
|
35
|
+
- Custom error classes for better error handling
|
|
36
|
+
- Constants module for shared application values
|
|
37
|
+
- Utility modules for path resolution and text formatting
|
|
38
|
+
- Improved initial templates with nested docs structure
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
- Refactored sidebar rendering to use partial templates
|
|
42
|
+
- Modular CSS architecture (split into variables, reset, typography, components, layout)
|
|
43
|
+
- Enhanced router with routing resolution result pattern
|
|
44
|
+
|
|
45
|
+
### Removed
|
|
46
|
+
- Legacy syntax.css in favor of modular code.css
|
|
47
|
+
|
|
10
48
|
## [0.1.0] - 2025-11-04
|
|
11
49
|
|
|
12
50
|
### Added
|
|
@@ -25,6 +63,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
25
63
|
- Initial gem structure
|
|
26
64
|
- Project scaffolding
|
|
27
65
|
|
|
28
|
-
[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
|
|
68
|
+
[0.2.0]: https://github.com/sanifhimani/docyard/compare/v0.1.0...v0.2.0
|
|
29
69
|
[0.1.0]: https://github.com/sanifhimani/docyard/compare/v0.0.1...v0.1.0
|
|
30
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,13 +5,22 @@
|
|
|
5
5
|
|
|
6
6
|
> Documentation generator for Ruby
|
|
7
7
|
|
|
8
|
-
**Early development** - Core features work, but missing
|
|
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
|
|
14
|
+
- **Sidebar navigation** - Automatic sidebar with nested folders and collapsible sections
|
|
12
15
|
- **Hot reload** - Changes appear instantly while you write
|
|
13
16
|
- **GitHub Flavored Markdown** - Tables, task lists, strikethrough
|
|
14
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
|
|
15
24
|
- **YAML frontmatter** - Add metadata to your pages
|
|
16
25
|
- **Customizable error pages** - Make 404/500 pages your own
|
|
17
26
|
|
|
@@ -56,8 +65,14 @@ docyard init
|
|
|
56
65
|
This creates:
|
|
57
66
|
```
|
|
58
67
|
docs/
|
|
59
|
-
index.md
|
|
60
|
-
getting-started
|
|
68
|
+
index.md # Home page
|
|
69
|
+
getting-started/
|
|
70
|
+
introduction.md # Getting started guide
|
|
71
|
+
installation.md # Installation instructions
|
|
72
|
+
quick-start.md # Quick start guide
|
|
73
|
+
core-concepts/
|
|
74
|
+
file-structure.md # File structure guide
|
|
75
|
+
markdown.md # Markdown guide
|
|
61
76
|
```
|
|
62
77
|
|
|
63
78
|
### Start Development Server
|
|
@@ -112,6 +127,33 @@ Write links with `.md` extension, they'll be automatically cleaned:
|
|
|
112
127
|
[Guide](./guide/index.md) → /guide
|
|
113
128
|
```
|
|
114
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
|
+
|
|
115
157
|
### Directory Structure
|
|
116
158
|
|
|
117
159
|
```
|
|
@@ -163,11 +205,18 @@ bundle exec rubocop
|
|
|
163
205
|
|
|
164
206
|
## Roadmap
|
|
165
207
|
|
|
166
|
-
-
|
|
167
|
-
-
|
|
168
|
-
-
|
|
169
|
-
-
|
|
170
|
-
-
|
|
208
|
+
**v0.3.0 - Recently shipped:**
|
|
209
|
+
- Configuration system (docyard.yml)
|
|
210
|
+
- Logo and favicon support
|
|
211
|
+
- Dark mode with theme toggle
|
|
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
|
|
219
|
+
- Static site generation (`docyard build`)
|
|
171
220
|
|
|
172
221
|
## Contributing
|
|
173
222
|
|
|
@@ -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
|