docyard 0.5.0 → 0.7.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 +1 -1
- data/CHANGELOG.md +34 -1
- data/lib/docyard/build/static_generator.rb +3 -44
- data/lib/docyard/builder.rb +14 -4
- data/lib/docyard/cli.rb +6 -3
- data/lib/docyard/components/aliases.rb +29 -0
- data/lib/docyard/components/base_processor.rb +6 -0
- data/lib/docyard/components/processors/callout_processor.rb +124 -0
- data/lib/docyard/components/processors/code_block_diff_preprocessor.rb +106 -0
- data/lib/docyard/components/processors/code_block_focus_preprocessor.rb +79 -0
- data/lib/docyard/components/processors/code_block_options_preprocessor.rb +78 -0
- data/lib/docyard/components/processors/code_block_processor.rb +175 -0
- data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +127 -0
- data/lib/docyard/components/processors/heading_anchor_processor.rb +39 -0
- data/lib/docyard/components/processors/icon_processor.rb +53 -0
- data/lib/docyard/components/processors/table_of_contents_processor.rb +68 -0
- data/lib/docyard/components/processors/table_wrapper_processor.rb +22 -0
- data/lib/docyard/components/processors/tabs_processor.rb +48 -0
- data/lib/docyard/components/registry.rb +4 -4
- data/lib/docyard/components/support/code_block/feature_extractor.rb +117 -0
- data/lib/docyard/components/support/code_block/icon_detector.rb +44 -0
- data/lib/docyard/components/support/code_block/line_parser.rb +84 -0
- data/lib/docyard/components/support/code_block/line_wrapper.rb +50 -0
- data/lib/docyard/components/support/code_block/patterns.rb +55 -0
- data/lib/docyard/components/support/code_detector.rb +61 -0
- data/lib/docyard/components/support/tabs/icon_detector.rb +62 -0
- data/lib/docyard/components/support/tabs/parser.rb +195 -0
- data/lib/docyard/components/support/tabs/range_finder.rb +46 -0
- data/lib/docyard/config/branding_resolver.rb +74 -0
- data/lib/docyard/{constants.rb → config/constants.rb} +1 -0
- data/lib/docyard/config/validator.rb +8 -0
- data/lib/docyard/config.rb +17 -1
- data/lib/docyard/{prev_next_builder.rb → navigation/prev_next_builder.rb} +2 -2
- data/lib/docyard/{sidebar → navigation/sidebar}/renderer.rb +3 -14
- data/lib/docyard/{sidebar → navigation/sidebar}/tree_builder.rb +9 -2
- data/lib/docyard/{sidebar_builder.rb → navigation/sidebar_builder.rb} +3 -15
- data/lib/docyard/{icons → rendering/icons}/file_types.rb +0 -13
- data/lib/docyard/{icons → rendering/icons}/phosphor.rb +4 -1
- data/lib/docyard/{markdown.rb → rendering/markdown.rb} +23 -14
- data/lib/docyard/{renderer.rb → rendering/renderer.rb} +24 -20
- data/lib/docyard/search/build_indexer.rb +74 -0
- data/lib/docyard/search/dev_indexer.rb +110 -0
- data/lib/docyard/search/pagefind_support.rb +31 -0
- data/lib/docyard/{asset_handler.rb → server/asset_handler.rb} +1 -1
- data/lib/docyard/{server.rb → server/dev_server.rb} +32 -9
- data/lib/docyard/{preview_server.rb → server/preview_server.rb} +1 -1
- data/lib/docyard/{rack_application.rb → server/rack_application.rb} +53 -50
- data/lib/docyard/server/resolution_result.rb +29 -0
- data/lib/docyard/{router.rb → server/router.rb} +4 -4
- data/lib/docyard/templates/assets/css/code.css +12 -4
- data/lib/docyard/templates/assets/css/components/code-block.css +427 -24
- data/lib/docyard/templates/assets/css/components/navigation.css +12 -9
- data/lib/docyard/templates/assets/css/components/search.css +549 -0
- data/lib/docyard/templates/assets/css/components/tabs.css +50 -44
- data/lib/docyard/templates/assets/css/layout.css +15 -1
- data/lib/docyard/templates/assets/css/variables.css +44 -0
- data/lib/docyard/templates/assets/js/components/search.js +685 -0
- data/lib/docyard/templates/layouts/default.html.erb +14 -2
- data/lib/docyard/templates/partials/_code_block.html.erb +50 -2
- data/lib/docyard/templates/partials/_heading_anchor.html.erb +1 -1
- data/lib/docyard/templates/partials/_prev_next.html.erb +1 -1
- data/lib/docyard/templates/partials/_search_modal.html.erb +45 -0
- data/lib/docyard/templates/partials/_search_trigger.html.erb +22 -0
- data/lib/docyard/utils/html_helpers.rb +14 -0
- data/lib/docyard/utils/path_resolver.rb +2 -1
- data/lib/docyard/utils/url_helpers.rb +20 -0
- data/lib/docyard/version.rb +1 -1
- data/lib/docyard.rb +22 -15
- metadata +57 -36
- data/lib/docyard/components/callout_processor.rb +0 -121
- data/lib/docyard/components/code_block_processor.rb +0 -55
- data/lib/docyard/components/code_detector.rb +0 -59
- data/lib/docyard/components/heading_anchor_processor.rb +0 -34
- data/lib/docyard/components/icon_detector.rb +0 -57
- data/lib/docyard/components/icon_processor.rb +0 -51
- data/lib/docyard/components/table_of_contents_processor.rb +0 -64
- data/lib/docyard/components/table_wrapper_processor.rb +0 -18
- data/lib/docyard/components/tabs_parser.rb +0 -60
- data/lib/docyard/components/tabs_processor.rb +0 -44
- data/lib/docyard/routing/resolution_result.rb +0 -31
- /data/lib/docyard/{sidebar → navigation/sidebar}/config_parser.rb +0 -0
- /data/lib/docyard/{sidebar → navigation/sidebar}/file_system_scanner.rb +0 -0
- /data/lib/docyard/{sidebar → navigation/sidebar}/item.rb +0 -0
- /data/lib/docyard/{sidebar → navigation/sidebar}/title_extractor.rb +0 -0
- /data/lib/docyard/{icons → rendering/icons}/LICENSE.phosphor +0 -0
- /data/lib/docyard/{icons.rb → rendering/icons.rb} +0 -0
- /data/lib/docyard/{language_mapping.rb → rendering/language_mapping.rb} +0 -0
- /data/lib/docyard/{file_watcher.rb → server/file_watcher.rb} +0 -0
- /data/lib/docyard/{errors.rb → utils/errors.rb} +0 -0
- /data/lib/docyard/{logging.rb → utils/logging.rb} +0 -0
|
@@ -12,6 +12,7 @@ module Docyard
|
|
|
12
12
|
validate_site_section
|
|
13
13
|
validate_branding_section
|
|
14
14
|
validate_build_section
|
|
15
|
+
validate_markdown_section
|
|
15
16
|
|
|
16
17
|
raise ConfigError, format_errors if @errors.any?
|
|
17
18
|
end
|
|
@@ -48,6 +49,13 @@ module Docyard
|
|
|
48
49
|
validate_boolean(build["clean"], "build.clean")
|
|
49
50
|
end
|
|
50
51
|
|
|
52
|
+
def validate_markdown_section
|
|
53
|
+
markdown = @config["markdown"]
|
|
54
|
+
return unless markdown
|
|
55
|
+
|
|
56
|
+
validate_boolean(markdown["lineNumbers"], "markdown.lineNumbers") if markdown.key?("lineNumbers")
|
|
57
|
+
end
|
|
58
|
+
|
|
51
59
|
def validate_string(value, field_name)
|
|
52
60
|
return if value.nil?
|
|
53
61
|
return if value.is_a?(String)
|
data/lib/docyard/config.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "yaml"
|
|
4
4
|
require_relative "config/validator"
|
|
5
|
-
require_relative "constants"
|
|
5
|
+
require_relative "config/constants"
|
|
6
6
|
|
|
7
7
|
module Docyard
|
|
8
8
|
class Config
|
|
@@ -34,6 +34,14 @@ module Docyard
|
|
|
34
34
|
"prev_text" => "Previous",
|
|
35
35
|
"next_text" => "Next"
|
|
36
36
|
}
|
|
37
|
+
},
|
|
38
|
+
"markdown" => {
|
|
39
|
+
"lineNumbers" => false
|
|
40
|
+
},
|
|
41
|
+
"search" => {
|
|
42
|
+
"enabled" => true,
|
|
43
|
+
"placeholder" => "Search documentation...",
|
|
44
|
+
"exclude" => []
|
|
37
45
|
}
|
|
38
46
|
}.freeze
|
|
39
47
|
|
|
@@ -74,6 +82,14 @@ module Docyard
|
|
|
74
82
|
@navigation ||= ConfigSection.new(data["navigation"])
|
|
75
83
|
end
|
|
76
84
|
|
|
85
|
+
def markdown
|
|
86
|
+
@markdown ||= ConfigSection.new(data["markdown"])
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def search
|
|
90
|
+
@search ||= ConfigSection.new(data["search"])
|
|
91
|
+
end
|
|
92
|
+
|
|
77
93
|
private
|
|
78
94
|
|
|
79
95
|
def load_config_data
|
|
@@ -5,7 +5,9 @@ require "erb"
|
|
|
5
5
|
module Docyard
|
|
6
6
|
module Sidebar
|
|
7
7
|
class Renderer
|
|
8
|
-
|
|
8
|
+
include Utils::UrlHelpers
|
|
9
|
+
|
|
10
|
+
PARTIALS_PATH = File.join(__dir__, "../../templates/partials")
|
|
9
11
|
|
|
10
12
|
attr_reader :site_title, :base_url
|
|
11
13
|
|
|
@@ -39,19 +41,6 @@ module Docyard
|
|
|
39
41
|
Icons.render(name.to_s.tr("_", "-"), weight) || ""
|
|
40
42
|
end
|
|
41
43
|
|
|
42
|
-
def link_path(path)
|
|
43
|
-
return path if path.nil? || path.start_with?("http://", "https://")
|
|
44
|
-
|
|
45
|
-
"#{base_url.chomp('/')}#{path}"
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def normalize_base_url(url)
|
|
49
|
-
return "/" if url.nil? || url.empty?
|
|
50
|
-
|
|
51
|
-
url = "/#{url}" unless url.start_with?("/")
|
|
52
|
-
url.end_with?("/") ? url : "#{url}/"
|
|
53
|
-
end
|
|
54
|
-
|
|
55
44
|
def render_tree_with_sections(items)
|
|
56
45
|
filtered_items = items.reject { |item| item[:title]&.downcase == site_title.downcase }
|
|
57
46
|
grouped_items = group_by_section(filtered_items)
|
|
@@ -29,6 +29,7 @@ module Docyard
|
|
|
29
29
|
|
|
30
30
|
def transform_directory(item, relative_base)
|
|
31
31
|
dir_path = File.join(relative_base, item[:name])
|
|
32
|
+
children = transform_items(item[:children], dir_path)
|
|
32
33
|
|
|
33
34
|
{
|
|
34
35
|
title: Utils::TextFormatter.titleize(item[:name]),
|
|
@@ -36,11 +37,17 @@ module Docyard
|
|
|
36
37
|
active: false,
|
|
37
38
|
type: :directory,
|
|
38
39
|
collapsible: true,
|
|
39
|
-
collapsed:
|
|
40
|
-
children:
|
|
40
|
+
collapsed: !active_child?(children),
|
|
41
|
+
children: children
|
|
41
42
|
}
|
|
42
43
|
end
|
|
43
44
|
|
|
45
|
+
def active_child?(children)
|
|
46
|
+
children.any? do |child|
|
|
47
|
+
child[:active] || active_child?(child[:children] || [])
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
44
51
|
def transform_file(item, relative_base)
|
|
45
52
|
file_path = File.join(relative_base, "#{item[:name]}#{Constants::MARKDOWN_EXTENSION}")
|
|
46
53
|
full_file_path = File.join(docs_path, file_path)
|
|
@@ -50,11 +50,7 @@ module Docyard
|
|
|
50
50
|
def config_sidebar_items
|
|
51
51
|
return [] unless config
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
config.dig("sidebar", "items") || config.dig(:sidebar, :items) || []
|
|
55
|
-
else
|
|
56
|
-
config.sidebar&.items || []
|
|
57
|
-
end
|
|
53
|
+
config.sidebar&.items || []
|
|
58
54
|
end
|
|
59
55
|
|
|
60
56
|
def config_parser
|
|
@@ -84,19 +80,11 @@ module Docyard
|
|
|
84
80
|
end
|
|
85
81
|
|
|
86
82
|
def extract_base_url
|
|
87
|
-
|
|
88
|
-
config.dig(:build, :base_url) || "/"
|
|
89
|
-
else
|
|
90
|
-
config&.build&.base_url || "/"
|
|
91
|
-
end
|
|
83
|
+
config&.build&.base_url || "/"
|
|
92
84
|
end
|
|
93
85
|
|
|
94
86
|
def extract_site_title
|
|
95
|
-
|
|
96
|
-
config[:site_title] || "Documentation"
|
|
97
|
-
else
|
|
98
|
-
config&.site&.title || "Documentation"
|
|
99
|
-
end
|
|
87
|
+
config&.site&.title || "Documentation"
|
|
100
88
|
end
|
|
101
89
|
end
|
|
102
90
|
end
|
|
@@ -8,7 +8,6 @@ module Docyard
|
|
|
8
8
|
# License: CC BY-SA 4.0 (see LICENSE.vscode-icons)
|
|
9
9
|
# Copyright (c) 2016 Roberto Huertas
|
|
10
10
|
module FileTypes
|
|
11
|
-
# SVG content for each file type icon
|
|
12
11
|
# rubocop:disable Layout/LineLength
|
|
13
12
|
ICONS = {
|
|
14
13
|
"css" => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" aria-labelledby="css-logo-title css-logo-description"><title>CSS Logo</title><g style="display:inline"><path fill="#639" d="M1.995 1.994h23.52a4.48 4.48 0 0 1 4.48 4.48v19.04a4.48 4.48 0 0 1-4.48 4.48H6.475a4.48 4.48 0 0 1-4.48-4.48Z"/><path fill="#fff" d="M9.079 24.87v-4.704c0-1.876 1.204-2.884 3.024-2.884 1.792-.028 2.912 1.148 2.856 3.136h-2.072c.056-.756-.28-1.316-.84-1.288-.7 0-.896.476-.896 1.372v4.088c0 .868.28 1.288.896 1.316.644 0 .896-.644.84-1.372h2.072c.112 2.044-1.176 3.248-2.996 3.22-1.764 0-2.884-.98-2.884-2.884zm6.636-.336h1.932c.028.896.308 1.456.924 1.456.616 0 .84-.364.84-1.204 0-.7-.308-1.092-1.064-1.456l-.728-.336c-1.288-.616-1.82-1.372-1.82-2.884 0-1.68 1.064-2.856 2.8-2.856 1.736 0 2.66 1.204 2.688 3.164h-1.876c0-.812-.168-1.372-.784-1.372-.56 0-.84.28-.84.98s.252.98.924 1.26l.672.308c1.428.672 2.044 1.54 2.044 3.164 0 1.932-1.092 2.996-2.884 2.996-1.792 0-2.8-1.232-2.828-3.22zm6.328 0h1.96c0 .896.308 1.456.896 1.456.588 0 .84-.364.84-1.204 0-.7-.28-1.092-1.064-1.456l-.728-.336c-1.288-.616-1.792-1.372-1.792-2.884 0-1.68 1.036-2.856 2.8-2.856 1.764 0 2.632 1.204 2.688 3.164h-1.876c-.028-.812-.196-1.372-.812-1.372-.56 0-.812.28-.812.98s.224.98.896 1.26l.7.308c1.4.672 2.016 1.54 2.016 3.164 0 1.932-1.092 2.996-2.884 2.996-1.792 0-2.8-1.232-2.828-3.22z"/></g></svg>',
|
|
@@ -35,7 +34,6 @@ module Docyard
|
|
|
35
34
|
}.freeze
|
|
36
35
|
# rubocop:enable Layout/LineLength
|
|
37
36
|
|
|
38
|
-
# Map of file extensions/aliases to icon names
|
|
39
37
|
EXTENSIONS = {
|
|
40
38
|
"css" => "css",
|
|
41
39
|
"go" => "go",
|
|
@@ -62,10 +60,6 @@ module Docyard
|
|
|
62
60
|
"postgresql" => "pgsql"
|
|
63
61
|
}.freeze
|
|
64
62
|
|
|
65
|
-
# Get the SVG content for a file extension
|
|
66
|
-
#
|
|
67
|
-
# @param extension [String] The file extension (e.g., "js", "ts")
|
|
68
|
-
# @return [String, nil] The SVG content, or nil if not found
|
|
69
63
|
def self.svg(extension)
|
|
70
64
|
icon_name = EXTENSIONS[extension.to_s.downcase]
|
|
71
65
|
return nil unless icon_name
|
|
@@ -73,17 +67,10 @@ module Docyard
|
|
|
73
67
|
ICONS[icon_name]
|
|
74
68
|
end
|
|
75
69
|
|
|
76
|
-
# Check if an icon exists for a file extension
|
|
77
|
-
#
|
|
78
|
-
# @param extension [String] The file extension to check
|
|
79
|
-
# @return [Boolean] true if icon exists
|
|
80
70
|
def self.exists?(extension)
|
|
81
71
|
EXTENSIONS.key?(extension.to_s.downcase)
|
|
82
72
|
end
|
|
83
73
|
|
|
84
|
-
# Get all available file extensions
|
|
85
|
-
#
|
|
86
|
-
# @return [Array<String>] Array of supported extensions
|
|
87
74
|
def self.available
|
|
88
75
|
EXTENSIONS.keys.sort
|
|
89
76
|
end
|
|
@@ -36,7 +36,10 @@ module Docyard
|
|
|
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
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
|
+
"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"/>',
|
|
40
|
+
"magnifying-glass" => '<path d="M229.66,218.34l-50.07-50.06a88.11,88.11,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z"/>',
|
|
41
|
+
"command" => '<path d="M180,144H160V112h20a36,36,0,1,0-36-36V96H112V76a36,36,0,1,0-36,36H96v32H76a36,36,0,1,0,36,36V160h32v20a36,36,0,1,0,36-36ZM160,76a20,20,0,1,1,20,20H160ZM56,76a20,20,0,0,1,40,0V96H76A20,20,0,0,1,56,76ZM96,180a20,20,0,1,1-20-20H96Zm16-68h32v32H112Zm68,88a20,20,0,0,1-20-20V160h20a20,20,0,0,1,0,40Z"/>',
|
|
42
|
+
"hash" => '<path d="M224,88H175.4l8.47-46.57a8,8,0,0,0-15.74-2.86l-9,49.43H111.4l8.47-46.57a8,8,0,0,0-15.74-2.86L95.14,88H48a8,8,0,0,0,0,16H92.23L83.5,152H32a8,8,0,0,0,0,16H80.6l-8.47,46.57a8,8,0,0,0,6.44,9.3A7.79,7.79,0,0,0,80,224a8,8,0,0,0,7.86-6.57l9-49.43H144.6l-8.47,46.57a8,8,0,0,0,6.44,9.3,7.79,7.79,0,0,0,1.43.13,8,8,0,0,0,7.86-6.57l9-49.43H208a8,8,0,0,0,0-16H163.77l8.73-48H224a8,8,0,0,0,0-16Zm-68.5,64H107.77l8.73-48h47.73Z"/>'
|
|
40
43
|
},
|
|
41
44
|
"bold" => {
|
|
42
45
|
"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"/>'
|
|
@@ -3,24 +3,31 @@
|
|
|
3
3
|
require "kramdown"
|
|
4
4
|
require "kramdown-parser-gfm"
|
|
5
5
|
require "yaml"
|
|
6
|
-
require_relative "components/registry"
|
|
7
|
-
require_relative "components/base_processor"
|
|
8
|
-
require_relative "components/callout_processor"
|
|
9
|
-
require_relative "components/tabs_processor"
|
|
10
|
-
require_relative "components/icon_processor"
|
|
11
|
-
require_relative "components/code_block_processor"
|
|
12
|
-
require_relative "components/
|
|
13
|
-
require_relative "components/
|
|
14
|
-
require_relative "components/
|
|
6
|
+
require_relative "../components/registry"
|
|
7
|
+
require_relative "../components/base_processor"
|
|
8
|
+
require_relative "../components/processors/callout_processor"
|
|
9
|
+
require_relative "../components/processors/tabs_processor"
|
|
10
|
+
require_relative "../components/processors/icon_processor"
|
|
11
|
+
require_relative "../components/processors/code_block_processor"
|
|
12
|
+
require_relative "../components/processors/code_snippet_import_preprocessor"
|
|
13
|
+
require_relative "../components/processors/code_block_options_preprocessor"
|
|
14
|
+
require_relative "../components/processors/code_block_diff_preprocessor"
|
|
15
|
+
require_relative "../components/processors/code_block_focus_preprocessor"
|
|
16
|
+
require_relative "../components/processors/table_wrapper_processor"
|
|
17
|
+
require_relative "../components/processors/heading_anchor_processor"
|
|
18
|
+
require_relative "../components/processors/table_of_contents_processor"
|
|
19
|
+
require_relative "../components/aliases"
|
|
15
20
|
|
|
16
21
|
module Docyard
|
|
17
22
|
class Markdown
|
|
18
23
|
FRONTMATTER_REGEX = /\A---\s*\n(.*?\n)---\s*\n/m
|
|
19
24
|
|
|
20
|
-
attr_reader :raw
|
|
25
|
+
attr_reader :raw, :config
|
|
21
26
|
|
|
22
|
-
def initialize(raw)
|
|
27
|
+
def initialize(raw, config: nil)
|
|
23
28
|
@raw = raw.freeze
|
|
29
|
+
@config = config
|
|
30
|
+
@context = {}
|
|
24
31
|
end
|
|
25
32
|
|
|
26
33
|
def frontmatter
|
|
@@ -56,7 +63,7 @@ module Docyard
|
|
|
56
63
|
end
|
|
57
64
|
|
|
58
65
|
def toc
|
|
59
|
-
@
|
|
66
|
+
@context[:toc] || []
|
|
60
67
|
end
|
|
61
68
|
|
|
62
69
|
private
|
|
@@ -75,7 +82,9 @@ module Docyard
|
|
|
75
82
|
end
|
|
76
83
|
|
|
77
84
|
def render_html
|
|
78
|
-
|
|
85
|
+
@context[:config] = config&.data
|
|
86
|
+
|
|
87
|
+
preprocessed_content = Components::Registry.run_preprocessors(content, @context)
|
|
79
88
|
|
|
80
89
|
raw_html = Kramdown::Document.new(
|
|
81
90
|
preprocessed_content,
|
|
@@ -85,7 +94,7 @@ module Docyard
|
|
|
85
94
|
parse_block_html: true
|
|
86
95
|
).to_html
|
|
87
96
|
|
|
88
|
-
Components::Registry.run_postprocessors(raw_html)
|
|
97
|
+
Components::Registry.run_postprocessors(raw_html, @context)
|
|
89
98
|
end
|
|
90
99
|
end
|
|
91
100
|
end
|
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "erb"
|
|
4
|
-
require_relative "constants"
|
|
4
|
+
require_relative "../config/constants"
|
|
5
5
|
|
|
6
6
|
module Docyard
|
|
7
7
|
class Renderer
|
|
8
|
-
|
|
9
|
-
ERRORS_PATH = File.join(__dir__, "templates", "errors")
|
|
10
|
-
PARTIALS_PATH = File.join(__dir__, "templates", "partials")
|
|
8
|
+
include Utils::UrlHelpers
|
|
11
9
|
|
|
12
|
-
|
|
10
|
+
LAYOUTS_PATH = File.join(__dir__, "../templates", "layouts")
|
|
11
|
+
ERRORS_PATH = File.join(__dir__, "../templates", "errors")
|
|
12
|
+
PARTIALS_PATH = File.join(__dir__, "../templates", "partials")
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
attr_reader :layout_path, :base_url, :config
|
|
15
|
+
|
|
16
|
+
def initialize(layout: "default", base_url: "/", config: nil)
|
|
15
17
|
@layout_path = File.join(LAYOUTS_PATH, "#{layout}.html.erb")
|
|
16
18
|
@base_url = normalize_base_url(base_url)
|
|
19
|
+
@config = config
|
|
17
20
|
end
|
|
18
21
|
|
|
19
22
|
def render_file(file_path, sidebar_html: "", prev_next_html: "", branding: {})
|
|
20
23
|
markdown_content = File.read(file_path)
|
|
21
|
-
markdown = Markdown.new(markdown_content)
|
|
24
|
+
markdown = Markdown.new(markdown_content, config: config)
|
|
22
25
|
|
|
23
26
|
html_content = strip_md_from_links(markdown.html)
|
|
24
27
|
toc = markdown.toc
|
|
@@ -79,21 +82,8 @@ module Docyard
|
|
|
79
82
|
"#{base_url}#{path}"
|
|
80
83
|
end
|
|
81
84
|
|
|
82
|
-
def link_path(path)
|
|
83
|
-
return path if path.nil? || path.start_with?("http://", "https://")
|
|
84
|
-
|
|
85
|
-
"#{base_url.chomp('/')}#{path}"
|
|
86
|
-
end
|
|
87
|
-
|
|
88
85
|
private
|
|
89
86
|
|
|
90
|
-
def normalize_base_url(url)
|
|
91
|
-
return "/" if url.nil? || url.empty?
|
|
92
|
-
|
|
93
|
-
url = "/#{url}" unless url.start_with?("/")
|
|
94
|
-
url.end_with?("/") ? url : "#{url}/"
|
|
95
|
-
end
|
|
96
|
-
|
|
97
87
|
def assign_content_variables(content, page_title, sidebar_html, prev_next_html, toc)
|
|
98
88
|
@content = content
|
|
99
89
|
@page_title = page_title
|
|
@@ -103,15 +93,29 @@ module Docyard
|
|
|
103
93
|
end
|
|
104
94
|
|
|
105
95
|
def assign_branding_variables(branding)
|
|
96
|
+
assign_site_branding(branding)
|
|
97
|
+
assign_display_options(branding)
|
|
98
|
+
assign_search_options(branding)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def assign_site_branding(branding)
|
|
106
102
|
@site_title = branding[:site_title] || Constants::DEFAULT_SITE_TITLE
|
|
107
103
|
@site_description = branding[:site_description] || ""
|
|
108
104
|
@logo = branding[:logo] || Constants::DEFAULT_LOGO_PATH
|
|
109
105
|
@logo_dark = branding[:logo_dark]
|
|
110
106
|
@favicon = branding[:favicon] || Constants::DEFAULT_FAVICON_PATH
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def assign_display_options(branding)
|
|
111
110
|
@display_logo = branding[:display_logo].nil? || branding[:display_logo]
|
|
112
111
|
@display_title = branding[:display_title].nil? || branding[:display_title]
|
|
113
112
|
end
|
|
114
113
|
|
|
114
|
+
def assign_search_options(branding)
|
|
115
|
+
@search_enabled = branding[:search_enabled].nil? || branding[:search_enabled]
|
|
116
|
+
@search_placeholder = branding[:search_placeholder] || "Search documentation..."
|
|
117
|
+
end
|
|
118
|
+
|
|
115
119
|
def strip_md_from_links(html)
|
|
116
120
|
html.gsub(/href="([^"]+)\.md"/, 'href="\1"')
|
|
117
121
|
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module Docyard
|
|
6
|
+
module Search
|
|
7
|
+
class BuildIndexer
|
|
8
|
+
include PagefindSupport
|
|
9
|
+
|
|
10
|
+
PAGEFIND_COMMAND = "npx"
|
|
11
|
+
|
|
12
|
+
attr_reader :config, :output_dir, :verbose
|
|
13
|
+
|
|
14
|
+
def initialize(config, verbose: false)
|
|
15
|
+
@config = config
|
|
16
|
+
@output_dir = config.build.output_dir
|
|
17
|
+
@verbose = verbose
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def index
|
|
21
|
+
return 0 unless search_enabled?
|
|
22
|
+
|
|
23
|
+
log "Generating search index..."
|
|
24
|
+
|
|
25
|
+
unless pagefind_available?
|
|
26
|
+
warn_pagefind_missing
|
|
27
|
+
return 0
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
run_pagefind
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def warn_pagefind_missing
|
|
36
|
+
log_warning "[!] Search index skipped: Pagefind not found"
|
|
37
|
+
log_warning " Install with: npm install -g pagefind"
|
|
38
|
+
log_warning " Or run: npx pagefind --site #{output_dir}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def run_pagefind
|
|
42
|
+
args = build_pagefind_args(output_dir)
|
|
43
|
+
log "Running: npx #{args.join(' ')}" if verbose
|
|
44
|
+
|
|
45
|
+
stdout, stderr, status = Open3.capture3(PAGEFIND_COMMAND, *args)
|
|
46
|
+
|
|
47
|
+
if status.success?
|
|
48
|
+
page_count = extract_page_count(stdout)
|
|
49
|
+
log "[+] Generated search index (#{page_count} pages indexed)"
|
|
50
|
+
page_count
|
|
51
|
+
else
|
|
52
|
+
log_warning "[!] Search indexing failed: #{stderr}"
|
|
53
|
+
0
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def extract_page_count(output)
|
|
58
|
+
if output =~ /Indexed (\d+) page/i
|
|
59
|
+
Regexp.last_match(1).to_i
|
|
60
|
+
else
|
|
61
|
+
0
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def log(message)
|
|
66
|
+
puts message
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def log_warning(message)
|
|
70
|
+
warn message
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
require "open3"
|
|
6
|
+
require "tty-progressbar"
|
|
7
|
+
|
|
8
|
+
module Docyard
|
|
9
|
+
module Search
|
|
10
|
+
class DevIndexer
|
|
11
|
+
include PagefindSupport
|
|
12
|
+
|
|
13
|
+
attr_reader :docs_path, :config, :temp_dir, :pagefind_path
|
|
14
|
+
|
|
15
|
+
def initialize(docs_path:, config:)
|
|
16
|
+
@docs_path = docs_path
|
|
17
|
+
@config = config
|
|
18
|
+
@temp_dir = nil
|
|
19
|
+
@pagefind_path = nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def generate
|
|
23
|
+
return unless search_enabled?
|
|
24
|
+
return unless pagefind_available?
|
|
25
|
+
|
|
26
|
+
@temp_dir = Dir.mktmpdir("docyard-search-")
|
|
27
|
+
generate_html_files
|
|
28
|
+
run_pagefind
|
|
29
|
+
@pagefind_path = File.join(temp_dir, "pagefind")
|
|
30
|
+
|
|
31
|
+
log_success
|
|
32
|
+
pagefind_path
|
|
33
|
+
rescue StandardError => e
|
|
34
|
+
warn "[!] Search index generation failed: #{e.message}"
|
|
35
|
+
cleanup
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def cleanup
|
|
40
|
+
return unless temp_dir && Dir.exist?(temp_dir)
|
|
41
|
+
|
|
42
|
+
FileUtils.rm_rf(temp_dir)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def pagefind_available?
|
|
48
|
+
result = super
|
|
49
|
+
warn "[!] Search disabled: Pagefind not found (npm install -g pagefind)" unless result
|
|
50
|
+
result
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def generate_html_files
|
|
54
|
+
markdown_files = Dir.glob(File.join(docs_path, "**", "*.md"))
|
|
55
|
+
renderer = Renderer.new(base_url: "/", config: config)
|
|
56
|
+
|
|
57
|
+
progress = TTY::ProgressBar.new(
|
|
58
|
+
"Indexing search [:bar] :current/:total (:percent)",
|
|
59
|
+
total: markdown_files.size,
|
|
60
|
+
width: 50
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
markdown_files.each do |file_path|
|
|
64
|
+
generate_html_file(file_path, renderer)
|
|
65
|
+
progress.advance
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def generate_html_file(markdown_file, renderer)
|
|
70
|
+
relative_path = markdown_file.delete_prefix("#{docs_path}/")
|
|
71
|
+
output_path = determine_output_path(relative_path)
|
|
72
|
+
|
|
73
|
+
html = renderer.render_file(markdown_file, branding: branding_options)
|
|
74
|
+
|
|
75
|
+
FileUtils.mkdir_p(File.dirname(output_path))
|
|
76
|
+
File.write(output_path, html)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def determine_output_path(relative_path)
|
|
80
|
+
base_name = File.basename(relative_path, ".md")
|
|
81
|
+
dir_name = File.dirname(relative_path)
|
|
82
|
+
|
|
83
|
+
if base_name == "index"
|
|
84
|
+
File.join(temp_dir, dir_name, "index.html")
|
|
85
|
+
else
|
|
86
|
+
File.join(temp_dir, dir_name, base_name, "index.html")
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def branding_options
|
|
91
|
+
BrandingResolver.new(config).resolve
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def run_pagefind
|
|
95
|
+
args = build_pagefind_args(temp_dir)
|
|
96
|
+
stdout, stderr, status = Open3.capture3("npx", *args)
|
|
97
|
+
|
|
98
|
+
raise "Pagefind failed: #{stderr}" unless status.success?
|
|
99
|
+
|
|
100
|
+
stdout
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def log_success
|
|
104
|
+
page_count = Dir.glob(File.join(temp_dir, "**", "*.html")).size
|
|
105
|
+
puts "=> Search index generated (#{page_count} pages)"
|
|
106
|
+
puts "=> Temp directory: #{temp_dir}" if ENV["DOCYARD_DEBUG"]
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module Docyard
|
|
6
|
+
module Search
|
|
7
|
+
module PagefindSupport
|
|
8
|
+
def search_enabled?
|
|
9
|
+
config.search.enabled != false
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def pagefind_available?
|
|
13
|
+
_stdout, _stderr, status = Open3.capture3("npx", "pagefind", "--version")
|
|
14
|
+
status.success?
|
|
15
|
+
rescue Errno::ENOENT
|
|
16
|
+
false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def build_pagefind_args(site_dir)
|
|
20
|
+
args = ["pagefind", "--site", site_dir]
|
|
21
|
+
|
|
22
|
+
exclusions = config.search.exclude || []
|
|
23
|
+
exclusions.each do |pattern|
|
|
24
|
+
args += ["--exclude-selectors", pattern]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
args
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|