asuka 0.5.3
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.
- 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
|