docyard 0.8.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.
Files changed (189) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -1
  3. data/README.md +8 -253
  4. data/exe/docyard +6 -0
  5. data/lib/docyard/build/asset_bundler.rb +2 -2
  6. data/lib/docyard/build/file_copier.rb +12 -5
  7. data/lib/docyard/build/llms_txt_generator.rb +103 -0
  8. data/lib/docyard/build/sitemap_generator.rb +1 -1
  9. data/lib/docyard/build/static_generator.rb +115 -79
  10. data/lib/docyard/builder.rb +6 -2
  11. data/lib/docyard/cli.rb +14 -4
  12. data/lib/docyard/components/aliases.rb +12 -0
  13. data/lib/docyard/components/processors/abbreviation_processor.rb +72 -0
  14. data/lib/docyard/components/processors/accordion_processor.rb +81 -0
  15. data/lib/docyard/components/processors/badge_processor.rb +72 -0
  16. data/lib/docyard/components/processors/callout_processor.rb +9 -3
  17. data/lib/docyard/components/processors/cards_processor.rb +100 -0
  18. data/lib/docyard/components/processors/code_block_extended_fence_postprocessor.rb +24 -0
  19. data/lib/docyard/components/processors/code_block_extended_fence_preprocessor.rb +44 -0
  20. data/lib/docyard/components/processors/code_block_options_preprocessor.rb +34 -3
  21. data/lib/docyard/components/processors/code_block_processor.rb +11 -24
  22. data/lib/docyard/components/processors/code_group_processor.rb +182 -0
  23. data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +7 -1
  24. data/lib/docyard/components/processors/custom_anchor_processor.rb +42 -0
  25. data/lib/docyard/components/processors/file_tree_processor.rb +150 -0
  26. data/lib/docyard/components/processors/icon_processor.rb +8 -2
  27. data/lib/docyard/components/processors/image_caption_processor.rb +96 -0
  28. data/lib/docyard/components/processors/include_processor.rb +86 -0
  29. data/lib/docyard/components/processors/steps_processor.rb +89 -0
  30. data/lib/docyard/components/processors/tabs_processor.rb +9 -1
  31. data/lib/docyard/components/processors/tooltip_processor.rb +57 -0
  32. data/lib/docyard/components/processors/video_embed_processor.rb +207 -0
  33. data/lib/docyard/components/support/code_block/feature_extractor.rb +3 -1
  34. data/lib/docyard/components/support/code_block/icon_detector.rb +5 -12
  35. data/lib/docyard/components/support/code_block/line_number_resolver.rb +30 -0
  36. data/lib/docyard/components/support/code_detector.rb +2 -12
  37. data/lib/docyard/components/support/code_group/html_builder.rb +118 -0
  38. data/lib/docyard/components/support/markdown_code_block_helper.rb +56 -0
  39. data/lib/docyard/components/support/tabs/icon_detector.rb +6 -2
  40. data/lib/docyard/components/support/tabs/parser.rb +6 -23
  41. data/lib/docyard/config/analytics_resolver.rb +24 -0
  42. data/lib/docyard/config/branding_resolver.rb +84 -58
  43. data/lib/docyard/config/key_validator.rb +30 -0
  44. data/lib/docyard/config/logo_detector.rb +39 -0
  45. data/lib/docyard/config/schema.rb +39 -0
  46. data/lib/docyard/config/section.rb +21 -0
  47. data/lib/docyard/config/validation_helpers.rb +83 -0
  48. data/lib/docyard/config/validator.rb +45 -144
  49. data/lib/docyard/config/validators/navigation.rb +43 -0
  50. data/lib/docyard/config/validators/section.rb +114 -0
  51. data/lib/docyard/config.rb +45 -96
  52. data/lib/docyard/constants.rb +59 -0
  53. data/lib/docyard/{utils/errors.rb → errors.rb} +6 -0
  54. data/lib/docyard/initializer.rb +100 -49
  55. data/lib/docyard/navigation/page_navigation_builder.rb +65 -0
  56. data/lib/docyard/navigation/sidebar/auto_builder.rb +107 -0
  57. data/lib/docyard/navigation/sidebar/cache.rb +96 -0
  58. data/lib/docyard/navigation/sidebar/config_builder.rb +179 -0
  59. data/lib/docyard/navigation/sidebar/distributed_builder.rb +145 -0
  60. data/lib/docyard/navigation/sidebar/item.rb +6 -1
  61. data/lib/docyard/navigation/sidebar/local_config_loader.rb +69 -3
  62. data/lib/docyard/navigation/sidebar/renderer.rb +18 -3
  63. data/lib/docyard/navigation/sidebar_builder.rb +43 -81
  64. data/lib/docyard/rendering/branding_variables.rb +65 -0
  65. data/lib/docyard/rendering/icon_helpers.rb +14 -1
  66. data/lib/docyard/rendering/icons/devicons.rb +63 -0
  67. data/lib/docyard/rendering/icons.rb +26 -27
  68. data/lib/docyard/rendering/markdown.rb +20 -15
  69. data/lib/docyard/rendering/og_helpers.rb +36 -0
  70. data/lib/docyard/rendering/renderer.rb +87 -58
  71. data/lib/docyard/rendering/template_resolver.rb +14 -0
  72. data/lib/docyard/routing/fallback_resolver.rb +3 -3
  73. data/lib/docyard/search/build_indexer.rb +2 -2
  74. data/lib/docyard/search/dev_indexer.rb +36 -28
  75. data/lib/docyard/search/pagefind_support.rb +1 -1
  76. data/lib/docyard/server/asset_handler.rb +40 -15
  77. data/lib/docyard/server/dev_server.rb +90 -55
  78. data/lib/docyard/server/file_watcher.rb +68 -18
  79. data/lib/docyard/server/pagefind_handler.rb +1 -1
  80. data/lib/docyard/server/preview_server.rb +29 -33
  81. data/lib/docyard/server/rack_application.rb +38 -70
  82. data/lib/docyard/server/router.rb +11 -7
  83. data/lib/docyard/server/sse_server.rb +157 -0
  84. data/lib/docyard/server/static_file_app.rb +42 -0
  85. data/lib/docyard/templates/assets/css/components/abbreviation.css +86 -0
  86. data/lib/docyard/templates/assets/css/components/accordion.css +138 -0
  87. data/lib/docyard/templates/assets/css/components/badges.css +47 -0
  88. data/lib/docyard/templates/assets/css/components/banner.css +233 -0
  89. data/lib/docyard/templates/assets/css/components/breadcrumbs.css +2 -1
  90. data/lib/docyard/templates/assets/css/components/callout.css +26 -6
  91. data/lib/docyard/templates/assets/css/components/cards.css +100 -0
  92. data/lib/docyard/templates/assets/css/components/code-block.css +14 -2
  93. data/lib/docyard/templates/assets/css/components/code-group.css +294 -0
  94. data/lib/docyard/templates/assets/css/components/feedback.css +126 -0
  95. data/lib/docyard/templates/assets/css/components/figure.css +22 -0
  96. data/lib/docyard/templates/assets/css/components/file-tree.css +125 -0
  97. data/lib/docyard/templates/assets/css/components/heading-anchor.css +21 -13
  98. data/lib/docyard/templates/assets/css/components/icon.css +5 -0
  99. data/lib/docyard/templates/assets/css/components/lightbox.css +65 -0
  100. data/lib/docyard/templates/assets/css/components/nav-menu.css +20 -4
  101. data/lib/docyard/templates/assets/css/components/navigation.css +32 -3
  102. data/lib/docyard/templates/assets/css/components/page-actions.css +131 -0
  103. data/lib/docyard/templates/assets/css/components/prev-next.css +20 -22
  104. data/lib/docyard/templates/assets/css/components/search.css +6 -10
  105. data/lib/docyard/templates/assets/css/components/steps.css +122 -0
  106. data/lib/docyard/templates/assets/css/components/tab-bar.css +7 -4
  107. data/lib/docyard/templates/assets/css/components/table-of-contents.css +57 -11
  108. data/lib/docyard/templates/assets/css/components/tabs.css +13 -5
  109. data/lib/docyard/templates/assets/css/components/theme-toggle.css +3 -1
  110. data/lib/docyard/templates/assets/css/components/tooltip.css +113 -0
  111. data/lib/docyard/templates/assets/css/components/video.css +41 -0
  112. data/lib/docyard/templates/assets/css/landing.css +82 -13
  113. data/lib/docyard/templates/assets/css/layout.css +17 -0
  114. data/lib/docyard/templates/assets/css/markdown.css +25 -3
  115. data/lib/docyard/templates/assets/css/variables.css +13 -1
  116. data/lib/docyard/templates/assets/js/components/abbreviation.js +85 -0
  117. data/lib/docyard/templates/assets/js/components/banner.js +81 -0
  118. data/lib/docyard/templates/assets/js/components/code-group.js +286 -0
  119. data/lib/docyard/templates/assets/js/components/copy-page.js +115 -0
  120. data/lib/docyard/templates/assets/js/components/feedback.js +66 -0
  121. data/lib/docyard/templates/assets/js/components/file-tree.js +39 -0
  122. data/lib/docyard/templates/assets/js/components/lightbox.js +72 -0
  123. data/lib/docyard/templates/assets/js/components/navigation.js +3 -3
  124. data/lib/docyard/templates/assets/js/components/search.js +3 -3
  125. data/lib/docyard/templates/assets/js/components/table-of-contents.js +12 -6
  126. data/lib/docyard/templates/assets/js/components/tabs.js +45 -22
  127. data/lib/docyard/templates/assets/js/components/tooltip.js +118 -0
  128. data/lib/docyard/templates/assets/js/hot-reload.js +44 -0
  129. data/lib/docyard/templates/errors/404.html.erb +114 -5
  130. data/lib/docyard/templates/errors/500.html.erb +173 -10
  131. data/lib/docyard/templates/init/_sidebar.yml +36 -0
  132. data/lib/docyard/templates/init/docyard.yml +36 -0
  133. data/lib/docyard/templates/init/pages/components.md +146 -0
  134. data/lib/docyard/templates/init/pages/getting-started.md +94 -0
  135. data/lib/docyard/templates/init/pages/index.md +22 -0
  136. data/lib/docyard/templates/layouts/default.html.erb +11 -0
  137. data/lib/docyard/templates/layouts/splash.html.erb +15 -1
  138. data/lib/docyard/templates/partials/_accordion.html.erb +9 -0
  139. data/lib/docyard/templates/partials/_analytics.html.erb +24 -0
  140. data/lib/docyard/templates/partials/_banner.html.erb +27 -0
  141. data/lib/docyard/templates/partials/_card.html.erb +23 -0
  142. data/lib/docyard/templates/partials/_code_block.html.erb +1 -1
  143. data/lib/docyard/templates/partials/_feedback.html.erb +14 -0
  144. data/lib/docyard/templates/partials/_footer.html.erb +1 -1
  145. data/lib/docyard/templates/partials/_head.html.erb +79 -4
  146. data/lib/docyard/templates/partials/_icon_library.html.erb +8 -0
  147. data/lib/docyard/templates/partials/_nav_group.html.erb +6 -0
  148. data/lib/docyard/templates/partials/_nav_leaf.html.erb +3 -0
  149. data/lib/docyard/templates/partials/_page_actions.html.erb +21 -0
  150. data/lib/docyard/templates/partials/_scripts.html.erb +6 -3
  151. data/lib/docyard/templates/partials/_step.html.erb +14 -0
  152. data/lib/docyard/templates/partials/_tabs.html.erb +4 -1
  153. data/lib/docyard/utils/git_info.rb +157 -0
  154. data/lib/docyard/utils/hash_utils.rb +31 -0
  155. data/lib/docyard/utils/html_helpers.rb +8 -0
  156. data/lib/docyard/utils/logging.rb +44 -3
  157. data/lib/docyard/utils/path_resolver.rb +0 -10
  158. data/lib/docyard/utils/path_utils.rb +73 -0
  159. data/lib/docyard/version.rb +1 -1
  160. data/lib/docyard.rb +2 -2
  161. metadata +114 -47
  162. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -31
  163. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  164. data/.github/pull_request_template.md +0 -14
  165. data/.github/workflows/ci.yml +0 -49
  166. data/.rubocop.yml +0 -42
  167. data/CODE_OF_CONDUCT.md +0 -132
  168. data/CONTRIBUTING.md +0 -55
  169. data/LICENSE.vscode-icons +0 -42
  170. data/Rakefile +0 -8
  171. data/lib/docyard/config/constants.rb +0 -31
  172. data/lib/docyard/navigation/sidebar/children_discoverer.rb +0 -51
  173. data/lib/docyard/navigation/sidebar/config_parser.rb +0 -208
  174. data/lib/docyard/navigation/sidebar/file_resolver.rb +0 -78
  175. data/lib/docyard/navigation/sidebar/file_system_scanner.rb +0 -78
  176. data/lib/docyard/navigation/sidebar/metadata_extractor.rb +0 -69
  177. data/lib/docyard/navigation/sidebar/metadata_reader.rb +0 -47
  178. data/lib/docyard/navigation/sidebar/path_prefixer.rb +0 -34
  179. data/lib/docyard/navigation/sidebar/sorter.rb +0 -21
  180. data/lib/docyard/navigation/sidebar/title_extractor.rb +0 -25
  181. data/lib/docyard/navigation/sidebar/tree_builder.rb +0 -139
  182. data/lib/docyard/rendering/icons/LICENSE.phosphor +0 -21
  183. data/lib/docyard/rendering/icons/file_types.rb +0 -79
  184. data/lib/docyard/rendering/icons/phosphor.rb +0 -90
  185. data/lib/docyard/rendering/language_mapping.rb +0 -52
  186. data/lib/docyard/templates/assets/js/reload.js +0 -98
  187. data/lib/docyard/templates/partials/_icon.html.erb +0 -1
  188. data/lib/docyard/templates/partials/_icon_file_extension.html.erb +0 -1
  189. data/sig/docyard.rbs +0 -4
@@ -1,65 +1,111 @@
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"
7
10
 
8
11
  module Docyard
9
12
  module Build
10
13
  class StaticGenerator
11
- attr_reader :config, :verbose, :renderer
14
+ PARALLEL_THRESHOLD = 10
15
+
16
+ attr_reader :config, :verbose, :sidebar_cache
12
17
 
13
18
  def initialize(config, verbose: false)
14
19
  @config = config
15
20
  @verbose = verbose
16
- @renderer = Renderer.new(base_url: config.build.base, config: config)
21
+ @sidebar_cache = nil
17
22
  end
18
23
 
19
24
  def generate
25
+ build_sidebar_cache
26
+ Utils::GitInfo.prefetch_timestamps(docs_path) if show_last_updated?
20
27
  copy_custom_landing_page if custom_landing_page?
21
28
 
22
29
  markdown_files = collect_markdown_files
23
- puts "\n[✓] Found #{markdown_files.size} markdown files"
30
+ Docyard.logger.info("\n[✓] Found #{markdown_files.size} markdown files")
31
+
32
+ generate_all_pages(markdown_files)
33
+ generate_error_page
34
+
35
+ markdown_files.size
36
+ ensure
37
+ Utils::GitInfo.clear_cache
38
+ end
39
+
40
+ private
24
41
 
42
+ def generate_all_pages(markdown_files)
25
43
  progress = TTY::ProgressBar.new(
26
44
  "Generating pages [:bar] :current/:total (:percent)",
27
45
  total: markdown_files.size,
28
46
  width: 50
29
47
  )
48
+ mutex = Mutex.new
30
49
 
31
- markdown_files.each do |file_path|
32
- generate_page(file_path)
33
- progress.advance
50
+ Logging.start_buffering
51
+ if markdown_files.size >= PARALLEL_THRESHOLD
52
+ generate_pages_in_parallel(markdown_files, progress, mutex)
53
+ else
54
+ generate_pages_sequentially(markdown_files, progress)
34
55
  end
35
-
36
- markdown_files.size
56
+ Logging.flush_warnings
37
57
  end
38
58
 
39
- private
40
-
41
59
  def custom_landing_page?
42
- File.file?("docs/index.html")
60
+ File.file?(File.join(docs_path, "index.html"))
43
61
  end
44
62
 
45
63
  def copy_custom_landing_page
46
64
  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)
65
+ safe_file_write(output_path) do
66
+ FileUtils.mkdir_p(File.dirname(output_path))
67
+ FileUtils.cp(File.join(docs_path, "index.html"), output_path)
68
+ end
49
69
  log "[✓] Copied custom landing page (index.html)"
50
70
  end
51
71
 
52
72
  def collect_markdown_files
53
- files = Dir.glob(File.join("docs", "**", "*.md"))
54
- files.reject! { |f| f == "docs/index.md" } if custom_landing_page?
73
+ files = Dir.glob(File.join(docs_path, "**", "*.md"))
74
+ files.reject! { |f| f == File.join(docs_path, "index.md") } if custom_landing_page?
55
75
  files
56
76
  end
57
77
 
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)
78
+ def generate_pages_in_parallel(markdown_files, progress, mutex)
79
+ Parallel.each(markdown_files, in_threads: Parallel.processor_count) do |file_path|
80
+ generate_page(file_path, thread_local_renderer)
81
+ mutex.synchronize { progress.advance }
82
+ ensure
83
+ Thread.current[:docyard_build_renderer] = nil
84
+ end
85
+ end
86
+
87
+ def generate_pages_sequentially(markdown_files, progress)
88
+ renderer = build_renderer
89
+ markdown_files.each do |file_path|
90
+ generate_page(file_path, renderer)
91
+ progress.advance
92
+ end
93
+ end
94
+
95
+ def thread_local_renderer
96
+ Thread.current[:docyard_build_renderer] ||= build_renderer
97
+ end
98
+
99
+ def build_renderer
100
+ Renderer.new(base_url: config.build.base, config: config)
101
+ end
102
+
103
+ def generate_page(markdown_file_path, renderer)
104
+ relative_path = markdown_file_path.delete_prefix("#{docs_path}/")
105
+ output_path = Utils::PathUtils.markdown_to_html_output(relative_path, config.build.output)
106
+ current_path = Utils::PathUtils.markdown_file_to_url(markdown_file_path, docs_path)
61
107
 
62
- html_content = render_markdown_file(markdown_file_path, current_path)
108
+ html_content = render_markdown_file(markdown_file_path, current_path, renderer)
63
109
  html_content = apply_search_exclusion(html_content, current_path)
64
110
  write_output(output_path, html_content)
65
111
  end
@@ -79,10 +125,10 @@ module Docyard
79
125
  end
80
126
  end
81
127
 
82
- def render_markdown_file(markdown_file_path, current_path)
128
+ def render_markdown_file(markdown_file_path, current_path, renderer)
83
129
  markdown = Markdown.new(File.read(markdown_file_path))
84
130
  template_resolver = TemplateResolver.new(markdown.frontmatter, config.data)
85
- branding = branding_options
131
+ branding = BrandingResolver.new(config).resolve
86
132
 
87
133
  navigation = build_navigation_html(template_resolver, current_path, markdown, branding[:header_ctas])
88
134
  renderer.render_file(markdown_file_path, **navigation, branding: branding,
@@ -91,82 +137,72 @@ module Docyard
91
137
  end
92
138
 
93
139
  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
- }
140
+ navigation_builder.build(
141
+ current_path: current_path,
142
+ markdown: markdown,
143
+ header_ctas: header_ctas,
144
+ show_sidebar: template_resolver.show_sidebar?
145
+ )
102
146
  end
103
147
 
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
148
+ def navigation_builder
149
+ @navigation_builder ||= Navigation::PageNavigationBuilder.new(
150
+ docs_path: docs_path,
151
+ config: config,
152
+ sidebar_cache: sidebar_cache
153
+ )
108
154
  end
109
155
 
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")
156
+ def write_output(output_path, html_content)
157
+ safe_file_write(output_path) do
158
+ FileUtils.mkdir_p(File.dirname(output_path))
159
+ File.write(output_path, html_content)
121
160
  end
161
+ log "Generated: #{output_path}" if verbose
122
162
  end
123
163
 
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}"
133
- end
164
+ def safe_file_write(path, &block)
165
+ block.call
166
+ rescue Errno::EACCES => e
167
+ raise BuildError, "Permission denied writing to #{path}: #{e.message}"
168
+ rescue Errno::ENOSPC => e
169
+ raise BuildError, "Disk full, cannot write to #{path}: #{e.message}"
170
+ rescue Errno::EROFS => e
171
+ raise BuildError, "Read-only filesystem, cannot write to #{path}: #{e.message}"
172
+ rescue SystemCallError => e
173
+ raise BuildError, "Failed to write #{path}: #{e.message}"
134
174
  end
135
175
 
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
176
+ def build_sidebar_cache
177
+ @sidebar_cache = Sidebar::Cache.new(
178
+ docs_path: docs_path,
179
+ config: config
142
180
  )
181
+ @sidebar_cache.build
143
182
  end
144
183
 
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
184
+ def show_last_updated?
185
+ config.repo.url && config.repo.last_updated != false
152
186
  end
153
187
 
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)
188
+ def docs_path
189
+ config.source
158
190
  end
159
191
 
160
- def breadcrumbs_enabled?
161
- config.navigation.breadcrumbs != false
192
+ def log(message)
193
+ Docyard.logger.info(message) if verbose
162
194
  end
163
195
 
164
- def branding_options
165
- BrandingResolver.new(config).resolve
196
+ def generate_error_page
197
+ output_path = File.join(config.build.output, "404.html")
198
+ html_content = load_error_page_content
199
+ safe_file_write(output_path) { File.write(output_path, html_content) }
200
+ log "[✓] Generated 404.html"
166
201
  end
167
202
 
168
- def log(message)
169
- puts message if verbose
203
+ def load_error_page_content
204
+ error_page = File.join(docs_path, "404.html")
205
+ File.exist?(error_page) ? File.read(error_page) : build_renderer.render_not_found
170
206
  end
171
207
  end
172
208
  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
@@ -2,17 +2,29 @@
2
2
 
3
3
  module Docyard
4
4
  module Components
5
+ AbbreviationProcessor = Processors::AbbreviationProcessor
6
+ AccordionProcessor = Processors::AccordionProcessor
7
+ BadgeProcessor = Processors::BadgeProcessor
8
+ StepsProcessor = Processors::StepsProcessor
9
+ CardsProcessor = Processors::CardsProcessor
5
10
  CalloutProcessor = Processors::CalloutProcessor
6
11
  CodeBlockProcessor = Processors::CodeBlockProcessor
12
+ CodeGroupProcessor = Processors::CodeGroupProcessor
7
13
  CodeBlockDiffPreprocessor = Processors::CodeBlockDiffPreprocessor
8
14
  CodeBlockFocusPreprocessor = Processors::CodeBlockFocusPreprocessor
9
15
  CodeBlockOptionsPreprocessor = Processors::CodeBlockOptionsPreprocessor
10
16
  CodeSnippetImportPreprocessor = Processors::CodeSnippetImportPreprocessor
17
+ CustomAnchorProcessor = Processors::CustomAnchorProcessor
18
+ ImageCaptionProcessor = Processors::ImageCaptionProcessor
19
+ IncludeProcessor = Processors::IncludeProcessor
20
+ VideoEmbedProcessor = Processors::VideoEmbedProcessor
21
+ FileTreeProcessor = Processors::FileTreeProcessor
11
22
  HeadingAnchorProcessor = Processors::HeadingAnchorProcessor
12
23
  IconProcessor = Processors::IconProcessor
13
24
  TableOfContentsProcessor = Processors::TableOfContentsProcessor
14
25
  TableWrapperProcessor = Processors::TableWrapperProcessor
15
26
  TabsProcessor = Processors::TabsProcessor
27
+ TooltipProcessor = Processors::TooltipProcessor
16
28
 
17
29
  CodeDetector = Support::CodeDetector
18
30
  IconDetector = Support::Tabs::IconDetector
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_processor"
4
+ require_relative "../support/markdown_code_block_helper"
5
+
6
+ module Docyard
7
+ module Components
8
+ module Processors
9
+ class AbbreviationProcessor < BaseProcessor
10
+ include Support::MarkdownCodeBlockHelper
11
+
12
+ DEFINITION_PATTERN = /^\*\[([^\]]+)\]:\s*(.+)$/
13
+ self.priority = 5
14
+
15
+ def preprocess(content)
16
+ abbreviations = extract_abbreviations_outside_code_blocks(content)
17
+ return content if abbreviations.empty?
18
+
19
+ process_outside_code_blocks(content) do |segment|
20
+ segment = remove_definitions(segment)
21
+ apply_abbreviations(segment, abbreviations)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def extract_abbreviations_outside_code_blocks(content)
28
+ abbreviations = {}
29
+ process_outside_code_blocks(content) do |segment|
30
+ segment.scan(DEFINITION_PATTERN) do |term, definition|
31
+ abbreviations[term] = definition.strip
32
+ end
33
+ segment
34
+ end
35
+ abbreviations
36
+ end
37
+
38
+ def remove_definitions(content)
39
+ content.gsub(/^[ \t]*\*\[([^\]]+)\]:\s*.+$\n?/, "")
40
+ end
41
+
42
+ def apply_abbreviations(content, abbreviations)
43
+ abbreviations.each do |term, definition|
44
+ pattern = build_term_pattern(term)
45
+ content = content.gsub(pattern) do |match|
46
+ build_abbr_tag(match, definition)
47
+ end
48
+ end
49
+ content
50
+ end
51
+
52
+ def build_term_pattern(term)
53
+ escaped = Regexp.escape(term)
54
+ /(?<![<\w])#{escaped}(?![>\w])/
55
+ end
56
+
57
+ def build_abbr_tag(term, definition)
58
+ escaped_definition = escape_html(definition)
59
+ %(<abbr class="docyard-abbr" data-definition="#{escaped_definition}">#{term}</abbr>)
60
+ end
61
+
62
+ def escape_html(text)
63
+ text.to_s
64
+ .gsub("&", "&amp;")
65
+ .gsub("<", "&lt;")
66
+ .gsub(">", "&gt;")
67
+ .gsub('"', "&quot;")
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../rendering/icons"
4
+ require_relative "../../rendering/renderer"
5
+ require_relative "../base_processor"
6
+ require_relative "../support/markdown_code_block_helper"
7
+ require "kramdown"
8
+ require "kramdown-parser-gfm"
9
+
10
+ module Docyard
11
+ module Components
12
+ module Processors
13
+ class AccordionProcessor < BaseProcessor
14
+ include Support::MarkdownCodeBlockHelper
15
+
16
+ self.priority = 10
17
+
18
+ DETAILS_PATTERN = /^:::details(?:\{([^}]*)\})?\s*\n(.*?)^:::\s*$/m
19
+
20
+ def preprocess(markdown)
21
+ @code_block_ranges = find_code_block_ranges(markdown)
22
+
23
+ markdown.gsub(DETAILS_PATTERN) do
24
+ match = Regexp.last_match
25
+ next match[0] if inside_code_block?(match.begin(0), @code_block_ranges)
26
+
27
+ attributes = parse_attributes(match[1])
28
+ content_markdown = match[2]
29
+
30
+ title = attributes["title"] || "Details"
31
+ open = attributes.key?("open")
32
+ content_html = render_markdown_content(content_markdown.strip)
33
+
34
+ wrap_in_nomarkdown(render_accordion_html(title, content_html, open))
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def parse_attributes(attr_string)
41
+ return {} if attr_string.nil? || attr_string.empty?
42
+
43
+ attrs = {}
44
+ attr_string.scan(/(\w+)(?:="([^"]*)")?/) do |key, value|
45
+ attrs[key] = value || true
46
+ end
47
+ attrs
48
+ end
49
+
50
+ def render_markdown_content(content_markdown)
51
+ return "" if content_markdown.empty?
52
+
53
+ Kramdown::Document.new(
54
+ content_markdown,
55
+ input: "GFM",
56
+ hard_wrap: false,
57
+ syntax_highlighter: "rouge"
58
+ ).to_html
59
+ end
60
+
61
+ def wrap_in_nomarkdown(html)
62
+ "{::nomarkdown}\n#{html}\n{:/nomarkdown}"
63
+ end
64
+
65
+ def render_accordion_html(title, content_html, open)
66
+ icon_svg = Icons.render("caret-right") || ""
67
+ renderer = Renderer.new
68
+
69
+ renderer.render_partial(
70
+ "_accordion", {
71
+ title: title,
72
+ content_html: content_html,
73
+ icon_svg: icon_svg,
74
+ open: open
75
+ }
76
+ )
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_processor"
4
+
5
+ module Docyard
6
+ module Components
7
+ module Processors
8
+ class BadgeProcessor < BaseProcessor
9
+ self.priority = 15
10
+
11
+ BADGE_PATTERN = /:badge\[([^\]]*)\](?:\{([^}]*)\})?/
12
+
13
+ VALID_TYPES = %w[default success warning danger].freeze
14
+
15
+ def postprocess(html)
16
+ segments = split_preserving_code_blocks(html)
17
+
18
+ segments.map do |segment|
19
+ segment[:type] == :code ? segment[:content] : process_segment(segment[:content])
20
+ end.join
21
+ end
22
+
23
+ private
24
+
25
+ def split_preserving_code_blocks(html)
26
+ segments = []
27
+ current_pos = 0
28
+
29
+ html.scan(%r{<(code|pre)[^>]*>.*?</\1>}m) do
30
+ match_start = Regexp.last_match.begin(0)
31
+ match_end = Regexp.last_match.end(0)
32
+
33
+ segments << { type: :text, content: html[current_pos...match_start] } if match_start > current_pos
34
+ segments << { type: :code, content: html[match_start...match_end] }
35
+
36
+ current_pos = match_end
37
+ end
38
+
39
+ segments << { type: :text, content: html[current_pos..] } if current_pos < html.length
40
+
41
+ segments.empty? ? [{ type: :text, content: html }] : segments
42
+ end
43
+
44
+ def process_segment(content)
45
+ content.gsub(BADGE_PATTERN) do
46
+ text = Regexp.last_match(1)
47
+ attrs = Regexp.last_match(2)
48
+
49
+ render_badge(text, parse_attributes(attrs))
50
+ end
51
+ end
52
+
53
+ def parse_attributes(attrs_string)
54
+ return {} if attrs_string.nil? || attrs_string.empty?
55
+
56
+ attrs = {}
57
+ attrs_string.scan(/(\w+)=["'\u201C\u201D]([^"'\u201C\u201D]*)["'\u201C\u201D]/) do |key, value|
58
+ attrs[key] = value
59
+ end
60
+ attrs
61
+ end
62
+
63
+ def render_badge(text, attrs)
64
+ type = attrs["type"] || "default"
65
+ type = "default" unless VALID_TYPES.include?(type)
66
+
67
+ %(<span class="docyard-badge docyard-badge--#{type}">#{text}</span>)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -3,6 +3,7 @@
3
3
  require_relative "../../rendering/icons"
4
4
  require_relative "../../rendering/renderer"
5
5
  require_relative "../base_processor"
6
+ require_relative "../support/markdown_code_block_helper"
6
7
  require "kramdown"
7
8
  require "kramdown-parser-gfm"
8
9
 
@@ -10,6 +11,8 @@ module Docyard
10
11
  module Components
11
12
  module Processors
12
13
  class CalloutProcessor < BaseProcessor
14
+ include Support::MarkdownCodeBlockHelper
15
+
13
16
  self.priority = 10
14
17
 
15
18
  CALLOUT_TYPES = {
@@ -29,6 +32,7 @@ module Docyard
29
32
  }.freeze
30
33
 
31
34
  def preprocess(markdown)
35
+ @code_block_ranges = find_code_block_ranges(markdown)
32
36
  process_container_syntax(markdown)
33
37
  end
34
38
 
@@ -40,8 +44,10 @@ module Docyard
40
44
 
41
45
  def process_container_syntax(markdown)
42
46
  markdown.gsub(/^:::[ \t]*(\w+)(?:[ \t]+([^\n]+?))?[ \t]*\n(.*?)^:::[ \t]*$/m) do
43
- process_callout_match(Regexp.last_match(0), Regexp.last_match(1), Regexp.last_match(2),
44
- Regexp.last_match(3))
47
+ match = Regexp.last_match
48
+ next match[0] if inside_code_block?(match.begin(0), @code_block_ranges)
49
+
50
+ process_callout_match(match[0], match[1], match[2], match[3])
45
51
  end
46
52
  end
47
53
 
@@ -106,7 +112,7 @@ module Docyard
106
112
  end
107
113
 
108
114
  def render_callout_html(type, title, content_html, icon_name)
109
- icon_svg = Icons.render(icon_name, "duotone") || ""
115
+ icon_svg = Icons.render(icon_name, "regular") || ""
110
116
  renderer = Renderer.new
111
117
 
112
118
  renderer.render_partial(