• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

benwbrum / fromthepage / 18083086799

29 Sep 2025 01:37AM UTC coverage: 64.777% (-0.02%) from 64.796%
18083086799

push

github

web-flow
Fix flash messages not displaying properly on landing page (#4928)

* Initial plan

* Add flash message display to landing page

Co-authored-by: benwbrum <199961+benwbrum@users.noreply.github.com>

* Fix linter errors with rubocop -a

Co-authored-by: benwbrum <199961+benwbrum@users.noreply.github.com>

* Add flash message styling to landing page

Co-authored-by: benwbrum <199961+benwbrum@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: benwbrum <199961+benwbrum@users.noreply.github.com>

1835 of 3355 branches covered (54.69%)

Branch coverage included in aggregate %.

8048 of 11902 relevant lines covered (67.62%)

109.54 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.52
/app/models/xml_source_processor.rb
1
module XmlSourceProcessor
1✔
2
  def validate_source
1✔
3
    if self.source_text.blank?
3,658✔
4
      return
3,514✔
5
    end
6
    # Skip subject linking validation for field-based collections
7
    # and collections with subjects disabled
8
    if self.collection&.field_based || self.collection&.subjects_disabled
144✔
9
      return
8✔
10
    end
11
    validate_links(self.source_text)
136✔
12
  end
13

14
  def validate_source_translation
1✔
15
    if self.source_translation.blank?
3,655✔
16
      return
3,604✔
17
    end
18
    # Skip subject linking validation for field-based collections
19
    # and collections with subjects disabled
20
    if self.collection&.field_based || self.collection&.subjects_disabled
51!
21
      return
1✔
22
    end
23
    validate_links(self.source_translation)
50✔
24
  end
25

26
  # check the text for problems or typos with the subject links
27
  def validate_links(text)
1✔
28
    error_scope = [ :activerecord, :errors, :models, :xml_source_processor ]
186✔
29
    # split on all begin-braces
30
    tags = text.split('[[')
186✔
31
    # remove the initial string which occurs before the first tag
32
    debug("validate_source: tags to process are #{tags.inspect}")
186✔
33
    tags = tags - [ tags[0] ]
186✔
34
    debug("validate_source: massaged tags to process are #{tags.inspect}")
186✔
35
    for tag in tags
186✔
36
      debug(tag)
114✔
37

38
      if tag.include?(']]]')
114✔
39
        errors.add(:base, I18n.t('subject_linking_error', scope: error_scope) + I18n.t('tags_should_not_use_3_brackets', scope: error_scope))
1✔
40
        return
1✔
41
      end
42
      unless tag.include?(']]')
113✔
43
        tag = tag.strip
3✔
44
        errors.add(:base, I18n.t('subject_linking_error', scope: error_scope) + I18n.t('wrong_number_of_closing_braces', tag: '"[['+tag+'"', scope: error_scope))
3✔
45
      end
46

47
      # just pull the pieces between the braces
48
      inner_tag = tag.split(']]')[0]
113✔
49
      if inner_tag =~ /^\s*$/
113✔
50
        errors.add(:base, I18n.t('subject_linking_error', scope: error_scope) + I18n.t('blank_tag_in', tag: '"[['+tag+'"', scope: error_scope))
1✔
51
      end
52

53
      # check for unclosed single bracket
54
      if inner_tag.include?('[')
113✔
55
        unless inner_tag.include?(']')
1!
56
          errors.add(:base, I18n.t('subject_linking_error', scope: error_scope) + I18n.t('unclosed_bracket_within', tag: '"'+inner_tag+'"', scope: error_scope))
1✔
57
        end
58
      end
59
      # check for blank title or display name with pipes
60
      if inner_tag.include?('|')
113✔
61
        tag_parts = inner_tag.split('|')
10✔
62
        debug("validate_source: inner tag parts are #{tag_parts.inspect}")
10✔
63
        if tag_parts[0] =~ /^\s*$/
10✔
64
          errors.add(:base, I18n.t('subject_linking_error', scope: error_scope) + I18n.t('blank_subject_in', tag: '"[['+inner_tag+']]"', scope: error_scope))
1✔
65
        end
66
        if tag_parts[1] =~ /^\s*$/
10✔
67
          errors.add(:base, I18n.t('subject_linking_error', scope: error_scope) + I18n.t('blank_text_in', tag: '"[['+inner_tag+']]"', scope: error_scope))
1✔
68
        end
69
      end
70
    end
71
    #    return errors.size > 0
72
  end
73

74
def source_text=(text)
1✔
75
    self.source_text_will_change!
175✔
76
    super
175✔
77
end
78

79
def source_translation=(text)
1✔
80
  self.source_translation_will_change!
55✔
81
  super
55✔
82
end
83

84
  ##############################################
85
  # All code to convert transcriptions from source
86
  # format to canonical xml format belongs here.
87
  ##############################################
88
  def process_source
1✔
89
    if source_text_changed?
259✔
90
      self.xml_text = wiki_to_xml(self, Page::TEXT_TYPE::TRANSCRIPTION)
61✔
91
    end
92

93
    if self.respond_to?(:source_translation) && source_translation_changed?
259✔
94
      self.xml_translation = wiki_to_xml(self, Page::TEXT_TYPE::TRANSLATION)
12✔
95
    end
96
  end
97

98
  def wiki_to_xml(page, text_type)
1✔
99
    subjects_disabled = page.collection.subjects_disabled
79✔
100

101
    source_text = case text_type
79✔
102
    when Page::TEXT_TYPE::TRANSCRIPTION
66✔
103
                    page.source_text
66✔
104
    when Page::TEXT_TYPE::TRANSLATION
13✔
105
                    page.source_translation
13✔
106
    else
×
107
                    ''
×
108
    end
109

110
    xml_string = String.new(source_text)
79✔
111
    xml_string = process_latex_snippets(xml_string)
79✔
112
    xml_string = clean_bad_braces(xml_string)
79✔
113
    xml_string = clean_script_tags(xml_string)
79✔
114
    xml_string = process_square_braces(xml_string) unless subjects_disabled
79✔
115
    xml_string = process_linewise_markup(xml_string)
79✔
116
    xml_string = process_line_breaks(xml_string)
79✔
117
    xml_string = valid_xml_from_source(xml_string)
79✔
118
    xml_string = update_links_and_xml(xml_string, false, text_type)
79✔
119
    xml_string = postprocess_xml_markup(xml_string)
79✔
120
    postprocess_sections
79✔
121
    xml_string
79✔
122
  end
123

124

125
  # remove script tags from HTML to prevent javascript injection
126
  def clean_script_tags(text)
1✔
127
    # text.gsub(/<script.*?<\/script>/m, '')
128
    text.gsub(/<\/?script.*?>/m, '')
79✔
129
  end
130

131
  BAD_SHIFT_REGEX = /\[\[([[[:alpha:]][[:blank:]]|,\(\)\-[[:digit:]]]+)\}\}/
1✔
132
  def clean_bad_braces(text)
1✔
133
    text.gsub BAD_SHIFT_REGEX, '[[\\1]]'
79✔
134
  end
135

136
  BRACE_REGEX = /\[\[.*?\]\]/m
1✔
137
  def process_square_braces(text)
1✔
138
    # find all the links
139
    wikilinks = text.scan(BRACE_REGEX)
76✔
140
    wikilinks.each do |wikilink_contents|
76✔
141
      # strip braces
142
      munged = wikilink_contents.sub('[[', '')
30✔
143
      munged = munged.sub(']]', '')
30✔
144

145
      # extract the title and display
146
      if munged.include? '|'
30✔
147
        parts = munged.split '|'
10✔
148
        title = parts[0]
10✔
149
        verbatim = parts[1]
10✔
150
      else
20✔
151
        title = munged
20✔
152
        verbatim = munged
20✔
153
      end
154

155
      title = canonicalize_title(title)
30✔
156

157
      replacement = "<link target_title=\"#{title}\">#{verbatim}</link>"
30✔
158
      text.sub!(wikilink_contents, replacement)
30✔
159
    end
160

161
    text
76✔
162
  end
163

164
  def remove_square_braces(text)
1✔
165
    new_text = text.scan(BRACE_REGEX)
3✔
166
    new_text.each do |results|
3✔
167
      changed = results
3✔
168
      # remove title
169
      if results.include?('|')
3!
170
        changed = results.sub(/\[\[.*?\|/, '')
×
171
      end
172
      changed = changed.sub('[[', '')
3✔
173
      changed = changed.sub(']]', '')
3✔
174

175
      text.sub!(results, changed)
3✔
176
    end
177
    text
3✔
178
  end
179

180
  LATEX_SNIPPET = /(\{\{tex:?(.*?):?tex\}\})/m
1✔
181
  def process_latex_snippets(text)
1✔
182
    return text unless self.respond_to? :tex_figures
79✔
183
    replacements = {}
64✔
184
    figures = self.tex_figures.to_a
64✔
185

186
    text.scan(LATEX_SNIPPET).each_with_index do |pair, i|
64✔
187
      with_tags = pair[0]
×
188
      contents = pair[1]
×
189

190
      replacements[with_tags] = "<texFigure position=\"#{i+1}\"/>" # position attribute in acts as list starts with 1
×
191

192
      figure = figures[i] || TexFigure.new
×
193
      figure.source = contents unless figure.source == contents
×
194
      figures[i] = figure
×
195
    end
196

197
    self.tex_figures = figures
64✔
198
    replacements.each_pair do |s, r|
64✔
199
      text.sub!(s, r)
×
200
    end
201

202
    text
64✔
203
  end
204

205
  HEADER = /\s\|\s/
1✔
206
  SEPARATOR = /---.*\|/
1✔
207
  ROW = HEADER
1✔
208

209
  def process_linewise_markup(text)
1✔
210
    @tables = []
79✔
211
    @sections = []
79✔
212
    new_lines = []
79✔
213
    current_table = nil
79✔
214
    text.lines.each do |line|
79✔
215
      # first deal with any sections
216
      line = process_any_sections(line)
100✔
217
      # look for a header
218
      if !current_table
100✔
219
        if line.match(HEADER)
100!
220
          line.chomp
×
221
          current_table = { header: [], rows: [], section: @sections.last }
×
222
          # fill the header
223
          cells = line.split(/\s*\|\s*/)
×
224
          cells.shift if line.match(/^\|/) # remove leading pipe
×
225
          current_table[:header] = cells.map { |cell_title| cell_title.sub(/^!\s*/, '') }
×
226
          heading = cells.map do |cell|
×
227
            if cell.match(/^!/)
×
228
              "<th class=\"bang\">#{cell.sub(/^!\s*/, '')}</th>"
×
229
            else
×
230
              "<th>#{cell}</th>"
×
231
            end
232
          end.join(' ')
233
          new_lines << "<table class=\"tabular\">\n<thead>\n<tr>#{heading}</tr></thead>"
×
234
        else
235
          # no current table, no table contents -- NO-OP
100✔
236
          new_lines << line
100✔
237
        end
238
      else
239
        # this is either an end or a separator
×
240
        if line.match(SEPARATOR)
×
241
          # NO-OP
×
242
        elsif line.match(ROW)
×
243
          # remove leading and trailing delimiters
×
244
          clean_line=line.chomp.sub(/^\s*\|/, '').sub(/\|\s*$/, '')
×
245
          # fill the row
246
          cells = clean_line.split(/\s*\|\s*/, -1) # -1 means "don't prune empty values at the end"
×
247
          current_table[:rows] << cells
×
248
          rowline = ''
×
249
          cells.each_with_index do |cell, _i|
×
250
            rowline += "<td>#{cell}</td> "
×
251
          end
252

253
          if current_table[:rows].size == 1
×
254
            new_lines << '<tbody>'
×
255
          end
256
          new_lines << "<tr>#{rowline}</tr>"
×
257
        else
258
          # finished the last row
×
259
          unless current_table[:rows].empty? # only process tables with bodies
×
260
            @tables << current_table
×
261
            new_lines << '</tbody>'
×
262
          end
263
          new_lines << '</table><lb/>'
×
264
          current_table = nil
×
265
        end
266
      end
267
    end
268

269
    if current_table
79✔
270
      # unclosed table
×
271
      @tables << current_table
×
272
      unless current_table[:rows].empty? # only process tables with bodies
×
273
        @tables << current_table
×
274
        new_lines << '</tbody>'
×
275
      end
276
      new_lines << '</table><lb/>'
×
277
    end
278
    # do something with the table data
279
    new_lines.join(' ')
79✔
280
  end
281

282
  def process_any_sections(line)
1✔
283
    6.downto(2) do |depth|
100✔
284
      line.scan(/(={#{depth}}([^=]+)={#{depth}})/).each do |section_match|
500✔
285
        wiki_title = section_match[1].strip
×
286
        if wiki_title.length > 0
×
287
          verbatim = XmlSourceProcessor.cell_to_plaintext(wiki_title)
×
288
          safe_verbatim = verbatim.gsub(/"/, '&quot;')
×
289
          line = line.sub(section_match.first, "<entryHeading title=\"#{safe_verbatim}\" depth=\"#{depth}\" >#{wiki_title}</entryHeading>")
×
290
          @sections << Section.new(title: wiki_title, depth: depth)
×
291
        end
292
      end
293
    end
294

295
    line
100✔
296
  end
297

298
  def postprocess_sections
1✔
299
    @sections.each do |section|
79✔
300
      doc = XmlSourceProcessor.cell_to_xml(section.title)
×
301
      doc.elements.each('//link') do |e|
×
302
        title = e.attributes['target_title']
×
303
        article = collection.articles.where(title: title).first
×
304
        if article
×
305
          e.add_attribute('target_id', article.id.to_s)
×
306
        end
307
      end
308
      section.title = XmlSourceProcessor.xml_to_cell(doc)
×
309
    end
310
  end
311

312

313
  def canonicalize_title(title)
1✔
314
    # kill all tags
315
    title = title.gsub(/<.*?>/, '')
30✔
316
    # linebreaks -> spaces
317
    title = title.gsub(/\n/, ' ')
30✔
318
    # multiple spaces -> single spaces
319
    title = title.gsub(/\s+/, ' ')
30✔
320
    # change double quotes to proper xml
321
    title = title.gsub(/\"/, '&quot;')
30✔
322
    title
30✔
323
  end
324

325
  # transformations converting source mode transcription to xml
326
  def process_line_breaks(text)
1✔
327
    text="<p>#{text}</p>"
79✔
328
    text = text.gsub(/\s*\n\s*\n\s*/, '</p><p>')
79✔
329
    text = text.gsub(/([[:word:]]+)-\r\n\s*/, '\1<lb break="no" />')
79✔
330
    text = text.gsub(/\r\n\s*/, '<lb/>')
79✔
331
    text = text.gsub(/([[:word:]]+)-\n\s*/, '\1<lb break="no" />')
79✔
332
    text = text.gsub(/\n\s*/, '<lb/>')
79✔
333
    text = text.gsub(/([[:word:]]+)-\r\s*/, '\1<lb break="no" />')
79✔
334
    text = text.gsub(/\r\s*/, '<lb/>')
79✔
335
    text
79✔
336
  end
337

338
  def valid_xml_from_source(source)
1✔
339
    source = source || ''
79✔
340
    safe = source.gsub /\&/, '&amp;'
79✔
341
    safe.gsub! /\&amp;amp;/, '&amp;'
79✔
342
    safe.gsub! /[^\u0009\u000A\u000D\u0020-\uD7FF\uE000-\uFFFD\u10000-\u10FFFF]/, ' '
79✔
343

344
    string = <<EOF
79✔
345
    <?xml version="1.0" encoding="UTF-8"?>
346
      <page>
347
        #{safe}
348
      </page>
349
EOF
350
  end
351

352
  def update_links_and_xml(xml_string, preview_mode = false, text_type)
1✔
353
    # first clear out the existing links
354
    # log the count of articles before and after
355
    clear_links(text_type) unless preview_mode
79!
356

357
    candidate_articles = collection.articles.left_joins(:article_versions)
79✔
358
    page_update_timestamp = 1.hour.ago
79✔
359

360
    processed = ''
79✔
361
    # process it
362
    doc = REXML::Document.new xml_string
79✔
363
    doc.elements.each('//link') do |element|
79✔
364
      # default the title to the text if it's not specified
365
      if !(title = element.attributes['target_title'])
30!
366
        title = element.text
×
367
      end
368
      # display_text = element.text
369
      display_text = ''
30✔
370
      element.children.each do |e|
30✔
371
        display_text += e.to_s
30✔
372
      end
373
      debug("link display_text = #{display_text}")
30✔
374
      # change the xml version of quotes back to double quotes for article title
375
      title = title.gsub('&quot;', '"')
30✔
376

377
      article = candidate_articles.find_by(title: title)
30✔
378

379
      if article.nil?
30✔
380
        article = candidate_articles.where('article_versions.title': title)
9✔
381
                                    .where('article_versions.created_on > ?', page_update_timestamp)
382
                                    .first
383
        if article.present?
9✔
384
          display_text = article.title
1✔
385
          title = article.title
1✔
386
        end
387
      end
388

389
      # create new blank articles if they don't exist already
390
      if article.nil?
30✔
391
        article = Article.new
8✔
392
        article.title = title
8✔
393
        article.collection = collection
8✔
394
        article.created_by_id = Current.user.id if Current.user.present?
8✔
395
        article.save! unless preview_mode
8!
396
      end
397

398
      link_id = create_link(article, display_text, text_type) unless preview_mode
30!
399
      # now update the attribute
400
      link_element = REXML::Element.new('link')
30✔
401
      element.children.each { |c| link_element.add(c) }
60✔
402
      link_element.add_attribute('target_title', title)
30✔
403
      debug('element='+link_element.inspect)
30✔
404
      debug('article='+article.inspect)
30✔
405
      link_element.add_attribute('target_id', article.id.to_s) unless preview_mode
30!
406
      link_element.add_attribute('link_id', link_id.to_s) unless preview_mode
30!
407
      element.replace_with(link_element)
30✔
408
    end
409
    doc.write(processed)
79✔
410
    processed
79✔
411
  end
412

413
  # handle XML-dependent post-processing
414
  def postprocess_xml_markup(xml_string)
1✔
415
    doc = REXML::Document.new xml_string
79✔
416
    processed = ''
79✔
417
    doc.elements.each('//lb') do |element|
79✔
418
      if element.previous_element && element.previous_sibling.node_type == :element && element.previous_element.name == 'lb'
6!
419
        pre = doc.to_s
×
420
        element.parent.elements.delete(element)
×
421
      end
422
    end
423
    doc.write(processed)
79✔
424
    processed
79✔
425
  end
426

427

428
  CELL_PREFIX = "<?xml version='1.0' encoding='UTF-8'?><cell>"
1✔
429
  CELL_SUFFIX = '</cell>'
1✔
430

431
  def self.cell_to_xml(cell)
1✔
432
    REXML::Document.new(CELL_PREFIX + cell.gsub('&', '&amp;') + CELL_SUFFIX)
3✔
433
  end
434

435
  def self.xml_to_cell(doc)
1✔
436
    text = ''
×
437
    doc.write(text)
×
438
    text.sub(CELL_PREFIX, '').sub(CELL_SUFFIX, '')
×
439
  end
440

441
  def self.cell_to_plaintext(cell)
1✔
442
    doc = cell_to_xml(cell)
3✔
443
    doc.each_element('.//text()') { |e| p e.text }.join
3✔
444
  end
445

446
  def self.cell_to_subject(cell)
1✔
447
    doc = cell_to_xml(cell)
×
448
    subjects = ''
×
449
    doc.elements.each('//link') do |e|
×
450
      title = e.attributes['target_title']
×
451
      subjects << title
×
452
      subjects << "\n"
×
453
    end
454
    subjects
×
455
  end
456

457
  def self.cell_to_category(cell)
1✔
458
    doc = cell_to_xml(cell)
×
459
    categories = ''
×
460
    doc.elements.each('//link') do |e|
×
461
      id = e.attributes['target_id']
×
462
      if id
×
463
        article = Article.find(id)
×
464
        article.categories.each do |category|
×
465
          categories << category.title
×
466
          categories << "\n"
×
467
        end
468
      end
469
    end
470
    categories
×
471
  end
472

473
  ##############################################
474
  # Code to rename links within the text.
475
  # This assumes that the name change has already
476
  # taken place within the article table in the DB
477
  ##############################################
478
  def rename_article_links(old_title, new_title)
1✔
479
    title_regex =
480
      Regexp.escape(old_title)
13✔
481
        .gsub('\\ ', ' ') # Regexp.escape converts ' ' to '\\ ' for some reason -- undo this
482
        .gsub(/\s+/, '\s+') # convert multiple whitespaces into 1+n space characters
483

484
    self.source_text = rename_link_in_text(source_text, title_regex, new_title)
13✔
485

486
    # Articles don't have translations, but we still need to update pages.source_translation
487
    if has_attribute?(:source_translation) && !source_translation.nil?
13✔
488
      self.source_translation = rename_link_in_text(source_translation, title_regex, new_title)
5✔
489
    end
490
  end
491

492
  def rename_link_in_text(text, title_regex, new_title)
1✔
493
    if new_title == ''
18✔
494
      # Link deleted, remove [[ ]] but keep the original title text
495

496
      # Handle links of the form [[Old Title|Display Text]] => Display Text
3✔
497
      text = text.gsub(/\[\[#{title_regex}\|([^\]]+)\]\]/i, '\1')
3✔
498
      # Handle links of the form [[Old Title]] => Old Title
499
      text = text.gsub(/\[\[(#{title_regex})\]\]/i, '\1')
3✔
500
    else
501
      # Replace the title part in [[Old Title|Display Text]]
15✔
502
      text = text.gsub(/\[\[#{title_regex}\|/i, "[[#{new_title}|")
15✔
503
      # Replace [[Old Title]] with [[New Title|Old Title]]
504
      text = text.gsub(/\[\[(#{title_regex})\]\]/i, "[[#{new_title}|\\1]]")
15✔
505
    end
506

507
    text
18✔
508
  end
509

510

511
  def pipe_tables_formatting(text)
1✔
512
    # since Pandoc Pipe Tables extension requires pipe characters at the beginning and end of each line we must add them
513
    # to the beginning and end of each line
514
    text.split("\n").map { |line| "|#{line}|" }.join("\n")
10✔
515
  end
516

517
  def xml_table_to_markdown_table(table_element, pandoc_format = false, plaintext_export = false)
1✔
518
    text_table = ''
12✔
519

520
    # clean up in-cell line-breaks
521
    table_element.xpath('//lb').each { |n| n.replace(' ') }
50✔
522

523
    # Sanitize single quotes with backticks
524
    # table_element.xpath('//*').each { |n| n.content.gsub("'", '`') }
525

526
    # calculate the widths of each column based on max(header, cell[0...end])
527
    column_count = ([ table_element.xpath('//th').count ] + table_element.xpath('//tr').map { |e| e.xpath('td').count }).max
36✔
528
    column_widths = {}
12✔
529
    1.upto(column_count) do |column_index|
12✔
530
      longest_cell = (table_element.xpath("//tr/td[position()=#{column_index}]").map { |e| e.text().length }.max || 0)
72✔
531
      corresponding_heading = heading_length = table_element.xpath("//th[position()=#{column_index}]").first
36✔
532
      heading_length = corresponding_heading.nil? ? 0 : corresponding_heading.text().length
36!
533
      column_widths[column_index] = [ longest_cell, heading_length ].max
36✔
534
    end
535

536
    # print the header as markdown
537
    cell_strings = []
12✔
538
    table_element.xpath('//th').each_with_index do |e, i|
12✔
539
      cell_strings << e.text.rjust(column_widths[i+1], ' ')
36✔
540
    end
541
    text_table << cell_strings.join(' | ') << "\n"
12✔
542

543
    # print the separator
544
    text_table << column_count.times.map { |i| ''.rjust(column_widths[i+1], '-') }.join(' | ') << "\n"
48✔
545

546
    # print each row as markdown
547
    table_element.xpath('//tr').each do |row_element|
12✔
548
      text_table << row_element.xpath('td').map do |e|
24✔
549
        width = 80 # default for hand-coded tables
36✔
550
        index = e.path.match(/.*td\[(\d+)\]/)
36✔
551
        if index
36✔
552
          width = column_widths[index[1].to_i] || 80
36✔
553
        else
×
554
          width = column_widths.values.first
×
555
        end
556

557
        if plaintext_export
36✔
558
          e.text.rjust(width, ' ')
36✔
559
        else
×
560
          inner_html = xml_to_pandoc_md(e.to_s.gsub("'", '&#39;'), false, false, nil, false).gsub("\n", '')
×
561
          inner_html.rjust(width, ' ')
×
562
        end
563
      end.join(' | ') << "\n"
564
    end
565
    if pandoc_format
12✔
566
      text_table = pipe_tables_formatting(text_table)
2✔
567
    end
568

569
    "#{text_table}\n\n"
12✔
570
  end
571

572

573

574
  def debug(msg)
1✔
575
    logger.debug("DEBUG: #{msg}")
586✔
576
  end
577
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc