summaryrefslogtreecommitdiff
path: root/lib/rdoc/markup/fragments.rb
blob: 39b63cae226ccd20bb60c4e278a18024730f8523 (plain)
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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
require 'rdoc/markup'
require 'rdoc/markup/lines'

class RDoc::Markup

  ##
  # A Fragment is a chunk of text, subclassed as a paragraph, a list
  # entry, or verbatim text.

  class Fragment
    attr_reader   :level, :param, :txt
    attr_accessor :type

    ######
    # This is a simple factory system that lets us associate fragement
    # types (a string) with a subclass of fragment

    TYPE_MAP = {}

    def self.type_name(name)
      TYPE_MAP[name] = self
    end

    def self.for(line)
      klass =  TYPE_MAP[line.type] ||
        raise("Unknown line type: '#{line.type.inspect}:' '#{line.text}'")
      return klass.new(line.level, line.param, line.flag, line.text)
    end

    def initialize(level, param, type, txt)
      @level = level
      @param = param
      @type  = type
      @txt   = ""
      add_text(txt) if txt
    end

    def add_text(txt)
      @txt << " " if @txt.length > 0
      @txt << txt.tr_s("\n ", "  ").strip
    end

    def to_s
      "L#@level: #{self.class.name.split('::')[-1]}\n#@txt"
    end

  end

  ##
  # A paragraph is a fragment which gets wrapped to fit. We remove all
  # newlines when we're created, and have them put back on output.

  class Paragraph < Fragment
    type_name :PARAGRAPH
  end

  class BlankLine < Paragraph
    type_name :BLANK
  end

  class Heading < Paragraph
    type_name :HEADING

    def head_level
      @param.to_i
    end
  end

  ##
  # A List is a fragment with some kind of label

  class ListBase < Paragraph
    LIST_TYPES = [
      :BULLET,
      :NUMBER,
      :UPPERALPHA,
      :LOWERALPHA,
      :LABELED,
      :NOTE,
    ]
  end

  class ListItem < ListBase
    type_name :LIST

    #  def label
    #    am = AttributeManager.new(@param)
    #    am.flow
    #  end
  end

  class ListStart < ListBase
    def initialize(level, param, type)
      super(level, param, type, nil)
    end
  end

  class ListEnd < ListBase
    def initialize(level, type)
      super(level, "", type, nil)
    end
  end

  ##
  # Verbatim code contains lines that don't get wrapped.

  class Verbatim < Fragment
    type_name  :VERBATIM

    def add_text(txt)
      @txt << txt.chomp << "\n"
    end

  end

  ##
  # A horizontal rule

  class Rule < Fragment
    type_name :RULE
  end

  ##
  # Collect groups of lines together. Each group will end up containing a flow
  # of text.

  class LineCollection

    def initialize
      @fragments = []
    end

    def add(fragment)
      @fragments << fragment
    end

    def each(&b)
      @fragments.each(&b)
    end

    def to_a # :nodoc:
      @fragments.map {|fragment| fragment.to_s}
    end

    ##
    # Factory for different fragment types

    def fragment_for(*args)
      Fragment.for(*args)
    end

    ##
    # Tidy up at the end

    def normalize
      change_verbatim_blank_lines
      add_list_start_and_ends
      add_list_breaks
      tidy_blank_lines
    end

    def to_s
      @fragments.join("\n----\n")
    end

    def accept(am, visitor)
      visitor.start_accepting

      @fragments.each do |fragment|
        case fragment
        when Verbatim
          visitor.accept_verbatim(am, fragment)
        when Rule
          visitor.accept_rule(am, fragment)
        when ListStart
          visitor.accept_list_start(am, fragment)
        when ListEnd
          visitor.accept_list_end(am, fragment)
        when ListItem
          visitor.accept_list_item(am, fragment)
        when BlankLine
          visitor.accept_blank_line(am, fragment)
        when Heading
          visitor.accept_heading(am, fragment)
        when Paragraph
          visitor.accept_paragraph(am, fragment)
        end
      end

      visitor.end_accepting
    end

    private

    # If you have:
    #
    #    normal paragraph text.
    #
    #       this is code
    #   
    #       and more code
    #
    # You'll end up with the fragments Paragraph, BlankLine, Verbatim,
    # BlankLine, Verbatim, BlankLine, etc.
    #
    # The BlankLine in the middle of the verbatim chunk needs to be changed to
    # a real verbatim newline, and the two verbatim blocks merged

    def change_verbatim_blank_lines
      frag_block = nil
      blank_count = 0
      @fragments.each_with_index do |frag, i|
        if frag_block.nil?
          frag_block = frag if Verbatim === frag
        else
          case frag
          when Verbatim
            blank_count.times { frag_block.add_text("\n") }
            blank_count = 0
            frag_block.add_text(frag.txt)
            @fragments[i] = nil    # remove out current fragment
          when BlankLine
            if frag_block
              blank_count += 1
              @fragments[i] = nil
            end
          else
            frag_block = nil
            blank_count = 0
          end
        end
      end
      @fragments.compact!
    end

    ##
    # List nesting is implicit given the level of indentation. Make it
    # explicit, just to make life a tad easier for the output processors

    def add_list_start_and_ends
      level = 0
      res = []
      type_stack = []

      @fragments.each do |fragment|
        # $stderr.puts "#{level} : #{fragment.class.name} : #{fragment.level}"
        new_level = fragment.level
        while (level < new_level)
          level += 1
          type = fragment.type
          res << ListStart.new(level, fragment.param, type) if type
          type_stack.push type
          # $stderr.puts "Start: #{level}"
        end

        while level > new_level
          type = type_stack.pop
          res << ListEnd.new(level, type) if type
          level -= 1
          # $stderr.puts "End: #{level}, #{type}"
        end

        res << fragment
        level = fragment.level
      end
      level.downto(1) do |i|
        type = type_stack.pop
        res << ListEnd.new(i, type) if type
      end

      @fragments = res
    end

    ##
    # Inserts start/ends between list entries at the same level that have
    # different element types

    def add_list_breaks
      res = @fragments

      @fragments = []
      list_stack = []

      res.each do |fragment|
        case fragment
        when ListStart
          list_stack.push fragment
        when ListEnd
          start = list_stack.pop
          fragment.type = start.type
        when ListItem
          l = list_stack.last
          if fragment.type != l.type
            @fragments << ListEnd.new(l.level, l.type)
            start = ListStart.new(l.level, fragment.param, fragment.type)
            @fragments << start
            list_stack.pop
            list_stack.push start
          end
        else
          ;
        end
        @fragments << fragment
      end
    end

    ##
    # Tidy up the blank lines:
    # * change Blank/ListEnd into ListEnd/Blank
    # * remove blank lines at the front

    def tidy_blank_lines
      (@fragments.size - 1).times do |i|
        if @fragments[i].kind_of?(BlankLine) and 
            @fragments[i+1].kind_of?(ListEnd)
          @fragments[i], @fragments[i+1] = @fragments[i+1], @fragments[i] 
        end
      end

      # remove leading blanks
      @fragments.each_with_index do |f, i|
        break unless f.kind_of? BlankLine
        @fragments[i] = nil
      end

      @fragments.compact!
    end

  end

end