#!/usr/bin/env ruby # frozen_string_literal: true require "optparse" require "pathname" require "strscan" require "rdoc" require "prism" OPTIONS = {} OptionParser .new do |opts| opts.banner = "Usage: rdoc-to-md RAILS_ROOT [options]" opts.on("-a", "Apply changes") opts.on("--only=FOLDERS", Array) end .parse!(into: OPTIONS) RAILS_PATH = File.expand_path("..", __dir__) folders = Dir["#{RAILS_PATH}/*/*.gemspec"].map { |p| Pathname.new(p).dirname } unless OPTIONS[:only].nil? folders.filter! { |path| OPTIONS[:only].include?(File.basename(path)) } end class Comment class << self def from(comment_nodes) comments_source_lines = source_lines_for(comment_nodes) if comments_source_lines.first == "##" MetaComment else Comment end.new(comments_source_lines) end private def source_lines_for(comment_nodes) comment_nodes.map { _1.location.slice } end end def initialize(source_lines) @source_lines = source_lines strip_hash_prefix! end def write!(out, indentation) as_markdown.each_line do |new_markdown_line| out << commented(new_markdown_line, indentation).rstrip << "\n" end end private attr_reader :source_lines def strip_hash_prefix! source_lines.each { |line| line.delete_prefix!("#") line.delete_prefix!(" ") } end def commented(markdown, indentation) (" " * indentation) + "# " + markdown end def as_markdown converter.convert(source_lines.join("\n")) end def converter RDoc::Markup::ToMarkdown.new end end class MetaComment < Comment def write!(out, indentation) spaces = " " * indentation out << spaces << "##\n" # ## out << commented(source_lines[1], indentation) << "\n" # # :method: ... super end private def as_markdown converter.convert(content_after_directive) end def content_after_directive source_lines[2..].join("\n") end end class CommentVisitor < Prism::BasicVisitor attr_reader :new_comments, :old_comment_lines def initialize # starting line => full block comment @new_comments = {} @old_comment_lines = Set.new end def method_missing(_, node) comments = node.location.comments process(comments) if process?(comments) visit_child_nodes(node) end private def process?(comments) return false if comments.empty? if comments.any?(&:trailing?) return false if comments.all?(&:trailing?) raise "only some comments are trailing?" end true end def process(comments) old_comment_range = line_range_for(comments) old_comment_range.each { @old_comment_lines << _1 } @new_comments[old_comment_range.begin] = Comment.from(comments) end def line_range_for(comments) comments.first.location.start_line..comments.last.location.start_line end end class CodeBlockConverter def initialize(file_path) @file_path = file_path @parse_result = Prism.parse_file(@file_path) @parse_result.attach_comments! @cv = CommentVisitor.new @source = @parse_result.source.source @parse_result.value.accept(@cv) end def convert! new_source = output if @source.include?(MD_DIRECTIVE) || new_source == @source $stdout.write "." else File.write(@file_path, output) $stdout.write "C" end end def print if output != @source $stdout.write "C" else $stdout.write "." end end private MD_DIRECTIVE = "# :markup: markdown" def output out = +"" @source.each_line.with_index do |old_line, i| line_number = i + 1 out << "\n" << MD_DIRECTIVE << "\n" if line_number == 2 if @cv.old_comment_lines.include?(line_number) if new_comment = @cv.new_comments[line_number] indentation = old_line.index("#") new_comment.write!(out, indentation) end else out << old_line end end out end end folders.each do |folder| ruby_files = Dir["#{folder}/{app,lib}/**/*.rb"] ruby_files.each do |file_path| converter = CodeBlockConverter.new(file_path) if OPTIONS[:a] converter.convert! else converter.print end end end