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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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