docyard 0.7.0 → 0.9.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 (155) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -1
  3. data/CHANGELOG.md +43 -1
  4. data/lib/docyard/build/asset_bundler.rb +22 -7
  5. data/lib/docyard/build/file_copier.rb +49 -27
  6. data/lib/docyard/build/sitemap_generator.rb +6 -6
  7. data/lib/docyard/build/static_generator.rb +85 -12
  8. data/lib/docyard/builder.rb +6 -6
  9. data/lib/docyard/components/aliases.rb +12 -0
  10. data/lib/docyard/components/processors/abbreviation_processor.rb +72 -0
  11. data/lib/docyard/components/processors/accordion_processor.rb +81 -0
  12. data/lib/docyard/components/processors/badge_processor.rb +72 -0
  13. data/lib/docyard/components/processors/callout_processor.rb +8 -2
  14. data/lib/docyard/components/processors/cards_processor.rb +100 -0
  15. data/lib/docyard/components/processors/code_block_options_preprocessor.rb +23 -2
  16. data/lib/docyard/components/processors/code_block_processor.rb +6 -0
  17. data/lib/docyard/components/processors/code_group_processor.rb +198 -0
  18. data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +6 -1
  19. data/lib/docyard/components/processors/custom_anchor_processor.rb +42 -0
  20. data/lib/docyard/components/processors/file_tree_processor.rb +151 -0
  21. data/lib/docyard/components/processors/image_caption_processor.rb +96 -0
  22. data/lib/docyard/components/processors/include_processor.rb +86 -0
  23. data/lib/docyard/components/processors/steps_processor.rb +89 -0
  24. data/lib/docyard/components/processors/tabs_processor.rb +9 -1
  25. data/lib/docyard/components/processors/tooltip_processor.rb +57 -0
  26. data/lib/docyard/components/processors/video_embed_processor.rb +196 -0
  27. data/lib/docyard/components/support/code_group/html_builder.rb +122 -0
  28. data/lib/docyard/components/support/markdown_code_block_helper.rb +56 -0
  29. data/lib/docyard/config/branding_resolver.rb +121 -17
  30. data/lib/docyard/config/constants.rb +6 -4
  31. data/lib/docyard/config/logo_detector.rb +39 -0
  32. data/lib/docyard/config/validator.rb +122 -99
  33. data/lib/docyard/config.rb +40 -42
  34. data/lib/docyard/initializer.rb +15 -76
  35. data/lib/docyard/navigation/breadcrumb_builder.rb +133 -0
  36. data/lib/docyard/navigation/prev_next_builder.rb +4 -1
  37. data/lib/docyard/navigation/sidebar/children_discoverer.rb +51 -0
  38. data/lib/docyard/navigation/sidebar/config_parser.rb +136 -108
  39. data/lib/docyard/navigation/sidebar/file_resolver.rb +90 -0
  40. data/lib/docyard/navigation/sidebar/file_system_scanner.rb +2 -1
  41. data/lib/docyard/navigation/sidebar/item.rb +50 -7
  42. data/lib/docyard/navigation/sidebar/local_config_loader.rb +51 -0
  43. data/lib/docyard/navigation/sidebar/metadata_extractor.rb +71 -0
  44. data/lib/docyard/navigation/sidebar/metadata_reader.rb +51 -0
  45. data/lib/docyard/navigation/sidebar/path_prefixer.rb +34 -0
  46. data/lib/docyard/navigation/sidebar/renderer.rb +60 -38
  47. data/lib/docyard/navigation/sidebar/sorter.rb +21 -0
  48. data/lib/docyard/navigation/sidebar/tree_builder.rb +100 -26
  49. data/lib/docyard/navigation/sidebar/tree_filter.rb +55 -0
  50. data/lib/docyard/navigation/sidebar_builder.rb +105 -36
  51. data/lib/docyard/rendering/icon_helpers.rb +13 -0
  52. data/lib/docyard/rendering/icons/phosphor.rb +26 -1
  53. data/lib/docyard/rendering/markdown.rb +29 -1
  54. data/lib/docyard/rendering/renderer.rb +75 -34
  55. data/lib/docyard/rendering/template_resolver.rb +172 -0
  56. data/lib/docyard/routing/fallback_resolver.rb +92 -0
  57. data/lib/docyard/search/build_indexer.rb +1 -1
  58. data/lib/docyard/search/dev_indexer.rb +51 -6
  59. data/lib/docyard/search/pagefind_support.rb +2 -0
  60. data/lib/docyard/server/asset_handler.rb +25 -19
  61. data/lib/docyard/server/pagefind_handler.rb +63 -0
  62. data/lib/docyard/server/preview_server.rb +1 -1
  63. data/lib/docyard/server/rack_application.rb +81 -64
  64. data/lib/docyard/templates/assets/css/code.css +18 -51
  65. data/lib/docyard/templates/assets/css/components/abbreviation.css +86 -0
  66. data/lib/docyard/templates/assets/css/components/accordion.css +138 -0
  67. data/lib/docyard/templates/assets/css/components/badges.css +47 -0
  68. data/lib/docyard/templates/assets/css/components/banner.css +202 -0
  69. data/lib/docyard/templates/assets/css/components/breadcrumbs.css +143 -0
  70. data/lib/docyard/templates/assets/css/components/callout.css +67 -67
  71. data/lib/docyard/templates/assets/css/components/cards.css +100 -0
  72. data/lib/docyard/templates/assets/css/components/code-block.css +190 -282
  73. data/lib/docyard/templates/assets/css/components/code-group.css +281 -0
  74. data/lib/docyard/templates/assets/css/components/figure.css +22 -0
  75. data/lib/docyard/templates/assets/css/components/file-tree.css +124 -0
  76. data/lib/docyard/templates/assets/css/components/heading-anchor.css +36 -15
  77. data/lib/docyard/templates/assets/css/components/icon.css +0 -1
  78. data/lib/docyard/templates/assets/css/components/lightbox.css +65 -0
  79. data/lib/docyard/templates/assets/css/components/logo.css +0 -2
  80. data/lib/docyard/templates/assets/css/components/nav-menu.css +237 -0
  81. data/lib/docyard/templates/assets/css/components/navigation.css +193 -167
  82. data/lib/docyard/templates/assets/css/components/prev-next.css +68 -48
  83. data/lib/docyard/templates/assets/css/components/search.css +186 -174
  84. data/lib/docyard/templates/assets/css/components/steps.css +122 -0
  85. data/lib/docyard/templates/assets/css/components/tab-bar.css +163 -0
  86. data/lib/docyard/templates/assets/css/components/table-of-contents.css +127 -114
  87. data/lib/docyard/templates/assets/css/components/tabs.css +119 -160
  88. data/lib/docyard/templates/assets/css/components/theme-toggle.css +48 -44
  89. data/lib/docyard/templates/assets/css/components/tooltip.css +113 -0
  90. data/lib/docyard/templates/assets/css/components/video.css +41 -0
  91. data/lib/docyard/templates/assets/css/landing.css +815 -0
  92. data/lib/docyard/templates/assets/css/layout.css +489 -87
  93. data/lib/docyard/templates/assets/css/main.css +1 -3
  94. data/lib/docyard/templates/assets/css/markdown.css +113 -93
  95. data/lib/docyard/templates/assets/css/reset.css +0 -3
  96. data/lib/docyard/templates/assets/css/typography.css +43 -41
  97. data/lib/docyard/templates/assets/css/variables.css +268 -208
  98. data/lib/docyard/templates/assets/favicon.svg +7 -8
  99. data/lib/docyard/templates/assets/fonts/Inter-Variable.ttf +0 -0
  100. data/lib/docyard/templates/assets/js/components/abbreviation.js +85 -0
  101. data/lib/docyard/templates/assets/js/components/banner.js +81 -0
  102. data/lib/docyard/templates/assets/js/components/code-block.js +24 -42
  103. data/lib/docyard/templates/assets/js/components/code-group.js +283 -0
  104. data/lib/docyard/templates/assets/js/components/file-tree.js +39 -0
  105. data/lib/docyard/templates/assets/js/components/heading-anchor.js +26 -24
  106. data/lib/docyard/templates/assets/js/components/lightbox.js +72 -0
  107. data/lib/docyard/templates/assets/js/components/navigation.js +181 -70
  108. data/lib/docyard/templates/assets/js/components/search.js +0 -75
  109. data/lib/docyard/templates/assets/js/components/sidebar-toggle.js +29 -0
  110. data/lib/docyard/templates/assets/js/components/tab-navigation.js +145 -0
  111. data/lib/docyard/templates/assets/js/components/table-of-contents.js +153 -66
  112. data/lib/docyard/templates/assets/js/components/tabs.js +31 -69
  113. data/lib/docyard/templates/assets/js/components/tooltip.js +118 -0
  114. data/lib/docyard/templates/assets/js/theme.js +0 -3
  115. data/lib/docyard/templates/assets/logo-dark.svg +8 -2
  116. data/lib/docyard/templates/assets/logo.svg +7 -4
  117. data/lib/docyard/templates/config/docyard.yml.erb +37 -34
  118. data/lib/docyard/templates/errors/404.html.erb +1 -1
  119. data/lib/docyard/templates/errors/500.html.erb +1 -1
  120. data/lib/docyard/templates/layouts/default.html.erb +19 -67
  121. data/lib/docyard/templates/layouts/splash.html.erb +177 -0
  122. data/lib/docyard/templates/partials/_accordion.html.erb +9 -0
  123. data/lib/docyard/templates/partials/_banner.html.erb +27 -0
  124. data/lib/docyard/templates/partials/_breadcrumbs.html.erb +24 -0
  125. data/lib/docyard/templates/partials/_card.html.erb +23 -0
  126. data/lib/docyard/templates/partials/_code_block.html.erb +5 -3
  127. data/lib/docyard/templates/partials/_doc_footer.html.erb +25 -0
  128. data/lib/docyard/templates/partials/_features.html.erb +15 -0
  129. data/lib/docyard/templates/partials/_footer.html.erb +42 -0
  130. data/lib/docyard/templates/partials/_head.html.erb +22 -0
  131. data/lib/docyard/templates/partials/_header.html.erb +49 -0
  132. data/lib/docyard/templates/partials/_heading_anchor.html.erb +3 -1
  133. data/lib/docyard/templates/partials/_hero.html.erb +27 -0
  134. data/lib/docyard/templates/partials/_nav_group.html.erb +31 -11
  135. data/lib/docyard/templates/partials/_nav_leaf.html.erb +4 -1
  136. data/lib/docyard/templates/partials/_nav_menu.html.erb +42 -0
  137. data/lib/docyard/templates/partials/_nav_nested_section.html.erb +11 -0
  138. data/lib/docyard/templates/partials/_nav_section.html.erb +1 -1
  139. data/lib/docyard/templates/partials/_prev_next.html.erb +8 -2
  140. data/lib/docyard/templates/partials/_scripts.html.erb +7 -0
  141. data/lib/docyard/templates/partials/_search_modal.html.erb +2 -6
  142. data/lib/docyard/templates/partials/_search_trigger.html.erb +2 -6
  143. data/lib/docyard/templates/partials/_sidebar.html.erb +21 -4
  144. data/lib/docyard/templates/partials/_step.html.erb +14 -0
  145. data/lib/docyard/templates/partials/_tab_bar.html.erb +25 -0
  146. data/lib/docyard/templates/partials/_table_of_contents.html.erb +12 -12
  147. data/lib/docyard/templates/partials/_table_of_contents_toggle.html.erb +1 -3
  148. data/lib/docyard/templates/partials/_tabs.html.erb +2 -2
  149. data/lib/docyard/templates/partials/_theme_toggle.html.erb +2 -11
  150. data/lib/docyard/version.rb +1 -1
  151. metadata +70 -5
  152. data/lib/docyard/templates/markdown/getting-started/installation.md.erb +0 -77
  153. data/lib/docyard/templates/markdown/guides/configuration.md.erb +0 -202
  154. data/lib/docyard/templates/markdown/guides/markdown-features.md.erb +0 -247
  155. data/lib/docyard/templates/markdown/index.md.erb +0 -82
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ class TemplateResolver
5
+ BACKGROUNDS = %w[grid glow mesh none].freeze
6
+ DEFAULT_BACKGROUND = "grid"
7
+
8
+ attr_reader :frontmatter, :site_config
9
+
10
+ def initialize(frontmatter, site_config = {})
11
+ @frontmatter = frontmatter || {}
12
+ @site_config = site_config || {}
13
+ end
14
+
15
+ def landing?
16
+ landing_config.any?
17
+ end
18
+
19
+ def template
20
+ landing? ? "splash" : "default"
21
+ end
22
+
23
+ def show_sidebar?
24
+ if landing?
25
+ landing_config.fetch("sidebar", false)
26
+ else
27
+ true
28
+ end
29
+ end
30
+
31
+ def show_toc?
32
+ return false if landing?
33
+
34
+ true
35
+ end
36
+
37
+ def hero_config
38
+ return nil unless landing?
39
+
40
+ hero = landing_config["hero"]
41
+ return nil unless hero.is_a?(Hash)
42
+
43
+ symbolize_hero(hero)
44
+ end
45
+
46
+ def features_config
47
+ return nil unless landing?
48
+
49
+ features = landing_config["features"]
50
+ return nil unless features.is_a?(Array)
51
+
52
+ features.map { |f| symbolize_feature(f) }
53
+ end
54
+
55
+ def features_header_config
56
+ return nil unless landing?
57
+
58
+ header = landing_config["features_header"]
59
+ return nil unless header.is_a?(Hash)
60
+
61
+ {
62
+ label: header["label"],
63
+ title: header["title"],
64
+ description: header["description"]
65
+ }.compact
66
+ end
67
+
68
+ def footer_config
69
+ return nil unless landing?
70
+
71
+ footer = landing_config["footer"]
72
+ return nil unless footer.is_a?(Hash)
73
+
74
+ {
75
+ links: normalize_footer_links(footer["links"])
76
+ }
77
+ end
78
+
79
+ def to_options
80
+ {
81
+ template: template,
82
+ landing: landing?,
83
+ show_sidebar: show_sidebar?,
84
+ show_toc: show_toc?,
85
+ hero: hero_config,
86
+ features: features_config,
87
+ features_header: features_header_config,
88
+ footer: footer_config
89
+ }
90
+ end
91
+
92
+ private
93
+
94
+ def normalize_footer_links(links)
95
+ return nil unless links.is_a?(Array)
96
+
97
+ links.map do |link|
98
+ next unless link.is_a?(Hash)
99
+
100
+ { text: link["text"], link: link["link"] }
101
+ end.compact
102
+ end
103
+
104
+ def landing_config
105
+ @landing_config ||= frontmatter["landing"] || site_config["landing"] || {}
106
+ end
107
+
108
+ def symbolize_hero(hero)
109
+ background = hero["background"]
110
+ validated_bg = BACKGROUNDS.include?(background) ? background : DEFAULT_BACKGROUND
111
+
112
+ {
113
+ background: validated_bg,
114
+ badge: hero["badge"],
115
+ name: hero["name"],
116
+ title: hero["title"],
117
+ tagline: hero["tagline"],
118
+ gradient: hero.fetch("gradient", true),
119
+ image: symbolize_image(hero["image"]),
120
+ actions: symbolize_actions(hero["actions"])
121
+ }.compact
122
+ end
123
+
124
+ def symbolize_image(image)
125
+ return nil unless image.is_a?(Hash)
126
+
127
+ if image["light"] || image["dark"]
128
+ {
129
+ light: image["light"],
130
+ dark: image["dark"],
131
+ alt: image["alt"]
132
+ }.compact
133
+ else
134
+ {
135
+ src: image["src"],
136
+ alt: image["alt"]
137
+ }.compact
138
+ end
139
+ end
140
+
141
+ def symbolize_actions(actions)
142
+ return nil unless actions.is_a?(Array)
143
+
144
+ actions.map do |action|
145
+ {
146
+ text: action["text"],
147
+ link: action["link"],
148
+ icon: action["icon"],
149
+ variant: action["variant"] || "primary",
150
+ target: action["target"],
151
+ rel: action["rel"]
152
+ }.compact
153
+ end
154
+ end
155
+
156
+ def symbolize_feature(feature)
157
+ return {} unless feature.is_a?(Hash)
158
+
159
+ {
160
+ title: feature["title"],
161
+ description: feature["description"],
162
+ icon: feature["icon"],
163
+ color: feature["color"],
164
+ link: feature["link"],
165
+ link_text: feature["link_text"],
166
+ size: feature["size"],
167
+ target: feature["target"],
168
+ rel: feature["rel"]
169
+ }.compact
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Routing
5
+ class FallbackResolver
6
+ attr_reader :docs_path, :sidebar_builder
7
+
8
+ def initialize(docs_path:, sidebar_builder:)
9
+ @docs_path = docs_path
10
+ @sidebar_builder = sidebar_builder
11
+ end
12
+
13
+ def resolve_fallback(request_path)
14
+ return nil if file_exists?(request_path)
15
+
16
+ find_first_item_in_section(request_path)
17
+ end
18
+
19
+ private
20
+
21
+ def file_exists?(request_path)
22
+ clean_path = sanitize_path(request_path)
23
+
24
+ file_path = File.join(docs_path, "#{clean_path}.md")
25
+ return true if File.file?(file_path)
26
+
27
+ index_path = File.join(docs_path, clean_path, "index.md")
28
+ File.file?(index_path)
29
+ end
30
+
31
+ 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")
35
+ end
36
+
37
+ def find_first_item_in_section(request_path)
38
+ tree = sidebar_builder.tree
39
+
40
+ if root_path?(request_path)
41
+ find_first_navigable_item(tree)
42
+ else
43
+ section = find_section_in_tree(tree, request_path)
44
+ section ? find_first_navigable_item(section[:children] || []) : nil
45
+ end
46
+ end
47
+
48
+ def root_path?(request_path)
49
+ request_path.nil? || request_path == "/" || request_path.empty?
50
+ end
51
+
52
+ def find_section_in_tree(tree, path)
53
+ normalized_path = normalize_path(path)
54
+
55
+ tree.each do |item|
56
+ return item if path_matches_section?(item, normalized_path)
57
+
58
+ if item[:children]&.any?
59
+ found = find_section_in_tree(item[:children], path)
60
+ return found if found
61
+ end
62
+ end
63
+
64
+ nil
65
+ end
66
+
67
+ def normalize_path(path)
68
+ path.to_s.delete_prefix("/").delete_suffix("/").downcase
69
+ end
70
+
71
+ def path_matches_section?(item, normalized_path)
72
+ return false unless item[:type] == :directory
73
+
74
+ item_path = item[:title].to_s.downcase.gsub(/\s+/, "-")
75
+ item_path == normalized_path
76
+ end
77
+
78
+ def find_first_navigable_item(items)
79
+ items.each do |item|
80
+ return item[:path] if item[:path] && item[:type] == :file
81
+
82
+ if item[:children]&.any?
83
+ found = find_first_navigable_item(item[:children])
84
+ return found if found
85
+ end
86
+ end
87
+
88
+ nil
89
+ end
90
+ end
91
+ end
92
+ end
@@ -13,7 +13,7 @@ module Docyard
13
13
 
14
14
  def initialize(config, verbose: false)
15
15
  @config = config
16
- @output_dir = config.build.output_dir
16
+ @output_dir = config.build.output
17
17
  @verbose = verbose
18
18
  end
19
19
 
@@ -25,10 +25,10 @@ module Docyard
25
25
 
26
26
  @temp_dir = Dir.mktmpdir("docyard-search-")
27
27
  generate_html_files
28
- run_pagefind
28
+ page_count = run_pagefind
29
29
  @pagefind_path = File.join(temp_dir, "pagefind")
30
30
 
31
- log_success
31
+ log_success(page_count)
32
32
  pagefind_path
33
33
  rescue StandardError => e
34
34
  warn "[!] Search index generation failed: #{e.message}"
@@ -52,6 +52,8 @@ module Docyard
52
52
 
53
53
  def generate_html_files
54
54
  markdown_files = Dir.glob(File.join(docs_path, "**", "*.md"))
55
+ markdown_files = filter_excluded_files(markdown_files)
56
+ markdown_files = filter_non_indexable_files(markdown_files)
55
57
  renderer = Renderer.new(base_url: "/", config: config)
56
58
 
57
59
  progress = TTY::ProgressBar.new(
@@ -66,6 +68,45 @@ module Docyard
66
68
  end
67
69
  end
68
70
 
71
+ def filter_excluded_files(files)
72
+ exclude_patterns = config.search.exclude || []
73
+ return files if exclude_patterns.empty?
74
+
75
+ files.reject do |file_path|
76
+ url_path = file_to_url_path(file_path)
77
+ exclude_patterns.any? { |pattern| File.fnmatch(pattern, url_path, File::FNM_PATHNAME) }
78
+ end
79
+ end
80
+
81
+ def filter_non_indexable_files(files)
82
+ files.reject do |file_path|
83
+ content = File.read(file_path)
84
+ markdown = Markdown.new(content)
85
+ frontmatter = markdown.frontmatter
86
+
87
+ uses_splash_template?(frontmatter)
88
+ end
89
+ end
90
+
91
+ def uses_splash_template?(frontmatter)
92
+ return true if frontmatter["template"] == "splash"
93
+ return true if frontmatter.key?("landing")
94
+
95
+ frontmatter.key?("hero") || frontmatter.key?("features")
96
+ end
97
+
98
+ def file_to_url_path(file_path)
99
+ relative_path = file_path.delete_prefix("#{docs_path}/")
100
+ base_name = File.basename(relative_path, ".md")
101
+ dir_name = File.dirname(relative_path)
102
+
103
+ if base_name == "index"
104
+ dir_name == "." ? "/" : "/#{dir_name}"
105
+ else
106
+ dir_name == "." ? "/#{base_name}" : "/#{dir_name}/#{base_name}"
107
+ end
108
+ end
109
+
69
110
  def generate_html_file(markdown_file, renderer)
70
111
  relative_path = markdown_file.delete_prefix("#{docs_path}/")
71
112
  output_path = determine_output_path(relative_path)
@@ -97,12 +138,16 @@ module Docyard
97
138
 
98
139
  raise "Pagefind failed: #{stderr}" unless status.success?
99
140
 
100
- stdout
141
+ extract_page_count(stdout)
142
+ end
143
+
144
+ def extract_page_count(output)
145
+ match = output.match(/Indexed (\d+) page/i)
146
+ match ? match[1].to_i : 0
101
147
  end
102
148
 
103
- def log_success
104
- page_count = Dir.glob(File.join(temp_dir, "**", "*.html")).size
105
- puts "=> Search index generated (#{page_count} pages)"
149
+ def log_success(page_count)
150
+ puts "=> Search index generated (#{page_count} pages indexed)"
106
151
  puts "=> Temp directory: #{temp_dir}" if ENV["DOCYARD_DEBUG"]
107
152
  end
108
153
  end
@@ -21,6 +21,8 @@ module Docyard
21
21
 
22
22
  exclusions = config.search.exclude || []
23
23
  exclusions.each do |pattern|
24
+ next if pattern.start_with?("/")
25
+
24
26
  args += ["--exclude-selectors", pattern]
25
27
  end
26
28
 
@@ -2,8 +2,7 @@
2
2
 
3
3
  module Docyard
4
4
  class AssetHandler
5
- ASSETS_PATH = File.join(__dir__, "../templates", "assets")
6
- USER_ASSETS_PATH = "docs/assets"
5
+ TEMPLATES_ASSETS_PATH = File.join(__dir__, "../templates", "assets")
7
6
 
8
7
  CONTENT_TYPES = {
9
8
  ".css" => "text/css; charset=utf-8",
@@ -11,41 +10,48 @@ module Docyard
11
10
  ".png" => "image/png",
12
11
  ".jpg" => "image/jpeg",
13
12
  ".jpeg" => "image/jpeg",
13
+ ".gif" => "image/gif",
14
+ ".webp" => "image/webp",
15
+ ".avif" => "image/avif",
14
16
  ".svg" => "image/svg+xml",
15
- ".woff" => "font/woff2",
17
+ ".woff" => "font/woff",
16
18
  ".woff2" => "font/woff2",
17
- ".ico" => "image/x-icon"
19
+ ".ttf" => "font/ttf",
20
+ ".ico" => "image/x-icon",
21
+ ".pdf" => "application/pdf",
22
+ ".mp4" => "video/mp4",
23
+ ".webm" => "video/webm"
18
24
  }.freeze
19
25
 
20
- def serve(request_path)
21
- asset_path = extract_asset_path(request_path)
26
+ def serve_docyard_assets(request_path)
27
+ asset_path = request_path.delete_prefix("/_docyard/")
22
28
 
23
29
  return forbidden_response if directory_traversal?(asset_path)
24
30
 
25
31
  return serve_components_css if asset_path == "css/components.css"
26
32
  return serve_components_js if asset_path == "js/components.js"
27
33
 
28
- file_path = build_file_path(asset_path)
34
+ file_path = File.join(TEMPLATES_ASSETS_PATH, asset_path)
29
35
  return not_found_response unless File.file?(file_path)
30
36
 
31
37
  serve_file(file_path)
32
38
  end
33
39
 
34
- private
40
+ def serve_public_file(request_path)
41
+ asset_path = request_path.delete_prefix("/")
35
42
 
36
- def extract_asset_path(request_path)
37
- request_path.delete_prefix("/assets/")
38
- end
43
+ return nil if directory_traversal?(asset_path)
39
44
 
40
- def directory_traversal?(path)
41
- path.include?("..")
45
+ file_path = File.join(Constants::PUBLIC_DIR, asset_path)
46
+ return nil unless File.file?(file_path)
47
+
48
+ serve_file(file_path)
42
49
  end
43
50
 
44
- def build_file_path(asset_path)
45
- user_path = File.join(USER_ASSETS_PATH, asset_path)
46
- return user_path if File.file?(user_path)
51
+ private
47
52
 
48
- File.join(ASSETS_PATH, asset_path)
53
+ def directory_traversal?(path)
54
+ path.include?("..")
49
55
  end
50
56
 
51
57
  def serve_file(file_path)
@@ -61,7 +67,7 @@ module Docyard
61
67
  end
62
68
 
63
69
  def concatenate_component_css
64
- components_dir = File.join(ASSETS_PATH, "css", "components")
70
+ components_dir = File.join(TEMPLATES_ASSETS_PATH, "css", "components")
65
71
  return "" unless Dir.exist?(components_dir)
66
72
 
67
73
  css_files = Dir.glob(File.join(components_dir, "*.css"))
@@ -74,7 +80,7 @@ module Docyard
74
80
  end
75
81
 
76
82
  def concatenate_component_js
77
- components_dir = File.join(ASSETS_PATH, "js", "components")
83
+ components_dir = File.join(TEMPLATES_ASSETS_PATH, "js", "components")
78
84
  return "" unless Dir.exist?(components_dir)
79
85
 
80
86
  js_files = Dir.glob(File.join(components_dir, "*.js"))
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ class PagefindHandler
5
+ CONTENT_TYPES = {
6
+ ".js" => "application/javascript; charset=utf-8",
7
+ ".css" => "text/css; charset=utf-8",
8
+ ".json" => "application/json; charset=utf-8"
9
+ }.freeze
10
+
11
+ def initialize(pagefind_path:, config:)
12
+ @pagefind_path = pagefind_path
13
+ @config = config
14
+ end
15
+
16
+ def serve(path)
17
+ relative_path = path.delete_prefix(Constants::PAGEFIND_PREFIX)
18
+ return not_found if relative_path.include?("..")
19
+
20
+ file_path = resolve_file(relative_path)
21
+ return not_found unless file_path && File.file?(file_path)
22
+
23
+ serve_file(file_path)
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :pagefind_path, :config
29
+
30
+ def resolve_file(relative_path)
31
+ return File.join(pagefind_path, relative_path) if pagefind_path && Dir.exist?(pagefind_path)
32
+
33
+ output_dir = config&.build&.output_dir || "dist"
34
+ File.join(output_dir, "pagefind", relative_path)
35
+ end
36
+
37
+ def serve_file(file_path)
38
+ content = File.binread(file_path)
39
+ content_type = content_type_for(file_path)
40
+
41
+ [Constants::STATUS_OK, build_headers(content_type), [content]]
42
+ end
43
+
44
+ def build_headers(content_type)
45
+ {
46
+ "Content-Type" => content_type,
47
+ "Cache-Control" => "no-cache, no-store, must-revalidate",
48
+ "Pragma" => "no-cache",
49
+ "Expires" => "0"
50
+ }
51
+ end
52
+
53
+ def content_type_for(file_path)
54
+ extension = File.extname(file_path)
55
+ CONTENT_TYPES.fetch(extension, "application/octet-stream")
56
+ end
57
+
58
+ def not_found
59
+ message = "Pagefind not found. Run 'docyard build' first."
60
+ [Constants::STATUS_NOT_FOUND, { "Content-Type" => "text/plain" }, [message]]
61
+ end
62
+ end
63
+ end
@@ -12,7 +12,7 @@ module Docyard
12
12
  def initialize(port: DEFAULT_PORT)
13
13
  @port = port
14
14
  @config = Config.load
15
- @output_dir = File.expand_path(@config.build.output_dir)
15
+ @output_dir = File.expand_path(@config.build.output)
16
16
  end
17
17
 
18
18
  def start