markdown.rb 8.11 KB
Newer Older
1 2
require 'html/pipeline'

3
module Gitlab
4
  # Custom parser for GitLab-flavored Markdown
5
  #
6
  # See the files in `lib/gitlab/markdown/` for specific processing information.
7
  module Markdown
8 9
    # Convert a Markdown String into an HTML-safe String of HTML
    #
10 11 12 13 14 15 16 17
    # Note that while the returned HTML will have been sanitized of dangerous
    # HTML, it may post a risk of information leakage if it's not also passed
    # through `post_process`.
    #
    # Also note that the returned String is always HTML, not XHTML. Views
    # requiring XHTML, such as Atom feeds, need to call `post_process` on the
    # result, providing the appropriate `pipeline` option.
    #
18 19 20 21
    # markdown - Markdown String
    # context  - Hash of context options passed to our HTML Pipeline
    #
    # Returns an HTML-safe String
22
    def self.render(text, context = {})
23
      cache_key = context.delete(:cache_key)
24
      cache_key = full_cache_key(cache_key, context[:pipeline])
25

26 27 28 29 30 31 32
      if cache_key
        Rails.cache.fetch(cache_key) do
          cacheless_render(text, context)
        end
      else
        cacheless_render(text, context)
      end
33 34
    end

35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
    def self.render_result(text, context = {})
      pipeline = context[:pipeline] || :full

      html_pipeline = html_pipelines[pipeline]

      transformers = context_transformers[pipeline]
      context = transformers.reduce(context) { |context, transformer| transformer.call(context) }

      html_pipeline.call(text, context)
    end

    def self.cached?(cache_key, pipeline: :full)
      cache_key = full_cache_key(cache_key, pipeline)
      cache_key ? Rails.cache.exist?(cache_key) : false
    end
50

51 52 53 54 55
    # Perform post-processing on an HTML String
    #
    # This method is used to perform state-dependent changes to a String of
    # HTML, such as removing references that the current user doesn't have
    # permission to make (`RedactorFilter`).
56
    #
57 58 59 60 61
    # html     - String to process
    # context  - Hash of options to customize output
    #            :pipeline  - Symbol pipeline type
    #            :project   - Project
    #            :user      - User object
62 63
    #
    # Returns an HTML-safe String
64
    def self.post_process(html, context)
65
      html_pipeline = html_pipelines[:post_process]
skv's avatar
skv committed
66

67
      if context[:xhtml]
68
        html_pipeline.to_document(html, context).to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML)
69
      else
70
        html_pipeline.to_html(html, context)
71
      end.html_safe
72 73
    end

74
    private
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94

    # Provide autoload paths for filters to prevent a circular dependency error
    autoload :AutolinkFilter,               'gitlab/markdown/autolink_filter'
    autoload :CommitRangeReferenceFilter,   'gitlab/markdown/commit_range_reference_filter'
    autoload :CommitReferenceFilter,        'gitlab/markdown/commit_reference_filter'
    autoload :EmojiFilter,                  'gitlab/markdown/emoji_filter'
    autoload :ExternalIssueReferenceFilter, 'gitlab/markdown/external_issue_reference_filter'
    autoload :ExternalLinkFilter,           'gitlab/markdown/external_link_filter'
    autoload :IssueReferenceFilter,         'gitlab/markdown/issue_reference_filter'
    autoload :LabelReferenceFilter,         'gitlab/markdown/label_reference_filter'
    autoload :MarkdownFilter,               'gitlab/markdown/markdown_filter'
    autoload :MergeRequestReferenceFilter,  'gitlab/markdown/merge_request_reference_filter'
    autoload :RedactorFilter,               'gitlab/markdown/redactor_filter'
    autoload :RelativeLinkFilter,           'gitlab/markdown/relative_link_filter'
    autoload :SanitizationFilter,           'gitlab/markdown/sanitization_filter'
    autoload :SnippetReferenceFilter,       'gitlab/markdown/snippet_reference_filter'
    autoload :SyntaxHighlightFilter,        'gitlab/markdown/syntax_highlight_filter'
    autoload :TableOfContentsFilter,        'gitlab/markdown/table_of_contents_filter'
    autoload :TaskListFilter,               'gitlab/markdown/task_list_filter'
    autoload :UserReferenceFilter,          'gitlab/markdown/user_reference_filter'
95
    autoload :UploadLinkFilter,             'gitlab/markdown/upload_link_filter'
96 97 98

    def self.gfm_filters
      @gfm_filters ||= [
99
        Gitlab::Markdown::SyntaxHighlightFilter,
100
        Gitlab::Markdown::SanitizationFilter,
101

102
        Gitlab::Markdown::UploadLinkFilter,
103
        Gitlab::Markdown::EmojiFilter,
104
        Gitlab::Markdown::TableOfContentsFilter,
105
        Gitlab::Markdown::AutolinkFilter,
106
        Gitlab::Markdown::ExternalLinkFilter,
107

108 109 110 111 112 113 114
        Gitlab::Markdown::UserReferenceFilter,
        Gitlab::Markdown::IssueReferenceFilter,
        Gitlab::Markdown::ExternalIssueReferenceFilter,
        Gitlab::Markdown::MergeRequestReferenceFilter,
        Gitlab::Markdown::SnippetReferenceFilter,
        Gitlab::Markdown::CommitRangeReferenceFilter,
        Gitlab::Markdown::CommitReferenceFilter,
115 116
        Gitlab::Markdown::LabelReferenceFilter,

117
        Gitlab::Markdown::TaskListFilter
118 119
      ]
    end
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
    def self.all_filters
      @all_filters ||= {
        plain_markdown: [
          Gitlab::Markdown::MarkdownFilter
        ],
        gfm: gfm_filters,

        full:           [:plain_markdown, :gfm],
        atom:           :full,
        email:          :full,
        description:    :full,
        single_line:    :gfm,

        asciidoc: [
          Gitlab::Markdown::RelativeLinkFilter
        ],

        post_process: [
          Gitlab::Markdown::RelativeLinkFilter, 
          Gitlab::Markdown::RedactorFilter
        ],

        reference_extraction: [
          Gitlab::Markdown::ReferenceGathererFilter
        ]
      }
    end
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
    def self.all_context_transformers
      @all_context_transformers ||= {
        gfm: {
          only_path: true,

          # EmojiFilter
          asset_host: Gitlab::Application.config.asset_host,
          asset_root: Gitlab.config.gitlab.base_url
        },
        full: :gfm,

        atom: [
          :full, 
          { 
            only_path: false, 
            xhtml: true 
          }
        ],
        email: [
          :full,
          { 
            only_path: false
          }
        ],
        description: [
          :full,
          { 
            # SanitizationFilter
            inline_sanitization: true
          }
        ],
        single_line: :gfm,

        post_process: {
          post_process: true
184 185
        }
      }
186 187 188 189 190 191 192 193 194
    end

    def self.html_filters
      @html_filters ||= Hash.new do |hash, pipeline|
        filters = get_filters(pipeline)
        hash[pipeline] = filters if pipeline.is_a?(Symbol)
        filters
      end
    end
195 196 197 198

    def self.html_pipelines
      @html_pipelines ||= Hash.new do |hash, pipeline|
        filters = get_filters(pipeline)
199 200 201
        html_pipeline = HTML::Pipeline.new(filters)
        hash[pipeline] = html_pipeline if pipeline.is_a?(Symbol)
        html_pipeline
202 203 204
      end
    end

205 206 207 208 209 210
    def self.context_transformers
      @context_transformers ||= Hash.new do |hash, pipeline|
        transformers = get_context_transformers(pipeline)
        hash[pipeline] = transformers if pipeline.is_a?(Symbol)
        transformers
      end
211 212
    end

213 214 215 216 217 218
    def self.get_filters(pipelines)
      Array.wrap(pipelines).flat_map do |pipeline|
        case pipeline
        when Class
          pipeline
        when Symbol
219
          html_filters[all_filters[pipeline]]
220
        when Array
221
          html_filters[pipeline]
222 223 224 225 226 227 228 229 230 231 232 233
        end
      end.compact
    end

    def self.get_context_transformers(pipelines)
      Array.wrap(pipelines).flat_map do |pipeline|
        case pipeline
        when Hash
          ->(context) { context.merge(pipeline) }
        when Proc
          pipeline
        when Symbol
234
          context_transformers[all_context_transformers[pipeline]]
235
        when Array
236
          context_transformers[pipeline]
237 238
        end
      end.compact
239
    end
240

241 242 243 244 245 246 247 248 249 250
    def self.cacheless_render(text, context = {})
      result = render_result(text, context)
      output = result[:output]
      if output.respond_to?(:to_html)
        output.to_html
      else
        output.to_s
      end
    end

251
    def self.full_cache_key(cache_key, pipeline = :full)
252 253
      return unless cache_key && pipeline.is_a?(Symbol)

254 255
      ["markdown", *cache_key, pipeline]
    end
256 257
  end
end