Skip to content

Commit

Permalink
Multiple shortcode instances (#45)
Browse files Browse the repository at this point in the history
* Allow multiple instances of Shortcode with independent configurations

* specs work with multiple instances version now, still need to write more tests

* added spec for multiple shortcode instances

* made the include helpers backwards compatible

* get the haml template rendering spec to pass

* Updated readme and changelog

* Update README.md
  • Loading branch information
toddnestor authored and kernow committed May 25, 2017
1 parent 12c7aed commit 93c6a4e
Show file tree
Hide file tree
Showing 19 changed files with 319 additions and 170 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
@@ -1,3 +1,16 @@
## 2.0.0 (May 21, 2017)

Features:

- Shortcode converted from module to a class
- Separate instances of Shortcode have separate configurations
- Updated to parslet 1.8.0
- Updated to rspec 3.6

Misc:

- Testing against the latest versions of Rails 5.0 & 5.1

## 1.1.1 (September 14, 2015)

Features:
Expand Down
78 changes: 59 additions & 19 deletions README.md
Expand Up @@ -36,8 +36,11 @@ Rails versions 4.1, 4.2, 5.0 and 5.1.

Shortcode is very simple to use, simply call the `process` method and pass it a string containing shortcode markup.

You can create multiple instances of `Shortcode` with separate configurations for each.

```ruby
Shortcode.process("[quote]Hello World[/quote]")
shortcode = Shortcode.new
shortcode.process("[quote]Hello World[/quote]")
```

In a Rails app, you can create helper methods to handle your shortcoded content and use them in your views with something similar to `<%= content_html @page.content %>`. Those two helper method can be used if your content contains html to be escaped or not.
Expand All @@ -59,12 +62,23 @@ end
Any tags you wish to use with Shortcode need to be configured in the setup block, there are 2 types of tag, `block_tags` and `self_closing_tags`. Block tags have a matching open and close tag such as `[quote]A quote[/quote]`, self closing tags have no close tag, for example `[gallery]`. To define the tags Shortcode should parse do so in the configuration (in a Rails initializer for example) as follows:

```ruby
Shortcode.setup do |config|
shortcode = Shortcode.new
shortcode.setup do |config|
config.block_tags = [:quote, :list]
config.self_closing_tags = [:gallery, :widget]
end
```

Note that you can call the setup block multiple times if need be and add to it.

For example:

```ruby
shortcode.setup do |config|
config.block_tags << :other_tag
end
```

### Templates

Each shortcode tag needs a template in order to translate the shortcode into html (or other output). Templates can be written in erb, haml or slim and work in
Expand Down Expand Up @@ -118,7 +132,9 @@ The alternative way to define templates is to set them using the `templates` con
values containing a template string. For instance:

```ruby
Shortcode.setup do |config|
shortcode = Shortcode.new

shortcode.setup do |config|
config.templates = { gallery: 'template code' }
end
```
Expand All @@ -133,7 +149,9 @@ Note: it's NOT possible to load templates from a config option AND from the file
If you wish to use custom helper modules in templates you can do so by specifying the helpers in a setup block which should be an array. Methods in the helper modules will then become available within all templates.

```ruby
Shortcode.setup do |config|
shortcode = Shortcode.new

shortcode.setup do |config|
config.helpers = [CustomHelper, AnotherCustomHelper]
end
```
Expand Down Expand Up @@ -167,15 +185,15 @@ class GalleryPresenter

private

def images
Image.where("id IN (?)", @attributes[:ids])
end
def images
Image.where("id IN (?)", @attributes[:ids])
end
end
```

#### Using additional attributes

At times you may want to pass through additional attributes to a presenter, for instance if you have a [gallery] shortcode tag and you want to pull out all images for a post, this can be achived using additional attributes with a presenter.
At times you may want to pass through additional attributes to a presenter, for instance if you have a [gallery] shortcode tag and you want to pull out all images for a post, this can be achieved using additional attributes with a presenter.

```ruby
class GalleryPresenter
Expand All @@ -199,34 +217,39 @@ class GalleryPresenter

private

def images
@additional_attributes[:images].map &:url
end
def images
@additional_attributes[:images].map &:url
end
end
```

# The hash containing the images attribute is passed through to the presenter
# as the additional_attributes argument
Shortcode.process('[gallery]', { images: @post.images })
The hash containing the images attribute is passed through to the presenter as the additional_attributes argument to the `process` method.

```ruby
shortcode = Shortcode.new
shortcode.process('[gallery]', { images: @post.images })
```

#### Registering presenters

To register a presenter simply call `Shortcode.register_presenter` passing the presenter class e.g.
To register a presenter simply call `register_presenter` passing the presenter class e.g.

```ruby
shortcode = Shortcode.new

# A single presenter
Shortcode.register_presenter(CustomPresenter)
shortcode.register_presenter(CustomPresenter)

# Or multiple presenters in one call
Shortcode.register_presenter(CustomPresenter, AnotherPresenter)

shortcode.register_presenter(CustomPresenter, AnotherPresenter)
```

### Configuration

```ruby
Shortcode.setup do |config|
shortcode = Shortcode.new

shortcode.setup do |config|

# the template parser to use
config.template_parser = :erb # :erb, :haml, :slim supported, :erb is default
Expand Down Expand Up @@ -256,6 +279,23 @@ Shortcode.setup do |config|
end
```

### Singleton

You can optionally use Shortcode as a singleton instance with the same configuration throughout.

To do this, you call methods directly on the `Shortcode` class.

For example:

```ruby
Shortcode.setup do |config|
config.block_tags = [:quote]
end

Shortcode.register_presenter(QuotePresenterClass)

Shortcode.process('[quote]Some quote[/quote]')
```

## Contributing

Expand Down
38 changes: 26 additions & 12 deletions lib/shortcode.rb
Expand Up @@ -9,32 +9,46 @@
require 'slim'
rescue LoadError; end

module Shortcode
class Shortcode
# This is providedc for backwards compatibility
def self.process(string, additional_attributes=nil)
singleton.process(string, additional_attributes)
end

class << self
attr_writer :configuration
# This is provided for backwards compatibility
def self.singleton
@instance ||= new
end

def self.process(string, additional_attributes=nil)
Shortcode::Processor.new.process string, additional_attributes
# This is providedc for backwards compatibility
def self.setup(&prc)
singleton.setup(&prc)
end

# This is providedc for backwards compatibility
def self.register_presenter(*presenters)
singleton.register_presenter(*presenters)
end

def self.setup
def process(string, additional_attributes=nil)
Shortcode::Processor.new.process(string, configuration, additional_attributes)
end

def setup
yield configuration
end

def self.register_presenter(*presenters)
def register_presenter(*presenters)
presenters.each do |presenter|
Shortcode::Presenter.register presenter
configuration.register_presenter(presenter)
end
end

private

def self.configuration
@configuration ||= Configuration.new
end

def configuration
@configuration ||= Configuration.new
end
end

require 'shortcode/version'
Expand Down
8 changes: 8 additions & 0 deletions lib/shortcode/configuration.rb
Expand Up @@ -11,6 +11,9 @@ class Shortcode::Configuration
# Assigns helper modules to be included in templates
attr_accessor :helpers

# Allows presenters to be set that can be used to process shortcode arguments before rendered
attr_accessor :presenters

# Set the supported block_tags
attr_reader :block_tags
def block_tags=(block_tags)
Expand Down Expand Up @@ -38,5 +41,10 @@ def initialize
@self_closing_tags = []
@attribute_quote_type = '"'
@use_attribute_quotes = true
@presenters = {}
end

def register_presenter(presenter)
Shortcode::Presenter.register(self, presenter)
end
end
2 changes: 1 addition & 1 deletion lib/shortcode/exceptions.rb
@@ -1,4 +1,4 @@
module Shortcode
class Shortcode

# Raised when the template file can not be found
class TemplateNotFound < StandardError; end
Expand Down
88 changes: 59 additions & 29 deletions lib/shortcode/parser.rb
@@ -1,43 +1,73 @@
class Shortcode::Parser < Parslet::Parser
class Shortcode::Parser
def initialize(configuration)
@configuration = configuration
setup_rules
end

rule(:block_tag) { match_any_of Shortcode.configuration.block_tags }
rule(:self_closing_tag) { match_any_of Shortcode.configuration.self_closing_tags }
def parse(string)
klass_instance.parse(string)
end

rule(:quotes) { str(Shortcode.configuration.attribute_quote_type) }
def open(*args)
klass_instance.open(*args)
end

rule(:space) { str(' ').repeat(1) }
rule(:space?) { space.maybe }
rule(:newline) { (str("\r\n") | str("\n")) >> space? }
rule(:newline?) { newline.maybe }
rule(:whitespace) { (space | newline).repeat(1) }
private

rule(:key) { match('[a-zA-Z0-9\-_]').repeat(1) }
# This allows us to create a new class with the rules for the specific configuration
def klass
@klass ||= Class.new(Parslet::Parser)
end

rule(:value_with_quotes) { quotes >> (quotes.absent? >> any).repeat.as(:value) >> quotes }
rule(:value_without_quotes) { quotes.absent? >> ( (str(']') | whitespace).absent? >> any ).repeat.as(:value) }
rule(:value) { Shortcode.configuration.use_attribute_quotes ? value_with_quotes : (value_without_quotes | value_with_quotes) }
def klass_instance
@klass_instance ||= klass.new
end

rule(:option) { key.as(:key) >> str('=') >> value }
rule(:options) { (str(' ') >> option).repeat(1) }
rule(:options?) { options.repeat(0, 1) }
def setup_rules
define_match_any_of

rule(:open) { str('[') >> block_tag.as(:open) >> options?.as(:options) >> str(']') >> newline? }
rule(:close) { str('[/') >> block_tag.as(:close) >> str(']') >> newline? }
rule(:open_close) { str('[') >> self_closing_tag.as(:open_close) >> options?.as(:options) >> str(']') >> newline? }
shortcode_configuration = @configuration
klass.rule(:block_tag) { match_any_of shortcode_configuration.block_tags }
klass.rule(:self_closing_tag) { match_any_of shortcode_configuration.self_closing_tags }

rule(:text) { ((close | block | open_close).absent? >> any).repeat(1).as(:text) }
rule(:block) { (open >> (block | text | open_close).repeat.as(:inner) >> close) }
klass.rule(:quotes) { str(shortcode_configuration.attribute_quote_type) }

rule(:body) { (block | text | open_close).repeat.as(:body) }
root(:body)
klass.rule(:space) { str(' ').repeat(1) }
klass.rule(:space?) { space.maybe }
klass.rule(:newline) { (str("\r\n") | str("\n")) >> space? }
klass.rule(:newline?) { newline.maybe }
klass.rule(:whitespace) { (space | newline).repeat(1) }

private
klass.rule(:key) { match('[a-zA-Z0-9\-_]').repeat(1) }

klass.rule(:value_with_quotes) { quotes >> (quotes.absent? >> any).repeat.as(:value) >> quotes }
klass.rule(:value_without_quotes) { quotes.absent? >> ( (str(']') | whitespace).absent? >> any ).repeat.as(:value) }
klass.rule(:value) { shortcode_configuration.use_attribute_quotes ? value_with_quotes : (value_without_quotes | value_with_quotes) }

klass.rule(:option) { key.as(:key) >> str('=') >> value }
klass.rule(:options) { (str(' ') >> option).repeat(1) }
klass.rule(:options?) { options.repeat(0, 1) }

def match_any_of(tags)
return str('') if tags.length < 1
tags.map{ |tag| str(tag) }.inject do |tag_chain, tag|
tag_chain.send :|, tag
klass.rule(:open) { str('[') >> block_tag.as(:open) >> options?.as(:options) >> str(']') >> newline? }
klass.rule(:close) { str('[/') >> block_tag.as(:close) >> str(']') >> newline? }
klass.rule(:open_close) { str('[') >> self_closing_tag.as(:open_close) >> options?.as(:options) >> str(']') >> newline? }

klass.rule(:text) { ((close | block | open_close).absent? >> any).repeat(1).as(:text) }
klass.rule(:block) { (open >> (block | text | open_close).repeat.as(:inner) >> close) }

klass.rule(:body) { (block | text | open_close).repeat.as(:body) }
klass.root(:body)
end

def define_match_any_of
klass.send(:define_method, :match_any_of) do |tags|
if tags.length < 1
return str('')
else
tags.map{ |tag| str(tag) }.inject do |tag_chain, tag|
tag_chain.send :|, tag
end
end
end

end
end

0 comments on commit 93c6a4e

Please sign in to comment.