summaryrefslogtreecommitdiff
path: root/lib/rouge/formatters/html_gitlab.rb
blob: 50b1db88c0e9922c29451ed0df8e054341d7a80a (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
require 'cgi'

module Rouge
  module Formatters
    class HTMLGitlab < Rouge::Formatter
      tag 'html_gitlab'

      # Creates a new <tt>Rouge::Formatter::HTMLGitlab</tt> instance.
      #
      # [+nowrap+]          If set to True, don't wrap the output at all, not
      #                     even inside a <tt><pre></tt> tag (default: false).
      # [+cssclass+]        CSS class for the wrapping <tt><div></tt> tag
      #                     (default: 'highlight').
      # [+linenos+]         If set to 'table', output line numbers as a table
      #                     with two cells, one containing the line numbers,
      #                     the other the whole code. This is copy paste friendly,
      #                     but may cause alignment problems with some browsers
      #                     or fonts. If set to 'inline', the line numbers will
      #                     be integrated in the <tt><pre></tt> tag that contains
      #                     the code (default: nil).
      # [+linenostart+]     The line number for the first line (default: 1).
      # [+lineanchors+]     If set to true the formatter will wrap each output
      #                     line in an anchor tag with a name of L-linenumber.
      #                     This allows easy linking to certain lines
      #                     (default: false).
      # [+lineanchorsid+]   If lineanchors is true the name of the anchors can
      #                     be changed with lineanchorsid to e.g. foo-linenumber
      #                     (default: 'L').
      def initialize(
          nowrap: false,
          cssclass: 'highlight',
          linenos: nil,
          linenostart: 1,
          lineanchors: false,
          lineanchorsid: 'L'
      )
        @nowrap = nowrap
        @cssclass = cssclass
        @linenos = linenos
        @linenostart = linenostart
        @lineanchors = lineanchors
        @lineanchorsid = lineanchorsid
      end

      def render(tokens)
        case @linenos
        when 'table'
          render_tableized(tokens)
        when 'inline'
          render_untableized(tokens)
        else
          render_untableized(tokens)
        end
      end

      alias_method :format, :render

      private

      def render_untableized(tokens)
        data = process_tokens(tokens)

        html = ''
        html << "<pre class=\"#{@cssclass}\"><code>" unless @nowrap
        html << wrap_lines(data[:code])
        html << "</code></pre>\n" unless @nowrap
        html
      end

      def render_tableized(tokens)
        data = process_tokens(tokens)

        html = ''
        html << "<div class=\"#{@cssclass}\">" unless @nowrap
        html << '<table><tbody>'
        html << "<td class=\"linenos\"><pre>"
        html << wrap_linenos(data[:numbers])
        html << '</pre></td>'
        html << "<td class=\"lines\"><pre><code>"
        html << wrap_lines(data[:code])
        html << '</code></pre></td>'
        html << '</tbody></table>'
        html << '</div>' unless @nowrap
        html
      end

      def process_tokens(tokens)
        rendered = []
        current_line = ''

        tokens.each do |tok, val|
          # In the case of multi-line values (e.g. comments), we need to apply
          # styling to each line since span elements are inline.
          val.lines.each do |line|
            stripped = line.chomp
            current_line << span(tok, stripped)

            if line.end_with?("\n")
              rendered << current_line
              current_line = ''
            end
          end
        end

        # Add leftover text
        rendered << current_line if current_line.present?

        num_lines = rendered.size
        numbers = (@linenostart..num_lines + @linenostart - 1).to_a

        { numbers: numbers, code: rendered }
      end

      def wrap_linenos(numbers)
        numbers.join("\n")
      end

      def wrap_lines(lines)
        if @lineanchors
          lines = lines.each_with_index.map do |line, index|
            number = index + @linenostart

            if @linenos == 'inline'
              "<a name=\"L#{number}\"></a>" \
              "<span class=\"linenos\">#{number}</span>" \
              "<span id=\"#{@lineanchorsid}#{number}\" class=\"line\">#{line}" \
              '</span>'
            else
              "<span id=\"#{@lineanchorsid}#{number}\" class=\"line\">#{line}" \
              '</span>'
            end
          end
        elsif @linenos == 'inline'
          lines = lines.each_with_index.map do |line, index|
            number = index + @linenostart
            "<span class=\"linenos\">#{number}</span>#{line}"
          end
        end

        lines.join("\n")
      end

      def span(tok, val)
        # http://stackoverflow.com/a/1600584/2587286
        val = CGI.escapeHTML(val)

        if tok.shortname.empty?
          val
        else
          "<span class=\"#{tok.shortname}\">#{val}</span>"
        end
      end
    end
  end
end