lib/readme_yard.rb
# frozen_string_literal: true
require "yard-readme"
require "tty-markdown"
require_relative "readme_yard/version"
require_relative "readme_yard/error"
require_relative "readme_yard/logger"
require_relative "readme_yard/readme_tag"
require_relative "readme_yard/example_tag"
require_relative "readme_yard/comment_tag"
require_relative "readme_yard/source_tag"
require_relative "readme_yard/object_tag"
YARDReadme::DocstringParser.readme_tag_names = %w[comment source object]
#
# @readme
# Build a better README with [YARD](https://yardoc.org)
# by generating it straight from the source.
#
# This gem aims to minimize the effort needed to keep your
# README, documentation, and source code synced, useful,
# and correct.
#
# Look at the [README_YARD.md](https://github.com/mattruzicka/readme_yard/blob/main/README_YARD.md)
# template for this project to see how it works.
# If you're reading the README, that means this text is here
# because the custom `{@readme ReadmeYard}` markdown tag is in
# README_YARD.md and `readme build` was run at the command line.
#
# Here's the [full documentation](https://rubydoc.info/github/mattruzicka/readme_yard).
#
class ReadmeYard
#
# @readme object
#
# @example
# ReadmeYard.hello_world #=> "Hello 🌎 🌍 🌏"
#
def self.hello_world
"Hello 🌎 🌍 🌏"
end
#
# @see #command_line_usage
#
def self.call(arg, options)
readme_yard = ReadmeYard.new
case arg
when "build"
readme_yard.build(options: options)
when "doc"
readme_yard.doc(options: options)
else
puts TTY::Markdown.parse(readme_yard.command_line_usage)
end
rescue Error => e
puts TTY::Markdown.parse(e.message)
end
TAG_CLASS_LOOKUP = { "readme" => ReadmeTag,
"example" => ExampleTag,
"source" => SourceTag,
"comment" => CommentTag,
"object" => ObjectTag }.freeze
def self.lookup_tag_class(tag_name)
TAG_CLASS_LOOKUP[tag_name]
end
def initialize
@readme_path = "./README.md"
@readme_yard_path = "./README_YARD.md"
end
attr_accessor :readme_path, :readme_yard_path
#
# This method returns the following `@readme` text
#
# @readme
# ## Command Line Usage
#
# `readme` - Prints command line usage.
#
# `readme build` - Reads from README_YARD.md and writes to README.md.
#
# `readme doc` - Same as `readme build` + generates yard docs.
#
def command_line_usage
yard_parse_this_file
yard_object = YARD::Registry.at("#{self.class.name}##{__method__}")
yard_tags = yard_object.tags(:readme)
"\n#{ReadmeTag.format_markdown(yard_object, yard_tags)}\n\n"
end
#
# @readme
# Reads from README_YARD.md and writes to README.md
#
# Forwards options to yardoc
#
# `-n` only generate .yardoc database, no documentation.
#
# `-q` silence yardoc output statements
#
def build(options: "-nq")
find_or_upsert_yardopts
run_yardoc(options: options)
File.write(readme_path, gsub_tags!(readme_yard_md))
end
#
# @readme Same as "build" + generates yard docs.
#
def doc(options: "-q")
build(options: options || "-q")
end
def run_yardoc(options: "-nq")
YARD::CLI::Yardoc.run(options || "-nq")
end
private
def readme_yard_md
return File.read(readme_yard_path) if File.exist?(readme_yard_path)
create_new_readme_yard_md
end
def create_new_readme_yard_md
new_readme_yard_md = File.read(readme_path) if File.exist?(readme_path)
new_readme_yard_md ||= default_readme_yard_md
File.write(readme_yard_path, new_readme_yard_md)
new_readme_yard_md
end
def default_readme_yard_md
yard_parse_this_file
+"Welcome to the README_YARD 🌿 🥏\n\n" \
"Check out this project's" \
" [README](https://github.com/mattruzicka/readme_yard#readme)" \
" for usage.\n\n{@readme ReadmeYard#default_readme_yard_md}"
end
YARDOPTS_FILE = ".yardopts"
def find_or_upsert_yardopts
File.exist?(YARDOPTS_FILE) ? update_yardopts_file : create_yardopts_file
end
def update_yardopts_file
text = File.read(YARDOPTS_FILE)
text_addition = build_yardopts_text_addition(text)
File.open(YARDOPTS_FILE, "a") { |f| f.write(text_addition) } if text_addition
end
def build_yardopts_text_addition(yardopts_text)
return if yardopts_text.match?(/\s*--plugin\s+readme\W/)
readme_plugin_opts = default_readme_plugin_opts(yardopts_text)
case yardopts_text
when /\s*--markup\s+markdown/, /\s*-m\s+markdown/
readme_plugin_opts
when /\s*--markup\s/, /\s*-m\s/
warn_about_supported_markdown
readme_plugin_opts
else
readme_plugin_opts << "--markup markdown\n"
end
end
def default_readme_plugin_opts(yardopts_text)
readme_opts = +""
readme_opts << "\n" unless yardopts_text.lines.last.include?("\n")
readme_opts << "--plugin readme\n"
end
def create_yardopts_file
File.write(YARDOPTS_FILE, "--plugin readme\n--markup markdown\n")
end
def gsub_tags!(markdown)
markdown.gsub!(/([^`]|^)(\{@\w+\s.+\})/) { |string| format_tag_markdown(string) }
markdown
end
def format_tag_markdown(string)
first_char = string[0]
return string if first_char == "`"
tag_name = extract_tag_name(string)
object_path = extract_object_path(string)
yard_object = YARD::Registry.at(object_path)
raise Error.object_not_found(object_path, tag_name) unless yard_object
yard_tags_markdown = format_yard_tags_markdown(yard_object, tag_name, string)
"#{first_char if first_char != "{"}#{yard_tags_markdown}"
end
def format_yard_tags_markdown(yard_object, tag_name, string)
yard_tags = yard_object.tags(tag_name)
if yard_tags.empty?
warn_about_yard_tags_not_found(yard_object, tag_name)
string
else
tag_class = self.class.lookup_tag_class(tag_name)
tag_class.format_markdown(yard_object, yard_tags)
end
end
def extract_tag_name(tag_string)
tag_string.match(/{@(\w+)\s/).captures.first
end
def extract_object_path(tag_string)
tag_string.match(/\s(\b.+)}/).captures.first
end
def yard_parse_this_file
gem_spec = Gem::Specification.find_by_name("readme_yard")
current_file_path = File.join(gem_spec.full_gem_path, "lib", "readme_yard.rb")
YARD.parse(current_file_path)
end
def warn_about_supported_markdown
Logger.warn "\n*Readme Yard* works best with markdown." \
"\nConsider adding `--markup markdown` to your `.yardopts` file.\n\n"
end
def warn_about_yard_tags_not_found(yard_object, tag_name)
Logger.warn "The `@#{tag_name}` *Readme Yard* tag is missing from #{yard_object}."
end
end