zard-doc 0.0.1 → 0.0.2
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/README.md +17 -10
- data/lib/zard/doc/build.rb +105 -0
- data/lib/zard/doc/build_configuration.rb +127 -0
- data/lib/zard/doc/build_export.rb +100 -0
- data/lib/zard/doc/build_publisher.rb +206 -0
- data/lib/zard/doc/build_renderer.rb +306 -0
- data/lib/zard/doc/cli.rb +100 -2
- data/lib/zard/doc/reference_markup.rb +271 -0
- data/lib/zard/doc/renderer.rb +160 -21
- data/lib/zard/doc/version.rb +1 -1
- data/lib/zard/doc.rb +2 -1
- metadata +10 -4
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module Zard
|
|
6
|
+
module Doc
|
|
7
|
+
class BuildRenderer
|
|
8
|
+
attr_reader :diagnostics, :links
|
|
9
|
+
|
|
10
|
+
def initialize(scope_documents:, scope_files:, scopes:, layout:, include_undocumented: false)
|
|
11
|
+
@diagnostics = []
|
|
12
|
+
@links = {}
|
|
13
|
+
@scopes = scopes
|
|
14
|
+
@renderers = scope_documents.keys.sort.to_h do |name|
|
|
15
|
+
[name, ScopeRenderer.new(name, scope_documents.fetch(name), scope_files.fetch(name), scopes.fetch(name), layout, self, include_undocumented: include_undocumented)]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def render
|
|
20
|
+
pages = {}
|
|
21
|
+
add_page(pages, "index.md", "# API documentation\n\n" + @renderers.map { |name, renderer| "- [#{renderer.label(name)}](#{renderer.url(renderer.index_path)})" }.join("\n") + "\n")
|
|
22
|
+
@renderers.each_value do |renderer|
|
|
23
|
+
renderer.pages.each { |path, text| add_page(pages, path, text) }
|
|
24
|
+
end
|
|
25
|
+
pages
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def dependencies(name)
|
|
29
|
+
@scopes.fetch(name).fetch(:references, []).uniq.filter_map { |dependency| @renderers[dependency] }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def export_declarations
|
|
33
|
+
@renderers.transform_values(&:export_declarations)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def record_link(from, path, anchor)
|
|
37
|
+
targets = (@links[from] ||= [])
|
|
38
|
+
target = {"path" => path, "anchor" => anchor}
|
|
39
|
+
targets << target unless targets.include?(target)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def warn_ambiguity(reference, origin, candidates)
|
|
43
|
+
locations = candidates.map { |renderer, declaration| "#{renderer.name}:#{declaration.span.path}:#{declaration.span.start_line}" }
|
|
44
|
+
message = "warning: ambiguous reference #{reference.inspect} at #{origin.span.path}:#{origin.span.start_line}; candidates: #{locations.join(", ")}"
|
|
45
|
+
@diagnostics << message unless @diagnostics.include?(message)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def warn_relationship(kind, expression, origin)
|
|
49
|
+
message = "warning: unresolved #{kind} documentation relationship #{expression.inspect} at #{origin.span.path}:#{origin.span.start_line}"
|
|
50
|
+
@diagnostics << message unless @diagnostics.include?(message)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def add_page(pages, path, text)
|
|
56
|
+
raise BuildError, "output collision: #{path}" if pages.key?(path) || pages.keys.any? { |other| other.start_with?("#{path}/") || path.start_with?("#{other}/") }
|
|
57
|
+
|
|
58
|
+
pages[path] = text
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class ScopeRenderer < Renderer
|
|
62
|
+
attr_reader :name, :index_path
|
|
63
|
+
|
|
64
|
+
def initialize(name, documents, files, config, layout, build, include_undocumented: false)
|
|
65
|
+
@name = name
|
|
66
|
+
@build = build
|
|
67
|
+
@layout = layout
|
|
68
|
+
@destination = config.fetch(:destination)
|
|
69
|
+
@include_undocumented = include_undocumented
|
|
70
|
+
@index_path = join_path("index.md")
|
|
71
|
+
@declarations = documents.sort_by(&:path).flat_map(&:declarations)
|
|
72
|
+
@container_names = @declarations.select { |declaration| %i[class module constant].include?(declaration.kind) }.map { |declaration| qualified_name(declaration) }
|
|
73
|
+
@targets = Hash.new { |hash, key| hash[key] = [] }
|
|
74
|
+
@declarations.each { |declaration| reference_names(declaration).each { |reference| @targets[reference] << declaration } }
|
|
75
|
+
@paths = {}
|
|
76
|
+
@anchors = {}
|
|
77
|
+
@text = {}
|
|
78
|
+
@descriptions = {}
|
|
79
|
+
@page_owners = {}
|
|
80
|
+
identities = @declarations.group_by { |declaration| anchor_identity(declaration) }
|
|
81
|
+
ordinals = Hash.new(0)
|
|
82
|
+
@declarations.each do |declaration|
|
|
83
|
+
identity_key = anchor_identity(declaration)
|
|
84
|
+
source = files.fetch(File.expand_path(declaration.span.path))
|
|
85
|
+
ordinals[[identity_key, source]] += 1
|
|
86
|
+
next unless renderable?(declaration)
|
|
87
|
+
|
|
88
|
+
owner = namespace_page(declaration)
|
|
89
|
+
relative = (layout == "file") ? files.fetch(File.expand_path(declaration.span.path)) : "#{owner.gsub("::", "/")}.md"
|
|
90
|
+
path = join_path(relative)
|
|
91
|
+
identity = (layout == "file") ? File.expand_path(declaration.span.path) : owner
|
|
92
|
+
raise BuildError, "output collision: #{path}" if path == @index_path || (@page_owners.key?(path) && @page_owners[path] != identity)
|
|
93
|
+
|
|
94
|
+
@page_owners[path] = identity
|
|
95
|
+
@paths[declaration] = path
|
|
96
|
+
key = identity_key
|
|
97
|
+
key += "\0#{source}\0#{ordinals[[identity_key, source]]}" if identities.fetch(identity_key).length > 1
|
|
98
|
+
@anchors[declaration] = "zard-#{key.bytes.map { |byte| "%02x" % byte }.join}"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def pages
|
|
103
|
+
@paths.each_key do |declaration|
|
|
104
|
+
text = declaration.documentation.select { |tag| tag.name == :text }.map(&:description).join("\n")
|
|
105
|
+
@text[declaration] = render_prose(text, declaration)
|
|
106
|
+
@descriptions[declaration] = declaration.documentation.to_h do |tag|
|
|
107
|
+
description = case tag.name
|
|
108
|
+
when :example, :raw, :text then tag.description
|
|
109
|
+
when :see then render_reference(tag.description, declaration)
|
|
110
|
+
else render_prose(tag.description, declaration)
|
|
111
|
+
end
|
|
112
|
+
[tag, description]
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
result = @paths.keys.group_by { |declaration| @paths.fetch(declaration) }.sort.to_h do |path, declarations|
|
|
116
|
+
sections = declarations.map do |declaration|
|
|
117
|
+
"#{render_declaration(declaration)}\n\nSource: #{label("#{declaration.span.path}:#{declaration.span.start_line}")}."
|
|
118
|
+
end
|
|
119
|
+
[path, "#{sections.join("\n\n")}\n"]
|
|
120
|
+
end
|
|
121
|
+
navigation = result.keys.map { |path| "- [#{label(Pathname.new(path).relative_path_from(Pathname.new(File.dirname(index_path))).to_s)}](#{relative_url(index_path, path)})" }
|
|
122
|
+
result[index_path] = "# #{label(name)}\n\n#{navigation.join("\n")}\n"
|
|
123
|
+
result
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def lookup(candidates, path, containers: false)
|
|
127
|
+
path = path.delete_prefix("::")
|
|
128
|
+
candidates.each do |candidate|
|
|
129
|
+
targets = @targets[candidate].uniq
|
|
130
|
+
unless targets.empty?
|
|
131
|
+
if (@layout == "namespace" || containers) && targets.map(&:kind).uniq.one? && targets.all? { |target| %i[class module].include?(target.kind) }
|
|
132
|
+
published = targets.find { |target| renderable?(target) }
|
|
133
|
+
return [:found, [published]] if published
|
|
134
|
+
end
|
|
135
|
+
return [:found, targets]
|
|
136
|
+
end
|
|
137
|
+
if path.start_with?("#", ".") && @container_names.include?(candidate.split(/[.#]/, 2).first)
|
|
138
|
+
return [:shadowed, []]
|
|
139
|
+
end
|
|
140
|
+
first_constant = path.split(/::|[.#]/).first.to_s
|
|
141
|
+
local_constant = "#{candidate.delete_suffix(path)}#{first_constant}"
|
|
142
|
+
return [:shadowed, []] if !first_constant.empty? && @container_names.include?(local_constant)
|
|
143
|
+
end
|
|
144
|
+
[:missing, []]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def destination(declaration)
|
|
148
|
+
[@paths.fetch(declaration), @anchors.fetch(declaration)]
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def export_declarations
|
|
152
|
+
@paths.map do |declaration, path|
|
|
153
|
+
descriptions = @descriptions.fetch(declaration).to_h do |tag, description|
|
|
154
|
+
[tag, (tag.name == :text) ? render_prose(tag.description, declaration) : description]
|
|
155
|
+
end
|
|
156
|
+
{declaration: declaration, page: path, anchor: @anchors.fetch(declaration),
|
|
157
|
+
display_name: reference_names(declaration).first || display_name(declaration),
|
|
158
|
+
references: reference_names(declaration), descriptions: descriptions, relationships: relationship_entries(declaration)}
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def relationship_targets(declaration)
|
|
163
|
+
@targets.fetch(qualified_name(declaration)).select { |item| %i[class module].include?(item.kind) && published?(item) }.map do |item|
|
|
164
|
+
path, anchor = destination(item)
|
|
165
|
+
{scope: name, declaration_id: "#{name}:#{anchor}", path: path, anchor: anchor}
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def published?(declaration)
|
|
170
|
+
renderable?(declaration)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def label(value)
|
|
174
|
+
escape_label(value)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def url(path)
|
|
178
|
+
path.bytes.map { |byte| byte.chr.match?(/[A-Za-z0-9\-._~\/]/) ? byte.chr : "%%%02X" % byte }.join
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
private
|
|
182
|
+
|
|
183
|
+
def renderable?(declaration)
|
|
184
|
+
declaration.visibility == :public && (@include_undocumented || super)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def join_path(path)
|
|
188
|
+
Pathname.new(File.join(@destination, path)).cleanpath.to_s
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def anchor_identity(declaration)
|
|
192
|
+
reference = reference_names(declaration).first
|
|
193
|
+
[declaration.kind, reference || display_name(declaration), declaration.refinement].join("\0")
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def namespace_page(declaration)
|
|
197
|
+
return qualified_name(declaration) if %i[class module].include?(declaration.kind)
|
|
198
|
+
|
|
199
|
+
if declaration.kind.to_s.start_with?("singleton_") && !declaration.refinement
|
|
200
|
+
owner = reference_owner(declaration)
|
|
201
|
+
return owner.delete_prefix("::") if owner&.match?(/\A(?:::)?[[:upper:]\p{Lt}][[:word:]]*(?:::[[:upper:]\p{Lt}][[:word:]]*)*\z/)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
declaration.namespace || "_top-level"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def relative_url(from, to)
|
|
208
|
+
url(Pathname.new(to).relative_path_from(Pathname.new(File.dirname(from))).to_s)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def resolve_scoped(reference, declaration, outer: false, containers: false)
|
|
212
|
+
return if !reference || declaration.refinement
|
|
213
|
+
|
|
214
|
+
path = reference.sub(/\A((?:::)?[[:upper:]\p{Lt}][[:word:]]*(?:::[[:upper:]\p{Lt}][[:word:]]*)*)?(?:\?\.|\.#)/) { "#{$1}." }
|
|
215
|
+
candidates = if path.start_with?("::")
|
|
216
|
+
[path.delete_prefix("::")]
|
|
217
|
+
elsif path.start_with?("#", ".")
|
|
218
|
+
owner = declaration.kind.to_s.start_with?("singleton_") ? reference_owner(declaration) : declaration.namespace
|
|
219
|
+
owner = qualified_name(declaration) if %i[class module].include?(declaration.kind)
|
|
220
|
+
["#{owner&.delete_prefix("::")}#{path}"]
|
|
221
|
+
else
|
|
222
|
+
namespace = (!outer && %i[class module].include?(declaration.kind)) ? qualified_name(declaration) : declaration.namespace
|
|
223
|
+
lexical_candidates(path, namespace)
|
|
224
|
+
end
|
|
225
|
+
status, targets = lookup(candidates, path, containers: containers)
|
|
226
|
+
matches = targets.map { |target| [self, target] }
|
|
227
|
+
if status == :missing
|
|
228
|
+
matches = @build.dependencies(name).flat_map do |renderer|
|
|
229
|
+
_, dependency_targets = renderer.lookup(candidates, path, containers: containers)
|
|
230
|
+
dependency_targets.map { |target| [renderer, target] }
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
if matches.length > 1
|
|
234
|
+
@build.warn_ambiguity(reference, declaration, matches)
|
|
235
|
+
return
|
|
236
|
+
end
|
|
237
|
+
match = matches.first
|
|
238
|
+
return unless match
|
|
239
|
+
|
|
240
|
+
match if match.first.published?(match.last)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def relationship_entries(declaration)
|
|
244
|
+
return [] unless %i[class module].include?(declaration.kind) && !declaration.refinement
|
|
245
|
+
|
|
246
|
+
entries = []
|
|
247
|
+
entries << [:superclass, declaration.superclass, declaration.superclass_span] if declaration.superclass
|
|
248
|
+
entries << [:container_builder, declaration.container_builder, declaration.container_builder_span] if declaration.container_builder
|
|
249
|
+
entries.concat(declaration.mixins.map { |mixin| [mixin.kind, mixin.target, mixin.span] })
|
|
250
|
+
entries.map do |kind, expression, span|
|
|
251
|
+
constant = expression.match?(/\A(?:::)?[[:upper:]\p{Lt}][[:word:]]*(?:::[[:upper:]\p{Lt}][[:word:]]*)*\z/)
|
|
252
|
+
match = resolve_scoped(expression, declaration, outer: kind == :superclass, containers: true) if constant && kind != :container_builder
|
|
253
|
+
expected_kind = (kind == :superclass) ? :class : :module
|
|
254
|
+
match = nil if match && match.last.kind != expected_kind
|
|
255
|
+
targets = match ? match.first.relationship_targets(match.last) : []
|
|
256
|
+
targets.each { |target| @build.record_link(@paths.fetch(declaration), target.fetch(:path), target.fetch(:anchor)) }
|
|
257
|
+
status = if !targets.empty?
|
|
258
|
+
:resolved
|
|
259
|
+
elsif constant && kind != :container_builder
|
|
260
|
+
:unresolved
|
|
261
|
+
else
|
|
262
|
+
:expression
|
|
263
|
+
end
|
|
264
|
+
@build.warn_relationship(kind, expression, declaration) unless status == :resolved
|
|
265
|
+
{kind: kind, expression: expression, span: span, status: status, targets: targets}
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def link_destination(match, declaration)
|
|
270
|
+
path, anchor = match.first.destination(match.last)
|
|
271
|
+
@build.record_link(@paths.fetch(declaration), path, anchor)
|
|
272
|
+
"#{relative_url(@paths.fetch(declaration), path)}##{anchor}"
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def render_prose(description, declaration)
|
|
276
|
+
ReferenceMarkup.rewrite(description) do |scheme, reference, label|
|
|
277
|
+
yield if block_given?
|
|
278
|
+
match = resolve_scoped(reference, declaration)
|
|
279
|
+
next unless match
|
|
280
|
+
next if (scheme == "c") != %i[class module].include?(match.last.kind)
|
|
281
|
+
|
|
282
|
+
title = label || reference
|
|
283
|
+
unless label && ReferenceMarkup.code_label?(label)
|
|
284
|
+
title = title.gsub(/\\([\\\[\]])/, '\1') if label
|
|
285
|
+
title = escape_label(title)
|
|
286
|
+
end
|
|
287
|
+
"[#{title}](#{link_destination(match, declaration)})"
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def render_reference(description, declaration)
|
|
292
|
+
explicit = false
|
|
293
|
+
prose = render_prose(description, declaration) { explicit = true }
|
|
294
|
+
return prose if explicit
|
|
295
|
+
|
|
296
|
+
reference, label = description.strip.split(/\s+/, 2)
|
|
297
|
+
return description unless reference
|
|
298
|
+
return super if reference.match?(/\A(?:https?:\/\/|mailto:)/)
|
|
299
|
+
|
|
300
|
+
match = resolve_scoped(reference, declaration)
|
|
301
|
+
match ? "[#{escape_label(label || reference)}](#{link_destination(match, declaration)})" : description
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
data/lib/zard/doc/cli.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "optparse"
|
|
4
4
|
require "zard/doc"
|
|
5
|
+
require "zard/doc/build"
|
|
5
6
|
|
|
6
7
|
module Zard
|
|
7
8
|
module Doc
|
|
@@ -28,8 +29,12 @@ module Zard
|
|
|
28
29
|
lint_command
|
|
29
30
|
when "render"
|
|
30
31
|
render_command
|
|
32
|
+
when "format"
|
|
33
|
+
format_command
|
|
34
|
+
when "build"
|
|
35
|
+
build_command
|
|
31
36
|
else
|
|
32
|
-
usage_error("Expected the lint or
|
|
37
|
+
usage_error("Expected the lint, render, format, or build command.")
|
|
33
38
|
end
|
|
34
39
|
rescue OptionParser::ParseError => error
|
|
35
40
|
usage_error(error.message)
|
|
@@ -37,6 +42,56 @@ module Zard
|
|
|
37
42
|
|
|
38
43
|
private
|
|
39
44
|
|
|
45
|
+
def build_command
|
|
46
|
+
config = nil
|
|
47
|
+
layout = nil
|
|
48
|
+
format = "markdown"
|
|
49
|
+
scopes = []
|
|
50
|
+
lens_root = nil
|
|
51
|
+
rigor_command = nil
|
|
52
|
+
lens_workers = 4
|
|
53
|
+
lens_config = nil
|
|
54
|
+
OptionParser.new do |parser|
|
|
55
|
+
parser.on("--config FILE", "Read the documentation build configuration") { |value| config = value }
|
|
56
|
+
parser.on("--layout LAYOUT", %w[namespace file], "Override page organization") { |value| layout = value }
|
|
57
|
+
parser.on("--format FORMAT", %w[markdown json], "Write Markdown pages or export JSON to stdout") { |value| format = value }
|
|
58
|
+
parser.on("--scope NAME", "Build a scope and its documentation dependencies (repeatable)") { |value| scopes << value }
|
|
59
|
+
parser.on("--lens-root DIR", "Enrich JSON using experimental Rigor lens (requires zard-rigor)") { |value| lens_root = value }
|
|
60
|
+
parser.on("--rigor-command FILE", "Supported Rigor executable (default: rigor)") { |value| rigor_command = value }
|
|
61
|
+
parser.on("--lens-workers N", Integer, "Lens workers (default: 4)") { |value| lens_workers = value }
|
|
62
|
+
parser.on("--lens-config FILE", "Rigor configuration, relative to lens root") { |value| lens_config = value }
|
|
63
|
+
end.parse!(@argv)
|
|
64
|
+
return usage_error("Pass --config FILE without positional arguments.") unless config && @argv.empty?
|
|
65
|
+
|
|
66
|
+
return usage_error("--lens-root requires --format json.") if lens_root && format != "json"
|
|
67
|
+
return usage_error("--rigor-command requires --lens-root.") if rigor_command && !lens_root
|
|
68
|
+
return usage_error("--lens-config requires --lens-root.") if lens_config && !lens_root
|
|
69
|
+
return usage_error("--lens-workers must be non-negative.") if lens_workers.negative?
|
|
70
|
+
|
|
71
|
+
enricher = lens_enricher(lens_root, rigor_command || "rigor", lens_workers, lens_config) if lens_root
|
|
72
|
+
builder = Build.new(config, layout: layout, scopes: scopes, format: format, stdout: @stdout, enricher: enricher)
|
|
73
|
+
builder.run
|
|
74
|
+
rescue BuildError => error
|
|
75
|
+
@stderr.puts error.message
|
|
76
|
+
error.status
|
|
77
|
+
ensure
|
|
78
|
+
builder&.diagnostics&.each { |diagnostic| @stderr.puts diagnostic }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def lens_enricher(root, command, workers, config)
|
|
82
|
+
require "zard/rigor"
|
|
83
|
+
adapter = Zard::Rigor::Lens.new(root: root, command: command, workers: workers, config: config)
|
|
84
|
+
lambda do |data, base:|
|
|
85
|
+
adapter.enrich(data, base: base)
|
|
86
|
+
rescue Zard::Rigor::Error => error
|
|
87
|
+
raise BuildError, error.message
|
|
88
|
+
end
|
|
89
|
+
rescue LoadError => error
|
|
90
|
+
raise BuildError, "Install the optional zard-rigor gem to use --lens-root: #{error.message}"
|
|
91
|
+
rescue SystemCallError => error
|
|
92
|
+
raise BuildError, "Invalid lens project root: #{error.message}"
|
|
93
|
+
end
|
|
94
|
+
|
|
40
95
|
def lint_command
|
|
41
96
|
lint_option_parser.parse!(@argv)
|
|
42
97
|
return usage_error("Pass at least one Ruby source file.") if @argv.empty?
|
|
@@ -72,6 +127,46 @@ module Zard
|
|
|
72
127
|
end.uniq.sort
|
|
73
128
|
end
|
|
74
129
|
|
|
130
|
+
def format_command
|
|
131
|
+
check = false
|
|
132
|
+
write = false
|
|
133
|
+
OptionParser.new do |parser|
|
|
134
|
+
parser.on("--check", "Check whether formatting would change source") { check = true }
|
|
135
|
+
parser.on("--write", "Write formatted source to files") { write = true }
|
|
136
|
+
end.parse!(@argv)
|
|
137
|
+
return usage_error("Choose either --check or --write.") if check && write
|
|
138
|
+
return usage_error("Pass at least one Ruby source file.") if @argv.empty?
|
|
139
|
+
|
|
140
|
+
paths = source_paths(@argv)
|
|
141
|
+
return usage_error("No Ruby source files were found.") if paths.empty?
|
|
142
|
+
return usage_error("Standard input cannot be used with --write.") if write && paths.include?("-")
|
|
143
|
+
return usage_error("Pass one input for stdout, or use --check or --write.") if !check && !write && paths.length != 1
|
|
144
|
+
|
|
145
|
+
results = paths.map do |path|
|
|
146
|
+
source = read_source(path)
|
|
147
|
+
return 2 unless source
|
|
148
|
+
|
|
149
|
+
[path, Zard.format(source, path: path)]
|
|
150
|
+
end
|
|
151
|
+
results.each do |_, result|
|
|
152
|
+
result.diagnostics.each { |diagnostic| @stderr.puts format_diagnostic(diagnostic) }
|
|
153
|
+
end
|
|
154
|
+
return 1 if results.any? { |_, result| result.diagnostics.any? { |diagnostic| diagnostic.severity == :error } }
|
|
155
|
+
return (results.any? { |_, result| result.changed? }) ? 1 : 0 if check
|
|
156
|
+
|
|
157
|
+
if write
|
|
158
|
+
results.each do |path, result|
|
|
159
|
+
File.binwrite(path, result.source) if result.changed?
|
|
160
|
+
rescue SystemCallError => error
|
|
161
|
+
@stderr.puts "#{path}: #{error.message}"
|
|
162
|
+
return 2
|
|
163
|
+
end
|
|
164
|
+
else
|
|
165
|
+
@stdout.print results.first[1].source
|
|
166
|
+
end
|
|
167
|
+
0
|
|
168
|
+
end
|
|
169
|
+
|
|
75
170
|
def lint(paths)
|
|
76
171
|
failed = false
|
|
77
172
|
input_error = false
|
|
@@ -106,7 +201,7 @@ module Zard
|
|
|
106
201
|
diagnostics.each { |diagnostic| @stderr.puts format_diagnostic(diagnostic) }
|
|
107
202
|
return 1 if diagnostics.any? { |diagnostic| diagnostic.severity == :error }
|
|
108
203
|
|
|
109
|
-
markdown =
|
|
204
|
+
markdown = Renderer.render_documents(documents)
|
|
110
205
|
@stdout.print markdown
|
|
111
206
|
0
|
|
112
207
|
end
|
|
@@ -145,6 +240,9 @@ module Zard
|
|
|
145
240
|
Usage:
|
|
146
241
|
zard-doc lint [--fail-on error|warning] PATH...
|
|
147
242
|
zard-doc render PATH...
|
|
243
|
+
zard-doc format [--check | --write] PATH...
|
|
244
|
+
zard-doc build --config FILE [--scope NAME ...] [--layout namespace|file] [--format markdown|json]
|
|
245
|
+
[--lens-root DIR [--rigor-command FILE] [--lens-workers N] [--lens-config FILE]]
|
|
148
246
|
USAGE
|
|
149
247
|
end
|
|
150
248
|
end
|