moro-piki_doc 0.0.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/Changelog ADDED
@@ -0,0 +1,8 @@
1
+ == 0.0.2.1 / 2009-04-30
2
+
3
+ * fake version up to genarate gem on GitHub.
4
+
5
+ == 0.0.1 / 2009-04-20
6
+
7
+ * initial release
8
+
data/README.ja.hiki ADDED
@@ -0,0 +1,71 @@
1
+ ! PikiDocについて
2
+
3
+ !! はじめに
4
+
5
+ [[HikiDoc|http://hikidoc.rubyforge.org/svn/trunk/README.ja]]を拡張し、
6
+ HikiDocの機能の一つである「プラグイン」機能を簡単にを追加できるようにした
7
+ ライブラリです。
8
+
9
+ 本家HikiDocライブラリもバンドルしているため、このライブラリを入れるだけで
10
+ 使えます。
11
+
12
+ !! 使い方
13
+
14
+ プラグインを追加する部分以外は、本家の機能がそのまま使えます。
15
+
16
+ require 'piki_doc'
17
+ PikiDoc.to_xhtml(<<-EOS
18
+ ! PikiDocについて
19
+
20
+ !! はじめに
21
+ EOS
22
+
23
+ !!! プラグインの使い方
24
+
25
+ あらかじめPikiDoc.register()にプラグインを指定してください。今のところ、Gist
26
+ プラグインとASINプラグインがバンドルされています。
27
+
28
+ PikiDoc.register(PikiDoc::Bundles::Gist.new, PikiDoc::Bundles::Asin.new)
29
+ PikiDoc.to_xhtml(<<-HTML)
30
+ !!!! ASIN
31
+
32
+ インライン {{asin('4797336625', 'morodiary05-22')}}
33
+
34
+ {{asin('4797336625', 'morodiary05-22')}}
35
+
36
+ !!!! Gist
37
+
38
+ インライン {{gist('88086')}}
39
+
40
+ {{gist('88086')}}
41
+ HTML
42
+
43
+ 次のように出力されます
44
+
45
+ !!!! ASIN
46
+
47
+ インライン {{asin('4797336625', 'morodiary05-22')}}
48
+
49
+ {{asin('4797336625', 'morodiary05-22')}}
50
+
51
+ !!!! Gist
52
+
53
+ インライン {{gist(88086)}}
54
+
55
+ {{gist(88086)}}
56
+
57
+ !! プラグインの自作方法
58
+
59
+ lib/piki_doc/bundles/内のサンプルと、features/*内の外部仕様を参考にしてください。
60
+
61
+ !! Thanks
62
+
63
+ !!! HikiDoc authors
64
+
65
+ - 2005, Kazuhiko <kazuhiko@fdiary.net>
66
+ - 2007, Minero Aoki
67
+
68
+ !! ライセンス
69
+
70
+ HikiDoc本家と同じ修正BSDライセンスです。
71
+
data/Rakefile ADDED
@@ -0,0 +1,97 @@
1
+ $: << "./lib"
2
+ require 'piki_doc'
3
+
4
+ require 'rake/clean'
5
+ require 'rake/testtask'
6
+ require 'rake/packagetask'
7
+ require 'rake/gempackagetask'
8
+ require 'spec/rake/spectask'
9
+ require 'cucumber/rake/task'
10
+
11
+ NAME = ENV["GEMNAME"] || "piki_doc"
12
+ HOMEPAGE = "http://github.com/moro/#{NAME}/"
13
+ AUTHOR = "MOROHASHI Kyosuke"
14
+ EMAIL = "moronatural@gmail.com"
15
+ DESCRIPTION = "extended HikiDoc to able to plug plugins."
16
+ VERS = PikiDoc::Version
17
+
18
+ CLEAN.include ['**/.*.sw?', '*.gem', '.config']
19
+ RDOC_OPTS = [
20
+ '--title', "#{NAME} documentation",
21
+ "--charset", "utf-8",
22
+ "--opname", "index.html",
23
+ "--line-numbers",
24
+ "--main", "README.ja.hiki",
25
+ "--inline-source",
26
+ ]
27
+
28
+ Spec::Rake::SpecTask.new do |t|
29
+ t.warning = true
30
+ t.spec_opts = %w[--format progress --color]
31
+ end
32
+
33
+ Cucumber::Rake::Task.new do |t|
34
+ t.cucumber_opts = "--format progress --language ja"
35
+ end
36
+
37
+ gemspec = Gem::Specification.new do |s|
38
+ s.name = NAME
39
+ s.version = VERS
40
+ s.platform = Gem::Platform::RUBY
41
+ s.has_rdoc = false
42
+ s.extra_rdoc_files = ["README.ja.hiki", "Changelog"]
43
+ s.rdoc_options += RDOC_OPTS + ['--exclude', '^(examples|extras)/']
44
+ s.summary = DESCRIPTION
45
+ s.description = DESCRIPTION
46
+ s.author = AUTHOR
47
+ s.email = EMAIL
48
+ s.homepage = HOMEPAGE
49
+ s.executables = []
50
+ s.require_path = "lib"
51
+ s.test_files = Dir["spec/**/*_spec.rb"] + Dir["features/**/*.rb"] + Dir["features/*.feature"]
52
+
53
+ s.files = %w(README.ja.hiki Changelog Rakefile) +
54
+ Dir.glob("{bin,doc,lib,templates,generators,extras,website,script}/**/*") +
55
+ Dir.glob("ext/**/*.{h,c,rb}") +
56
+ Dir.glob("examples/**/*.rb") +
57
+ Dir.glob("tools/*.rb") +
58
+ Dir.glob("rails/*.rb")
59
+
60
+ s.extensions = FileList["ext/**/extconf.rb"].to_a
61
+ end
62
+
63
+ Rake::GemPackageTask.new(gemspec) do |p|
64
+ p.need_tar = true
65
+ p.gem_spec = gemspec
66
+ end
67
+
68
+ task :install do
69
+ name = "#{NAME}-#{VERS}.gem"
70
+ sh %{rake package}
71
+ sh %{gem install pkg/#{name}}
72
+ end
73
+
74
+ task :uninstall => [:clean] do
75
+ sh %{gem uninstall #{NAME}}
76
+ end
77
+
78
+ desc 'Show information about the gem.'
79
+ task :debug_gem do
80
+ puts gemspec.to_ruby
81
+ end
82
+
83
+ desc 'Update gem spec'
84
+ task :gemspec do
85
+ open("#{NAME}.gemspec", 'w').write gemspec.to_ruby
86
+ end
87
+
88
+ desc "output example from README.ja.hiki"
89
+ task "readme_example" do
90
+ require 'piki_doc'
91
+ require 'piki_doc/bundles/gist'
92
+ require 'piki_doc/bundles/asin'
93
+
94
+ PikiDoc.register(PikiDoc::Bundles::Gist.new, PikiDoc::Bundles::Asin.new)
95
+ puts PikiDoc.to_xhtml(File.read("README.ja.hiki"))
96
+ end
97
+
@@ -0,0 +1,24 @@
1
+ フィーチャ: Asinプラグインを試す
2
+ プラグイン作者として
3
+ Asinプラグインがちゃんと動いていることを確認したい
4
+
5
+ 背景:
6
+ 前提 "Asin"プラグインを登録する
7
+
8
+ シナリオ: ブロックプラグイン
9
+ もし 以下のHiki記法テキストをXHTMLにする
10
+ """
11
+ ! ASIN Railsレシピブック
12
+ {{asin('4797336625', 'morodiary05-22')}}
13
+ """
14
+ # かつ 結果を目視
15
+ ならば "div.plugin.asin > iframe"要素が含まれること
16
+
17
+ シナリオ: インラインプラグイン
18
+ もし 以下のHiki記法テキストをXHTMLにする
19
+ """
20
+ Railsレシピブック {{asin('4797336625', 'morodiary05-22')}}を書きました。
21
+ """
22
+ # かつ 結果を目視
23
+ ならば "span.plugin.asin > a[href='http://www.amazon.co.jp/exec/obidos/ASIN/4797336625/morodiary05-22/ref=nosim']"要素が含まれること
24
+
@@ -0,0 +1,27 @@
1
+ フィーチャ: Gistプラグインを試す
2
+ プラグイン作者として
3
+ Gistプラグインがちゃんと動いていることを確認したい
4
+
5
+ 背景:
6
+ 前提 "Gist"プラグインを登録する
7
+
8
+ シナリオ: ブロックプラグイン
9
+ もし 以下のHiki記法テキストをXHTMLにする
10
+ """
11
+ ! Gist
12
+ {{gist(123456)}}
13
+ """
14
+ # かつ 結果を目視
15
+ ならば "div.plugin.gist > script[src='http://gist.github.com/123456.js']"要素が含まれること
16
+ かつ "h1"要素のテキストは"Gist"であること
17
+
18
+ シナリオ: インラインプラグイン
19
+ もし 以下のHiki記法テキストをXHTMLにする
20
+ """
21
+ ! Gist
22
+ こちらをご覧ください。{{gist(123456)}}
23
+ """
24
+ # かつ 結果を目視
25
+ ならば "a[href='http://gist.github.com/123456']"要素が含まれること
26
+ かつ "h1"要素のテキストは"Gist"であること
27
+
@@ -0,0 +1,27 @@
1
+ require 'piki_doc'
2
+ require 'piki_doc/bundles/gist'
3
+ require 'piki_doc/bundles/asin'
4
+ require 'nokogiri'
5
+
6
+ When /^"([^\"]*)"プラグインを登録する$/ do |name|
7
+ PikiDoc.register(PikiDoc::Bundles.const_get(name).new)
8
+ end
9
+
10
+ When /^以下のHiki記法テキストをXHTMLにする$/ do |text|
11
+ @out = PikiDoc.to_xhtml(text)
12
+ end
13
+
14
+ Then /^"([^\"]*)"要素が含まれること$/ do |selector|
15
+ @doc ||= Nokogiri::HTML(@out)
16
+ @doc.css(selector).should_not be_empty
17
+ end
18
+
19
+ When /^"([^\"]*)"要素のテキストは"([^\"]*)"であること$/ do |selector, text|
20
+ @doc ||= Nokogiri::HTML(@out)
21
+ @doc.css(selector).text.should == text
22
+ end
23
+
24
+ When /^結果を目視$/ do
25
+ at_exit{ ["-" * 80, @out, "-" * 80].each{|s| puts s } }
26
+ end
27
+
@@ -0,0 +1,6 @@
1
+ # coding: utf-8
2
+ $KCODE = "u" if RUBY_VERSION < "1.9"
3
+
4
+ $LOAD_PATH << File.expand_path("../../lib", File.dirname(__FILE__))
5
+ require 'spec/expectations'
6
+
data/lib/piki_doc.rb ADDED
@@ -0,0 +1,11 @@
1
+
2
+ module PikiDoc
3
+ Version = "0.0.2.1"
4
+
5
+ autoload "Document", "piki_doc/document"
6
+ def register(*plugins); PikiDoc::Document.register(*plugins); end
7
+ def to_html(src, options={}); PikiDoc::Document.to_html(src, options={}) ; end
8
+ def to_xhtml(src, options={}); PikiDoc::Document.to_xhtml(src, options={}) ; end
9
+ module_function :register, :to_html, :to_xhtml
10
+ end
11
+
@@ -0,0 +1,25 @@
1
+ module PikiDoc
2
+ module Bundles
3
+ class Asin
4
+ include PluginAdapter
5
+ def initialize(host = "www.amazon.co.jp", iframe_host = "rcm-jp.amazon.co.jp")
6
+ @host = host
7
+ @iframe_host = iframe_host
8
+ end
9
+
10
+ def inline_plugin(src)
11
+ (asin, id), = src.scan(/\(\s*'(\d+)',\s*'([a-z0-9\-]+)'\s*\)/)
12
+ plugin_dom("span", <<-HTML)
13
+ <a href="http://#{@host}/exec/obidos/ASIN/#{asin}/#{id}/ref=nosim">Amazon</a>
14
+ HTML
15
+ end
16
+ def block_plugin(src)
17
+ (asin, id), = src.scan(/\(\s*'(\d+)',\s*'([a-z0-9\-]+)'\s*\)/)
18
+ plugin_dom("div", <<-HTML)
19
+ <iframe src="http://#{@iframe_host}/e/cm?t=#{id}&o=9&p=8&l=as1&asins=#{asin}&fc1=000000&IS2=1&lt1=_blank&m=amazon&lc1=0000FF&bc1=000000&bg1=FFFFFF&f=ifr&nou=1"
20
+ style="width:120px;height:240px;" scrolling="no" marginwidth="0" marginheight="0" frameborder="0"></iframe>
21
+ HTML
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,19 @@
1
+ require 'piki_doc/bundles/plugin_adapter'
2
+
3
+ module PikiDoc
4
+ module Bundles
5
+ class Gist
6
+ include PluginAdapter
7
+ def inline_plugin(src)
8
+ (id,), = src.scan(/gist\((\d+)\)/)
9
+ %Q[<a href="http://gist.github.com/#{id}">Gist:#{id}</a>]
10
+ end
11
+
12
+ private
13
+ def plugin(src)
14
+ (id,), = src.scan(/gist\((\d+)\)/)
15
+ %Q[<script src="http://gist.github.com/#{id}.js"></script>]
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,34 @@
1
+ module PikiDoc
2
+ module Bundles
3
+ module PluginAdapter
4
+ RE = /\A([a-z]+)\(.[^\)]+\)/
5
+ def accept?(src)
6
+ (name,_), = src.scan(RE)
7
+ name == plugin_name
8
+ end
9
+
10
+ def inline_plugin(src)
11
+ plugin_dom("span", plugin(src))
12
+ end
13
+
14
+ def block_plugin(src)
15
+ plugin_dom("div", plugin(src))
16
+ end
17
+
18
+ private
19
+ def plugin(src); raise NotImplementedError; end
20
+
21
+ def plugin_name
22
+ self.class.name.split("::").last.
23
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
24
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
25
+ downcase
26
+ end
27
+
28
+ def plugin_dom(tag, content)
29
+ %Q[<#{tag} class='plugin #{plugin_name}'>#{content}</#{tag}>]
30
+ end
31
+ end
32
+ end
33
+ end
34
+
@@ -0,0 +1,32 @@
1
+ require 'uri'
2
+ require 'piki_doc/output'
3
+ require 'piki_doc/image_uri_autolink_fix'
4
+ require 'vendor/hikidoc'
5
+
6
+ module PikiDoc
7
+ class Document < ::HikiDoc
8
+ @@plugins ||= []
9
+ include ImageUriAutolinkFix
10
+
11
+ class << self
12
+ def plugins; @@plugins; end
13
+ def register(*plugins)
14
+ @@plugins.concat(plugins)
15
+ end
16
+
17
+ def lint(plugin)
18
+ [:inline_plugin, :block_plugin].any?{|m| plugin.respond_to?(m) } && \
19
+ plugin.respond_to?(:accept?)
20
+ end
21
+
22
+ def to_xhtml(src, options = {})
23
+ new(::PikiDoc::HTMLOutput.new(" />", @@plugins), options).compile(src)
24
+ end
25
+
26
+ def to_html(src, options = {})
27
+ new(::PikiDoc::HTMLOutput.new(">"), @@plugins, options).compile(src)
28
+ end
29
+ end
30
+ end
31
+ end
32
+
@@ -0,0 +1,12 @@
1
+ module PikiDoc
2
+ module ImageUriAutolinkFix
3
+ private
4
+ def compile_uri_autolink(uri)
5
+ if(image?(path = URI(uri).path))
6
+ @output.image_hyperlink(uri, path.split(/\//).last)
7
+ else
8
+ super
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,27 @@
1
+ require 'vendor/hikidoc'
2
+
3
+ module PikiDoc
4
+ class HTMLOutput < ::HikiDoc::HTMLOutput
5
+ def initialize(suffix, plugins)
6
+ super(suffix)
7
+ @inlines = plugins.select{|p| p.respond_to?(:inline_plugin) }
8
+ @blocks = plugins.select{|p| p.respond_to?(:block_plugin) }
9
+ end
10
+
11
+ def inline_plugin(src)
12
+ if plugin = @inlines.detect{|p| p.accept?(src) }
13
+ plugin.inline_plugin(src)
14
+ else
15
+ super
16
+ end
17
+ end
18
+
19
+ def block_plugin(src)
20
+ if plugin = @blocks.detect{|p| p.accept?(src) }
21
+ @f.puts plugin.block_plugin(src)
22
+ else
23
+ super
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,902 @@
1
+ # Copyright (c) 2005, Kazuhiko <kazuhiko@fdiary.net>
2
+ # Copyright (c) 2007 Minero Aoki
3
+ # All rights reserved.
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are
7
+ # met:
8
+ #
9
+ # * Redistributions of source code must retain the above copyright
10
+ # notice, this list of conditions and the following disclaimer.
11
+ # * Redistributions in binary form must reproduce the above
12
+ # copyright notice, this list of conditions and the following
13
+ # disclaimer in the documentation and/or other materials provided
14
+ # with the distribution.
15
+ # * Neither the name of the HikiDoc nor the names of its
16
+ # contributors may be used to endorse or promote products derived
17
+ # from this software without specific prior written permission.
18
+ #
19
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ require "stringio"
32
+ require "strscan"
33
+ require "uri"
34
+ begin
35
+ require "syntax/convertors/html"
36
+ rescue LoadError
37
+ end
38
+
39
+ class HikiDoc
40
+ VERSION = "0.0.4" # FIXME
41
+
42
+ class Error < StandardError
43
+ end
44
+
45
+ class UnexpectedError < Error
46
+ end
47
+
48
+ def HikiDoc.to_html(src, options = {})
49
+ new(HTMLOutput.new(">"), options).compile(src)
50
+ end
51
+
52
+ def HikiDoc.to_xhtml(src, options = {})
53
+ new(HTMLOutput.new(" />"), options).compile(src)
54
+ end
55
+
56
+ def initialize(output, options = {})
57
+ @output = output
58
+ @options = default_options.merge(options)
59
+ @header_re = nil
60
+ @level = options[:level] || 1
61
+ @plugin_syntax = options[:plugin_syntax] || method(:valid_plugin_syntax?)
62
+ end
63
+
64
+ def compile(src)
65
+ @output.reset
66
+ escape_plugin_blocks(src) {|escaped|
67
+ compile_blocks escaped
68
+ @output.finish
69
+ }
70
+ end
71
+
72
+ # for backward compatibility
73
+ def to_html
74
+ $stderr.puts("warning: HikiDoc#to_html is deprecated. Please use HikiDoc.to_html or HikiDoc.to_xhtml instead.")
75
+ self.class.to_html(@output, @options)
76
+ end
77
+
78
+ private
79
+
80
+ def default_options
81
+ {
82
+ :allow_bracket_inline_image => true,
83
+ :use_wiki_name => true,
84
+ :use_not_wiki_name => true,
85
+ }
86
+ end
87
+
88
+ #
89
+ # Plugin
90
+ #
91
+
92
+ def valid_plugin_syntax?(code)
93
+ /['"]/ !~ code.gsub(/'(?:[^\\']+|\\.)*'|"(?:[^\\"]+|\\.)*"/m, "")
94
+ end
95
+
96
+ def escape_plugin_blocks(text)
97
+ s = StringScanner.new(text)
98
+ buf = ""
99
+ @plugin_blocks = []
100
+ while chunk = s.scan_until(/\{\{/)
101
+ tail = chunk[-2, 2]
102
+ chunk[-2, 2] = ""
103
+ buf << chunk
104
+ # plugin
105
+ if block = extract_plugin_block(s)
106
+ @plugin_blocks.push block
107
+ buf << "\0#{@plugin_blocks.size - 1}\0"
108
+ else
109
+ buf << "{{"
110
+ end
111
+ end
112
+ buf << s.rest
113
+ yield(buf)
114
+ end
115
+
116
+ def restore_plugin_block(str)
117
+ str.gsub(/\0(\d+)\0/) {
118
+ "{{" + plugin_block($1.to_i) + "}}"
119
+ }
120
+ end
121
+
122
+ def evaluate_plugin_block(str, buf = nil)
123
+ buf ||= @output.container
124
+ str.split(/(\0\d+\0)/).each do |s|
125
+ if s[0, 1] == "\0" and s[-1, 1] == "\0"
126
+ buf << @output.inline_plugin(plugin_block(s[1..-2].to_i))
127
+ else
128
+ buf << @output.text(s)
129
+ end
130
+ end
131
+ buf
132
+ end
133
+
134
+ def plugin_block(id)
135
+ @plugin_blocks[id] or raise UnexpectedError, "must not happen: #{id.inspect}"
136
+ end
137
+
138
+ def extract_plugin_block(s)
139
+ pos = s.pos
140
+ buf = ""
141
+ while chunk = s.scan_until(/\}\}/)
142
+ buf << chunk
143
+ buf.chomp!("}}")
144
+ if @plugin_syntax.call(buf)
145
+ return buf
146
+ end
147
+ buf << "}}"
148
+ end
149
+ s.pos = pos
150
+ nil
151
+ end
152
+
153
+ #
154
+ # Block Level
155
+ #
156
+
157
+ def compile_blocks(src)
158
+ f = LineInput.new(StringIO.new(src))
159
+ while line = f.peek
160
+ case line
161
+ when COMMENT_RE
162
+ f.gets
163
+ when HEADER_RE
164
+ compile_header f.gets
165
+ when HRULE_RE
166
+ f.gets
167
+ compile_hrule
168
+ when LIST_RE
169
+ compile_list f
170
+ when DLIST_RE
171
+ compile_dlist f
172
+ when TABLE_RE
173
+ compile_table f
174
+ when BLOCKQUOTE_RE
175
+ compile_blockquote f
176
+ when INDENTED_PRE_RE
177
+ compile_indented_pre f
178
+ when BLOCK_PRE_OPEN_RE
179
+ compile_block_pre f
180
+ else
181
+ if /^$/ =~ line
182
+ f.gets
183
+ next
184
+ end
185
+ compile_paragraph f
186
+ end
187
+ end
188
+ end
189
+
190
+ COMMENT_RE = %r<\A//>
191
+
192
+ def skip_comments(f)
193
+ f.while_match(COMMENT_RE) do |line|
194
+ end
195
+ end
196
+
197
+ HEADER_RE = /\A!+/
198
+
199
+ def compile_header(line)
200
+ @header_re ||= /\A!{1,#{7 - @level}}/
201
+ level = @level + (line.slice!(@header_re).size - 1)
202
+ title = strip(line)
203
+ @output.headline level, compile_inline(title)
204
+ end
205
+
206
+ HRULE_RE = /\A----$/
207
+
208
+ def compile_hrule
209
+ @output.hrule
210
+ end
211
+
212
+ ULIST = "*"
213
+ OLIST = "#"
214
+ LIST_RE = /\A#{Regexp.union(ULIST, OLIST)}+/
215
+
216
+ def compile_list(f)
217
+ typestack = []
218
+ level = 0
219
+ @output.list_begin
220
+ f.while_match(LIST_RE) do |line|
221
+ list_type = (line[0,1] == ULIST ? "ul" : "ol")
222
+ new_level = line.slice(LIST_RE).size
223
+ item = strip(line.sub(LIST_RE, ""))
224
+ if new_level > level
225
+ (new_level - level).times do
226
+ typestack.push list_type
227
+ @output.list_open list_type
228
+ @output.listitem_open
229
+ end
230
+ @output.listitem compile_inline(item)
231
+ elsif new_level < level
232
+ (level - new_level).times do
233
+ @output.listitem_close
234
+ @output.list_close typestack.pop
235
+ end
236
+ @output.listitem_close
237
+ @output.listitem_open
238
+ @output.listitem compile_inline(item)
239
+ elsif list_type == typestack.last
240
+ @output.listitem_close
241
+ @output.listitem_open
242
+ @output.listitem compile_inline(item)
243
+ else
244
+ @output.listitem_close
245
+ @output.list_close typestack.pop
246
+ @output.list_open list_type
247
+ @output.listitem_open
248
+ @output.listitem compile_inline(item)
249
+ typestack.push list_type
250
+ end
251
+ level = new_level
252
+ skip_comments f
253
+ end
254
+ level.times do
255
+ @output.listitem_close
256
+ @output.list_close typestack.pop
257
+ end
258
+ @output.list_end
259
+ end
260
+
261
+ DLIST_RE = /\A:/
262
+
263
+ def compile_dlist(f)
264
+ @output.dlist_open
265
+ f.while_match(DLIST_RE) do |line|
266
+ dt, dd = split_dlitem(line.sub(DLIST_RE, ""))
267
+ @output.dlist_item compile_inline(dt), compile_inline(dd)
268
+ skip_comments f
269
+ end
270
+ @output.dlist_close
271
+ end
272
+
273
+ def split_dlitem(line)
274
+ re = /\A((?:#{BRACKET_LINK_RE}|.)*?):/o
275
+ if m = re.match(line)
276
+ return m[1], m.post_match
277
+ else
278
+ return line, ""
279
+ end
280
+ end
281
+
282
+ TABLE_RE = /\A\|\|/
283
+
284
+ def compile_table(f)
285
+ lines = []
286
+ f.while_match(TABLE_RE) do |line|
287
+ lines.push line
288
+ skip_comments f
289
+ end
290
+ @output.table_open
291
+ lines.each do |line|
292
+ @output.table_record_open
293
+ split_columns(line.sub(TABLE_RE, "")).each do |col|
294
+ mid = col.sub!(/\A!/, "") ? "table_head" : "table_data"
295
+ span = col.slice!(/\A[\^>]*/)
296
+ rs = span_count(span, "^")
297
+ cs = span_count(span, ">")
298
+ @output.__send__(mid, compile_inline(col), rs, cs)
299
+ end
300
+ @output.table_record_close
301
+ end
302
+ @output.table_close
303
+ end
304
+
305
+ def split_columns(str)
306
+ cols = str.split(/\|\|/)
307
+ cols.pop if cols.last.chomp.empty?
308
+ cols
309
+ end
310
+
311
+ def span_count(str, ch)
312
+ c = str.count(ch)
313
+ c == 0 ? nil : c + 1
314
+ end
315
+
316
+ BLOCKQUOTE_RE = /\A""[ \t]?/
317
+
318
+ def compile_blockquote(f)
319
+ @output.blockquote_open
320
+ lines = []
321
+ f.while_match(BLOCKQUOTE_RE) do |line|
322
+ lines.push line.sub(BLOCKQUOTE_RE, "")
323
+ skip_comments f
324
+ end
325
+ compile_blocks lines.join("")
326
+ @output.blockquote_close
327
+ end
328
+
329
+ INDENTED_PRE_RE = /\A[ \t]/
330
+
331
+ def compile_indented_pre(f)
332
+ lines = f.span(INDENTED_PRE_RE)\
333
+ .map {|line| rstrip(line.sub(INDENTED_PRE_RE, "")) }
334
+ text = restore_plugin_block(lines.join("\n"))
335
+ @output.preformatted(@output.text(text))
336
+ end
337
+
338
+ BLOCK_PRE_OPEN_RE = /\A<<<\s*(\w+)?/
339
+ BLOCK_PRE_CLOSE_RE = /\A>>>/
340
+
341
+ def compile_block_pre(f)
342
+ m = BLOCK_PRE_OPEN_RE.match(f.gets) or raise UnexpectedError, "must not happen"
343
+ str = restore_plugin_block(f.break(BLOCK_PRE_CLOSE_RE).join.chomp)
344
+ f.gets
345
+ @output.block_preformatted(str, m[1])
346
+ end
347
+
348
+ BLANK = /\A$/
349
+ PARAGRAPH_END_RE = Regexp.union(BLANK,
350
+ HEADER_RE, HRULE_RE, LIST_RE, DLIST_RE,
351
+ BLOCKQUOTE_RE, TABLE_RE,
352
+ INDENTED_PRE_RE, BLOCK_PRE_OPEN_RE)
353
+
354
+ def compile_paragraph(f)
355
+ lines = f.break(PARAGRAPH_END_RE)\
356
+ .reject {|line| COMMENT_RE =~ line }
357
+ if lines.size == 1 and /\A\0(\d+)\0\z/ =~ strip(lines[0])
358
+ @output.block_plugin plugin_block($1.to_i)
359
+ else
360
+ line_buffer = @output.container(:paragraph)
361
+ lines.each_with_index do |line, i|
362
+ buffer = @output.container
363
+ line_buffer << buffer
364
+ compile_inline(lstrip(line).chomp, buffer)
365
+ end
366
+ @output.paragraph(line_buffer)
367
+ end
368
+ end
369
+
370
+ #
371
+ # Inline Level
372
+ #
373
+
374
+ BRACKET_LINK_RE = /\[\[.+?\]\]/
375
+ URI_RE = /(?:https?|ftp|file|mailto):[A-Za-z0-9;\/?:@&=+$,\-_.!~*\'()#%]+/
376
+ WIKI_NAME_RE = /\b(?:[A-Z]+[a-z\d]+){2,}\b/
377
+
378
+ def inline_syntax_re
379
+ if @options[:use_wiki_name]
380
+ if @options[:use_not_wiki_name]
381
+ / (#{BRACKET_LINK_RE})
382
+ | (#{URI_RE})
383
+ | (#{MODIFIER_RE})
384
+ | (\^?#{WIKI_NAME_RE})
385
+ /xo
386
+ else
387
+ / (#{BRACKET_LINK_RE})
388
+ | (#{URI_RE})
389
+ | (#{MODIFIER_RE})
390
+ | (#{WIKI_NAME_RE})
391
+ /xo
392
+ end
393
+ else
394
+ / (#{BRACKET_LINK_RE})
395
+ | (#{URI_RE})
396
+ | (#{MODIFIER_RE})
397
+ /xo
398
+ end
399
+ end
400
+
401
+ def compile_inline(str, buf = nil)
402
+ buf ||= @output.container
403
+ re = inline_syntax_re
404
+ pending_str = nil
405
+ while m = re.match(str)
406
+ str = m.post_match
407
+
408
+ link, uri, mod, wiki_name = m[1, 4]
409
+ if wiki_name and wiki_name[0, 1] == "^"
410
+ pending_str = m.pre_match + wiki_name[1..-1]
411
+ next
412
+ end
413
+
414
+ pre_str = "#{pending_str}#{m.pre_match}"
415
+ pending_str = nil
416
+ evaluate_plugin_block(pre_str, buf)
417
+ compile_inline_markup(buf, link, uri, mod, wiki_name)
418
+ end
419
+ evaluate_plugin_block(pending_str || str, buf)
420
+ buf
421
+ end
422
+
423
+ def compile_inline_markup(buf, link, uri, mod, wiki_name)
424
+ case
425
+ when link
426
+ buf << compile_bracket_link(link[2...-2])
427
+ when uri
428
+ buf << compile_uri_autolink(uri)
429
+ when mod
430
+ buf << compile_modifier(mod)
431
+ when wiki_name
432
+ buf << @output.wiki_name(wiki_name)
433
+ else
434
+ raise UnexpectedError, "must not happen"
435
+ end
436
+ end
437
+
438
+ def compile_bracket_link(link)
439
+ if m = /\A(?>[^|\\]+|\\.)*\|/.match(link)
440
+ title = m[0].chop
441
+ uri = m.post_match
442
+ fixed_uri = fix_uri(uri)
443
+ if can_image_link?(uri)
444
+ @output.image_hyperlink(fixed_uri, title)
445
+ else
446
+ @output.hyperlink(fixed_uri, compile_modifier(title))
447
+ end
448
+ else
449
+ fixed_link = fix_uri(link)
450
+ if can_image_link?(link)
451
+ @output.image_hyperlink(fixed_link)
452
+ else
453
+ @output.hyperlink(fixed_link, @output.text(link))
454
+ end
455
+ end
456
+ end
457
+
458
+ def can_image_link?(uri)
459
+ image?(uri) and @options[:allow_bracket_inline_image]
460
+ end
461
+
462
+ def compile_uri_autolink(uri)
463
+ if image?(uri)
464
+ @output.image_hyperlink(fix_uri(uri))
465
+ else
466
+ @output.hyperlink(fix_uri(uri), @output.text(uri))
467
+ end
468
+ end
469
+
470
+ def fix_uri(uri)
471
+ if /\A(?:https?|ftp|file):(?!\/\/)/ =~ uri
472
+ uri.sub(/\A\w+:/, "")
473
+ else
474
+ uri
475
+ end
476
+ end
477
+
478
+ IMAGE_EXTS = %w(.jpg .jpeg .gif .png)
479
+
480
+ def image?(uri)
481
+ IMAGE_EXTS.include?(File.extname(uri).downcase)
482
+ end
483
+
484
+ STRONG = "'''"
485
+ EM = "''"
486
+ DEL = "=="
487
+
488
+ STRONG_RE = /'''.+?'''/
489
+ EM_RE = /''.+?''/
490
+ DEL_RE = /==.+?==/
491
+
492
+ MODIFIER_RE = Regexp.union(STRONG_RE, EM_RE, DEL_RE)
493
+
494
+ MODTAG = {
495
+ STRONG => "strong",
496
+ EM => "em",
497
+ DEL => "del"
498
+ }
499
+
500
+ def compile_modifier(str)
501
+ buf = @output.container
502
+ while m = / (#{MODIFIER_RE})
503
+ /xo.match(str)
504
+ evaluate_plugin_block(m.pre_match, buf)
505
+ case
506
+ when chunk = m[1]
507
+ mod, s = split_mod(chunk)
508
+ mid = MODTAG[mod]
509
+ buf << @output.__send__(mid, compile_inline(s))
510
+ else
511
+ raise UnexpectedError, "must not happen"
512
+ end
513
+ str = m.post_match
514
+ end
515
+ evaluate_plugin_block(str, buf)
516
+ buf
517
+ end
518
+
519
+ def split_mod(str)
520
+ case str
521
+ when /\A'''/
522
+ return str[0, 3], str[3...-3]
523
+ when /\A''/
524
+ return str[0, 2], str[2...-2]
525
+ when /\A==/
526
+ return str[0, 2], str[2...-2]
527
+ else
528
+ raise UnexpectedError, "must not happen: #{str.inspect}"
529
+ end
530
+ end
531
+
532
+ def strip(str)
533
+ rstrip(lstrip(str))
534
+ end
535
+
536
+ def rstrip(str)
537
+ str.sub(/[ \t\r\n\v\f]+\z/, "")
538
+ end
539
+
540
+ def lstrip(str)
541
+ str.sub(/\A[ \t\r\n\v\f]+/, "")
542
+ end
543
+
544
+
545
+ class HTMLOutput
546
+ def initialize(suffix = " />")
547
+ @suffix = suffix
548
+ @f = nil
549
+ end
550
+
551
+ def reset
552
+ @f = StringIO.new
553
+ end
554
+
555
+ def finish
556
+ @f.string
557
+ end
558
+
559
+ def container(_for=nil)
560
+ case _for
561
+ when :paragraph
562
+ []
563
+ else
564
+ ""
565
+ end
566
+ end
567
+
568
+ #
569
+ # Procedures
570
+ #
571
+
572
+ def headline(level, title)
573
+ @f.puts "<h#{level}>#{title}</h#{level}>"
574
+ end
575
+
576
+ def hrule
577
+ @f.puts "<hr#{@suffix}"
578
+ end
579
+
580
+ def list_begin
581
+ end
582
+
583
+ def list_end
584
+ @f.puts
585
+ end
586
+
587
+ def list_open(type)
588
+ @f.puts "<#{type}>"
589
+ end
590
+
591
+ def list_close(type)
592
+ @f.print "</#{type}>"
593
+ end
594
+
595
+ def listitem_open
596
+ @f.print "<li>"
597
+ end
598
+
599
+ def listitem_close
600
+ @f.puts "</li>"
601
+ end
602
+
603
+ def listitem(item)
604
+ @f.print item
605
+ end
606
+
607
+ def dlist_open
608
+ @f.puts "<dl>"
609
+ end
610
+
611
+ def dlist_close
612
+ @f.puts "</dl>"
613
+ end
614
+
615
+ def dlist_item(dt, dd)
616
+ case
617
+ when dd.empty?
618
+ @f.puts "<dt>#{dt}</dt>"
619
+ when dt.empty?
620
+ @f.puts "<dd>#{dd}</dd>"
621
+ else
622
+ @f.puts "<dt>#{dt}</dt>"
623
+ @f.puts "<dd>#{dd}</dd>"
624
+ end
625
+ end
626
+
627
+ def table_open
628
+ @f.puts %Q(<table border="1">)
629
+ end
630
+
631
+ def table_close
632
+ @f.puts "</table>"
633
+ end
634
+
635
+ def table_record_open
636
+ @f.print "<tr>"
637
+ end
638
+
639
+ def table_record_close
640
+ @f.puts "</tr>"
641
+ end
642
+
643
+ def table_head(item, rs, cs)
644
+ @f.print "<th#{tdattr(rs, cs)}>#{item}</th>"
645
+ end
646
+
647
+ def table_data(item, rs, cs)
648
+ @f.print "<td#{tdattr(rs, cs)}>#{item}</td>"
649
+ end
650
+
651
+ def tdattr(rs, cs)
652
+ buf = ""
653
+ buf << %Q( rowspan="#{rs}") if rs
654
+ buf << %Q( colspan="#{cs}") if cs
655
+ buf
656
+ end
657
+ private :tdattr
658
+
659
+ def blockquote_open
660
+ @f.print "<blockquote>"
661
+ end
662
+
663
+ def blockquote_close
664
+ @f.puts "</blockquote>"
665
+ end
666
+
667
+ def block_preformatted(str, info)
668
+ syntax = info ? info.downcase : nil
669
+ if syntax
670
+ begin
671
+ convertor = Syntax::Convertors::HTML.for_syntax(syntax)
672
+ @f.puts convertor.convert(str)
673
+ return
674
+ rescue NameError, RuntimeError
675
+ end
676
+ end
677
+ preformatted(text(str))
678
+ end
679
+
680
+ def preformatted(str)
681
+ @f.print "<pre>"
682
+ @f.print str
683
+ @f.puts "</pre>"
684
+ end
685
+
686
+ def paragraph(lines)
687
+ @f.puts "<p>#{lines.join("\n")}</p>"
688
+ end
689
+
690
+ def block_plugin(str)
691
+ @f.puts %Q(<div class="plugin">{{#{escape_html(str)}}}</div>)
692
+ end
693
+
694
+ #
695
+ # Functions
696
+ #
697
+
698
+ def hyperlink(uri, title)
699
+ %Q(<a href="#{escape_html_param(uri)}">#{title}</a>)
700
+ end
701
+
702
+ def wiki_name(name)
703
+ hyperlink(name, text(name))
704
+ end
705
+
706
+ def image_hyperlink(uri, alt = nil)
707
+ alt ||= uri.split(/\//).last
708
+ alt = escape_html(alt)
709
+ %Q(<img src="#{escape_html_param(uri)}" alt="#{alt}"#{@suffix})
710
+ end
711
+
712
+ def strong(item)
713
+ "<strong>#{item}</strong>"
714
+ end
715
+
716
+ def em(item)
717
+ "<em>#{item}</em>"
718
+ end
719
+
720
+ def del(item)
721
+ "<del>#{item}</del>"
722
+ end
723
+
724
+ def text(str)
725
+ escape_html(str)
726
+ end
727
+
728
+ def inline_plugin(src)
729
+ %Q(<span class="plugin">{{#{src}}}</span>)
730
+ end
731
+
732
+ #
733
+ # Utilities
734
+ #
735
+
736
+ def escape_html_param(str)
737
+ escape_quote(escape_html(str))
738
+ end
739
+
740
+ def escape_html(text)
741
+ text.gsub(/&/, "&amp;").gsub(/</, "&lt;").gsub(/>/, "&gt;")
742
+ end
743
+
744
+ def unescape_html(text)
745
+ text.gsub(/&gt;/, ">").gsub(/&lt;/, "<").gsub(/&amp;/, "&")
746
+ end
747
+
748
+ def escape_quote(text)
749
+ text.gsub(/"/, "&quot;")
750
+ end
751
+ end
752
+
753
+
754
+ class LineInput
755
+ def initialize(f)
756
+ @input = f
757
+ @buf = []
758
+ @lineno = 0
759
+ @eof_p = false
760
+ end
761
+
762
+ def inspect
763
+ "\#<#{self.class} file=#{@input.inspect} line=#{lineno()}>"
764
+ end
765
+
766
+ def eof?
767
+ @eof_p
768
+ end
769
+
770
+ def lineno
771
+ @lineno
772
+ end
773
+
774
+ def gets
775
+ unless @buf.empty?
776
+ @lineno += 1
777
+ return @buf.pop
778
+ end
779
+ return nil if @eof_p # to avoid ARGF blocking.
780
+ line = @input.gets
781
+ line = line.sub(/\r\n/, "\n") if line
782
+ @eof_p = line.nil?
783
+ @lineno += 1
784
+ line
785
+ end
786
+
787
+ def ungets(line)
788
+ return unless line
789
+ @lineno -= 1
790
+ @buf.push line
791
+ line
792
+ end
793
+
794
+ def peek
795
+ line = gets()
796
+ ungets line if line
797
+ line
798
+ end
799
+
800
+ def next?
801
+ peek() ? true : false
802
+ end
803
+
804
+ def skip_blank_lines
805
+ n = 0
806
+ while line = gets()
807
+ unless line.strip.empty?
808
+ ungets line
809
+ return n
810
+ end
811
+ n += 1
812
+ end
813
+ n
814
+ end
815
+
816
+ def gets_if(re)
817
+ line = gets()
818
+ if not line or not (re =~ line)
819
+ ungets line
820
+ return nil
821
+ end
822
+ line
823
+ end
824
+
825
+ def gets_unless(re)
826
+ line = gets()
827
+ if not line or re =~ line
828
+ ungets line
829
+ return nil
830
+ end
831
+ line
832
+ end
833
+
834
+ def each
835
+ while line = gets()
836
+ yield line
837
+ end
838
+ end
839
+
840
+ def while_match(re)
841
+ while line = gets()
842
+ unless re =~ line
843
+ ungets line
844
+ return
845
+ end
846
+ yield line
847
+ end
848
+ nil
849
+ end
850
+
851
+ def getlines_while(re)
852
+ buf = []
853
+ while_match(re) do |line|
854
+ buf.push line
855
+ end
856
+ buf
857
+ end
858
+
859
+ alias span getlines_while # from Haskell
860
+
861
+ def until_match(re)
862
+ while line = gets()
863
+ if re =~ line
864
+ ungets line
865
+ return
866
+ end
867
+ yield line
868
+ end
869
+ nil
870
+ end
871
+
872
+ def getlines_until(re)
873
+ buf = []
874
+ until_match(re) do |line|
875
+ buf.push line
876
+ end
877
+ buf
878
+ end
879
+
880
+ alias break getlines_until # from Haskell
881
+
882
+ def until_terminator(re)
883
+ while line = gets()
884
+ return if re =~ line # discard terminal line
885
+ yield line
886
+ end
887
+ nil
888
+ end
889
+
890
+ def getblock(term_re)
891
+ buf = []
892
+ until_terminator(term_re) do |line|
893
+ buf.push line
894
+ end
895
+ buf
896
+ end
897
+ end
898
+ end
899
+
900
+ if __FILE__ == $0
901
+ puts HikiDoc.to_html(ARGF.read(nil))
902
+ end