docyard 0.8.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -1
  3. data/README.md +8 -253
  4. data/exe/docyard +6 -0
  5. data/lib/docyard/build/asset_bundler.rb +2 -2
  6. data/lib/docyard/build/file_copier.rb +12 -5
  7. data/lib/docyard/build/llms_txt_generator.rb +103 -0
  8. data/lib/docyard/build/sitemap_generator.rb +1 -1
  9. data/lib/docyard/build/static_generator.rb +115 -79
  10. data/lib/docyard/builder.rb +6 -2
  11. data/lib/docyard/cli.rb +14 -4
  12. data/lib/docyard/components/aliases.rb +12 -0
  13. data/lib/docyard/components/processors/abbreviation_processor.rb +72 -0
  14. data/lib/docyard/components/processors/accordion_processor.rb +81 -0
  15. data/lib/docyard/components/processors/badge_processor.rb +72 -0
  16. data/lib/docyard/components/processors/callout_processor.rb +9 -3
  17. data/lib/docyard/components/processors/cards_processor.rb +100 -0
  18. data/lib/docyard/components/processors/code_block_extended_fence_postprocessor.rb +24 -0
  19. data/lib/docyard/components/processors/code_block_extended_fence_preprocessor.rb +44 -0
  20. data/lib/docyard/components/processors/code_block_options_preprocessor.rb +34 -3
  21. data/lib/docyard/components/processors/code_block_processor.rb +11 -24
  22. data/lib/docyard/components/processors/code_group_processor.rb +182 -0
  23. data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +7 -1
  24. data/lib/docyard/components/processors/custom_anchor_processor.rb +42 -0
  25. data/lib/docyard/components/processors/file_tree_processor.rb +150 -0
  26. data/lib/docyard/components/processors/icon_processor.rb +8 -2
  27. data/lib/docyard/components/processors/image_caption_processor.rb +96 -0
  28. data/lib/docyard/components/processors/include_processor.rb +86 -0
  29. data/lib/docyard/components/processors/steps_processor.rb +89 -0
  30. data/lib/docyard/components/processors/tabs_processor.rb +9 -1
  31. data/lib/docyard/components/processors/tooltip_processor.rb +57 -0
  32. data/lib/docyard/components/processors/video_embed_processor.rb +207 -0
  33. data/lib/docyard/components/support/code_block/feature_extractor.rb +3 -1
  34. data/lib/docyard/components/support/code_block/icon_detector.rb +5 -12
  35. data/lib/docyard/components/support/code_block/line_number_resolver.rb +30 -0
  36. data/lib/docyard/components/support/code_detector.rb +2 -12
  37. data/lib/docyard/components/support/code_group/html_builder.rb +118 -0
  38. data/lib/docyard/components/support/markdown_code_block_helper.rb +56 -0
  39. data/lib/docyard/components/support/tabs/icon_detector.rb +6 -2
  40. data/lib/docyard/components/support/tabs/parser.rb +6 -23
  41. data/lib/docyard/config/analytics_resolver.rb +24 -0
  42. data/lib/docyard/config/branding_resolver.rb +84 -58
  43. data/lib/docyard/config/key_validator.rb +30 -0
  44. data/lib/docyard/config/logo_detector.rb +39 -0
  45. data/lib/docyard/config/schema.rb +39 -0
  46. data/lib/docyard/config/section.rb +21 -0
  47. data/lib/docyard/config/validation_helpers.rb +83 -0
  48. data/lib/docyard/config/validator.rb +45 -144
  49. data/lib/docyard/config/validators/navigation.rb +43 -0
  50. data/lib/docyard/config/validators/section.rb +114 -0
  51. data/lib/docyard/config.rb +45 -96
  52. data/lib/docyard/constants.rb +59 -0
  53. data/lib/docyard/{utils/errors.rb → errors.rb} +6 -0
  54. data/lib/docyard/initializer.rb +100 -49
  55. data/lib/docyard/navigation/page_navigation_builder.rb +65 -0
  56. data/lib/docyard/navigation/sidebar/auto_builder.rb +107 -0
  57. data/lib/docyard/navigation/sidebar/cache.rb +96 -0
  58. data/lib/docyard/navigation/sidebar/config_builder.rb +179 -0
  59. data/lib/docyard/navigation/sidebar/distributed_builder.rb +145 -0
  60. data/lib/docyard/navigation/sidebar/item.rb +6 -1
  61. data/lib/docyard/navigation/sidebar/local_config_loader.rb +69 -3
  62. data/lib/docyard/navigation/sidebar/renderer.rb +18 -3
  63. data/lib/docyard/navigation/sidebar_builder.rb +43 -81
  64. data/lib/docyard/rendering/branding_variables.rb +65 -0
  65. data/lib/docyard/rendering/icon_helpers.rb +14 -1
  66. data/lib/docyard/rendering/icons/devicons.rb +63 -0
  67. data/lib/docyard/rendering/icons.rb +26 -27
  68. data/lib/docyard/rendering/markdown.rb +20 -15
  69. data/lib/docyard/rendering/og_helpers.rb +36 -0
  70. data/lib/docyard/rendering/renderer.rb +87 -58
  71. data/lib/docyard/rendering/template_resolver.rb +14 -0
  72. data/lib/docyard/routing/fallback_resolver.rb +3 -3
  73. data/lib/docyard/search/build_indexer.rb +2 -2
  74. data/lib/docyard/search/dev_indexer.rb +36 -28
  75. data/lib/docyard/search/pagefind_support.rb +1 -1
  76. data/lib/docyard/server/asset_handler.rb +40 -15
  77. data/lib/docyard/server/dev_server.rb +90 -55
  78. data/lib/docyard/server/file_watcher.rb +68 -18
  79. data/lib/docyard/server/pagefind_handler.rb +1 -1
  80. data/lib/docyard/server/preview_server.rb +29 -33
  81. data/lib/docyard/server/rack_application.rb +38 -70
  82. data/lib/docyard/server/router.rb +11 -7
  83. data/lib/docyard/server/sse_server.rb +157 -0
  84. data/lib/docyard/server/static_file_app.rb +42 -0
  85. data/lib/docyard/templates/assets/css/components/abbreviation.css +86 -0
  86. data/lib/docyard/templates/assets/css/components/accordion.css +138 -0
  87. data/lib/docyard/templates/assets/css/components/badges.css +47 -0
  88. data/lib/docyard/templates/assets/css/components/banner.css +233 -0
  89. data/lib/docyard/templates/assets/css/components/breadcrumbs.css +2 -1
  90. data/lib/docyard/templates/assets/css/components/callout.css +26 -6
  91. data/lib/docyard/templates/assets/css/components/cards.css +100 -0
  92. data/lib/docyard/templates/assets/css/components/code-block.css +14 -2
  93. data/lib/docyard/templates/assets/css/components/code-group.css +294 -0
  94. data/lib/docyard/templates/assets/css/components/feedback.css +126 -0
  95. data/lib/docyard/templates/assets/css/components/figure.css +22 -0
  96. data/lib/docyard/templates/assets/css/components/file-tree.css +125 -0
  97. data/lib/docyard/templates/assets/css/components/heading-anchor.css +21 -13
  98. data/lib/docyard/templates/assets/css/components/icon.css +5 -0
  99. data/lib/docyard/templates/assets/css/components/lightbox.css +65 -0
  100. data/lib/docyard/templates/assets/css/components/nav-menu.css +20 -4
  101. data/lib/docyard/templates/assets/css/components/navigation.css +32 -3
  102. data/lib/docyard/templates/assets/css/components/page-actions.css +131 -0
  103. data/lib/docyard/templates/assets/css/components/prev-next.css +20 -22
  104. data/lib/docyard/templates/assets/css/components/search.css +6 -10
  105. data/lib/docyard/templates/assets/css/components/steps.css +122 -0
  106. data/lib/docyard/templates/assets/css/components/tab-bar.css +7 -4
  107. data/lib/docyard/templates/assets/css/components/table-of-contents.css +57 -11
  108. data/lib/docyard/templates/assets/css/components/tabs.css +13 -5
  109. data/lib/docyard/templates/assets/css/components/theme-toggle.css +3 -1
  110. data/lib/docyard/templates/assets/css/components/tooltip.css +113 -0
  111. data/lib/docyard/templates/assets/css/components/video.css +41 -0
  112. data/lib/docyard/templates/assets/css/landing.css +82 -13
  113. data/lib/docyard/templates/assets/css/layout.css +17 -0
  114. data/lib/docyard/templates/assets/css/markdown.css +25 -3
  115. data/lib/docyard/templates/assets/css/variables.css +13 -1
  116. data/lib/docyard/templates/assets/js/components/abbreviation.js +85 -0
  117. data/lib/docyard/templates/assets/js/components/banner.js +81 -0
  118. data/lib/docyard/templates/assets/js/components/code-group.js +286 -0
  119. data/lib/docyard/templates/assets/js/components/copy-page.js +115 -0
  120. data/lib/docyard/templates/assets/js/components/feedback.js +66 -0
  121. data/lib/docyard/templates/assets/js/components/file-tree.js +39 -0
  122. data/lib/docyard/templates/assets/js/components/lightbox.js +72 -0
  123. data/lib/docyard/templates/assets/js/components/navigation.js +3 -3
  124. data/lib/docyard/templates/assets/js/components/search.js +3 -3
  125. data/lib/docyard/templates/assets/js/components/table-of-contents.js +12 -6
  126. data/lib/docyard/templates/assets/js/components/tabs.js +45 -22
  127. data/lib/docyard/templates/assets/js/components/tooltip.js +118 -0
  128. data/lib/docyard/templates/assets/js/hot-reload.js +44 -0
  129. data/lib/docyard/templates/errors/404.html.erb +114 -5
  130. data/lib/docyard/templates/errors/500.html.erb +173 -10
  131. data/lib/docyard/templates/init/_sidebar.yml +36 -0
  132. data/lib/docyard/templates/init/docyard.yml +36 -0
  133. data/lib/docyard/templates/init/pages/components.md +146 -0
  134. data/lib/docyard/templates/init/pages/getting-started.md +94 -0
  135. data/lib/docyard/templates/init/pages/index.md +22 -0
  136. data/lib/docyard/templates/layouts/default.html.erb +11 -0
  137. data/lib/docyard/templates/layouts/splash.html.erb +15 -1
  138. data/lib/docyard/templates/partials/_accordion.html.erb +9 -0
  139. data/lib/docyard/templates/partials/_analytics.html.erb +24 -0
  140. data/lib/docyard/templates/partials/_banner.html.erb +27 -0
  141. data/lib/docyard/templates/partials/_card.html.erb +23 -0
  142. data/lib/docyard/templates/partials/_code_block.html.erb +1 -1
  143. data/lib/docyard/templates/partials/_feedback.html.erb +14 -0
  144. data/lib/docyard/templates/partials/_footer.html.erb +1 -1
  145. data/lib/docyard/templates/partials/_head.html.erb +79 -4
  146. data/lib/docyard/templates/partials/_icon_library.html.erb +8 -0
  147. data/lib/docyard/templates/partials/_nav_group.html.erb +6 -0
  148. data/lib/docyard/templates/partials/_nav_leaf.html.erb +3 -0
  149. data/lib/docyard/templates/partials/_page_actions.html.erb +21 -0
  150. data/lib/docyard/templates/partials/_scripts.html.erb +6 -3
  151. data/lib/docyard/templates/partials/_step.html.erb +14 -0
  152. data/lib/docyard/templates/partials/_tabs.html.erb +4 -1
  153. data/lib/docyard/utils/git_info.rb +157 -0
  154. data/lib/docyard/utils/hash_utils.rb +31 -0
  155. data/lib/docyard/utils/html_helpers.rb +8 -0
  156. data/lib/docyard/utils/logging.rb +44 -3
  157. data/lib/docyard/utils/path_resolver.rb +0 -10
  158. data/lib/docyard/utils/path_utils.rb +73 -0
  159. data/lib/docyard/version.rb +1 -1
  160. data/lib/docyard.rb +2 -2
  161. metadata +114 -47
  162. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -31
  163. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  164. data/.github/pull_request_template.md +0 -14
  165. data/.github/workflows/ci.yml +0 -49
  166. data/.rubocop.yml +0 -42
  167. data/CODE_OF_CONDUCT.md +0 -132
  168. data/CONTRIBUTING.md +0 -55
  169. data/LICENSE.vscode-icons +0 -42
  170. data/Rakefile +0 -8
  171. data/lib/docyard/config/constants.rb +0 -31
  172. data/lib/docyard/navigation/sidebar/children_discoverer.rb +0 -51
  173. data/lib/docyard/navigation/sidebar/config_parser.rb +0 -208
  174. data/lib/docyard/navigation/sidebar/file_resolver.rb +0 -78
  175. data/lib/docyard/navigation/sidebar/file_system_scanner.rb +0 -78
  176. data/lib/docyard/navigation/sidebar/metadata_extractor.rb +0 -69
  177. data/lib/docyard/navigation/sidebar/metadata_reader.rb +0 -47
  178. data/lib/docyard/navigation/sidebar/path_prefixer.rb +0 -34
  179. data/lib/docyard/navigation/sidebar/sorter.rb +0 -21
  180. data/lib/docyard/navigation/sidebar/title_extractor.rb +0 -25
  181. data/lib/docyard/navigation/sidebar/tree_builder.rb +0 -139
  182. data/lib/docyard/rendering/icons/LICENSE.phosphor +0 -21
  183. data/lib/docyard/rendering/icons/file_types.rb +0 -79
  184. data/lib/docyard/rendering/icons/phosphor.rb +0 -90
  185. data/lib/docyard/rendering/language_mapping.rb +0 -52
  186. data/lib/docyard/templates/assets/js/reload.js +0 -98
  187. data/lib/docyard/templates/partials/_icon.html.erb +0 -1
  188. data/lib/docyard/templates/partials/_icon_file_extension.html.erb +0 -1
  189. data/sig/docyard.rbs +0 -4
@@ -2,7 +2,7 @@
2
2
  <% if has_title %>
3
3
  <div class="docyard-code-block__header">
4
4
  <% if @icon %>
5
- <span class="docyard-code-block__icon"><% if @icon_source == "file-extension" %><%= icon_file_extension(@icon) %><% elsif @icon_source == "phosphor" %><%= icon(@icon) %><% end %></span>
5
+ <span class="docyard-code-block__icon"><% if @icon_source == "language" %><%= icon_for_language(@icon) %><% elsif @icon_source == "file-extension" %><%= icon_file_extension(@icon) %><% elsif @icon_source == "phosphor" %><%= icon(@icon) %><% end %></span>
6
6
  <% end %>
7
7
  <span class="docyard-code-block__title" title="<%= @title %>"><%= @title %></span>
8
8
  <button class="docyard-code-block__copy" aria-label="Copy code to clipboard" data-code="<%= @code_text %>">
@@ -0,0 +1,14 @@
1
+ <% if @feedback_enabled %>
2
+ <div class="feedback" data-pagefind-ignore>
3
+ <p class="feedback__question"><%= @feedback_question %></p>
4
+ <div class="feedback__buttons">
5
+ <button type="button" class="feedback__btn" data-feedback="yes" aria-label="Yes, this page was helpful">
6
+ <%= icon(:thumbs_up) %>
7
+ </button>
8
+ <button type="button" class="feedback__btn" data-feedback="no" aria-label="No, this page was not helpful">
9
+ <%= icon(:thumbs_down) %>
10
+ </button>
11
+ </div>
12
+ <p class="feedback__thanks" aria-live="polite" hidden>Thanks for your feedback!</p>
13
+ </div>
14
+ <% end %>
@@ -17,7 +17,7 @@
17
17
  %>
18
18
  <div class="<%= footer_class %>">
19
19
  <% if @credits %>
20
- <a href="https://docyard.org" target="_blank" rel="noopener noreferrer" class="footer-attribution">Built with Docyard</a>
20
+ <a href="https://docyard.dev" target="_blank" rel="noopener noreferrer" class="footer-attribution">Built with Docyard</a>
21
21
  <% end %>
22
22
 
23
23
  <% if has_footer_links %>
@@ -5,18 +5,93 @@
5
5
  <title><%= @page_title %> | <%= @site_title %></title>
6
6
  <link rel="icon" href="<%= asset_path(@favicon) %>" type="image/svg+xml">
7
7
 
8
+ <% if @og_enabled %>
9
+ <link rel="canonical" href="<%= @og_url %>">
10
+
11
+ <meta property="og:type" content="website">
12
+ <meta property="og:site_name" content="<%= @site_title %>">
13
+ <meta property="og:title" content="<%= @page_title %>">
14
+ <meta property="og:url" content="<%= @og_url %>">
15
+ <% if @og_description && !@og_description.empty? %>
16
+ <meta property="og:description" content="<%= @og_description %>">
17
+ <% end %>
18
+ <% if @og_image %>
19
+ <meta property="og:image" content="<%= @og_image %>">
20
+ <% end %>
21
+
22
+ <meta name="twitter:card" content="<%= @og_image ? 'summary_large_image' : 'summary' %>">
23
+ <meta name="twitter:title" content="<%= @page_title %>">
24
+ <% if @og_description && !@og_description.empty? %>
25
+ <meta name="twitter:description" content="<%= @og_description %>">
26
+ <% end %>
27
+ <% if @og_image %>
28
+ <meta name="twitter:image" content="<%= @og_image %>">
29
+ <% end %>
30
+ <% if @og_twitter %>
31
+ <meta name="twitter:site" content="@<%= @og_twitter.delete_prefix('@') %>">
32
+ <% end %>
33
+ <% end %>
34
+
8
35
  <link rel="preload" href="/_docyard/fonts/Inter-Variable.ttf" as="font" type="font/ttf" crossorigin>
9
36
 
37
+ <%= render_partial('_icon_library') %>
38
+
10
39
  <script>
11
40
  (function() {
12
- const theme = localStorage.getItem('theme') ||
13
- (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
14
- document.documentElement.classList.toggle('dark', theme === 'dark');
41
+ var html = document.documentElement;
42
+
43
+ html.classList.add('no-transition');
44
+
45
+ var theme = localStorage.getItem('theme') ||
46
+ (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
47
+ html.classList.toggle('dark', theme === 'dark');
15
48
 
16
49
  if (localStorage.getItem('docyard_sidebar_collapsed') === 'true') {
17
- document.documentElement.classList.add('sidebar-collapsed');
50
+ html.classList.add('sidebar-collapsed');
18
51
  }
52
+
53
+ var BANNER_KEY = 'docyard-announcement-dismissed';
54
+ try {
55
+ var dismissed = localStorage.getItem(BANNER_KEY);
56
+ if (dismissed) {
57
+ var dismissedAt = parseInt(dismissed, 10);
58
+ var sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
59
+ if (dismissedAt > sevenDaysAgo) {
60
+ html.classList.add('banner-dismissed');
61
+ }
62
+ }
63
+ } catch (e) {}
64
+
65
+ requestAnimationFrame(function() {
66
+ requestAnimationFrame(function() {
67
+ html.classList.remove('no-transition');
68
+ });
69
+ });
19
70
  })();
20
71
  </script>
21
72
 
22
73
  <link rel="stylesheet" href="/_docyard/css/main.css">
74
+
75
+ <% if @primary_color && (@primary_color[:light] || @primary_color[:dark]) %>
76
+ <style>
77
+ <% light_color = @primary_color[:light] %>
78
+ <% dark_color = @primary_color[:dark] || light_color %>
79
+ <% if light_color %>
80
+ :root {
81
+ --primary: <%= light_color %>;
82
+ --sidebar-primary: <%= light_color %>;
83
+ }
84
+ .dark {
85
+ --primary: <%= dark_color %>;
86
+ --sidebar-primary: <%= dark_color %>;
87
+ }
88
+ <% elsif dark_color %>
89
+ .dark {
90
+ --primary: <%= dark_color %>;
91
+ --sidebar-primary: <%= dark_color %>;
92
+ }
93
+ <% end %>
94
+ </style>
95
+ <% end %>
96
+
97
+ <%= render_partial('_analytics') %>
@@ -0,0 +1,8 @@
1
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@phosphor-icons/web@2.1.2/src/regular/style.css">
2
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@phosphor-icons/web@2.1.2/src/bold/style.css">
3
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@phosphor-icons/web@2.1.2/src/fill/style.css">
4
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@phosphor-icons/web@2.1.2/src/light/style.css">
5
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@phosphor-icons/web@2.1.2/src/thin/style.css">
6
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@phosphor-icons/web@2.1.2/src/duotone/style.css">
7
+
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/devicons/devicon@latest/devicon.min.css">
@@ -7,6 +7,9 @@
7
7
  <span class="nav-item-icon"><%= icon(@icon) %></span>
8
8
  <% end %>
9
9
  <span class="nav-item-text"><%= @title %></span>
10
+ <% if @badge %>
11
+ <span class="docyard-badge docyard-badge--<%= @badge_type || 'default' %>"><%= @badge %></span>
12
+ <% end %>
10
13
  </span>
11
14
  <span class="nav-group-icon"><%= icon(:caret_right) %></span>
12
15
  </a>
@@ -17,6 +20,9 @@
17
20
  <span class="nav-item-icon"><%= icon(@icon) %></span>
18
21
  <% end %>
19
22
  <span class="nav-item-text"><%= @title %></span>
23
+ <% if @badge %>
24
+ <span class="docyard-badge docyard-badge--<%= @badge_type || 'default' %>"><%= @badge %></span>
25
+ <% end %>
20
26
  </span>
21
27
  <span class="nav-group-icon"><%= icon(:caret_right) %></span>
22
28
  </button>
@@ -3,6 +3,9 @@
3
3
  <span class="nav-item-icon"><%= icon(@icon) %></span>
4
4
  <% end %>
5
5
  <span class="nav-item-text"><%= @title %></span>
6
+ <% if @badge %>
7
+ <span class="docyard-badge docyard-badge--<%= @badge_type || 'default' %>"><%= @badge %></span>
8
+ <% end %>
6
9
  <% if @target == "_blank" %>
7
10
  <span class="nav-item-external"><%= icon(:link_external) %></span>
8
11
  <% end %>
@@ -0,0 +1,21 @@
1
+ <% if @raw_markdown || @show_edit_link || @show_last_updated %>
2
+ <div class="page-actions">
3
+ <% if @raw_markdown %>
4
+ <button type="button" class="page-actions__copy-btn" data-copy-page>
5
+ <%= icon(:copy) %>
6
+ <span class="page-actions__copy-text">Copy page</span>
7
+ </button>
8
+ <% end %>
9
+ <% if @show_edit_link && @edit_url %>
10
+ <a href="<%= @edit_url %>" class="page-actions__edit-link" target="_blank" rel="noopener noreferrer">
11
+ <%= icon(:pencil_simple_line) %>
12
+ Edit this page
13
+ </a>
14
+ <% end %>
15
+ <% if @show_last_updated && @last_updated %>
16
+ <div class="page-actions__last-updated">
17
+ Last updated <time datetime="<%= @last_updated[:iso] %>" title="<%= @last_updated[:formatted] %>"><%= @last_updated[:relative] %></time>
18
+ </div>
19
+ <% end %>
20
+ </div>
21
+ <% end %>
@@ -2,6 +2,9 @@
2
2
  <%= render_partial('_search_modal') %>
3
3
  <% end %>
4
4
 
5
- <script src="/_docyard/js/theme.js"></script>
6
- <script src="/_docyard/js/components.js"></script>
7
- <script src="/_docyard/js/reload.js"></script>
5
+ <script src="/_docyard/js/theme.js" defer></script>
6
+ <script src="/_docyard/js/components.js" defer></script>
7
+ <% if @dev_mode %>
8
+ <script>window.__DOCYARD_SSE_PORT__ = <%= @sse_port %>;</script>
9
+ <script src="/_docyard/js/hot-reload.js" defer></script>
10
+ <% end %>
@@ -0,0 +1,14 @@
1
+ <div class="docyard-step<%= ' docyard-step--last' if @is_last %>">
2
+ <div class="docyard-step__indicator">
3
+ <span class="docyard-step__number"><%= @number %></span>
4
+ <% unless @is_last %><div class="docyard-step__connector"></div><% end %>
5
+ </div>
6
+ <div class="docyard-step__content">
7
+ <h3 class="docyard-step__title"><%= @title %></h3>
8
+ <% unless @content_html.empty? %>
9
+ <div class="docyard-step__body">
10
+ <%= @content_html %>
11
+ </div>
12
+ <% end %>
13
+ </div>
14
+ </div>
@@ -9,10 +9,13 @@
9
9
  id="tab-<%= @group_id %>-<%= index %>"
10
10
  class="docyard-tabs__tab"
11
11
  tabindex="<%= index == 0 ? '0' : '-1' %>"
12
+ data-tab-name="<%= tab[:name].downcase %>"
12
13
  >
13
14
  <% if tab[:icon] %>
14
15
  <span class="docyard-tabs__icon">
15
- <% if tab[:icon_source] == "file-extension" %>
16
+ <% if tab[:icon_source] == "language" %>
17
+ <%= icon_for_language(tab[:icon]) %>
18
+ <% elsif tab[:icon_source] == "file-extension" %>
16
19
  <%= icon_file_extension(tab[:icon]) %>
17
20
  <% elsif tab[:icon_source] == "phosphor" %>
18
21
  <%= icon(tab[:icon]) %>
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module Docyard
6
+ module Utils
7
+ class GitInfo
8
+ TIME_UNITS = [
9
+ [60, "minute"],
10
+ [3600, "hour"],
11
+ [86_400, "day"],
12
+ [604_800, "week"],
13
+ [2_592_000, "month"],
14
+ [31_536_000, "year"]
15
+ ].freeze
16
+
17
+ class << self
18
+ attr_accessor :timestamp_cache
19
+
20
+ def prefetch_timestamps(docs_path = "docs")
21
+ return unless git_repository?
22
+
23
+ @timestamp_cache = fetch_all_timestamps(docs_path)
24
+ end
25
+
26
+ def clear_cache
27
+ @timestamp_cache = nil
28
+ end
29
+
30
+ def cached_timestamp(file_path)
31
+ return nil unless @timestamp_cache
32
+
33
+ @timestamp_cache[file_path]
34
+ end
35
+
36
+ def git_repository?
37
+ File.directory?(".git") || system("git", "rev-parse", "--git-dir", out: File::NULL, err: File::NULL)
38
+ end
39
+
40
+ private
41
+
42
+ def fetch_all_timestamps(docs_path)
43
+ output, _, status = Open3.capture3("git", "log", "--pretty=format:%cI", "--name-only", "--", "#{docs_path}/")
44
+ return {} unless status.success?
45
+
46
+ parse_git_log_output(output)
47
+ end
48
+
49
+ def parse_git_log_output(output)
50
+ timestamps = {}
51
+ current_timestamp = nil
52
+
53
+ output.each_line do |line|
54
+ line = line.strip
55
+ next if line.empty?
56
+
57
+ if line.match?(/^\d{4}-\d{2}-\d{2}T/)
58
+ current_timestamp = Time.parse(line)
59
+ elsif current_timestamp && !timestamps.key?(line)
60
+ timestamps[line] = current_timestamp
61
+ end
62
+ end
63
+
64
+ timestamps
65
+ end
66
+ end
67
+
68
+ attr_reader :repo_url, :branch, :edit_path
69
+
70
+ def initialize(repo_url:, branch: "main", edit_path: "docs")
71
+ @repo_url = repo_url
72
+ @branch = branch
73
+ @edit_path = edit_path
74
+ end
75
+
76
+ def edit_url(file_path)
77
+ return nil unless repo_url
78
+
79
+ relative_path = extract_relative_path(file_path)
80
+ return nil unless relative_path
81
+
82
+ normalized_url = repo_url.chomp("/")
83
+ "#{normalized_url}/edit/#{branch}/#{edit_path}/#{relative_path}"
84
+ end
85
+
86
+ def last_updated(file_path)
87
+ return nil unless file_path && File.exist?(file_path)
88
+ return nil unless self.class.git_repository?
89
+
90
+ timestamp = git_last_commit_time(file_path)
91
+ return nil unless timestamp
92
+
93
+ {
94
+ time: timestamp,
95
+ iso: timestamp.iso8601,
96
+ formatted: format_datetime(timestamp),
97
+ formatted_short: format_date_short(timestamp),
98
+ relative: relative_time(timestamp)
99
+ }
100
+ end
101
+
102
+ private
103
+
104
+ def extract_relative_path(file_path)
105
+ return nil unless file_path
106
+
107
+ match = file_path.match(%r{#{Regexp.escape(edit_path)}/(.+)$})
108
+ match ? match[1] : nil
109
+ end
110
+
111
+ def git_last_commit_time(file_path)
112
+ cached = self.class.cached_timestamp(file_path)
113
+ return cached if cached
114
+
115
+ fetch_single_timestamp(file_path)
116
+ end
117
+
118
+ def fetch_single_timestamp(file_path)
119
+ output, _, status = Open3.capture3("git", "log", "-1", "--format=%cI", "--", file_path)
120
+ return nil unless status.success?
121
+ return nil if output.strip.empty?
122
+
123
+ Time.parse(output.strip)
124
+ rescue ArgumentError
125
+ nil
126
+ end
127
+
128
+ def format_datetime(time)
129
+ time.strftime("%B %-d, %Y at %-I:%M %p")
130
+ end
131
+
132
+ def format_date_short(time)
133
+ time.strftime("%b %-d, %Y")
134
+ end
135
+
136
+ def relative_time(time)
137
+ seconds = Time.now - time
138
+ return "just now" if seconds < TIME_UNITS.first.first
139
+
140
+ divisor, unit = find_time_unit(seconds)
141
+ pluralize((seconds / divisor).to_i, unit)
142
+ end
143
+
144
+ def find_time_unit(seconds)
145
+ TIME_UNITS.reverse_each do |threshold, unit|
146
+ return [threshold, unit] if seconds >= threshold
147
+ end
148
+ TIME_UNITS.first
149
+ end
150
+
151
+ def pluralize(count, word)
152
+ suffix = count == 1 ? "" : "s"
153
+ "#{count} #{word}#{suffix} ago"
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Utils
5
+ module HashUtils
6
+ module_function
7
+
8
+ def deep_merge(hash1, hash2)
9
+ hash1.merge(hash2) do |_key, v1, v2|
10
+ if v2.nil?
11
+ v1
12
+ elsif v1.is_a?(Hash) && v2.is_a?(Hash)
13
+ deep_merge(v1, v2)
14
+ else
15
+ v2
16
+ end
17
+ end
18
+ end
19
+
20
+ def deep_dup(hash)
21
+ hash.transform_values do |value|
22
+ case value
23
+ when Hash then deep_dup(value)
24
+ when Array then value.map { |v| v.is_a?(Hash) ? deep_dup(v) : v }
25
+ else value
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -3,6 +3,14 @@
3
3
  module Docyard
4
4
  module Utils
5
5
  module HtmlHelpers
6
+ def escape_html(text)
7
+ text.to_s
8
+ .gsub("&", "&amp;")
9
+ .gsub("<", "&lt;")
10
+ .gsub(">", "&gt;")
11
+ .gsub('"', "&quot;")
12
+ end
13
+
6
14
  def escape_html_attribute(text)
7
15
  text.gsub('"', "&quot;")
8
16
  .gsub("'", "&#39;")
@@ -15,6 +15,32 @@ module Docyard
15
15
  logger.level = Logger.const_get(level.to_s.upcase)
16
16
  end
17
17
 
18
+ def start_buffering
19
+ @buffered_warnings = []
20
+ @buffering = true
21
+ end
22
+
23
+ def stop_buffering
24
+ @buffering = false
25
+ warnings = @buffered_warnings || []
26
+ @buffered_warnings = []
27
+ warnings
28
+ end
29
+
30
+ def buffering?
31
+ @buffering == true
32
+ end
33
+
34
+ def buffer_warning(message)
35
+ @buffered_warnings ||= []
36
+ @buffered_warnings << message
37
+ end
38
+
39
+ def flush_warnings
40
+ warnings = stop_buffering
41
+ warnings.each { |msg| logger.warn(msg) }
42
+ end
43
+
18
44
  private
19
45
 
20
46
  def default_logger
@@ -25,9 +51,24 @@ module Docyard
25
51
  end
26
52
 
27
53
  def log_formatter
28
- proc do |severity, datetime, _progname, msg|
29
- timestamp = datetime.strftime("%Y-%m-%d %H:%M:%S")
30
- "[#{timestamp}] [Docyard] [#{severity}] #{msg}\n"
54
+ proc do |severity, _datetime, _progname, msg|
55
+ if severity == "WARN" && buffering?
56
+ buffer_warning(msg)
57
+ nil
58
+ else
59
+ format_message(severity, msg)
60
+ end
61
+ end
62
+ end
63
+
64
+ def format_message(severity, msg)
65
+ case severity
66
+ when "DEBUG"
67
+ "[DEBUG] #{msg}\n"
68
+ when "INFO"
69
+ "#{msg}\n"
70
+ else
71
+ "[#{severity}] #{msg}\n"
31
72
  end
32
73
  end
33
74
  end
@@ -16,16 +16,6 @@ module Docyard
16
16
  normalized = "/#{normalized}" unless normalized.start_with?("/")
17
17
  normalized
18
18
  end
19
-
20
- def self.to_url(relative_path)
21
- normalize(relative_path)
22
- end
23
-
24
- def self.ancestor?(parent_path, child_path)
25
- return false if parent_path.nil?
26
-
27
- child_path.start_with?(parent_path) && child_path != parent_path
28
- end
29
19
  end
30
20
  end
31
21
  end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Docyard
6
+ module Utils
7
+ module PathUtils
8
+ module_function
9
+
10
+ def sanitize_url_path(request_path)
11
+ decoded = decode_path(request_path)
12
+ clean = decoded.delete_prefix("/").delete_suffix("/")
13
+ clean = "index" if clean.empty?
14
+ clean.delete_suffix(".md")
15
+ end
16
+
17
+ def safe_path?(requested_path, base_dir)
18
+ return false if requested_path.nil? || base_dir.nil?
19
+
20
+ expanded_base = File.expand_path(base_dir)
21
+ expanded_path = File.expand_path(requested_path, base_dir)
22
+ expanded_path.start_with?("#{expanded_base}/") || expanded_path == expanded_base
23
+ end
24
+
25
+ def resolve_safe_path(relative_path, base_dir)
26
+ return nil if relative_path.nil? || base_dir.nil?
27
+
28
+ decoded = decode_path(relative_path)
29
+ full_path = File.join(base_dir, decoded)
30
+ expanded = File.expand_path(full_path)
31
+ expanded_base = File.expand_path(base_dir)
32
+
33
+ return nil unless expanded.start_with?("#{expanded_base}/")
34
+
35
+ expanded
36
+ end
37
+
38
+ def decode_path(path)
39
+ decoded = URI.decode_www_form_component(path.to_s)
40
+ decoded.gsub(/\\+/, "/")
41
+ rescue ArgumentError
42
+ path.to_s
43
+ end
44
+
45
+ def markdown_file_to_url(file_path, docs_path)
46
+ relative_path = file_path.delete_prefix("#{docs_path}/")
47
+ relative_path_to_url(relative_path)
48
+ end
49
+
50
+ def relative_path_to_url(relative_path)
51
+ base_name = File.basename(relative_path, ".md")
52
+ dir_name = File.dirname(relative_path)
53
+
54
+ if base_name == "index"
55
+ dir_name == "." ? "/" : "/#{dir_name}"
56
+ else
57
+ dir_name == "." ? "/#{base_name}" : "/#{dir_name}/#{base_name}"
58
+ end
59
+ end
60
+
61
+ def markdown_to_html_output(relative_path, output_dir)
62
+ base_name = File.basename(relative_path, ".md")
63
+ dir_name = File.dirname(relative_path)
64
+
65
+ if base_name == "index"
66
+ File.join(output_dir, dir_name, "index.html")
67
+ else
68
+ File.join(output_dir, dir_name, base_name, "index.html")
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Docyard
4
- VERSION = "0.8.0"
4
+ VERSION = "1.0.0"
5
5
  end
data/lib/docyard.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "docyard/version"
4
- require_relative "docyard/config/constants"
5
- require_relative "docyard/utils/errors"
4
+ require_relative "docyard/constants"
5
+ require_relative "docyard/errors"
6
6
  require_relative "docyard/utils/logging"
7
7
 
8
8
  require_relative "docyard/utils/text_formatter"