motion-markdown-it 4.4.0 → 8.4.1

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 (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