kramdown-asciidoc 1.0.0.alpha.4 → 1.0.0.alpha.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +15 -0
  3. data/README.adoc +1 -1
  4. data/lib/kramdown-asciidoc.rb +1 -0
  5. data/lib/kramdown-asciidoc/converter.rb +297 -266
  6. data/lib/kramdown-asciidoc/version.rb +1 -1
  7. data/lib/kramdown-asciidoc/writer.rb +114 -0
  8. data/spec/scenarios/blockquote/deep-nested.adoc +3 -0
  9. data/spec/scenarios/codespan/constrained-triple.adoc +1 -0
  10. data/spec/scenarios/codespan/constrained-triple.md +1 -0
  11. data/spec/scenarios/dl/compound.adoc +11 -0
  12. data/spec/scenarios/{dlist → dl}/compound.md +0 -0
  13. data/spec/scenarios/dl/nested-mixed.adoc +11 -0
  14. data/spec/scenarios/{dlist → dl}/nested-mixed.md +0 -0
  15. data/spec/scenarios/dl/nested.adoc +15 -0
  16. data/spec/scenarios/{dlist → dl}/nested.md +0 -0
  17. data/spec/scenarios/dl/simple.adoc +9 -0
  18. data/spec/scenarios/dl/simple.md +10 -0
  19. data/spec/scenarios/entity/reverse.adoc +2 -0
  20. data/spec/scenarios/entity/reverse.md +2 -0
  21. data/spec/scenarios/p/admonition/in-blockquote.adoc +1 -0
  22. data/spec/scenarios/p/admonition/in-blockquote.md +1 -0
  23. data/spec/scenarios/p/admonition/in-compound-blockquote.adoc +5 -0
  24. data/spec/scenarios/p/admonition/in-compound-blockquote.md +3 -0
  25. data/spec/scenarios/p/admonition/plain.adoc +2 -0
  26. data/spec/scenarios/p/admonition/plain.md +2 -0
  27. data/spec/scenarios/strong/menu.adoc +1 -1
  28. data/spec/scenarios/table/alignment.adoc +7 -0
  29. data/spec/scenarios/table/alignment.md +4 -0
  30. data/spec/scenarios/text/nbsp.adoc +1 -0
  31. data/spec/scenarios/text/nbsp.md +1 -0
  32. data/spec/scenarios/xml_comment/line-adjacent-to-text.adoc +2 -0
  33. data/spec/scenarios/xml_comment/line-adjacent-to-text.md +1 -0
  34. metadata +39 -18
  35. data/spec/scenarios/dlist/compound.adoc +0 -13
  36. data/spec/scenarios/dlist/nested-mixed.adoc +0 -11
  37. data/spec/scenarios/dlist/nested.adoc +0 -23
  38. data/spec/scenarios/dlist/simple.adoc +0 -5
  39. data/spec/scenarios/dlist/simple.md +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 53feb28cc869ddb3f85b8806d347ca52198d57f723505c65bad3ebc20390fd67
4
- data.tar.gz: a33879646334f075770400e329ba801e9aa0865d36d2bcc98e6392edd5dd1b84
3
+ metadata.gz: 047f74e6cf78d83f0044f72410f8df7e5338800cf1f5796de4c919a0ff79ed36
4
+ data.tar.gz: ab08640754fff574b51a5ef5f0c400b493ff17f31d54163a33f4af5e115a64ab
5
5
  SHA512:
6
- metadata.gz: 6428d7de7c8aa6a729b4ccb14b825afcff1855f4b3ec8f9786469c020eb8694b284276ee41e7b9f0e19743e703ba6de1e6ac11c2a408a992e99d5f8a90a40355
7
- data.tar.gz: ff5cb27de8a2416771e40125015e2fe2fc8c5cdb60adbe8b4cad363761b13598c97a72b0fa75962181b92c8971ef29a7e0e77f5b4bb41b8712abd6d17bc6360d
6
+ metadata.gz: 3170271705bb355ad77d7aa262724f07e789947b4eaf4e0c3ee09fbf48409cce910c445011f825be92c53f90e30b17d9d2562e9c627a4b0b86ca764d05748331
7
+ data.tar.gz: 6412723030a019d5c039206c5f4ada159b4c6c75f728648a7161ad989cb9990c938e56be6e48986b5c28e80cbff738b522d8c82248de1a6b7564eca4307e9652
data/CHANGELOG.adoc CHANGED
@@ -5,6 +5,21 @@
5
5
  This document provides a high-level view of the changes to {project-name} by release.
6
6
  For a detailed view of what has changed, refer to the {uri-repo}/commits/master[commit history] on GitHub.
7
7
 
8
+ == 1.0.0.alpha.5 (2018-06-19) - @mojavelinux
9
+
10
+ === Added
11
+
12
+ * recognize Hint as admonition label; map to TIP
13
+ * replace no-break space with \{nbsp}
14
+
15
+ === Changed
16
+
17
+ * rewrite converter to use a structured writer
18
+ * remove blockquote enclosure around simple admonition block
19
+ * revert \& back to &
20
+ * use separate list level for dl
21
+ * fold description list item to one line if primary text is a single line
22
+
8
23
  == 1.0.0.alpha.4 (2018-06-12) - @mojavelinux
9
24
 
10
25
  === Added
data/README.adoc CHANGED
@@ -1,6 +1,6 @@
1
1
  = {project-name} (Markdown to AsciiDoc)
2
2
  Dan Allen <https://github.com/mojavelinux>
3
- v1.0.0.alpha.4, 2018-06-12
3
+ v1.0.0.alpha.5, 2018-06-19
4
4
  // Aliases:
5
5
  :project-name: Kramdown AsciiDoc
6
6
  :project-handle: kramdown-asciidoc
@@ -1,5 +1,6 @@
1
1
  require 'kramdown'
2
2
  require_relative 'kramdown-asciidoc/converter'
3
+ require_relative 'kramdown-asciidoc/writer'
3
4
  autoload :YAML, 'yaml'
4
5
 
5
6
  class Kramdown::Parser::Html::ElementConverter
@@ -10,15 +10,6 @@ module Kramdown; module AsciiDoc
10
10
  TocDirectiveTip = '<!-- TOC '
11
11
  TocDirectiveRx = /^<!-- TOC .*<!-- \/TOC -->/m
12
12
 
13
- def self.replace_toc source, attributes
14
- if source.include? TocDirectiveTip
15
- attributes['toc'] = 'macro'
16
- source.gsub TocDirectiveRx, 'toc::[]'
17
- else
18
- source
19
- end
20
- end
21
-
22
13
  # TODO return original source if YAML can't be parsed
23
14
  def self.extract_front_matter source, attributes
24
15
  if (line_i = (lines = source.each_line).first) && line_i.chomp == '---'
@@ -42,12 +33,23 @@ module Kramdown; module AsciiDoc
42
33
  end
43
34
  end
44
35
 
36
+ def self.replace_toc source, attributes
37
+ if source.include? TocDirectiveTip
38
+ attributes['toc'] = 'macro'
39
+ source.gsub TocDirectiveRx, 'toc::[]'
40
+ else
41
+ source
42
+ end
43
+ end
44
+
45
45
  class Converter < ::Kramdown::Converter::Base
46
- RESOLVE_ENTITY_TABLE = { 60 => '<', 62 => '>', 124 => '|' }
47
- ADMON_LABELS = %w(Note Tip Caution Warning Important Attention).map {|l| [l, l] }.to_h
46
+ RESOLVE_ENTITY_TABLE = { 38 => '&', 60 => '<', 62 => '>', 124 => '|' }
47
+ ADMON_LABELS = %w(Note Tip Caution Warning Important Attention Hint).map {|l| [l, l] }.to_h
48
48
  ADMON_MARKERS = ADMON_LABELS.map {|l, _| %(#{l}: ) }
49
+ ADMON_MARKERS_ASCIIDOC = %w(NOTE TIP CAUTION WARNING IMPORTANT).map {|l| %(#{l}: ) }
49
50
  ADMON_FORMATTED_MARKERS = ADMON_LABELS.map {|l, _| [%(#{l}:), l] }.to_h
50
- ADMON_TYPE_MAP = ADMON_LABELS.map {|l, _| [l, l.upcase] }.to_h.merge 'Attention' => 'IMPORTANT'
51
+ ADMON_TYPE_MAP = ADMON_LABELS.map {|l, _| [l, l.upcase] }.to_h.merge 'Attention' => 'IMPORTANT', 'Hint' => 'TIP'
52
+ BLOCK_TYPES = [:p, :blockquote, :codeblock]
51
53
  DLIST_MARKERS = %w(:: ;; ::: ::::)
52
54
  # FIXME here we reverse the smart quotes; add option to allow them (needs to be handled carefully)
53
55
  SMART_QUOTE_ENTITY_TO_MARKUP = { ldquo: ?", rdquo: ?", lsquo: ?', rsquo: ?' }
@@ -77,12 +79,13 @@ module Kramdown; module AsciiDoc
77
79
  right: '>',
78
80
  }
79
81
 
80
- ApostropheRx = /\b’\b/
82
+ NON_DEFAULT_TABLE_ALIGNMENTS = [:center, :right]
83
+
81
84
  CommentPrefixRx = /^ *! ?/m
82
85
  CssPropDelimRx = /\s*;\s*/
83
86
  MenuRefRx = /^([\p{Word}&].*?)\s>\s([\p{Word}&].*(?:\s>\s|$))+/
84
87
  ReplaceableTextRx = /[-=]>|<[-=]|\.\.\./
85
- StartOfLinesRx = /^/m
88
+ SmartApostropheRx = /\b’\b/
86
89
  TrailingSpaceRx = / +$/
87
90
  TypographicSymbolRx = /[“”‘’—–…]/
88
91
  XmlCommentRx = /\A<!--(.*)-->\Z/m
@@ -90,14 +93,12 @@ module Kramdown; module AsciiDoc
90
93
  VoidElement = Element.new nil
91
94
 
92
95
  LF = ?\n
93
- LFx2 = LF * 2
94
96
 
95
97
  def initialize root, opts
96
98
  super
97
- @header = []
98
99
  @attributes = opts[:attributes] || {}
99
100
  @imagesdir = (@attributes.delete 'implicit-imagesdir') || @attributes['imagesdir']
100
- @last_heading_level = nil
101
+ @current_heading_level = nil
101
102
  end
102
103
 
103
104
  def convert el, opts = {}
@@ -105,29 +106,27 @@ module Kramdown; module AsciiDoc
105
106
  end
106
107
 
107
108
  def convert_root el, opts
108
- el = extract_prologue el, opts
109
- body = %(#{(inner el, (opts.merge rstrip: true)).gsub TrailingSpaceRx, ''}#{LF})
109
+ writer = Writer.new
110
+ el = extract_prologue el, (opts.merge writer: writer)
111
+ traverse el, (opts.merge writer: writer)
110
112
  if (fallback_doctitle = @attributes.delete 'title')
111
- @header << %(= #{fallback_doctitle}) if @header.empty?
113
+ writer.doctitle ||= fallback_doctitle
112
114
  end
113
- @attributes.each {|k, v| @header << %(:#{k}: #{v}) } unless @attributes.empty?
114
- @header.empty? ? body : %(#{@header.join LF}#{body == LF ? '' : LFx2}#{body})
115
- end
116
-
117
- def convert_blank el, opts
118
- nil
115
+ writer.add_attributes @attributes unless @attributes.empty?
116
+ writer.to_s.gsub TrailingSpaceRx, ''
119
117
  end
120
118
 
121
119
  def convert_heading el, opts
122
- result = []
123
- style = []
120
+ (writer = opts[:writer]).start_block
124
121
  level = el.options[:level]
125
- if (discrete = @last_heading_level && level > @last_heading_level + 1)
122
+ style = []
123
+ # Q: should writer track last heading level?
124
+ if (discrete = @current_heading_level && level > @current_heading_level + 1)
126
125
  # TODO make block title promotion an option (allow certain levels and titles)
127
- #if ((raw_text = el.options[:raw_text]) == 'Example' || raw_text == 'Examples') &&
128
126
  if level == 5 && (next_2_siblings = (siblings = opts[:parent].children).slice (siblings.index el) + 1, 2) &&
129
127
  next_2_siblings.any? {|sibling| sibling.type == :codeblock }
130
- return %(.#{inner el, opts}#{LF})
128
+ writer.add_line %(.#{compose_text el, strip: true})
129
+ return
131
130
  end
132
131
  style << 'discrete'
133
132
  end
@@ -139,39 +138,36 @@ module Kramdown; module AsciiDoc
139
138
  elsif (role = el.attr['class'])
140
139
  style << %(.#{role.tr ' ', '.'})
141
140
  end
142
- result << %([#{style.join}]) unless style.empty?
143
- result << %(#{'=' * level} #{inner el, opts})
144
- @last_heading_level = level unless discrete
145
- if level == 1 && opts[:result].empty?
146
- @header += result
141
+ lines = []
142
+ lines << %([#{style.join}]) unless style.empty?
143
+ lines << %(#{'=' * level} #{compose_text el, strip: true})
144
+ if level == 1 && writer.empty? && @current_heading_level != 1
145
+ writer.header.push(*lines)
147
146
  nil
148
147
  else
149
148
  @attributes['doctype'] = 'book' if level == 1
150
- %(#{result.join LF}#{LFx2})
149
+ writer.add_lines lines
151
150
  end
151
+ @current_heading_level = level unless discrete
152
+ nil
152
153
  end
153
154
 
154
155
  # Kramdown incorrectly uses the term header for headings
155
156
  alias convert_header convert_heading
156
157
 
158
+ def convert_blank el, opts
159
+ end
160
+
157
161
  def convert_p el, opts
158
- if (parent = opts[:parent]) && (parent.type == :li || parent.type == :dd)
159
- # NOTE :prev option not set indicates primary text; convert_li appends LF
160
- return inner el, opts unless opts[:prev]
161
- parent.options[:compound] = true
162
- opts[:result].pop unless opts[:result][-1]
163
- prefix, suffix = %(#{LF}+#{LF}), ''
164
- else
165
- prefix, suffix = '', LFx2
166
- end
162
+ (writer = opts[:writer]).start_block
167
163
  if (children = el.children).empty?
168
- contents = '{blank}'
164
+ lines = ['{blank}']
169
165
  # NOTE detect plain admonition marker (e.g, Note: ...)
166
+ # TODO these conditionals could be optimized
170
167
  elsif (child_i = children[0]).type == :text && (child_i_text = child_i.value).start_with?(*ADMON_MARKERS)
171
168
  marker, child_i_text = child_i_text.split ': ', 2
172
- child_i = clone child_i, value: %(#{ADMON_TYPE_MAP[marker]}: #{child_i_text})
173
- el = clone el, children: [child_i] + (children.drop 1)
174
- contents = inner el, opts
169
+ children = [(clone child_i, value: %(#{ADMON_TYPE_MAP[marker]}: #{child_i_text}))] + (children.drop 1)
170
+ lines = compose_text children, parent: el, strip: true, split: true
175
171
  # NOTE detect formatted admonition marker (e.g., *Note:* ...)
176
172
  elsif (child_i.type == :strong || child_i.type == :em) &&
177
173
  (marker_el = child_i.children[0]) && ((marker = ADMON_FORMATTED_MARKERS[marker_el.value]) ||
@@ -179,329 +175,345 @@ module Kramdown; module AsciiDoc
179
175
  ((child_ii_text = child_ii.value).start_with? ': ')))
180
176
  children = children.drop 1
181
177
  children[0] = clone child_ii, value: (child_ii_text.slice 1, child_ii_text.length) if child_ii
182
- el = clone el, children: children
183
- contents = %(#{ADMON_TYPE_MAP[marker]}:#{inner el, opts})
178
+ # Q: should we only rstrip?
179
+ lines = compose_text children, parent: el, strip: true, split: true
180
+ lines.unshift %(#{ADMON_TYPE_MAP[marker]}: #{lines.shift})
184
181
  else
185
- contents = inner el, opts
182
+ lines = compose_text el, strip: true, split: true
186
183
  end
187
- %(#{prefix}#{contents}#{suffix})
184
+ writer.add_lines lines
188
185
  end
189
186
 
190
- # TODO detect admonition masquerading as blockquote
187
+ # Q: should we delete blank line between blocks in a nested conversation?
188
+ # TODO use shorthand for blockquote when contents is paragraph only; or always?
191
189
  def convert_blockquote el, opts
192
- result = []
193
- if (parent = opts[:parent]) && (parent.type == :li || parent.type == :dd)
194
- parent.options[:compound] = true
195
- list_continuation = %(#{LF}+)
196
- suffix = ''
190
+ (writer = opts[:writer]).start_block
191
+ traverse el, (opts.merge writer: (block_writer = Writer.new), blockquote_depth: (depth = opts[:blockquote_depth] || 0) + 1)
192
+ contents = block_writer.body
193
+ if contents[0].start_with?(*ADMON_MARKERS_ASCIIDOC) && !(contents.include? '')
194
+ writer.add_lines contents
197
195
  else
198
- suffix = LFx2
199
- end
200
- if (current_line = opts[:result].pop)
201
- opts[:result] << current_line.chomp
202
- end
203
- boundary = '____' + ((depth = opts[:blockquote_depth] || 0) > 0 ? '__' * depth : '')
204
- contents = inner el, (opts.merge rstrip: true, blockquote_depth: depth + 1)
205
- if (contents.include? LF) && ((attribution_line = (lines = contents.split LF).pop).start_with? '-- ')
206
- attribution = attribution_line.slice 3, attribution_line.length
207
- result << %([,#{attribution}])
208
- lines.pop while lines.size > 0 && lines[-1].empty?
209
- contents = lines.join LF
210
- end
211
- result << boundary
212
- result << contents
213
- result << boundary
214
- result.unshift list_continuation if list_continuation
215
- %(#{result.join LF}#{suffix})
196
+ if contents.size > 1 && (contents[-1].start_with? '-- ')
197
+ attribution = (attribution_line = contents.pop).slice 3, attribution_line.length
198
+ writer.add_line %([,#{attribution}])
199
+ contents.pop while contents.size > 0 && contents[-1].empty?
200
+ end
201
+ # Q: should writer handle delimited block nesting?
202
+ delimiter = '____' + (depth > 0 ? '__' * depth : '')
203
+ writer.start_delimited_block delimiter
204
+ writer.add_lines contents
205
+ writer.end_delimited_block
206
+ end
216
207
  end
217
208
 
209
+ # TODO match logic from ditarx
218
210
  def convert_codeblock el, opts
219
- result = []
220
- if (parent = opts[:parent]) && (parent.type == :li || parent.type == :dd)
221
- parent.options[:compound] = true
222
- if (current_line = opts[:result].pop)
223
- opts[:result] << current_line.chomp
224
- end unless opts[:result].empty?
225
- list_continuation = %(#{LF}+)
226
- suffix = ''
227
- else
228
- suffix = LFx2
211
+ writer = opts[:writer]
212
+ # NOTE hack to down-convert level-5 heading to block title
213
+ if (current_line = writer.current_line) && (!(current_line.start_with? '.') || (current_line.start_with? '. '))
214
+ writer.start_block
229
215
  end
230
- contents = el.value.rstrip
216
+ lines = el.value.rstrip.split LF
231
217
  if (lang = el.attr['class'])
232
218
  # NOTE Kramdown always prefixes class with language-
233
219
  # TODO remap lang if requested
234
- result << %([source,#{lang = lang.slice 9, lang.length}])
235
- elsif (prompt = contents.start_with? '$ ')
236
- result << %([source,#{lang = 'console'}]) if contents.include? LFx2
220
+ writer.add_line %([source,#{lang = lang.slice 9, lang.length}])
221
+ elsif (prompt = lines[0].start_with? '$ ')
222
+ writer.add_line %([source,#{lang = 'console'}]) if lines.include? ''
237
223
  end
238
224
  if lang || (el.options[:fenced] && !prompt)
239
- result << '----'
240
- result << contents
241
- result << '----'
242
- elsif !prompt && (contents.include? LFx2)
243
- result << '....'
244
- result << contents
245
- result << '....'
225
+ writer.add_line '----'
226
+ writer.add_lines lines
227
+ writer.add_line '----'
228
+ elsif !prompt && (lines.include? '')
229
+ writer.add_line '....'
230
+ writer.add_lines lines
231
+ writer.add_line '....'
246
232
  else
247
- list_continuation = LF if list_continuation
248
- result << (contents.gsub StartOfLinesRx, ' ')
233
+ # NOTE clear the list continuation (is the condition necessary?)
234
+ writer.clear_line if writer.current_line == '+'
235
+ writer.add_line lines.map {|l| %( #{l}) }
249
236
  end
250
- result.unshift list_continuation if list_continuation
251
- %(#{result.join LF}#{suffix})
252
237
  end
253
238
 
254
- def convert_ul el, opts
255
- # TODO create do_in_level block
256
- level = opts[:list_level] ? (opts[:list_level] += 1) : (opts[:list_level] = 1)
257
- # REVIEW this is whack
258
- if (parent = opts[:parent]) && (parent.type == :li || parent.type == :dd)
259
- prefix = parent.options[:compound] ? LFx2 : (opts[:result][-1] ? '' : LF)
260
- else
261
- prefix = ''
239
+ def convert_img el, opts
240
+ if !(parent = opts[:parent]) || parent.type == :p && parent.children.size == 1
241
+ style = []
242
+ if (id = el.attr['id'])
243
+ style << %(##{id})
244
+ end
245
+ if (role = el.attr['class'])
246
+ style << %(.#{role.tr ' ', '.'})
247
+ end
248
+ block_attributes_line = %([#{style.join}]) unless style.empty?
249
+ block = true
250
+ end
251
+ macro_attrs = [nil]
252
+ if (alt_text = el.attr['alt'])
253
+ macro_attrs[0] = alt_text unless alt_text.empty?
254
+ end
255
+ if (width = el.attr['width'])
256
+ macro_attrs << width
257
+ elsif (css = el.attr['style']) && (width_css = (css.split CssPropDelimRx).find {|p| p.start_with? 'width:' })
258
+ width = (width_css.slice (width_css.index ':') + 1, width_css.length).strip
259
+ width = width.to_f.round unless width.end_with? '%'
260
+ macro_attrs << width
261
+ end
262
+ if macro_attrs.size == 1 && (alt_text = macro_attrs.pop)
263
+ macro_attrs << alt_text
264
+ end
265
+ if (url = opts[:url])
266
+ macro_attrs << %(link=#{url})
267
+ end
268
+ src = el.attr['src']
269
+ if (imagesdir = @imagesdir) && (src.start_with? %(#{imagesdir}/))
270
+ src = src.slice imagesdir.length + 1, src.length
262
271
  end
263
- contents = inner el, (opts.merge rstrip: true)
264
- if level == 1
265
- suffix = LFx2
266
- opts.delete :list_level
272
+ writer = opts[:writer]
273
+ if block
274
+ writer.start_block
275
+ writer.add_line block_attributes_line if block_attributes_line
276
+ writer.add_line %(image::#{src}[#{macro_attrs.join ','}])
267
277
  else
268
- suffix = LF
269
- opts[:list_level] -= 1
278
+ writer.append %(image:#{src}[#{macro_attrs.join ','}])
270
279
  end
271
- %(#{prefix}#{contents}#{suffix})
280
+ end
281
+
282
+ def convert_ul el, opts
283
+ nested = (parent = opts[:parent]) && (parent.type == :li || parent.type == :dd)
284
+ (writer = opts[:writer]).start_list nested && parent.type != :dd && !parent.options[:compound]
285
+ level_opt = el.type == :dl ? :dlist_level : :list_level
286
+ level = opts[level_opt] ? (opts[level_opt] += 1) : (opts[level_opt] = 1)
287
+ traverse el, opts
288
+ opts.delete level_opt if (opts[level_opt] -= 1) < 1
289
+ writer.end_list nested
272
290
  end
273
291
 
274
292
  alias convert_ol convert_ul
275
293
  alias convert_dl convert_ul
276
294
 
277
295
  def convert_li el, opts
278
- prefix = (prev = opts[:prev]) && prev.options[:compound] ? LF : ''
296
+ writer = opts[:writer]
297
+ writer.add_blank_line if (prev = opts[:prev]) && prev.options[:compound]
279
298
  marker = opts[:parent].type == :ol ? '.' : '*'
280
299
  indent = (level = opts[:list_level]) - 1
281
- %(#{prefix}#{indent > 0 ? ' ' * indent : ''}#{marker * level} #{(inner el, (opts.merge rstrip: true))}#{LF})
300
+ primary, remaining = [(children = el.children.dup).shift, children]
301
+ lines = compose_text [primary], parent: el, strip: true, split: true
302
+ lines.unshift %(#{indent > 0 ? ' ' * indent : ''}#{marker * level} #{lines.shift})
303
+ writer.add_lines lines
304
+ unless remaining.empty?
305
+ next_node = remaining.find {|n| n.type != :blank }
306
+ el.options[:compound] = true if next_node && (BLOCK_TYPES.include? next_node.type)
307
+ traverse remaining, (opts.merge parent: el)
308
+ end
282
309
  end
283
310
 
284
311
  def convert_dt el, opts
285
- prefix = opts[:prev] ? LF : ''
286
- marker = DLIST_MARKERS[opts[:list_level] - 1]
287
- %(#{prefix}#{inner el, opts}#{marker}#{LF})
312
+ term = compose_text el, strip: true
313
+ marker = DLIST_MARKERS[opts[:dlist_level] - 1]
314
+ #opts[:writer].add_blank_line if (prev = opts[:prev]) && prev.options[:compound]
315
+ opts[:writer].add_blank_line if opts[:prev]
316
+ opts[:writer].add_line %(#{term}#{marker})
288
317
  end
289
318
 
290
319
  def convert_dd el, opts
291
- %(#{inner el, (opts.merge rstrip: true)}#{LF})
320
+ primary, remaining = [(children = el.children.dup).shift, children]
321
+ primary_lines = compose_text [primary], parent: el, strip: true, split: true
322
+ if primary_lines.size == 1
323
+ opts[:writer].append %( #{primary_lines[0]})
324
+ else
325
+ el.options[:compound] = true
326
+ opts[:writer].add_lines primary_lines
327
+ end
328
+ unless remaining.empty?
329
+ next_node = remaining.find {|n| n.type != :blank }
330
+ el.options[:compound] = true if next_node && (BLOCK_TYPES.include? next_node.type)
331
+ traverse remaining, (opts.merge parent: el)
332
+ end
292
333
  end
293
334
 
294
335
  def convert_table el, opts
295
336
  head = nil
296
337
  cols = (alignments = el.options[:alignment]).size
297
- if alignments.any? {|align| align == :center || align == :right }
338
+ if alignments.any? {|align| NON_DEFAULT_TABLE_ALIGNMENTS.include? align }
298
339
  colspecs = alignments.map {|align| TABLE_ALIGNMENTS[align] }.join ','
299
340
  colspecs = %("#{colspecs}") if cols > 1
300
341
  end
301
- table_buf = ['|===']
342
+ table_buffer = ['|===']
302
343
  el.children.each do |container|
303
344
  container.children.each do |row|
304
- row_buf = []
345
+ row_buffer = []
305
346
  row.children.each do |cell|
306
- cell_contents = inner cell, opts
347
+ # TODO if using sentence-per-line, append to row_buffer as separate lines
348
+ cell_contents = compose_text cell
307
349
  cell_contents = cell_contents.gsub '|', '\|' if cell_contents.include? '|'
308
- row_buf << %(| #{cell_contents})
350
+ row_buffer << %(| #{cell_contents})
309
351
  end
310
352
  if container.type == :thead
311
353
  head = true
312
- row_buf = [row_buf * ' ', '']
354
+ row_buffer = [row_buffer * ' ', '']
313
355
  elsif cols > 1
314
- row_buf << ''
356
+ row_buffer << ''
315
357
  end
316
- table_buf.concat row_buf
358
+ table_buffer.concat row_buffer
317
359
  end
318
360
  end
361
+ table_buffer.pop if table_buffer[-1] == ''
362
+ table_buffer << '|==='
363
+ (writer = opts[:writer]).start_block
319
364
  if colspecs
320
- table_buf.unshift %([cols=#{colspecs}])
365
+ writer.add_line %([cols=#{colspecs}])
321
366
  elsif !head && cols > 1
322
- table_buf.unshift %([cols=#{cols}*])
367
+ writer.add_line %([cols=#{cols}*])
323
368
  end
324
- table_buf.pop if table_buf[-1] == ''
325
- table_buf << '|==='
326
- %(#{table_buf * LF}#{LFx2})
369
+ opts[:writer].add_lines table_buffer
327
370
  end
328
371
 
329
372
  def convert_hr el, opts
330
- %('''#{LFx2})
373
+ (writer = opts[:writer]).start_block
374
+ writer.add_line '\'\'\''
331
375
  end
332
376
 
333
- def convert_text el, opts
334
- if (text = el.value).include? '++'
335
- @attributes['pp'] = '{plus}{plus}'
336
- text = text.gsub '++', '{pp}'
337
- end
338
- text = text.gsub '^', '{caret}' if (text.include? '^') && text != '^'
339
- text = text.gsub '<=', '\<=' if text.include? '<='
340
- if text.ascii_only?
341
- text
377
+ def convert_a el, opts
378
+ if (url = el.attr['href']).start_with? '#'
379
+ opts[:writer].append %(<<#{url.slice 1, url.length},#{compose_text el, strip: true}>>)
380
+ elsif url.start_with? 'https://', 'http://'
381
+ if (children = el.children).size == 1 && (child_i = el.children[0]).type == :img
382
+ convert_img child_i, parent: opts[:parent], index: 0, url: url, writer: opts[:writer]
383
+ else
384
+ bare = ((text = compose_text el, strip: true).chomp '/') == (url.chomp '/')
385
+ url = url.gsub '__', '%5F%5F' if (url.include? '__')
386
+ opts[:writer].append bare ? url : %(#{url}[#{text.gsub ']', '\]'}])
387
+ end
388
+ elsif url.end_with? '.md'
389
+ opts[:writer].append %(xref:#{url.slice 0, url.length - 3}.adoc[#{(compose_text el, strip: true).gsub ']', '\]'}])
342
390
  else
343
- (text.gsub ApostropheRx, ?').gsub TypographicSymbolRx, TYPOGRAPHIC_SYMBOL_TO_MARKUP
391
+ opts[:writer].append %(link:#{url}[#{(compose_text el, strip: true).gsub ']', '\]'}])
344
392
  end
345
- end
393
+ end
346
394
 
347
395
  def convert_codespan el, opts
348
- (val = el.value) =~ ReplaceableTextRx ? %(`+#{val}+`) : %(`#{val}`)
396
+ opts[:writer].append (val = el.value) =~ ReplaceableTextRx ? %(`+#{val}+`) : %(`#{val}`)
349
397
  end
350
398
 
351
399
  def convert_em el, opts
352
- %(_#{inner el, opts}_)
400
+ opts[:writer].append %(_#{compose_text el}_)
353
401
  end
354
402
 
355
403
  def convert_strong el, opts
356
- content = inner el, opts
357
- if (content.include? ' > ') && MenuRefRx =~ content
404
+ text = compose_text el
405
+ if (text.include? ' > ') && MenuRefRx =~ text
358
406
  @attributes['experimental'] = ''
359
- %(menu:#{$1}[#{$2}])
407
+ opts[:writer].append %(menu:#{$1}[#{$2}])
360
408
  else
361
- %(*#{content}*)
409
+ opts[:writer].append %(*#{text}*)
362
410
  end
363
411
  end
364
412
 
413
+ def convert_text el, opts
414
+ if (text = el.value).include? '++'
415
+ @attributes['pp'] = '{plus}{plus}'
416
+ text = text.gsub '++', '{pp}'
417
+ end
418
+ # Q: should we replace with single space instead?
419
+ text = text.gsub ' ', '{nbsp}' if text.include? ' '
420
+ text = text.gsub '^', '{caret}' if (text.include? '^') && text != '^'
421
+ text = text.gsub '<=', '\<=' if text.include? '<='
422
+ unless text.ascii_only?
423
+ text = (text.gsub SmartApostropheRx, ?').gsub TypographicSymbolRx, TYPOGRAPHIC_SYMBOL_TO_MARKUP
424
+ end
425
+ opts[:writer].append text
426
+ end
427
+
365
428
  # NOTE this logic assumes the :hard_wrap option is disabled in the parser
366
429
  def convert_br el, opts
367
- if (current_line = opts[:result][-1])
368
- prefix = (current_line.end_with? ' ') ? '' : ' '
430
+ writer = opts[:writer]
431
+ if writer.empty?
432
+ writer.append '{blank} +'
369
433
  else
370
- prefix = '{blank} '
434
+ writer.append %(#{(writer.current_line.end_with? ' ') ? '' : ' '}+)
371
435
  end
372
436
  if el.options[:html_tag]
373
437
  siblings = opts[:parent].children
374
- suffix = (next_el = siblings[(siblings.index el) + 1] || VoidElement).type == :text && (next_el.value.start_with? LF) ? '' : LF
375
- else
376
- suffix = ''
438
+ unless (next_el = siblings[(siblings.index el) + 1] || VoidElement).type == :text && (next_el.value.start_with? LF)
439
+ writer.add_blank_line
440
+ end
377
441
  end
378
- %(#{prefix}+#{suffix})
379
- end
380
-
381
- def convert_smart_quote el, opts
382
- SMART_QUOTE_ENTITY_TO_MARKUP[el.value]
383
442
  end
384
443
 
385
444
  def convert_entity el, opts
386
- RESOLVE_ENTITY_TABLE[el.value.code_point] || el.options[:original]
445
+ opts[:writer].append RESOLVE_ENTITY_TABLE[el.value.code_point] || el.options[:original]
387
446
  end
388
447
 
389
- def convert_a el, opts
390
- if (url = el.attr['href']).start_with? '#'
391
- %(<<#{url.slice 1, url.length},#{inner el, opts}>>)
392
- elsif url.start_with? 'https://', 'http://'
393
- if (children = el.children).size == 1 && (child_i = el.children[0]).type == :img
394
- convert_img child_i, parent: opts[:parent], index: 0, url: url
395
- else
396
- bare = ((contents = inner el, opts).chomp '/') == (url.chomp '/')
397
- url = url.gsub '__', '%5F%5F' if (url.include? '__')
398
- bare ? url : %(#{url}[#{contents.gsub ']', '\]'}])
399
- end
400
- elsif url.end_with? '.md'
401
- %(xref:#{url.slice 0, url.length - 3}.adoc[#{(inner el, opts).gsub ']', '\]'}])
402
- else
403
- %(link:#{url}[#{(inner el, opts).gsub ']', '\]'}])
404
- end
405
- end
406
-
407
- def convert_img el, opts
408
- if !(parent = opts[:parent]) || parent.type == :p && parent.children.size == 1
409
- style = []
410
- if (id = el.attr['id'])
411
- style << %(##{id})
412
- end
413
- if (role = el.attr['class'])
414
- style << %(.#{role.tr ' ', '.'})
415
- end
416
- macro_prefix = style.empty? ? 'image::' : ([%([#{style.join}]), 'image::'].join LF)
417
- else
418
- macro_prefix = 'image:'
419
- end
420
- macro_attrs = [nil]
421
- if (alt_text = el.attr['alt'])
422
- macro_attrs[0] = alt_text unless alt_text.empty?
423
- end
424
- if (width = el.attr['width'])
425
- macro_attrs << width
426
- elsif (css = el.attr['style']) && (width_css = (css.split CssPropDelimRx).find {|p| p.start_with? 'width:' })
427
- width = (width_css.slice (width_css.index ':') + 1, width_css.length).strip
428
- width = width.to_f.round unless width.end_with? '%'
429
- macro_attrs << width
430
- end
431
- if macro_attrs.size == 1 && (alt_text = macro_attrs.pop)
432
- macro_attrs << alt_text
433
- end
434
- if (url = opts[:url])
435
- macro_attrs << %(link=#{url})
436
- end
437
- src = el.attr['src']
438
- if (imagesdir = @imagesdir) && (src.start_with? %(#{imagesdir}/))
439
- src = src.slice imagesdir.length + 1, src.length
440
- end
441
- %(#{macro_prefix}#{src}[#{macro_attrs.join ','}])
448
+ def convert_smart_quote el, opts
449
+ opts[:writer].append SMART_QUOTE_ENTITY_TO_MARKUP[el.value]
442
450
  end
443
451
 
444
452
  # NOTE leave enabled so we can down-convert mdash to --
445
453
  def convert_typographic_sym el, opts
446
- TYPOGRAPHIC_ENTITY_TO_MARKUP[el.value]
454
+ opts[:writer].append TYPOGRAPHIC_ENTITY_TO_MARKUP[el.value]
447
455
  end
448
456
 
449
457
  def convert_html_element el, opts
450
- if (tagname = el.value) == 'div' && (child_i = el.children[0]) && child_i.options[:transparent] &&
451
- (child_i_i = child_i.children[0])
458
+ if (tag = el.value) == 'div' && (child_i = el.children[0]) && child_i.options[:transparent] && (child_i_i = child_i.children[0])
452
459
  if child_i_i.type == :img
453
- return convert_img child_i_i, (opts.merge parent: child_i, index: 0) if child_i.children.size == 1
454
- elsif child_i_i.value == 'span' && ((role = el.attr['class'] || '').start_with? 'note') &&
455
- child_i_i.attr['class'] == 'notetitle'
460
+ convert_img child_i_i, (opts.merge parent: child_i, index: 0) if child_i.children.size == 1
461
+ return
462
+ elsif child_i_i.value == 'span' && ((role = el.attr['class'] || '').start_with? 'note') && child_i_i.attr['class'] == 'notetitle'
456
463
  marker = ADMON_FORMATTED_MARKERS[(child_i_i.children[0] || VoidElement).value] || 'Note'
457
- (el = child_i.dup).children = el.children.drop 1
458
- return %(#{ADMON_TYPE_MAP[marker]}:#{inner el, opts}#{LFx2})
464
+ lines = compose_text (child_i.children.drop 1), parent: child_i, strip: true, split: true
465
+ lines.unshift %(#{ADMON_TYPE_MAP[marker]}: #{lines.shift})
466
+ opts[:writer].start_block
467
+ opts[:writer].add_lines lines
468
+ return
459
469
  end
460
470
  end
461
- contents = inner el, (opts.merge rstrip: el.options[:category] == :block)
471
+
472
+ contents = compose_text el, (opts.merge strip: el.options[:category] == :block)
462
473
  attrs = (attrs = el.attr).empty? ? '' : attrs.map {|k, v| %( #{k}="#{v}") }.join
463
- case tagname
474
+ case tag
464
475
  when 'del'
465
- %([.line-through]##{contents}#)
476
+ opts[:writer].append %([.line-through]##{contents}#)
466
477
  when 'sup'
467
- %(^#{contents}^)
478
+ opts[:writer].append %(^#{contents}^)
468
479
  when 'sub'
469
- %(~#{contents}~)
480
+ opts[:writer].append %(~#{contents}~)
470
481
  else
471
- %(+++<#{tagname}#{attrs}>+++#{contents}+++</#{tagname}>+++)
482
+ opts[:writer].append %(+++<#{tag}#{attrs}>+++#{contents}+++</#{tag}>+++)
472
483
  end
473
484
  end
474
485
 
475
486
  def convert_xml_comment el, opts
487
+ writer = opts[:writer]
476
488
  XmlCommentRx =~ el.value
477
- comment_text = ($1.include? ' !') ? ($1.gsub CommentPrefixRx, '').strip : $1.strip
489
+ lines = (($1.include? ' !') ? ($1.gsub CommentPrefixRx, '').strip : $1.strip).split LF
478
490
  #siblings = (parent = opts[:parent]) ? parent.children : []
479
491
  if (el.options[:category] == :block)# || (!opts[:result][-1] && siblings[-1] == el)
480
- if comment_text.empty?
481
- %(//#{LFx2})
482
- elsif comment_text.include? LF
483
- %(////#{LF}#{comment_text}#{LF}////#{LFx2})
492
+ writer.start_block
493
+ if lines.empty?
494
+ writer.add_line '//'
495
+ # Q: should we only use block form if empty line is present?
496
+ elsif lines.size > 1
497
+ writer.add_line '////'
498
+ writer.add_lines lines
499
+ writer.add_line '////'
484
500
  else
485
- %(// #{comment_text}#{LFx2})
501
+ writer.add_line %(// #{lines[0]})
486
502
  end
487
503
  else
488
- if (current_line = opts[:result][-1])
489
- if current_line.end_with? LF
490
- prefix = ''
491
- else
492
- prefix = LF
493
- opts[:result][-1] = (current_line = current_line.rstrip) if current_line.end_with? ' '
494
- end
495
- else
496
- prefix = ''
504
+ if (current_line = writer.current_line) && !(current_line.end_with? LF)
505
+ start_new_line = true
506
+ # FIXME cleaner API here (writer#strip_line?)
507
+ writer.current_line.rstrip! if current_line.end_with? ' '
497
508
  end
498
- siblings = (parent = opts[:parent]) && parent.children
499
- suffix = siblings && siblings[(siblings.index el) + 1] ? LF : ''
500
- if comment_text.include? LF
501
- %(#{prefix}#{comment_text.gsub StartOfLinesRx, '// '}#{suffix})
509
+ lines = lines.map {|l| %(// #{l}) }
510
+ if start_new_line
511
+ writer.add_lines lines
502
512
  else
503
- %(#{prefix}// #{comment_text}#{suffix})
513
+ writer.append lines.shift
514
+ writer.add_lines lines unless lines.empty?
504
515
  end
516
+ writer.add_blank_line
505
517
  end
506
518
  end
507
519
 
@@ -509,22 +521,12 @@ module Kramdown; module AsciiDoc
509
521
  if (child_i = (children = el.children)[0] || VoidElement).type == :xml_comment
510
522
  (prologue_el = el.dup).children = children.take_while {|child| child.type == :xml_comment || child.type == :blank }
511
523
  (el = el.dup).children = children.drop prologue_el.children.size
512
- @header += [%(#{inner prologue_el, (opts.merge rstrip: true)})]
524
+ traverse prologue_el, (opts.merge writer: (prologue_writer = Writer.new))
525
+ opts[:writer].header.push(*prologue_writer.body)
513
526
  end
514
527
  el
515
528
  end
516
529
 
517
- def inner el, opts
518
- rstrip = opts.delete :rstrip
519
- result = []
520
- prev = nil
521
- el.children.each_with_index do |child, idx|
522
- result << (send %(convert_#{child.type}), child, (opts.merge parent: el, index: idx, result: result, prev: prev))
523
- prev = child
524
- end
525
- rstrip ? result.join.rstrip : result.join
526
- end
527
-
528
530
  def clone el, properties
529
531
  el = el.dup
530
532
  properties.each do |name, value|
@@ -532,6 +534,35 @@ module Kramdown; module AsciiDoc
532
534
  end
533
535
  el
534
536
  end
537
+
538
+ def traverse el, opts = {}
539
+ prev = nil
540
+ if ::Array === el
541
+ nodes = el
542
+ parent = opts[:parent]
543
+ else
544
+ nodes = (parent = el).children
545
+ end
546
+ nodes.each_with_index do |child, idx|
547
+ convert child, (opts.merge parent: parent, index: idx, prev: prev)
548
+ prev = child
549
+ end
550
+ nil
551
+ end
552
+
553
+ # Q: should we support rstrip in addition to strip?
554
+ # TODO add escaping of closing square bracket
555
+ # TODO reflow text
556
+ def compose_text el, opts = {}
557
+ strip = opts.delete :strip
558
+ split = opts.delete :split
559
+ # Q: do we want to merge or just start fresh?
560
+ traverse el, (opts.merge writer: (span_writer = Writer.new))
561
+ # NOTE there should only ever be one line
562
+ text = span_writer.body.join LF
563
+ text = text.strip if strip
564
+ split ? (text.split LF) : text
565
+ end
535
566
  end
536
567
  end; end
537
568