markascend 0.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.
- 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
|