docyard 0.3.0 → 0.5.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -2
  3. data/README.md +80 -33
  4. data/lib/docyard/build/asset_bundler.rb +139 -0
  5. data/lib/docyard/build/file_copier.rb +105 -0
  6. data/lib/docyard/build/sitemap_generator.rb +57 -0
  7. data/lib/docyard/build/static_generator.rb +141 -0
  8. data/lib/docyard/builder.rb +104 -0
  9. data/lib/docyard/cli.rb +19 -0
  10. data/lib/docyard/components/heading_anchor_processor.rb +34 -0
  11. data/lib/docyard/components/table_of_contents_processor.rb +64 -0
  12. data/lib/docyard/components/table_wrapper_processor.rb +18 -0
  13. data/lib/docyard/config.rb +15 -2
  14. data/lib/docyard/icons/phosphor.rb +3 -1
  15. data/lib/docyard/initializer.rb +80 -14
  16. data/lib/docyard/markdown.rb +19 -0
  17. data/lib/docyard/prev_next_builder.rb +159 -0
  18. data/lib/docyard/preview_server.rb +72 -0
  19. data/lib/docyard/rack_application.rb +25 -3
  20. data/lib/docyard/renderer.rb +33 -8
  21. data/lib/docyard/sidebar/config_parser.rb +180 -0
  22. data/lib/docyard/sidebar/item.rb +58 -0
  23. data/lib/docyard/sidebar/renderer.rb +33 -6
  24. data/lib/docyard/sidebar_builder.rb +45 -1
  25. data/lib/docyard/templates/assets/css/components/callout.css +1 -1
  26. data/lib/docyard/templates/assets/css/components/code-block.css +2 -2
  27. data/lib/docyard/templates/assets/css/components/heading-anchor.css +77 -0
  28. data/lib/docyard/templates/assets/css/components/navigation.css +65 -7
  29. data/lib/docyard/templates/assets/css/components/prev-next.css +114 -0
  30. data/lib/docyard/templates/assets/css/components/table-of-contents.css +269 -0
  31. data/lib/docyard/templates/assets/css/components/tabs.css +3 -2
  32. data/lib/docyard/templates/assets/css/components/theme-toggle.css +8 -0
  33. data/lib/docyard/templates/assets/css/layout.css +58 -1
  34. data/lib/docyard/templates/assets/css/markdown.css +20 -11
  35. data/lib/docyard/templates/assets/css/variables.css +1 -0
  36. data/lib/docyard/templates/assets/js/components/heading-anchor.js +90 -0
  37. data/lib/docyard/templates/assets/js/components/navigation.js +225 -0
  38. data/lib/docyard/templates/assets/js/components/table-of-contents.js +301 -0
  39. data/lib/docyard/templates/assets/js/theme.js +2 -185
  40. data/lib/docyard/templates/config/docyard.yml.erb +32 -10
  41. data/lib/docyard/templates/layouts/default.html.erb +10 -2
  42. data/lib/docyard/templates/markdown/getting-started/installation.md.erb +46 -12
  43. data/lib/docyard/templates/markdown/guides/configuration.md.erb +202 -0
  44. data/lib/docyard/templates/markdown/guides/markdown-features.md.erb +247 -0
  45. data/lib/docyard/templates/markdown/index.md.erb +55 -59
  46. data/lib/docyard/templates/partials/_heading_anchor.html.erb +1 -0
  47. data/lib/docyard/templates/partials/_nav_group.html.erb +10 -4
  48. data/lib/docyard/templates/partials/_nav_leaf.html.erb +9 -1
  49. data/lib/docyard/templates/partials/_prev_next.html.erb +23 -0
  50. data/lib/docyard/templates/partials/_table_of_contents.html.erb +45 -0
  51. data/lib/docyard/templates/partials/_table_of_contents_toggle.html.erb +8 -0
  52. data/lib/docyard/version.rb +1 -1
  53. data/lib/docyard.rb +8 -0
  54. metadata +67 -10
  55. data/lib/docyard/templates/markdown/components/callouts.md.erb +0 -204
  56. data/lib/docyard/templates/markdown/components/icons.md.erb +0 -125
  57. data/lib/docyard/templates/markdown/components/tabs.md.erb +0 -686
  58. data/lib/docyard/templates/markdown/configuration.md.erb +0 -202
  59. data/lib/docyard/templates/markdown/core-concepts/file-structure.md.erb +0 -61
  60. data/lib/docyard/templates/markdown/core-concepts/markdown.md.erb +0 -90
  61. data/lib/docyard/templates/markdown/getting-started/introduction.md.erb +0 -30
  62. data/lib/docyard/templates/markdown/getting-started/quick-start.md.erb +0 -56
  63. data/lib/docyard/templates/partials/_icons.html.erb +0 -11
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "tty-progressbar"
5
+
6
+ module Docyard
7
+ class Builder
8
+ attr_reader :config, :clean, :verbose, :start_time
9
+
10
+ def initialize(clean: true, verbose: false)
11
+ @config = Config.new
12
+ @clean = clean
13
+ @verbose = verbose
14
+ @start_time = Time.now
15
+ end
16
+
17
+ def build
18
+ prepare_output_directory
19
+ log "Building static site..."
20
+
21
+ pages_built = generate_static_pages
22
+ bundles_created = bundle_assets
23
+ assets_copied = copy_static_files
24
+ generate_seo_files
25
+
26
+ display_summary(pages_built, bundles_created, assets_copied)
27
+ true
28
+ rescue StandardError => e
29
+ error "Build failed: #{e.message}"
30
+ error e.backtrace.first if verbose
31
+ false
32
+ end
33
+
34
+ private
35
+
36
+ def prepare_output_directory
37
+ output_dir = config.build.output_dir
38
+
39
+ if clean && Dir.exist?(output_dir)
40
+ log "[✓] Cleaning #{output_dir}/ directory"
41
+ FileUtils.rm_rf(output_dir)
42
+ end
43
+
44
+ FileUtils.mkdir_p(output_dir)
45
+ end
46
+
47
+ def generate_static_pages
48
+ require_relative "build/static_generator"
49
+ generator = Build::StaticGenerator.new(config, verbose: verbose)
50
+ generator.generate
51
+ end
52
+
53
+ def bundle_assets
54
+ require_relative "build/asset_bundler"
55
+ bundler = Build::AssetBundler.new(config, verbose: verbose)
56
+ bundler.bundle
57
+ end
58
+
59
+ def copy_static_files
60
+ require_relative "build/file_copier"
61
+ copier = Build::FileCopier.new(config, verbose: verbose)
62
+ copier.copy
63
+ end
64
+
65
+ def generate_seo_files
66
+ require_relative "build/sitemap_generator"
67
+ sitemap_gen = Build::SitemapGenerator.new(config)
68
+ sitemap_gen.generate
69
+
70
+ File.write(File.join(config.build.output_dir, "robots.txt"), robots_txt_content)
71
+ log "[✓] Generated robots.txt"
72
+ end
73
+
74
+ def robots_txt_content
75
+ base_url = config.build.base_url
76
+ base_url = "#{base_url}/" unless base_url.end_with?("/")
77
+
78
+ <<~ROBOTS
79
+ User-agent: *
80
+ Allow: /
81
+
82
+ Sitemap: #{base_url}sitemap.xml
83
+ ROBOTS
84
+ end
85
+
86
+ def display_summary(pages, bundles, assets)
87
+ elapsed = Time.now - start_time
88
+
89
+ puts "\n#{'=' * 50}"
90
+ puts "Build complete in #{format('%.2f', elapsed)}s"
91
+ puts "Output: #{config.build.output_dir}/"
92
+ puts "#{pages} pages, #{bundles} bundles, #{assets} static files"
93
+ puts "=" * 50
94
+ end
95
+
96
+ def log(message)
97
+ puts message
98
+ end
99
+
100
+ def error(message)
101
+ warn "[ERROR] #{message}"
102
+ end
103
+ end
104
+ end
data/lib/docyard/cli.rb CHANGED
@@ -19,6 +19,25 @@ module Docyard
19
19
  exit(1) unless initializer.run
20
20
  end
21
21
 
22
+ desc "build", "Build static site for production"
23
+ method_option :clean, type: :boolean, default: true, desc: "Clean output directory before building"
24
+ method_option :verbose, type: :boolean, default: false, aliases: "-v", desc: "Show verbose output"
25
+ def build
26
+ require_relative "builder"
27
+ builder = Docyard::Builder.new(
28
+ clean: options[:clean],
29
+ verbose: options[:verbose]
30
+ )
31
+ exit(1) unless builder.build
32
+ end
33
+
34
+ desc "preview", "Preview the built site locally"
35
+ method_option :port, type: :numeric, default: 4000, aliases: "-p", desc: "Port to run preview server on"
36
+ def preview
37
+ require_relative "preview_server"
38
+ Docyard::PreviewServer.new(port: options[:port]).start
39
+ end
40
+
22
41
  desc "serve", "Start the development server"
23
42
  method_option :port, type: :numeric, default: 4200, aliases: "-p", desc: "Port to run the server on"
24
43
  method_option :host, type: :string, default: "localhost", aliases: "-h", desc: "Host to bind the server to"
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Components
5
+ class HeadingAnchorProcessor < BaseProcessor
6
+ self.priority = 30
7
+
8
+ def postprocess(html)
9
+ add_anchor_links(html)
10
+ end
11
+
12
+ private
13
+
14
+ def add_anchor_links(html)
15
+ html.gsub(%r{<(h[2-6])\s+id="([^"]+)">(.*?)</\1>}m) do |_match|
16
+ tag = Regexp.last_match(1)
17
+ id = Regexp.last_match(2)
18
+ content = Regexp.last_match(3)
19
+
20
+ anchor_html = render_anchor_link(id)
21
+
22
+ "<#{tag} id=\"#{id}\">#{content}#{anchor_html}</#{tag}>"
23
+ end
24
+ end
25
+
26
+ def render_anchor_link(id)
27
+ renderer = Renderer.new
28
+ renderer.render_partial("_heading_anchor", {
29
+ id: id
30
+ })
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Components
5
+ class TableOfContentsProcessor < BaseProcessor
6
+ self.priority = 35
7
+
8
+ def postprocess(html)
9
+ headings = extract_headings(html)
10
+ Thread.current[:docyard_toc] = headings
11
+ html
12
+ end
13
+
14
+ private
15
+
16
+ def extract_headings(html)
17
+ headings = []
18
+
19
+ html.scan(%r{<(h[2-4])\s+id="([^"]+)">(.*?)</\1>}m) do
20
+ level = Regexp.last_match(1)[1].to_i
21
+ id = Regexp.last_match(2)
22
+ text = strip_html(Regexp.last_match(3))
23
+
24
+ headings << {
25
+ level: level,
26
+ id: id,
27
+ text: text
28
+ }
29
+ end
30
+
31
+ build_hierarchy(headings)
32
+ end
33
+
34
+ def build_hierarchy(headings)
35
+ return [] if headings.empty?
36
+
37
+ root = []
38
+ stack = []
39
+
40
+ headings.each do |heading|
41
+ heading[:children] = []
42
+
43
+ stack.pop while stack.any? && stack.last[:level] >= heading[:level]
44
+
45
+ if stack.empty?
46
+ root << heading
47
+ else
48
+ stack.last[:children] << heading
49
+ end
50
+
51
+ stack << heading
52
+ end
53
+
54
+ root
55
+ end
56
+
57
+ def strip_html(text)
58
+ text.gsub(%r{<a[^>]*class="heading-anchor"[^>]*>.*?</a>}, "")
59
+ .gsub(/<[^>]+>/, "")
60
+ .strip
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Components
5
+ class TableWrapperProcessor < BaseProcessor
6
+ self.priority = 100
7
+
8
+ def postprocess(html)
9
+ wrapped = html.gsub(/<table([^>]*)>/) do
10
+ attributes = Regexp.last_match(1)
11
+ "<div class=\"table-wrapper\"><table#{attributes}>"
12
+ end
13
+
14
+ wrapped.gsub("</table>", "</table></div>")
15
+ end
16
+ end
17
+ end
18
+ end
@@ -25,7 +25,16 @@ module Docyard
25
25
  "base_url" => "/",
26
26
  "clean" => true
27
27
  },
28
- "sidebar" => nil
28
+ "sidebar" => {
29
+ "items" => []
30
+ },
31
+ "navigation" => {
32
+ "footer" => {
33
+ "enabled" => true,
34
+ "prev_text" => "Previous",
35
+ "next_text" => "Next"
36
+ }
37
+ }
29
38
  }.freeze
30
39
 
31
40
  attr_reader :data, :file_path
@@ -58,7 +67,11 @@ module Docyard
58
67
  end
59
68
 
60
69
  def sidebar
61
- data["sidebar"]
70
+ @sidebar ||= ConfigSection.new(data["sidebar"])
71
+ end
72
+
73
+ def navigation
74
+ @navigation ||= ConfigSection.new(data["navigation"])
62
75
  end
63
76
 
64
77
  private
@@ -27,6 +27,7 @@ module Docyard
27
27
  "sun" => '<path d="M120,40V16a8,8,0,0,1,16,0V40a8,8,0,0,1-16,0Zm72,88a64,64,0,1,1-64-64A64.07,64.07,0,0,1,192,128Zm-16,0a48,48,0,1,0-48,48A48.05,48.05,0,0,0,176,128ZM58.34,69.66A8,8,0,0,0,69.66,58.34l-16-16A8,8,0,0,0,42.34,53.66Zm0,116.68-16,16a8,8,0,0,0,11.32,11.32l16-16a8,8,0,0,0-11.32-11.32ZM192,72a8,8,0,0,0,5.66-2.34l16-16a8,8,0,0,0-11.32-11.32l-16,16A8,8,0,0,0,192,72Zm5.66,114.34a8,8,0,0,0-11.32,11.32l16,16a8,8,0,0,0,11.32-11.32ZM48,128a8,8,0,0,0-8-8H16a8,8,0,0,0,0,16H40A8,8,0,0,0,48,128Zm80,80a8,8,0,0,0-8,8v24a8,8,0,0,0,16,0V216A8,8,0,0,0,128,208Zm112-88H216a8,8,0,0,0,0,16h24a8,8,0,0,0,0-16Z"/>',
28
28
  "link-external" => '<path d="M224,104a8,8,0,0,1-16,0V59.32l-66.33,66.34a8,8,0,0,1-11.32-11.32L196.68,48H152a8,8,0,0,1,0-16h64a8,8,0,0,1,8,8Zm-40,24a8,8,0,0,0-8,8v72H48V80h72a8,8,0,0,0,0-16H48A16,16,0,0,0,32,80V208a16,16,0,0,0,16,16H176a16,16,0,0,0,16-16V136A8,8,0,0,0,184,128Z"/>',
29
29
  "copy" => '<path d="M216,32H88a8,8,0,0,0-8,8V80H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H168a8,8,0,0,0,8-8V176h40a8,8,0,0,0,8-8V40A8,8,0,0,0,216,32ZM160,208H48V96H160Zm48-48H176V88a8,8,0,0,0-8-8H96V48H208Z"/>',
30
+ "caret-right" => '<path d="M181.66,133.66l-80,80a8,8,0,0,1-11.32-11.32L164.69,128,90.34,53.66a8,8,0,0,1,11.32-11.32l80,80A8,8,0,0,1,181.66,133.66Z"/>',
30
31
  "github" => '<path d="M208.31,75.68A59.78,59.78,0,0,0,202.93,28,8,8,0,0,0,196,24a59.75,59.75,0,0,0-48,24H124A59.75,59.75,0,0,0,76,24a8,8,0,0,0-6.93,4,59.78,59.78,0,0,0-5.38,47.68A58.14,58.14,0,0,0,56,104v8a56.06,56.06,0,0,0,48.44,55.47A39.8,39.8,0,0,0,96,192v8H72a24,24,0,0,1-24-24A40,40,0,0,0,8,136a8,8,0,0,0,0,16,24,24,0,0,1,24,24,40,40,0,0,0,40,40H96v16a8,8,0,0,0,16,0V192a24,24,0,0,1,48,0v40a8,8,0,0,0,16,0V192a39.8,39.8,0,0,0-8.44-24.53A56.06,56.06,0,0,0,216,112v-8A58.14,58.14,0,0,0,208.31,75.68ZM200,112a40,40,0,0,1-40,40H112a40,40,0,0,1-40-40v-8a41.74,41.74,0,0,1,6.9-22.48,8,8,0,0,0,1.1-7.69,43.81,43.81,0,0,1,.79-33.58,43.88,43.88,0,0,1,32.32,20.06,8,8,0,0,0,6.71,3.69h32.35a8,8,0,0,0,6.74-3.69,43.87,43.87,0,0,1,32.32-20.06,43.81,43.81,0,0,1,.77,33.58,8.09,8.09,0,0,0,1,7.65,41.76,41.76,0,0,1,7,22.52Z"/>',
31
32
  "question" => '<path d="M140,180a12,12,0,1,1-12-12A12,12,0,0,1,140,180ZM128,72c-22.06,0-40,16.15-40,36v4a8,8,0,0,0,16,0v-4c0-11,10.77-20,24-20s24,9,24,20-10.77,20-24,20a8,8,0,0,0-8,8v8a8,8,0,0,0,16,0v-.72c18.24-3.35,32-17.9,32-35.28C168,88.15,150.06,72,128,72Zm104,56A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z"/>',
32
33
  "lightbulb" => '<path d="M176,232a8,8,0,0,1-8,8H88a8,8,0,0,1,0-16h80A8,8,0,0,1,176,232Zm40-128a87.55,87.55,0,0,1-33.64,69.21A16.24,16.24,0,0,0,176,186v6a16,16,0,0,1-16,16H96a16,16,0,0,1-16-16v-6a16,16,0,0,0-6.23-12.66A87.59,87.59,0,0,1,40,104.49C39.74,56.83,78.26,17.14,125.88,16A88,88,0,0,1,216,104Zm-16,0a72,72,0,0,0-73.74-72c-39,.92-70.47,33.39-70.26,72.39a71.65,71.65,0,0,0,27.64,56.3A32,32,0,0,1,96,186v6h64v-6a32.15,32.15,0,0,1,12.47-25.35A71.65,71.65,0,0,0,200,104Zm-16.11-9.34a57.6,57.6,0,0,0-46.56-46.55,8,8,0,0,0-2.66,15.78c16.57,2.79,30.63,16.85,33.44,33.45A8,8,0,0,0,176,104a9,9,0,0,0,1.35-.11A8,8,0,0,0,183.89,94.66Z"/>',
@@ -34,7 +35,8 @@ module Docyard
34
35
  "warning-octagon" => '<path d="M120,136V80a8,8,0,0,1,16,0v56a8,8,0,0,1-16,0ZM232,91.55v72.9a15.86,15.86,0,0,1-4.69,11.31l-51.55,51.55A15.86,15.86,0,0,1,164.45,232H91.55a15.86,15.86,0,0,1-11.31-4.69L28.69,175.76A15.86,15.86,0,0,1,24,164.45V91.55a15.86,15.86,0,0,1,4.69-11.31L80.24,28.69A15.86,15.86,0,0,1,91.55,24h72.9a15.86,15.86,0,0,1,11.31,4.69l51.55,51.55A15.86,15.86,0,0,1,232,91.55Zm-16,0L164.45,40H91.55L40,91.55v72.9L91.55,216h72.9L216,164.45ZM128,160a12,12,0,1,0,12,12A12,12,0,0,0,128,160Z"/>',
35
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"/>',
36
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"/>',
37
- "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"/>'
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"/>'
38
40
  },
39
41
  "bold" => {
40
42
  "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"/>'
@@ -10,14 +10,9 @@ module Docyard
10
10
 
11
11
  TEMPLATES = {
12
12
  "index.md" => "index.md.erb",
13
- "getting-started/introduction.md" => "getting-started/introduction.md.erb",
14
13
  "getting-started/installation.md" => "getting-started/installation.md.erb",
15
- "getting-started/quick-start.md" => "getting-started/quick-start.md.erb",
16
- "core-concepts/file-structure.md" => "core-concepts/file-structure.md.erb",
17
- "core-concepts/markdown.md" => "core-concepts/markdown.md.erb",
18
- "components/callouts.md" => "components/callouts.md.erb",
19
- "components/icons.md" => "components/icons.md.erb",
20
- "components/tabs.md" => "components/tabs.md.erb"
14
+ "guides/markdown-features.md" => "guides/markdown-features.md.erb",
15
+ "guides/configuration.md" => "guides/configuration.md.erb"
21
16
  }.freeze
22
17
 
23
18
  def initialize(path = ".")
@@ -79,16 +74,87 @@ module Docyard
79
74
  end
80
75
 
81
76
  def print_success
82
- puts "Docyard initialized successfully!"
77
+ print_banner
78
+ print_created_files
79
+ print_next_steps
80
+ end
81
+
82
+ def print_banner
83
+ puts ""
84
+ puts "┌─────────────────────────────────────────────────────────────┐"
85
+ puts "│ ✓ Docyard initialized successfully │"
86
+ puts "└─────────────────────────────────────────────────────────────┘"
87
+ puts ""
88
+ end
89
+
90
+ def print_created_files
91
+ puts "Created files:"
83
92
  puts ""
84
- puts "Created:"
85
- TEMPLATES.each_key { |file| puts " #{DOCS_DIR}/#{file}" }
86
- puts " docyard.yml (configuration - optional)"
93
+ print_file_tree
87
94
  puts ""
95
+ end
96
+
97
+ def print_next_steps
88
98
  puts "Next steps:"
89
- puts " 1. Edit your markdown files in #{DOCS_DIR}/"
90
- puts " 2. Customize docyard.yml (optional)"
91
- puts " 3. Run 'docyard serve' to preview your documentation locally"
99
+ puts ""
100
+ puts " Start development server:"
101
+ puts " docyard serve"
102
+ puts " → http://localhost:4200"
103
+ puts ""
104
+ puts " Build for production:"
105
+ puts " docyard build"
106
+ puts ""
107
+ puts " Preview production build:"
108
+ puts " docyard preview"
109
+ puts ""
110
+ end
111
+
112
+ def print_file_tree
113
+ puts " ├── docs/"
114
+
115
+ grouped_files = TEMPLATES.keys.group_by { |file| File.dirname(file) }
116
+ sorted_dirs = grouped_files.keys.sort
117
+
118
+ sorted_dirs.each_with_index do |dir, dir_idx|
119
+ print_directory_group(dir, grouped_files[dir], dir_idx == sorted_dirs.length - 1)
120
+ end
121
+
122
+ puts " └── docyard.yml"
123
+ end
124
+
125
+ def print_directory_group(dir, files, is_last_dir)
126
+ sorted_files = files.sort
127
+
128
+ if dir == "."
129
+ print_root_files(sorted_files, is_last_dir)
130
+ else
131
+ print_subdirectory(dir, sorted_files, is_last_dir)
132
+ end
133
+ end
134
+
135
+ def print_root_files(files, is_last_dir)
136
+ files.each_with_index do |file, idx|
137
+ is_last = idx == files.length - 1 && is_last_dir
138
+ prefix = is_last ? " │ └──" : " │ ├──"
139
+ puts "#{prefix} #{file}"
140
+ end
141
+ end
142
+
143
+ def print_subdirectory(dir, files, is_last_dir)
144
+ dir_prefix = is_last_dir ? " │ └──" : " │ ├──"
145
+ puts "#{dir_prefix} #{dir}/"
146
+
147
+ files.each_with_index do |file, idx|
148
+ print_subdirectory_file(file, idx, files.length, is_last_dir)
149
+ end
150
+ end
151
+
152
+ def print_subdirectory_file(file, idx, total, is_last_dir)
153
+ is_last_file = idx == total - 1
154
+ file_prefix = is_last_dir ? " │ " : " │ │ "
155
+ file_prefix += is_last_file ? "└──" : "├──"
156
+ basename = File.basename(file)
157
+ puts "#{file_prefix} #{basename}"
92
158
  end
93
159
  end
94
160
  end
@@ -9,6 +9,9 @@ require_relative "components/callout_processor"
9
9
  require_relative "components/tabs_processor"
10
10
  require_relative "components/icon_processor"
11
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"
12
15
 
13
16
  module Docyard
14
17
  class Markdown
@@ -40,6 +43,22 @@ module Docyard
40
43
  frontmatter["description"]
41
44
  end
42
45
 
46
+ def sidebar_icon
47
+ frontmatter.dig("sidebar", "icon")
48
+ end
49
+
50
+ def sidebar_text
51
+ frontmatter.dig("sidebar", "text")
52
+ end
53
+
54
+ def sidebar_collapsed
55
+ frontmatter.dig("sidebar", "collapsed")
56
+ end
57
+
58
+ def toc
59
+ @toc ||= Thread.current[:docyard_toc] || []
60
+ end
61
+
43
62
  private
44
63
 
45
64
  def parse_frontmatter
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "renderer"
4
+ require_relative "utils/path_resolver"
5
+
6
+ module Docyard
7
+ class PrevNextBuilder
8
+ attr_reader :sidebar_tree, :current_path, :frontmatter, :config
9
+
10
+ def initialize(sidebar_tree:, current_path:, frontmatter: {}, config: {})
11
+ @sidebar_tree = sidebar_tree
12
+ @current_path = Utils::PathResolver.normalize(current_path)
13
+ @frontmatter = frontmatter
14
+ @config = config
15
+ end
16
+
17
+ def prev_next_links
18
+ return nil unless enabled?
19
+
20
+ {
21
+ prev: build_prev_link,
22
+ next: build_next_link
23
+ }
24
+ end
25
+
26
+ def to_html
27
+ links = prev_next_links
28
+ return "" if links.nil? || (links[:prev].nil? && links[:next].nil?)
29
+
30
+ Renderer.new.render_partial(
31
+ "_prev_next", {
32
+ prev: links[:prev],
33
+ next: links[:next],
34
+ prev_text: config_prev_text,
35
+ next_text: config_next_text
36
+ }
37
+ )
38
+ end
39
+
40
+ private
41
+
42
+ def enabled?
43
+ return false if config_disabled?
44
+ return false if frontmatter_disabled?
45
+
46
+ true
47
+ end
48
+
49
+ def config_disabled?
50
+ return false if config.nil? || config.empty?
51
+
52
+ config == false || config["enabled"] == false || config[:enabled] == false
53
+ end
54
+
55
+ def frontmatter_disabled?
56
+ frontmatter["prev"] == false && frontmatter["next"] == false
57
+ end
58
+
59
+ def build_prev_link
60
+ return nil if frontmatter["prev"] == false
61
+
62
+ return build_frontmatter_link(frontmatter["prev"]) if frontmatter["prev"]
63
+
64
+ auto_prev_link
65
+ end
66
+
67
+ def build_next_link
68
+ return nil if frontmatter["next"] == false
69
+
70
+ return build_frontmatter_link(frontmatter["next"]) if frontmatter["next"]
71
+
72
+ auto_next_link
73
+ end
74
+
75
+ def build_frontmatter_link(value)
76
+ case value
77
+ when String
78
+ find_link_by_text(value)
79
+ when Hash
80
+ {
81
+ title: value["text"] || value[:text],
82
+ path: value["link"] || value[:link]
83
+ }
84
+ end
85
+ end
86
+
87
+ def find_link_by_text(text)
88
+ flat_links.find { |link| link[:title].downcase == text.downcase }
89
+ end
90
+
91
+ def auto_prev_link
92
+ index = current_page_index
93
+ return nil unless index&.positive?
94
+
95
+ flat_links[index - 1]
96
+ end
97
+
98
+ def auto_next_link
99
+ index = current_page_index
100
+ return nil unless index && index < flat_links.length - 1
101
+
102
+ flat_links[index + 1]
103
+ end
104
+
105
+ def current_page_index
106
+ @current_page_index ||= flat_links.find_index do |link|
107
+ normalized_path(link[:path]) == normalized_path(current_path)
108
+ end
109
+ end
110
+
111
+ def flat_links
112
+ @flat_links ||= begin
113
+ links = []
114
+ flatten_tree(sidebar_tree, links)
115
+ links.uniq { |link| normalized_path(link[:path]) }
116
+ end
117
+ end
118
+
119
+ def flatten_tree(items, links)
120
+ items.each do |item|
121
+ links << build_link(item) if valid_navigation_item?(item)
122
+ flatten_tree(item[:children], links) if item[:children]&.any?
123
+ end
124
+ end
125
+
126
+ def valid_navigation_item?(item)
127
+ item[:type] == :file && item[:path] && !external_link?(item[:path])
128
+ end
129
+
130
+ def build_link(item)
131
+ {
132
+ title: item[:footer_text] || item[:title],
133
+ path: item[:path]
134
+ }
135
+ end
136
+
137
+ def external_link?(path)
138
+ path.start_with?("http://", "https://")
139
+ end
140
+
141
+ def normalized_path(path)
142
+ return "" if path.nil?
143
+
144
+ path.gsub(/[?#].*$/, "")
145
+ end
146
+
147
+ def config_prev_text
148
+ return "Previous" if config.nil? || config.empty?
149
+
150
+ config["prev_text"] || config[:prev_text] || "Previous"
151
+ end
152
+
153
+ def config_next_text
154
+ return "Next" if config.nil? || config.empty?
155
+
156
+ config["next_text"] || config[:next_text] || "Next"
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webrick"
4
+ require_relative "config"
5
+
6
+ module Docyard
7
+ class PreviewServer
8
+ DEFAULT_PORT = 4000
9
+
10
+ attr_reader :port, :output_dir
11
+
12
+ def initialize(port: DEFAULT_PORT)
13
+ @port = port
14
+ @config = Config.load
15
+ @output_dir = File.expand_path(@config.build.output_dir)
16
+ end
17
+
18
+ def start
19
+ validate_output_directory!
20
+ print_server_info
21
+
22
+ server = create_server
23
+ trap("INT") { shutdown_server(server) }
24
+
25
+ server.start
26
+ end
27
+
28
+ private
29
+
30
+ def validate_output_directory!
31
+ return if File.directory?(output_dir)
32
+
33
+ abort "Error: #{output_dir}/ directory not found.\n" \
34
+ "Run `docyard build` first to build the site."
35
+ end
36
+
37
+ def print_server_info
38
+ puts "Preview server starting..."
39
+ puts "=> Serving from: #{output_dir}/"
40
+ puts "=> Running at: http://localhost:#{port}"
41
+ puts "=> Press Ctrl+C to stop\n"
42
+ end
43
+
44
+ def create_server
45
+ WEBrick::HTTPServer.new(
46
+ Port: port,
47
+ DocumentRoot: output_dir,
48
+ AccessLog: [],
49
+ Logger: WEBrick::Log.new(File::NULL),
50
+ MimeTypes: mime_types
51
+ )
52
+ end
53
+
54
+ def mime_types
55
+ WEBrick::HTTPUtils::DefaultMimeTypes.merge(
56
+ {
57
+ "css" => "text/css",
58
+ "js" => "application/javascript",
59
+ "json" => "application/json",
60
+ "svg" => "image/svg+xml",
61
+ "woff" => "font/woff",
62
+ "woff2" => "font/woff2"
63
+ }
64
+ )
65
+ end
66
+
67
+ def shutdown_server(server)
68
+ puts "\nShutting down preview server..."
69
+ server.shutdown
70
+ end
71
+ end
72
+ end