docyard 1.1.0 → 1.3.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 +29 -1
- data/README.md +56 -11
- data/lib/docyard/build/asset_bundler.rb +17 -1
- data/lib/docyard/build/social_cards/card_renderer.rb +132 -0
- data/lib/docyard/build/social_cards/doc_card.rb +97 -0
- data/lib/docyard/build/social_cards/homepage_card.rb +98 -0
- data/lib/docyard/build/social_cards_generator.rb +188 -0
- data/lib/docyard/build/step_runner.rb +2 -0
- data/lib/docyard/builder.rb +10 -0
- data/lib/docyard/cli.rb +44 -0
- data/lib/docyard/config/branding_resolver.rb +1 -1
- data/lib/docyard/config/schema/definition.rb +2 -1
- data/lib/docyard/config/schema/sections.rb +0 -1
- data/lib/docyard/config/schema/simple_sections.rb +7 -0
- data/lib/docyard/config.rb +3 -1
- data/lib/docyard/customizer.rb +196 -0
- data/lib/docyard/deploy/adapters/base.rb +56 -0
- data/lib/docyard/deploy/adapters/cloudflare.rb +38 -0
- data/lib/docyard/deploy/adapters/github_pages.rb +60 -0
- data/lib/docyard/deploy/adapters/netlify.rb +37 -0
- data/lib/docyard/deploy/adapters/vercel.rb +36 -0
- data/lib/docyard/deploy/deployer.rb +95 -0
- data/lib/docyard/deploy/platform_detector.rb +32 -0
- data/lib/docyard/errors.rb +2 -0
- data/lib/docyard/rendering/markdown.rb +4 -0
- data/lib/docyard/rendering/og_helpers.rb +19 -1
- data/lib/docyard/rendering/renderer.rb +10 -1
- data/lib/docyard/search/build_indexer.rb +8 -3
- data/lib/docyard/search/dev_indexer.rb +3 -2
- data/lib/docyard/search/pagefind_binary.rb +185 -0
- data/lib/docyard/search/pagefind_support.rb +9 -7
- data/lib/docyard/server/asset_handler.rb +28 -2
- data/lib/docyard/server/dev_server.rb +1 -0
- data/lib/docyard/server/file_watcher.rb +10 -5
- data/lib/docyard/server/rack_application.rb +1 -1
- data/lib/docyard/templates/assets/css/variables.css +0 -6
- data/lib/docyard/templates/assets/js/components/abbreviation.js +20 -11
- data/lib/docyard/templates/assets/js/components/code-block.js +8 -3
- data/lib/docyard/templates/assets/js/components/code-group.js +20 -3
- data/lib/docyard/templates/assets/js/components/file-tree.js +9 -3
- data/lib/docyard/templates/assets/js/components/heading-anchor.js +71 -72
- data/lib/docyard/templates/assets/js/components/lightbox.js +10 -3
- data/lib/docyard/templates/assets/js/components/tabs.js +8 -3
- data/lib/docyard/templates/assets/js/components/tooltip.js +32 -23
- data/lib/docyard/templates/assets/js/hot-reload.js +42 -2
- data/lib/docyard/templates/partials/_head.html.erb +4 -0
- data/lib/docyard/templates/partials/_scripts.html.erb +3 -0
- data/lib/docyard/version.rb +1 -1
- data/lib/docyard.rb +2 -0
- metadata +14 -1
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "yaml"
|
|
5
|
+
require "parallel"
|
|
6
|
+
require_relative "social_cards/card_renderer"
|
|
7
|
+
require_relative "social_cards/homepage_card"
|
|
8
|
+
require_relative "social_cards/doc_card"
|
|
9
|
+
|
|
10
|
+
module Docyard
|
|
11
|
+
module Build
|
|
12
|
+
class SocialCardsGenerator
|
|
13
|
+
OUTPUT_DIR = "_docyard/og"
|
|
14
|
+
FRONTMATTER_REGEX = /\A---\s*\n(.*?\n)---\s*\n/m
|
|
15
|
+
HOMEPAGE_PATHS = ["/", "/index"].freeze
|
|
16
|
+
PARALLEL_THRESHOLD = 5
|
|
17
|
+
|
|
18
|
+
attr_reader :config, :verbose
|
|
19
|
+
|
|
20
|
+
def initialize(config, verbose: false)
|
|
21
|
+
@config = config
|
|
22
|
+
@verbose = verbose
|
|
23
|
+
@generated_cards = []
|
|
24
|
+
@failed_cards = []
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def generate
|
|
28
|
+
ensure_vips_available!
|
|
29
|
+
pages = collect_pages
|
|
30
|
+
generate_cards_for(pages)
|
|
31
|
+
[successful_count, build_verbose_details]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def successful_count
|
|
35
|
+
@generated_cards.compact.size
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def card_path_for(url_path)
|
|
39
|
+
File.join("/", OUTPUT_DIR, url_to_filename(url_path))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def generate_cards_for(pages)
|
|
45
|
+
if pages.size >= PARALLEL_THRESHOLD
|
|
46
|
+
results = Parallel.map(pages, in_threads: Parallel.processor_count) do |page|
|
|
47
|
+
generate_card(page)
|
|
48
|
+
end
|
|
49
|
+
@generated_cards = results.compact
|
|
50
|
+
else
|
|
51
|
+
pages.each { |page| @generated_cards << generate_card(page) }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def collect_pages
|
|
56
|
+
pages = []
|
|
57
|
+
|
|
58
|
+
Dir.glob(File.join(docs_path, "**", "*.md")).each do |file_path|
|
|
59
|
+
frontmatter = extract_frontmatter(file_path)
|
|
60
|
+
relative_path = file_path.delete_prefix("#{docs_path}/")
|
|
61
|
+
url_path = markdown_to_url(relative_path)
|
|
62
|
+
|
|
63
|
+
pages << build_page_data(file_path, url_path, frontmatter)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
pages
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def build_page_data(file_path, url_path, frontmatter)
|
|
70
|
+
social_cards_config = frontmatter["social_cards"] || {}
|
|
71
|
+
section = derive_section(url_path)
|
|
72
|
+
|
|
73
|
+
{
|
|
74
|
+
file_path: file_path,
|
|
75
|
+
url_path: url_path,
|
|
76
|
+
title: resolve_title(social_cards_config, frontmatter, url_path),
|
|
77
|
+
description: social_cards_config["description"] || frontmatter["description"],
|
|
78
|
+
section: section,
|
|
79
|
+
is_homepage: homepage?(url_path)
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def resolve_title(social_cards_config, frontmatter, url_path)
|
|
84
|
+
title = social_cards_config["title"]
|
|
85
|
+
return title if title && !title.strip.empty?
|
|
86
|
+
|
|
87
|
+
title = frontmatter["title"]
|
|
88
|
+
return title if title && !title.strip.empty?
|
|
89
|
+
|
|
90
|
+
derive_title_from_path(url_path)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def ensure_vips_available!
|
|
94
|
+
require "vips"
|
|
95
|
+
rescue LoadError
|
|
96
|
+
raise Docyard::Error, SocialCards::CardRenderer::VIPS_INSTALL_INSTRUCTIONS
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def generate_card(page)
|
|
100
|
+
output_path = File.join(config.build.output, OUTPUT_DIR, url_to_filename(page[:url_path]))
|
|
101
|
+
card = build_card_for(page)
|
|
102
|
+
card.render(output_path)
|
|
103
|
+
output_path
|
|
104
|
+
rescue StandardError => e
|
|
105
|
+
log_card_error(page, e.message)
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def log_card_error(page, message)
|
|
110
|
+
@failed_cards << page[:url_path]
|
|
111
|
+
Docyard.logger.warn("Social card generation failed for #{page[:url_path]}: #{message}")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def build_card_for(page)
|
|
115
|
+
if page[:is_homepage]
|
|
116
|
+
SocialCards::HomepageCard.new(config, title: page[:title])
|
|
117
|
+
else
|
|
118
|
+
SocialCards::DocCard.new(
|
|
119
|
+
config,
|
|
120
|
+
title: page[:title],
|
|
121
|
+
section: page[:section],
|
|
122
|
+
description: page[:description]
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def extract_frontmatter(file_path)
|
|
128
|
+
content = File.read(file_path)
|
|
129
|
+
match = content.match(FRONTMATTER_REGEX)
|
|
130
|
+
return {} unless match
|
|
131
|
+
|
|
132
|
+
YAML.safe_load(match[1]) || {}
|
|
133
|
+
rescue Psych::SyntaxError
|
|
134
|
+
{}
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def markdown_to_url(relative_path)
|
|
138
|
+
path = relative_path
|
|
139
|
+
.delete_suffix(".md")
|
|
140
|
+
.delete_suffix("/index")
|
|
141
|
+
|
|
142
|
+
path = "index" if path == "index" || path.empty?
|
|
143
|
+
"/#{path}"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def url_to_filename(url_path)
|
|
147
|
+
path = url_path.delete_prefix("/")
|
|
148
|
+
path = "index" if path.empty?
|
|
149
|
+
"#{path}.png"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def homepage?(url_path)
|
|
153
|
+
HOMEPAGE_PATHS.include?(url_path)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def derive_section(url_path)
|
|
157
|
+
parts = url_path.delete_prefix("/").split("/")
|
|
158
|
+
return nil if parts.size <= 1
|
|
159
|
+
|
|
160
|
+
parts.first.tr("-", " ").split.map(&:capitalize).join(" ")
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def derive_title_from_path(url_path)
|
|
164
|
+
filename = File.basename(url_path)
|
|
165
|
+
filename = "Home" if filename == "index" || filename.empty?
|
|
166
|
+
filename.tr("-", " ").split.map(&:capitalize).join(" ")
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def docs_path
|
|
170
|
+
config.source
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def build_verbose_details
|
|
174
|
+
return nil unless verbose
|
|
175
|
+
|
|
176
|
+
details = @generated_cards.compact.map do |path|
|
|
177
|
+
path.delete_prefix("#{config.build.output}/")
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
@failed_cards.each do |url_path|
|
|
181
|
+
details << "FAILED: #{url_path}"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
details
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -5,6 +5,7 @@ module Docyard
|
|
|
5
5
|
class StepRunner
|
|
6
6
|
STEP_SHORT_LABELS = {
|
|
7
7
|
"Generating pages" => "Pages",
|
|
8
|
+
"Social cards" => "Cards",
|
|
8
9
|
"Bundling assets" => "Assets",
|
|
9
10
|
"Copying files" => "Files",
|
|
10
11
|
"Generating SEO" => "SEO",
|
|
@@ -59,6 +60,7 @@ module Docyard
|
|
|
59
60
|
def format_result(label, result)
|
|
60
61
|
case label
|
|
61
62
|
when "Generating pages" then "done (#{result} pages)"
|
|
63
|
+
when "Social cards" then "done (#{result} cards)"
|
|
62
64
|
when "Bundling assets" then format_assets_result(result)
|
|
63
65
|
when "Copying files" then "done (#{result} files)"
|
|
64
66
|
when "Generating SEO" then "done (#{result.join(', ')})"
|
data/lib/docyard/builder.rb
CHANGED
|
@@ -52,6 +52,7 @@ module Docyard
|
|
|
52
52
|
@step_runner.run("Generating pages") { generate_static_pages }
|
|
53
53
|
@step_runner.run("Bundling assets") { bundle_assets }
|
|
54
54
|
@step_runner.run("Copying files") { copy_static_files }
|
|
55
|
+
@step_runner.run("Social cards") { generate_social_cards } if social_cards_enabled?
|
|
55
56
|
@step_runner.run("Generating SEO") { generate_seo_files }
|
|
56
57
|
@step_runner.run("Indexing search") { generate_search_index }
|
|
57
58
|
end
|
|
@@ -130,6 +131,15 @@ module Docyard
|
|
|
130
131
|
Search::BuildIndexer.new(config, verbose: verbose).index
|
|
131
132
|
end
|
|
132
133
|
|
|
134
|
+
def generate_social_cards
|
|
135
|
+
require_relative "build/social_cards_generator"
|
|
136
|
+
Build::SocialCardsGenerator.new(config, verbose: verbose).generate
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def social_cards_enabled?
|
|
140
|
+
config.social_cards&.enabled == true
|
|
141
|
+
end
|
|
142
|
+
|
|
133
143
|
def robots_txt_content
|
|
134
144
|
base = config.build.base
|
|
135
145
|
base = "#{base}/" unless base.end_with?("/")
|
data/lib/docyard/cli.rb
CHANGED
|
@@ -80,6 +80,40 @@ module Docyard
|
|
|
80
80
|
exit(doctor.run)
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
desc "deploy", "Deploy the built site to a hosting platform"
|
|
84
|
+
method_option :to, type: :string, desc: "Target platform (vercel, netlify, cloudflare, github-pages)"
|
|
85
|
+
method_option :prod, type: :boolean, default: true, desc: "Deploy to production"
|
|
86
|
+
method_option :skip_build, type: :boolean, default: false, desc: "Skip building before deploy"
|
|
87
|
+
def deploy
|
|
88
|
+
apply_global_options
|
|
89
|
+
require_relative "deploy/deployer"
|
|
90
|
+
deployer = Deploy::Deployer.new(
|
|
91
|
+
to: options[:to],
|
|
92
|
+
production: options[:prod],
|
|
93
|
+
skip_build: options[:skip_build]
|
|
94
|
+
)
|
|
95
|
+
exit(1) unless deployer.deploy
|
|
96
|
+
rescue ConfigError => e
|
|
97
|
+
print_config_error(e)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
desc "customize", "Generate theme customization files"
|
|
101
|
+
method_option :minimal, type: :boolean, default: false, aliases: "-m",
|
|
102
|
+
desc: "Generate minimal files without comments"
|
|
103
|
+
def customize
|
|
104
|
+
apply_global_options
|
|
105
|
+
require_relative "customizer"
|
|
106
|
+
Docyard::Customizer.new(minimal: options[:minimal]).generate
|
|
107
|
+
rescue ConfigError => e
|
|
108
|
+
print_config_error(e)
|
|
109
|
+
rescue Errno::EACCES => e
|
|
110
|
+
print_file_error("Permission denied", e.message)
|
|
111
|
+
rescue Errno::ENOSPC
|
|
112
|
+
print_file_error("Disk full", "No space left on device")
|
|
113
|
+
rescue SystemCallError => e
|
|
114
|
+
print_file_error("File operation failed", e.message)
|
|
115
|
+
end
|
|
116
|
+
|
|
83
117
|
private
|
|
84
118
|
|
|
85
119
|
def apply_global_options
|
|
@@ -94,5 +128,15 @@ module Docyard
|
|
|
94
128
|
puts
|
|
95
129
|
exit(1)
|
|
96
130
|
end
|
|
131
|
+
|
|
132
|
+
def print_file_error(title, message)
|
|
133
|
+
puts
|
|
134
|
+
puts " #{UI.bold('Docyard')} v#{VERSION}"
|
|
135
|
+
puts
|
|
136
|
+
puts " #{UI.red("#{title}:")}"
|
|
137
|
+
puts " #{message}"
|
|
138
|
+
puts
|
|
139
|
+
exit(1)
|
|
140
|
+
end
|
|
97
141
|
end
|
|
98
142
|
end
|
|
@@ -38,7 +38,7 @@ module Docyard
|
|
|
38
38
|
site_options, logo_options, search_options, credits_options, social_options,
|
|
39
39
|
navigation_options, tabs_options, announcement_options, repo_options, analytics_options,
|
|
40
40
|
color_options
|
|
41
|
-
].reduce({}, :merge)
|
|
41
|
+
].reduce({}, :merge).merge(social_cards_enabled: config.social_cards&.enabled == true)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def site_options
|
data/lib/docyard/config.rb
CHANGED
|
@@ -26,7 +26,8 @@ module Docyard
|
|
|
26
26
|
"repo" => { "url" => nil, "branch" => "main", "edit_path" => nil, "edit_link" => true,
|
|
27
27
|
"last_updated" => true },
|
|
28
28
|
"analytics" => { "google" => nil, "plausible" => nil, "fathom" => nil, "script" => nil },
|
|
29
|
-
"feedback" => { "enabled" => false, "question" => "Was this page helpful?" }
|
|
29
|
+
"feedback" => { "enabled" => false, "question" => "Was this page helpful?" },
|
|
30
|
+
"social_cards" => { "enabled" => false }
|
|
30
31
|
}.freeze
|
|
31
32
|
|
|
32
33
|
attr_reader :data, :file_path
|
|
@@ -63,6 +64,7 @@ module Docyard
|
|
|
63
64
|
def repo = @repo ||= Section.new(data["repo"])
|
|
64
65
|
def analytics = @analytics ||= Section.new(data["analytics"])
|
|
65
66
|
def feedback = @feedback ||= Section.new(data["feedback"])
|
|
67
|
+
def social_cards = @social_cards ||= Section.new(data["social_cards"])
|
|
66
68
|
|
|
67
69
|
def announcement
|
|
68
70
|
@announcement ||= data["announcement"] ? Section.new(data["announcement"]) : nil
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
class Customizer
|
|
5
|
+
CUSTOM_DIR = "_custom"
|
|
6
|
+
STYLES_FILE = "styles.css"
|
|
7
|
+
SCRIPTS_FILE = "scripts.js"
|
|
8
|
+
VARIABLES_PATH = File.join(__dir__, "templates", "assets", "css", "variables.css")
|
|
9
|
+
|
|
10
|
+
CATEGORY_PATTERNS = {
|
|
11
|
+
/^--sidebar-width/ => "Layout",
|
|
12
|
+
/^--sidebar/ => "Sidebar",
|
|
13
|
+
/^--code|^--diff/ => "Code",
|
|
14
|
+
/^--callout/ => "Callouts",
|
|
15
|
+
/^--table|^--hr/ => "Tables",
|
|
16
|
+
/^--search|^--overlay/ => "Search",
|
|
17
|
+
/^--font|^--text|^--leading/ => "Typography",
|
|
18
|
+
/^--spacing/ => "Spacing",
|
|
19
|
+
/^--toc|^--header|^--content|^--layout|^--tab-bar|^--secondary-header/ => "Layout",
|
|
20
|
+
/^--radius/ => "Radius",
|
|
21
|
+
/^--shadow/ => "Shadows",
|
|
22
|
+
/^--transition/ => "Transitions",
|
|
23
|
+
/^--z-/ => "Z-Index",
|
|
24
|
+
/^--ring/ => "Other"
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
CATEGORIES = %w[
|
|
28
|
+
Colors Sidebar Code Callouts Tables Search
|
|
29
|
+
Typography Spacing Layout Radius Shadows Transitions Z-Index Other
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
attr_reader :config, :minimal
|
|
33
|
+
|
|
34
|
+
def initialize(minimal: false)
|
|
35
|
+
@config = Config.load
|
|
36
|
+
@minimal = minimal
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def generate
|
|
40
|
+
validate_source_directory
|
|
41
|
+
create_custom_directory
|
|
42
|
+
write_styles_file
|
|
43
|
+
write_scripts_file
|
|
44
|
+
print_success
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def validate_source_directory
|
|
50
|
+
return if File.directory?(config.source)
|
|
51
|
+
|
|
52
|
+
raise ConfigError, "Source directory '#{config.source}' does not exist"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def custom_dir_path
|
|
56
|
+
@custom_dir_path ||= File.join(config.source, CUSTOM_DIR)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def styles_path
|
|
60
|
+
@styles_path ||= File.join(custom_dir_path, STYLES_FILE)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def scripts_path
|
|
64
|
+
@scripts_path ||= File.join(custom_dir_path, SCRIPTS_FILE)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def create_custom_directory
|
|
68
|
+
FileUtils.mkdir_p(custom_dir_path)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def write_styles_file
|
|
72
|
+
File.write(styles_path, minimal ? minimal_styles : annotated_styles)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def write_scripts_file
|
|
76
|
+
File.write(scripts_path, minimal ? minimal_scripts : annotated_scripts)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def annotated_styles
|
|
80
|
+
build_annotated_css(parse_variables)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def minimal_styles
|
|
84
|
+
build_minimal_css(parse_variables)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def parse_variables
|
|
88
|
+
content = File.read(VARIABLES_PATH)
|
|
89
|
+
{ light: extract_variables(content, ":root"), dark: extract_variables(content, ".dark") }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def extract_variables(content, selector)
|
|
93
|
+
match = content.match(/#{Regexp.escape(selector)}\s*\{([^}]+)\}/m)
|
|
94
|
+
return [] unless match
|
|
95
|
+
|
|
96
|
+
match[1].scan(/(--[\w-]+):\s*([^;]+);/).map { |name, value| { name: name, value: value.strip } }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def build_annotated_css(variables)
|
|
100
|
+
light = format_annotated(variables[:light])
|
|
101
|
+
dark = format_annotated(variables[:dark])
|
|
102
|
+
"#{css_header}:root {\n#{light}\n}\n\n.dark {\n#{dark}\n}\n"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def build_minimal_css(variables)
|
|
106
|
+
":root {\n#{format_minimal(variables[:light])}\n}\n\n.dark {\n#{format_minimal(variables[:dark])}\n}\n"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def css_header
|
|
110
|
+
<<~HEADER
|
|
111
|
+
/* =============================================================================
|
|
112
|
+
DOCYARD THEME CUSTOMIZATION
|
|
113
|
+
|
|
114
|
+
Uncomment and modify variables to customize your site.
|
|
115
|
+
Delete any variables you don't need to change.
|
|
116
|
+
|
|
117
|
+
Generated with: docyard customize
|
|
118
|
+
============================================================================= */
|
|
119
|
+
|
|
120
|
+
HEADER
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def format_annotated(vars)
|
|
124
|
+
group_variables(vars).flat_map do |category, category_vars|
|
|
125
|
+
[" /* #{category} */"] + category_vars.map { |v| " /* #{v[:name]}: #{v[:value]}; */" } + [""]
|
|
126
|
+
end.join("\n").rstrip
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def format_minimal(vars)
|
|
130
|
+
vars.map { |var| " /* #{var[:name]}: #{var[:value]}; */" }.join("\n")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def group_variables(vars)
|
|
134
|
+
groups = CATEGORIES.to_h { |cat| [cat, []] }
|
|
135
|
+
vars.each { |var| groups[categorize_variable(var[:name])] << var }
|
|
136
|
+
groups.reject { |_, v| v.empty? }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def categorize_variable(name)
|
|
140
|
+
CATEGORY_PATTERNS.each { |pattern, category| return category if name.match?(pattern) }
|
|
141
|
+
"Colors"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def annotated_scripts
|
|
145
|
+
"#{js_header}document.addEventListener('DOMContentLoaded', function() {\n // Your custom JavaScript here\n});\n"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def js_header
|
|
149
|
+
<<~HEADER
|
|
150
|
+
/* =============================================================================
|
|
151
|
+
DOCYARD CUSTOM SCRIPTS
|
|
152
|
+
|
|
153
|
+
This file is loaded on every page after the default scripts.
|
|
154
|
+
|
|
155
|
+
Generated with: docyard customize
|
|
156
|
+
============================================================================= */
|
|
157
|
+
|
|
158
|
+
HEADER
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def minimal_scripts
|
|
162
|
+
"document.addEventListener('DOMContentLoaded', function() {\n // Your custom JavaScript here\n});\n"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def print_success
|
|
166
|
+
puts
|
|
167
|
+
print_header
|
|
168
|
+
print_created_files
|
|
169
|
+
print_next_steps
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def print_header
|
|
173
|
+
puts " #{UI.bold('Docyard')} v#{VERSION}"
|
|
174
|
+
puts
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def print_created_files
|
|
178
|
+
puts " #{UI.success('Created:')}"
|
|
179
|
+
puts UI.dim(" #{relative_path(custom_dir_path)}/")
|
|
180
|
+
puts UI.dim(" #{STYLES_FILE}")
|
|
181
|
+
puts UI.dim(" #{SCRIPTS_FILE}")
|
|
182
|
+
puts
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def print_next_steps
|
|
186
|
+
puts " #{UI.bold('Next steps:')}"
|
|
187
|
+
puts " Edit #{UI.cyan(relative_path(styles_path))} to customize theme"
|
|
188
|
+
puts " Changes apply on next serve or build"
|
|
189
|
+
puts
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def relative_path(path)
|
|
193
|
+
path.sub("#{Dir.pwd}/", "")
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module Docyard
|
|
6
|
+
module Deploy
|
|
7
|
+
module Adapters
|
|
8
|
+
class Base
|
|
9
|
+
attr_reader :output_dir, :production, :config
|
|
10
|
+
|
|
11
|
+
def initialize(output_dir:, production:, config:)
|
|
12
|
+
@output_dir = output_dir
|
|
13
|
+
@production = production
|
|
14
|
+
@config = config
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def deploy
|
|
18
|
+
check_cli_installed!
|
|
19
|
+
run_deploy
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def platform_name
|
|
23
|
+
raise NotImplementedError
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def cli_name
|
|
29
|
+
raise NotImplementedError
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def cli_install_hint
|
|
33
|
+
raise NotImplementedError
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def run_deploy
|
|
37
|
+
raise NotImplementedError
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def check_cli_installed!
|
|
41
|
+
_, _, status = Open3.capture3("which", cli_name)
|
|
42
|
+
return if status.success?
|
|
43
|
+
|
|
44
|
+
raise DeployError, "'#{cli_name}' CLI not found. Install it with: #{cli_install_hint}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def execute_command(*)
|
|
48
|
+
stdout, stderr, status = Open3.capture3(*)
|
|
49
|
+
return stdout if status.success?
|
|
50
|
+
|
|
51
|
+
raise DeployError, "Deploy command failed: #{stderr.strip.empty? ? stdout.strip : stderr.strip}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Docyard
|
|
6
|
+
module Deploy
|
|
7
|
+
module Adapters
|
|
8
|
+
class Cloudflare < Base
|
|
9
|
+
def platform_name
|
|
10
|
+
"Cloudflare Pages"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def cli_name
|
|
16
|
+
"wrangler"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def cli_install_hint
|
|
20
|
+
"npm i -g wrangler"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def run_deploy
|
|
24
|
+
output = execute_command("wrangler", "pages", "deploy", output_dir, "--project-name=#{project_name}")
|
|
25
|
+
extract_url(output)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def project_name
|
|
29
|
+
config.title.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-|-\z/, "")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def extract_url(output)
|
|
33
|
+
output.match(%r{https://\S+\.pages\.dev\S*})&.to_s
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require_relative "base"
|
|
6
|
+
|
|
7
|
+
module Docyard
|
|
8
|
+
module Deploy
|
|
9
|
+
module Adapters
|
|
10
|
+
class GithubPages < Base
|
|
11
|
+
def platform_name
|
|
12
|
+
"GitHub Pages"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def cli_name
|
|
18
|
+
"gh"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def cli_install_hint
|
|
22
|
+
"https://cli.github.com"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def run_deploy
|
|
26
|
+
remote_url = fetch_remote_url
|
|
27
|
+
Dir.mktmpdir do |tmp|
|
|
28
|
+
prepare_deploy_dir(tmp)
|
|
29
|
+
push_to_gh_pages(tmp, remote_url)
|
|
30
|
+
end
|
|
31
|
+
build_pages_url(remote_url)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def fetch_remote_url
|
|
35
|
+
execute_command("git", "remote", "get-url", "origin").strip
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def prepare_deploy_dir(tmp)
|
|
39
|
+
FileUtils.cp_r("#{output_dir}/.", tmp)
|
|
40
|
+
execute_command("git", "-C", tmp, "init", "-b", "gh-pages")
|
|
41
|
+
execute_command("git", "-C", tmp, "add", ".")
|
|
42
|
+
execute_command("git", "-C", tmp, "commit", "-m", "Deploy via docyard")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def push_to_gh_pages(tmp, remote_url)
|
|
46
|
+
execute_command("git", "-C", tmp, "remote", "add", "origin", remote_url)
|
|
47
|
+
execute_command("git", "-C", tmp, "push", "--force", "origin", "gh-pages")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def build_pages_url(remote_url)
|
|
51
|
+
match = remote_url.match(%r{github\.com[:/]([^/]+)/([^/.]+)})
|
|
52
|
+
return nil unless match
|
|
53
|
+
|
|
54
|
+
owner, repo = match.captures
|
|
55
|
+
"https://#{owner}.github.io/#{repo}/"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|