pasta 0.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/.gitignore +19 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +57 -0
- data/Rakefile +9 -0
- data/bin/pasta +40 -0
- data/lib/pasta.rb +18 -0
- data/lib/pasta/cookbook.rb +13 -0
- data/lib/pasta/dish.rb +62 -0
- data/lib/pasta/error.rb +3 -0
- data/lib/pasta/ingredients.rb +49 -0
- data/lib/pasta/ingredients/anything.rb +12 -0
- data/lib/pasta/ingredients/atx_headings.rb +21 -0
- data/lib/pasta/ingredients/automatic_links.rb +14 -0
- data/lib/pasta/ingredients/blockquotes.rb +23 -0
- data/lib/pasta/ingredients/code.rb +15 -0
- data/lib/pasta/ingredients/codeblocks.rb +19 -0
- data/lib/pasta/ingredients/emphasis.rb +14 -0
- data/lib/pasta/ingredients/empty_lines.rb +13 -0
- data/lib/pasta/ingredients/html_entities.rb +17 -0
- data/lib/pasta/ingredients/links.rb +58 -0
- data/lib/pasta/ingredients/lists.rb +31 -0
- data/lib/pasta/ingredients/page_breaks.rb +14 -0
- data/lib/pasta/ingredients/paragraphs.rb +27 -0
- data/lib/pasta/ingredients/setext_headings.rb +20 -0
- data/lib/pasta/recipes/base.rb +40 -0
- data/lib/pasta/recipes/gruber.rb +27 -0
- data/lib/pasta/recipes/markdown.rb +11 -0
- data/lib/pasta/version.rb +3 -0
- data/pasta.gemspec +24 -0
- data/specs/base_spec.rb +8 -0
- data/specs/dish_spec.rb +30 -0
- data/specs/dsl_spec.rb +12 -0
- data/specs/recipes/gruber/automatic_links.yml +12 -0
- data/specs/recipes/gruber/blockquotes.yml +112 -0
- data/specs/recipes/gruber/code.yml +48 -0
- data/specs/recipes/gruber/codeblocks.yml +47 -0
- data/specs/recipes/gruber/emphasis.yml +57 -0
- data/specs/recipes/gruber/gruber_spec.rb +22 -0
- data/specs/recipes/gruber/headers.yml +88 -0
- data/specs/recipes/gruber/images.yml +49 -0
- data/specs/recipes/gruber/links.yml +184 -0
- data/specs/recipes/gruber/lists.yml +171 -0
- data/specs/recipes/gruber/page_breaks.yml +31 -0
- data/specs/recipes/gruber/paragraphs.yml +44 -0
- data/specs/spec_helper.rb +3 -0
- metadata +149 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
module Pasta
|
2
|
+
module Ingredients
|
3
|
+
|
4
|
+
HTML_ENTITIES = {
|
5
|
+
'&' => '&'
|
6
|
+
}
|
7
|
+
|
8
|
+
def html_entities(text, html)
|
9
|
+
HTML_ENTITIES.each do |entity, transformation|
|
10
|
+
html = text.gsub(/[#{entity}]/) do |match|
|
11
|
+
match = transformation
|
12
|
+
end
|
13
|
+
end
|
14
|
+
[text, html]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Pasta
|
2
|
+
module Ingredients
|
3
|
+
|
4
|
+
# Parses both links and images
|
5
|
+
# $1 => "!", $2 => link text, $4 => inline url, $5 => reference id
|
6
|
+
LINKS = /([!]*)\[(.+?)\](\((.+?)\)|\s?\[(.*?)\])/
|
7
|
+
|
8
|
+
LINK_DEFINITION = /^ {0,3}\[([\w\s]+?)\]: (.+?)(^\s*[\n\r]|\z)/m
|
9
|
+
|
10
|
+
def links(text, html)
|
11
|
+
find_link_definitions(text)
|
12
|
+
|
13
|
+
text.gsub!(LINKS) do |match|
|
14
|
+
link_text, target = $2, ''
|
15
|
+
if $4
|
16
|
+
# It's an inline link
|
17
|
+
target = $4
|
18
|
+
else
|
19
|
+
# It's a reference link
|
20
|
+
id = $5 == '' ? $2.downcase : $5.downcase
|
21
|
+
break unless @link_definitions && @link_definitions.key?(id)
|
22
|
+
target = @link_definitions[id][:target]
|
23
|
+
end
|
24
|
+
# format image or link
|
25
|
+
url, title = parse_link target
|
26
|
+
if $1 == '!'
|
27
|
+
match = "<img src=\"#{url}\" alt=\"#{link_text}\"#{title} />"
|
28
|
+
else
|
29
|
+
match = "<a href=\"#{url}\"#{title}>#{link_text}</a>"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
text = clean_link_definitions(text)
|
34
|
+
[text, html]
|
35
|
+
end
|
36
|
+
|
37
|
+
def find_link_definitions(text)
|
38
|
+
@link_definitions = {}
|
39
|
+
text.gsub(LINK_DEFINITION) do |match|
|
40
|
+
@link_definitions[$1.downcase] = {target: $2, used: false}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def clean_link_definitions(text)
|
45
|
+
@link_definitions.each do |key,val|
|
46
|
+
text.gsub!(/^ {0,3}\[#{key}\]: (.+?)(^\s*[\n\r]|\z)/mi, '')
|
47
|
+
end
|
48
|
+
text
|
49
|
+
end
|
50
|
+
|
51
|
+
def parse_link(text)
|
52
|
+
target = text.split(' ')
|
53
|
+
url = target[0].gsub(/[<>]/, '')
|
54
|
+
title = " title=#{target[1..-1].join(' ').strip}" if target.length > 1
|
55
|
+
[url, title]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Pasta
|
2
|
+
module Ingredients
|
3
|
+
|
4
|
+
LIST = /\A\s{0,3}(([-*+]|([0-9]+)\.) .+?)((^\s*[\n\r])|\z)/m
|
5
|
+
|
6
|
+
def lists(text, html)
|
7
|
+
text.sub!(LIST) do |match|
|
8
|
+
inner_text, inner_html, list_type = $1, '', 'ul'
|
9
|
+
|
10
|
+
# determine the list_type
|
11
|
+
list_type = 'ol' unless %w{- * +}.include? $1.strip[0]
|
12
|
+
|
13
|
+
# extract list items
|
14
|
+
inner_html = list_items(inner_text)
|
15
|
+
|
16
|
+
html << "<#{list_type}>\n#{inner_html}</#{list_type}>\n\n"
|
17
|
+
match = ''
|
18
|
+
end
|
19
|
+
[text, html]
|
20
|
+
end
|
21
|
+
|
22
|
+
def list_items(text)
|
23
|
+
html = ''
|
24
|
+
text.gsub(/^([-*+]|\d+\.) (.+?)$(?=([\n\r]([*+-]|(\d+\.))|\z))/m) do |match|
|
25
|
+
html << "<li>#{$2.gsub(/\n\s+/, ' ').strip}</li>\n"
|
26
|
+
match = ''
|
27
|
+
end
|
28
|
+
html
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Pasta
|
2
|
+
module Ingredients
|
3
|
+
|
4
|
+
PARAGRAPH = /\A(?![>])(.+?)((^\s*[\n\r])|\z)/m
|
5
|
+
|
6
|
+
def paragraphs(text, html)
|
7
|
+
text.sub!(PARAGRAPH) do |match|
|
8
|
+
|
9
|
+
inner_text, inner_html = $1, ''
|
10
|
+
inner_text.gsub!(/[ ]{2,}[\n\r]/, "<br />\n")
|
11
|
+
inner_text.strip!
|
12
|
+
|
13
|
+
if grammars.key? :paragraphs
|
14
|
+
grammars[:paragraphs].each do |rule|
|
15
|
+
inner_text, inner_html = send(rule, inner_text, '')
|
16
|
+
end
|
17
|
+
inner_text = inner_html unless inner_html == ''
|
18
|
+
end
|
19
|
+
|
20
|
+
html << "<p>#{inner_text}</p>\n\n" unless inner_text == ''
|
21
|
+
match = ''
|
22
|
+
end
|
23
|
+
[text, html]
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Pasta
|
2
|
+
module Ingredients
|
3
|
+
|
4
|
+
SETEXT_HEADING = /\A(.+?)[\n\r]([=|-]+)([\n\r]|\z)/
|
5
|
+
|
6
|
+
def setext_headings(text, html)
|
7
|
+
text.sub!(SETEXT_HEADING) do |match|
|
8
|
+
if grammars.key? :heading
|
9
|
+
grammars[:heading].each do |rule|
|
10
|
+
inner = send(rule, $1, '')
|
11
|
+
end
|
12
|
+
end
|
13
|
+
level = ($2[0] == '=') ? 1 : 2
|
14
|
+
html << "<h#{level}>#{$1}</h#{level}>\n" unless $1.nil?
|
15
|
+
match = ''
|
16
|
+
end
|
17
|
+
[text, html]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'pasta/ingredients'
|
2
|
+
|
3
|
+
module Pasta
|
4
|
+
module Recipes
|
5
|
+
class Base
|
6
|
+
|
7
|
+
include Pasta::Ingredients
|
8
|
+
|
9
|
+
# Public. Generic to_x parser that converts a string to the specified output format.
|
10
|
+
# Uses a depth first strategy to parse a text according to incredient rules as defined in a grammar
|
11
|
+
# Raises a Pasta::Error if a grammar has not been defined
|
12
|
+
#
|
13
|
+
# text - a String to be parsed
|
14
|
+
#
|
15
|
+
# Example
|
16
|
+
# to_html('some yummy text')
|
17
|
+
# # => '<p>some yummy text</p>'
|
18
|
+
#
|
19
|
+
# Returns a String
|
20
|
+
def method_missing(method, *args, &block)
|
21
|
+
if method.to_s =~ /^to_(.+)$/
|
22
|
+
# ensure a grammar has been defined
|
23
|
+
raise Pasta::Error.new "A grammar for #{$1} has not been defined in recipe #{self.class.name}" if grammars($1.to_sym).nil?
|
24
|
+
|
25
|
+
# Depth first parsing - each ingredient should accept and return an input and output
|
26
|
+
input, output = args[0], ''
|
27
|
+
while input != "" do
|
28
|
+
grammars($1.to_sym).each do |rule|
|
29
|
+
input, output = send(rule, input, output)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
output.strip # remove any trailing whitespace
|
33
|
+
else
|
34
|
+
super
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Pasta
|
2
|
+
module Recipes
|
3
|
+
class Gruber < Base
|
4
|
+
|
5
|
+
# Define the grammars
|
6
|
+
grammar(:html) {
|
7
|
+
[:automatic_links, :links, :empty_lines, :page_breaks, :codeblocks, :blockquotes, :lists, :atx_headings, :setext_headings, :paragraphs]
|
8
|
+
}
|
9
|
+
grammar(:blockquotes) {
|
10
|
+
[:empty_lines, :page_breaks, :codeblocks, :blockquotes, :lists, :atx_headings, :setext_headings, :paragraphs]
|
11
|
+
}
|
12
|
+
grammar(:lists) {
|
13
|
+
[:empty_lines, :codeblocks, :blockquotes, :lists, :atx_headings, :setext_headings, :paragraphs]
|
14
|
+
}
|
15
|
+
grammar(:atx_headings) {
|
16
|
+
[:emphasis]
|
17
|
+
}
|
18
|
+
grammar(:setext_headings) {
|
19
|
+
[:emphasis]
|
20
|
+
}
|
21
|
+
grammar(:paragraphs) {
|
22
|
+
[:code, :emphasis]
|
23
|
+
}
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/pasta.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'pasta/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "pasta"
|
8
|
+
spec.version = Pasta::VERSION
|
9
|
+
spec.authors = ["Dave Kinkead"]
|
10
|
+
spec.email = ["dave@kinkead.com.au"]
|
11
|
+
spec.description = %q{À la carte markdown parsing}
|
12
|
+
spec.summary = %q{Pasta is an extensible markdown text parser with a delicious menu of options}
|
13
|
+
spec.homepage = "https://github.com/davekinkead"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|specs|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "markdown-testsuite"
|
24
|
+
end
|
data/specs/base_spec.rb
ADDED
data/specs/dish_spec.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Pasta::Dish do
|
4
|
+
|
5
|
+
specify { ->{ Pasta::Dish.new }.must_raise ArgumentError }
|
6
|
+
|
7
|
+
let(:spaghetti) { Pasta::Dish.new 'Long, thin, cylindrical strings of durum' }
|
8
|
+
|
9
|
+
specify { spaghetti.must_be_kind_of Pasta::Dish }
|
10
|
+
specify { spaghetti.must_respond_to :with }
|
11
|
+
specify { spaghetti.must_respond_to :make }
|
12
|
+
|
13
|
+
let(:fettuccine) { Pasta::Dish.new 'Flat, thick, strings of egg and durum'}
|
14
|
+
|
15
|
+
describe "#with" do
|
16
|
+
it "raises an error if a recipe can't be found" do
|
17
|
+
->{ fettuccine.with(:unknown) }.must_raise Pasta::Error
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "#make" do
|
22
|
+
it "raises an error if a recipe can't be found" do
|
23
|
+
->{ fettuccine.make(:unknown) }.must_raise Pasta::Error
|
24
|
+
end
|
25
|
+
|
26
|
+
it "converts the sauce text into a string" do
|
27
|
+
fettuccine.make(:html).must_be_kind_of String
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/specs/dsl_spec.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Pasta do
|
4
|
+
specify { Pasta.must_respond_to :prepare }
|
5
|
+
specify { ->{ Pasta.prepare }.must_raise ArgumentError }
|
6
|
+
specify { Pasta.prepare('some yummy text').must_be_kind_of Pasta::Dish }
|
7
|
+
specify { Pasta.prepare('a handful of text').with(:gruber).make(:html).must_equal '<p>a handful of text</p>' }
|
8
|
+
|
9
|
+
it "uses :markdown as the default recipe" do
|
10
|
+
Pasta.prepare('thick, soft balls of flour and egg').make(:html).must_equal '<p>thick, soft balls of flour and egg</p>'
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
overview: |
|
2
|
+
Markdown supports a shortcut style for creating “automatic” links for URLs and email addresses: simply surround the URL or email address with angle brackets. What this means is that if you want to show the actual text of a URL or email address, and also have it be a clickable link.
|
3
|
+
|
4
|
+
http://daringfireball.net/projects/markdown/syntax
|
5
|
+
tests:
|
6
|
+
- name: Automatic Links
|
7
|
+
desc: URL inside html tags are automatically parsed
|
8
|
+
text: |
|
9
|
+
<http://example.com/>
|
10
|
+
html: |
|
11
|
+
<p><a href="http://example.com/">http://example.com/</a></p>
|
12
|
+
|
@@ -0,0 +1,112 @@
|
|
1
|
+
overview: |
|
2
|
+
Markdown uses email-style > characters for blockquoting. If you’re familiar with quoting passages of text in an email message, then you know how to create a blockquote in Markdown. It looks best if you hard wrap the text and put a > before every line:
|
3
|
+
|
4
|
+
> This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
|
5
|
+
> consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
|
6
|
+
> Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
|
7
|
+
>
|
8
|
+
> Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
|
9
|
+
> id sem consectetuer libero luctus adipiscing.
|
10
|
+
|
11
|
+
Markdown allows you to be lazy and only put the > before the first line of a hard-wrapped paragraph:
|
12
|
+
|
13
|
+
> This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
|
14
|
+
consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
|
15
|
+
Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
|
16
|
+
|
17
|
+
> Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
|
18
|
+
id sem consectetuer libero luctus adipiscing.
|
19
|
+
|
20
|
+
Blockquotes can be nested (i.e. a blockquote-in-a-blockquote) by adding additional levels of >:
|
21
|
+
|
22
|
+
> This is the first level of quoting.
|
23
|
+
>
|
24
|
+
> > This is nested blockquote.
|
25
|
+
>
|
26
|
+
> Back to the first level.
|
27
|
+
|
28
|
+
Blockquotes can contain other Markdown elements, including headers, lists, and code blocks:
|
29
|
+
|
30
|
+
> ## This is a header.
|
31
|
+
>
|
32
|
+
> 1. This is the first list item.
|
33
|
+
> 2. This is the second list item.
|
34
|
+
>
|
35
|
+
> Here's some example code:
|
36
|
+
>
|
37
|
+
> return shell_exec("echo $input | $markdown_script");
|
38
|
+
|
39
|
+
Any decent text editor should make email-style quoting easy. For example, with BBEdit, you can make a selection and choose Increase Quote Level from the Text menu.
|
40
|
+
|
41
|
+
http://daringfireball.net/projects/markdown/syntax
|
42
|
+
tests:
|
43
|
+
- name: Single line block quote
|
44
|
+
desc: Quoting a single line of text
|
45
|
+
text: |
|
46
|
+
> This is a snazzy quote
|
47
|
+
html: |
|
48
|
+
<blockquote>
|
49
|
+
<p>This is a snazzy quote</p>
|
50
|
+
</blockquote>
|
51
|
+
|
52
|
+
- name: Double line block quote
|
53
|
+
desc: Quoting a two paragraphs of text
|
54
|
+
text: |
|
55
|
+
> This is a snazzy quote
|
56
|
+
>
|
57
|
+
> It's longer than you think!
|
58
|
+
html: |
|
59
|
+
<blockquote>
|
60
|
+
<p>This is a snazzy quote</p>
|
61
|
+
|
62
|
+
<p>It's longer than you think!</p>
|
63
|
+
</blockquote>
|
64
|
+
|
65
|
+
- name: Lazy double quotes
|
66
|
+
desc: Quoting two lines of text with only lead >
|
67
|
+
text: |
|
68
|
+
> This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
|
69
|
+
consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
|
70
|
+
Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
|
71
|
+
html: |
|
72
|
+
<blockquote>
|
73
|
+
<p>This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
|
74
|
+
consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
|
75
|
+
Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.</p>
|
76
|
+
</blockquote>
|
77
|
+
|
78
|
+
- name: Multiple blockquotes
|
79
|
+
desc: Using multiple blockquotes in succession
|
80
|
+
text: |
|
81
|
+
> This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
|
82
|
+
consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
|
83
|
+
|
84
|
+
> Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
|
85
|
+
html: |
|
86
|
+
<blockquote>
|
87
|
+
<p>This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
|
88
|
+
consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.</p>
|
89
|
+
</blockquote>
|
90
|
+
|
91
|
+
<blockquote>
|
92
|
+
<p>Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.</p>
|
93
|
+
</blockquote>
|
94
|
+
|
95
|
+
- name: Nested blockquotes
|
96
|
+
desc: Blockquotes can contain blockquotes
|
97
|
+
text: |
|
98
|
+
> This is the first level of quoting.
|
99
|
+
>
|
100
|
+
> > This is nested blockquote.
|
101
|
+
>
|
102
|
+
> Back to the first level.
|
103
|
+
html: |
|
104
|
+
<blockquote>
|
105
|
+
<p>This is the first level of quoting.</p>
|
106
|
+
|
107
|
+
<blockquote>
|
108
|
+
<p>This is nested blockquote.</p>
|
109
|
+
</blockquote>
|
110
|
+
|
111
|
+
<p>Back to the first level.</p>
|
112
|
+
</blockquote>
|