whatthegem 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +56 -0
- data/exe/whatthegem +20 -0
- data/lib/whatthegem/changes/markdown_parser.rb +41 -0
- data/lib/whatthegem/changes/parser.rb +42 -0
- data/lib/whatthegem/changes/rdoc_parser.rb +42 -0
- data/lib/whatthegem/changes/releases_parser.rb +27 -0
- data/lib/whatthegem/changes.rb +120 -0
- data/lib/whatthegem/commands.rb +101 -0
- data/lib/whatthegem/core_ext.rb +30 -0
- data/lib/whatthegem/gem/bundled.rb +55 -0
- data/lib/whatthegem/gem/github.rb +94 -0
- data/lib/whatthegem/gem/rubygems.rb +29 -0
- data/lib/whatthegem/gem.rb +64 -0
- data/lib/whatthegem/help.rb +31 -0
- data/lib/whatthegem/hobject.rb +61 -0
- data/lib/whatthegem/i.rb +56 -0
- data/lib/whatthegem/info.rb +58 -0
- data/lib/whatthegem/stats/definitions.rb +50 -0
- data/lib/whatthegem/stats/meters.rb +78 -0
- data/lib/whatthegem/stats.rb +12 -0
- data/lib/whatthegem/template.rb +53 -0
- data/lib/whatthegem/tty-markdown_patch.rb +9 -0
- data/lib/whatthegem/usage/extractor.rb +104 -0
- data/lib/whatthegem/usage.rb +47 -0
- data/lib/whatthegem.rb +12 -0
- metadata +251 -0
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'gems'
|
2
|
+
require 'octokit'
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
module WhatTheGem
|
6
|
+
class Gem
|
7
|
+
GITHUB_URI_PATTERN = %r{^https?://(www\.)?github\.com/}
|
8
|
+
|
9
|
+
NoGem = Struct.new(:name) do
|
10
|
+
def exists?
|
11
|
+
false
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.fetch(name)
|
16
|
+
# Empty hash in rubygems info means it does not exist.
|
17
|
+
# FIXME: Could be wrong in case of: a) private gems and b) "local-only" command checks
|
18
|
+
new(name).then { |gem| gem.rubygems.info.empty? ? NoGem.new(name) : gem }
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :name
|
22
|
+
|
23
|
+
def initialize(name)
|
24
|
+
@name = name
|
25
|
+
end
|
26
|
+
|
27
|
+
def exists?
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
31
|
+
memoize def rubygems
|
32
|
+
RubyGems.new(name)
|
33
|
+
end
|
34
|
+
|
35
|
+
memoize def github
|
36
|
+
rubygems.info.to_h.values_at(:source_code_uri, :homepage_uri)
|
37
|
+
.then(&method(:detect_repo_id))
|
38
|
+
&.then(&GitHub.method(:new))
|
39
|
+
end
|
40
|
+
|
41
|
+
memoize def specs
|
42
|
+
::Gem::Specification.select { |s| s.name == name }.sort_by(&:version)
|
43
|
+
end
|
44
|
+
|
45
|
+
memoize def bundled
|
46
|
+
Bundled.fetch(name)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
# FIXME: active_record's actual path is https://github.com/rails/rails/tree/v5.2.1/activerecord
|
52
|
+
def detect_repo_id(urls)
|
53
|
+
# Octokit can't correctly guess repo slug from https://github.com/bitaxis/annotate_models.git
|
54
|
+
urls.grep(GITHUB_URI_PATTERN).first
|
55
|
+
&.sub(/\.git$/, '')
|
56
|
+
&.then(&Octokit::Repository.method(:from_url))
|
57
|
+
&.slug
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
require_relative 'gem/rubygems'
|
63
|
+
require_relative 'gem/github'
|
64
|
+
require_relative 'gem/bundled'
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module WhatTheGem
|
2
|
+
class Help < Command
|
3
|
+
TEMPLATE = Template.parse(<<~HELP)
|
4
|
+
`whatthegem` is a small tool for fetching information about Ruby gems from various sources.
|
5
|
+
|
6
|
+
**Usage:** `whatthegem <gemname> [<command>]`
|
7
|
+
|
8
|
+
Known commands:
|
9
|
+
|
10
|
+
{% for command in commands %}
|
11
|
+
* `{{command.handle}}`: {{command.description}}{% endfor %}
|
12
|
+
HELP
|
13
|
+
|
14
|
+
def initialize(*)
|
15
|
+
end
|
16
|
+
|
17
|
+
def locals
|
18
|
+
{commands: commands}
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def full_output
|
24
|
+
markdown output
|
25
|
+
end
|
26
|
+
|
27
|
+
def commands
|
28
|
+
Command.registry.values.map(&:meta).map(&:to_h)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module WhatTheGem
|
2
|
+
class Hobject
|
3
|
+
class << self
|
4
|
+
def deep(hash_or_array)
|
5
|
+
deep_value(hash_or_array)
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def deep_value(val)
|
11
|
+
case
|
12
|
+
when val.respond_to?(:to_hash)
|
13
|
+
val.to_hash.transform_values(&method(:deep_value)).then(&method(:new))
|
14
|
+
when val.respond_to?(:to_ary)
|
15
|
+
val.to_ary.map(&method(:deep_value))
|
16
|
+
else
|
17
|
+
val
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(hash)
|
23
|
+
@hash = hash.transform_keys(&:to_sym).freeze
|
24
|
+
@hash.each do |key, val|
|
25
|
+
define_singleton_method(key) { val }
|
26
|
+
define_singleton_method("#{key}?") { !!val }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_h
|
31
|
+
@hash
|
32
|
+
end
|
33
|
+
|
34
|
+
def merge(other)
|
35
|
+
Hobject.new(to_h.merge(other.to_h))
|
36
|
+
end
|
37
|
+
|
38
|
+
def deep_to_h
|
39
|
+
@hash.transform_values(&method(:val_to_h))
|
40
|
+
end
|
41
|
+
|
42
|
+
def inspect
|
43
|
+
'#<Hobject(%s)>' % @hash.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')
|
44
|
+
end
|
45
|
+
|
46
|
+
alias to_s inspect
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def val_to_h(value)
|
51
|
+
case value
|
52
|
+
when Hobject
|
53
|
+
value.deep_to_h
|
54
|
+
when Array
|
55
|
+
value.map(&method(:val_to_h))
|
56
|
+
else
|
57
|
+
value
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/lib/whatthegem/i.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'rdoc'
|
2
|
+
require 'kramdown'
|
3
|
+
require 'pastel'
|
4
|
+
|
5
|
+
# Will be necessary when switching to kramdown-2, but tty-markdown doesn't support it yet.
|
6
|
+
# require 'kramdown-parser-gfm'
|
7
|
+
|
8
|
+
module WhatTheGem
|
9
|
+
# I for Internal
|
10
|
+
# @private
|
11
|
+
module I
|
12
|
+
extend self
|
13
|
+
|
14
|
+
module Callable
|
15
|
+
def to_proc
|
16
|
+
proc { |*args| call(*args) }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
Pastel = ::Pastel.new
|
21
|
+
|
22
|
+
module Kramdowns
|
23
|
+
extend self
|
24
|
+
|
25
|
+
def elements(source)
|
26
|
+
Kramdown::Document.new(source, input: 'GFM').root.children
|
27
|
+
end
|
28
|
+
|
29
|
+
# Somehow there is no saner methods for converting parsed element back to source :shrug:
|
30
|
+
def el2md(el)
|
31
|
+
el.options[:encoding] = 'UTF-8'
|
32
|
+
el.attr.replace({}) # don't render header anchors
|
33
|
+
Kramdown::Converter::Kramdown.convert(el, line_width: 1000).first
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
module RDocs
|
38
|
+
extend self
|
39
|
+
|
40
|
+
def parts(source)
|
41
|
+
RDoc::Comment.new(source).parse.parts
|
42
|
+
end
|
43
|
+
|
44
|
+
def part2md(part)
|
45
|
+
formatter = RDoc::Markup::ToMarkdown.new
|
46
|
+
RDoc::Markup::Document.new(part).accept(formatter)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def ago_text(tm)
|
51
|
+
diff = TimeMath.measure(tm, Time.now)
|
52
|
+
unit, num = diff.detect { |_, v| !v.zero? }
|
53
|
+
"#{num} #{unit} ago"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module WhatTheGem
|
2
|
+
class Info < Command
|
3
|
+
register description: '(default) General information about the gem'
|
4
|
+
|
5
|
+
# About info shortening: It is because minitest pastes their whole README
|
6
|
+
# there, including a quotations of how they are better than RSpec.
|
7
|
+
# (At the same time, RSpec's info reads "BDD for Ruby".)
|
8
|
+
|
9
|
+
TEMPLATE = Template.parse(<<~INFO)
|
10
|
+
Latest version: {{info.version}} ({{age}})
|
11
|
+
Installed versions: {% if specs %}{{ specs | map:"version" | join: ", "}}{% else %}—{% endif %}
|
12
|
+
{% if current %}
|
13
|
+
Most recent installed at: {{current.dir}}
|
14
|
+
{% endif %}
|
15
|
+
{% unless bundled.type == 'nobundle' %}
|
16
|
+
In your bundle: {% if bundled.type == 'notbundled' %}—{% else
|
17
|
+
%}{{ bundled.version }} at {{ bundled.dir }}{% endif %}
|
18
|
+
{% endunless %}
|
19
|
+
|
20
|
+
Try also:
|
21
|
+
{% for command in commands %}
|
22
|
+
`whatthegem {{info.name}} {{command.handle}}` -- {{command.description}}{% endfor %}
|
23
|
+
INFO
|
24
|
+
|
25
|
+
def locals
|
26
|
+
{
|
27
|
+
info: gem.rubygems.info,
|
28
|
+
age: age,
|
29
|
+
specs: specs,
|
30
|
+
current: specs.last,
|
31
|
+
bundled: gem.bundled.to_h,
|
32
|
+
commands: commands
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def age
|
39
|
+
gem.rubygems.versions
|
40
|
+
.first&.dig(:created_at)
|
41
|
+
&.then(&Time.method(:parse))&.then(&I.method(:ago_text))
|
42
|
+
end
|
43
|
+
|
44
|
+
def specs
|
45
|
+
gem.specs.map { |spec|
|
46
|
+
{
|
47
|
+
name: spec.name,
|
48
|
+
version: spec.version.to_s,
|
49
|
+
dir: spec.gem_dir
|
50
|
+
}
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
def commands
|
55
|
+
Command.registry.values.-([self.class]).map(&:meta).map(&:to_h)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'time_math'
|
2
|
+
|
3
|
+
module WhatTheGem
|
4
|
+
class Stats
|
5
|
+
module Definitions
|
6
|
+
require_relative 'meters'
|
7
|
+
|
8
|
+
FIFTY_PLUS = proc { |res| res == 50 ? '50+' : res }
|
9
|
+
HAS_REACTION = proc { |i| i[:labels].any? || i[:comments].positive? }
|
10
|
+
|
11
|
+
def self.meters
|
12
|
+
@meters ||= []
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.metric(name, *path, thresholds: nil, &block)
|
16
|
+
meters << Meter.new(name, path, thresholds, block || :itself.to_proc)
|
17
|
+
end
|
18
|
+
|
19
|
+
# TODO: Replace with TimeCalc when it'll be ready.
|
20
|
+
# TimeMath.month.decrease(now) => TimeCalc.now.-(1, :month)
|
21
|
+
T = TimeMath
|
22
|
+
now = Time.now
|
23
|
+
|
24
|
+
metric 'Downloads', :rubygems, :info, :downloads, thresholds: [5_000, 10_000]
|
25
|
+
metric 'Latest version',
|
26
|
+
:rubygems, :versions, 0, :created_at,
|
27
|
+
thresholds: [T.year.decrease(now), T.month.decrease(now, 2)],
|
28
|
+
&Time.method(:parse)
|
29
|
+
|
30
|
+
metric 'Used by', :rubygems, :reverse_dependencies, :count, thresholds: [10, 100]
|
31
|
+
|
32
|
+
metric 'Stars', :github, :repo, :stargazers_count, thresholds: [100, 500]
|
33
|
+
metric 'Forks', :github, :repo, :forks_count, thresholds: [5, 20]
|
34
|
+
metric 'Last commit',
|
35
|
+
:github, :last_commit, :commit, :committer, :date,
|
36
|
+
thresholds: [T.month.decrease(now, 4), T.month.decrease(now)]
|
37
|
+
|
38
|
+
metric('Open issues', :github, :open_issues, :count, &FIFTY_PLUS)
|
39
|
+
metric('...without reaction', :github, :open_issues, thresholds: [-20, -4]) { |issues|
|
40
|
+
issues.reject(&HAS_REACTION).count
|
41
|
+
}
|
42
|
+
metric('...last reaction',
|
43
|
+
:github, :open_issues,
|
44
|
+
thresholds: [T.month.decrease(now), T.week.decrease(now)]
|
45
|
+
) { |issues| issues.detect(&HAS_REACTION)&.dig(:updated_at) }
|
46
|
+
metric('Closed issues', :github, :closed_issues, :count, &FIFTY_PLUS)
|
47
|
+
metric '...last closed', :github, :closed_issues, 0, :closed_at
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'pastel'
|
2
|
+
|
3
|
+
module WhatTheGem
|
4
|
+
class Stats
|
5
|
+
class Meter < Struct.new(:name, :path, :thresholds, :block)
|
6
|
+
def call(gem)
|
7
|
+
dig(gem, *path)&.then(&block)
|
8
|
+
.then { |val| Metric.new(name, val, *thresholds) }
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def dig(value, first = nil, *rest)
|
14
|
+
return value unless first
|
15
|
+
case value
|
16
|
+
when nil
|
17
|
+
value
|
18
|
+
when ->(v) { first.is_a?(Symbol) && v.respond_to?(first) }
|
19
|
+
dig(value.public_send(first), *rest)
|
20
|
+
when Hash, Array
|
21
|
+
value.dig(first, *rest)
|
22
|
+
else
|
23
|
+
fail "Can't dig #{first} in #{value}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Metric
|
29
|
+
attr_reader :value, :name, :color
|
30
|
+
|
31
|
+
def initialize(name, value, *thresholds)
|
32
|
+
@name = name
|
33
|
+
@value = value
|
34
|
+
@color = deduce_color(*thresholds)
|
35
|
+
end
|
36
|
+
|
37
|
+
def format
|
38
|
+
'%20s: %s' % [name, colorized_value]
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def colorized_value
|
44
|
+
Pastel.new.send(color, formatted_value)
|
45
|
+
end
|
46
|
+
|
47
|
+
def formatted_value
|
48
|
+
case value
|
49
|
+
when nil
|
50
|
+
'—'
|
51
|
+
when String
|
52
|
+
value
|
53
|
+
when Numeric
|
54
|
+
# 100000 => 100,000
|
55
|
+
value.to_s.chars.reverse.each_slice(3).to_a.map(&:join).join(',').reverse
|
56
|
+
when Date, Time
|
57
|
+
I.ago_text(value)
|
58
|
+
else
|
59
|
+
fail ArgumentError, "Unformattable #{value.inspect}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def deduce_color(red = nil, yellow = nil)
|
64
|
+
return :dark if value.nil?
|
65
|
+
return :white if !yellow # no thresholds given
|
66
|
+
|
67
|
+
# special trick to tell "lower is better" from "higher is better" situations
|
68
|
+
val = red.is_a?(Numeric) && red < 0 ? -value : value
|
69
|
+
|
70
|
+
case
|
71
|
+
when val < red then :red
|
72
|
+
when val < yellow then :yellow
|
73
|
+
else :green
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module WhatTheGem
|
2
|
+
class Stats < Command
|
3
|
+
register description: 'Gem freshness and popularity stats'
|
4
|
+
|
5
|
+
def output
|
6
|
+
Definitions.meters.map { |meter| meter.call(gem).format }.join("\n")
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
require_relative 'stats/meters'
|
12
|
+
require_relative 'stats/definitions'
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'liquid'
|
2
|
+
require 'rouge'
|
3
|
+
|
4
|
+
module WhatTheGem
|
5
|
+
class Template < Liquid::Template
|
6
|
+
module Filters
|
7
|
+
def paragraphs(text, num)
|
8
|
+
# split on markdown-alike paragraph break (\n\n), or "paragraph, then list" (\n* )
|
9
|
+
text.split(/\n(?:\n|(?= *\*))/).first(num).join("\n\n").gsub(/\n +/, "\n").strip
|
10
|
+
end
|
11
|
+
|
12
|
+
def reflow(text)
|
13
|
+
text.tr("\n", ' ')
|
14
|
+
end
|
15
|
+
|
16
|
+
def nfirst(array, num)
|
17
|
+
array.first(num)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Consistently shift all markdown headers - ### - so they would be at least minlevel deep
|
21
|
+
def md_header_shift(text, minlevel)
|
22
|
+
current_min = text.scan(/^(\#+) /).flatten.map(&:length).min
|
23
|
+
return text if !current_min || current_min > minlevel
|
24
|
+
shift = minlevel - current_min
|
25
|
+
text.gsub(/^(\#+) /, '#' * shift + '\\1 ')
|
26
|
+
end
|
27
|
+
|
28
|
+
def rouge(text)
|
29
|
+
lexer = Rouge::Lexers::Ruby.new
|
30
|
+
Rouge::Formatters::Terminal256.new(Rouge::Themes::Base16::Monokai.new).format(lexer.lex(text))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.parse(src)
|
35
|
+
new.parse(src.chomp.gsub(/\n *({%.+?%})\n/, "\\1\n"))
|
36
|
+
end
|
37
|
+
|
38
|
+
def parse(src)
|
39
|
+
super(src, error_mode: :strict)
|
40
|
+
end
|
41
|
+
|
42
|
+
def render(data, **options)
|
43
|
+
super(Hm.(data).transform_keys(&:to_s).to_h, filters: [Filters], **options)
|
44
|
+
end
|
45
|
+
|
46
|
+
alias call render
|
47
|
+
|
48
|
+
def to_proc
|
49
|
+
# proc { |data| render(data) }
|
50
|
+
method(:call).to_proc
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'ripper'
|
2
|
+
|
3
|
+
module WhatTheGem
|
4
|
+
class Usage
|
5
|
+
# TODO:
|
6
|
+
# more robustly ignore "installation" section or just remove gem 'foo' / gem install
|
7
|
+
# -> select only ```ruby section from markdown (if they exist)?
|
8
|
+
# -> more removal patterns? Like `rake something`
|
9
|
+
#
|
10
|
+
class Extractor
|
11
|
+
REMOVE_BLOCKS = [
|
12
|
+
%{gem ['"]}, # gem install instructions
|
13
|
+
'gem install',
|
14
|
+
'bundle install',
|
15
|
+
'rake ',
|
16
|
+
|
17
|
+
'rails g ', # rails generator
|
18
|
+
'git clone', # instructions to contribute
|
19
|
+
|
20
|
+
'\\$', # bash command
|
21
|
+
'ruby (\S+)$', # run one Ruby command
|
22
|
+
|
23
|
+
'Copyright ' # Sometimes they render license in ```
|
24
|
+
]
|
25
|
+
|
26
|
+
REMOVE_BLOCKS_RE = /^#{REMOVE_BLOCKS.join('|')}/
|
27
|
+
|
28
|
+
extend I::Callable
|
29
|
+
|
30
|
+
class CodeBlock < Struct.new(:body)
|
31
|
+
def initialize(body)
|
32
|
+
super(try_sanitize(body))
|
33
|
+
end
|
34
|
+
|
35
|
+
def ruby?
|
36
|
+
# Ripper returns nil for anything but correct Ruby
|
37
|
+
#
|
38
|
+
# TODO: Unfortunately, Ripper is fixed to current version syntax, so trying this trick on
|
39
|
+
# Ruby 2.4 with something that uses Ruby 2.7 features will unhelpfully return "no, not Ruby"
|
40
|
+
#
|
41
|
+
# Maybe trying parser gem could lead to the same effect
|
42
|
+
!Ripper.sexp(body).nil?
|
43
|
+
end
|
44
|
+
|
45
|
+
def service?
|
46
|
+
body.match?(REMOVE_BLOCKS_RE)
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_h
|
50
|
+
{body: body}
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def try_sanitize(text)
|
56
|
+
text
|
57
|
+
.gsub(/^>> /, '') # Imitating work from IRB, TODO: various IRB/Pry patterns
|
58
|
+
.gsub(/^(=> )/, '# \\1') # Output in IRB, should be hidden as comment
|
59
|
+
.gsub(/^(Results: )/, '# \\1') # Output, in some gems
|
60
|
+
.gsub(/^( *)(\.{2,})/, '\\1# \\2') # "...and so on..." in code examples
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.call(file)
|
65
|
+
new(file).call
|
66
|
+
end
|
67
|
+
|
68
|
+
def initialize(file)
|
69
|
+
@file = file
|
70
|
+
end
|
71
|
+
|
72
|
+
def call
|
73
|
+
code_blocks(file.read).map(&CodeBlock.method(:new)).reject(&:service?).select(&:ruby?)
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
attr_reader :file
|
79
|
+
|
80
|
+
memoize def format
|
81
|
+
case file.basename.to_s
|
82
|
+
when /\.(md|markdown)$/i
|
83
|
+
:markdown
|
84
|
+
else
|
85
|
+
:rdoc
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def code_blocks(content)
|
90
|
+
__send__("#{format}_code_blocks", content)
|
91
|
+
end
|
92
|
+
|
93
|
+
def markdown_code_blocks(content)
|
94
|
+
I::Kramdowns.elements(content)
|
95
|
+
.select { |c| c.type == :codeblock }
|
96
|
+
.map(&:value).map(&:strip)
|
97
|
+
end
|
98
|
+
|
99
|
+
def rdoc_code_blocks(content)
|
100
|
+
I::RDocs.parts(content).grep(RDoc::Markup::Verbatim).map(&:text)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module WhatTheGem
|
2
|
+
# TODO
|
3
|
+
# use Piotr's markdown formatter
|
4
|
+
#
|
5
|
+
# friendly report of "usage instructions not found"
|
6
|
+
#
|
7
|
+
# If gem not found locally -- fetch from GitHub
|
8
|
+
#
|
9
|
+
class Usage < Command
|
10
|
+
register description: 'Gem usage examples'
|
11
|
+
|
12
|
+
TEMPLATE = Template.parse(<<~USAGE)
|
13
|
+
{% for u in usage %}
|
14
|
+
{{ u.body | rouge }}
|
15
|
+
{% endfor %}
|
16
|
+
USAGE
|
17
|
+
|
18
|
+
README_NOT_FOUND = I::Pastel.red.bold("Can't find `README` locally or in repo to extract usage.")
|
19
|
+
|
20
|
+
def locals
|
21
|
+
{
|
22
|
+
usage: readme.then(&Extractor).first(2).map(&:to_h)
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def output
|
29
|
+
return README_NOT_FOUND unless readme
|
30
|
+
super
|
31
|
+
end
|
32
|
+
|
33
|
+
memoize def readme
|
34
|
+
local_readme || github_readme
|
35
|
+
end
|
36
|
+
|
37
|
+
def local_readme
|
38
|
+
gem.specs.last&.gem_dir&.then(&Pathname.method(:new))&.glob('README{,.*}')&.first
|
39
|
+
end
|
40
|
+
|
41
|
+
def github_readme
|
42
|
+
gem.github&.readme
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
require_relative 'usage/extractor'
|
data/lib/whatthegem.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'hm'
|
2
|
+
|
3
|
+
module WhatTheGem
|
4
|
+
end
|
5
|
+
|
6
|
+
require_relative 'whatthegem/core_ext'
|
7
|
+
require_relative 'whatthegem/i'
|
8
|
+
|
9
|
+
require_relative 'whatthegem/gem'
|
10
|
+
require_relative 'whatthegem/template'
|
11
|
+
require_relative 'whatthegem/hobject'
|
12
|
+
require_relative 'whatthegem/commands'
|