File: rdoc-to-md

package info (click to toggle)
rails 2%3A7.2.2.1%2Bdfsg-7
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 43,352 kB
  • sloc: ruby: 349,799; javascript: 30,703; yacc: 46; sql: 43; sh: 29; makefile: 27
file content (214 lines) | stat: -rwxr-xr-x 4,392 bytes parent folder | download
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