motion-markdown-it 4.4.0 → 8.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +69 -16
  3. data/lib/motion-markdown-it.rb +7 -5
  4. data/lib/motion-markdown-it/common/html_blocks.rb +6 -2
  5. data/lib/motion-markdown-it/common/utils.rb +19 -4
  6. data/lib/motion-markdown-it/helpers/helper_wrapper.rb +9 -0
  7. data/lib/motion-markdown-it/helpers/parse_link_destination.rb +8 -7
  8. data/lib/motion-markdown-it/index.rb +60 -18
  9. data/lib/motion-markdown-it/parser_block.rb +7 -10
  10. data/lib/motion-markdown-it/parser_inline.rb +50 -14
  11. data/lib/motion-markdown-it/presets/commonmark.rb +7 -1
  12. data/lib/motion-markdown-it/presets/default.rb +4 -3
  13. data/lib/motion-markdown-it/presets/zero.rb +6 -1
  14. data/lib/motion-markdown-it/renderer.rb +46 -14
  15. data/lib/motion-markdown-it/rules_block/blockquote.rb +167 -31
  16. data/lib/motion-markdown-it/rules_block/code.rb +4 -3
  17. data/lib/motion-markdown-it/rules_block/fence.rb +9 -4
  18. data/lib/motion-markdown-it/rules_block/heading.rb +8 -3
  19. data/lib/motion-markdown-it/rules_block/hr.rb +10 -5
  20. data/lib/motion-markdown-it/rules_block/html_block.rb +6 -3
  21. data/lib/motion-markdown-it/rules_block/lheading.rb +64 -26
  22. data/lib/motion-markdown-it/rules_block/list.rb +91 -22
  23. data/lib/motion-markdown-it/rules_block/paragraph.rb +14 -9
  24. data/lib/motion-markdown-it/rules_block/reference.rb +24 -14
  25. data/lib/motion-markdown-it/rules_block/state_block.rb +79 -24
  26. data/lib/motion-markdown-it/rules_block/table.rb +52 -26
  27. data/lib/motion-markdown-it/rules_core/normalize.rb +1 -23
  28. data/lib/motion-markdown-it/rules_core/replacements.rb +22 -2
  29. data/lib/motion-markdown-it/rules_core/smartquotes.rb +41 -12
  30. data/lib/motion-markdown-it/rules_inline/autolink.rb +5 -4
  31. data/lib/motion-markdown-it/rules_inline/balance_pairs.rb +48 -0
  32. data/lib/motion-markdown-it/rules_inline/emphasis.rb +104 -149
  33. data/lib/motion-markdown-it/rules_inline/entity.rb +2 -2
  34. data/lib/motion-markdown-it/rules_inline/escape.rb +5 -3
  35. data/lib/motion-markdown-it/rules_inline/image.rb +12 -23
  36. data/lib/motion-markdown-it/rules_inline/link.rb +20 -25
  37. data/lib/motion-markdown-it/rules_inline/newline.rb +2 -1
  38. data/lib/motion-markdown-it/rules_inline/state_inline.rb +60 -1
  39. data/lib/motion-markdown-it/rules_inline/strikethrough.rb +81 -97
  40. data/lib/motion-markdown-it/rules_inline/text_collapse.rb +40 -0
  41. data/lib/motion-markdown-it/token.rb +46 -1
  42. data/lib/motion-markdown-it/version.rb +1 -1
  43. data/spec/motion-markdown-it/markdown_it_spec.rb +2 -2
  44. data/spec/motion-markdown-it/misc_spec.rb +90 -14
  45. data/spec/motion-markdown-it/testgen_helper.rb +1 -1
  46. data/spec/spec_helper.rb +2 -3
  47. metadata +13 -13
  48. data/lib/motion-markdown-it/common/url_schemas.rb +0 -173
  49. data/spec/motion-markdown-it/bench_mark_spec.rb +0 -44
@@ -7,20 +7,20 @@ module MarkdownIt
7
7
  class ParserBlock
8
8
 
9
9
  attr_accessor :ruler
10
-
10
+
11
11
  RULES = [
12
12
  # First 2 params - rule name & source. Secondary array - list of rules,
13
13
  # which can be terminated by this one.
14
+ [ 'table', lambda { |state, startLine, endLine, silent| RulesBlock::Table.table(state, startLine, endLine, silent) }, [ 'paragraph', 'reference' ] ],
14
15
  [ 'code', lambda { |state, startLine, endLine, silent| RulesBlock::Code.code(state, startLine, endLine, silent) } ],
15
16
  [ 'fence', lambda { |state, startLine, endLine, silent| RulesBlock::Fence.fence(state, startLine, endLine, silent) }, [ 'paragraph', 'reference', 'blockquote', 'list' ] ],
16
- [ 'blockquote', lambda { |state, startLine, endLine, silent| RulesBlock::Blockquote.blockquote(state, startLine, endLine, silent) }, [ 'paragraph', 'reference', 'list' ] ],
17
+ [ 'blockquote', lambda { |state, startLine, endLine, silent| RulesBlock::Blockquote.blockquote(state, startLine, endLine, silent) }, [ 'paragraph', 'reference', 'blockquote', 'list' ] ],
17
18
  [ 'hr', lambda { |state, startLine, endLine, silent| RulesBlock::Hr.hr(state, startLine, endLine, silent) }, [ 'paragraph', 'reference', 'blockquote', 'list' ] ],
18
19
  [ 'list', lambda { |state, startLine, endLine, silent| RulesBlock::List.list(state, startLine, endLine, silent) }, [ 'paragraph', 'reference', 'blockquote' ] ],
19
20
  [ 'reference', lambda { |state, startLine, endLine, silent| RulesBlock::Reference.reference(state, startLine, endLine, silent) } ],
20
21
  [ 'heading', lambda { |state, startLine, endLine, silent| RulesBlock::Heading.heading(state, startLine, endLine, silent) }, [ 'paragraph', 'reference', 'blockquote' ] ],
21
22
  [ 'lheading', lambda { |state, startLine, endLine, silent| RulesBlock::Lheading.lheading(state, startLine, endLine, silent) } ],
22
23
  [ 'html_block', lambda { |state, startLine, endLine, silent| RulesBlock::HtmlBlock.html_block(state, startLine, endLine, silent) }, [ 'paragraph', 'reference', 'blockquote' ] ],
23
- [ 'table', lambda { |state, startLine, endLine, silent| RulesBlock::Table.table(state, startLine, endLine, silent) }, [ 'paragraph', 'reference' ] ],
24
24
  [ 'paragraph', lambda { |state, startLine, endLine, silent| RulesBlock::Paragraph.paragraph(state, startLine) } ]
25
25
  ]
26
26
 
@@ -47,14 +47,14 @@ module MarkdownIt
47
47
  line = startLine
48
48
  hasEmptyLines = false
49
49
  maxNesting = state.md.options[:maxNesting]
50
-
50
+
51
51
  while line < endLine
52
52
  state.line = line = state.skipEmptyLines(line)
53
53
  break if line >= endLine
54
54
 
55
55
  # Termination condition for nested calls.
56
56
  # Nested calls currently used for blockquotes & lists
57
- break if state.tShift[line] < state.blkIndent
57
+ break if state.sCount[line] < state.blkIndent
58
58
 
59
59
  # If nesting level exceeded - skip tail to the end. That's not ordinary
60
60
  # situation and we should not care about content.
@@ -74,7 +74,7 @@ module MarkdownIt
74
74
  break if ok
75
75
  end
76
76
 
77
- # set state.tight iff we had an empty line before current tag
77
+ # set state.tight if we had an empty line before current tag
78
78
  # i.e. latest empty line should not count
79
79
  state.tight = !hasEmptyLines
80
80
 
@@ -88,9 +88,6 @@ module MarkdownIt
88
88
  if line < endLine && state.isEmpty(line)
89
89
  hasEmptyLines = true
90
90
  line += 1
91
-
92
- # two empty lines should stop the parser in list mode
93
- break if line < endLine && state.parentType == 'list' && state.isEmpty(line)
94
91
  state.line = line
95
92
  end
96
93
  end
@@ -102,7 +99,7 @@ module MarkdownIt
102
99
  #------------------------------------------------------------------------------
103
100
  def parse(src, md, env, outTokens)
104
101
 
105
- reutrn [] if !src
102
+ return if !src
106
103
 
107
104
  state = RulesBlock::StateBlock.new(src, md, env, outTokens)
108
105
 
@@ -5,9 +5,9 @@
5
5
  #------------------------------------------------------------------------------
6
6
  module MarkdownIt
7
7
  class ParserInline
8
-
9
- attr_accessor :ruler
10
-
8
+
9
+ attr_accessor :ruler, :ruler2
10
+
11
11
  #------------------------------------------------------------------------------
12
12
  # Parser rules
13
13
 
@@ -16,8 +16,8 @@ module MarkdownIt
16
16
  [ 'newline', lambda { |state, startLine| RulesInline::Newline.newline(state, startLine) } ],
17
17
  [ 'escape', lambda { |state, startLine| RulesInline::Escape.escape(state, startLine) } ],
18
18
  [ 'backticks', lambda { |state, startLine| RulesInline::Backticks.backtick(state, startLine) } ],
19
- [ 'strikethrough', lambda { |state, startLine| RulesInline::Strikethrough.strikethrough(state, startLine) } ],
20
- [ 'emphasis', lambda { |state, startLine| RulesInline::Emphasis.emphasis(state, startLine) } ],
19
+ [ 'strikethrough', lambda { |state, silent| RulesInline::Strikethrough.tokenize(state, silent) } ],
20
+ [ 'emphasis', lambda { |state, silent| RulesInline::Emphasis.tokenize(state, silent) } ],
21
21
  [ 'link', lambda { |state, startLine| RulesInline::Link.link(state, startLine) } ],
22
22
  [ 'image', lambda { |state, startLine| RulesInline::Image.image(state, startLine) } ],
23
23
  [ 'autolink', lambda { |state, startLine| RulesInline::Autolink.autolink(state, startLine) } ],
@@ -25,6 +25,12 @@ module MarkdownIt
25
25
  [ 'entity', lambda { |state, startLine| RulesInline::Entity.entity(state, startLine) } ],
26
26
  ]
27
27
 
28
+ RULES2 = [
29
+ [ 'balance_pairs', lambda { |state| RulesInline::BalancePairs.link_pairs(state) } ],
30
+ [ 'strikethrough', lambda { |state| RulesInline::Strikethrough.postProcess(state) } ],
31
+ [ 'emphasis', lambda { |state| RulesInline::Emphasis.postProcess(state) } ],
32
+ [ 'text_collapse', lambda { |state| RulesInline::TextCollapse.text_collapse(state) } ]
33
+ ];
28
34
 
29
35
  #------------------------------------------------------------------------------
30
36
  def initialize
@@ -36,6 +42,16 @@ module MarkdownIt
36
42
  RULES.each do |rule|
37
43
  @ruler.push(rule[0], rule[1])
38
44
  end
45
+
46
+ # ParserInline#ruler2 -> Ruler
47
+ #
48
+ # [[Ruler]] instance. Second ruler used for post-processing
49
+ # (e.g. in emphasis-like rules).
50
+ @ruler2 = Ruler.new
51
+
52
+ RULES2.each do |rule|
53
+ @ruler2.push(rule[0], rule[1])
54
+ end
39
55
  end
40
56
 
41
57
  # Skip single token by running all rules in validation mode;
@@ -47,28 +63,42 @@ module MarkdownIt
47
63
  len = rules.length
48
64
  maxNesting = state.md.options[:maxNesting]
49
65
  cache = state.cache
50
-
66
+ ok = false
51
67
 
52
68
  if cache[pos] != nil
53
69
  state.pos = cache[pos]
54
70
  return
55
71
  end
56
72
 
57
- # istanbul ignore else
58
73
  if state.level < maxNesting
59
74
  0.upto(len -1) do |i|
60
- if rules[i].call(state, true)
61
- cache[pos] = state.pos
62
- return
63
- end
75
+ # Increment state.level and decrement it later to limit recursion.
76
+ # It's harmless to do here, because no tokens are created. But ideally,
77
+ # we'd need a separate private state variable for this purpose.
78
+ state.level += 1
79
+ ok = rules[i].call(state, true)
80
+ state.level -= 1
81
+
82
+ break if ok
64
83
  end
84
+ else
85
+ # Too much nesting, just skip until the end of the paragraph.
86
+ #
87
+ # NOTE: this will cause links to behave incorrectly in the following case,
88
+ # when an amount of `[` is exactly equal to `maxNesting + 1`:
89
+ #
90
+ # [[[[[[[[[[[[[[[[[[[[[foo]()
91
+ #
92
+ # TODO: remove this workaround when CM standard will allow nested links
93
+ # (we can replace it by preventing links from being parsed in
94
+ # validation mode)
95
+ state.pos = state.posMax
65
96
  end
66
97
 
67
- state.pos += 1
98
+ state.pos += 1 if !ok
68
99
  cache[pos] = state.pos
69
100
  end
70
101
 
71
-
72
102
  # Generate tokens for input range
73
103
  #------------------------------------------------------------------------------
74
104
  def tokenize(state)
@@ -115,7 +145,13 @@ module MarkdownIt
115
145
  state = RulesInline::StateInline.new(str, md, env, outTokens)
116
146
 
117
147
  tokenize(state)
118
- end
119
148
 
149
+ rules = @ruler2.getRules('')
150
+ len = rules.length
151
+
152
+ 0.upto(len - 1) do |i|
153
+ rules[i].call(state)
154
+ end
155
+ end
120
156
  end
121
157
  end
@@ -23,7 +23,8 @@ module MarkdownIt
23
23
  quotes: "\u201c\u201d\u2018\u2019", # “”‘’
24
24
 
25
25
  # Highlighter function. Should return escaped HTML,
26
- # or '' if input not changed
26
+ # or '' if the source string is not changed and should be escaped externaly.
27
+ # If result starts with <pre... internal wrapper is skipped.
27
28
  #
28
29
  # function (/*str, lang*/) { return ''; }
29
30
  #
@@ -69,6 +70,11 @@ module MarkdownIt
69
70
  'link',
70
71
  'newline',
71
72
  'text'
73
+ ],
74
+ rules2: [
75
+ 'balance_pairs',
76
+ 'emphasis',
77
+ 'text_collapse'
72
78
  ]
73
79
  }
74
80
  }
@@ -20,16 +20,17 @@ module MarkdownIt
20
20
  #
21
21
  # For example, you can use '«»„“' for Russian, '„“‚‘' for German,
22
22
  # and ['«\xA0', '\xA0»', '‹\xA0', '\xA0›'] for French (including nbsp).
23
- quotes: "\u201c\u201d\u2018\u2019", # “”‘’
23
+ quotes: "\u201c\u201d\u2018\u2019", # “”‘’
24
24
 
25
25
  # Highlighter function. Should return escaped HTML,
26
- # or '' if input not changed
26
+ # or '' if the source string is not changed and should be escaped externaly.
27
+ # If result starts with <pre... internal wrapper is skipped.
27
28
  #
28
29
  # function (/*str, lang*/) { return ''; }
29
30
  #
30
31
  highlight: nil,
31
32
 
32
- maxNesting: 20 # Internal protection, recursion limit
33
+ maxNesting: 100 # Internal protection, recursion limit
33
34
  },
34
35
 
35
36
  components: {
@@ -24,7 +24,8 @@ module MarkdownIt
24
24
  quotes: "\u201c\u201d\u2018\u2019", # “”‘’
25
25
 
26
26
  # Highlighter function. Should return escaped HTML,
27
- # or '' if input not changed
27
+ # or '' if the source string is not changed and should be escaped externaly.
28
+ # If result starts with <pre... internal wrapper is skipped.
28
29
  #
29
30
  # function (/*str, lang*/) { return ''; }
30
31
  #
@@ -52,6 +53,10 @@ module MarkdownIt
52
53
  inline: {
53
54
  rules: [
54
55
  'text'
56
+ ],
57
+ rules2: [
58
+ 'balance_pairs',
59
+ 'text_collapse'
55
60
  ]
56
61
  }
57
62
  }
@@ -10,16 +10,24 @@ module MarkdownIt
10
10
  extend MarkdownIt::Common::Utils
11
11
 
12
12
  attr_accessor :rules
13
-
13
+
14
14
  # Default Rules
15
15
  #------------------------------------------------------------------------------
16
- def self.code_inline(tokens, idx)
17
- return '<code>' + escapeHtml(tokens[idx].content) + '</code>'
16
+ def self.code_inline(tokens, idx, options, env, renderer)
17
+ token = tokens[idx]
18
+
19
+ return '<code' + renderer.renderAttrs(token) + '>' +
20
+ escapeHtml(tokens[idx].content) +
21
+ '</code>'
18
22
  end
19
23
 
20
24
  #------------------------------------------------------------------------------
21
- def self.code_block(tokens, idx)
22
- return '<pre><code>' + escapeHtml(tokens[idx].content) + "</code></pre>\n"
25
+ def self.code_block(tokens, idx, options, env, renderer)
26
+ token = tokens[idx]
27
+
28
+ return '<pre' + renderer.renderAttrs(token) + '><code>' +
29
+ escapeHtml(tokens[idx].content) +
30
+ "</code></pre>\n"
23
31
  end
24
32
 
25
33
  #------------------------------------------------------------------------------
@@ -30,7 +38,6 @@ module MarkdownIt
30
38
 
31
39
  if !info.empty?
32
40
  langName = info.split(/\s+/)[0]
33
- token.attrPush([ 'class', options[:langPrefix] + langName ])
34
41
  end
35
42
 
36
43
  if options[:highlight]
@@ -39,7 +46,33 @@ module MarkdownIt
39
46
  highlighted = escapeHtml(token.content)
40
47
  end
41
48
 
42
- return '<pre><code' + renderer.renderAttrs(token) + '>' + highlighted + "</code></pre>\n"
49
+ if highlighted.start_with?('<pre')
50
+ return highlighted + "\n"
51
+ end
52
+
53
+ # If language exists, inject class gently, without modifying original token.
54
+ # May be, one day we will add .clone() for token and simplify this part, but
55
+ # now we prefer to keep things local.
56
+ if !info.empty?
57
+ i = token.attrIndex('class')
58
+ tmpAttrs = token.attrs ? token.attrs.dup : []
59
+
60
+ if i < 0
61
+ tmpAttrs.push([ 'class', options[:langPrefix] + langName ])
62
+ else
63
+ tmpAttrs[i][1] += ' ' + options[:langPrefix] + langName
64
+ end
65
+
66
+ # Fake token just to render attributes
67
+ tmpToken = Token.new(nil, nil, nil)
68
+ tmpToken.attrs = tmpAttrs
69
+
70
+ return '<pre><code' + renderer.renderAttrs(tmpToken) + '>' +
71
+ highlighted +
72
+ "</code></pre>\n"
73
+ end
74
+
75
+ return '<pre><code' + renderer.renderAttrs(token) + '>' + highlighted + "</code></pre>\n"
43
76
  end
44
77
 
45
78
  #------------------------------------------------------------------------------
@@ -84,8 +117,8 @@ module MarkdownIt
84
117
  #------------------------------------------------------------------------------
85
118
  def initialize
86
119
  @default_rules = {
87
- 'code_inline' => lambda {|tokens, idx, options, env, renderer| Renderer.code_inline(tokens, idx)},
88
- 'code_block' => lambda {|tokens, idx, options, env, renderer| Renderer.code_block(tokens, idx)},
120
+ 'code_inline' => lambda {|tokens, idx, options, env, renderer| Renderer.code_inline(tokens, idx, options, env, renderer)},
121
+ 'code_block' => lambda {|tokens, idx, options, env, renderer| Renderer.code_block(tokens, idx, options, env, renderer)},
89
122
  'fence' => lambda {|tokens, idx, options, env, renderer| Renderer.fence(tokens, idx, options, env, renderer)},
90
123
  'image' => lambda {|tokens, idx, options, env, renderer| Renderer.image(tokens, idx, options, env, renderer)},
91
124
  'hardbreak' => lambda {|tokens, idx, options, env, renderer| Renderer.hardbreak(tokens, idx, options)},
@@ -94,7 +127,7 @@ module MarkdownIt
94
127
  'html_block' => lambda {|tokens, idx, options, env, renderer| Renderer.html_block(tokens, idx)},
95
128
  'html_inline' => lambda {|tokens, idx, options, env, renderer| Renderer.html_inline(tokens, idx)}
96
129
  }
97
-
130
+
98
131
  # Renderer#rules -> Object
99
132
  #
100
133
  # Contains render rules for tokens. Can be updated and extended.
@@ -110,7 +143,7 @@ module MarkdownIt
110
143
  # var result = md.renderInline(...);
111
144
  # ```
112
145
  #
113
- # Each rule is called as independed static function with fixed signature:
146
+ # Each rule is called as independet static function with fixed signature:
114
147
  #
115
148
  # ```javascript
116
149
  # function my_token_render(tokens, idx, options, env, renderer) {
@@ -220,7 +253,7 @@ module MarkdownIt
220
253
 
221
254
  0.upto(tokens.length - 1) do |i|
222
255
  type = tokens[i].type
223
-
256
+
224
257
  if rules[type] != nil
225
258
  result += rules[type].call(tokens, i, options, env, self)
226
259
  else
@@ -244,11 +277,10 @@ module MarkdownIt
244
277
  #------------------------------------------------------------------------------
245
278
  def renderInlineAsText(tokens, options, env)
246
279
  result = ''
247
- rules = @rules
248
280
 
249
281
  0.upto(tokens.length - 1) do |i|
250
282
  if tokens[i].type == 'text'
251
- result += rules['text'].call(tokens, i, options, env, self)
283
+ result += tokens[i].content
252
284
  elsif tokens[i].type == 'image'
253
285
  result += renderInlineAsText(tokens[i].children, options, env)
254
286
  end
@@ -3,38 +3,93 @@
3
3
  module MarkdownIt
4
4
  module RulesBlock
5
5
  class Blockquote
6
+ extend Common::Utils
6
7
 
7
8
  #------------------------------------------------------------------------------
8
9
  def self.blockquote(state, startLine, endLine, silent)
9
- pos = state.bMarks[startLine] + state.tShift[startLine]
10
- max = state.eMarks[startLine]
10
+ oldLineMax = state.lineMax
11
+ pos = state.bMarks[startLine] + state.tShift[startLine]
12
+ max = state.eMarks[startLine]
13
+
14
+ # if it's indented more than 3 spaces, it should be a code block
15
+ return false if (state.sCount[startLine] - state.blkIndent >= 4)
11
16
 
12
17
  # check the block quote marker
13
18
  return false if state.src.charCodeAt(pos) != 0x3E # >
14
19
  pos += 1
15
-
20
+
16
21
  # we know that it's going to be a valid blockquote,
17
22
  # so no point trying to find the end of it in silent mode
18
23
  return true if silent
19
24
 
25
+ # skip spaces after ">" and re-calculate offset
26
+ initial = offset = state.sCount[startLine] + pos - (state.bMarks[startLine] + state.tShift[startLine])
27
+
20
28
  # skip one optional space after '>'
21
- pos += 1 if state.src.charCodeAt(pos) == 0x20
29
+ if state.src.charCodeAt(pos) == 0x20 # space
30
+ # ' > test '
31
+ # ^ -- position start of line here:
32
+ pos += 1
33
+ initial += 1
34
+ offset +=1
35
+ adjustTab = false
36
+ spaceAfterMarker = true
37
+ elsif state.src.charCodeAt(pos) == 0x09 # tab
38
+ spaceAfterMarker = true
22
39
 
23
- oldIndent = state.blkIndent
24
- state.blkIndent = 0
40
+ if ((state.bsCount[startLine] + offset) % 4 == 3)
41
+ # ' >\t test '
42
+ # ^ -- position start of line here (tab has width===1)
43
+ pos += 1
44
+ initial += 1
45
+ offset += 1
46
+ adjustTab = false
47
+ else
48
+ # ' >\t test '
49
+ # ^ -- position start of line here + shift bsCount slightly
50
+ # to make extra space appear
51
+ adjustTab = true
52
+ end
53
+ else
54
+ spaceAfterMarker = false
55
+ end
25
56
 
26
57
  oldBMarks = [ state.bMarks[startLine] ]
27
58
  state.bMarks[startLine] = pos
28
59
 
29
- # check if we have an empty blockquote
30
- pos = pos < max ? state.skipSpaces(pos) : pos
31
- lastLineEmpty = pos >= max
60
+ while pos < max
61
+ ch = state.src.charCodeAt(pos)
62
+
63
+ if isSpace(ch)
64
+ if ch == 0x09
65
+ offset += 4 - (offset + state.bsCount[startLine] + (adjustTab ? 1 : 0)) % 4
66
+ else
67
+ offset += 1
68
+ end
69
+ else
70
+ break
71
+ end
72
+
73
+ pos += 1
74
+ end
75
+
76
+ oldBSCount = [ state.bsCount[startLine] ]
77
+ state.bsCount[startLine] = state.sCount[startLine] + 1 + (spaceAfterMarker ? 1 : 0)
78
+
79
+ lastLineEmpty = pos >= max
80
+
81
+ oldSCount = [ state.sCount[startLine] ]
82
+ state.sCount[startLine] = offset - initial
32
83
 
33
84
  oldTShift = [ state.tShift[startLine] ]
34
85
  state.tShift[startLine] = pos - state.bMarks[startLine]
35
86
 
36
87
  terminatorRules = state.md.block.ruler.getRules('blockquote')
37
88
 
89
+ oldParentType = state.parentType
90
+ state.parentType = 'blockquote'
91
+ wasOutdented = false
92
+
38
93
  # Search the end of the block
39
94
  #
40
95
  # Block ends with either:
@@ -48,15 +103,23 @@ module MarkdownIt
48
103
  # >
49
104
  # test
50
105
  # ```
51
- # 3. another tag
106
+ # 3. another tag:
52
107
  # ```
53
108
  # > test
54
109
  # - - -
55
110
  # ```
56
111
  nextLine = startLine + 1
57
112
  while nextLine < endLine
58
- break if state.tShift[nextLine] < oldIndent
59
-
113
+ # check if it's outdented, i.e. it's inside list item and indented
114
+ # less than said list item:
115
+ #
116
+ # ```
117
+ # 1. anything
118
+ # > current blockquote
119
+ # 2. checking this line
120
+ # ```
121
+ wasOutdented = true if (state.sCount[nextLine] < state.blkIndent)
122
+
60
123
  pos = state.bMarks[nextLine] + state.tShift[nextLine]
61
124
  max = state.eMarks[nextLine]
62
125
 
@@ -65,19 +128,69 @@ module MarkdownIt
65
128
  break
66
129
  end
67
130
 
68
- if state.src.charCodeAt(pos) == 0x3E # >
131
+ if state.src.charCodeAt(pos) == 0x3E && !wasOutdented # >
69
132
  pos += 1
70
133
  # This line is inside the blockquote.
71
134
 
135
+ # skip spaces after ">" and re-calculate offset
136
+ initial = offset = state.sCount[nextLine] + pos - (state.bMarks[nextLine] + state.tShift[nextLine])
137
+
72
138
  # skip one optional space after '>'
73
- pos += 1 if state.src.charCodeAt(pos) == 0x20
139
+ if state.src.charCodeAt(pos) == 0x20 # space
140
+ # ' > test '
141
+ # ^ -- position start of line here:
142
+ pos += 1
143
+ initial += 1
144
+ offset += 1
145
+ adjustTab = false
146
+ spaceAfterMarker = true
147
+ elsif state.src.charCodeAt(pos) == 0x09 # tab
148
+ spaceAfterMarker = true
149
+
150
+ if ((state.bsCount[nextLine] + offset) % 4 == 3)
151
+ # ' >\t test '
152
+ # ^ -- position start of line here (tab has width===1)
153
+ pos += 1
154
+ initial += 1
155
+ offset += 1
156
+ adjustTab = false
157
+ else
158
+ # ' >\t test '
159
+ # ^ -- position start of line here + shift bsCount slightly
160
+ # to make extra space appear
161
+ adjustTab = true
162
+ end
163
+ else
164
+ spaceAfterMarker = false
165
+ end
74
166
 
75
167
  oldBMarks.push(state.bMarks[nextLine])
76
168
  state.bMarks[nextLine] = pos
77
169
 
78
- pos = pos < max ? state.skipSpaces(pos) : pos
170
+ while pos < max
171
+ ch = state.src.charCodeAt(pos)
172
+
173
+ if isSpace(ch)
174
+ if ch == 0x09
175
+ offset += 4 - (offset + state.bsCount[nextLine] + (adjustTab ? 1 : 0)) % 4
176
+ else
177
+ offset += 1
178
+ end
179
+ else
180
+ break
181
+ end
182
+
183
+ pos += 1
184
+ end
185
+
79
186
  lastLineEmpty = pos >= max
80
187
 
188
+ oldBSCount.push(state.bsCount[nextLine])
189
+ state.bsCount[nextLine] = state.sCount[nextLine] + 1 + (spaceAfterMarker ? 1 : 0)
190
+
191
+ oldSCount.push(state.sCount[nextLine])
192
+ state.sCount[nextLine] = offset - initial\
193
+
81
194
  oldTShift.push(state.tShift[nextLine])
82
195
  state.tShift[nextLine] = pos - state.bMarks[nextLine]
83
196
  nextLine += 1
@@ -97,39 +210,62 @@ module MarkdownIt
97
210
  break
98
211
  end
99
212
  end
100
- break if terminate
213
+
214
+ if terminate
215
+ # Quirk to enforce "hard termination mode" for paragraphs;
216
+ # normally if you call `tokenize(state, startLine, nextLine)`,
217
+ # paragraphs will look below nextLine for paragraph continuation,
218
+ # but if blockquote is terminated by another tag, they shouldn't
219
+ state.lineMax = nextLine
220
+
221
+ if state.blkIndent != 0
222
+ # state.blkIndent was non-zero, we now set it to zero,
223
+ # so we need to re-calculate all offsets to appear as
224
+ # if indent wasn't changed
225
+ oldBMarks.push(state.bMarks[nextLine])
226
+ oldBSCount.push(state.bsCount[nextLine])
227
+ oldTShift.push(state.tShift[nextLine])
228
+ oldSCount.push(state.sCount[nextLine])
229
+ state.sCount[nextLine] -= state.blkIndent
230
+ end
231
+
232
+ break
233
+ end
101
234
 
102
235
  oldBMarks.push(state.bMarks[nextLine])
236
+ oldBSCount.push(state.bsCount[nextLine])
103
237
  oldTShift.push(state.tShift[nextLine])
238
+ oldSCount.push(state.sCount[nextLine])
104
239
 
105
- # A negative number means that this is a paragraph continuation
240
+ # A negative indentation means that this is a paragraph continuation
106
241
  #
107
- # Any negative number will do the job here, but it's better for it
108
- # to be large enough to make any bugs obvious.
109
- state.tShift[nextLine] = -1
242
+ state.sCount[nextLine] = -1
110
243
  nextLine += 1
111
244
  end
112
245
 
113
- oldParentType = state.parentType
114
- state.parentType = 'blockquote'
246
+ oldIndent = state.blkIndent
247
+ state.blkIndent = 0
115
248
 
116
- token = state.push('blockquote_open', 'blockquote', 1)
117
- token.markup = '>'
118
- token.map = lines = [ startLine, 0 ]
249
+ token = state.push('blockquote_open', 'blockquote', 1)
250
+ token.markup = '>'
251
+ token.map = lines = [ startLine, 0 ]
119
252
 
120
253
  state.md.block.tokenize(state, startLine, nextLine)
121
254
 
122
- token = state.push('blockquote_close', 'blockquote', -1)
123
- token.markup = '>'
255
+ token = state.push('blockquote_close', 'blockquote', -1)
256
+ token.markup = '>'
124
257
 
125
- state.parentType = oldParentType
126
- lines[1] = state.line
258
+ state.lineMax = oldLineMax;
259
+ state.parentType = oldParentType
260
+ lines[1] = state.line
127
261
 
128
262
  # Restore original tShift; this might not be necessary since the parser
129
263
  # has already been here, but just to make sure we can do that.
130
264
  (0...oldTShift.length).each do |i|
131
- state.bMarks[i + startLine] = oldBMarks[i]
132
- state.tShift[i + startLine] = oldTShift[i]
265
+ state.bMarks[i + startLine] = oldBMarks[i]
266
+ state.tShift[i + startLine] = oldTShift[i]
267
+ state.sCount[i + startLine] = oldSCount[i]
268
+ state.bsCount[i + startLine] = oldBSCount[i]
133
269
  end
134
270
  state.blkIndent = oldIndent
135
271
  return true