docyard 0.9.0 → 1.0.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 +43 -0
- data/README.md +8 -253
- data/exe/docyard +6 -0
- data/lib/docyard/build/asset_bundler.rb +2 -2
- data/lib/docyard/build/file_copier.rb +12 -5
- data/lib/docyard/build/llms_txt_generator.rb +103 -0
- data/lib/docyard/build/sitemap_generator.rb +1 -1
- data/lib/docyard/build/static_generator.rb +115 -79
- data/lib/docyard/builder.rb +6 -2
- data/lib/docyard/cli.rb +14 -4
- data/lib/docyard/components/processors/callout_processor.rb +1 -1
- data/lib/docyard/components/processors/code_block_extended_fence_postprocessor.rb +24 -0
- data/lib/docyard/components/processors/code_block_extended_fence_preprocessor.rb +44 -0
- data/lib/docyard/components/processors/code_block_options_preprocessor.rb +11 -1
- data/lib/docyard/components/processors/code_block_processor.rb +5 -24
- data/lib/docyard/components/processors/code_group_processor.rb +6 -22
- data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +1 -0
- data/lib/docyard/components/processors/file_tree_processor.rb +1 -2
- data/lib/docyard/components/processors/icon_processor.rb +8 -2
- data/lib/docyard/components/processors/include_processor.rb +10 -10
- data/lib/docyard/components/processors/video_embed_processor.rb +14 -3
- data/lib/docyard/components/support/code_block/feature_extractor.rb +3 -1
- data/lib/docyard/components/support/code_block/icon_detector.rb +5 -12
- data/lib/docyard/components/support/code_block/line_number_resolver.rb +30 -0
- data/lib/docyard/components/support/code_detector.rb +2 -12
- data/lib/docyard/components/support/code_group/html_builder.rb +2 -6
- data/lib/docyard/components/support/tabs/icon_detector.rb +6 -2
- data/lib/docyard/components/support/tabs/parser.rb +6 -23
- data/lib/docyard/config/analytics_resolver.rb +24 -0
- data/lib/docyard/config/branding_resolver.rb +58 -27
- data/lib/docyard/config/key_validator.rb +30 -0
- data/lib/docyard/config/logo_detector.rb +8 -8
- data/lib/docyard/config/schema.rb +39 -0
- data/lib/docyard/config/section.rb +21 -0
- data/lib/docyard/config/validation_helpers.rb +83 -0
- data/lib/docyard/config/validator.rb +45 -144
- data/lib/docyard/config/validators/navigation.rb +43 -0
- data/lib/docyard/config/validators/section.rb +114 -0
- data/lib/docyard/config.rb +46 -102
- data/lib/docyard/constants.rb +59 -0
- data/lib/docyard/{utils/errors.rb → errors.rb} +6 -0
- data/lib/docyard/initializer.rb +100 -49
- data/lib/docyard/navigation/page_navigation_builder.rb +65 -0
- data/lib/docyard/navigation/sidebar/auto_builder.rb +107 -0
- data/lib/docyard/navigation/sidebar/cache.rb +96 -0
- data/lib/docyard/navigation/sidebar/config_builder.rb +179 -0
- data/lib/docyard/navigation/sidebar/distributed_builder.rb +145 -0
- data/lib/docyard/navigation/sidebar/local_config_loader.rb +69 -3
- data/lib/docyard/navigation/sidebar/renderer.rb +12 -1
- data/lib/docyard/navigation/sidebar_builder.rb +43 -81
- data/lib/docyard/rendering/branding_variables.rb +65 -0
- data/lib/docyard/rendering/icon_helpers.rb +14 -1
- data/lib/docyard/rendering/icons/devicons.rb +63 -0
- data/lib/docyard/rendering/icons.rb +26 -27
- data/lib/docyard/rendering/markdown.rb +5 -23
- data/lib/docyard/rendering/og_helpers.rb +36 -0
- data/lib/docyard/rendering/renderer.rb +87 -59
- data/lib/docyard/rendering/template_resolver.rb +14 -0
- data/lib/docyard/routing/fallback_resolver.rb +3 -3
- data/lib/docyard/search/build_indexer.rb +2 -2
- data/lib/docyard/search/dev_indexer.rb +36 -28
- data/lib/docyard/search/pagefind_support.rb +1 -1
- data/lib/docyard/server/asset_handler.rb +39 -15
- data/lib/docyard/server/dev_server.rb +90 -55
- data/lib/docyard/server/file_watcher.rb +68 -18
- data/lib/docyard/server/pagefind_handler.rb +1 -1
- data/lib/docyard/server/preview_server.rb +29 -33
- data/lib/docyard/server/rack_application.rb +38 -70
- data/lib/docyard/server/router.rb +11 -7
- data/lib/docyard/server/sse_server.rb +157 -0
- data/lib/docyard/server/static_file_app.rb +42 -0
- data/lib/docyard/templates/assets/css/components/banner.css +31 -0
- data/lib/docyard/templates/assets/css/components/breadcrumbs.css +2 -1
- data/lib/docyard/templates/assets/css/components/callout.css +26 -6
- data/lib/docyard/templates/assets/css/components/code-block.css +4 -2
- data/lib/docyard/templates/assets/css/components/code-group.css +20 -7
- data/lib/docyard/templates/assets/css/components/feedback.css +126 -0
- data/lib/docyard/templates/assets/css/components/file-tree.css +5 -4
- data/lib/docyard/templates/assets/css/components/icon.css +5 -0
- data/lib/docyard/templates/assets/css/components/nav-menu.css +20 -4
- data/lib/docyard/templates/assets/css/components/navigation.css +25 -3
- data/lib/docyard/templates/assets/css/components/page-actions.css +131 -0
- data/lib/docyard/templates/assets/css/components/prev-next.css +14 -7
- data/lib/docyard/templates/assets/css/components/search.css +6 -10
- data/lib/docyard/templates/assets/css/components/tab-bar.css +7 -4
- data/lib/docyard/templates/assets/css/components/table-of-contents.css +57 -11
- data/lib/docyard/templates/assets/css/components/tabs.css +12 -4
- data/lib/docyard/templates/assets/css/components/theme-toggle.css +3 -1
- data/lib/docyard/templates/assets/css/landing.css +82 -13
- data/lib/docyard/templates/assets/css/layout.css +17 -0
- data/lib/docyard/templates/assets/css/markdown.css +22 -2
- data/lib/docyard/templates/assets/css/variables.css +13 -1
- data/lib/docyard/templates/assets/js/components/code-group.js +4 -1
- data/lib/docyard/templates/assets/js/components/copy-page.js +115 -0
- data/lib/docyard/templates/assets/js/components/feedback.js +66 -0
- data/lib/docyard/templates/assets/js/components/file-tree.js +5 -5
- data/lib/docyard/templates/assets/js/components/navigation.js +3 -3
- data/lib/docyard/templates/assets/js/components/search.js +3 -3
- data/lib/docyard/templates/assets/js/components/table-of-contents.js +12 -6
- data/lib/docyard/templates/assets/js/components/tabs.js +45 -22
- data/lib/docyard/templates/assets/js/components/tooltip.js +4 -4
- data/lib/docyard/templates/assets/js/hot-reload.js +44 -0
- data/lib/docyard/templates/errors/404.html.erb +114 -5
- data/lib/docyard/templates/errors/500.html.erb +173 -10
- data/lib/docyard/templates/init/_sidebar.yml +36 -0
- data/lib/docyard/templates/init/docyard.yml +36 -0
- data/lib/docyard/templates/init/pages/components.md +146 -0
- data/lib/docyard/templates/init/pages/getting-started.md +94 -0
- data/lib/docyard/templates/init/pages/index.md +22 -0
- data/lib/docyard/templates/layouts/default.html.erb +10 -0
- data/lib/docyard/templates/layouts/splash.html.erb +14 -1
- data/lib/docyard/templates/partials/_analytics.html.erb +24 -0
- data/lib/docyard/templates/partials/_banner.html.erb +1 -1
- data/lib/docyard/templates/partials/_code_block.html.erb +1 -1
- data/lib/docyard/templates/partials/_feedback.html.erb +14 -0
- data/lib/docyard/templates/partials/_footer.html.erb +1 -1
- data/lib/docyard/templates/partials/_head.html.erb +79 -4
- data/lib/docyard/templates/partials/_icon_library.html.erb +8 -0
- data/lib/docyard/templates/partials/_page_actions.html.erb +21 -0
- data/lib/docyard/templates/partials/_scripts.html.erb +6 -3
- data/lib/docyard/templates/partials/_tabs.html.erb +4 -1
- data/lib/docyard/utils/git_info.rb +157 -0
- data/lib/docyard/utils/hash_utils.rb +31 -0
- data/lib/docyard/utils/html_helpers.rb +8 -0
- data/lib/docyard/utils/logging.rb +44 -3
- data/lib/docyard/utils/path_resolver.rb +0 -10
- data/lib/docyard/utils/path_utils.rb +73 -0
- data/lib/docyard/version.rb +1 -1
- data/lib/docyard.rb +2 -2
- metadata +77 -47
- data/.github/ISSUE_TEMPLATE/bug_report.md +0 -31
- data/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
- data/.github/pull_request_template.md +0 -14
- data/.github/workflows/ci.yml +0 -49
- data/.rubocop.yml +0 -42
- data/CODE_OF_CONDUCT.md +0 -132
- data/CONTRIBUTING.md +0 -55
- data/LICENSE.vscode-icons +0 -42
- data/Rakefile +0 -8
- data/lib/docyard/config/constants.rb +0 -31
- data/lib/docyard/navigation/sidebar/children_discoverer.rb +0 -51
- data/lib/docyard/navigation/sidebar/config_parser.rb +0 -208
- data/lib/docyard/navigation/sidebar/file_resolver.rb +0 -90
- data/lib/docyard/navigation/sidebar/file_system_scanner.rb +0 -78
- data/lib/docyard/navigation/sidebar/metadata_extractor.rb +0 -71
- data/lib/docyard/navigation/sidebar/metadata_reader.rb +0 -51
- data/lib/docyard/navigation/sidebar/path_prefixer.rb +0 -34
- data/lib/docyard/navigation/sidebar/sorter.rb +0 -21
- data/lib/docyard/navigation/sidebar/title_extractor.rb +0 -25
- data/lib/docyard/navigation/sidebar/tree_builder.rb +0 -140
- data/lib/docyard/rendering/icons/LICENSE.phosphor +0 -21
- data/lib/docyard/rendering/icons/file_types.rb +0 -79
- data/lib/docyard/rendering/icons/phosphor.rb +0 -93
- data/lib/docyard/rendering/language_mapping.rb +0 -52
- data/lib/docyard/templates/assets/js/reload.js +0 -98
- data/lib/docyard/templates/partials/_icon.html.erb +0 -1
- data/lib/docyard/templates/partials/_icon_file_extension.html.erb +0 -1
- data/sig/docyard.rbs +0 -4
|
@@ -16,35 +16,34 @@ module Docyard
|
|
|
16
16
|
def preprocess(content)
|
|
17
17
|
@current_file = context[:current_file]
|
|
18
18
|
@docs_root = context[:docs_root] || "docs"
|
|
19
|
-
@included_files = Set.new
|
|
20
19
|
|
|
21
20
|
process_outside_code_blocks(content) do |segment|
|
|
22
|
-
process_includes(segment)
|
|
21
|
+
process_includes(segment, Set.new)
|
|
23
22
|
end
|
|
24
23
|
end
|
|
25
24
|
|
|
26
25
|
private
|
|
27
26
|
|
|
28
|
-
def process_includes(content)
|
|
29
|
-
content.gsub(INCLUDE_PATTERN) { |_| process_include(Regexp.last_match) }
|
|
27
|
+
def process_includes(content, included_files)
|
|
28
|
+
content.gsub(INCLUDE_PATTERN) { |_| process_include(Regexp.last_match, included_files) }
|
|
30
29
|
end
|
|
31
30
|
|
|
32
|
-
def process_include(match)
|
|
31
|
+
def process_include(match, included_files)
|
|
33
32
|
filepath = match[1]
|
|
34
33
|
full_path = resolve_path(filepath)
|
|
35
34
|
|
|
36
|
-
error = validate_include(filepath, full_path)
|
|
35
|
+
error = validate_include(filepath, full_path, included_files)
|
|
37
36
|
return error if error
|
|
38
37
|
|
|
39
|
-
|
|
38
|
+
updated_included_files = included_files.dup.add(full_path)
|
|
40
39
|
file_content = File.read(full_path)
|
|
41
40
|
|
|
42
|
-
process_includes(file_content.strip)
|
|
41
|
+
process_includes(file_content.strip, updated_included_files)
|
|
43
42
|
end
|
|
44
43
|
|
|
45
|
-
def validate_include(filepath, full_path)
|
|
44
|
+
def validate_include(filepath, full_path, included_files)
|
|
46
45
|
return include_error(filepath, "File not found") unless full_path && File.exist?(full_path)
|
|
47
|
-
return include_error(filepath, "Circular include detected") if
|
|
46
|
+
return include_error(filepath, "Circular include detected") if included_files.include?(full_path)
|
|
48
47
|
return include_error(filepath, "Use code snippets for non-markdown files") unless markdown_file?(filepath)
|
|
49
48
|
|
|
50
49
|
nil
|
|
@@ -78,6 +77,7 @@ module Docyard
|
|
|
78
77
|
end
|
|
79
78
|
|
|
80
79
|
def include_error(filepath, message)
|
|
80
|
+
Docyard.logger.warn("Include failed: #{filepath} - #{message}")
|
|
81
81
|
"> [!WARNING]\n> Include error: #{filepath} - #{message}\n"
|
|
82
82
|
end
|
|
83
83
|
end
|
|
@@ -147,14 +147,25 @@ module Docyard
|
|
|
147
147
|
end
|
|
148
148
|
|
|
149
149
|
def build_wrapper_style(attrs)
|
|
150
|
-
|
|
150
|
+
width = validate_dimension(attrs["width"])
|
|
151
|
+
height = validate_dimension(attrs["height"])
|
|
152
|
+
return "" unless width || height
|
|
151
153
|
|
|
152
154
|
styles = []
|
|
153
|
-
styles << "max-width: #{
|
|
154
|
-
styles << "height: #{
|
|
155
|
+
styles << "max-width: #{width}px" if width
|
|
156
|
+
styles << "height: #{height}px" if height
|
|
155
157
|
" style=\"#{styles.join('; ')}\""
|
|
156
158
|
end
|
|
157
159
|
|
|
160
|
+
def validate_dimension(value)
|
|
161
|
+
return nil if value.nil? || value.to_s.empty?
|
|
162
|
+
|
|
163
|
+
int_value = value.to_s.to_i
|
|
164
|
+
return nil unless int_value.positive? && int_value <= 10_000
|
|
165
|
+
|
|
166
|
+
int_value
|
|
167
|
+
end
|
|
168
|
+
|
|
158
169
|
def build_iframe_attrs(url, attrs, provider)
|
|
159
170
|
iframe_attrs = [
|
|
160
171
|
"src=\"#{url}\"",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "patterns"
|
|
4
|
+
require_relative "../../../rendering/icons"
|
|
4
5
|
|
|
5
6
|
module Docyard
|
|
6
7
|
module Components
|
|
@@ -18,7 +19,8 @@ module Docyard
|
|
|
18
19
|
cleaned = markdown.gsub(CODE_FENCE_REGEX) do
|
|
19
20
|
block_data = extract_block_data(Regexp.last_match)
|
|
20
21
|
blocks << block_data
|
|
21
|
-
|
|
22
|
+
highlight_lang = Icons.highlight_language(block_data[:lang])
|
|
23
|
+
"```#{highlight_lang}\n#{block_data[:cleaned_content]}```"
|
|
22
24
|
end
|
|
23
25
|
{ cleaned_markdown: cleaned, blocks: blocks }
|
|
24
26
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../../../rendering/
|
|
3
|
+
require_relative "../../../rendering/icons"
|
|
4
4
|
|
|
5
5
|
module Docyard
|
|
6
6
|
module Components
|
|
@@ -22,20 +22,13 @@ module Docyard
|
|
|
22
22
|
}
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
icon, icon_source
|
|
26
|
-
{ title: title, icon: icon, icon_source: icon_source }
|
|
25
|
+
{ title: title, icon: language, icon_source: "language" }
|
|
27
26
|
end
|
|
28
27
|
|
|
29
|
-
def
|
|
30
|
-
return
|
|
28
|
+
def render_icon(language)
|
|
29
|
+
return "" if language.nil? || language.to_s.empty?
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
%w[terminal-window phosphor]
|
|
34
|
-
elsif (ext = LanguageMapping.extension_for(language))
|
|
35
|
-
[ext, "file-extension"]
|
|
36
|
-
else
|
|
37
|
-
%w[file phosphor]
|
|
38
|
-
end
|
|
31
|
+
Icons.render_for_language(language)
|
|
39
32
|
end
|
|
40
33
|
end
|
|
41
34
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
module Components
|
|
5
|
+
module Support
|
|
6
|
+
module CodeBlock
|
|
7
|
+
module LineNumberResolver
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def enabled?(option)
|
|
11
|
+
return false if option == ":no-line-numbers"
|
|
12
|
+
|
|
13
|
+
option&.start_with?(":line-numbers") || false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def start_line(option)
|
|
17
|
+
return 1 unless option&.include?("=")
|
|
18
|
+
|
|
19
|
+
option.split("=").last.to_i
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def generate_numbers(code_text, start = 1)
|
|
23
|
+
line_count = [code_text.lines.count, 1].max
|
|
24
|
+
(start...(start + line_count)).to_a
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../../rendering/
|
|
3
|
+
require_relative "../../rendering/icons"
|
|
4
4
|
|
|
5
5
|
module Docyard
|
|
6
6
|
module Components
|
|
@@ -20,7 +20,7 @@ module Docyard
|
|
|
20
20
|
language = extract_language
|
|
21
21
|
return nil unless language
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
{ language: language }
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
private
|
|
@@ -45,16 +45,6 @@ module Docyard
|
|
|
45
45
|
|
|
46
46
|
lang_line.downcase
|
|
47
47
|
end
|
|
48
|
-
|
|
49
|
-
def icon_for_language(language)
|
|
50
|
-
if LanguageMapping.terminal_language?(language)
|
|
51
|
-
{ icon: "terminal-window", source: "phosphor" }
|
|
52
|
-
elsif (extension = LanguageMapping.extension_for(language))
|
|
53
|
-
{ icon: extension, source: "file-extension" }
|
|
54
|
-
else
|
|
55
|
-
{ icon: "file", source: "phosphor" }
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
48
|
end
|
|
59
49
|
end
|
|
60
50
|
end
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../code_block/icon_detector"
|
|
4
3
|
require_relative "../../../rendering/icons"
|
|
5
4
|
|
|
6
5
|
module Docyard
|
|
@@ -53,17 +52,14 @@ module Docyard
|
|
|
53
52
|
class="docyard-code-group__tab"
|
|
54
53
|
tabindex="#{tabindex}"
|
|
55
54
|
data-label="#{escape_html(block[:label])}"
|
|
56
|
-
>#{icon_html}
|
|
55
|
+
>#{icon_html}<span class="docyard-code-group__tab-label">#{escape_html(block[:label])}</span></button>
|
|
57
56
|
HTML
|
|
58
57
|
end
|
|
59
58
|
|
|
60
59
|
def render_icon(lang)
|
|
61
60
|
return "" if lang.nil? || lang.empty?
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
return "" unless icon && icon_source == "file-extension"
|
|
65
|
-
|
|
66
|
-
Icons.render_file_extension(icon) || ""
|
|
62
|
+
Icons.render_for_language(lang)
|
|
67
63
|
end
|
|
68
64
|
|
|
69
65
|
def build_copy_button
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../code_detector"
|
|
4
|
+
require_relative "../../../rendering/icons"
|
|
4
5
|
|
|
5
6
|
module Docyard
|
|
6
7
|
module Components
|
|
@@ -41,10 +42,13 @@ module Docyard
|
|
|
41
42
|
detected = CodeDetector.detect(tab_content)
|
|
42
43
|
return nil unless detected
|
|
43
44
|
|
|
45
|
+
language = detected[:language]
|
|
46
|
+
return nil unless Icons.devicon?(language)
|
|
47
|
+
|
|
44
48
|
{
|
|
45
49
|
name: tab_name,
|
|
46
|
-
icon:
|
|
47
|
-
icon_source:
|
|
50
|
+
icon: language,
|
|
51
|
+
icon_source: "language"
|
|
48
52
|
}
|
|
49
53
|
end
|
|
50
54
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "../code_block/feature_extractor"
|
|
4
4
|
require_relative "../code_block/icon_detector"
|
|
5
5
|
require_relative "../code_block/line_wrapper"
|
|
6
|
+
require_relative "../code_block/line_number_resolver"
|
|
6
7
|
require_relative "icon_detector"
|
|
7
8
|
require_relative "../../../rendering/icons"
|
|
8
9
|
require_relative "../../../rendering/renderer"
|
|
@@ -21,6 +22,7 @@ module Docyard
|
|
|
21
22
|
CodeBlockFeatureExtractor = CodeBlock::FeatureExtractor
|
|
22
23
|
CodeBlockIconDetector = CodeBlock::IconDetector
|
|
23
24
|
CodeBlockLineWrapper = CodeBlock::LineWrapper
|
|
25
|
+
LineNumbers = CodeBlock::LineNumberResolver
|
|
24
26
|
|
|
25
27
|
def self.parse(content)
|
|
26
28
|
new(content).parse
|
|
@@ -120,15 +122,15 @@ module Docyard
|
|
|
120
122
|
focus_lines: block_data[:focus_lines] || {},
|
|
121
123
|
error_lines: block_data[:error_lines] || {},
|
|
122
124
|
warning_lines: block_data[:warning_lines] || {},
|
|
123
|
-
start_line:
|
|
125
|
+
start_line: LineNumbers.start_line(block_data[:option])
|
|
124
126
|
}
|
|
125
127
|
CodeBlockLineWrapper.wrap_code_block(html, wrapper_data)
|
|
126
128
|
end
|
|
127
129
|
|
|
128
130
|
def build_full_locals(processed_html, code_text, block_data)
|
|
129
131
|
title_data = CodeBlockIconDetector.detect(block_data[:title], block_data[:lang])
|
|
130
|
-
show_line_numbers =
|
|
131
|
-
start_line =
|
|
132
|
+
show_line_numbers = LineNumbers.enabled?(block_data[:option])
|
|
133
|
+
start_line = LineNumbers.start_line(block_data[:option])
|
|
132
134
|
|
|
133
135
|
base_locals(processed_html, code_text, show_line_numbers, start_line)
|
|
134
136
|
.merge(feature_locals(block_data))
|
|
@@ -141,7 +143,7 @@ module Docyard
|
|
|
141
143
|
code_text: escape_html_attribute(code_text),
|
|
142
144
|
copy_icon: Icons.render("copy", "regular") || "",
|
|
143
145
|
show_line_numbers: show_line_numbers,
|
|
144
|
-
line_numbers: show_line_numbers ?
|
|
146
|
+
line_numbers: show_line_numbers ? LineNumbers.generate_numbers(code_text, start_line) : [],
|
|
145
147
|
start_line: start_line
|
|
146
148
|
}
|
|
147
149
|
end
|
|
@@ -164,25 +166,6 @@ module Docyard
|
|
|
164
166
|
}
|
|
165
167
|
end
|
|
166
168
|
|
|
167
|
-
def line_numbers_enabled?(block_option)
|
|
168
|
-
return false if block_option == ":no-line-numbers"
|
|
169
|
-
return true if block_option&.start_with?(":line-numbers")
|
|
170
|
-
|
|
171
|
-
false
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
def extract_start_line(block_option)
|
|
175
|
-
return 1 unless block_option&.include?("=")
|
|
176
|
-
|
|
177
|
-
block_option.split("=").last.to_i
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
def generate_line_numbers(code_text, start_line)
|
|
181
|
-
line_count = code_text.lines.count
|
|
182
|
-
line_count = 1 if line_count.zero?
|
|
183
|
-
(start_line...(start_line + line_count)).to_a
|
|
184
|
-
end
|
|
185
|
-
|
|
186
169
|
def extract_code_text(html)
|
|
187
170
|
text = html.gsub(/<[^>]+>/, "")
|
|
188
171
|
text = CGI.unescapeHTML(text)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
module AnalyticsResolver
|
|
5
|
+
def analytics_options
|
|
6
|
+
analytics = config.analytics
|
|
7
|
+
{
|
|
8
|
+
analytics_google: analytics.google,
|
|
9
|
+
analytics_plausible: analytics.plausible,
|
|
10
|
+
analytics_fathom: analytics.fathom,
|
|
11
|
+
analytics_script: analytics.script,
|
|
12
|
+
has_analytics: any_analytics_configured?(analytics)
|
|
13
|
+
}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def any_analytics_configured?(analytics)
|
|
19
|
+
[analytics.google, analytics.plausible, analytics.fathom, analytics.script].any? do |value|
|
|
20
|
+
value.is_a?(String) && !value.strip.empty?
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -1,22 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "logo_detector"
|
|
4
|
+
require_relative "analytics_resolver"
|
|
4
5
|
|
|
5
6
|
module Docyard
|
|
6
7
|
class BrandingResolver
|
|
8
|
+
include AnalyticsResolver
|
|
9
|
+
|
|
7
10
|
def initialize(config)
|
|
8
11
|
@config = config
|
|
9
12
|
end
|
|
10
13
|
|
|
11
|
-
SOCIAL_ICON_MAP = {
|
|
12
|
-
"x" => "x-logo", "twitter" => "x-logo", "discord" => "discord-logo",
|
|
13
|
-
"linkedin" => "linkedin-logo", "youtube" => "youtube-logo", "instagram" => "instagram-logo",
|
|
14
|
-
"facebook" => "facebook-logo", "tiktok" => "tiktok-logo", "twitch" => "twitch-logo",
|
|
15
|
-
"reddit" => "reddit-logo", "mastodon" => "mastodon-logo", "threads" => "threads-logo",
|
|
16
|
-
"pinterest" => "pinterest-logo", "medium" => "medium-logo", "slack" => "slack-logo",
|
|
17
|
-
"gitlab" => "gitlab-logo"
|
|
18
|
-
}.freeze
|
|
19
|
-
|
|
20
14
|
def resolve
|
|
21
15
|
return default_branding unless config
|
|
22
16
|
|
|
@@ -40,31 +34,31 @@ module Docyard
|
|
|
40
34
|
end
|
|
41
35
|
|
|
42
36
|
def config_branding_options
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
.merge(navigation_options)
|
|
49
|
-
.merge(tabs_options)
|
|
50
|
-
.merge(announcement_options)
|
|
37
|
+
[
|
|
38
|
+
site_options, logo_options, search_options, credits_options, social_options,
|
|
39
|
+
navigation_options, tabs_options, announcement_options, repo_options, analytics_options,
|
|
40
|
+
color_options
|
|
41
|
+
].reduce({}, :merge)
|
|
51
42
|
end
|
|
52
43
|
|
|
53
44
|
def site_options
|
|
54
45
|
{
|
|
55
46
|
site_title: config.title || Constants::DEFAULT_SITE_TITLE,
|
|
56
47
|
site_description: config.description || "",
|
|
57
|
-
|
|
48
|
+
site_url: config.url,
|
|
49
|
+
og_image: config.og_image,
|
|
50
|
+
twitter: config.twitter,
|
|
51
|
+
favicon: config.branding.favicon || LogoDetector.auto_detect_favicon(public_dir: config.public_dir)
|
|
58
52
|
}
|
|
59
53
|
end
|
|
60
54
|
|
|
61
55
|
def logo_options
|
|
62
56
|
branding = config.branding
|
|
63
|
-
logo = branding.logo || LogoDetector.auto_detect_logo
|
|
57
|
+
logo = branding.logo || LogoDetector.auto_detect_logo(public_dir: config.public_dir)
|
|
64
58
|
has_custom_logo = !logo.nil?
|
|
65
59
|
{
|
|
66
60
|
logo: logo || Constants::DEFAULT_LOGO_PATH,
|
|
67
|
-
logo_dark: LogoDetector.detect_dark_logo(logo) || Constants::DEFAULT_LOGO_DARK_PATH,
|
|
61
|
+
logo_dark: LogoDetector.detect_dark_logo(logo, public_dir: config.public_dir) || Constants::DEFAULT_LOGO_DARK_PATH,
|
|
68
62
|
has_custom_logo: has_custom_logo
|
|
69
63
|
}
|
|
70
64
|
end
|
|
@@ -84,22 +78,28 @@ module Docyard
|
|
|
84
78
|
end
|
|
85
79
|
|
|
86
80
|
def social_options
|
|
87
|
-
|
|
88
|
-
{
|
|
89
|
-
social: normalize_social_links(socials)
|
|
90
|
-
}
|
|
81
|
+
{ social: normalize_social_links(config.socials || {}) }
|
|
91
82
|
end
|
|
92
83
|
|
|
93
84
|
def normalize_social_links(socials)
|
|
94
85
|
return [] unless socials.is_a?(Hash) && socials.any?
|
|
95
86
|
|
|
96
|
-
socials.filter_map { |platform, url| build_social_link(platform.to_s, url) }
|
|
87
|
+
socials.except("custom").filter_map { |platform, url| build_social_link(platform.to_s, url) } +
|
|
88
|
+
build_custom_social_links(socials["custom"])
|
|
97
89
|
end
|
|
98
90
|
|
|
99
91
|
def build_social_link(platform, url)
|
|
100
|
-
|
|
92
|
+
{ platform: platform, url: url, icon: Constants::SOCIAL_ICON_MAP[platform] || platform } if valid_url?(url)
|
|
93
|
+
end
|
|
101
94
|
|
|
102
|
-
|
|
95
|
+
def build_custom_social_links(custom)
|
|
96
|
+
return [] unless custom.is_a?(Array)
|
|
97
|
+
|
|
98
|
+
custom.filter_map do |item|
|
|
99
|
+
next unless item.is_a?(Hash) && item["icon"] && valid_url?(item["href"])
|
|
100
|
+
|
|
101
|
+
{ platform: "custom", url: item["href"], icon: item["icon"] }
|
|
102
|
+
end
|
|
103
103
|
end
|
|
104
104
|
|
|
105
105
|
def valid_url?(url)
|
|
@@ -174,5 +174,36 @@ module Docyard
|
|
|
174
174
|
link: button["link"] || announcement.link
|
|
175
175
|
}
|
|
176
176
|
end
|
|
177
|
+
|
|
178
|
+
def repo_options
|
|
179
|
+
repo = config.repo
|
|
180
|
+
has_repo_url = !repo.url.nil? && !repo.url.empty?
|
|
181
|
+
{
|
|
182
|
+
repo_url: repo.url,
|
|
183
|
+
repo_branch: repo.branch || "main",
|
|
184
|
+
repo_edit_path: repo.edit_path || config.source,
|
|
185
|
+
show_edit_link: has_repo_url && repo.edit_link != false,
|
|
186
|
+
show_last_updated: has_repo_url && repo.last_updated != false
|
|
187
|
+
}
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def color_options
|
|
191
|
+
color = config.branding.color
|
|
192
|
+
{ primary_color: normalize_color(color) }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def normalize_color(color)
|
|
196
|
+
return nil if color.nil?
|
|
197
|
+
|
|
198
|
+
if color.is_a?(Hash)
|
|
199
|
+
light = color["light"]
|
|
200
|
+
dark = color["dark"]
|
|
201
|
+
return nil if light.nil? && dark.nil?
|
|
202
|
+
|
|
203
|
+
{ light: light, dark: dark }.compact
|
|
204
|
+
elsif color.is_a?(String) && !color.strip.empty?
|
|
205
|
+
{ light: color.strip }
|
|
206
|
+
end
|
|
207
|
+
end
|
|
177
208
|
end
|
|
178
209
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
class Config
|
|
5
|
+
module KeyValidator
|
|
6
|
+
class << self
|
|
7
|
+
def validate(hash, valid_keys, context:)
|
|
8
|
+
return [] unless hash.is_a?(Hash)
|
|
9
|
+
|
|
10
|
+
unknown = hash.keys.map(&:to_s) - valid_keys
|
|
11
|
+
unknown.map { |key| build_error(key, valid_keys, context) }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def build_error(key, valid_keys, context)
|
|
17
|
+
suggestion = find_suggestion(key, valid_keys)
|
|
18
|
+
msg = "unknown key '#{key}'"
|
|
19
|
+
msg += ". Did you mean '#{suggestion}'?" if suggestion
|
|
20
|
+
{ context: context, message: msg }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def find_suggestion(key, valid_keys)
|
|
24
|
+
checker = DidYouMean::SpellChecker.new(dictionary: valid_keys)
|
|
25
|
+
checker.correct(key).first
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -4,23 +4,23 @@ module Docyard
|
|
|
4
4
|
module LogoDetector
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
|
-
def auto_detect_logo
|
|
8
|
-
detect_public_file("logo", %w[svg png])
|
|
7
|
+
def auto_detect_logo(public_dir: "docs/public")
|
|
8
|
+
detect_public_file("logo", %w[svg png], public_dir: public_dir)
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def auto_detect_favicon
|
|
12
|
-
detect_public_file("favicon", %w[ico svg png])
|
|
11
|
+
def auto_detect_favicon(public_dir: "docs/public")
|
|
12
|
+
detect_public_file("favicon", %w[ico svg png], public_dir: public_dir)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def detect_public_file(name, extensions)
|
|
15
|
+
def detect_public_file(name, extensions, public_dir: "docs/public")
|
|
16
16
|
extensions.each do |ext|
|
|
17
|
-
path = File.join(
|
|
17
|
+
path = File.join(public_dir, "#{name}.#{ext}")
|
|
18
18
|
return "#{name}.#{ext}" if File.exist?(path)
|
|
19
19
|
end
|
|
20
20
|
nil
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
def detect_dark_logo(logo)
|
|
23
|
+
def detect_dark_logo(logo, public_dir: "docs/public")
|
|
24
24
|
return nil unless logo
|
|
25
25
|
|
|
26
26
|
ext = File.extname(logo)
|
|
@@ -31,7 +31,7 @@ module Docyard
|
|
|
31
31
|
dark_path = File.join(File.dirname(logo), dark_filename)
|
|
32
32
|
File.exist?(dark_path) ? dark_path : logo
|
|
33
33
|
else
|
|
34
|
-
dark_path = File.join(
|
|
34
|
+
dark_path = File.join(public_dir, dark_filename)
|
|
35
35
|
File.exist?(dark_path) ? dark_filename : logo
|
|
36
36
|
end
|
|
37
37
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
class Config
|
|
5
|
+
module Schema
|
|
6
|
+
TOP_LEVEL = %w[
|
|
7
|
+
title description url og_image twitter source
|
|
8
|
+
branding socials tabs sidebar
|
|
9
|
+
build search navigation announcement
|
|
10
|
+
repo analytics feedback
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
SECTIONS = {
|
|
14
|
+
"branding" => %w[logo favicon credits copyright color],
|
|
15
|
+
"build" => %w[output base],
|
|
16
|
+
"search" => %w[enabled placeholder exclude],
|
|
17
|
+
"navigation" => %w[cta breadcrumbs],
|
|
18
|
+
"repo" => %w[url branch edit_path edit_link last_updated],
|
|
19
|
+
"analytics" => %w[google plausible fathom script],
|
|
20
|
+
"announcement" => %w[text link button dismissible],
|
|
21
|
+
"feedback" => %w[enabled question]
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
TAB = %w[text href icon external].freeze
|
|
25
|
+
|
|
26
|
+
CTA = %w[text href variant external].freeze
|
|
27
|
+
|
|
28
|
+
ANNOUNCEMENT_BUTTON = %w[text link].freeze
|
|
29
|
+
|
|
30
|
+
SIDEBAR_ITEM = %w[text icon badge badge_type items collapsed index group collapsible].freeze
|
|
31
|
+
|
|
32
|
+
SIDEBAR_EXTERNAL_LINK = %w[link text icon target].freeze
|
|
33
|
+
|
|
34
|
+
SOCIALS_BUILTIN = %w[github twitter discord slack linkedin youtube bluesky custom].freeze
|
|
35
|
+
|
|
36
|
+
CUSTOM_SOCIAL = %w[icon href].freeze
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
class Config
|
|
5
|
+
class Section
|
|
6
|
+
def initialize(data)
|
|
7
|
+
@data = data || {}
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def method_missing(method, *args)
|
|
11
|
+
return super unless args.empty?
|
|
12
|
+
|
|
13
|
+
@data[method.to_s]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def respond_to_missing?(method, include_private = false)
|
|
17
|
+
@data.key?(method.to_s) || super
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|