docyard 0.9.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -0
  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/processors/callout_processor.rb +1 -1
  13. data/lib/docyard/components/processors/code_block_extended_fence_postprocessor.rb +24 -0
  14. data/lib/docyard/components/processors/code_block_extended_fence_preprocessor.rb +44 -0
  15. data/lib/docyard/components/processors/code_block_options_preprocessor.rb +11 -1
  16. data/lib/docyard/components/processors/code_block_processor.rb +5 -24
  17. data/lib/docyard/components/processors/code_group_processor.rb +6 -22
  18. data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +1 -0
  19. data/lib/docyard/components/processors/file_tree_processor.rb +1 -2
  20. data/lib/docyard/components/processors/icon_processor.rb +8 -2
  21. data/lib/docyard/components/processors/include_processor.rb +10 -10
  22. data/lib/docyard/components/processors/video_embed_processor.rb +14 -3
  23. data/lib/docyard/components/support/code_block/feature_extractor.rb +3 -1
  24. data/lib/docyard/components/support/code_block/icon_detector.rb +5 -12
  25. data/lib/docyard/components/support/code_block/line_number_resolver.rb +30 -0
  26. data/lib/docyard/components/support/code_detector.rb +2 -12
  27. data/lib/docyard/components/support/code_group/html_builder.rb +2 -6
  28. data/lib/docyard/components/support/tabs/icon_detector.rb +6 -2
  29. data/lib/docyard/components/support/tabs/parser.rb +6 -23
  30. data/lib/docyard/config/analytics_resolver.rb +24 -0
  31. data/lib/docyard/config/branding_resolver.rb +58 -27
  32. data/lib/docyard/config/key_validator.rb +30 -0
  33. data/lib/docyard/config/logo_detector.rb +8 -8
  34. data/lib/docyard/config/schema.rb +39 -0
  35. data/lib/docyard/config/section.rb +21 -0
  36. data/lib/docyard/config/validation_helpers.rb +83 -0
  37. data/lib/docyard/config/validator.rb +45 -144
  38. data/lib/docyard/config/validators/navigation.rb +43 -0
  39. data/lib/docyard/config/validators/section.rb +114 -0
  40. data/lib/docyard/config.rb +46 -102
  41. data/lib/docyard/constants.rb +59 -0
  42. data/lib/docyard/{utils/errors.rb → errors.rb} +6 -0
  43. data/lib/docyard/initializer.rb +100 -49
  44. data/lib/docyard/navigation/page_navigation_builder.rb +65 -0
  45. data/lib/docyard/navigation/sidebar/auto_builder.rb +107 -0
  46. data/lib/docyard/navigation/sidebar/cache.rb +96 -0
  47. data/lib/docyard/navigation/sidebar/config_builder.rb +179 -0
  48. data/lib/docyard/navigation/sidebar/distributed_builder.rb +145 -0
  49. data/lib/docyard/navigation/sidebar/local_config_loader.rb +69 -3
  50. data/lib/docyard/navigation/sidebar/renderer.rb +12 -1
  51. data/lib/docyard/navigation/sidebar_builder.rb +43 -81
  52. data/lib/docyard/rendering/branding_variables.rb +65 -0
  53. data/lib/docyard/rendering/icon_helpers.rb +14 -1
  54. data/lib/docyard/rendering/icons/devicons.rb +63 -0
  55. data/lib/docyard/rendering/icons.rb +26 -27
  56. data/lib/docyard/rendering/markdown.rb +5 -23
  57. data/lib/docyard/rendering/og_helpers.rb +36 -0
  58. data/lib/docyard/rendering/renderer.rb +87 -59
  59. data/lib/docyard/rendering/template_resolver.rb +14 -0
  60. data/lib/docyard/routing/fallback_resolver.rb +3 -3
  61. data/lib/docyard/search/build_indexer.rb +2 -2
  62. data/lib/docyard/search/dev_indexer.rb +36 -28
  63. data/lib/docyard/search/pagefind_support.rb +1 -1
  64. data/lib/docyard/server/asset_handler.rb +39 -15
  65. data/lib/docyard/server/dev_server.rb +90 -55
  66. data/lib/docyard/server/file_watcher.rb +68 -18
  67. data/lib/docyard/server/pagefind_handler.rb +1 -1
  68. data/lib/docyard/server/preview_server.rb +29 -33
  69. data/lib/docyard/server/rack_application.rb +38 -70
  70. data/lib/docyard/server/router.rb +11 -7
  71. data/lib/docyard/server/sse_server.rb +157 -0
  72. data/lib/docyard/server/static_file_app.rb +42 -0
  73. data/lib/docyard/templates/assets/css/components/banner.css +31 -0
  74. data/lib/docyard/templates/assets/css/components/breadcrumbs.css +2 -1
  75. data/lib/docyard/templates/assets/css/components/callout.css +26 -6
  76. data/lib/docyard/templates/assets/css/components/code-block.css +4 -2
  77. data/lib/docyard/templates/assets/css/components/code-group.css +20 -7
  78. data/lib/docyard/templates/assets/css/components/feedback.css +126 -0
  79. data/lib/docyard/templates/assets/css/components/file-tree.css +5 -4
  80. data/lib/docyard/templates/assets/css/components/icon.css +5 -0
  81. data/lib/docyard/templates/assets/css/components/nav-menu.css +20 -4
  82. data/lib/docyard/templates/assets/css/components/navigation.css +25 -3
  83. data/lib/docyard/templates/assets/css/components/page-actions.css +131 -0
  84. data/lib/docyard/templates/assets/css/components/prev-next.css +14 -7
  85. data/lib/docyard/templates/assets/css/components/search.css +6 -10
  86. data/lib/docyard/templates/assets/css/components/tab-bar.css +7 -4
  87. data/lib/docyard/templates/assets/css/components/table-of-contents.css +57 -11
  88. data/lib/docyard/templates/assets/css/components/tabs.css +12 -4
  89. data/lib/docyard/templates/assets/css/components/theme-toggle.css +3 -1
  90. data/lib/docyard/templates/assets/css/landing.css +82 -13
  91. data/lib/docyard/templates/assets/css/layout.css +17 -0
  92. data/lib/docyard/templates/assets/css/markdown.css +22 -2
  93. data/lib/docyard/templates/assets/css/variables.css +13 -1
  94. data/lib/docyard/templates/assets/js/components/code-group.js +4 -1
  95. data/lib/docyard/templates/assets/js/components/copy-page.js +115 -0
  96. data/lib/docyard/templates/assets/js/components/feedback.js +66 -0
  97. data/lib/docyard/templates/assets/js/components/file-tree.js +5 -5
  98. data/lib/docyard/templates/assets/js/components/navigation.js +3 -3
  99. data/lib/docyard/templates/assets/js/components/search.js +3 -3
  100. data/lib/docyard/templates/assets/js/components/table-of-contents.js +12 -6
  101. data/lib/docyard/templates/assets/js/components/tabs.js +45 -22
  102. data/lib/docyard/templates/assets/js/components/tooltip.js +4 -4
  103. data/lib/docyard/templates/assets/js/hot-reload.js +44 -0
  104. data/lib/docyard/templates/errors/404.html.erb +114 -5
  105. data/lib/docyard/templates/errors/500.html.erb +173 -10
  106. data/lib/docyard/templates/init/_sidebar.yml +36 -0
  107. data/lib/docyard/templates/init/docyard.yml +36 -0
  108. data/lib/docyard/templates/init/pages/components.md +146 -0
  109. data/lib/docyard/templates/init/pages/getting-started.md +94 -0
  110. data/lib/docyard/templates/init/pages/index.md +22 -0
  111. data/lib/docyard/templates/layouts/default.html.erb +10 -0
  112. data/lib/docyard/templates/layouts/splash.html.erb +14 -1
  113. data/lib/docyard/templates/partials/_analytics.html.erb +24 -0
  114. data/lib/docyard/templates/partials/_banner.html.erb +1 -1
  115. data/lib/docyard/templates/partials/_code_block.html.erb +1 -1
  116. data/lib/docyard/templates/partials/_feedback.html.erb +14 -0
  117. data/lib/docyard/templates/partials/_footer.html.erb +1 -1
  118. data/lib/docyard/templates/partials/_head.html.erb +79 -4
  119. data/lib/docyard/templates/partials/_icon_library.html.erb +8 -0
  120. data/lib/docyard/templates/partials/_page_actions.html.erb +21 -0
  121. data/lib/docyard/templates/partials/_scripts.html.erb +6 -3
  122. data/lib/docyard/templates/partials/_tabs.html.erb +4 -1
  123. data/lib/docyard/utils/git_info.rb +157 -0
  124. data/lib/docyard/utils/hash_utils.rb +31 -0
  125. data/lib/docyard/utils/html_helpers.rb +8 -0
  126. data/lib/docyard/utils/logging.rb +44 -3
  127. data/lib/docyard/utils/path_resolver.rb +0 -10
  128. data/lib/docyard/utils/path_utils.rb +73 -0
  129. data/lib/docyard/version.rb +1 -1
  130. data/lib/docyard.rb +2 -2
  131. metadata +77 -47
  132. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -31
  133. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  134. data/.github/pull_request_template.md +0 -14
  135. data/.github/workflows/ci.yml +0 -49
  136. data/.rubocop.yml +0 -42
  137. data/CODE_OF_CONDUCT.md +0 -132
  138. data/CONTRIBUTING.md +0 -55
  139. data/LICENSE.vscode-icons +0 -42
  140. data/Rakefile +0 -8
  141. data/lib/docyard/config/constants.rb +0 -31
  142. data/lib/docyard/navigation/sidebar/children_discoverer.rb +0 -51
  143. data/lib/docyard/navigation/sidebar/config_parser.rb +0 -208
  144. data/lib/docyard/navigation/sidebar/file_resolver.rb +0 -90
  145. data/lib/docyard/navigation/sidebar/file_system_scanner.rb +0 -78
  146. data/lib/docyard/navigation/sidebar/metadata_extractor.rb +0 -71
  147. data/lib/docyard/navigation/sidebar/metadata_reader.rb +0 -51
  148. data/lib/docyard/navigation/sidebar/path_prefixer.rb +0 -34
  149. data/lib/docyard/navigation/sidebar/sorter.rb +0 -21
  150. data/lib/docyard/navigation/sidebar/title_extractor.rb +0 -25
  151. data/lib/docyard/navigation/sidebar/tree_builder.rb +0 -140
  152. data/lib/docyard/rendering/icons/LICENSE.phosphor +0 -21
  153. data/lib/docyard/rendering/icons/file_types.rb +0 -79
  154. data/lib/docyard/rendering/icons/phosphor.rb +0 -93
  155. data/lib/docyard/rendering/language_mapping.rb +0 -52
  156. data/lib/docyard/templates/assets/js/reload.js +0 -98
  157. data/lib/docyard/templates/partials/_icon.html.erb +0 -1
  158. data/lib/docyard/templates/partials/_icon_file_extension.html.erb +0 -1
  159. 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
@@ -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
@@ -124,6 +124,7 @@ module Docyard
124
124
  end
125
125
 
126
126
  def import_error(filepath, message)
127
+ Docyard.logger.warn("Code snippet import failed: #{filepath} - #{message}")
127
128
  "```\nError importing #{filepath}: #{message}\n```"
128
129
  end
129
130
  end
@@ -2,7 +2,6 @@
2
2
 
3
3
  require_relative "../base_processor"
4
4
  require_relative "../support/markdown_code_block_helper"
5
- require_relative "../../rendering/icons"
6
5
 
7
6
  module Docyard
8
7
  module Components
@@ -135,7 +134,7 @@ module Docyard
135
134
 
136
135
  def icon_for(type)
137
136
  icon_name = type == :folder ? "folder-open" : "file-text"
138
- Icons.render(icon_name)
137
+ %(<i class="ph ph-#{icon_name}" aria-hidden="true"></i>)
139
138
  end
140
139
 
141
140
  def escape_html(text)
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../../rendering/icons"
4
3
  require_relative "../base_processor"
5
4
 
6
5
  module Docyard
@@ -10,6 +9,7 @@ module Docyard
10
9
  self.priority = 20
11
10
 
12
11
  ICON_PATTERN = /:([a-z][a-z0-9-]*):(?:([a-z]+):)?/i
12
+ VALID_WEIGHTS = %w[regular bold fill light thin duotone].freeze
13
13
 
14
14
  def postprocess(html)
15
15
  segments = split_preserving_code_blocks(html)
@@ -44,9 +44,15 @@ module Docyard
44
44
  content.gsub(ICON_PATTERN) do
45
45
  icon_name = Regexp.last_match(1)
46
46
  weight = Regexp.last_match(2) || "regular"
47
- Icons.render(icon_name, weight) || Regexp.last_match(0)
47
+ render_icon(icon_name, weight)
48
48
  end
49
49
  end
50
+
51
+ def render_icon(name, weight)
52
+ weight = "regular" unless VALID_WEIGHTS.include?(weight)
53
+ weight_class = weight == "regular" ? "ph" : "ph-#{weight}"
54
+ %(<i class="#{weight_class} ph-#{name}" aria-hidden="true"></i>)
55
+ end
50
56
  end
51
57
  end
52
58
  end