motion-markdown-it 0.4.0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +243 -0
- data/lib/motion-markdown-it.rb +71 -0
- data/lib/motion-markdown-it/common/entities.rb +1084 -0
- data/lib/motion-markdown-it/common/html_blocks.rb +60 -0
- data/lib/motion-markdown-it/common/html_re.rb +28 -0
- data/lib/motion-markdown-it/common/string.rb +14 -0
- data/lib/motion-markdown-it/common/url_schemas.rb +173 -0
- data/lib/motion-markdown-it/common/utils.rb +216 -0
- data/lib/motion-markdown-it/helpers/parse_link_destination.rb +75 -0
- data/lib/motion-markdown-it/helpers/parse_link_label.rb +51 -0
- data/lib/motion-markdown-it/helpers/parse_link_title.rb +48 -0
- data/lib/motion-markdown-it/index.rb +507 -0
- data/lib/motion-markdown-it/parser_block.rb +113 -0
- data/lib/motion-markdown-it/parser_core.rb +46 -0
- data/lib/motion-markdown-it/parser_inline.rb +121 -0
- data/lib/motion-markdown-it/presets/commonmark.rb +76 -0
- data/lib/motion-markdown-it/presets/default.rb +42 -0
- data/lib/motion-markdown-it/presets/zero.rb +59 -0
- data/lib/motion-markdown-it/renderer.rb +286 -0
- data/lib/motion-markdown-it/ruler.rb +327 -0
- data/lib/motion-markdown-it/rules_block/blockquote.rb +138 -0
- data/lib/motion-markdown-it/rules_block/code.rb +35 -0
- data/lib/motion-markdown-it/rules_block/fence.rb +94 -0
- data/lib/motion-markdown-it/rules_block/heading.rb +56 -0
- data/lib/motion-markdown-it/rules_block/hr.rb +45 -0
- data/lib/motion-markdown-it/rules_block/html_block.rb +73 -0
- data/lib/motion-markdown-it/rules_block/lheading.rb +54 -0
- data/lib/motion-markdown-it/rules_block/list.rb +242 -0
- data/lib/motion-markdown-it/rules_block/paragraph.rb +51 -0
- data/lib/motion-markdown-it/rules_block/reference.rb +161 -0
- data/lib/motion-markdown-it/rules_block/state_block.rb +184 -0
- data/lib/motion-markdown-it/rules_block/table.rb +161 -0
- data/lib/motion-markdown-it/rules_core/block.rb +20 -0
- data/lib/motion-markdown-it/rules_core/inline.rb +20 -0
- data/lib/motion-markdown-it/rules_core/linkify.rb +138 -0
- data/lib/motion-markdown-it/rules_core/normalize.rb +44 -0
- data/lib/motion-markdown-it/rules_core/replacements.rb +90 -0
- data/lib/motion-markdown-it/rules_core/smartquotes.rb +158 -0
- data/lib/motion-markdown-it/rules_core/state_core.rb +20 -0
- data/lib/motion-markdown-it/rules_inline/autolink.rb +74 -0
- data/lib/motion-markdown-it/rules_inline/backticks.rb +51 -0
- data/lib/motion-markdown-it/rules_inline/emphasis.rb +172 -0
- data/lib/motion-markdown-it/rules_inline/entity.rb +51 -0
- data/lib/motion-markdown-it/rules_inline/escape.rb +55 -0
- data/lib/motion-markdown-it/rules_inline/html_inline.rb +49 -0
- data/lib/motion-markdown-it/rules_inline/image.rb +158 -0
- data/lib/motion-markdown-it/rules_inline/link.rb +153 -0
- data/lib/motion-markdown-it/rules_inline/newline.rb +47 -0
- data/lib/motion-markdown-it/rules_inline/state_inline.rb +57 -0
- data/lib/motion-markdown-it/rules_inline/strikethrough.rb +130 -0
- data/lib/motion-markdown-it/rules_inline/text.rb +94 -0
- data/lib/motion-markdown-it/token.rb +134 -0
- data/lib/motion-markdown-it/version.rb +5 -0
- data/spec/motion-markdown-it/bench_mark_spec.rb +44 -0
- data/spec/motion-markdown-it/commonmark_spec.rb +16 -0
- data/spec/motion-markdown-it/markdown_it_spec.rb +18 -0
- data/spec/motion-markdown-it/misc_spec.rb +277 -0
- data/spec/motion-markdown-it/ruler_spec.rb +153 -0
- data/spec/motion-markdown-it/testgen_helper.rb +68 -0
- data/spec/motion-markdown-it/token_spec.rb +17 -0
- data/spec/motion-markdown-it/utils_spec.rb +82 -0
- data/spec/spec_helper.rb +6 -0
- metadata +158 -0
@@ -0,0 +1,138 @@
|
|
1
|
+
# Block quotes
|
2
|
+
#------------------------------------------------------------------------------
|
3
|
+
module MarkdownIt
|
4
|
+
module RulesBlock
|
5
|
+
class Blockquote
|
6
|
+
|
7
|
+
#------------------------------------------------------------------------------
|
8
|
+
def self.blockquote(state, startLine, endLine, silent)
|
9
|
+
pos = state.bMarks[startLine] + state.tShift[startLine]
|
10
|
+
max = state.eMarks[startLine]
|
11
|
+
|
12
|
+
# check the block quote marker
|
13
|
+
return false if state.src.charCodeAt(pos) != 0x3E # >
|
14
|
+
pos += 1
|
15
|
+
|
16
|
+
# we know that it's going to be a valid blockquote,
|
17
|
+
# so no point trying to find the end of it in silent mode
|
18
|
+
return true if silent
|
19
|
+
|
20
|
+
# skip one optional space after '>'
|
21
|
+
pos += 1 if state.src.charCodeAt(pos) == 0x20
|
22
|
+
|
23
|
+
oldIndent = state.blkIndent
|
24
|
+
state.blkIndent = 0
|
25
|
+
|
26
|
+
oldBMarks = [ state.bMarks[startLine] ]
|
27
|
+
state.bMarks[startLine] = pos
|
28
|
+
|
29
|
+
# check if we have an empty blockquote
|
30
|
+
pos = pos < max ? state.skipSpaces(pos) : pos
|
31
|
+
lastLineEmpty = pos >= max
|
32
|
+
|
33
|
+
oldTShift = [ state.tShift[startLine] ]
|
34
|
+
state.tShift[startLine] = pos - state.bMarks[startLine]
|
35
|
+
|
36
|
+
terminatorRules = state.md.block.ruler.getRules('blockquote')
|
37
|
+
|
38
|
+
# Search the end of the block
|
39
|
+
#
|
40
|
+
# Block ends with either:
|
41
|
+
# 1. an empty line outside:
|
42
|
+
# ```
|
43
|
+
# > test
|
44
|
+
#
|
45
|
+
# ```
|
46
|
+
# 2. an empty line inside:
|
47
|
+
# ```
|
48
|
+
# >
|
49
|
+
# test
|
50
|
+
# ```
|
51
|
+
# 3. another tag
|
52
|
+
# ```
|
53
|
+
# > test
|
54
|
+
# - - -
|
55
|
+
# ```
|
56
|
+
nextLine = startLine + 1
|
57
|
+
while nextLine < endLine
|
58
|
+
pos = state.bMarks[nextLine] + state.tShift[nextLine]
|
59
|
+
max = state.eMarks[nextLine]
|
60
|
+
|
61
|
+
if pos >= max
|
62
|
+
# Case 1: line is not inside the blockquote, and this line is empty.
|
63
|
+
break
|
64
|
+
end
|
65
|
+
|
66
|
+
if state.src.charCodeAt(pos) == 0x3E # >
|
67
|
+
pos += 1
|
68
|
+
# This line is inside the blockquote.
|
69
|
+
|
70
|
+
# skip one optional space after '>'
|
71
|
+
pos += 1 if state.src.charCodeAt(pos) == 0x20
|
72
|
+
|
73
|
+
oldBMarks.push(state.bMarks[nextLine])
|
74
|
+
state.bMarks[nextLine] = pos
|
75
|
+
|
76
|
+
pos = pos < max ? state.skipSpaces(pos) : pos
|
77
|
+
lastLineEmpty = pos >= max
|
78
|
+
|
79
|
+
oldTShift.push(state.tShift[nextLine])
|
80
|
+
state.tShift[nextLine] = pos - state.bMarks[nextLine]
|
81
|
+
nextLine += 1
|
82
|
+
next
|
83
|
+
else
|
84
|
+
pos += 1
|
85
|
+
end
|
86
|
+
|
87
|
+
# Case 2: line is not inside the blockquote, and the last line was empty.
|
88
|
+
break if lastLineEmpty
|
89
|
+
|
90
|
+
# Case 3: another tag found.
|
91
|
+
terminate = false
|
92
|
+
(0...terminatorRules.length).each do |i|
|
93
|
+
if terminatorRules[i].call(state, nextLine, endLine, true)
|
94
|
+
terminate = true
|
95
|
+
break
|
96
|
+
end
|
97
|
+
end
|
98
|
+
break if terminate
|
99
|
+
|
100
|
+
oldBMarks.push(state.bMarks[nextLine])
|
101
|
+
oldTShift.push(state.tShift[nextLine])
|
102
|
+
|
103
|
+
# A negative number means that this is a paragraph continuation
|
104
|
+
#
|
105
|
+
# Any negative number will do the job here, but it's better for it
|
106
|
+
# to be large enough to make any bugs obvious.
|
107
|
+
state.tShift[nextLine] = -1337
|
108
|
+
nextLine += 1
|
109
|
+
end
|
110
|
+
|
111
|
+
oldParentType = state.parentType
|
112
|
+
state.parentType = 'blockquote'
|
113
|
+
|
114
|
+
token = state.push('blockquote_open', 'blockquote', 1)
|
115
|
+
token.markup = '>'
|
116
|
+
token.map = lines = [ startLine, 0 ]
|
117
|
+
|
118
|
+
state.md.block.tokenize(state, startLine, nextLine)
|
119
|
+
|
120
|
+
token = state.push('blockquote_close', 'blockquote', -1)
|
121
|
+
token.markup = '>'
|
122
|
+
|
123
|
+
state.parentType = oldParentType
|
124
|
+
lines[1] = state.line
|
125
|
+
|
126
|
+
# Restore original tShift; this might not be necessary since the parser
|
127
|
+
# has already been here, but just to make sure we can do that.
|
128
|
+
(0...oldTShift.length).each do |i|
|
129
|
+
state.bMarks[i + startLine] = oldBMarks[i]
|
130
|
+
state.tShift[i + startLine] = oldTShift[i]
|
131
|
+
end
|
132
|
+
state.blkIndent = oldIndent
|
133
|
+
return true
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# Code block (4 spaces padded)
|
2
|
+
#------------------------------------------------------------------------------
|
3
|
+
module MarkdownIt
|
4
|
+
module RulesBlock
|
5
|
+
class Code
|
6
|
+
|
7
|
+
#------------------------------------------------------------------------------
|
8
|
+
def self.code(state, startLine, endLine, silent = true)
|
9
|
+
return false if (state.tShift[startLine] - state.blkIndent < 4)
|
10
|
+
|
11
|
+
last = nextLine = startLine + 1
|
12
|
+
while nextLine < endLine
|
13
|
+
if state.isEmpty(nextLine)
|
14
|
+
nextLine += 1
|
15
|
+
next
|
16
|
+
end
|
17
|
+
if (state.tShift[nextLine] - state.blkIndent >= 4)
|
18
|
+
nextLine += 1
|
19
|
+
last = nextLine
|
20
|
+
next
|
21
|
+
end
|
22
|
+
break
|
23
|
+
end
|
24
|
+
|
25
|
+
state.line = nextLine
|
26
|
+
|
27
|
+
token = state.push('code_block', 'code', 0)
|
28
|
+
token.content = state.getLines(startLine, last, 4 + state.blkIndent, true)
|
29
|
+
token.map = [ startLine, state.line ]
|
30
|
+
return true
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# fences (``` lang, ~~~ lang)
|
2
|
+
#------------------------------------------------------------------------------
|
3
|
+
module MarkdownIt
|
4
|
+
module RulesBlock
|
5
|
+
class Fence
|
6
|
+
|
7
|
+
#------------------------------------------------------------------------------
|
8
|
+
def self.fence(state, startLine, endLine, silent)
|
9
|
+
haveEndMarker = false
|
10
|
+
pos = state.bMarks[startLine] + state.tShift[startLine]
|
11
|
+
max = state.eMarks[startLine]
|
12
|
+
|
13
|
+
return false if pos + 3 > max
|
14
|
+
|
15
|
+
marker = state.src.charCodeAt(pos)
|
16
|
+
|
17
|
+
if marker != 0x7E && marker != 0x60 # != ~ && != `
|
18
|
+
return false
|
19
|
+
end
|
20
|
+
|
21
|
+
# scan marker length
|
22
|
+
mem = pos;
|
23
|
+
pos = state.skipChars(pos, marker)
|
24
|
+
len = pos - mem
|
25
|
+
|
26
|
+
return false if len < 3
|
27
|
+
|
28
|
+
markup = state.src.slice(mem...pos)
|
29
|
+
params = state.src.slice(pos...max)
|
30
|
+
|
31
|
+
return false if params.include?('`')
|
32
|
+
|
33
|
+
# Since start is found, we can report success here in validation mode
|
34
|
+
return true if silent
|
35
|
+
|
36
|
+
# search end of block
|
37
|
+
nextLine = startLine
|
38
|
+
|
39
|
+
while true
|
40
|
+
nextLine += 1
|
41
|
+
if nextLine >= endLine
|
42
|
+
# unclosed block should be autoclosed by end of document.
|
43
|
+
# also block seems to be autoclosed by end of parent
|
44
|
+
break
|
45
|
+
end
|
46
|
+
|
47
|
+
pos = mem = state.bMarks[nextLine] + state.tShift[nextLine]
|
48
|
+
max = state.eMarks[nextLine];
|
49
|
+
|
50
|
+
if pos < max && state.tShift[nextLine] < state.blkIndent
|
51
|
+
# non-empty line with negative indent should stop the list:
|
52
|
+
# - ```
|
53
|
+
# test
|
54
|
+
break
|
55
|
+
end
|
56
|
+
|
57
|
+
next if state.src.charCodeAt(pos) != marker
|
58
|
+
|
59
|
+
if state.tShift[nextLine] - state.blkIndent >= 4
|
60
|
+
# closing fence should be indented less than 4 spaces
|
61
|
+
next
|
62
|
+
end
|
63
|
+
|
64
|
+
pos = state.skipChars(pos, marker)
|
65
|
+
|
66
|
+
# closing code fence must be at least as long as the opening one
|
67
|
+
next if pos - mem < len
|
68
|
+
|
69
|
+
# make sure tail has spaces only
|
70
|
+
pos = state.skipSpaces(pos)
|
71
|
+
|
72
|
+
next if pos < max
|
73
|
+
|
74
|
+
haveEndMarker = true
|
75
|
+
# found!
|
76
|
+
break
|
77
|
+
end
|
78
|
+
|
79
|
+
# If a fence has heading spaces, they should be removed from its inner block
|
80
|
+
len = state.tShift[startLine]
|
81
|
+
state.line = nextLine + (haveEndMarker ? 1 : 0)
|
82
|
+
|
83
|
+
token = state.push('fence', 'code', 0)
|
84
|
+
token.info = params
|
85
|
+
token.content = state.getLines(startLine + 1, nextLine, len, true)
|
86
|
+
token.markup = markup
|
87
|
+
token.map = [ startLine, state.line ]
|
88
|
+
|
89
|
+
return true
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# heading (#, ##, ...)
|
2
|
+
#------------------------------------------------------------------------------
|
3
|
+
module MarkdownIt
|
4
|
+
module RulesBlock
|
5
|
+
class Heading
|
6
|
+
|
7
|
+
#------------------------------------------------------------------------------
|
8
|
+
def self.heading(state, startLine, endLine, silent)
|
9
|
+
pos = state.bMarks[startLine] + state.tShift[startLine]
|
10
|
+
max = state.eMarks[startLine]
|
11
|
+
ch = state.src.charCodeAt(pos)
|
12
|
+
|
13
|
+
return false if (ch != 0x23 || pos >= max)
|
14
|
+
|
15
|
+
# count heading level
|
16
|
+
level = 1
|
17
|
+
pos += 1
|
18
|
+
ch = state.src.charCodeAt(pos)
|
19
|
+
while (ch == 0x23 && pos < max && level <= 6) # '#'
|
20
|
+
level += 1
|
21
|
+
pos += 1
|
22
|
+
ch = state.src.charCodeAt(pos)
|
23
|
+
end
|
24
|
+
|
25
|
+
return false if (level > 6 || (pos < max && ch != 0x20)) # space
|
26
|
+
|
27
|
+
return true if (silent)
|
28
|
+
|
29
|
+
# Let's cut tails like ' ### ' from the end of string
|
30
|
+
|
31
|
+
max = state.skipCharsBack(max, 0x20, pos) # space
|
32
|
+
tmp = state.skipCharsBack(max, 0x23, pos) # '#'
|
33
|
+
if (tmp > pos && state.src.charCodeAt(tmp - 1) == 0x20) # space
|
34
|
+
max = tmp
|
35
|
+
end
|
36
|
+
|
37
|
+
state.line = startLine + 1
|
38
|
+
|
39
|
+
token = state.push('heading_open', "h#{level.to_s}", 1)
|
40
|
+
token.markup = '########'.slice(0...level)
|
41
|
+
token.map = [ startLine, state.line ]
|
42
|
+
|
43
|
+
token = state.push('inline', '', 0)
|
44
|
+
token.content = state.src.slice(pos...max).strip
|
45
|
+
token.map = [ startLine, state.line ]
|
46
|
+
token.children = []
|
47
|
+
|
48
|
+
token = state.push('heading_close', "h#{level.to_s}", -1)
|
49
|
+
token.markup = '########'.slice(0...level)
|
50
|
+
|
51
|
+
return true
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# Horizontal rule
|
2
|
+
#------------------------------------------------------------------------------
|
3
|
+
module MarkdownIt
|
4
|
+
module RulesBlock
|
5
|
+
class Hr
|
6
|
+
|
7
|
+
#------------------------------------------------------------------------------
|
8
|
+
def self.hr(state, startLine, endLine, silent)
|
9
|
+
pos = state.bMarks[startLine] + state.tShift[startLine]
|
10
|
+
max = state.eMarks[startLine]
|
11
|
+
marker = state.src.charCodeAt(pos)
|
12
|
+
pos += 1
|
13
|
+
|
14
|
+
# Check hr marker
|
15
|
+
if (marker != 0x2A && # *
|
16
|
+
marker != 0x2D && # -
|
17
|
+
marker != 0x5F) # _
|
18
|
+
return false
|
19
|
+
end
|
20
|
+
|
21
|
+
# markers can be mixed with spaces, but there should be at least 3 one
|
22
|
+
|
23
|
+
cnt = 1
|
24
|
+
while (pos < max)
|
25
|
+
ch = state.src.charCodeAt(pos)
|
26
|
+
pos += 1
|
27
|
+
return false if (ch != marker && ch != 0x20) # space
|
28
|
+
cnt += 1 if (ch == marker)
|
29
|
+
end
|
30
|
+
|
31
|
+
return false if (cnt < 3)
|
32
|
+
return true if (silent)
|
33
|
+
|
34
|
+
state.line = startLine + 1
|
35
|
+
|
36
|
+
token = state.push('hr', 'hr', 0)
|
37
|
+
token.map = [ startLine, state.line ]
|
38
|
+
token.markup = marker.chr * (cnt + 1)
|
39
|
+
|
40
|
+
return true
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# HTML block
|
2
|
+
#------------------------------------------------------------------------------
|
3
|
+
module MarkdownIt
|
4
|
+
module RulesBlock
|
5
|
+
class HtmlBlock
|
6
|
+
|
7
|
+
HTML_TAG_OPEN_RE = /^<([a-zA-Z][a-zA-Z0-9]{0,14})[\s\/>]/
|
8
|
+
HTML_TAG_CLOSE_RE = /^<\/([a-zA-Z][a-zA-Z0-9]{0,14})[\s>]/
|
9
|
+
|
10
|
+
#------------------------------------------------------------------------------
|
11
|
+
def self.isLetter(ch)
|
12
|
+
lc = ch | 0x20; # to lower case
|
13
|
+
return (lc >= 0x61) && (lc <= 0x7a) # >= a and <= z
|
14
|
+
end
|
15
|
+
|
16
|
+
#------------------------------------------------------------------------------
|
17
|
+
def self.html_block(state, startLine, endLine, silent)
|
18
|
+
pos = state.bMarks[startLine]
|
19
|
+
max = state.eMarks[startLine]
|
20
|
+
shift = state.tShift[startLine]
|
21
|
+
|
22
|
+
pos += shift
|
23
|
+
|
24
|
+
return false if !state.md.options[:html]
|
25
|
+
return false if shift > 3 || (pos + 2) >= max
|
26
|
+
return false if state.src.charCodeAt(pos) != 0x3C # <
|
27
|
+
|
28
|
+
ch = state.src.charCodeAt(pos + 1)
|
29
|
+
|
30
|
+
if ch == 0x21 || ch == 0x3F # ! or ?
|
31
|
+
# Directive start / comment start / processing instruction start
|
32
|
+
return true if silent
|
33
|
+
|
34
|
+
elsif ch == 0x2F || isLetter(ch) # /
|
35
|
+
|
36
|
+
# Probably start or end of tag
|
37
|
+
if ch == 0x2F # \
|
38
|
+
# closing tag
|
39
|
+
match = state.src.slice(pos...max).match(HTML_TAG_CLOSE_RE)
|
40
|
+
return false if (!match)
|
41
|
+
else
|
42
|
+
# opening tag
|
43
|
+
match = state.src.slice(pos...max).match(HTML_TAG_OPEN_RE)
|
44
|
+
return false if !match
|
45
|
+
end
|
46
|
+
|
47
|
+
# Make sure tag name is valid
|
48
|
+
return false if HTML_BLOCKS[match[1].downcase] != true
|
49
|
+
return true if silent
|
50
|
+
|
51
|
+
else
|
52
|
+
return false
|
53
|
+
end
|
54
|
+
|
55
|
+
# If we are here - we detected HTML block.
|
56
|
+
# Let's roll down till empty line (block end).
|
57
|
+
nextLine = startLine + 1
|
58
|
+
while nextLine < state.lineMax && !state.isEmpty(nextLine)
|
59
|
+
nextLine += 1
|
60
|
+
end
|
61
|
+
|
62
|
+
state.line = nextLine
|
63
|
+
|
64
|
+
token = state.push('html_block', '', 0)
|
65
|
+
token.map = [ startLine, state.line ]
|
66
|
+
token.content = state.getLines(startLine, nextLine, 0, true)
|
67
|
+
|
68
|
+
return true
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|