docrb 0.2.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 74814d0dca672ec2099fcb9092660f198b93ccb3b454979201ad18be4db5fb25
4
+ data.tar.gz: 34660996e9148eebe8a1757f04f0be4c2f19300d9de1fd43f793e3d7e97502c6
5
+ SHA512:
6
+ metadata.gz: 6f53696d60c6bf4634dd2c2fe093c24a786b1f6cd9e87432a73e8ca4c5298d2f9c868cd8b8640dbd65bfc67b130272d2708ea6e7cdd01da142da33278dae0625
7
+ data.tar.gz: a98ca47316bdc564de37a6144dfa8613f49717313d7dee5424ce5feca10f6e91f80353a0078233461d3f6dd4202ce0ecb3635ef6641326b3e325f28b6234562c
data/.editorconfig ADDED
@@ -0,0 +1,21 @@
1
+ root = true
2
+
3
+ [*]
4
+ end_of_line = lf
5
+ indent_size = 4
6
+ indent_style = space
7
+ insert_final_newline = true
8
+ tab_width = 8
9
+ trim_trailing_whitespace = true
10
+
11
+ [*.gemspec]
12
+ indent_size = 2
13
+
14
+ [*.rb]
15
+ indent_size = 2
16
+
17
+ [*.yml]
18
+ indent_size = 2
19
+
20
+ [./bin/*]
21
+ indent_size = 2
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,70 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.2
3
+ NewCops: enable
4
+ Exclude:
5
+ - spec/**/*
6
+
7
+ Style/StringLiterals:
8
+ Enabled: true
9
+ EnforcedStyle: double_quotes
10
+
11
+ Style/StringLiteralsInInterpolation:
12
+ Enabled: true
13
+ EnforcedStyle: double_quotes
14
+
15
+ Style/Documentation:
16
+ Exclude:
17
+ - lib/docrb/module_extensions.rb
18
+
19
+ Layout/LineLength:
20
+ Max: 120
21
+ Exclude:
22
+ - exe/*
23
+ - lib/docrb/comment_parser.rb
24
+ - lib/docrb/ruby_parser.rb
25
+
26
+ Metrics/ClassLength:
27
+ Enabled: false
28
+
29
+ Metrics/MethodLength:
30
+ Enabled: false
31
+
32
+ Naming/MethodParameterName:
33
+ Exclude:
34
+ - spec/**/*
35
+
36
+ Metrics/BlockLength:
37
+ Exclude:
38
+ - exe/*
39
+ - lib/docrb/ruby_parser.rb
40
+
41
+ Metrics/PerceivedComplexity:
42
+ Exclude:
43
+ - exe/*
44
+ - lib/docrb/doc_compiler/base_container.rb
45
+ - lib/docrb/doc_compiler/base_container/computations.rb
46
+ - lib/docrb/doc_compiler/object_container.rb
47
+ - lib/docrb/resolvable.rb
48
+ - lib/docrb/ruby_parser.rb
49
+
50
+ Metrics/CyclomaticComplexity:
51
+ Exclude:
52
+ - exe/*
53
+ - lib/docrb/doc_compiler/base_container.rb
54
+ - lib/docrb/doc_compiler/base_container/computations.rb
55
+ - lib/docrb/doc_compiler/object_container.rb
56
+ - lib/docrb/resolvable.rb
57
+ - lib/docrb/ruby_parser.rb
58
+
59
+ Lint/ShadowingOuterLocalVariable:
60
+ Exclude:
61
+ - exe/*
62
+
63
+ Metrics/AbcSize:
64
+ Enabled: false
65
+
66
+ Metrics/ModuleLength:
67
+ Enabled: false
68
+
69
+ Lint/BooleanSymbol:
70
+ Enabled: false
data/Gemfile ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in docrb.gemspec
6
+ gemspec
7
+
8
+ group :development do
9
+ gem "rake", "~> 13.0"
10
+
11
+ gem "rspec", "~> 3.0"
12
+
13
+ gem "rubocop", "~> 1.8"
14
+
15
+ gem "byebug"
16
+
17
+ gem "awesome_print"
18
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,79 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ docrb (0.2.0)
5
+ docrb-html (~> 0.2)
6
+ parser (~> 3.2)
7
+ redcarpet (~> 3.6)
8
+ rouge (~> 4.1)
9
+
10
+ GEM
11
+ remote: https://rubygems.org/
12
+ specs:
13
+ ast (2.4.2)
14
+ awesome_print (1.9.2)
15
+ byebug (11.1.3)
16
+ diff-lcs (1.5.0)
17
+ docrb-html (0.2.5)
18
+ nokogiri (~> 1.14)
19
+ sassc (~> 2.4)
20
+ ffi (1.15.5)
21
+ json (2.6.3)
22
+ nokogiri (1.14.2-arm64-darwin)
23
+ racc (~> 1.4)
24
+ nokogiri (1.14.2-x86_64-darwin)
25
+ racc (~> 1.4)
26
+ parallel (1.22.1)
27
+ parser (3.2.1.0)
28
+ ast (~> 2.4.1)
29
+ racc (1.6.2)
30
+ rainbow (3.1.1)
31
+ rake (13.0.6)
32
+ redcarpet (3.6.0)
33
+ regexp_parser (2.7.0)
34
+ rexml (3.2.5)
35
+ rouge (4.1.0)
36
+ rspec (3.12.0)
37
+ rspec-core (~> 3.12.0)
38
+ rspec-expectations (~> 3.12.0)
39
+ rspec-mocks (~> 3.12.0)
40
+ rspec-core (3.12.1)
41
+ rspec-support (~> 3.12.0)
42
+ rspec-expectations (3.12.2)
43
+ diff-lcs (>= 1.2.0, < 2.0)
44
+ rspec-support (~> 3.12.0)
45
+ rspec-mocks (3.12.3)
46
+ diff-lcs (>= 1.2.0, < 2.0)
47
+ rspec-support (~> 3.12.0)
48
+ rspec-support (3.12.0)
49
+ rubocop (1.45.1)
50
+ json (~> 2.3)
51
+ parallel (~> 1.10)
52
+ parser (>= 3.2.0.0)
53
+ rainbow (>= 2.2.2, < 4.0)
54
+ regexp_parser (>= 1.8, < 3.0)
55
+ rexml (>= 3.2.5, < 4.0)
56
+ rubocop-ast (>= 1.24.1, < 2.0)
57
+ ruby-progressbar (~> 1.7)
58
+ unicode-display_width (>= 2.4.0, < 3.0)
59
+ rubocop-ast (1.26.0)
60
+ parser (>= 3.2.1.0)
61
+ ruby-progressbar (1.11.0)
62
+ sassc (2.4.0)
63
+ ffi (~> 1.9)
64
+ unicode-display_width (2.4.2)
65
+
66
+ PLATFORMS
67
+ arm64-darwin-22
68
+ x86_64-darwin-20
69
+
70
+ DEPENDENCIES
71
+ awesome_print
72
+ byebug
73
+ docrb!
74
+ rake (~> 13.0)
75
+ rspec (~> 3.0)
76
+ rubocop (~> 1.8)
77
+
78
+ BUNDLED WITH
79
+ 2.2.22
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
data/bin/console ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "docrb"
6
+ require "byebug"
7
+
8
+ # You can add fixtures and/or initialization code here to make experimenting
9
+ # with your gem easier. You can also use a different console, if you like.
10
+
11
+ # (If you use this, don't forget to add pry to your Gemfile!)
12
+ # require "pry"
13
+ # Pry.start
14
+
15
+ require "irb"
16
+ IRB.start(__FILE__)
data/bin/json ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "docrb"
6
+ require "byebug"
7
+ require "json"
8
+
9
+ def run
10
+ data = Docrb.parse_folder(ARGV[0])
11
+ compiler = Docrb::DocCompiler.new
12
+ data.each { |f| compiler.append(f) }
13
+ puts compiler.to_h.to_json
14
+ end
15
+
16
+ run
data/bin/md ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "docrb"
6
+
7
+ data = File.read(ARGV[0]).to_s
8
+ puts Docrb::Markdown.render(data)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/docrb.gemspec ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/docrb/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "docrb"
7
+ spec.version = Docrb::VERSION
8
+ spec.authors = ["Victor Gama"]
9
+ spec.email = ["hey@vito.io"]
10
+
11
+ spec.summary = "An opinionated documentation parser"
12
+ spec.description = spec.summary
13
+ spec.homepage = "https://github.com/heyvito/docrb"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.2"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "#{spec.homepage}/tree/trunk/lib/docrb"
19
+ spec.metadata["changelog_uri"] = spec.homepage
20
+
21
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
22
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
23
+ end
24
+ spec.bindir = "exe"
25
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
26
+ spec.require_paths = ["lib"]
27
+
28
+ spec.add_dependency "docrb-html", "~> 0.2"
29
+ spec.add_dependency "parser", "~> 3.2"
30
+ spec.add_dependency "redcarpet", "~> 3.6"
31
+ spec.add_dependency "rouge", "~> 4.1"
32
+ spec.metadata["rubygems_mfa_required"] = "true"
33
+ end
data/exe/docrb ADDED
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "json"
6
+ require "fileutils"
7
+ require "optparse"
8
+ require "open3"
9
+ require "tmpdir"
10
+ require "renderer"
11
+
12
+ require "docrb"
13
+
14
+ def exec(cmd, *args, **kwargs)
15
+ _, stdout_and_err, wait_thread = Open3.popen2e(cmd, *args, **kwargs)
16
+ status = wait_thread.value
17
+ {
18
+ code: status.exitstatus,
19
+ output: stdout_and_err.read.chomp
20
+ }
21
+ end
22
+
23
+ def git(*args, **kwargs)
24
+ exec("git", *args, **kwargs)
25
+ end
26
+
27
+ def render_html(*args, **kwargs)
28
+ exec("docrb-html", *args, **kwargs)
29
+ end
30
+
31
+ def run
32
+ options = {}
33
+ opts = OptionParser.new do |opts|
34
+ opts.banner = "Usage: bin/docrb [options] [input directory] [output directory]"
35
+ gemspec_info = "When omitted, Docrb attempts to extract information from a .gemspec file in the provided input directory."
36
+
37
+ opts.on("--help", "Prints this help") do
38
+ puts opts
39
+ exit
40
+ end
41
+
42
+ opts.on("-bPATH", "--base=PATH",
43
+ "Base directory to search for source files. Defaults to the provided input directory.") do |b|
44
+ options[:base] = b
45
+ end
46
+
47
+ opts.on("-rPATH", "--readme=PATH",
48
+ "Path for README.md file. When omitted, Docrb searches for a README.md file in the provided input directory.") do |r|
49
+ options[:readme] = r
50
+ end
51
+
52
+ opts.on("-nNAME", "--name=NAME", "Name of the project being documented. #{gemspec_info}") do |n|
53
+ options[:name] = n
54
+ end
55
+
56
+ opts.on("-sSUMMARY", "--summary=SUMMARY", "Short summary of the project being documented. #{gemspec_info}") do |d|
57
+ options[:summary] = d
58
+ end
59
+
60
+ opts.on("-hURL", "--host=URL", "URL for the gem's hosted URL. #{gemspec_info}") do |u|
61
+ options[:host_url] = u
62
+ end
63
+
64
+ opts.on("-gURL", "--git-repo=URL",
65
+ "URL for the repository containing the documented project. When omitted, Docrb attempts to extract this information from the .git directory present in the provided input directory, if any.") do |u|
66
+ options[:git_url] = u
67
+ end
68
+
69
+ opts.on("--authors a,b,c", "List of name of project authors. #{gemspec_info}") do |list|
70
+ options[:authors] = list
71
+ end
72
+
73
+ opts.on("-lLICENSE", "--license=LICENSE", "The project's license. #{gemspec_info}") do |license|
74
+ options[:license] = license
75
+ end
76
+ end
77
+
78
+ opts.parse!
79
+
80
+ if ARGV.length != 2
81
+ puts opts
82
+ exit(1)
83
+ end
84
+
85
+ input = ARGV[0]
86
+ output = ARGV[1]
87
+
88
+ unless File.exist? input
89
+ puts "#{input}: Does not exist"
90
+ exit(1)
91
+ end
92
+
93
+ if File.exist? output
94
+ unless File.directory? output
95
+ puts "#{output}: Exists and is not a directory."
96
+ exit(1)
97
+ end
98
+ else
99
+ begin
100
+ FileUtils.mkdir_p output
101
+ rescue StandardError => e
102
+ puts e
103
+ exit(1)
104
+ end
105
+ end
106
+
107
+ tmp_output = Dir.mktmpdir
108
+ data_path = File.join(tmp_output, "data.json")
109
+ markdown_path = File.join(tmp_output, "readme.html")
110
+ metadata_path = File.join(tmp_output, "metadata.json")
111
+ readme_path = File.join(input, "README.md")
112
+ base_path = input
113
+
114
+ if options[:readme]
115
+ readme_path = options[:readme]
116
+ unless File.exist? readme_path
117
+ puts "--readme was provided, but #{readme_path} could not be found."
118
+ exit(1)
119
+ end
120
+ end
121
+
122
+ spec = Docrb::Spec.parse_folder(input) || {}
123
+
124
+ %i[name summary host_url git_url authors license].each do |k|
125
+ spec[k] = options[k] if options.key? k
126
+ end
127
+
128
+ if spec[:name] == "" || spec[:name].nil?
129
+ puts "Docrb could not detect the project name. Please check your .gemspec, or provide one manually using --name."
130
+ exit(1)
131
+ end
132
+
133
+ if spec[:summary] == "" || spec[:summary].nil?
134
+ puts "Docrb could not detect the project's summary. Please check your .gemspec, or provide one manually using --summary."
135
+ exit(1)
136
+ end
137
+
138
+ if options[:base]
139
+ path = File.join(input, options[:base])
140
+ unless File.exist? path
141
+ puts "--base was provided, but #{path} does not exist."
142
+ exit(1)
143
+ end
144
+ base_path = path
145
+ end
146
+
147
+ git_tip = nil
148
+ git_root = nil
149
+ git_status = git("status", "--porcelain", chdir: input)
150
+ if (git_status[:code]).zero?
151
+ if git_status[:output].length.positive?
152
+ puts "WARNING: Your local git copy seems to be dirty. Consider commiting your changes before generating docs."
153
+ end
154
+ tip_data = git("rev-parse", "HEAD", chdir: input)
155
+ if tip_data[:code] != 0
156
+ puts "ERROR: Acquiring repository information failed:"
157
+ puts tip_data[:output]
158
+ puts "------- 8< cut here"
159
+ puts "Aborting."
160
+ exit 1
161
+ end
162
+ toplevel = git("rev-parse", "--show-toplevel", chdir: input)
163
+ if toplevel[:code] != 0
164
+ puts "ERROR: Acquiring repository toplevel failed:"
165
+ puts tip_data[:output]
166
+ puts "------- 8< cut here"
167
+ puts "Aborting."
168
+ exit 1
169
+ end
170
+
171
+ git_tip = tip_data[:output]
172
+ git_root = toplevel[:output]
173
+ else
174
+ puts "git status returned status #{git_status[:code]}; avoiding git..."
175
+ end
176
+
177
+ spec[:git_tip] = git_tip if git_tip
178
+ spec[:git_root] = git_root if git_root
179
+ spec[:timestamp] = Time.now.utc.strftime("%A, %d %b %Y %l:%M %p GMT")
180
+ spec[:version] = Docrb::VERSION
181
+
182
+ data = Docrb.parse_folder(base_path)
183
+ compiler = Docrb::DocCompiler.new
184
+ data.each { |f| compiler.append(f) }
185
+
186
+ File.write(data_path, compiler.to_h.to_json)
187
+ puts "Created: #{data_path}"
188
+
189
+ if File.exist? readme_path
190
+ puts "Found #{readme_path}"
191
+ md = File.read(readme_path).to_s
192
+ result = Docrb::Markdown.render(md)
193
+ File.write(markdown_path, result)
194
+ puts "Created #{markdown_path}"
195
+ else
196
+ puts "#{readme_path} does not exist. Skipping..."
197
+ end
198
+ File.write(metadata_path, spec.to_json)
199
+ puts "Created #{metadata_path}"
200
+
201
+ puts "Generating HTML into #{output}"
202
+ Renderer.new(tmp_output, output).render
203
+ end
204
+
205
+ run
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docrb
4
+ class CommentParser
5
+ # CodeExampleBlock represents a list of characters of a code example
6
+ class CodeExampleBlock
7
+ attr_reader :code
8
+
9
+ def initialize
10
+ @code = []
11
+ end
12
+
13
+ def <<(text_block)
14
+ @code << "\n" unless empty?
15
+ @code << text_block.text
16
+ end
17
+
18
+ def empty?
19
+ @code.empty?
20
+ end
21
+
22
+ def normalize
23
+ @code
24
+ .map { |txt| txt.split("\n") }
25
+ .map { |item| item.empty? ? "" : item }
26
+ .flatten
27
+ .map { |line| line.gsub(/^\s{2}/, "") }
28
+ .join("\n")
29
+ end
30
+
31
+ def to_h
32
+ { type: :code_example, contents: normalize }
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docrb
4
+ class CommentParser
5
+ # CodeExampleParser attempts to extract code examples from a documentation
6
+ # block.
7
+ class CodeExampleParser
8
+ def self.process(components)
9
+ new_components = []
10
+ code_example_group = CodeExampleBlock.new
11
+ components.each do |c|
12
+ if !c.is_a?(TextBlock) || !c.text.start_with?(" ")
13
+ unless code_example_group.empty?
14
+ new_components << code_example_group
15
+ code_example_group = CodeExampleBlock.new
16
+ end
17
+ new_components << c
18
+ next
19
+ end
20
+
21
+ code_example_group << c
22
+ end
23
+
24
+ new_components << code_example_group unless code_example_group.empty?
25
+ new_components
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docrb
4
+ class CommentParser
5
+ # FieldBlock represents a list of fields obtained by FieldListParser
6
+ class FieldBlock
7
+ attr_reader :fields
8
+
9
+ def initialize(fields)
10
+ @fields = fields
11
+ end
12
+
13
+ def to_h
14
+ { type: :field_block, contents: fields }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docrb
4
+ class CommentParser
5
+ # FieldListParser parses a field block (representing arguments of an method,
6
+ # for instance) into a specialised structure.
7
+ class FieldListParser
8
+ FIELD_FORMAT_REGEXP = /^([a-z_][0-9a-z_]*:?)\s+- /i
9
+
10
+ def initialize(text)
11
+ @text = text
12
+ @data = []
13
+ @current = []
14
+ @result = {}
15
+ @dash_index = nil
16
+ end
17
+
18
+ def detect
19
+ @text.each_char do |c|
20
+ next @current << c unless c == LINE_BREAK
21
+ return false unless handle_linebreak
22
+ end
23
+ return false unless handle_linebreak
24
+
25
+ flush_current_field!
26
+ true
27
+ end
28
+
29
+ def handle_linebreak
30
+ return true if @current.empty?
31
+
32
+ # Here's a linebreak. Handle it as needed.
33
+ @current = @current.join
34
+ if infer_field_alignment(@current)
35
+ flush_current_field!
36
+ @data << @current
37
+ else
38
+ # This is not a field. May be a continuation.
39
+ # Can it be a continuation?
40
+ return false if @data.empty?
41
+
42
+ # Yep. It may be. Is it?
43
+ return false unless continuation?(@current)
44
+
45
+ # It is. Append to data.
46
+ @data << @current.strip
47
+ end
48
+ @current = []
49
+ true
50
+ end
51
+
52
+ def flush_current_field!
53
+ return if @data.empty?
54
+
55
+ data = @data.join(" ")
56
+ @data = []
57
+ field_name = FIELD_FORMAT_REGEXP.match(data)[1]
58
+ text = data.slice(@dash_index + 1...).strip
59
+ @result[field_name] = text
60
+ end
61
+
62
+ def continuation?(line)
63
+ return false unless @dash_index
64
+
65
+ # until dash_index, we should have spaces
66
+ return false if line.length < @dash_index
67
+
68
+ return false unless line.slice(0..@dash_index + 1).strip.empty?
69
+
70
+ true
71
+ end
72
+
73
+ def infer_field_alignment(line)
74
+ # is the line a field?
75
+ return false unless FIELD_FORMAT_REGEXP.match?(line)
76
+
77
+ if @dash_index
78
+ # does it match the same alignment?
79
+ return false if line[@dash_index] != DASH
80
+ else
81
+ @dash_index = line.index(DASH)
82
+ end
83
+
84
+ true
85
+ end
86
+
87
+ attr_reader :result
88
+ end
89
+ end
90
+ end