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
@@ -2,12 +2,25 @@
2
2
 
3
3
  module Docyard
4
4
  module IconHelpers
5
+ VALID_WEIGHTS = %w[regular bold fill light thin duotone].freeze
6
+
5
7
  def icon(name, weight = "regular")
6
- Icons.render(name.to_s.tr("_", "-"), weight) || ""
8
+ name = name.to_s
9
+ return name if name.strip.start_with?("<svg")
10
+
11
+ name = name.tr("_", "-")
12
+ weight = weight.to_s
13
+ weight = "regular" unless VALID_WEIGHTS.include?(weight)
14
+ weight_class = weight == "regular" ? "ph" : "ph-#{weight}"
15
+ %(<i class="#{weight_class} ph-#{name}" aria-hidden="true"></i>)
7
16
  end
8
17
 
9
18
  def icon_file_extension(extension)
10
19
  Icons.render_file_extension(extension) || ""
11
20
  end
21
+
22
+ def icon_for_language(language)
23
+ Icons.render_for_language(language) || ""
24
+ end
12
25
  end
13
26
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Icons
5
+ module Devicons
6
+ MAP = {
7
+ "bash" => "devicon-bash-plain", "bun" => "devicon-bun-plain colored",
8
+ "c" => "devicon-c-plain colored", "cargo" => "devicon-rust-original colored",
9
+ "clj" => "devicon-clojure-plain colored", "clojure" => "devicon-clojure-plain colored",
10
+ "coffee" => "devicon-coffeescript-original colored", "coffeescript" => "devicon-coffeescript-original colored",
11
+ "composer" => "devicon-composer-plain colored", "cpp" => "devicon-cplusplus-plain colored",
12
+ "cs" => "devicon-csharp-plain colored", "csharp" => "devicon-csharp-plain colored",
13
+ "css" => "devicon-css3-plain colored", "dart" => "devicon-dart-plain colored",
14
+ "docker" => "devicon-docker-plain colored", "dockerfile" => "devicon-docker-plain colored",
15
+ "elixir" => "devicon-elixir-plain colored", "elm" => "devicon-elm-plain colored",
16
+ "erb" => "devicon-ruby-plain colored", "erl" => "devicon-erlang-plain colored",
17
+ "erlang" => "devicon-erlang-plain colored", "ex" => "devicon-elixir-plain colored",
18
+ "gem" => "devicon-ruby-plain colored", "go" => "devicon-go-plain colored",
19
+ "golang" => "devicon-go-plain colored", "gql" => "devicon-graphql-plain colored",
20
+ "gradle" => "devicon-gradle-original colored", "graphql" => "devicon-graphql-plain colored",
21
+ "groovy" => "devicon-groovy-plain colored", "h" => "devicon-c-plain colored",
22
+ "haml" => "devicon-ruby-plain colored", "haskell" => "devicon-haskell-plain colored",
23
+ "homebrew" => "devicon-homebrew-plain colored", "hs" => "devicon-haskell-plain colored",
24
+ "htm" => "devicon-html5-plain colored", "html" => "devicon-html5-plain colored",
25
+ "html5" => "devicon-html5-plain colored", "java" => "devicon-java-plain colored",
26
+ "javascript" => "devicon-javascript-plain colored", "js" => "devicon-javascript-plain colored",
27
+ "json" => "devicon-json-plain colored", "jsx" => "devicon-react-original colored",
28
+ "kotlin" => "devicon-kotlin-plain colored", "kt" => "devicon-kotlin-plain colored",
29
+ "less" => "devicon-less-plain-wordmark colored", "lua" => "devicon-lua-plain colored",
30
+ "markdown" => "devicon-markdown-original", "maven" => "devicon-maven-plain colored",
31
+ "md" => "devicon-markdown-original", "mysql" => "devicon-mysql-plain colored",
32
+ "nginx" => "devicon-nginx-original colored", "nim" => "devicon-nim-plain colored",
33
+ "nix" => "devicon-nixos-plain colored", "npm" => "devicon-npm-original-wordmark colored",
34
+ "nuget" => "devicon-nuget-original colored", "objc" => "devicon-objectivec-plain colored",
35
+ "objectivec" => "devicon-objectivec-plain colored", "ocaml" => "devicon-ocaml-plain colored",
36
+ "pacman" => "devicon-archlinux-plain colored", "perl" => "devicon-perl-plain colored",
37
+ "pgsql" => "devicon-postgresql-plain colored", "php" => "devicon-php-plain colored",
38
+ "pip" => "devicon-python-plain colored", "pl" => "devicon-perl-plain colored",
39
+ "pnpm" => "devicon-pnpm-plain colored", "postgres" => "devicon-postgresql-plain colored",
40
+ "postgresql" => "devicon-postgresql-plain colored", "py" => "devicon-python-plain colored",
41
+ "python" => "devicon-python-plain colored", "r" => "devicon-r-plain colored",
42
+ "rb" => "devicon-ruby-plain colored", "rs" => "devicon-rust-original colored",
43
+ "ruby" => "devicon-ruby-plain colored", "rust" => "devicon-rust-original colored",
44
+ "sass" => "devicon-sass-original colored", "scala" => "devicon-scala-plain colored",
45
+ "scss" => "devicon-sass-original colored", "sql" => "devicon-azuresqldatabase-plain colored",
46
+ "svelte" => "devicon-svelte-plain colored", "swift" => "devicon-swift-plain colored",
47
+ "terraform" => "devicon-terraform-plain colored", "tf" => "devicon-terraform-plain colored",
48
+ "ts" => "devicon-typescript-plain colored", "tsx" => "devicon-react-original colored",
49
+ "typescript" => "devicon-typescript-plain colored", "vim" => "devicon-vim-plain colored",
50
+ "vue" => "devicon-vuejs-plain colored", "xml" => "devicon-xml-plain colored",
51
+ "yaml" => "devicon-yaml-plain colored", "yml" => "devicon-yaml-plain colored",
52
+ "yarn" => "devicon-yarn-plain colored", "zig" => "devicon-zig-plain colored"
53
+ }.freeze
54
+
55
+ HIGHLIGHT_ALIASES = {
56
+ "apt" => "bash", "bun" => "bash", "cargo" => "bash", "composer" => "bash",
57
+ "gem" => "bash", "gradle" => "bash", "homebrew" => "bash", "maven" => "bash",
58
+ "npm" => "bash", "nuget" => "bash", "pacman" => "bash", "pip" => "bash",
59
+ "pnpm" => "bash", "yarn" => "bash"
60
+ }.freeze
61
+ end
62
+ end
63
+ end
@@ -1,40 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "icons/phosphor"
4
- require_relative "icons/file_types"
5
- require_relative "renderer"
3
+ require_relative "icons/devicons"
6
4
 
7
5
  module Docyard
8
6
  module Icons
9
- LIBRARIES = {
10
- phosphor: PHOSPHOR
11
- }.freeze
7
+ VALID_WEIGHTS = %w[regular bold fill light thin duotone].freeze
12
8
 
13
9
  def self.render(name, weight = "regular")
14
- icon_data = LIBRARIES.dig(:phosphor, weight, name)
15
- return nil unless icon_data
16
-
17
- Renderer.new.render_partial(
18
- "_icon", {
19
- name: name,
20
- icon_data: icon_data
21
- }
22
- )
10
+ name = name.to_s.tr("_", "-")
11
+ weight = weight.to_s
12
+ weight = "regular" unless VALID_WEIGHTS.include?(weight)
13
+ weight_class = weight == "regular" ? "ph" : "ph-#{weight}"
14
+ %(<i class="#{weight_class} ph-#{name}" aria-hidden="true"></i>)
15
+ end
16
+
17
+ def self.render_for_language(language)
18
+ devicon_class = Devicons::MAP[language.to_s.downcase]
19
+ return %(<i class="#{devicon_class}" aria-hidden="true"></i>) if devicon_class
20
+
21
+ ""
23
22
  end
24
23
 
25
24
  def self.render_file_extension(extension)
26
- svg_content = FileTypes.svg(extension)
27
-
28
- if svg_content
29
- Renderer.new.render_partial(
30
- "_icon_file_extension", {
31
- extension: extension,
32
- svg_content: svg_content
33
- }
34
- )
35
- else
36
- render("file")
37
- end
25
+ devicon_class = Devicons::MAP[extension.to_s.downcase]
26
+ return %(<i class="#{devicon_class}" aria-hidden="true"></i>) if devicon_class
27
+
28
+ ""
29
+ end
30
+
31
+ def self.highlight_language(language)
32
+ Devicons::HIGHLIGHT_ALIASES[language.to_s.downcase] || language
33
+ end
34
+
35
+ def self.devicon?(language)
36
+ Devicons::MAP.key?(language.to_s.downcase)
38
37
  end
39
38
  end
40
39
  end
@@ -19,6 +19,8 @@ require_relative "../components/processors/include_processor"
19
19
  require_relative "../components/processors/code_block_options_preprocessor"
20
20
  require_relative "../components/processors/code_block_diff_preprocessor"
21
21
  require_relative "../components/processors/code_block_focus_preprocessor"
22
+ require_relative "../components/processors/code_block_extended_fence_preprocessor"
23
+ require_relative "../components/processors/code_block_extended_fence_postprocessor"
22
24
  require_relative "../components/processors/table_wrapper_processor"
23
25
  require_relative "../components/processors/heading_anchor_processor"
24
26
  require_relative "../components/processors/custom_anchor_processor"
@@ -63,28 +65,8 @@ module Docyard
63
65
  frontmatter["description"]
64
66
  end
65
67
 
66
- def sidebar_icon
67
- frontmatter.dig("sidebar", "icon")
68
- end
69
-
70
- def sidebar_text
71
- frontmatter.dig("sidebar", "text")
72
- end
73
-
74
- def sidebar_collapsed
75
- frontmatter.dig("sidebar", "collapsed")
76
- end
77
-
78
- def sidebar_order
79
- frontmatter.dig("sidebar", "order")
80
- end
81
-
82
- def sidebar_badge
83
- frontmatter.dig("sidebar", "badge")
84
- end
85
-
86
- def sidebar_badge_type
87
- frontmatter.dig("sidebar", "badge_type")
68
+ def og_image
69
+ frontmatter["og_image"]
88
70
  end
89
71
 
90
72
  def toc
@@ -109,7 +91,7 @@ module Docyard
109
91
  def render_html
110
92
  @context[:config] = config&.data
111
93
  @context[:current_file] = @file_path
112
- @context[:docs_root] = "docs"
94
+ @context[:docs_root] = config&.source || "docs"
113
95
 
114
96
  preprocessed_content = Components::Registry.run_preprocessors(content, @context)
115
97
 
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module OgHelpers
5
+ def assign_og_variables(branding, page_description, page_og_image, current_path)
6
+ site_url = branding[:site_url]
7
+ @og_enabled = !site_url.nil? && !site_url.empty?
8
+ return unless @og_enabled
9
+
10
+ @og_url = build_canonical_url(site_url, current_path)
11
+ @og_description = page_description || @site_description
12
+ @og_image = build_og_image_url(site_url, page_og_image || branding[:og_image])
13
+ @og_twitter = branding[:twitter]
14
+ end
15
+
16
+ private
17
+
18
+ def build_canonical_url(site_url, current_path)
19
+ base = site_url.chomp("/")
20
+ path = current_path.start_with?("/") ? current_path : "/#{current_path}"
21
+ "#{base}#{path}"
22
+ end
23
+
24
+ def build_og_image_url(site_url, og_image)
25
+ return nil if og_image.nil?
26
+
27
+ if og_image.start_with?("http://", "https://")
28
+ og_image
29
+ else
30
+ base = site_url.chomp("/")
31
+ path = og_image.start_with?("/") ? og_image : "/#{og_image}"
32
+ "#{base}#{path}"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -1,53 +1,84 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "erb"
4
- require_relative "../config/constants"
4
+ require_relative "../constants"
5
+ require_relative "../utils/git_info"
5
6
  require_relative "icon_helpers"
7
+ require_relative "og_helpers"
8
+ require_relative "branding_variables"
6
9
 
7
10
  module Docyard
8
11
  class Renderer
9
12
  include Utils::UrlHelpers
13
+ include Utils::HtmlHelpers
10
14
  include IconHelpers
15
+ include OgHelpers
16
+ include BrandingVariables
11
17
 
12
18
  LAYOUTS_PATH = File.join(__dir__, "../templates", "layouts")
13
19
  ERRORS_PATH = File.join(__dir__, "../templates", "errors")
14
20
  PARTIALS_PATH = File.join(__dir__, "../templates", "partials")
15
21
  DEFAULT_LAYOUT = "default"
16
22
 
17
- attr_reader :base_url, :config
23
+ attr_reader :base_url, :config, :dev_mode, :sse_port
18
24
 
19
- def initialize(base_url: "/", config: nil)
25
+ def initialize(base_url: "/", config: nil, dev_mode: false, sse_port: nil)
20
26
  @base_url = normalize_base_url(base_url)
21
27
  @config = config
28
+ @dev_mode = dev_mode
29
+ @sse_port = sse_port
22
30
  end
23
31
 
24
32
  def render_file(file_path, sidebar_html: "", prev_next_html: "", breadcrumbs: nil, branding: {},
25
33
  template_options: {}, current_path: "/")
26
- markdown = Markdown.new(File.read(file_path), config: config, file_path: file_path)
34
+ raw_content = File.read(file_path)
35
+ markdown = Markdown.new(raw_content, config: config, file_path: file_path)
27
36
 
28
37
  render(
29
38
  content: strip_md_from_links(markdown.html),
30
39
  page_title: markdown.title || Constants::DEFAULT_SITE_TITLE,
40
+ page_description: markdown.description,
41
+ page_og_image: markdown.og_image,
31
42
  navigation: build_navigation(sidebar_html, prev_next_html, markdown.toc, breadcrumbs),
32
43
  branding: branding,
33
44
  template_options: template_options,
34
- current_path: current_path
45
+ current_path: current_path,
46
+ file_path: file_path,
47
+ raw_markdown: raw_content
35
48
  )
36
49
  end
37
50
 
51
+ def render_for_search(file_path)
52
+ markdown = Markdown.new(File.read(file_path), config: config, file_path: file_path)
53
+ title = markdown.title || Constants::DEFAULT_SITE_TITLE
54
+ content = strip_md_from_links(markdown.html)
55
+
56
+ <<~HTML
57
+ <!DOCTYPE html>
58
+ <html>
59
+ <head><title>#{escape_html(title)}</title></head>
60
+ <body><main data-pagefind-body>#{content}</main></body>
61
+ </html>
62
+ HTML
63
+ end
64
+
38
65
  def build_navigation(sidebar_html, prev_next_html, toc, breadcrumbs)
39
66
  { sidebar_html: sidebar_html, prev_next_html: prev_next_html, toc: toc, breadcrumbs: breadcrumbs }
40
67
  end
41
68
 
42
- def render(content:, page_title: Constants::DEFAULT_SITE_TITLE, navigation: {}, branding: {},
43
- template_options: {}, current_path: "/")
69
+ def render(content:, page_title: Constants::DEFAULT_SITE_TITLE, page_description: nil, page_og_image: nil,
70
+ navigation: {}, branding: {}, template_options: {}, current_path: "/", file_path: nil,
71
+ raw_markdown: nil)
44
72
  layout = template_options[:template] || DEFAULT_LAYOUT
45
73
  layout_path = File.join(LAYOUTS_PATH, "#{layout}.html.erb")
46
74
  template = File.read(layout_path)
47
75
 
48
- assign_content_variables(content, page_title, navigation)
76
+ assign_content_variables(content, page_title, navigation, raw_markdown)
49
77
  assign_branding_variables(branding, current_path)
78
+ assign_og_variables(branding, page_description, page_og_image, current_path)
50
79
  assign_template_variables(template_options)
80
+ assign_git_info(branding, file_path)
81
+ assign_feedback_variables
51
82
 
52
83
  ERB.new(template).result(binding)
53
84
  end
@@ -58,7 +89,7 @@ module Docyard
58
89
 
59
90
  def render_server_error(error)
60
91
  @error_message = error.message
61
- @backtrace = error.backtrace.join("\n")
92
+ @backtrace = error.backtrace&.join("\n") || "No backtrace available"
62
93
  render_error_template(500)
63
94
  end
64
95
 
@@ -68,15 +99,37 @@ module Docyard
68
99
  ERB.new(template).result(binding)
69
100
  end
70
101
 
102
+ VALID_IVAR_PATTERN = /\A[a-z_][a-z0-9_]*\z/i
103
+
71
104
  def render_partial(name, locals = {})
72
105
  partial_path = File.join(PARTIALS_PATH, "#{name}.html.erb")
73
106
  template = File.read(partial_path)
74
107
 
75
- locals.each { |key, value| instance_variable_set("@#{key}", value) }
108
+ locals.each do |key, value|
109
+ validate_variable_name!(key)
110
+ instance_variable_set("@#{key}", value)
111
+ end
76
112
 
77
113
  ERB.new(template).result(binding)
78
114
  end
79
115
 
116
+ def render_custom_visual(file_path)
117
+ return "" if file_path.nil? || file_path.empty?
118
+
119
+ source_dir = config&.source || "docs"
120
+ full_path = File.join(source_dir, file_path)
121
+
122
+ return "" unless File.exist?(full_path)
123
+
124
+ File.read(full_path)
125
+ end
126
+
127
+ def validate_variable_name!(name)
128
+ return if name.to_s.match?(VALID_IVAR_PATTERN)
129
+
130
+ raise ArgumentError, "Invalid variable name: #{name}"
131
+ end
132
+
80
133
  def asset_path(path)
81
134
  return path if path.nil? || path.start_with?("http://", "https://")
82
135
 
@@ -85,61 +138,14 @@ module Docyard
85
138
 
86
139
  private
87
140
 
88
- def assign_content_variables(content, page_title, navigation)
141
+ def assign_content_variables(content, page_title, navigation, raw_markdown)
89
142
  @content = content
90
143
  @page_title = page_title
91
144
  @sidebar_html = navigation[:sidebar_html] || ""
92
145
  @prev_next_html = navigation[:prev_next_html] || ""
93
146
  @toc = navigation[:toc] || []
94
147
  @breadcrumbs = navigation[:breadcrumbs]
95
- end
96
-
97
- def assign_branding_variables(branding, current_path = "/")
98
- assign_site_branding(branding)
99
- assign_search_options(branding)
100
- assign_credits_and_social(branding)
101
- assign_tabs(branding, current_path)
102
- end
103
-
104
- def assign_site_branding(branding)
105
- @site_title = branding[:site_title] || Constants::DEFAULT_SITE_TITLE
106
- @site_description = branding[:site_description] || ""
107
- @logo = branding[:logo] || Constants::DEFAULT_LOGO_PATH
108
- @logo_dark = branding[:logo_dark]
109
- @favicon = branding[:favicon] || Constants::DEFAULT_FAVICON_PATH
110
- @has_custom_logo = branding[:has_custom_logo] || false
111
- end
112
-
113
- def assign_search_options(branding)
114
- @search_enabled = branding[:search_enabled].nil? || branding[:search_enabled]
115
- @search_placeholder = branding[:search_placeholder] || "Search documentation..."
116
- end
117
-
118
- def assign_credits_and_social(branding)
119
- @credits = branding[:credits] != false
120
- @copyright = branding[:copyright]
121
- @social = branding[:social] || []
122
- @header_ctas = branding[:header_ctas] || []
123
- @announcement = branding[:announcement]
124
- end
125
-
126
- def assign_tabs(branding, current_path)
127
- tabs = branding[:tabs] || []
128
- @tabs = tabs.map { |tab| tab.merge(active: tab_active?(tab[:href], current_path)) }
129
- @has_tabs = branding[:has_tabs] || false
130
- @current_path = current_path
131
- end
132
-
133
- def tab_active?(tab_href, current_path)
134
- return false if tab_href.nil? || current_path.nil?
135
- return false if tab_href.start_with?("http://", "https://")
136
-
137
- normalized_tab = tab_href.chomp("/")
138
- normalized_current = current_path.chomp("/")
139
-
140
- return true if normalized_tab == normalized_current
141
-
142
- current_path.start_with?("#{normalized_tab}/")
148
+ @raw_markdown = raw_markdown
143
149
  end
144
150
 
145
151
  def assign_template_variables(template_options)
@@ -160,5 +166,27 @@ module Docyard
160
166
  def strip_md_from_links(html)
161
167
  html.gsub(/href="([^"]+)\.md"/, 'href="\1"')
162
168
  end
169
+
170
+ def assign_git_info(branding, file_path)
171
+ @show_edit_link = branding[:show_edit_link] && file_path
172
+ @show_last_updated = branding[:show_last_updated] && file_path
173
+ return unless @show_edit_link || @show_last_updated
174
+
175
+ git_info = Utils::GitInfo.new(
176
+ repo_url: branding[:repo_url],
177
+ branch: branding[:repo_branch],
178
+ edit_path: branding[:repo_edit_path]
179
+ )
180
+
181
+ @edit_url = git_info.edit_url(file_path) if @show_edit_link
182
+ @last_updated = git_info.last_updated(file_path) if @show_last_updated
183
+ end
184
+
185
+ def assign_feedback_variables
186
+ return unless config
187
+
188
+ @feedback_enabled = config.feedback.enabled
189
+ @feedback_question = config.feedback.question
190
+ end
163
191
  end
164
192
  end
@@ -117,6 +117,7 @@ module Docyard
117
117
  tagline: hero["tagline"],
118
118
  gradient: hero.fetch("gradient", true),
119
119
  image: symbolize_image(hero["image"]),
120
+ custom_visual: symbolize_custom_visual(hero["custom_visual"]),
120
121
  actions: symbolize_actions(hero["actions"])
121
122
  }.compact
122
123
  end
@@ -138,6 +139,19 @@ module Docyard
138
139
  end
139
140
  end
140
141
 
142
+ def symbolize_custom_visual(custom_visual)
143
+ return nil if custom_visual.nil?
144
+
145
+ if custom_visual.is_a?(String)
146
+ { html: custom_visual, placement: "side" }
147
+ elsif custom_visual.is_a?(Hash)
148
+ {
149
+ html: custom_visual["html"],
150
+ placement: custom_visual["placement"] || "side"
151
+ }
152
+ end
153
+ end
154
+
141
155
  def symbolize_actions(actions)
142
156
  return nil unless actions.is_a?(Array)
143
157
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../utils/path_utils"
4
+
3
5
  module Docyard
4
6
  module Routing
5
7
  class FallbackResolver
@@ -29,9 +31,7 @@ module Docyard
29
31
  end
30
32
 
31
33
  def sanitize_path(request_path)
32
- clean = request_path.to_s.delete_prefix("/").delete_suffix("/")
33
- clean = "index" if clean.empty?
34
- clean.delete_suffix(".md")
34
+ Utils::PathUtils.sanitize_url_path(request_path)
35
35
  end
36
36
 
37
37
  def find_first_item_in_section(request_path)
@@ -63,11 +63,11 @@ module Docyard
63
63
  end
64
64
 
65
65
  def log(message)
66
- puts message
66
+ Docyard.logger.info(message)
67
67
  end
68
68
 
69
69
  def log_warning(message)
70
- warn message
70
+ Docyard.logger.warn(message)
71
71
  end
72
72
  end
73
73
  end