docyard 0.3.0 → 0.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -1
- data/README.md +55 -33
- data/lib/docyard/build/asset_bundler.rb +139 -0
- data/lib/docyard/build/file_copier.rb +105 -0
- data/lib/docyard/build/sitemap_generator.rb +57 -0
- data/lib/docyard/build/static_generator.rb +141 -0
- data/lib/docyard/builder.rb +104 -0
- data/lib/docyard/cli.rb +19 -0
- data/lib/docyard/components/table_wrapper_processor.rb +18 -0
- data/lib/docyard/config.rb +4 -2
- data/lib/docyard/icons/phosphor.rb +1 -0
- data/lib/docyard/initializer.rb +80 -14
- data/lib/docyard/markdown.rb +13 -0
- data/lib/docyard/preview_server.rb +72 -0
- data/lib/docyard/rack_application.rb +1 -1
- data/lib/docyard/renderer.rb +17 -3
- data/lib/docyard/sidebar/config_parser.rb +180 -0
- data/lib/docyard/sidebar/item.rb +58 -0
- data/lib/docyard/sidebar/renderer.rb +33 -6
- data/lib/docyard/sidebar_builder.rb +45 -1
- data/lib/docyard/templates/assets/css/components/callout.css +1 -1
- data/lib/docyard/templates/assets/css/components/code-block.css +2 -2
- data/lib/docyard/templates/assets/css/components/navigation.css +65 -7
- data/lib/docyard/templates/assets/css/components/tabs.css +3 -2
- data/lib/docyard/templates/assets/css/components/theme-toggle.css +8 -0
- data/lib/docyard/templates/assets/css/markdown.css +20 -11
- data/lib/docyard/templates/assets/js/components/navigation.js +221 -0
- data/lib/docyard/templates/assets/js/theme.js +2 -185
- data/lib/docyard/templates/config/docyard.yml.erb +32 -10
- data/lib/docyard/templates/layouts/default.html.erb +1 -1
- data/lib/docyard/templates/markdown/getting-started/installation.md.erb +46 -12
- data/lib/docyard/templates/markdown/guides/configuration.md.erb +202 -0
- data/lib/docyard/templates/markdown/guides/markdown-features.md.erb +247 -0
- data/lib/docyard/templates/markdown/index.md.erb +55 -59
- data/lib/docyard/templates/partials/_nav_group.html.erb +10 -4
- data/lib/docyard/templates/partials/_nav_leaf.html.erb +9 -1
- data/lib/docyard/version.rb +1 -1
- data/lib/docyard.rb +8 -0
- metadata +55 -10
- data/lib/docyard/templates/markdown/components/callouts.md.erb +0 -204
- data/lib/docyard/templates/markdown/components/icons.md.erb +0 -125
- data/lib/docyard/templates/markdown/components/tabs.md.erb +0 -686
- data/lib/docyard/templates/markdown/configuration.md.erb +0 -202
- data/lib/docyard/templates/markdown/core-concepts/file-structure.md.erb +0 -61
- data/lib/docyard/templates/markdown/core-concepts/markdown.md.erb +0 -90
- data/lib/docyard/templates/markdown/getting-started/introduction.md.erb +0 -30
- data/lib/docyard/templates/markdown/getting-started/quick-start.md.erb +0 -56
- 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,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
|
data/lib/docyard/config.rb
CHANGED
|
@@ -25,7 +25,9 @@ module Docyard
|
|
|
25
25
|
"base_url" => "/",
|
|
26
26
|
"clean" => true
|
|
27
27
|
},
|
|
28
|
-
"sidebar" =>
|
|
28
|
+
"sidebar" => {
|
|
29
|
+
"items" => []
|
|
30
|
+
}
|
|
29
31
|
}.freeze
|
|
30
32
|
|
|
31
33
|
attr_reader :data, :file_path
|
|
@@ -58,7 +60,7 @@ module Docyard
|
|
|
58
60
|
end
|
|
59
61
|
|
|
60
62
|
def sidebar
|
|
61
|
-
data["sidebar"]
|
|
63
|
+
@sidebar ||= ConfigSection.new(data["sidebar"])
|
|
62
64
|
end
|
|
63
65
|
|
|
64
66
|
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"/>',
|
data/lib/docyard/initializer.rb
CHANGED
|
@@ -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
|
-
"
|
|
16
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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 "
|
|
90
|
-
puts "
|
|
91
|
-
puts "
|
|
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
|
data/lib/docyard/markdown.rb
CHANGED
|
@@ -9,6 +9,7 @@ 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"
|
|
12
13
|
|
|
13
14
|
module Docyard
|
|
14
15
|
class Markdown
|
|
@@ -40,6 +41,18 @@ module Docyard
|
|
|
40
41
|
frontmatter["description"]
|
|
41
42
|
end
|
|
42
43
|
|
|
44
|
+
def sidebar_icon
|
|
45
|
+
frontmatter.dig("sidebar", "icon")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def sidebar_text
|
|
49
|
+
frontmatter.dig("sidebar", "text")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def sidebar_collapsed
|
|
53
|
+
frontmatter.dig("sidebar", "collapsed")
|
|
54
|
+
end
|
|
55
|
+
|
|
43
56
|
private
|
|
44
57
|
|
|
45
58
|
def parse_frontmatter
|
|
@@ -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
|
data/lib/docyard/renderer.rb
CHANGED
|
@@ -9,10 +9,11 @@ module Docyard
|
|
|
9
9
|
ERRORS_PATH = File.join(__dir__, "templates", "errors")
|
|
10
10
|
PARTIALS_PATH = File.join(__dir__, "templates", "partials")
|
|
11
11
|
|
|
12
|
-
attr_reader :layout_path
|
|
12
|
+
attr_reader :layout_path, :base_url
|
|
13
13
|
|
|
14
|
-
def initialize(layout: "default")
|
|
14
|
+
def initialize(layout: "default", base_url: "/")
|
|
15
15
|
@layout_path = File.join(LAYOUTS_PATH, "#{layout}.html.erb")
|
|
16
|
+
@base_url = normalize_base_url(base_url)
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
def render_file(file_path, sidebar_html: "", branding: {})
|
|
@@ -66,11 +67,24 @@ module Docyard
|
|
|
66
67
|
def asset_path(path)
|
|
67
68
|
return path if path.nil? || path.start_with?("http://", "https://")
|
|
68
69
|
|
|
69
|
-
"
|
|
70
|
+
"#{base_url}#{path}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def link_path(path)
|
|
74
|
+
return path if path.nil? || path.start_with?("http://", "https://")
|
|
75
|
+
|
|
76
|
+
"#{base_url.chomp('/')}#{path}"
|
|
70
77
|
end
|
|
71
78
|
|
|
72
79
|
private
|
|
73
80
|
|
|
81
|
+
def normalize_base_url(url)
|
|
82
|
+
return "/" if url.nil? || url.empty?
|
|
83
|
+
|
|
84
|
+
url = "/#{url}" unless url.start_with?("/")
|
|
85
|
+
url.end_with?("/") ? url : "#{url}/"
|
|
86
|
+
end
|
|
87
|
+
|
|
74
88
|
def assign_content_variables(content, page_title, sidebar_html)
|
|
75
89
|
@content = content
|
|
76
90
|
@page_title = page_title
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "item"
|
|
4
|
+
require_relative "title_extractor"
|
|
5
|
+
|
|
6
|
+
module Docyard
|
|
7
|
+
module Sidebar
|
|
8
|
+
class ConfigParser
|
|
9
|
+
attr_reader :config_items, :docs_path, :current_path, :title_extractor
|
|
10
|
+
|
|
11
|
+
def initialize(config_items, docs_path:, current_path: "/", title_extractor: TitleExtractor.new)
|
|
12
|
+
@config_items = config_items || []
|
|
13
|
+
@docs_path = docs_path
|
|
14
|
+
@current_path = Utils::PathResolver.normalize(current_path)
|
|
15
|
+
@title_extractor = title_extractor
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def parse
|
|
19
|
+
parse_items(config_items)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def parse_items(items, base_path = "")
|
|
25
|
+
items.map do |item_config|
|
|
26
|
+
parse_item(item_config, base_path)
|
|
27
|
+
end.compact
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def parse_item(item_config, base_path)
|
|
31
|
+
case item_config
|
|
32
|
+
when String
|
|
33
|
+
resolve_file_item(item_config, base_path)
|
|
34
|
+
when Hash
|
|
35
|
+
parse_hash_item(item_config, base_path)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def parse_hash_item(item_config, base_path)
|
|
40
|
+
return parse_link_item(item_config) if link_item?(item_config)
|
|
41
|
+
return parse_nested_item(item_config, base_path) if nested_item?(item_config)
|
|
42
|
+
return resolve_file_item(item_config.keys.first, base_path, {}) if nil_value_item?(item_config)
|
|
43
|
+
|
|
44
|
+
slug = item_config.keys.first
|
|
45
|
+
options = item_config.values.first || {}
|
|
46
|
+
resolve_file_item(slug, base_path, options)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def link_item?(config)
|
|
50
|
+
config.key?("link") || config.key?(:link)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def nested_item?(config)
|
|
54
|
+
config.size == 1 && config.values.first.is_a?(Hash)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def nil_value_item?(config)
|
|
58
|
+
config.size == 1 && config.values.first.nil?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def parse_link_item(config)
|
|
62
|
+
link = config["link"] || config[:link]
|
|
63
|
+
text = config["text"] || config[:text]
|
|
64
|
+
icon = config["icon"] || config[:icon]
|
|
65
|
+
target = config["target"] || config[:target] || "_blank"
|
|
66
|
+
|
|
67
|
+
Item.new(
|
|
68
|
+
text: text,
|
|
69
|
+
link: link,
|
|
70
|
+
path: link,
|
|
71
|
+
icon: icon,
|
|
72
|
+
target: target,
|
|
73
|
+
type: :external
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def parse_nested_item(item_config, base_path)
|
|
78
|
+
slug = item_config.keys.first.to_s
|
|
79
|
+
options = item_config.values.first || {}
|
|
80
|
+
nested_items = extract_nested_items(options)
|
|
81
|
+
|
|
82
|
+
dir_path = File.join(docs_path, base_path, slug)
|
|
83
|
+
|
|
84
|
+
if File.directory?(dir_path)
|
|
85
|
+
build_directory_item(slug, options, nested_items, base_path)
|
|
86
|
+
elsif nested_items.any?
|
|
87
|
+
build_file_with_children_item(slug, options, nested_items, base_path)
|
|
88
|
+
else
|
|
89
|
+
resolve_file_item(slug, base_path, options)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def extract_nested_items(options)
|
|
94
|
+
options["items"] || options[:items] || []
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def extract_common_options(options)
|
|
98
|
+
{
|
|
99
|
+
text: options["text"] || options[:text],
|
|
100
|
+
icon: options["icon"] || options[:icon],
|
|
101
|
+
collapsed: options["collapsed"] || options[:collapsed] || false
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def build_directory_item(slug, options, nested_items, base_path)
|
|
106
|
+
common_opts = extract_common_options(options)
|
|
107
|
+
new_base_path = File.join(base_path, slug)
|
|
108
|
+
parsed_items = parse_items(nested_items, new_base_path)
|
|
109
|
+
|
|
110
|
+
Item.new(
|
|
111
|
+
slug: slug,
|
|
112
|
+
text: common_opts[:text] || Utils::TextFormatter.titleize(slug),
|
|
113
|
+
icon: common_opts[:icon],
|
|
114
|
+
collapsed: common_opts[:collapsed],
|
|
115
|
+
items: parsed_items,
|
|
116
|
+
type: :directory
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def build_file_with_children_item(slug, options, nested_items, base_path)
|
|
121
|
+
common_opts = extract_common_options(options)
|
|
122
|
+
file_path = File.join(docs_path, base_path, "#{slug}.md")
|
|
123
|
+
url_path = Utils::PathResolver.to_url(File.join(base_path, slug))
|
|
124
|
+
resolved_text = common_opts[:text] || extract_file_title(file_path, slug)
|
|
125
|
+
|
|
126
|
+
Item.new(
|
|
127
|
+
slug: slug,
|
|
128
|
+
text: resolved_text,
|
|
129
|
+
path: url_path,
|
|
130
|
+
icon: common_opts[:icon],
|
|
131
|
+
collapsed: common_opts[:collapsed],
|
|
132
|
+
items: parse_items(nested_items, base_path),
|
|
133
|
+
active: current_path == url_path,
|
|
134
|
+
type: :file
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def extract_file_title(file_path, slug)
|
|
139
|
+
File.exist?(file_path) ? title_extractor.extract(file_path) : Utils::TextFormatter.titleize(slug)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def resolve_file_item(slug, base_path, options = {})
|
|
143
|
+
slug_str = slug.to_s
|
|
144
|
+
options ||= {}
|
|
145
|
+
|
|
146
|
+
file_path = File.join(docs_path, base_path, "#{slug_str}.md")
|
|
147
|
+
url_path = Utils::PathResolver.to_url(File.join(base_path, slug_str))
|
|
148
|
+
|
|
149
|
+
frontmatter = extract_frontmatter_metadata(file_path)
|
|
150
|
+
text = resolve_item_text(slug_str, file_path, options, frontmatter[:text])
|
|
151
|
+
icon = resolve_item_icon(options, frontmatter[:icon])
|
|
152
|
+
final_path = options["link"] || options[:link] || url_path
|
|
153
|
+
|
|
154
|
+
Item.new(
|
|
155
|
+
slug: slug_str, text: text, path: final_path, icon: icon,
|
|
156
|
+
active: current_path == final_path, type: :file
|
|
157
|
+
)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def extract_frontmatter_metadata(file_path)
|
|
161
|
+
return { text: nil, icon: nil } unless File.exist?(file_path)
|
|
162
|
+
|
|
163
|
+
markdown = Markdown.new(File.read(file_path))
|
|
164
|
+
{
|
|
165
|
+
text: markdown.sidebar_text || markdown.title,
|
|
166
|
+
icon: markdown.sidebar_icon
|
|
167
|
+
}
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def resolve_item_text(slug, file_path, options, frontmatter_text)
|
|
171
|
+
text = options["text"] || options[:text] || frontmatter_text
|
|
172
|
+
text || extract_file_title(file_path, slug)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def resolve_item_icon(options, frontmatter_icon)
|
|
176
|
+
options["icon"] || options[:icon] || frontmatter_icon
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
module Sidebar
|
|
5
|
+
class Item
|
|
6
|
+
attr_reader :slug, :text, :icon, :link, :target, :collapsed, :items, :path, :active, :type
|
|
7
|
+
|
|
8
|
+
def initialize(**options)
|
|
9
|
+
@slug = options[:slug]
|
|
10
|
+
@text = options[:text]
|
|
11
|
+
@icon = options[:icon]
|
|
12
|
+
@link = options[:link]
|
|
13
|
+
@target = options[:target] || "_self"
|
|
14
|
+
@collapsed = options[:collapsed] || false
|
|
15
|
+
@items = options[:items] || []
|
|
16
|
+
@path = options[:path] || options[:link]
|
|
17
|
+
@active = options[:active] || false
|
|
18
|
+
@type = options[:type] || :file
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def external?
|
|
22
|
+
return false if path.nil?
|
|
23
|
+
|
|
24
|
+
path.start_with?("http://", "https://")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def children?
|
|
28
|
+
items.any?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def title
|
|
32
|
+
text
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def children
|
|
36
|
+
items
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def collapsible?
|
|
40
|
+
children?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to_h
|
|
44
|
+
{
|
|
45
|
+
title: title,
|
|
46
|
+
path: path,
|
|
47
|
+
icon: icon,
|
|
48
|
+
active: active,
|
|
49
|
+
type: type,
|
|
50
|
+
collapsed: collapsed,
|
|
51
|
+
collapsible: collapsible?,
|
|
52
|
+
target: target,
|
|
53
|
+
children: children.map(&:to_h)
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|