zard-doc 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d4a7ff58279b52ea8ca3e15ffdf25f54214d20c5ececaeb641282e85050d3d5a
4
+ data.tar.gz: 9ede39071e88157e47387cf29ae4b4c04a8af656c396e26c783824b671c52ce4
5
+ SHA512:
6
+ metadata.gz: ed9d9d91a23b003910e34db882cfec727154822a60bf66c92e2f996bd9c2c94d0796c15e9603be569846b43985585eae8f74a24a722ca0fe3e60b7d985217ebe
7
+ data.tar.gz: 9d9d1e2a24d28de2d6080bc397734ab2f1de2d4623dc9cc4c3e70335e3801c0e8ee6d3aeff7b48e36a1234fd3d950407eb660d7452e9ab0c0f31c2a1ec657d45
data/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ Mozilla Public License Version 2.0
2
+ ==================================
3
+
4
+ This package is distributed under the Mozilla Public License 2.0.
5
+ The complete license text is available at https://mozilla.org/MPL/2.0/.
data/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # zard-doc
2
+
3
+ `zard-doc` renders API documentation from the versioned model produced by `zard`.
4
+
5
+ See the [ZARD usage guide](https://github.com/rigortype/zard/blob/master/docs/usage.md) for installation, canonical tag syntax, model access, limitations, and exit codes. The `0.0.1` candidate is not yet published on RubyGems. Until then, install both gems from the repository:
6
+
7
+ ```ruby
8
+ gem "zard", github: "rigortype/zard"
9
+ gem "zard-doc", github: "rigortype/zard"
10
+ ```
11
+
12
+ After publication, use the released pair:
13
+
14
+ ```ruby
15
+ gem "zard", "~> 0.0.1"
16
+ gem "zard-doc", "~> 0.0.1"
17
+ ```
18
+
19
+ ```ruby
20
+ require "zard"
21
+ require "zard/doc"
22
+
23
+ document = Zard.parse(source, path: "lib/example.rb")
24
+ markdown = Zard::Doc.render(document)
25
+ ```
26
+
27
+ Descriptions may continue across plain comment lines until the next annotation or contract.
28
+
29
+ Run the linter against Ruby source files with:
30
+
31
+ ```console
32
+ zard-doc lint lib
33
+ zard-doc lint --fail-on warning lib/example.rb
34
+ ```
35
+
36
+ Render Markdown to standard output with:
37
+
38
+ ```console
39
+ zard-doc render lib
40
+ ```
41
+
42
+ Directories are searched recursively for Ruby source files. Files are processed in stable sorted order and duplicate paths are ignored.
43
+ Unreadable inputs are reported together; rendering never emits partial Markdown when any input cannot be read.
44
+ Use `-` as a path to read Ruby source from standard input.
45
+
46
+ `lint` exits with 0 when no diagnostic meets the configured failure threshold, 1 when a diagnostic meets it, and 2 for invalid usage or unreadable input. The default threshold is `error`; `--fail-on warning` also fails on warnings. `render` exits with 1 for error diagnostics, 2 for invalid usage or unreadable input, and 0 otherwise. Diagnostics go to standard error for `render`; lint diagnostics go to standard output.
data/exe/zard-doc ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "zard/doc/cli"
5
+
6
+ exit Zard::Doc::CLI.run(ARGV)
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require "zard/doc"
5
+
6
+ module Zard
7
+ module Doc
8
+ class CLI
9
+ SEVERITY_RANK = {error: 2, warning: 1, info: 0}.freeze
10
+
11
+ def self.run(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr)
12
+ new(argv, stdin, stdout, stderr).run
13
+ end
14
+
15
+ def initialize(argv, stdin, stdout, stderr)
16
+ @argv = argv.dup
17
+ @stdin = stdin
18
+ @stdout = stdout
19
+ @stderr = stderr
20
+ @fail_on = :error
21
+ end
22
+
23
+ def run
24
+ return help if @argv.first == "--help" || @argv.first == "-h"
25
+
26
+ case @argv.shift
27
+ when "lint"
28
+ lint_command
29
+ when "render"
30
+ render_command
31
+ else
32
+ usage_error("Expected the lint or render command.")
33
+ end
34
+ rescue OptionParser::ParseError => error
35
+ usage_error(error.message)
36
+ end
37
+
38
+ private
39
+
40
+ def lint_command
41
+ lint_option_parser.parse!(@argv)
42
+ return usage_error("Pass at least one Ruby source file.") if @argv.empty?
43
+
44
+ paths = source_paths(@argv)
45
+ return usage_error("No Ruby source files were found.") if paths.empty?
46
+
47
+ lint(paths)
48
+ end
49
+
50
+ def lint_option_parser
51
+ OptionParser.new do |parser|
52
+ parser.banner = usage
53
+ parser.on("--fail-on LEVEL", %w[error warning], "Minimum severity that exits unsuccessfully") do |level|
54
+ @fail_on = level.to_sym
55
+ end
56
+ end
57
+ end
58
+
59
+ def render_command
60
+ OptionParser.new.parse!(@argv)
61
+ return usage_error("Pass at least one Ruby source file.") if @argv.empty?
62
+
63
+ paths = source_paths(@argv)
64
+ return usage_error("No Ruby source files were found.") if paths.empty?
65
+
66
+ render(paths)
67
+ end
68
+
69
+ def source_paths(inputs)
70
+ inputs.flat_map do |input|
71
+ File.directory?(input) ? Dir.glob(File.join(input, "**", "*.rb")).sort : input
72
+ end.uniq.sort
73
+ end
74
+
75
+ def lint(paths)
76
+ failed = false
77
+ input_error = false
78
+
79
+ paths.each do |path|
80
+ source = read_source(path)
81
+ unless source
82
+ input_error = true
83
+ next
84
+ end
85
+
86
+ document = Zard.parse(source, path: path)
87
+ document.diagnostics.each do |diagnostic|
88
+ @stdout.puts format_diagnostic(diagnostic)
89
+ failed ||= failing?(diagnostic)
90
+ end
91
+ end
92
+
93
+ return 2 if input_error
94
+
95
+ failed ? 1 : 0
96
+ end
97
+
98
+ def render(paths)
99
+ documents = paths.filter_map do |path|
100
+ source = read_source(path)
101
+ Zard.parse(source, path: path) if source
102
+ end
103
+ return 2 if documents.length != paths.length
104
+
105
+ diagnostics = documents.flat_map(&:diagnostics)
106
+ diagnostics.each { |diagnostic| @stderr.puts format_diagnostic(diagnostic) }
107
+ return 1 if diagnostics.any? { |diagnostic| diagnostic.severity == :error }
108
+
109
+ markdown = documents.map { |document| Zard::Doc.render(document) }.reject(&:empty?).join("\n")
110
+ @stdout.print markdown
111
+ 0
112
+ end
113
+
114
+ def read_source(path)
115
+ return @stdin.read if path == "-"
116
+
117
+ File.read(path)
118
+ rescue SystemCallError => error
119
+ @stderr.puts "#{path}: #{error.message}"
120
+ nil
121
+ end
122
+
123
+ def failing?(diagnostic)
124
+ SEVERITY_RANK.fetch(diagnostic.severity) >= SEVERITY_RANK.fetch(@fail_on)
125
+ end
126
+
127
+ def format_diagnostic(diagnostic)
128
+ span = diagnostic.span
129
+ "#{span.path}:#{span.start_line}:#{span.start_column + 1}: #{diagnostic.severity} #{diagnostic.code} #{diagnostic.message}"
130
+ end
131
+
132
+ def help
133
+ @stdout.puts usage
134
+ 0
135
+ end
136
+
137
+ def usage_error(message)
138
+ @stderr.puts message
139
+ @stderr.puts usage
140
+ 2
141
+ end
142
+
143
+ def usage
144
+ <<~USAGE.chomp
145
+ Usage:
146
+ zard-doc lint [--fail-on error|warning] PATH...
147
+ zard-doc render PATH...
148
+ USAGE
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zard
4
+ module Doc
5
+ class Renderer
6
+ def self.render(document)
7
+ new(document).render
8
+ end
9
+
10
+ def initialize(document)
11
+ @document = document
12
+ end
13
+
14
+ def render
15
+ sections = @document.declarations.filter_map { |declaration| render_declaration(declaration) }
16
+ return "" if sections.empty?
17
+
18
+ "#{sections.join("\n\n")}\n"
19
+ end
20
+
21
+ private
22
+
23
+ def render_declaration(declaration)
24
+ return unless declaration.visibility == :public
25
+
26
+ documentation = declaration.documentation.reject { |tag| tag.name == :raw }
27
+ return if documentation.empty?
28
+
29
+ parts = [heading(declaration)]
30
+ parts << "Superclass: `#{declaration.superclass}`." if declaration.superclass
31
+ parts << render_container_builder(declaration) if declaration.container_builder
32
+ append_mixins(parts, declaration)
33
+ parts << "Alias of `#{display_alias_target(declaration)}`." if declaration.alias_target
34
+ text = documentation.select { |tag| tag.name == :text }.map(&:description)
35
+ parts << text.join("\n") unless text.empty?
36
+
37
+ deprecated = documentation.select { |tag| tag.name == :deprecated }
38
+ parts << "### Deprecated\n\n#{deprecated.map(&:description).join("\n\n")}" unless deprecated.empty?
39
+
40
+ notes = documentation.select { |tag| tag.name == :note }
41
+ parts << "### Notes\n\n#{notes.map(&:description).join("\n\n")}" unless notes.empty?
42
+
43
+ examples = documentation.select { |tag| tag.name == :example }
44
+ parts << "### Examples\n\n#{examples.map(&:description).join("\n\n")}" unless examples.empty?
45
+
46
+ parameters = documentation.select { |tag| tag.name == :param }
47
+ unless parameters.empty?
48
+ items = parameters.map { |tag| list_item("`#{tag.subject}` — ", tag.description) }
49
+ parts << "### Parameters\n\n#{items.join("\n")}"
50
+ end
51
+
52
+ returns = documentation.select { |tag| tag.name == :return }
53
+ parts << "### Returns\n\n#{returns.map(&:description).join("\n\n")}" unless returns.empty?
54
+
55
+ documentation.select { |tag| tag.name == :option }.group_by(&:owner).each do |owner, options|
56
+ items = options.map { |tag| list_item("`#{tag.subject}` — ", tag.description) }
57
+ parts << "### Options for `#{owner}`\n\n#{items.join("\n")}"
58
+ end
59
+
60
+ raises = documentation.select { |tag| tag.name == :raise }
61
+ unless raises.empty?
62
+ items = raises.map { |tag| list_item("`#{tag.subject}` — ", tag.description) }
63
+ parts << "### Raises\n\n#{items.join("\n")}"
64
+ end
65
+
66
+ yield_parameters = documentation.select { |tag| tag.name == :yieldparam }
67
+ unless yield_parameters.empty?
68
+ items = yield_parameters.map { |tag| list_item("`#{tag.subject}` — ", tag.description) }
69
+ parts << "### Yield parameters\n\n#{items.join("\n")}"
70
+ end
71
+
72
+ yield_returns = documentation.select { |tag| tag.name == :yieldreturn }
73
+ parts << "### Yields\n\n#{yield_returns.map(&:description).join("\n\n")}" unless yield_returns.empty?
74
+
75
+ see_also = documentation.select { |tag| tag.name == :see }
76
+ unless see_also.empty?
77
+ items = see_also.map { |tag| list_item("", tag.description) }
78
+ parts << "### See also\n\n#{items.join("\n")}"
79
+ end
80
+ parts.join("\n\n")
81
+ end
82
+
83
+ def heading(declaration)
84
+ return "## Alias `#{display_name(declaration)}`" if declaration.alias_target
85
+
86
+ case declaration.kind
87
+ when :class
88
+ "## Class `#{qualified_name(declaration)}`"
89
+ when :module
90
+ "## Module `#{qualified_name(declaration)}`"
91
+ when :constant
92
+ "## Constant `#{qualified_name(declaration)}`"
93
+ when :refinement
94
+ "## Refinement `#{refinement_owner(declaration)}`"
95
+ when :instance_attribute_reader, :singleton_attribute_reader
96
+ "## Attribute reader `#{display_name(declaration)}`"
97
+ when :instance_attribute_writer, :singleton_attribute_writer
98
+ "## Attribute writer `#{display_name(declaration)}`"
99
+ when :instance_attribute_accessor, :singleton_attribute_accessor
100
+ "## Attribute accessor `#{display_name(declaration)}`"
101
+ else
102
+ "## `#{display_name(declaration)}(#{declaration.parameters.join(", ")})`"
103
+ end
104
+ end
105
+
106
+ def append_mixins(parts, declaration)
107
+ {include: "Includes", prepend: "Prepends", extend: "Extends"}.each do |kind, heading|
108
+ targets = declaration.mixins.select { |mixin| mixin.kind == kind }.map(&:target)
109
+ parts << "### #{heading}\n\n#{targets.map { |target| "- `#{target}`" }.join("\n")}" unless targets.empty?
110
+ end
111
+ end
112
+
113
+ def render_container_builder(declaration)
114
+ builder = declaration.container_builder
115
+ label = if declaration.kind == :module
116
+ "Module builder"
117
+ else
118
+ "Class builder"
119
+ end
120
+ return "#{label}: `#{builder}`." unless builder.include?("\n")
121
+
122
+ "#{label}:\n\n```ruby\n#{dedent_continuation(builder)}\n```"
123
+ end
124
+
125
+ def dedent_continuation(source)
126
+ first, *continuation = source.lines(chomp: true)
127
+ margins = continuation.reject { |line| line.strip.empty? }.map { |line| line[/\A[\t ]*/].length }
128
+ margin = margins.min || 0
129
+ [first, *continuation.map { |line| line[margin..] }].join("\n")
130
+ end
131
+
132
+ def qualified_name(declaration)
133
+ [declaration.namespace, declaration.name].compact.join("::")
134
+ end
135
+
136
+ def display_name(declaration)
137
+ if declaration.refinement
138
+ separator = declaration.kind.to_s.start_with?("singleton_") ? "." : "#"
139
+ return "#{refinement_owner(declaration)}#{separator}#{declaration.name}"
140
+ end
141
+
142
+ if declaration.kind.to_s.start_with?("singleton_")
143
+ owner = singleton_owner(declaration)
144
+ return owner ? "#{owner}.#{declaration.name}" : declaration.name
145
+ end
146
+ return declaration.name unless declaration.namespace
147
+
148
+ "#{declaration.namespace}##{declaration.name}"
149
+ end
150
+
151
+ def display_alias_target(declaration)
152
+ if declaration.refinement
153
+ separator = declaration.kind.to_s.start_with?("singleton_") ? "." : "#"
154
+ return "#{refinement_owner(declaration)}#{separator}#{declaration.alias_target}"
155
+ end
156
+
157
+ if declaration.kind.to_s.start_with?("singleton_")
158
+ owner = singleton_owner(declaration)
159
+ return owner ? "#{owner}.#{declaration.alias_target}" : declaration.alias_target
160
+ end
161
+ return declaration.alias_target unless declaration.namespace
162
+
163
+ "#{declaration.namespace}##{declaration.alias_target}"
164
+ end
165
+
166
+ def singleton_owner(declaration)
167
+ return declaration.receiver unless declaration.receiver.nil? || declaration.receiver == "self"
168
+
169
+ declaration.namespace || declaration.receiver
170
+ end
171
+
172
+ def refinement_owner(declaration)
173
+ namespace = declaration.namespace
174
+ target = declaration.refinement || declaration.name
175
+ namespace ? "#{namespace}[#{target}]" : "[#{target}]"
176
+ end
177
+
178
+ def list_item(prefix, description)
179
+ first, *continuation = description.split("\n", -1)
180
+ lines = ["- #{prefix}#{first}"]
181
+ continuation.each { |line| lines << (line.empty? ? "" : " #{line}") }
182
+ lines.join("\n")
183
+ end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zard
4
+ module Doc
5
+ VERSION = "0.0.1"
6
+ end
7
+ end
data/lib/zard/doc.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zard"
4
+ require_relative "doc/version"
5
+ require_relative "doc/renderer"
6
+
7
+ module Zard
8
+ module Doc
9
+ def self.render(document)
10
+ Renderer.render(document)
11
+ end
12
+
13
+ private_constant :Renderer
14
+ end
15
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: zard-doc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - USAMI Kenta
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: zard
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: 0.0.1
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: 0.0.1
26
+ description: zard-doc renders API documentation from the versioned ZARD document model.
27
+ email:
28
+ - tadsan@zonu.me
29
+ executables:
30
+ - zard-doc
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - LICENSE
35
+ - README.md
36
+ - exe/zard-doc
37
+ - lib/zard/doc.rb
38
+ - lib/zard/doc/cli.rb
39
+ - lib/zard/doc/renderer.rb
40
+ - lib/zard/doc/version.rb
41
+ homepage: https://github.com/rigortype/zard
42
+ licenses:
43
+ - MPL-2.0
44
+ metadata:
45
+ allowed_push_host: https://rubygems.org
46
+ homepage_uri: https://github.com/rigortype/zard
47
+ rubygems_mfa_required: 'true'
48
+ source_code_uri: https://github.com/rigortype/zard
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: 3.2.0
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirements: []
63
+ rubygems_version: 4.0.20
64
+ specification_version: 4
65
+ summary: Markdown API documentation for ZARD
66
+ test_files: []