docyard 0.5.0 → 0.7.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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/CHANGELOG.md +34 -1
  4. data/lib/docyard/build/static_generator.rb +3 -44
  5. data/lib/docyard/builder.rb +14 -4
  6. data/lib/docyard/cli.rb +6 -3
  7. data/lib/docyard/components/aliases.rb +29 -0
  8. data/lib/docyard/components/base_processor.rb +6 -0
  9. data/lib/docyard/components/processors/callout_processor.rb +124 -0
  10. data/lib/docyard/components/processors/code_block_diff_preprocessor.rb +106 -0
  11. data/lib/docyard/components/processors/code_block_focus_preprocessor.rb +79 -0
  12. data/lib/docyard/components/processors/code_block_options_preprocessor.rb +78 -0
  13. data/lib/docyard/components/processors/code_block_processor.rb +175 -0
  14. data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +127 -0
  15. data/lib/docyard/components/processors/heading_anchor_processor.rb +39 -0
  16. data/lib/docyard/components/processors/icon_processor.rb +53 -0
  17. data/lib/docyard/components/processors/table_of_contents_processor.rb +68 -0
  18. data/lib/docyard/components/processors/table_wrapper_processor.rb +22 -0
  19. data/lib/docyard/components/processors/tabs_processor.rb +48 -0
  20. data/lib/docyard/components/registry.rb +4 -4
  21. data/lib/docyard/components/support/code_block/feature_extractor.rb +117 -0
  22. data/lib/docyard/components/support/code_block/icon_detector.rb +44 -0
  23. data/lib/docyard/components/support/code_block/line_parser.rb +84 -0
  24. data/lib/docyard/components/support/code_block/line_wrapper.rb +50 -0
  25. data/lib/docyard/components/support/code_block/patterns.rb +55 -0
  26. data/lib/docyard/components/support/code_detector.rb +61 -0
  27. data/lib/docyard/components/support/tabs/icon_detector.rb +62 -0
  28. data/lib/docyard/components/support/tabs/parser.rb +195 -0
  29. data/lib/docyard/components/support/tabs/range_finder.rb +46 -0
  30. data/lib/docyard/config/branding_resolver.rb +74 -0
  31. data/lib/docyard/{constants.rb → config/constants.rb} +1 -0
  32. data/lib/docyard/config/validator.rb +8 -0
  33. data/lib/docyard/config.rb +17 -1
  34. data/lib/docyard/{prev_next_builder.rb → navigation/prev_next_builder.rb} +2 -2
  35. data/lib/docyard/{sidebar → navigation/sidebar}/renderer.rb +3 -14
  36. data/lib/docyard/{sidebar → navigation/sidebar}/tree_builder.rb +9 -2
  37. data/lib/docyard/{sidebar_builder.rb → navigation/sidebar_builder.rb} +3 -15
  38. data/lib/docyard/{icons → rendering/icons}/file_types.rb +0 -13
  39. data/lib/docyard/{icons → rendering/icons}/phosphor.rb +4 -1
  40. data/lib/docyard/{markdown.rb → rendering/markdown.rb} +23 -14
  41. data/lib/docyard/{renderer.rb → rendering/renderer.rb} +24 -20
  42. data/lib/docyard/search/build_indexer.rb +74 -0
  43. data/lib/docyard/search/dev_indexer.rb +110 -0
  44. data/lib/docyard/search/pagefind_support.rb +31 -0
  45. data/lib/docyard/{asset_handler.rb → server/asset_handler.rb} +1 -1
  46. data/lib/docyard/{server.rb → server/dev_server.rb} +32 -9
  47. data/lib/docyard/{preview_server.rb → server/preview_server.rb} +1 -1
  48. data/lib/docyard/{rack_application.rb → server/rack_application.rb} +53 -50
  49. data/lib/docyard/server/resolution_result.rb +29 -0
  50. data/lib/docyard/{router.rb → server/router.rb} +4 -4
  51. data/lib/docyard/templates/assets/css/code.css +12 -4
  52. data/lib/docyard/templates/assets/css/components/code-block.css +427 -24
  53. data/lib/docyard/templates/assets/css/components/navigation.css +12 -9
  54. data/lib/docyard/templates/assets/css/components/search.css +549 -0
  55. data/lib/docyard/templates/assets/css/components/tabs.css +50 -44
  56. data/lib/docyard/templates/assets/css/layout.css +15 -1
  57. data/lib/docyard/templates/assets/css/variables.css +44 -0
  58. data/lib/docyard/templates/assets/js/components/search.js +685 -0
  59. data/lib/docyard/templates/layouts/default.html.erb +14 -2
  60. data/lib/docyard/templates/partials/_code_block.html.erb +50 -2
  61. data/lib/docyard/templates/partials/_heading_anchor.html.erb +1 -1
  62. data/lib/docyard/templates/partials/_prev_next.html.erb +1 -1
  63. data/lib/docyard/templates/partials/_search_modal.html.erb +45 -0
  64. data/lib/docyard/templates/partials/_search_trigger.html.erb +22 -0
  65. data/lib/docyard/utils/html_helpers.rb +14 -0
  66. data/lib/docyard/utils/path_resolver.rb +2 -1
  67. data/lib/docyard/utils/url_helpers.rb +20 -0
  68. data/lib/docyard/version.rb +1 -1
  69. data/lib/docyard.rb +22 -15
  70. metadata +57 -36
  71. data/lib/docyard/components/callout_processor.rb +0 -121
  72. data/lib/docyard/components/code_block_processor.rb +0 -55
  73. data/lib/docyard/components/code_detector.rb +0 -59
  74. data/lib/docyard/components/heading_anchor_processor.rb +0 -34
  75. data/lib/docyard/components/icon_detector.rb +0 -57
  76. data/lib/docyard/components/icon_processor.rb +0 -51
  77. data/lib/docyard/components/table_of_contents_processor.rb +0 -64
  78. data/lib/docyard/components/table_wrapper_processor.rb +0 -18
  79. data/lib/docyard/components/tabs_parser.rb +0 -60
  80. data/lib/docyard/components/tabs_processor.rb +0 -44
  81. data/lib/docyard/routing/resolution_result.rb +0 -31
  82. /data/lib/docyard/{sidebar → navigation/sidebar}/config_parser.rb +0 -0
  83. /data/lib/docyard/{sidebar → navigation/sidebar}/file_system_scanner.rb +0 -0
  84. /data/lib/docyard/{sidebar → navigation/sidebar}/item.rb +0 -0
  85. /data/lib/docyard/{sidebar → navigation/sidebar}/title_extractor.rb +0 -0
  86. /data/lib/docyard/{icons → rendering/icons}/LICENSE.phosphor +0 -0
  87. /data/lib/docyard/{icons.rb → rendering/icons.rb} +0 -0
  88. /data/lib/docyard/{language_mapping.rb → rendering/language_mapping.rb} +0 -0
  89. /data/lib/docyard/{file_watcher.rb → server/file_watcher.rb} +0 -0
  90. /data/lib/docyard/{errors.rb → utils/errors.rb} +0 -0
  91. /data/lib/docyard/{logging.rb → utils/logging.rb} +0 -0
@@ -12,6 +12,7 @@ module Docyard
12
12
  validate_site_section
13
13
  validate_branding_section
14
14
  validate_build_section
15
+ validate_markdown_section
15
16
 
16
17
  raise ConfigError, format_errors if @errors.any?
17
18
  end
@@ -48,6 +49,13 @@ module Docyard
48
49
  validate_boolean(build["clean"], "build.clean")
49
50
  end
50
51
 
52
+ def validate_markdown_section
53
+ markdown = @config["markdown"]
54
+ return unless markdown
55
+
56
+ validate_boolean(markdown["lineNumbers"], "markdown.lineNumbers") if markdown.key?("lineNumbers")
57
+ end
58
+
51
59
  def validate_string(value, field_name)
52
60
  return if value.nil?
53
61
  return if value.is_a?(String)
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "yaml"
4
4
  require_relative "config/validator"
5
- require_relative "constants"
5
+ require_relative "config/constants"
6
6
 
7
7
  module Docyard
8
8
  class Config
@@ -34,6 +34,14 @@ module Docyard
34
34
  "prev_text" => "Previous",
35
35
  "next_text" => "Next"
36
36
  }
37
+ },
38
+ "markdown" => {
39
+ "lineNumbers" => false
40
+ },
41
+ "search" => {
42
+ "enabled" => true,
43
+ "placeholder" => "Search documentation...",
44
+ "exclude" => []
37
45
  }
38
46
  }.freeze
39
47
 
@@ -74,6 +82,14 @@ module Docyard
74
82
  @navigation ||= ConfigSection.new(data["navigation"])
75
83
  end
76
84
 
85
+ def markdown
86
+ @markdown ||= ConfigSection.new(data["markdown"])
87
+ end
88
+
89
+ def search
90
+ @search ||= ConfigSection.new(data["search"])
91
+ end
92
+
77
93
  private
78
94
 
79
95
  def load_config_data
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "renderer"
4
- require_relative "utils/path_resolver"
3
+ require_relative "../rendering/renderer"
4
+ require_relative "../utils/path_resolver"
5
5
 
6
6
  module Docyard
7
7
  class PrevNextBuilder
@@ -5,7 +5,9 @@ require "erb"
5
5
  module Docyard
6
6
  module Sidebar
7
7
  class Renderer
8
- PARTIALS_PATH = File.join(__dir__, "../templates/partials")
8
+ include Utils::UrlHelpers
9
+
10
+ PARTIALS_PATH = File.join(__dir__, "../../templates/partials")
9
11
 
10
12
  attr_reader :site_title, :base_url
11
13
 
@@ -39,19 +41,6 @@ module Docyard
39
41
  Icons.render(name.to_s.tr("_", "-"), weight) || ""
40
42
  end
41
43
 
42
- def link_path(path)
43
- return path if path.nil? || path.start_with?("http://", "https://")
44
-
45
- "#{base_url.chomp('/')}#{path}"
46
- end
47
-
48
- def normalize_base_url(url)
49
- return "/" if url.nil? || url.empty?
50
-
51
- url = "/#{url}" unless url.start_with?("/")
52
- url.end_with?("/") ? url : "#{url}/"
53
- end
54
-
55
44
  def render_tree_with_sections(items)
56
45
  filtered_items = items.reject { |item| item[:title]&.downcase == site_title.downcase }
57
46
  grouped_items = group_by_section(filtered_items)
@@ -29,6 +29,7 @@ module Docyard
29
29
 
30
30
  def transform_directory(item, relative_base)
31
31
  dir_path = File.join(relative_base, item[:name])
32
+ children = transform_items(item[:children], dir_path)
32
33
 
33
34
  {
34
35
  title: Utils::TextFormatter.titleize(item[:name]),
@@ -36,11 +37,17 @@ module Docyard
36
37
  active: false,
37
38
  type: :directory,
38
39
  collapsible: true,
39
- collapsed: false,
40
- children: transform_items(item[:children], dir_path)
40
+ collapsed: !active_child?(children),
41
+ children: children
41
42
  }
42
43
  end
43
44
 
45
+ def active_child?(children)
46
+ children.any? do |child|
47
+ child[:active] || active_child?(child[:children] || [])
48
+ end
49
+ end
50
+
44
51
  def transform_file(item, relative_base)
45
52
  file_path = File.join(relative_base, "#{item[:name]}#{Constants::MARKDOWN_EXTENSION}")
46
53
  full_file_path = File.join(docs_path, file_path)
@@ -50,11 +50,7 @@ module Docyard
50
50
  def config_sidebar_items
51
51
  return [] unless config
52
52
 
53
- if config.is_a?(Hash)
54
- config.dig("sidebar", "items") || config.dig(:sidebar, :items) || []
55
- else
56
- config.sidebar&.items || []
57
- end
53
+ config.sidebar&.items || []
58
54
  end
59
55
 
60
56
  def config_parser
@@ -84,19 +80,11 @@ module Docyard
84
80
  end
85
81
 
86
82
  def extract_base_url
87
- if config.is_a?(Hash)
88
- config.dig(:build, :base_url) || "/"
89
- else
90
- config&.build&.base_url || "/"
91
- end
83
+ config&.build&.base_url || "/"
92
84
  end
93
85
 
94
86
  def extract_site_title
95
- if config.is_a?(Hash)
96
- config[:site_title] || "Documentation"
97
- else
98
- config&.site&.title || "Documentation"
99
- end
87
+ config&.site&.title || "Documentation"
100
88
  end
101
89
  end
102
90
  end
@@ -8,7 +8,6 @@ module Docyard
8
8
  # License: CC BY-SA 4.0 (see LICENSE.vscode-icons)
9
9
  # Copyright (c) 2016 Roberto Huertas
10
10
  module FileTypes
11
- # SVG content for each file type icon
12
11
  # rubocop:disable Layout/LineLength
13
12
  ICONS = {
14
13
  "css" => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" aria-labelledby="css-logo-title css-logo-description"><title>CSS Logo</title><g style="display:inline"><path fill="#639" d="M1.995 1.994h23.52a4.48 4.48 0 0 1 4.48 4.48v19.04a4.48 4.48 0 0 1-4.48 4.48H6.475a4.48 4.48 0 0 1-4.48-4.48Z"/><path fill="#fff" d="M9.079 24.87v-4.704c0-1.876 1.204-2.884 3.024-2.884 1.792-.028 2.912 1.148 2.856 3.136h-2.072c.056-.756-.28-1.316-.84-1.288-.7 0-.896.476-.896 1.372v4.088c0 .868.28 1.288.896 1.316.644 0 .896-.644.84-1.372h2.072c.112 2.044-1.176 3.248-2.996 3.22-1.764 0-2.884-.98-2.884-2.884zm6.636-.336h1.932c.028.896.308 1.456.924 1.456.616 0 .84-.364.84-1.204 0-.7-.308-1.092-1.064-1.456l-.728-.336c-1.288-.616-1.82-1.372-1.82-2.884 0-1.68 1.064-2.856 2.8-2.856 1.736 0 2.66 1.204 2.688 3.164h-1.876c0-.812-.168-1.372-.784-1.372-.56 0-.84.28-.84.98s.252.98.924 1.26l.672.308c1.428.672 2.044 1.54 2.044 3.164 0 1.932-1.092 2.996-2.884 2.996-1.792 0-2.8-1.232-2.828-3.22zm6.328 0h1.96c0 .896.308 1.456.896 1.456.588 0 .84-.364.84-1.204 0-.7-.28-1.092-1.064-1.456l-.728-.336c-1.288-.616-1.792-1.372-1.792-2.884 0-1.68 1.036-2.856 2.8-2.856 1.764 0 2.632 1.204 2.688 3.164h-1.876c-.028-.812-.196-1.372-.812-1.372-.56 0-.812.28-.812.98s.224.98.896 1.26l.7.308c1.4.672 2.016 1.54 2.016 3.164 0 1.932-1.092 2.996-2.884 2.996-1.792 0-2.8-1.232-2.828-3.22z"/></g></svg>',
@@ -35,7 +34,6 @@ module Docyard
35
34
  }.freeze
36
35
  # rubocop:enable Layout/LineLength
37
36
 
38
- # Map of file extensions/aliases to icon names
39
37
  EXTENSIONS = {
40
38
  "css" => "css",
41
39
  "go" => "go",
@@ -62,10 +60,6 @@ module Docyard
62
60
  "postgresql" => "pgsql"
63
61
  }.freeze
64
62
 
65
- # Get the SVG content for a file extension
66
- #
67
- # @param extension [String] The file extension (e.g., "js", "ts")
68
- # @return [String, nil] The SVG content, or nil if not found
69
63
  def self.svg(extension)
70
64
  icon_name = EXTENSIONS[extension.to_s.downcase]
71
65
  return nil unless icon_name
@@ -73,17 +67,10 @@ module Docyard
73
67
  ICONS[icon_name]
74
68
  end
75
69
 
76
- # Check if an icon exists for a file extension
77
- #
78
- # @param extension [String] The file extension to check
79
- # @return [Boolean] true if icon exists
80
70
  def self.exists?(extension)
81
71
  EXTENSIONS.key?(extension.to_s.downcase)
82
72
  end
83
73
 
84
- # Get all available file extensions
85
- #
86
- # @return [Array<String>] Array of supported extensions
87
74
  def self.available
88
75
  EXTENSIONS.keys.sort
89
76
  end
@@ -36,7 +36,10 @@ module Docyard
36
36
  "siren" => '<path d="M120,16V8a8,8,0,0,1,16,0v8a8,8,0,0,1-16,0Zm80,32a8,8,0,0,0,5.66-2.34l8-8a8,8,0,0,0-11.32-11.32l-8,8A8,8,0,0,0,200,48ZM50.34,45.66A8,8,0,0,0,61.66,34.34l-8-8A8,8,0,0,0,42.34,37.66Zm87,26.45a8,8,0,1,0-2.64,15.78C153.67,91.08,168,108.32,168,128a8,8,0,0,0,16,0C184,100.6,163.93,76.57,137.32,72.11ZM232,176v24a16,16,0,0,1-16,16H40a16,16,0,0,1-16-16V176a16,16,0,0,1,16-16V128a88,88,0,0,1,88.67-88c48.15.36,87.33,40.29,87.33,89v31A16,16,0,0,1,232,176ZM56,160H200V129c0-40-32.05-72.71-71.45-73H128a72,72,0,0,0-72,72Zm160,40V176H40v24H216Z"/>',
37
37
  "file" => '<path d="M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40V216a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160ZM200,216H56V40h88V88a8,8,0,0,0,8,8h48V216Z"/>',
38
38
  "terminal-window" => '<path d="M128,128a8,8,0,0,1-3,6.25l-40,32a8,8,0,1,1-10-12.5L107.19,128,75,102.25a8,8,0,1,1,10-12.5l40,32A8,8,0,0,1,128,128Zm48,24H136a8,8,0,0,0,0,16h40a8,8,0,0,0,0-16Zm56-96V200a16,16,0,0,1-16,16H40a16,16,0,0,1-16-16V56A16,16,0,0,1,40,40H216A16,16,0,0,1,232,56ZM216,200V56H40V200H216Z"/>',
39
- "list-dashes" => '<path d="M88,64a8,8,0,0,1,8-8H216a8,8,0,0,1,0,16H96A8,8,0,0,1,88,64Zm128,56H96a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16Zm0,64H96a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16ZM56,56H40a8,8,0,0,0,0,16H56a8,8,0,0,0,0-16Zm0,64H40a8,8,0,0,0,0,16H56a8,8,0,0,0,0-16Zm0,64H40a8,8,0,0,0,0,16H56a8,8,0,0,0,0-16Z"/>'
39
+ "list-dashes" => '<path d="M88,64a8,8,0,0,1,8-8H216a8,8,0,0,1,0,16H96A8,8,0,0,1,88,64Zm128,56H96a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16Zm0,64H96a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16ZM56,56H40a8,8,0,0,0,0,16H56a8,8,0,0,0,0-16Zm0,64H40a8,8,0,0,0,0,16H56a8,8,0,0,0,0-16Zm0,64H40a8,8,0,0,0,0,16H56a8,8,0,0,0,0-16Z"/>',
40
+ "magnifying-glass" => '<path d="M229.66,218.34l-50.07-50.06a88.11,88.11,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z"/>',
41
+ "command" => '<path d="M180,144H160V112h20a36,36,0,1,0-36-36V96H112V76a36,36,0,1,0-36,36H96v32H76a36,36,0,1,0,36,36V160h32v20a36,36,0,1,0,36-36ZM160,76a20,20,0,1,1,20,20H160ZM56,76a20,20,0,0,1,40,0V96H76A20,20,0,0,1,56,76ZM96,180a20,20,0,1,1-20-20H96Zm16-68h32v32H112Zm68,88a20,20,0,0,1-20-20V160h20a20,20,0,0,1,0,40Z"/>',
42
+ "hash" => '<path d="M224,88H175.4l8.47-46.57a8,8,0,0,0-15.74-2.86l-9,49.43H111.4l8.47-46.57a8,8,0,0,0-15.74-2.86L95.14,88H48a8,8,0,0,0,0,16H92.23L83.5,152H32a8,8,0,0,0,0,16H80.6l-8.47,46.57a8,8,0,0,0,6.44,9.3A7.79,7.79,0,0,0,80,224a8,8,0,0,0,7.86-6.57l9-49.43H144.6l-8.47,46.57a8,8,0,0,0,6.44,9.3,7.79,7.79,0,0,0,1.43.13,8,8,0,0,0,7.86-6.57l9-49.43H208a8,8,0,0,0,0-16H163.77l8.73-48H224a8,8,0,0,0,0-16Zm-68.5,64H107.77l8.73-48h47.73Z"/>'
40
43
  },
41
44
  "bold" => {
42
45
  "heart" => '<path d="M178,36c-20.09,0-37.92,7.93-50,21.56C115.92,43.93,98.09,36,78,36a66.08,66.08,0,0,0-66,66c0,72.34,105.81,130.14,110.31,132.57a12,12,0,0,0,11.38,0C138.19,232.14,244,174.34,244,102A66.08,66.08,0,0,0,178,36Zm-5.49,142.36A328.69,328.69,0,0,1,128,210.16a328.69,328.69,0,0,1-44.51-31.8C61.82,159.77,36,131.42,36,102A42,42,0,0,1,78,60c17.8,0,32.7,9.4,38.89,24.54a12,12,0,0,0,22.22,0C145.3,69.4,160.2,60,178,60a42,42,0,0,1,42,42C220,131.42,194.18,159.77,172.51,178.36Z"/>'
@@ -3,24 +3,31 @@
3
3
  require "kramdown"
4
4
  require "kramdown-parser-gfm"
5
5
  require "yaml"
6
- require_relative "components/registry"
7
- require_relative "components/base_processor"
8
- require_relative "components/callout_processor"
9
- require_relative "components/tabs_processor"
10
- require_relative "components/icon_processor"
11
- require_relative "components/code_block_processor"
12
- require_relative "components/table_wrapper_processor"
13
- require_relative "components/heading_anchor_processor"
14
- require_relative "components/table_of_contents_processor"
6
+ require_relative "../components/registry"
7
+ require_relative "../components/base_processor"
8
+ require_relative "../components/processors/callout_processor"
9
+ require_relative "../components/processors/tabs_processor"
10
+ require_relative "../components/processors/icon_processor"
11
+ require_relative "../components/processors/code_block_processor"
12
+ require_relative "../components/processors/code_snippet_import_preprocessor"
13
+ require_relative "../components/processors/code_block_options_preprocessor"
14
+ require_relative "../components/processors/code_block_diff_preprocessor"
15
+ require_relative "../components/processors/code_block_focus_preprocessor"
16
+ require_relative "../components/processors/table_wrapper_processor"
17
+ require_relative "../components/processors/heading_anchor_processor"
18
+ require_relative "../components/processors/table_of_contents_processor"
19
+ require_relative "../components/aliases"
15
20
 
16
21
  module Docyard
17
22
  class Markdown
18
23
  FRONTMATTER_REGEX = /\A---\s*\n(.*?\n)---\s*\n/m
19
24
 
20
- attr_reader :raw
25
+ attr_reader :raw, :config
21
26
 
22
- def initialize(raw)
27
+ def initialize(raw, config: nil)
23
28
  @raw = raw.freeze
29
+ @config = config
30
+ @context = {}
24
31
  end
25
32
 
26
33
  def frontmatter
@@ -56,7 +63,7 @@ module Docyard
56
63
  end
57
64
 
58
65
  def toc
59
- @toc ||= Thread.current[:docyard_toc] || []
66
+ @context[:toc] || []
60
67
  end
61
68
 
62
69
  private
@@ -75,7 +82,9 @@ module Docyard
75
82
  end
76
83
 
77
84
  def render_html
78
- preprocessed_content = Components::Registry.run_preprocessors(content)
85
+ @context[:config] = config&.data
86
+
87
+ preprocessed_content = Components::Registry.run_preprocessors(content, @context)
79
88
 
80
89
  raw_html = Kramdown::Document.new(
81
90
  preprocessed_content,
@@ -85,7 +94,7 @@ module Docyard
85
94
  parse_block_html: true
86
95
  ).to_html
87
96
 
88
- Components::Registry.run_postprocessors(raw_html)
97
+ Components::Registry.run_postprocessors(raw_html, @context)
89
98
  end
90
99
  end
91
100
  end
@@ -1,24 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "erb"
4
- require_relative "constants"
4
+ require_relative "../config/constants"
5
5
 
6
6
  module Docyard
7
7
  class Renderer
8
- LAYOUTS_PATH = File.join(__dir__, "templates", "layouts")
9
- ERRORS_PATH = File.join(__dir__, "templates", "errors")
10
- PARTIALS_PATH = File.join(__dir__, "templates", "partials")
8
+ include Utils::UrlHelpers
11
9
 
12
- attr_reader :layout_path, :base_url
10
+ LAYOUTS_PATH = File.join(__dir__, "../templates", "layouts")
11
+ ERRORS_PATH = File.join(__dir__, "../templates", "errors")
12
+ PARTIALS_PATH = File.join(__dir__, "../templates", "partials")
13
13
 
14
- def initialize(layout: "default", base_url: "/")
14
+ attr_reader :layout_path, :base_url, :config
15
+
16
+ def initialize(layout: "default", base_url: "/", config: nil)
15
17
  @layout_path = File.join(LAYOUTS_PATH, "#{layout}.html.erb")
16
18
  @base_url = normalize_base_url(base_url)
19
+ @config = config
17
20
  end
18
21
 
19
22
  def render_file(file_path, sidebar_html: "", prev_next_html: "", branding: {})
20
23
  markdown_content = File.read(file_path)
21
- markdown = Markdown.new(markdown_content)
24
+ markdown = Markdown.new(markdown_content, config: config)
22
25
 
23
26
  html_content = strip_md_from_links(markdown.html)
24
27
  toc = markdown.toc
@@ -79,21 +82,8 @@ module Docyard
79
82
  "#{base_url}#{path}"
80
83
  end
81
84
 
82
- def link_path(path)
83
- return path if path.nil? || path.start_with?("http://", "https://")
84
-
85
- "#{base_url.chomp('/')}#{path}"
86
- end
87
-
88
85
  private
89
86
 
90
- def normalize_base_url(url)
91
- return "/" if url.nil? || url.empty?
92
-
93
- url = "/#{url}" unless url.start_with?("/")
94
- url.end_with?("/") ? url : "#{url}/"
95
- end
96
-
97
87
  def assign_content_variables(content, page_title, sidebar_html, prev_next_html, toc)
98
88
  @content = content
99
89
  @page_title = page_title
@@ -103,15 +93,29 @@ module Docyard
103
93
  end
104
94
 
105
95
  def assign_branding_variables(branding)
96
+ assign_site_branding(branding)
97
+ assign_display_options(branding)
98
+ assign_search_options(branding)
99
+ end
100
+
101
+ def assign_site_branding(branding)
106
102
  @site_title = branding[:site_title] || Constants::DEFAULT_SITE_TITLE
107
103
  @site_description = branding[:site_description] || ""
108
104
  @logo = branding[:logo] || Constants::DEFAULT_LOGO_PATH
109
105
  @logo_dark = branding[:logo_dark]
110
106
  @favicon = branding[:favicon] || Constants::DEFAULT_FAVICON_PATH
107
+ end
108
+
109
+ def assign_display_options(branding)
111
110
  @display_logo = branding[:display_logo].nil? || branding[:display_logo]
112
111
  @display_title = branding[:display_title].nil? || branding[:display_title]
113
112
  end
114
113
 
114
+ def assign_search_options(branding)
115
+ @search_enabled = branding[:search_enabled].nil? || branding[:search_enabled]
116
+ @search_placeholder = branding[:search_placeholder] || "Search documentation..."
117
+ end
118
+
115
119
  def strip_md_from_links(html)
116
120
  html.gsub(/href="([^"]+)\.md"/, 'href="\1"')
117
121
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module Docyard
6
+ module Search
7
+ class BuildIndexer
8
+ include PagefindSupport
9
+
10
+ PAGEFIND_COMMAND = "npx"
11
+
12
+ attr_reader :config, :output_dir, :verbose
13
+
14
+ def initialize(config, verbose: false)
15
+ @config = config
16
+ @output_dir = config.build.output_dir
17
+ @verbose = verbose
18
+ end
19
+
20
+ def index
21
+ return 0 unless search_enabled?
22
+
23
+ log "Generating search index..."
24
+
25
+ unless pagefind_available?
26
+ warn_pagefind_missing
27
+ return 0
28
+ end
29
+
30
+ run_pagefind
31
+ end
32
+
33
+ private
34
+
35
+ def warn_pagefind_missing
36
+ log_warning "[!] Search index skipped: Pagefind not found"
37
+ log_warning " Install with: npm install -g pagefind"
38
+ log_warning " Or run: npx pagefind --site #{output_dir}"
39
+ end
40
+
41
+ def run_pagefind
42
+ args = build_pagefind_args(output_dir)
43
+ log "Running: npx #{args.join(' ')}" if verbose
44
+
45
+ stdout, stderr, status = Open3.capture3(PAGEFIND_COMMAND, *args)
46
+
47
+ if status.success?
48
+ page_count = extract_page_count(stdout)
49
+ log "[+] Generated search index (#{page_count} pages indexed)"
50
+ page_count
51
+ else
52
+ log_warning "[!] Search indexing failed: #{stderr}"
53
+ 0
54
+ end
55
+ end
56
+
57
+ def extract_page_count(output)
58
+ if output =~ /Indexed (\d+) page/i
59
+ Regexp.last_match(1).to_i
60
+ else
61
+ 0
62
+ end
63
+ end
64
+
65
+ def log(message)
66
+ puts message
67
+ end
68
+
69
+ def log_warning(message)
70
+ warn message
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "tmpdir"
5
+ require "open3"
6
+ require "tty-progressbar"
7
+
8
+ module Docyard
9
+ module Search
10
+ class DevIndexer
11
+ include PagefindSupport
12
+
13
+ attr_reader :docs_path, :config, :temp_dir, :pagefind_path
14
+
15
+ def initialize(docs_path:, config:)
16
+ @docs_path = docs_path
17
+ @config = config
18
+ @temp_dir = nil
19
+ @pagefind_path = nil
20
+ end
21
+
22
+ def generate
23
+ return unless search_enabled?
24
+ return unless pagefind_available?
25
+
26
+ @temp_dir = Dir.mktmpdir("docyard-search-")
27
+ generate_html_files
28
+ run_pagefind
29
+ @pagefind_path = File.join(temp_dir, "pagefind")
30
+
31
+ log_success
32
+ pagefind_path
33
+ rescue StandardError => e
34
+ warn "[!] Search index generation failed: #{e.message}"
35
+ cleanup
36
+ nil
37
+ end
38
+
39
+ def cleanup
40
+ return unless temp_dir && Dir.exist?(temp_dir)
41
+
42
+ FileUtils.rm_rf(temp_dir)
43
+ end
44
+
45
+ private
46
+
47
+ def pagefind_available?
48
+ result = super
49
+ warn "[!] Search disabled: Pagefind not found (npm install -g pagefind)" unless result
50
+ result
51
+ end
52
+
53
+ def generate_html_files
54
+ markdown_files = Dir.glob(File.join(docs_path, "**", "*.md"))
55
+ renderer = Renderer.new(base_url: "/", config: config)
56
+
57
+ progress = TTY::ProgressBar.new(
58
+ "Indexing search [:bar] :current/:total (:percent)",
59
+ total: markdown_files.size,
60
+ width: 50
61
+ )
62
+
63
+ markdown_files.each do |file_path|
64
+ generate_html_file(file_path, renderer)
65
+ progress.advance
66
+ end
67
+ end
68
+
69
+ def generate_html_file(markdown_file, renderer)
70
+ relative_path = markdown_file.delete_prefix("#{docs_path}/")
71
+ output_path = determine_output_path(relative_path)
72
+
73
+ html = renderer.render_file(markdown_file, branding: branding_options)
74
+
75
+ FileUtils.mkdir_p(File.dirname(output_path))
76
+ File.write(output_path, html)
77
+ end
78
+
79
+ def determine_output_path(relative_path)
80
+ base_name = File.basename(relative_path, ".md")
81
+ dir_name = File.dirname(relative_path)
82
+
83
+ if base_name == "index"
84
+ File.join(temp_dir, dir_name, "index.html")
85
+ else
86
+ File.join(temp_dir, dir_name, base_name, "index.html")
87
+ end
88
+ end
89
+
90
+ def branding_options
91
+ BrandingResolver.new(config).resolve
92
+ end
93
+
94
+ def run_pagefind
95
+ args = build_pagefind_args(temp_dir)
96
+ stdout, stderr, status = Open3.capture3("npx", *args)
97
+
98
+ raise "Pagefind failed: #{stderr}" unless status.success?
99
+
100
+ stdout
101
+ end
102
+
103
+ def log_success
104
+ page_count = Dir.glob(File.join(temp_dir, "**", "*.html")).size
105
+ puts "=> Search index generated (#{page_count} pages)"
106
+ puts "=> Temp directory: #{temp_dir}" if ENV["DOCYARD_DEBUG"]
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module Docyard
6
+ module Search
7
+ module PagefindSupport
8
+ def search_enabled?
9
+ config.search.enabled != false
10
+ end
11
+
12
+ def pagefind_available?
13
+ _stdout, _stderr, status = Open3.capture3("npx", "pagefind", "--version")
14
+ status.success?
15
+ rescue Errno::ENOENT
16
+ false
17
+ end
18
+
19
+ def build_pagefind_args(site_dir)
20
+ args = ["pagefind", "--site", site_dir]
21
+
22
+ exclusions = config.search.exclude || []
23
+ exclusions.each do |pattern|
24
+ args += ["--exclude-selectors", pattern]
25
+ end
26
+
27
+ args
28
+ end
29
+ end
30
+ end
31
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Docyard
4
4
  class AssetHandler
5
- ASSETS_PATH = File.join(__dir__, "templates", "assets")
5
+ ASSETS_PATH = File.join(__dir__, "../templates", "assets")
6
6
  USER_ASSETS_PATH = "docs/assets"
7
7
 
8
8
  CONTENT_TYPES = {