whatthegem 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 +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'
|