raconteur 0.1.0
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 +9 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/Guardfile +42 -0
- data/LICENSE.txt +21 -0
- data/README.md +94 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lib/raconteur.rb +49 -0
- data/lib/raconteur/config.rb +27 -0
- data/lib/raconteur/parse.rb +111 -0
- data/lib/raconteur/processor.rb +118 -0
- data/lib/raconteur/setting.rb +54 -0
- data/lib/raconteur/version.rb +3 -0
- data/raconteur.gemspec +30 -0
- metadata +158 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c64b155fb75f4fa4e92ca6d4c0109c7bf71905f0
|
4
|
+
data.tar.gz: 668db905ee4fbc62b71df3a73dcbf46ce2d4b21f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 336e2825fd8658bbf8a7dcc493b7594527f49ce1346f921f8a669ac1ee45c565e91bea6a623e01a00464e67038b92169b2eaad327f4fcd7cc7e00ea68bd2f5b5
|
7
|
+
data.tar.gz: 252382f235f204f32d1a4f5f1fd63113a7ccfeb3a6705261708c5a35c1fadbcb627253206c90929d22ccd0e22b4443724bac7ffc8e5309f36671394fef8ec50d
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
## Uncomment and set this to only include directories you want to watch
|
5
|
+
# directories %w(app lib config test spec features) \
|
6
|
+
# .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")}
|
7
|
+
|
8
|
+
## Note: if you are using the `directories` clause above and you are not
|
9
|
+
## watching the project directory ('.'), then you will want to move
|
10
|
+
## the Guardfile to a watched dir and symlink it back, e.g.
|
11
|
+
#
|
12
|
+
# $ mkdir config
|
13
|
+
# $ mv Guardfile config/
|
14
|
+
# $ ln -s config/Guardfile .
|
15
|
+
#
|
16
|
+
# and, you'll have to watch "config/Guardfile" instead of "Guardfile"
|
17
|
+
|
18
|
+
guard :minitest do
|
19
|
+
# with Minitest::Unit
|
20
|
+
watch(%r{^test/(.*)\/?(.*)_test\.rb$})
|
21
|
+
watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}test_#{m[2]}.rb" }
|
22
|
+
watch(%r{^test/test_helper\.rb$}) { 'test' }
|
23
|
+
|
24
|
+
# with Minitest::Spec
|
25
|
+
# watch(%r{^spec/(.*)_spec\.rb$})
|
26
|
+
# watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
27
|
+
# watch(%r{^spec/spec_helper\.rb$}) { 'spec' }
|
28
|
+
|
29
|
+
# Rails 4
|
30
|
+
# watch(%r{^app/(.+)\.rb$}) { |m| "test/#{m[1]}_test.rb" }
|
31
|
+
# watch(%r{^app/controllers/application_controller\.rb$}) { 'test/controllers' }
|
32
|
+
# watch(%r{^app/controllers/(.+)_controller\.rb$}) { |m| "test/integration/#{m[1]}_test.rb" }
|
33
|
+
# watch(%r{^app/views/(.+)_mailer/.+}) { |m| "test/mailers/#{m[1]}_mailer_test.rb" }
|
34
|
+
# watch(%r{^lib/(.+)\.rb$}) { |m| "test/lib/#{m[1]}_test.rb" }
|
35
|
+
# watch(%r{^test/.+_test\.rb$})
|
36
|
+
# watch(%r{^test/test_helper\.rb$}) { 'test' }
|
37
|
+
|
38
|
+
# Rails < 4
|
39
|
+
# watch(%r{^app/controllers/(.*)\.rb$}) { |m| "test/functional/#{m[1]}_test.rb" }
|
40
|
+
# watch(%r{^app/helpers/(.*)\.rb$}) { |m| "test/helpers/#{m[1]}_test.rb" }
|
41
|
+
# watch(%r{^app/models/(.*)\.rb$}) { |m| "test/unit/#{m[1]}_test.rb" }
|
42
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Jamie Appleseed
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# Raconteur
|
2
|
+
|
3
|
+
With Raconteur, you can define custom text tags and have them parsed according to your specifications, allowing you to do some neat pre- and post-processing of your texts. You could for instance insert dynamic content fetched from your database whenever {{ customer-quote: id=43 }} appears in your text.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Raconteur is a Ruby gem so install like any other gem. For example, if you're using Bundler, then add this to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'raconteur'
|
11
|
+
```
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
Raconteur is based around the notion of processors. Each processor has a tag-name and a set of instructions for how it should perform the content replacement and what it should insert.
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
@raconteur = Raconteur.new
|
19
|
+
@raconteur.processors.register!('customer-quote', {
|
20
|
+
template: '<div class="quote" id="customer-quote-{{ id }}">\'{{ text }}\' - {{ author }}</div>',
|
21
|
+
handler: lambda do |settings|
|
22
|
+
quote = CustomerQuote.find(settings[:id])
|
23
|
+
{ author: quote.author_name, text: quote.citation, id: quote.id }
|
24
|
+
end
|
25
|
+
})
|
26
|
+
@raconteur.parse("Here's what some of our customers are saying about the product:\n{{ customer-quote: id=43 }}\n{{ customer-quote: id=266 }}\n{{ customer-quote: id=7 }}")
|
27
|
+
# -- outputs --
|
28
|
+
# Here's what some of our customers are saying:
|
29
|
+
# <div class="quote" id="customer-quote-43">'The most amazing usability report I have ever read!' - James Newman</div>
|
30
|
+
# <div class="quote" id="customer-quote-266">'I cannot believe how incredible the benchmark database is!' - Jane Newton</div>
|
31
|
+
# <div class="quote" id="customer-quote-7">'Spetacular customer service.' - John Oldling</div>
|
32
|
+
```
|
33
|
+
|
34
|
+
Here's what's happening in the above example:
|
35
|
+
|
36
|
+
- We first create a new instance of Raconteur. (Tip: You can create multiple Raconteur instances, each with its own settings and set of processors.)
|
37
|
+
- We then register a processor with the tag-name 'customer-quote'. This means that any instances of {{ customer-quote }} in the parsed text will be replaced according to the settings of this processor.
|
38
|
+
- The processor has a <code>template</code> and a <code>handler</code>. The <code>template</code> is a simple string with {{ variables }} that are replaced with dynamic content. The <code>handler</code> loads the customer quotes from our database based on the id that was passed in as a setting in the customer-quote tags (i.e. {{ customer-quote: id=43 }} invokes the handler with a settings hash of { id: "43" }).
|
39
|
+
- Finally, we call the <code>parse</code> function on our Raconteur instance, which runs over the text and replaces all instances that match its registered processors according to their specifications.
|
40
|
+
|
41
|
+
Processors must always have a tag-name name along with either a template defined, a handler defined, or both. If a handler returns a hash (like in the above example), it should have a template defined as well – Raconteur will take the returned hash and use its keys as replacement variables for the template (again, like seen in the example above). Handlers may also return a string, in which Raconteur will use this as the replacement, allowing you full control over the replaced text. If there's only a template defined, Raconteur will simply pass in any inputted tag settings as variables and then replace the tag with that template.
|
42
|
+
|
43
|
+
Processors may also be used for wrapping tags, which is a great way to encapsulate sections in your text. Let's look at an example:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
@raconteur = Raconteur.new
|
47
|
+
@raconteur.processors.register!('gallery', {
|
48
|
+
template: '<div class="aside"><p class="box-label">Aside:</p>{{ _yield_ }}</div>'
|
49
|
+
})
|
50
|
+
@raconteur.processors.register!('image', {
|
51
|
+
handler: lambda do |settings|
|
52
|
+
image = MediaLibrary.find(settings[:id]).image
|
53
|
+
"<div class=\"graphic\"><img src=\"#{image.url}\" /><p class=\"caption\">#{image.caption}</p></div>"
|
54
|
+
end
|
55
|
+
})
|
56
|
+
@raconteur.parse("Some paragraph text.\n\n{{ image: id=43 }}\n\nAnother paragraph.\n\n{{% aside %}}\n\nAdditional tangentially-related text.\n\n{{ image: id=125 }}\n\nWe're really getting off-topic here.\n\n{{% end %}}\n\nOk, back to regular text.")
|
57
|
+
# -- outputs --
|
58
|
+
# Some paragraph text.
|
59
|
+
#
|
60
|
+
# <div class="graphic"><img src="http://some-url.com/some-path-for-image-43.jpg" /><p class="caption">A captivating caption text for the image.</p></div>
|
61
|
+
#
|
62
|
+
# Another paragraph.
|
63
|
+
#
|
64
|
+
# <div class="aside"><p class="box-label">Aside:</p>
|
65
|
+
#
|
66
|
+
# Additional tangentially-related text.
|
67
|
+
#
|
68
|
+
# <div class="graphic"><img src="http://some-url.com/some-path-for-image-125.jpg" /><p class="caption">Another fascinating caption for another incredible image, but this time wrapped within an aside!</p></div>
|
69
|
+
#
|
70
|
+
# We're really getting off-topic here.
|
71
|
+
#
|
72
|
+
# </div>
|
73
|
+
#
|
74
|
+
# Ok, back to regular text.
|
75
|
+
```
|
76
|
+
|
77
|
+
Wrappers are registered like all other processors but are invoked a lille differently in the text by having percentage symbols added to their curly braces and needing an {{% end %}} tag to signify when they should end. Wrapper templates work the same except they have a special {{ _yield_ }} variable passed into them, which holds the contents of the wrapped content.
|
78
|
+
|
79
|
+
In the above example, you'll notice that regular tags (in this case {{ image }}) can be used within wrappers. Wrappers may also be nested within each other. Any tags (both wrappers and regular blocks) nested within a wrapper will have a special _scope_ variable passed to their <code>handler</code> method, allowing you to customize the behavior of a tag based on its surrounding context. For instance, you could register an {{ image }} tag which renders differently depending on whether it is placed within a {{% gallery %}} wrapper or not.
|
80
|
+
|
81
|
+
Let's take a look at tags:
|
82
|
+
|
83
|
+
- Tags are wrapper by two curly braces and their name should be a string without any white-space <code>{{ like-this }}</code>.
|
84
|
+
- Tags don't need any settings. Something simple like <code>{{ page-count }}</code> will work perfectly fine.
|
85
|
+
- If you do want to pass in settings to a tag, the tag-name should be followed by a colon and a set of key-value pairs. The key must not include any white-space characters and will be converted to an underscored symbol for the settings hash. The separator should be an equals '=' symbol. The value may either be a word without any white-space characters, or it can be "a text wrapped by quotes". <code>{{ definition: term=UXD + description="User Experience Design (aka UXD + UED + XD) refers to the ..." }}</code>
|
86
|
+
- To escape quotes within a quoted settings value, put a backslash in front of the quote. You may alternatively configure Raconteur to use a different symbol for wrapping the text. Both non-white-space values and quoted values may be used in the same tag (as seen in the above "User Experience Design" example where the <code>term</code> isn't quoted but the <code>description</code> is). The key-value pairs in settings don't need to be separated by anything other than a white-space character but it can greatly help readability to include some character (such as a '+' symbol, as seen in the above "User Experience Design" example).
|
87
|
+
|
88
|
+
## Contributing
|
89
|
+
|
90
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/raconteur.
|
91
|
+
|
92
|
+
## License
|
93
|
+
|
94
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "raconteur"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/lib/raconteur.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
class Raconteur
|
2
|
+
DEFAULTS = {
|
3
|
+
processors: [],
|
4
|
+
settings: {
|
5
|
+
closing_tag: 'end',
|
6
|
+
setting_quotes: '"'
|
7
|
+
}
|
8
|
+
}
|
9
|
+
ORIGINAL_DEFAULTS = Marshal.load(Marshal.dump(DEFAULTS)).freeze
|
10
|
+
ATTRS = DEFAULTS.keys.freeze
|
11
|
+
|
12
|
+
# Bootstrap attributes
|
13
|
+
def initialize(customizations={})
|
14
|
+
@data = Marshal.load(Marshal.dump(DEFAULTS))
|
15
|
+
@raconteur = self
|
16
|
+
end
|
17
|
+
|
18
|
+
# Parse the inputted text with the registered processors
|
19
|
+
def parse(text="", scope=nil)
|
20
|
+
Raconteur::Parse.scoped self, text, scope
|
21
|
+
end
|
22
|
+
|
23
|
+
# Prettier print
|
24
|
+
def inspect
|
25
|
+
"#<Raconteur:0x#{object_id} #{ATTRS.map { |att| "@#{att}=#{send(att).inspect}" }.join(', ')}>"
|
26
|
+
end
|
27
|
+
|
28
|
+
# Accessing settings and processors
|
29
|
+
def settings
|
30
|
+
Raconteur::Setting.scoped self
|
31
|
+
end
|
32
|
+
def processors
|
33
|
+
Raconteur::Processor.scoped self
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def data
|
40
|
+
@data
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
require "raconteur/version"
|
46
|
+
require "raconteur/config"
|
47
|
+
require "raconteur/processor"
|
48
|
+
require "raconteur/setting"
|
49
|
+
require "raconteur/parse"
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class Raconteur::Config
|
2
|
+
|
3
|
+
# return current default settings for Raconteur
|
4
|
+
def self.default_settings
|
5
|
+
Raconteur::DEFAULTS[:settings]
|
6
|
+
end
|
7
|
+
# revert default settings for Raconteur to original defaults
|
8
|
+
def self.revert_to_original_defaults!
|
9
|
+
Raconteur::DEFAULTS[:settings] = original_default_settings
|
10
|
+
end
|
11
|
+
# return a copy of the original default settings
|
12
|
+
def self.original_default_settings
|
13
|
+
Marshal.load(Marshal.dump(Raconteur::ORIGINAL_DEFAULTS[:settings]))
|
14
|
+
end
|
15
|
+
# Let user override defaults for Raconteur
|
16
|
+
class << self
|
17
|
+
Raconteur::Config.default_settings.keys.each do |att|
|
18
|
+
define_method(att) do
|
19
|
+
self.default_settings[att]
|
20
|
+
end
|
21
|
+
define_method("#{att}=") do |val|
|
22
|
+
self.default_settings[att] = val
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
class Raconteur::Parse
|
2
|
+
|
3
|
+
def self.scoped(raconteur, document, scope)
|
4
|
+
@@raconteur = raconteur
|
5
|
+
parse_blocks(parse_wrappers(document, scope), scope)
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.parse_wrappers(document, scope)
|
9
|
+
output = document
|
10
|
+
regex = /{{%(.*?)%}}/m
|
11
|
+
# scan for all instances of wrappers, i.e. {{% tag-name %}}
|
12
|
+
wrappers = output.scan(regex).flatten
|
13
|
+
unless wrappers.empty?
|
14
|
+
# if wrappers, loop over all opening tags in reverse order (this way the inner-most wrapper is always processed before its parent wrappers)
|
15
|
+
wrappers.map(&:strip).reject { |str| str == @@raconteur.settings.closing_tag }.reverse.each do |open_tag|
|
16
|
+
# loop over each registered processor
|
17
|
+
@@raconteur.processors.each do |processor|
|
18
|
+
# check if the processor matches the tag
|
19
|
+
match = open_tag.match(processor.regex)
|
20
|
+
if match
|
21
|
+
# if there is a match, parse the tag's settings, if it has any
|
22
|
+
match_settings = parse_settings(match[:settings])
|
23
|
+
# merge in the nested wrapper scope, if present
|
24
|
+
match_settings.merge!(_scope_: scope) if scope
|
25
|
+
# identify the tag and its contents
|
26
|
+
regex = /.*(?<tag>{{%\s*#{Regexp.quote(open_tag)}\s*%}}(?<content>.*?){{%\s*#{Regexp.quote(@@raconteur.settings.closing_tag)}\s*%}})/m
|
27
|
+
wrapper_match = output.match(regex)
|
28
|
+
if wrapper_match
|
29
|
+
# if there is a match, run its contents through raconteur with the wrapper set as scope, to perform all necessary replacements (this allows nesting and wrapper customizations)
|
30
|
+
content = @@raconteur.parse(wrapper_match[:content], {
|
31
|
+
tag: open_tag,
|
32
|
+
processor: processor,
|
33
|
+
settings: match_settings
|
34
|
+
})
|
35
|
+
# set _yield_ variable with the (parsed) inner contents of wrapper tag
|
36
|
+
match_settings.merge!(_yield_: content)
|
37
|
+
# execute processor and replace output with result
|
38
|
+
content = processor.execute(content, match_settings)
|
39
|
+
# replace wrapper and its contents with the parsed content
|
40
|
+
output = output.sub(wrapper_match[:tag], content)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
output
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.parse_blocks(output, scope)
|
50
|
+
# replace all instances of {{ some-tag }}
|
51
|
+
output.gsub(/{{(.*?)}}/m) do |raw_str|
|
52
|
+
# the matched string if no processing occurs
|
53
|
+
output = raw_str
|
54
|
+
# clean the matched string to only get the tag and its settings
|
55
|
+
str = output.gsub(/^\{\{\%?\s*|\s*\%?\}\}$/, '')
|
56
|
+
# loop over each registered processor
|
57
|
+
@@raconteur.processors.each do |processor|
|
58
|
+
# check if the processor matches the tag
|
59
|
+
match = str.match(processor.regex)
|
60
|
+
if match
|
61
|
+
# parse the tag's settings, if any
|
62
|
+
match_settings = self.parse_settings(match[:settings])
|
63
|
+
# merge in the nested wrapper scope, if present
|
64
|
+
match_settings.merge!(_scope_: scope) if scope
|
65
|
+
# execute processor and replace output with result
|
66
|
+
output = processor.execute(output, match_settings)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
output
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# input: (string) 'id=353 + report-title="E-Commerce Checkout Usability Report"'
|
74
|
+
# output: (hash) { id: '353', report_title: 'E-Commerce Checkout Usability Report' }
|
75
|
+
def self.parse_settings(str)
|
76
|
+
# regex escape 'quote' character
|
77
|
+
quote = Regexp.quote(@@raconteur.settings.setting_quotes)
|
78
|
+
# Parsing logic:
|
79
|
+
# First, one or more non-white-space characters,
|
80
|
+
# .. followed by an equal sign '=' character,
|
81
|
+
# .. followed by either:
|
82
|
+
# a) 1+ non-white-space characters, or
|
83
|
+
# b) a string wrapped by the 'quote' character at both ends
|
84
|
+
# (instances of the 'quote' character within the string can be escaped by a backward-slash '\')
|
85
|
+
regex = /([^\s]+)\=(#{quote}.*?[^\\]#{quote}|[^\s]+)/mi
|
86
|
+
# prepare a fresh hash for the parsed settings
|
87
|
+
parsed_settings = {}
|
88
|
+
# loop over all key-value setting pairs in the string
|
89
|
+
str.scan(regex).each do |setting_str|
|
90
|
+
# grap keys and turn them into underscored symbols
|
91
|
+
key = setting_str[0].strip.gsub('-','_').to_sym
|
92
|
+
# strip values from whitespace and surrounding quote characters
|
93
|
+
value = setting_str[1].strip.gsub(/^#{quote}|#{quote}$/mi, '').gsub(/\\#{quote}/mi, quote)
|
94
|
+
# add to settings hash
|
95
|
+
parsed_settings[key] = value
|
96
|
+
end
|
97
|
+
# return the parsed settings (transformed from string to hash)
|
98
|
+
parsed_settings
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.render_template(template, data)
|
102
|
+
output = template
|
103
|
+
# loop over data hash (for value substitution)
|
104
|
+
data.each do |tag, value|
|
105
|
+
# replace dynamic values in the template
|
106
|
+
output = output.gsub(/{{\s*#{tag}\s*}}/, value.to_s)
|
107
|
+
end
|
108
|
+
output
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
class Raconteur::Processor
|
2
|
+
DEFAULTS = {
|
3
|
+
tag: nil,
|
4
|
+
template: nil,
|
5
|
+
handler: nil,
|
6
|
+
settings: {}
|
7
|
+
}.freeze
|
8
|
+
ATTRS = DEFAULTS.keys.freeze
|
9
|
+
|
10
|
+
# register new processor by providing a tag name + any settings (optional)
|
11
|
+
def self.register!(tag, customizations={})
|
12
|
+
if find(tag)
|
13
|
+
raise 'Processor already exists!'
|
14
|
+
else
|
15
|
+
all << Raconteur::Processor.new(tag, customizations)
|
16
|
+
end
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
# delete existing processor by tag name
|
21
|
+
def self.deregister!(tag)
|
22
|
+
all.delete(find(tag))
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
# update existing processor by providing its tag name and passing in any customizations (optional)
|
27
|
+
def self.update!(tag, customizations={})
|
28
|
+
deregister!(tag)
|
29
|
+
register!(tag, customizations)
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
# scoped
|
34
|
+
def self.scoped(raconteur)
|
35
|
+
@@raconteur = raconteur
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
# return array of all processors
|
40
|
+
def self.all
|
41
|
+
@@raconteur.send(:data)[:processors]
|
42
|
+
end
|
43
|
+
|
44
|
+
# find processor by tag name
|
45
|
+
def self.find(tag)
|
46
|
+
all.detect { |processor| processor.tag == tag }
|
47
|
+
end
|
48
|
+
|
49
|
+
# print array
|
50
|
+
def self.inspect
|
51
|
+
"#{all} (Raconteur::Processor array)"
|
52
|
+
end
|
53
|
+
|
54
|
+
# treat class as array
|
55
|
+
def self.method_missing(method_sym, *arguments, &block)
|
56
|
+
if !arguments.empty? && block_given?
|
57
|
+
all.send(method_sym, *arguments, &block)
|
58
|
+
elsif !arguments.empty?
|
59
|
+
all.send(method_sym, *arguments)
|
60
|
+
elsif block_given?
|
61
|
+
all.send(method_sym, &block)
|
62
|
+
else
|
63
|
+
all.send(method_sym)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
# -- Instances --
|
69
|
+
|
70
|
+
# new processor
|
71
|
+
def initialize(tag, customizations={})
|
72
|
+
@data = Marshal.load(Marshal.dump(DEFAULTS))
|
73
|
+
@processor = self
|
74
|
+
@processor.tag = tag
|
75
|
+
@processor.template = customizations[:template] if customizations[:template].is_a?(String)
|
76
|
+
@processor.handler = customizations[:handler] if customizations[:handler].is_a?(Proc)
|
77
|
+
@processor
|
78
|
+
end
|
79
|
+
|
80
|
+
# prettier print
|
81
|
+
def inspect
|
82
|
+
"#<Raconteur::Processor:0x#{object_id} #{ATTRS.map { |att| "@#{att}=#{@processor.send(att)}" }.join(', ')}>"
|
83
|
+
end
|
84
|
+
|
85
|
+
# piecemeal access and manipulation of processor attributes
|
86
|
+
ATTRS.each do |att|
|
87
|
+
define_method(att) do
|
88
|
+
@data[att]
|
89
|
+
end
|
90
|
+
define_method("#{att}=") do |val|
|
91
|
+
@data[att] = val
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# regex for matching tag and its settings
|
96
|
+
def regex
|
97
|
+
/^\s*#{Regexp.quote(@processor.tag)}:?(?<settings>.*?)?\s*$/im
|
98
|
+
end
|
99
|
+
|
100
|
+
# execute the processor
|
101
|
+
def execute(content="", settings={})
|
102
|
+
output = content
|
103
|
+
if self.handler
|
104
|
+
# if the processor has a custom handler, then pass everything to it for processing
|
105
|
+
output = self.handler.call(settings)
|
106
|
+
if output.is_a?(Hash) && self.template
|
107
|
+
# if the handler returns a hash and has a template, then pass the hash as options to the template
|
108
|
+
# (this allows for custom variable setting and overriding before the template is rendered)
|
109
|
+
output = Raconteur::Parse.render_template(self.template, output)
|
110
|
+
end
|
111
|
+
elsif self.template
|
112
|
+
# if there's no handler but there is a processor, simply render the template with the tag's settings
|
113
|
+
output = Raconteur::Parse.render_template(self.template, settings)
|
114
|
+
end
|
115
|
+
output
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
class Raconteur::Setting
|
2
|
+
|
3
|
+
# scoped
|
4
|
+
def self.scoped(raconteur)
|
5
|
+
@@raconteur = raconteur
|
6
|
+
self
|
7
|
+
end
|
8
|
+
|
9
|
+
# return hash of all settings
|
10
|
+
def self.all
|
11
|
+
@@raconteur.send(:data)[:settings]
|
12
|
+
end
|
13
|
+
|
14
|
+
# revert the settings of this Raconteur instance to the current default settings for Raconteur
|
15
|
+
def self.revert_to_defaults!
|
16
|
+
self.all.delete_if { true }.merge!(Marshal.load(Marshal.dump(Raconteur::Config.default_settings)))
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
# revert the settings of this Raconteur instance to the original default settings of Raconteur
|
21
|
+
def self.revert_to_original_defaults!
|
22
|
+
self.all.delete_if { true }.merge!(Marshal.load(Marshal.dump(Raconteur::Config.original_default_settings)))
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
# piecemeal access and manipulation of settings
|
27
|
+
Raconteur::Config.default_settings.keys.each do |att|
|
28
|
+
define_singleton_method(att) do
|
29
|
+
all[att]
|
30
|
+
end
|
31
|
+
define_singleton_method("#{att}=") do |val|
|
32
|
+
all[att] = val
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# print hash
|
37
|
+
def self.inspect
|
38
|
+
"#{all} (Raconteur::Setting hash)"
|
39
|
+
end
|
40
|
+
|
41
|
+
# treat class as hash
|
42
|
+
def self.method_missing(method_sym, *arguments, &block)
|
43
|
+
if !arguments.empty? && block_given?
|
44
|
+
all.send(method_sym, *arguments, &block)
|
45
|
+
elsif !arguments.empty?
|
46
|
+
all.send(method_sym, *arguments)
|
47
|
+
elsif block_given?
|
48
|
+
all.send(method_sym, &block)
|
49
|
+
else
|
50
|
+
all.send(method_sym)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
data/raconteur.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'raconteur/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "raconteur"
|
8
|
+
spec.version = Raconteur::VERSION
|
9
|
+
spec.authors = ["Jamie Appleseed"]
|
10
|
+
spec.email = ["jamieappleseed@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Custom text tag parsing}
|
13
|
+
spec.description = %q{Define custom text tags and have Raconteur parse them as per your specifications}
|
14
|
+
spec.homepage = "https://github.com/JamieAppleseed/raconteur"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
spec.add_development_dependency "byebug"
|
25
|
+
spec.add_development_dependency "minitest"
|
26
|
+
spec.add_development_dependency "guard"
|
27
|
+
spec.add_development_dependency "guard-minitest"
|
28
|
+
spec.add_development_dependency "kramdown"
|
29
|
+
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: raconteur
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jamie Appleseed
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-11-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.10'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.10'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: byebug
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: guard
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: guard-minitest
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: kramdown
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description: Define custom text tags and have Raconteur parse them as per your specifications
|
112
|
+
email:
|
113
|
+
- jamieappleseed@gmail.com
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- ".gitignore"
|
119
|
+
- ".travis.yml"
|
120
|
+
- Gemfile
|
121
|
+
- Guardfile
|
122
|
+
- LICENSE.txt
|
123
|
+
- README.md
|
124
|
+
- Rakefile
|
125
|
+
- bin/console
|
126
|
+
- bin/setup
|
127
|
+
- lib/raconteur.rb
|
128
|
+
- lib/raconteur/config.rb
|
129
|
+
- lib/raconteur/parse.rb
|
130
|
+
- lib/raconteur/processor.rb
|
131
|
+
- lib/raconteur/setting.rb
|
132
|
+
- lib/raconteur/version.rb
|
133
|
+
- raconteur.gemspec
|
134
|
+
homepage: https://github.com/JamieAppleseed/raconteur
|
135
|
+
licenses:
|
136
|
+
- MIT
|
137
|
+
metadata: {}
|
138
|
+
post_install_message:
|
139
|
+
rdoc_options: []
|
140
|
+
require_paths:
|
141
|
+
- lib
|
142
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
143
|
+
requirements:
|
144
|
+
- - ">="
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: '0'
|
147
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - ">="
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
requirements: []
|
153
|
+
rubyforge_project:
|
154
|
+
rubygems_version: 2.4.5.1
|
155
|
+
signing_key:
|
156
|
+
specification_version: 4
|
157
|
+
summary: Custom text tag parsing
|
158
|
+
test_files: []
|