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.
Files changed (165) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +57 -1
  3. data/README.md +8 -253
  4. data/exe/docyard +6 -0
  5. data/lib/docyard/build/asset_bundler.rb +24 -2
  6. data/lib/docyard/build/error_page_generator.rb +33 -0
  7. data/lib/docyard/build/file_copier.rb +12 -5
  8. data/lib/docyard/build/file_writer.rb +19 -0
  9. data/lib/docyard/build/llms_txt_generator.rb +103 -0
  10. data/lib/docyard/build/root_fallback_generator.rb +66 -0
  11. data/lib/docyard/build/sitemap_generator.rb +1 -1
  12. data/lib/docyard/build/static_generator.rb +119 -81
  13. data/lib/docyard/builder.rb +6 -2
  14. data/lib/docyard/cli.rb +14 -4
  15. data/lib/docyard/components/processors/callout_processor.rb +1 -1
  16. data/lib/docyard/components/processors/code_block_extended_fence_postprocessor.rb +24 -0
  17. data/lib/docyard/components/processors/code_block_extended_fence_preprocessor.rb +44 -0
  18. data/lib/docyard/components/processors/code_block_options_preprocessor.rb +11 -1
  19. data/lib/docyard/components/processors/code_block_processor.rb +5 -24
  20. data/lib/docyard/components/processors/code_group_processor.rb +6 -22
  21. data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +1 -0
  22. data/lib/docyard/components/processors/file_tree_processor.rb +1 -2
  23. data/lib/docyard/components/processors/icon_processor.rb +8 -2
  24. data/lib/docyard/components/processors/include_processor.rb +10 -10
  25. data/lib/docyard/components/processors/video_embed_processor.rb +14 -3
  26. data/lib/docyard/components/support/code_block/feature_extractor.rb +3 -1
  27. data/lib/docyard/components/support/code_block/icon_detector.rb +5 -12
  28. data/lib/docyard/components/support/code_block/line_number_resolver.rb +30 -0
  29. data/lib/docyard/components/support/code_detector.rb +2 -12
  30. data/lib/docyard/components/support/code_group/html_builder.rb +2 -6
  31. data/lib/docyard/components/support/tabs/icon_detector.rb +6 -2
  32. data/lib/docyard/components/support/tabs/parser.rb +6 -23
  33. data/lib/docyard/config/analytics_resolver.rb +24 -0
  34. data/lib/docyard/config/branding_resolver.rb +58 -27
  35. data/lib/docyard/config/key_validator.rb +30 -0
  36. data/lib/docyard/config/logo_detector.rb +8 -8
  37. data/lib/docyard/config/schema.rb +39 -0
  38. data/lib/docyard/config/section.rb +21 -0
  39. data/lib/docyard/config/validation_helpers.rb +83 -0
  40. data/lib/docyard/config/validator.rb +45 -144
  41. data/lib/docyard/config/validators/navigation.rb +43 -0
  42. data/lib/docyard/config/validators/section.rb +114 -0
  43. data/lib/docyard/config.rb +46 -102
  44. data/lib/docyard/constants.rb +59 -0
  45. data/lib/docyard/{utils/errors.rb → errors.rb} +6 -0
  46. data/lib/docyard/initializer.rb +100 -49
  47. data/lib/docyard/navigation/breadcrumb_builder.rb +45 -6
  48. data/lib/docyard/navigation/page_navigation_builder.rb +65 -0
  49. data/lib/docyard/navigation/sidebar/auto_builder.rb +107 -0
  50. data/lib/docyard/navigation/sidebar/cache.rb +96 -0
  51. data/lib/docyard/navigation/sidebar/config_builder.rb +179 -0
  52. data/lib/docyard/navigation/sidebar/distributed_builder.rb +145 -0
  53. data/lib/docyard/navigation/sidebar/local_config_loader.rb +69 -3
  54. data/lib/docyard/navigation/sidebar/renderer.rb +12 -1
  55. data/lib/docyard/navigation/sidebar_builder.rb +43 -81
  56. data/lib/docyard/rendering/branding_variables.rb +65 -0
  57. data/lib/docyard/rendering/icon_helpers.rb +14 -1
  58. data/lib/docyard/rendering/icons/devicons.rb +63 -0
  59. data/lib/docyard/rendering/icons.rb +26 -27
  60. data/lib/docyard/rendering/markdown.rb +5 -23
  61. data/lib/docyard/rendering/og_helpers.rb +36 -0
  62. data/lib/docyard/rendering/renderer.rb +96 -61
  63. data/lib/docyard/rendering/template_resolver.rb +14 -0
  64. data/lib/docyard/routing/fallback_resolver.rb +3 -3
  65. data/lib/docyard/search/build_indexer.rb +2 -2
  66. data/lib/docyard/search/dev_indexer.rb +36 -28
  67. data/lib/docyard/search/pagefind_support.rb +1 -1
  68. data/lib/docyard/server/asset_handler.rb +39 -15
  69. data/lib/docyard/server/dev_server.rb +90 -55
  70. data/lib/docyard/server/file_watcher.rb +68 -18
  71. data/lib/docyard/server/pagefind_handler.rb +1 -1
  72. data/lib/docyard/server/preview_server.rb +29 -33
  73. data/lib/docyard/server/rack_application.rb +39 -71
  74. data/lib/docyard/server/router.rb +11 -7
  75. data/lib/docyard/server/sse_server.rb +157 -0
  76. data/lib/docyard/server/static_file_app.rb +42 -0
  77. data/lib/docyard/templates/assets/css/components/banner.css +31 -0
  78. data/lib/docyard/templates/assets/css/components/breadcrumbs.css +2 -1
  79. data/lib/docyard/templates/assets/css/components/callout.css +26 -6
  80. data/lib/docyard/templates/assets/css/components/code-block.css +4 -2
  81. data/lib/docyard/templates/assets/css/components/code-group.css +20 -7
  82. data/lib/docyard/templates/assets/css/components/feedback.css +126 -0
  83. data/lib/docyard/templates/assets/css/components/file-tree.css +5 -4
  84. data/lib/docyard/templates/assets/css/components/heading-anchor.css +2 -2
  85. data/lib/docyard/templates/assets/css/components/icon.css +5 -0
  86. data/lib/docyard/templates/assets/css/components/nav-menu.css +20 -4
  87. data/lib/docyard/templates/assets/css/components/navigation.css +25 -3
  88. data/lib/docyard/templates/assets/css/components/page-actions.css +131 -0
  89. data/lib/docyard/templates/assets/css/components/prev-next.css +14 -7
  90. data/lib/docyard/templates/assets/css/components/search.css +6 -10
  91. data/lib/docyard/templates/assets/css/components/tab-bar.css +9 -6
  92. data/lib/docyard/templates/assets/css/components/table-of-contents.css +63 -17
  93. data/lib/docyard/templates/assets/css/components/tabs.css +12 -4
  94. data/lib/docyard/templates/assets/css/components/theme-toggle.css +3 -1
  95. data/lib/docyard/templates/assets/css/landing.css +82 -13
  96. data/lib/docyard/templates/assets/css/layout.css +32 -16
  97. data/lib/docyard/templates/assets/css/markdown.css +22 -2
  98. data/lib/docyard/templates/assets/css/variables.css +14 -1
  99. data/lib/docyard/templates/assets/js/components/code-group.js +4 -1
  100. data/lib/docyard/templates/assets/js/components/copy-page.js +115 -0
  101. data/lib/docyard/templates/assets/js/components/feedback.js +66 -0
  102. data/lib/docyard/templates/assets/js/components/file-tree.js +5 -5
  103. data/lib/docyard/templates/assets/js/components/navigation.js +3 -3
  104. data/lib/docyard/templates/assets/js/components/search.js +3 -3
  105. data/lib/docyard/templates/assets/js/components/table-of-contents.js +12 -6
  106. data/lib/docyard/templates/assets/js/components/tabs.js +45 -22
  107. data/lib/docyard/templates/assets/js/components/tooltip.js +4 -4
  108. data/lib/docyard/templates/assets/js/hot-reload.js +44 -0
  109. data/lib/docyard/templates/errors/404.html.erb +125 -5
  110. data/lib/docyard/templates/errors/500.html.erb +184 -10
  111. data/lib/docyard/templates/errors/redirect.html.erb +12 -0
  112. data/lib/docyard/templates/init/_sidebar.yml +36 -0
  113. data/lib/docyard/templates/init/docyard.yml +36 -0
  114. data/lib/docyard/templates/init/pages/components.md +146 -0
  115. data/lib/docyard/templates/init/pages/getting-started.md +94 -0
  116. data/lib/docyard/templates/init/pages/index.md +22 -0
  117. data/lib/docyard/templates/layouts/default.html.erb +10 -0
  118. data/lib/docyard/templates/layouts/splash.html.erb +14 -1
  119. data/lib/docyard/templates/partials/_analytics.html.erb +24 -0
  120. data/lib/docyard/templates/partials/_banner.html.erb +1 -1
  121. data/lib/docyard/templates/partials/_code_block.html.erb +1 -1
  122. data/lib/docyard/templates/partials/_feedback.html.erb +14 -0
  123. data/lib/docyard/templates/partials/_footer.html.erb +1 -1
  124. data/lib/docyard/templates/partials/_head.html.erb +80 -5
  125. data/lib/docyard/templates/partials/_icon_library.html.erb +8 -0
  126. data/lib/docyard/templates/partials/_page_actions.html.erb +21 -0
  127. data/lib/docyard/templates/partials/_scripts.html.erb +6 -3
  128. data/lib/docyard/templates/partials/_tabs.html.erb +4 -1
  129. data/lib/docyard/utils/git_info.rb +157 -0
  130. data/lib/docyard/utils/hash_utils.rb +31 -0
  131. data/lib/docyard/utils/html_helpers.rb +8 -0
  132. data/lib/docyard/utils/logging.rb +44 -3
  133. data/lib/docyard/utils/path_resolver.rb +0 -10
  134. data/lib/docyard/utils/path_utils.rb +73 -0
  135. data/lib/docyard/version.rb +1 -1
  136. data/lib/docyard.rb +2 -2
  137. metadata +81 -47
  138. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -31
  139. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  140. data/.github/pull_request_template.md +0 -14
  141. data/.github/workflows/ci.yml +0 -49
  142. data/.rubocop.yml +0 -42
  143. data/CODE_OF_CONDUCT.md +0 -132
  144. data/CONTRIBUTING.md +0 -55
  145. data/LICENSE.vscode-icons +0 -42
  146. data/Rakefile +0 -8
  147. data/lib/docyard/config/constants.rb +0 -31
  148. data/lib/docyard/navigation/sidebar/children_discoverer.rb +0 -51
  149. data/lib/docyard/navigation/sidebar/config_parser.rb +0 -208
  150. data/lib/docyard/navigation/sidebar/file_resolver.rb +0 -90
  151. data/lib/docyard/navigation/sidebar/file_system_scanner.rb +0 -78
  152. data/lib/docyard/navigation/sidebar/metadata_extractor.rb +0 -71
  153. data/lib/docyard/navigation/sidebar/metadata_reader.rb +0 -51
  154. data/lib/docyard/navigation/sidebar/path_prefixer.rb +0 -34
  155. data/lib/docyard/navigation/sidebar/sorter.rb +0 -21
  156. data/lib/docyard/navigation/sidebar/title_extractor.rb +0 -25
  157. data/lib/docyard/navigation/sidebar/tree_builder.rb +0 -140
  158. data/lib/docyard/rendering/icons/LICENSE.phosphor +0 -21
  159. data/lib/docyard/rendering/icons/file_types.rb +0 -79
  160. data/lib/docyard/rendering/icons/phosphor.rb +0 -93
  161. data/lib/docyard/rendering/language_mapping.rb +0 -52
  162. data/lib/docyard/templates/assets/js/reload.js +0 -98
  163. data/lib/docyard/templates/partials/_icon.html.erb +0 -1
  164. data/lib/docyard/templates/partials/_icon_file_extension.html.erb +0 -1
  165. 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
- puts "[✓] Generated sitemap.xml (#{urls.size} URLs)"
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/prev_next_builder"
6
- require_relative "../navigation/breadcrumb_builder"
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
- attr_reader :config, :verbose, :renderer
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
- @renderer = Renderer.new(base_url: config.build.base, config: config)
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
- puts "\n[✓] Found #{markdown_files.size} markdown files"
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
- markdown_files.each do |file_path|
32
- generate_page(file_path)
33
- progress.advance
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?("docs/index.html")
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
- FileUtils.mkdir_p(File.dirname(output_path))
48
- FileUtils.cp("docs/index.html", output_path)
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("docs", "**", "*.md"))
54
- files.reject! { |f| f == "docs/index.md" } if custom_landing_page?
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 generate_page(markdown_file_path)
59
- output_path = determine_output_path(markdown_file_path)
60
- current_path = determine_current_path(markdown_file_path)
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 = branding_options
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
- return { sidebar_html: "", prev_next_html: "", breadcrumbs: nil } unless template_resolver.show_sidebar?
95
-
96
- sidebar_builder = build_sidebar_instance(current_path, header_ctas)
97
- {
98
- sidebar_html: sidebar_builder.to_html,
99
- prev_next_html: build_prev_next(sidebar_builder, current_path, markdown),
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 determine_output_path(markdown_file_path)
111
- relative_path = markdown_file_path.delete_prefix("docs/")
112
- base_name = File.basename(relative_path, ".md")
113
- dir_name = File.dirname(relative_path)
114
-
115
- output_dir = config.build.output
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 determine_current_path(markdown_file_path)
125
- relative_path = markdown_file_path.delete_prefix("docs/")
126
- base_name = File.basename(relative_path, ".md")
127
- dir_name = File.dirname(relative_path)
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 build_sidebar_instance(current_path, header_ctas = [])
137
- SidebarBuilder.new(
138
- docs_path: "docs",
139
- current_path: current_path,
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 build_prev_next(sidebar_builder, current_path, markdown)
146
- PrevNextBuilder.new(
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 build_breadcrumbs(sidebar_tree, current_path)
155
- return nil unless breadcrumbs_enabled?
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 breadcrumbs_enabled?
161
- config.navigation.breadcrumbs != false
186
+ def log(message)
187
+ Docyard.logger.info(message) if verbose
162
188
  end
163
189
 
164
- def branding_options
165
- BrandingResolver.new(config).resolve
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 log(message)
169
- puts message if verbose
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
@@ -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
- puts message
111
+ Docyard.logger.info(message)
108
112
  end
109
113
 
110
114
  def error(message)
111
- warn "[ERROR] #{message}"
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
- def init
18
- initializer = Docyard::Initializer.new
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
- server = Docyard::Server.new(
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, "duotone") || ""
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
- "```#{match[1]}"
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 = extract_start_line(opts[:option])
84
- show_line_numbers = determine_line_numbers(opts[:option])
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 ? generate_line_numbers(code_text, start_line) : [],
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: extract_start_line(block_data[:option])
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 = line_numbers_enabled?(block_data[:option])
149
- start = extract_start_line(block_data[:option])
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 ? generate_line_numbers(code_text, start) : [], start_line: start
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