1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
|
#!/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
|