Class: Aspera::Markdown

Inherits:
Object
  • Object
show all
Defined in:
lib/aspera/markdown.rb

Overview

Formatting for Markdown

Constant Summary collapse

FORMATS =

Matches: bold, code, or an HTML entity (&, ©, 💩)

/(?:\*\*(?<bold>[^*]+?)\*\*)|(?:`(?<code>[^`]+)`)|&(?<entity>(?:[A-Za-z][A-Za-z0-9]{1,31}|#\d{1,7}|#x[0-9A-Fa-f]{1,6}));/m
HTML_BREAK =
'<br/>'
COL_WIDTH =
80
HEADING_RE =

Extract the table of contents from a Markdown document.

Returns:

  • (Array<Hash>)

    array of { level, title, anchor }

/^(\#{1,6})\s+(.+)$/

Class Method Summary collapse

Class Method Details

.admonition(lines, type: 'INFO') ⇒ String

Generate a GitHub-flavoured admonition block

Parameters:

  • lines (Array<String>)

    lines of the admonition body

  • type (String) (defaults to: 'INFO')

    admonition type: NOTE, CAUTION, WARNING, IMPORTANT, TIP, INFO

Returns:

  • (String)

    markdown admonition block



112
113
114
# File 'lib/aspera/markdown.rb', line 112

def admonition(lines, type: 'INFO')
  "> [!#{type}]\n#{lines.map { |l| "> #{l}" }.join("\n")}\n\n"
end

.code(lines, type: 'shell') ⇒ String

Generate a fenced code block

Parameters:

  • lines (Array<String>)

    lines of code

  • type (String) (defaults to: 'shell')

    language identifier for syntax highlighting

Returns:

  • (String)

    markdown fenced code block



120
121
122
# File 'lib/aspera/markdown.rb', line 120

def code(lines, type: 'shell')
  "```#{type}\n#{lines.join("\n")}\n```\n\n"
end

.extract_section(content, anchor) ⇒ String?

Extract the content of a single section (heading + body until next heading of same/higher level).

Parameters:

  • content (String)

    full Markdown source

  • anchor (String)

    GitHub anchor slug (without #)

Returns:

  • (String, nil)

    the section content, or nil if not found



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/aspera/markdown.rb', line 55

def extract_section(content, anchor)
  seen = {}
  section_level = nil
  result = []
  content.each_line do |line|
    m = line.match(HEADING_RE)
    if m
      slug = heading_to_anchor(m[2].strip, seen: seen)
      if section_level.nil?
        # not yet found: check if this heading matches
        next unless slug == anchor
        section_level = m[1].length
      elsif m[1].length <= section_level
        # already in section: stop at same/higher level heading
        break
      end
    end
    result << line if section_level
  end
  result.empty? ? nil : result.join
end

.heading(title, level: 1) ⇒ String

Generate a markdown heading

Parameters:

  • title (String)

    heading text

  • level (Integer) (defaults to: 1)

    heading level (1–6)

Returns:

  • (String)

    markdown heading



104
105
106
# File 'lib/aspera/markdown.rb', line 104

def heading(title, level: 1)
  "#{'#' * level} #{title}\n\n"
end

.heading_to_anchor(text, seen: nil) ⇒ String

Convert a Markdown heading text to a GitHub-flavoured anchor. Rules: downcase, keep letters/digits/spaces/hyphens, replace spaces with hyphens. Duplicate anchors are disambiguated by appending -1, -2, … (pass a seen Hash to track).

Parameters:

  • text (String)

    raw heading text (without leading # and spaces)

  • seen (Hash{String=>Integer}, nil) (defaults to: nil)

    mutable counter; pass the same Hash across a document

Returns:

  • (String)

    anchor slug (without leading #)



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/aspera/markdown.rb', line 19

def heading_to_anchor(text, seen: nil)
  slug = text
    .downcase
    .gsub(/[`*_]/, '')           # strip inline code/bold/italic markers
    .gsub(/&[a-z]+;/, '')        # strip HTML entities
    .gsub(/[^\w\s-]/, '')        # keep word chars, spaces, hyphens
    .gsub(/\s+/, '-')            # spaces → hyphens
    .squeeze('-')                # collapse consecutive hyphens
    .strip
  if seen
    count = seen[slug].to_i
    seen[slug] = count + 1
    slug = "#{slug}-#{count}" if count > 0
  end
  slug
end

.icode(text) ⇒ String

Wrap text in inline code backticks

Parameters:

  • text (String)

    text to wrap

Returns:

  • (String)

    inline code span



127
128
129
# File 'lib/aspera/markdown.rb', line 127

def icode(text)
  "`#{text}`"
end

.list(items) ⇒ String

Generate markdown list from the provided list

Parameters:

  • items (Array<String>)

    list of items

Returns:

  • (String)

    markdown unordered list



96
97
98
# File 'lib/aspera/markdown.rb', line 96

def list(items)
  items.map { |i| "- #{i}" }.join("\n")
end

.paragraph(text) ⇒ String

Wrap text in a markdown paragraph (trailing blank line)

Parameters:

  • text (String)

    paragraph content

Returns:

  • (String)

    paragraph with trailing newlines



134
135
136
# File 'lib/aspera/markdown.rb', line 134

def paragraph(text)
  "#{text}\n\n"
end

.table(table) ⇒ String

Generate markdown from the provided 2D table

Parameters:

  • table (Array<Array<String>>)

    2D array of strings

Returns:



80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/aspera/markdown.rb', line 80

def table(table)
  # get max width of each columns
  col_widths = table.transpose.map do |col|
    [col.flat_map { |c| c.to_s.delete('`').split(HTML_BREAK).map(&:size) }.max, COL_WIDTH].min
  end
  headings = table.shift
  table.unshift(col_widths.map { |col_width| '-' * col_width })
  table.unshift(headings)
  lines = table.map { |line| "| #{line.map { |i| i.to_s.gsub('\\', '\\\\').gsub('|', '\|') }.join(' | ')} |\n" }
  lines[1] = lines[1].tr(' ', '-')
  return lines.join.chomp
end

.toc(content) ⇒ Object



41
42
43
44
45
46
47
48
49
# File 'lib/aspera/markdown.rb', line 41

def toc(content)
  seen = {}
  content.each_line.filter_map do |line|
    m = line.match(HEADING_RE)
    next unless m
    title = m[2].strip
    {level: m[1].length, title: title, anchor: heading_to_anchor(title, seen: seen)}
  end
end