Class: Kramdown::Parser::AtlassianDocumentFormat

Inherits:
Base
  • Object
show all
Defined in:
lib/kramdown/parser/atlassian_document_format.rb

Overview

Parses an Atlassian Document Format (ADF) json into a Kramdown AST tree, for conversion to another format. The primary goal is to convert in GitLab Markdown.

This parser does NOT resolve external resources, such as media/attachments. A special url is generated for media based on the id, for example

![jira-10050-field-description](adf-media://79411c6b-50e0-477f-b4ed-ac3a5887750c)

so that a later filter/process can resolve those.

Constant Summary collapse

TOP_LEVEL_BLOCK_NODES =
%w[blockquote
bulletList
codeBlock
heading
mediaGroup
mediaSingle
orderedList
panel
paragraph
rule
table].freeze
CHILD_BLOCK_NODES =
%w[listItem
media
table_cell
table_header
table_row].freeze
INLINE_NODES =
%w[emoji
hardBreak
inlineCard
mention
text].freeze
MARKS =
%w[code
em
link
strike
strong
subsup
textColor
underline].freeze
TABLE_CELL_NODES =
%w[blockquote
bulletList
codeBlock
heading
mediaGroup
orderedList
panel
paragraph
rule].freeze
LIST_ITEM_NODES =
%w[bulletList
codeBlock
mediaSingle
orderedList
paragraph].freeze
PANEL_NODES =
%w[bulletList
heading
orderedList
paragraph].freeze
PANEL_EMOJIS =
{ info: ':information_source:',
note: ':notepad_spiral:',
warning: ':warning:',
success: ':white_check_mark:',
error: ':octagonal_sign:' }.freeze
CODE_BLOCK_DEFAULT_LANGUAGE =

The default language for code blocks is ‘java`, as indicated in You can’t change the default in Jira. There was a comment that indicated Confluence can set the default language.

'java'

Instance Method Summary collapse

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method, *args) ⇒ Object (private)

Raises:

  • (NotImplementedError)


376
377
378
# File 'lib/kramdown/parser/atlassian_document_format.rb', line 376

def method_missing(method, *args)
  raise NotImplementedError, "method `#{method}` not implemented yet"
end

Instance Method Details

#parseObject



87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/kramdown/parser/atlassian_document_format.rb', line 87

def parse
  ast = Gitlab::Json.parse(@source)

  validate_document(ast)

  process_content(@root, ast, TOP_LEVEL_BLOCK_NODES)
rescue ::JSON::ParserError => e
  msg = 'Invalid Atlassian Document Format JSON'
  Gitlab::AppLogger.error msg
  Gitlab::AppLogger.error e

  raise ::Kramdown::Error, msg
end

#process_blockquote(element, ast_node) ⇒ Object



109
110
111
112
113
114
# File 'lib/kramdown/parser/atlassian_document_format.rb', line 109

def process_blockquote(element, ast_node)
  new_element = Element.new(:blockquote)
  element.children << new_element

  process_content(new_element, ast_node, TOP_LEVEL_BLOCK_NODES)
end

#process_bullet_list(element, ast_node) ⇒ Object



116
117
118
119
120
121
# File 'lib/kramdown/parser/atlassian_document_format.rb', line 116

def process_bullet_list(element, ast_node)
  new_element = Element.new(:ul)
  element.children << new_element

  process_content(new_element, ast_node, %w[listItem])
end

#process_code_block(element, ast_node) ⇒ Object



123
124
125
126
127
128
# File 'lib/kramdown/parser/atlassian_document_format.rb', line 123

def process_code_block(element, ast_node)
  code_text = gather_text(ast_node)
  lang = ast_node.dig('attrs', 'language') || CODE_BLOCK_DEFAULT_LANGUAGE

  element.children << Element.new(:codeblock, code_text, {}, { lang: lang })
end

#process_content(element, ast_node, allowed_types) ⇒ Object



101
102
103
104
105
106
107
# File 'lib/kramdown/parser/atlassian_document_format.rb', line 101

def process_content(element, ast_node, allowed_types)
  ast_node['content'].each do |node|
    next unless allowed_types.include?(node['type'])

    public_send("process_#{node['type'].underscore}", element, node) # rubocop:disable GitlabSecurity/PublicSend
  end
end

#process_emoji(element, ast_node) ⇒ Object



130
131
132
133
134
135
# File 'lib/kramdown/parser/atlassian_document_format.rb', line 130

def process_emoji(element, ast_node)
  emoji = ast_node.dig('attrs', 'text') || ast_node.dig('attrs', 'shortName')
  return unless emoji

  add_text(emoji, element, :text)
end

#process_hard_break(element, ast_node) ⇒ Object



137
138
139
# File 'lib/kramdown/parser/atlassian_document_format.rb', line 137

def process_hard_break(element, ast_node)
  element.children << Element.new(:br)
end

#process_heading(element, ast_node) ⇒ Object



141
142
143
144
145
146
147
148
149
# File 'lib/kramdown/parser/atlassian_document_format.rb', line 141

def process_heading(element, ast_node)
  level = ast_node.dig('attrs', 'level').to_i.clamp(1, 6)
  options = { level: level }
  new_element = Element.new(:header, nil, nil, options)
  element.children << new_element

  process_content(new_element, ast_node, INLINE_NODES)
  extract_element_text(new_element, new_element.options[:raw_text] = +'')
end

#process_inline_card(element, ast_node) ⇒ Object



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/kramdown/parser/atlassian_document_format.rb', line 151

def process_inline_card(element, ast_node)
  url  = ast_node.dig('attrs', 'url')
  data = ast_node.dig('attrs', 'data')

  if url
    # we don't pull a description from the link and create a panel,
    # just convert to a normal link
    new_element = Element.new(:text, url)
    element.children << wrap_element(new_element, :a, nil, { 'href' => url })
  elsif data
    # data is JSONLD (https://json-ld.org/), so for now output
    # as a codespan of text, with `adf-inlineCard: ` at the start
    text = "adf-inlineCard: #{data}"
    element.children << Element.new(:codespan, text, nil, { lang: 'adf-inlinecard' })
  end
end

#process_list_item(element, ast_node) ⇒ Object



168
169
170
171
172
173
# File 'lib/kramdown/parser/atlassian_document_format.rb', line 168

def process_list_item(element, ast_node)
  new_element = Element.new(:li)
  element.children << new_element

  process_content(new_element, ast_node, LIST_ITEM_NODES)
end

#process_media(element, ast_node) ⇒ Object



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/kramdown/parser/atlassian_document_format.rb', line 175

def process_media(element, ast_node)
  media_url = "adf-media://#{ast_node['attrs']['id']}"

  case ast_node['attrs']['type']
  when 'file'
    attrs = { 'src' => media_url, 'alt' => ast_node['attrs']['collection'] }
    media_element = Element.new(:img, nil, attrs)
  when 'link'
    attrs = { 'href' => media_url }
    media_element = wrap_element(Element.new(:text, media_url), :a, nil, attrs)
  end

  media_element = wrap_element(media_element, :p)
  element.children << media_element
end

#process_media_group(element, ast_node) ⇒ Object

wraps a group media element. Currently ignore attrs.layout and attrs.width



202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/kramdown/parser/atlassian_document_format.rb', line 202

def process_media_group(element, ast_node)
  ul_element = Element.new(:ul)
  element.children << ul_element

  ast_node['content'].each do |node|
    next unless node['type'] == 'media'

    li_element = Element.new(:li)
    ul_element.children << li_element

    process_media(li_element, node)
  end
end

#process_media_single(element, ast_node) ⇒ Object

wraps a single media element. Currently ignore attrs.layout and attrs.width



193
194
195
196
197
198
# File 'lib/kramdown/parser/atlassian_document_format.rb', line 193

def process_media_single(element, ast_node)
  new_element = Element.new(:p)
  element.children << new_element

  process_content(new_element, ast_node, %w[media])
end

#process_mention(element, ast_node) ⇒ Object



216
217
218
219
220
221
222
223
224
225
226
# File 'lib/kramdown/parser/atlassian_document_format.rb', line 216

def process_mention(element, ast_node)
  # Make it `@adf-mention:` since there is no guarantee that it is
  # a valid username in our system.  This gives us an
  # opportunity to replace it later. Mention name can have
  # spaces, so double quote it
  mention_text = ast_node.dig('attrs', 'text')&.gsub('@', '')
  mention_text = %("#{mention_text}") if mention_text&.include?(' ')
  mention_text = %(@adf-mention:#{mention_text})

  add_text(mention_text, element, :text)
end

#process_ordered_list(element, ast_node) ⇒ Object



228
229
230
231
232
233
234
# File 'lib/kramdown/parser/atlassian_document_format.rb', line 228

def process_ordered_list(element, ast_node)
  # `attrs.order` is not supported in the Kramdown AST
  new_element = Element.new(:ol)
  element.children << new_element

  process_content(new_element, ast_node, %w[listItem])
end

#process_panel(element, ast_node) ⇒ Object

since we don’t have something similar, then put <hr> around it and add a bolded status text (eg: “Error:”) to the front of it.



238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/kramdown/parser/atlassian_document_format.rb', line 238

def process_panel(element, ast_node)
  panel_type = ast_node.dig('attrs', 'panelType')
  return unless %w[info note warning success error].include?(panel_type)

  panel_header_text = "#{PANEL_EMOJIS[panel_type.to_sym]} "
  panel_header_element = Element.new(:text, panel_header_text)

  new_element = Element.new(:blockquote)
  new_element.children << panel_header_element
  element.children << new_element

  process_content(new_element, ast_node, PANEL_NODES)
end

#process_paragraph(element, ast_node) ⇒ Object



252
253
254
255
256
257
# File 'lib/kramdown/parser/atlassian_document_format.rb', line 252

def process_paragraph(element, ast_node)
  new_element = Element.new(:p)
  element.children << new_element

  process_content(new_element, ast_node, INLINE_NODES)
end

#process_rule(element, ast_node) ⇒ Object



259
260
261
# File 'lib/kramdown/parser/atlassian_document_format.rb', line 259

def process_rule(element, ast_node)
  element.children << Element.new(:hr)
end

#process_table(element, ast_node) ⇒ Object



263
264
265
266
267
268
269
270
271
# File 'lib/kramdown/parser/atlassian_document_format.rb', line 263

def process_table(element, ast_node)
  table = Element.new(:table, nil, nil, { alignment: [:default, :default] })
  element.children << table

  tbody = Element.new(:tbody)
  table.children << tbody

  process_content(tbody, ast_node, %w[tableRow])
end

#process_table_cell(element, ast_node) ⇒ Object

we ignore the attributes, attrs.background, attrs.colspan, attrs.colwidth, and attrs.rowspan



275
276
277
278
279
280
# File 'lib/kramdown/parser/atlassian_document_format.rb', line 275

def process_table_cell(element, ast_node)
  new_element = Element.new(:td)
  element.children << new_element

  process_content(new_element, ast_node, TABLE_CELL_NODES)
end

#process_table_header(element, ast_node) ⇒ Object

we ignore the attributes, attrs.background, attrs.colspan, attrs.colwidth, and attrs.rowspan



284
285
286
287
288
289
# File 'lib/kramdown/parser/atlassian_document_format.rb', line 284

def process_table_header(element, ast_node)
  new_element = Element.new(:th)
  element.children << new_element

  process_content(new_element, ast_node, TABLE_CELL_NODES)
end

#process_table_row(element, ast_node) ⇒ Object



291
292
293
294
295
296
# File 'lib/kramdown/parser/atlassian_document_format.rb', line 291

def process_table_row(element, ast_node)
  new_element = Element.new(:tr)
  element.children << new_element

  process_content(new_element, ast_node, %w[tableHeader tableCell])
end

#process_text(element, ast_node) ⇒ Object



298
299
300
301
302
# File 'lib/kramdown/parser/atlassian_document_format.rb', line 298

def process_text(element, ast_node)
  new_element = Element.new(:text, ast_node['text'])
  new_element = apply_marks(new_element, ast_node, MARKS)
  element.children << new_element
end