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,220 @@
|
|
1
|
+
module Markascend
|
2
|
+
class Parser
|
3
|
+
REC_START = /\A[\+\-\>]\ /
|
4
|
+
NON_PARA_START = /
|
5
|
+
^[\+\-\>]\ # rec block
|
6
|
+
|
|
7
|
+
^\|\ *(?!\d)\w*\ *$ # block code
|
8
|
+
|
|
9
|
+
^h[1-6](?:\#\w+(?:-\w+)*)?\ # header
|
10
|
+
/x
|
11
|
+
REC_BLOCK_STARTS = {
|
12
|
+
'+ ' => /\+\ /,
|
13
|
+
'- ' => /\-\ /,
|
14
|
+
'> ' => /\>\ /
|
15
|
+
}
|
16
|
+
|
17
|
+
def initialize env, src
|
18
|
+
@src = StringScanner.new src
|
19
|
+
@env = env
|
20
|
+
@top_level = @env.srcs.empty?
|
21
|
+
@env.srcs.push @src
|
22
|
+
end
|
23
|
+
|
24
|
+
def parse
|
25
|
+
@out = []
|
26
|
+
while parse_new_line or parse_rec_block or parse_hx or parse_block_code or parse_paragraph
|
27
|
+
end
|
28
|
+
unless @src.eos?
|
29
|
+
@env.warn 'reached end of input'
|
30
|
+
end
|
31
|
+
@env.srcs.pop
|
32
|
+
|
33
|
+
@out.map! do |(node, content)|
|
34
|
+
case node
|
35
|
+
when :footnode_id_ref
|
36
|
+
if content < 1 or content > @env.footnotes.size
|
37
|
+
raise "footnote not defined: #{content}"
|
38
|
+
end
|
39
|
+
%Q|<a href="#footnote-#{content}">#{content}</a>|
|
40
|
+
when :footnode_acronym_ref
|
41
|
+
unless index = @env.footnotes.find_index{|k, _| k == content }
|
42
|
+
raise "footnote note defined: #{content}"
|
43
|
+
end
|
44
|
+
%Q|<a href="#footnote-#{index + 1}">#{content}</a>|
|
45
|
+
else
|
46
|
+
node
|
47
|
+
end
|
48
|
+
end
|
49
|
+
@out.join
|
50
|
+
end
|
51
|
+
|
52
|
+
def warnings
|
53
|
+
@env.warnings
|
54
|
+
end
|
55
|
+
|
56
|
+
def parse_new_line
|
57
|
+
if @src.scan(/\ *\n/)
|
58
|
+
@out << '<br>'
|
59
|
+
true
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def parse_rec_block
|
64
|
+
return unless @src.match? REC_START
|
65
|
+
|
66
|
+
# first elem, scans the whole of following string:
|
67
|
+
# |
|
68
|
+
# + line 1 of the li. an embed \macro
|
69
|
+
# macro content
|
70
|
+
# line 2 of the li.
|
71
|
+
# line 3 of the li.
|
72
|
+
#
|
73
|
+
# NOTE that first line is always not indented
|
74
|
+
line, block = scan_line_and_block 2
|
75
|
+
return unless line
|
76
|
+
rec_start = line[REC_START]
|
77
|
+
wrapper_begin, elem_begin, elem_end, wrapper_end =
|
78
|
+
case rec_start
|
79
|
+
when '+ '; ['<ol>', '<li>', '</li>', '</ol>']
|
80
|
+
when '- '; ['<ul>', '<li>', '</li>', '</ul>']
|
81
|
+
when '> '; ['', '<quote>', '</quote>', '']
|
82
|
+
end
|
83
|
+
elems = ["#{line[2..-1]}#{block}"]
|
84
|
+
|
85
|
+
# followed up elems
|
86
|
+
block_start_re = REC_BLOCK_STARTS[rec_start]
|
87
|
+
while @src.match?(block_start_re)
|
88
|
+
line, block = scan_line_and_block 2
|
89
|
+
break unless line
|
90
|
+
elems << "#{line[2..-1]}#{block}"
|
91
|
+
end
|
92
|
+
|
93
|
+
# generate
|
94
|
+
@out << wrapper_begin
|
95
|
+
elems.each do |elem|
|
96
|
+
@out << elem_begin
|
97
|
+
elem.rstrip!
|
98
|
+
@out << Parser.new(@env, elem).parse
|
99
|
+
@out << elem_end
|
100
|
+
end
|
101
|
+
@out << wrapper_end
|
102
|
+
true
|
103
|
+
end
|
104
|
+
|
105
|
+
def parse_hx
|
106
|
+
hx = @src.scan /h[1-6](\#\w+(-\w+)*)?\ /
|
107
|
+
return unless hx
|
108
|
+
hx.strip!
|
109
|
+
|
110
|
+
# fiddle id
|
111
|
+
if @env.sandbox
|
112
|
+
if @env.toc
|
113
|
+
id = "-#{@env.toc.size}"
|
114
|
+
end
|
115
|
+
else
|
116
|
+
if hx.size > 2
|
117
|
+
id = hx[3..-1]
|
118
|
+
elsif @env.toc
|
119
|
+
id = "-#{@env.toc.size}"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
if id
|
123
|
+
id_attr = %Q{ id="#{id}"}
|
124
|
+
end
|
125
|
+
hx = hx[0...2]
|
126
|
+
|
127
|
+
@out << "<#{hx}#{id_attr}>"
|
128
|
+
line, block = scan_line_and_block
|
129
|
+
if line
|
130
|
+
out = []
|
131
|
+
LineUnit.new(@env, line, block).parse(out)
|
132
|
+
out.pop if out.last == :"<br>"
|
133
|
+
out.each do |token|
|
134
|
+
@out << token
|
135
|
+
end
|
136
|
+
if id and @env.toc
|
137
|
+
@env.toc[id] = [hx[1].to_i, out.join]
|
138
|
+
end
|
139
|
+
end
|
140
|
+
@out << "</#{hx}>"
|
141
|
+
|
142
|
+
# consume one more empty line if possible
|
143
|
+
@src.scan /\ *\n/ if (!block or block.empty?)
|
144
|
+
true
|
145
|
+
end
|
146
|
+
|
147
|
+
def parse_block_code
|
148
|
+
if lang = @src.scan(/\|\ *(?!\d)\w*\ *\n/)
|
149
|
+
lang = lang[1..-1].strip
|
150
|
+
if lang.empty? and @env.hi
|
151
|
+
lang = @env.hi
|
152
|
+
end
|
153
|
+
block = @src.scan(/
|
154
|
+
(
|
155
|
+
\ *\n # empty line
|
156
|
+
|
|
157
|
+
\ {2,}.*\n # line indented equal to 2 or more than 2
|
158
|
+
)*
|
159
|
+
/x)
|
160
|
+
block.gsub!(/^ /, '')
|
161
|
+
block.rstrip!
|
162
|
+
@out << (::Markascend.hilite block, lang)
|
163
|
+
true
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def parse_paragraph
|
168
|
+
@src.match?(/\ */)
|
169
|
+
indent = @src.matched_size
|
170
|
+
line, block = scan_line_and_block
|
171
|
+
if line
|
172
|
+
@out << :"<p>" if @top_level
|
173
|
+
LineUnit.new(@env, line, block).parse(@out)
|
174
|
+
|
175
|
+
# same indent and not matching rec/code blocks
|
176
|
+
while (@src.match?(/\ */); @src.matched_size) == indent and !@src.match?(NON_PARA_START)
|
177
|
+
line, block = scan_line_and_block
|
178
|
+
break unless line
|
179
|
+
LineUnit.new(@env, line, block).parse(@out)
|
180
|
+
end
|
181
|
+
# consume one more empty line if possible
|
182
|
+
@src.scan /\ *\n/
|
183
|
+
# delete back last <br>
|
184
|
+
@out.pop if @out.last == :"<br>"
|
185
|
+
@out << :"</p>" if @top_level
|
186
|
+
true
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def scan_line_and_block undent=:all
|
191
|
+
if line = @src.scan(/.+?(?:\n|\z)/)
|
192
|
+
block = @src.scan(/
|
193
|
+
(?:\ *\n)* # leading empty lines
|
194
|
+
(?<indent>\ {2,})
|
195
|
+
.*?(?:\n|\z) # first line content
|
196
|
+
(
|
197
|
+
\g<indent>\ *
|
198
|
+
.*?(?:\n|\z) # rest line content
|
199
|
+
|
|
200
|
+
(?:\ *\n)* # rest empty line
|
201
|
+
)*
|
202
|
+
/x)
|
203
|
+
block = nil if block =~ /\A\s*\z/
|
204
|
+
# undent block
|
205
|
+
if block
|
206
|
+
if undent == :all
|
207
|
+
/
|
208
|
+
(?:\ *\n)*
|
209
|
+
(?<indent>\ {2,})
|
210
|
+
/x =~ block
|
211
|
+
block.gsub! /^#{indent}/, ''
|
212
|
+
else
|
213
|
+
block.gsub! /^\ \ /, ''
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
[line, block]
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Markascend
|
2
|
+
# video
|
3
|
+
|
4
|
+
class Macro
|
5
|
+
# accepts:
|
6
|
+
# |
|
7
|
+
# @somebody
|
8
|
+
def parse_twitter
|
9
|
+
if content.start_with?('@')
|
10
|
+
text = ::Markascend.escape_html content
|
11
|
+
link = "https://twitter.com/#{::Markascend.escape_attr content[1..-1]}"
|
12
|
+
else
|
13
|
+
# TODO embed tweet
|
14
|
+
raise 'not implemented yet'
|
15
|
+
end
|
16
|
+
%Q{<a href="#{link}">#{text}</a>}
|
17
|
+
end
|
18
|
+
|
19
|
+
# accepts:
|
20
|
+
# |
|
21
|
+
# @somebody
|
22
|
+
def parse_weibo
|
23
|
+
if content.start_with?('@')
|
24
|
+
text = ::Markascend.escape_html content
|
25
|
+
link = "https://weibo.com/#{::Markascend.escape_attr content[1..-1]}"
|
26
|
+
else
|
27
|
+
# TODO embed tweet
|
28
|
+
raise 'not implemented yet'
|
29
|
+
end
|
30
|
+
%Q{<a href="#{link}">#{text}</a>}
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse_wiki
|
34
|
+
%Q|<a href="http://en.wikipedia.org/wiki/#{content}">#{content}</a>|
|
35
|
+
end
|
36
|
+
|
37
|
+
# embed gist, accepts:
|
38
|
+
# |
|
39
|
+
# luikore/737238
|
40
|
+
# gist.github.com/luikore/737238
|
41
|
+
# https://gist.github.com/luikore/737238
|
42
|
+
def parse_gist
|
43
|
+
src = content.strip
|
44
|
+
if src =~ /\A\w+(\-\w+)*\/\d+/
|
45
|
+
src = "https://gist.github.com/#{src}"
|
46
|
+
else
|
47
|
+
src.sub! /\A(?=gist\.github\.com)/, 'https://'
|
48
|
+
end
|
49
|
+
src.sub!(/((?<!\.js)|\.git)$/, '.js')
|
50
|
+
%Q|<script src="#{src}"></script>|
|
51
|
+
end
|
52
|
+
|
53
|
+
# embed video, calculates embed iframe by urls from various simple formats, but not accept iframe code
|
54
|
+
def parse_video
|
55
|
+
# standard
|
56
|
+
unless /\A\s*(?<width>\d+)x(?<height>\d+)\s+(?<url>.+)\z/ =~ content
|
57
|
+
env.warn 'can not parse \video content, should be "#{WIDTH}x#{HEIGHT} #{URL}"'
|
58
|
+
return
|
59
|
+
end
|
60
|
+
|
61
|
+
case url
|
62
|
+
when /youtu\.?be/
|
63
|
+
# NOTE merging them into one regexp fails (because longest match?)
|
64
|
+
unless id = url[/(?<=watch\?v=)\w+/] || url[/(?<=embed\/)\w+/] || url[/(?<=youtu\.be\/)\w+/]
|
65
|
+
env.warn 'can not parse youtube id'
|
66
|
+
return
|
67
|
+
end
|
68
|
+
%Q|<iframe width="#{width}" height="#{height}" src="https://www.youtube-nocookie.com/embed/#{id}?rel=0" frameborder="0" allowfullscreen></iframe>|
|
69
|
+
when /vimeo/
|
70
|
+
unless id = url[/(?<=vimeo\.com\/)\w+/]
|
71
|
+
env.warn 'can not parse vimeo id, should use link like this "http://vimeo.com/#{DIGITS}"'
|
72
|
+
return
|
73
|
+
end
|
74
|
+
%Q|<iframe width="#{width}" height="#{height}" src="https://player.vimeo.com/video/#{id}" frameborder="0" allowFullScreen></iframe>|
|
75
|
+
when /sm/
|
76
|
+
unless id = url[/\bsm\d+/]
|
77
|
+
env.warn 'can not find "sm#{DIGITS}" from link'
|
78
|
+
return
|
79
|
+
end
|
80
|
+
%Q|<script src="https://ext.nicovideo.jp/thumb_watch/#{id}?w=#{width}&h=#{height}"></script>"|
|
81
|
+
else
|
82
|
+
env.warn 'failed to parse video link, currently only youtube, vimeo and niconico are supported'
|
83
|
+
return
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
data/rakefile
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
def osascript src
|
2
|
+
IO.popen 'osascript', 'r+' do |f|
|
3
|
+
f.puts src
|
4
|
+
f.close_write
|
5
|
+
f.read
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def get_default_browser
|
10
|
+
# search handler for http
|
11
|
+
res = osascript <<-APPLESCRIPT
|
12
|
+
tell (system attribute "sysv") to set MacOS_version to it mod 4096 div 16
|
13
|
+
if MacOS_version is 5 then
|
14
|
+
set {a1, a2} to {1, 2}
|
15
|
+
else
|
16
|
+
set {a1, a2} to {2, 1}
|
17
|
+
end if
|
18
|
+
set pListpath to (path to preferences as Unicode text) & "com.apple.LaunchServices.plist"
|
19
|
+
tell application "System Events"
|
20
|
+
repeat with i in property list items of property list item 1 of contents of property list file pListpath
|
21
|
+
if value of property list item a2 of i is "http" then
|
22
|
+
return value of property list item a1 of i
|
23
|
+
end if
|
24
|
+
end repeat
|
25
|
+
return "com.apple.Safari"
|
26
|
+
end tell
|
27
|
+
APPLESCRIPT
|
28
|
+
case res
|
29
|
+
when /canary/
|
30
|
+
"Google Chrome Canary"
|
31
|
+
when /chrome/
|
32
|
+
"Google Chrome"
|
33
|
+
when /safari/
|
34
|
+
"Safari"
|
35
|
+
else
|
36
|
+
raise "browser not supported yet"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# [window, tab]
|
41
|
+
def search_tab_with_url browser, url
|
42
|
+
case browser
|
43
|
+
when /Chrome/
|
44
|
+
res = osascript <<-APPLESCRIPT
|
45
|
+
set urls to ""
|
46
|
+
tell application "#{browser}"
|
47
|
+
set i to 1
|
48
|
+
repeat with w in windows
|
49
|
+
set j to 1
|
50
|
+
repeat with t in (tabs of w)
|
51
|
+
set u to the URL of t
|
52
|
+
set u to (i as text) & "," & (j as text) & u
|
53
|
+
set urls to urls & u & return
|
54
|
+
set j to j + 1
|
55
|
+
end repeat
|
56
|
+
set i to i + 1
|
57
|
+
end repeat
|
58
|
+
end tell
|
59
|
+
return urls
|
60
|
+
APPLESCRIPT
|
61
|
+
when /Safari/
|
62
|
+
# URL is not set when loading, so need a stupid null testing
|
63
|
+
res = osascript <<-APPLESCRIPT
|
64
|
+
set urls to ""
|
65
|
+
tell application "Safari"
|
66
|
+
set i to 1
|
67
|
+
repeat with w in windows
|
68
|
+
set j to 1
|
69
|
+
repeat with t in (tabs of w)
|
70
|
+
set u to the URL of t
|
71
|
+
set isNull to false
|
72
|
+
try
|
73
|
+
get u
|
74
|
+
on error
|
75
|
+
set isNull to true
|
76
|
+
end try
|
77
|
+
if isNull then
|
78
|
+
else
|
79
|
+
set u to (i as text) & "," & (j as text) & u
|
80
|
+
set urls to urls & u & return
|
81
|
+
end if
|
82
|
+
set j to j + 1
|
83
|
+
end repeat
|
84
|
+
set i to i + 1
|
85
|
+
end repeat
|
86
|
+
end tell
|
87
|
+
return urls
|
88
|
+
APPLESCRIPT
|
89
|
+
end
|
90
|
+
res.strip.split(/[\r\n]+/).each do |u|
|
91
|
+
if u.index url
|
92
|
+
/^(?<window>\d+),(?<tab>\d+)/ =~ u
|
93
|
+
return [window, tab]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
nil
|
97
|
+
end
|
98
|
+
|
99
|
+
def activate_url url
|
100
|
+
browser = get_default_browser
|
101
|
+
window, tab = search_tab_with_url browser, url
|
102
|
+
url.gsub! '"', '\"'
|
103
|
+
if tab # activate existing doc
|
104
|
+
case browser
|
105
|
+
when /Chrome/
|
106
|
+
osascript <<-APPLESCRIPT
|
107
|
+
tell application "#{browser}"
|
108
|
+
activate
|
109
|
+
set active tab index of window #{window} to #{tab}
|
110
|
+
reload tab #{tab} of window #{window}
|
111
|
+
end tell
|
112
|
+
APPLESCRIPT
|
113
|
+
when /Safari/
|
114
|
+
osascript <<-APPLESCRIPT
|
115
|
+
tell application "#{browser}"
|
116
|
+
activate
|
117
|
+
tell window #{window} to set current tab to tab #{tab}
|
118
|
+
set the url of the front document to "#{url}"
|
119
|
+
end tell
|
120
|
+
APPLESCRIPT
|
121
|
+
end
|
122
|
+
else # open new doc
|
123
|
+
case browser
|
124
|
+
when /Chrome/
|
125
|
+
# NOTE some user sets handler for .html to editor than browser
|
126
|
+
# so we don't use `open` command here
|
127
|
+
osascript <<-APPLESCRIPT
|
128
|
+
tell application "#{browser}"
|
129
|
+
activate
|
130
|
+
make new tab at the end of tabs of window 1
|
131
|
+
set the URL of active tab of window 1 to "#{url}"
|
132
|
+
end tell
|
133
|
+
APPLESCRIPT
|
134
|
+
when /Safari/
|
135
|
+
osascript <<-APPLESCRIPT
|
136
|
+
tell application "#{browser}"
|
137
|
+
activate
|
138
|
+
make new document at the end of documents
|
139
|
+
set the url of the front document to "#{url}"
|
140
|
+
end tell
|
141
|
+
APPLESCRIPT
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
desc "run tests"
|
147
|
+
task :default do
|
148
|
+
dir = File.dirname __FILE__
|
149
|
+
Dir.glob "#{dir}/test/*_test.rb" do |f|
|
150
|
+
require f
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
desc "generate gh-pages"
|
155
|
+
task :site do
|
156
|
+
# layout
|
157
|
+
require "slim"
|
158
|
+
template = Slim::Template.new "doc/layout.slim"
|
159
|
+
|
160
|
+
# html
|
161
|
+
require_relative "lib/markascend"
|
162
|
+
%w[syntax api].each do |doc|
|
163
|
+
File.open "gh-pages/#{doc}.html", 'w' do |f|
|
164
|
+
f.puts template.render{ Markascend.compile File.read "doc/#{doc}.ma" }
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# css
|
169
|
+
system 'sass --compass -C doc/style.sass gh-pages/style.css'
|
170
|
+
end
|
171
|
+
|
172
|
+
desc "preview gh-pages in default browser"
|
173
|
+
task :preview => :site do
|
174
|
+
url = File.expand_path "gh-pages/syntax.html"
|
175
|
+
activate_url "file://#{url}"
|
176
|
+
end
|