docyard 0.9.0 → 1.0.1
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 +57 -1
- data/README.md +8 -253
- data/exe/docyard +6 -0
- data/lib/docyard/build/asset_bundler.rb +24 -2
- data/lib/docyard/build/error_page_generator.rb +33 -0
- data/lib/docyard/build/file_copier.rb +12 -5
- data/lib/docyard/build/file_writer.rb +19 -0
- data/lib/docyard/build/llms_txt_generator.rb +103 -0
- data/lib/docyard/build/root_fallback_generator.rb +66 -0
- data/lib/docyard/build/sitemap_generator.rb +1 -1
- data/lib/docyard/build/static_generator.rb +119 -81
- 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/breadcrumb_builder.rb +45 -6
- 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 +96 -61
- 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 +39 -71
- 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/heading-anchor.css +2 -2
- 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 +9 -6
- data/lib/docyard/templates/assets/css/components/table-of-contents.css +63 -17
- 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 +32 -16
- data/lib/docyard/templates/assets/css/markdown.css +22 -2
- data/lib/docyard/templates/assets/css/variables.css +14 -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 +125 -5
- data/lib/docyard/templates/errors/500.html.erb +184 -10
- data/lib/docyard/templates/errors/redirect.html.erb +12 -0
- 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 +80 -5
- 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 +81 -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
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
module Build
|
|
5
|
+
class RootFallbackGenerator
|
|
6
|
+
attr_reader :config, :docs_path, :sidebar_cache, :renderer
|
|
7
|
+
|
|
8
|
+
def initialize(config:, docs_path:, sidebar_cache:, renderer:)
|
|
9
|
+
@config = config
|
|
10
|
+
@docs_path = docs_path
|
|
11
|
+
@sidebar_cache = sidebar_cache
|
|
12
|
+
@renderer = renderer
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def generate_if_needed
|
|
16
|
+
return if root_index_exists?
|
|
17
|
+
|
|
18
|
+
first_path = find_first_navigable_path
|
|
19
|
+
return unless first_path
|
|
20
|
+
|
|
21
|
+
generate_redirect_page(first_path)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def root_index_exists?
|
|
27
|
+
File.exist?(File.join(docs_path, "index.md")) ||
|
|
28
|
+
File.exist?(File.join(docs_path, "index.html"))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def find_first_navigable_path
|
|
32
|
+
return nil unless sidebar_cache&.tree&.any?
|
|
33
|
+
|
|
34
|
+
find_first_file_in_tree(sidebar_cache.tree)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def find_first_file_in_tree(items)
|
|
38
|
+
items.each do |item|
|
|
39
|
+
return item[:path] if item[:type] == :file && item[:path]
|
|
40
|
+
|
|
41
|
+
if item[:children]&.any?
|
|
42
|
+
nested = find_first_file_in_tree(item[:children])
|
|
43
|
+
return nested if nested
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def generate_redirect_page(target_path)
|
|
51
|
+
output_path = File.join(config.build.output, "index.html")
|
|
52
|
+
full_target = build_full_target_url(target_path)
|
|
53
|
+
|
|
54
|
+
FileUtils.mkdir_p(File.dirname(output_path))
|
|
55
|
+
File.write(output_path, renderer.render_redirect(full_target))
|
|
56
|
+
|
|
57
|
+
full_target
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def build_full_target_url(target_path)
|
|
61
|
+
base = config.build.base&.chomp("/") || ""
|
|
62
|
+
"#{base}#{target_path}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -18,7 +18,7 @@ module Docyard
|
|
|
18
18
|
output_path = File.join(config.build.output, "sitemap.xml")
|
|
19
19
|
File.write(output_path, sitemap_content)
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
Docyard.logger.info("[✓] Generated sitemap.xml (#{urls.size} URLs)")
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
private
|
|
@@ -1,65 +1,117 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "parallel"
|
|
3
4
|
require "tty-progressbar"
|
|
4
5
|
require_relative "../rendering/template_resolver"
|
|
5
|
-
require_relative "../navigation/
|
|
6
|
-
require_relative "../navigation/
|
|
6
|
+
require_relative "../navigation/page_navigation_builder"
|
|
7
|
+
require_relative "../navigation/sidebar/cache"
|
|
8
|
+
require_relative "../utils/path_utils"
|
|
9
|
+
require_relative "../utils/git_info"
|
|
10
|
+
require_relative "root_fallback_generator"
|
|
11
|
+
require_relative "error_page_generator"
|
|
12
|
+
require_relative "file_writer"
|
|
7
13
|
|
|
8
14
|
module Docyard
|
|
9
15
|
module Build
|
|
10
16
|
class StaticGenerator
|
|
11
|
-
|
|
17
|
+
include FileWriter
|
|
18
|
+
|
|
19
|
+
PARALLEL_THRESHOLD = 10
|
|
20
|
+
|
|
21
|
+
attr_reader :config, :verbose, :sidebar_cache
|
|
12
22
|
|
|
13
23
|
def initialize(config, verbose: false)
|
|
14
24
|
@config = config
|
|
15
25
|
@verbose = verbose
|
|
16
|
-
@
|
|
26
|
+
@sidebar_cache = nil
|
|
17
27
|
end
|
|
18
28
|
|
|
19
29
|
def generate
|
|
30
|
+
build_sidebar_cache
|
|
31
|
+
Utils::GitInfo.prefetch_timestamps(docs_path) if show_last_updated?
|
|
20
32
|
copy_custom_landing_page if custom_landing_page?
|
|
21
33
|
|
|
22
34
|
markdown_files = collect_markdown_files
|
|
23
|
-
|
|
35
|
+
Docyard.logger.info("\n[✓] Found #{markdown_files.size} markdown files")
|
|
36
|
+
|
|
37
|
+
generate_all_pages(markdown_files)
|
|
38
|
+
generate_error_page
|
|
39
|
+
generate_root_fallback_if_needed
|
|
40
|
+
|
|
41
|
+
markdown_files.size
|
|
42
|
+
ensure
|
|
43
|
+
Utils::GitInfo.clear_cache
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
24
47
|
|
|
48
|
+
def generate_all_pages(markdown_files)
|
|
25
49
|
progress = TTY::ProgressBar.new(
|
|
26
50
|
"Generating pages [:bar] :current/:total (:percent)",
|
|
27
51
|
total: markdown_files.size,
|
|
28
52
|
width: 50
|
|
29
53
|
)
|
|
54
|
+
mutex = Mutex.new
|
|
30
55
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
progress
|
|
56
|
+
Logging.start_buffering
|
|
57
|
+
if markdown_files.size >= PARALLEL_THRESHOLD
|
|
58
|
+
generate_pages_in_parallel(markdown_files, progress, mutex)
|
|
59
|
+
else
|
|
60
|
+
generate_pages_sequentially(markdown_files, progress)
|
|
34
61
|
end
|
|
35
|
-
|
|
36
|
-
markdown_files.size
|
|
62
|
+
Logging.flush_warnings
|
|
37
63
|
end
|
|
38
64
|
|
|
39
|
-
private
|
|
40
|
-
|
|
41
65
|
def custom_landing_page?
|
|
42
|
-
File.file?("
|
|
66
|
+
File.file?(File.join(docs_path, "index.html"))
|
|
43
67
|
end
|
|
44
68
|
|
|
45
69
|
def copy_custom_landing_page
|
|
46
70
|
output_path = File.join(config.build.output, "index.html")
|
|
47
|
-
|
|
48
|
-
|
|
71
|
+
safe_file_write(output_path) do
|
|
72
|
+
FileUtils.mkdir_p(File.dirname(output_path))
|
|
73
|
+
FileUtils.cp(File.join(docs_path, "index.html"), output_path)
|
|
74
|
+
end
|
|
49
75
|
log "[✓] Copied custom landing page (index.html)"
|
|
50
76
|
end
|
|
51
77
|
|
|
52
78
|
def collect_markdown_files
|
|
53
|
-
files = Dir.glob(File.join(
|
|
54
|
-
files.reject! { |f| f == "
|
|
79
|
+
files = Dir.glob(File.join(docs_path, "**", "*.md"))
|
|
80
|
+
files.reject! { |f| f == File.join(docs_path, "index.md") } if custom_landing_page?
|
|
55
81
|
files
|
|
56
82
|
end
|
|
57
83
|
|
|
58
|
-
def
|
|
59
|
-
|
|
60
|
-
|
|
84
|
+
def generate_pages_in_parallel(markdown_files, progress, mutex)
|
|
85
|
+
Parallel.each(markdown_files, in_threads: Parallel.processor_count) do |file_path|
|
|
86
|
+
generate_page(file_path, thread_local_renderer)
|
|
87
|
+
mutex.synchronize { progress.advance }
|
|
88
|
+
ensure
|
|
89
|
+
Thread.current[:docyard_build_renderer] = nil
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def generate_pages_sequentially(markdown_files, progress)
|
|
94
|
+
renderer = build_renderer
|
|
95
|
+
markdown_files.each do |file_path|
|
|
96
|
+
generate_page(file_path, renderer)
|
|
97
|
+
progress.advance
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def thread_local_renderer
|
|
102
|
+
Thread.current[:docyard_build_renderer] ||= build_renderer
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def build_renderer
|
|
106
|
+
Renderer.new(base_url: config.build.base, config: config)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def generate_page(markdown_file_path, renderer)
|
|
110
|
+
relative_path = markdown_file_path.delete_prefix("#{docs_path}/")
|
|
111
|
+
output_path = Utils::PathUtils.markdown_to_html_output(relative_path, config.build.output)
|
|
112
|
+
current_path = Utils::PathUtils.markdown_file_to_url(markdown_file_path, docs_path)
|
|
61
113
|
|
|
62
|
-
html_content = render_markdown_file(markdown_file_path, current_path)
|
|
114
|
+
html_content = render_markdown_file(markdown_file_path, current_path, renderer)
|
|
63
115
|
html_content = apply_search_exclusion(html_content, current_path)
|
|
64
116
|
write_output(output_path, html_content)
|
|
65
117
|
end
|
|
@@ -79,10 +131,10 @@ module Docyard
|
|
|
79
131
|
end
|
|
80
132
|
end
|
|
81
133
|
|
|
82
|
-
def render_markdown_file(markdown_file_path, current_path)
|
|
134
|
+
def render_markdown_file(markdown_file_path, current_path, renderer)
|
|
83
135
|
markdown = Markdown.new(File.read(markdown_file_path))
|
|
84
136
|
template_resolver = TemplateResolver.new(markdown.frontmatter, config.data)
|
|
85
|
-
branding =
|
|
137
|
+
branding = BrandingResolver.new(config).resolve
|
|
86
138
|
|
|
87
139
|
navigation = build_navigation_html(template_resolver, current_path, markdown, branding[:header_ctas])
|
|
88
140
|
renderer.render_file(markdown_file_path, **navigation, branding: branding,
|
|
@@ -91,82 +143,68 @@ module Docyard
|
|
|
91
143
|
end
|
|
92
144
|
|
|
93
145
|
def build_navigation_html(template_resolver, current_path, markdown, header_ctas)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
breadcrumbs: build_breadcrumbs(sidebar_builder.tree, current_path)
|
|
101
|
-
}
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def write_output(output_path, html_content)
|
|
105
|
-
FileUtils.mkdir_p(File.dirname(output_path))
|
|
106
|
-
File.write(output_path, html_content)
|
|
107
|
-
log "Generated: #{output_path}" if verbose
|
|
146
|
+
navigation_builder.build(
|
|
147
|
+
current_path: current_path,
|
|
148
|
+
markdown: markdown,
|
|
149
|
+
header_ctas: header_ctas,
|
|
150
|
+
show_sidebar: template_resolver.show_sidebar?
|
|
151
|
+
)
|
|
108
152
|
end
|
|
109
153
|
|
|
110
|
-
def
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if base_name == "index"
|
|
118
|
-
File.join(output_dir, dir_name, "index.html")
|
|
119
|
-
else
|
|
120
|
-
File.join(output_dir, dir_name, base_name, "index.html")
|
|
121
|
-
end
|
|
154
|
+
def navigation_builder
|
|
155
|
+
@navigation_builder ||= Navigation::PageNavigationBuilder.new(
|
|
156
|
+
docs_path: docs_path,
|
|
157
|
+
config: config,
|
|
158
|
+
sidebar_cache: sidebar_cache
|
|
159
|
+
)
|
|
122
160
|
end
|
|
123
161
|
|
|
124
|
-
def
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if base_name == "index"
|
|
130
|
-
dir_name == "." ? "/" : "/#{dir_name}"
|
|
131
|
-
else
|
|
132
|
-
dir_name == "." ? "/#{base_name}" : "/#{dir_name}/#{base_name}"
|
|
162
|
+
def write_output(output_path, html_content)
|
|
163
|
+
safe_file_write(output_path) do
|
|
164
|
+
FileUtils.mkdir_p(File.dirname(output_path))
|
|
165
|
+
File.write(output_path, html_content)
|
|
133
166
|
end
|
|
167
|
+
log "Generated: #{output_path}" if verbose
|
|
134
168
|
end
|
|
135
169
|
|
|
136
|
-
def
|
|
137
|
-
|
|
138
|
-
docs_path:
|
|
139
|
-
|
|
140
|
-
config: config,
|
|
141
|
-
header_ctas: header_ctas
|
|
170
|
+
def build_sidebar_cache
|
|
171
|
+
@sidebar_cache = Sidebar::Cache.new(
|
|
172
|
+
docs_path: docs_path,
|
|
173
|
+
config: config
|
|
142
174
|
)
|
|
175
|
+
@sidebar_cache.build
|
|
143
176
|
end
|
|
144
177
|
|
|
145
|
-
def
|
|
146
|
-
|
|
147
|
-
sidebar_tree: sidebar_builder.tree,
|
|
148
|
-
current_path: current_path,
|
|
149
|
-
frontmatter: markdown.frontmatter,
|
|
150
|
-
config: {}
|
|
151
|
-
).to_html
|
|
178
|
+
def show_last_updated?
|
|
179
|
+
config.repo.url && config.repo.last_updated != false
|
|
152
180
|
end
|
|
153
181
|
|
|
154
|
-
def
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
BreadcrumbBuilder.new(sidebar_tree: sidebar_tree, current_path: current_path)
|
|
182
|
+
def docs_path
|
|
183
|
+
config.source
|
|
158
184
|
end
|
|
159
185
|
|
|
160
|
-
def
|
|
161
|
-
|
|
186
|
+
def log(message)
|
|
187
|
+
Docyard.logger.info(message) if verbose
|
|
162
188
|
end
|
|
163
189
|
|
|
164
|
-
def
|
|
165
|
-
|
|
190
|
+
def generate_error_page
|
|
191
|
+
ErrorPageGenerator.new(
|
|
192
|
+
config: config,
|
|
193
|
+
docs_path: docs_path,
|
|
194
|
+
renderer: build_renderer
|
|
195
|
+
).generate
|
|
196
|
+
log "[✓] Generated 404.html"
|
|
166
197
|
end
|
|
167
198
|
|
|
168
|
-
def
|
|
169
|
-
|
|
199
|
+
def generate_root_fallback_if_needed
|
|
200
|
+
generator = RootFallbackGenerator.new(
|
|
201
|
+
config: config,
|
|
202
|
+
docs_path: docs_path,
|
|
203
|
+
sidebar_cache: sidebar_cache,
|
|
204
|
+
renderer: build_renderer
|
|
205
|
+
)
|
|
206
|
+
target = generator.generate_if_needed
|
|
207
|
+
log "[✓] Generated root redirect to #{target}" if target
|
|
170
208
|
end
|
|
171
209
|
end
|
|
172
210
|
end
|
data/lib/docyard/builder.rb
CHANGED
|
@@ -68,6 +68,10 @@ module Docyard
|
|
|
68
68
|
sitemap_gen = Build::SitemapGenerator.new(config)
|
|
69
69
|
sitemap_gen.generate
|
|
70
70
|
|
|
71
|
+
require_relative "build/llms_txt_generator"
|
|
72
|
+
llms_gen = Build::LlmsTxtGenerator.new(config)
|
|
73
|
+
llms_gen.generate
|
|
74
|
+
|
|
71
75
|
File.write(File.join(config.build.output, "robots.txt"), robots_txt_content)
|
|
72
76
|
log "[+] Generated robots.txt"
|
|
73
77
|
end
|
|
@@ -104,11 +108,11 @@ module Docyard
|
|
|
104
108
|
end
|
|
105
109
|
|
|
106
110
|
def log(message)
|
|
107
|
-
|
|
111
|
+
Docyard.logger.info(message)
|
|
108
112
|
end
|
|
109
113
|
|
|
110
114
|
def error(message)
|
|
111
|
-
|
|
115
|
+
Docyard.logger.error(message)
|
|
112
116
|
end
|
|
113
117
|
end
|
|
114
118
|
end
|
data/lib/docyard/cli.rb
CHANGED
|
@@ -13,9 +13,11 @@ module Docyard
|
|
|
13
13
|
puts "docyard #{Docyard::VERSION}"
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
desc "init", "Initialize a new docyard project"
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
desc "init [PROJECT_NAME]", "Initialize a new docyard project"
|
|
17
|
+
method_option :force, type: :boolean, default: false, aliases: "-f",
|
|
18
|
+
desc: "Overwrite existing files"
|
|
19
|
+
def init(project_name = nil)
|
|
20
|
+
initializer = Docyard::Initializer.new(project_name, force: options[:force])
|
|
19
21
|
exit(1) unless initializer.run
|
|
20
22
|
end
|
|
21
23
|
|
|
@@ -29,6 +31,9 @@ module Docyard
|
|
|
29
31
|
verbose: options[:verbose]
|
|
30
32
|
)
|
|
31
33
|
exit(1) unless builder.build
|
|
34
|
+
rescue ConfigError => e
|
|
35
|
+
Docyard.logger.error(e.message)
|
|
36
|
+
exit(1)
|
|
32
37
|
end
|
|
33
38
|
|
|
34
39
|
desc "preview", "Preview the built site locally"
|
|
@@ -45,12 +50,17 @@ module Docyard
|
|
|
45
50
|
desc: "Enable search indexing (slower startup)"
|
|
46
51
|
def serve
|
|
47
52
|
require_relative "server/dev_server"
|
|
48
|
-
|
|
53
|
+
config = Docyard::Config.load
|
|
54
|
+
server = Docyard::DevServer.new(
|
|
49
55
|
port: options[:port],
|
|
50
56
|
host: options[:host],
|
|
57
|
+
docs_path: config.source,
|
|
51
58
|
search: options[:search]
|
|
52
59
|
)
|
|
53
60
|
server.start
|
|
61
|
+
rescue ConfigError => e
|
|
62
|
+
Docyard.logger.error(e.message)
|
|
63
|
+
exit(1)
|
|
54
64
|
end
|
|
55
65
|
end
|
|
56
66
|
end
|
|
@@ -112,7 +112,7 @@ module Docyard
|
|
|
112
112
|
end
|
|
113
113
|
|
|
114
114
|
def render_callout_html(type, title, content_html, icon_name)
|
|
115
|
-
icon_svg = Icons.render(icon_name, "
|
|
115
|
+
icon_svg = Icons.render(icon_name, "regular") || ""
|
|
116
116
|
renderer = Renderer.new
|
|
117
117
|
|
|
118
118
|
renderer.render_partial(
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../base_processor"
|
|
4
|
+
|
|
5
|
+
module Docyard
|
|
6
|
+
module Components
|
|
7
|
+
module Processors
|
|
8
|
+
# Restores placeholders after all code block processing.
|
|
9
|
+
# Runs after CodeBlockProcessor to ensure the HTML is fully rendered.
|
|
10
|
+
class CodeBlockExtendedFencePostprocessor < BaseProcessor
|
|
11
|
+
self.priority = 25
|
|
12
|
+
|
|
13
|
+
BACKTICK_PLACEHOLDER = "\u200B\u200B\u200B"
|
|
14
|
+
CODE_MARKER_PLACEHOLDER = "\u200B!\u200Bcode"
|
|
15
|
+
|
|
16
|
+
def postprocess(html)
|
|
17
|
+
html
|
|
18
|
+
.gsub(BACKTICK_PLACEHOLDER, "`")
|
|
19
|
+
.gsub(CODE_MARKER_PLACEHOLDER, "[!code")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../base_processor"
|
|
4
|
+
|
|
5
|
+
module Docyard
|
|
6
|
+
module Components
|
|
7
|
+
module Processors
|
|
8
|
+
# Handles 4+ backtick code fences (extended fences) for showing raw syntax.
|
|
9
|
+
# Escapes special markers so they display as raw text instead of being processed.
|
|
10
|
+
# Example: ```` (4 backticks) wrapping ```js shows the raw markdown syntax.
|
|
11
|
+
class CodeBlockExtendedFencePreprocessor < BaseProcessor
|
|
12
|
+
self.priority = 1
|
|
13
|
+
|
|
14
|
+
# Match 4+ backticks, capture language, content, and closing backticks
|
|
15
|
+
EXTENDED_FENCE_REGEX = /^(`{4,})(\w*)[^\n]*\n(.*?)^\1/m
|
|
16
|
+
|
|
17
|
+
# Placeholders using zero-width spaces
|
|
18
|
+
BACKTICK_PLACEHOLDER = "\u200B\u200B\u200B"
|
|
19
|
+
CODE_MARKER_PLACEHOLDER = "\u200B!\u200Bcode"
|
|
20
|
+
|
|
21
|
+
def preprocess(content)
|
|
22
|
+
content.gsub(EXTENDED_FENCE_REGEX) { |_| process_extended_fence(Regexp.last_match) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def process_extended_fence(match)
|
|
28
|
+
lang = match[2]
|
|
29
|
+
code = match[3].chomp
|
|
30
|
+
|
|
31
|
+
# Replace backticks and code markers with placeholders
|
|
32
|
+
# This prevents other preprocessors from matching/processing them
|
|
33
|
+
escaped_code = code
|
|
34
|
+
.gsub("`", BACKTICK_PLACEHOLDER)
|
|
35
|
+
.gsub("[!code", CODE_MARKER_PLACEHOLDER)
|
|
36
|
+
|
|
37
|
+
# Output as regular 3-backtick fence so it goes through normal processing
|
|
38
|
+
lang_spec = lang.empty? ? "text" : lang
|
|
39
|
+
"```#{lang_spec}\n#{escaped_code}\n```"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../base_processor"
|
|
4
|
+
require_relative "../../rendering/icons"
|
|
4
5
|
|
|
5
6
|
module Docyard
|
|
6
7
|
module Components
|
|
@@ -11,6 +12,7 @@ module Docyard
|
|
|
11
12
|
CODE_FENCE_REGEX = /^```(\w+)(?:\s*\[([^\]]+)\])?(:\S+)?(?:\s*\{([^}\n]+)\})?/
|
|
12
13
|
TABS_BLOCK_REGEX = /^:::[ \t]*tabs[ \t]*\n.*?^:::[ \t]*$/m
|
|
13
14
|
CODE_GROUP_BLOCK_REGEX = /^:::[ \t]*code-group[ \t]*\n.*?^:::[ \t]*$/m
|
|
15
|
+
EXCLUDED_LANGUAGES = %w[filetree].freeze
|
|
14
16
|
|
|
15
17
|
def preprocess(content)
|
|
16
18
|
context[:code_block_options] ||= []
|
|
@@ -40,8 +42,16 @@ module Docyard
|
|
|
40
42
|
position = match.begin(0)
|
|
41
43
|
return match[0] if inside_special_block?(position)
|
|
42
44
|
|
|
45
|
+
original_lang = match[1]
|
|
46
|
+
return match[0] if excluded_language?(original_lang)
|
|
47
|
+
|
|
43
48
|
store_code_block_options(match)
|
|
44
|
-
|
|
49
|
+
highlight_lang = Icons.highlight_language(original_lang)
|
|
50
|
+
"```#{highlight_lang}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def excluded_language?(lang)
|
|
54
|
+
EXCLUDED_LANGUAGES.include?(lang&.downcase)
|
|
45
55
|
end
|
|
46
56
|
|
|
47
57
|
def inside_special_block?(position)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../rendering/icons"
|
|
4
|
-
require_relative "../../rendering/language_mapping"
|
|
5
4
|
require_relative "../../rendering/renderer"
|
|
6
5
|
require_relative "../base_processor"
|
|
7
6
|
require_relative "../support/code_block/icon_detector"
|
|
8
7
|
require_relative "../support/code_block/line_wrapper"
|
|
8
|
+
require_relative "../support/code_block/line_number_resolver"
|
|
9
9
|
require_relative "../support/tabs/range_finder"
|
|
10
10
|
|
|
11
11
|
module Docyard
|
|
@@ -18,6 +18,7 @@ module Docyard
|
|
|
18
18
|
self.priority = 20
|
|
19
19
|
|
|
20
20
|
CodeBlockIconDetector = Support::CodeBlock::IconDetector
|
|
21
|
+
LineNumbers = Support::CodeBlock::LineNumberResolver
|
|
21
22
|
TabsRangeFinder = Support::Tabs::RangeFinder
|
|
22
23
|
|
|
23
24
|
def postprocess(html)
|
|
@@ -36,7 +37,6 @@ module Docyard
|
|
|
36
37
|
@focus_lines = context[:code_block_focus_lines] || []
|
|
37
38
|
@error_lines = context[:code_block_error_lines] || []
|
|
38
39
|
@warning_lines = context[:code_block_warning_lines] || []
|
|
39
|
-
@global_line_numbers = context.dig(:config, "markdown", "lineNumbers") || false
|
|
40
40
|
@tabs_ranges = TabsRangeFinder.find_ranges(html)
|
|
41
41
|
end
|
|
42
42
|
|
|
@@ -80,8 +80,8 @@ module Docyard
|
|
|
80
80
|
def extract_block_data(inner_html)
|
|
81
81
|
opts = current_block_options
|
|
82
82
|
code_text = extract_code_text(inner_html)
|
|
83
|
-
start_line =
|
|
84
|
-
show_line_numbers =
|
|
83
|
+
start_line = LineNumbers.start_line(opts[:option])
|
|
84
|
+
show_line_numbers = LineNumbers.enabled?(opts[:option])
|
|
85
85
|
title_data = CodeBlockIconDetector.detect(opts[:title], opts[:lang])
|
|
86
86
|
|
|
87
87
|
build_block_data(code_text, opts, show_line_numbers, start_line, title_data)
|
|
@@ -106,7 +106,7 @@ module Docyard
|
|
|
106
106
|
error_lines: @error_lines[@block_index] || {},
|
|
107
107
|
warning_lines: @warning_lines[@block_index] || {},
|
|
108
108
|
show_line_numbers: show_line_numbers,
|
|
109
|
-
line_numbers: show_line_numbers ?
|
|
109
|
+
line_numbers: show_line_numbers ? LineNumbers.generate_numbers(code_text, start_line) : [],
|
|
110
110
|
start_line: start_line,
|
|
111
111
|
title: title_data[:title],
|
|
112
112
|
icon: title_data[:icon],
|
|
@@ -123,25 +123,6 @@ module Docyard
|
|
|
123
123
|
wrap_code_block(original_html, block_data)
|
|
124
124
|
end
|
|
125
125
|
|
|
126
|
-
def determine_line_numbers(block_option)
|
|
127
|
-
return false if block_option == ":no-line-numbers"
|
|
128
|
-
return true if block_option&.start_with?(":line-numbers")
|
|
129
|
-
|
|
130
|
-
@global_line_numbers
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def extract_start_line(block_option)
|
|
134
|
-
return 1 unless block_option&.include?("=")
|
|
135
|
-
|
|
136
|
-
block_option.split("=").last.to_i
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def generate_line_numbers(code_text, start_line)
|
|
140
|
-
line_count = code_text.lines.count
|
|
141
|
-
line_count = 1 if line_count.zero?
|
|
142
|
-
(start_line...(start_line + line_count)).to_a
|
|
143
|
-
end
|
|
144
|
-
|
|
145
126
|
def inside_tabs?(position)
|
|
146
127
|
@tabs_ranges.any? { |range| range.cover?(position) }
|
|
147
128
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "../base_processor"
|
|
4
4
|
require_relative "../support/code_block/feature_extractor"
|
|
5
5
|
require_relative "../support/code_block/line_wrapper"
|
|
6
|
+
require_relative "../support/code_block/line_number_resolver"
|
|
6
7
|
require_relative "../support/code_group/html_builder"
|
|
7
8
|
require_relative "../support/markdown_code_block_helper"
|
|
8
9
|
require_relative "../../rendering/icons"
|
|
@@ -26,6 +27,7 @@ module Docyard
|
|
|
26
27
|
|
|
27
28
|
CodeBlockFeatureExtractor = Support::CodeBlock::FeatureExtractor
|
|
28
29
|
CodeBlockLineWrapper = Support::CodeBlock::LineWrapper
|
|
30
|
+
LineNumbers = Support::CodeBlock::LineNumberResolver
|
|
29
31
|
CodeGroupHtmlBuilder = Support::CodeGroup::HtmlBuilder
|
|
30
32
|
|
|
31
33
|
def preprocess(content)
|
|
@@ -136,7 +138,7 @@ module Docyard
|
|
|
136
138
|
focus_lines: block_data[:focus_lines] || {},
|
|
137
139
|
error_lines: block_data[:error_lines] || {},
|
|
138
140
|
warning_lines: block_data[:warning_lines] || {},
|
|
139
|
-
start_line:
|
|
141
|
+
start_line: LineNumbers.start_line(block_data[:option])
|
|
140
142
|
}
|
|
141
143
|
end
|
|
142
144
|
|
|
@@ -145,13 +147,13 @@ module Docyard
|
|
|
145
147
|
end
|
|
146
148
|
|
|
147
149
|
def base_locals(processed_html, code_text, block_data)
|
|
148
|
-
show_ln =
|
|
149
|
-
start =
|
|
150
|
+
show_ln = LineNumbers.enabled?(block_data[:option])
|
|
151
|
+
start = LineNumbers.start_line(block_data[:option])
|
|
150
152
|
|
|
151
153
|
{
|
|
152
154
|
code_block_html: processed_html, code_text: escape_html_attribute(code_text),
|
|
153
155
|
copy_icon: Icons.render("copy", "regular") || "", show_line_numbers: show_ln,
|
|
154
|
-
line_numbers: show_ln ?
|
|
156
|
+
line_numbers: show_ln ? LineNumbers.generate_numbers(code_text, start) : [], start_line: start
|
|
155
157
|
}
|
|
156
158
|
end
|
|
157
159
|
|
|
@@ -167,24 +169,6 @@ module Docyard
|
|
|
167
169
|
{ title: nil, icon: nil, icon_source: nil }
|
|
168
170
|
end
|
|
169
171
|
|
|
170
|
-
def line_numbers_enabled?(option)
|
|
171
|
-
return false if option == ":no-line-numbers"
|
|
172
|
-
return true if option&.start_with?(":line-numbers")
|
|
173
|
-
|
|
174
|
-
false
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
def extract_start_line(option)
|
|
178
|
-
return 1 unless option&.include?("=")
|
|
179
|
-
|
|
180
|
-
option.split("=").last.to_i
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
def generate_line_numbers(code_text, start_line)
|
|
184
|
-
count = [code_text.lines.count, 1].max
|
|
185
|
-
(start_line...(start_line + count)).to_a
|
|
186
|
-
end
|
|
187
|
-
|
|
188
172
|
def extract_code_text(html)
|
|
189
173
|
CGI.unescapeHTML(html.gsub(/<[^>]+>/, "")).strip
|
|
190
174
|
end
|