asuka 0.5.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +6 -0
- data/LICENSE +20 -0
- data/README.rdoc +17 -0
- data/Rakefile +44 -0
- data/VERSION +1 -0
- data/bin/asuka +7 -0
- data/lib/asuka.rb +16 -0
- data/lib/asuka/accumulator.rb +27 -0
- data/lib/asuka/document.rb +122 -0
- data/lib/asuka/formatter.rb +53 -0
- data/lib/asuka/line_formatter.rb +24 -0
- data/lib/asuka/parser.rb +42 -0
- data/lib/asuka/rules.rb +106 -0
- data/samples/samples.asuka +98 -0
- data/spec/integration/parser_spec.rb +297 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/unit/accumulator_spec.rb +79 -0
- data/spec/unit/blockquote_spec.rb +25 -0
- data/spec/unit/document_spec.rb +59 -0
- data/spec/unit/formatter_spec.rb +93 -0
- data/spec/unit/header_spec.rb +12 -0
- data/spec/unit/line_formatter_spec.rb +132 -0
- data/spec/unit/line_group_spec.rb +14 -0
- data/spec/unit/paragraph_spec.rb +25 -0
- data/spec/unit/rule_spec.rb +11 -0
- data/spec/unit/unordered_list_spec.rb +25 -0
- metadata +114 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Jonathan Palardy
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
= asuka
|
2
|
+
|
3
|
+
Safe and pleasant text to HTML converer.
|
4
|
+
|
5
|
+
== Note on Patches/Pull Requests
|
6
|
+
|
7
|
+
* Fork the project.
|
8
|
+
* Make your feature addition or bug fix.
|
9
|
+
* Add tests for it. This is important so I don't break it in a
|
10
|
+
future version unintentionally.
|
11
|
+
* Commit, do not mess with rakefile, version, or history.
|
12
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
13
|
+
* Send me a pull request. Bonus points for topic branches.
|
14
|
+
|
15
|
+
== Copyright
|
16
|
+
|
17
|
+
Copyright (c) 2010 Jonathan Palardy. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "asuka"
|
8
|
+
gem.summary = %Q{Safe and pleasant text-to-HTML converter.}
|
9
|
+
gem.description = %Q{Markdown and textile -inspired markup that's XSS safe.}
|
10
|
+
gem.email = "jonathan@fetlife.com"
|
11
|
+
gem.homepage = "http://fetlife.com"
|
12
|
+
gem.authors = ["Jonathan Palardy"]
|
13
|
+
gem.add_development_dependency "rspec", ">= 1.2.9"
|
14
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
15
|
+
end
|
16
|
+
rescue LoadError
|
17
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
18
|
+
end
|
19
|
+
|
20
|
+
require 'spec/rake/spectask'
|
21
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
22
|
+
spec.libs << 'lib' << 'spec'
|
23
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
24
|
+
end
|
25
|
+
|
26
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
27
|
+
spec.libs << 'lib' << 'spec'
|
28
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
29
|
+
spec.rcov = true
|
30
|
+
end
|
31
|
+
|
32
|
+
task :spec => :check_dependencies
|
33
|
+
|
34
|
+
task :default => :spec
|
35
|
+
|
36
|
+
require 'rake/rdoctask'
|
37
|
+
Rake::RDocTask.new do |rdoc|
|
38
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
39
|
+
|
40
|
+
rdoc.rdoc_dir = 'rdoc'
|
41
|
+
rdoc.title = "asuka #{version}"
|
42
|
+
rdoc.rdoc_files.include('README*')
|
43
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
44
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.5.3
|
data/bin/asuka
ADDED
data/lib/asuka.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
|
2
|
+
require 'asuka/accumulator'
|
3
|
+
require 'asuka/document'
|
4
|
+
|
5
|
+
require 'asuka/formatter'
|
6
|
+
require 'asuka/line_formatter'
|
7
|
+
require 'asuka/rules'
|
8
|
+
|
9
|
+
require 'asuka/parser'
|
10
|
+
|
11
|
+
module Asuka
|
12
|
+
def self.new(*args, &block)
|
13
|
+
Asuka::Document.new(*args, &block)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Asuka
|
2
|
+
class Accumulator
|
3
|
+
def initialize
|
4
|
+
@lines = []
|
5
|
+
end
|
6
|
+
|
7
|
+
def push(line)
|
8
|
+
lines << line
|
9
|
+
end
|
10
|
+
|
11
|
+
def empty?
|
12
|
+
lines.empty?
|
13
|
+
end
|
14
|
+
|
15
|
+
def flush
|
16
|
+
return [] if empty?
|
17
|
+
|
18
|
+
yield @lines if block_given?
|
19
|
+
|
20
|
+
result, @lines = lines, []
|
21
|
+
result
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
attr_accessor :lines
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module Asuka
|
2
|
+
module HTMLable
|
3
|
+
def to_html
|
4
|
+
raise NotImplementedError, "%s does not implement the #to_html method" % [self.class.name]
|
5
|
+
end
|
6
|
+
|
7
|
+
def wrap(tag, content, indent=false)
|
8
|
+
["<#{tag}>", content, "</#{tag}>"].join(indent ? "\n" : "")
|
9
|
+
end
|
10
|
+
|
11
|
+
def wrap_indent(tag, content)
|
12
|
+
wrap(tag, content, true)
|
13
|
+
end
|
14
|
+
|
15
|
+
def ==(other)
|
16
|
+
to_html == other.to_html
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class LineGroup
|
21
|
+
include HTMLable
|
22
|
+
|
23
|
+
def initialize(lines)
|
24
|
+
@lines = lines
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
attr_accessor :lines
|
29
|
+
end
|
30
|
+
|
31
|
+
class Document < LineGroup
|
32
|
+
def initialize(lines, parser=Parser.new)
|
33
|
+
super(lines)
|
34
|
+
@parser = parser
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_html
|
38
|
+
children.map { |child| child.to_html }.join("\n")
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
attr_accessor :parser
|
43
|
+
|
44
|
+
def children
|
45
|
+
@children ||= parse
|
46
|
+
end
|
47
|
+
|
48
|
+
def parse
|
49
|
+
parser.parse(lines)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class Block < LineGroup
|
54
|
+
def initialize(lines, tag)
|
55
|
+
super(lines)
|
56
|
+
@tag = tag
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_html
|
60
|
+
wrap tag, lines.join("<br/>\n")
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
attr_accessor :tag
|
65
|
+
end
|
66
|
+
|
67
|
+
class Paragraph < Block
|
68
|
+
def initialize(lines)
|
69
|
+
super(lines, "p")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class Blockquote < Block
|
74
|
+
def initialize(lines)
|
75
|
+
super(lines, "blockquote")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class List < LineGroup
|
80
|
+
def initialize(lines, tag)
|
81
|
+
super(lines)
|
82
|
+
@tag = tag
|
83
|
+
end
|
84
|
+
|
85
|
+
def to_html
|
86
|
+
wrap_indent tag, lines.map { |line| ["<li>", line, "</li>"].join }.join("\n")
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
attr_accessor :tag
|
91
|
+
end
|
92
|
+
|
93
|
+
class UnorderedList < List
|
94
|
+
def initialize(lines)
|
95
|
+
super(lines, "ul")
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
class Rule
|
100
|
+
include HTMLable
|
101
|
+
|
102
|
+
def to_html
|
103
|
+
"<hr/>"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class Header
|
108
|
+
include HTMLable
|
109
|
+
|
110
|
+
def initialize(line, level)
|
111
|
+
@line = line
|
112
|
+
@tag = "h#{level}"
|
113
|
+
end
|
114
|
+
|
115
|
+
def to_html
|
116
|
+
wrap tag, line
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
attr_accessor :line, :tag
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
module Asuka
|
4
|
+
class Formatter
|
5
|
+
def default_steps
|
6
|
+
[:bold, :italic, :named_link]
|
7
|
+
end
|
8
|
+
|
9
|
+
def bold(line)
|
10
|
+
line.gsub(/\*\*([^*]+)\*\*/, '<strong>\1</strong>')
|
11
|
+
end
|
12
|
+
|
13
|
+
def italic(line)
|
14
|
+
line.gsub(/\*([^*]+)\*/, '<em>\1</em>')
|
15
|
+
end
|
16
|
+
|
17
|
+
# helper
|
18
|
+
def make_link(text, href, tags={})
|
19
|
+
tags = tags.merge(:href => href.strip)
|
20
|
+
tags_to_attrs = tags.sort_by { |name, value| name.to_s }.
|
21
|
+
map { |name, value| %Q{ #{name}="#{value}"} }
|
22
|
+
|
23
|
+
"<a#{tags_to_attrs}>#{text.strip}</a>"
|
24
|
+
end
|
25
|
+
|
26
|
+
def named_link(line)
|
27
|
+
# up to ] (spaces allowed)
|
28
|
+
up_to = /[^\]]+?/
|
29
|
+
brackets_without_http = /\[(#{up_to})\]/
|
30
|
+
|
31
|
+
# up to ] (spaces NOT allowed)
|
32
|
+
up_to = /[^\]\s]+/
|
33
|
+
brackets_with_http = /\[\s*(https?:\/\/#{up_to})\s*\]/
|
34
|
+
|
35
|
+
line.gsub(/#{brackets_without_http}#{brackets_with_http}/) { |match| make_link($1, $2) }.
|
36
|
+
gsub(/#{brackets_with_http}#{brackets_without_http}/) { |match| make_link($2, $1) }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class PreFormatter
|
41
|
+
def default_steps
|
42
|
+
[:strip, :html_escape]
|
43
|
+
end
|
44
|
+
|
45
|
+
def strip(line)
|
46
|
+
line.strip
|
47
|
+
end
|
48
|
+
|
49
|
+
def html_escape(line)
|
50
|
+
CGI::escapeHTML(line)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Asuka
|
2
|
+
class LineFormatter
|
3
|
+
attr_accessor :steps, :formatter
|
4
|
+
|
5
|
+
def initialize(formatter, *steps)
|
6
|
+
steps = steps.flatten
|
7
|
+
|
8
|
+
if steps.empty? && formatter.respond_to?(:default_steps)
|
9
|
+
@steps = formatter.default_steps
|
10
|
+
else
|
11
|
+
@steps = steps
|
12
|
+
end
|
13
|
+
|
14
|
+
@formatter = formatter
|
15
|
+
end
|
16
|
+
|
17
|
+
def format(line)
|
18
|
+
steps.inject(line) { |line, step| formatter.send(step, line) }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
DEFAULT_LINE_FORMATTER = LineFormatter.new(Formatter.new)
|
23
|
+
DEFAULT_PRE_LINE_FORMATTER = LineFormatter.new(PreFormatter.new)
|
24
|
+
end
|
data/lib/asuka/parser.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require "stringio"
|
2
|
+
|
3
|
+
module Asuka
|
4
|
+
class Parser
|
5
|
+
attr_accessor :line_formatter, :pre_line_formatter
|
6
|
+
|
7
|
+
def rules
|
8
|
+
[Rules::Initial, Rules::UnorderedList, Rules::Blockquote, Rules::Rule, Rules::Header, Rules::Paragraph]
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(line_formatter=DEFAULT_LINE_FORMATTER, pre_line_formatter=DEFAULT_PRE_LINE_FORMATTER)
|
12
|
+
@line_formatter = line_formatter
|
13
|
+
@pre_line_formatter = pre_line_formatter
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse(content)
|
17
|
+
lines = content.respond_to?(:join) ? content : StringIO.new(content).readlines
|
18
|
+
parse_lines(lines)
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse_lines(lines)
|
22
|
+
result = []
|
23
|
+
acc = Accumulator.new
|
24
|
+
|
25
|
+
set = Rules::Set.new(rules.map { |rule| rule.new(acc, result, line_formatter) })
|
26
|
+
|
27
|
+
pstate = set.rules.first
|
28
|
+
lines.each do |line|
|
29
|
+
line = pre_line_formatter.format(line)
|
30
|
+
|
31
|
+
cstate = set.rule_for(line)
|
32
|
+
pstate.transition if cstate != pstate
|
33
|
+
cstate.process(line)
|
34
|
+
|
35
|
+
pstate = cstate
|
36
|
+
end
|
37
|
+
pstate.transition # flush last state
|
38
|
+
|
39
|
+
result
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/asuka/rules.rb
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
module Asuka
|
2
|
+
module Rules
|
3
|
+
class Set
|
4
|
+
attr_accessor :rules
|
5
|
+
def initialize(rules=[])
|
6
|
+
@rules = rules
|
7
|
+
end
|
8
|
+
|
9
|
+
def rule_for(line)
|
10
|
+
rules.detect { |r| r.match?(line) }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Abstract
|
15
|
+
attr_reader :acc, :result, :line_formatter
|
16
|
+
|
17
|
+
def initialize(acc, result, line_formatter)
|
18
|
+
@acc = acc
|
19
|
+
@result = result
|
20
|
+
@line_formatter = line_formatter
|
21
|
+
end
|
22
|
+
|
23
|
+
def match?(line)
|
24
|
+
raise NotImplementedError, "%s does not implement the #match? method" % [self.class.name]
|
25
|
+
end
|
26
|
+
|
27
|
+
def transition
|
28
|
+
# no-op
|
29
|
+
end
|
30
|
+
|
31
|
+
def process(line)
|
32
|
+
# no-op
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class Initial < Abstract
|
37
|
+
def match?(line)
|
38
|
+
line == ""
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class Paragraph < Abstract
|
43
|
+
# override
|
44
|
+
def match?(line)
|
45
|
+
true # paragraph match everything!
|
46
|
+
end
|
47
|
+
|
48
|
+
def transition
|
49
|
+
result << Asuka::Paragraph.new(acc.flush)
|
50
|
+
end
|
51
|
+
|
52
|
+
def process(line)
|
53
|
+
acc.push(line_formatter.format(line))
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class Blockquote < Abstract
|
58
|
+
def match?(line)
|
59
|
+
line =~ /^> /
|
60
|
+
end
|
61
|
+
|
62
|
+
def transition
|
63
|
+
result << Asuka::Blockquote.new(acc.flush)
|
64
|
+
end
|
65
|
+
|
66
|
+
def process(line)
|
67
|
+
acc.push(line_formatter.format(line[5..-1]))
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class UnorderedList < Abstract
|
72
|
+
def match?(line)
|
73
|
+
line =~ /^\* /
|
74
|
+
end
|
75
|
+
|
76
|
+
def transition
|
77
|
+
result << Asuka::UnorderedList.new(acc.flush)
|
78
|
+
end
|
79
|
+
|
80
|
+
def process(line)
|
81
|
+
acc.push(line_formatter.format(line[2..-1]))
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class Rule < Abstract
|
86
|
+
def match?(line)
|
87
|
+
line =~ /^(-{3,}|_{3,})$/
|
88
|
+
end
|
89
|
+
|
90
|
+
def process(line)
|
91
|
+
result << Asuka::Rule.new
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
class Header < Abstract
|
96
|
+
def match?(line)
|
97
|
+
line =~ /^\#{1,6} /
|
98
|
+
end
|
99
|
+
|
100
|
+
def process(line)
|
101
|
+
level = line[/^\#{1,6}/].size
|
102
|
+
result << Asuka::Header.new(line_formatter.format(line[level+1..-1]), level)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|