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,44 @@
|
|
1
|
+
# Normalize input string
|
2
|
+
#------------------------------------------------------------------------------
|
3
|
+
module MarkdownIt
|
4
|
+
module RulesCore
|
5
|
+
class Normalize
|
6
|
+
|
7
|
+
TABS_SCAN_RE = /[\n\t]/
|
8
|
+
NEWLINES_RE = /\r[\n\u0085]|[\u2424\u2028\u0085]/
|
9
|
+
NULL_RE = /\u0000/
|
10
|
+
|
11
|
+
|
12
|
+
#------------------------------------------------------------------------------
|
13
|
+
def self.inline(state)
|
14
|
+
# Normalize newlines
|
15
|
+
str = state.src.gsub(NEWLINES_RE, "\n")
|
16
|
+
|
17
|
+
# Replace NULL characters
|
18
|
+
str = str.gsub(NULL_RE, '\uFFFD')
|
19
|
+
|
20
|
+
# Replace tabs with proper number of spaces (1..4)
|
21
|
+
if str.include?("\t")
|
22
|
+
lineStart = 0
|
23
|
+
lastTabPos = 0
|
24
|
+
|
25
|
+
str = str.gsub(TABS_SCAN_RE) do
|
26
|
+
md = Regexp.last_match
|
27
|
+
match = md.to_s
|
28
|
+
offset = md.begin(0)
|
29
|
+
if str.charCodeAt(offset) == 0x0A
|
30
|
+
lineStart = offset + 1
|
31
|
+
lastTabPos = 0
|
32
|
+
next match
|
33
|
+
end
|
34
|
+
result = ' '.slice_to_end((offset - lineStart - lastTabPos) % 4)
|
35
|
+
lastTabPos = offset - lineStart + 1
|
36
|
+
result
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
state.src = str
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# Simple typographyc replacements
|
2
|
+
#
|
3
|
+
# '' → ‘’
|
4
|
+
# "" → “”. Set '«»' for Russian, '„“' for German, empty to disable
|
5
|
+
# (c) (C) → ©
|
6
|
+
# (tm) (TM) → ™
|
7
|
+
# (r) (R) → ®
|
8
|
+
# +- → ±
|
9
|
+
# (p) (P) -> §
|
10
|
+
# ... → … (also ?.... → ?.., !.... → !..)
|
11
|
+
# ???????? → ???, !!!!! → !!!, `,,` → `,`
|
12
|
+
# -- → –, --- → —
|
13
|
+
#------------------------------------------------------------------------------
|
14
|
+
module MarkdownIt
|
15
|
+
module RulesCore
|
16
|
+
class Replacements
|
17
|
+
|
18
|
+
# TODO:
|
19
|
+
# - fractionals 1/2, 1/4, 3/4 -> ½, ¼, ¾
|
20
|
+
# - miltiplication 2 x 4 -> 2 × 4
|
21
|
+
|
22
|
+
RARE_RE = /\+-|\.\.|\?\?\?\?|!!!!|,,|--/
|
23
|
+
|
24
|
+
SCOPED_ABBR_RE = /\((c|tm|r|p)\)/i
|
25
|
+
SCOPED_ABBR = {
|
26
|
+
'c' => '©',
|
27
|
+
'r' => '®',
|
28
|
+
'p' => '§',
|
29
|
+
'tm' => '™'
|
30
|
+
}
|
31
|
+
|
32
|
+
#------------------------------------------------------------------------------
|
33
|
+
def self.replaceFn(match, name)
|
34
|
+
return SCOPED_ABBR[name.downcase]
|
35
|
+
end
|
36
|
+
|
37
|
+
#------------------------------------------------------------------------------
|
38
|
+
def self.replace_scoped(inlineTokens)
|
39
|
+
(inlineTokens.length - 1).downto(0) do |i|
|
40
|
+
token = inlineTokens[i]
|
41
|
+
if (token.type == 'text')
|
42
|
+
token.content = token.content.gsub(SCOPED_ABBR_RE) {|match| self.replaceFn(match, $1)}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
#------------------------------------------------------------------------------
|
48
|
+
def self.replace_rare(inlineTokens)
|
49
|
+
(inlineTokens.length - 1).downto(0) do |i|
|
50
|
+
token = inlineTokens[i]
|
51
|
+
if (token.type == 'text')
|
52
|
+
if (RARE_RE =~ token.content)
|
53
|
+
token.content = token.content.
|
54
|
+
gsub(/\+-/, '±').
|
55
|
+
# .., ..., ....... -> …
|
56
|
+
# but ?..... & !..... -> ?.. & !..
|
57
|
+
gsub(/\.{2,}/, '…').gsub(/([?!])…/, "\\1..").
|
58
|
+
gsub(/([?!]){4,}/, '\\1\\1\\1').gsub(/,{2,}/, ',').
|
59
|
+
# em-dash
|
60
|
+
gsub(/(^|[^-])---([^-]|$)/m, "\\1\u2014\\2").
|
61
|
+
# en-dash
|
62
|
+
gsub(/(^|\s)--(\s|$)/m, "\\1\u2013\\2").
|
63
|
+
gsub(/(^|[^-\s])--([^-\s]|$)/m, "\\1\u2013\\2")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
#------------------------------------------------------------------------------
|
71
|
+
def self.replace(state)
|
72
|
+
return if (!state.md.options[:typographer])
|
73
|
+
|
74
|
+
(state.tokens.length - 1).downto(0) do |blkIdx|
|
75
|
+
next if (state.tokens[blkIdx].type != 'inline')
|
76
|
+
|
77
|
+
if (SCOPED_ABBR_RE =~ state.tokens[blkIdx].content)
|
78
|
+
replace_scoped(state.tokens[blkIdx].children)
|
79
|
+
end
|
80
|
+
|
81
|
+
if (RARE_RE =~ state.tokens[blkIdx].content)
|
82
|
+
replace_rare(state.tokens[blkIdx].children)
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# Convert straight quotation marks to typographic ones
|
2
|
+
#------------------------------------------------------------------------------
|
3
|
+
module MarkdownIt
|
4
|
+
module RulesCore
|
5
|
+
class Smartquotes
|
6
|
+
extend Common::Utils
|
7
|
+
|
8
|
+
QUOTE_TEST_RE = /['"]/
|
9
|
+
QUOTE_RE = /['"]/
|
10
|
+
APOSTROPHE = "\u2019" # ’
|
11
|
+
|
12
|
+
|
13
|
+
#------------------------------------------------------------------------------
|
14
|
+
def self.replaceAt(str, index, ch)
|
15
|
+
return str[0, index] + ch + str.slice_to_end(index + 1)
|
16
|
+
end
|
17
|
+
|
18
|
+
#------------------------------------------------------------------------------
|
19
|
+
def self.process_inlines(tokens, state)
|
20
|
+
stack = []
|
21
|
+
|
22
|
+
(0...tokens.length).each do |i|
|
23
|
+
token = tokens[i]
|
24
|
+
|
25
|
+
thisLevel = tokens[i].level
|
26
|
+
|
27
|
+
j = stack.length - 1
|
28
|
+
while j >= 0
|
29
|
+
break if (stack[j][:level] <= thisLevel)
|
30
|
+
j -= 1
|
31
|
+
end
|
32
|
+
|
33
|
+
# stack.length = j + 1
|
34
|
+
stack = (j < stack.length ? stack.slice(0, j + 1) : stack.fill(nil, stack.length...(j+1)))
|
35
|
+
|
36
|
+
next if (token.type != 'text')
|
37
|
+
|
38
|
+
text = token.content
|
39
|
+
pos = 0
|
40
|
+
max = text.length
|
41
|
+
|
42
|
+
# OUTER loop
|
43
|
+
while pos < max
|
44
|
+
continue_outer_loop = false
|
45
|
+
t = QUOTE_RE.match(text, pos)
|
46
|
+
break if t.nil?
|
47
|
+
|
48
|
+
canOpen = true
|
49
|
+
canClose = true
|
50
|
+
pos = t.begin(0) + 1
|
51
|
+
isSingle = (t[0] == "'")
|
52
|
+
|
53
|
+
# treat begin/end of the line as a whitespace
|
54
|
+
lastChar = t.begin(0) - 1 >= 0 ? text.charCodeAt(t.begin(0) - 1) : 0x20
|
55
|
+
nextChar = pos < max ? text.charCodeAt(pos) : 0x20
|
56
|
+
|
57
|
+
isLastPunctChar = isMdAsciiPunct(lastChar) || isPunctChar(lastChar.chr)
|
58
|
+
isNextPunctChar = isMdAsciiPunct(nextChar) || isPunctChar(nextChar.chr)
|
59
|
+
|
60
|
+
isLastWhiteSpace = isWhiteSpace(lastChar)
|
61
|
+
isNextWhiteSpace = isWhiteSpace(nextChar)
|
62
|
+
|
63
|
+
if (isNextWhiteSpace)
|
64
|
+
canOpen = false
|
65
|
+
elsif (isNextPunctChar)
|
66
|
+
if (!(isLastWhiteSpace || isLastPunctChar))
|
67
|
+
canOpen = false
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
if (isLastWhiteSpace)
|
72
|
+
canClose = false
|
73
|
+
elsif (isLastPunctChar)
|
74
|
+
if (!(isNextWhiteSpace || isNextPunctChar))
|
75
|
+
canClose = false
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
if (nextChar == 0x22 && t[0] == '"') # "
|
80
|
+
if (lastChar >= 0x30 && lastChar <= 0x39) # >= 0 && <= 9
|
81
|
+
# special case: 1"" - count first quote as an inch
|
82
|
+
canClose = canOpen = false
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
if (canOpen && canClose)
|
87
|
+
# treat this as the middle of the word
|
88
|
+
canOpen = false
|
89
|
+
canClose = isNextPunctChar
|
90
|
+
end
|
91
|
+
|
92
|
+
if (!canOpen && !canClose)
|
93
|
+
# middle of word
|
94
|
+
if (isSingle)
|
95
|
+
token.content = replaceAt(token.content, t.begin(0), APOSTROPHE)
|
96
|
+
end
|
97
|
+
next
|
98
|
+
end
|
99
|
+
|
100
|
+
if (canClose)
|
101
|
+
# this could be a closing quote, rewind the stack to get a match
|
102
|
+
j = stack.length - 1
|
103
|
+
while j >= 0
|
104
|
+
item = stack[j]
|
105
|
+
break if (stack[j][:level] < thisLevel)
|
106
|
+
if (item[:single] == isSingle && stack[j][:level] == thisLevel)
|
107
|
+
item = stack[j]
|
108
|
+
if (isSingle)
|
109
|
+
tokens[item[:token]].content = replaceAt(tokens[item[:token]].content, item[:pos], state.md.options[:quotes][2])
|
110
|
+
token.content = replaceAt(token.content, t.begin(0), state.md.options[:quotes][3])
|
111
|
+
else
|
112
|
+
tokens[item[:token]].content = replaceAt(tokens[item[:token]].content, item[:pos], state.md.options[:quotes][0])
|
113
|
+
token.content = replaceAt(token.content, t.begin(0), state.md.options[:quotes][1])
|
114
|
+
end
|
115
|
+
# stack.length = j
|
116
|
+
stack = (j < stack.length ? stack.slice(0, j) : stack.fill(nil, stack.length...(j)))
|
117
|
+
continue_outer_loop = true # continue OUTER;
|
118
|
+
break
|
119
|
+
end
|
120
|
+
j -= 1
|
121
|
+
end
|
122
|
+
end
|
123
|
+
next if continue_outer_loop
|
124
|
+
|
125
|
+
if (canOpen)
|
126
|
+
stack.push({
|
127
|
+
token: i,
|
128
|
+
pos: t.begin(0),
|
129
|
+
single: isSingle,
|
130
|
+
level: thisLevel
|
131
|
+
})
|
132
|
+
elsif (canClose && isSingle)
|
133
|
+
token.content = replaceAt(token.content, t.begin(0), APOSTROPHE)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
#------------------------------------------------------------------------------
|
141
|
+
def self.smartquotes(state)
|
142
|
+
return if (!state.md.options[:typographer])
|
143
|
+
|
144
|
+
blkIdx = state.tokens.length - 1
|
145
|
+
while blkIdx >= 0
|
146
|
+
if (state.tokens[blkIdx].type != 'inline' || !(QUOTE_TEST_RE =~ state.tokens[blkIdx].content))
|
147
|
+
blkIdx -= 1
|
148
|
+
next
|
149
|
+
end
|
150
|
+
|
151
|
+
process_inlines(state.tokens[blkIdx].children, state)
|
152
|
+
blkIdx -= 1
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# Core state object
|
2
|
+
#------------------------------------------------------------------------------
|
3
|
+
module MarkdownIt
|
4
|
+
module RulesCore
|
5
|
+
class StateCore
|
6
|
+
|
7
|
+
attr_accessor :src, :env, :tokens, :inlineMode, :md
|
8
|
+
|
9
|
+
#------------------------------------------------------------------------------
|
10
|
+
def initialize(src, md, env)
|
11
|
+
@src = src
|
12
|
+
@env = env
|
13
|
+
@tokens = []
|
14
|
+
@inlineMode = false
|
15
|
+
@md = md # link to parser instance
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# Process autolinks '<protocol:...>'
|
2
|
+
#------------------------------------------------------------------------------
|
3
|
+
module MarkdownIt
|
4
|
+
module RulesInline
|
5
|
+
class Autolink
|
6
|
+
|
7
|
+
EMAIL_RE = /^<([a-zA-Z0-9.!#$\%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)>/
|
8
|
+
AUTOLINK_RE = /^<([a-zA-Z.\-]{1,25}):([^<>\x00-\x20]*)>/
|
9
|
+
|
10
|
+
|
11
|
+
#------------------------------------------------------------------------------
|
12
|
+
def self.autolink(state, silent)
|
13
|
+
pos = state.pos
|
14
|
+
|
15
|
+
return false if (state.src.charCodeAt(pos) != 0x3C) # <
|
16
|
+
|
17
|
+
tail = state.src.slice_to_end(pos)
|
18
|
+
|
19
|
+
return false if !tail.include?('>')
|
20
|
+
|
21
|
+
if (AUTOLINK_RE =~ tail)
|
22
|
+
linkMatch = tail.match(AUTOLINK_RE)
|
23
|
+
|
24
|
+
return false if !URL_SCHEMAS.include?(linkMatch[1].downcase)
|
25
|
+
|
26
|
+
url = linkMatch[0].slice(1...-1)
|
27
|
+
fullUrl = state.md.normalizeLink.call(url)
|
28
|
+
return false if (!state.md.validateLink.call(fullUrl))
|
29
|
+
|
30
|
+
if (!silent)
|
31
|
+
token = state.push('link_open', 'a', 1)
|
32
|
+
token.attrs = [ [ 'href', fullUrl ] ]
|
33
|
+
|
34
|
+
token = state.push('text', '', 0)
|
35
|
+
token.content = state.md.normalizeLinkText.call(url)
|
36
|
+
|
37
|
+
token = state.push('link_close', 'a', -1)
|
38
|
+
end
|
39
|
+
|
40
|
+
state.pos += linkMatch[0].length
|
41
|
+
return true
|
42
|
+
end
|
43
|
+
|
44
|
+
if (EMAIL_RE =~ tail)
|
45
|
+
emailMatch = tail.match(EMAIL_RE)
|
46
|
+
|
47
|
+
url = emailMatch[0].slice(1...-1)
|
48
|
+
fullUrl = state.md.normalizeLink.call('mailto:' + url)
|
49
|
+
return false if (!state.md.validateLink.call(fullUrl))
|
50
|
+
|
51
|
+
if (!silent)
|
52
|
+
token = state.push('link_open', 'a', 1)
|
53
|
+
token.attrs = [ [ 'href', fullUrl ] ]
|
54
|
+
token.markup = 'autolink'
|
55
|
+
token.info = 'auto'
|
56
|
+
|
57
|
+
token = state.push('text', '', 0)
|
58
|
+
token.content = state.md.normalizeLinkText.call(url)
|
59
|
+
|
60
|
+
token = state.push('link_close', 'a', -1)
|
61
|
+
token.markup = 'autolink'
|
62
|
+
token.info = 'auto'
|
63
|
+
end
|
64
|
+
|
65
|
+
state.pos += emailMatch[0].length
|
66
|
+
return true
|
67
|
+
end
|
68
|
+
|
69
|
+
return false
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# Parse backticks
|
2
|
+
#------------------------------------------------------------------------------
|
3
|
+
module MarkdownIt
|
4
|
+
module RulesInline
|
5
|
+
class Backticks
|
6
|
+
|
7
|
+
#------------------------------------------------------------------------------
|
8
|
+
def self.backtick(state, silent)
|
9
|
+
pos = state.pos
|
10
|
+
ch = state.src.charCodeAt(pos)
|
11
|
+
|
12
|
+
return false if (ch != 0x60) # `
|
13
|
+
|
14
|
+
start = pos
|
15
|
+
pos += 1
|
16
|
+
max = state.posMax
|
17
|
+
|
18
|
+
while (pos < max && state.src.charCodeAt(pos) == 0x60) # `
|
19
|
+
pos += 1
|
20
|
+
end
|
21
|
+
|
22
|
+
marker = state.src.slice(start...pos)
|
23
|
+
|
24
|
+
matchStart = matchEnd = pos
|
25
|
+
|
26
|
+
while ((matchStart = state.src.index('`', matchEnd)) != nil)
|
27
|
+
matchEnd = matchStart + 1
|
28
|
+
|
29
|
+
while (matchEnd < max && state.src.charCodeAt(matchEnd) == 0x60) # `
|
30
|
+
matchEnd += 1
|
31
|
+
end
|
32
|
+
|
33
|
+
if (matchEnd - matchStart == marker.length)
|
34
|
+
if (!silent)
|
35
|
+
token = state.push('code_inline', 'code', 0)
|
36
|
+
token.markup = marker
|
37
|
+
token.content = state.src.slice(pos...matchStart).gsub(/[ \n]+/, ' ').strip
|
38
|
+
end
|
39
|
+
state.pos = matchEnd
|
40
|
+
return true
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
state.pending += marker if (!silent)
|
45
|
+
state.pos += marker.length
|
46
|
+
return true
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
# Process *this* and _that_
|
2
|
+
#------------------------------------------------------------------------------
|
3
|
+
module MarkdownIt
|
4
|
+
module RulesInline
|
5
|
+
class Emphasis
|
6
|
+
extend MarkdownIt::Common::Utils
|
7
|
+
|
8
|
+
# parse sequence of emphasis markers,
|
9
|
+
# "start" should point at a valid marker
|
10
|
+
#------------------------------------------------------------------------------
|
11
|
+
def self.scanDelims(state, start)
|
12
|
+
pos = start
|
13
|
+
can_open = true
|
14
|
+
can_close = true
|
15
|
+
max = state.posMax
|
16
|
+
marker = state.src.charCodeAt(start)
|
17
|
+
|
18
|
+
# treat beginning of the line as a whitespace
|
19
|
+
lastChar = start > 0 ? state.src.charCodeAt(start - 1) : 0x20
|
20
|
+
|
21
|
+
while (pos < max && state.src.charCodeAt(pos) == marker)
|
22
|
+
pos += 1
|
23
|
+
end
|
24
|
+
|
25
|
+
if (pos >= max)
|
26
|
+
can_open = false
|
27
|
+
end
|
28
|
+
|
29
|
+
count = pos - start
|
30
|
+
|
31
|
+
# treat end of the line as a whitespace
|
32
|
+
nextChar = pos < max ? state.src.charCodeAt(pos) : 0x20
|
33
|
+
|
34
|
+
isLastPunctChar = isMdAsciiPunct(lastChar) || isPunctChar(lastChar.chr(Encoding::UTF_8))
|
35
|
+
isNextPunctChar = isMdAsciiPunct(nextChar) || isPunctChar(nextChar.chr(Encoding::UTF_8))
|
36
|
+
|
37
|
+
isLastWhiteSpace = isWhiteSpace(lastChar)
|
38
|
+
isNextWhiteSpace = isWhiteSpace(nextChar)
|
39
|
+
|
40
|
+
if (isNextWhiteSpace)
|
41
|
+
can_open = false
|
42
|
+
elsif (isNextPunctChar)
|
43
|
+
if (!(isLastWhiteSpace || isLastPunctChar))
|
44
|
+
can_open = false
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
if (isLastWhiteSpace)
|
49
|
+
can_close = false
|
50
|
+
elsif (isLastPunctChar)
|
51
|
+
if (!(isNextWhiteSpace || isNextPunctChar))
|
52
|
+
can_close = false
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
if (marker == 0x5F) # _
|
57
|
+
if (can_open && can_close)
|
58
|
+
# "_" inside a word can neither open nor close an emphasis
|
59
|
+
can_open = false
|
60
|
+
can_close = isNextPunctChar
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
return { can_open: can_open, can_close: can_close, delims: count }
|
65
|
+
end
|
66
|
+
|
67
|
+
#------------------------------------------------------------------------------
|
68
|
+
def self.emphasis(state, silent)
|
69
|
+
max = state.posMax
|
70
|
+
start = state.pos
|
71
|
+
marker = state.src.charCodeAt(start)
|
72
|
+
|
73
|
+
return false if (marker != 0x5F && marker != 0x2A) # _ *
|
74
|
+
return false if (silent) # don't run any pairs in validation mode
|
75
|
+
|
76
|
+
res = scanDelims(state, start)
|
77
|
+
startCount = res[:delims]
|
78
|
+
if (!res[:can_open])
|
79
|
+
state.pos += startCount
|
80
|
+
# Earlier we checked !silent, but this implementation does not need it
|
81
|
+
state.pending += state.src.slice(start...state.pos)
|
82
|
+
return true
|
83
|
+
end
|
84
|
+
|
85
|
+
state.pos = start + startCount
|
86
|
+
stack = [ startCount ]
|
87
|
+
|
88
|
+
while (state.pos < max)
|
89
|
+
if (state.src.charCodeAt(state.pos) == marker)
|
90
|
+
res = scanDelims(state, state.pos)
|
91
|
+
count = res[:delims]
|
92
|
+
if (res[:can_close])
|
93
|
+
oldCount = stack.pop()
|
94
|
+
newCount = count
|
95
|
+
|
96
|
+
while (oldCount != newCount)
|
97
|
+
if (newCount < oldCount)
|
98
|
+
stack.push(oldCount - newCount)
|
99
|
+
break
|
100
|
+
end
|
101
|
+
|
102
|
+
# assert(newCount > oldCount)
|
103
|
+
newCount -= oldCount
|
104
|
+
|
105
|
+
break if (stack.length == 0)
|
106
|
+
state.pos += oldCount
|
107
|
+
oldCount = stack.pop()
|
108
|
+
end
|
109
|
+
|
110
|
+
if (stack.length == 0)
|
111
|
+
startCount = oldCount
|
112
|
+
found = true
|
113
|
+
break
|
114
|
+
end
|
115
|
+
state.pos += count
|
116
|
+
next
|
117
|
+
end
|
118
|
+
|
119
|
+
stack.push(count) if (res[:can_open])
|
120
|
+
state.pos += count
|
121
|
+
next
|
122
|
+
end
|
123
|
+
|
124
|
+
state.md.inline.skipToken(state)
|
125
|
+
end
|
126
|
+
|
127
|
+
if (!found)
|
128
|
+
# parser failed to find ending tag, so it's not valid emphasis
|
129
|
+
state.pos = start
|
130
|
+
return false
|
131
|
+
end
|
132
|
+
|
133
|
+
# found!
|
134
|
+
state.posMax = state.pos
|
135
|
+
state.pos = start + startCount
|
136
|
+
|
137
|
+
# Earlier we checked !silent, but this implementation does not need it
|
138
|
+
|
139
|
+
# we have `startCount` starting and ending markers,
|
140
|
+
# now trying to serialize them into tokens
|
141
|
+
count = startCount
|
142
|
+
while count > 1
|
143
|
+
token = state.push('strong_open', 'strong', 1)
|
144
|
+
token.markup = marker.chr + marker.chr
|
145
|
+
count -= 2
|
146
|
+
end
|
147
|
+
if (count % 2 == 1)
|
148
|
+
token = state.push('em_open', 'em', 1)
|
149
|
+
token.markup = marker.chr
|
150
|
+
end
|
151
|
+
|
152
|
+
state.md.inline.tokenize(state)
|
153
|
+
|
154
|
+
if (count % 2 == 1)
|
155
|
+
token = state.push('em_close', 'em', -1)
|
156
|
+
token.markup = marker.chr
|
157
|
+
end
|
158
|
+
count = startCount
|
159
|
+
while count > 1
|
160
|
+
token = state.push('strong_close', 'strong', -1)
|
161
|
+
token.markup = marker.chr + marker.chr
|
162
|
+
count -= 2
|
163
|
+
end
|
164
|
+
|
165
|
+
state.pos = state.posMax + startCount
|
166
|
+
state.posMax = max
|
167
|
+
return true
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|