markascend 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +9 -0
- data/copying +18 -0
- data/doc/api.ma +65 -0
- data/doc/index.ma +23 -0
- data/doc/syntax.ma +264 -0
- data/lib/markascend.rb +172 -0
- data/lib/markascend/builtin_macros.rb +154 -0
- data/lib/markascend/env.rb +68 -0
- data/lib/markascend/line_unit.rb +226 -0
- data/lib/markascend/macro.rb +13 -0
- data/lib/markascend/parser.rb +220 -0
- data/lib/markascend/popular_company_macros.rb +87 -0
- data/rakefile +176 -0
- data/readme +4 -0
- data/test/builtin_macros_test.rb +73 -0
- data/test/line_unit_test.rb +40 -0
- data/test/markascend_test.rb +19 -0
- data/test/parser_test.rb +93 -0
- data/test/popular_company_macros_test.rb +42 -0
- data/test/test_helper.rb +21 -0
- metadata +64 -0
@@ -0,0 +1,154 @@
|
|
1
|
+
module Markascend
|
2
|
+
class Macro
|
3
|
+
%w[del underline sub sup].each do |m|
|
4
|
+
eval <<-RUBY
|
5
|
+
def parse_#{m}
|
6
|
+
"<#{m}>\#{::Markascend.escape_html content}</#{m}>"
|
7
|
+
end
|
8
|
+
RUBY
|
9
|
+
end
|
10
|
+
|
11
|
+
def parse_img
|
12
|
+
s = ::StringScanner.new content
|
13
|
+
unless src = s.scan(/(\\\ |\S)+/)
|
14
|
+
env.warn "require src for \\img"
|
15
|
+
return
|
16
|
+
end
|
17
|
+
alt = Macro.scan_attr s, 'alt'
|
18
|
+
s.skip /\s+/
|
19
|
+
href = Macro.scan_attr s, 'href'
|
20
|
+
s.skip /\s+/
|
21
|
+
if alt2 = Macro.scan_attr(s, 'alt')
|
22
|
+
alt = alt2
|
23
|
+
end
|
24
|
+
unless s.eos?
|
25
|
+
env.warn "parse error for content of \\img"
|
26
|
+
return
|
27
|
+
end
|
28
|
+
|
29
|
+
alt = ::Markascend.escape_attr alt
|
30
|
+
if env.inline_img
|
31
|
+
begin
|
32
|
+
if env.pwd
|
33
|
+
Dir.chdir env.pwd do
|
34
|
+
data = open src, 'rb', &:read
|
35
|
+
end
|
36
|
+
else
|
37
|
+
data = open src, 'rb', &:read
|
38
|
+
end
|
39
|
+
mime = ::Markascend.mime data
|
40
|
+
src = "data:#{mime};base64,#{::Base64.strict_encode64 data}"
|
41
|
+
rescue
|
42
|
+
env.warn $!.message
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
img = %Q|<img src="#{src}" alt="#{alt}"/>|
|
47
|
+
if href
|
48
|
+
href = ::Markascend.escape_attr href
|
49
|
+
%Q|<a href="#{href}">#{img}</a>|
|
50
|
+
else
|
51
|
+
img
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def parse_html
|
56
|
+
# TODO sanitize in strict mode
|
57
|
+
content
|
58
|
+
end
|
59
|
+
|
60
|
+
def parse_slim
|
61
|
+
::Slim::Template.new(){content}.render env.scope
|
62
|
+
end
|
63
|
+
|
64
|
+
def parse_csv
|
65
|
+
Macro.generate_csv content, false
|
66
|
+
end
|
67
|
+
|
68
|
+
def parse_headless_csv
|
69
|
+
Macro.generate_csv content, true
|
70
|
+
end
|
71
|
+
|
72
|
+
def parse_latex
|
73
|
+
%Q|<code class="latex">#{content}</code>|
|
74
|
+
end
|
75
|
+
|
76
|
+
def parse_options
|
77
|
+
yaml = ::YAML.load(content) rescue nil
|
78
|
+
if yaml.is_a?(Hash)
|
79
|
+
env.options.merge! yaml
|
80
|
+
else
|
81
|
+
env.warn '\options content should be a yaml hash'
|
82
|
+
end
|
83
|
+
''
|
84
|
+
end
|
85
|
+
|
86
|
+
def parse_hi
|
87
|
+
# TODO validate syntax name
|
88
|
+
env.hi = content == 'none' ? nil : content
|
89
|
+
''
|
90
|
+
end
|
91
|
+
|
92
|
+
def parse_dot
|
93
|
+
err, out, code = nil
|
94
|
+
::Open3.popen3 'dot', '-Tpng' do |i, o, e, t|
|
95
|
+
i.puts content
|
96
|
+
i.close
|
97
|
+
err = e.read
|
98
|
+
out = o.read
|
99
|
+
code = t.value.to_i
|
100
|
+
e.close
|
101
|
+
o.close
|
102
|
+
end
|
103
|
+
if code != 0
|
104
|
+
env.warn err
|
105
|
+
return
|
106
|
+
end
|
107
|
+
|
108
|
+
data = ::Base64.strict_encode64 out
|
109
|
+
%Q|<img src="data:image/png;base64,#{data}" alt="#{Markascend.escape_attr content}"/>|
|
110
|
+
end
|
111
|
+
|
112
|
+
def Macro.generate_csv content, headless
|
113
|
+
rows = ::CSV.parse(content)
|
114
|
+
return if rows.empty?
|
115
|
+
|
116
|
+
table = "<table>"
|
117
|
+
unless headless
|
118
|
+
table << "<thead>"
|
119
|
+
head = rows.shift
|
120
|
+
head.map!{|e| "<th>#{::Markascend.escape_html e}</th>" }
|
121
|
+
table << "<tr>#{head.join}</tr>"
|
122
|
+
table << "</thead>"
|
123
|
+
end
|
124
|
+
|
125
|
+
table << "<tbody>"
|
126
|
+
rows.each do |row|
|
127
|
+
row.map!{|e| "<td>#{::Markascend.escape_html e}</td>" }
|
128
|
+
table << "<tr>#{row.join}</tr>"
|
129
|
+
end
|
130
|
+
table << "</tbody></table>"
|
131
|
+
end
|
132
|
+
|
133
|
+
def Macro.scan_attr strscan, attr_name
|
134
|
+
pos = strscan.pos
|
135
|
+
strscan.skip /\s+/
|
136
|
+
unless strscan.peek(attr_name.size) == attr_name
|
137
|
+
strscan.pos = pos
|
138
|
+
return
|
139
|
+
end
|
140
|
+
strscan.pos += attr_name.size
|
141
|
+
|
142
|
+
if strscan.scan(/\s*=\s*/)
|
143
|
+
# http://www.w3.org/TR/html5/syntax.html#attributes-0
|
144
|
+
if v = strscan.scan(/(["']).*?\1/)
|
145
|
+
v[1...-1]
|
146
|
+
else
|
147
|
+
strscan.scan(/\w+/)
|
148
|
+
end
|
149
|
+
else
|
150
|
+
attr_name
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Markascend
|
2
|
+
class Env
|
3
|
+
attr_reader :autolink, :inline_img, :sandbox, :toc, :pwd
|
4
|
+
attr_reader :macros, :line_units, :scope, :options, :footnotes, :srcs, :warnings
|
5
|
+
attr_accessor :hi
|
6
|
+
|
7
|
+
def initialize(autolink: %w[http https ftp mailto], inline_img: false, sandbox: false, toc: false, **opts)
|
8
|
+
@autolink = autolink
|
9
|
+
@inline_img = inline_img
|
10
|
+
@sandbox = sandbox
|
11
|
+
@toc = toc ? {} : false # {id => [x, header_content]}
|
12
|
+
|
13
|
+
if opts[:path]
|
14
|
+
pwd = File.dirname opts[:path]
|
15
|
+
if File.directory?(pwd)
|
16
|
+
@pwd = pwd
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
if opts[:macros]
|
21
|
+
@macros = {}
|
22
|
+
opts[:macros].each do |m|
|
23
|
+
meth = "parse_#{m}"
|
24
|
+
if Macro.respond_to?(meth)
|
25
|
+
@macros[m] = meth
|
26
|
+
else
|
27
|
+
raise ArgumentError, "macro processor #{meth} not defined"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
elsif @sandbox
|
31
|
+
@macros = SANDBOX_MACROS
|
32
|
+
else
|
33
|
+
@macros = DEFAULT_MACROS
|
34
|
+
end
|
35
|
+
|
36
|
+
if opts[:line_units]
|
37
|
+
@line_units = opts[:line_units].map do |m|
|
38
|
+
meth = "parse_#{m}"
|
39
|
+
if LineUnit.respond_to?(meth)
|
40
|
+
meth
|
41
|
+
else
|
42
|
+
raise ArgumentError, "line-unit parser #{meth} not defined"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
else
|
46
|
+
@line_units = DEFAULT_LINE_UNITS
|
47
|
+
end
|
48
|
+
|
49
|
+
@scope = opts[:scope] || Object.new.send(:binding)
|
50
|
+
@options = {} # for \options macro
|
51
|
+
@footnotes = {} # {abbrev => details}. for [.] and [:] elements
|
52
|
+
@srcs = [] # recursive parser stack, everyone has the contiguous right one scanned
|
53
|
+
@warnings = {} # {line => message}
|
54
|
+
@hi = nil # current syntax hiliter
|
55
|
+
end
|
56
|
+
|
57
|
+
def warn msg
|
58
|
+
if @srcs.size
|
59
|
+
current_src = @srcs.last
|
60
|
+
line = @srcs.first.string.count("\n") - current_src.string[(current_src.pos)..-1].count("\n")
|
61
|
+
@warnings[line] = msg
|
62
|
+
else
|
63
|
+
# warnings without source is set to line 0
|
64
|
+
@warnings[0] = msg
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,226 @@
|
|
1
|
+
module Markascend
|
2
|
+
LineUnit = ::Struct.new :env, :line, :block
|
3
|
+
# process a line with (maybe) a followed up indented block
|
4
|
+
class LineUnit
|
5
|
+
def parse out=nil
|
6
|
+
@out = []
|
7
|
+
@src = ::StringScanner.new line
|
8
|
+
parsers = env.line_units
|
9
|
+
while parsers.any?{|p| send p}
|
10
|
+
end
|
11
|
+
|
12
|
+
if out
|
13
|
+
@out.each do |token|
|
14
|
+
out << token
|
15
|
+
end
|
16
|
+
else
|
17
|
+
@out.join
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# the same as markdown
|
22
|
+
def parse_inline_code
|
23
|
+
if s = @src.scan(/
|
24
|
+
(`{1,})(\ ?)
|
25
|
+
.*?
|
26
|
+
\2\1
|
27
|
+
/x)
|
28
|
+
s =~ /^
|
29
|
+
(`{1,})(\ ?)
|
30
|
+
(.*?)
|
31
|
+
\2\1
|
32
|
+
$/x
|
33
|
+
@out << (::Markascend.hilite $3, env.hi, true)
|
34
|
+
true
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# no escape chars, but \\ and \$ are treated as atomic parsing units
|
39
|
+
def parse_math
|
40
|
+
if (s = @src.scan /\$(?:\\[\\\$]|[^\$])*\$/)
|
41
|
+
@out << '<code class="math">'
|
42
|
+
@out << (::Markascend.escape_html s[1...-1])
|
43
|
+
@out << '</code>'
|
44
|
+
true
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def parse_auto_link
|
49
|
+
# TODO make protocol configurable
|
50
|
+
if (s = @src.scan /(https|http|ftp|mailto)\:\/\/\S+/)
|
51
|
+
s.gsub! /"/, '\\"'
|
52
|
+
@out << '<a href="#{s}">'
|
53
|
+
@out << (::Markascend.escape_html s)
|
54
|
+
@out << '</a>'
|
55
|
+
true
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def parse_macro
|
60
|
+
start = @src.pos
|
61
|
+
return unless macro = @src.scan(/\\(?!\d)\w+/)
|
62
|
+
macro = macro[1..-1]
|
63
|
+
if block = scan_lexical_parens
|
64
|
+
inline_content = @src.string[start..(@src.pos)]
|
65
|
+
elsif block = scan_recursive_braces
|
66
|
+
inline_content = @src.string[start..(@src.pos)]
|
67
|
+
else
|
68
|
+
block = self.block
|
69
|
+
end
|
70
|
+
@out << ::Markascend::Macro.new(env, block, inline_content).parse(macro)
|
71
|
+
true
|
72
|
+
end
|
73
|
+
|
74
|
+
def parse_link
|
75
|
+
pos = @src.pos
|
76
|
+
return unless @src.scan(/\[/)
|
77
|
+
|
78
|
+
footnote = @src.scan(/[\.\:]/)
|
79
|
+
content = ''
|
80
|
+
loop do
|
81
|
+
if res = @src.scan(/\]/)
|
82
|
+
break
|
83
|
+
elsif res = parse_char(false)
|
84
|
+
content << res
|
85
|
+
else
|
86
|
+
# failed
|
87
|
+
@src.pos = pos
|
88
|
+
return
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
if footnote and env.sandbox
|
93
|
+
scan_lexical_parens || scan_recursive_braces
|
94
|
+
@out << ::Markascend.escape_html(@src.string[pos..(@src.pos)])
|
95
|
+
return true
|
96
|
+
end
|
97
|
+
|
98
|
+
case footnote
|
99
|
+
when '.'
|
100
|
+
unless explain = scan_lexical_parens || scan_recursive_braces
|
101
|
+
@src.pos = pos
|
102
|
+
return
|
103
|
+
end
|
104
|
+
|
105
|
+
footnotes = env.footnotes
|
106
|
+
if content =~ /\A\s*\z/
|
107
|
+
content = footnotes.size + 1
|
108
|
+
end
|
109
|
+
raise "Already defined footnote: #{content}" if footnotes.has_key?(content)
|
110
|
+
footnotes[content] = explain
|
111
|
+
# TODO id prefix configurable
|
112
|
+
@out << %Q|<a href="#footnote-#{footnotes.size}">#{content}</a>|
|
113
|
+
true
|
114
|
+
when ':'
|
115
|
+
footnotes = env.footnotes
|
116
|
+
if content =~ /\A\s*(\d+)\s*\z/
|
117
|
+
@out << [:footnote_id_ref, $1.to_i]
|
118
|
+
else
|
119
|
+
content.strip!
|
120
|
+
@out << [:footnote_acronym_ref, content]
|
121
|
+
end
|
122
|
+
true
|
123
|
+
else
|
124
|
+
if addr = scan_lexical_parens || scan_recursive_braces
|
125
|
+
# TODO smarter addr to recognize things like a.b.com
|
126
|
+
addr = ::Markascend.escape_attr addr
|
127
|
+
@out << %Q|<a href="#{addr}">#{content}</a>|
|
128
|
+
true
|
129
|
+
else
|
130
|
+
@src.pos = pos
|
131
|
+
nil
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def parse_bold_italic emit=true
|
137
|
+
pos = @src.pos
|
138
|
+
if s = @src.scan(/\*\*/)
|
139
|
+
term = /\*\*/
|
140
|
+
out = ['<b>']
|
141
|
+
out_end = '</b>'
|
142
|
+
elsif s = @src.scan(/\*/)
|
143
|
+
term = /\*/
|
144
|
+
out = ['<i>']
|
145
|
+
out_end = '</i>'
|
146
|
+
else
|
147
|
+
return
|
148
|
+
end
|
149
|
+
|
150
|
+
loop do
|
151
|
+
if res = @src.scan(/\\[\\\*]/)
|
152
|
+
out << res[1]
|
153
|
+
elsif @src.scan(term)
|
154
|
+
out << out_end
|
155
|
+
break
|
156
|
+
elsif res = parse_bold_italic(false)
|
157
|
+
out << res
|
158
|
+
elsif res = parse_char(false)
|
159
|
+
out << res
|
160
|
+
else
|
161
|
+
# failed
|
162
|
+
@src.pos = pos
|
163
|
+
return
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
if emit
|
168
|
+
@out << out.join
|
169
|
+
true
|
170
|
+
else
|
171
|
+
out.join
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def parse_char emit=true
|
176
|
+
# entity list generated by:
|
177
|
+
# ruby -rregexp_optimized_union -e'puts Regexp.optimized_union($stdin.read.scan(/(?<=ENTITY )\w+/)).source' < tools/html4_entities.dtd
|
178
|
+
# dtd from http://www.w3.org/TR/html4/sgml/entities.html
|
179
|
+
if c = @src.scan(/
|
180
|
+
&(?:
|
181
|
+
n(?:bsp|ot|tilde)|i(?:excl|quest|grave|acute|circ|uml)|c(?:e(?:nt|dil)|urren|opy|cedil)|p(?:ound|lusmn|ara)|y(?:en|acute|uml)|brvbar|s(?:ect|hy|up[123]|zlig)|u(?:ml|grave|acute|circ|uml)|o(?:rd[fm]|grave|acute|circ|tilde|uml|slash)|laquo|r(?:eg|aquo)|m(?:acr|i(?:cro|ddot))|d(?:eg|ivide)|a(?:c(?:ute|irc)|grave|acute|tilde|uml|ring|elig)|frac(?:1[24]|34)|A(?:grave|acute|circ|tilde|uml|ring|Elig)|Ccedil|E(?:grave|acute|circ|uml|TH)|I(?:grave|acute|circ|uml)|Ntilde|O(?:grave|acute|circ|tilde|uml|slash)|t(?:imes|horn)|U(?:grave|acute|circ|uml)|Yacute|THORN|e(?:grave|acute|circ|uml|th)
|
182
|
+
|\#\d{4}
|
183
|
+
|\#x\h{4}
|
184
|
+
);
|
185
|
+
/x)
|
186
|
+
elsif c = @src.scan(/\\\W/)
|
187
|
+
c = ::Markascend.escape_html c[1]
|
188
|
+
elsif c = @src.scan(/\n/)
|
189
|
+
c = :'<br>'
|
190
|
+
elsif c = @src.scan(/./)
|
191
|
+
c = ::Markascend.escape_html c
|
192
|
+
else
|
193
|
+
return
|
194
|
+
end
|
195
|
+
|
196
|
+
if emit
|
197
|
+
@out << c
|
198
|
+
true
|
199
|
+
else
|
200
|
+
c
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# {...}
|
205
|
+
def scan_recursive_braces
|
206
|
+
if s = @src.scan(/
|
207
|
+
(?<braces> \{
|
208
|
+
([^\{]+ | \g<braces>)* # recursive rule
|
209
|
+
\})
|
210
|
+
/x)
|
211
|
+
s[1...-1]
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# (...)
|
216
|
+
def scan_lexical_parens
|
217
|
+
if s = @src.scan(/
|
218
|
+
\(
|
219
|
+
(?: \\ [\\\)] | [^\)] )+ # lexical rule
|
220
|
+
\)
|
221
|
+
/x)
|
222
|
+
s[1...-1].gsub(/\\[\)\\]/){|x| x[1]}
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Markascend
|
2
|
+
# inline contains the src for fallback
|
3
|
+
Macro = ::Struct.new :env, :content, :inline
|
4
|
+
class Macro
|
5
|
+
def parse name
|
6
|
+
self.content ||= ''
|
7
|
+
if meth = env.macros[name]
|
8
|
+
res = send meth
|
9
|
+
end
|
10
|
+
res or (inline ? ::Markascend.escape_html(inline) : "\\#{name}")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|