Module: Docscribe::InlineRewriter::DocBlock

Defined in:
lib/docscribe/inline_rewriter/doc_block.rb

Overview

Text-preserving doc-block parsing and tag sorting helpers.

This module operates on existing comment blocks and is used to:

  • preserve user-authored tag text exactly

  • append generated missing tag entries

  • sort only configured sortable tags

  • preserve boundaries such as prose comments and blank comment lines

Defined Under Namespace

Classes: Entry

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.generatedObject

Parameters:

  • value (Object)

Returns:

  • (Object)


47
48
49
50
51
52
53
54
55
56
# File 'lib/docscribe/inline_rewriter/doc_block.rb', line 47

Entry = Struct.new(
  :kind,
  :tag,
  :lines,
  :subject,
  :option_owner,
  :generated,
  :index,
  keyword_init: true
)

.indexObject

Parameters:

  • value (Object)

Returns:

  • (Object)


47
48
49
50
51
52
53
54
55
56
# File 'lib/docscribe/inline_rewriter/doc_block.rb', line 47

Entry = Struct.new(
  :kind,
  :tag,
  :lines,
  :subject,
  :option_owner,
  :generated,
  :index,
  keyword_init: true
)

.kindObject

Parameters:

  • value (Object)

Returns:

  • (Object)


47
48
49
50
51
52
53
54
55
56
# File 'lib/docscribe/inline_rewriter/doc_block.rb', line 47

Entry = Struct.new(
  :kind,
  :tag,
  :lines,
  :subject,
  :option_owner,
  :generated,
  :index,
  keyword_init: true
)

.linesObject

Parameters:

  • value (Object)

Returns:

  • (Object)


47
48
49
50
51
52
53
54
55
56
# File 'lib/docscribe/inline_rewriter/doc_block.rb', line 47

Entry = Struct.new(
  :kind,
  :tag,
  :lines,
  :subject,
  :option_owner,
  :generated,
  :index,
  keyword_init: true
)

.option_ownerObject

Parameters:

  • value (Object)

Returns:

  • (Object)


47
48
49
50
51
52
53
54
55
56
# File 'lib/docscribe/inline_rewriter/doc_block.rb', line 47

Entry = Struct.new(
  :kind,
  :tag,
  :lines,
  :subject,
  :option_owner,
  :generated,
  :index,
  keyword_init: true
)

.subjectObject

Parameters:

  • value (Object)

Returns:

  • (Object)


47
48
49
50
51
52
53
54
55
56
# File 'lib/docscribe/inline_rewriter/doc_block.rb', line 47

Entry = Struct.new(
  :kind,
  :tag,
  :lines,
  :subject,
  :option_owner,
  :generated,
  :index,
  keyword_init: true
)

.tagObject

Parameters:

  • value (Object)

Returns:

  • (Object)


47
48
49
50
51
52
53
54
55
56
# File 'lib/docscribe/inline_rewriter/doc_block.rb', line 47

Entry = Struct.new(
  :kind,
  :tag,
  :lines,
  :subject,
  :option_owner,
  :generated,
  :index,
  keyword_init: true
)

Class Method Details

.blank_comment_line?(line) ⇒ Boolean

Note:

module_function: when included, also defines #blank_comment_line? (instance visibility: private)

Whether a line is a blank comment separator such as ‘#`.

Parameters:

  • line (String)

Returns:

  • (Boolean)


378
379
380
# File 'lib/docscribe/inline_rewriter/doc_block.rb', line 378

def blank_comment_line?(line)
  !!(line =~ /^\s*#\s*$/)
end

.build_groups(entries) ⇒ Array<Array<Entry>>

Note:

module_function: when included, also defines #build_groups (instance visibility: private)

Group entries so related ‘@option` tags stay attached to their owning `@param`.

Parameters:

Returns:



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
# File 'lib/docscribe/inline_rewriter/doc_block.rb', line 196

def build_groups(entries)
  groups = []
  i = 0

  while i < entries.length
    entry = entries[i]

    if entry.tag == 'param'
      group = [entry]
      i += 1

      while i < entries.length &&
            entries[i].tag == 'option' &&
            entries[i].option_owner &&
            entries[i].option_owner == entry.subject
        group << entries[i]
        i += 1
      end

      groups << group
    else
      groups << [entry]
      i += 1
    end
  end

  groups
end

.build_priority(tag_order) ⇒ Hash{String=>Integer}

Note:

module_function: when included, also defines #build_priority (instance visibility: private)

Build a tag priority map from configured order.

Parameters:

  • tag_order (Array<String>)

Returns:

  • (Hash{String=>Integer})


240
241
242
# File 'lib/docscribe/inline_rewriter/doc_block.rb', line 240

def build_priority(tag_order)
  normalized_tag_order(tag_order).each_with_index.to_h
end

.comment_line?(line) ⇒ Boolean

Note:

module_function: when included, also defines #comment_line? (instance visibility: private)

Whether a line is any comment line.

Parameters:

  • line (String)

Returns:

  • (Boolean)


369
370
371
# File 'lib/docscribe/inline_rewriter/doc_block.rb', line 369

def comment_line?(line)
  !!(line =~ /^\s*#/)
end

.consume_tag_entry(lines, start_idx, index:, sortable_tags:) ⇒ Array<(Entry, Integer)>

Note:

module_function: when included, also defines #consume_tag_entry (instance visibility: private)

Consume one sortable top-level tag entry and its continuation lines.

Continuation lines are comment lines that belong to the same logical tag entry until a new sortable tag line or a blank comment separator is encountered.

Parameters:

  • lines (Array<String>)
  • start_idx (Integer)
  • index (Integer)

    stable original index

  • sortable_tags (Array<String>)

Returns:

  • (Array<(Entry, Integer)>)

    parsed entry and next index



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
# File 'lib/docscribe/inline_rewriter/doc_block.rb', line 264

def consume_tag_entry(lines, start_idx, index:, sortable_tags:)
  first = lines[start_idx]
  tag = extract_tag(first)

  entry_lines = [first]
  i = start_idx + 1

  while i < lines.length
    line = lines[i]
    break if sortable_top_level_tag_line?(line, sortable_tags)
    break if blank_comment_line?(line)
    break unless continuation_comment_line?(line)

    entry_lines << line
    i += 1
  end

  entry = Entry.new(
    kind: :tag,
    tag: tag,
    lines: entry_lines,
    subject: extract_subject(first, tag),
    option_owner: extract_option_owner(first),
    generated: false,
    index: index
  )

  [entry, i]
end

.continuation_comment_line?(line) ⇒ Boolean

Note:

module_function: when included, also defines #continuation_comment_line? (instance visibility: private)

Whether a comment line should be treated as a continuation of the previous tag entry.

Parameters:

  • line (String)

Returns:

  • (Boolean)


387
388
389
# File 'lib/docscribe/inline_rewriter/doc_block.rb', line 387

def continuation_comment_line?(line)
  !!(line =~ /^\s*#[ \t]{2,}\S/)
end

.extract_option_owner(line) ⇒ String?

Note:

module_function: when included, also defines #extract_option_owner (instance visibility: private)

Extract the owning options-hash param name from an ‘@option` line.

Parameters:

  • line (String)

Returns:

  • (String, nil)


330
331
332
# File 'lib/docscribe/inline_rewriter/doc_block.rb', line 330

def extract_option_owner(line)
  line[/^\s*#\s*@option\b\s+(\S+)/, 1]
end

.extract_param_name(line) ⇒ String?

Note:

module_function: when included, also defines #extract_param_name (instance visibility: private)

Extract a parameter name from a ‘@param` line.

Supports both:

  • ‘@param [Type] name`

  • ‘@param name [Type]`

Parameters:

  • line (String)

Returns:

  • (String, nil)


318
319
320
321
322
323
# File 'lib/docscribe/inline_rewriter/doc_block.rb', line 318

def extract_param_name(line)
  return Regexp.last_match(1) if line =~ /^\s*#\s*@param\b\s+\[[^\]]+\]\s+(\S+)/
  return Regexp.last_match(1) if line =~ /^\s*#\s*@param\b\s+(\S+)\s+\[[^\]]+\]/

  nil
end

.extract_subject(line, tag) ⇒ String?

Note:

module_function: when included, also defines #extract_subject (instance visibility: private)

Extract the grouping subject for a sortable tag.

Currently only ‘@param` entries carry a subject, used to keep `@option` tags attached.

Parameters:

  • line (String)
  • tag (String)

Returns:

  • (String, nil)


302
303
304
305
306
307
# File 'lib/docscribe/inline_rewriter/doc_block.rb', line 302

def extract_subject(line, tag)
  case tag
  when 'param'
    extract_param_name(line)
  end
end

.extract_tag(line) ⇒ String?

Note:

module_function: when included, also defines #extract_tag (instance visibility: private)

Extract a top-level tag name without the leading ‘@`.

Parameters:

  • line (String)

Returns:

  • (String, nil)


351
352
353
# File 'lib/docscribe/inline_rewriter/doc_block.rb', line 351

def extract_tag(line)
  line[/^\s*#\s*@(\w+)/, 1]
end

.group_priority(group, priority) ⇒ Integer

Note:

module_function: when included, also defines #group_priority (instance visibility: private)

Compute the priority of a grouped sortable unit.

Parameters:

  • group (Array<Entry>)
  • priority (Hash{String=>Integer})

Returns:

  • (Integer)


231
232
233
# File 'lib/docscribe/inline_rewriter/doc_block.rb', line 231

def group_priority(group, priority)
  priority.fetch(group.first.tag, priority.length)
end

.merge(existing_lines, missing_lines:, sort_tags:, tag_order:, filter_existing: {}) ⇒ Array<String>

Note:

module_function: when included, also defines #merge (instance visibility: private)

Merge existing doc lines with newly generated missing tag lines.

Existing text is preserved exactly. If sorting is enabled, only sortable tag runs are normalized according to the configured tag order.

Parameters:

  • existing_lines (Array<String>)

    existing doc block lines

  • missing_lines (Array<String>)

    generated tag lines to add

  • sort_tags (Boolean)

    whether sortable tags should be reordered

  • tag_order (Array<String>)

    configured sortable tag order

  • filter_existing (Hash) (defaults to: {})

    Param documentation.

Returns:

  • (Array<String>)


70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/docscribe/inline_rewriter/doc_block.rb', line 70

def merge(existing_lines, missing_lines:, sort_tags:, tag_order:, filter_existing: {})
  existing_entries = parse(existing_lines, tag_order: tag_order)
  missing_entries = parse_generated(missing_lines, tag_order: tag_order)

  filter_param_names = filter_existing[:param_names] || []
  filter_return = !!filter_existing[:return]

  existing_entries = existing_entries.reject do |e|
    (e.kind == :tag && e.tag == 'param' && filter_param_names.include?(e.subject)) ||
      (e.kind == :tag && e.tag == 'return' && filter_return)
  end

  entries = existing_entries + missing_entries
  entries = sort(entries, tag_order: tag_order) if sort_tags

  render(entries)
end

.normalized_tag_order(tag_order) ⇒ Array<String>

Note:

module_function: when included, also defines #normalized_tag_order (instance visibility: private)

Normalize configured tag names by removing leading ‘@`.

Parameters:

  • tag_order (Array<String>)

Returns:

  • (Array<String>)


249
250
251
# File 'lib/docscribe/inline_rewriter/doc_block.rb', line 249

def normalized_tag_order(tag_order)
  Array(tag_order).map { |t| t.to_s.sub(/\A@/, '') }
end

.parse(lines, tag_order:) ⇒ Array<Entry>

Note:

module_function: when included, also defines #parse (instance visibility: private)

Parse a doc block into structured entries.

Only tags listed in ‘tag_order` are treated as sortable tag entries. Other lines become `:other` entries and act as sort boundaries.

Parameters:

  • lines (Array<String>)

    comment block lines

  • tag_order (Array<String>)

    configured sortable tag order

Returns:



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
# File 'lib/docscribe/inline_rewriter/doc_block.rb', line 110

def parse(lines, tag_order:)
  sortable_tags = normalized_tag_order(tag_order)
  entries = []
  i = 0
  index = 0

  while i < lines.length
    line = lines[i]

    if sortable_top_level_tag_line?(line, sortable_tags)
      entry, i = consume_tag_entry(lines, i, index: index, sortable_tags: sortable_tags)
      entries << entry
    else
      entries << Entry.new(
        kind: :other,
        lines: [line],
        generated: false,
        index: index
      )
      i += 1
    end

    index += 1
  end

  entries
end

.parse_generated(lines, tag_order:) ⇒ Array<Entry>

Note:

module_function: when included, also defines #parse_generated (instance visibility: private)

Parse generated missing tag lines and mark them as generated entries.

Parameters:

  • lines (Array<String>)

    generated lines

  • tag_order (Array<String>)

    configured sortable tag order

Returns:



94
95
96
97
98
99
# File 'lib/docscribe/inline_rewriter/doc_block.rb', line 94

def parse_generated(lines, tag_order:)
  parse(lines, tag_order: tag_order).map do |entry|
    entry.generated = true if entry.kind == :tag
    entry
  end
end

.render(entries) ⇒ Array<String>

Note:

module_function: when included, also defines #render (instance visibility: private)

Render parsed entries back into comment lines.

Parameters:

Returns:

  • (Array<String>)


171
172
173
# File 'lib/docscribe/inline_rewriter/doc_block.rb', line 171

def render(entries)
  entries.flat_map(&:lines)
end

.sort(entries, tag_order:) ⇒ Array<Entry>

Note:

module_function: when included, also defines #sort (instance visibility: private)

Sort parsed entries by configured tag order, preserving boundaries between tag runs.

Parameters:

  • entries (Array<Entry>)

    parsed entries

  • tag_order (Array<String>)

    configured sortable tag order

Returns:



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/docscribe/inline_rewriter/doc_block.rb', line 144

def sort(entries, tag_order:)
  out = []
  priority = build_priority(tag_order)
  i = 0

  while i < entries.length
    if entries[i].kind == :tag
      run = []
      while i < entries.length && entries[i].kind == :tag
        run << entries[i]
        i += 1
      end
      out.concat(sort_run(run, priority: priority))
    else
      out << entries[i]
      i += 1
    end
  end

  out
end

.sort_run(entries, priority:) ⇒ Array<Entry>

Note:

module_function: when included, also defines #sort_run (instance visibility: private)

Sort one contiguous run of sortable tag entries.

Parameters:

  • entries (Array<Entry>)

    contiguous tag run

  • priority (Hash{String=>Integer})

    tag priority map

Returns:



181
182
183
184
185
186
187
188
189
# File 'lib/docscribe/inline_rewriter/doc_block.rb', line 181

def sort_run(entries, priority:)
  groups = build_groups(entries)

  groups
    .each_with_index
    .sort_by { |(group, idx)| [group_priority(group, priority), idx] }
    .map(&:first)
    .flatten
end

.sortable_top_level_tag_line?(line, sortable_tags) ⇒ Boolean

Note:

module_function: when included, also defines #sortable_top_level_tag_line? (instance visibility: private)

Whether a line is a sortable top-level tag line.

Parameters:

  • line (String)
  • sortable_tags (Array<String>)

Returns:

  • (Boolean)


340
341
342
343
344
# File 'lib/docscribe/inline_rewriter/doc_block.rb', line 340

def sortable_top_level_tag_line?(line, sortable_tags)
  return false unless top_level_tag_line?(line)

  sortable_tags.include?(extract_tag(line))
end

.top_level_tag_line?(line) ⇒ Boolean

Note:

module_function: when included, also defines #top_level_tag_line? (instance visibility: private)

Whether a line begins a top-level YARD-style tag.

Parameters:

  • line (String)

Returns:

  • (Boolean)


360
361
362
# File 'lib/docscribe/inline_rewriter/doc_block.rb', line 360

def top_level_tag_line?(line)
  !!(line =~ /^\s*#\s*@\w+/)
end