pocketbook 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +308 -0
- data/bin/pocketbook +5 -0
- data/bin/test-render +49 -0
- data/lib/pocketbook/book.rb +28 -0
- data/lib/pocketbook/book_renderer/chapter.rb +85 -0
- data/lib/pocketbook/book_renderer/front_matter.rb +27 -0
- data/lib/pocketbook/book_renderer/metadata.rb +70 -0
- data/lib/pocketbook/book_renderer/pdf.rb +42 -0
- data/lib/pocketbook/book_renderer/toc.rb +55 -0
- data/lib/pocketbook/book_renderer.rb +140 -0
- data/lib/pocketbook/book_template.rb +40 -0
- data/lib/pocketbook/cli/options_parser.rb +344 -0
- data/lib/pocketbook/cli/runner.rb +505 -0
- data/lib/pocketbook/cli/watch_command.rb +275 -0
- data/lib/pocketbook/cli.rb +12 -0
- data/lib/pocketbook/core_stylesheet.rb +20 -0
- data/lib/pocketbook/pdf_document.rb +96 -0
- data/lib/pocketbook/render_request.rb +67 -0
- data/lib/pocketbook/styles/core/01_tokens.css +11 -0
- data/lib/pocketbook/styles/core/02_pages.css +72 -0
- data/lib/pocketbook/styles/core/03_layout.css +162 -0
- data/lib/pocketbook/styles/core/04_toc.css +62 -0
- data/lib/pocketbook/styles/core/05_content.css +49 -0
- data/lib/pocketbook/styles/core/06_running.css +48 -0
- data/lib/pocketbook/styles/core/07_print.css +12 -0
- data/lib/pocketbook/theme/manifest.rb +244 -0
- data/lib/pocketbook/theme.rb +268 -0
- data/lib/pocketbook/version.rb +3 -0
- data/lib/pocketbook.rb +29 -0
- data/themes/basic/styles/plain.css +63 -0
- data/themes/basic/template.html.erb +30 -0
- data/themes/basic/theme.yml +8 -0
- data/themes/classic/styles/base.css +250 -0
- data/themes/classic/styles/dark.css +19 -0
- data/themes/classic/styles/light.css +12 -0
- data/themes/classic/styles/sepia.css +17 -0
- data/themes/classic/template.html.erb +72 -0
- data/themes/classic/theme.yml +17 -0
- metadata +136 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
require "erb"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
require_relative "book_template"
|
|
5
|
+
require_relative "core_stylesheet"
|
|
6
|
+
require_relative "book_renderer/front_matter"
|
|
7
|
+
require_relative "book_renderer/chapter"
|
|
8
|
+
require_relative "book_renderer/metadata"
|
|
9
|
+
require_relative "book_renderer/toc"
|
|
10
|
+
require_relative "book_renderer/pdf"
|
|
11
|
+
|
|
12
|
+
module Pocketbook
|
|
13
|
+
class BookRenderer
|
|
14
|
+
DEFAULT_METADATA = Metadata::DEFAULT_VALUES
|
|
15
|
+
|
|
16
|
+
def initialize(inputs:, theme:, output_path: nil, metadata: {})
|
|
17
|
+
@inputs = inputs
|
|
18
|
+
@theme = theme
|
|
19
|
+
@output_path = output_path
|
|
20
|
+
@cli_metadata = symbolize_keys(metadata)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def render
|
|
24
|
+
raise ArgumentError, "Output PDF path is required" if blank?(@output_path)
|
|
25
|
+
|
|
26
|
+
validate_paths!
|
|
27
|
+
|
|
28
|
+
chapters, front_matter = chapter_compiler.compile(inputs: @inputs)
|
|
29
|
+
metadata = resolved_metadata(front_matter)
|
|
30
|
+
|
|
31
|
+
toc_targets = toc_builder.targets(chapters: chapters)
|
|
32
|
+
html_without_toc_pages = build_document_html(chapters, metadata, toc_page_numbers: {})
|
|
33
|
+
toc_page_numbers = resolve_toc_page_numbers(html_without_toc_pages, toc_targets)
|
|
34
|
+
html = build_document_html(chapters, metadata, toc_page_numbers: toc_page_numbers)
|
|
35
|
+
|
|
36
|
+
FileUtils.mkdir_p(File.dirname(@output_path))
|
|
37
|
+
generate_pdf_to_path(html, @output_path)
|
|
38
|
+
|
|
39
|
+
@output_path
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def render_html
|
|
43
|
+
validate_paths!
|
|
44
|
+
|
|
45
|
+
chapters, front_matter = chapter_compiler.compile(inputs: @inputs)
|
|
46
|
+
metadata = resolved_metadata(front_matter)
|
|
47
|
+
|
|
48
|
+
build_document_html(chapters, metadata, toc_page_numbers: {})
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def validate_paths!
|
|
54
|
+
raise ArgumentError, "At least one markdown input file is required" if @inputs.empty?
|
|
55
|
+
|
|
56
|
+
@inputs.each do |path|
|
|
57
|
+
raise ArgumentError, "Input markdown file not found: #{path}" unless File.exist?(path)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def build_document_html(chapters, metadata, toc_page_numbers:)
|
|
62
|
+
template = BookTemplate.new(template_source: @theme.template_source)
|
|
63
|
+
template.render(
|
|
64
|
+
body_html: build_body_html(chapters),
|
|
65
|
+
toc_html: build_toc_html(chapters, toc_page_numbers: toc_page_numbers),
|
|
66
|
+
styles_css: @theme.styles_css,
|
|
67
|
+
core_css: core_css,
|
|
68
|
+
metadata: metadata,
|
|
69
|
+
escape_html: method(:h)
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def build_body_html(chapters)
|
|
74
|
+
chapters.map { |chapter| chapter.article_html(escape_html: method(:h)) }.join("\n")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def build_toc_html(chapters, toc_page_numbers:)
|
|
78
|
+
toc_builder.build(chapters: chapters, escape_html: method(:h), page_numbers: toc_page_numbers)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def generate_pdf_to_path(html, output_path)
|
|
82
|
+
pdf_writer.write(html: html, output_path: output_path)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def resolve_toc_page_numbers(html, toc_targets)
|
|
86
|
+
return {} if toc_targets.empty?
|
|
87
|
+
|
|
88
|
+
Dir.mktmpdir("pocketbook-toc") do |dir|
|
|
89
|
+
temp_pdf_path = File.join(dir, "toc-pass.pdf")
|
|
90
|
+
generate_pdf_to_path(html, temp_pdf_path)
|
|
91
|
+
pdf_writer.toc_page_numbers(pdf_path: temp_pdf_path, toc_targets: toc_targets)
|
|
92
|
+
end
|
|
93
|
+
rescue StandardError
|
|
94
|
+
{}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def resolved_metadata(front_matter)
|
|
98
|
+
metadata_resolver.resolve(
|
|
99
|
+
theme_defaults: @theme.defaults_metadata,
|
|
100
|
+
cli_metadata: @cli_metadata,
|
|
101
|
+
front_matter: front_matter,
|
|
102
|
+
first_input_path: @inputs.first
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def h(value)
|
|
107
|
+
ERB::Util.html_escape(value.to_s)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def core_css
|
|
111
|
+
CoreStylesheet.load
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def chapter_compiler
|
|
115
|
+
@chapter_compiler ||= Chapter.new
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def metadata_resolver
|
|
119
|
+
@metadata_resolver ||= Metadata.new
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def toc_builder
|
|
123
|
+
@toc_builder ||= Toc.new
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def pdf_writer
|
|
127
|
+
@pdf_writer ||= Pdf.new
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def blank?(value)
|
|
131
|
+
value.nil? || value.to_s.strip.empty?
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def symbolize_keys(hash)
|
|
135
|
+
hash.each_with_object({}) do |(key, value), output|
|
|
136
|
+
output[key.to_sym] = value
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require "erb"
|
|
2
|
+
|
|
3
|
+
module Pocketbook
|
|
4
|
+
class BookTemplate
|
|
5
|
+
Context = Struct.new(
|
|
6
|
+
:body_html,
|
|
7
|
+
:toc_html,
|
|
8
|
+
:styles_css,
|
|
9
|
+
:core_css,
|
|
10
|
+
:metadata,
|
|
11
|
+
:escape_html,
|
|
12
|
+
keyword_init: true
|
|
13
|
+
) do
|
|
14
|
+
def h(value)
|
|
15
|
+
escape_html.call(value)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def get_binding
|
|
19
|
+
binding
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(template_source:)
|
|
24
|
+
@template = ERB.new(template_source, trim_mode: "-")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def render(body_html:, toc_html:, styles_css:, core_css:, metadata:, escape_html:)
|
|
28
|
+
context = Context.new(
|
|
29
|
+
body_html: body_html,
|
|
30
|
+
toc_html: toc_html,
|
|
31
|
+
styles_css: styles_css,
|
|
32
|
+
core_css: core_css,
|
|
33
|
+
metadata: metadata,
|
|
34
|
+
escape_html: escape_html
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@template.result(context.get_binding)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
require "optparse"
|
|
2
|
+
require_relative "../render_request"
|
|
3
|
+
|
|
4
|
+
module Pocketbook
|
|
5
|
+
module CLI
|
|
6
|
+
class OptionsParser
|
|
7
|
+
DEFAULT_DEBOUNCE_MS = 350
|
|
8
|
+
DEFAULT_THEME_NAME = "classic"
|
|
9
|
+
DEFAULT_THEME_PATH = Pocketbook.bundled_theme_path(DEFAULT_THEME_NAME)
|
|
10
|
+
DEFAULT_THEME_DIRECTORY = Pocketbook.user_themes_path
|
|
11
|
+
|
|
12
|
+
class ParsedOptions
|
|
13
|
+
attr_reader :inputs, :theme_path, :style, :template_override, :output_path, :metadata, :debounce_ms, :diagnostics, :open_output
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@inputs = []
|
|
17
|
+
@theme_path = DEFAULT_THEME_PATH
|
|
18
|
+
@style = nil
|
|
19
|
+
@template_override = nil
|
|
20
|
+
@output_path = "output/book.pdf"
|
|
21
|
+
@metadata = {}
|
|
22
|
+
@debounce_ms = DEFAULT_DEBOUNCE_MS
|
|
23
|
+
@diagnostics = false
|
|
24
|
+
@open_output = false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def add_inputs(path_value)
|
|
28
|
+
path_value.to_s.split(",").each do |value|
|
|
29
|
+
normalized = value.strip
|
|
30
|
+
next if normalized.empty?
|
|
31
|
+
|
|
32
|
+
@inputs << normalized
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def theme_path=(value)
|
|
37
|
+
@theme_path = value
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def style=(value)
|
|
41
|
+
@style = value
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def template_override=(value)
|
|
45
|
+
@template_override = value
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def output_path=(value)
|
|
49
|
+
@output_path = value
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def metadata_value(key, value)
|
|
53
|
+
@metadata[key] = value
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def debounce_ms=(value)
|
|
57
|
+
raise OptionParser::InvalidArgument, "--debounce-ms must be zero or positive" if value.negative?
|
|
58
|
+
|
|
59
|
+
@debounce_ms = value
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def diagnostics!
|
|
63
|
+
@diagnostics = true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def open_output!
|
|
67
|
+
@open_output = true
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def validate!
|
|
71
|
+
raise OptionParser::MissingArgument, "at least one markdown input file is required" if @inputs.empty?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def to_request
|
|
75
|
+
Pocketbook::RenderRequest.new(
|
|
76
|
+
inputs: @inputs,
|
|
77
|
+
theme_path: @theme_path,
|
|
78
|
+
style: @style,
|
|
79
|
+
template_override: @template_override,
|
|
80
|
+
output_path: @output_path,
|
|
81
|
+
metadata: @metadata,
|
|
82
|
+
debounce_ms: @debounce_ms,
|
|
83
|
+
diagnostics: @diagnostics,
|
|
84
|
+
open_output: @open_output
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
ThemeNewRequest = Struct.new(:name, :target_root, :with_template, :force, keyword_init: true)
|
|
90
|
+
ThemeResolveRequest = Struct.new(:theme_path, :style, keyword_init: true)
|
|
91
|
+
ThemeGetRequest = Struct.new(:url, :name, :into, :force, :dry_run, :ref, keyword_init: true)
|
|
92
|
+
|
|
93
|
+
def initialize(stdout: $stdout)
|
|
94
|
+
@stdout = stdout
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def parse(argv)
|
|
98
|
+
args = argv.dup
|
|
99
|
+
command = resolve_command(args)
|
|
100
|
+
|
|
101
|
+
case command
|
|
102
|
+
when :build
|
|
103
|
+
request, parse_status = parse_render_options(args, watch: false)
|
|
104
|
+
[:build, request, parse_status]
|
|
105
|
+
when :watch
|
|
106
|
+
request, parse_status = parse_render_options(args, watch: true)
|
|
107
|
+
[:watch, request, parse_status]
|
|
108
|
+
when :theme
|
|
109
|
+
parse_theme_options(args)
|
|
110
|
+
else
|
|
111
|
+
raise OptionParser::InvalidArgument, "Unknown command '#{command}'"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def resolve_command(args)
|
|
118
|
+
first = args.first
|
|
119
|
+
return :build if first.nil? || first.start_with?("-")
|
|
120
|
+
|
|
121
|
+
case first
|
|
122
|
+
when "build"
|
|
123
|
+
args.shift
|
|
124
|
+
:build
|
|
125
|
+
when "watch"
|
|
126
|
+
args.shift
|
|
127
|
+
:watch
|
|
128
|
+
when "theme"
|
|
129
|
+
args.shift
|
|
130
|
+
:theme
|
|
131
|
+
else
|
|
132
|
+
:build
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def parse_render_options(args, watch: false)
|
|
137
|
+
options = ParsedOptions.new
|
|
138
|
+
|
|
139
|
+
parser = OptionParser.new do |opts|
|
|
140
|
+
opts.banner = if watch
|
|
141
|
+
"Usage: pocketbook watch FILE.md [MORE.md ...] [options]"
|
|
142
|
+
else
|
|
143
|
+
"Usage: pocketbook build FILE.md [MORE.md ...] [options]"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
unless watch
|
|
147
|
+
opts.separator ""
|
|
148
|
+
opts.separator "Commands:"
|
|
149
|
+
opts.separator " build Build a PDF (default command)"
|
|
150
|
+
opts.separator " watch Rebuild PDF on file changes"
|
|
151
|
+
opts.separator " theme new Scaffold a new theme"
|
|
152
|
+
opts.separator " theme validate Validate a theme reference"
|
|
153
|
+
opts.separator " theme inspect Show resolved theme assets"
|
|
154
|
+
opts.separator " theme get Download theme from GitHub manifest URL"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
opts.on("-t", "--theme NAME_OR_PATH", "Theme name, directory, or theme.yml (default: bundled #{DEFAULT_THEME_NAME} theme)") { |path| options.theme_path = path }
|
|
158
|
+
opts.on("-s", "--style NAME", "Style variant inside the selected theme") { |value| options.style = value }
|
|
159
|
+
opts.on("--template PATH", "Template ERB override file") { |path| options.template_override = path }
|
|
160
|
+
opts.on("-o", "--output PATH", "Output PDF path (default: output/book.pdf)") { |path| options.output_path = path }
|
|
161
|
+
opts.on("--diagnostics", "Print resolved configuration diagnostics after build") { options.diagnostics! }
|
|
162
|
+
opts.on("--open", "Open the generated PDF with the OS default viewer") { options.open_output! }
|
|
163
|
+
|
|
164
|
+
opts.on("--title TEXT", "Book title") { |value| options.metadata_value(:title, value) }
|
|
165
|
+
opts.on("--subtitle TEXT", "Book subtitle") { |value| options.metadata_value(:subtitle, value) }
|
|
166
|
+
opts.on("--author TEXT", "Book author") { |value| options.metadata_value(:author, value) }
|
|
167
|
+
opts.on("--publisher TEXT", "Publisher label") { |value| options.metadata_value(:publisher, value) }
|
|
168
|
+
opts.on("--backcover TEXT", "Backcover copy") { |value| options.metadata_value(:backcover_text, value) }
|
|
169
|
+
opts.on("--size TEXT", "Book size (CSS format, ex: 6in 9in or 6inx9in)") { |value| options.metadata_value(:size, value) }
|
|
170
|
+
|
|
171
|
+
if watch
|
|
172
|
+
opts.on("--debounce-ms INTEGER", Integer, "Debounce window in milliseconds before rebuild (default: #{DEFAULT_DEBOUNCE_MS})") do |value|
|
|
173
|
+
options.debounce_ms = value
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
opts.on("-v", "--version", "Print version") do
|
|
178
|
+
@stdout.puts("Pocketbook #{Pocketbook::VERSION}")
|
|
179
|
+
return [nil, :exit]
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
opts.on("-h", "--help", "Print this help") do
|
|
183
|
+
@stdout.puts(opts)
|
|
184
|
+
return [nil, :exit]
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
parser.parse!(args)
|
|
189
|
+
args.each { |path| options.add_inputs(path) }
|
|
190
|
+
|
|
191
|
+
options.validate!
|
|
192
|
+
[options.to_request, :ok]
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def parse_theme_options(args)
|
|
196
|
+
command = args.shift
|
|
197
|
+
case command
|
|
198
|
+
when "new"
|
|
199
|
+
parse_theme_new(args)
|
|
200
|
+
when "validate"
|
|
201
|
+
parse_theme_validate(args)
|
|
202
|
+
when "inspect"
|
|
203
|
+
parse_theme_inspect(args)
|
|
204
|
+
when "get"
|
|
205
|
+
parse_theme_get(args)
|
|
206
|
+
when nil, "-h", "--help", "help"
|
|
207
|
+
print_theme_help
|
|
208
|
+
[nil, nil, :exit]
|
|
209
|
+
else
|
|
210
|
+
raise OptionParser::InvalidArgument, "Unknown theme command '#{command}'"
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def parse_theme_new(args)
|
|
215
|
+
options = {
|
|
216
|
+
target_root: DEFAULT_THEME_DIRECTORY,
|
|
217
|
+
with_template: false,
|
|
218
|
+
force: false
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
parser = OptionParser.new do |opts|
|
|
222
|
+
opts.banner = "Usage: pocketbook theme new NAME [options]"
|
|
223
|
+
opts.separator ""
|
|
224
|
+
opts.separator "Options:"
|
|
225
|
+
opts.on("--path DIR", "Theme directory root (default: #{DEFAULT_THEME_DIRECTORY})") { |value| options[:target_root] = value }
|
|
226
|
+
opts.on("--with-template", "Create template.html.erb scaffold") { options[:with_template] = true }
|
|
227
|
+
opts.on("--force", "Overwrite scaffold files when they already exist") { options[:force] = true }
|
|
228
|
+
opts.on("-h", "--help", "Print this help") do
|
|
229
|
+
@stdout.puts(opts)
|
|
230
|
+
return [nil, nil, :exit]
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
parser.parse!(args)
|
|
235
|
+
|
|
236
|
+
name = args.shift
|
|
237
|
+
raise OptionParser::MissingArgument, "theme name is required" if blank?(name)
|
|
238
|
+
raise OptionParser::InvalidArgument, "Unexpected arguments: #{args.join(' ')}" unless args.empty?
|
|
239
|
+
|
|
240
|
+
request = ThemeNewRequest.new(
|
|
241
|
+
name: name.to_s.strip,
|
|
242
|
+
target_root: options[:target_root].to_s.strip,
|
|
243
|
+
with_template: options[:with_template],
|
|
244
|
+
force: options[:force]
|
|
245
|
+
)
|
|
246
|
+
[:theme_new, request, :ok]
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def parse_theme_validate(args)
|
|
250
|
+
parse_theme_resolve(args, :theme_validate, "Usage: pocketbook theme validate [THEME_NAME_OR_PATH] [--style NAME]")
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def parse_theme_inspect(args)
|
|
254
|
+
parse_theme_resolve(args, :theme_inspect, "Usage: pocketbook theme inspect [THEME_NAME_OR_PATH] [--style NAME]")
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def parse_theme_get(args)
|
|
258
|
+
options = {
|
|
259
|
+
name: nil,
|
|
260
|
+
into: DEFAULT_THEME_DIRECTORY,
|
|
261
|
+
force: false,
|
|
262
|
+
dry_run: false,
|
|
263
|
+
ref: nil
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
parser = OptionParser.new do |opts|
|
|
267
|
+
opts.banner = "Usage: pocketbook theme get GITHUB_THEME_YML_URL [options]"
|
|
268
|
+
opts.on("--name NAME", "Destination theme name override") { |value| options[:name] = value }
|
|
269
|
+
opts.on("--into DIR", "Destination theme directory root (default: #{DEFAULT_THEME_DIRECTORY})") { |value| options[:into] = value }
|
|
270
|
+
opts.on("--ref REF", "Git reference override (branch/tag/sha)") { |value| options[:ref] = value }
|
|
271
|
+
opts.on("--force", "Overwrite existing destination theme directory") { options[:force] = true }
|
|
272
|
+
opts.on("--dry-run", "Print planned downloads without writing files") { options[:dry_run] = true }
|
|
273
|
+
opts.on("-h", "--help", "Print this help") do
|
|
274
|
+
@stdout.puts(opts)
|
|
275
|
+
return [nil, nil, :exit]
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
parser.parse!(args)
|
|
280
|
+
url = args.shift
|
|
281
|
+
raise OptionParser::MissingArgument, "theme manifest URL is required" if blank?(url)
|
|
282
|
+
raise OptionParser::InvalidArgument, "Unexpected arguments: #{args.join(' ')}" unless args.empty?
|
|
283
|
+
|
|
284
|
+
request = ThemeGetRequest.new(
|
|
285
|
+
url: url.to_s.strip,
|
|
286
|
+
name: normalize_optional_string(options[:name]),
|
|
287
|
+
into: options[:into].to_s.strip,
|
|
288
|
+
force: options[:force],
|
|
289
|
+
dry_run: options[:dry_run],
|
|
290
|
+
ref: normalize_optional_string(options[:ref])
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
[:theme_get, request, :ok]
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def parse_theme_resolve(args, command, usage)
|
|
297
|
+
options = {
|
|
298
|
+
style: nil
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
parser = OptionParser.new do |opts|
|
|
302
|
+
opts.banner = usage
|
|
303
|
+
opts.on("-s", "--style NAME", "Style variant to resolve") { |value| options[:style] = value }
|
|
304
|
+
opts.on("-h", "--help", "Print this help") do
|
|
305
|
+
@stdout.puts(opts)
|
|
306
|
+
return [nil, nil, :exit]
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
parser.parse!(args)
|
|
311
|
+
theme_path = args.shift || DEFAULT_THEME_NAME
|
|
312
|
+
raise OptionParser::InvalidArgument, "Unexpected arguments: #{args.join(' ')}" unless args.empty?
|
|
313
|
+
|
|
314
|
+
request = ThemeResolveRequest.new(theme_path: theme_path, style: options[:style])
|
|
315
|
+
[command, request, :ok]
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def print_theme_help
|
|
319
|
+
@stdout.puts <<~TEXT
|
|
320
|
+
Usage: pocketbook theme <command> [options]
|
|
321
|
+
|
|
322
|
+
Commands:
|
|
323
|
+
new NAME Scaffold a new theme directory
|
|
324
|
+
validate [THEME] Validate that a theme resolves correctly
|
|
325
|
+
inspect [THEME] Print resolved theme details
|
|
326
|
+
get URL Download a theme from a GitHub theme.yml URL
|
|
327
|
+
TEXT
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def normalize_optional_string(value)
|
|
331
|
+
return nil if value.nil?
|
|
332
|
+
|
|
333
|
+
normalized = value.to_s.strip
|
|
334
|
+
return nil if normalized.empty?
|
|
335
|
+
|
|
336
|
+
normalized
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def blank?(value)
|
|
340
|
+
value.nil? || value.to_s.strip.empty?
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|