pax_global_header00006660000000000000000000000064136036561270014523gustar00rootroot0000000000000052 comment=a085164d64020a273949153999f01e7879885913 mustermann-1.1.1/000077500000000000000000000000001360365612700137145ustar00rootroot00000000000000mustermann-1.1.1/.gitignore000066400000000000000000000002551360365612700157060ustar00rootroot00000000000000*.gem *.rbc .bundle .config .yardoc .test_queue_stats .coverage Gemfile.lock InstalledFiles _yardoc doc/ lib/bundler/man pkg rdoc spec/reports test/tmp test/version_tmp tmp mustermann-1.1.1/.rspec000066400000000000000000000001071360365612700150270ustar00rootroot00000000000000-P '*/spec/**_spec.rb' -r bundler/setup --default-path . --color --tty mustermann-1.1.1/.travis.yml000066400000000000000000000005061360365612700160260ustar00rootroot00000000000000language: ruby sudo: false cache: bundler before_install: - gem update bundler rvm: - 2.2.10 - 2.3.8 - 2.4.5 - 2.5.3 - 2.6.5 - 2.7.0 - ruby-head matrix: allow_failures: - rvm: ruby-head fast_finish: true notifications: recipients: - mail@zzak.io slack: sinatrarb:LQGhUfGYcqRgRzwKea0bqUhY mustermann-1.1.1/.yardopts000066400000000000000000000000711360365612700155600ustar00rootroot00000000000000--charset utf-8 mustermann*/{lib,app}/**/*.rb ext/**/*.c mustermann-1.1.1/Gemfile000066400000000000000000000003351360365612700152100ustar00rootroot00000000000000source 'https://rubygems.org' require File.expand_path('../support/lib/support/projects', __FILE__) gem 'ruby2_keywords' path '.' do Support::Projects.each { |name| gem(name) } gem 'support', group: :development end mustermann-1.1.1/LICENSE000066400000000000000000000021171360365612700147220ustar00rootroot00000000000000Copyright (c) 2013-2017 Konstantin Haase Copyright (c) 2016-2017 Zachary Scott Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. mustermann-1.1.1/README.md000066400000000000000000000371111360365612700151760ustar00rootroot00000000000000# The Amazing Mustermann [![Build Status](https://travis-ci.org/sinatra/mustermann.svg?branch=master)](https://travis-ci.org/sinatra/mustermann) [![Coverage Status](https://coveralls.io/repos/github/rkh/mustermann/badge.svg?branch=master)](https://coveralls.io/github/rkh/mustermann?branch=master) [![Code Climate](https://img.shields.io/codeclimate/github/rkh/mustermann.svg)](https://codeclimate.com/github/rkh/mustermann) [![Dependency Status](https://gemnasium.com/rkh/mustermann.svg)](https://gemnasium.com/rkh/mustermann) [![Gem Version](https://img.shields.io/gem/v/mustermann.svg)](https://rubygems.org/gems/mustermann) [![Inline docs](http://inch-ci.org/github/rkh/mustermann.svg)](http://inch-ci.org/github/rkh/mustermann) [![Documentation](http://img.shields.io/:yard-docs-38c800.svg)](http://www.rubydoc.info/gems/mustermann/frames) [![License](http://img.shields.io/:license-MIT-38c800.svg)](http://rkh.mit-license.org) [![Badges](http://img.shields.io/:badges-9/9-38c800.svg)](http://img.shields.io) This repository contains multiple projects (each installable as separate gems). * **[mustermann](https://github.com/sinatra/mustermann/blob/master/mustermann/README.md): Your personal string matching expert. This is probably what you're looking for.** * [mustermann-contrib](https://github.com/sinatra/mustermann/blob/master/mustermann-contrib/README.md): A meta gem depending on all other official mustermann gems. * [mustermann-fileutils](https://github.com/sinatra/mustermann/blob/master/mustermann-contrib/README.md#-mustermann-fileutils): Efficient file system operations using Mustermann patterns. * [mustermann-strscan](https://github.com/sinatra/mustermann/blob/master/mustermann-contrib/README.md#-mustermann-strscan): A version of Ruby's [StringScanner](http://ruby-doc.org/stdlib-2.0/libdoc/strscan/rdoc/StringScanner.html) made for pattern objects. * [mustermann-visualizer](https://github.com/sinatra/mustermann/blob/master/mustermann-contrib/README.md#-mustermann-visualizer): Syntax highlighting and tree visualization for patterns.' * A selection of pattern types for mustermann, each as their own little library, see [below](#-pattern-types). ## Git versions with Bundler You can easily use the latest edge version from GitHub of any of these gems via [Bundler](http://bundler.io/): ``` ruby git 'https://github.com/rkh/mustermann.git' do gem 'mustermann' gem 'mustermann-rails' end ``` ## Pattern Types The `identity`, `regexp` and `sinatra` types are included in the `mustermann` gem, all the other types have their own gems.
Type Example Compatible with Notes
cake /:prefix/** CakePHP
express /:prefix+/:id(\d+) Express, pillar.js
flask /<prefix>/<int:id> Flask, Werkzeug
identity /image.png any software using strings Exact string matching (no parameter parsing).
pyramid /{prefix:.*}/{id} Pyramid, Pylons
rails /:slug(.:ext) Ruby on Rails, Journey, HTTP Router, Hanami, Scalatra (if configured), NYNY
regexp /(?<slug>[^\/]+) Oniguruma, Onigmo, regular expressions Created when you pass a regexp to Mustermann.new.
Does not support expanding or generating templates.
shell /*.{png,jpg} Unix Shell (bash, zsh) Does not support expanding or generating templates.
simple /:slug.:ext Sinatra (1.x), Scalatra, Dancer, Finatra, Spark, RCRouter, kick.js Implementation is a direct copy from Sinatra 1.3.
It is the predecessor of sinatra. Does not support expanding or generating templates.
sinatra /:slug(.:ext)? Sinatra (2.x), Padrino (>= 0.13.0), Pendragon, Angelo This is the default and the only type "invented here".
It is a superset of simple and has a common subset with template (and others).
uri-template /{+pre}/{page}{?q} RFC 6570, JSON API, JSON Home Documents and many more Standardized URI templates, can be generated from most other types.
Any software using Mustermann is obviously compatible with at least one of the above. ## Requirements Mustermann depends on [tool](https://github.com/rkh/tool) (which has been extracted from Mustermann and Sinatra 2.0), and a Ruby 2.2 compatible Ruby implementation. It is known to work on MRI 2.2. JRuby will hopefully be supported with the release of **JRuby 9000**. **Rubinius** is not currently supported. As of Rubinius 2.3.1, a large portion of the specs pass (3870 out of 3943), but certain parts are not working yet. If you need Ruby 1.9 support, you might be able to use the **unofficial** [mustermann19](https://rubygems.org/gems/mustermann19) gem based on [namusyaka's fork](https://github.com/namusyaka/mustermann19). ## Release History Mustermann follows [Semantic Versioning 2.0](http://semver.org/). Anything documented in the README or via YARD and not declared private is part of the public API. ### Stable Releases * **Mustermann 1.1.1** (2020-01-04) * Make sure that `require`ing ruby2_keywords when needed. Fixes [#102](https://github.com/sinatra/mustermann/issues/103) [@Annih](https://github.com/Annih) * **Mustermann 1.1.0** (2019-12-30) * Proper handling of `Mustermann::ExpandError`. Fixes [#88](https://github.com/sinatra/mustermann/issues/88) [@namusyaka](https://github.com/namusyaka) * Support Ruby 3 keyword arguments. [@mame](https://github.com/mame) * At the same time, we dropped a support that accepts options followed by mappings on `Mustermann::Mapper`. [Reference commit](https://github.com/sinatra/mustermann/pull/97/commits/4e134f5b46d8e5886b0f1590f5ff3f6ea4d2e81a) * Improve documentation and development. [@horaciob](https://github.com/horaciob), [@epistrephein](https://github.com/epistrephein), [@jbampton](https://github.com/jbampton), [@jkowens](https://github.com/jkowens), [@junaruga](https://github.com/junaruga) * **Mustermann 1.0.3** (2018-08-17) * Handle `with_look_ahead` on SafeRenderer. Fixes [sinatra/sinatra#1409](https://github.com/sinatra/sinatra/issues/1409) [@namusyaka](https://github.com/namusyaka) * Fix `EqualityMap#fetch` to be compatible with the fallback `Hash#fetch`. Fixes [#89](https://github.com/sinatra/mustermann/issues/89) [@eregon](https://github.com/eregon) * Improve code base and documentation. [@sonots](https://github.com/sonots), [@iguchi1124](https://github.com/iguchi1124) * **Mustermann 1.0.2** (2018-02-17) * Look ahead same patterns as its own when concatenation. Fixes [sinatra/sinatra#1361](https://github.com/sinatra/sinatra/issues/1361) [@namusyaka](https://github.com/namusyaka) * Improve development support and documentation. [@EdwardBetts](https://github.com/EdwardBetts), [@284km](https://github.com/284km), [@yb66](https://github.com/yb66) and [@garybernhardt](https://github.com/garybernhardt) * **Mustermann 1.0.1** (2017-08-26) #### Docs * Updating readme to list Ruby 2.2 as minimum [commit](https://github.com/sinatra/mustermann/commit/7c65d9637ed81c194e3d05f0ccf3cfe76f0cf53e) (@cassidycodes) * Fix rendering of HTML table [commit](https://github.com/sinatra/mustermann/commit/119a61f0e589cb9e917d8c901800a202bb66ff3b) (@stevenwilkin) * Update summary and description in gemspec file. [commit](https://github.com/sinatra/mustermann/commit/04de221a809527c2be8c3f08c40a4fcd53f2bd53) (@junaruga) #### Fixes * avoid infinite loop by removing comments when receiving extended regexp [commit](https://github.com/sinatra/mustermann/commit/fa20301167e1b22882415f1181c5e4e2d76b6ac6) (@namusyaka) * avoid unintended conflict of namespace [commit](https://github.com/sinatra/mustermann/commit/d3c9531d372522d693fa5f768f13dbaa1d881d88) (@namusyaka) * use Regexp#source instead of Regexp#inspect [commit](https://github.com/sinatra/mustermann/pull/73/commits/e9213748bda1773b1ad9838ef57a296f92c471e7) (@namusyaka) * **Mustermann 1.0.0** (2017-03-05) * First stable release. * Includes `mustermann`, and `mustermann-contrib` gems * Sinatra patterns: Allow | outside of parens. * Add concatenation support (`Mustermann::Pattern#+`). * `Mustermann::Sinatra#|` may now generate a Sinatra pattern instead of a real composite. * Add syntax highlighting support for composite patterns. * Remove routers (they were out of scope for the main gem). * Rails patterns: Add Rails 5.0 compatibility mode, make it default. * Moved `tool` gem `EqualityMap` to `Mustermann::EqualityMap` in core * Improve documentation. ### Development Releases * **Mustermann 0.4.0** (2014-11-26) * More Infos: [RubyGems.org](https://rubygems.org/gems/mustermann/versions/0.4.0), [RubyDoc.info](http://www.rubydoc.info/gems/mustermann/0.4.0/frames), [GitHub.com](https://github.com/rkh/mustermann/tree/v0.4.0) * Split into multiple gems. * Add `Pattern#to_proc`. * Add `Pattern#|`, `Pattern#&` and `Pattern#^`. * Add `Pattern#peek`, `Pattern#peek_size`, `Pattern#peek_match` and `Pattern#peek_params`. * Add `Mustermann::StringScanner`. * Add `Pattern#to_templates`. * Add `|` syntax to `sinatra` templates. * Add template style placeholders to `sinatra` templates. * Add `cake`, `express`, `flask` and `pyramid` patterns. * Allow passing in additional value behavior directly to `Pattern#expand`. * Fix expanding of multiple splats. * Add expanding to `identity` patterns. * Add `mustermann-fileutils`. * Make expander accept hashes with string keys. * Allow named splats to be named splat. * Support multiple Rails versions. * Type option can be set to nil to get the default type. * Add `mustermann-visualizer`. * **Mustermann 0.3.1** (2014-09-12) * More Infos: [RubyGems.org](https://rubygems.org/gems/mustermann/versions/0.3.1), [RubyDoc.info](http://www.rubydoc.info/gems/mustermann/0.3.1/frames), [GitHub.com](https://github.com/rkh/mustermann/tree/v0.3.1) * Speed up pattern generation and matching (thanks [Daniel Mendler](https://github.com/minad)) * Small change so `Mustermann === Mustermann.new('...')` returns `true`. * **Mustermann 0.3.0** (2014-08-18) * More Infos: [RubyGems.org](https://rubygems.org/gems/mustermann/versions/0.3.0), [RubyDoc.info](http://www.rubydoc.info/gems/mustermann/0.3.0/frames), [GitHub.com](https://github.com/rkh/mustermann/tree/v0.3.0) * Add `regexp` pattern. * Add named splats to Sinatra patterns. * Add `Mustermann::Mapper`. * Improve duck typing support. * Improve documentation. * **Mustermann 0.2.0** (2013-08-24) * More Infos: [RubyGems.org](https://rubygems.org/gems/mustermann/versions/0.2.0), [RubyDoc.info](http://www.rubydoc.info/gems/mustermann/0.2.0/frames), [GitHub.com](https://github.com/rkh/mustermann/tree/v0.2.0) * Add first class expander objects. * Add params casting for expander. * Add simple router and rack router. * Add weak equality map to significantly improve performance. * Fix Ruby warnings. * Improve documentation. * Refactor pattern validation, AST transformations. * Increase test coverage (from 100%+ to 100%++). * Improve JRuby compatibility. * Work around bug in 2.0.0-p0. * **Mustermann 0.1.0** (2013-05-12) * More Infos: [RubyGems.org](https://rubygems.org/gems/mustermann/versions/0.1.0), [RubyDoc.info](http://www.rubydoc.info/gems/mustermann/0.1.0/frames), [GitHub.com](https://github.com/rkh/mustermann/tree/v0.1.0) * Add `Pattern#expand` for generating strings from patterns. * Add better internal API for working with the AST. * Improved documentation. * Avoids parsing the path twice when used as Sinatra extension. * Better exceptions for unknown pattern types. * Better handling of edge cases around extend. * More specs to ensure API stability. * Largely rework internals of Sinatra, Rails and Template patterns. * **Mustermann 0.0.1** (2013-04-27) * More Infos: [RubyGems.org](https://rubygems.org/gems/mustermann/versions/0.0.1), [RubyDoc.info](http://www.rubydoc.info/gems/mustermann/0.0.1/frames), [GitHub.com](https://github.com/rkh/mustermann/tree/v0.0.1) * Initial Release. mustermann-1.1.1/Rakefile000066400000000000000000000011121360365612700153540ustar00rootroot00000000000000task(:rspec) { ruby '-S rspec' } task(:doc_stats) { ruby '-S yard stats' } task default: [:rspec, :doc_stats] task :pkg do require 'bundler/setup' require 'support/projects' require 'mustermann/version' rm_rf 'pkg' mkdir 'pkg' Support::Projects.each do |project| cd project do ruby "-S gem build #{project}.gemspec" mv "#{project}-#{Mustermann::VERSION}.gem", '../pkg/' end end end task release: :pkg do cd 'pkg' do Support::Projects.each do |project| ruby "-S gem push #{project}-#{Mustermann::VERSION}.gem" end end end mustermann-1.1.1/mustermann-contrib/000077500000000000000000000000001360365612700175435ustar00rootroot00000000000000mustermann-1.1.1/mustermann-contrib/LICENSE000066400000000000000000000021171360365612700205510ustar00rootroot00000000000000Copyright (c) 2013-2017 Konstantin Haase Copyright (c) 2016-2017 Zachary Scott Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. mustermann-1.1.1/mustermann-contrib/README.md000066400000000000000000001107211360365612700210240ustar00rootroot00000000000000# The Amazing Mustermann - Contrib Edition This is a meta gem that depends on all mustermann gems. ``` console $ gem install mustermann-contrib Successfully installed mustermann-1.0.0 Successfully installed mustermann-contrib-1.0.0 ... ``` Also handy for your `Gemfile`: ``` ruby gem 'mustermann-contrib' ``` Alternatively, you can use latest HEAD from github: ```ruby github 'sinatra/mustermann' do gem 'mustermann' gem 'mustermann-contrib' end ``` # CakePHP Syntax for Mustermann This gem implements the `cake` pattern type for Mustermann. It is compatible with [CakePHP](http://cakephp.org/) 2.x and 3.x. ## Overview **Supported options:** `capture`, `except`, `greedy`, `space_matches_plus`, `uri_decode`, and `ignore_unknown_options`. **External documentation:** [CakePHP 2.0 Routing](http://book.cakephp.org/2.0/en/development/routing.html), [CakePHP 3.0 Routing](http://book.cakephp.org/3.0/en/development/routing.html) CakePHP patterns feature captures and unnamed splats. Captures are prefixed with a colon and splats are either a single asterisk (parsing segments into an array) or a double asterisk (parsing segments as a single string). ``` ruby require 'mustermann/cake' Mustermann.new('/:name/*', type: :cake).params('/a/b/c') # => { name: 'a', splat: ['b', 'c'] } Mustermann.new('/:name/**', type: :cake).params('/a/b/c') # => { name: 'a', splat: 'b/c' } pattern = Mustermann.new('/:name') pattern.respond_to? :expand # => true pattern.expand(name: 'foo') # => '/foo' pattern.respond_to? :to_templates # => true pattern.to_templates # => ['/{name}'] ``` ## Syntax
Syntax Element Description
:name Captures anything but a forward slash in a semi-greedy fashion. Capture is named name. Capture behavior can be modified with capture and greedy option.
* Captures anything in a non-greedy fashion. Capture is named splat. It is always an array of captures, as you can use it more than once in a pattern.
** Captures anything in a non-greedy fashion. Capture is named splat. It is always an array of captures, as you can use it more than once in a pattern. The value matching a single ** will be split at slashes when parsed into params.
/ Matches forward slash. Does not match URI encoded version of forward slash.
any other character Matches exactly that character or a URI encoded version of it.
# Express Syntax for Mustermann This gem implements the `express` pattern type for Mustermann. It is compatible with [Express](http://expressjs.com/) and [pillar.js](https://pillarjs.github.io/). ## Overview **Supported options:** `capture`, `except`, `greedy`, `space_matches_plus`, `uri_decode`, and `ignore_unknown_options`. **External documentation:** [path-to-regexp](https://github.com/pillarjs/path-to-regexp#path-to-regexp), [live demo](http://forbeslindesay.github.io/express-route-tester/) Express patterns feature named captures (with repetition support via suffixes) that start with a colon and can have an optional regular expression constraint or unnamed captures that require a constraint. ``` ruby require 'mustermann/express' Mustermann.new('/:name/:rest+', type: :express).params('/a/b/c') # => { name: 'a', rest: 'b/c' } pattern = Mustermann.new('/:name', type: :express) pattern.respond_to? :expand # => true pattern.expand(name: 'foo') # => '/foo' pattern.respond_to? :to_templates # => true pattern.to_templates # => ['/{name}'] ``` ## Syntax
Syntax Element Description
:name Captures anything but a forward slash in a semi-greedy fashion. Capture is named name. Capture behavior can be modified with capture and greedy option.
:name+ Captures one or more segments (with segments being separated by forward slashes). Capture is named name. Capture behavior can be modified with capture option.
:name* Captures zero or more segments (with segments being separated by forward slashes). Capture is named name. Capture behavior can be modified with capture option.
:name? Captures anything but a forward slash in a semi-greedy fashion. Capture is named name. Also matches an empty string. Capture behavior can be modified with capture and greedy option.
:name(regexp) Captures anything matching the regexp regular expression. Capture is named name. Capture behavior can be modified with capture.
(regexp) Captures anything matching the regexp regular expression. Capture is named splat. Capture behavior can be modified with capture.
/ Matches forward slash. Does not match URI encoded version of forward slash.
any other character Matches exactly that character or a URI encoded version of it.
# FileUtils for Mustermann This gem implements efficient file system operations for Mustermann patterns. ## Globbing All operations work on a list of files described by one or more pattern. ``` ruby require 'mustermann/file_utils' Mustermann::FileUtils[':base.:ext'] # => ['example.txt'] Mustermann::FileUtils.glob(':base.:ext') do |file, params| file # => "example.txt" params # => {"base"=>"example", "ext"=>"txt"} end ``` To avoid having to loop over all files and see if they match, it will generate a glob pattern resembling the Mustermann pattern as closely as possible. ``` ruby require 'mustermann/file_utils' Mustermann::FileUtils.glob_pattern('/:name') # => '/*' Mustermann::FileUtils.glob_pattern('src/:path/:file.(js|rb)') # => 'src/**/*/*.{js,rb}' Mustermann::FileUtils.glob_pattern('{a,b}/*', type: :shell) # => '{a,b}/*' pattern = Mustermann.new('/foo/:page', '/bar/:page') # => # Mustermann::FileUtils.glob_pattern(pattern) # => "{/foo/*,/bar/*}" ``` ## Mapping It is also possible to search for files and have their paths mapped onto another path in one method call: ``` ruby require 'mustermann/file_utils' Mustermann::FileUtils.glob_map(':base.:ext' => ':base.bak.:ext') # => {'example.txt' => 'example.bak.txt'} Mustermann::FileUtils.glob_map(':base.:ext' => :base) { |file, mapped| mapped } # => ['example'] ``` This mechanism allows things like copying, renaming and linking files: ``` ruby require 'mustermann/file_utils' # copies example.txt to example.bak.txt Mustermann::FileUtils.cp(':base.:ext' => ':base.bak.:ext') # copies Foo.app/example.txt to Foo.back.app/example.txt Mustermann::FileUtils.cp_r(':base.:ext' => ':base.bak.:ext') # creates a symbolic link from bin/example to lib/example.rb Mustermann::FileUtils.ln_s('lib/:name.rb' => 'bin/:name') ``` # Flask Syntax for Mustermann This gem implements the `flask` pattern type for Mustermann. It is compatible with [Flask](http://flask.pocoo.org/) and [Werkzeug](http://werkzeug.pocoo.org/). ## Overview **Supported options:** `capture`, `except`, `greedy`, `space_matches_plus`, `uri_decode`, `converters` and `ignore_unknown_options` **External documentation:** [Werkzeug: URL Routing](http://werkzeug.pocoo.org/docs/0.9/routing/) ``` ruby require 'mustermann/flask' Mustermann.new('//', type: :flask).params('/a/b/c') # => { prefix: 'a', page: 'b/c' } pattern = Mustermann.new('/', type: :flask) pattern.respond_to? :expand # => true pattern.expand(name: 'foo') # => '/foo' pattern.respond_to? :to_templates # => true pattern.to_templates # => ['/{name}'] ``` ## Syntax
Syntax Element Description
<name> Captures anything but a forward slash in a semi-greedy fashion. Capture is named name. Capture behavior can be modified with capture and greedy option.
<converter:name> Captures depending on the converter constraint. Capture is named name. Capture behavior can be modified with capture and greedy option. See below.
<converter(arguments):name> Captures depending on the converter constraint. Capture is named name. Capture behavior can be modified with capture and greedy option. Arguments are separated by comma. An argument can be a simple string, a string enclosed in single or double quotes, or a key value pair (keys and values being separated by an equal sign). See below.
/ Matches forward slash. Does not match URI encoded version of forward slash.
any other character Matches exactly that character or a URI encoded version of it.
## Converters ### Builtin Converters #### `string` Possible arguments: `minlength`, `maxlength`, `length` Captures anything but a forward slash in a semi-greedy fashion. Capture behavior can be modified with capture and greedy option. This is also the default converter. Examples: ``` ``` #### `int` Possible arguments: `min`, `max`, `fixed_digits` Captures digits. Captured value will be converted to an Integer. Examples: ``` ``` #### `float` Possible arguments: `min`, `max` Captures digits with a dot. Captured value will be converted to an Float. Examples: ``` ``` #### `path` Captures anything in a non-greedy fashion. Example: ``` ``` #### `any` Possible arguments: List of accepted strings. Captures anything that matches one of the arguments exactly. Example: ``` ``` ### Custom Converters [Flask patterns](#-pattern-details-flask) support registering custom converters. A converter object may implement any of the following methods: * `convert`: Should return a block converting a string value to whatever value should end up in the `params` hash. * `constraint`: Should return a regular expression limiting which input string will match the capture. * `new`: Returns an object that may respond to `convert` and/or `constraint` as described above. Any arguments used for the converter inside the pattern will be passed to `new`. ``` ruby require 'mustermann/flask' SimpleConverter = Struct.new(:constraint, :convert) id_converter = SimpleConverter.new(/\d/, -> s { s.to_i }) class NumConverter def initialize(base: 10) @base = Integer(base) end def convert -> s { s.to_i(@base) } end def constraint @base > 10 ? /[\da-#{(@base-1).to_s(@base)}]/ : /[0-#{@base-1}]/ end end pattern = Mustermann.new('///', type: :flask, converters: { id: id_converter, num: NumConverter}) pattern.params('/10/12/f1') # => {"id"=>10, "oct"=>10, "hex"=>241} ``` ### Global Converters It is also possible to register a converter for all flask patterns, using `register_converter`: ``` ruby Mustermann::Flask.register_converter(:id, id_converter) Mustermann::Flask.register_converter(:num, NumConverter) pattern = Mustermann.new('///', type: :flask) pattern.params('/10/12/f1') # => {"id"=>10, "oct"=>10, "hex"=>241} ``` There is a handy syntax for quickly creating new converter classes: When you pass a block instead of a converter object, it will yield a generic converter with setters and getters for `convert` and `constraint`, and any arguments passed to the converter. ``` ruby require 'mustermann/flask' Mustermann::Flask.register_converter(:id) do |converter| converter.constraint = /\d/ converter.convert = -> s { s.to_i } end Mustermann::Flask.register_converter(:num) do |converter, base: 10| converter.constraint = base > 10 ? /[\da-#{(@base-1).to_s(base)}]/ : /[0-#{base-1}]/ converter.convert = -> s { s.to_i(base) } end pattern = Mustermann.new('///', type: :flask) pattern.params('/10/12/f1') # => {"id"=>10, "oct"=>10, "hex"=>241} ``` ### Subclassing Registering global converters will make these available for all Flask patterns. It might even override already registered converters. This global state might break unrelated code. It is therefore recommended that, if you don't want to pass in the converters option for every pattern, you create your own subclass of `Mustermann::Flask`. ``` ruby require 'mustermann/flask' MyFlask = Class.new(Mustermann::Flask) MyFlask.register_converter(:id) do |converter| converter.constraint = /\d/ converter.convert = -> s { s.to_i } end MyFlask.register_converter(:num) do |converter, base: 10| converter.constraint = base > 10 ? /[\da-#{(@base-1).to_s(base)}]/ : /[0-#{base-1}]/ converter.convert = -> s { s.to_i(base) } end pattern = MyFlask.new('///') pattern.params('/10/12/f1') # => {"id"=>10, "oct"=>10, "hex"=>241} ``` You can even register this type for usage with `Mustermann.new`: ``` ruby Mustermann.register(:my_flask, MyFlask) pattern = Mustermann.new('///', type: :my_flask) pattern.params('/10/12/f1') # => {"id"=>10, "oct"=>10, "hex"=>241} ``` # Pyramid Syntax for Mustermann This gem implements the `pyramid` pattern type for Mustermann. It is compatible with [Pyramid](http://www.pylonsproject.org/projects/pyramid/about) and [Pylons](http://www.pylonsproject.org/projects/pylons-framework/about). ## Overview **Supported options:** `capture`, `except`, `greedy`, `space_matches_plus`, `uri_decode` and `ignore_unknown_options` **External Documentation:** [Pylons Framework: URL Configuration](http://docs.pylonsproject.org/projects/pylons-webframework/en/latest/configuration.html#url-config), [Pylons Book: Routes in Detail](http://pylonsbook.com/en/1.0/urls-routing-and-dispatch.html#routes-in-detail), [Pyramid: Route Pattern Syntax](http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/urldispatch.html#route-pattern-syntax) ``` ruby require 'mustermann/pyramid' Mustermann.new('/{prefix}/*suffix', type: :pyramid).params('/a/b/c') # => { prefix: 'a', suffix: ['b', 'c'] } pattern = Mustermann.new('/{name}', type: :pyramid) pattern.respond_to? :expand # => true pattern.expand(name: 'foo') # => '/foo' pattern.respond_to? :to_templates # => true pattern.to_templates # => ['/{name}'] ``` ## Syntax
Syntax Element Description
{name} Captures anything but a forward slash in a semi-greedy fashion. Capture is named name. Capture behavior can be modified with capture and greedy option.
{name:regexp} Captures anything matching the regexp regular expression. Capture is named name. Capture behavior can be modified with capture.
*name Captures anything in a non-greedy fashion. Capture is named name.
/ Matches forward slash. Does not match URI encoded version of forward slash.
any other character Matches exactly that character or a URI encoded version of it.
# Rails Syntax for Mustermann This gem implements the `rails` pattern type for Mustermann. It is compatible with [Ruby on Rails](http://rubyonrails.org/), [Journey](https://github.com/rails/journey), the [http_router gem](https://github.com/joshbuddy/http_router), [Lotus](http://lotusrb.org/) and [Scalatra](http://scalatra.org/) (if [configured](http://scalatra.org/2.3/guides/http/routes.html#toc_248)) ## Overview **Supported options:** `capture`, `except`, `greedy`, `space_matches_plus`, `uri_decode`, `version`, and `ignore_unknown_options`. **External documentation:** [Ruby on Rails Guides: Routing](http://guides.rubyonrails.org/routing.html). ``` ruby require 'mustermann' pattern = Mustermann.new('/:example', type: :rails) pattern === "/foo.bar" # => true pattern === "/foo/bar" # => false pattern.params("/foo.bar") # => { "example" => "foo.bar" } pattern.params("/foo/bar") # => nil pattern = Mustermann.new('/:example(/:optional)', type: :rails) pattern === "/foo.bar" # => true pattern === "/foo/bar" # => true pattern.params("/foo.bar") # => { "example" => "foo.bar", "optional" => nil } pattern.params("/foo/bar") # => { "example" => "foo", "optional" => "bar" } pattern = Mustermann.new('/*example', type: :rails) pattern === "/foo.bar" # => true pattern === "/foo/bar" # => true pattern.params("/foo.bar") # => { "example" => "foo.bar" } pattern.params("/foo/bar") # => { "example" => "foo/bar" } ``` ## Rails Compatibility Rails syntax changed over time. You can target different Ruby on Rails versions by setting the `version` option to the desired Rails version. The default is `4.2`. Versions prior to `2.3` are not supported. ``` ruby require 'mustermann' Mustermann.new('/', type: :rails, version: "2.3") Mustermann.new('/', type: :rails, version: "3.0.0") require 'rails' Mustermann.new('/', type: :rails, version: Rails::VERSION::STRING) ``` ## Syntax
Syntax Element Description
:name Captures anything but a forward slash in a semi-greedy fashion. Capture is named name. Capture behavior can be modified with tt>capture and greedy option.
*name Captures anything in a non-greedy fashion. Capture is named name.
(expression) Enclosed expression is optional. Not available in 2.3 compatibility mode.
/ Matches forward slash. Does not match URI encoded version of forward slash.
\x In 3.x compatibility mode and starting with 4.2: Matches x or URI encoded version of x. For instance \* matches *.
In 4.0 or 4.1 compatibility mode: \ is ignored, x is parsed normally.
expression | expression 3.2+ mode: This will raise a `Mustermann::ParseError`. While Ruby on Rails happily parses this character, it will result in broken routes due to a buggy implementation.
5.0 mode: It will match if any of the nested expressions matches.
any other character Matches exactly that character or a URI encoded version of it.
# Shell Syntax for Mustermann This gem implements the `shell` pattern type for Mustermann. It is compatible with common Unix shells (like bash or zsh). ## Overview **Supported options:** `uri_decode` and `ignore_unknown_options`. **External documentation:** [Ruby's fnmatch](http://www.ruby-doc.org/core-2.1.4/File.html#method-c-fnmatch), [Wikipedia: Glob (programming)](http://en.wikipedia.org/wiki/Glob_(programming)) ``` ruby require 'mustermann' pattern = Mustermann.new('/*', type: :shell) pattern === "/foo.bar" # => true pattern === "/foo/bar" # => false pattern = Mustermann.new('/**/*', type: :shell) pattern === "/foo.bar" # => true pattern === "/foo/bar" # => true pattern = Mustermann.new('/{foo,bar}', type: :shell) pattern === "/foo" # => true pattern === "/bar" # => true pattern === "/baz" # => false ``` ## Syntax
Syntax Element Description
* Matches anything but a slash.
** Matches anything.
[set] Matches one character in set.
{a,b} Matches a or b.
\x Matches x or URI encoded version of x. For instance \* matches *.
any other character Matches exactly that character or a URI encoded version of it.
# Simple Syntax for Mustermann This gem implements the `simple` pattern type for Mustermann. It is compatible with [Sinatra](http://www.sinatrarb.com/) (1.x), [Scalatra](http://scalatra.org/) and [Dancer](http://perldancer.org/). ## Overview **Supported options:** `greedy`, `space_matches_plus`, `uri_decode` and `ignore_unknown_options`. This is useful for porting an application that relies on this behavior to a later Sinatra version and to make sure Sinatra 2.0 patterns do not decrease performance. Simple patterns internally use the same code older Sinatra versions used for compiling the pattern. Error messages for broken patterns will therefore not be as informative as for other pattern implementations. ``` ruby require 'mustermann' pattern = Mustermann.new('/:example', type: :simple) pattern === "/foo.bar" # => true pattern === "/foo/bar" # => false pattern.params("/foo.bar") # => { "example" => "foo.bar" } pattern.params("/foo/bar") # => nil pattern = Mustermann.new('/:example/?:optional?', type: :simple) pattern === "/foo.bar" # => true pattern === "/foo/bar" # => true pattern.params("/foo.bar") # => { "example" => "foo.bar", "optional" => nil } pattern.params("/foo/bar") # => { "example" => "foo", "optional" => "bar" } pattern = Mustermann.new('/*', type: :simple) pattern === "/foo.bar" # => true pattern === "/foo/bar" # => true pattern.params("/foo.bar") # => { "splat" => ["foo.bar"] } pattern.params("/foo/bar") # => { "splat" => ["foo/bar"] } ``` ## Syntax
Syntax Element Description
:name Captures anything but a forward slash in a greedy fashion. Capture is named name.
* Captures anything in a non-greedy fashion. Capture is named splat. It is always an array of captures, as you can use * more than once in a pattern.
x? Makes x optional. For instance foo? matches foo or fo.
/ Matches forward slash. Does not match URI encoded version of forward slash.
any special character Matches exactly that character or a URI encoded version of it.
any other character Matches exactly that character.
# String Scanner for Mustermann This gem implements `Mustermann::StringScanner`, a tool inspired by Ruby's [`StringScanner`]() class. ``` ruby require 'mustermann/string_scanner' scanner = Mustermann::StringScanner.new("here is our example string") scanner.scan("here") # => "here" scanner.getch # => " " if scanner.scan(":verb our") scanner.scan(:noun, capture: :word) scanner[:verb] # => "is" scanner[:nound] # => "example" end scanner.rest # => "string" ``` You can pass it pattern objects directly: ``` ruby pattern = Mustermann.new(':name') scanner.check(pattern) ``` Or have `#scan` (and other methods) check these for you. ``` ruby scanner.check('{name}', type: :template) ``` You can also pass in default options for ad hoc patterns when creating the scanner: ``` ruby scanner = Mustermann::StringScanner.new(input, type: :shell) ``` # URI Template Syntax for Mustermann This gem implements the `uri-template` (or `template`) pattern type for Mustermann. It is compatible with [RFC 6570](https://tools.ietf.org/html/rfc6570) (level 4), [JSON API](http://jsonapi.org/), [JSON Home Documents](http://tools.ietf.org/html/draft-nottingham-json-home-02) and [many more](https://code.google.com/p/uri-templates/wiki/Implementations) ## Overview **Supported options:** `capture`, `except`, `greedy`, `space_matches_plus`, `uri_decode`, and `ignore_unknown_options`. Please keep the following in mind: > "Some URI Templates can be used in reverse for the purpose of variable matching: comparing the template to a fully formed URI in order to extract the variable parts from that URI and assign them to the named variables. Variable matching only works well if the template expressions are delimited by the beginning or end of the URI or by characters that cannot be part of the expansion, such as reserved characters surrounding a simple string expression. In general, regular expression languages are better suited for variable matching." > — *RFC 6570, Sec 1.5: "Limitations"* ``` ruby require 'mustermann' pattern = Mustermann.new('/{example}', type: :template) pattern === "/foo.bar" # => true pattern === "/foo/bar" # => false pattern.params("/foo.bar") # => { "example" => "foo.bar" } pattern.params("/foo/bar") # => nil pattern = Mustermann.new("{/segments*}/{page}{.ext,cmpr:2}", type: :template) pattern.params("/a/b/c.tar.gz") # => {"segments"=>["a","b"], "page"=>"c", "ext"=>"tar", "cmpr"=>"gz"} ``` ## Generating URI Templates You do not need to use URI templates (and this gem) if all you want is reusing them for hypermedia links. Most other pattern types support generating these (via `#to_pattern`): ``` ruby require 'mustermann' Mustermann.new('/:name').to_templates # => ['/{name}'] ``` Moreover, Mustermann's default pattern type implements a subset of URI templates (`{capture}` and `{+capture}`) and can therefore also be used for simple templates/ ``` ruby require 'mustermann' Mustermann.new('/{name}').expand(name: "example") # => "/example" ``` ## Syntax
Syntax Element Description
{o var m, var m, ...} Captures expansion. Operator o: + # . / ; ? & or none. Modifier m: :num * or none.
/ Matches forward slash. Does not match URI encoded version of forward slash.
any other character Matches exactly that character or a URI encoded version of it.
The operators `+` and `#` will always match non-greedy, whereas all other operators match semi-greedy by default. All modifiers and operators are supported. However, it does not parse lists as single values without the *explode* modifier (aka *star*). Parametric operators (`;`, `?` and `&`) currently only match parameters in given order. Note that it differs from URI templates in that it takes the unescaped version of special character instead of the escaped version. If you reuse the exact same templates and expose them via an external API meant for expansion, you should set `uri_decode` to `false` in order to conform with the specification. # Mustermann Pattern Visualizer With this gem, you can visualize the internal structure of a Mustermann pattern: * You can generate a **syntax highlighted** version of a pattern object. Both HTML/CSS based highlighting and ANSI color code based highlighting is supported. * You can turn a pattern object into a **tree** (with ANSI color codes) representing the internal AST. This of course only works for AST based patterns. ## Syntax Highlighting ![](highlighting.png) Loading `mustermann/visualizer` will automatically add `to_html` and `to_ansi` to pattern objects. ``` ruby require 'mustermann/visualizer' puts Mustermann.new('/:name').to_ansi puts Mustermann.new('/:name').to_html ``` Alternatively, you can also create a separate `highlight` object, which allows finer grained control and more formats: ``` ruby require 'mustermann/visualizer' pattern = Mustermann.new('/:name') highlight = Mustermann::Visualizer.highlight(pattern) puts highlight.to_ansi ``` ### `inspect` mode By default, the highlighted string will be a colored version of `to_s`. It is also possible to produce a colored version of `inspect` ``` ruby require 'mustermann/visualizer' pattern = Mustermann.new('/:name') # directly from the pattern puts pattern.to_ansi(inspect: true) # via the highlighter highlight = Mustermann::Visualizer.highlight(pattern, inspect: true) puts highlight.to_ansi ``` ### Themes ![](theme.png) element | inherits style from | default theme | note -------------|---------------------|---------------|------------------------- default | | #839496 | ANSI `\e[10m` if not set special | default | #268bd2 | capture | special | #cb4b16 | name | | #b58900 | always inside `capture` char | default | | expression | capture | | only exists in URI templates composition | special | | meta style, does not exist directly composite | composition | | used for composite patterns (contains `root`s) group | composition | | union | composition | | optional | special | | root | default | | wraps the whole pattern separator | char | #93a1a1 | splat | capture | | named_splat | splat | | variable | capture | | always inside `expression` escaped | char | #93a1a1 | escaped_char | | | always inside `escaped` quote | special | #dc322f | always outside of `root` type | special | | always inside `composite`, outside of `root` illegal | special | #8b0000 | You can set theme any of the above elements. The default theme will only be applied if no custom theming is used. ``` ruby # custom theme with highlight object highlight = Mustermann::Visualizer.highlight(pattern, special: "#08f") puts highlight.to_ansi ``` Themes apply both to ANSI and to HTML/CSS output. The exact ANSI code used depends on the terminal and its capabilities. ### HTML and CSS By default, the syntax elements will be translated into `span` tags with `style` attributes. ``` ruby Mustermann.new('/:name').to_html ``` ``` html /:name ``` You can also set the `css` option to `true` to make it include a stylesheet instead. ``` ruby Mustermann.new('/:name').to_html(css: true) ``` ``` html /:name ``` Or you can set it to `false`, which will omit `style` attributes, but include `class` attributes. ``` html /:name ``` It is possible to change the class prefix and the tag used. ``` ruby Mustermann.new('/:name').to_html(css: false, class_prefix: "mm_", tag: "tt") ``` ``` html /:name ``` If you create a highlight object, you can ask it for its `stylesheet`. ``` erb <% highlight = Mustermann::Visualizer.highlight("/:name") %> <%= highlight.to_html(css: false) %> ``` ### Other formats If you create a highlight object, you have two other formats available: Hansi template strings and s-expression like strings. These might be useful if you want to check how a theme will be applied or as intermediate format for highlighting by other means. ``` ruby require 'mustermann/visualizer' highlight = Mustermann::Visualizer.highlight("/:page") puts highlight.to_hansi_template puts highlight.to_sexp ``` **Hansi template strings** wrap elements in tags that are similar to XML tags (though they are not, entity encoding and attributes are not supported, escaping works with a slash, so an escaped `>` would be `\>`, not `>`). ``` xml /:page ``` The **s-expression like syntax** looks as follows: ``` (root (separator /) (capture : (name page))) ``` * An expression is enclosed by parens and contains elements separated by spaces. The first element in the expression type (corresponding to themeable elements). These are simple strings. The other elements are either expressions, simple strings or full strings. * Simple strings do not contain spaces, parens, single or double quotes or any character that needs to be escaped. * Full strings are Ruby strings enclosed by double quotes. * Spaces before or after parens are optional. ### IRB/Pry integration When `mustermann` is being loaded from within an IRB or Pry session, it will automatically load `mustermann/visualizer` too, if possible. When displayed as result, it will be highlighted. ![](irb.png) In Pry, this will even work when nested inside other objects (like as element on an array). ## Tree Rendering ![](tree.png) Loading `mustermann/visualizer` will automatically add `to_tree` to pattern objects. ``` ruby require 'mustermann/visualizer' puts Mustermann.new("/:page(.:ext)?/*action").to_tree ``` For patterns not based on an AST (shell, simple, regexp), it will print out a single line: pattern (not AST based) "/example" It will display a tree for identity patterns. While these are not based on an AST internally, Mustermann supports generating an AST for these patterns. mustermann-1.1.1/mustermann-contrib/examples/000077500000000000000000000000001360365612700213615ustar00rootroot00000000000000mustermann-1.1.1/mustermann-contrib/examples/highlighting.rb000066400000000000000000000022741360365612700243600ustar00rootroot00000000000000require 'bundler/setup' require 'mustermann/visualizer' Hansi.mode = ARGV[0].to_i if ARGV.any? def self.example(type, *patterns) print Hansi.render(:bold, " #{type}: ".ljust(14)) patterns.each do |pattern| pattern = Mustermann.new(pattern, type: type) space_after = pattern.to_s.size > 24 ? " " : " " * (25 - pattern.to_s.size) highlight = Mustermann::Visualizer.highlight(pattern, inspect: true) print highlight.to_ansi + space_after end puts end puts example(:cake, '/:prefix/**') example(:express, '/:prefix+/:id(\d+)', '/:page/:slug+') example(:flask, '//', '/user/') example(:identity, '/image.png') example(:pyramid, '/{prefix:.*}/{id}', '/{page}/*slug') example(:rails, '/:slug(.:ext)') example(:regexp, '/(?[^/]+)', '/(?:page|user)/(\d+)') example(:shell, '/**/*', '/\{a,b\}/{a,b}') example(:simple, '/:page/*slug') example(:sinatra, '/:page/*slug', '/users/{id}?') example(:template, '/{+pre}/{page}{?q,p}', '/users/{id}?') puts example(:composition) composite = Mustermann.new("/{a}", "/{b}/{c}") puts " " + composite.to_ansi puts " " + (Mustermann.new("/") ^ composite).to_ansi putsmustermann-1.1.1/mustermann-contrib/highlighting.png000066400000000000000000002534461360365612700227340ustar00rootroot00000000000000PNG  IHDRa? iCCPICC ProfileH wTS-@轷{"ͮȠ#(:TGG@ƂbȠ> 6T z'[gg}V>ZO(LG22E!n!PA A txB& ucqMtB >|(-e |cYb3sWV q@tO$r1`uQe&db,O%`|c|߈s22c:M cj c/[}O3sg_3Cِ&9cJ $bse a%v" yjWʆQW56H61j56"e52LL LL^jƘ00bfmnv잹yy+  E Kzn˗VVVn[37[YٴLjr v;7v'>g!͡ar;j8G8NqN?9:;h$4u҂Қ#;2q3e22._>,4 GveME+KVtgbWrr~uʭ}w4_:?3J[W>Y}kl\3um:d] Olм1mfo6En)T)P8mEE[6moI2r랭_/T|*^Ǫ%m,)۷=s;˥W Y(xkٮVusvVTuڳ}ϧZvw{^粯NO)?ݮliOܟ?sniTj,iܔ4bn-kCrڦ:qݸQr~K/7;=ǘNJ;Ε]]8䷦'jNʝ,;E=Uxj3gg;uFs.:vu '._<~{+WnAΫWz |uo޸J@zCyo:}e& 1-b 6׊$Ky@"̚{@,;7@o>1ՉR,"rh}/Ty6SiTXtXML:com.adobe.xmp 865 412 q@IDATx} XT`Fn3mFTLLR LS`&R_A߹@ON=/s*c'Kʎp쨐dr83r:o ìk]w"@ D"0aa"@ D"!9"lD"@ @C D"@D"@ >##D"@ #6@ D"8D"@ 䈰 @ D"#9>"0@ D"9"lD"@ @C D"@D"@ >##D"@ #6@ D"8D"@ 䈰 @ D"#9>"0@ D"9"lD"@ @C D"@D"@ >##D"@ #6@ D"8D"@ 䈰 @ D"#O)q D c83*h(Ubsuo"*YE=]7Gz;9n5,- h{վƆ/lLצ[;dO5ڨV#v)|IkA ,D`ݻwgٲ$ENbhF2⩒kRo ;+"5CKFJQ_:qxH_K;:ViK@ kB'k%8ADg'ADz3ih/H.}kA \gS nxށ5 62oowO /D"ՐDx.F+~&ysvNZiq{y'SM=`Y#. zb9m ~|qk/9+/4"@P vE;4Jhץ\7{A. bI:q N?C<5"8Or$YhwNx7{k+KYD"@( y gZyK`hvVUa(d+` O֢~oK:?+|XUc4W@ 3+ 22DM9|hqPl|dhmAJ9%sxE]|2bȘ“3woVlRHq@3Ϝ+̛|IF0@ lƻrGXdhRT!Il>"dL\[9<ŘqT1. {2iXWEڼ԰Ⲣ;x6; #U-1וKb"@ 4−$at1= MLMM*j*J˪dQVfPs"SHsuez\hARpFq37>$ 25'#VZWQfq') *W0¨p_&e(FVRimUIذ jY^U9y2o7<摸!"_K #oB#7? ;~KUt k=k4?]TemS8L(c` ݊6bu] ێx@R*z3fPS~}=Nmid5i<5Mi8ű:&VY4_PL@ p )L&&#J9Sy0WVcUUJJD(KRD8ݲS1X899)2!899Ԡ .ȉITڂRb(sV.sٙ.˺僧/]|Y~lIl.4:tlrc %/44n#8D Hvlu5`龡_01r\1vEK.ˎp;~D\//B'k^xKCԍJ}Dm?`d._:K >p5Vpv .:Z͗#D"#`(TdL;ҏjjqhkDYQ%.#Rʑ|Jzj(%聿viC+̡L%zy̋y&jZb= 'Z$ )12<唀5zǼ͗1F DE Ppu:ZEςcͺGr;1WʪkI_Fus`壈ȋ2P ڥk^:j_ٰ ΅ ]b84<,0,JV9J$Hc3Y'NogK A 58"L8hq2 J?Sċ*)* S%*hn©k g XZfr%6:z1|7Ɉb0LC]1m lZ5CҚw[[vD"C #Xj`sA^#ODD tCQ@RSHE'gJzyѲbl3<)ڥc.UUC _)bዀ7QnfL8.|`r;Fs!DA  %yJ"ȭNr&dX)p2(HٞHB8lH\]iH5e#yQBWWaB!órҊ!c'ҟZy>['$wT cY~ߤHy~RY?~1IyVӗGH+eRh}-FGIJn4!4Q\9\|r &8<|@  ` pJ wKjk+d'Eϙ\GQ| 9_joTBCysĴSLYe3A?cr*ũbڂr4"YBtFE w7+QT),H&$)YPJRM)>RXZ3!2rQUVS.進v2Hf'_O64 Dk-nj:4Wu>w%W~(c܂ț$M*z,ooER!&$)* iuP"@ "`Y b XVTŴDU36t_*=6)ΐgblQKH.ȝF'kc1J"k*YIb&QI biV 1caV K+\"7!#Y9WloS}-4Šީk>JCXuӛu3L&|4v@͂qĀ2"ApK!:Ys;jW/T*s['@ T `gEɅjqbA\la3չ^"UQLrB#Op2K.dF= Έ ): wلQ[Nl<iHZI2 Oy/[i~#b8s #<~>1†Jg *~5 9CKoL$z44dba4;ss>!!Eh@u_''@  0]ӳ&T*ʔ{$ݣ)+KZ/(6J1" `iuU _g Z$0x~b)ǧ'G+umpeǀ` ZsZ=bhPrwŋװ[Oۑaچŵw1Oj[P6֎2&>sjONϩ j@ sKXI#@qĈ.*@ D`\  D"@s G/,d͆@ GrY vy_^VX)D"Eb=5#jĆbMnC*XGD"L%#N%P7D"@ ىӛ Z @ D`*q*х!D"N G Z @ D`*q*х!D"N G Z @ D`*q*х!D"N.t|w*Ṻ7ўw_7`K`j_cy{ϗB6&kӧ/4Y'^!Y\ 5 9<_5x]+Jp Aخ>&3qb錢Q9_aQi-i9KBWq,mePd'Sl OmսM}#vi|w!{t1#SO*Zئfjjc>ԇ (|L8r5[ڶa#S~:“F~5i-81R  p(AEBG=wH<و v\,irwo+."CW_)IY T׽OVr‘bgg<[txnS` %8ADg'ADz3ih/H.}kA \gS nZ޺[Z#QdiaDV0_)$+E̱X Ǹt?Nn~eSujX}_Ls9b6f(Lx-T{O>]=ge%57Η{y'Sͧj :pD%AݨkuFVA~Jjh!1/6phs׷~{iyO}VMjy8N@}Ø7#o1@0 @*s"y/|ܗħixAg :ϻbc6#vEҁ2VB]p rK2a|7eBǵVQ߷S1-2 Aq舻?ퟶt?p:;i3{Q罰eIbj+gej&q5bx822ɈtG M.IdZ4++g*sEt>x*y[#Px O ҷ 4ц {A;/hTR0=6(qTg=:Eqf0%i9[V/RFl k Qgّ6 D#Ǩisjq^?՟k8Q!dy304e0˨è|[zL 8YͭNuj~DQjq(9EЎ/CE&=0,^Qx]8nN ;kN'Xw'YV9DI-xm $Ԛ7*ds[[U%1I2Mt`߇0.dNB[oz@Vf{A gsgލ8_C2%`8Z08#6Ҭ*4K +.+j:K[pPUeݟW*eRpW[R\URkksceq2)=,M!))*JVdm[Ɛ(Ym 9vZk Md) Iyws|IRSmvF![y0AxIYCOv 4c?Fz.iPMWxxq`ʅ~L&i>mD/1!N]B5{?@)~GU%'J:B\)WoJFLjl()kO Ls(s]t;'8mXDq׻twSz G崇KD&Еd*Zj#onb5]o_d\hH,|z]4RiY4HgGWoHj~8HҿWtB<GѸdd*1s|gf {7i6ڇI0l[HKƼD{Dk99}uǴVjS/C>˖/ T`=b=~76e$2Vk>Jdɖq O BvhMEb)fqb֔P*:*p5' )t 8c9%"H5g$ٱR8GMD >55VD>{Gyvʬ$ rD)b( 2Kx)dT^JJ`3)[ݧaЊQ^]m,"J1 L~|-I{t9M |;յ m8`~?]'~aTr۪ߒ^ki 5;x?N RSR/?`q-5E$lK;bFFx3Q5KE1 w<7vcnsg[0xx`sLhndcy,EhN_nQL}Uڧ%=hױsZh;ԖqӒ+CѴF3I ASYMƩV[AZԆOjFV`l0<6^eiB}T6DONNL(✂+2yd_TV|uf]tj1E##D"?2HY.6+Kt"9^(]eeEZ=ۉJ"H,kKىRGNi$e'&%^PEMR󗞔]ªwY|E]|񐹄[ Uq{dcW_ihFpF+r:v|9$$ z~-tʫ Ja9.qpe%CeG8H?z`>Ybn&P ~ax+v^qz`0Wcu1hUFUܴ7\MUmW.3{=Aبz[ޚ{t `WV|`V/DNd'v3z 'u^f%*/ ' Uڧ^c ug{BLs$o->DN+]=L=(Z91NόEFir3Ss I ѠDHHʅbrCN9T~Đk0gY v4UY(ۄDW*RIO%vҎّr:BAOBãq%I OzISuYYYIID³#DOv\ʇr3K__D\V'@◅NNTb%NAzhqDzp]ဖwh|x!{m2,!>B0k#XVo#}ĘCرS{n K$[qjim{8eM9q{FzNMzBfHDt9o 8=.rsV&ֻUo!:Xtˮjlԫхb:fL.b>J:LQ#3+JI(ŇP%I8ա>GV9&2*]!HPDp"߭̉bNJĘwPA=ˉ=u*v&Gk9HVZb(ԏ#֪Yl0ܜJ ^kKD̗ddB81)3}I٬Ɨ0*3ƃ6ޟ.]$d|5G{L*/;muA o }.rhwdnRa@n<`q AQ&Ĝɿ헦66×hz!{[If'Enhsr-NXx=^D٬װoCzo\>{OǵZ*\ځS_Q)~BwX#*u"3ɮQ`ˊ _#Hyc>1<`P/F-B` =xiIO<>ĺun|"'Шg?ꕇ|] E*wA{{qiZj̗/ ! )""/Z ã=Y`sZG!BJYWw02[h'/`"5/+}_Q-EUR>ϼܰ|I5OP;)urT<ʫ71\aY)T&?E.@A1~(8}{[I$J*#/wQZՈ~MFaF53QY?PqJgm"HGAme.5x-ѷB/!3i,}"x9kF`Jx=En{kЇfd6` Ϻoo jŁڼ? KH7'O D*wxӀ~AlM=XSoh$Dg梔 WoN\!'uӧ'`''&3&3_&))(Uy;¤|Iڞ$fruգ z1|7= F%:c p7xjKBҚw0.omJRvX^k]#ǻm퐞ݥ?EFGl-+05ț_'舤Eljs~\Utg֏;m=h+Ic.Ud5R )ŜFyvNDLRLOddzP~jg:hwFea9q]ⵅa:@ Xd yC((g OT?2=YG_"kz.fl?w5#WQ;RH$Lhxc=Urp,w<@^O~[iT%d)-oni{׾j:3&tBDrEñ LqǑFHQQxv̮4>: !`H`~)r3"K]To;,^o‰9{,v66FqD=D% ؏2/_*L:_ ClCWC?l}泷NHaE4ǪMj'aF#)OoB˻n+bJٌMtni AO ȹ# v4i!O|kDuum} ?ot[c:'b}fd`mt)ƚ-g9 iQ_y rjD4usυ4]RKG/iX 1%[҇LAuu}/\Uӽ5\q&'VQ-tȌAj1H0p;򊛕(Vd$rpvES$ 1j8:+%IiΎ(~>q"tx\#BkJL RE"ȢZ3뛲e 4/%`!:I١<3R{+-5}_%i5Ők;(nW.$ƿ;&N\BC#ё~UWy>]rb01O]7Iڛε7T-H\j7DdʽxB+l^h*gX`F^˞#T׶^*n:v%z?N>w˛c8^/8)mv"pirџ.4g!~cBiqZm*{ħ}3>=5uOdǍľ%ZҒC#'O?~˕]ґtTmy*9(hʥDTFf儨mʆ6 %(d &EuKY/ju9?3uh[,za*SieUjEjuB/^^[Oۑaچŵw3(#blͲzry,"-@j1R<|ƞ$zWZREt4s3}3v͵dHOuwGFqgr`f}?֗Iu/D3OLQg@e7a?\5agP6q4g"3K6~7V Di^:+>:h+U@f*v+^]foܝY` sQޮi:[ }DV@CEhW{d{rṳXYC5H4W}+Qo/y;-] 9:xz ;ϒ el oڿlHщD:ctD֊nuO&9Q)b ׂCs-_xƣ {u(=`ܠ3+40LsXN5ρ?MoPQG0^ϛkY5ioB-=pstm״4ky8eA[d9q{A8M,#"})˒B̀{ZeU_3 w=ዂϥBfB-~- ">; "[ID{Ap;^ Ҹ\:gv}HK.6(x>sD4~Ka p&/EB !`9!ɺ5;N&A {j"T#=w㵧t "Jh,Α٠3Ǐӻ_~Egh藬 Dj'HM~?9$; d{T{;- NoYJq<9LtM%x^/os:]풠nymvU_❕y*opE 聛d^\u0ViFN_Uj4wBNOS52nbOy2Wvy4Y Tqϻ,oF>1뗬Ѫ!s\!sJu{%j3(kh︾o_b[VѸ}~~|U.`Cʿ}9asn]DE=|??8nic g#oʏ}N!1IcK³-gZyK7V"Lv 6Vsf{R9kt ѼoZq:+<%Qz%NS{ MsQL=A~%>g(qRff`ՓF#݅\/뼋`+(6B H{ vQ0I2qdG M.I׋YJ $T\#GI 1(Df9E i^g=ۭ _l|gtWm8ƆFv(tRJ{ ,ڐF51suE8nѿ~GQGXc= #F$-g JEʈ 7{_!!ϲ#ZlFQeEݙh1ġFuh])EH>@]|2C,wϴ}3H 2 !k[ ٔFzFwg_uqUU6Y~w>z??"zfA^(]a3Fcm5?(ḏtkϫ'V FxT^:^(YmTu]a;ņh+Wok+l''oHt@+ہ*I#x֞Dži\a\bz[Ck=}#1LɾnqhYwq3V7:.Uu?ێ-D[B5jCu;LeLI'›LҷlTwпޙg̮'yEld 93hsqbSTYJS3J8$ J~73ɻT4XR3T&k9<k 2=(*0E;SŸNܓkhmvF![y0|?~aeV^l![Fj-ior1^mǵ3!پ+\^dŹAľg/KLӣ}P BsDe+0U(9r WoJFLjl(@ qo(Dф?dhoŠuc?Yu[/o~},kK\"G~N祾 82j#UG֪>!Maerw!QwtF7`@IDATkQ;BNBF z7TG~ C}awz EtK}юsơ>)`$gfm@;,s^Op-#o6ƶ !o?սzWn0`12wEig}Ji6:],6(R{ (GOm6dFZ #!n +7 5b/7;;.*K1ѽ zrKɊo_X!3涄rQ_? jd(QmQA|4RGTinTMFςcD=|WC#  Z`4'E3W'6x4>% #~"yIPA;Z[ہ6+ W`"AY/ XpgoϵR|TvX[-mlŚS5脌u~ zp"my o3J'|cG+i._A"lއ03|WMRDvB꒵ib:s*:@vk^ddNB#_24(hE $(*keύ#q~fMG&<E++w ؂h+H,kJwiMcѼRRWJQAI?NLJ Ju22a7ldn_q!cK–R5PW}goGG6и)'8"YHݵd[9$$K uZWQ7Q(Zw ꁹ^^x*šts V7*;7)e[?XDqz`0W.l8^`{뵔Xy~J-B#5>i|H7^P#lns! \$214`'t"/eӄz_!azt;I8i!IoRCʪ+ɗ O B9m5E£5 27~"ъOaaoo_3ZAզhA$AϊmkG5cD,n_WA9ܬ4QL+~To_#^,׈axt]MDn5 J/[KVi6-M(qD%d<~bMAwS1z:Av<1S*1TE͉[9 *J4:pEێ WQCChfaQ!/ ?©XyOgQO9q1Pw8"=IR8 g.|٣#l8[ޑM<[fưlՙ_.6Aq#qKgί<)v,s!P1D+0E#Ê?M@>d`~ :_Af[ /Z 1ȞH ! ipQ𸠏 #Ok10}8E_曝-pc\O}@;M[ A8VUS;#ABes'Y@qCMJ!nZ8HQƦiK_ Ih鯂q^ N4M5!<'}ct}T>qW]I=Sӧ5q8:.WȰ\~T~ F\A ŪPN BEڔ|A/" ㈱*hqV,7[*2 EkvɏoHKQNh 1>mSKU116ޟ/͑#~ԛ"c-HDw4O̲t›@߭ $&۰^ Qxfh4(6i!zN,؏?&מtp]Qla-`Gb %~ "BKedO t|h%tln+jlN_cD,Gި9S/\w'62|Xs'j, ~ϗ?WtM [6DzAŎ=u$Az/Zaۻk1a95zhS~)yξ#?VhqQZՈcTц,aЏۍK_xf , R~ɚ8I&"HhkD2KK%afܡ6e?(՜dаU2Ҭ#Mf1J<0yӷZe>xOw~4Vp~2A9:,Wb/#Aoa>f{kHLǏWi*CYWI Co2^ɜdh}RL%Yɿe;ӪOL _W6,}r2Ha"X\Yz(8=+D{[I/K*# ^`Ű޿XNTw 1[Áol%4XPbGr{MZI/4 E~x4MI7rz;H+T%xTR/bj0ĉkmthB}ӴqVlA7^05Ao}vcjFs\xml1P}>LJlii)B ,4Q՜F,D5Y/H5=')8"3z8152Iq^NNAa҃:yDgxq *)* N1cvgdo?Yw:4g_tH.DHP!hGaL /~O.omJ ->Ux/{;vkyrVN=ԯLjڠG0L1D`iEްNx.XehZWɯ= BO rkZզByFL73cq~\0O|X <9CRz+%_~jg>FGp Q~e=zgQ9:; W[mrtO7c Nf"ƌ#`f$3Ta)?:oQq\+V VD ^,o}mf:ElIr;Qwfh}3m#/5x ̑v!Pˉ n M~Q()ќӏ Dr3N^ݍe4mKfY yMxQdx~Hv (MK5Є~Vy0jFJjҏ$ȊS @dyQBWWaB֡8JAN]˜~f 3~;!1>s`hڏUԀi2r |xR]uI+eR5Tu'Dqd-:+|=9@ֆZ ɫU2F:* ~|W[`u:2?2>dGwZ]W?{hǙOGr3| mO,ѫG$8n\k<ҴqD'׼/5XI*N=hRrf3٩b@))E8/KL^eJ,ʦi$VdNU{5G҃QI 4f9"vKZ u7F#+@}aExQ5 RROo0T|tB͂qD!H+W}/o6揨-OEևp /{su*C*|=c*_2c ]tON_M:?H*DgsȜ|AbX$YGnIS.ՑpNfɅHr\8#X0eH2 &켗`^\x>ofỵGN<~>1!`C}pj@s^D'"k4QmQb"HiA! X鷳gY&S<ۆk.ؖGO!aq2%H&9p4b9{=o(87zڎ k6,=x2lͲzry" VU'a9γ`5<ԊEXd@7Ej1R<| I0M Eq溻}F{U## 4,GhtD88[zLF2Uqw%uY 雱k.͠ʨ);sbc𲹮 꾟zKrd0h= &;%8՚^`mEf@]^o~?5F_'f@ D` `E nV`iV+X\{} ->[gې=j^eGbj9ԕ5C C@>"y3yڤ._pp2!BhLBOL#FcaD`#qZv uu_^ًys-i}Z8q\D@γ# WH@VbN\]']Em{6wg>{)oWq][k%kgZX(B0|%|@|O z?}s}]sc66vf이@ 9"lD"@ >=[D1D"@ 䈰 @ D""9-""@ DrD D"@lx @ D"9"lD"@ x#+RhcR3>=}s:譾[}VL15ԽSU髣sbhD"@ >:`9UK=S ͩցDVzPn*3N~XO{D"@ލ#Z|7R.p(U"'\z4M?dn*p&[GD"L~]%=P,URrK 7fC!o +W?9p+5nFXښ+{|?c`aX@<D"x#"dwq.,v#Zp>}Eы{A·s%N˅D"1Jccߣ挶!"!x6E@ y∨|g^5§"h(\vjKT:(镎gBkꏽRX\%Wbr@/+G?}}yNa5r*}Wt8_rBZ.pQ=B:?쟮Y )m-1v45Wv c -pw˄ܸ).pҹ$78rӦŋGvaD"@@ Qkt94i8+ SJLͨNYT][~t/o kȫv攓d;S 3VS9,?ԁp8MSG (YPczNb;>lGErA.Ŵ| M#/=X]TqGOU I2#qV)~v< / {#`S7_h GD̆FƵ]|)-8;}/ 5:6EUF^$C}+W)Rw Y;!h}yVֺbsd7v]7:?%T[ ^?^ճ鬕X&>R3g/pur:u#\@ GƤHT}q}\ϵ:L4L(s4&y\C5B\slM$ *i'IDE;z8̝eU S/)j(W|CtTstGt#u2 Wɀ=W_zzY)v >3w3#)@v.oΧ/ AX,62E.yCW[_\d"[Fh@A:_hNƅs"bT1$K/ݷcƠi2|Lz'羈tʊ!G'ISK.-ggqO1({d-W0#M_]_m%i"@ "K-o(E Dα-d2?RM$}Yf,+GJxTQ:޽{gJ֪|HJg3QqNYQ+cx] `+J (TU=Ј"q$V Gg P4g3ChY)b>L {sl;q[?z2Ō~[`/SۖNon,-\[ Gw՟GΓFjO6ښL͓6C*jϑD%O=t!ix3{>:S~ۓtTԲ;AB_5`^t".8VWM$߫o x|mlcD"8"*f؋M%uEDԨOe2(ML#P5Ay ŶյL1⼪l6#gҁL2,F4qZ Y ` ؆T .ў'-&m05}I.oxXFKID m7tbĽ7nB,KQRK=*r*u:q.;YG )GDfF=h)D"xw9"sʪeLh_D.#)"WWC-@Q+^ 1i8&FyN #I"@%<ǵGT!wtJJ$"JˈeCEH*UӜSUX6Z84՝"i5Y|R /l~пA0R߼~[$RQ̜t!E#0y;4AK/7Flez&x.zG{R_},:a "@ "`H K2KtNJ!`dn,SUBQݣ34:V`wGZ@H)k _2b}zjp?ѧm%oDvYI8=X%'X$&nj^ }iGMg!t& D"13)z֛X"AF\Ma=tu&숇eN3X|( *Ky]Pp ә9Ϙd3U[[΄ 6}ϖ[(gRvJZˆKCuiOFL ZWJՎ ~IOyi'ʠ,D"@Z jSsSb1Djll5T>NNfIPRA,p$/~fTdWSU.t,*h3Tw$;ZѨꪊ "={M.D"&TMWzxK"9$Լ}1UENQ=dd`FO:Y 3uG%ЙY\lCs)dliKSc¬c(JcTDx5Ew&RAJ'd*T>pr1Wqq@Fu7&65hCPO|NG't۟ԳXO>8+$8TY۽ٖD"pƃ2S j*U\S򂼗SJ()TxY {T =YZGQFY/+.fe3;* 28ئߜuLBQT+.XUMkɱG':y%̰!?>VDBJid9uΌ)T* itMRqbarK:q{dWb|_Ihh=szWxU΢u1IT_\׌'!]ϕ>~ڋ]C+gs[A`ϕuooh/[#f9@ 'P! Oʸb]9'kK-{ފBz b l>z*>_W['-)-֓ P jbS#Iè=z3XTAv&HDPE?T:$LeJHʺ%D9K޼CF&g HW66k,yh zSlMR ?~Kt"((z:|:49}ݭ4b\sϯ^{m4)/7s޵ų ;=Y!LC D}< L˳03+^]CFHU V)$g(:ܮ.IgIRZ<;A2g;Z\n!Qn)=@'X1EL&.̗P*Lr"8HqJ&JzƍK_/jzOfw&UĐA??U_2GnEڂץ<5Q\r~/?sjaoGO[SDž Sgm ~!pgq!Gdl޻s,rĤ/&>,7SvzE LA D}ܾ}}i/IʺZFHS~_WQM}," IbtU9u-Wjy~(!I_dKU߳UUH( lJ᭎IO8!ڳ^woĈEɡt}ޝɶmjrw`nbq9A!!gScmcfs 6o FŚPzVr_a}0f曇ͼB"@' G%q Z{jl渏b;V6"dGf+FKqĀ̝O ɻ@ {{ѻѠ| WW۞DD\ q/y髥+V[42$1>vĨ7Ë5{y "@^ҹ";DT}fw$yx[,@ gcL MřN;rٴF`Dv6[6޹+i#pxJx"@;p<❸ ٱZj@;a]_(H2 qoLq4_|}>v<)"q€X~uMɡ gNN"ścWA]3QO5faWt?]ORF|YQi mdQ~}v ԟm>w2Ԅ"XwSR37"zo3DC327KQcMC[I߉[@`o W  W/xZv3iU@OQpŒuV{( ^@`-ģ;iW{zl!~xwSnK0LL"bѣw![3<]_Ѵ=ߋ14ؑq/ƭ]iX jmC2=3oKHh]<+ws VTkGToRF!֗5ւ3V1~%yNQ#cepZ^(U截csʭ$4`f;騕=^* lSTeh$ٜNo_ev^cU[6>Gח&m.lQAAF^zҴ9Z$;H\4"H) Gb츀 Ƿ0fڎk]KLuϚ7z qzr2jFho8x"4tM|CF w0[RJ*er9ݡB kU^~LjDD\DvqI9bF$kphA2j+yd2Pkn6^n5dчMS, Y46Mֵ~v"<{;"KO&>'N rW[㍷iۘk]0h5\i7HUΤ)f}BF(}C",2e =`X9ypӘe--8ADO^q=>lfПI|gn-lv?~7v %٨r$1G,kK=Ⱥ,<)^i!&O\ّ!/ /Ou2~S%"nCJg9JQ)y7ikca84e'[}0-!סJpK.i ?RM䮲}YVvR%89\.N==zdb9i#p`9%΂!'V'a#ͥ q[5`ܾQ$[gHI=k-vjubLөm<. nA'ah8d402ZxvlMX;uO¬ ȺD0o[2+=cJ:=rGςChEh lBGD,mc^7dH ݟML/5j{@#LxK\za$O$b~n%!L;=ju.PWl a'L-L;vOU 56.Wr"$s ($88H$\TUoڢB ɱ?~FI;N^s<@"& c!uP;~$ܣR Ҷ 3G`uEJXR>ED">WP[[[SӤkdYnFs-I\`4 tdidj`t /M_<Dp. Bnc] l8pa""]pj9|= 7l-.D>nR;v_M3D-$+<{g.@t"ɚ&7@fq/)"nf6H_O7?]R( {b{N#жuOsWZ@IDAT^g "@''9Q[`hs?-ܐMD`~[r@ɓV7J>e[.&u1̞~}<_,yfj}q*Af39o,9tPd*!?Y)90a(NlBso&%)EpNYت23_-侚N]/LʪCɁLעbGNDr=Ԛaé*h]q[䁟'2 Y ?:]} & םѿ-2XAx]H9td[1IaxTxL8@; 31t5k3L{r.wkW[mSlNtm޴ƇDЄMUM<(@CV9ָpkaFR1H`[ rmjc_\=r﷐ >DŽ|Ϧ¦iߖq6D qWD}FszKϹ XWЃR(z"GKtGRTrp3FUE2)JˉX\ʁ:^38wX1e"r+r+ ɉ0I=PqZZ/]ds&3L\CHB<'T{E}}dd/e٫YJр cWJbKd뭇)SFT\1% {^I0fNKpݙGtjbƨx+Caf[1 2A=K=$HOA[=ĭVJ`?կXt9Z|@6bӞ|ԃ?\LPxznp"GzkY-0d!p$DwO[gq,eptR@WmҀ0↱vJ=ȠDI;B2:4m3_ ۷.to|r79Q]Ȅ/`$}7]h?*@vz6,VhCdpWV()wW00oJ) //~ [_$^`]o]rVuW!eەt#B0cFJ ΀A+v߾miwZ[(1Tb߸F4xF쁾ݑOtGm;f0mKz]E2{JEx01AL~𙿆0W6Zɔ]E"0Dޭ:ގx[p#n:xx†w +f0ōJ'c,jh@_ zms8OomT_^lϹm!x<3fLug`9ܲ)!n:7QRKJxΨnKw96uY4/ɧ@EgЙ\K0AF:A,%ۡ؃XH {9A?3!MJJW؟V q/k{Jֱ.X9+m܌g4 W!:`:2<Bo : "ruNߑ,{@r2z=;bJL!row$y@fk!S|cBБkjuKC%ߖfvqfc?ڬnnٲ´)ߧX t%zU= ʺ'bP vjeu BV'q$iR(fؐ}b։2`jVQIuͼWPVyl27!{9M%IWZ]Y*$WUN#+) Pk* kn=[ľטYͅ~ك'_Xe+bwlO$fk;>|uh4o^,+Uv+vEOֳzr?^t"ja;jCI㩳vUpѬk[.#ܗiwbᜁ(ZKugȵBk;u']m"9Gv0cFf GqM/5[Z:dp -M2E.J.ﳶO{ֆ:}hD'&PۍS-"vzlO7MQmov[*5ZiWfY6-FV.,CCR-➱N0O I:i{Q$ŠͩC^%j ff#?h?g*0 >X 1 m.+{iZR*1tsBzl!I%=vy X{#i&{1\$9{dw`뗮Qc~u Cuq-^A`v[wś [%HwSN%]8?-Ym,ʿ _ijn(QG3zqǺ|cﱾҴxа}[P[] I= *5"PKą{Th:a/7QQnٹݑh] 42i>/y7_4+ t}I-=}[mQ&CQ]=UQ ;R ΀h;0"xQ![T ]~=y6J ]%ͨ0401a4q4~L`ۈ+e9t`0ABwMj(q75A'`.>\* "%e3'"^jf3q:#88bhoQєRsY "&!bZG &ٟ{( lfS 8 }^X3Yz$t_Ҷͱ6x(Pʈmv,_w\ S5h% :lTELwpj% i¢~Aכnicm3FDtF{oVz1u1b&Lepx@ȵOC={9q<]AhXO|!8 3_)1DBM2xҟ0iƺ&_JmdBn  -F \9en\iuXxf9mh!޻3𗤿n8f^ Ad 7W{fOϯ,m0gDDVKȎ*5،l5P@d6D;˛o/uh%D"ٺ3Ѿ1~ [H>.ݓ' e G0@ D"^~G@ DAr{ZBO D"x $@ DAr{ZBO D"x $@ DAr{ZBO D"x r_fausdoͩh]eIM;Xkf{n~=6҃e}x=62OmX}W}R/j }F(YUZ9Szn! D` 0GWwprGm̢ɍ?Bn7TٱɹaT͵+ۗ[CZ*$=&kMyw쨽ot}젓{<JtO =$ IjR_:& DA`-@\|Dg :{2~c "< 8% &zAax%wm\?[8awctlhW CE볶;'(07XF8G?8,ڶ%'NTD"Oyˮ(*)%q^I@ fPXOurWjܯ5 gӟ,#U[-Tl({쇖``%:xd@LC #> s\|Rd-&ò@ "]\{~z_[}EыAr"'=9!jiG'Kc^yʒ3\* YP`BZ*}8n6#`KVMtSUbe"L~6J2en셿)vnY՞\6tV:. HBц#7t`}v1e#_% d{MLqDm b 8J`N+04tM|CF,<Ӎ%u{=u^ Hl(',.c# ~K.'IB߼:5->D"0GD8d4g3GJkUxB/)8)9IDP_+Wbr\rJ27&5EB8@g>[Y_"QN^^fմ7(2L̲Q-1U1QOYb"TӰA&MdG-C z3 #%/& =}Wuou3&j}Y2Ԣ1 ?buԴ(io"xE+CyJ㍷iۘk]0h5(@q~2X E(jvx{YK Ngcبđ枯'>krDY64J>I Y!D` %h K5E9!W[[A,nHH(wn9DʊsM%g* ؔdj7G>!yޠDRMnj^Sn)9vqF"k=Uedb{:FŽ ydFh[29- r @w[Lg:zC$;@@+_1j1_A:s2e{%cO?8eWo(" kw^bXJ{d v ¬ %f- oC#A~m(2:0(;g{'q?D"0>Gg|K#HSnj:* g %N1ytt%t&)0jSz!25SLJyx0gx "%~tE:ok M0.л:KdsH?&Q֛YMRDsQ! qHC<³n_>𙋤I'X#ƭ>Bێ%` Xk?Rn ib(yݜg D`#m8u$kν×`O"A<' mS6$1)6W.r rs%"cIݮ6lt-L~^`C^v]mI^ULT$2NN'IT%l_ÐɶgTC+=o' "~:ϟφu2S+W;UcSD"0}LvdCYQ^EL qn(5iե bt$QK UPaLUIr:Uc %%RM B8%ت6Wm9-"Bԗ CCY<9S*(;٤m)nIm{fus3ը,0E*'a-n zU= "R?nKWOMNvkc +z7 ީn Q @~G5:A%Om)'2;Kaw ))z?rSqFUi`"@XZDx5Ԭc ttfw"[D`牴ڎFy@wW;ҔR`?~bШa|ʙ\D@ lO$k;>|uh4o^,+Uڇ9ja;DPxꬵh">_}[yNM6=3Hy61RDkHD"8}ΆCˊ sqʨ:$|mQ.mIssLf/Uڲ$U(ItC:PZj)E *lzw'O τeJHʺ%b`9Ө7 юN{9hd DudvaHn>|/AÚZiP[]bd? 5JyYU5L9UXO9gg=K ~rpM>4>Y7P M{]10kO[F0D?c5ĀOmiIGB%va TG&_VW=v+I(D"0/p33o\j:3p\@Ll DeJvGtM4\-qC,\RX&0 dj'%9WkK'#Ӳ#?#Ŋm27.}ItF]~#bXDm}c;zs z>iŲp׏$,CX kve= !S뒷MAD8xշ7ظe`˘cF!L5kzEaH,JvU3$T(r]W_dzS&1  'f;`odF}/Ar#SeE@ ۷oq+=‹gZ3;b^jjtIJ UUH(;չ|Tr{l(N,pd h2}W>,a&& f,;+00LlnhP>(c2\5E8/mHqlza?)3?#bE!w[dKkk31|<2ӁeY~./ץo)SCCC@ }{_;Gqz-93Vt3 G!Q+ayKh_JGkWWMW AMgnn+԰{3E[rL1[+3yq:h?m./伻Az“B`'HplAhkVka*zp ӿl,*ӉSis% UM-&uuTN{s?s3h?Bsz?P[Γj>kF?xQBCxCpcKt_ _ p>@ٖ ҏoҌ7,nlGAΨtWZ:8o j NP`n^* ]A`Ѷe,'lKN2["*޿u];"on,x`1z GtI@18K[a]Lm%; 7&q}C [xѣY'HFڎVțsH,ٵnbCWTlh- O~eQ7"!/'m}{h$xIےF;0H=<CC[$x~%AS5P\Mo^E_[ 5s AF]}}>:'UHgˆ =ڒ˧B>zVΨy*+w:OaD1W (P1nqQ}n7OD;+(Za|>/M~5߯Dg(Pؔ-=Ͻմ(~,IX^?~1^74}:e\k. }=͆ X;,Yʢ0LJreDi*Lo?D Ƅ=ѿ/"|ot%p죋4SfeY<8AslČrͽr]gXѶs8D$$~n=5ݲs<HR_t@4S8=#uG P[@8nPG] T)%'-$mg"w߅!!<)1.y*&A_9;B<-\Z,w:Gz v(ܙ+ENvyUS_YXX\)Wa #Ȑ5JY{J$8DZWb4եJ*dzLEF |E$%U%F:5;Q>!eaVA :^QUUQTI\LJ Hr2|ϖ|)YRpDd/q_~h;DyL%ɨBIB2p `BĐɏsvv}uD8bGǽvJZ$.fs*M=3oKHhvdDwӧ(: ad=21TqA4v'%SJtq5ݾ۽c E\u˰XRaQoO?Y(;w*K 0uGpі&kΐnq0g'2io颅iڴg;D,  LVyj ٔ;+;DP-' ^Vɑo)n _…S(hI"v=u[?KpI?0z%tSj>ր/(huM'%&H f t؇$?v1 l,Fofd$E[ 5~} /[y"[ &>eeO_Ut 7`әGXÄ'zTi?[lmݧL҈X4&(ke2C >}_2:=Y=.Խ;w^F& kIv_~(U戲ԬR9q)Ksk+ڏeK6YF"&) զ);xIC}*/+wn)a*AWׂRYW9F!֗5牭=xEAF^zh6e{H"Iyi 8E""Kk*9hEi셿!huˢ׮3K:u n x1l՚ǰT` UBlwZL:K馨 x$cjN>d>f裂x๯ﳆEhEW\%?)0Y ?W! Y]r}9FTfK2]7 ۰xdI'pU=[boF/Eկxtt@~ h]6[OW_2/ZWl~.#QJTr* fg4yQm4:|c*>䦝FU{0jMbՏL"Lot`n?Niv)IWmɃ̂G#*ۿ>=t@z[Sk(uzװvԓ#٫(LD 377CD1!c{h(HɭJ_I!jRIsTk,j*Q"TzStuͩEDSd⪦D%trVQ/NI#dq3lņls5D{@^L&nb vؼA{4HI% @1 ւ S;; 83;Yv*lkSSڪ`, AĄ$\s sɕ{>|{y}|;VҟNr9 SWѻ yn[\vcFXT^aD~_!q0)ѝ+P*UaFj)hfTë9p~>ϣ'.qK2{N[cVf7Uۧۂ/RF0rj XDyUQ#\Ls?"{MDtBL`!ta]L$!ΦD;8wZL7vK"ja*'ח$av"lMضua\03' )$AdmӶ[R>t:px/--aWirEg0 C04fd%u mׯUpuuȖ^>_JؘWE8A!s ʯUfRtpDt ki%Xc;s*S)?iA$ HR~yuq&$#'8]-:f$"N %e҃YtsG&;}78ao7`aoJM0V60IRTwןlԁ$)SFĚ9]CgUJ^աnP^G]q bL\|1,* [w_{&8;X -`T{]`8.m =[9/;~ދuyB#lRo]փ YXҟGM_pl).ڿ7jFNABvtKv+vAG\|CY]k[Q_]]]Y٢iȻӃm2v` vrz`#=ugB׼/^BDG>#HWi')oW4WT:+.5R@`{ɨ")"o/׻ ֛,&(#!>N0jk,WUXm$Ք|& eTZ"=g{& A7?JNR|$[D[nL.q([:f]O^H Me6XDBbCH7= Os/-}u1!{>&z ϴġLAFxu<3 rΟϧƸC騂+K©U]I4OC\|=] @#iujN"wjQ£u+i㙶t#ܤkI4I oj3Rqg`AĬ,ucDk<ƮG7m   JdN|`dAzw)5xk8j?M y'{xU`^-"ĠwtOv"J'"QGu%Nw5L*xLz(t0}!0ݽN\#Zb/:t59_ _% AٗA܃Hx))OCЃTG{/i{Eh&ı bwzd@Qw+ %y$)Y<@`:ŕzM6n_+tq';Q% ]0nxX=5ݽ4 k W"hsJ r澭A4H恕Y,JW0iCeYeːNlj rR;-=zNM "Y!}0 "^m]]-'><ТUꁎWA݂U?*0Gh"hjz.?-|)t֬B3QKl &L_ ÍCb0x31? D6p%I}lT`hOߝBXQĕ(kH+-ã}zs4(^IտsCӅ ?=$Tf);֛lDۘ0K| c#)C''ިaCbRVD4 75).|bcC}gWu;2 ݴ/Vi7 ($}^[W#ocS'=ugDa:Vv[7K\,:R] KH>J JJF1{h:M Rzt 5%1(,R!ۈ;x`O HSx{]/o쯧nŠگfdGNtYδaözY1Yfz~%f&a<=̯9%S;or|3cm(Hz5Wv D@Ĥn.q6bkSt/1E9 k4ؔ'lq8ss:mnAQ[R7+;ƊCAp?=C\D8 1f䕷'I OU)YO[=/DK\w4"G#Sϡi*]]'DE_uaEiF|jDeK-Z' pfgZ][!g S ⸄{{:a_?}c0hF228S6)& ͱVUk+ը2YBqw/$ug^=эwphzR~GOX9[.ouc_U{(|_J͵:}6N :݄tnUp3)Y/DzA`ҖQBN5j)NsjЏCAz]7$bŜƫP/=Ĥq75 }O&3)~'꛻/*1H2zAc>OWUL}qԲ]tF:[`SYw&PG=)d~Gj';|7S |ix{SGg+%@>\xk67:Qs_64C^oBG]'H4e?KSK3|X܇M^?YiaW5͝~nOHgE.q.;țy|NmFV%+0n⤣;DyY=Z~$_$b4pN'X@h.BL}bC7;l4%3umek$W뿻x fn)QRDWs13Gҭ)><[Cʱ |sh7*;*+Ikk;?"wo7S㡜[͘r y:qj΢$#26\grvLBEE`w8{@9uG$Hםq-"afcC5fDl- Zҟa[95z:(ILK5'|tK 4j.ԜՉcTכtXpSզFZؾ#nu]^OS1T㺪XlS;3f6 ~&O- }4RIï4cz \ fǼLm|gVKOZߌS۴iڰ7:RwM6`ҝa}S!`cCO٫"Za /`ϲ6 4iߙwP\ l\gJ2,-0٠K@03ta'i΃%=|杇vaOln呻Mhɹ§H̆FpQadA<=L]l]3]fjz"hYgӶ}nqo".W,M^Bmhk3 N2xdQә^o3NF@2C^Lgq/-c6D?v㻨p}n?_ v0n`k:J\(X!^j *jj@: *ЊU蛉S:`D7P5rIJ4KEdLZJ)tC7uU *:maO p{Ngݞ|# Niϝ+ 1{S`ut6;PlV02t}c:[vwdw^~I0 G~ ÆNKoP;FjԹ&3y-B9k4CKK`!\?#|unۖ˨zsdx_,c̏;W+?{Ԍ(wn 4h|O~w`Tx𽃄^̔O  \7ecpeYMuX<׮F_{]Db7xO`^3\ƲdC#:׌- A=mf#~s{_$AMKοelTLב9QQ$SL P΃uKKYcm@!sJY!n/j S;K3 {aV!UV(`Q=1ՠnl`֌+.SrKP_ a+S,TFz:z a\pGg=Πޮ۬ a% {ȎwSe3мD[CxS&8[fuŲjUIdK\l+&Tfϱ }QHxe}a7 *%87Pu>CoLat/p|#zU&:".eoR[AstZDLX1p7B=s"왝]5s,f_ZDk?;эp}k^ vm٫z 3MDDD l5u}cx,J{en` 7? 4ueJ*@bcv!>{b<2Pv%FCGAOF1Ut4RՅ aQoG*-usD`5;#¸6z}GN[LoL⻚'NxBoP;2,>uF'dP"3C„eKk{wN vc)~s޷C#.4 .0q ۾D( ?(gGu^ g]9ND FƮe!c(zw^^R=0";g8nɯ4[m]簨+Ad֭[P XDMَ9[ހ<"[׊_zf"OOq doH\^W !F PGOb܆|Xrg™)(,&SbF98/IgY!05xk3 es0~xC֨GI|Xwἦ YϮ+ u^NT0";Vu;prgpfw}x:o-Q^m ZBNڈ`h'3l5~. zbzGDDPu=x'<}B` =_KX9 (dzGl#$FR߽{OA yI\s)݆:g3hcB(^4Cpy'R{"/Ox23say" %K%X"hj[:qojhAH{*AxŸuZvEVm0";Mޑkl!zg妦6xl,ž$u ddg|FTOqSz[!LR0hk _;zZMߏ\H"M,ckƸ7":~H`~D/3a̽tk0";M# zݑD6 p[}x0X5aoа1Kljfv߈L@=<؛{#F4;[.+:'; jÉ:3 {A[>D`v4etlBxYoEBgg7 08@:SEEXAA.U+"hq꼍¬2 T -V˔*H̖Q$EIN|L + 0u] kS妛@6\dlS&, O>_wKZ-usD`6nTWxADNju_w?>خ͝w_{/>{L޹dbv \#g+oRZ4^A>/Ns'%o &ӱDT(!f8;y= /.ڂ<=q%2t0٘e\UJxL fDK hէf2$T)90t dtS<ўj;j #tP(!!`$W&85N0&2k(vpDD'D 4d*ꣳ @pM0>wA*c9Cœ/جj|EG Q[/ϵ<9wQp G?Lq8 lS? f:*Owt}hkHVedˈ`ժ(!,4T=F01.tTWɉ$i~ea* 4D&MC^Qm/M8[Q_]]]Y٢iɤ颋 Y^؉-co"b'VSk^=aMki;/e3t D"0wy6I'M^n] XyކmH>oSN0>vkbj\¨$V Nq"!u{,kuOzwAdS,_K%n)P`.x,1qmO[eI\8WUA+ x9??''B_[{<O2Kum9 BHhQ-"%an\\PHfmƜb2O$!:Dswn"jdrtJ'@&]衇||cw§ !}n }:{M`"fo;6^ B%gا [qBooЀߐBޫĬBcCZQ#&w2'j\$״=ƑERGnW݄F' |}5 0NLjECo3ﰐS=K %g VoV$vT0X=F/eP/όĚOP(-sơ8naP50P[LUqM,n!4{6j8l֓W|¢¢7?LZ |bb] upjδfE( 3{RjXD-iO%o 49;(D;R7^s>%_.c 4xo`z mEq:]Gq6ˬ*'g+;?8h(*3AG4:"Ty>AeMkpL^ѩ%n,5[:{=c&%ۦZZdYp=3f''\Npߎ鋏}r`x:<ʪ*+?UH qpra",R7X %r- IumIrOQpnK.HLb^`XD`Y"0g3b;hm/Mr3Q{n QbC_~RF{m ^\UZ{ )=,4."I:MN[D[9 Tp\RC}Wo"Y١t#,Q.AqV qVjjEMAF9YLE:Mt7vT(VZQ[U!^%(RS.>y$vۓ⧗TdARѨJ jdG5^Biq D▾. D`!0gmNbGyASϰr5;>;~UMczzhHD/4Js$o iּY }8$e#"1Ѩ!20[<&M-ngߐ_[zn53BiUfU ϖ>fW3RVR +9.ڈ2Z#+ȒkFU&rrl#CXfR:o 4 M56.n ` ,"Lpɳ3NW.}%\mA$<.ֻRxaX9 ̼ݽ˦2ŵK 8N=R f!: AyDy7f_ z]%#fЗjE`cF(m^B6^'MjRC "Mv.pEIWvx)$XIleɉUqv("K]Vhl-Zg*fkS./0-ɺL: ܾVA薄 D3CoqKY]4DABUf8bݐ2?hMz͡Jg8׺) C' .:78KjeKkgF KJ:teN6+ nim: >qv:F+hyw<%TށQ=zMC]-U]y#Ԗ)^?7@dɖu=>pmƛef'ӷSLvܙUk(Mut A¢)6g%Tn/P(m[Y#`5Dz"Ih"fvX;*fvps;rxߗ XO"F.<#.q*ϖv"ZseՉr#2 -FU\Q1^-IMyоDn@n GHSj҇9LHD"@"f)gNentY lJЕ/Ce@ +HY>`i1W`{=} n7xQs%ݿx@ .G`=<!VR(HW3QȣBr"0~WNlWD7ncXb+^d(v:5҅fv9?bgJC D&;fE 2eGdrQRNq`ahbq?3aUL ^Z+ H*C G{?;؉}3Go=e/jV1tOؗ@1Q1\"@ 3z9ҪE\Z"t7q5A"gm滀 cn#/`Q ?{zI<D",y@+ n]Fo YQKtز`x/8VH{%OAD"p-#k _;zZMwr"4-DQ&1fV Bj "Lى\fmmiv &,;]Vlˑ՟8)JN?Eʛk\ɼ\ǕWC W`e<>0ӯa+ˊٞm /='O&_]x?C&|6 *^;q`'JXǻn#%{^#w:t=@Gtd/ {ǤsLO{"n}vG'ӯO[$"STH&!'|r /ƍrdmT=۹ /D&<>TԸ M7ʻ#8X-\6fFfh_Vžkԭu7FwxZ|ډ; mIpwӓv$o)W,@ 5+J79.Y dafǠbJL,a?GdGNgA0l,LoȕE!5Q}We!ugDO=G=|#33񈜨CYSUuBy,+3Kid6!Zi/UܤY+D"p[@[ED$;X&f#E_@DϦ͉zl[Bº@@XLeb{ D%EMt1 b`HTBL4$؛lD"XYף%c$a1Y<_]_IYaz`ej Kk$Wc]*,^qB[D׊tVed׫yNYQ"ҹ颺*T:*g%5Ytk2dGv9-lXkIrǺ[z$HS/{oTUIj{9&G~|u7~SC*?O”b"8 X=G>IJBw`;+x@'{b3_}8 F+f*R$I.#4DoOiBy]Wת/DNSO$ UWivEPD",'w=3S'"3ntU@)+N+:rN`@%!H0"EVV:Fĩ4 4D&j$-6e˭쨯lѴ,Ģ঒@n{ ]+w߉Eexᙨ#AwǗ(o{7fhspclͤ "H1ЎF"LW?|~s )lĝЫΑ2Ay>pm:lcvn@ޙ/#dD#_X еWOdKT(~+rF=LD hU^@ +%q\6W(H8 Ml@(OD9S^JJ&^[XO&)a5Z+:Ig")"VTF\JNG=1eYn\*+jKkn=Ua:8t }-a4/aX&M3 (>&RwoҴC`w*psTv˧ÍPc9(8} "hCw.B8asgנrVQik?)ۄNqlAB r@%ɯǧ˔8CcyO(Izh{(b+g"YYEii/,;lV:L"5 .Gsjx>(l֋UQ LǶh&l#?STivfIQ%د=6KJ[M0 5 7Y y>q񱡾TeN]i̩"@ለ T& X"XeݱϿ6`}uM]~5{-Wvtht X~f@H=~|iƍ厠Ɖf<+IflR=MV|lMc 2،mg{#<,2~2+}n*yiv3LP$xCŒ6D"^ .%Z^QRTTZ!'dx)쩢zt>Vd IzҌ(-YR:sNH➌2`1]#|`änU- KClŠ)"X8[ug)ј݂|+j :nV4w޺ƊCg k1VtP}ͶC Gvet`z v1µQD &*[]ό^ؚ/L5"NUQmҐT_&i0Sj(GZU~9ٙM20Mg+gwmv}gR-Y_ja;鴴<=չ vK L@ +8^=B+?UHl/%BL0_,:b"e2{,C/m֖$GSMWN4]#.!]睿o9+'&pwc >Ǘ;f;*J{4--"!wnr38"29|5mJZbFxOgs&Pl۫'Rw \zMVdBk=E5r!DAz`u]YAA/`AIU\բ(VdA0EQ?y$8JKVXۏPˎUEiȢ_P]]L~dM C'\豝%J`V*gdQ#_{NhU%5sֹqNH}%?l0jFtV߼2|O5m{hyKt|]q+ a'C3[_h}7{hB(H+xKj;>ix{SMO!w[t 13'Z&)^ݫ%WQ;+ۧ`G͕oW~خ zqj vݱc[]]CtI~DQ=R6})!-p gt)ΔW\K3K( A >@VbCQ~YE?؁hN6Ub'eC`*TevTk5NeTJG:ӮFa}YAI2;TBQ*5HJioRlN0PkuH5@YO}Jye֯Q0=.G {'~(*9w4)INPN-|=;!VũJ|d4K<.\ݳƫM6ҷ6[up dzܠbwj.ԜcTכ{s)]s;*iжq`"@V"N.,۩ޮ۬ ??ЇQְ:F4!H D"X#|T]sS*+}Fmoֈ.?\\c} kmZ'FE=2:D"XHMCYx9=9qRz#fo2 C( f˰X\_? _׎\C  K@ D"0Ooޫ @ D"X  G\ a1D"@#. @ D`q@ D"XF@.*D"@ Br@ D`!U蜶\{pzAEkpܡp}^q[>^A~~KĮxD{֫5;P׌Nyo "(X>P.ҡ++mm Ft}۬@j 2RdB}GulsG{o GU ; H #I#;-x+/i}DmE0K×]:)ZHB W6OFI+>l1WmX{OG-eq}X;8rf`dA!`X*,ZAl>8k^Ȑ~lz&@"`p̡ƮI6o K҆5n@ <#7uJ;nK1|0#+۝2v)j m}MsBaRA`F/'WV:@QW*t^};BinB[m7|DpLs8.2X8HzcϞ{HsvpQ5BSA%>v d#|oG3/cحK8D7zk"XlmZ0<+$NŒ A\+˟l=_(aZX8 -͖\dVF]Ok&A$EFFgIaFu~DumkGOWQ .W$e($2PyiVn"9PdNZwjkK *PD]V~HV.x\T %IGdU+jw@,a~,"(kT!8#CnfiyndAA:%(Y'cɀ.QRڕ4p o9<&n0}vbFz$~%}Bf[mؑ=/t;t;G_P|}\?^;i>jݸ|1v[NJ1oG[?nl\L!R#|)7^FhʑbS6Blz&HFMfL] *j\Luw7ޚ [`T0GK-fITB/OB4]Ag߆mjEɬ"OeLܵ.uO3=owGLO[vm΍"^T}EtG\phqJaE&$uj <y: Im !}輤x|b/.P[>1 Tt7qt9 Rf$AEQQ*ޘG)tPTJdeN_@c#:IIQ]E}Vv&FYWQsISL=3<]J28ZK*u猫"*9"f N/fjDUEzMGtÔ:ӗNT(,"8u%G+W̴nzP6?<>eI p7כMuU u^A3{3&vݷ7Ϗn?X#پG4ޫ}>LVG^ ;MQ[;X2uL*VǼ5egkё>}WA\Cz 7kwJ~ԝ<|ʟM/]g=a[6;h sCނ#Cj@ؾmZQ[$LX!`RVl[ˮޥO#9>lKF|t 5; M턍"fj9ÔOhXնn狉Ybz.lhpӏiG BLJgkv p}cb2xP65~Lgָ%f5ffP.8v򃨆n#<_gT~ 62勀K8 "\IvnnzUw몫xIqJxNfJ5rx$=+NQ^Hz,ЌFdRٹI"DY_ dyx7HfFIj*\XWFsD@NR+JNGex h**E{p(ýv8(40bdP)o؍M5?Q_ CiM۹O3$<S>鏩ѯx]WΘ^aD~grm]G4oEDN/7mߩh(y[&nxΛxp>?H>GDvjpwS&Mo9Rx e݈GV(J`"0=lk' lx4]"g4:L5;]]}@Q*M.K_=,)OIlPrΠtFE4g'Dwkgdu مr=n h84mYZ;x*6LQSU} {/=~D`h!HdT!<P $чż2@Jמ»d8G uAn=sT$;T{*Xh*'IX]P,X$uCPT" W!Q* ERu +:+~:ҙ)Ͷ4g /0=({mEv/}k͚BMdS<:ZRoAXOQ.5[; T/foN:^9aKP˓ߜG$ԋ;[=|A%Wf^;aoy,Ktl\4kDpϘ3g.2hl=;/ErvF7;oL4R>x`4ƨI~+7CC)yCQVAHঋ%F|WՌMK]c̢yhͪDHI7cϭ? 5jz? 2ոeVZ:~|>e. )<%Ɔ ӍqOh7DUUmZGw mL9ʻ+R`vxPTϭe+ExyV%zDĬ5W[w] ڌ J;ұRG c5bQSq#%gb?j>: ?z > {>* C:1 {ncZ]eq$& IgA,_G9X)5HXWq?t iVD{hsL~fO](INъ%gxtSy7X^BӐ/E-"P"F'R}~K?K?4M=/Ш=!"H-M7΢Ë'!g@ h((p RphUF)" \ʼn?o`82 f/Xɗ=% =wg}B$x/4_9zMrvGϙ~px^h]-ҧ%DP<(2~a/ !ոJcWwoOYjkXyl$lmɒٹg[!MrDh#}>MЍ{DF3H!)nLɩ&ꁱ܊ iS`qtiE%HԠ_7sV~MHlT-dZlWYi[GgӘyH4){YjF1fT1:|)p<fTjX QXaLǎ_~& |;,"WL5_T3Ԩ.—G˖s^rhDP fCH;Rβi "BeQ@xuTioJXi=}K@dHF ҩ2tBOܣ"<(!w_S ֿNC}V96KpS݇şܸ4ÇÞ[Hq|~ ?QcILrAo&e/Yz.T5Bh^U'4);h:sl1mDgha/)>/2d&>FjH:h0M,_I'eTa#b}b ZEwʝ>_Q8gPyF}U% Be`rF%w:6=Ӽ3V L֫S]NҐAA@h@@?@,2cϷ;tt~4|Ѱg_#dDrrLecrG_GũEa 6jphQ:<z¬CmE奥+^iu}yqS˂h DMO*iAֺtMNapNd+o[,IN& "r`"'RQ0QJo#pvoѝꅔU/5-?w~v)ffz6MGc΢$HEcVyb+F A|:V\yUbD|-7Y0U!{2h vugbO%9;d)?;GA^Z ut6?jWG90V%K2 b}VaG!⪶49dV+/'gꓮu%r3E4n9hݹ "qB:UvALcl.UqQ鮏DaV ݹc)N(s28bbxIas8ioƸLkگ#IK[ ]g^$ Fa% @"z盺x*d JS7=VY}W)SerWԆtTc㕒7db[ĮxCN[/ܺLg> g":tU8;|Ğrbm aLrkQ +u4(N d6wǦTS7ݥ )гW39+ψ ->FOSIdh64aYAe_nmD_^-l@ $lCy+\*>-d? iL~b~ O&VMr`F [agtPtt|$bˤ*1[/͋?/kw$\{N?2jY3xXCA~87DL|}96 h9%ObIp\2qI;l>&1VB%5䷦x[ET/>N0%is,lԷ]M.d"@>\PPY8WQ/~8t(}p>\hCnFnE׿[E)De] cYAuaf兹E+ҁ\O(jeuy~qc"ϋ?ڤb41P&i]wy7 +&5w.QBKzhe,nJIɓKym׮ u#*xUu({/xi[w4׌4}$S(ܐ7W~eZoOh;\-r$t%>v~~P+] 5a^5cݷΕ ΢q@~e@~irepRUwk*+UfAɬ0L~wI[jjCO?AV7J\9wd N7&@ Dq^Vb2śIQ?=hVSI89ҹΔR.x ߻.-]ƶj;BLɳRs}3@C8%c%b3v]7Qg sDڪ=fVpW`N`A5I\TQ*)@ERf+O%Y% :!5Q 4Q?{QiRv`\+f3M]඼jSkɹ7 :wb;.FL%䛸/҇믴l+E NvcP/AߎP$+Jv.lW] 3~8;՘Fu,G}/l}5F\{B[?yBpTɫ|r/Up\wPD#w0 ,׿Ө9KHL R\k0Z4SM T۲uwքz<-Wl:iN?JwDwST(Mds2"R}"? kǙ[fwhӝ䀗Bߤ=ѫg {Ukv!pUᏞ[oPq!5HJty0 0! Si$m#v #d+o&ZQշOJIx̪dnz/Pt9we7v5|j'[wYqhP^j0 `75#^iE;V=i~hl2,݄n, 1whI/Z=<ێE681NL 6<Ţ08ěPםVANȶP`,@VkC#H=\zAEa" Nz?6.D&)-k6F8(z 4;=48279~Ǯ;;Z/~'&k]ldmk{NJFj50}!"Nq!< } ?6{cv1e~fܴ]Wgns)QK Yv!֐F">wv 11P}{O ak˃ (sg XmC@=`L,m /<Ĩl~=\Ո@  x_ 2N>;4YGGi#l^W`;&0/0Xt-p-8TrNg_q<MSmhL.+)Nx細x¨' }DOkxHfxjrYb]C@z`=e?;cSNߞE.jq ;p*@_g zx(J gcOa#zʕ= Fxn~{˓CSZͬ|Y 涪CDL"|7>w:C-!D"8 i!D"@3GW "@ Aw("@ |FA D"x0@B D"@q>_X7D"@ 7B`$UW~Y\GlNW'SiH(ɻ-xfo"+_hX$7#O2n¡E{:dQh! JV} 1atmk֓g!e,6PXo۷yD7P 쇉j/TJ 4%:m ^{z_-C }k ;=9M2rV-:ݶҷ|v mYA{'Ԫ?:fC}oM Zecě c4kJ2VC'\㶺RM)IvD"µj5-JDnEW*5uwE5gNiD$ 6 "N .>Fz`^<9*+  Q;-j?P#oENG d+;>Rb}N5_nmP#;w>l!&yٙMJa/>aP@7"AX݊6ճ9uYwgΎ\Fq ټ&4%-ZS'X $ꓺ mGoجfʁCeB^8I6HZmőЧ՗1ߋ%B_e$6*ݞ͓uNPۖԿ}_rĬ%D,8wspR5}DY*qpYUR X%AL-O0luY׈n; Q= ٩аŦgG>Rd<{kzDx0y zMŏ/acZ H֣;;v㫳08p1B^)Qu.ߚޒpcUY6[m,(9;!8vJun^/abx4!}311q cBd|/ϗF:W(9`!$Jrd*uUZbMMlk{F ekoh|(M773WKMdN:uƖNZE(Ƌq^‹KMk~ZɜG¨KeYwhlK!٢tn[?aڼ0D?XâtWไ˗#K ʂB 5O )*E 8##%t$mz@zcw]EAx$[y"~WU֟{kJ3+ Euex{N@ E(VqtgVu/8pAMNIHY⛸NlB/ԺN7]tV! 'U^AS[hT_@PWsƯ(%C<{vΈP>r󬶪l`7oc቉V 82}F[s%t՘N=Uъ#Z|B?@5BL8(qvP.be[aŞ5ywyb 1ך͏*n"Cf;7zE/H"ڒO_Fa\=2~+u(kN5oԡV}ke izKnaGx)fb!T;" tӀ>q oLED֔,<5z 's왯p5Rƕa@r~ YB]y0FBu6#*ODizAia#SQ;q ":"{**>_Lg?߃K{WgY*]}=^𙒂RNOow]]Il>0=/?E4ԝ>EԹНVD—_iZ/?6'eӫ)PǠf..d^|IMG(7ph?мDhT?BoXz?胄 C(Q5ەVeԶ슶1)$T%t>s󟓨&|{IiWD HhӨD95J-;q܋`t&qֈ!)^YUcm/q0.FR+A$$BnjH=xo_D)eW]>U{12ʭ!˵\`]"ҠCdtZE|tJ6(}Gmݶt. lYhA7GK>-f&-|,=~J#mG׫zzxQiL'bdKMun~~,4k3鹫 AѼRO.:P%9 #Uu[ ϫLLzey <7剴Ԕ 6l9Hdkg5{#$h sD,?+<Ԥɬa#vmgTxvYk3I)I՛(oJ6xrD=R&Xq$y6Ajp`hAaoΣmg/|V7IßHzͪz ._aqV\!G)4  Ź.eF<$+D,*4Nj<)I^EY<`Z*\6L#O?"8AzQnzpa5\F9!sNuǪJp6uۼg&eH Y01v,K(ZER;-pŇ0dz5'V0\9 0>P'5wެc&O?bZNXhlzwQT>rA T݌skP"He-[[B\r׈miZ1 ryRV8skxy > bD1e+୻-Mۅ9 <3wHA#Eqױn[Rqvϻk'vdE:0I܏htmW_MFSYyB!<.Pш!m2TΛ҂7s,2F+!L4TDs Vh6cuxvC )˹Ow~, ҲJM`qn[A+5OI$&D˶+Ckh~,E+e1{=-zp'HǪ2~[nbq,Sf98l}+wp\W\mr;OB%{?5 _Y}8ozMlS%-dNcZ'ȑFf!`B<"bkG&kK|X *G13s[oKwwLO?nbA+ |AP12U:fV=wQ6C{`WLL|6!1`~p);$]DC]إ,ŕxt*3fK9F.4);h:k>$>Ƚ׆?}.|O˄m7Õĵvs|Dbݟƀmb^cỹgB˟$0>f␁uحYZ$$[d!HCt O^ %vk?F34G(qZ` g\9!bBXE< I^=zw#۟Iյ**S]n _3; {Ґ*XGEf6I:Y]N;l$wgR-w17 6DGir8UΖ8I)Ugbd. {Lѧ> u zj 1Lݸ% 3%!g_`DV^&Dc uPz>jeKs#JӝgZM+5_0P*DVuF %.BwTZC@WʮFtgd!hui1Jb+F A~֪+_L]o&6? rT҉uwmUsO?%3Èg!`yuJW]mѠ#WV#9yNqZpy;0;+ԕV1wAaܬgO)|Ӥ4KU cE[@ڇ>%?d5> CCѵ鮏DYV BjVdߝa… (WtV)j /&ح$4i7ꥮD@ Gc%u<# IhjJRta^TAL/.m$SwǦTɘ5ݺ+3b#"rO8 YgMSY\y\p&fc_qLI {U,Τ6N4Q*VdJt$fn|(o%zm=>K"t7R6?qg/"- 1ǯ_m[%ƪٶG~ݟ֢Ö{[.eB,S/T6O~87bcU-杈S7GHN"S)#7()꙳tR9i*-͓Z۳)ewIgOF4*7Θ03DcNSה;{fF.te{]uianĢRc֫8~M]cccu IAK>Pex=_I=crȎ72rK?Q^(Ŵ7pu5Αʟn/Qp܇ڤbĪwXoqelX1s鍺}XtnlUOwuL pǝW$54klZ-ޡ5(]""f|^kvePQ:AE_>׾W\ۺfD~$i%&f\,dBV߽|,@FTFէߚ9]) `B褞nA%-55!EO˧- ݈ʹ#'.6TGuzqvV71}G ZD=_d7Y~zL=|ΙlŸK;SoVW.iLv8e\s}vg_ D z d7;{TZQNXK(VsuPSW?SQLAfsf}"kMWFALn:_X! ,''qwrסJLDW}ۉJ.(T"ɀvQƺ6q{Hk(ϦIف:"`oȐ`aՈjN8B IDATV\N,kѠoG(( UnoR`iUKX [XйQ _In 5bADoKb_[Pd7'fHY䙃L+/g[HtΆ*Rl`Lu !l*hy4j,F!AGl^<uq9K!ւaM4_miM(Xߚw+_ӂ4 w]='8gn!^mټ͓(s X;n)0AӘ=mef71h1G劲t72&YQyix D 'C b94Ҽc +:K%&Zy!)m(NT0DNh92**N=X*An}56Ձy3؋?o] oMpdoMxKE~e^W:%q;[_41b Rp&ږ^%Ң ?N~0'͊Ⱦ.%y1LGsQwKY̆-i+ 5D cNb!+}!Fd +}kmOhRJ|,-bɦ&'3نШ<${~?5 i֧׮1F2V.;$޾oy*I%yY+)*2ʿSUQo(1cFYءN L{"`+mNVsU}n1 WƘn/l5N'e:J3rEB18R"ǂ6Nͺ cب ^l^p$@n~w^w xBtwB7^ iSi#lIN7J:eCx`bIlc8uv5+AmE~PeoBD/8Ao%VdG~PhkaiuGkD)r+DŽe*,Jt+  @Í7~#l?Yf4`wg8> Vna8u$-r, C||i RvTF<2QdE Xt,z[{:&(&щ.AwkqKEBt+^k}? 6Cݹ)< 9y:`>h-d͊-ӡD**_8o0ċzϛں"s;_jfkb1ybUp Ygc!{D" #:6,@ D"`{c&6,D"@ EzaZD"@\@.B D"P@ Dx(D"@ EzaZD"@\@.B D"P@ Dx(D"@ EzaZD"@\@.B D"P@ Dx(D"@ EzaZD"@\@.B D"P@ Dx(D"@ EzaZD"@\@.B D"P@ Dx(D"@ EzaZD"@\@.B D"P@ Dx(D"@ EzaZD"@\@.B D"P@ Dx(D"@ EzaZD"@\@UcӯIENDB`mustermann-1.1.1/mustermann-contrib/irb.png000066400000000000000000000560031360365612700210310ustar00rootroot00000000000000PNG  IHDR] iCCPICC ProfileH wTS-@轷{"ͮȠ#(:TGG@ƂbȠ> 6T z'[gg}V>ZO(LG22E!n!PA A txB& ucqMtB >|(-e |cYb3sWV q@tO$r1`uQe&db,O%`|c|߈s22c:M cj c/[}O3sg_3Cِ&9cJ $bse a%v" yjWʆQW56H61j56"e52LL LL^jƘ00bfmnv잹yy+  E Kzn˗VVVn[37[YٴLjr v;7v'>g!͡ar;j8G8NqN?9:;h$4u҂Қ#;2q3e22._>,4 GveME+KVtgbWrr~uʭ}w4_:?3J[W>Y}kl\3um:d] Olм1mfo6En)T)P8mEE[6moI2r랭_/T|*^Ǫ%m,)۷=s;˥W Y(xkٮVusvVTuڳ}ϧZvw{^粯NO)?ݮliOܟ?sniTj,iܔ4bn-kCrڦ:qݸQr~K/7;=ǘNJ;Ε]]8䷦'jNʝ,;E=Uxj3gg;uFs.:vu '._<~{+WnAΫWz |uo޸J@zCyo:}e& 1-b 6׊$Ky@"̚{@,;7@o>1ՉR,"rh}/Ty6SiTXtXML:com.adobe.xmp 542 137 ;@IDATx} \SW@xI@DPAEE ijrۂ{/ogL`g& V#vl+P ƆX UA5(h$P}><$ Ϗُ^k-ׯ_'3`NrKO~2`F`$!p 6-#8,F`, cF`D!Mˈ*, F# `2J`Fش`06-cF`D!Mˈ*, F# Wa6VD>!>1F=~ I q0O91(-~!E:gCvCP ԥ?MP|I nt=hk:]z~Ż_+NZuhLlSr?>G[!RCee@\,;yN2m٦Ι)7v|}R]7KG( H8UClW\$17ȅʆnOnV2W4}J'6,+LOh_wGJ5?k񔔌D)JJ~V=SR-YiYqdj+rX;21;*Jsr"S4Z &LF7絣( F,Fm1]eCVPg\;"棡4*Q49Mk F A_L -SLBۑP2Udfv>}h OIc Yv#ZI3hRGX8jZ!;{Ք:4߫sm4)gÿPyIiy-䳪dbk,vRֽq3!$ 8JKJ8qGɶm%;>$5/Y"XT^RAFOFNN[R~__PRmǎmv짲":n#mΚ9m;h1O;/pj668#?i:7O[!ܜ8+%Z$Kہm" )cUǝa&ɋ :, ,ȎOo^RAdUBՀb d/qP%{/͝ <&l̺ŌJ3$& ^fBL^0uttC+Ih@Tg5hh4򫘕L"LxJ.:Jf]Lk!3X]탷AP&y2Wnpj5O}rCW^`VCAdT#$,CX8tzS$R)xU$Q"s1CLYTQk4JB^!Vku\E;S]B4t2t*]%R % | FMOEu'>»ZW5zF1F0opȕU&x{ -z5T#FD_PE7q=)]Qb m` >CS'i0hXӠUsZF6v嗪++5e yy ⢢b4~y`X4Bç^]:Js\r5Q/R)U5 J*c1/GIdUʊ/w\ GcIl !+6R*F)%VTZ X)]i:I7b۞@c_3^].˯zӓZ3A:Dt t[^v1-h;JTJfl'PAVȠGV0%l x!in tiaM )(9 f2EU-휭1rvp W}AɬkR)3`mQ)Ja @(2P.;SjNK`u\ |u= A\C*㱕أi2@M>A]_UYYj4jU׆.rkk1LA_v\ɘof P\^Zô?2=[4!ǘfƵ'u)RLK@4',542ԍLѝبV] :r X%FLH\ XTY.QvIHϔ&7D7쏣mRkvfΑ4?+k l=/+ _﷖vZ e齻 Pdڲ'5}*"I 4)sڼ&ͮ w  )IC,TYiK9LjyXt,QZM>;J$=GIAs* #NEڢ4 zH@H-C_(4?OB5'bR-xKg9m+Y{?Ls-/wG!__EnA~et0&H*)t{WJ#ҮUXLM冉sQvkOV DC N^HPkV&Q`Zii8A;EPIi^&{;js 2Wod}WѹQm^w%.ZX/Ori4|B+r {]G6eˢϭ/@k;D;%ȣj$Fcp+kQL,0e+6pvU E;ĉOVہ͹]Vm|y%ۑRj d"W[A3-.+ԞSt3J|RIٛvj&^MU~$*mi= Dpkc!R2_cz 1jXaH8ʇ@iCkuRG=ܼ};~]eb;WzP\="n(]&=A8."F/7~h |OF]J0,~&ܓNjRPIT늙Z&LatغjUo^8|dҝ/nI}M$/VӼ j2'95}or%^t@`5]E)89gmyW: Om_{uxx>Ǐr}QJ!;'Hg/ˑIKo|b@U G:Av qLy/tB<ƶȮRc0 7j4S`pCBd,̲04Mq)z)51)@ 0!%iBM;ٻk>۟J` %GJڶn; 8cϰ!lZ,?/s~G ,m%WڱkCBOH%^x:lՎCtc$0i= /1wiUl]X|ZA d%u9) +%D1*yHbT4Ir1VtO|zo9iCCiڶrڲgn՚jm 4%нV><]*n FQJ$/<"AO^l[M-21K9pI_ f)S֖*n _k0+?:w5zpCܤm4TWsCbF"MA/ "#L`hP_jb %G`7t2l+C Ah1#0w1"xA(,'K]v*T^9UУʄB50I*CFTFW(ݙc!NmiB)DJx1.bKfwu^)]iCϵY?.wlٓq|M{Tx21:1Y o~o]dr9KJ`NDШ ]4#Atu(1LQ/#IЁtPFDKt"`L F=rBq)n D(},,^QB+!<!(yBL"į.rB~,ID+~H^#o<%K6\xRyv{owmuhkT;'Jzl(X{,p48m{K5JZv͕Qs鉡$LRy_'PS/<VS$X"\4JRjݦÓ|Uн췌5.JBY4tw%A,wB.Q*5Ɲ33T8*¥гFHJ7~i<30!1<$wΎ3ζ_;9aVl^ZB`6zϝÅL 8YA_Dc鰴/cIt84֖sW"cjt|l#X>=xPHP(|eXzPYz.}}` ,iN&#-]m j~DTl|bP Ŕ;_[[}DDIq.#9a^ȴƁ!#ƴ /f 'bÙ`07ش<SJcF`!M+,x~)(^ `~lZˏ]886 k±6-#0(F`l 'F9b-0BTXF#06el## iAE`0clZF9b-0BTXF#06el## iAE`0cnZlJsV("F#m3Ojȯ^?f%|e!#4F?GH`0~#0MߚF#0L iqX,KáwT={GW㎒mJv|IJָd}-XZlܻ|9hܻDx)T^R)^`n cKӫQYEeڢ·PŻАuZLT1@p2ZgL:Vrq(gKWWe Z<򜨍 ئѼd* { Y?va0 ue ́ gqT LNfڙJPi4JQRJI~YbjF|(4tb&!NF#ps 0ҔɁ®BŮB"#JE"F jB iX,ɡPUٲ75[+NY!eiuK˓Iwfi{_΍{raJc0y㏽F̌򨎙W+ kז4iv=_+@&,y˞mK-y(o)X^ZKTA11uɏvA󫊡]qLB!r+F` !iuju֗WVigm^Q@ Y ~XfBԹYiK]-~FZ6@v^).7h$%'x.^aش@0F&CO>M^$B<52Nn Lf]DgIR\js>%x\-Uj#yY0%`07!Kif *h9Qe-_=ܽ0:mlGDO$\N\hpF#C^]}S`غjUo^K)Kw1'-+Q}NRO/D"/VaԼِ'rqF`4#gh|jwnҷ2ԛmTfﲈGOK"j4i0lHs_F#0ӴO.+UѼ3661+\kVs{{bE/ _LD]{7n YHIrMEr\2 FKsevZSO`bF#0;֖*x.bj fWӌ`JmͦJ[|#enY3d_o1WsE pLThj[&RE T)'B(F#0 [z -yw-NB06óin79EjhdjBI+aY#TzJID\^ F*4ԵzF<&-cM/⧦+%zrbi[Biٹ6WGm;H qlcQ; l"UuM 1@`دa0Xڌ@am Wѱ3<&QK[285)DGMOB7 -F#FƎiF0kF#aEL`0clZB)b0BUXF#0e,"#(iQŁ`0clZB)b0BUXF#0e,"#(iQŁ`0clZB)b0BUXF#0묎m6[v$|B|b v"~ I qM,ښ;_emyXڏ! -O82@.iZ7x $h{& %[끧kDF} 6_|mvZUt+^(n谻`mQG_ւ># nT6E`Ҷ֧4f'yV/C0#/ A2,JᏪ*aC)> ~Z5/q{d1Q9$sj(孕73d5o8̺2WU ))0s˧O2(26h9_CQ0򇟙g\L[i80ْHD^Z)ue׺{K%bhY2֪-VrE6o1 СU_j1;dkT~*5Bsea񀒽Hl3J?EO7EPi:̇M:-? jJj2T:5  T":hb|q?rt/$ӏBe/657*zF7єдy"|YCĊ@,n5ﻟbceEN(uT,j1)~DZJvcCӐjHSI<8]LusA6j*3FC1ZLbG'\n/Epg9@"C C/3}qu2򡮙\p!26:+ymYGw(11CjItj 9 I;J&xנ CLئ".†t B_f0_onَTl&3ˇwS9q7vxR핟8-mx# G HkWw?E%IɩYRP"*?=FQ(m(?fv=ƀjZ=jnM`RaRPiWiMiRJ{<YUljv0he[Mk],e7)ل.wX,gvW(oۉwgm䳷Y G bKہm2(ῌU瑑لyEzmU,mM% ׌N:wlVN2%ardj#lm()ٶMo@7m rJ[]/߼~*Y_PxSh) Wɾ99Kj[YS%G<{ YJ`BۧHjmBېN 7"O[pvxQ41g&!߶WRYȫZD$^Tz,wu#l~^B!-`93YqӚa[NhBL^0utt̜)Fإ, u~3!l֣@T `N<;<RT5nPUmxv9E[8!+.5jXL /Cf7UkyЏZ UY:_9aF3Vnd!g`Rku5JgܭtE"[BݠcS2-l'g<^4mn֫i}jTk ^a^UZZVICJS8 ʬ޻_'FbS:(y-wЇ yVwP9Cy[Y>\Z\pbmPBW\G,7xg. [G-pj*̐_PE7=J)N yT}Ihe`P!j8/@eaCA."v-hוk3K*Sjt:]CMuZdTl#qZvkktjU L"#ESJo.'Ua9.nrBeVjTz ʔ*u^O)T|)Oi@mW^TaLJkg,ʃ(}BIL 0։y G=R^/vLYU, Ʉjk#D.C+#˯&zӇgw{Q0ՂAD25pG)p9t50 ,;ndZ( U_MnkmQ)JaJ~*+J tZhtEn7b3e RHϠlZ᚝]_; Ws꣕A_ըnFezOqx|=}z, Q}@3:hBTg}ez0ww= (]U\8ݩK8qٗ.M Vf@Mƴ\ﮯjU58kCA \`hii1L. Zano؅G3uɘoyQWVg(.;e#ԀV[`e@մ0mmSxV]ʇbmU e4)SP^Gsz,j# e3؝Q!1sVq ;SysJy)Od hn``X\ġ hM*,Ŕ7m\MhMY鮞4nw@ %2QD"/NS5~0u$h◬곝=эgZ6-_mhˊB®Kvl*OdO ,![^[CɴeOkTI/$ (ҤM;k4/U*T*$ȊV2y,)= oIݟ.9y2B jpB0'-=a`3a%Yѣj{Ǜ}vHz0UXVZRVL[J<,:m("29qq|~~%GLnWM2u6mD:7\vxsFNԝl-$9AyQqb ~m/XO'Ö#")ݳ>2jգ\xxA2/(\"!y,װ)3҈$pC|TH"]XLMJ4 [{7V P%mR()ԚIa[Rh! yh`Ɔ" ̌[6z͏]d~IXN}i7-$?ywG7>c]5ب7ql:)ȋiݵH}WV-m^,Nde=X!ȑ T~.z˖- Vr̃"w!l|cw`cpSQ%ß,+Xoº"M.߳+Ox͐A::N~Q 2/ɼwm26$SzYh=JZQYVAP%B$?5$_Lہ͹5HYՈO[[Rzk42VⲂLȻSSr0+%Bxx&eoکxi|lDû(@T"v CbQBE K.أ۷7u]&*?yq;/v2qXZS5D˕`BF&|#URޖ:"͵3\_D(EL|=<g,jyן-ߚ.#3_2/,%RH&ĀsDh-V΢=)"Hg1yeG{KhEjx3>D H{A{CJ'ʮPzŃ!% .H3n2nj]qJpG<%^m]7sd2e7P/d| ȸ!ô71Dpr򜵯tp_ZHm_{ox>GXITi^lHbӹ ][E bmPκׄuG#WD^x yE*MnZtecCK׭v[18ULǬ9[*/J絶T#Y0p£_+it$ZbB%OUـ !'Ks*&U|]B5xBP9vpP] O-E%ÞKDCF>h9 s 6jBd(!V`'Q<%%&O+f 4 {t܅̾p#?NGrGKm'Jk~b^.vٳi^<7 Sy:1O[ f:N?$=jÜ QY1Kk>LUV q+뻛wf[?yWsh('j*M>c'\EU*G_ b`.}+h/}0$'#|26W4DȀxY5s9ɷ ȓCzXC6$QQKgUR2wH-Hqg4xC%IrDpq vڙ㍧$"%pS {|9D^rɥ.`_65ɋ+MkI Q ID\^5N.Wb(3ȉl'umpj4ruMε>j#֪K&oVW^L,~j雈ٰ\FM͒Q+]g4KT9IЉtnxL*I>+Q)5Ɲ3y3p,*T&&&SWi \zp W}ee!yn.GwE[7rKFH@Ryb={3~s;OGOgǙsg/^0+U6/-0){;[W'G zC3aڂےsn)Svx=":vFұYz.}}` ,NOx\͏OtoxH&ǃUdf 쯭>""v줸"kS*@h;5 WJA2wYNls !!V7Fi=ȓmL7jrU}{8 i_#]ޒ`Ȋ۞XhFbLXcz8l==>߬ͯ._g lZN K{p75WJ=j |J1rbԴ0R!GM>d6-!:EYCg_uR-Nq ~J%솹 쮞l&{Gs$Q`BL;CEdxӓs&pM_I8v2E8f& R'RhK9%!i1^f6_b0B'4x1 d0&ǿ&au@}>7 <ދ)Z~=w׼xR6oY]N%~='y蹟`±cl  ZȮ ~T-) LucS҄xmr~n$a"zكUaoE@2~e cV7!炃i_La5cn5|x["[p02RA?ye!)wr ͝LOr¤Xkz£;M<39oBn_&HkO^Ϝh/]Ñ@(G X@.1UB@h±/|1 ߋŅK 8jɜE ?-HY˗ = F*{E M7|GML/ml 5f#~/&rw!WJ ͈|K2lY¢2mQ$X1KC'?7pZuDYI7;qtGZ0A]AퟜF qW'Y{CK_.WH؊tWN=d2`7u=MI8xO^&%5p8Qəfƙϫ~WC*!{?TF_wU]8A"|Ӏe|Í4OIKQ;Ovj9f+I/;iZIT vQUn{wF<%b~~iWc:y7ʈSOc}qn^Z#*[_$,B|;>1> =wDIzÓ" 8<^ h%@Kgޏ9째>]]% xS'sAl~4*qi}՟Me۽yP ィdgCp8Ǣ/>C`s4X|EK+M/?JS~|k40+J}gl{lnV#9 M d0r  @И%`B,='kAI尞lH/=S+'*2*`poNl?@DGJR=nCkW!jAv%$.s&E8;/t%g 6URVS7aB/Z8~+)Mtn$o?$Ϛ ^edȲ2"Kg'Fw&}o|B-H8vvCX@5)iF@kcoh+eДKj(_~ţ/KW9l3(*Q6Tc 'Dc aP̢rjOX3%3tN^&z 9_Y^ \a s{J kW.r?\?&#&GyF:{hCB]fPBqmo{+SKgܚn?MxoS '/ߠvt׶Q'0i|u] LPl}?{{ƭX0ඞ<x ݎFr3]Y9=hs{'..<ϓ$I6R qܳ׹Ȇg,Xa +xǟ#@8Div^'6dQs};d?+f:_0:C|kH`<1wW0+r HoNzq}Zʁ?<\t4'9{3 qځE0rV P6^+ܖȾ\G崻}3x>ا81ˇ 'BH܃e!=ߣF0#r;CK.ͬ'-ȘnI&xV|C?:)3q+2/&K;ۣUrN,OW^\Hϣ{vS90.-?'姍 E78[ɤ;iI㿪p^h$y #oMl9Ae`4-?1ӵYb&ʂڵ%M] 9?-99;=?AzCf$ @=;MmĔ{J47-8b$ESe3# ݕo S3xK3AL2ea\pγ_ЮP$/]#`:h8;p&kh~t-7 0 8$]8 kp>QyyIDAT%.Df>!@_!qG>r#d`4-Nqr+4)sڼA÷gAyϙE5p;i'+Q#)==w~sMHd$7&\%DO#7T{ޜ8)cUh;zF/NGk'vL^Z5`X4}Ȝմ]aܾ~)$|w΢exdzIы1+בx3]i({:;ަ,$VyP]w3E'x<ݏ!o1xaVnCx!8l,׍33BNL8.>)'/+dc< 1tж9z:v=_ =5MbUF l&T۞wr4jvm/ܥI9Sd?Mf:oa~-2: Lx:ƽ*¿fg}6w@DtFp l5GCuGT0g,sE&r @vC@ iR&IsZڹ[ߙ9D>|CGȬ<9QŽ 7&@R0*9+å%xFLY0ؗkي6-R~aʒ_>p)y0/W_QLc疧NsW\5Oq9ST}s%(z?am"b He4յM%1&ᑮ.E=qa+ɼDħy"!J';20wVJjvߋ1?ouվ޼ne$S|qcNG"[B$,}YWwOvꇉttURҧ'q3DW+})4-c},kfA/_/9q|>$AGLXwΣ!('ѓ1@鶆8܇S&CrV3ݮp Y/'&zD8) =HHC('zq7գ_ֽ};؀9=$_87ˍSyS+v{P1-ޜ7DB13/fV`=9>g/>4xzfe3t&9!F Y$f Վ> ̊B FHoIḒCmqAWQ AI9%,=z5!˒]?f]ߞjxZG7跧,+VT$ðg=Aɽs{&n)-/#Y =W4,stϹ6D N=`@dbd|Z@=|e'g7lĬ\r%pF539ۃKp5e rs 500Xj`2΋h$3M]uXKW?|/]Sy)A ~Y:]mΒ*NSd7N}:}C`=s:}^!9C&E"ֺW%)/oL+F9?$p)Os}".MPt_C<ȍ7woxrfWdw};:?isyU"b/EkdK{?K*E}w]ݾxGYܰsT+QS|[MmFi NR([k60c}gϴ)sNS&;}kh ̱ry#m IJY?5 \yvxOդOk{LߴV0#%qР~ʮ$ :P#XM7RLJto.5uybx2x}u+V̝:zwo>>rDduHI8sxBJÙa&s,(v`gG6+tu?YyIcp˚gn^PRKw'ewN m |o[lA$fE|Ssk$9tDN=3SD qMN^t@0g~"^~G|8y3L9x5>{_3fJǓ#%t2|7Q 1w-u_ޮ58\GVf:ճğt%%1R=̟Dk:PJM<ȽQ`n<^GGؿ/Qܥ=RN<Grg8 4LWAG!,r\lGa ,"yV#'6Ʉ.!C~)xΚ#w+ wv>zmiXGG?3>Ci]|>G]0.7{nYTlx{qfG?uӣ#CW!>:!]'À SLqx%pFyǣsYqm/l\SSn|xɞaܬ{f3yrSRLz0" O}ɬ$ (n;9Z}'u Šl 6GR'RFrǧ^֮O MKs`VNh|$+wJ.yBMG_+45#kv(89r/yƃt)pP`qtAA yBNm B`鞵wOBqKI8? 1c6yzWBY$ߠY񕍫FZ[ ׯ{Q4] &$L\c=y-x)vpb!a㤱"d+pgTdr?wJ?5~z|h=ߜu8ϼuv &J]=W]9Sf̜륌OQ5Dd>.O^46ȹi=;ά=W:vI(} <-"8rRTl=S`oh&$lr8ɏxqi>l1gF#p"H#}B`0>ش>F#2ش B#``0F``2d1F##M `0!#Mː! 0F6-|<#`""OIENDB`mustermann-1.1.1/mustermann-contrib/lib/000077500000000000000000000000001360365612700203115ustar00rootroot00000000000000mustermann-1.1.1/mustermann-contrib/lib/mustermann/000077500000000000000000000000001360365612700225025ustar00rootroot00000000000000mustermann-1.1.1/mustermann-contrib/lib/mustermann/cake.rb000066400000000000000000000007771360365612700237450ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/ast/pattern' module Mustermann # CakePHP style pattern implementation. # # @example # Mustermann.new('/:foo', type: :cake) === '/bar' # => true # # @see Mustermann::Pattern # @see file:README.md#cake Syntax description in the README class Cake < AST::Pattern register :cake on(?:) { |c| node(:capture) { scan(/\w+/) } } on(?*) { |c| node(:splat, convert: (-> e { e.split('/') } unless scan(?*))) } end end mustermann-1.1.1/mustermann-contrib/lib/mustermann/express.rb000066400000000000000000000020161360365612700245170ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/ast/pattern' module Mustermann # Express style pattern implementation. # # @example # Mustermann.new('/:foo', type: :express) === '/bar' # => true # # @see Mustermann::Pattern # @see file:README.md#flask Syntax description in the README class Express < AST::Pattern register :express on(nil, ??, ?+, ?*, ?)) { |c| unexpected(c) } on(?:) { |c| node(:capture) { scan(/\w+/) } } on(?() { |c| node(:splat, constraint: read_brackets(?(, ?))) } suffix ??, after: :capture do |char, element| unexpected(char) unless element.is_a? :capture node(:optional, element) end suffix ?*, after: :capture do |match, element| node(:named_splat, element.name) end suffix ?+, after: :capture do |match, element| node(:named_splat, element.name, constraint: ".+") end suffix ?(, after: :capture do |match, element| element.constraint = read_brackets(?(, ?)) element end end end mustermann-1.1.1/mustermann-contrib/lib/mustermann/file_utils.rb000066400000000000000000000155421360365612700251750ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/file_utils/glob_pattern' require 'mustermann/mapper' require 'fileutils' module Mustermann # Implements handy file operations using patterns. module FileUtils extend self # Turn a Mustermann pattern into glob pattern. # # @example # require 'mustermann/file_utils' # # Mustermann::FileUtils.glob_pattern('/:name') # => '/*' # Mustermann::FileUtils.glob_pattern('src/:path/:file.(js|rb)') # => 'src/**/*/*.{js,rb}' # Mustermann::FileUtils.glob_pattern('{a,b}/*', type: :shell) # => '{a,b}/*' # # pattern = Mustermann.new('/foo/:page', '/bar/:page') # => # # Mustermann::FileUtils.glob_pattern(pattern) # => "{/foo/*,/bar/*}" # # @param [Object] pattern the object to turn into a glob pattern. # @return [String] the glob pattern def glob_pattern(*pattern, **options) pattern_with_glob_pattern(*pattern, **options).last end # Uses the given pattern(s) to search for files and directories. # # @example # require 'mustermann/file_utils' # Mustermann::FileUtils.glob(':base.:ext') # => ['example.txt'] # # Mustermann::FileUtils.glob(':base.:ext') do |file, params| # file # => "example.txt" # params # => {"base"=>"example", "ext"=>"txt"} # end def glob(*pattern, **options, &block) raise ArgumentError, "no pattern given" if pattern.empty? pattern, glob_pattern = pattern_with_glob_pattern(*pattern, **options) results = [] unless block Dir.glob(glob_pattern) do |result| next unless params = pattern.params(result) block ? block[result, params] : results << result end results end # Allows to search for files an map these onto other strings. # # @example # require 'mustermann/file_utils' # # Mustermann::FileUtils.glob_map(':base.:ext' => ':base.bak.:ext') # => {'example.txt' => 'example.bak.txt'} # Mustermann::FileUtils.glob_map(':base.:ext' => :base) { |file, mapped| mapped } # => ['example'] # # @see Mustermann::Mapper def glob_map(map = {}, **options, &block) map = Mapper === map ? map : Mapper.new(map, **options) mapped = glob(*map.to_h.keys).map { |f| [f, unescape(map[f])] } block ? mapped.map(&block) : Hash[mapped] end # Copies files based on a pattern mapping. # # @example # require 'mustermann/file_utils' # # # copies example.txt to example.bak.txt # Mustermann::FileUtils.cp(':base.:ext' => ':base.bak.:ext') # # @see #glob_map def cp(map = {}, recursive: false, **options) utils_opts, opts = split_options(:preserve, :dereference_root, :remove_destination, **options) cp_method = recursive ? :cp_r : :cp glob_map(map, **opts) { |o,n| f.send(cp_method, o, n, **utils_opts) } end # Copies files based on a pattern mapping, recursively. # # @example # require 'mustermann/file_utils' # # # copies Foo.app/example.txt to Foo.back.app/example.txt # Mustermann::FileUtils.cp_r(':base.:ext' => ':base.bak.:ext') # # @see #glob_map def cp_r(map = {}, **options) cp(map, recursive: true, **options) end # Moves files based on a pattern mapping. # # @example # require 'mustermann/file_utils' # # # moves example.txt to example.bak.txt # Mustermann::FileUtils.mv(':base.:ext' => ':base.bak.:ext') # # @see #glob_map def mv(map = {}, **options) utils_opts, opts = split_options(**options) glob_map(map, **opts) { |o,n| f.mv(o, n, **utils_opts) } end # Creates links based on a pattern mapping. # # @example # require 'mustermann/file_utils' # # # creates a link from bin/example to lib/example.rb # Mustermann::FileUtils.ln('lib/:name.rb' => 'bin/:name') # # @see #glob_map def ln(map = {}, symbolic: false, **options) utils_opts, opts = split_options(**options) link_method = symbolic ? :ln_s : :ln glob_map(map, **opts) { |o,n| f.send(link_method, o, n, **utils_opts) } end # Creates symbolic links based on a pattern mapping. # # @example # require 'mustermann/file_utils' # # # creates a symbolic link from bin/example to lib/example.rb # Mustermann::FileUtils.ln_s('lib/:name.rb' => 'bin/:name') # # @see #glob_map def ln_s(map = {}, **options) ln(map, symbolic: true, **options) end # Creates symbolic links based on a pattern mapping. # Overrides potentailly existing files. # # @example # require 'mustermann/file_utils' # # # creates a symbolic link from bin/example to lib/example.rb # Mustermann::FileUtils.ln_sf('lib/:name.rb' => 'bin/:name') # # @see #glob_map def ln_sf(map = {}, **options) ln(map, symbolic: true, force: true, **options) end # Splits options into those meant for Mustermann and those # meant for ::FileUtils. # # @!visibility private def split_options(*utils_option_names, **options) utils_options, pattern_options = {}, {} utils_option_names += %i[force noop verbose] options.each do |key, value| list = utils_option_names.include?(key) ? utils_options : pattern_options list[key] = value end [utils_options, pattern_options] end # Create a Mustermann pattern from whatever the input is and turn it into # a glob pattern. # # @!visibility private def pattern_with_glob_pattern(*pattern, **options) options[:uri_decode] ||= false pattern = Mustermann.new(*pattern.flatten, **options) @glob_patterns ||= {} @glob_patterns[pattern] ||= GlobPattern.generate(pattern) [pattern, @glob_patterns[pattern]] end # The FileUtils method to use. # @!visibility private def f ::FileUtils end # Unescape an URI escaped string. # @!visibility private def unescape(string) @uri ||= URI::Parser.new @uri.unescape(string) end # Create a new version of Mustermann::FileUtils using a different ::FileUtils module. # @see DryRun # @!visibility private def with_file_utils(&block) Module.new do include Mustermann::FileUtils define_method(:f, &block) private(:f) extend self end end private :pattern_with_glob_pattern, :split_options, :f, :unescape alias_method :copy, :cp alias_method :move, :mv alias_method :link, :ln alias_method :symlink, :ln_s alias_method :[], :glob DryRun ||= with_file_utils { ::FileUtils::DryRun } NoWrite ||= with_file_utils { ::FileUtils::NoWrite } Verbose ||= with_file_utils { ::FileUtils::Verbose } end end mustermann-1.1.1/mustermann-contrib/lib/mustermann/file_utils/000077500000000000000000000000001360365612700246415ustar00rootroot00000000000000mustermann-1.1.1/mustermann-contrib/lib/mustermann/file_utils/glob_pattern.rb000066400000000000000000000034521360365612700276520ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/ast/translator' module Mustermann module FileUtils # AST Translator to turn Mustermann patterns into glob patterns. # @!visibility private class GlobPattern < Mustermann::AST::Translator # Character that need to be escaped in glob patterns. # @!visibility private ESCAPE = %w([ ] { } * ** \\) # Turn a Mustermann pattern into glob pattern. # @param [#to_glob, #to_ast, Object] pattern the object to turn into a glob pattern. # @return [String] the glob pattern # @!visibility private def self.generate(pattern) return pattern.to_glob if pattern.respond_to? :to_glob return new.translate(pattern.to_ast) if pattern.respond_to? :to_ast return "**/*" unless pattern.is_a? Mustermann::Composite "{#{pattern.patterns.map { |p| generate(p) }.join(',')}}" end translate(:root, :group, :expression) { t(payload) || "" } translate(:separator, :char) { t.escape(payload) } translate(:capture) { constraint ? "**/*" : "*" } translate(:optional) { "{#{t(payload)},}" } translate(:named_splat, :splat) { "**/*" } translate(:with_look_ahead) { t(head) + t(payload) } translate(:union) { "{#{payload.map { |e| t(e) }.join(',')}}" } translate(Array) { map { |e| t(e) }.join } # Escape with a slash rather than URI escaping. # @!visibility private def escape(char) ESCAPE.include?(char) ? "\\#{char}" : char end end end end mustermann-1.1.1/mustermann-contrib/lib/mustermann/fileutils.rb000066400000000000000000000000401360365612700250210ustar00rootroot00000000000000require 'mustermann/file_utils' mustermann-1.1.1/mustermann-contrib/lib/mustermann/flask.rb000066400000000000000000000155721360365612700241410ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/ast/pattern' module Mustermann # Flask style pattern implementation. # # @example # Mustermann.new('/', type: :flask) === '/bar' # => true # # @see Mustermann::Pattern # @see file:README.md#flask Syntax description in the README class Flask < AST::Pattern include Concat::Native register :flask on(nil, ?>, ?:) { |c| unexpected(c) } on(?<) do |char| converter_name = expect(/\w+/, char: char) args, opts = scan(?() ? read_args(?=, ?)) : [[], {}] if scan(?:) name = read_escaped(?>) else converter_name, name = 'default', converter_name expect(?>) end converter = pattern.converters.fetch(converter_name) { unexpected("converter %p" % converter_name) } converter = converter.new(*args, **opts) if converter.respond_to? :new constraint = converter.constraint if converter.respond_to? :constraint convert = converter.convert if converter.respond_to? :convert qualifier = converter.qualifier if converter.respond_to? :qualifier node_type = converter.node_type if converter.respond_to? :node_type node_type ||= :capture node(node_type, name, convert: convert, constraint: constraint, qualifier: qualifier) end # A class for easy creating of converters. # @see Mustermann::Flask#register_converter class Converter # Constraint on the format used for the capture. # Should be a regexp (or a string corresponding to a regexp) # @see Mustermann::Flask#register_converter attr_accessor :constraint # Callback # Should be a Proc. # @see Mustermann::Flask#register_converter attr_accessor :convert # Constraint on the format used for the capture. # Should be a regexp (or a string corresponding to a regexp) # @see Mustermann::Flask#register_converter # @!visibility private attr_accessor :node_type # Constraint on the format used for the capture. # Should be a regexp (or a string corresponding to a regexp) # @see Mustermann::Flask#register_converter # @!visibility private attr_accessor :qualifier # @!visibility private def self.create(&block) Class.new(self) do define_method(:initialize) { |*a, **o| block[self, *a, **o] } end end # Makes sure a given value falls inbetween a min and a max. # Uses the passed block to convert the value from a string to whatever # format you'd expect. # # @example # require 'mustermann/flask' # # class MyPattern < Mustermann::Flask # register_converter(:x) { between(5, 15, &:to_i) } # end # # pattern = MyPattern.new('') # pattern.params('/12') # => { 'id' => 12 } # pattern.params('/16') # => { 'id' => 15 } # # @see Mustermann::Flask#register_converter def between(min, max) self.convert = proc do |input| value = yield(input) value = yield(min) if min and value < yield(min) value = yield(max) if max and value > yield(max) value end end end # Generally available converters. # @!visibility private def self.converters(inherited = true) return @converters ||= {} unless inherited defaults = superclass.respond_to?(:converters) ? superclass.converters : {} defaults.merge(converters(false)) end # Allows you to register your own converters. # # It is reommended to use this on a subclass, so to not influence other subsystems # using flask templates. # # The object passed in as converter can implement #convert and/or #constraint. # # It can also instead implement #new, which will then return an object responding # to some of these methods. Arguments from the flask pattern will be passed to #new. # # If passed a block, it will be yielded to with a {Mustermann::Flask::Converter} # instance and any arguments in the flask pattern. # # @example with simple object # require 'mustermann/flask' # # MyPattern = Class.new(Mustermann::Flask) # up_converter = Struct.new(:convert).new(:upcase.to_proc) # MyPattern.register_converter(:upper, up_converter) # # MyPattern.new("/").params('/foo') # => { "name" => "FOO" } # # @example with block # require 'mustermann/flask' # # MyPattern = Class.new(Mustermann::Flask) # MyPattern.register_converter(:upper) { |c| c.convert = :upcase.to_proc } # # MyPattern.new("/").params('/foo') # => { "name" => "FOO" } # # @example with converter class # require 'mustermann/flasl' # # class MyPattern < Mustermann::Flask # class Converter # attr_reader :convert # def initialize(send: :to_s) # @convert = send.to_sym.to_proc # end # end # # register_converter(:t, Converter) # end # # MyPattern.new("/").params('/Foo') # => { "name" => "FOO" } # MyPattern.new("/").params('/Foo') # => { "name" => "foo" } # # @param [#to_s] name converter name # @param [#new, #convert, #constraint, nil] converter def self.register_converter(name, converter = nil, &block) converter ||= Converter.create(&block) converters(false)[name.to_s] = converter end register_converter(:string) do |converter, minlength: nil, maxlength: nil, length: nil| converter.qualifier = "{%s,%s}" % [minlength || 1, maxlength] if minlength or maxlength converter.qualifier = "{%s}" % length if length end register_converter(:int) do |converter, min: nil, max: nil, fixed_digits: false| converter.constraint = /\d/ converter.qualifier = "{#{fixed_digits}}" if fixed_digits converter.between(min, max) { |string| Integer(string) } end register_converter(:float) do |converter, min: nil, max: nil| converter.constraint = /\d*\.?\d+/ converter.qualifier = "" converter.between(min, max) { |string| Float(string) } end register_converter(:path) do |converter| converter.node_type = :named_splat end register_converter(:any) do |converter, *strings| strings = strings.map { |s| Regexp.escape(s) unless s == {} }.compact converter.qualifier = "" converter.constraint = Regexp.union(*strings) end register_converter(:default, converters['string']) supported_options :converters attr_reader :converters def initialize(input, converters: {}, **options) @converters = self.class.converters.dup converters.each { |k,v| @converters[k.to_s] = v } if converters super(input, **options) end end end mustermann-1.1.1/mustermann-contrib/lib/mustermann/pyramid.rb000066400000000000000000000013661360365612700245020ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/ast/pattern' module Mustermann # Pyramid style pattern implementation. # # @example # Mustermann.new('/', type: :pryamid) === '/bar' # => true # # @see Mustermann::Pattern # @see file:README.md#pryamid Syntax description in the README class Pyramid < AST::Pattern register :pyramid on(nil, ?}) { |c| unexpected(c) } on(?{) do |char| name = expect(/\w+/, char: char) constraint = read_brackets(?{, ?}) if scan(?:) expect(?}) unless constraint node(:capture, name, constraint: constraint) end on(?*) do |char| node(:named_splat, expect(/\w+$/, char: char), convert: -> e { e.split(?/) }) end end end mustermann-1.1.1/mustermann-contrib/lib/mustermann/rails.rb000066400000000000000000000025101360365612700241370ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/ast/pattern' require 'mustermann/versions' module Mustermann # Rails style pattern implementation. # # @example # Mustermann.new('/:foo', type: :rails) === '/bar' # => true # # @see Mustermann::Pattern # @see file:README.md#rails Syntax description in the README class Rails < AST::Pattern extend Versions register :rails # first parser, no optional parts version('2.3') do on(nil) { |c| unexpected(c) } on(?*) { |c| node(:named_splat) { scan(/\w+/) } } on(?:) { |c| node(:capture) { scan(/\w+/) } } end # rack-mount version('3.0', '3.1') do on(?)) { |c| unexpected(c) } on(?() { |c| node(:optional, node(:group) { read unless scan(?)) }) } on(?\\) { |c| node(:char, expect(/./)) } end # stand-alone journey version('3.2') do on(?|) { |c| raise ParseError, "the implementation of | is broken in ActionDispatch, cannot compile compatible pattern" } on(?\\) { |c| node(:char, c) } end # embedded journey, broken (ignored) escapes version('4.0', '4.1') { on(?\\) { |c| read } } # escapes got fixed in 4.2 version('4.2') { on(?\\) { |c| node(:char, expect(/./)) } } # Rails 5.0 fixes | version('5.0') { on(?|) { |c| node(:or) }} end end mustermann-1.1.1/mustermann-contrib/lib/mustermann/shell.rb000066400000000000000000000034731360365612700241450ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/pattern' require 'mustermann/simple_match' module Mustermann # Matches strings that are identical to the pattern. # # @example # Mustermann.new('/*.*', type: :shell) === '/bar' # => false # # @see Mustermann::Pattern # @see file:README.md#shell Syntax description in the README class Shell < Pattern include Concat::Native register :shell # @!visibility private # @return [#highlight, nil] # highlighing logic for mustermann-visualizer, # nil if mustermann-visualizer hasn't been loaded def highlighter return unless defined? Mustermann::Visualizer::Highlighter @@highlighter ||= Mustermann::Visualizer::Highlighter.create do on('\\') { |matched| escaped(matched, scanner.getch) } on(/[\*\[\]]/, :special) on("{") { nested(:union, ?{, ?}, ?,) } end end # @param (see Mustermann::Pattern#initialize) # @return (see Mustermann::Pattern#initialize) # @see (see Mustermann::Pattern#initialize) def initialize(string, **options) @flags = File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB super(string, **options) end # @param (see Mustermann::Pattern#===) # @return (see Mustermann::Pattern#===) # @see (see Mustermann::Pattern#===) def ===(string) File.fnmatch? @string, unescape(string), @flags end # @param (see Mustermann::Pattern#peek_size) # @return (see Mustermann::Pattern#peek_size) # @see (see Mustermann::Pattern#peek_size) def peek_size(string) @peek_string ||= @string + "{**,/**,/**/*}" super if File.fnmatch? @peek_string, unescape(string), @flags end # Used by {Mustermann::FileUtils} to not use a generic glob pattern. alias_method :to_glob, :to_s end end mustermann-1.1.1/mustermann-contrib/lib/mustermann/simple.rb000066400000000000000000000035771360365612700243340ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/regexp_based' module Mustermann # Sinatra 1.3 style pattern implementation. # # @example # Mustermann.new('/:foo', type: :simple) === '/bar' # => true # # @see Mustermann::Pattern # @see file:README.md#simple Syntax description in the README class Simple < RegexpBased register :simple supported_options :greedy, :space_matches_plus instance_delegate highlighter: 'self.class' # @!visibility private # @return [#highlight, nil] # highlighing logic for mustermann-visualizer, # nil if mustermann-visualizer hasn't been loaded def self.highlighter return unless defined? Mustermann::Visualizer::Highlighter @highlighter ||= Mustermann::Visualizer::Highlighter.create do on(/:(\w+)/) { |matched| element(:capture, ':') { element(:name, matched[1..-1]) } } on("*" => :splat, "?" => :optional) end end def compile(greedy: true, uri_decode: true, space_matches_plus: true, **options) pattern = @string.gsub(/[^\?\%\\\/\:\*\w]/) { |c| encoded(c, uri_decode, space_matches_plus) } pattern.gsub!(/((:\w+)|\*)/) do |match| match == "*" ? "(?.*?)" : "(?<#{$2[1..-1]}>[^/?#]+#{?? unless greedy})" end Regexp.new(pattern) rescue SyntaxError, RegexpError => error type = error.message["invalid group name"] ? CompileError : ParseError raise type, error.message, error.backtrace end def encoded(char, uri_decode, space_matches_plus) return Regexp.escape(char) unless uri_decode parser = URI::Parser.new encoded = Regexp.union(parser.escape(char), parser.escape(char, /./).downcase, parser.escape(char, /./).upcase) encoded = Regexp.union(encoded, encoded('+', true, true)) if space_matches_plus and char == " " encoded end private :compile, :encoded end end mustermann-1.1.1/mustermann-contrib/lib/mustermann/string_scanner.rb000066400000000000000000000244301360365612700260510ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/pattern_cache' require 'delegate' module Mustermann # Class inspired by Ruby's StringScanner to scan an input string using multiple patterns. # # @example # require 'mustermann/string_scanner' # scanner = Mustermann::StringScanner.new("here is our example string") # # scanner.scan("here") # => "here" # scanner.getch # => " " # # if scanner.scan(":verb our") # scanner.scan(:noun, capture: :word) # scanner[:verb] # => "is" # scanner[:nound] # => "example" # end # # scanner.rest # => "string" # # @note # This structure is not thread-safe, you should not scan on the same StringScanner instance concurrently. # Even if it was thread-safe, scanning concurrently would probably lead to unwanted behaviour. class StringScanner # Exception raised if scan/unscan operation cannot be performed. ScanError = Class.new(::ScanError) PATTERN_CACHE = PatternCache.new private_constant :PATTERN_CACHE # Patterns created by {#scan} will be globally cached, since we assume that there is a finite number # of different patterns used and that they are more likely to be reused than not. # This method allows clearing the cache. # # @see Mustermann::PatternCache def self.clear_cache PATTERN_CACHE.clear end # @return [Integer] number of cached patterns # @see clear_cache # @api private def self.cache_size PATTERN_CACHE.size end # Encapsulates return values for {StringScanner#scan}, {StringScanner#check}, and friends. # Behaves like a String (the substring which matched the pattern), but also exposes its position # in the main string and any params parsed from it. class ScanResult < DelegateClass(String) # The scanner this result came from. # @example # require 'mustermann/string_scanner' # scanner = Mustermann::StringScanner.new('foo/bar') # scanner.scan(:name).scanner == scanner # => true attr_reader :scanner # @example # require 'mustermann/string_scanner' # scanner = Mustermann::StringScanner.new('foo/bar') # scanner.scan(:name).position # => 0 # scanner.getch.position # => 3 # scanner.scan(:name).position # => 4 # # @return [Integer] position the substring starts at attr_reader :position alias_method :pos, :position # @example # require 'mustermann/string_scanner' # scanner = Mustermann::StringScanner.new('foo/bar') # scanner.scan(:name).length # => 3 # scanner.getch.length # => 1 # scanner.scan(:name).length # => 3 # # @return [Integer] length of the substring attr_reader :length # Params parsed from the substring. # Will not include params from previous scan results. # # @example # require 'mustermann/string_scanner' # scanner = Mustermann::StringScanner.new('foo/bar') # scanner.scan(:name).params # => { "name" => "foo" } # scanner.getch.params # => {} # scanner.scan(:name).params # => { "name" => "bar" } # # @see Mustermann::StringScanner#params # @see Mustermann::StringScanner#[] # # @return [Hash] params parsed from the substring attr_reader :params # @api private def initialize(scanner, position, length, params = {}) @scanner, @position, @length, @params = scanner, position, length, params end # @api private # @!visibility private def __getobj__ @__getobj__ ||= scanner.to_s[position, length] end end # @return [Hash] default pattern options used for {#scan} and similar methods # @see #initialize attr_reader :pattern_options # Params from all previous matches from {#scan} and {#scan_until}, # but not from {#check} and {#check_until}. Changes can be reverted # with {#unscan} and it can be completely cleared via {#reset}. # # @return [Hash] current params attr_reader :params # @return [Integer] current scan position on the input string attr_accessor :position alias_method :pos, :position alias_method :pos=, :position= # @example with different default type # require 'mustermann/string_scanner' # scanner = Mustermann::StringScanner.new("foo/bar/baz", type: :shell) # scanner.scan('*') # => "foo" # scanner.scan('**/*') # => "/bar/baz" # # @param [String] string the string to scan # @param [Hash] pattern_options default options used for {#scan} def initialize(string = "", **pattern_options) @pattern_options = pattern_options @string = String(string).dup reset end # Resets the {#position} to the start and clears all {#params}. # @return [Mustermann::StringScanner] the scanner itself def reset @position = 0 @params = {} @history = [] self end # Moves the position to the end of the input string. # @return [Mustermann::StringScanner] the scanner itself def terminate track_result ScanResult.new(self, @position, size - @position) self end # Checks if the given pattern matches any substring starting at the current position. # # If it does, it will advance the current {#position} to the end of the substring and merges any params parsed # from the substring into {#params}. # # @param (see Mustermann.new) # @return [Mustermann::StringScanner::ScanResult, nil] the matched substring, nil if it didn't match def scan(pattern, **options) track_result check(pattern, **options) end # Checks if the given pattern matches any substring starting at any position after the current position. # # If it does, it will advance the current {#position} to the end of the substring and merges any params parsed # from the substring into {#params}. # # @param (see Mustermann.new) # @return [Mustermann::StringScanner::ScanResult, nil] the matched substring, nil if it didn't match def scan_until(pattern, **options) result, prefix = check_until_with_prefix(pattern, **options) track_result(prefix, result) end # Reverts the last operation that advanced the position. # # Operations advancing the position: {#terminate}, {#scan}, {#scan_until}, {#getch}. # @return [Mustermann::StringScanner] the scanner itself def unscan raise ScanError, 'unscan failed: previous match record not exist' if @history.empty? previous = @history[0..-2] reset previous.each { |r| track_result(*r) } self end # Checks if the given pattern matches any substring starting at the current position. # # Does not affect {#position} or {#params}. # # @param (see Mustermann.new) # @return [Mustermann::StringScanner::ScanResult, nil] the matched substring, nil if it didn't match def check(pattern, **options) params, length = create_pattern(pattern, **options).peek_params(rest) ScanResult.new(self, @position, length, params) if params end # Checks if the given pattern matches any substring starting at any position after the current position. # # Does not affect {#position} or {#params}. # # @param (see Mustermann.new) # @return [Mustermann::StringScanner::ScanResult, nil] the matched substring, nil if it didn't match def check_until(pattern, **options) check_until_with_prefix(pattern, **options).first end def check_until_with_prefix(pattern, **options) start = @position @position += 1 until eos? or result = check(pattern, **options) prefix = ScanResult.new(self, start, @position - start) if result [result, prefix] ensure @position = start end # Reads a single character and advances the {#position} by one. # @return [Mustermann::StringScanner::ScanResult, nil] the character, nil if at end of string def getch track_result ScanResult.new(self, @position, 1) unless eos? end # Appends the given string to the string being scanned # # @example # require 'mustermann/string_scanner' # scanner = Mustermann::StringScanner.new # scanner << "foo" # scanner.scan(/.+/) # => "foo" # # @param [String] string will be appended # @return [Mustermann::StringScanner] the scanner itself def <<(string) @string << string self end # @return [true, false] whether or not the end of the string has been reached def eos? @position >= @string.size end # @return [true, false] whether or not the current position is at the start of a line def beginning_of_line? @position == 0 or @string[@position - 1] == "\n" end # @return [String] outstanding string not yet matched, empty string at end of input string def rest @string[@position..-1] || "" end # @return [Integer] number of character remaining to be scanned def rest_size @position > size ? 0 : size - @position end # Allows to peek at a number of still unscanned characters without advacing the {#position}. # # @param [Integer] length how many characters to look at # @return [String] the substring def peek(length = 1) @string[@position, length] end # Shorthand for accessing {#params}. Accepts symbols as keys. def [](key) params[key.to_s] end # (see #params) def to_h params.dup end # @return [String] the input string # @see #initialize # @see #<< def to_s @string.dup end # @return [Integer] size of the input string def size @string.size end # @!visibility private def inspect "#<%p %d/%d @ %p>" % [ self.class, @position, @string.size, @string ] end # @!visibility private def create_pattern(pattern, **options) PATTERN_CACHE.create_pattern(pattern, **options, **pattern_options) end # @!visibility private def track_result(*results) results.compact! @history << results if results.any? results.each do |result| @params.merge! result.params @position += result.length end results.last end private :create_pattern, :track_result, :check_until_with_prefix end end mustermann-1.1.1/mustermann-contrib/lib/mustermann/strscan.rb000066400000000000000000000000441360365612700245020ustar00rootroot00000000000000require 'mustermann/string_scanner' mustermann-1.1.1/mustermann-contrib/lib/mustermann/template.rb000066400000000000000000000035371360365612700246520ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/ast/pattern' module Mustermann # URI template pattern implementation. # # @example # Mustermann.new('/{foo}') === '/bar' # => true # # @see Mustermann::Pattern # @see file:README.md#template Syntax description in the README # @see http://tools.ietf.org/html/rfc6570 RFC 6570 class Template < AST::Pattern include Concat::Native register :template, :uri_template on ?{ do |char| variable = proc do start = pos match = expect(/(?\w+)(?:\:(?\d{1,4})|(?\*))?/) node(:variable, match[:name], prefix: match[:prefix], explode: match[:explode], start: start) end operator = buffer.scan(/[\+\#\.\/;\?\&\=\,\!\@\|]/) expression = node(:expression, [variable[]], operator: operator) { variable[] if scan(?,) } expression if expect(?}) end on(?}) { |c| unexpected(c) } # @!visibility private def compile(*args, **options) @split_params = {} super(*args, split_params: @split_params, **options) end # @!visibility private def map_param(key, value) return super unless variable = @split_params[key] value = value.split variable[:separator] value.map! { |e| e.sub(/\A#{key}=/, '') } if variable[:parametric] value.map! { |e| super(key, e) } end # @!visibility private def always_array?(key) @split_params.include? key end # Identity patterns support generating templates (the logic is quite complex, though). # # @example (see Mustermann::Pattern#to_templates) # @param (see Mustermann::Pattern#to_templates) # @return (see Mustermann::Pattern#to_templates) # @see Mustermann::Pattern#to_templates def to_templates [to_s] end private :compile, :map_param, :always_array? end end mustermann-1.1.1/mustermann-contrib/lib/mustermann/uri_template.rb000066400000000000000000000000351360365612700255170ustar00rootroot00000000000000require 'mustermann/template'mustermann-1.1.1/mustermann-contrib/lib/mustermann/versions.rb000066400000000000000000000024541360365612700247040ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann # Mixin that adds support for multiple versions of the same type. # @see Mustermann::Rails # @!visibility private module Versions # Checks if class has mulitple versions available and picks one that matches the version option. # @!visibility private def new(*args, version: nil, **options) return super(*args, **options) unless versions.any? self[version].new(*args, **options) end # @return [Hash] version to subclass mapping. # @!visibility private def versions @versions ||= {} end # Defines a new version. # @!visibility private def version(*list, inherit_from: nil, &block) superclass = self[inherit_from] || self subclass = Class.new(superclass, &block) list.each { |v| versions[v] = subclass } end # Resolve a subclass for a given version string. # @!visibility private def [](version) return versions.values.last unless version detected = versions.detect { |v,_| version.start_with?(v) } raise ArgumentError, 'unsupported version %p' % version unless detected detected.last end # @!visibility private def name super || superclass.name end # @!visibility private def inspect name end end end mustermann-1.1.1/mustermann-contrib/lib/mustermann/visualizer.rb000066400000000000000000000022141360365612700252230ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/visualizer/highlight' require 'mustermann/visualizer/tree_renderer' require 'mustermann/visualizer/pattern_extension' module Mustermann # Namespace for Mustermann visualization logic. module Visualizer extend self # @example creating a highlight object # require 'mustermann/visualizer' # # pattern = Mustermann.new('/:name') # highlight = Mustermann::Visualizer.highlight(pattern) # # puts highlight.to_ansi # # @return [Mustermann::Visualizer::Highlight] highlight object for given pattern # @param (see Mustermann::Visualizer::Highlight#initialize) def highlight(pattern, **options) Highlight.new(pattern, **options) end # @example creating a tree object # require 'mustermann/visualizer' # # pattern = Mustermann.new('/:name') # tree = Mustermann::Visualizer.tree(pattern) # # puts highlight.to_s # # @return [Mustermann::Visualizer::Tree] tree object for given pattern def tree(pattern, **options) TreeRenderer.render(pattern, **options) end end end mustermann-1.1.1/mustermann-contrib/lib/mustermann/visualizer/000077500000000000000000000000001360365612700246775ustar00rootroot00000000000000mustermann-1.1.1/mustermann-contrib/lib/mustermann/visualizer/highlight.rb000066400000000000000000000077301360365612700272020ustar00rootroot00000000000000# frozen_string_literal: true require 'hansi' require 'mustermann' require 'mustermann/visualizer/highlighter' require 'mustermann/visualizer/renderer/ansi' require 'mustermann/visualizer/renderer/hansi_template' require 'mustermann/visualizer/renderer/html' require 'mustermann/visualizer/renderer/sexp' module Mustermann module Visualizer # Meta class for highlight objects. # @see Mustermann::Visualizer#highlight class Highlight # @!visibility private attr_reader :pattern, :theme # @!visibility private DEFAULT_THEME = Hansi::Theme.new(:solarized, default: :base0, separator: :base1, escaped: :base1, capture: :orange, name: :yellow, special: :blue, quote: :red, illegal: :darkred ) # @!visibility private BASE_THEME = Hansi::Theme.new( special: :default, capture: :special, char: :default, expression: :capture, composition: :special, group: :composition, union: :composition, optional: :special, root: :default, separator: :char, splat: :capture, named_splat: :splat, variable: :capture, escaped: :char, quote: :special, type: :special, illegal: :special ) # @!visibility private def initialize(pattern, type: nil, inspect: nil, **theme) @pattern = Mustermann.new(pattern, type: type) @inspect = inspect.nil? ? pattern.is_a?(Mustermann::Composite) : inspect theme = theme.any? ? Hansi::Theme.new(**theme) : DEFAULT_THEME @theme = BASE_THEME.merge(theme) end # @example # require 'mustermann/visualizer' # # pattern = Mustermann.new('/:name') # highlight = Mustermann::Visualizer.highlight(pattern) # # puts highlight.to_hansi_template # # @return [String] Hansi template representation of the pattern def to_hansi_template(**options) render_with(Renderer::HansiTemplate, **options) end # @example # require 'mustermann/visualizer' # # pattern = Mustermann.new('/:name') # highlight = Mustermann::Visualizer.highlight(pattern) # # puts highlight.to_ansi # # @return [String] ANSI colorized version of the pattern def to_ansi(**options) render_with(Renderer::ANSI, **options) end # @example # require 'mustermann/visualizer' # # pattern = Mustermann.new('/:name') # highlight = Mustermann::Visualizer.highlight(pattern) # # puts highlight.to_html # # @return [String] HTML rendering of the pattern def to_html(**options) render_with(Renderer::HTML, **options) end # @example # require 'mustermann/visualizer' # # pattern = Mustermann.new('/:name') # highlight = Mustermann::Visualizer.highlight(pattern) # # puts highlight.to_sexp # # @return [String] s-expression like representation of the pattern def to_sexp(**options) render_with(Renderer::Sexp, **options) end # @return [Mustermann::Pattern] the pattern used to create the highlight object def to_pattern pattern end # @return [String] string representation of the pattern def to_s pattern.to_s end # @return [String] stylesheet for HTML output from the pattern def stylesheet(**options) Renderer::HTML.new(self, **options).stylesheet end # @!visibility private def render_with(renderer, **options) options[:inspect] = @inspect if options[:inspect].nil? renderer.new(self, **options).render end # @!visibility private def render(renderer) Highlighter.highlight(pattern, renderer) end end end end mustermann-1.1.1/mustermann-contrib/lib/mustermann/visualizer/highlighter.rb000066400000000000000000000023501360365612700275220ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/visualizer/highlighter/ast' require 'mustermann/visualizer/highlighter/ad_hoc' require 'mustermann/visualizer/highlighter/composite' require 'mustermann/visualizer/highlighter/dummy' require 'mustermann/visualizer/highlighter/regular' module Mustermann module Visualizer # @!visibility private module Highlighter extend self # @return [String] highlighted string # @!visibility private def highlight(pattern, renderer) highlighter_for(pattern).highlight(pattern, renderer) end # @return [#highlight] Highlighter for given pattern # @!visibility private def highlighter_for(pattern) return pattern.highlighter if pattern.respond_to? :highlighter and pattern.highlighter consts = constants.map { |name| const_get(name) } highlighter = consts.detect { |c| c.respond_to? :highlight? and c.highlight? pattern } highlighter || Dummy end # Used to generate highlighting rules on the fly. # @see {Mustermann::Shell#highlighter} # @see {Mustermann::Simple#highlighter} # @!visibility private def create(&block) Class.new(AdHoc, &block) end end end end mustermann-1.1.1/mustermann-contrib/lib/mustermann/visualizer/highlighter/000077500000000000000000000000001360365612700271755ustar00rootroot00000000000000mustermann-1.1.1/mustermann-contrib/lib/mustermann/visualizer/highlighter/ad_hoc.rb000066400000000000000000000056201360365612700307420ustar00rootroot00000000000000# frozen_string_literal: true require 'strscan' module Mustermann module Visualizer # @!visibility private module Highlighter # Used to generate highlighting rules on the fly. # @see {Mustermann::Shell#highlighter} # @see {Mustermann::Simple#highlighter} # @!visibility private class AdHoc # @!visibility private def self.highlight(pattern, renderer) new(pattern, renderer).highlight end # @!visibility private def self.rules @rules ||= {} end # @!visibility private def self.on(regexp, type = nil, &callback) return regexp.map { |key, value| on(key, value, &callback) } if regexp.is_a? Hash raise ArgumentError, 'needs type or callback' unless type or callback callback ||= proc { |matched| element(type, matched) } regexp = Regexp.new(Regexp.escape(regexp)) unless regexp.is_a? Regexp rules[regexp] = callback end # @!visibility private attr_reader :pattern, :renderer, :rules, :output, :scanner def initialize(pattern, renderer) @pattern = pattern @renderer = renderer @output = String.new @rules = self.class.rules @scanner = ::StringScanner.new(pattern.to_s) end # @!visibility private def highlight(stop = /\Z/) output << renderer.pre(:root) until scanner.eos? or scanner.check(stop) position = scanner.pos apply(scanner) read_char(scanner) if position == scanner.pos and not scanner.check(stop) end output << renderer.post(:root) end # @!visibility private def apply(scanner) rules.each do |regexp, callback| next unless result = scanner.scan(regexp) instance_exec(result, &callback) end end # @!visibility private def read_char(scanner) return unless char = scanner.getch type = char == ?/ ? :separator : :char element(type, char) end # @!visibility private def escaped(content = ?\\, char) element(:escaped, content) { element(:escaped_char, char) } end # @!visibility private def nested(type, opening, closing, *separators) element(type, opening) do char = nil until char == closing or scanner.eos? highlight(Regexp.union(closing, *separators)) char = scanner.getch output << char if char end end end # @!visibility private def element(type, content = nil) output << renderer.pre(type) output << renderer.escape(content) if content yield if block_given? output << renderer.post(type) end end end end end mustermann-1.1.1/mustermann-contrib/lib/mustermann/visualizer/highlighter/ast.rb000066400000000000000000000065231360365612700303170ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/ast/translator' module Mustermann module Visualizer # @!visibility private module Highlighter # Provides highlighting for AST based patterns # @!visibility private class AST Index = Struct.new(:type, :start, :stop, :payload) { undef :to_a } Indexer = Mustermann::AST::Translator.create do translate(:node) { |i| Index.new(type, start, stop, Array(t(payload, i)).flatten.compact) } translate(Array) { |i| map { |e| t(e, i) } } translate(Object) { |i| } translate(:with_look_ahead) do |input| [t(head, input), *t(payload, input)] end translate(:expression) do |input| index = Index.new(type, start, stop, Array(t(payload, input)).compact) index.payload.delete_if { |e| e.type == :separator } index end translate(:capture) do |input| substring = input[start, length] if substart = substring.index(name) substart += start substop = substart + name.length payload = [Index.new(:name, substart, substop, [])] end Index.new(type, start, stop, payload || []) end translate(:char) do |input| substring = input[start, length] if payload == substring Index.new(type, start, stop, []) elsif substart = substring.index(payload) substart += start substop = substart + payload.length Index.new(:escaped, start, stop, [Index.new(:escaped_char, substart, substop, [])]) else Index.new(:escaped, start, stop, []) end end end private_constant(:Index, :Indexer) # @!visibility private def self.highlight?(pattern) pattern.respond_to? :to_ast end # @!visibility private def self.highlight(pattern, renderer) new(pattern, renderer).highlight end # @!visibility private def initialize(pattern, renderer) @ast = pattern.to_ast @string = pattern.to_s @renderer = renderer end # @!visibility private def highlight index = Indexer.translate(@ast, @string) inject_literals(index) render(index) end # @!visibility private def render(index) return @renderer.escape(@string[index.start..index.stop-1]) if index.type == :literal payload = index.payload.map { |i| render(i) }.join "#{ @renderer.pre(index.type) }#{ payload }#{ @renderer.post(index.type) }" end # @!visibility private def inject_literals(index) start, old_payload, index.payload = index.start, index.payload, [] old_payload.each do |element| index.payload << literal(start, element.start) if start < element.start index.payload << element inject_literals(element) start = element.stop end index.payload << literal(start, index.stop) if start < index.stop end # @!visibility private def literal(start, stop) Index.new(:literal, start, stop, []) end end end end end mustermann-1.1.1/mustermann-contrib/lib/mustermann/visualizer/highlighter/composite.rb000066400000000000000000000027501360365612700315300ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann module Visualizer # @!visibility private module Highlighter # @!visibility private module Composite extend self # @!visibility private def highlight?(pattern) pattern.is_a? Mustermann::Composite end # @!visibility private def highlight(pattern, renderer) operator = " #{pattern.operator} " patterns = pattern.patterns.map { |p| highlight_nested(p, renderer) }.join(quote(renderer, operator)) renderer.pre(:composite) + patterns + renderer.post(:composite) end # @!visibility private def highlight_nested(pattern, renderer) highlighter = Highlighter.highlighter_for(pattern) if highlighter.respond_to? :nested_highlight highlighter.nested_highlight(pattern, renderer) else type = quote(renderer, pattern.class.name[/[^:]+$/].downcase + ":", :type) quote = quote(renderer, ?") type + quote + highlighter.highlight(pattern, renderer) + quote end end # @!visibility private def nested_highlight(pattern, renderer) quote(renderer, ?() + highlight(pattern, renderer) + quote(renderer, ?)) end # @!visibility private def quote(renderer, string, type = :quote) renderer.pre(type) + renderer.escape(string, string) + renderer.post(type) end end end end end mustermann-1.1.1/mustermann-contrib/lib/mustermann/visualizer/highlighter/dummy.rb000066400000000000000000000010731360365612700306560ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann module Visualizer # @!visibility private module Highlighter # Provides highlighting for patterns that don't have a highlighter. # @!visibility private module Dummy # @!visibility private def self.highlight(pattern, renderer) output = String.new output << renderer.pre(:root) << renderer.pre(:unknown) output << renderer.escape(pattern.to_s) output << renderer.post(:unknown) << renderer.post(:root) end end end end end mustermann-1.1.1/mustermann-contrib/lib/mustermann/visualizer/highlighter/regular.rb000066400000000000000000000065451360365612700311750ustar00rootroot00000000000000# frozen_string_literal: true require 'strscan' module Mustermann module Visualizer # @!visibility private module Highlighter # Provides highlighting for {Mustermann::Regular} # @!visibility private class Regular # @!visibility private SPECIAL_ESCAPE = ['w', 'W', 'd', 'D', 'h', 'H', 's', 'S', 'G', 'b', 'B'] private_constant(:SPECIAL_ESCAPE) # @!visibility private def self.highlight?(pattern) pattern.class.name == "Mustermann::Regular" end # @!visibility private def self.highlight(pattern, renderer) new(renderer).highlight(pattern) end # @!visibility private attr_reader :renderer, :output, :scanner # @!visibility private def initialize(renderer) @renderer = renderer @output = String.new end # @!visibility private def highlight(pattern) output << renderer.pre(:root) @scanner = ::StringScanner.new(pattern.to_s) scan output << renderer.post(:root) end # @!visibility private def scan(stop = nil) until scanner.eos? case char = scanner.getch when stop then return char when ?/ then element(:separator, char) when Regexp.escape(char) then element(:char, char) when ?\\ then escaped(scanner.getch) when ?( then potential_capture when ?[ then char_class when ?^, ?$ then element(:illegal, char) when ?{ then element(:special, "\{#{scanner.scan(/[^\}]*\}/)}") else element(:special, char) end end end # @!visibility private def char_class if result = scanner.scan(/\[:\w+:\]\]/) element(:special, "[#{result}") else element(:special, ?[) element(:special, ?^) if scanner.scan(/\^/) end end # @!visibility private def potential_capture if scanner.scan(/\?<(\w+)>/) element(:capture, "(?<") do element(:name, scanner[1]) output << ">" << scan(?)) end elsif scanner.scan(/\?(?:(?:-\w+)?:|>|<=|/)}") when 'p', 'u' then element(:special, "\\#{char}#{scanner.scan(/\{[^\}]*\}/)}") when ?/ then element(:separator, char) else element(:escaped, ?\\) { element(:escaped_char, char) } end end # @!visibility private def element(type, content = nil) output << renderer.pre(type) output << renderer.escape(content) if content yield if block_given? output << renderer.post(type) end end end end end mustermann-1.1.1/mustermann-contrib/lib/mustermann/visualizer/pattern_extension.rb000066400000000000000000000046151360365612700310030ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann module Visualizer # Mixin that will be added to {Mustermann::Pattern}. module PatternExtension prepend_features Composite prepend_features Pattern # @example # puts Mustermann.new("/:page").to_ansi # # @return [String] ANSI colorized version of the pattern. def to_ansi(inspect: nil, **theme) Visualizer.highlight(self, **theme).to_ansi(inspect: inspect) end # @example # puts Mustermann.new("/:page").to_html # # @return [String] HTML version of the pattern. def to_html(inspect: nil, tag: :span, class_prefix: "mustermann_", css: :inline, **theme) Visualizer.highlight(self, **theme).to_html(inspect: inspect, tag: tag, class_prefix: class_prefix, css: css) end # @example # puts Mustermann.new("/:page").to_tree # # @return [String] tree version of the pattern. def to_tree Visualizer.tree(self).to_s end # If invoked directly by puts: ANSI colorized version of the pattern. # If invoked by anything else: String version of the pattern. # # @example # require 'mustermann/visualizer' # pattern = Mustermann.new('/:page') # puts pattern # will have color # puts pattern.to_s # will not have color # # @return [String] non-colorized or colorized version of the pattern def to_s caller_locations.first.label == 'puts' ? to_ansi : super end # If invoked directly by IRB, same as {#color_inspect}, otherwise same as {Mustermann::Pattern#inspect}. def inspect caller_locations.first.base_label == '' ? color_inspect : super end # @return [String] ANSI colorized version of {Mustermann::Pattern#inspect} def color_inspect(base_color = nil, **theme) base_color ||= Highlight::DEFAULT_THEME[:base01] template = is_a?(Composite) ? "*#<%p:(*%s*)>*" : "*#<%p:*%s*>*" Hansi.render(template, self.class, to_ansi(inspect: true, **theme), {"*" => base_color}) end # If invoked directly by IRB, same as {#color_inspect}, otherwise same as Object#pretty_print. def pretty_print(q) if q.class.name.to_s[/[^:]+$/] == "ColorPrinter" q.text(color_inspect, inspect.length) else super end end end end end mustermann-1.1.1/mustermann-contrib/lib/mustermann/visualizer/renderer/000077500000000000000000000000001360365612700265055ustar00rootroot00000000000000mustermann-1.1.1/mustermann-contrib/lib/mustermann/visualizer/renderer/ansi.rb000066400000000000000000000011511360365612700277620ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann module Visualizer # @!visibility private module Renderer # Generates ANSI colored strings. # @!visibility private class ANSI # @!visibility private def initialize(target, mode: Hansi.mode, **options) @target = target @mode = mode @options = options end # @!visibility private def render template = @target.to_hansi_template(**@options) Hansi.render(template, tags: true, theme: @target.theme, mode: @mode) end end end end end mustermann-1.1.1/mustermann-contrib/lib/mustermann/visualizer/renderer/generic.rb000066400000000000000000000022761360365612700304550ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann module Visualizer # @!visibility private module Renderer # Logic shared by most renderers. class Generic # @!visibility private def initialize(target, inspect: nil, add_qoutes: true) @target = target @inspect = inspect @add_qoutes = !target.pattern.is_a?(Mustermann::Composite) end # @!visibility private def render quote = "#{pre(:quote)}#{escape_string(?")}#{post(:quote)}" if @inspect and @add_qoutes pre(:pattern).to_s + preamble.to_s + quote.to_s + @target.render(self) + quote.to_s + post(:pattern).to_s end # @!visibility private def preamble end # @!visibility private def escape(value, inspect_value = value.to_s.inspect[1..-2]) escape_string(@inspect ? inspect_value : value.to_s) end # @!visibility private def escape_string(string) string end # @!visibility private def pre(type) "" end # @!visibility private def post(type) "" end end end end end mustermann-1.1.1/mustermann-contrib/lib/mustermann/visualizer/renderer/hansi_template.rb000066400000000000000000000014211360365612700320250ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/visualizer/renderer/generic' module Mustermann module Visualizer # @!visibility private module Renderer # Generates Hansi template string. # @see Mustermann::Visualizer::Renderer::ANSI # @!visibility private class HansiTemplate < Generic # @!visibility private def initialize(*, **) @hansi = Hansi::StringRenderer.new(tags: true) super end # @!visibility private def escape_string(string) @hansi.escape(string) end # @!visibility private def pre(type) "<#{type}>" end # @!visibility private def post(type) "" end end end end end mustermann-1.1.1/mustermann-contrib/lib/mustermann/visualizer/renderer/html.rb000066400000000000000000000027041360365612700300010ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/visualizer/renderer/generic' require 'cgi' module Mustermann module Visualizer # @!visibility private module Renderer # Generates HTML output. # @!visibility private class HTML < Generic # @!visibility private def initialize(target, tag: :span, class_prefix: "mustermann_", css: :inline, **options) raise ArgumentError, 'css option %p not supported, should be true, false or inline' if css != true and css != false and css != :inline super(target, **options) @css, @tag, @class_prefix = css, tag, class_prefix end # @!visibility private def preamble "" % stylesheet if @css == true end # @!visibility private def stylesheet @target.theme.to_css { |name| ".#{@class_prefix}pattern .#{@class_prefix}#{name}" } end # @!visibility private def escape_string(string) CGI.escape_html(string) end # @!visibility private def pre(type) if @css == :inline return "" unless rule = @target.theme[type] "<#{@tag} style=\"#{rule.to_css_rule}\">" else "<#{@tag} class=\"#{@class_prefix}#{type}\">" end end # @!visibility private def post(type) "" end end end end end mustermann-1.1.1/mustermann-contrib/lib/mustermann/visualizer/renderer/sexp.rb000066400000000000000000000016111360365612700300100ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/visualizer/renderer/generic' module Mustermann module Visualizer # @!visibility private module Renderer # Generates a s-expression like string. # @!visibility private class Sexp < Generic # @!visibility private def render @inspect = false super.gsub(/ ?\)( \))*/) { |s| s.gsub(' ', '') }.strip end # @!visibility private def pre(type) "(#{type} " if type != :pattern end # @!visibility private def escape_string(input) inspect = input.inspect input = inspect if inspect != "\"#{input}\"" input = inspect if input =~ /[\s\"\'\(\)]/ input + " " end # @!visibility private def post(type) ") " if type != :pattern end end end end end mustermann-1.1.1/mustermann-contrib/lib/mustermann/visualizer/tree.rb000066400000000000000000000035411360365612700261660ustar00rootroot00000000000000# frozen_string_literal: true require 'hansi' module Mustermann module Visualizer # Represents a (sub)tree and at the same time a node in the tree. class Tree # @!visibility private attr_reader :line, :children, :prefix_color, :before, :after # @!visibility private def initialize(line, *children, prefix_color: :default, before: "", after: "") @line = line @children = children @prefix_color = prefix_color @before = before @after = after end # used for positioning {#after} # @!visibility private def line_widths(offset = 0) child_widths = children.flat_map { |c| c.line_widths(offset + 2) } width = length(line + before) + offset [width, *child_widths] end # Renders the tree. # @return [String] rendered version of the tree def to_s render("", "", line_widths.max) end # Renders tree, including nesting. # @!visibility private def render(first_prefix, prefix, width) output = before + Hansi.render(prefix_color, first_prefix) + line output = ljust(output, width) + " " + after + "\n" children[0..-2].each { |child| output += child.render(prefix + "├ ", prefix + "│ ", width) } output += children.last.render(prefix + "└ ", prefix + " ", width) if children.last output end # @!visibility private def length(string) deansi(string).length end # @!visibility private def deansi(string) string.gsub(/\e\[[^m]+m/, '') end # @!visibility private def ljust(string, width) missing = width - length(string) append = missing > 0 ? " " * missing : "" string + append end private :ljust, :deansi, :length end end end mustermann-1.1.1/mustermann-contrib/lib/mustermann/visualizer/tree_renderer.rb000066400000000000000000000053771360365612700300650ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/visualizer/tree' require 'mustermann/ast/translator' require 'hansi' module Mustermann module Visualizer # Turns an AST into a Tree # @!visibility private class TreeRenderer < AST::Translator TEMPLATE = '"%s%s%s" ' THEME = Hansi::Theme[:solarized] PREFIX_COLOR = THEME[:violet] FakeNode = Struct.new(:type, :start, :stop, :length) private_constant(:TEMPLATE, :THEME, :PREFIX_COLOR, :FakeNode) # Takes a pattern (or pattern string and option) and turns it into a tree. # Runs translation if pattern implements to_ast, otherwise returns single # node tree. # # @!visibility private def self.render(pattern, **options) pattern &&= Mustermann.new(pattern, **options) renderer = new(pattern.to_s) if pattern.respond_to? :to_ast renderer.translate(pattern.to_ast) else length = renderer.string.length node = FakeNode.new("pattern (not AST based)", 0, length, length) renderer.tree(node) end end # @!visibility private attr_reader :string # @!visibility private def initialize(string) @string = string end # access a substring of the pattern, in inspect mode # @!visibility private def sub(*args) string[*args].inspect[1..-2] end # creates a tree node # @!visibility private def tree(node, *children, **typed_children) children += children_for(typed_children) children = children.flatten.grep(Tree) infos = sub(0, node.start), sub(node.start, node.length), sub(node.stop..-1) description = Hansi.render(THEME[:green], node.type.to_s.tr("_", " ")) after = Hansi.render(TEMPLATE, *infos, theme: THEME, tags: true) Tree.new(description, *children, after: after, prefix_color: PREFIX_COLOR) end # Take a hash with trees as values and turn the keys into trees, too. # Read again if that didn't make sense. # @!visibility private def children_for(list) list.map do |key, value| value = Array(value).flatten if value.any? after = " " * string.inspect.length + " " description = Hansi.render(THEME[:orange], key.to_s) Tree.new(description, *value, after: after, prefix_color: PREFIX_COLOR) end end end translate(:node) { t.tree(node, payload: t(payload)) } translate(:with_look_ahead) { t.tree(node, head: t(head), payload: t(payload)) } translate(Array) { map { |e| t(e) }} translate(Object) { } end end end mustermann-1.1.1/mustermann-contrib/mustermann-contrib.gemspec000066400000000000000000000016571360365612700247500ustar00rootroot00000000000000$:.unshift File.expand_path("../../mustermann/lib", __FILE__) require "mustermann/version" Gem::Specification.new do |s| s.name = "mustermann-contrib" s.version = Mustermann::VERSION s.authors = ["Konstantin Haase", "Zachary Scott"] s.email = "sinatrarb@googlegroups.com" s.homepage = "https://github.com/sinatra/mustermann" s.summary = %q{Collection of extensions for Mustermann} s.description = %q{Adds many plugins to Mustermann} s.license = 'MIT' s.required_ruby_version = '>= 2.2.0' s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } s.add_dependency 'mustermann', Mustermann::VERSION s.add_dependency 'hansi', '~> 0.2.0' end mustermann-1.1.1/mustermann-contrib/spec/000077500000000000000000000000001360365612700204755ustar00rootroot00000000000000mustermann-1.1.1/mustermann-contrib/spec/cake_spec.rb000066400000000000000000000052351360365612700227440ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/cake' describe Mustermann::Cake do extend Support::Pattern pattern '' do it { should match('') } it { should_not match('/') } it { should expand.to('') } it { should_not expand(a: 1) } it { should generate_template('') } it { should respond_to(:expand) } it { should respond_to(:to_templates) } end pattern '/' do it { should match('/') } it { should_not match('/foo') } it { should expand.to('/') } it { should_not expand(a: 1) } end pattern '/foo' do it { should match('/foo') } it { should_not match('/bar') } it { should_not match('/foo.bar') } it { should expand.to('/foo') } it { should_not expand(a: 1) } end pattern '/foo/bar' do it { should match('/foo/bar') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } it { should expand.to('/foo/bar') } it { should_not expand(a: 1) } end pattern '/:foo' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo.bar') .capturing foo: 'foo.bar' } it { should match('/%0Afoo') .capturing foo: '%0Afoo' } it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } example { pattern.params('/foo') .should be == {"foo" => "foo"} } example { pattern.params('/f%20o') .should be == {"foo" => "f o"} } example { pattern.params('').should be_nil } it { should expand(foo: 'bar') .to('/bar') } it { should expand(foo: 'b r') .to('/b%20r') } it { should expand(foo: 'foo/bar') .to('/foo%2Fbar') } it { should_not expand(foo: 'foo', bar: 'bar') } it { should_not expand(bar: 'bar') } it { should_not expand } it { should generate_template('/{foo}') } end pattern '/*' do it { should match('/') } it { should match('/foo') } it { should match('/foo/bar') } example { pattern.params('/foo/bar') .should be == {"splat" => ["foo", "bar"]}} it { should generate_template('/{+splat}') } end pattern '/**' do it { should match('/') .capturing splat: '' } it { should match('/foo') .capturing splat: 'foo' } it { should match('/foo/bar') .capturing splat: 'foo/bar' } example { pattern.params('/foo/bar') .should be == {"splat" => ["foo/bar"]} } it { should generate_template('/{+splat}') } end end mustermann-1.1.1/mustermann-contrib/spec/express_spec.rb000066400000000000000000000157651360365612700235430ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/express' describe Mustermann::Express do extend Support::Pattern pattern '' do it { should match('') } it { should_not match('/') } it { should expand.to('') } it { should_not expand(a: 1) } it { should generate_template('') } it { should respond_to(:expand) } it { should respond_to(:to_templates) } end pattern '/' do it { should match('/') } it { should_not match('/foo') } it { should expand.to('/') } it { should_not expand(a: 1) } end pattern '/foo' do it { should match('/foo') } it { should_not match('/bar') } it { should_not match('/foo.bar') } it { should expand.to('/foo') } it { should_not expand(a: 1) } end pattern '/foo/bar' do it { should match('/foo/bar') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } it { should expand.to('/foo/bar') } it { should_not expand(a: 1) } end pattern '/:foo' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo.bar') .capturing foo: 'foo.bar' } it { should match('/%0Afoo') .capturing foo: '%0Afoo' } it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } example { pattern.params('/foo') .should be == {"foo" => "foo"} } example { pattern.params('/f%20o') .should be == {"foo" => "f o"} } example { pattern.params('').should be_nil } it { should expand(foo: 'bar') .to('/bar') } it { should expand(foo: 'b r') .to('/b%20r') } it { should expand(foo: 'foo/bar') .to('/foo%2Fbar') } it { should_not expand(foo: 'foo', bar: 'bar') } it { should_not expand(bar: 'bar') } it { should_not expand } it { should generate_template('/{foo}') } end pattern '/:foo+' do it { should_not match('/') } it { should match('/foo') .capturing foo: 'foo' } it { should match('/foo/bar') .capturing foo: 'foo/bar' } it { should expand .to('/') } it { should expand(foo: nil) .to('/') } it { should expand(foo: '') .to('/') } it { should expand(foo: 'foo') .to('/foo') } it { should expand(foo: 'foo/bar') .to('/foo/bar') } it { should expand(foo: 'foo.bar') .to('/foo.bar') } it { should generate_template('/{+foo}') } end pattern '/:foo?' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo.bar') .capturing foo: 'foo.bar' } it { should match('/%0Afoo') .capturing foo: '%0Afoo' } it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' } it { should match('/') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/foo/') } example { pattern.params('/foo') .should be == {"foo" => "foo"} } example { pattern.params('/f%20o') .should be == {"foo" => "f o"} } example { pattern.params('/') .should be == {"foo" => nil } } it { should expand(foo: 'bar') .to('/bar') } it { should expand(foo: 'b r') .to('/b%20r') } it { should expand(foo: 'foo/bar') .to('/foo%2Fbar') } it { should expand .to('/') } it { should_not expand(foo: 'foo', bar: 'bar') } it { should_not expand(bar: 'bar') } it { should generate_template('/{foo}') } it { should generate_template('/') } end pattern '/:foo*' do it { should match('/') .capturing foo: '' } it { should match('/foo') .capturing foo: 'foo' } it { should match('/foo/bar') .capturing foo: 'foo/bar' } it { should expand .to('/') } it { should expand(foo: nil) .to('/') } it { should expand(foo: '') .to('/') } it { should expand(foo: 'foo') .to('/foo') } it { should expand(foo: 'foo/bar') .to('/foo/bar') } it { should expand(foo: 'foo.bar') .to('/foo.bar') } it { should generate_template('/{+foo}') } end pattern '/:foo(.*)' do it { should match('/') .capturing foo: '' } it { should match('/foo') .capturing foo: 'foo' } it { should match('/foo/bar') .capturing foo: 'foo/bar' } it { should expand(foo: '') .to('/') } it { should expand(foo: 'foo') .to('/foo') } it { should expand(foo: 'foo/bar') .to('/foo/bar') } it { should expand(foo: 'foo.bar') .to('/foo.bar') } it { should generate_template('/{foo}') } end pattern '/:foo(\d+)' do it { should_not match('/') } it { should_not match('/foo') } it { should match('/15') .capturing foo: '15' } it { should generate_template('/{foo}') } end pattern '/:foo(\d+|bar)' do it { should_not match('/') } it { should_not match('/foo') } it { should match('/15') .capturing foo: '15' } it { should match('/bar') .capturing foo: 'bar' } it { should generate_template('/{foo}') } end pattern '/:foo(\))' do it { should_not match('/') } it { should_not match('/foo') } it { should match('/)').capturing foo: ')' } it { should generate_template('/{foo}') } end pattern '/:foo(prefix(\d+|bar))' do it { should_not match('/prefix') } it { should_not match('/prefixfoo') } it { should match('/prefix15') .capturing foo: 'prefix15' } it { should match('/prefixbar') .capturing foo: 'prefixbar' } it { should generate_template('/{foo}') } end pattern '/(.+)' do it { should_not match('/') } it { should match('/foo') .capturing splat: 'foo' } it { should match('/foo/bar') .capturing splat: 'foo/bar' } it { should generate_template('/{+splat}') } end pattern '/(foo(a|b))' do it { should_not match('/') } it { should match('/fooa') .capturing splat: 'fooa' } it { should match('/foob') .capturing splat: 'foob' } it { should generate_template('/{+splat}') } end context 'invalid syntax' do example 'unexpected closing parenthesis' do expect { Mustermann::Express.new('foo)bar') }. to raise_error(Mustermann::ParseError, 'unexpected ) while parsing "foo)bar"') end example 'missing closing parenthesis' do expect { Mustermann::Express.new('foo(bar') }. to raise_error(Mustermann::ParseError, 'unexpected end of string while parsing "foo(bar"') end example 'unexpected ?' do expect { Mustermann::Express.new('foo?bar') }. to raise_error(Mustermann::ParseError, 'unexpected ? while parsing "foo?bar"') end example 'unexpected *' do expect { Mustermann::Express.new('foo*bar') }. to raise_error(Mustermann::ParseError, 'unexpected * while parsing "foo*bar"') end end end mustermann-1.1.1/mustermann-contrib/spec/file_utils_spec.rb000066400000000000000000000064671360365612700242100ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/file_utils' describe Mustermann::FileUtils do subject(:utils) { Mustermann::FileUtils } include FileUtils before do @pwd = Dir.pwd @tmp_dir = File.join(__dir__, 'tmp') mkdir_p(@tmp_dir) chdir(@tmp_dir) touch("foo.txt") touch("foo.rb") touch("bar.txt") touch("bar.rb") end after do chdir(@pwd) if @pwd rm_rf(@tmp_dir) if @tmp_dir end describe :glob_pattern do example { utils.glob_pattern('/:foo') .should be == '/*' } example { utils.glob_pattern('/*foo') .should be == '/**/*' } example { utils.glob_pattern('/(ab|c)?/:foo/d/*bar') .should be == '/{{ab,c},}/*/d/**/*' } example { utils.glob_pattern('/a', '/b') .should be == '{/a,/b}' } example { utils.glob_pattern('**/*', type: :shell) .should be == '**/*' } example { utils.glob_pattern(/can't parse this/) .should be == '**/*' } example { utils.glob_pattern('/foo', type: :identity) .should be == '/foo' } example { utils.glob_pattern('/fo*', type: :identity) .should be == '/fo\\*' } end describe :glob do example do utils.glob(":name.txt").sort.should be == ['bar.txt', 'foo.txt'] end example do extensions = [] utils.glob("foo.:ext") { |file, params| extensions << params['ext'] } extensions.sort.should be == ['rb', 'txt'] end example do utils.glob(":name.:ext", capture: { ext: 'rb', name: 'foo' }).should be == ['foo.rb'] end end describe :glob_map do example do utils.glob_map({':name.rb' => ':name/init.rb'}).should be == { "bar.rb" => "bar/init.rb", "foo.rb" => "foo/init.rb" } end example do result = {} returned = utils.glob_map({':name.rb' => ':name/init.rb'}) { |k, v| result[v] = k.upcase } returned.sort .should be == ["BAR.RB", "FOO.RB"] result["bar/init.rb"] .should be == "BAR.RB" end end describe :cp do example do utils.cp({':name.rb' => ':name.ruby', ':name.txt' => ':name.md'}) File.should be_exist('foo.ruby') File.should be_exist('bar.md') File.should be_exist('bar.txt') end end describe :cp_r do example do mkdir_p "foo/bar" utils.cp_r({'foo/:name' => :name}) File.should be_directory('bar') end end describe :mv do example do utils.mv({':name.rb' => ':name.ruby', ':name.txt' => ':name.md'}) File.should be_exist('foo.ruby') File.should be_exist('bar.md') File.should_not be_exist('bar.txt') end end describe :ln do example do utils.ln({':name.rb' => ':name.ruby', ':name.txt' => ':name.md'}) File.should be_exist('foo.ruby') File.should be_exist('bar.md') File.should be_exist('bar.txt') end end describe :ln_s do example do utils.ln_s({':name.rb' => ':name.ruby', ':name.txt' => ':name.md'}) File.should be_symlink('foo.ruby') File.should be_symlink('bar.md') File.should be_exist('bar.txt') end end describe :ln_sf do example do utils.ln_sf({':name.rb' => ':name.txt'}) File.should be_symlink('foo.txt') end end end mustermann-1.1.1/mustermann-contrib/spec/flask_spec.rb000066400000000000000000000303411360365612700231350ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/flask' describe Mustermann::Flask do extend Support::Pattern pattern '' do it { should match('') } it { should_not match('/') } it { should expand.to('') } it { should_not expand(a: 1) } it { should generate_template('') } it { should respond_to(:expand) } it { should respond_to(:to_templates) } end pattern '/' do it { should match('/') } it { should_not match('/foo') } it { should expand.to('/') } it { should_not expand(a: 1) } end pattern '/foo' do it { should match('/foo') } it { should_not match('/bar') } it { should_not match('/foo.bar') } it { should expand.to('/foo') } it { should_not expand(a: 1) } end pattern '/foo/bar' do it { should match('/foo/bar') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } it { should expand.to('/foo/bar') } it { should_not expand(a: 1) } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo.bar') .capturing foo: 'foo.bar' } it { should match('/%0Afoo') .capturing foo: '%0Afoo' } it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } example { pattern.params('/foo') .should be == {"foo" => "foo"} } example { pattern.params('/f%20o') .should be == {"foo" => "f o"} } example { pattern.params('').should be_nil } it { should expand(foo: 'bar') .to('/bar') } it { should expand(foo: 'b r') .to('/b%20r') } it { should expand(foo: 'foo/bar') .to('/foo%2Fbar') } it { should_not expand(foo: 'foo', bar: 'bar') } it { should_not expand(bar: 'bar') } it { should_not expand } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo.bar') .capturing foo: 'foo.bar' } it { should match('/%0Afoo') .capturing foo: '%0Afoo' } it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } example { pattern.params('/foo') .should be == {"foo" => "foo"} } example { pattern.params('/f%20o') .should be == {"foo" => "f o"} } example { pattern.params('').should be_nil } it { should expand(foo: 'bar') .to('/bar') } it { should expand(foo: 'b r') .to('/b%20r') } it { should expand(foo: 'foo/bar') .to('/foo%2Fbar') } it { should_not expand(foo: 'foo', bar: 'bar') } it { should_not expand(bar: 'bar') } it { should_not expand } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo.bar') .capturing foo: 'foo.bar' } it { should match('/%0Afoo') .capturing foo: '%0Afoo' } it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' } it { should_not match('/f') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/f') .capturing foo: 'f' } it { should match('/fo') .capturing foo: 'fo' } it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should_not match('/fooo') } it { should_not match('/foo.bar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should_not match('/f') } it { should_not match('/fo') } it { should_not match('/fooo') } it { should_not match('/foo.bar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/42').capturing foo: '42' } it { should_not match('/1.0') } it { should_not match('/.5') } it { should_not match('/foo') } it { should_not match('/bar') } it { should_not match('/foo.bar') } it { should_not match('/%0Afoo') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } example { pattern.params('/42').should be == {"foo" => 42} } it { should expand(foo: 12).to('/12') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/42').capturing foo: '42' } it { should_not match('/1.0') } it { should_not match('/.5') } it { should_not match('/foo') } it { should_not match('/bar') } it { should_not match('/foo.bar') } it { should_not match('/%0Afoo') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } example { pattern.params('/42').should be == {"foo" => 42} } it { should expand(foo: 12).to('/12') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should_not match('/f') } it { should_not match('/fo') } it { should_not match('/fooo') } it { should_not match('/foo.bar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should_not match('/baz') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should_not match('/f') } it { should_not match('/fo') } it { should_not match('/fooo') } it { should_not match('/foo.bar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should_not match('/baz') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo,bar') .capturing foo: 'foo,bar' } it { should_not match('/f') } it { should_not match('/fo') } it { should_not match('/fooo') } it { should_not match('/foo.bar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should_not match('/baz') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo,bar') .capturing foo: 'foo,bar' } it { should_not match('/f') } it { should_not match('/fo') } it { should_not match('/fooo') } it { should_not match('/foo.bar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should_not match('/baz') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo,bar') .capturing foo: 'foo,bar' } it { should_not match('/f') } it { should_not match('/fo') } it { should_not match('/fooo') } it { should_not match('/foo.bar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should_not match('/baz') } it { should generate_template('/{foo}') } end pattern '/' do example { pattern.params('/42').should be == {"foo" => 42} } example { pattern.params('/52').should be == {"foo" => 50} } example { pattern.params('/2').should be == {"foo" => 5} } end pattern '/' do example { pattern.params('/42.5').should be == {"foo" => 42.5} } example { pattern.params('/52.5').should be == {"foo" => 50.5} } example { pattern.params('/2.5').should be == {"foo" => 5.0} } end pattern '///' do it { should match('/foo/42/42') .capturing foo: '42', bar: '42' } it { should match('/foo/1.0/1') .capturing foo: '1.0', bar: '1' } it { should match('/foo/.5/0') .capturing foo: '.5', bar: '0' } it { should_not match('/foo/1/1.0') } it { should_not match('/foo/1.0/1.0') } it { should generate_template('/{prefix}/{foo}/{bar}') } example do pattern.params('/foo/1.0/1').should be == { "prefix" => "foo", "foo" => 1.0, "bar" => 1 } end end pattern '/' do it { should match('/') .capturing foo: '' } it { should match('/foo') .capturing foo: 'foo' } it { should match('/foo/bar') .capturing foo: 'foo/bar' } it { should expand .to('/') } it { should expand(foo: nil) .to('/') } it { should expand(foo: '') .to('/') } it { should expand(foo: 'foo') .to('/foo') } it { should expand(foo: 'foo/bar') .to('/foo/bar') } it { should expand(foo: 'foo.bar') .to('/foo.bar') } it { should generate_template('/{+foo}') } end converter = Struct.new(:convert).new(:upcase.to_proc) pattern '/', converters: { foo: converter } do it { should match('/foo').capturing bar: 'foo' } example { pattern.params('/foo').should be == {"bar" => "FOO"} } end context 'invalid syntax' do example 'unexpected end of capture' do expect { Mustermann::Flask.new('foo>bar') }. to raise_error(Mustermann::ParseError, 'unexpected > while parsing "foo>bar"') end example 'missing end of capture' do expect { Mustermann::Flask.new('foo') }. to raise_error(Mustermann::ParseError, 'unexpected converter "bar" while parsing "foo"') end example 'broken argument synax' do expect { Mustermann::Flask.new('') }. to raise_error(Mustermann::ParseError, 'unexpected = while parsing ""') end example 'missing )' do expect { Mustermann::Flask.new('' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo.bar') .capturing foo: 'foo.bar' } it { should match('/%0Afoo') .capturing foo: '%0Afoo' } it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } example { pattern.params('/foo') .should be == {"foo" => "foo"} } example { pattern.params('/f%20o') .should be == {"foo" => "f o"} } example { pattern.params('').should be_nil } it { should expand(foo: 'bar') .to('/bar') } it { should expand(foo: 'b r') .to('/b%20r') } it { should expand(foo: 'foo/bar') .to('/foo%2Fbar') } it { should_not expand(foo: 'foo', bar: 'bar') } it { should_not expand(bar: 'bar') } it { should_not expand } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo.bar') .capturing foo: 'foo.bar' } it { should match('/%0Afoo') .capturing foo: '%0Afoo' } it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } example { pattern.params('/foo') .should be == {"foo" => "foo"} } example { pattern.params('/f%20o') .should be == {"foo" => "f o"} } example { pattern.params('').should be_nil } it { should expand(foo: 'bar') .to('/bar') } it { should expand(foo: 'b r') .to('/b%20r') } it { should expand(foo: 'foo/bar') .to('/foo%2Fbar') } it { should_not expand(foo: 'foo', bar: 'bar') } it { should_not expand(bar: 'bar') } it { should_not expand } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo.bar') .capturing foo: 'foo.bar' } it { should match('/%0Afoo') .capturing foo: '%0Afoo' } it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' } it { should_not match('/f') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/f') .capturing foo: 'f' } it { should match('/fo') .capturing foo: 'fo' } it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should_not match('/fooo') } it { should_not match('/foo.bar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should_not match('/f') } it { should_not match('/fo') } it { should_not match('/fooo') } it { should_not match('/foo.bar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/42').capturing foo: '42' } it { should_not match('/1.0') } it { should_not match('/.5') } it { should_not match('/foo') } it { should_not match('/bar') } it { should_not match('/foo.bar') } it { should_not match('/%0Afoo') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } example { pattern.params('/42').should be == {"foo" => 42} } it { should expand(foo: 12).to('/12') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/42').capturing foo: '42' } it { should_not match('/1.0') } it { should_not match('/.5') } it { should_not match('/foo') } it { should_not match('/bar') } it { should_not match('/foo.bar') } it { should_not match('/%0Afoo') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } example { pattern.params('/42').should be == {"foo" => 42} } it { should expand(foo: 12).to('/12') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should_not match('/f') } it { should_not match('/fo') } it { should_not match('/fooo') } it { should_not match('/foo.bar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should_not match('/baz') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should_not match('/f') } it { should_not match('/fo') } it { should_not match('/fooo') } it { should_not match('/foo.bar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should_not match('/baz') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo,bar') .capturing foo: 'foo,bar' } it { should_not match('/f') } it { should_not match('/fo') } it { should_not match('/fooo') } it { should_not match('/foo.bar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should_not match('/baz') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo,bar') .capturing foo: 'foo,bar' } it { should_not match('/f') } it { should_not match('/fo') } it { should_not match('/fooo') } it { should_not match('/foo.bar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should_not match('/baz') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo,bar') .capturing foo: 'foo,bar' } it { should_not match('/f') } it { should_not match('/fo') } it { should_not match('/fooo') } it { should_not match('/foo.bar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should_not match('/baz') } it { should generate_template('/{foo}') } end pattern '/' do example { pattern.params('/42').should be == {"foo" => 42} } example { pattern.params('/52').should be == {"foo" => 50} } example { pattern.params('/2').should be == {"foo" => 5} } end pattern '/' do example { pattern.params('/42.5').should be == {"foo" => 42.5} } example { pattern.params('/52.5').should be == {"foo" => 50.5} } example { pattern.params('/2.5').should be == {"foo" => 5.0} } end pattern '///' do it { should match('/foo/42/42') .capturing foo: '42', bar: '42' } it { should match('/foo/1.0/1') .capturing foo: '1.0', bar: '1' } it { should match('/foo/.5/0') .capturing foo: '.5', bar: '0' } it { should_not match('/foo/1/1.0') } it { should_not match('/foo/1.0/1.0') } it { should generate_template('/{prefix}/{foo}/{bar}') } example do pattern.params('/foo/1.0/1').should be == { "prefix" => "foo", "foo" => 1.0, "bar" => 1 } end end pattern '/' do it { should match('/') .capturing foo: '' } it { should match('/foo') .capturing foo: 'foo' } it { should match('/foo/bar') .capturing foo: 'foo/bar' } it { should expand .to('/') } it { should expand(foo: nil) .to('/') } it { should expand(foo: '') .to('/') } it { should expand(foo: 'foo') .to('/foo') } it { should expand(foo: 'foo/bar') .to('/foo/bar') } it { should expand(foo: 'foo.bar') .to('/foo.bar') } it { should generate_template('/{+foo}') } end pattern '/' do it { should match('/foo').capturing bar: 'foo' } it { should generate_template('/{bar}') } example do expect { Mustermann::Flask.new('/') }.to \ raise_error(Mustermann::ParseError, 'unexpected converter "foo" while parsing "/"') end end context 'invalid syntax' do example 'unexpected end of capture' do expect { FlaskSubclass.new('foo>bar') }. to raise_error(Mustermann::ParseError, 'unexpected > while parsing "foo>bar"') end example 'missing end of capture' do expect { FlaskSubclass.new('foo') }. to raise_error(Mustermann::ParseError, 'unexpected converter "bar" while parsing "foo"') end example 'broken argument synax' do expect { FlaskSubclass.new('') }. to raise_error(Mustermann::ParseError, 'unexpected = while parsing ""') end example 'missing )' do expect { FlaskSubclass.new('/:name' end specify :to_tree do pattern.to_tree.should be == Mustermann::Visualizer.tree(pattern).to_s end specify :color_inspect do pattern.color_inspect.should include(pattern.to_ansi(inspect: true)) pattern.color_inspect.should include("# "foo"} } example { pattern.params('/f%20o') .should be == {"foo" => "f o"} } example { pattern.params('').should be_nil } it { should expand(foo: 'bar') .to('/bar') } it { should expand(foo: 'b r') .to('/b%20r') } it { should expand(foo: 'foo/bar') .to('/foo%2Fbar') } it { should_not expand(foo: 'foo', bar: 'bar') } it { should_not expand(bar: 'bar') } it { should_not expand } it { should generate_template('/{foo}') } end pattern '/*foo' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/foo/bar') .capturing foo: 'foo/bar' } it { should expand .to('/') } it { should expand(foo: nil) .to('/') } it { should expand(foo: '') .to('/') } it { should expand(foo: 'foo') .to('/foo') } it { should expand(foo: 'foo/bar') .to('/foo/bar') } it { should expand(foo: 'foo.bar') .to('/foo.bar') } example { pattern.params("/foo/bar").should be == {"foo" => ["foo", "bar"]}} it { should generate_template('/{+foo}') } end pattern '/{foo:.*}' do it { should match('/') .capturing foo: '' } it { should match('/foo') .capturing foo: 'foo' } it { should match('/foo/bar') .capturing foo: 'foo/bar' } it { should expand(foo: '') .to('/') } it { should expand(foo: 'foo') .to('/foo') } it { should expand(foo: 'foo/bar') .to('/foo/bar') } it { should expand(foo: 'foo.bar') .to('/foo.bar') } example { pattern.params("/foo/bar").should be == {"foo" => "foo/bar"}} it { should generate_template('/{foo}') } end end mustermann-1.1.1/mustermann-contrib/spec/rails_spec.rb000066400000000000000000000572051360365612700231570ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/rails' describe Mustermann::Rails do extend Support::Pattern pattern '' do it { should match('') } it { should_not match('/') } it { should expand.to('') } it { should_not expand(a: 1) } it { should generate_template('') } it { should respond_to(:expand) } it { should respond_to(:to_templates) } end pattern '/' do it { should match('/') } it { should_not match('/foo') } it { should expand.to('/') } it { should_not expand(a: 1) } end pattern '/foo' do it { should match('/foo') } it { should_not match('/bar') } it { should_not match('/foo.bar') } it { should expand.to('/foo') } it { should_not expand(a: 1) } end pattern '/foo/bar' do it { should match('/foo/bar') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } it { should expand.to('/foo/bar') } it { should_not expand(a: 1) } end pattern '/:foo' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo.bar') .capturing foo: 'foo.bar' } it { should match('/%0Afoo') .capturing foo: '%0Afoo' } it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } example { pattern.params('/foo') .should be == {"foo" => "foo"} } example { pattern.params('/f%20o') .should be == {"foo" => "f o"} } example { pattern.params('').should be_nil } it { should expand(foo: 'bar') .to('/bar') } it { should expand(foo: 'b r') .to('/b%20r') } it { should expand(foo: 'foo/bar') .to('/foo%2Fbar') } it { should_not expand(foo: 'foo', bar: 'bar') } it { should_not expand(bar: 'bar') } it { should_not expand } it { should generate_template('/{foo}') } end pattern '/föö' do it { should match("/f%C3%B6%C3%B6") } it { should expand.to("/f%C3%B6%C3%B6") } it { should_not expand(a: 1) } end pattern "/:foo/:bar" do it { should match('/foo/bar') .capturing foo: 'foo', bar: 'bar' } it { should match('/foo.bar/bar.foo') .capturing foo: 'foo.bar', bar: 'bar.foo' } it { should match('/user@example.com/name') .capturing foo: 'user@example.com', bar: 'name' } it { should match('/10.1/te.st') .capturing foo: '10.1', bar: 'te.st' } it { should match('/10.1.2/te.st') .capturing foo: '10.1.2', bar: 'te.st' } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } example { pattern.params('/bar/foo').should be == {"foo" => "bar", "bar" => "foo"} } example { pattern.params('').should be_nil } it { should expand(foo: 'foo', bar: 'bar').to('/foo/bar') } it { should_not expand(foo: 'foo') } it { should_not expand(bar: 'bar') } it { should generate_template('/{foo}/{bar}') } end pattern '/hello/:person' do it { should match('/hello/Frank').capturing person: 'Frank' } it { should expand(person: 'Frank') .to '/hello/Frank' } it { should expand(person: 'Frank?') .to '/hello/Frank%3F' } it { should generate_template('/hello/{person}') } end pattern '/?:foo?/?:bar?' do it { should match('/?hello?/?world?').capturing foo: 'hello', bar: 'world' } it { should_not match('/hello/world/') } it { should expand(foo: 'hello', bar: 'world').to('/%3Fhello%3F/%3Fworld%3F') } it { should generate_template('/?{foo}?/?{bar}?') } end pattern '/:foo_bar' do it { should match('/hello').capturing foo_bar: 'hello' } it { should expand(foo_bar: 'hello').to('/hello') } it { should generate_template('/{foo_bar}') } end pattern '/*foo' do it { should match('/') .capturing foo: '' } it { should match('/foo') .capturing foo: 'foo' } it { should match('/foo/bar') .capturing foo: 'foo/bar' } it { should expand .to('/') } it { should expand(foo: nil) .to('/') } it { should expand(foo: '') .to('/') } it { should expand(foo: 'foo') .to('/foo') } it { should expand(foo: 'foo/bar') .to('/foo/bar') } it { should expand(foo: 'foo.bar') .to('/foo.bar') } it { should generate_template('/{+foo}') } end pattern '/*splat' do it { should match('/') .capturing splat: '' } it { should match('/foo') .capturing splat: 'foo' } it { should match('/foo/bar') .capturing splat: 'foo/bar' } it { should generate_template('/{+splat}') } end pattern '/:foo/*bar' do it { should match("/foo/bar/baz") .capturing foo: 'foo', bar: 'bar/baz' } it { should match("/foo%2Fbar/baz") .capturing foo: 'foo%2Fbar', bar: 'baz' } it { should match("/foo/") .capturing foo: 'foo', bar: '' } it { should match('/h%20w/h%20a%20y') .capturing foo: 'h%20w', bar: 'h%20a%20y' } it { should_not match('/foo') } it { should expand(foo: 'foo') .to('/foo/') } it { should expand(foo: 'foo', bar: 'bar') .to('/foo/bar') } it { should expand(foo: 'foo', bar: 'foo/bar') .to('/foo/foo/bar') } it { should expand(foo: 'foo/bar', bar: 'bar') .to('/foo%2Fbar/bar') } it { should generate_template('/{foo}/{+bar}') } end pattern '/test$/' do it { should match('/test$/') } it { should expand.to('/test$/') } end pattern '/te+st/' do it { should match('/te+st/') } it { should_not match('/test/') } it { should_not match('/teest/') } it { should expand.to('/te+st/') } end pattern "/path with spaces" do it { should match('/path%20with%20spaces') } it { should match('/path%2Bwith%2Bspaces') } it { should match('/path+with+spaces') } it { should expand.to('/path%20with%20spaces') } it { should generate_template('/path%20with%20spaces') } end pattern '/foo&bar' do it { should match('/foo&bar') } end pattern '/*a/:foo/*b/*c' do it { should match('/bar/foo/bling/baz/boom').capturing a: 'bar', foo: 'foo', b: 'bling', c: 'baz/boom' } example { pattern.params('/bar/foo/bling/baz/boom').should be == { "a" => 'bar', "foo" => 'foo', "b" => 'bling', "c" => 'baz/boom' } } it { should expand(a: 'bar', foo: 'foo', b: 'bling', c: 'baz/boom').to('/bar/foo/bling/baz/boom') } it { should generate_template('/{+a}/{foo}/{+b}/{+c}') } end pattern '/test.bar' do it { should match('/test.bar') } it { should_not match('/test0bar') } end pattern '/:file.:ext' do it { should match('/pony.jpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%2Ejpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%2ejpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%E6%AD%A3%2Ejpg') .capturing file: 'pony%E6%AD%A3', ext: 'jpg' } it { should match('/pony%e6%ad%a3%2ejpg') .capturing file: 'pony%e6%ad%a3', ext: 'jpg' } it { should match('/pony正%2Ejpg') .capturing file: 'pony正', ext: 'jpg' } it { should match('/pony正%2ejpg') .capturing file: 'pony正', ext: 'jpg' } it { should match('/pony正..jpg') .capturing file: 'pony正.', ext: 'jpg' } it { should_not match('/.jpg') } it { should expand(file: 'pony', ext: 'jpg').to('/pony.jpg') } end pattern '/:a(x)' do it { should match('/a') .capturing a: 'a' } it { should match('/xa') .capturing a: 'xa' } it { should match('/axa') .capturing a: 'axa' } it { should match('/ax') .capturing a: 'a' } it { should match('/axax') .capturing a: 'axa' } it { should match('/axaxx') .capturing a: 'axax' } it { should expand(a: 'x').to('/xx') } it { should expand(a: 'a').to('/ax') } it { should generate_template('/{a}x') } it { should generate_template('/{a}') } end pattern '/:user(@:host)' do it { should match('/foo@bar') .capturing user: 'foo', host: 'bar' } it { should match('/foo.foo@bar') .capturing user: 'foo.foo', host: 'bar' } it { should match('/foo@bar.bar') .capturing user: 'foo', host: 'bar.bar' } it { should expand(user: 'foo') .to('/foo') } it { should expand(user: 'foo', host: 'bar') .to('/foo@bar') } it { should generate_template('/{user}') } it { should generate_template('/{user}@{host}') } end pattern '/:file(.:ext)' do it { should match('/pony') .capturing file: 'pony', ext: nil } it { should match('/pony.jpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%2Ejpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%2ejpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony.png.jpg') .capturing file: 'pony.png', ext: 'jpg' } it { should match('/pony.') .capturing file: 'pony.' } it { should_not match('/.jpg') } it { should expand(file: 'pony') .to('/pony') } it { should expand(file: 'pony', ext: 'jpg') .to('/pony.jpg') } it { should generate_template('/{file}') } it { should generate_template('/{file}.{ext}') } end pattern '/:id/test.bar' do it { should match('/3/test.bar') .capturing id: '3' } it { should match('/2/test.bar') .capturing id: '2' } it { should match('/2E/test.bar') .capturing id: '2E' } it { should match('/2e/test.bar') .capturing id: '2e' } it { should match('/%2E/test.bar') .capturing id: '%2E' } end pattern '/10/:id' do it { should match('/10/test') .capturing id: 'test' } it { should match('/10/te.st') .capturing id: 'te.st' } end pattern '/10.1/:id' do it { should match('/10.1/test') .capturing id: 'test' } it { should match('/10.1/te.st') .capturing id: 'te.st' } end pattern '/:foo.:bar/:id' do it { should match('/10.1/te.st') .capturing foo: "10", bar: "1", id: "te.st" } it { should match('/10.1.2/te.st') .capturing foo: "10.1", bar: "2", id: "te.st" } end pattern '/:a/:b(.)(:c)' do it { should match('/a/b') .capturing a: 'a', b: 'b', c: nil } it { should match('/a/b.c') .capturing a: 'a', b: 'b', c: 'c' } it { should match('/a.b/c') .capturing a: 'a.b', b: 'c', c: nil } it { should match('/a.b/c.d') .capturing a: 'a.b', b: 'c', c: 'd' } it { should_not match('/a.b/c.d/e') } it { should expand(a: ?a, b: ?b) .to('/a/b.') } it { should expand(a: ?a, b: ?b, c: ?c) .to('/a/b.c') } it { should generate_template('/{a}/{b}') } it { should generate_template('/{a}/{b}.') } it { should generate_template('/{a}/{b}.{c}') } end pattern '/:a(foo:b)' do it { should match('/barfoobar') .capturing a: 'bar', b: 'bar' } it { should match('/barfoobarfoobar') .capturing a: 'barfoobar', b: 'bar' } it { should match('/bar') .capturing a: 'bar', b: nil } it { should_not match('/') } it { should expand(a: ?a) .to('/a') } it { should expand(a: ?a, b: ?b) .to('/afoob') } it { should generate_template('/{a}foo{b}') } it { should generate_template('/{a}') } it { should_not generate_template('/{a}foo') } end pattern '/fo(o)' do it { should match('/fo') } it { should match('/foo') } it { should_not match('') } it { should_not match('/') } it { should_not match('/f') } it { should_not match('/fooo') } it { should expand.to('/foo') } end pattern '/foo?' do it { should match('/foo?') } it { should_not match('/foo\?') } it { should_not match('/fo') } it { should_not match('/foo') } it { should_not match('') } it { should_not match('/') } it { should_not match('/f') } it { should_not match('/fooo') } it { should expand.to('/foo%3F') } end pattern '/:fOO' do it { should match('/a').capturing fOO: 'a' } end pattern '/:_X' do it { should match('/a').capturing _X: 'a' } end pattern '/:f00' do it { should match('/a').capturing f00: 'a' } end pattern '/:foo(/:bar)/:baz' do it { should match('/foo/bar/baz').capturing foo: 'foo', bar: 'bar', baz: 'baz' } it { should expand(foo: ?a, baz: ?b) .to('/a/b') } it { should expand(foo: ?a, baz: ?b, bar: ?x) .to('/a/x/b') } end pattern '/:foo', capture: /\d+/ do it { should match('/1') .capturing foo: '1' } it { should match('/123') .capturing foo: '123' } it { should_not match('/') } it { should_not match('/foo') } end pattern '/:foo', capture: /\d+/ do it { should match('/1') .capturing foo: '1' } it { should match('/123') .capturing foo: '123' } it { should_not match('/') } it { should_not match('/foo') } end pattern '/:foo', capture: '1' do it { should match('/1').capturing foo: '1' } it { should_not match('/') } it { should_not match('/foo') } it { should_not match('/123') } end pattern '/:foo', capture: 'a.b' do it { should match('/a.b') .capturing foo: 'a.b' } it { should match('/a%2Eb') .capturing foo: 'a%2Eb' } it { should match('/a%2eb') .capturing foo: 'a%2eb' } it { should_not match('/ab') } it { should_not match('/afb') } it { should_not match('/a1b') } it { should_not match('/a.bc') } end pattern '/:foo(/:bar)', capture: :alpha do it { should match('/abc') .capturing foo: 'abc', bar: nil } it { should match('/a/b') .capturing foo: 'a', bar: 'b' } it { should match('/a') .capturing foo: 'a', bar: nil } it { should_not match('/1/2') } it { should_not match('/a/2') } it { should_not match('/1/b') } it { should_not match('/1') } it { should_not match('/1/') } it { should_not match('/a/') } it { should_not match('//a') } end pattern '/:foo', capture: ['foo', 'bar', /\d+/] do it { should match('/1') .capturing foo: '1' } it { should match('/123') .capturing foo: '123' } it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should_not match('/') } it { should_not match('/baz') } it { should_not match('/foo1') } end pattern '/:foo:bar:baz', capture: { foo: :alpha, bar: /\d+/ } do it { should match('/ab123xy-1') .capturing foo: 'ab', bar: '123', baz: 'xy-1' } it { should match('/ab123') .capturing foo: 'ab', bar: '12', baz: '3' } it { should_not match('/123abcxy-1') } it { should_not match('/abcxy-1') } it { should_not match('/abc1') } end pattern '/:foo', capture: { foo: ['foo', 'bar', /\d+/] } do it { should match('/1') .capturing foo: '1' } it { should match('/123') .capturing foo: '123' } it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should_not match('/') } it { should_not match('/baz') } it { should_not match('/foo1') } end pattern '/:file(.:ext)', capture: { ext: ['jpg', 'png'] } do it { should match('/pony') .capturing file: 'pony', ext: nil } it { should match('/pony.jpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%2Ejpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%2ejpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony.png') .capturing file: 'pony', ext: 'png' } it { should match('/pony%2Epng') .capturing file: 'pony', ext: 'png' } it { should match('/pony%2epng') .capturing file: 'pony', ext: 'png' } it { should match('/pony.png.jpg') .capturing file: 'pony.png', ext: 'jpg' } it { should match('/pony.jpg.png') .capturing file: 'pony.jpg', ext: 'png' } it { should match('/pony.gif') .capturing file: 'pony.gif', ext: nil } it { should match('/pony.') .capturing file: 'pony.', ext: nil } it { should_not match('.jpg') } end pattern '/:file(:ext)', capture: { ext: ['.jpg', '.png', '.tar.gz'] } do it { should match('/pony') .capturing file: 'pony', ext: nil } it { should match('/pony.jpg') .capturing file: 'pony', ext: '.jpg' } it { should match('/pony.png') .capturing file: 'pony', ext: '.png' } it { should match('/pony.png.jpg') .capturing file: 'pony.png', ext: '.jpg' } it { should match('/pony.jpg.png') .capturing file: 'pony.jpg', ext: '.png' } it { should match('/pony.tar.gz') .capturing file: 'pony', ext: '.tar.gz' } it { should match('/pony.gif') .capturing file: 'pony.gif', ext: nil } it { should match('/pony.') .capturing file: 'pony.', ext: nil } it { should_not match('/.jpg') } end pattern '/:a(@:b)', capture: { b: /\d+/ } do it { should match('/a') .capturing a: 'a', b: nil } it { should match('/a@1') .capturing a: 'a', b: '1' } it { should match('/a@b') .capturing a: 'a@b', b: nil } it { should match('/a@1@2') .capturing a: 'a@1', b: '2' } end pattern '/:a(b)', greedy: false do it { should match('/ab').capturing a: 'a' } end pattern '/:file(.:ext)', greedy: false do it { should match('/pony') .capturing file: 'pony', ext: nil } it { should match('/pony.jpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony.png.jpg') .capturing file: 'pony', ext: 'png.jpg' } end pattern '/:controller(/:action(/:id(.:format)))' do it { should match('/content').capturing controller: 'content' } end pattern '/fo(o)', uri_decode: false do it { should match('/foo') } it { should match('/fo') } it { should_not match('/fo(o)') } end pattern '/foo/bar', uri_decode: false do it { should match('/foo/bar') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } end pattern "/path with spaces", uri_decode: false do it { should match('/path with spaces') } it { should_not match('/path%20with%20spaces') } it { should_not match('/path%2Bwith%2Bspaces') } it { should_not match('/path+with+spaces') } end pattern "/path with spaces", space_matches_plus: false do it { should match('/path%20with%20spaces') } it { should_not match('/path%2Bwith%2Bspaces') } it { should_not match('/path+with+spaces') } end context 'invalid syntax' do example 'unexpected closing parenthesis' do expect { Mustermann::Rails.new('foo)bar') }. to raise_error(Mustermann::ParseError, 'unexpected ) while parsing "foo)bar"') end example 'missing closing parenthesis' do expect { Mustermann::Rails.new('foo(bar') }. to raise_error(Mustermann::ParseError, 'unexpected end of string while parsing "foo(bar"') end end context 'invalid capture names' do example 'empty name' do expect { Mustermann::Rails.new('/:/') }. to raise_error(Mustermann::CompileError, "capture name can't be empty: \"/:/\"") end example 'named splat' do expect { Mustermann::Rails.new('/:splat/') }. to raise_error(Mustermann::CompileError, "capture name can't be splat: \"/:splat/\"") end example 'named captures' do expect { Mustermann::Rails.new('/:captures/') }. to raise_error(Mustermann::CompileError, "capture name can't be captures: \"/:captures/\"") end example 'with capital letter' do expect { Mustermann::Rails.new('/:Foo/') }. to raise_error(Mustermann::CompileError, "capture name must start with underscore or lower case letter: \"/:Foo/\"") end example 'with integer' do expect { Mustermann::Rails.new('/:1a/') }. to raise_error(Mustermann::CompileError, "capture name must start with underscore or lower case letter: \"/:1a/\"") end example 'same name twice' do expect { Mustermann::Rails.new('/:foo(/:bar)/:bar') }. to raise_error(Mustermann::CompileError, "can't use the same capture name twice: \"/:foo(/:bar)/:bar\"") end end context 'Regexp compatibility' do describe :=== do example('non-matching') { Mustermann::Rails.new("/") .should_not be === '/foo' } example('matching') { Mustermann::Rails.new("/:foo") .should be === '/foo' } end describe :=~ do example('non-matching') { Mustermann::Rails.new("/") .should_not be =~ '/foo' } example('matching') { Mustermann::Rails.new("/:foo") .should be =~ '/foo' } context 'String#=~' do example('non-matching') { "/foo".should_not be =~ Mustermann::Rails.new("/") } example('matching') { "/foo".should be =~ Mustermann::Rails.new("/:foo") } end end describe :to_regexp do example('empty pattern') { Mustermann::Rails.new('').to_regexp.should be == /\A(?-mix:)\Z/ } context 'Regexp.try_convert' do example('empty pattern') { Regexp.try_convert(Mustermann::Rails.new('')).should be == /\A(?-mix:)\Z/ } end end end context 'Proc compatibility' do describe :to_proc do example { Mustermann::Rails.new("/").to_proc.should be_a(Proc) } example('non-matching') { Mustermann::Rails.new("/") .to_proc.call('/foo').should be == false } example('matching') { Mustermann::Rails.new("/:foo") .to_proc.call('/foo').should be == true } end end context "peeking" do subject(:pattern) { Mustermann::Rails.new(":name") } describe :peek_size do example { pattern.peek_size("foo bar/blah") .should be == "foo bar".size } example { pattern.peek_size("foo%20bar/blah") .should be == "foo%20bar".size } example { pattern.peek_size("/foo bar") .should be_nil } end describe :peek_match do example { pattern.peek_match("foo bar/blah") .to_s .should be == "foo bar" } example { pattern.peek_match("foo%20bar/blah") .to_s .should be == "foo%20bar" } example { pattern.peek_match("/foo bar") .should be_nil } end describe :peek_params do example { pattern.peek_params("foo bar/blah") .should be == [{"name" => "foo bar"}, "foo bar".size] } example { pattern.peek_params("foo%20bar/blah") .should be == [{"name" => "foo bar"}, "foo%20bar".size] } example { pattern.peek_params("/foo bar") .should be_nil } end end context 'version compatibility' do context '2.3' do pattern '(foo)', version: '2.3' do it { should_not match("") } it { should_not match("foo") } it { should match("(foo)") } end pattern '\\:name', version: '2.3' do it { should match('%5cfoo').capturing(name: 'foo') } end end context '3.0' do pattern '(foo)', version: '3.0' do it { should match("") } it { should match("foo") } end pattern '\\:name', version: '3.0' do it { should match(':name') } it { should_not match(':foo') } end end context '3.2' do pattern '\\:name', version: '3.2' do it { should match('%5cfoo').capturing(name: 'foo') } end end context '4.0' do pattern '\\:name', version: '4.0' do it { should match('foo').capturing(name: 'foo') } end end context '4.2' do pattern '\\:name', version: '4.2' do it { should match(':name') } it { should_not match(':foo') } end end context '5.0' do pattern 'foo|bar', version: '5.0' do it { should match('foo') } it { should match('bar') } end end end end mustermann-1.1.1/mustermann-contrib/spec/shell_spec.rb000066400000000000000000000110151360365612700231410ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/shell' describe Mustermann::Shell do extend Support::Pattern pattern '' do it { should match('') } it { should_not match('/') } it { should_not respond_to(:expand) } it { should_not respond_to(:to_templates) } end pattern '/' do it { should match('/') } it { should_not match('/foo') } example { pattern.params('/').should be == {} } example { pattern.params('').should be_nil } end pattern '/foo' do it { should match('/foo') } it { should_not match('/bar') } it { should_not match('/foo.bar') } end pattern '/foo/bar' do it { should match('/foo/bar') } it { should match('/foo%2Fbar') } it { should match('/foo%2fbar') } end pattern '/*/bar' do it { should match('/foo/bar') } it { should match('/bar/bar') } it { should match('/foo%2Fbar') } it { should match('/foo%2fbar') } it { should_not match('/foo/foo/bar') } it { should_not match('/bar/foo') } end pattern '/**/foo' do it { should match('/a/b/c/foo') } it { should match('/a/b/c/foo') } it { should match('/a/.b/c/foo') } it { should match('/a/.b/c/foo') } end pattern '/:foo' do it { should match('/:foo') } it { should match('/%3Afoo') } it { should_not match('/foo') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } end pattern '/föö' do it { should match("/f%C3%B6%C3%B6") } end pattern '/test$/' do it { should match('/test$/') } end pattern '/te+st/' do it { should match('/te+st/') } it { should_not match('/test/') } it { should_not match('/teest/') } end pattern "/path with spaces" do it { should match('/path%20with%20spaces') } it { should_not match('/path%2Bwith%2Bspaces') } it { should_not match('/path+with+spaces') } end pattern '/foo&bar' do it { should match('/foo&bar') } end pattern '/test.bar' do it { should match('/test.bar') } it { should_not match('/test0bar') } end pattern '/{foo,bar}' do it { should match('/foo') } it { should match('/bar') } it { should_not match('/foobar') } end pattern '/foo/bar', uri_decode: false do it { should match('/foo/bar') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } end pattern "/path with spaces", uri_decode: false do it { should_not match('/path%20with%20spaces') } it { should_not match('/path%2Bwith%2Bspaces') } it { should_not match('/path+with+spaces') } end describe :=~ do example { '/foo'.should be =~ Mustermann::Shell.new('/foo') } end context "peeking" do subject(:pattern) { Mustermann::Shell.new("foo*/") } describe :peek_size do example { pattern.peek_size("foo bar/blah") .should be == "foo bar/".size } example { pattern.peek_size("foo%20bar/blah") .should be == "foo%20bar/".size } example { pattern.peek_size("/foo bar") .should be_nil } context 'with just * as pattern' do subject(:pattern) { Mustermann::Shell.new('*') } example { pattern.peek_size('foo') .should be == 3 } example { pattern.peek_size('foo/bar') .should be == 3 } example { pattern.peek_size('foo/bar/baz') .should be == 3 } example { pattern.peek_size('foo/bar/baz/blah') .should be == 3 } end end describe :peek_match do example { pattern.peek_match("foo bar/blah") .to_s .should be == "foo bar/" } example { pattern.peek_match("foo%20bar/blah") .to_s .should be == "foo%20bar/" } example { pattern.peek_match("/foo bar") .should be_nil } end describe :peek_params do example { pattern.peek_params("foo bar/blah") .should be == [{}, "foo bar/".size] } example { pattern.peek_params("foo%20bar/blah") .should be == [{}, "foo%20bar/".size] } example { pattern.peek_params("/foo bar") .should be_nil } end end context "highlighting" do let(:pattern) { Mustermann::Shell.new("/**,*/\\*/{a,b}") } subject(:sexp) { Mustermann::Visualizer.highlight(pattern).to_sexp } it { should be == '(root (separator /) (special *) (special *) (char ,) (special *) (separator /) (escaped "\\\\" (escaped_char *)) (separator /) (union { (root (char a)) ,(root (char b)) }))' } end end mustermann-1.1.1/mustermann-contrib/spec/simple_spec.rb000066400000000000000000000217341360365612700233340ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/simple' require 'mustermann/visualizer' describe Mustermann::Simple do extend Support::Pattern pattern '' do it { should match('') } it { should_not match('/') } it { should_not respond_to(:expand) } it { should_not respond_to(:to_templates) } end pattern '/' do it { should match('/') } it { should_not match('/foo') } end pattern '/foo' do it { should match('/foo') } it { should_not match('/bar') } it { should_not match('/foo.bar') } end pattern '/foo/bar' do it { should match('/foo/bar') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } end pattern '/:foo' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo.bar') .capturing foo: 'foo.bar' } it { should match('/%0Afoo') .capturing foo: '%0Afoo' } it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } end pattern '/föö' do it { should match("/f%C3%B6%C3%B6") } end pattern "/:foo/:bar" do it { should match('/foo/bar') .capturing foo: 'foo', bar: 'bar' } it { should match('/foo.bar/bar.foo') .capturing foo: 'foo.bar', bar: 'bar.foo' } it { should match('/user@example.com/name') .capturing foo: 'user@example.com', bar: 'name' } it { should match('/10.1/te.st') .capturing foo: '10.1', bar: 'te.st' } it { should match('/10.1.2/te.st') .capturing foo: '10.1.2', bar: 'te.st' } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } example { pattern.params('/bar/foo').should be == {"foo" => "bar", "bar" => "foo"} } example { pattern.params('').should be_nil } end pattern '/hello/:person' do it { should match('/hello/Frank').capturing person: 'Frank' } end pattern '/?:foo?/?:bar?' do it { should match('/hello/world') .capturing foo: 'hello', bar: 'world' } it { should match('/hello') .capturing foo: 'hello', bar: nil } it { should match('/') .capturing foo: nil, bar: nil } it { should match('') .capturing foo: nil, bar: nil } it { should_not match('/hello/world/') } end pattern '/*' do it { should match('/') .capturing splat: '' } it { should match('/foo') .capturing splat: 'foo' } it { should match('/foo/bar') .capturing splat: 'foo/bar' } example { pattern.params('/foo').should be == {"splat" => ["foo"]} } end pattern '/:foo/*' do it { should match("/foo/bar/baz") .capturing foo: 'foo', splat: 'bar/baz' } it { should match("/foo/") .capturing foo: 'foo', splat: '' } it { should match('/h%20w/h%20a%20y') .capturing foo: 'h%20w', splat: 'h%20a%20y' } it { should_not match('/foo') } example { pattern.params('/bar/foo').should be == {"splat" => ["foo"], "foo" => "bar"} } example { pattern.params('/bar/foo/f%20o').should be == {"splat" => ["foo/f o"], "foo" => "bar"} } end pattern '/test$/' do it { should match('/test$/') } end pattern '/te+st/' do it { should match('/te+st/') } it { should_not match('/test/') } it { should_not match('/teest/') } end pattern "/path with spaces" do it { should match('/path%20with%20spaces') } it { should match('/path%2Bwith%2Bspaces') } it { should match('/path+with+spaces') } end pattern '/foo&bar' do it { should match('/foo&bar') } end pattern '/*/:foo/*/*' do it { should match('/bar/foo/bling/baz/boom') } it "should capture all splat parts" do match = pattern.match('/bar/foo/bling/baz/boom') match.captures.should be == ['bar', 'foo', 'bling', 'baz/boom'] match.names.should be == ['splat', 'foo'] end it 'should map to proper params' do pattern.params('/bar/foo/bling/baz/boom').should be == { "foo" => "foo", "splat" => ['bar', 'bling', 'baz/boom'] } end end pattern '/test.bar' do it { should match('/test.bar') } it { should_not match('/test0bar') } end pattern '/:file.:ext' do it { should match('/pony.jpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%2Ejpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%2ejpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%E6%AD%A3%2Ejpg') .capturing file: 'pony%E6%AD%A3', ext: 'jpg' } it { should match('/pony%e6%ad%a3%2ejpg') .capturing file: 'pony%e6%ad%a3', ext: 'jpg' } it { should match('/pony正%2Ejpg') .capturing file: 'pony正', ext: 'jpg' } it { should match('/pony正%2ejpg') .capturing file: 'pony正', ext: 'jpg' } it { should match('/pony正..jpg') .capturing file: 'pony正.', ext: 'jpg' } it { should_not match('/.jpg') } end pattern '/:id/test.bar' do it { should match('/3/test.bar') .capturing id: '3' } it { should match('/2/test.bar') .capturing id: '2' } it { should match('/2E/test.bar') .capturing id: '2E' } it { should match('/2e/test.bar') .capturing id: '2e' } it { should match('/%2E/test.bar') .capturing id: '%2E' } end pattern '/10/:id' do it { should match('/10/test') .capturing id: 'test' } it { should match('/10/te.st') .capturing id: 'te.st' } end pattern '/10.1/:id' do it { should match('/10.1/test') .capturing id: 'test' } it { should match('/10.1/te.st') .capturing id: 'te.st' } end pattern '/foo?' do it { should match('/fo') } it { should match('/foo') } it { should_not match('') } it { should_not match('/') } it { should_not match('/f') } it { should_not match('/fooo') } end pattern '/:fOO' do it { should match('/a').capturing fOO: 'a' } end pattern '/:_X' do it { should match('/a').capturing _X: 'a' } end pattern '/:f00' do it { should match('/a').capturing f00: 'a' } end pattern '/:foo.?' do it { should match('/a.').capturing foo: 'a.' } it { should match('/xy').capturing foo: 'xy' } end pattern '/(a)' do it { should match('/(a)') } it { should_not match('/a') } end pattern '/:foo.?', greedy: false do it { should match('/a.').capturing foo: 'a' } it { should match('/xy').capturing foo: 'xy' } end pattern '/foo?', uri_decode: false do it { should match('/foo') } it { should match('/fo') } it { should_not match('/foo?') } end pattern '/foo/bar', uri_decode: false do it { should match('/foo/bar') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } end pattern "/path with spaces", uri_decode: false do it { should match('/path with spaces') } it { should_not match('/path%20with%20spaces') } it { should_not match('/path%2Bwith%2Bspaces') } it { should_not match('/path+with+spaces') } end pattern "/path with spaces", space_matches_plus: false do it { should match('/path%20with%20spaces') } it { should_not match('/path%2Bwith%2Bspaces') } it { should_not match('/path+with+spaces') } end context 'error handling' do example '? at beginning of route' do expect { Mustermann::Simple.new('?foobar') }. to raise_error(Mustermann::ParseError) end example 'invalid capture name' do expect { Mustermann::Simple.new('/:1a/') }. to raise_error(Mustermann::CompileError) end end context "peeking" do subject(:pattern) { Mustermann::Simple.new(":name") } describe :peek_size do example { pattern.peek_size("foo bar/blah") .should be == "foo bar".size } example { pattern.peek_size("foo%20bar/blah") .should be == "foo%20bar".size } example { pattern.peek_size("/foo bar") .should be_nil } end describe :peek_match do example { pattern.peek_match("foo bar/blah") .to_s .should be == "foo bar" } example { pattern.peek_match("foo%20bar/blah") .to_s .should be == "foo%20bar" } example { pattern.peek_match("/foo bar") .should be_nil } end describe :peek_params do example { pattern.peek_params("foo bar/blah") .should be == [{"name" => "foo bar"}, "foo bar".size] } example { pattern.peek_params("foo%20bar/blah") .should be == [{"name" => "foo bar"}, "foo%20bar".size] } example { pattern.peek_params("/foo bar") .should be_nil } end end context "highlighting" do let(:pattern) { Mustermann::Simple.new("/:name?/*") } subject(:sexp) { Mustermann::Visualizer.highlight(pattern).to_sexp } it { should be == "(root (separator /) (capture : (name name)) (optional ?) (separator /) (splat *))" } end end mustermann-1.1.1/mustermann-contrib/spec/string_scanner_spec.rb000066400000000000000000000133601360365612700250560ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/string_scanner' describe Mustermann::StringScanner do include Support::ScanMatcher subject(:scanner) { Mustermann::StringScanner.new(example_string) } let(:example_string) { "foo bar" } describe :scan do it { should scan("foo") } it { should scan(/foo/) } it { should scan(:name) } it { should scan(":name") } it { should_not scan(" ") } it { should_not scan("bar") } example do should scan("foo") should scan(" ") should scan("bar") end example do scanner.position = 4 should scan("bar") end example do should scan("foo") scanner.reset should scan("foo") end end describe :check do it { should check("foo") } it { should check(/foo/) } it { should check(:name) } it { should check(":name") } it { should_not check(" ") } it { should_not check("bar") } example do should check("foo") should_not check(" ") should_not check("bar") should check("foo") end example do scanner.position = 4 should check("bar") end end describe :scan_until do it { should scan_until("foo") } it { should scan_until(":name") } it { should scan_until(" ") } it { should scan_until("bar") } it { should_not scan_until("baz") } example do should scan_until(" ") should check("bar") end example do should scan_until(" ") scanner.reset should scan("foo") end end describe :check_until do it { should check_until("foo") } it { should check_until(":name") } it { should check_until(" ") } it { should check_until("bar") } it { should_not check_until("baz") } example do should check_until(" ") should_not check("bar") end end describe :getch do example { scanner.getch.should be == "f" } example do scanner.scan("foo") scanner.getch.should be == " " should scan("bar") end example do scanner.getch scanner.reset should scan("foo") end end describe :<< do example do should_not scan_until("baz") scanner << " baz" scanner.to_s.should be == "foo bar baz" should scan_until("baz") end end describe :eos? do it { should_not be_eos } example do scanner.position = 7 should be_eos end end describe :beginning_of_line? do let(:example_string) { "foo\nbar" } it { should be_beginning_of_line } example do scanner.position = 2 should_not be_beginning_of_line end example do scanner.position = 3 should_not be_beginning_of_line end example do scanner.position = 4 should be_beginning_of_line end end describe :rest do example { scanner.rest.should be == "foo bar" } example do scanner.position = 4 scanner.rest.should be == "bar" end end describe :rest_size do example { scanner.rest_size.should be == 7 } example do scanner.position = 4 scanner.rest_size.should be == 3 end end describe :peek do example { scanner.peek(3).should be == "foo" } example do scanner.peek(3).should be == "foo" scanner.peek(3).should be == "foo" end example do scanner.position = 4 scanner.peek(3).should be == "bar" end end describe :inspect do example { scanner.inspect.should be == '#' } example do scanner.position = 4 scanner.inspect.should be == '#' end end describe :[] do example do should scan(:name) scanner['name'].should be == "foo bar" end example do should scan(:name, capture: /\S+/) scanner['name'].should be == "foo" should scan(" :name", capture: /\S+/) scanner['name'].should be == "bar" end example do should scan(":a", capture: /\S+/) should scan(" :b", capture: /\S+/) scanner['a'].should be == "foo" scanner['b'].should be == "bar" end example do a = scanner.scan(":a", capture: /\S+/) b = scanner.scan(" :b", capture: /\S+/) a.params['a'].should be == 'foo' b.params['b'].should be == 'bar' a.params['b'].should be_nil b.params['a'].should be_nil end example do result = scanner.check(":a", capture: /\S+/) result.params['a'].should be == 'foo' scanner['a'].should be_nil end example do should scan(:name) scanner.reset scanner['name'].should be_nil end end describe :unscan do example do should scan(:name, capture: /\S+/) scanner['name'].should be == "foo" should scan(" :name", capture: /\S+/) scanner['name'].should be == "bar" scanner.unscan scanner['name'].should be == "foo" scanner.rest.should be == " bar" end example do should scan_until(" ") scanner.unscan scanner.rest.should be == "foo bar" end example do expect { scanner.unscan }.to raise_error(Mustermann::StringScanner::ScanError, 'unscan failed: previous match record not exist') end end describe :terminate do example do scanner.terminate scanner.should be_eos end end describe :to_h do example { scanner.to_h.should be == {} } example do end end describe :to_s do example { scanner.to_s.should be == "foo bar" } end describe :clear_cache do example do scanner.scan("foo") Mustermann::StringScanner.clear_cache Mustermann::StringScanner.cache_size.should be == 0 end end end mustermann-1.1.1/mustermann-contrib/spec/template_spec.rb000066400000000000000000000777071360365612700236710ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/template' describe Mustermann::Template do extend Support::Pattern pattern '' do it { should match('') } it { should_not match('/') } it { should respond_to(:expand) } it { should respond_to(:to_templates) } end pattern '/' do it { should match('/') } it { should_not match('/foo') } end pattern '/foo' do it { should match('/foo') } it { should_not match('/bar') } it { should_not match('/foo.bar') } end pattern '/foo/bar' do it { should match('/foo/bar') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } end pattern '/:foo' do it { should match('/:foo') } it { should match('/%3Afoo') } it { should_not match('/foo') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } end pattern '/föö' do it { should match("/f%C3%B6%C3%B6") } end pattern '/test$/' do it { should match('/test$/') } end pattern '/te+st/' do it { should match('/te+st/') } it { should_not match('/test/') } it { should_not match('/teest/') } end pattern "/path with spaces" do it { should match('/path%20with%20spaces') } it { should match('/path%2Bwith%2Bspaces') } it { should match('/path+with+spaces') } end pattern '/foo&bar' do it { should match('/foo&bar') } end pattern '/test.bar' do it { should match('/test.bar') } it { should_not match('/test0bar') } end pattern "/path with spaces", space_matches_plus: false do it { should match('/path%20with%20spaces') } it { should_not match('/path%2Bwith%2Bspaces') } it { should_not match('/path+with+spaces') } end pattern "/path with spaces", uri_decode: false do it { should_not match('/path%20with%20spaces') } it { should_not match('/path%2Bwith%2Bspaces') } it { should_not match('/path+with+spaces') } end context 'level 1' do context 'without operator' do pattern '/hello/{person}' do it { should match('/hello/Frank').capturing person: 'Frank' } it { should match('/hello/a_b~c').capturing person: 'a_b~c' } it { should match('/hello/a.%20').capturing person: 'a.%20' } it { should_not match('/hello/:') } it { should_not match('/hello//') } it { should_not match('/hello/?') } it { should_not match('/hello/#') } it { should_not match('/hello/[') } it { should_not match('/hello/]') } it { should_not match('/hello/@') } it { should_not match('/hello/!') } it { should_not match('/hello/*') } it { should_not match('/hello/+') } it { should_not match('/hello/,') } it { should_not match('/hello/;') } it { should_not match('/hello/=') } example { pattern.params('/hello/Frank').should be == {'person' => 'Frank'} } end pattern "/{foo}/{bar}" do it { should match('/foo/bar') .capturing foo: 'foo', bar: 'bar' } it { should match('/foo.bar/bar.foo') .capturing foo: 'foo.bar', bar: 'bar.foo' } it { should match('/10.1/te.st') .capturing foo: '10.1', bar: 'te.st' } it { should match('/10.1.2/te.st') .capturing foo: '10.1.2', bar: 'te.st' } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } end end end context 'level 2' do context 'operator +' do pattern '/hello/{+person}' do it { should match('/hello/Frank') .capturing person: 'Frank' } it { should match('/hello/a_b~c') .capturing person: 'a_b~c' } it { should match('/hello/a.%20') .capturing person: 'a.%20' } it { should match('/hello/a/%20') .capturing person: 'a/%20' } it { should match('/hello/:') .capturing person: ?: } it { should match('/hello//') .capturing person: ?/ } it { should match('/hello/?') .capturing person: ?? } it { should match('/hello/#') .capturing person: ?# } it { should match('/hello/[') .capturing person: ?[ } it { should match('/hello/]') .capturing person: ?] } it { should match('/hello/@') .capturing person: ?@ } it { should match('/hello/!') .capturing person: ?! } it { should match('/hello/*') .capturing person: ?* } it { should match('/hello/+') .capturing person: ?+ } it { should match('/hello/,') .capturing person: ?, } it { should match('/hello/;') .capturing person: ?; } it { should match('/hello/=') .capturing person: ?= } end pattern "/{+foo}/{bar}" do it { should match('/foo/bar') .capturing foo: 'foo', bar: 'bar' } it { should match('/foo.bar/bar.foo') .capturing foo: 'foo.bar', bar: 'bar.foo' } it { should match('/foo/bar/bar.foo') .capturing foo: 'foo/bar', bar: 'bar.foo' } it { should match('/10.1/te.st') .capturing foo: '10.1', bar: 'te.st' } it { should match('/10.1.2/te.st') .capturing foo: '10.1.2', bar: 'te.st' } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } end end context 'operator #' do pattern '/hello/{#person}' do it { should match('/hello/#Frank') .capturing person: 'Frank' } it { should match('/hello/#a_b~c') .capturing person: 'a_b~c' } it { should match('/hello/#a.%20') .capturing person: 'a.%20' } it { should match('/hello/#a/%20') .capturing person: 'a/%20' } it { should match('/hello/#:') .capturing person: ?: } it { should match('/hello/#/') .capturing person: ?/ } it { should match('/hello/#?') .capturing person: ?? } it { should match('/hello/##') .capturing person: ?# } it { should match('/hello/#[') .capturing person: ?[ } it { should match('/hello/#]') .capturing person: ?] } it { should match('/hello/#@') .capturing person: ?@ } it { should match('/hello/#!') .capturing person: ?! } it { should match('/hello/#*') .capturing person: ?* } it { should match('/hello/#+') .capturing person: ?+ } it { should match('/hello/#,') .capturing person: ?, } it { should match('/hello/#;') .capturing person: ?; } it { should match('/hello/#=') .capturing person: ?= } it { should_not match('/hello/Frank') } it { should_not match('/hello/a_b~c') } it { should_not match('/hello/a.%20') } it { should_not match('/hello/:') } it { should_not match('/hello//') } it { should_not match('/hello/?') } it { should_not match('/hello/#') } it { should_not match('/hello/[') } it { should_not match('/hello/]') } it { should_not match('/hello/@') } it { should_not match('/hello/!') } it { should_not match('/hello/*') } it { should_not match('/hello/+') } it { should_not match('/hello/,') } it { should_not match('/hello/;') } it { should_not match('/hello/=') } example { pattern.params('/hello/#Frank').should be == {'person' => 'Frank'} } end pattern "/{+foo}/{#bar}" do it { should match('/foo/#bar') .capturing foo: 'foo', bar: 'bar' } it { should match('/foo.bar/#bar.foo') .capturing foo: 'foo.bar', bar: 'bar.foo' } it { should match('/foo/bar/#bar.foo') .capturing foo: 'foo/bar', bar: 'bar.foo' } it { should match('/10.1/#te.st') .capturing foo: '10.1', bar: 'te.st' } it { should match('/10.1.2/#te.st') .capturing foo: '10.1.2', bar: 'te.st' } it { should_not match('/foo%2F#bar') } it { should_not match('/foo%2f#bar') } example { pattern.params('/hello/#Frank').should be == {'foo' => 'hello', 'bar' => 'Frank'} } end end end context 'level 3' do context 'without operator' do pattern "{a,b,c}" do it { should match("~x,42,_").capturing a: '~x', b: '42', c: '_' } it { should_not match("~x,42") } it { should_not match("~x/42") } it { should_not match("~x#42") } it { should_not match("~x,42,_#42") } example { pattern.params('d,f,g').should be == {'a' => 'd', 'b' => 'f', 'c' => 'g'} } end end context 'operator +' do pattern "{+a,b,c}" do it { should match("~x,42,_") .capturing a: '~x', b: '42', c: '_' } it { should match("~x,42,_#42") .capturing a: '~x', b: '42', c: '_#42' } it { should match("~/x,42,_/42") .capturing a: '~/x', b: '42', c: '_/42' } it { should_not match("~x,42") } it { should_not match("~x/42") } it { should_not match("~x#42") } end end context 'operator #' do pattern "{#a,b,c}" do it { should match("#~x,42,_") .capturing a: '~x', b: '42', c: '_' } it { should match("#~x,42,_#42") .capturing a: '~x', b: '42', c: '_#42' } it { should match("#~/x,42,_#42") .capturing a: '~/x', b: '42', c: '_#42' } it { should_not match("~x,42,_") } it { should_not match("~x,42,_#42") } it { should_not match("~/x,42,_#42") } it { should_not match("~x,42") } it { should_not match("~x/42") } it { should_not match("~x#42") } end end context 'operator .' do pattern '/hello/{.person}' do it { should match('/hello/.Frank') .capturing person: 'Frank' } it { should match('/hello/.a_b~c') .capturing person: 'a_b~c' } it { should_not match('/hello/.:') } it { should_not match('/hello/./') } it { should_not match('/hello/.?') } it { should_not match('/hello/.#') } it { should_not match('/hello/.[') } it { should_not match('/hello/.]') } it { should_not match('/hello/.@') } it { should_not match('/hello/.!') } it { should_not match('/hello/.*') } it { should_not match('/hello/.+') } it { should_not match('/hello/.,') } it { should_not match('/hello/.;') } it { should_not match('/hello/.=') } it { should_not match('/hello/Frank') } it { should_not match('/hello/a_b~c') } it { should_not match('/hello/a.%20') } it { should_not match('/hello/:') } it { should_not match('/hello//') } it { should_not match('/hello/?') } it { should_not match('/hello/#') } it { should_not match('/hello/[') } it { should_not match('/hello/]') } it { should_not match('/hello/@') } it { should_not match('/hello/!') } it { should_not match('/hello/*') } it { should_not match('/hello/+') } it { should_not match('/hello/,') } it { should_not match('/hello/;') } it { should_not match('/hello/=') } end pattern "{.a,b,c}" do it { should match(".~x.42._").capturing a: '~x', b: '42', c: '_' } it { should_not match(".~x,42") } it { should_not match(".~x/42") } it { should_not match(".~x#42") } it { should_not match(".~x,42,_") } it { should_not match("~x.42._") } end end context 'operator /' do pattern '/hello{/person}' do it { should match('/hello/Frank') .capturing person: 'Frank' } it { should match('/hello/a_b~c') .capturing person: 'a_b~c' } it { should_not match('/hello//:') } it { should_not match('/hello///') } it { should_not match('/hello//?') } it { should_not match('/hello//#') } it { should_not match('/hello//[') } it { should_not match('/hello//]') } it { should_not match('/hello//@') } it { should_not match('/hello//!') } it { should_not match('/hello//*') } it { should_not match('/hello//+') } it { should_not match('/hello//,') } it { should_not match('/hello//;') } it { should_not match('/hello//=') } it { should_not match('/hello/:') } it { should_not match('/hello//') } it { should_not match('/hello/?') } it { should_not match('/hello/#') } it { should_not match('/hello/[') } it { should_not match('/hello/]') } it { should_not match('/hello/@') } it { should_not match('/hello/!') } it { should_not match('/hello/*') } it { should_not match('/hello/+') } it { should_not match('/hello/,') } it { should_not match('/hello/;') } it { should_not match('/hello/=') } end pattern "{/a,b,c}" do it { should match("/~x/42/_").capturing a: '~x', b: '42', c: '_' } it { should_not match("/~x,42") } it { should_not match("/~x.42") } it { should_not match("/~x#42") } it { should_not match("/~x,42,_") } it { should_not match("~x/42/_") } end end context 'operator ;' do pattern '/hello/{;person}' do it { should match('/hello/;person=Frank') .capturing person: 'Frank' } it { should match('/hello/;person=a_b~c') .capturing person: 'a_b~c' } it { should match('/hello/;person') .capturing person: nil } it { should_not match('/hello/;persona=Frank') } it { should_not match('/hello/;persona=a_b~c') } it { should_not match('/hello/;person=:') } it { should_not match('/hello/;person=/') } it { should_not match('/hello/;person=?') } it { should_not match('/hello/;person=#') } it { should_not match('/hello/;person=[') } it { should_not match('/hello/;person=]') } it { should_not match('/hello/;person=@') } it { should_not match('/hello/;person=!') } it { should_not match('/hello/;person=*') } it { should_not match('/hello/;person=+') } it { should_not match('/hello/;person=,') } it { should_not match('/hello/;person=;') } it { should_not match('/hello/;person==') } it { should_not match('/hello/;Frank') } it { should_not match('/hello/;a_b~c') } it { should_not match('/hello/;a.%20') } it { should_not match('/hello/:') } it { should_not match('/hello//') } it { should_not match('/hello/?') } it { should_not match('/hello/#') } it { should_not match('/hello/[') } it { should_not match('/hello/]') } it { should_not match('/hello/@') } it { should_not match('/hello/!') } it { should_not match('/hello/*') } it { should_not match('/hello/+') } it { should_not match('/hello/,') } it { should_not match('/hello/;') } it { should_not match('/hello/=') } end pattern "{;a,b,c}" do it { should match(";a=~x;b=42;c=_") .capturing a: '~x', b: '42', c: '_' } it { should match(";a=~x;b;c=_") .capturing a: '~x', b: nil, c: '_' } it { should_not match(";a=~x;c=_;b=42").capturing a: '~x', b: '42', c: '_' } it { should_not match(";a=~x;b=42") } it { should_not match("a=~x;b=42") } it { should_not match(";a=~x;b=#42;c") } it { should_not match(";a=~x,b=42,c=_") } it { should_not match("~x;b=42;c=_") } end end context 'operator ?' do pattern '/hello/{?person}' do it { should match('/hello/?person=Frank') .capturing person: 'Frank' } it { should match('/hello/?person=a_b~c') .capturing person: 'a_b~c' } it { should match('/hello/?person') .capturing person: nil } it { should_not match('/hello/?persona=Frank') } it { should_not match('/hello/?persona=a_b~c') } it { should_not match('/hello/?person=:') } it { should_not match('/hello/?person=/') } it { should_not match('/hello/?person=?') } it { should_not match('/hello/?person=#') } it { should_not match('/hello/?person=[') } it { should_not match('/hello/?person=]') } it { should_not match('/hello/?person=@') } it { should_not match('/hello/?person=!') } it { should_not match('/hello/?person=*') } it { should_not match('/hello/?person=+') } it { should_not match('/hello/?person=,') } it { should_not match('/hello/?person=;') } it { should_not match('/hello/?person==') } it { should_not match('/hello/?Frank') } it { should_not match('/hello/?a_b~c') } it { should_not match('/hello/?a.%20') } it { should_not match('/hello/:') } it { should_not match('/hello//') } it { should_not match('/hello/?') } it { should_not match('/hello/#') } it { should_not match('/hello/[') } it { should_not match('/hello/]') } it { should_not match('/hello/@') } it { should_not match('/hello/!') } it { should_not match('/hello/*') } it { should_not match('/hello/+') } it { should_not match('/hello/,') } it { should_not match('/hello/;') } it { should_not match('/hello/=') } end pattern "{?a,b,c}" do it { should match("?a=~x&b=42&c=_") .capturing a: '~x', b: '42', c: '_' } it { should match("?a=~x&b&c=_") .capturing a: '~x', b: nil, c: '_' } it { should_not match("?a=~x&c=_&b=42").capturing a: '~x', b: '42', c: '_' } it { should_not match("?a=~x&b=42") } it { should_not match("a=~x&b=42") } it { should_not match("?a=~x&b=#42&c") } it { should_not match("?a=~x,b=42,c=_") } it { should_not match("~x&b=42&c=_") } end end context 'operator &' do pattern '/hello/{&person}' do it { should match('/hello/&person=Frank') .capturing person: 'Frank' } it { should match('/hello/&person=a_b~c') .capturing person: 'a_b~c' } it { should match('/hello/&person') .capturing person: nil } it { should_not match('/hello/&persona=Frank') } it { should_not match('/hello/&persona=a_b~c') } it { should_not match('/hello/&person=:') } it { should_not match('/hello/&person=/') } it { should_not match('/hello/&person=?') } it { should_not match('/hello/&person=#') } it { should_not match('/hello/&person=[') } it { should_not match('/hello/&person=]') } it { should_not match('/hello/&person=@') } it { should_not match('/hello/&person=!') } it { should_not match('/hello/&person=*') } it { should_not match('/hello/&person=+') } it { should_not match('/hello/&person=,') } it { should_not match('/hello/&person=;') } it { should_not match('/hello/&person==') } it { should_not match('/hello/&Frank') } it { should_not match('/hello/&a_b~c') } it { should_not match('/hello/&a.%20') } it { should_not match('/hello/:') } it { should_not match('/hello//') } it { should_not match('/hello/?') } it { should_not match('/hello/#') } it { should_not match('/hello/[') } it { should_not match('/hello/]') } it { should_not match('/hello/@') } it { should_not match('/hello/!') } it { should_not match('/hello/*') } it { should_not match('/hello/+') } it { should_not match('/hello/,') } it { should_not match('/hello/;') } it { should_not match('/hello/=') } end pattern "{&a,b,c}" do it { should match("&a=~x&b=42&c=_") .capturing a: '~x', b: '42', c: '_' } it { should match("&a=~x&b&c=_") .capturing a: '~x', b: nil, c: '_' } it { should_not match("&a=~x&c=_&b=42").capturing a: '~x', b: '42', c: '_' } it { should_not match("&a=~x&b=42") } it { should_not match("a=~x&b=42") } it { should_not match("&a=~x&b=#42&c") } it { should_not match("&a=~x,b=42,c=_") } it { should_not match("~x&b=42&c=_") } end end end context 'level 4' do context 'without operator' do context 'prefix' do pattern '{a:3}/bar' do it { should match('foo/bar') .capturing a: 'foo' } it { should match('fo/bar') .capturing a: 'fo' } it { should match('f/bar') .capturing a: 'f' } it { should_not match('fooo/bar') } end pattern '{a:3}{b}' do it { should match('foobar') .capturing a: 'foo', b: 'bar' } end end context 'expand' do pattern '{a*}' do it { should match('a') .capturing a: 'a' } it { should match('a,b') .capturing a: 'a,b' } it { should match('a,b,c') .capturing a: 'a,b,c' } it { should_not match('a,b/c') } it { should_not match('a,') } example { pattern.params('a').should be == { 'a' => ['a'] }} example { pattern.params('a,b').should be == { 'a' => ['a', 'b'] }} end pattern '{a*},{b}' do it { should match('a,b') .capturing a: 'a', b: 'b' } it { should match('a,b,c') .capturing a: 'a,b', b: 'c' } it { should_not match('a,b/c') } it { should_not match('a,') } example { pattern.params('a,b').should be == { 'a' => ['a'], 'b' => 'b' }} example { pattern.params('a,b,c').should be == { 'a' => ['a', 'b'], 'b' => 'c' }} end pattern '{a*,b}' do it { should match('a,b') .capturing a: 'a', b: 'b' } it { should match('a,b,c') .capturing a: 'a,b', b: 'c' } it { should_not match('a,b/c') } it { should_not match('a,') } example { pattern.params('a,b').should be == { 'a' => ['a'], 'b' => 'b' }} example { pattern.params('a,b,c').should be == { 'a' => ['a', 'b'], 'b' => 'c' }} end end end context 'operator +' do pattern '/{a}/{+b}' do it { should match('/foo/bar/baz').capturing(a: 'foo', b: 'bar/baz') } it { should expand(a: 'foo/bar', b: 'foo/bar').to('/foo%2Fbar/foo/bar') } end context 'prefix' do pattern '{+a:3}/bar' do it { should match('foo/bar') .capturing a: 'foo' } it { should match('fo/bar') .capturing a: 'fo' } it { should match('f/bar') .capturing a: 'f' } it { should_not match('fooo/bar') } end pattern '{+a:3}{b}' do it { should match('foobar') .capturing a: 'foo', b: 'bar' } end end context 'expand' do pattern '{+a*}' do it { should match('a') .capturing a: 'a' } it { should match('a,b') .capturing a: 'a,b' } it { should match('a,b,c') .capturing a: 'a,b,c' } it { should match('a,b/c') .capturing a: 'a,b/c' } end pattern '{+a*},{b}' do it { should match('a,b') .capturing a: 'a', b: 'b' } it { should match('a,b,c') .capturing a: 'a,b', b: 'c' } it { should_not match('a,b/c') } it { should_not match('a,') } example { pattern.params('a,b').should be == { 'a' => ['a'], 'b' => 'b' }} example { pattern.params('a,b,c').should be == { 'a' => ['a', 'b'], 'b' => 'c' }} end end end context 'operator #' do context 'prefix' do pattern '{#a:3}/bar' do it { should match('#foo/bar') .capturing a: 'foo' } it { should match('#fo/bar') .capturing a: 'fo' } it { should match('#f/bar') .capturing a: 'f' } it { should_not match('#fooo/bar') } end pattern '{#a:3}{b}' do it { should match('#foobar') .capturing a: 'foo', b: 'bar' } end end context 'expand' do pattern '{#a*}' do it { should match('#a') .capturing a: 'a' } it { should match('#a,b') .capturing a: 'a,b' } it { should match('#a,b,c') .capturing a: 'a,b,c' } it { should match('#a,b/c') .capturing a: 'a,b/c' } example { pattern.params('#a,b').should be == { 'a' => ['a', 'b'] }} example { pattern.params('#a,b,c').should be == { 'a' => ['a', 'b', 'c'] }} end pattern '{#a*,b}' do it { should match('#a,b') .capturing a: 'a', b: 'b' } it { should match('#a,b,c') .capturing a: 'a,b', b: 'c' } it { should_not match('#a,') } example { pattern.params('#a,b').should be == { 'a' => ['a'], 'b' => 'b' }} example { pattern.params('#a,b,c').should be == { 'a' => ['a', 'b'], 'b' => 'c' }} end end end context 'operator .' do context 'prefix' do pattern '{.a:3}/bar' do it { should match('.foo/bar') .capturing a: 'foo' } it { should match('.fo/bar') .capturing a: 'fo' } it { should match('.f/bar') .capturing a: 'f' } it { should_not match('.fooo/bar') } end pattern '{.a:3}{b}' do it { should match('.foobar') .capturing a: 'foo', b: 'bar' } end end context 'expand' do pattern '{.a*}' do it { should match('.a') .capturing a: 'a' } it { should match('.a.b') .capturing a: 'a.b' } it { should match('.a.b.c') .capturing a: 'a.b.c' } it { should_not match('.a.b,c') } it { should_not match('.a,') } end pattern '{.a*,b}' do it { should match('.a.b') .capturing a: 'a', b: 'b' } it { should match('.a.b.c') .capturing a: 'a.b', b: 'c' } it { should_not match('.a.b/c') } it { should_not match('.a.') } example { pattern.params('.a.b').should be == { 'a' => ['a'], 'b' => 'b' }} example { pattern.params('.a.b.c').should be == { 'a' => ['a', 'b'], 'b' => 'c' }} end end end context 'operator /' do context 'prefix' do pattern '{/a:3}/bar' do it { should match('/foo/bar') .capturing a: 'foo' } it { should match('/fo/bar') .capturing a: 'fo' } it { should match('/f/bar') .capturing a: 'f' } it { should_not match('/fooo/bar') } end pattern '{/a:3}{b}' do it { should match('/foobar') .capturing a: 'foo', b: 'bar' } end end context 'expand' do pattern '{/a*}' do it { should match('/a') .capturing a: 'a' } it { should match('/a/b') .capturing a: 'a/b' } it { should match('/a/b/c') .capturing a: 'a/b/c' } it { should_not match('/a/b,c') } it { should_not match('/a,') } end pattern '{/a*,b}' do it { should match('/a/b') .capturing a: 'a', b: 'b' } it { should match('/a/b/c') .capturing a: 'a/b', b: 'c' } it { should_not match('/a/b,c') } it { should_not match('/a/') } example { pattern.params('/a/b').should be == { 'a' => ['a'], 'b' => 'b' }} example { pattern.params('/a/b/c').should be == { 'a' => ['a', 'b'], 'b' => 'c' }} end end end context 'operator ;' do context 'prefix' do pattern '{;a:3}/bar' do it { should match(';a=foo/bar') .capturing a: 'foo' } it { should match(';a=fo/bar') .capturing a: 'fo' } it { should match(';a=f/bar') .capturing a: 'f' } it { should_not match(';a=fooo/bar') } end pattern '{;a:3}{b}' do it { should match(';a=foobar') .capturing a: 'foo', b: 'bar' } end end context 'expand' do pattern '{;a*}' do it { should match(';a=1') .capturing a: 'a=1' } it { should match(';a=1;a=2') .capturing a: 'a=1;a=2' } it { should match(';a=1;a=2;a=3') .capturing a: 'a=1;a=2;a=3' } it { should_not match(';a=1;a=2;b=3') } it { should_not match(';a=1;a=2;a=3,') } end pattern '{;a*,b}' do it { should match(';a=1;b') .capturing a: 'a=1', b: nil } it { should match(';a=2;a=2;b=1') .capturing a: 'a=2;a=2', b: '1' } it { should_not match(';a;b;c') } it { should_not match(';a;') } example { pattern.params(';a=2;a=2;b').should be == { 'a' => ['2', '2'], 'b' => nil }} end end end context 'operator ?' do context 'prefix' do pattern '{?a:3}/bar' do it { should match('?a=foo/bar') .capturing a: 'foo' } it { should match('?a=fo/bar') .capturing a: 'fo' } it { should match('?a=f/bar') .capturing a: 'f' } it { should_not match('?a=fooo/bar') } end pattern '{?a:3}{b}' do it { should match('?a=foobar') .capturing a: 'foo', b: 'bar' } end end context 'expand' do pattern '{?a*}' do it { should match('?a=1') .capturing a: 'a=1' } it { should match('?a=1&a=2') .capturing a: 'a=1&a=2' } it { should match('?a=1&a=2&a=3') .capturing a: 'a=1&a=2&a=3' } it { should_not match('?a=1&a=2&b=3') } it { should_not match('?a=1&a=2&a=3,') } end pattern '{?a*,b}' do it { should match('?a=1&b') .capturing a: 'a=1', b: nil } it { should match('?a=2&a=2&b=1') .capturing a: 'a=2&a=2', b: '1' } it { should_not match('?a&b&c') } it { should_not match('?a&') } example { pattern.params('?a=2&a=2&b').should be == { 'a' => ['2', '2'], 'b' => nil }} end end end context 'operator &' do context 'prefix' do pattern '{&a:3}/bar' do it { should match('&a=foo/bar') .capturing a: 'foo' } it { should match('&a=fo/bar') .capturing a: 'fo' } it { should match('&a=f/bar') .capturing a: 'f' } it { should_not match('&a=fooo/bar') } end pattern '{&a:3}{b}' do it { should match('&a=foobar') .capturing a: 'foo', b: 'bar' } end end context 'expand' do pattern '{&a*}' do it { should match('&a=1') .capturing a: 'a=1' } it { should match('&a=1&a=2') .capturing a: 'a=1&a=2' } it { should match('&a=1&a=2&a=3') .capturing a: 'a=1&a=2&a=3' } it { should_not match('&a=1&a=2&b=3') } it { should_not match('&a=1&a=2&a=3,') } end pattern '{&a*,b}' do it { should match('&a=1&b') .capturing a: 'a=1', b: nil } it { should match('&a=2&a=2&b=1') .capturing a: 'a=2&a=2', b: '1' } it { should_not match('&a&b&c') } it { should_not match('&a&') } example { pattern.params('&a=2&a=2&b').should be == { 'a' => ['2', '2'], 'b' => nil }} example { pattern.params('&a=2&a=%20&b').should be == { 'a' => ['2', ' '], 'b' => nil }} end end end end context 'invalid syntax' do example 'unexpected closing bracket' do expect { Mustermann::Template.new('foo}bar') }. to raise_error(Mustermann::ParseError, 'unexpected } while parsing "foo}bar"') end example 'missing closing bracket' do expect { Mustermann::Template.new('foo{bar') }. to raise_error(Mustermann::ParseError, 'unexpected end of string while parsing "foo{bar"') end end context "peeking" do subject(:pattern) { Mustermann::Template.new("{name}bar") } describe :peek_size do example { pattern.peek_size("foo%20bar/blah") .should be == "foo%20bar".size } example { pattern.peek_size("/foo bar") .should be_nil } end describe :peek_match do example { pattern.peek_match("foo%20bar/blah") .to_s .should be == "foo%20bar" } example { pattern.peek_match("/foo bar") .should be_nil } end describe :peek_params do example { pattern.peek_params("foo%20bar/blah") .should be == [{"name" => "foo "}, "foo%20bar".size] } example { pattern.peek_params("/foo bar") .should be_nil } end end end mustermann-1.1.1/mustermann-contrib/spec/visualizer_spec.rb000066400000000000000000000207501360365612700242350ustar00rootroot00000000000000require 'support' require 'mustermann/visualizer' describe Mustermann::Visualizer do subject(:highlight) { Mustermann::Visualizer.highlight(pattern) } before { Hansi.mode = 256 } after { Hansi.mode = nil } describe :highlight do context :sinatra do context "/a" do let(:pattern) { Mustermann.new("/a") } its(:to_ansi) { should be == "\e[0m\e[38;5;246m\e[38;5;246m\e[38;5;247m/\e[0m\e[38;5;246m\e[38;5;246m\e[38;5;246ma\e[0m" } its(:to_html) { should be == '/a' } its(:to_sexp) { should be == '(root (separator /) (char a))' } its(:to_pattern) { should be == pattern } its(:to_s) { should be == "/a" } its(:stylesheet) { should include(".mustermann_pattern .mustermann_illegal {\n color: #8b0000;") } example do highlight.to_html(css: false).should be == '/a' end example do renderer = Mustermann::Visualizer::Renderer::Generic result = highlight.render_with(renderer) result.should be == pattern.to_s end end context '/:name' do let(:pattern) { Mustermann.new("/:name") } its(:to_sexp) { should be == "(root (separator /) (capture : (name name)))" } end context '/{name}' do let(:pattern) { Mustermann.new("/{name}") } its(:to_sexp) { should be == "(root (separator /) (capture { (name name) }))" } end context '/{+name}' do let(:pattern) { Mustermann.new("/{+name}") } its(:to_sexp) { should be == "(root (separator /) (named_splat {+ (name name) }))" } end context ':user(@:host)?' do let(:pattern) { Mustermann.new(':user(@:host)?') } its(:to_sexp) { should be == '(root (capture : (name user)) (optional (group "(" (char @) (capture : (name host)) ")") ?))' } end context 'a b' do let(:pattern) { Mustermann.new('a b') } its(:to_sexp) { should be == '(root (char a) (char " ") (char b))' } end context 'a|b' do let(:pattern) { Mustermann.new('a|b') } its(:to_sexp) { should be == '(root (union (char a) | (char b)))' } end context '(a|b)' do let(:pattern) { Mustermann.new('(a|b)c') } its(:to_sexp) { should be == '(root (union "(" (char a) | (char b) ")") (char c))' } end context '\:a' do let(:pattern) { Mustermann.new('\:a') } its(:to_sexp) { should be == '(root (escaped "\\\\" (escaped_char :)) (char a))' } end end context :regexp do context 'a' do let(:pattern) { Mustermann.new('a', type: :regexp) } its(:to_sexp) { should be == '(root (char a))' } end context '/(\d+)' do let(:pattern) { Mustermann.new('/(\d+)', type: :regexp) } its(:to_sexp) { should be == '(root (separator /) (capture "(" (special "\\\\d") (special +))))' } end context '\A' do let(:pattern) { Mustermann.new('\A', type: :regexp, check_anchors: false) } its(:to_sexp) { should be == '(root (illegal "\\\\A"))' } end context '(?.)\g' do let(:pattern) { Mustermann.new('(?.)\g', type: :regexp) } its(:to_sexp) { should be == '(root (capture "(?<" (name name) >(special .))) (special "\\\\g"))' } end context '\p{Ll}' do let(:pattern) { Mustermann.new('\p{Ll}', type: :regexp) } its(:to_sexp) { should be == '(root (special "\\\\p{Ll}"))' } end context '\/' do let(:pattern) { Mustermann.new('\/', type: :regexp) } its(:to_sexp) { should be == '(root (separator /))' } end context '\[' do let(:pattern) { Mustermann.new('\[', type: :regexp) } its(:to_sexp) { should be == '(root (escaped "\\\\" (escaped_char [)))' } end context '^' do let(:pattern) { Mustermann.new('^', type: :regexp, check_anchors: false) } its(:to_sexp) { should be == '(root (illegal ^))' } end context '(?-mix:.)' do let(:pattern) { Mustermann.new('(?-mix:.)', type: :regexp) } its(:to_sexp) { should be == '(root (special "(?-mix:") (special .) (special ")"))' } end context '[a\d]' do let(:pattern) { Mustermann.new('[a\d]', type: :regexp) } its(:to_sexp) { should be == '(root (special [) (char a) (special "\\\\d") (special ]))' } end context '[^a-z]' do let(:pattern) { Mustermann.new('[^a-z]', type: :regexp) } its(:to_sexp) { should be == '(root (special [) (special ^) (char a) (special -) (char z) (special ]))' } end context '[[:digit:]]' do let(:pattern) { Mustermann.new('[[:digit:]]', type: :regexp) } its(:to_sexp) { should be == '(root (special [[:digit:]]))' } end context 'a{1,}' do let(:pattern) { Mustermann.new('a{1,}', type: :regexp) } its(:to_sexp) { should be == "(root (char a) (special {1,}))" } end end context :template do context '/{name}' do let(:pattern) { Mustermann.new("/{+foo,bar*}", type: :template) } its(:to_sexp) { should be == "(root (separator /) (expression {+ (variable (name foo)) , (variable (name bar) *) }))" } end end context "custom AST based pattern" do let(:my_type) { Class.new(Mustermann::AST::Pattern) { on('x') { |*| node(:char, "o") } }} let(:pattern) { Mustermann.new("fxx", type: my_type) } its(:to_sexp) { should be == "(root (char f) (escaped x) (escaped x))" } end context "without known highlighter" do let(:pattern) { Mustermann::Pattern.new("foo") } its(:to_sexp) { should be == "(root (unknown foo))" } end context :composite do let(:pattern) { Mustermann.new(":a", ":b") ^ Mustermann.new(":c") } its(:to_sexp) do should be == '(composite (quote "(") (composite (type sinatra:) (quote "\\"") ' \ '(root (capture : (name a))) (quote "\\"") (quote " | ") (type sinatra:) (quote ' \ '"\\"") (root (capture : (name b))) (quote "\\"")) (quote ")") (quote " ^ ") (type ' \ 'sinatra:) (quote "\\"") (root (capture : (name c))) (quote "\\""))' end end end describe :tree do subject(:tree) { Mustermann::Visualizer.tree(pattern) } context :sinatra do context "/:a(@:b)" do let(:pattern) { Mustermann.new("/:a(@:b)") } let(:tree_data) do <<-TREE.gsub(/^\s+/, '') \e[38;5;61m\e[0m\e[38;5;100mroot\e[0m \e[0m\e[38;5;66m\"\e[0m\e[38;5;66m\e[38;5;242m\e[0m\e[38;5;66m\e[4m\e[38;5;100m/:a(@:b)\e[0m\e[38;5;66m\e[38;5;242m\e[0m\e[38;5;66m\" \e[0m \e[38;5;61m└ \e[0m\e[38;5;166mpayload\e[0m \e[38;5;61m ├ \e[0m\e[38;5;100mseparator\e[0m \e[0m\e[38;5;66m\"\e[0m\e[38;5;66m\e[38;5;242m\e[0m\e[38;5;66m\e[4m\e[38;5;100m/\e[0m\e[38;5;66m\e[38;5;242m:a(@:b)\e[0m\e[38;5;66m\" \e[0m \e[38;5;61m ├ \e[0m\e[38;5;100mcapture\e[0m \e[0m\e[38;5;66m\"\e[0m\e[38;5;66m\e[38;5;242m/\e[0m\e[38;5;66m\e[4m\e[38;5;100m:a\e[0m\e[38;5;66m\e[38;5;242m(@:b)\e[0m\e[38;5;66m\" \e[0m \e[38;5;61m └ \e[0m\e[38;5;100mgroup\e[0m \e[0m\e[38;5;66m\"\e[0m\e[38;5;66m\e[38;5;242m/:a\e[0m\e[38;5;66m\e[4m\e[38;5;100m(@:b)\e[0m\e[38;5;66m\e[38;5;242m\e[0m\e[38;5;66m\" \e[0m \e[38;5;61m └ \e[0m\e[38;5;166mpayload\e[0m \e[38;5;61m ├ \e[0m\e[38;5;100mchar\e[0m \e[0m\e[38;5;66m\"\e[0m\e[38;5;66m\e[38;5;242m/:a(\e[0m\e[38;5;66m\e[4m\e[38;5;100m@\e[0m\e[38;5;66m\e[38;5;242m:b)\e[0m\e[38;5;66m\" \e[0m \e[38;5;61m └ \e[0m\e[38;5;100mcapture\e[0m \e[0m\e[38;5;66m\"\e[0m\e[38;5;66m\e[38;5;242m/:a(@\e[0m\e[38;5;66m\e[4m\e[38;5;100m:b\e[0m\e[38;5;66m\e[38;5;242m)\e[0m\e[38;5;66m\" \e[0m TREE end its(:to_s) { should be == tree_data } end end context :shell do context "/**/*" do let(:pattern) { Mustermann.new("/**/*", type: :shell) } let(:tree_data) { "\e[38;5;61m\e[0m\e[38;5;100mpattern (not AST based)\e[0m \e[0m\e[38;5;66m\"\e[0m\e[38;5;66m\e[38;5;242m\e[0m\e[38;5;66m\e[4m\e[38;5;100m/**/*\e[0m\e[38;5;66m\e[38;5;242m\e[0m\e[38;5;66m\" \e[0m\n" } its(:to_s) { should be == tree_data } end end end endmustermann-1.1.1/mustermann-contrib/theme.png000066400000000000000000000261701360365612700213610ustar00rootroot00000000000000PNG  IHDR.X9? iCCPICC ProfileH wTS-@轷{"ͮȠ#(:TGG@ƂbȠ> 6T z'[gg}V>ZO(LG22E!n!PA A txB& ucqMtB >|(-e |cYb3sWV q@tO$r1`uQe&db,O%`|c|߈s22c:M cj c/[}O3sg_3Cِ&9cJ $bse a%v" yjWʆQW56H61j56"e52LL LL^jƘ00bfmnv잹yy+  E Kzn˗VVVn[37[YٴLjr v;7v'>g!͡ar;j8G8NqN?9:;h$4u҂Қ#;2q3e22._>,4 GveME+KVtgbWrr~uʭ}w4_:?3J[W>Y}kl\3um:d] Olм1mfo6En)T)P8mEE[6moI2r랭_/T|*^Ǫ%m,)۷=s;˥W Y(xkٮVusvVTuڳ}ϧZvw{^粯NO)?ݮliOܟ?sniTj,iܔ4bn-kCrڦ:qݸQr~K/7;=ǘNJ;Ε]]8䷦'jNʝ,;E=Uxj3gg;uFs.:vu '._<~{+WnAΫWz |uo޸J@zCyo:}e& 1-b 6׊$Ky@"̚{@,;7@o>1ՉR,"rh}/Ty6SiTXtXML:com.adobe.xmp 770 46 M qIDATx]xř % A?A?, G.< Ujݠr\,jc+K.=Z\ㆶKjI˦ $֍6D6]ޙ7&@37̼7f@@ |^E@@ $@ (c14zQy9皳ioO;qJ[F}K2{Y_ϡZ>ɗ=vh[|r Jƺ{9ӯp `PL$g@o{C19ua}Dq|񘔁ɼvVC J рۑ(k$1{K HLTEQ4%R:v9,,3wC"-l 5;묘fPkƺk\jG%#v-hCcF]G~ nڂH7>1H,THƫ%Щ3-kThЦ]L48EkWnDd@eҬJ6łn,(_9b ^N7V7LjH le@0Tq X=쎬#l *C͝!$C v{TNlR4^SJ2[WF,g 9YY)$JgYQf#v{C*]Onp< Uj$B=QҺo!`7t G(D 'lz[/71 Xb 5s5CzuԕICG&^9al-*wUէ 49i2D:g39x$FۈGjKltHIup HzY\!A^4Q=٧ KGd{NVvʧӠ<# ťӧN7k-_l˾/*Dg-CiVVUku-DH P[f!3fi)Ypi=IMdGVh@44PKk6,#El۫kSdo;mTYkcZ8flm/_խX}zoXâe%%%ko)AcfNq$_3*VvYckkUf{zV6 Ƞ8?U(v/ǡ:6Wh =uЕݵ_)F)JL{wmA %[Rެʚ"G?uGCtP34V#!Vܲi uV{nj"y(llCa\5LMru # %Ik#GeT/\ GcZh6?G2;+R$ahoa$7(MUY 9%2bBlG: t<t%’1*efkmu2(n77虐e awJ*+58֛"56 w:rZW/{,Qu$F/з+$tVNޤwzp'Y[[I3 qȳ<G7#w2ca&a8tNC<{ W 9շ$9_ʹ|l%GXJeS-Jb2W΅X(Ȓ ߼wS77n:>Cu!⠧ګ^]ˇ0ݣ1ț6oBhmlI0۲aSJJ=:z7dcKx85 ;L, IVo (WVLvʅQҢ̺,NiE( 4*eIR$jfusEh+.Űue#NuҡlN?e2S>Y674٩֒Q01VlX@Gc7 VWUKDKޡC|$_|N!D6YXeU~<2IMwtr'&0ea8@|3Բ45y.uWe!F XiMdL'Fܞ2 (;Y1]Έ~-]B²(CdX/aJٲ'T83thjAc\>Zsa?=!`&Ϋ2, Yy)_z\XBYF>nȍLkn =>m,t;v{܃lALC a{Q]~TFW Rʮ4J#0cƍ$V+^`;HwH 0SxڡԢ!EJM) #+)QT^{+vHH,h(Tg1$U {mOGw(TȤpg[)zV/m ׹$h Oˮ?G8=HjpV C Ql ɃX sY, `!5m^K<)'2QSLvD4(,@KuGN4joNF }4!SaFC 7Q&%)ҝRݝᦦP7"]< &G$~,sPBp]u6i;K{drh3ΖQKģ݈#nj~q9j ť˦^V[<&Wښ1sYZ-Cy2q9dU C ?67Z ~[mV*Hηg2,ʴ;IOJд%L8|rF_}Y7.aΝ3n^YKo,ZgegC4XU+gР`*z2Ш@VCy?D# #F_m!l :Z> O6&^l 3dfC pjPG@ACE@@ 0@ E@@ !px @@ `p@@  qq&i`t5Þ7SG#HT(+a?BZ_ʽ{:X_#j @"`6d '#ߠGB| =1)9 l__W|xFBŦ"jVBpd(ky3l1q;q7z=1%Gu;Zu-rfT(w^.KpД w,~f 2Cr遽G`0F&@@ F m pS[>L MFGAxOpgOQvR l(!v Y'!všzDMɟRX@@ 0rlrx7]03m̃({Kߠ͉(tlo'YR70QX6[f FÉx\sRDTO JmU(U @Z8 cͯm < p/4F>XL}  a|NKn%R?I[n RyJ^ tqȷ/č@@ 3A],p^eFd#yv{[j2#rҚ> !{OI9 gIo*^YQJKf&Jw@@ L`AOR{dfSoL^"-e W\?|d2z-S$:K{6-%늤0n?o_ZI_ѻQn+׭ RH/9ӧɺ@@ <)N#M`:߇zĕ_KNz|x 4T&xu8c%0A @$)xymC2o2a/8@|6GJWW =ooo Md`S$ljTeCG`,Ë~X 덎M@@ #<tUI0Tr̂[? kn G\ =}0a]Cy2kw'4Vl:XE'tE gk ڈ@@  HY$9^PQ)o>_Q)}Axn\wt4ëOB`)QpL!??k.[)4*@ d7(9y? 5Wwç1H8·IL7IxP8enZYe0#,PxYUAc'b@@ |E8;cЋ @` 4`=@@ gbgX'@l! ܠ+YF@Agx@@ ۬_ྒ*B3T2@@ wز?IENDB`mustermann-1.1.1/mustermann-contrib/tree.png000066400000000000000000003101431360365612700212120ustar00rootroot00000000000000PNG  IHDRb iCCPICC ProfileH wTS-@轷{"ͮȠ#(:TGG@ƂbȠ> 6T z'[gg}V>ZO(LG22E!n!PA A txB& ucqMtB >|(-e |cYb3sWV q@tO$r1`uQe&db,O%`|c|߈s22c:M cj c/[}O3sg_3Cِ&9cJ $bse a%v" yjWʆQW56H61j56"e52LL LL^jƘ00bfmnv잹yy+  E Kzn˗VVVn[37[YٴLjr v;7v'>g!͡ar;j8G8NqN?9:;h$4u҂Қ#;2q3e22._>,4 GveME+KVtgbWrr~uʭ}w4_:?3J[W>Y}kl\3um:d] Olм1mfo6En)T)P8mEE[6moI2r랭_/T|*^Ǫ%m,)۷=s;˥W Y(xkٮVusvVTuڳ}ϧZvw{^粯NO)?ݮliOܟ?sniTj,iܔ4bn-kCrڦ:qݸQr~K/7;=ǘNJ;Ε]]8䷦'jNʝ,;E=Uxj3gg;uFs.:vu '._<~{+WnAΫWz |uo޸J@zCyo:}e& 1-b 6׊$Ky@"̚{@,;7@o>1ՉR,"rh}/Ty6SiTXtXML:com.adobe.xmp 697 504 Pz@IDATx} \TGwi/ 4ЬbPFŘ\2F<}y3}m3i[D1Q("6K7KC櫻u^fb_֭{ԩԩsN_:B!@A`x@ BC  B!` +BC B  B!` +BC B @ f +/oe*xMcE hS]MU 7[n(vNY2 IF< aWJp,#B'U eUh]R:k*`0q֕{uնߊxW B!0Q7th fe(P`q``s;iaS%}0Gp;,hՅUSWPIDBQq~<]`ro\1꒸"r^ÃA[=HGҼc= P=P(N X݊V-yKA ,D:8>Jl:.?g!_X.f<țޠźs']t6Of~9JE^7-]ljV#ݙyh:Zx- t~GDu[#e/QB[so M[=3OQzN#[3?QNCxnx 8VC ⬟1bdz}'iz3/ Ety %- #=XN\YCJWțҁ"quRqu{ظX^ێs Dz{|2U-\$8vLL^҇UH/u6:T<)V=&OIMt`X:qSMfiuVdlO&SR۟?h$z;UZlX_O搖IIU5(EBNY嵼#JRN^: j@%w aɼy~xv 'uw%噄$~4t꒵53fID\acwTV \.|㹨 x TWsTI UH"qS!͹! )|wlM w·ۊI/pVd۟.=DP?ną :®N/ڟ}aud0u$0Qǽ;g#wy%VVz.Jn_25B!0غ)a'vC`$#(.xp k9\@j84+0w2NL]U@ӱ*5"͎D/lq~!񫹔",ό4@),ͬ>||&7d7NCD0VV+>|PPݭW g >>y0KkqEY44x˙+qD4˸e.AT^["BMù r/>xkx$^hnN`R!W /+ !sM <_e^`X?%\NBrm#ijÏ0|~Ӟ9mRz.H; Ūui۴WeD1DE@^?τ!:*T yc)B7P-s?TC~m~0gbAyZ,CPJ." EJX~l,ЮHc]\=oF9u7O\aٜedqy5>v> oP p֩u!\NO[C-u:z^ӀK;(nKd}A7?]%*kxTv(%vq.jvZGXo`xzD YYEIFLZY}a\W`n_B* {{iiR}<6B(눈]-h\Joժ@W1EI #yڡ~&,'ZQ PZt S e}YADt-5J]tEw KyJ%`Li#5v#~]6rm{;Rtf}abc[3[ hDPs>\q6񽜵NJT.iJTI"eO#kʯ#5 +;4Er-I[KxM4uGr3E2ꑴKp/;' |J*5<\Ӈ{EGJϏl2Q!7hnhř@\~]leq'ʛ~5uQo形N~;0׋7|aͱ&>gA']X&"QʌM̌S:ԒX%NíH7j g)}{9qZb[%|6u4̲äCQ/E=!rsE^,uϴzogf殻t=nvdêKڂ^_C`Jj"P8_sO$}Xy-ѰP({ ]u\aX>nuԯrV%:o/@۫fŐQSR<#~g_s< u?' QE@㆖h.GMˇXAGVaj+}>c#;:5wP3+In6ĬWX%=ku*Sm(m&mLEyrّLDD&E_,9ME*ln6CdML.L7BT:z$38qYjeGh\ҌuQA t%kNt 5lb'qNW^G^N1<WSQ>eM5:PēT ZnWm\^kH6lhͺM/Y+HehHMt*ژݝ0s- .Y$zdžIF!@L(0SLr6wfS vTΔڤsÊgm0kO_ቯDG@P(9pA#ߡf09ٟ c+=(@ &;^$5|]aءB!x@[< O!@ G D ʈ@ #tcR"BI@ OSFeD u3-VL#嫉xL!E]g MNu?0!@ F Jr圠JĂ8m6jèO9u8cYLru^ >~:(@ 㺂\Z^5 y>Iu;(e^߉M33<2'0 1B!@L 0)L7B,/ 5D B@ۨB B@+h1^WSx@ kT2[ ꦲ6?o }7ru==(Ttb֒rY[^RU{cD;.VQm۹2&v,Ai9]B!@L\W;3N;]%CY෷|uP?AwJ!rh쩠n00xsݨW/43abNw)%L|pg/0cH9պZ'@n@ $A5shw 5R mfS 򄟝|FQpYG[H,`{ٞ'w{JJUuўAbCMTί|oq3 V 4#9r 銂UқDID0B!@ &$zr=)*IO}0'836/[8҂> K~R+hzw$7,½1+ sߴS̴nZeO!yI,EoY|oEopp`B,ğ$͔&c3iQn;3h¬GB"0PzT[FnE\8l6eMGh{3\h;ɋ?'L۠=/T D6-v.U|B_Ag´ל݆.Fhe]ɟKW]@ܵ[E 9-S%|@YI􋂈GB!__PQ^zE07%MVI!KeKhauϫMg-|zBq_p X.KyT[$@"!gN@DB!@L tElf0U>RGKe4Y7-$;z@;uV/Ȇ?uԭ[3 I:( bJtMz|9r . $!@ #`\W`pRVXhPL{S,0%%l=t D> '.Y^񢥎*0H0lq,2ky{ɲ A @(?4 {il;/;FEi86j$}|vQo{3] bxEb$bS,e~$ʮ{]#B`!`tkAaΉ_n6~gȮP ?) mB_?za6wgO$%QISSp?w{j$fjEF[ ﴿*aӉpBM-hD!B!@Lͮc R9a9AA*QHT'w[O},7U|o1 y1 JyK0J)W20˩ jv.־H7q rB!xeϴcMo e\'Q(44{Ef=Cٞ<̗ 4#B`! F/bh)YIwi»~ #%E B@(Po/DhNuP!@ QD`t!=,#)FVR>L)B!@ ~-o#0J &9:`G/Pgi0:l-!@ 8 ,1b@ x"0|B!@ HW@u!@ S ]:B!@ HW@u!@ S ]:B!@ HW@u!@ S L!VP*\]Vۯb*H?;3):]@'e"<(W[$޽6('YR+W!Q<A.ɱ-66f$#o)= U-B 8{;Oˮd+ms}jYRuhx|uf唈qT,T/ÐemFo1nKi7&RP. tPEhحx|u(Sƺ<r ``{+YP$LZL'3CÒpa:N) _ڒ3[Խ:`1aVx_\lCqS{_87mJw&`G{ mu̔PqB}ʺ%|< # M?G1)ڹ##'bwkuЉd آ#c1%Wp/0Æ*:l_Xy>- Xq@QSǴ[S,8xJ']Q o`,bDW0R g@+<.u!y? m(@?SV3YE?КI2bz&Y_`&&ު\$k}sڣ3oUزfgQ٧&*qJ`z#KpC}Z ۗ?׍`O\fdKU搄Bɞ+~+!\rs@'(.h6u]Zxm;^“符Wv5lv6t;*sf.]ywĘn6/0}fKm8Ύܐk7eIui՟ZqJNɁXl{ҕjڎsiaOs& pK\f@25( !ę|9ghY{\ X/\&;5C6j)7>wg̟3dH5]-QyֱK8l33~࢈]$ܰwInܳw*D3}zH䜸@J2XȠqT]]jU* UH GEW.)z W+bٲ1n'T)uܒőN⫧rrxO~PBśaqJPNO>-,όtJ,07:D|_˩BRYp]:;8u3uW!4GB` ")1=E5Mtͤ@S?Ҙۆޝ3`v໼++@F{SM\qv,Ͼ:2kid,S (l]ₔL_ber<NDh Z A{(ڀ[xgm=t' L3'l'|_M{*545m+>>cv4-ţ^J?+L\E!+ߍ 5%"8q:rj޽|j$-;]3ԨÂzu&)-!z6ѽW@M;JعA:UTK,:jPHs%.RД<]%ў8`.-8uSa(X܊JY;/lj0U29ujkZ$d"A~5΢9*&zAk Q7lMHN1-N^$;^KӖ׊a^z1µi JQ`Fo:흢{@˯TZQx;)$jPx:SDnxcAtcRell%.od.I#J= >>[0h0~_C3Ҕ-lÌT;fTWJ$XLY+<) ;|lACP%e.rQZrbyD YL\Ҹ0naWt_b2 cDǁoǏ+Vq_*dk<6ۋx4ʈg:[ {$UtE!pztV܂WkK`CSgBvykWFg?n۔4#:dnvil|l{H'0*/~Z+ + W^myGI>zc9̴P|ci}_?u>Cl[ i./- \IYc'-WZ';fY\C@OtSVx\Ӂ|a}CI%$8wKPb8xEM V!iC'Q["9ߺz{Fwnۖh\Kcc$6/)Ƴv)9)_;{S24[K:ausЉK%0}1Œ,T璺dW~DO@Xfʬe/ 1NG`kOI #}"/2D5~_Qѩ&EW՟v} lȇC)ĎD#2]t3>4Pt43.'#B@rKh5~mvf9k.LJcc_RWލ WXmY˳Є]ۖۓi/?#髣8M[pwP̷Na_qo?HWm.!9(͖)o~IՐ}0oglߎB.ޘLݜM+N f/?(i/2*:yx>kgJm/H-jƤ5Y]V|-{%AMhKd;`XO* ИI) ;w߯4 8C"oę\vģܟH7bi?$oqeQt:^490q٫U}mpPw"B3XlĥgɊI1ϥ<-BzOǠuIҶd~PpWJc i|J 2yct_" z%|?dtF8߿d'S\\i'<T֥m<y$}P{.W75f!vitx/şg"Iʗ+]R^ԢS//^wmpNw|DaV6e-[oniƃ{d핲PtSʱB Wy 3 Xi,Xyڳ,W]!e LY`7x!$4-nܞ ,('4bg C yǿ{1xF:VH6{m9SoG8y-6-8D'2t"iͯ!@դvXJQP{/k %|J53J#Z"bq[6u5A">)Ņ3 ˈB'ӡ./B)Shp]=VVF:c,nq:aJ$R `LvPÎv#"@3=<3a~RiDfOSHEFp)VXOCSX$\յՑ:"bW)nV4UC+ ]L`JN-Bt0ڥ#yZUT+1?GҢϪxqx jݪ{غ<,O) XN]B;tE7׶ҪZ%v0dCIpc* Ld>6}rZ=k+(MK9FY+k'G6 uT)+8mi̠nj. I`8y ު$ҳ",!бHg7GWQs,CIBs3}E6:u\3uEQюx)v}$AD{HyeҚH/+,Z>&|E5v+$t`ݻD[D5kTʪ"CuX0cDFXRC;eY]N>]7fXVXO'Ύ?UQY^tRB G1ӟ4`|Pn!4!Rݖ˸%Uw%*G6ۊdMw {[ۮ 9ޮһU0RT3ޙhPWW]Oٛ;GD&E_KN&ʉaL˛k>(~;*6WK7kDX'<Èbf]\/Pw ɘNxoi]vLso.<,'aOY o|۳Yꊯ2-u`w|C.:7ݗ=U)j˅FnH"L.m N6Jw.hŁ9E.{#s%d|m./·`n.Y*8t2$!bqœjlY~ґ+;|,|"aS<8X\|U.;{:3U;?_p;)AĊk[sB\i44_ΞwIxo\a!#f8| J+@IDATV"rƣe͚S^ wR~lŐ)-h}y0TGNT#ޡ0qX“dcn_zlRYq!mQ>aGm7Ns4QtGNl8 {w- ~;uj*`;_?o[ڑw8!{w. ov#7 ?s Oi-ڦ,Z*l. wna6'л~*b 3{Q6i /YC),Bx5ڷ!hD>!c~FV#}>dώwL*`}{ `&'&].;wPZfz `}sm4oYstF՞0NeGKjWfVeG[~ "{j2|)0 3\tomĭL2Q_D!Qte4N:>wu4~J Zu@SKI ,4|Uޖ 0 Nv6?kJ拫n;Xv0nؔ+|zhn^9cec*(]!aɚ~Zꝑ(!(]i_ 7!X o:_t#`=}k r]~S%+*q˜O;+įPłKWr*+J^{k2,Q*-KecjRƼ(B`! u\"Ÿ uq_Iz^"!§F"sqpȥZ̑Xq  *F̈́I;X?->{˴odD`|u0Ņo2q/MlIt' Qq!#Z95C: Kh97QDA.SR W+!>bFIFB} G%<7/_w18 :O_p"bdBXVv+SzFQ`/$Nuhqv͓(m~y77Cgh]*_ R?v`^4ˋCLT2 ~eC{kg 36>.&ܙ' \HN#tqD<;40[ViWS~U͖JݫS*?ܦ< aWJp,#B'M4\g߽3ѺhUy~K@^v%XiS[ߊxwT+?S"ƍ֧BQSTyXyw9︢辜_>̶_ښ)Y[+u6e5O"\eJJ>x &:]piFG)yK7۹kEY7 ~TUy֗ϵCͻވ7S˵߂yȪש+ "Y!(8Tr|?ňZ%ȼu}G)p*5q5H* 2^$ d/qk]j\ɅꠐsW}l$A+5n^ =ί.XLhHoN->w7ǁ+ Ê(fJo >~<>\qDs Wy W5OВ!4MQr7EO܉.Q醇Ya^TU>qbqP ک(0?K裆j뒪D,`}]̒8ӏS֒ [Saxv҆6Eq&,ʾB |"!>Y P]x1gD]u j1 ɠ 8f wPTQšCʢd?~Sbv2>/yX<*?SXsvڭa.R۟?h$JϴJi.n,ꑓrJEdocG7^ <%϶ۉ?;V WV3Z#01y=HVyRn~ ~`xӫɔyNZzs'Q-$ZC4Jj!U^;d;) GQ{ZUrٯPW$͛ó[3v͛ZX* }< b'"yw̿HH@a۰wInrU[E7] Bm8ΎX憬?]B5;}zH䜸@T3`RPϛʎX\tpE?ܼ"-kx+U `dq꩜:x-`%Zp[1&UOG".m,W'詠͸^]wꝚVhVI£{w6_G|~໼G96Z)3pΥK%9\uQF,.8Nͱ_?Ȭa;SjhBuE;/U gf9O:4t=@D4f0Oaf0,07:xsWdpD5{ö.~qAJhe&zR&#.h8yDnhn˒ Q\) o5R_˩BRYT;8u3uW!pDɤAxHj.5h[zsJji@fh9ܭWջ|PPݭW gl̓V)3S/`&ڦ \\ADq?e\2^$ե.4Qc*dk68r[Q)k8Qc9}5a~ CԌlet]A~L)x <ͿV,|{g09j3!0vn3L* rN;;@?/G߰}7qGGt o9uMo>?$\4V 2̵+zZ*r1A]&c Q{MՃE%r^:`Vb3u+${+J=T;3eQ:B$35?}6h- ykOjiBjl[PAi+]/u) -X%/ x4e~{]O<*É SZQx;)$jPx:SDnx߆ wY]Qy`OMs~LPty!kйTkhnh/-0OFY!7P/,=Vi>V2;Ff4b27z],Qn;9-PtMz."|l>|Jⶅt\6lb0^;ElTgKihV iOj}F뒬1 O&Wj >1!E"ݕMbN矘FͶ k6%]Lk"b@Ds|ytWIRr/Fl߱62;iqًCw0X~|b0?wiI6԰j{ڒ=6c4eTkA-C@톖Yuwcj`oB#Y҂xF*Oő3 3Q͇bN5)Af NHi=JD|9Ya˟K1y:vfE}/z"YR!uUwBRVh虛VګI!N&꒕nŧA`ܲWRm?}X-^ڕ$+ѼvzRQ;j^q{7.?V_96dR/+#02:> ȅ-=p)@n?ͩ{gNԎRp)<8+"dǓ?nWx;:HsƕE|z<E܀Cl^#4IFLy6nCD Y絃pJ@g$UKJRE;ox \vgbvߺmDѣD"ίA^[ELԝ1k0\Hx#F?<{_ߴggĤ =;rrɊ=NYEd}jĭ~m ]&$ EJX~4q]gsWt)ư麬Q f0jCu>T9̼zJt~|(nۍs7s1?M/Ӌdv4J XWϼy ,# Pc1^V ;τRRh_‚e!U-1D]b뷏qZ|R&Ww|b. 9J_R5!e/lvo +$ nܞ Ԅrd*d֬u/!ޗEudv 4Kll"$4q-.cs'fs̼̋yL^2/O,3g4Ѹ1nA l , [nl_ߺUNUݪSU._ |ɯ/ .@5S;>h zV;MfEfHTӋuğt)Tװj^ Sp2ts *ujՓQ?\Up2WZ&LA{k0`A8u zJ0+Q(`(`~r615ͨiߨ'~r)% 7U%MFWtR! К9]4P{ P{.R 1 !)wk\n SːPAN2a3fSw?\ߠQATr.}?t 1BKH]9aL \n~/ ieV/F:vkK=R| M fl0Rmfn_贚>ؑa/b*b=93w&A&Up/?65ܙq2`I`t vHi5ܣM4ZnlΆQ^⽹8rO6*5,h =LJ׃y^ ؚ١Ht2:e)|`6ϑO]H0odd$&M>W[ᚡǼSO8Y@떩A8 [Й-18YcHZsL AN2sJ3[)&)k WTUF!..a>#p^qB&%[ }{Ma 7б}m],°Z.&W9? $ RJIȹA2 ᲈ\A[I:fzwlu{/&Fg2iQm{yG }TLׅE/aFX,kRT)eSsjfRǀBI[EϚ8f\70~:tu:IJv:j̅-S/@O׉RMt) u^d]"?77˜ Дյ/LnINJ6YK&b= os]b Es$yO1 Oq4l(AlI!%Z !a0,=vabj  5IqJ0ֲp59ˍ oR`a(p _ \G+zR|j:9lu^H7r]i&&y>^׻ Ǐ}I^櫮jӕU7ruVr[ mYF+yTAZDl0[,wka ~o9IH2Ϯ%le͙Kr4= 3eLR&)Y,|dVQ] 6t&3eٓ϶5}~^!ZKHghr{%@1ŴA`^" L-GF$rj ޽-vg/Fo$O:7VN3藬 sNܦЙ%.hh* ~cGi}kRxn@5-]}l"NEiTYK5}xLQo@SubBkYShs$wJf)B67ܫMCv}Z'ZZwԠlB,hu6fhTxAW"2IOsϪay;Ki\&t%B^n+z-C}95%g'{jV-ObœDdUN$r#5a#('.-Te/L܂ӶNIqF VJDݭE@gٙVҋ2]We0*JՖ#Q59v 664nm $[ҋ3uyǢXI]Yuwp9ޜ0'V`c'b+s}i4On6߷.5rkNvP'NZi;_DcF}6.++'vb0y#+hNaz|=Frmbb-.J ގngGC̊B*|0߀0"5 ) -qCYzKpq* iM>x;Ywj;E8|6L{/&˩]çn'7؋ɐ+4uus'IL~ZOIXqFdZTy 7?/\y;^ա&j!Aa' ISo\uŪdҲ[ELp /QSMLjEe/6evXQD -~Z>Zn}ׂemo%A&_"mvݺ__JΟ v ܭuN~ԗ Ze}+ Di)5:MA0{h3];C8ҫW#8L<˨A<5#g_R&G&;Dl #-O}s NQ˔Ppʓ|+EZ;M;׎|իS a/ɸ.h4}kRRT|vm]S'ʕR\r5nn1r1R' ig @xL`|Po: W٩i#99G(̀/H4/u՘^SBÕiOV)L汋u_%L:oy¶#4=u[0]]>=d%fE^Z'L4OBT_8فםkpAsU(DsɯZM 3J1wJ ǡ6'F?ⲍ?瀽I?+39[}iMeAF,=[ ^)~ vPNFM.)O`GڸbaMh<5&俵$ux^+żk0 ּz|UT74&9# 8a 5}U2xg =zqg?ุ]I5򮦶NY?`2'g7#}`ϿS҈oo(=܍[~tH-݀稑3|}\ :ܺ®Ojs̲)BQb@ `ő :]dQK2fxqFķE^L1YNw?i|48FjUravvEkHfl oem\ҧ,DXP#kX*pTe{JFG>0DCNeԊ5Wn43W0Ɍ \|.h}{"0!r`!Gna .gM} buyC?LhLjNN&x'L]) s2%G<.Xlw+%l1FPkHّggOT kh8F&cNOڥOf9bCfFg֐kPG 6#V2v_ҎNE'8>QjffyQC+"EBx4/<pdi?`a{]AȯO[f5A בČ!@  TVJp'IW٩vj`lu/?n`ƣ7A](!@ +2U*MG O2eObuwOgbz"]>fbrs!dp}<8Då8 s&X a=P Aqӱ Gf5m]2]EVB |{8G_M1h />P6t|FY~,_5bn.3_m.@E]sڭ%y u<0JeM|[-.Qs Q ko Cus e<@Q'dsMhmRiA A3͊;OY`Q㨆 _y0(8=T(ˢ :d!:ѐ5=9cf :N/l`+ץ\bʪ($E;g'IkӶL79#b&#ւuX"_l!|}CT$-B`!@ IUw!> Ce~+oʒɣ\k`Mc@Q_Si6='+ h}ۉo ha.g"9`b+<.uK'=}Q :Cy߈{` b&pVsL%9`sJʴpWZwJm58c (/YE3ŞSik`'i{`*lj|`]MVf3 +jZBh' II]ͩbpx R'Y79rUr̂Xݠ4٩N؁i I8]W[x1⌼-ȫ1#,4?rȩ}l k$4%kWJUˑU<~9h}iOY7HIcLW53.ȑH %MC*vjXٵ 'wcF.^<'VB p̏rT^<] 2*lɂx=|o1&%A!@~峫JXwwsSoáyHtMuΦCi}/%_ºq=`5l6"hE1~k yxx%ƪXD VռrYRN#%R{o&%^3 r2*C&>#' qjGQG\^mњ 'Zx SӼ":O!h{DЪHu.d|)9 Oҟ >Qu$(KRix6Ȯ 52 i^(udyRs*펴Swb453t]Lu#魟.0P".%hڬ`J !}[Կ%^ۍM2V'-SG8X.ߔ4%Kʢ>FjtݨxU9bv K(ƞYt]A#9Ϳ2i6D{T_OīQ,PUb,.Ίݟk^a7~4zڗZM7-6ѓ4Q$5~WY4mipo1LkNtӆT-='lliίiߣ5SiwA%?t%F X Ѭ?e4~_}3u9#hACUSXF;eeZ?(7V+PcdYL^̄4u!-E>nRhtXN%:H+W_+50V+Z|Gӂ^kƤ~w?@&nyϧ+ ߷`wY_L{JѼ_7a4}PmFUejaHxYАD6|o/嵕\t}<煸D<l%*\ʍ9|<95$6PxuQSVS>V[ ^[GN,AV2iA Ij&3VӒ6 ^;Rn.47u (Q/™h;?Iw%pN/?&pۼ2.]. yR⡻vo1bi W^S// 9_) *:dc*0Vm&5,m ~fSDTd1Qd& EaSi*Md_lQ1Z34aHKimT}*aTm' HMa;E Dp&+H'e/9AhLHWw]|S{䜔XwN2>>ߘW:Hrׯ^ % JY`nyPf(?-s$57nXۙ|Lp*>𷷉PkXEfuf9(> x;.13ShIr#v^#gx1nK[> VPEo$JiZfgzaoh U`%lj˂u 0j?`%w03ua@HW08pzԘ'&oE{K][(`~JW(qy>E܏[6X,T +̗{)X]dymIT4ohT$u%'L"5CsX*ig,"1P=j:J$%(oCMU/EEmPw/)Ó55i/k{-Q 9PQ =0eM۟#'3g͏T4MV%',=K姹UXC<]BS0?ÖGFfj9Dm(&GO$J35XOΌ'_l#8݅+sUXdʍզI5у.p H:b~/01li)AP!~%Z !FaUJn0y( =-`imN!5DR9g@oΈ :,5 eyS'R1gWnlA$fƾ$Am)-ZV1;G͂NՍJJ|m,a TC{QzKɺŝ(A_!*;|mR^ ?uYփ } "!-8hn޾B83q 9Y,|VQ] #c3F.Q2`S P﬩P3}Fvv'Ư7n&bwM[q.A4ˤ<0#Zh Z'ɏ k,6[4i|욭VG@$e]-5+"f;k6T"Cs繑*`/3uAb"gbh/2$ @r4]tlqC_~{2KCTR@ǁop !0I&!̄fyPSZw4є-jA Ksk#YC!唁hTU G<Tm?!ɱKLO`33UצmZ /\l*J4bX%-T$Lb\#ggdA!x*cշGWe0*Tmy΍0[+C(/x&M &2w$%{|ȭ2(:gܐ{1'ym}ַ4bfekRل ++I g_mq1Fϲhde%Ubm#g`0uɈ ̓E0^sPˎ{<@Z/e"plZ>WiJ&d͉ #+(+.J ގn;cGv(~ kO0N@CslX<^( /-;uo_Qq#xq}ͭ-Wn3y'/raCTI!UT'#ѕs̚:;~(t^Q UsOӯ v/C" Kָ+y[m}(ܙ쭭uz!:|;nޭxNִ]:w~EL7YS#U-N{N2Ÿl(Eg螗sz:3ɺk*+==2*b#N_6.[>V`IJkTk;$wQDt7T\+:5T3ݦ,t}C"V7R|E@=0)!uVN~IJECm`Q:x(*~EJ7/wg毿|?#fVJ06Y7We&y9׫}dz9,oT@C坁>lf~&N|N ; oB~v?&ƎD]"㚹t^n,r<|MBkq`{ԫM`JxIW+04Ҽn>g\w҅w$D/6(ȯ*/Gh7"iic)34_/kհU*eGLpkZ*>BX׸"0]AYsX "Ԏx}$$?R "G1@qY~8YAQ"}] ÄםkV*{sU(DsNHZύ6&(b޶jӍ"_wę~vgZrx:k;L ^93ȽMoSkG$~iZ ,*C?TDpDZ+Zmć _&ֿGqe9`4l|ﱶP%s[84zkM{#nV z~n|}oft~)`i_24M4!O>%?.niݥdƾ>$y+rW^{*K$wԧxF*<0|Yl__7y0<~ E nJ8\GpD/HkSciԒ⬿c_Kc VTZ:pAE' ,H0&d:M%N\@LI4*K.u/qfWc}S 71*Г>OȄ1sؚf쑭cڈg2ȺD ` ݹu)W59GYJ'p kʔ=-f0mdJ -ؤŤgE2@&JѦac܃l8Zt'-ĭfxI2P,Mk{g `0Y`Ѝ*[XN^B zS젒W3s^bS(dum=0qwq* inzzZ)6ݗޕ\+O 1-Zکv`LR}iMeAF,=oXmG*BGgŶf~;D ;]j2\T~ci+{WDYJZ8%@}"JI ?`<<\m\m4J1g)ӟ%\,df5`M0^|]2R\/ E0F)L˻Q72nJQ?k:$ns~.Z|n]a'p-fٔevʱRjCxRf‚G&詡vJA0Ϋ%uW^3evSf 3o-;D'|:6ǃfP9w!vU7f..<>֢k u!}qB;) Pޡ90 #E@q2'z Ukmެҽ`ёO<Uc,͸<|WHO0xe+#Niˬ jX!JB! TVJp'IW ]\Vp . KߍEfncyu V*@ D`xu%)'{ !L3^*@1\tB!`dh@ +NyCG@M%JzixPӗzȚ3w~WtLquԾR}u=wbatʩ4Mj4@B!@<581o:(vKjHxߩÃS:Eq+t{,0^tTȨ]Ss)]g, 7 ~Y]FNwo]Q`'9rIZ$@B1Ch;j`0?4kt8>wdS`{X{I>a;E. U❱x_96qԂ3}/N`Eo]vrFA9/ ? Y>#O [)'s9_?:CX!*D@ *cGW(:QZ_Jm~V=9?;q6yf"'%Op Q2,"mZ;@$Jۿ_v_[kV m%! _uR=[ޅOȍANM]z[9fIɣI@EB!x|xy0fpܸOΒNjM&xk뗟GMigt`HjXS)Yy3|hLsuXEd7+m7~+]Q .'!-?y7 AB! `{| SUq"D)͍zt/uS Z4A'[J0縨l> ƣ_'X{) 'BNJl Y'@DwB!x@Aq;,hNGP& aOt$m hMho>wd`t5G{Mcݓp:bQ yոT)"n?w)k߷A;B!@< ]˻5\慨|I;D>O{W/tPλhK)^K) 7[zbF/qvG₮ܮ Rܗn#vu憊oO)z yoX/'ܬK=ŧjwķE!Q B!@<яAnb[W |ۙpsz(bsݻv7 Q8J%/7@Ԋ@n'~q2/oŏRg^;&|,eWOd|ј~zPRQa@  h^bQsppiL(O{ċ,:x9k5MC[RtdK{{n9&OO GB7#R4r! q Q)Im q+:yϏ/cSAr4j8& yMJƻy9}oQ.Ĝ{0SlVt&OV8p'9+/T+<N@ .D@ 8#j `t1! ƢrM.h};^Jh`|X>ƶ^N&+ ldipf #0m㺐>8gy &P5wf?m_R@ eWW@ Ǧٺim4}~K@Yr!PQgVNw1y -D(F~d]cϸȹAqT9,Uյrۯ)L+9ڑ՗ c`#LcG'+esWq%I+C^Waq !W{NJ` o #T+hwu@g=̨EuWY}…X'6_w?.\V(H?ֈyRO@!ngg, +xmRVNTV֊eb\|hvC`ȩ(Z2Z(ë,ׅ\6ylV$ؤ`yGQwglƥ7+Tw9\Q~+E)t1篏 ]Kv9~*g*m?rduWiխƊ~8>3KΆZ) C<\RRC)_%1pvSekKOUr{ JJJ8L\$Sn`^'2r=}f,/=~`aeB D*w?wXB&_u+*FR s&?nmyӊ4{oGBk޴Գ\'8QŠ`^Tg7Y+ץ\bʪ($>g'IkӶL~BҢuX`)_l!|I"cbH珯> 9^`j/[1|SLQ2/MWƙ,oS?EvI 2&,GN_7IW@Eo5U؁Xd4>'RwnjE #qo2r3<$޹U{3[CFSZwJm58c (/P1΂Rgmtj֗27t\,,R))fyC S%ɾ NʿνEuY0FC'ܻ"9d y>XnP8{[=2q㽁P`F닪<{v+LZK3?9ZW!)YxkY[m,e3@y'bw"n\aW])kWad,G &.?tI^YTx׊>\z/Z<]@{$.TOpaZdw*l%^tIv#)=hTn|QޜAdM9-R5M^ uws{uZ6jC̷6 '9i#V^VWo\. |&k=%S3 e#%vIBZ)j9PC@}iOY7H(vqxn"ШqAOLSaۇU.ZmBb%v!ĵ.Ҍ_Bŋ )[!%P4fopҲ% }t 1طPBZϵGY*.Lt7o, 5\_t6f^+K%~=`l6"hE1~XYՌ.k9]:95$Om7]ǦK޽?9R67`{zxQ#BcB\2}- ܼumJX0qq]Nb$&۪KowБ8;W1$w wnӶ8wW|R.igC#8iB=?7xx_MPmrm]j }4/Tߕ}Vj-) i6I\_%驩3 r2*C&>g{{Skۃ\JAlpzk6,c[Z,{jvHilNx4cSf2| *sO7`KP?z:S!c(pXF=ߍ7ld\`|"^bu]LY[?]`rVWjKдYN/˫,'3MxzR/bgyIc{J})ُtOk4߯) ;w>*j^ygy~ T?o&.g-Hcj NY3 ]/Xp1n3ZN PL^7)6i”jF/Zټ'[( F0# F,Aź~Ѵ431iҚxc2ݟQ O-RWKG})uko̥b;DMmMu7S+BPD\zFɣ2WU!ecCC۴{$w"FExPY ָF{D}ZEݤ>J 4i~~i~³MG?r!qi޺pcKkC`?!r2Kj2䣢*b|yϚ4_s?Mh(zU]a3ttk8הndq2XIpʖ\RӥR@̏# toD)Y, PIi5b XtF (zL%TA6od<|d$(5'3cC`{qL>~jb]qZDG\w:m wDJ[a?)0!iծ >¥GTi{$XȤċ ]RuO-z)ܭK- x|Q#:UwzV2A[n6[Ʋ0dthВVEEmuIYSUVMrx0v7SːLg˄oSʞ񍚺|w! Hq9?a3upKMP;ˉ辄?t 1IH]9aL \n~/ iUPaT0\TC@ /n?9=ExQ| !mP!H5}3/b*c=93,.j&VQ}6Yh?#"ZoI`8E*'&rw/?Et2bq :P;y<#ZڒLS;94} x^ zDGMbqU ూ -nqMZExke> zTyy 1]H:BN D?0Nƴ\@ۧ/eNI=R~pf+囒pO qq x+x %' igw-{f-Ozkkk“XN'%CfGӥs&ez.WM2?l 7eYCDoM 1:J|!`sbPrBTJ%"3pN(JNa,Rۥ"D@g ;eЍg\)=u/0>ϠQTu[Lhp|);EZrlű p5%k넚:~SlN 5h3:JȊKoOYj_1G)뽇yO1G\Bߖ(AjI!%Z !aFjze} XTGA7M74Kl#" D7%c4NL3f^E_&7&&gfӌƸ1.A"T@,M/_w뾽&`ݏ[uԩ߭[uԩsEZ: ֫/K7LI~n:n d~?>IXtlV 57h5!ܚ_D 1 egr({]>|e 6ˍ=tzy&:iIАvi 1* ;%? LYE;.o2& ȶԗյ`vt>p 4VUwZ`PmI==s?F#QSv{zxj" ]= F]]Ð1pd)]5ACZyR,(a#uC_j֥( iHKHiſȡt۞~Qv!IG/q{L/NEx(Ѹnbgb8p|"`YLI~ 뛫MP3MRSN.N3ظckw Ԗ,h!!$⬛ SvzRNu7В_ |+"j+XS_b#=3_gq;%uV<*鑫[j9kWQ#4HKϗB~zAl3^v.Q?Rɥ vqtkwk'Zy3ir~8ѩŚNh]rdVz-  hnhJ( }mWN5Պ[Z/E:i% [Θt.&E?O-] _w⻯^{pNSr7[jM>%M6V ^u]ܳ PyM2͉J2OZ\bgE!٠lPxY:y 0ΚB)1QKWFm+f'DT_^8DEh"Ndf ~_OY-sbn46-*\< Hy6R2vfGiLV^+_#tl"hЄkZ*;Wzd1F\CS“ue3qH3>Ǿ_EĨU8)6D%¿tEAȜsڻRP|_:mƭܡ(׵4n7 ZBItDŹ suK#;{覀}^q-yf׷iN5iٳ#e V6DqYm4VD*).f;]`;޷Dr7cy X8E;G pŔcu/yYA$h(aպj'vҏ5>1;5&.Ls!L;X2LEYP+pޱvJ;atX3'j`jhn^2aIc`!/0y޲ct+*eFH(]u^ 7!XQ9umI)sxb+ ?^> 87*noL pjfT(U)% ?liVUx 35 چrݰ6[m$@(h< sQӟ sw_1鑵˽eF)QOnmp9 [g{X[~z׆e )h䕋_? =G,C+wk7ij@E --}xϔ97}K9+(9gҺs!S}bt^j1ao˅lΚ?)b]I4g(#B`4 0pwڒDO>]ZEş;{X_<"2X)$jh5ȳ,~vŲ)D7ZaOTbSw9q{jA{ч຃assw7"1"1C0!?:Pa^!oˋė8TL ]WB!@ hFU!@ OHWx23%B!@ ʇ@ '+<!@ }E }EC nddR].mUhQPغźGi#SHY%ly&6*! m]9WSi1+(V8Ǫ G42]9;{&*bŤֳtGwB`Z]A!/. EtPw+8Tvk,aڭ~A61@]i8/G #h;[=U@xhgDD?I H|B!0m4 F BG !lh@!@G`h ˀb@@^Ü*[V` l oC uk\C>UjJ[Me^[g> ,<8A[U :?G$ oNVJZS7]><"ak7ʩ"9r'EY#ui!zB!+Xp!S=۝}ׅq,耞ԥ*]M_ܨ(=JD@Ap\-NUهaZ{H?uëZ&ZsZO) dUAѮUNnVdm]B!0h5A$~an"YuoQoClUZߨΗkY-)v6Yma`ž3*.RY_uN|g$nw6XQoUU4E!C~<ċHt&G;["'ɻ+(Ok?*=ٳ>x#?_`Z!ma /ucCg:U[m$ޢ_HpS O8d!|,o0\EOpr&4&-CXhB!+XX ElӍa%䙉ͼ@VW90 oLۭbXK.kSRk#?D575MEUt'6BQ o^qxw\rVȩIi.%,迹4~S?B!@ <80c߆9=Eg$rR~Gu:X$^IK}^cf~O !XjwfPnAOez ^9nb~B`@@€8Lmh+ UB8;>35fQI㛰E\ܙ ]?CYu(7l{#1nSprfW1]&*mVrs~B`@@€9tJrDrqi?y!co|k9W.%"A~ ( 7[z`Nf/QFk¬2Ro#vUC]TrYeHc`摛5?Xr|FR0+$JD ~ @ :4w|CTަPox;3m=.yx}'EY}ԭ}_Yi ܭ,¤t. V>þW,_Tx*Cb eR~_Kd&tMO FPRQi!@ +3yDC>^gCo륀1 ;ޟ2=Nhr6'$IXN!Pܴd{wv 9~#T~OH"BB``!)$?n vل `10Uy2t|3  [Vl;g3'(eUJ9utr}QnĜu48Rh&WqƆ*/T)j*n[;?D@ P[EQh 0 G`pȂ!6lN!&FUZ+ 15Mb_LW˫ܖd!MW@B!(VW`nXҭq~`%}de0!Wd@B`"0q:_4Fdt$&eف# $+B!@ &C+ fU0޽zX#9FQ5 CdBI@`h}D*u 0 M@CH$y`: B!*0THrB!02@+熤F P!tB@ FsCR#B`@P!A D #!B!0T ]aF B`d"6ӏ熤F 5ʪ=@bEFzr@}yQe#r-2 X^:Xck9[hhY9f>GesCL牾މ!ASx, Yk[gW-ͺ&a-DR5pqP]I9VS1؂':Pˮﴉ+|ROSt?h(O_т0.cy!cC-(XqĐn7\-1IՇ~~*GSSWPHzrASnSӁ[wϻPD%y/ RF%)|xmI/NAZ*ϪBH^`[~CuUn>t,?s PD<,B5&9a9,pt\ P/ğ) <޺8AP W:5Hz*))n ')4l?qb-)fM7T eeaƟ?9+2G/ΰ-ꊪ M}v+U`_}]^5qp܏zԭ 02_xps"<_]V]n>nphvN+DjѷNjq3ǚ iԩdMbLLGBuU28JD Ip-5?enuW2&y5bht|*>>!2ȓǵg(mJ6+JO爷 BiKcG&;[ЍZhV;[=$[ڥ!TBIlŤkw}#gy xhuPB!0B`:eށu 7 T$v>^ĕ#}.MJQ&5DoO /NϾ\ y˻|V},б_sf[3ӃL h{7ַ1sa["LVu?W>ٹ##'&mo5(ifXVI:pClр1%qWX`CmeNwq:S/Ϟ麚JO:`9qF]Q H;L/#d A#<6i^8o[bPU_jS@S`2\/{PእZs57M؁o0[jN,k*NT>\m$j}3wYLK~:)&?vYFm-|%+\ƹb,/hN!g \/w?dE[O$=Oy(}JOsISr-눊R1hyZ I &jɢAg$ȟ1_l۞5X#PhlykVP[CuzRU-]. Xr@w%l^i;\/w…3c={');vHWuyS"\h^N#[ (Rr ԋFB !uиyxl˫xx&;vPٜYOj DU#ʀ(Ia%PvJ_[yiA8"ԃ\Cuzqy}p2uR<!e-F.+T/)n$-:T罠@ >66XSbrw1; ~T{/xˍ7Wk5~Fеg`z,y[mq[]z% iB?hm.v>'Wg}N(N?sBWhERl6~BMy·onLOJazi/cKQ6UQЫn[/n񣦻wvYE]eݲԋV8DڹIXϟ0/nxI8].yFT?" nKP9u!Sa(YoF%.1וaObq joVO)ȸ ri[e9Z$=Z^ `1䣗 gXĥR9Mz&){$5:4Hm< {F{/H}fٜC;H1 N.:ٖ7iK}{aY_s0F@g>9{r{|*wbmI)3_NĜjlOR>) I&ou\j\d ziE|6O/1HGa2'k?K֊JA|v\Ի5^ Y&\].^ƽ!hLNմo/5 rQBUayN婼=s4m;i,47&,M(gv45ݯci(+4 $~i֑Lu9s6tfϛA}C>^JyA4Qݢәh?oK '>q%~l*\Q7aF eU~ _R6~=Q&uFl䔧ˠi*M2%Gϩ +&Xaoo0nyMڬ=)3 n!سe-5څN~:HiHp,RӥJҴ@,-#tw,RNI~"_xZ3(yb9TA#p7mmZ ?F2`x.5' b4kwKr0y% ^:},F8,VJr:d9y>dݪK)fc,6D+TOW8n[p1w OSj _wc<-1~G&m7ns~yy;Fд5(jql߰[ȇxl='8BNI1C-uqq!r01`6;&o꼓?V7l$POc ے3 OAPW?1hXҶ=w!a@]x&4;e\pxN|HGdoVM{ B7f*sNsg9a0rgq/ޞ@;X^+{.)U:ֳ<@D@&HXanPY&jcogEyi3=sbmlrZf& ng.jڜi=]O$xcIM.k{'%<-bfbl,ypF)%hîu> *+NRds6Pivt6(^wk/`tAL<~wN@QE(Z Y4ga㮕ɛ J&zM.(n9/9Uu=L7WhoziKUkN.@Q QK*sCj@$Dey : —QF" ߬ H%Q^[fk*'aFI /̣[xnѧ0gUNlI0H J\=q|'@L.Ln" &j@t1PQ1 p6@en^H* 9XUʹo)<6ƎVOtjws14Ne3 745+u2^s$}A!i<ʿﰐ@U}7[׬E4EkUX&3g&]Q "+nyYu +̮JSH ~e~{[b ($ҳV)i&qcNJL~ixqVaiÒ`Z{NxU(त+iW_ %/XPn={{w7O83Kky rnZTbhLG*Nb6&a 7hb18v/$,[%/ִ0&E2J6ڃ+!5Sa愤G7QI'Tf_4aEeKcLQaֵ%-TPv\(A9i4mUN>6+ƟRbb tMˇ\&'['oDI\FIM,*B)@#8OhyK?xo(Ԥ J>_t?`h^KWAA;:k4oxF‚fV%`R5<$ |C0`)ԥn\r8fW蜩,6Qw$ _Y9zڌzd^r0,a :(ogkKIs+elEbXjfKTHk(#}ܐ>^COsCo[J [k~1p▻/`OQy*R0 N`W_`س)pHgҭt(gS=-.!tz-Yt,᪛[s-EyP^4Zh R_ZWvV {-6pdUu7 Pm5N7@zzqElFԘGHsӖ`{խwhL5e{&ڀNc|;ŒDgximtO -~Y~BQ|Ґ8%n=r(8@IMy=Bvۓ_\W>~L/NEx(!Wb8p|"`Y*ц5o2A\|}sU|j)RjW&l&+nYb`ڒ-;TuAʮWO[O'բ hIͯli>|ChubNl]VD=Zf oߦ$:U.&ʛ43uxՒTY2ޜ^Ӭ144ظQ2hVٜz baX.;Z7O75%+[\t`?FG*M:4}{xAV byRGWˇ"ֳa5|ץ,;W킇N]J'm$*j")N咫SWW6<{P2/IJ6HF[TԨ:w4y?ǑJv 1I;R-PWKKʊ*D}iKR"i!O6ӇOW4AiI)C7NHXA3^M'~*b_x|rkl:녕Z68xY% oWX\bQ.o=VU_M[pz_*;H[H|N9v(mc٪$'Oo'^:Ng0h 6=a{S7_ӲJFoL hH)kKȶ(BQ_Y'%"I4-ˆT(j(\b/].oYq;["QمL5JVΩc̴Q[ [~k-Ï`n,/Zy ]1|;;SQM;U\xcμ&'<Ň Q/ WO/7'/׿>vGLb01ڮZ{_C }D?>W]R׷ゥM AoLy!q&z81uK;-"r2fH^=v忉&3.0?4noTM9l:oB5_IJStU:%`g}$͸anzKq8$ G6sҧdN-f tɺY53$;+ǶhՔ\r_gBFo9f]{/Kcvx+VVk/֖IB`ƿebʱt^UoA r')޷FG Us89~Uwٱ1qa 1O%߱ H{[*7:hf'%oKыrL@ѱj-5=xڙ#kq;ۥGLN_=^) kE߰<IybòENSZ#>C魉p cw*5q{<Ҏ ݹus5!KHՄXNsL%эe]&oٶ>>pGya1Com^K,^kev2YDɛ\W6m:{"œ& >Pk(JCyо&E#tNp0؈_1R+~&kN߃-HaƊew{D#~Pkn2<3=gHՇ1E: xUQr;9YՀ;Ɂ͵׸A )}+v\=@& L_ڒRBˇ6E?DȎ3V&rꃣa/O}?=\xv]-/0l JZq6ı*eZgEGM( ;IG;3[%=Fo{m!_xe Ĉ%²T7~4AamHITAHe.wsMj ΢=]v@T]r QFj#(n=?٘P?w%Yܫ_f䳟6(68܄ik˷֖>gqُP.,wdiF t2) l܅NĊ*՝A;Z} qV'l5 -HY Gh6>%jl9V֕~sM= jc'i$P5.c~'O\.}HB`uX;ENQ 푙I;&8@ ͮ@ ViK?rVQ`Ξ9/~h@.Oznt6ٺR`Z,73`)ַqG ikP3nR TJ%Vfd|>:P(Fi?0\w?lnRd2od=E F`h+ʹNYA>Vꪣ F Ϩ7:d~kN b!^]B!& uIp#]a]1F3Ⰲ@``lWKhf* k PWp aQQF`hu@ߗe4<#`{*WhgDi |B`x!|@ B`!tD<11EuC  ]Z yslY/ڿ)pf/s =TM)m7=ؗmL7VeoU*g@#.:9[ 5ej#v3HAn8Μ-~Rt5RfY+G +[('$c=iQMR_!,z4!> ]a<%I]xt,sXwyf";~3U/u* Lz/vժ4VR8TTZi.QM`MVU5 gߺP}\grkRK 6>o.MߔD<@ F3"Y-D@My mݣQp\nL[-,U$n]7) XEB5&iDisY>dx]hC:V}&,rsqgnt] eסްǸM iRx]Lt JNʖ˷)ҖZTE #+5R,mnť-'T[ȏI?DWsW+kv|_񻤶T-eO\@Qsr4{2Z ofix1֗:/ /EZ5X.'7ܬkw5Y!Q"B!&}R>$Gzum x}5΋3͝wRĜGQ8J"LKbjE 3 8;{"xMP24;/Q˩ݬ,KfO`%~0F}q&:><Ðz)`v i7p=vʆ􄤑x:i4ˉ3daa.vX!ru r^w"t=< iZQ!pU7lBr !G˵I ḣ\JJe99AA.Pʁ>-\uw$欣(ő*EC 0J܎36PxY.JQS)vr~~H7A BB`!0=aMV$@#t2! 9t $Ui-(,&4} 0Y^a,^s[.'4^EG`hu놵!j'B^%}de0!W!@ VW6B' r`%.ݸ6 -C)B!0 A)^=,#9FQ5_ CdB? oc$EyG*u 0 M@CH$y`؎0PBq tǁ:*!@ #tyVHRB!@<8PGe"B` t󬐤B!x ]qD A #Y!IB!8@@@ ڤ>rb%w++nlw㟀"$PV?*#,2ғˋ*;L9k? E{u#8K`YBTQG}y̨l= ye9>,tRTcUZ:z7dn5 mI JʱF6.DR7';P˸|=Bkꗟ|DS|gMwu=/0q曀|j1bitCER\X8ön+/4QXv+Uh5,fQ2Z2~(Odx-|͍HHe11@}X?Klfm!nB˝M$}{'\?s9ѐ&M"5B>c,.ʫ[/j7eaX  Ϩuגz}bht|*>>!2ȓǵg(m nQoH[;5Qے Xf5[ߘ>Cr(i+Ťkw}#g㣥P߳A1@IDAT'B!0C2@o 7T `Z8}?}\ĕC!,qJQp| I)@'4\qSnx_^CyS!;W Ӝ;gM)la3+,dU哢z92HI}v b;xR?~R )/&mj/sz'.7NOLݒ9QY8 5꒼qc"&T\]Q :P/#dؤza'([l4aCUe~#"&i?Ǜ=?*,F 5U]Yw<8:+UtBTto)[;+H?^P"J]N+*oۿv\ّVtB3ԨbW۳S~x$wj[rLDU5?a&eʩ h(Q$o$nє5ݖ—sBN5 PQ`ߌJ\>c+5`y joh/9QƿɲIMv9΢: RCmeb R_⨙k6'kLxéSh}q@ghRbX\Grt7_jvfz ®1^aLE}ÜÝ*jNAPp}d&ĶTMutngG'=K$%^17U8Bt>x2hnIzdU?t` xj{|[4˄K+eLVӴoa0J*,~ENC]Q!K|`C]Ι4{jF529ɯ)>6W}p$o: ,lˋ15~uedi"2?OOq95-<+&Xaoo0ny2nK=Y{R(?9ޮ=[XB@=_S]x䧃̞_*n J g( lY, PIi &9,MQ/\<-߼z{FW|ݴ!d;M;~dO]kN hkwKA/y-B`Pa>06V#.RV[X<li;s^]#uO JRŌҠO[Irޮ6x!;%@_'˦́ F :e:a\ -N1`U¾ )ފ2E\86ol?U:Is,jj8":Ʀ!NdwZ}YB}}ZnC,). 4ٽ&G>+J dr/20uRg !]BN:1F}Iç +0pKo5(qWkgxsFb-> .|%—Mh籊Ti!6#Rn_W`WPdt\2PܠEaJ"q&j h_uOEdAٷ~I2'z~q]I2:x W[ᯤ/! X#'&Gl؅QTiGF4֌i#y8-w_-K9#[ӄ@xH <- FA Xں[k{[wkzZ[XB*PD@Pț(@x$@ $$?+$aQ#͜9s|gfΜ94ɒb&l8tj.M6\<:;6jRW8fsC.(겂?''.lP筜&NL{*7.1҄:"9~ dUzqQY@ig! P|!\??%? _ou8v3))p"a'7Ixmy~hDQ9ig3\0LsUomJ\)&w hMd94Q!nki+ȒB)`rE֜M5KjB&JgT_;-D)2D5΋99SYJ=}͏y97 v;4V.a㦳{&;7L)#;nh \&CQIN}~MC$`ƃ9b:u` >]ziCmU'~{ B G䃦^8_XW~#(ۣc3LI%9~x2Q?<-QxM_CP`{`DZTZыDPtjj~]ڛ)pCeH .OP(FDœ}xQo+ŗ/UxFk %_ܥΜP:8 Ĝ1^z~. U!YOʩz#wkͶOKB~e]s Йy}*V.z9cT\0G1Bfyʛ\kL&9r2vO<QVUJHg+w6-RWMxvhfkxk*74jYwP8+| 9?P:fF2^*6,"W:a$v]._I7C+6&"e} {Mg-)kQ$Zzӓ~&:8=~8Fp8:|M1l? N Tg{lFD _zU0V=zطgcBVPae%m@] ?1Y°1^B`6L)Nt5U5m7KtkEZ459jLP]j0+&xu0% ,r}ՙs=H j5&mnpo%BGmZMT|xt jAo`}2CKhU#5褻pmC ԘDa)\)@faGa9{Rd&߱';f;12Vl:{2@:ymQ>8kN"G̊hTLd/~GfQztK =He~{h%YmWiw.XPwWt;;JܐCa"l"Q;eBg -;gC^EDipp6KOl>`'e+ rx26x!H2g/ Lx ^7qKd0eg2+=}n;BY8Z3,TGʎѾ&ps!ʙ`jzayUw"aIL0g ~X.DC-ޘBj۞a4ŰBAlm(]Um|d|N~ٕ<1Gk_KlBrA.`Sy?n+pF"z.R!艰lD1f^Кe rGZFD!cae蠫+_bSuV +y`©+eIgeKVR!BnomIp#IgE=Y[*R.L)yں/x:/5Wj񔱎Dd4dB,P`o4xVSZ؀z˃EpʧtOr*WfTfaQKG:Zs Ϡ[[j*wb3hty>bJ:Mo?e"ig |3edfʮ)OG;p;T2zA_ BjUf_mULSvwOc(QA1)%\=c(% rd#) (g K(qs:>( {@Q9!擸TƗysO\N?G|veh+ g|Z)ڲ L6?J5M*m N W>LJ?@=0^ld7P T)D3S8\wP B>AliЁ_mp~?\-Ư^?}lK0Pqα3/,0n<33S6@ ?Wco߉wѩjaTTL3-묽{: 1/ D 1B,IΦMԝjo ޳hME@f8SW4##7o<M䏡O>o½ o(enF>6E1 ajotmޕZ_ƕl cc9\Qzuxg ežA9'N\1v {uz NDgww z @ ڲ$&;lLϗ/ؽx:J.)#D(@4eM?=ij>ɁtIb>8/d UtDiL[ܚiӊBPҖρ`rwc9Dz4`\9,Dcd{1 q3`ۢqj?N=l#r@;[R]BTHc۪5nfbuJ-]Q&.nA, [5S(Q㇧~#v ?/}94g)|-䨦[lRm_^WK! λvԾ֋w ڵfacTx7]9h'/Xt`+2=\Ca@-YYeg!>[m^{m"6uM𞺢KziTթLHV[z3] ْ3L1pN־1fIwelHVƪwFH3Z:e1GU:[VY6$Lܔ'!K>t<5U}ޤHFqf/AH,: ݂w?Q- v߫)Y2B!-l@#$v_*hDyY$ ڸE?W`namv]T"TnQu)i\oC U!H\Y^?;r III`B$<0YpV&++gbÎ哺f"W5|‰&GId88*_1JpwR?X [K}UVHU:6=qyW; pGufddH|qwbPnF9@r`P[eO[KK݅/5**m<5$@朾m0=BWBA܂+?`.VdXKE6R8k44 p6 *p@WkQЙ\?@KwAXR"mZ(ظ|6¿Xƍܹ:'3`Cjx] mGF0aS tT*jW9zדalyu#@9QE(?)KqxYCP{ ~X4`N0:S"1SB4)ԺERXuh/W[ۃԨNɠ}BmB1'!UW ;u>?ɀ\kA(9y՛"klZKJaBÞL@JPn\T,Iۊ"r>8}{$q"_eSy*뀐 e9Ř<\X@8B [9B Y4HrUI |LY` q\f:?;7#6- &ybh׶{U4vju0jt[G ewЕIRi6Pf)ؘUBO,B>~Ё^-6\yǴ7z$+ 5 #rR<@Ac^l# X ƫBlgg ii6ǮS>tcu7?Bs8Ip=L L'? -z+n2)Ea X.M;c@K{rŅ:jduoqύs`K"+6YKH |\{BPgyh9l87=`\{ޭ)ި"yXNPd(CPLſپmJc24/!fs׼k` ={rj3" ;Y&/6 a{}elE!zR %s*. _N[V_,R-L SL0W<{=|_Eɐ)rf'M0˾ "j]0"ˍ0qʜkk(DOI~, o'y{Y+Y Q)o@vٖ|7D{l҃j3 Lr`Vc{]5è栏 >e'0;ބ.7  o00 A~obyhIV nǍob^eݾ2:d3GYrTQ@pz5?\% ׂVb6R"R" xg텭ر t =ݒ z?z jpg"BRhwig4wS6 i}nq[PE)˅GEް"AqOz?0{),jh$W߾xi~͒c5(3rrkl2O Z Ϡ#Ũvڊ}%<}/ !Ym d; ؃ԴBB%R+](Rv h X޼-$U ݉J@JYQ'{lk+zǬMʦIo!|6íwty XZ.ծ'0LB( tFÐ Lc[ sko}Ak NP4}Ou9TU"U!>Fi8~a(p'QQXeC{1",@8$O tC3ۜ=Iaj4bRQ@Tļ|t J-&(il!hEQQ@;;Ǒ^Tp3&p0$X 1 \' '!i3UBXv*8:|@[؃ḤX~+hs||Yػc !j$X~JT&H4$EYsx"ǎc0 @H_p &Vu|F{iIUĠU]ߙGdheA@$&Exu4[cf;8kB?*J|0By>x\eA jyx[6bj LK+U36rC㬝cp^K0޾NTC0H[ >>ЧxT+ңK 0a&yKu^3K-qu ˼#P@f~%z]fׄAM%woCvNڧv ճ=-ԩR\RFTy&ņQMcXX~KqqucW\2& A#nl%~}jOeP/MlCeqg$J ;1`A9h?W6'yTC$KFH?w:5֙3ZXKFZNKT&,Mr`9`67QVg>i69S߉y+'5zs. "kt0zFU ] *' NQl(Y%ćUQ1ۤLUrc*7.ѷ$ҨtDPsfyxC@|!\??%? _8v3))p"a'7Ixmy~hD_^8M˫$ .hE?HNZfdkW1RWuUW2&*ču 5M|ȒNJDS$⩋<9[oEzK  5i s<LJ'@N ~ ?OSnTѻwD`t f8@JfLfuIUc`kڬՒ]殶Cn7 QYD ^ԪD!RŬ~ؖM̖:k*/l6f] EYC^^&;Ȇf&5֞*HgQ\Ek?o+,A+v=V{@h@uwFH7,wq<46|sjCHh2/~ س8kF0Xr)Jef~F{@/sΒU4{sKF~n9rswcѬJ pgX43cP`C܎!k={l;C & 3G'ܻU2{gWB*S9yKЩ֜aŻU=|"i;⒟^i+:g!Y/!g/*/_ |3gQ}ExBTnX4 ~Ǘy2*<.&&-DjA{*jJ suʄdI%cW|3Ğɰ5/loK5Y"ni({"o?Ws`OV eMvt G(v m٬a~]yb7Qq$ c 2'42Am;.hA]&,Jrip2vOOA: /_*@cr`3vb# UqIÌ^*6,"W:a(&'.rK5P[O01}WlLE>j:E3&júƶ(Je>lgiঀ1NSM]y_yM%~JJǕy%2VdD Y0Ir54P:k:OLU/!piop$uN{ _zU{(k pjM?׊BZTڎC!_m0^Q%$2ܷ\9kZ-F%2j7` f9{WcXfPHdsE`w2jڂ̍ߪ, so a$LOMv:.\l5Z(P7eU.ORe2BKX#dz=) 2kH3܃n ^U܊MSû`8ĭw99×Wl~-8yĬF_;Um7GEmꁀr| {Ol'qhy+єڍ+DD4+:%nHС0]s6b^ΨcDD3nm޳!n62 &`UAZ?߅P¸(ڟ88٠)*vx_v.pVXL`@}xAB_L/52/xY?/YJ{19 fhkͰPᏲd GHrf1h/ߑV_3BS>\''WTЅ'⾞GbpBŌnlk쓦#;\܌$&HO ~X.DCmKxom0bXRh66mb{d6H Y{ o(;#R KgWB@'9}/Q S 3PVv͘X;MiyDo5 +aKcوb̆̋aPDMBƓy@ʗت.,`V!~eC2#=qY5^B04 @gH,J׬jD$C::2z~ dmK)nnjޒSu_1I0ٝBU0hnJ]?QX[!9$kͰXuim3?»+Ťp71?x4ˑmcg$&l<4j~Sm| GO2˹KFE,="}Xq;섁 /V0 3_ \,[s3jIU?@r`fpy5 N S7ZYǦ ~[+F)S) 4fN,Y&DrKj}KpqF2,?9tf"H^p`W5S7+2pa~ }*Ѡ7Hr+<ɁLycz9[}pfK I 9@r33 4$H 9s^p Or$+ 2$H u\ 9@rɁq8@ 0&9@r/8o_p OrHp]RH.:ӡƚ!Uzyl 8'w40Asc[oCsyTN t#6IECR|';z}HW4EO'_f]UN wnXOx/~BC>+$ڿзIH‬`lAO)ϽU\a{k*~{,[Ͷ}ok~<)m'3Ja\Q\ vgfmZgוg(`f܁@߬ZSJ DFrV—ĭ)UZo. @gǫ}K͘'rq raa/<[itAF-üV1<}0ZU}FRzA2\OYxx8㛑 kǬ"( n>K|MEAv|(` T \]'(/mER,]B+e((@'PEA!j(0m ~jT2]a|.鬙..{jan>EM@FSTsF>8T&Wؙ=M%7C8_$~3:ZOٖlG8~Bs}}|9fyqe Eh}vc@چR ar/>xWQFEń:3=?@IDATڻb`OP{4A&.D$ygӁ&Y7 @o*:|x2ۼh鞒璪Q4oxmt&܋0MQq*td58`cStx\FlN]Em\@=6WwlxP6ʈFa]'vM=)z NDgww zȘ!]qGan{R^1jj񛱳OƨK?_`6([g &ދ45DKrS$t@vX2†*w&-ng~4iE_Whc!(i@Q0^]v;w1Мc 0DE 3sn,xbu{/ݿ>W~\58--(@3֪xLJp DuYyj, +'/Xt`mֳyoT&4;]oeIO7mޖX֖0M\܂Xt_@!.^',|8QZ/ީ>6. ծ5[FDhƭTFK\?am5\a]ڐm2 `d,?%k<,5guzy_dTfܿSWtA/:5 jKofWa:Xs8[ux))]7,v0 ]YXUqnCi@A+]V,&z`5 -٫V%uP!Ye<8tJg1 5@AVI,&h'a3x - Ӂ꜒%mH/aaY؅dAK^% 4}!s~C$xw.j$jXp_y98 t  H%Eó :>Mu #2N3Xn!)) ,W ʤP{EcBwر|R׬\6OC8D򞉸(V]Cܡ9 ݲ5Us)4{mQ>ؙQAAEUu`_C=ee܎_s2n00$DŔj c j(MvWQ%OTq>yp?sQ(C-ŕNBf^w6qi֏.V AC#W4A&ѩ,EF^(X9zqTGOt]8~ÐADȊJlXQAШ-[PAQ|o|{̱ xTqo;K`S!x,RTP $j=b1yG.ZUzsK*} bp a|:ޖBd(9p Ϥ5^|ghbFyD5V#T:/;r[K4%*? Xr4G&p y}p[IPRpWhF+o5>$h'^1*>hk=Tl\>_,jUa[%lX(T @yc },(2V aqx ƔG݂B  ,G類Q8Z*>RSPں:%%p Ti+į\w I Z} D"Ts@u7BY {Q}5P Kش3 ͞?g!Vt4Ɓ⺾D?WZ|3Qցルo oZ ~ 6-\u; ̀kϻ T$k>-So\.Ɍo&Qb= D_BLڒ揮)6,6+vF@,ؘ `nw4B^_)[_Q~GȪ{z.~{YMs*. _N[V_,ЄRv썖V|qX؄) HV+ {ohdH9.Bwm E(@"?^ϘErd۷fOCd/Fe[ IYƤbb#bp&0q~X/GQb4ڝ]QE~knx`,khoD ¹G _l=;ބ7 > t6ž*t k(cq&&G- 7G4qsKwQE%z Ata%C:Q Fq@Ȋ: FE F [cA3C0{iUj1K8oZ@۴9daɚѽi~6<5P=Vx JqEAY3|1A.!U*iD0 Tn(I5#_ٴ6IRM2x}pcjGfzL Titem 4 l]ueu!vΛaٜ^>n )Ё'A yFTJEIpB@}c Yҥ%v`;(0F0O+=gǮ^5gH% $<҇U|6=]Y'YwW$BLc:1*(ѯ·'qoGA9(#xq~ U4!3Duy_}V4g7 9:,윴O%@gzcJʔ cѕHma 6u8Gσ}P$~Im5!# I3%ŁMnU}ރ|pIOpzS%ƈ1N-*UA`j([խGN0K*jhjKfE :3]`Dk){P:mJrd:B1)m[X T$n~,I UJMׯ)tb4  2i:0)tmv2uCv RfW6 H!4pi o(q" xm# U!ywF  pӴ?h-Ʃnނşv84Pk1:kӭQFL?X-s&íAg^*TJ|`ݍG6dqg'ge"!905P4F38b/,@p0CH~]HrO_c41Xc 9D(4Nwc;~ګmL1N{g/-pR0cޙSkDj$ {8{f˅m.CŞs9ld(,^ ]glgqn`Sː\JRh;Q^iЈ,md}շr?ͮܜI[X#p4=q<< 2ifƠ m<Cz™wȘL0Y9z6 <ޭ;:PRQ_7OɻX燮NV[Uc'2nܝqNx)(KhE/!>vw`F)%E'LŬ~a{kݍ^WkI\76RΖoD=Bcrnf`{1-f+J}g;N0&j"sB#Cdx%M" $E𧃄ū}gje}UWQs|oZ&ٴlD뮦pT#+M6?N{l2\|ɀ݊u{#I V$Qu#Odp@ޒ`,BE_ dc/![،R_fU\c%Jp KŅȕNɉ ܂zkNP*NXJzwJ;IGyYgp/q|$x"^MA2qU/<v0]%n 5 NS 7::5P=eV)X!Q.cVdD YM@!WPOuPÿʥ19>2 +5%&x?y5U$hI\¥564[459jLP]j0+&xu0EqӋ%,\_eKϡZmMhbD'sj0 HP[Q#熞۾;1ڴ:.><:FC $7)814QW~}e;- IK{Mߙ9@Ȭ5褻pmC XEa)\)@faGa9{Rd&߱';f;mZŭt05@:ymQ>aWl~-ؕ#7?!/qO$+b 7?߅* N7$P9H/gAx ˑS8M !p~7Gd 7{ \hdzA`'e+ Ť޽ME:{s cX/LDJɄǾuCgdmqL*ꗌI \0h%JO}=<%XuyP{$٘䉜2ne4Y9L Q5-6ޡAPΕ(rI8 -1\+\_S^HnFuXj"Uk7r{yA;⍹~ /FS +T FæMJo㋤'6="qve!4r >њ5[;"|PgmehیÛaޔnwHDoVR>l ijȍ{ :ϱ-lt5_Xz\`qjN܊!~eC2#ohXFDhJaUH:{Eb)DP[[.O_-b)Vrf< C>m]<`n O+Xp]!r^jꝞd2O`oL FVWO fkaN-)n=SO?*,,jWGksp!qkKZ厒[_v.g_VYҧ4[GlX3KQ 908 ]( #<ͰRò' Q27zBjUfؑxULSvwOW I-ob9tuh#qfI7)%#9bq`J\g!K`9u)"ˆGľB+nV6Jd5.F@^pPXeޚXHr58ugj"&i"9` nM|x A(ИIVrԜLQYL:4 䖬 o 4<  <]2B5=u#Ҍn/IɁg Lc~ }*р;.NSRWzI 9@r`28vX7O" u#E 9@rɁ^P&9@r$ q q#9@r$H]$H 9@rH]w<$H 9@rH 9@r$ qY {mVsmg%y-[;J3xױ~L(g I~vkЬD_a3BZA6sʃ;4ϥb ޒDN dCP >)幷J9 /cEl@am{t\;-H۸7Nf-Fp7W8-Sk Q}vnoXܦUzוg(`f*)~j1SO)%/KҚJ mqz4T^w )We i6^t d܆a_yo]P{mfDeA 9s:9LgJhUAI9ȴ2%8I$h.6pN/i4r1Xw Q /ПsCE'qɯ.nՁ@>q; ?A\Y(8PϟeKhh˒B\ClHg'wQ G=Lp373ME&ы"(q*xB0 _]AKBaf0|pL!ܷ7s-Zt I3h6-#ixFR9Ǿ|9fyq>Eh}vc@چR v _<};> Zt:kB 0=Q,C!K+*!wk#@F }쥛%Kyv=ٙ2֙{eѪG'#!v7^<3$Mг" `#fA5Q؜ͻ@۸5=+͞pE=yWDً=(칋?8s c?Z'lݜR!hށD^ ctmY-ۿkHĢ]E~ L~u*J5Aូ=Y3uڟ'ދĴ3♩b4=3! }18.I,=lU{'J[ܚie+ʿB AI[v>ʵK޹3nЀqp%* X%Kf-Fb*j>4s4mV:gD{wg|Kc¢{r. ky{] n5r{ANZ󤧁YXoQi,kKW&.nA,#̕"ľ[WN̵ "zs,k59? I{AO5O^OWU)1W$j2'VU~;> 0P]llxzm)x[5S(Q㇧b\a1~lB[L& ? 7Qivd2gX `O,?%k<,5gn`9o/"]zK:NeBқYْ͚{NwvMk F;26$ i~k4C 頕.S3 R;"h_\G/fZ?AU)f :(S70ơS~>DFIWVF8Z d^{2$JV֨+be#?w:PShJH/԰`C(O,Xޛ{(ɚo"m`t]!ϓ%0)>(C}* A>(w5˞v̤Gt$bVCr`]{.0"; Mv(<}IP(~Xvư԰`ǵ74)y̑37-<qI*͇T[.uVve+W܅@!y/}9BĀX~1v}SBbOf X9Pyd9:sB`1uXY] ~BhȋB'R'k%4`E> 4/h[7,,,[f@$ޣGu.pw!p~S.|d)bg|\K}Cqm(-Z"7HIy4G C% f /ysuv9i: 7muЩʇ룅2| %49i6![ɮEKUQ$ҵ3ޓWJpHR[2Z=VZX~t-TM !.k'(eЁ=Odےoyok%&(ӡWAbWst 1Gyu~wv e Y.o 2:]( >2hl:Oxz2G#ֿW D[m%jť7iu[K-/4vý OK'~f% ~fe:'F% :uG#.𭌤]δ |ٹ<[EP'gCJV{ߴ(XVkt\UG^t[`eu!%z%mO[s 'ӛ~L Ś~Dry]H #_OȚ]AIrUi CLs~+rٸaimj#fZ즟kOpc"&) : .U'%ذߙ\^w+>3f Yt܃=)jrtD<>{YzaSR:U!No>m ~RV~=(82z0?Bu%buk(z{Sе(|4(6>mTnAr+h7Y-;IO sERjG/౲@qkYqo#!>BIESf6SKJ;M%T "{+3m2l,qx"LCsj4LJ76@;^A D)TWܣ_-. AġI,Xg7{ljCBmP.&"w%+|uo=8G•[͗|+(~[4s,,ݷMσ0$?|:]*'팡QP1Ѥn+=[@;2( 5wBS%눵N#ξ"kg`x֌U-=/Rrj0b1md(0BY;/4N dAs[쓯BCqD:qOT0r0%~Կe,F3@0rOExZL6} lg d/^P-dE2j]&޼ArW6*ΦI.6`nPPt+cϖ1c(Pw^ j5K?$1D^@! ;+ih[3b`xx(?g tEKԢpUZ gY\']INd 0I_*6O? =B}tPL18IFo=T䄣B}(w]!pڹ3>Oݖx` x@ϧ!דaEvkV^/pdii,>!&Q4֡{k(5 5慌 lGc,#<>מg s/8,M;5Zqd-ԲځO;+xct=#/^e~^so:!kN  7\lٿ2y:ٱrœLʴ0g'ihQN1T,OciF[:a58*v -JMeG,7-/m? '{=[)c, }k~]s@04~ mjҐCh*;031l&4LqJQ,)vtDD#=QӶ `⻳44)*ۼb; ŤWA`Ű"a8 ZEٍ-8 ΁<)24 JH>+7Hʢv46AzCYbqnK|&9r`h Y=J)fvK 02H7H1r6|Mf&Jͬ% &Tc]MPkJIuΜB|-(ݪ4T}>[W_FI ={)$t4'>?=mdbBv 7c[ו=Tz/EǛsH6_!S刔ҌGH4Iah%bP\\greO, [0=Ʌ 8yIWO4d[2K )&Hɧ,/v4`om -#mà KgazQmEqxzERŘYf \*/{ePßj7c2ihiO8Kq)_2,rS]wڤeRH;'y;TZi{eqW\r,`\]kizMZή+Z)8'eŸCD己{{ѫπenZhxK@$mN4`0,٤jH*̶D?|igjn:GV tOo %~yxgL{hFSZsO?9@Kgi"5w`G#Z\&dJp1)#="՘lh(,IDATx@R`d!pZ,z+w䑎3*Ń <;[-=&FR.&_XZwp\h(sd(5?',-7N>KC\h/1ȁKY;-#j5g`KЃ=Pںk'oGϻzgtu %?#7D$R] 99;;/Hbѳ)=^}! ~ś%!+h,:Jڔ&ɩĴAiy/~O7 opW *hْzd~[2n$g {mx|BkCϤ5͈m'*v!tr.\#[!zӋt;΅2#n-q&`Bnj}2 ~zmlo D'YHD8x#^AoB"'aY̛9;qpZ`Rٌ}}`.psrɿf'<`[ᑭ,F#L3"MS~l+L@#`0a0F#l+pA b0F`!mWF#i1F#0ܫs\bF#pA \´F#{`[a9.1F#` m.haZF#=>[IENDB`mustermann-1.1.1/mustermann/000077500000000000000000000000001360365612700161055ustar00rootroot00000000000000mustermann-1.1.1/mustermann/LICENSE000066400000000000000000000021171360365612700171130ustar00rootroot00000000000000Copyright (c) 2013-2017 Konstantin Haase Copyright (c) 2016-2017 Zachary Scott Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. mustermann-1.1.1/mustermann/README.md000066400000000000000000000715161360365612700173760ustar00rootroot00000000000000# The Amazing Mustermann *Make sure you view the correct docs: [latest release](http://rubydoc.info/gems/mustermann/frames), [master](http://rubydoc.info/github/rkh/mustermann/master/frames).* Welcome to [Mustermann](http://en.wikipedia.org/wiki/List_of_placeholder_names_by_language#German). Mustermann is your personal string matching expert. As an expert in the field of strings and patterns, Mustermann keeps its runtime dependencies to a minimum and is fully covered with specs and documentation. Given a string pattern, Mustermann will turn it into an object that behaves like a regular expression and has comparable performance characteristics. ``` ruby if '/foo/bar' =~ Mustermann.new('/foo/*') puts 'it works!' end case 'something.png' when Mustermann.new('foo/*') then puts "prefixed with foo" when Mustermann.new('*.pdf') then puts "it's a PDF" when Mustermann.new('*.png') then puts "it's an image" end pattern = Mustermann.new('/:prefix/*.*') pattern.params('/a/b.c') # => { "prefix" => "a", splat => ["b", "c"] } ``` ## Overview ### Features * **[Pattern Types](#-pattern-types):** Mustermann supports a wide variety of different pattern types, making it compatible with a large variety of existing software. * **[Fine Grained Control](#-available-options):** You can easily adjust matching behavior and add constraints to the placeholders and capture groups. * **[Binary Operators](#-binary-operators) and [Concatenation](#-concatenation):** Patterns can be combined into composite patterns using binary operators. * **[Regexp Look Alike](#-regexp-look-alike):** Mustermann patterns can be used as a replacement for regular expressions. * **[Parameter Parsing](#-parameter-parsing):** Mustermann can parse matched parameters into a Sinatra-style "params" hash, including type casting. * **[Peeking](#-peeking):** Lets you check if the beginning of a string matches a pattern. * **[Expanding](#-expanding):** Besides parsing a parameters from an input string, a pattern object can also be used to generate a string from a set of parameters. * **[Generating Templates](#-generating-templates):** This comes in handy when wanting to hand on patterns rather than fully expanded strings as part of an external API. * **[Proc Look Alike](#-proc-look-alike):** Pass on a pattern instead of a block. * **[Duck Typing](#-duck-typing):** You can create your own pattern-like objects by implementing `to_pattern`. * **[Performance](#-performance):** Patterns are implemented with both performance and a low memory footprint in mind. ### Additional Tooling These features are included in the library, but not loaded by default * **[Mapper](#-mapper):** A simple tool for mapping one string to another based on patterns. * **[Sinatra Integration](#-sinatra-integration):** Mustermann can be used as a [Sinatra](http://www.sinatrarb.com/) extension. Sinatra 2.0 and beyond will use Mustermann by default. ## Pattern Types Mustermann support multiple pattern types. A pattern type defines the syntax, matching semantics and whether certain features, like [expanding](#-expanding) and [generating templates](#-generating-templates), are available. You can create a pattern of a certain type by passing `type` option to `Mustermann.new`: ``` ruby require 'mustermann' pattern = Mustermann.new('/*/**', type: :shell) ``` Note that this will use the type as suggestion: When passing in a string argument, it will create a pattern of the given type, but it might choose a different type for other objects (a regular expression argument will always result in a [regexp](#-pattern-details-regexp) pattern, a symbol always in a [sinatra](#-pattern-details-sinatra) pattern, etc). Alternatively, you can also load and instantiate the pattern type directly: ``` ruby require 'mustermann/shell' pattern = Mustermann::Shell.new('/*/**') ``` Mustermann itself includes the [sinatra](#-sinatra-pattern), [identity](#-identity-pattern) and [regexp](#-regexp-pattern) pattern types. Other pattern types are available as separate gems. ## Binary Operators Patterns can be combined via binary operators. These are: * `|` (or): Resulting pattern matches if at least one of the input pattern matches. * `&` (and): Resulting pattern matches if all input patterns match. * `^` (xor): Resulting pattern matches if exactly one of the input pattern matches. ``` ruby require 'mustermann' first = Mustermann.new('/foo/:input') second = Mustermann.new('/:input/bar') first | second === "/foo/foo" # => true first | second === "/foo/bar" # => true first & second === "/foo/foo" # => false first & second === "/foo/bar" # => true first ^ second === "/foo/foo" # => true first ^ second === "/foo/bar" # => false ``` These resulting objects are fully functional pattern objects, allowing you to call methods like `params` or `to_proc` on them. Moreover, *or* patterns created solely from expandable patterns will also be expandable. The same logic also applies to generating templates from *or* patterns. ## Concatenation Similar to [Binary Operators](#-binary-operators), two patterns can be concatenated using `+`. ``` ruby require 'mustermann' prefix = Mustermann.new("/:prefix") about = prefix + "/about" about.params("/main/about") # => {"prefix" => "main"} ``` Patterns of different types can be mixed. The availability of `to_templates` and `expand` depends on the patterns being concatenated. ## Regexp Look Alike Pattern objects mimic Ruby's `Regexp` class by implementing `match`, `=~`, `===`, `names` and `named_captures`. ``` ruby require 'mustermann' pattern = Mustermann.new('/:page') pattern.match('/') # => nil pattern.match('/home') # => # pattern =~ '/home' # => 0 pattern === '/home' # => true (this allows using it in case statements) pattern.names # => ['page'] pattern.names # => {"page"=>[1]} pattern = Mustermann.new('/home', type: :identity) pattern.match('/') # => nil pattern.match('/home') # => # pattern =~ '/home' # => 0 pattern === '/home' # => true (this allows using it in case statements) pattern.names # => [] pattern.names # => {} ``` Moreover, patterns based on regular expressions (all but `identity` and `shell`) automatically convert to regular expressions when needed: ``` ruby require 'mustermann' pattern = Mustermann.new('/:page') union = Regexp.union(pattern, /^$/) union =~ "/foo" # => 0 union =~ "" # => 0 Regexp.try_convert(pattern) # => /.../ ``` This way, unless some code explicitly checks the class for a regular expression, you should be able to pass in a pattern object instead even if the code in question was not written with Mustermann in mind. ## Parameter Parsing Besides being a `Regexp` look-alike, Mustermann also adds a `params` method, that will give you a Sinatra-style hash: ``` ruby require 'mustermann' pattern = Mustermann.new('/:prefix/*.*') pattern.params('/a/b.c') # => { "prefix" => "a", splat => ["b", "c"] } ``` For patterns with typed captures, it will also automatically convert them: ``` ruby require 'mustermann' pattern = Mustermann.new('//', type: :flask) pattern.params('/page/10') # => { "prefix" => "page", "id" => 10 } ``` ## Peeking Peeking gives the option to match a pattern against the beginning of a string rather the full string. Patterns come with four methods for peeking: * `peek` returns the matching substring. * `peek_size` returns the number of characters matching. * `peek_match` will return a `MatchData` or `Mustermann::SimpleMatch` (just like `match` does for the full string) * `peek_params` will return the `params` hash parsed from the substring and the number of characters. All of the above will turn `nil` if there was no match. ``` ruby require 'mustermann' pattern = Mustermann.new('/:prefix') pattern.peek('/foo/bar') # => '/foo' pattern.peek_size('/foo/bar') # => 4 path_info = '/foo/bar' params, size = patter.peek_params(path_info) # params == { "prefix" => "foo" } rest = path_info[size..-1] # => "/bar" ``` ## Expanding Similarly to parsing, it is also possible to generate a string from a pattern by expanding it with a hash. For simple expansions, you can use `Pattern#expand`. ``` ruby pattern = Mustermann.new('/:file(.:ext)?') pattern.expand(file: 'pony') # => "/pony" pattern.expand(file: 'pony', ext: 'jpg') # => "/pony.jpg" pattern.expand(ext: 'jpg') # raises Mustermann::ExpandError ``` Expanding can be useful for instance when implementing link helpers. ### Expander Objects To get fine-grained control over expansion, you can use `Mustermann::Expander` directly. You can create an expander object directly from a string: ``` ruby require 'mustermann/expander' expander = Mustermann::Expander("/:file.jpg") expander.expand(file: 'pony') # => "/pony.jpg" expander = Mustermann::Expander(":file(.:ext)", type: :rails) expander.expand(file: 'pony', ext: 'jpg') # => "/pony.jpg" ``` Or you can pass it a pattern instance: ``` ruby require 'mustermann' pattern = Mustermann.new("/:file") require 'mustermann/expander' expander = Mustermann::Expander.new(pattern) ``` ### Expanding Multiple Patterns You can add patterns to an expander object via `<<`: ``` ruby require 'mustermann' expander = Mustermann::Expander.new expander << "/users/:user_id" expander << "/pages/:page_id" expander.expand(user_id: 15) # => "/users/15" expander.expand(page_id: 58) # => "/pages/58" ``` You can set pattern options when creating the expander: ``` ruby require 'mustermann' expander = Mustermann::Expander.new(type: :template) expander << "/users/{user_id}" expander << "/pages/{page_id}" ``` Additionally, it is possible to combine patterns of different types: ``` ruby require 'mustermann' expander = Mustermann::Expander.new expander << Mustermann.new("/users/{user_id}", type: :template) expander << Mustermann.new("/pages/:page_id", type: :rails) ``` ### Handling Additional Values The handling of additional values passed in to `expand` can be changed by setting the `additional_values` option: ``` ruby require 'mustermann' expander = Mustermann::Expander.new("/:slug", additional_values: :raise) expander.expand(slug: "foo", value: "bar") # raises Mustermann::ExpandError expander = Mustermann::Expander.new("/:slug", additional_values: :ignore) expander.expand(slug: "foo", value: "bar") # => "/foo" expander = Mustermann::Expander.new("/:slug", additional_values: :append) expander.expand(slug: "foo", value: "bar") # => "/foo?value=bar" ``` It is also possible to pass this directly to the `expand` call: ``` ruby require 'mustermann' pattern = Mustermann.new('/:slug') pattern.expand(:append, slug: "foo", value: "bar") # => "/foo?value=bar" ``` ## Generating Templates You can generate a list of URI templates that correspond to a Mustermann pattern (it is a list rather than a single template, as most pattern types are significantly more expressive than URI templates). This comes in quite handy since URI templates are not made for pattern matching. That way you can easily use a more precise template syntax and have it automatically generate hypermedia links for you. Template generation is supported by almost all patterns (notable exceptions are `shell`, `regexp` and `simple` patterns). ``` ruby require 'mustermann' Mustermann.new("/:name").to_templates # => ["/{name}"] Mustermann.new("/:foo(@:bar)?/*baz").to_templates # => ["/{foo}@{bar}/{+baz}", "/{foo}/{+baz}"] Mustermann.new("/{name}", type: :template).to_templates # => ["/{name}" ``` Union Composite patterns (with the | operator) support template generation if all patterns they are composed of also support it. ``` ruby require 'mustermann' pattern = Mustermann.new('/:name') pattern |= Mustermann.new('/{name}', type: :template) pattern |= Mustermann.new('/example/*nested') pattern.to_templates # => ["/{name}", "/example/{+nested}"] ``` If accepting arbitrary patterns, you can and should use `respond_to?` to check feature availability. ``` ruby if pattern.respond_to? :to_templates pattern.to_templates else warn "does not support template generation" end ``` ## Proc Look Alike Patterns implement `to_proc`: ``` ruby require 'mustermann' pattern = Mustermann.new('/foo') callback = pattern.to_proc # => # callback.call('/foo') # => true callback.call('/bar') # => false ``` They can therefore be easily passed to methods expecting a block: ``` ruby require 'mustermann' list = ["foo", "example@email.com", "bar"] pattern = Mustermann.new(":name@:domain.:tld") email = list.detect(&pattern) # => "example@email.com" ``` ## Mapper You can use a mapper to transform strings according to two or more mappings: ``` ruby require 'mustermann/mapper' mapper = Mustermann::Mapper.new("/:page(.:format)?" => ["/:page/view.:format", "/:page/view.html"]) mapper['/foo'] # => "/foo/view.html" mapper['/foo.xml'] # => "/foo/view.xml" mapper['/foo/bar'] # => "/foo/bar" ``` ## Sinatra Integration All patterns implement `match`, which means they can be dropped into Sinatra and other Rack routers: ``` ruby require 'sinatra' require 'mustermann' get Mustermann.new('/:foo') do params[:foo] end ``` In fact, since using this with Sinatra is the main use case, it comes with a build-in extension for **Sinatra 1.x**. ``` ruby require 'sinatra' require 'mustermann' register Mustermann # this will use Mustermann rather than the built-in pattern matching get '/:slug(.ext)?' do params[:slug] end ``` ### Configuration You can change what pattern type you want to use for your app via the `pattern` option: ``` ruby require 'sinatra/base' require 'mustermann' class MyApp < Sinatra::Base register Mustermann set :pattern, type: :shell get '/images/*.png' do send_file request.path_info end get '/index{.htm,.html,}' do erb :index end end ``` You can use the same setting for options: ``` ruby require 'sinatra' require 'mustermann' register Mustermann set :pattern, capture: { ext: %w[png jpg html txt] } get '/:slug(.:ext)?' do # slug will be 'foo' for '/foo.png' # slug will be 'foo.bar' for '/foo.bar' # slug will be 'foo.bar' for '/foo.bar.html' params[:slug] end ``` It is also possible to pass in options to a specific route: ``` ruby require 'sinatra' require 'mustermann' register Mustermann get '/:slug(.:ext)?', pattern: { greedy: false } do # slug will be 'foo' for '/foo.png' # slug will be 'foo' for '/foo.bar' # slug will be 'foo' for '/foo.bar.html' params[:slug] end ``` Of course, all of the above can be combined. Moreover, the `capture` and the `except` option can be passed to route directly. And yes, this also works with `before` and `after` filters. ``` ruby require 'sinatra/base' require 'sinatra/respond_with' require 'mustermann' class MyApp < Sinatra::Base register Mustermann, Sinatra::RespondWith set :pattern, capture: { id: /\d+/ } # id will only match digits # only capture extensions known to Rack before '*:ext', capture: Rack::Mime::MIME_TYPES.keys do content_type params[:ext] # set Content-Type request.path_info = params[:splat].first # drop the extension end get '/:id' do not_found unless page = Page.find params[:id] respond_with(page) end end ``` ### Why would I want this? * It gives you fine grained control over the pattern matching * Allows you to use different pattern styles in your app * The default is more robust and powerful than the built-in patterns * Sinatra 2.0 will use Mustermann internally * Better exceptions for broken route syntax ### Why not include this in Sinatra 1.x? * It would introduce breaking changes, even though these would be minor * Like Sinatra 2.0, Mustermann requires Ruby 2.0 or newer ## Duck Typing ### `to_pattern` All methods converting string input to pattern objects will also accept any arbitrary object that implements `to_pattern`: ``` ruby require 'mustermann' class MyObject def to_pattern(**options) Mustermann.new("/foo", **options) end end object = MyObject.new Mustermann.new(object, type: :rails) # => # ``` It might also be that you want to call `to_pattern` yourself instead of `Mustermann.new`. You can load `mustermann/to_pattern` to implement this method for strings, regular expressions and pattern objects: ``` ruby require 'mustermann/to_pattern' "/foo".to_pattern # => # "/foo".to_pattern(type: :rails) # => # %r{/foo}.to_pattern # => # "/foo".to_pattern.to_pattern # => # ``` You can also use the `Mustermann::ToPattern` mixin to easily add `to_pattern` to your own objects: ``` ruby require 'mustermann/to_pattern' class MyObject include Mustermann::ToPattern def to_s "/foo" end end MyObject.new.to_pattern # => # ``` ### `respond_to?` You can and should use `respond_to?` to check if a pattern supports certain features. ``` ruby require 'mustermann' pattern = Mustermann.new("/") puts "supports expanding" if pattern.respond_to? :expand puts "supports generating templates" if pattern.respond_to? :to_templates ``` Alternatively, you can handle a `NotImplementedError` raised from such a method. ``` ruby require 'mustermann' pattern = Mustermann.new("/") begin p pattern.to_templates rescue NotImplementedError puts "does not support generating templates" end ``` This behavior corresponds to what Ruby does, for instance for [`fork`](http://ruby-doc.org/core-2.1.1/NotImplementedError.html). ## Available Options ### `capture` Supported by: All types except `identity`, `shell` and `simple` patterns. Most pattern types support changing the strings named captures will match via the `capture` options. Possible values for a capture: ``` ruby # String: Matches the given string (or any URI encoded version of it) Mustermann.new('/index.:ext', capture: 'png') # Regexp: Matches the Regular expression Mustermann.new('/:id', capture: /\d+/) # Symbol: Matches POSIX character class Mustermann.new('/:id', capture: :digit) # Array of the above: Matches anything in the array Mustermann.new('/:id_or_slug', capture: [/\d+/, :word]) # Hash of the above: Looks up the hash entry by capture name and uses value for matching Mustermann.new('/:id.:ext', capture: { id: /\d+/, ext: ['png', 'jpg'] }) ``` Available POSIX character classes are: `:alnum`, `:alpha`, `:blank`, `:cntrl`, `:digit`, `:graph`, `:lower`, `:print`, `:punct`, `:space`, `:upper`, `:xdigit`, `:word` and `:ascii`. ### `except` Supported by: All types except `identity`, `shell` and `simple` patterns. Given you supply a second pattern via the except option. Any string that would match the primary pattern but also matches the except pattern will not result in a successful match. Feel free to read that again. Or just take a look at this example: ``` ruby pattern = Mustermann.new('/auth/*', except: '/auth/login') pattern === '/auth/dunno' # => true pattern === '/auth/login' # => false ``` Now, as said above, `except` treats the value as a pattern: ``` ruby pattern = Mustermann.new('/*anything', type: :rails, except: '/*anything.png') pattern === '/foo.jpg' # => true pattern === '/foo.png' # => false ``` ### `greedy` Supported by: All types except `identity` and `shell` patterns. Default value: `true` **Simple** patterns are greedy, meaning that for the pattern `:foo:bar?`, everything will be captured as `foo`, `bar` will always be `nil`. By setting `greedy` to `false`, `foo` will capture as little as possible (which in this case would only be the first letter), leaving the rest to `bar`. **All other** supported patterns are semi-greedy. This means `:foo(.:bar)?` (`:foo(.:bar)` for Rails patterns) will capture everything before the *last* dot as `foo`. For these two pattern types, you can switch into non-greedy mode by setting the `greedy` option to false. In that case `foo` will only capture the part before the *first* dot. Semi-greedy behavior is not specific to dots, it works with all characters or strings. For instance, `:a(foo:b)` will capture everything before the *last* `foo` as `a`, and `:foo(bar)?` will not capture a `bar` at the end. ``` ruby pattern = Mustermann.new(':a.:b', greedy: true) pattern.match('a.b.c.d') # => # pattern = Mustermann.new(':a.:b', greedy: false) pattern.match('a.b.c.d') # => # ``` ### `space_matches_plus` Supported by: All types except `identity`, `regexp` and `shell` patterns. Default value: `true` Most pattern types will by default also match a plus sign for a space in the pattern: ``` ruby Mustermann.new('a b') === 'a+b' # => true ``` You can disable this behavior via `space_matches_plus`: ``` ruby Mustermann.new('a b', space_matches_plus: false) === 'a+b' # => false ``` **Important:** This setting has no effect on captures, captures will always keep plus signs as plus sings and spaces as spaces: ``` ruby pattern = Mustermann.new(':x') pattern.match('a b')[:x] # => 'a b' pattern.match('a+b')[:x] # => 'a+b' ```` ### `uri_decode` Supported by all pattern types. Default value: `true` Usually, characters in the pattern will also match the URI encoded version of these characters: ``` ruby Mustermann.new('a b') === 'a b' # => true Mustermann.new('a b') === 'a%20b' # => true ``` You can avoid this by setting `uri_decode` to `false`: ``` ruby Mustermann.new('a b', uri_decode: false) === 'a b' # => true Mustermann.new('a b', uri_decode: false) === 'a%20b' # => false ``` ### `ignore_unknown_options` Supported by all patterns. Default value: `false` If you pass an option in that is not supported by the specific pattern type, Mustermann will raise an `ArgumentError`. By setting `ignore_unknown_options` to `true`, it will happily ignore the option. ## Performance It's generally a good idea to reuse pattern objects, since as much computation as possible is happening during object creation, so that the actual matching or expanding is quite fast. Pattern objects should be treated as immutable. Their internals have been designed for both performance and low memory usage. To reduce pattern compilation, `Mustermann.new` and `Mustermann::Pattern.new` might return the same instance when given the same arguments, if that instance has not yet been garbage collected. However, this is not guaranteed, so do not rely on object identity. ### String Matching When using a pattern instead of a regular expression for string matching, performance will usually be comparable. In certain cases, Mustermann might outperform naive, equivalent regular expressions. It achieves this by using look-ahead and atomic groups in ways that work well with a backtracking, NFA-based regular expression engine (such as the Oniguruma/Onigmo engine used by Ruby). It can be difficult and error prone to construct complex regular expressions using these techniques by hand. This only applies to patterns generating an AST internally (all but [identity](#-pattern-details-identity), [shell](#-pattern-details-shell), [simple](#-pattern-details-simple) and [regexp](#-pattern-details-regexp) patterns). When using a Mustermann pattern as a direct Regexp replacement (ie, via methods like `=~`, `match` or `===`), the overhead will be a single method dispatch, which some Ruby implementations might even eliminate with method inlining. This only applies to patterns using a regular expression internally (all but [identity](#-pattern-details-identity) and [shell](#-pattern-details-shell) patterns). ### Expanding Pattern expansion significantly outperforms other, widely used Ruby tools for generating URLs from URL patterns in most use cases. This comes with a few trade-offs: * As with pattern compilation, as much computation as possible has been shifted to compiling expansion rules. This will add compilation overhead, which is why patterns only generate these rules on the first invocation to `Mustermann::Pattern#expand`. Create a `Mustermann::Expander` instance yourself to get better control over the point in time this computation should happen. * Memory is sacrificed in favor of performance: The size of the expander object will grow linear with the number of possible combination for expansion keys ("/:foo/:bar" has one such combination, but "/(:foo/)?:bar?" has four) * Parsing a params hash from a string generated from another params hash might not result in two identical hashes, and vice versa. Specifically, expanding ignores capture constraints, type casting and greediness. * Partial expansion is (currently) not supported. ## Details on Pattern Types ### `identity` **Supported options:** [`uri_decode`](#-available-options--uri_decode), [`ignore_unknown_options`](#-available-options--ignore_unknown_options).
Syntax Element Description
any character Matches exactly that character or a URI escaped version of it.
### `regexp` **Supported options:** [`uri_decode`](#-available-options--uri_decode), [`ignore_unknown_options`](#-available-options--ignore_unknown_options), `check_anchors`. The pattern string (or actual Regexp instance) should not contain anchors (`^` outside of square brackets, `$`, `\A`, `\z`, or `\Z`). Anchors will be injected where necessary by Mustermann. By default, Mustermann will raise a `Mustermann::CompileError` if an anchor is encountered. If you still want it to contain anchors at your own risk, set the `check_anchors` option to `false`. Using anchors will break [peeking](#-peeking) and [concatenation](#-concatenation).
Syntax Element Description
any string Interpreted as regular expression.
### `sinatra` **Supported options:** [`capture`](#-available-options--capture), [`except`](#-available-options--except), [`greedy`](#-available-options--greedy), [`space_matches_plus`](#-available-options--space_matches_plus), [`uri_decode`](#-available-options--uri_decode), [`ignore_unknown_options`](#-available-options--ignore_unknown_options).
Syntax Element Description
:name or {name} Captures anything but a forward slash in a semi-greedy fashion. Capture is named name. Capture behavior can be modified with capture and greedy option.
*name or {+name} Captures anything in a non-greedy fashion. Capture is named name.
* or {+splat} Captures anything in a non-greedy fashion. Capture is named splat. It is always an array of captures, as you can use it more than once in a pattern.
(expression) Enclosed expression is a group. Useful when combined with ? to make it optional, or to separate two elements that would otherwise be parsed as one.
expression|expression|... Will match anything matching the nested expressions. May contain any other syntax element, including captures.
x? Makes x optional. For instance, (foo)? matches foo or an empty string.
/ Matches forward slash. Does not match URI encoded version of forward slash.
\x Matches x or URI encoded version of x. For instance \* matches *.
any other character Matches exactly that character or a URI encoded version of it.
mustermann-1.1.1/mustermann/bench/000077500000000000000000000000001360365612700171645ustar00rootroot00000000000000mustermann-1.1.1/mustermann/bench/capturing.rb000066400000000000000000000025661360365612700215160ustar00rootroot00000000000000$:.unshift File.expand_path('../lib', __dir__) require 'benchmark' require 'mustermann' require 'mustermann/regexp_based' require 'addressable/template' Mustermann.register(:regexp, Class.new(Mustermann::RegexpBased) { def compile(**options) Regexp.new(@string) end }, load: false) Mustermann.register(:addressable, Class.new(Mustermann::RegexpBased) { def compile(**options) Addressable::Template.new(@string) end }, load: false) list = [ [:sinatra, '/*/:name' ], [:rails, '/*prefix/:name' ], [:simple, '/*/:name' ], [:template, '{/prefix*}/{name}' ], [:regexp, '\A\/(?.*?)\/(?[^\/\?#]+)\Z' ], [:addressable, '{/prefix*}/{name}' ] ] def self.assert(value) fail unless value end string = '/a/b/c/d' name = 'd' GC.disable puts "Compilation:" Benchmark.bmbm do |x| list.each do |type, pattern| x.report(type) { 1_000.times { Mustermann.new(pattern, type: type) } } end end puts "", "Matching with two captures (one splat, one normal):" Benchmark.bmbm do |x| list.each do |type, pattern| pattern = Mustermann.new(pattern, type: type) x.report type do 10_000.times do match = pattern.match(string) assert match[:name] == name end end end endmustermann-1.1.1/mustermann/bench/regexp.rb000066400000000000000000000007741360365612700210130ustar00rootroot00000000000000require 'benchmark' puts " atomic vs normal segments ".center(52, '=') types = { normal: /\A\/(?:a|%61)\/(?[^\/\?#]+)(?:\/(?[^\/\?#]+))?\Z/, atomic: /\A\/(?:a|%61)\/(?(?>[^\/\?#]+))(?:\/(?(?>[^\/\?#]+)))?\Z/ } Benchmark.bmbm do |x| types.each do |name, regexp| string = "/a/" << ?a * 10000 << "/" << ?a * 5000 fail unless regexp.match(string) string << "/" fail if regexp.match(string) x.report name.to_s do 100.times { regexp.match(string) } end end endmustermann-1.1.1/mustermann/bench/simple_vs_sinatra.rb000066400000000000000000000015071360365612700232360ustar00rootroot00000000000000$:.unshift File.expand_path('../lib', __dir__) require 'benchmark' require 'mustermann/simple' require 'mustermann/sinatra' [Mustermann::Simple, Mustermann::Sinatra].each do |klass| puts "", " #{klass} ".center(64, '=') Benchmark.bmbm do |x| no_capture = klass.new("/simple") x.report("no captures, match") { 1_000.times { no_capture.match('/simple') } } x.report("no captures, miss") { 1_000.times { no_capture.match('/miss') } } simple = klass.new("/:name") x.report("simple, match") { 1_000.times { simple.match('/simple').captures } } x.report("simple, miss") { 1_000.times { simple.match('/mi/ss') } } splat = klass.new("/*") x.report("splat, match") { 1_000.times { splat.match("/a/b/c").captures } } x.report("splat, miss") { 1_000.times { splat.match("/a/b/c.miss") } } end puts end mustermann-1.1.1/mustermann/bench/template_vs_addressable.rb000066400000000000000000000017621360365612700243730ustar00rootroot00000000000000$:.unshift File.expand_path('../lib', __dir__) require 'benchmark' require 'mustermann/template' require 'addressable/template' [Mustermann::Template, Addressable::Template].each do |klass| puts "", " #{klass} ".center(64, '=') Benchmark.bmbm do |x| no_capture = klass.new("/simple") x.report("no captures, match") { 1_000.times { no_capture.match('/simple') } } x.report("no captures, miss") { 1_000.times { no_capture.match('/miss') } } simple = klass.new("/{match}") x.report("simple, match") { 1_000.times { simple.match('/simple').captures } } x.report("simple, miss") { 1_000.times { simple.match('/mi/ss') } } explode = klass.new("{/segments*}") x.report("explode, match") { 1_000.times { explode.match("/a/b/c").captures } } x.report("explode, miss") { 1_000.times { explode.match("/a/b/c.miss") } } expand = klass.new("/prefix/{foo}/something/{bar}") x.report("expand") { 100.times { expand.expand(foo: 'foo', bar: 'bar').to_s } } end puts end mustermann-1.1.1/mustermann/lib/000077500000000000000000000000001360365612700166535ustar00rootroot00000000000000mustermann-1.1.1/mustermann/lib/mustermann.rb000066400000000000000000000115371360365612700214000ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/pattern' require 'mustermann/composite' require 'mustermann/concat' require 'thread' # Namespace and main entry point for the Mustermann library. # # Under normal circumstances the only external API entry point you should be using is {Mustermann.new}. module Mustermann # Type to use if no type is given. # @api private DEFAULT_TYPE = :sinatra # Creates a new pattern based on input. # # * From {Mustermann::Pattern}: returns given pattern. # * From String: creates a pattern from the string, depending on type option (defaults to {Mustermann::Sinatra}) # * From Regexp: creates a {Mustermann::Regular} pattern. # * From Symbol: creates a {Mustermann::Sinatra} pattern with a single named capture named after the input. # * From an Array or multiple inputs: creates a new pattern from each element, combines them to a {Mustermann::Composite}. # * From anything else: Will try to call to_pattern on it or raise a TypeError. # # Note that if the input is a {Mustermann::Pattern}, Regexp or Symbol, the type option is ignored and if to_pattern is # called on the object, the type will be handed on but might be ignored by the input object. # # If you want to enforce the pattern type, you should create them via their expected class. # # @example creating patterns # require 'mustermann' # # Mustermann.new("/:name") # => # # Mustermann.new("/{name}", type: :template) # => # # Mustermann.new(/.*/) # => # # Mustermann.new(:name, capture: :word) # => # # Mustermann.new("/", "/*.jpg", type: :shell) # => # # # @example using custom #to_pattern # require 'mustermann' # # class MyObject # def to_pattern(**options) # Mustermann.new("/:name", **options) # end # end # # Mustermann.new(MyObject.new, type: :rails) # => # # # @example enforcing type # require 'mustermann/sinatra' # # Mustermann::Sinatra.new("/:name") # # @param [String, Pattern, Regexp, Symbol, #to_pattern, Array] # input The representation of the pattern # @param [Hash] options The options hash # @return [Mustermann::Pattern] pattern corresponding to string. # @raise (see []) # @raise (see Mustermann::Pattern.new) # @raise [TypeError] if the passed object cannot be converted to a pattern # @see file:README.md#Types_and_Options "Types and Options" in the README def self.new(*input, type: DEFAULT_TYPE, operator: :|, **options) type ||= DEFAULT_TYPE input = input.first if input.size < 2 case input when Pattern then input when Regexp then self[:regexp].new(input, **options) when String then self[type].new(input, **options) when Symbol then self[:sinatra].new(input.inspect, **options) when Array then input.map { |i| new(i, type: type, **options) }.inject(operator) else pattern = input.to_pattern(type: type, **options) if input.respond_to? :to_pattern raise TypeError, "#{input.class} can't be coerced into Mustermann::Pattern" if pattern.nil? pattern end end @mutex ||= Mutex.new @types ||= {} # Maps a type to its factory. # # @example # Mustermann[:sinatra] # => Mustermann::Sinatra # # @param [Symbol] name a pattern type identifier # @raise [ArgumentError] if the type is not supported # @return [Class, #new] pattern factory def self.[](name) return name if name.respond_to? :new @types.fetch(normalized = normalized_type(name)) do @mutex.synchronize do error = try_require "mustermann/#{normalized}" @types.fetch(normalized) { raise ArgumentError, "unsupported type %p#{" (#{error.message})" if error}" % name } end end end # @return [LoadError, nil] # @!visibility private def self.try_require(path) require(path) nil rescue LoadError => error raise(error) unless error.path == path error end # @!visibility private def self.register(name, type) @types[normalized_type(name)] = type end # @!visibility private def self.normalized_type(type) type.to_s.gsub('-', '_').downcase end # @!visibility private def self.extend_object(object) return super unless defined? ::Sinatra::Base and object.is_a? Class and object < ::Sinatra::Base require 'mustermann/extension' object.register Extension end end # :nocov: begin require 'mustermann/visualizer' if defined?(Pry) or defined?(IRB) rescue LoadError => error raise error unless error.path == 'mustermann/visualizer' $stderr.puts(error.message) if caller_locations[1].absolute_path =~ %r{/lib/pry/|/irb/|^\((?:irb|pry)\)$} end # :nocov: mustermann-1.1.1/mustermann/lib/mustermann/000077500000000000000000000000001360365612700210445ustar00rootroot00000000000000mustermann-1.1.1/mustermann/lib/mustermann/ast/000077500000000000000000000000001360365612700216335ustar00rootroot00000000000000mustermann-1.1.1/mustermann/lib/mustermann/ast/boundaries.rb000066400000000000000000000026241360365612700243170ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/ast/translator' module Mustermann module AST # Make sure #start and #stop is set on every node and within its parents #start and #stop. # @!visibility private class Boundaries < Translator # @return [Mustermann::AST::Node] the ast passed as first argument # @!visibility private def self.set_boundaries(ast, string: nil, start: 0, stop: string.length) new.translate(ast, start, stop) ast end translate(:node) do |start, stop| t.set_boundaries(node, start, stop) t(payload, node.start, node.stop) end translate(:with_look_ahead) do |start, stop| t.set_boundaries(node, start, stop) t(head, node.start, node.stop) t(payload, node.start, node.stop) end translate(Array) do |start, stop| each do |subnode| t(subnode, start, stop) start = subnode.stop end end translate(Object) { |*| node } # Checks that a node is within the given boundaries. # @!visibility private def set_boundaries(node, start, stop) node.start = start if node.start.nil? or node.start < start node.stop = node.start + node.min_size if node.stop.nil? or node.stop < node.start node.stop = stop if node.stop > stop end end end end mustermann-1.1.1/mustermann/lib/mustermann/ast/compiler.rb000066400000000000000000000144421360365612700237770ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/ast/translator' module Mustermann # @see Mustermann::AST::Pattern module AST # Regexp compilation logic. # @!visibility private class Compiler < Translator raises CompileError # Trivial compilations translate(Array) { |**o| map { |e| t(e, **o) }.join } translate(:node) { |**o| t(payload, **o) } translate(:separator) { |**o| Regexp.escape(payload) } translate(:optional) { |**o| "(?:%s)?" % t(payload, **o) } translate(:char) { |**o| t.encoded(payload, **o) } translate :union do |**options| "(?:%s)" % payload.map { |e| "(?:%s)" % t(e, **options) }.join(?|) end translate :expression do |greedy: true, **options| t(payload, allow_reserved: operator.allow_reserved, greedy: greedy && !operator.allow_reserved, parametric: operator.parametric, separator: operator.separator, **options) end translate :with_look_ahead do |**options| lookahead = each_leaf.inject("") do |ahead, element| ahead + t(element, skip_optional: true, lookahead: ahead, greedy: false, no_captures: true, **options).to_s end lookahead << (at_end ? '$' : '/') t(head, lookahead: lookahead, **options) + t(payload, **options) end # Capture compilation is complex. :( # @!visibility private class Capture < NodeTranslator register :capture # @!visibility private def translate(**options) return pattern(**options) if options[:no_captures] "(?<#{name}>#{translate(no_captures: true, **options)})" end # @return [String] regexp without the named capture # @!visibility private def pattern(capture: nil, **options) case capture when Symbol then from_symbol(capture, **options) when Array then from_array(capture, **options) when Hash then from_hash(capture, **options) when String then from_string(capture, **options) when nil then from_nil(**options) else capture end end private def qualified(string, greedy: true, **options) "#{string}#{qualifier || "+#{?? unless greedy}"}" end def with_lookahead(string, lookahead: nil, **options) lookahead ? "(?:(?!#{lookahead})#{string})" : string end def from_hash(hash, **options) pattern(capture: hash[name.to_sym], **options) end def from_array(array, **options) Regexp.union(*array.map { |e| pattern(capture: e, **options) }) end def from_symbol(symbol, **options) qualified(with_lookahead("[[:#{symbol}:]]", **options), **options) end def from_string(string, **options) Regexp.new(string.chars.map { |c| t.encoded(c, **options) }.join) end def from_nil(**options) qualified(with_lookahead(default(**options), **options), **options) end def default(**options) constraint || "[^/\\?#]" end end # @!visibility private class Splat < Capture register :splat, :named_splat # splats are always non-greedy # @!visibility private def pattern(**options) constraint || ".*?" end end # @!visibility private class Variable < Capture register :variable # @!visibility private def translate(**options) return super(**options) if explode or not options[:parametric] # Remove this line after fixing broken compatibility between 2.1 and 2.2 options.delete(:parametric) if options.has_key?(:parametric) parametric super(parametric: false, **options) end # @!visibility private def pattern(parametric: false, separator: nil, **options) register_param(parametric: parametric, separator: separator, **options) pattern = super(**options) pattern = parametric(pattern) if parametric pattern = "#{pattern}(?:#{Regexp.escape(separator)}#{pattern})*" if explode and separator pattern end # @!visibility private def parametric(string) "#{Regexp.escape(name)}(?:=#{string})?" end # @!visibility private def qualified(string, **options) prefix ? "#{string}{1,#{prefix}}" : super(string, **options) end # @!visibility private def default(allow_reserved: false, **options) allow_reserved ? '[\w\-\.~%\:/\?#\[\]@\!\$\&\'\(\)\*\+,;=]' : '[\w\-\.~%]' end # @!visibility private def register_param(parametric: false, split_params: nil, separator: nil, **options) return unless explode and split_params split_params[name] = { separator: separator, parametric: parametric } end end # @return [String] Regular expression for matching the given character in all representations # @!visibility private def encoded(char, uri_decode: true, space_matches_plus: true, **options) return Regexp.escape(char) unless uri_decode encoded = escape(char, escape: /./) list = [escape(char), encoded.downcase, encoded.upcase].uniq.map { |c| Regexp.escape(c) } if char == " " list << encoded('+') if space_matches_plus list << " " end "(?:%s)" % list.join("|") end # Compiles an AST to a regular expression. # @param [Mustermann::AST::Node] ast the tree # @return [Regexp] corresponding regular expression. # # @!visibility private def self.compile(ast, **options) new.compile(ast, **options) end # Compiles an AST to a regular expression. # @param [Mustermann::AST::Node] ast the tree # @return [Regexp] corresponding regular expression. # # @!visibility private def compile(ast, except: nil, **options) except &&= "(?!#{translate(except, no_captures: true, **options)}\\Z)" Regexp.new("#{except}#{translate(ast, **options)}") end end private_constant :Compiler end end mustermann-1.1.1/mustermann/lib/mustermann/ast/expander.rb000066400000000000000000000104151360365612700237670ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/ast/translator' require 'mustermann/ast/compiler' require 'ruby2_keywords' module Mustermann module AST # Looks at an AST, remembers the important bits of information to do an # ultra fast expansion. # # @!visibility private class Expander < Translator raises ExpandError translate Array do |*args| inject(t.pattern) do |pattern, element| t.add_to(pattern, t(element, *args)) end end translate :capture do |**options| t.for_capture(node, **options) end translate :named_splat, :splat do t.pattern + t.for_capture(node) end translate :expression do t(payload, allow_reserved: operator.allow_reserved) end translate :root, :group do t(payload) end translate :char do t.pattern(t.escape(payload, also_escape: /[\/\?#\&\=%]/).gsub(?%, "%%")) end translate :separator do t.pattern(payload.gsub(?%, "%%")) end translate :with_look_ahead do t.add_to(t(head), t(payload)) end translate :optional do nested = t(payload) nested += t.pattern unless nested.any? { |n| n.first.empty? } nested end translate :union do payload.map { |e| t(e) }.inject(:+) end # helper method for captures # @!visibility private def for_capture(node, **options) name = node.name.to_sym pattern('%s', name, name => /(?!#{pattern_for(node, **options)})./) end # maps sorted key list to sprintf patterns and filters # @!visibility private def mappings @mappings ||= {} end # all the known keys # @!visibility private def keys @keys ||= [] end # add a tree for expansion # @!visibility private def add(ast) translate(ast).each do |keys, pattern, filter| self.keys.concat(keys).uniq! mappings[keys.sort] ||= [keys, pattern, filter] end end # helper method for getting a capture's pattern. # @!visibility private def pattern_for(node, **options) Compiler.new.decorator_for(node).pattern(**options) end # @see Mustermann::Pattern#expand # @!visibility private def expand(values) adjusted = values.each_with_object({}){ |(key, value), new_hash| new_hash[value.instance_of?(Array) ? [key] * value.length : key] = value } keys, pattern, filters = mappings.fetch(adjusted.keys.flatten.sort) { error_for(values) } filters.each { |key, filter| adjusted[key] &&= escape(adjusted[key], also_escape: filter) } pattern % (adjusted[keys] || adjusted.values_at(*keys)) end # @see Mustermann::Pattern#expandable? # @!visibility private def expandable?(values) values = values.keys if values.respond_to? :keys values = values.sort if values.respond_to? :sort mappings.include? values end # @see Mustermann::Expander#with_rest # @!visibility private def expandable_keys(keys) mappings.keys.select { |k| (k - keys).empty? }.max_by(&:size) || keys end # helper method for raising an error for unexpandable values # @!visibility private def error_for(values) expansions = mappings.keys.map(&:inspect).join(" or ") raise error_class, "cannot expand with keys %p, possible expansions: %s" % [values.keys.sort, expansions] end # @see Mustermann::AST::Translator#expand # @!visibility private ruby2_keywords def escape(string, *args) # URI::Parser is pretty slow, let's not send every string to it, even if it's unnecessary string =~ /\A\w*\Z/ ? string : super end # Turns a sprintf pattern into our secret internal data structure. # @!visibility private def pattern(string = "", *keys, **filters) [[keys, string, filters]] end # Creates the product of two of our secret internal data structures. # @!visibility private def add_to(list, result) list << [[], ""] if list.empty? list.inject([]) { |l, (k1, p1, f1)| l + result.map { |k2, p2, f2| [k1+k2, p1+p2, f1.merge(f2)] } } end end end end mustermann-1.1.1/mustermann/lib/mustermann/ast/node.rb000066400000000000000000000130011360365612700231000ustar00rootroot00000000000000module Mustermann # @see Mustermann::AST::Pattern module AST # @!visibility private class Node # @!visibility private attr_accessor :payload, :start, :stop # @!visibility private # @param [Symbol] name of the node # @return [Class] factory for the node def self.[](name) @names ||= {} @names[name] ||= begin const_name = constant_name(name) Object.const_get(const_name) if Object.const_defined?(const_name) end end # Turns a class name into a node identifier. # @!visibility private def self.type name[/[^:]+$/].split(/(?<=.)(?=[A-Z])/).map(&:downcase).join(?_).to_sym end # @!visibility private # @param [Symbol] name of the node # @return [String] qualified name of factory for the node def self.constant_name(name) return self.name if name.to_sym == :node name = name.to_s.split(?_).map(&:capitalize).join "#{self.name}::#{name}" end # Helper for creating a new instance and calling #parse on it. # @return [Mustermann::AST::Node] # @!visibility private def self.parse(*args, &block) new(*args).tap { |n| n.parse(&block) } end # @!visibility private def initialize(payload = nil, **options) options.each { |key, value| public_send("#{key}=", value) } self.payload = payload end # @!visibility private def is_a?(type) type = Node[type] if type.is_a? Symbol super(type) end # Double dispatch helper for reading from the buffer into the payload. # @!visibility private def parse self.payload ||= [] while element = yield payload << element end end # Loop through all nodes that don't have child nodes. # @!visibility private def each_leaf(&block) return enum_for(__method__) unless block_given? called = false Array(payload).each do |entry| next unless entry.respond_to? :each_leaf entry.each_leaf(&block) called = true end yield(self) unless called end # @return [Integer] length of the substring # @!visibility private def length stop - start if start and stop end # @return [Integer] minimum size for a node # @!visibility private def min_size 0 end # Turns a class name into a node identifier. # @!visibility private def type self.class.type end # @!visibility private class Capture < Node # @see Mustermann::AST::Compiler::Capture#default # @!visibility private attr_accessor :constraint # @see Mustermann::AST::Compiler::Capture#qualified # @!visibility private attr_accessor :qualifier # @see Mustermann::AST::Pattern#map_param # @!visibility private attr_accessor :convert # @see Mustermann::AST::Node#parse # @!visibility private def parse self.payload ||= "" super end # @!visibility private alias_method :name, :payload end # @!visibility private class Char < Node # @return [Integer] minimum size for a node # @!visibility private def min_size 1 end end # AST node for template expressions. # @!visibility private class Expression < Node # @!visibility private attr_accessor :operator end # @!visibility private class Composition < Node # @!visibility private def initialize(payload = nil, **options) super(Array(payload), **options) end end # @!visibility private class Group < Composition end # @!visibility private class Union < Composition end # @!visibility private class Optional < Node end # @!visibility private class Or < Node end # @!visibility private class Root < Node # @!visibility private attr_accessor :pattern # Will trigger transform. # # @see Mustermann::AST::Node.parse # @!visibility private def self.parse(string, &block) root = new root.pattern = string root.parse(&block) root end end # @!visibility private class Separator < Node # @return [Integer] minimum size for a node # @!visibility private def min_size 1 end end # @!visibility private class Splat < Capture # @see Mustermann::AST::Node::Capture#name # @!visibility private def name "splat" end end # @!visibility private class NamedSplat < Splat # @see Mustermann::AST::Node::Capture#name # @!visibility private alias_method :name, :payload end # AST node for template variables. # @!visibility private class Variable < Capture # @!visibility private attr_accessor :prefix, :explode end # @!visibility private class WithLookAhead < Node # @!visibility private attr_accessor :head, :at_end # @!visibility private def initialize(payload, at_end, **options) super(**options) self.head, *self.payload = Array(payload) self.at_end = at_end end end end end end mustermann-1.1.1/mustermann/lib/mustermann/ast/param_scanner.rb000066400000000000000000000010711360365612700247700ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/ast/translator' module Mustermann module AST # Scans an AST for param converters. # @!visibility private # @see Mustermann::AST::Pattern#to_templates class ParamScanner < Translator # @!visibility private def self.scan_params(ast) new.translate(ast) end translate(:node) { t(payload) } translate(Array) { map { |e| t(e) }.inject(:merge) } translate(Object) { {} } translate(:capture) { convert ? { name => convert } : {} } end end end mustermann-1.1.1/mustermann/lib/mustermann/ast/parser.rb000066400000000000000000000174201360365612700234600ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/ast/node' require 'forwardable' require 'ruby2_keywords' require 'strscan' module Mustermann # @see Mustermann::AST::Pattern module AST # Simple, StringScanner based parser. # @!visibility private class Parser # @param [String] string to be parsed # @return [Mustermann::AST::Node] parse tree for string # @!visibility private def self.parse(string, **options) new(**options).parse(string) end # Defines another grammar rule for first character. # # @see Mustermann::Rails # @see Mustermann::Sinatra # @see Mustermann::Template # @!visibility private def self.on(*chars, &block) chars.each do |char| define_method("read %p" % char, &block) end end # Defines another grammar rule for a suffix. # # @see Mustermann::Sinatra # @!visibility private def self.suffix(pattern = /./, after: :node, &block) @suffix ||= [] @suffix << [pattern, after, block] if block @suffix end # @!visibility private attr_reader :buffer, :string, :pattern extend Forwardable def_delegators :buffer, :eos?, :getch, :pos # @!visibility private def initialize(pattern: nil, **options) @pattern = pattern end # @param [String] string to be parsed # @return [Mustermann::AST::Node] parse tree for string # @!visibility private def parse(string) @string = string @buffer = ::StringScanner.new(string) node(:root, string) { read unless eos? } end # @example # node(:char, 'x').compile =~ 'x' # => true # # @param [Symbol] type node type # @return [Mustermann::AST::Node] # @!visibility private ruby2_keywords def node(type, *args, &block) type = Node[type] unless type.respond_to? :new start = pos node = block ? type.parse(*args, &block) : type.new(*args) min_size(start, pos, node) end # Create a node for a character we don't have an explicit rule for. # # @param [String] char the character # @return [Mustermann::AST::Node] the node # @!visibility private def default_node(char) char == ?/ ? node(:separator, char) : node(:char, char) end # Reads the next element from the buffer. # @return [Mustermann::AST::Node] next element # @!visibility private def read start = pos char = getch method = "read %p" % char element= respond_to?(method) ? send(method, char) : default_node(char) min_size(start, pos, element) read_suffix(element) end # sets start on node to start if it's not set to a lower value. # sets stop on node to stop if it's not set to a higher value. # @return [Mustermann::AST::Node] the node passed as third argument # @!visibility private def min_size(start, stop, node) stop ||= start start ||= stop node.start = start unless node.start and node.start < start node.stop = stop unless node.stop and node.stop > stop node end # Checks for a potential suffix on the buffer. # @param [Mustermann::AST::Node] element node without suffix # @return [Mustermann::AST::Node] node with suffix # @!visibility private def read_suffix(element) self.class.suffix.inject(element) do |ele, (regexp, after, callback)| next ele unless ele.is_a?(after) and payload = scan(regexp) content = instance_exec(payload, ele, &callback) min_size(element.start, pos, content) end end # Wrapper around {StringScanner#scan} that turns strings into escaped # regular expressions and returns a MatchData if the regexp has any # named captures. # # @param [Regexp, String] regexp # @see StringScanner#scan # @return [String, MatchData, nil] # @!visibility private def scan(regexp) regexp = Regexp.new(Regexp.escape(regexp)) unless regexp.is_a? Regexp string = buffer.scan(regexp) regexp.names.any? ? regexp.match(string) : string end # Asserts a regular expression matches what's next on the buffer. # Will return corresponding MatchData if regexp includes named captures. # # @param [Regexp] regexp expected to match # @return [String, MatchData] the match # @raise [Mustermann::ParseError] if expectation wasn't met # @!visibility private def expect(regexp, char: nil, **options) scan(regexp) || unexpected(char, **options) end # Allows to read a string inside brackets. It does not expect the string # to start with an opening bracket. # # @example # buffer.string = "fo>ba" # read_brackets(?<, ?>) # => "fo" # buffer.rest # => "ba" # # @!visibility private def read_brackets(open, close, char: nil, escape: ?\\, quote: false, **options) result = String.new escape = false if escape.nil? while (current = getch) case current when close then return result when open then result << open << read_brackets(open, close) << close when escape then result << escape << getch else result << current end end unexpected(char, **options) end # Reads an argument string of the format arg1,args2,key:value # # @!visibility private def read_args(key_separator, close, separator: ?,, symbol_keys: true, **options) list, map = [], {} while buffer.peek(1) != close scan(separator) entries = read_list(close, separator, separator: key_separator, **options) case entries.size when 1 then list += entries when 2 then map[symbol_keys ? entries.first.to_sym : entries.first] = entries.last else unexpected(key_separator) end buffer.pos -= 1 end expect(close) [list, map] end # Reads a separated list with the ability to quote, escape and add spaces. # # @!visibility private def read_list(*close, separator: ?,, escape: ?\\, quotes: [?", ?'], ignore: " ", **options) result = [] while current = getch element = result.empty? ? result : result.last case current when *close then return result when ignore then nil # do nothing when separator then result << String.new when escape then element << getch when *quotes then element << read_escaped(current, escape: escape) else element << current end end unexpected(current, **options) end # Read a string until a terminating character, ignoring escaped versions of said character. # # @!visibility private def read_escaped(close, escape: ?\\, **options) result = String.new while current = getch case current when close then return result when escape then result << getch else result << current end end unexpected(current, **options) end # Helper for raising an exception for an unexpected character. # Will read character from buffer if buffer is passed in. # # @param [String, nil] char the unexpected character # @raise [Mustermann::ParseError, Exception] # @!visibility private def unexpected(char = nil, exception: ParseError) char ||= getch char = "space" if char == " " raise exception, "unexpected #{char || "end of string"} while parsing #{string.inspect}" end end end end mustermann-1.1.1/mustermann/lib/mustermann/ast/pattern.rb000066400000000000000000000104751360365612700236440ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/ast/parser' require 'mustermann/ast/boundaries' require 'mustermann/ast/compiler' require 'mustermann/ast/transformer' require 'mustermann/ast/validation' require 'mustermann/ast/template_generator' require 'mustermann/ast/param_scanner' require 'mustermann/regexp_based' require 'mustermann/expander' require 'mustermann/equality_map' module Mustermann # @see Mustermann::AST::Pattern module AST # Superclass for pattern styles that parse an AST from the string pattern. # @abstract class Pattern < Mustermann::RegexpBased supported_options :capture, :except, :greedy, :space_matches_plus extend Forwardable, SingleForwardable single_delegate on: :parser, suffix: :parser instance_delegate %i[parser compiler transformer validation template_generator param_scanner boundaries] => 'self.class' instance_delegate parse: :parser, transform: :transformer, validate: :validation, generate_templates: :template_generator, scan_params: :param_scanner, set_boundaries: :boundaries # @api private # @return [#parse] parser object for pattern # @!visibility private def self.parser return Parser if self == AST::Pattern const_set :Parser, Class.new(superclass.parser) unless const_defined? :Parser, false const_get :Parser end # @api private # @return [#compile] compiler object for pattern # @!visibility private def self.compiler Compiler end # @api private # @return [#set_boundaries] translator making sure start and stop is set on all nodes # @!visibility private def self.boundaries Boundaries end # @api private # @return [#transform] transformer object for pattern # @!visibility private def self.transformer Transformer end # @api private # @return [#validate] validation object for pattern # @!visibility private def self.validation Validation end # @api private # @return [#generate_templates] generates URI templates for pattern # @!visibility private def self.template_generator TemplateGenerator end # @api private # @return [#scan_params] param scanner for pattern # @!visibility private def self.param_scanner ParamScanner end # @!visibility private def compile(**options) options[:except] &&= parse options[:except] compiler.compile(to_ast, **options) rescue CompileError => error raise error.class, "#{error.message}: #{@string.inspect}", error.backtrace end # Internal AST representation of pattern. # @!visibility private def to_ast @ast_cache ||= EqualityMap.new @ast_cache.fetch(@string) do ast = parse(@string, pattern: self) ast &&= transform(ast) ast &&= set_boundaries(ast, string: @string) validate(ast) end end # All AST-based pattern implementations support expanding. # # @example (see Mustermann::Pattern#expand) # @param (see Mustermann::Pattern#expand) # @return (see Mustermann::Pattern#expand) # @raise (see Mustermann::Pattern#expand) # @see Mustermann::Pattern#expand # @see Mustermann::Expander def expand(behavior = nil, values = {}) @expander ||= Mustermann::Expander.new(self) @expander.expand(behavior, values) end # All AST-based pattern implementations support generating templates. # # @example (see Mustermann::Pattern#to_templates) # @param (see Mustermann::Pattern#to_templates) # @return (see Mustermann::Pattern#to_templates) # @see Mustermann::Pattern#to_templates def to_templates @to_templates ||= generate_templates(to_ast) end # @!visibility private # @see Mustermann::Pattern#map_param def map_param(key, value) return super unless param_converters.include? key param_converters[key][super] end # @!visibility private def param_converters @param_converters ||= scan_params(to_ast) end private :compile, :parse, :transform, :validate, :generate_templates, :param_converters, :scan_params, :set_boundaries end end end mustermann-1.1.1/mustermann/lib/mustermann/ast/template_generator.rb000066400000000000000000000021611360365612700260410ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/ast/translator' module Mustermann module AST # Turns an AST into an Array of URI templates representing the AST. # @!visibility private # @see Mustermann::AST::Pattern#to_templates class TemplateGenerator < Translator # @!visibility private def self.generate_templates(ast) new.translate(ast).uniq end # translate(:expression) is not needed, since template patterns simply call to_s translate(:root, :group) { t(payload) || [""] } translate(:separator, :char) { t.escape(payload) } translate(:capture) { "{#{name}}" } translate(:optional) { [t(payload), ""] } translate(:named_splat, :splat) { "{+#{name}}" } translate(:with_look_ahead) { t([head, payload]) } translate(:union) { payload.flat_map { |e| t(e) } } translate(Array) do map { |e| Array(t(e)) }.inject { |first, second| first.product(second).map(&:join) } end end end end mustermann-1.1.1/mustermann/lib/mustermann/ast/transformer.rb000066400000000000000000000137521360365612700245320ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/ast/translator' module Mustermann module AST # Takes a tree, turns it into an even better tree. # @!visibility private class Transformer < Translator # Transforms a tree. # @note might mutate handed in tree instead of creating a new one # @param [Mustermann::AST::Node] tree to be transformed # @return [Mustermann::AST::Node] transformed tree # @!visibility private def self.transform(tree) new.translate(tree) end # recursive descent translate(:node) do node.payload = t(payload) node end # ignore unknown objects on the tree translate(Object) { node } # turn a group containing or nodes into a union # @!visibility private class GroupTransformer < NodeTranslator register :group # @!visibility private def translate payload.flatten! if payload.is_a?(Array) return union if payload.any? { |e| e.is_a? :or } self.payload = t(payload) self end # @!visibility private def union groups = split_payload.map { |g| group(g) } Node[:union].new(groups, start: node.start, stop: node.stop) end # @!visibility private def group(elements) return t(elements.first) if elements.size == 1 start, stop = elements.first.start, elements.last.stop if elements.any? Node[:group].new(t(elements), start: start, stop: stop) end # @!visibility private def split_payload groups = [[]] payload.each { |e| e.is_a?(:or) ? groups << [] : groups.last << e } groups.map! end end # inject a union node right inside the root node if it contains or nodes # @!visibility private class RootTransformer < GroupTransformer register :root # @!visibility private def union self.payload = [super] self end end # URI expression transformations depending on operator # @!visibility private class ExpressionTransform < NodeTranslator register :expression # @!visibility private Operator ||= Struct.new(:separator, :allow_reserved, :prefix, :parametric) # Operators available for expressions. # @!visibility private OPERATORS ||= { nil => Operator.new(?,, false, false, false), ?+ => Operator.new(?,, true, false, false), ?# => Operator.new(?,, true, ?#, false), ?. => Operator.new(?., false, ?., false), ?/ => Operator.new(?/, false, ?/, false), ?; => Operator.new(?;, false, ?;, true), ?? => Operator.new(?&, false, ??, true), ?& => Operator.new(?&, false, ?&, true) } # Sets operator and inserts separators in between variables. # @!visibility private def translate self.operator = OPERATORS.fetch(operator) { raise CompileError, "#{operator} operator not supported" } separator = Node[:separator].new(operator.separator) prefix = Node[:separator].new(operator.prefix) self.payload = Array(payload.inject { |list, element| Array(list) << t(separator.dup) << t(element) }) payload.unshift(prefix) if operator.prefix self end end # Inserts with_look_ahead nodes wherever appropriate # @!visibility private class ArrayTransform < NodeTranslator register Array # the new array # @!visibility private def payload @payload ||= [] end # buffer for potential look ahead # @!visibility private def lookahead_buffer @lookahead_buffer ||= [] end # transform the array # @!visibility private def translate each { |e| track t(e) } payload.concat create_lookahead(lookahead_buffer, true) end # handle a single element from the array # @!visibility private def track(element) return list_for(element) << element if lookahead_buffer.empty? return lookahead_buffer << element if lookahead? element lookahead = lookahead_buffer.dup lookahead = create_lookahead(lookahead, false) if element.is_a? Node[:separator] lookahead_buffer.clear payload.concat(lookahead) << element end # turn look ahead buffer into look ahead node # @!visibility private def create_lookahead(elements, *args) return elements unless elements.size > 1 [Node[:with_look_ahead].new(elements, *args, start: elements.first.start, stop: elements.last.stop)] end # can the given element be used in a look-ahead? # @!visibility private def lookahead?(element, in_lookahead = false) case element when Node[:char] then in_lookahead when Node[:group] then lookahead_payload?(element.payload, in_lookahead) when Node[:optional] then lookahead?(element.payload, true) or expect_lookahead?(element.payload) end end # does the list of elements look look-ahead-ish to you? # @!visibility private def lookahead_payload?(payload, in_lookahead) return unless payload[0..-2].all? { |e| lookahead?(e, in_lookahead) } expect_lookahead?(payload.last) or lookahead?(payload.last, in_lookahead) end # can the current element deal with a look-ahead? # @!visibility private def expect_lookahead?(element) return element.class == Node[:capture] unless element.is_a? Node[:group] element.payload.all? { |e| expect_lookahead?(e) } end # helper method for deciding where to put an element for now # @!visibility private def list_for(element) expect_lookahead?(element) ? lookahead_buffer : payload end end end end end mustermann-1.1.1/mustermann/lib/mustermann/ast/translator.rb000066400000000000000000000076661360365612700243700ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/ast/node' require 'mustermann/error' require 'ruby2_keywords' require 'delegate' module Mustermann module AST # Implements translator pattern # # @abstract # @!visibility private class Translator # Encapsulates a single node translation # @!visibility private class NodeTranslator < DelegateClass(Node) # @param [Array] types list of types to register for. # @!visibility private def self.register(*types) types.each do |type| type = Node.constant_name(type) if type.is_a? Symbol translator.dispatch_table[type.to_s] = self end end # @param node [Mustermann::AST::Node, Object] # @param translator [Mustermann::AST::Translator] # # @!visibility private def initialize(node, translator) @translator = translator super(node) end # @!visibility private attr_reader :translator # shorthand for translating a nested object # @!visibility private ruby2_keywords def t(*args, &block) return translator unless args.any? translator.translate(*args, &block) end # @!visibility private alias_method :node, :__getobj__ end # maps types to translations # @!visibility private def self.dispatch_table @dispatch_table ||= {} end # some magic sauce so {NodeTranslator}s know whom to talk to for {#register} # @!visibility private def self.inherited(subclass) node_translator = Class.new(NodeTranslator) node_translator.define_singleton_method(:translator) { subclass } subclass.const_set(:NodeTranslator, node_translator) super end # DSL-ish method for specifying the exception class to use. # @!visibility private def self.raises(error) define_method(:error_class) { error } end # DSL method for defining single method translations. # @!visibility private def self.translate(*types, &block) Class.new(const_get(:NodeTranslator)) do register(*types) define_method(:translate, &block) end end # Enables quick creation of a translator object. # # @example # require 'mustermann' # require 'mustermann/ast/translator' # # translator = Mustermann::AST::Translator.create do # translate(:node) { [type, *t(payload)].flatten.compact } # translate(Array) { map { |e| t(e) } } # translate(Object) { } # end # # ast = Mustermann.new('/:name').to_ast # translator.translate(ast) # => [:root, :separator, :capture] # # @!visibility private def self.create(&block) Class.new(self, &block).new end raises Mustermann::Error # @param [Mustermann::AST::Node, Object] node to translate # @return decorator encapsulating translation # # @!visibility private def decorator_for(node) factory = node.class.ancestors.inject(nil) { |d,a| d || self.class.dispatch_table[a.name] } raise error_class, "#{self.class}: Cannot translate #{node.class}" unless factory factory.new(node, self) end # Start the translation dance for a (sub)tree. # @!visibility private ruby2_keywords def translate(node, *args, &block) result = decorator_for(node).translate(*args, &block) result = result.node while result.is_a? NodeTranslator result end # @return [String] escaped character # @!visibility private def escape(char, parser: URI::DEFAULT_PARSER, escape: parser.regexp[:UNSAFE], also_escape: nil) escape = Regexp.union(also_escape, escape) if also_escape char =~ escape ? parser.escape(char, Regexp.union(*escape)) : char end end end end mustermann-1.1.1/mustermann/lib/mustermann/ast/validation.rb000066400000000000000000000031761360365612700243210ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/ast/translator' module Mustermann module AST # Checks the AST for certain validations, like correct capture names. # # Internally a poor man's visitor (abusing translator to not have to implement a visitor). # @!visibility private class Validation < Translator # Runs validations. # # @param [Mustermann::AST::Node] ast to be validated # @return [Mustermann::AST::Node] the validated ast # @raise [Mustermann::AST::CompileError] if validation fails # @!visibility private def self.validate(ast) new.translate(ast) ast end translate(Object, :splat) {} translate(:node) { t(payload) } translate(Array) { each { |p| t(p)} } translate(:capture) { t.check_name(name, forbidden: ['captures', 'splat'])} translate(:variable, :named_splat) { t.check_name(name, forbidden: 'captures')} # @raise [Mustermann::CompileError] if name is not acceptable # @!visibility private def check_name(name, forbidden: []) raise CompileError, "capture name can't be empty" if name.nil? or name.empty? raise CompileError, "capture name must start with underscore or lower case letter" unless name =~ /^[a-z_]/ raise CompileError, "capture name can't be #{name}" if Array(forbidden).include? name raise CompileError, "can't use the same capture name twice" if names.include? name names << name end # @return [Array] list of capture names in tree # @!visibility private def names @names ||= [] end end end end mustermann-1.1.1/mustermann/lib/mustermann/caster.rb000066400000000000000000000060661360365612700226620ustar00rootroot00000000000000# frozen_string_literal: true require 'delegate' module Mustermann # Class for defining and running simple Hash transformations. # # @example # caster = Mustermann::Caster.new # caster.register(:foo) { |value| { bar: value.upcase } } # caster.cast(foo: "hello", baz: "world") # => { bar: "HELLO", baz: "world" } # # @see Mustermann::Expander#cast # # @!visibility private class Caster < DelegateClass(Array) # @param (see #register) # @!visibility private def initialize(*types, &block) super([]) register(*types, &block) end # @param [Array] types identifier for cast type (some need block) # @!visibility private def register(*types, &block) return if types.empty? and block.nil? types << Any.new(&block) if types.empty? types.each { |type| self << caster_for(type, &block) } end # @param [Symbol, Regexp, #cast, #===] type identifier for cast type (some need block) # @return [#cast] specific cast operation # @!visibility private def caster_for(type, &block) case type when Symbol, Regexp then Key.new(type, &block) else type.respond_to?(:cast) ? type : Value.new(type, &block) end end # Transforms a Hash. # @param [Hash] hash pre-transform Hash # @return [Hash] post-transform Hash # @!visibility private def cast(hash) return hash if empty? merge = {} hash.delete_if do |key, value| next unless casted = lazy.map { |e| e.cast(key, value) }.detect { |e| e } casted = { key => casted } unless casted.respond_to? :to_hash merge.update(casted.to_hash) end hash.update(merge) end # Class for block based casts that are triggered for every key/value pair. # @!visibility private class Any # @!visibility private def initialize(&block) @block = block end # @see Mustermann::Caster#cast # @!visibility private def cast(key, value) case @block.arity when 0 then @block.call when 1 then @block.call(value) else @block.call(key, value) end end end # Class for block based casts that are triggered for key/value pairs with a matching value. # @!visibility private class Value < Any # @param [#===] type used for matching values # @!visibility private def initialize(type, &block) @type = type super(&block) end # @see Mustermann::Caster#cast # @!visibility private def cast(key, value) super if @type === value end end # Class for block based casts that are triggered for key/value pairs with a matching key. # @!visibility private class Key < Any # @param [#===] type used for matching keys # @!visibility private def initialize(type, &block) @type = type super(&block) end # @see Mustermann::Caster#cast # @!visibility private def cast(key, value) super if @type === key end end end end mustermann-1.1.1/mustermann/lib/mustermann/composite.rb000066400000000000000000000062641360365612700234030ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann # Class for pattern objects composed of multiple patterns using binary logic. # @see Mustermann::Pattern#& # @see Mustermann::Pattern#| # @see Mustermann::Pattern#^ class Composite < Pattern attr_reader :patterns, :operator supported_options :operator, :type # @see Mustermann::Pattern.supported? def self.supported?(option, type: nil, **options) return true if super Mustermann[type || Mustermann::DEFAULT_TYPE].supported?(option, **options) end # @return [Mustermann::Pattern] a new composite pattern def self.new(*patterns, **options) patterns = patterns.flatten case patterns.size when 0 then raise ArgumentError, 'cannot create empty composite pattern' when 1 then patterns.first else super(patterns, **options) end end def initialize(patterns, operator: :|, **options) @operator = operator.to_sym @patterns = patterns.flat_map { |p| patterns_from(p, **options) } end # @see Mustermann::Pattern#== def ==(pattern) patterns == patterns_from(pattern) end # @see Mustermann::Pattern#eql? def eql?(pattern) patterns.eql? patterns_from(pattern) end # @see Mustermann::Pattern#hash def hash patterns.hash | operator.hash end # @see Mustermann::Pattern#=== def ===(string) patterns.map { |p| p === string }.inject(operator) end # @see Mustermann::Pattern#params def params(string) with_matching(string, :params) end # @see Mustermann::Pattern#match def match(string) with_matching(string, :match) end # @!visibility private def respond_to_special?(method) return false unless operator == :| patterns.all? { |p| p.respond_to?(method) } end # (see Mustermann::Pattern#expand) def expand(behavior = nil, values = {}) raise NotImplementedError, 'expanding not supported' unless respond_to? :expand @expander ||= Mustermann::Expander.new(*patterns) @expander.expand(behavior, values) end # (see Mustermann::Pattern#to_templates) def to_templates raise NotImplementedError, 'template generation not supported' unless respond_to? :to_templates patterns.flat_map(&:to_templates).uniq end # @return [String] the string representation of the pattern def to_s simple_inspect end # @!visibility private def inspect "#<%p:%s>" % [self.class, simple_inspect] end # @!visibility private def simple_inspect pattern_strings = patterns.map { |p| p.simple_inspect } "(#{pattern_strings.join(" #{operator} ")})" end # @!visibility private def with_matching(string, method) return unless self === string pattern = patterns.detect { |p| p === string } pattern.public_send(method, string) if pattern end # @!visibility private def patterns_from(pattern, **options) return pattern.patterns if pattern.is_a? Composite and pattern.operator == self.operator [options.empty? && pattern.is_a?(Pattern) ? pattern : Mustermann.new(pattern, **options)] end private :with_matching, :patterns_from end end mustermann-1.1.1/mustermann/lib/mustermann/concat.rb000066400000000000000000000100031360365612700226320ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann # Class for pattern objects that are a concatenation of other patterns. # @see Mustermann::Pattern#+ class Concat < Composite # Mixin for patterns to support native concatenation. # @!visibility private module Native # @see Mustermann::Pattern#+ # @!visibility private def +(other) other &&= Mustermann.new(other, type: :identity, **options) if (patterns = look_ahead(other)) && !patterns.empty? concat = (self + patterns.inject(:+)) concat + other.patterns.slice(patterns.length..-1).inject(:+) else return super unless native = native_concat(other) self.class.new(native, **options) end end # @!visibility private def look_ahead(other) return unless other.is_a?(Concat) other.patterns.take_while(&method(:native_concat?)) end # @!visibility private def native_concat(other) "#{self}#{other}" if native_concat?(other) end # @!visibility private def native_concat?(other) other.class == self.class and other.options == options end private :native_concat, :native_concat? end # Should not be used directly. # @!visibility private def initialize(*, **) super AST::Validation.validate(combined_ast) if respond_to? :expand end # @see Mustermann::Composite#operator # @return [Symbol] always :+ def operator :+ end # @see Mustermann::Pattern#=== def ===(string) peek_size(string) == string.size end # @see Mustermann::Pattern#match def match(string) peeked = peek_match(string) peeked if peeked.to_s == string end # @see Mustermann::Pattern#params def params(string) params, size = peek_params(string) params if size == string.size end # @see Mustermann::Pattern#peek_size def peek_size(string) pump(string) { |p,s| p.peek_size(s) } end # @see Mustermann::Pattern#peek_match def peek_match(string) pump(string, initial: SimpleMatch.new) do |pattern, substring| return unless match = pattern.peek_match(substring) [match, match.to_s.size] end end # @see Mustermann::Pattern#peek_params def peek_params(string) pump(string, inject_with: :merge, with_size: true) { |p, s| p.peek_params(s) } end # (see Mustermann::Pattern#expand) def expand(behavior = nil, values = {}) raise NotImplementedError, 'expanding not supported' unless respond_to? :expand @expander ||= Mustermann::Expander.new(self) { combined_ast } @expander.expand(behavior, values) end # (see Mustermann::Pattern#to_templates) def to_templates raise NotImplementedError, 'template generation not supported' unless respond_to? :to_templates @to_templates ||= patterns.inject(['']) { |list, pattern| list.product(pattern.to_templates).map(&:join) }.uniq end # @!visibility private def respond_to_special?(method) method = :to_ast if method.to_sym == :expand patterns.all? { |p| p.respond_to?(method) } end # used to generate results for various methods by scanning through an input string # @!visibility private def pump(string, inject_with: :+, initial: nil, with_size: false) substring = string results = Array(initial) patterns.each do |pattern| result, size = yield(pattern, substring) return unless result results << result size ||= result substring = substring[size..-1] end results = results.inject(inject_with) with_size ? [results, string.size - substring.size] : results end # generates one big AST from all patterns # will not check if patterns support AST generation # @!visibility private def combined_ast payload = patterns.map { |p| AST::Node[:group].new(p.to_ast.payload) } AST::Node[:root].new(payload) end private :combined_ast, :pump end end mustermann-1.1.1/mustermann/lib/mustermann/equality_map.rb000066400000000000000000000036361360365612700240730ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann # A simple wrapper around ObjectSpace::WeakMap that allows matching keys by equality rather than identity. # Used for caching. Note that `fetch` is not guaranteed to return the object, even if it has not been # garbage collected yet, especially when used concurrently. Therefore, the block passed to `fetch` has to # be idempotent. # # @example # class ExpensiveComputation # @map = Mustermann::EqualityMap.new # # def self.new(*args) # @map.fetch(args) { super } # end # end # # @see #fetch class EqualityMap attr_reader :map def self.new defined?(ObjectSpace::WeakMap) ? super : {} end def initialize @keys = {} @map = ObjectSpace::WeakMap.new end # @param [#hash] key for caching # @yield block that will be called to populate entry if missing (has to be idempotent) # @return value stored in map or result of block def fetch(key) identity = @keys[key.hash] if identity == key key = identity elsif key.frozen? key = key.dup end # it is ok that this is not thread-safe, worst case it has double cost in # generating, object equality is not guaranteed anyways @map[key] ||= track(key, yield) end # @param [#hash] key for identifying the object # @param [Object] object to be stored # @return [Object] same as the second parameter def track(key, object) object = object.dup if object.frozen? ObjectSpace.define_finalizer(object, finalizer(key.hash)) @keys[key.hash] = key object end # Finalizer proc needs to be generated in different scope so it doesn't keep a reference to the object. # # @param [Integer] hash for key # @return [Proc] finalizer callback def finalizer(hash) proc { @keys.delete(hash) } end private :track, :finalizer end end mustermann-1.1.1/mustermann/lib/mustermann/error.rb000066400000000000000000000010021360365612700225130ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann unless defined?(Mustermann::Error) Error = Class.new(StandardError) # Raised if anything goes wrong while generating a {Pattern}. CompileError = Class.new(Error) # Raised if anything goes wrong while compiling a {Pattern}. ParseError = Class.new(Error) # Raised if anything goes wrong while parsing a {Pattern}. ExpandError = Class.new(Error) # Raised if anything goes wrong while expanding a {Pattern}. end end mustermann-1.1.1/mustermann/lib/mustermann/expander.rb000066400000000000000000000206311360365612700232010ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/ast/expander' require 'mustermann/caster' require 'mustermann' module Mustermann # Allows fine-grained control over pattern expansion. # # @example # expander = Mustermann::Expander.new(additional_values: :append) # expander << "/users/:user_id" # expander << "/pages/:page_id" # # expander.expand(page_id: 58, format: :html5) # => "/pages/58?format=html5" class Expander attr_reader :patterns, :additional_values, :caster # @param [Array<#to_str, Mustermann::Pattern>] patterns list of patterns to expand, see {#add}. # @param [Symbol] additional_values behavior when encountering additional values, see {#expand}. # @param [Hash] options used when creating/expanding patterns, see {Mustermann.new}. def initialize(*patterns, additional_values: :raise, **options, &block) unless additional_values == :raise or additional_values == :ignore or additional_values == :append raise ArgumentError, "Illegal value %p for additional_values" % additional_values end @patterns = [] @api_expander = AST::Expander.new @additional_values = additional_values @options = options @caster = Caster.new add(*patterns, &block) end # Add patterns to expand. # # @example # expander = Mustermann::Expander.new # expander.add("/:a.jpg", "/:b.png") # expander.expand(a: "pony") # => "/pony.jpg" # # @param [Array<#to_str, Mustermann::Pattern>] patterns list of to add for expansion, Strings will be compiled to patterns. # @return [Mustermann::Expander] the expander def add(*patterns) patterns.each do |pattern| pattern = Mustermann.new(pattern, **@options) if block_given? @api_expander.add(yield(pattern)) else raise NotImplementedError, "expanding not supported for #{pattern.class}" unless pattern.respond_to? :to_ast @api_expander.add(pattern.to_ast) end @patterns << pattern end self end alias_method :<<, :add # Register a block as simple hash transformation that runs before expanding the pattern. # @return [Mustermann::Expander] the expander # # @overload cast # Register a block as simple hash transformation that runs before expanding the pattern for all entries. # # @example casting everything that implements to_param to param # expander.cast { |o| o.to_param if o.respond_to? :to_param } # # @yield every key/value pair # @yieldparam key [Symbol] omitted if block takes less than 2 # @yieldparam value [Object] omitted if block takes no arguments # @yieldreturn [Hash{Symbol: Object}] will replace key/value pair with returned hash # @yieldreturn [nil, false] will keep key/value pair in hash # @yieldreturn [Object] will replace value with returned object # # @overload cast(*type_matchers) # Register a block as simple hash transformation that runs before expanding the pattern for certain entries. # # @example convert user to user_id # expander = Mustermann::Expander.new('/users/:user_id') # expand.cast(:user) { |user| { user_id: user.id } } # # expand.expand(user: User.current) # => "/users/42" # # @example convert user, page, image to user_id, page_id, image_id # expander = Mustermann::Expander.new('/users/:user_id', '/pages/:page_id', '/:image_id.jpg') # expand.cast(:user, :page, :image) { |key, value| { "#{key}_id".to_sym => value.id } } # # expand.expand(user: User.current) # => "/users/42" # # @example casting to multiple key/value pairs # expander = Mustermann::Expander.new('/users/:user_id/:image_id.:format') # expander.cast(:image) { |i| { user_id: i.owner.id, image_id: i.id, format: i.format } } # # expander.expander(image: User.current.avatar) # => "/users/42/avatar.jpg" # # @example casting all ActiveRecord objects to param # expander.cast(ActiveRecord::Base, &:to_param) # # @param [Array] type_matchers # To identify key/value pairs to match against. # Regexps and Symbols match against key, everything else matches against value. # # @yield every key/value pair # @yieldparam key [Symbol] omitted if block takes less than 2 # @yieldparam value [Object] omitted if block takes no arguments # @yieldreturn [Hash{Symbol: Object}] will replace key/value pair with returned hash # @yieldreturn [nil, false] will keep key/value pair in hash # @yieldreturn [Object] will replace value with returned object # # @overload cast(*cast_objects) # # @param [Array<#cast>] cast_objects # Before expanding, will call #cast on these objects for each key/value pair. # Return value will be treated same as block return values described above. def cast(*types, &block) caster.register(*types, &block) self end # @example Expanding a pattern # pattern = Mustermann::Expander.new('/:name', '/:name.:ext') # pattern.expand(name: 'hello') # => "/hello" # pattern.expand(name: 'hello', ext: 'png') # => "/hello.png" # # @example Handling additional values # pattern = Mustermann::Expander.new('/:name', '/:name.:ext') # pattern.expand(:ignore, name: 'hello', ext: 'png', scale: '2x') # => "/hello.png" # pattern.expand(:append, name: 'hello', ext: 'png', scale: '2x') # => "/hello.png?scale=2x" # pattern.expand(:raise, name: 'hello', ext: 'png', scale: '2x') # raises Mustermann::ExpandError # # @example Setting additional values behavior for the expander object # pattern = Mustermann::Expander.new('/:name', '/:name.:ext', additional_values: :append) # pattern.expand(name: 'hello', ext: 'png', scale: '2x') # => "/hello.png?scale=2x" # # @param [Symbol] behavior # What to do with additional key/value pairs not present in the values hash. # Possible options: :raise, :ignore, :append. # # @param [Hash{Symbol: #to_s, Array<#to_s>}] values # Values to use for expansion. # # @return [String] expanded string # @raise [NotImplementedError] raised if expand is not supported. # @raise [Mustermann::ExpandError] raised if a value is missing or unknown def expand(behavior = nil, values = {}) behavior, values = nil, behavior if behavior.is_a? Hash values = map_values(values) case behavior || additional_values when :raise then @api_expander.expand(values) when :ignore then with_rest(values) { |uri, rest| uri } when :append then with_rest(values) { |uri, rest| append(uri, rest) } else raise ArgumentError, "unknown behavior %p" % behavior end end # @see Object#== def ==(other) return false unless other.class == self.class other.patterns == patterns and other.additional_values == additional_values end # @see Object#eql? def eql?(other) return false unless other.class == self.class other.patterns.eql? patterns and other.additional_values.eql? additional_values end # @see Object#hash def hash patterns.hash + additional_values.hash end def expandable?(values) return false unless values expandable, _ = split_values(map_values(values)) @api_expander.expandable? expandable end def with_rest(values) expandable, non_expandable = split_values(values) yield expand(:raise, slice(values, expandable)), slice(values, non_expandable) end def split_values(values) expandable = @api_expander.expandable_keys(values.keys) non_expandable = values.keys - expandable [expandable, non_expandable] end def slice(hash, keys) Hash[keys.map { |k| [k, hash[k]] }] end def append(uri, values) return uri unless values and values.any? entries = values.map { |pair| pair.map { |e| @api_expander.escape(e, also_escape: /[\/\?#\&\=%]/) }.join(?=) } "#{ uri }#{ uri[??]??&:?? }#{ entries.join(?&) }" end def map_values(values) values = values.dup @api_expander.keys.each { |key| values[key] ||= values.delete(key.to_s) if values.include? key.to_s } caster.cast(values).delete_if { |k, v| v.nil? } end private :with_rest, :slice, :append, :caster, :map_values, :split_values end end mustermann-1.1.1/mustermann/lib/mustermann/extension.rb000066400000000000000000000030041360365612700234020ustar00rootroot00000000000000# frozen_string_literal: true require 'sinatra/version' fail "no need to load the Mustermann extension for #{::Sinatra::VERSION}" if ::Sinatra::VERSION >= '2.0.0' require 'mustermann' module Mustermann # Sinatra 1.x extension switching default pattern parsing over to Mustermann. # # @example With classic Sinatra application # require 'sinatra' # require 'mustermann' # # register Mustermann # get('/:id', capture: /\d+/) { ... } # # @example With modular Sinatra application # require 'sinatra/base' # require 'mustermann' # # class MyApp < Sinatra::Base # register Mustermann # get('/:id', capture: /\d+/) { ... } # end # # @see file:README.md#Sinatra_Integration "Sinatra Integration" in the README module Extension def compile!(verb, path, block, except: nil, capture: nil, pattern: { }, **options) if path.respond_to? :to_str pattern[:except] = except if except pattern[:capture] = capture if capture if settings.respond_to? :pattern and settings.pattern? pattern.merge! settings.pattern do |key, local, global| next local unless local.is_a? Hash next global.merge(local) if global.is_a? Hash Hash.new(global).merge! local end end path = Mustermann.new(path, **pattern) condition { params.merge! path.params(captures: Array(params[:captures]), offset: -1) } end super(verb, path, block, options) end private :compile! end end mustermann-1.1.1/mustermann/lib/mustermann/identity.rb000066400000000000000000000055341360365612700232310ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/pattern' require 'mustermann/ast/node' module Mustermann # Matches strings that are identical to the pattern. # # @example # Mustermann.new('/:foo', type: :identity) === '/bar' # => false # # @see Mustermann::Pattern # @see file:README.md#identity Syntax description in the README class Identity < Pattern include Concat::Native register :identity # @param (see Mustermann::Pattern#===) # @return (see Mustermann::Pattern#===) # @see (see Mustermann::Pattern#===) def ===(string) unescape(string) == @string end # @param (see Mustermann::Pattern#peek_size) # @return (see Mustermann::Pattern#peek_size) # @see (see Mustermann::Pattern#peek_size) def peek_size(string) return unless unescape(string).start_with? @string return @string.size if string.start_with? @string # optimization @string.each_char.with_index.inject(0) do |count, (char, index)| char_size = 1 escaped = @@uri.escape(char, /./) char_size = escaped.size if string[index, escaped.size].downcase == escaped.downcase count + char_size end end # URI templates support generating templates (the logic is quite complex, though). # # @example (see Mustermann::Pattern#to_templates) # @param (see Mustermann::Pattern#to_templates) # @return (see Mustermann::Pattern#to_templates) # @see Mustermann::Pattern#to_templates def to_templates [@@uri.escape(to_s)] end # Generates an AST so it's compatible with {Mustermann::AST::Pattern}. # Not used internally by {Mustermann::Identity}. # @!visibility private def to_ast payload = @string.each_char.with_index.map { |c, i| AST::Node[c == ?/ ? :separator : :char].new(c, start: i, stop: i+1) } AST::Node[:root].new(payload, pattern: @string, start: 0, stop: @string.length) end # Identity patterns support expanding. # # This implementation does not use {Mustermann::Expander} internally to save memory and # compilation time. # # @example (see Mustermann::Pattern#expand) # @param (see Mustermann::Pattern#expand) # @return (see Mustermann::Pattern#expand) # @raise (see Mustermann::Pattern#expand) # @see Mustermann::Pattern#expand # @see Mustermann::Expander def expand(behavior = nil, values = {}) return to_s if values.empty? or behavior == :ignore raise ExpandError, "cannot expand with keys %p" % values.keys.sort if behavior == :raise raise ArgumentError, "unknown behavior %p" % behavior if behavior != :append params = values.map { |key, value| @@uri.escape(key.to_s) + "=" + @@uri.escape(value.to_s, /[^\w]/) } separator = @string.include?(??) ? ?& : ?? @string + separator + params.join(?&) end end end mustermann-1.1.1/mustermann/lib/mustermann/mapper.rb000066400000000000000000000060701360365612700226600ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/expander' module Mustermann # A mapper allows mapping one string to another based on pattern parsing and expanding. # # @example # require 'mustermann/mapper' # mapper = Mustermann::Mapper.new("/:foo" => "/:foo.html") # mapper['/example'] # => "/example.html" class Mapper # Creates a new mapper. # # @overload initialize(**options) # @param options [Hash] options The options hash # @yield block for generating mappings as a hash # @yieldreturn [Hash] see {#update} # # @example # require 'mustermann/mapper' # Mustermann::Mapper.new(type: :rails) {{ # "/:foo" => ["/:foo.html", "/:foo.:format"] # }} # # @overload initialize(**options) # @param options [Hash] options The options hash # @yield block for generating mappings as a hash # @yieldparam mapper [Mustermann::Mapper] the mapper instance # # @example # require 'mustermann/mapper' # Mustermann::Mapper.new(type: :rails) do |mapper| # mapper["/:foo"] = ["/:foo.html", "/:foo.:format"] # end # # @overload initialize(map = {}, **options) # @param map [Hash] see {#update} # @param [Hash] options The options hash # # @example map before options # require 'mustermann/mapper' # Mustermann::Mapper.new({"/:foo" => "/:foo.html"}, type: :rails) def initialize(map = {}, additional_values: :ignore, **options, &block) @map = [] @options = options @additional_values = additional_values block.arity == 0 ? update(yield) : yield(self) if block update(map) if map end # Add multiple mappings. # # @param map [Hash{String, Pattern: String, Pattern, Arry, Expander}] the mapping def update(map) map.to_h.each_pair do |input, output| input = Mustermann.new(input, **@options) output = Expander.new(*output, additional_values: @additional_values, **@options) unless output.is_a? Expander @map << [input, output] end end # @return [Hash{Patttern: Expander}] Hash version of the mapper. def to_h Hash[@map] end # Convert a string according to mappings. You can pass in additional params. # # @example mapping with and without additional parameters # mapper = Mustermann::Mapper.new("/:example" => "(/:prefix)?/:example.html") # def convert(input, values = {}) @map.inject(input) do |current, (pattern, expander)| params = pattern.params(current) params &&= Hash[values.merge(params).map { |k,v| [k.to_s, v] }] expander.expandable?(params) ? expander.expand(params) : current end end # Add a single mapping. # # @param key [String, Pattern] format of the input string # @param value [String, Pattern, Arry, Expander] format of the output string def []=(key, value) update key => value end alias_method :[], :convert end end mustermann-1.1.1/mustermann/lib/mustermann/pattern.rb000066400000000000000000000346111360365612700230530ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/error' require 'mustermann/simple_match' require 'mustermann/equality_map' require 'uri' module Mustermann # Superclass for all pattern implementations. # @abstract class Pattern include Mustermann @@uri ||= URI::Parser.new # List of supported options. # # @overload supported_options # @return [Array] list of supported options # @overload supported_options(*list) # Adds options to the list. # # @api private # @param [Symbol] *list adds options to the list of supported options # @return [Array] list of supported options def self.supported_options(*list) @supported_options ||= [] options = @supported_options.concat(list) options += superclass.supported_options if self < Pattern options end # Registers the pattern with Mustermann. # @see Mustermann.register # @!visibility private def self.register(*names) names.each { |name| Mustermann.register(name, self) } end # @param [Symbol] option The option to check. # @return [Boolean] Whether or not option is supported. def self.supported?(option, **options) supported_options.include? option end # @overload new(string, **options) # @param (see #initialize) # @raise (see #initialize) # @raise [ArgumentError] if some option is not supported # @return [Mustermann::Pattern] a new instance of Mustermann::Pattern # @see #initialize def self.new(string, ignore_unknown_options: false, **options) if ignore_unknown_options options = options.select { |key, value| supported?(key, **options) if key != :ignore_unknown_options } else unsupported = options.keys.detect { |key| not supported?(key, **options) } raise ArgumentError, "unsupported option %p for %p" % [unsupported, self] if unsupported end @map ||= EqualityMap.new @map.fetch([string, options]) { super(string, **options) { options } } end supported_options :uri_decode, :ignore_unknown_options attr_reader :uri_decode # options hash passed to new (with unsupported options removed) # @!visibility private attr_reader :options # @overload initialize(string, **options) # @param [String] string the string representation of the pattern # @param [Hash] options options for fine-tuning the pattern behavior # @raise [Mustermann::Error] if the pattern can't be generated from the string # @see file:README.md#Types_and_Options "Types and Options" in the README # @see Mustermann.new def initialize(string, uri_decode: true, **options) @uri_decode = uri_decode @string = string.to_s.dup @options = yield.freeze if block_given? end # @return [String] the string representation of the pattern def to_s @string.dup end # @param [String] string The string to match against # @return [MatchData, Mustermann::SimpleMatch, nil] MatchData or similar object if the pattern matches. # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-match Regexp#match # @see http://ruby-doc.org/core-2.0/MatchData.html MatchData # @see Mustermann::SimpleMatch def match(string) SimpleMatch.new(string) if self === string end # @param [String] string The string to match against # @return [Integer, nil] nil if pattern does not match the string, zero if it does. # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-3D-7E Regexp#=~ def =~(string) 0 if self === string end # @param [String] string The string to match against # @return [Boolean] Whether or not the pattern matches the given string # @note Needs to be overridden by subclass. # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-3D-3D-3D Regexp#=== def ===(string) raise NotImplementedError, 'subclass responsibility' end # Used by Ruby internally for hashing. # @return [Integer] same has value for patterns that are equal def hash self.class.hash | @string.hash | options.hash end # Two patterns are considered equal if they are of the same type, have the same pattern string # and the same options. # @return [true, false] def ==(other) other.class == self.class and other.to_s == @string and other.options == options end # Two patterns are considered equal if they are of the same type, have the same pattern string # and the same options. # @return [true, false] def eql?(other) other.class.eql?(self.class) and other.to_s.eql?(@string) and other.options.eql?(options) end # Tries to match the pattern against the beginning of the string (as opposed to the full string). # Will return the count of the matching characters if it matches. # # @example # pattern = Mustermann.new('/:name') # pattern.size("/Frank/Sinatra") # => 6 # # @param [String] string The string to match against # @return [Integer, nil] the number of characters that match def peek_size(string) # this is a very naive, unperformant implementation string.size.downto(0).detect { |s| self === string[0, s] } end # Tries to match the pattern against the beginning of the string (as opposed to the full string). # Will return the substring if it matches. # # @example # pattern = Mustermann.new('/:name') # pattern.peek("/Frank/Sinatra") # => "/Frank" # # @param [String] string The string to match against # @return [String, nil] matched subsctring def peek(string) size = peek_size(string) string[0, size] if size end # Tries to match the pattern against the beginning of the string (as opposed to the full string). # Will return a MatchData or similar instance for the matched substring. # # @example # pattern = Mustermann.new('/:name') # pattern.peek("/Frank/Sinatra") # => # # # @param [String] string The string to match against # @return [MatchData, Mustermann::SimpleMatch, nil] MatchData or similar object if the pattern matches. # @see #peek_params def peek_match(string) matched = peek(string) match(matched) if matched end # Tries to match the pattern against the beginning of the string (as opposed to the full string). # Will return a two element Array with the params parsed from the substring as first entry and the length of # the substring as second. # # @example # pattern = Mustermann.new('/:name') # params, _ = pattern.peek_params("/Frank/Sinatra") # # puts "Hello, #{params['name']}!" # Hello, Frank! # # @param [String] string The string to match against # @return [Array, nil] Array with params hash and length of substing if matched, nil otherwise def peek_params(string) match = peek_match(string) [params(captures: match), match.to_s.size] if match end # @return [Hash{String: Array}] capture names mapped to capture index. # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-named_captures Regexp#named_captures def named_captures {} end # @return [Array] capture names. # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-names Regexp#names def names [] end # @param [String] string the string to match against # @return [Hash{String: String, Array}, nil] Sinatra style params if pattern matches. def params(string = nil, captures: nil, offset: 0) return unless captures ||= match(string) params = named_captures.map do |name, positions| values = positions.map { |pos| map_param(name, captures[pos + offset]) }.flatten values = values.first if values.size < 2 and not always_array? name [name, values] end Hash[params] end # @note This method is only implemented by certain subclasses. # # @example Expanding a pattern # pattern = Mustermann.new('/:name(.:ext)?') # pattern.expand(name: 'hello') # => "/hello" # pattern.expand(name: 'hello', ext: 'png') # => "/hello.png" # # @example Checking if a pattern supports expanding # if pattern.respond_to? :expand # pattern.expand(name: "foo") # else # warn "does not support expanding" # end # # Expanding is supported by almost all patterns (notable exceptions are {Mustermann::Shell}, # {Mustermann::Regular} and {Mustermann::Simple}). # # Union {Mustermann::Composite} patterns (with the | operator) support expanding if all # patterns they are composed of also support it. # # @param (see Mustermann::Expander#expand) # @return [String] expanded string # @raise [NotImplementedError] raised if expand is not supported. # @raise [Mustermann::ExpandError] raised if a value is missing or unknown # @see Mustermann::Expander def expand(behavior = nil, values = {}) raise NotImplementedError, "expanding not supported by #{self.class}" end # @note This method is only implemented by certain subclasses. # # Generates a list of URI template strings representing the pattern. # # Note that this transformation is lossy and the strings matching these # templates might not match the pattern (and vice versa). # # This comes in quite handy since URI templates are not made for pattern matching. # That way you can easily use a more precise template syntax and have it automatically # generate hypermedia links for you. # # @example generating templates # Mustermann.new("/:name").to_templates # => ["/{name}"] # Mustermann.new("/:foo(@:bar)?/*baz").to_templates # => ["/{foo}@{bar}/{+baz}", "/{foo}/{+baz}"] # Mustermann.new("/{name}", type: :template).to_templates # => ["/{name}"] # # @example generating templates from composite patterns # pattern = Mustermann.new('/:name') # pattern |= Mustermann.new('/{name}', type: :template) # pattern |= Mustermann.new('/example/*nested') # pattern.to_templates # => ["/{name}", "/example/{+nested}"] # # Template generation is supported by almost all patterns (notable exceptions are # {Mustermann::Shell}, {Mustermann::Regular} and {Mustermann::Simple}). # Union {Mustermann::Composite} patterns (with the | operator) support template generation # if all patterns they are composed of also support it. # # @example Checking if a pattern supports expanding # if pattern.respond_to? :to_templates # pattern.to_templates # else # warn "does not support template generation" # end # # @return [Array] list of URI templates def to_templates raise NotImplementedError, "template generation not supported by #{self.class}" end # @overload |(other) # Creates a pattern that matches any string matching either one of the patterns. # If a string is supplied, it is treated as an identity pattern. # # @example # pattern = Mustermann.new('/foo/:name') | Mustermann.new('/:first/:second') # pattern === '/foo/bar' # => true # pattern === '/fox/bar' # => true # pattern === '/foo' # => false # # @overload &(other) # Creates a pattern that matches any string matching both of the patterns. # If a string is supplied, it is treated as an identity pattern. # # @example # pattern = Mustermann.new('/foo/:name') & Mustermann.new('/:first/:second') # pattern === '/foo/bar' # => true # pattern === '/fox/bar' # => false # pattern === '/foo' # => false # # @overload ^(other) # Creates a pattern that matches any string matching exactly one of the patterns. # If a string is supplied, it is treated as an identity pattern. # # @example # pattern = Mustermann.new('/foo/:name') ^ Mustermann.new('/:first/:second') # pattern === '/foo/bar' # => false # pattern === '/fox/bar' # => true # pattern === '/foo' # => false # # @param [Mustermann::Pattern, String] other the other pattern # @return [Mustermann::Pattern] a composite pattern def |(other) Mustermann::Composite.new(self, other, operator: __callee__, type: :identity) end alias_method :&, :| alias_method :^, :| # @example # require 'mustermann' # prefix = Mustermann.new("/:prefix") # about = prefix + "/about" # about.params("/main/about") # => {"prefix" => "main"} # # Creates a concatenated pattern by combingin self with the other pattern supplied. # Patterns of different types can be mixed. The availability of `to_templates` and # `expand` depends on the patterns being concatenated. # # String input is treated as identity pattern. # # @param [Mustermann::Pattern, String] other pattern to be appended # @return [Mustermann::Pattern] concatenated pattern def +(other) Concat.new(self, other, type: :identity) end # @example # pattern = Mustermann.new('/:a/:b') # strings = ["foo/bar", "/foo/bar", "/foo/bar/"] # strings.detect(&pattern) # => "/foo/bar" # # @return [Proc] proc wrapping {#===} def to_proc @to_proc ||= method(:===).to_proc end # @!visibility private # @return [Boolean] # @see Object#respond_to? def respond_to?(method, *args) return super unless %i[expand to_templates].include? method respond_to_special?(method) end # @!visibility private # @return [Boolean] # @see #respond_to? def respond_to_special?(method) method(method).owner != Mustermann::Pattern end # @!visibility private def inspect "#<%p:%p>" % [self.class, @string] end # @!visibility private def simple_inspect type = self.class.name[/[^:]+$/].downcase "%s:%p" % [type, @string] end # @!visibility private def map_param(key, value) unescape(value, true) end # @!visibility private def unescape(string, decode = uri_decode) return string unless decode and string @@uri.unescape(string) end # @!visibility private ALWAYS_ARRAY = %w[splat captures] # @!visibility private def always_array?(key) ALWAYS_ARRAY.include? key end private :unescape, :map_param, :respond_to_special? private_constant :ALWAYS_ARRAY end end mustermann-1.1.1/mustermann/lib/mustermann/pattern_cache.rb000066400000000000000000000027331360365612700241760ustar00rootroot00000000000000# frozen_string_literal: true require 'set' require 'thread' require 'mustermann' module Mustermann # A simple, persistent cache for creating repositories. # # @example # require 'mustermann/pattern_cache' # cache = Mustermann::PatternCache.new # # # use this instead of Mustermann.new # pattern = cache.create_pattern("/:name", type: :rails) # # @note # {Mustermann::Pattern.new} (which is used by {Mustermann.new}) will reuse instances that have # not yet been garbage collected. You only need an extra cache if you do not keep a reference to # the patterns around. # # @api private class PatternCache # @param [Hash] pattern_options default options used for {#create_pattern} def initialize(**pattern_options) @cached = Set.new @mutex = Mutex.new @pattern_options = pattern_options end # @param (see Mustermann.new) # @return (see Mustermann.new) # @raise (see Mustermann.new) # @see Mustermann.new def create_pattern(string, **pattern_options) pattern = Mustermann.new(string, **pattern_options, **@pattern_options) @mutex.synchronize { @cached.add(pattern) } unless @cached.include? pattern pattern end # Removes all pattern instances from the cache. def clear @mutex.synchronize { @cached.clear } end # @return [Integer] number of currently cached patterns def size @mutex.synchronize { @cached.size } end end end mustermann-1.1.1/mustermann/lib/mustermann/regexp.rb000066400000000000000000000000351360365612700226610ustar00rootroot00000000000000require 'mustermann/regular' mustermann-1.1.1/mustermann/lib/mustermann/regexp_based.rb000066400000000000000000000025561360365612700240310ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/pattern' require 'forwardable' module Mustermann # Superclass for patterns that internally compile to a regular expression. # @see Mustermann::Pattern # @abstract class RegexpBased < Pattern # @return [Regexp] regular expression equivalent to the pattern. attr_reader :regexp alias_method :to_regexp, :regexp # @param (see Mustermann::Pattern#initialize) # @return (see Mustermann::Pattern#initialize) # @see (see Mustermann::Pattern#initialize) def initialize(string, **options) super regexp = compile(**options) @peek_regexp = /\A#{regexp}/ @regexp = /\A#{regexp}\Z/ end # @param (see Mustermann::Pattern#peek_size) # @return (see Mustermann::Pattern#peek_size) # @see (see Mustermann::Pattern#peek_size) def peek_size(string) return unless match = peek_match(string) match.to_s.size end # @param (see Mustermann::Pattern#peek_match) # @return (see Mustermann::Pattern#peek_match) # @see (see Mustermann::Pattern#peek_match) def peek_match(string) @peek_regexp.match(string) end extend Forwardable def_delegators :regexp, :===, :=~, :match, :names, :named_captures def compile(**options) raise NotImplementedError, 'subclass responsibility' end private :compile end end mustermann-1.1.1/mustermann/lib/mustermann/regular.rb000066400000000000000000000030341360365612700230320ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/regexp_based' require 'strscan' module Mustermann # Regexp pattern implementation. # # @example # Mustermann.new('/.*', type: :regexp) === '/bar' # => true # # @see Mustermann::Pattern # @see file:README.md#simple Syntax description in the README class Regular < RegexpBased include Concat::Native register :regexp, :regular supported_options :check_anchors # @param (see Mustermann::Pattern#initialize) # @return (see Mustermann::Pattern#initialize) # @see (see Mustermann::Pattern#initialize) def initialize(string, check_anchors: true, **options) string = $1 if string.to_s =~ /\A\(\?\-mix\:(.*)\)\Z/ && string.inspect == "/#$1/" string = string.source.gsub!(/(?] empty array for imitating MatchData interface def names @names.dup end # @return [Array] empty array for imitating MatchData interface def captures @captures.dup end # @return [nil] imitates MatchData interface def [](*args) args.map! do |arg| next arg unless arg.is_a? Symbol or arg.is_a? String names.index(arg.to_s) end @captures[*args] end # @!visibility private def +(other) SimpleMatch.new(@string + other.to_s, names: @names + other.names, captures: @captures + other.captures) end # @return [String] string representation def inspect "#<%p %p>" % [self.class, @string] end end end mustermann-1.1.1/mustermann/lib/mustermann/sinatra.rb000066400000000000000000000062231360365612700230350ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/identity' require 'mustermann/ast/pattern' require 'mustermann/sinatra/parser' require 'mustermann/sinatra/safe_renderer' require 'mustermann/sinatra/try_convert' module Mustermann # Sinatra 2.0 style pattern implementation. # # @example # Mustermann.new('/:foo') === '/bar' # => true # # @see Mustermann::Pattern # @see file:README.md#sinatra Syntax description in the README class Sinatra < AST::Pattern include Concat::Native register :sinatra # Takes a string and espaces any characters that have special meaning for Sinatra patterns. # # @example # require 'mustermann/sinatra' # Mustermann::Sinatra.escape("/:name") # => "/\\:name" # # @param [#to_s] string the input string # @return [String] the escaped string def self.escape(string) string.to_s.gsub(/[\?\(\)\*:\\\|\{\}]/) { |c| "\\#{c}" } end # Tries to convert the given input object to a Sinatra pattern with the given options, without # changing its parsing semantics. # @return [Mustermann::Sinatra, nil] the converted pattern, if possible # @!visibility private def self.try_convert(input, **options) TryConvert.convert(input, **options) end # Creates a pattern that matches any string matching either one of the patterns. # If a string is supplied, it is treated as a fully escaped Sinatra pattern. # # If the other pattern is also a Sintara pattern, it might join the two to a third # sinatra pattern instead of generating a composite for efficiency reasons. # # This only happens if the sinatra pattern behaves exactly the same as a composite # would in regards to matching, parsing, expanding and template generation. # # @example # pattern = Mustermann.new('/foo/:name') | Mustermann.new('/:first/:second') # pattern === '/foo/bar' # => true # pattern === '/fox/bar' # => true # pattern === '/foo' # => false # # @param [Mustermann::Pattern, String] other the other pattern # @return [Mustermann::Pattern] a composite pattern # @see Mustermann::Pattern#| def |(other) return super unless converted = self.class.try_convert(other, **options) return super unless converted.names.empty? or names.empty? self.class.new(safe_string + "|" + converted.safe_string, **options) end # Generates a string represenation of the pattern that can safely be used for def interpolation # without changing its semantics. # # @example # require 'mustermann' # unsafe = Mustermann.new("/:name") # # Mustermann.new("#{unsafe}bar").params("/foobar") # => { "namebar" => "foobar" } # Mustermann.new("#{unsafe.safe_string}bar").params("/foobar") # => { "name" => "bar" } # # @return [String] string representatin of the pattern def safe_string @safe_string ||= SafeRenderer.translate(to_ast) end # @!visibility private def native_concat(other) return unless converted = self.class.try_convert(other, **options) safe_string + converted.safe_string end private :native_concat end end mustermann-1.1.1/mustermann/lib/mustermann/sinatra/000077500000000000000000000000001360365612700225055ustar00rootroot00000000000000mustermann-1.1.1/mustermann/lib/mustermann/sinatra/parser.rb000066400000000000000000000025351360365612700243330ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann class Sinatra < AST::Pattern # Sinatra syntax definition. # @!visibility private class Parser < AST::Parser on(nil, ??, ?)) { |c| unexpected(c) } on(?*) { |c| scan(/\w+/) ? node(:named_splat, buffer.matched) : node(:splat) } on(?:) { |c| node(:capture) { scan(/\w+/) } } on(?\\) { |c| node(:char, expect(/./)) } on(?() { |c| node(:group) { read unless scan(?)) } } on(?|) { |c| node(:or) } on ?{ do |char| current_pos = buffer.pos type = scan(?+) ? :named_splat : :capture name = expect(/[\w\.]+/) if type == :capture && scan(?|) buffer.pos = current_pos capture = proc do start = pos match = expect(/(?[^\|}]+)/) node(:capture, match[:capture], start: start) end grouped_captures = node(:group, [capture[]]) do if scan(?|) [min_size(pos - 1, pos, node(:or)), capture[]] end end grouped_captures if expect(?}) else type = :splat if type == :named_splat and name == 'splat' expect(?}) node(type, name) end end suffix ?? do |char, element| node(:optional, element) end end private_constant :Parser end end mustermann-1.1.1/mustermann/lib/mustermann/sinatra/safe_renderer.rb000066400000000000000000000024171360365612700256420ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann class Sinatra < AST::Pattern # Generates a string that can safely be concatenated with other strings # without chaning its semantics # @see #safe_string # @!visibility private SafeRenderer = AST::Translator.create do translate(:splat, :named_splat) { "{+#{name}}" } translate(:char, :separator) { Sinatra.escape(payload) } translate(:root) { t(payload) } translate(:group) { "(#{t(payload)})" } translate(:union) { "(#{t(payload, join: ?|)})" } translate(:optional) { "#{t(payload)}?" } translate(:with_look_ahead) { t([head, payload]) } translate(Array) { |join: ""| map { |e| t(e) }.join(join) } translate(:capture) do raise Mustermann::Error, 'cannot render variables' if node.is_a? :variable raise Mustermann::Error, 'cannot translate constraints' if constraint or qualifier or convert prefix = node.is_a?(:splat) ? "+" : "" "{#{prefix}#{name}}" end end private_constant :SafeRenderer end end mustermann-1.1.1/mustermann/lib/mustermann/sinatra/try_convert.rb000066400000000000000000000026301360365612700254110ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann class Sinatra < AST::Pattern # Tries to translate objects to Sinatra patterns. # @!visibility private class TryConvert < AST::Translator # @return [Mustermann::Sinatra, nil] # @!visibility private def self.convert(input, **options) new(options).translate(input) end # Expected options for the resulting pattern. # @!visibility private attr_reader :options # @!visibility private def initialize(options) @options = options end # @return [Mustermann::Sinatra] # @!visibility private def new(input, escape = false) input = Mustermann::Sinatra.escape(input) if escape Mustermann::Sinatra.new(input, **options) end # @return [true, false] whether or not expected pattern should have uri_decode option set # @!visibility private def uri_decode options.fetch(:uri_decode, true) end translate(Object) { nil } translate(String) { t.new(self, true) } translate(Identity) { t.new(self, true) if uri_decode == t.uri_decode } translate(Sinatra) { node if options == t.options } translate AST::Pattern do next unless options == t.options t.new(SafeRenderer.translate(to_ast)) rescue nil end end private_constant :TryConvert end end mustermann-1.1.1/mustermann/lib/mustermann/to_pattern.rb000066400000000000000000000027371360365612700235610ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' module Mustermann # Mixin for adding {#to_pattern} ducktyping to objects. # # @example # require 'mustermann/to_pattern' # # class Foo # include Mustermann::ToPattern # # def to_s # ":foo/:bar" # end # end # # Foo.new.to_pattern # => # # # By default included into String, Symbol, Regexp, Array and {Mustermann::Pattern}. module ToPattern PRIMITIVES = [String, Symbol, Array, Regexp, Mustermann::Pattern] private_constant :PRIMITIVES # Converts the object into a {Mustermann::Pattern}. # # @example converting a string # ":name.png".to_pattern # => # # # @example converting a string with options # "/*path".to_pattern(type: :rails) # => # # # @example converting a regexp # /.*/.to_pattern # => # # # @example converting a pattern # Mustermann.new("foo").to_pattern # => # # # @param [Hash] options The options hash. # @return [Mustermann::Pattern] pattern corresponding to object. def to_pattern(**options) input = self if PRIMITIVES.any? { |p| self.is_a? p } input ||= __getobj__ if respond_to?(:__getobj__) Mustermann.new(input || to_s, **options) end PRIMITIVES.each do |klass| append_features(klass) end end end mustermann-1.1.1/mustermann/lib/mustermann/version.rb000066400000000000000000000001121360365612700230500ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann VERSION ||= '1.1.1' end mustermann-1.1.1/mustermann/mustermann.gemspec000066400000000000000000000016331360365612700216460ustar00rootroot00000000000000$:.unshift File.expand_path("../lib", __FILE__) require "mustermann/version" Gem::Specification.new do |s| s.name = "mustermann" s.version = Mustermann::VERSION s.authors = ["Konstantin Haase", "Zachary Scott"] s.email = "sinatrarb@googlegroups.com" s.homepage = "https://github.com/sinatra/mustermann" s.summary = %q{Your personal string matching expert.} s.description = %q{A library implementing patterns that behave like regular expressions.} s.license = 'MIT' s.required_ruby_version = '>= 2.2.0' s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } s.add_runtime_dependency('ruby2_keywords', '~> 0.0.1') end mustermann-1.1.1/mustermann/spec/000077500000000000000000000000001360365612700170375ustar00rootroot00000000000000mustermann-1.1.1/mustermann/spec/ast_spec.rb000066400000000000000000000007161360365612700211710ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/ast/node' describe Mustermann::AST do describe :type do example { Mustermann::AST::Node[:char].type .should be == :char } example { Mustermann::AST::Node[:char].new.type .should be == :char } end describe :min_size do example { Mustermann::AST::Node[:char].new.min_size.should be == 1 } example { Mustermann::AST::Node[:node].new.min_size.should be == 0 } end end mustermann-1.1.1/mustermann/spec/composite_spec.rb000066400000000000000000000155701360365612700224100ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann' describe Mustermann::Composite do describe :new do example 'with no argument' do expect { Mustermann::Composite.new }. to raise_error(ArgumentError, 'cannot create empty composite pattern') end example 'with one argument' do pattern = Mustermann.new('/foo') Mustermann::Composite.new(pattern).should be == pattern end example 'with supported type specific arguments' do Mustermann::Composite.new("/a", "/b", greedy: true) end example 'with unsupported type specific arguments' do expect { Mustermann::Composite.new("/a", "/b", greedy: true, type: :identity) }.to raise_error(ArgumentError) end end context :| do subject(:pattern) { Mustermann.new('/foo/:name', '/:first/:second') } describe :== do example { subject.should be == subject } example { subject.should be == Mustermann.new('/foo/:name', '/:first/:second') } example { subject.should_not be == Mustermann.new('/foo/:name') } example { subject.should_not be == Mustermann.new('/foo/:name', '/:first/:second', operator: :&) } end describe :=== do example { subject.should be === "/foo/bar" } example { subject.should be === "/fox/bar" } example { subject.should_not be === "/foo" } end describe :params do example { subject.params("/foo/bar") .should be == { "name" => "bar" } } example { subject.params("/fox/bar") .should be == { "first" => "fox", "second" => "bar" } } example { subject.params("/foo") .should be_nil } end describe :=== do example { subject.should match("/foo/bar") } example { subject.should match("/fox/bar") } example { subject.should_not match("/foo") } end describe :expand do example { subject.should respond_to(:expand) } example { subject.expand(name: 'bar') .should be == '/foo/bar' } example { subject.expand(first: 'fox', second: 'bar') .should be == '/fox/bar' } context "without expandable patterns" do subject(:pattern) { Mustermann.new('/foo/:name', '/:first/:second', type: :simple) } example { subject.should_not respond_to(:expand) } example { expect { subject.expand(name: 'bar') }.to raise_error(NotImplementedError) } end end describe :to_templates do example { should respond_to(:to_templates) } example { should generate_templates('/foo/{name}', '/{first}/{second}') } context "without patterns implementing to_templates" do subject(:pattern) { Mustermann.new('/foo/:name', '/:first/:second', type: :simple) } example { should_not respond_to(:to_templates) } example { expect { subject.to_templates }.to raise_error(NotImplementedError) } end end describe :eql? do example { should be_eql(pattern) } example { should be_eql(Mustermann.new('/foo/:name', '/:first/:second', operator: :|)) } example { should_not be_eql(Mustermann.new('/bar/:name', '/:first/:second', operator: :|)) } example { should_not be_eql(Mustermann.new('/foo/:name', '/:first/:second', operator: :&)) } end end context :& do subject(:pattern) { Mustermann.new('/foo/:name', '/:first/:second', operator: :&) } describe :== do example { subject.should be == subject } example { subject.should be == Mustermann.new('/foo/:name', '/:first/:second', operator: :&) } example { subject.should_not be == Mustermann.new('/foo/:name') } example { subject.should_not be == Mustermann.new('/foo/:name', '/:first/:second') } end describe :=== do example { subject.should be === "/foo/bar" } example { subject.should_not be === "/fox/bar" } example { subject.should_not be === "/foo" } end describe :params do example { subject.params("/foo/bar") .should be == { "name" => "bar" } } example { subject.params("/fox/bar") .should be_nil } example { subject.params("/foo") .should be_nil } end describe :match do example { subject.should match("/foo/bar") } example { subject.should_not match("/fox/bar") } example { subject.should_not match("/foo") } end describe :expand do example { subject.should_not respond_to(:expand) } example { expect { subject.expand(name: 'bar') }.to raise_error(NotImplementedError) } end end context :^ do subject(:pattern) { Mustermann.new('/foo/:name', '/:first/:second', operator: :^) } describe :== do example { subject.should be == subject } example { subject.should_not be == Mustermann.new('/foo/:name', '/:first/:second') } example { subject.should_not be == Mustermann.new('/foo/:name') } example { subject.should_not be == Mustermann.new('/foo/:name', '/:first/:second', operator: :&) } end describe :=== do example { subject.should_not be === "/foo/bar" } example { subject.should be === "/fox/bar" } example { subject.should_not be === "/foo" } end describe :params do example { subject.params("/foo/bar") .should be_nil } example { subject.params("/fox/bar") .should be == { "first" => "fox", "second" => "bar" } } example { subject.params("/foo") .should be_nil } end describe :match do example { subject.should_not match("/foo/bar") } example { subject.should match("/fox/bar") } example { subject.should_not match("/foo") } end describe :expand do example { subject.should_not respond_to(:expand) } example { expect { subject.expand(name: 'bar') }.to raise_error(NotImplementedError) } end end describe :inspect do let(:sinatra) { Mustermann.new('x') } let(:shell) { Mustermann.new('x', type: :shell) } let(:identity) { Mustermann.new('x', type: :identity) } example { (sinatra | shell) .inspect.should include('(sinatra:"x" | shell:"x")') } example { (sinatra ^ shell) .inspect.should include('(sinatra:"x" ^ shell:"x")') } example { (sinatra | shell | identity) .inspect.should include('(sinatra:"x" | shell:"x" | identity:"x")') } example { (sinatra | shell & identity) .inspect.should include('(sinatra:"x" | (shell:"x" & identity:"x"))') } end end mustermann-1.1.1/mustermann/spec/concat_spec.rb000066400000000000000000000120201360365612700216400ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann' describe Mustermann::Concat do describe Mustermann::Concat::Native do context "sinatra + sinatra" do subject(:pattern) { Mustermann.new("/:foo") + Mustermann.new("/:bar") } its(:class) { should be == Mustermann::Sinatra } its(:to_s) { should be == "/{foo}/{bar}" } end context "sinatra + string" do subject(:pattern) { Mustermann.new("/:foo") + "/:bar" } its(:class) { should be == Mustermann::Sinatra } its(:to_s) { should be == "/{foo}/\\:bar" } end context "regular + regular" do subject(:pattern) { Mustermann.new(/foo/) + Mustermann.new(/bar/) } its(:class) { should be == Mustermann::Regular } its(:to_s) { should be == "foobar" } end context "sinatra + rails" do subject(:pattern) { Mustermann.new("/:foo") + Mustermann.new("/:bar", type: :rails) } its(:class) { should be == Mustermann::Sinatra } its(:to_s) { should be == "/{foo}/{bar}" } end context "sinatra + flask" do subject(:pattern) { Mustermann.new("/:foo") + Mustermann.new("/", type: :flask) } its(:class) { should be == Mustermann::Sinatra } its(:to_s) { should be == "/{foo}/{bar}" } end context "sinatra + flask (typed)" do subject(:pattern) { Mustermann.new("/:foo") + Mustermann.new("/", type: :flask) } its(:class) { should be == Mustermann::Concat } its(:to_s) { should be == '(sinatra:"/:foo" + flask:"/")' } end context "sinatra + sinatra (different options)" do subject(:pattern) { Mustermann.new("/:foo") + Mustermann.new("/:bar", uri_decode: false) } its(:class) { should be == Mustermann::Concat } its(:to_s) { should be == '(sinatra:"/:foo" + sinatra:"/:bar")' } end context "sinatra + rails (different options)" do subject(:pattern) { Mustermann.new("/:foo") + Mustermann.new("/:bar", type: :rails, uri_decode: false) } its(:class) { should be == Mustermann::Concat } its(:to_s) { should be == '(sinatra:"/:foo" + rails:"/:bar")' } end context "sinatra + rails (different options) + sinatra" do subject(:pattern) { Mustermann.new("/:foo") + Mustermann.new("/:bar", type: :rails, uri_decode: false) + Mustermann.new("/:baz") } its(:class) { should be == Mustermann::Concat } its(:to_s) { should be == '(sinatra:"/:foo" + rails:"/:bar" + sinatra:"/:baz")' } end context "sinatra + (sinatra + regular)" do subject(:pattern) { Mustermann.new("/foo") + (Mustermann.new("/bar") + Mustermann.new(/baz/)) } its(:class) { should be == Mustermann::Concat } its(:to_s) { should be == '(sinatra:"/foo/bar" + regular:"baz")' } end context "sinatra + (sinatra + rails (different options) + sinatra)" do subject(:pattern) { Mustermann.new("/foo") + (Mustermann.new("/bar") + Mustermann.new("/baz", type: :rails, uri_decode: false) + Mustermann.new("/boo")) } its(:class) { should be == Mustermann::Concat } its(:to_s) { should be == '(sinatra:"/foo/bar" + rails:"/baz" + sinatra:"/boo")' } end end subject(:pattern) { Mustermann::Concat.new("/:foo", "/:bar") } describe :=== do example { (pattern === "/foo/bar") .should be true } example { (pattern === "/foo/bar/") .should be false } example { (pattern === "/foo") .should be false } end describe :match do it { should match("/foo/bar").capturing(foo: "foo", bar: "bar") } it { should_not match("/foo/bar/") } it { should_not match("/foo/") } end describe :params do example { pattern.params("/foo/bar") .should be == { "foo" => "foo", "bar" => "bar" }} example { pattern.params("/foo/bar/") .should be_nil } example { pattern.params("/foo") .should be_nil } end describe :peek do example { pattern.peek("/foo/bar/baz") .should be == "/foo/bar" } example { pattern.peek("/foo") .should be_nil } end describe :peek_params do example { pattern.peek_params("/foo/bar/baz") .should be == [{ "foo" => "foo", "bar" => "bar" }, 8]} example { pattern.peek_params("/foo") .should be_nil } end describe :peek_match do example { pattern.peek_match("/foo/bar/baz").to_s .should be == "/foo/bar" } example { pattern.peek_match("/foo") .should be_nil } end describe :peek_size do example { pattern.peek_size("/foo/bar/baz") .should be == 8 } example { pattern.peek_size("/foo") .should be_nil } end describe :expand do it { should expand(foo: :bar, bar: :foo) .to('/bar/foo') } it { should expand(:append, foo: :bar, bar: :foo, baz: 42) .to('/bar/foo?baz=42') } it { should_not expand(foo: :bar) } end describe :to_templates do subject(:pattern) { Mustermann::Concat.new("/:foo|:bar", "(/:baz)?") } it { should generate_template("/{foo}/{baz}") } it { should generate_template("{bar}/{baz}") } it { should generate_template("/{foo}") } it { should generate_template("{bar}") } end end mustermann-1.1.1/mustermann/spec/equality_map_spec.rb000066400000000000000000000023301360365612700230660ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/equality_map' RSpec.describe Mustermann::EqualityMap do before { GC.disable } after { GC.enable } describe :fetch do subject { Mustermann::EqualityMap.new } specify 'with existing entry' do next if subject.is_a? Hash subject.fetch("foo") { "foo" } result = subject.fetch("foo") { "bar" } expect(result).to be == "foo" end specify 'with GC-removed entry' do next if subject.is_a? Hash subject.fetch(String.new('foo')) { "foo" } expect(subject.map).to receive(:[]).and_return(nil) result = subject.fetch(String.new('foo')) { "bar" } expect(result).to be == "bar" end specify 'allows a frozen key and value' do next if subject.is_a? Hash key = "foo".freeze value = "bar".freeze subject.fetch(key) { value } result = subject.fetch("foo".dup) { raise "not executed" } expect(result).to be == value expect(result).not_to equal value end specify 'allows only a single argument to be compatible with Hash#fetch' do expect { subject.fetch("foo", "bar", "baz") { "value" } }.to raise_error(ArgumentError) end end end mustermann-1.1.1/mustermann/spec/expander_spec.rb000066400000000000000000000142631360365612700222120ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/expander' describe Mustermann::Expander do it 'expands a pattern' do expander = Mustermann::Expander.new("/:foo.jpg") expander.expand(foo: 42).should be == "/42.jpg" end it 'expands multiple patterns' do expander = Mustermann::Expander.new << "/:foo.:ext" << "/:foo" expander.expand(foo: 42, ext: 'jpg').should be == "/42.jpg" expander.expand(foo: 23).should be == "/23" end it 'supports setting pattern options' do expander = Mustermann::Expander.new(type: :rails) << "/:foo(.:ext)" << "/:bar" expander.expand(foo: 42, ext: 'jpg').should be == "/42.jpg" expander.expand(foo: 42).should be == "/42" end it 'supports combining different pattern styles' do expander = Mustermann::Expander.new << Mustermann.new("/:foo(.:ext)", type: :rails) << Mustermann.new("/:bar", type: :sinatra) expander.expand(foo: 'pony', ext: 'jpg').should be == '/pony.jpg' expander.expand(bar: 23).should be == "/23" end it 'ignores nil values' do expander = Mustermann::Expander.new << Mustermann.new("/:foo(.:ext)?") expander.expand(foo: 'pony', ext: nil).should be == '/pony' end it 'supports splat' do expander = Mustermann::Expander.new << Mustermann.new("/foo/*/baz") expander.expand(splat: 'bar').should be == '/foo/bar/baz' end it 'supports multiple splats' do expander = Mustermann::Expander.new << Mustermann.new("/foo/*/bar/*") expander.expand(splat: [123, 456]).should be == '/foo/123/bar/456' end it 'supports identity patterns' do expander = Mustermann::Expander.new('/:foo', type: :identity) expander.expand.should be == '/:foo' end describe :additional_values do context "illegal value" do example { expect { Mustermann::Expander.new(additional_values: :foo) }.to raise_error(ArgumentError) } example { expect { Mustermann::Expander.new('/').expand(:foo, a: 10) }.to raise_error(ArgumentError) } end context :raise do subject(:expander) { Mustermann::Expander.new('/:a', additional_values: :raise) } example { expander.expand(a: ?a).should be == '/a' } example { expect { expander.expand(a: ?a, b: ?b) }.to raise_error(Mustermann::ExpandError) } example { expect { expander.expand(b: ?b) }.to raise_error(Mustermann::ExpandError) } end context :ignore do subject(:expander) { Mustermann::Expander.new('/:a', additional_values: :ignore) } example { expander.expand(a: ?a).should be == '/a' } example { expander.expand(a: ?a, b: ?b).should be == '/a' } example { expect { expander.expand(b: ?b) }.to raise_error(Mustermann::ExpandError) } example { expect { expander.expand(b: ?b, c: []) }.to raise_error(Mustermann::ExpandError) } example { expect { expander.expand(b: ?b, c: [], d: ?d) }.to raise_error(Mustermann::ExpandError) } end context :append do subject(:expander) { Mustermann::Expander.new('/:a', additional_values: :append) } example { expander.expand(a: ?a).should be == '/a' } example { expander.expand(a: ?a, b: ?b).should be == '/a?b=b' } example { expect { expander.expand(b: ?b) }.to raise_error(Mustermann::ExpandError) } end end describe :cast do subject(:expander) { Mustermann::Expander.new('/:a(/:b)?') } example { expander.cast { "FOOBAR" }.expand(a: "foo") .should be == "/FOOBAR" } example { expander.cast { |v| v.upcase }.expand(a: "foo") .should be == "/FOO" } example { expander.cast { |v| v.upcase }.expand(a: "foo", b: "bar") .should be == "/FOO/BAR" } example { expander.cast(:a) { |v| v.upcase }.expand(a: "foo", b: "bar") .should be == "/FOO/bar" } example { expander.cast(:a, :b) { |v| v.upcase }.expand(a: "foo", b: "bar") .should be == "/FOO/BAR" } example { expander.cast(Integer) { |k,v| "#{k}_#{v}" }.expand(a: "foo", b: 42) .should be == "/foo/b_42" } example do expander.cast(:a) { |v| v.upcase } expander.cast(:b) { |v| v.downcase } expander.expand(a: "fOo", b: "bAr").should be == "/FOO/bar" end end describe :== do example { Mustermann::Expander.new('/foo') .should be == Mustermann::Expander.new('/foo') } example { Mustermann::Expander.new('/foo') .should_not be == Mustermann::Expander.new('/bar') } example { Mustermann::Expander.new('/foo', type: :rails) .should be == Mustermann::Expander.new('/foo', type: :rails) } example { Mustermann::Expander.new('/foo', type: :rails) .should_not be == Mustermann::Expander.new('/foo', type: :sinatra) } end describe :hash do example { Mustermann::Expander.new('/foo') .hash.should be == Mustermann::Expander.new('/foo').hash } example { Mustermann::Expander.new('/foo') .hash.should_not be == Mustermann::Expander.new('/bar').hash } example { Mustermann::Expander.new('/foo', type: :rails) .hash.should be == Mustermann::Expander.new('/foo', type: :rails).hash } example { Mustermann::Expander.new('/foo', type: :rails) .hash.should_not be == Mustermann::Expander.new('/foo', type: :sinatra).hash } end describe :eql? do example { Mustermann::Expander.new('/foo') .should be_eql Mustermann::Expander.new('/foo') } example { Mustermann::Expander.new('/foo') .should_not be_eql Mustermann::Expander.new('/bar') } example { Mustermann::Expander.new('/foo', type: :rails) .should be_eql Mustermann::Expander.new('/foo', type: :rails) } example { Mustermann::Expander.new('/foo', type: :rails) .should_not be_eql Mustermann::Expander.new('/foo', type: :sinatra) } end describe :equal? do example { Mustermann::Expander.new('/foo') .should_not be_equal Mustermann::Expander.new('/foo') } example { Mustermann::Expander.new('/foo') .should_not be_equal Mustermann::Expander.new('/bar') } example { Mustermann::Expander.new('/foo', type: :rails) .should_not be_equal Mustermann::Expander.new('/foo', type: :rails) } example { Mustermann::Expander.new('/foo', type: :rails) .should_not be_equal Mustermann::Expander.new('/foo', type: :sinatra) } end end mustermann-1.1.1/mustermann/spec/extension_spec.rb000066400000000000000000000221411360365612700224120ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/extension' require 'sinatra/base' require 'rack/test' describe Mustermann::Extension do include Rack::Test::Methods subject :app do Sinatra.new do set :environment, :test register Mustermann end end it 'sets up the extension' do app.should be_a(Mustermann::Extension) end context 'uses Sinatra-style patterns by default' do before { app.get('/:slug(.:extension)?') { params[:slug] } } example { get('/foo') .body.should be == 'foo' } example { get('/foo.') .body.should be == 'foo.' } example { get('/foo.bar') .body.should be == 'foo' } example { get('/a%20b') .body.should be == 'a b' } end describe :except do before { app.get('/auth/*', except: '/auth/login') { 'ok' } } example { get('/auth/dunno').should be_ok } example { get('/auth/login').should_not be_ok } end describe :capture do context 'global' do before do app.set(:pattern, capture: { ext: %w[png jpg gif] }) app.get('/:slug(.:ext)?') { params[:slug] } end example { get('/foo.bar').body.should be == 'foo.bar' } example { get('/foo.png').body.should be == 'foo' } end context 'route local' do before do app.set(:pattern, nil) app.get('/:id', capture: /\d+/) { 'ok' } end example { get('/42').should be_ok } example { get('/foo').should_not be_ok } end context 'global and route local' do context 'global is a hash' do before do app.set(:pattern, capture: { id: /\d+/ }) app.get('/:id(.:ext)?', capture: { ext: 'png' }) { ?a } app.get('/:id', capture: { id: 'foo' }) { ?b } app.get('/:id', capture: :alpha) { ?c } end example { get('/20') .body.should be == ?a } example { get('/20.png') .body.should be == ?a } example { get('/foo') .body.should be == ?b } example { get('/bar') .body.should be == ?c } end context 'global is not a hash' do before do app.set(:pattern, capture: /\d+/) app.get('/:slug(.:ext)?', capture: { ext: 'png' }) { params[:slug] } app.get('/:slug', capture: :alpha) { 'ok' } end example { get('/20.png').should be_ok } example { get('/foo.png').should_not be_ok } example { get('/foo').should be_ok } example { get('/20.png') .body.should be == '20' } example { get('/42') .body.should be == '42' } example { get('/foo') .body.should be == 'ok' } end end end describe :pattern do describe :except do before { app.get('/auth/*', pattern: { except: '/auth/login' }) { 'ok' } } example { get('/auth/dunno').should be_ok } example { get('/auth/login').should_not be_ok } end describe :capture do context 'route local' do before do app.set(:pattern, nil) app.get('/:id', pattern: { capture: /\d+/ }) { 'ok' } end example { get('/42').should be_ok } example { get('/foo').should_not be_ok } end context 'global and route local' do context 'global is a hash' do before do app.set(:pattern, capture: { id: /\d+/ }) app.get('/:id(.:ext)?', pattern: { capture: { ext: 'png' }}) { ?a } app.get('/:id', pattern: { capture: { id: 'foo' }}) { ?b } app.get('/:id', pattern: { capture: :alpha }) { ?c } end example { get('/20') .body.should be == ?a } example { get('/20.png') .body.should be == ?a } example { get('/foo') .body.should be == ?b } example { get('/bar') .body.should be == ?c } end context 'global is not a hash' do before do app.set(:pattern, capture: /\d+/) app.get('/:slug(.:ext)?', pattern: { capture: { ext: 'png' }}) { params[:slug] } app.get('/:slug', pattern: { capture: :alpha }) { 'ok' } end example { get('/20.png').should be_ok } example { get('/foo.png').should_not be_ok } example { get('/foo').should be_ok } example { get('/20.png') .body.should be == '20' } example { get('/42') .body.should be == '42' } example { get('/foo') .body.should be == 'ok' } end end end describe :greedy do context 'default' do before { app.get('/:name.:ext') { params[:name] }} example { get('/foo.bar') .body.should be == 'foo' } example { get('/foo.bar.baz') .body.should be == 'foo.bar' } end context 'enabled' do before { app.get('/:name.:ext', pattern: { greedy: true }) { params[:name] }} example { get('/foo.bar') .body.should be == 'foo' } example { get('/foo.bar.baz') .body.should be == 'foo.bar' } end context 'disabled' do before { app.get('/:name.:ext', pattern: { greedy: false }) { params[:name] }} example { get('/foo.bar') .body.should be == 'foo' } example { get('/foo.bar.baz') .body.should be == 'foo' } end context 'global' do before do app.set(:pattern, greedy: false) app.get('/:name.:ext') { params[:name] } end example { get('/foo.bar') .body.should be == 'foo' } example { get('/foo.bar.baz') .body.should be == 'foo' } end end describe :space_matches_plus do context 'default' do before { app.get('/foo bar') { 'ok' }} example { get('/foo%20bar') .should be_ok } example { get('/foo+bar') .should be_ok } end context 'enabled' do before { app.get('/foo bar', pattern: { space_matches_plus: true }) { 'ok' }} example { get('/foo%20bar') .should be_ok } example { get('/foo+bar') .should be_ok } end context 'disabled' do before { app.get('/foo bar', pattern: { space_matches_plus: false }) { 'ok' }} example { get('/foo%20bar') .should be_ok } example { get('/foo+bar') .should_not be_ok } end context 'global' do before do app.set(:pattern, space_matches_plus: false) app.get('/foo bar') { 'ok' } end example { get('/foo%20bar') .should be_ok } example { get('/foo+bar') .should_not be_ok } end end describe :uri_decode do context 'default' do before { app.get('/&') { 'ok' }} example { get('/&') .should be_ok } example { get('/%26') .should be_ok } end context 'enabled' do before { app.get('/&', pattern: { uri_decode: true }) { 'ok' }} example { get('/&') .should be_ok } example { get('/%26') .should be_ok } end context 'disabled' do before { app.get('/&', pattern: { uri_decode: false }) { 'ok' }} example { get('/&') .should be_ok } example { get('/%26') .should_not be_ok } end context 'global' do before do app.set(:pattern, uri_decode: false) app.get('/&') { 'ok' } end example { get('/&') .should be_ok } example { get('/%26') .should_not be_ok } end end end describe :type do describe :identity do before do app.set(:pattern, type: :identity) app.get('/:foo') { 'ok' } end example { get('/:foo').should be_ok } example { get('/foo').should_not be_ok } end describe :rails do before do app.set(:pattern, type: :rails) app.get('/:slug(.:extension)') { params[:slug] } end example { get('/foo') .body.should be == 'foo' } example { get('/foo.') .body.should be == 'foo.' } example { get('/foo.bar') .body.should be == 'foo' } example { get('/a%20b') .body.should be == 'a b' } end describe :shell do before do app.set(:pattern, type: :shell) app.get('/{foo,bar}') { 'ok' } end example { get('/foo').should be_ok } example { get('/bar').should be_ok } end describe :simple do before do app.set(:pattern, type: :simple) app.get('/(a)') { 'ok' } end example { get('/(a)').should be_ok } example { get('/a').should_not be_ok } end describe :simple do before do app.set(:pattern, type: :template) app.get('/foo{/segments*}{.ext}') { "%p %p" % [params[:segments], params[:ext]] } end example { get('/foo/a.png').should be_ok } example { get('/foo/a').should_not be_ok } example { get('/foo/a.png').body.should be == '["a"] "png"' } example { get('/foo/a/b.png').body.should be == '["a", "b"] "png"' } end end context 'works with filters' do before do app.before('/auth/*', except: '/auth/login') { halt 'auth required' } app.get('/auth/login') { 'please log in' } end example { get('/auth/dunno').body.should be == 'auth required' } example { get('/auth/login').body.should be == 'please log in' } end end mustermann-1.1.1/mustermann/spec/identity_spec.rb000066400000000000000000000073231360365612700222340ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/identity' describe Mustermann::Identity do extend Support::Pattern pattern '' do it { should match('') } it { should_not match('/') } it { should respond_to(:expand) } it { should respond_to(:to_templates) } it { should generate_template('') } it { should expand.to('') } it { should expand(:ignore, a: 10).to('') } it { should expand(:append, a: 10).to('?a=10') } it { should_not expand(:raise, a: 10) } it { should_not expand(a: 10) } example do pattern.match('').inspect.should be == '#' end end pattern '/' do it { should match('/') } it { should_not match('/foo') } example { pattern.params('/').should be == {} } example { pattern.params('').should be_nil } it { should generate_template('/') } it { should expand.to('/') } end pattern '/foo' do it { should match('/foo') } it { should_not match('/bar') } it { should_not match('/foo.bar') } end pattern '/foo/bar' do it { should match('/foo/bar') } it { should match('/foo%2Fbar') } it { should match('/foo%2fbar') } end pattern '/:foo' do it { should match('/:foo') } it { should match('/%3Afoo') } it { should_not match('/foo') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should generate_template('/:foo') } it { should expand.to('/:foo') } end pattern '/föö' do it { should match("/f%C3%B6%C3%B6") } end pattern '/test$/' do it { should match('/test$/') } end pattern '/te+st/' do it { should match('/te+st/') } it { should_not match('/test/') } it { should_not match('/teest/') } end pattern "/path with spaces" do it { should match('/path%20with%20spaces') } it { should_not match('/path%2Bwith%2Bspaces') } it { should_not match('/path+with+spaces') } it { should generate_template('/path%20with%20spaces') } end pattern '/foo&bar' do it { should match('/foo&bar') } end pattern '/test.bar' do it { should match('/test.bar') } it { should_not match('/test0bar') } end pattern '/foo/bar', uri_decode: false do it { should match('/foo/bar') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } end pattern "/path with spaces", uri_decode: false do it { should_not match('/path%20with%20spaces') } it { should_not match('/path%2Bwith%2Bspaces') } it { should_not match('/path+with+spaces') } end context "peeking" do subject(:pattern) { Mustermann::Identity.new("foo bar") } describe :peek_size do example { pattern.peek_size("foo bar blah") .should be == "foo bar".size } example { pattern.peek_size("foo%20bar blah") .should be == "foo%20bar".size } example { pattern.peek_size("foobar") .should be_nil } end describe :peek_match do example { pattern.peek_match("foo bar blah").to_s .should be == "foo bar" } example { pattern.peek_match("foo%20bar blah").to_s .should be == "foo%20bar" } example { pattern.peek_match("foobar") .should be_nil } end describe :peek_params do example { pattern.peek_params("foo bar blah") .should be == [{}, "foo bar".size] } example { pattern.peek_params("foo%20bar blah") .should be == [{}, "foo%20bar".size] } example { pattern.peek_params("foobar") .should be_nil } end end end mustermann-1.1.1/mustermann/spec/mapper_spec.rb000066400000000000000000000063741360365612700216740ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/mapper' describe Mustermann::Mapper do describe :initialize do context 'accepts a block with no arguments, using the return value' do subject(:mapper) { Mustermann::Mapper.new(additional_values: :raise) {{ "/foo" => "/bar" }}} its(:to_h) { should be == { Mustermann.new("/foo") => Mustermann::Expander.new("/bar") } } example { mapper['/foo'].should be == '/bar' } example { mapper['/fox'].should be == '/fox' } end context 'accepts a block with argument, passes instance to it' do subject(:mapper) { Mustermann::Mapper.new(additional_values: :raise) { |m| m["/foo"] = "/bar" }} its(:to_h) { should be == { Mustermann.new("/foo") => Mustermann::Expander.new("/bar") } } example { mapper['/foo'].should be == '/bar' } example { mapper['/fox'].should be == '/fox' } end context 'accepts mappings followed by options' do subject(:mapper) { Mustermann::Mapper.new({ "/foo" => "/bar" }, additional_values: :raise) } its(:to_h) { should be == { Mustermann.new("/foo") => Mustermann::Expander.new("/bar") } } example { mapper['/foo'].should be == '/bar' } example { mapper['/fox'].should be == '/fox' } end context 'allows specifying type' do subject(:mapper) { Mustermann::Mapper.new({ "/foo" => "/bar" }, additional_values: :raise, type: :rails) } its(:to_h) { should be == { Mustermann.new("/foo", type: :rails) => Mustermann::Expander.new("/bar", type: :rails) } } example { mapper['/foo'].should be == '/bar' } example { mapper['/fox'].should be == '/fox' } end end describe :convert do subject(:mapper) { Mustermann::Mapper.new } context 'it maps params' do before { mapper["/:a"] = "/:a.html" } example { mapper["/foo"] .should be == "/foo.html" } example { mapper["/foo/bar"] .should be == "/foo/bar" } end context 'it supports named splats' do before { mapper["/*a"] = "/*a.html" } example { mapper["/foo"] .should be == "/foo.html" } example { mapper["/foo/bar"] .should be == "/foo/bar.html" } end context 'can map from patterns' do before { mapper[Mustermann.new("/:a")] = "/:a.html" } example { mapper["/foo"] .should be == "/foo.html" } example { mapper["/foo/bar"] .should be == "/foo/bar" } end context 'can map to patterns' do before { mapper[Mustermann.new("/:a")] = Mustermann.new("/:a.html") } example { mapper["/foo"] .should be == "/foo.html" } example { mapper["/foo/bar"] .should be == "/foo/bar" } end context 'can map to expanders' do before { mapper[Mustermann.new("/:a")] = Mustermann::Expander.new("/:a.html") } example { mapper["/foo"] .should be == "/foo.html" } example { mapper["/foo/bar"] .should be == "/foo/bar" } end context 'can map to array' do before { mapper["/:a"] = ["/:a.html", "/:a.:f"] } example { mapper["/foo"] .should be == "/foo.html" } example { mapper["/foo", "f" => 'x'] .should be == "/foo.x" } example { mapper["/foo", f: 'x'] .should be == "/foo.x" } example { mapper["/foo/bar"] .should be == "/foo/bar" } end end end mustermann-1.1.1/mustermann/spec/mustermann_spec.rb000066400000000000000000000105031360365612700225660ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann' require 'mustermann/extension' require 'sinatra/base' describe Mustermann do describe :new do context "string argument" do example { Mustermann.new('') .should be_a(Mustermann::Sinatra) } example { Mustermann.new('', type: :identity) .should be_a(Mustermann::Identity) } example { Mustermann.new('', type: :rails) .should be_a(Mustermann::Rails) } example { Mustermann.new('', type: :shell) .should be_a(Mustermann::Shell) } example { Mustermann.new('', type: :sinatra) .should be_a(Mustermann::Sinatra) } example { Mustermann.new('', type: :simple) .should be_a(Mustermann::Simple) } example { Mustermann.new('', type: :template) .should be_a(Mustermann::Template) } example { expect { Mustermann.new('', foo: :bar) }.to raise_error(ArgumentError, "unsupported option :foo for Mustermann::Sinatra") } example { expect { Mustermann.new('', type: :ast) }.to raise_error(ArgumentError, "unsupported type :ast (cannot load such file -- mustermann/ast)") } end context "pattern argument" do subject(:pattern) { Mustermann.new('') } example { Mustermann.new(pattern).should be == pattern } example { Mustermann.new(pattern, type: :rails).should be_a(Mustermann::Sinatra) } end context "regexp argument" do example { Mustermann.new(//) .should be_a(Mustermann::Regular) } example { Mustermann.new(//, type: :rails) .should be_a(Mustermann::Regular) } end context "argument implementing #to_pattern" do subject(:pattern) { Class.new { def to_pattern(**o) Mustermann.new('foo', **o) end }.new } example { Mustermann.new(pattern) .should be_a(Mustermann::Sinatra) } example { Mustermann.new(pattern, type: :rails) .should be_a(Mustermann::Rails) } example { Mustermann.new(pattern).to_s.should be == 'foo' } end context "multiple arguments" do example { Mustermann.new(':a', ':b/:a') .should be_a(Mustermann::Composite) } example { Mustermann.new(':a', ':b/:a').patterns.first .should be_a(Mustermann::Sinatra) } example { Mustermann.new(':a', ':b/:a').operator .should be == :| } example { Mustermann.new(':a', ':b/:a', operator: :&).operator .should be == :& } example { Mustermann.new(':a', ':b/:a', greedy: true) .should be_a(Mustermann::Composite) } example { Mustermann.new('/foo', ':bar') .should be_a(Mustermann::Sinatra) } example { Mustermann.new('/foo', ':bar').to_s .should be == "/foo|{bar}" } end context "invalid arguments" do it "raise a TypeError for unsupported types" do expect { Mustermann.new(10) }.to raise_error(TypeError, /(Integer|Fixnum) can't be coerced into Mustermann::Pattern/) end end end describe :[] do example { Mustermann[:identity] .should be == Mustermann::Identity } example { Mustermann[:rails] .should be == Mustermann::Rails } example { Mustermann[:shell] .should be == Mustermann::Shell } example { Mustermann[:sinatra] .should be == Mustermann::Sinatra } example { Mustermann[:simple] .should be == Mustermann::Simple } example { Mustermann[:template] .should be == Mustermann::Template } example { expect { Mustermann[:ast] }.to raise_error(ArgumentError, "unsupported type :ast (cannot load such file -- mustermann/ast)") } example { expect { Mustermann[:expander] }.to raise_error(ArgumentError, "unsupported type :expander") } end describe :extend_object do context 'special behavior for Sinatra only' do example { Object .new.extend(Mustermann).should be_a(Mustermann) } example { Object .new.extend(Mustermann).should_not be_a(Mustermann::Extension) } example { Class .new.extend(Mustermann).should be_a(Mustermann) } example { Class .new.extend(Mustermann).should_not be_a(Mustermann::Extension) } example { Sinatra .new.extend(Mustermann).should_not be_a(Mustermann) } example { Sinatra .new.extend(Mustermann).should be_a(Mustermann::Extension) } end end describe :=== do example { Mustermann.should be === Mustermann.new("") } end end mustermann-1.1.1/mustermann/spec/pattern_spec.rb000066400000000000000000000044641360365612700220630ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/pattern' require 'mustermann/sinatra' require 'mustermann/rails' describe Mustermann::Pattern do describe :=== do it 'raises a NotImplementedError when used directly' do expect { Mustermann::Pattern.new("") === "" }.to raise_error(NotImplementedError) end end describe :initialize do it 'raises an ArgumentError for unknown options' do expect { Mustermann::Pattern.new("", foo: :bar) }.to raise_error(ArgumentError) end it 'does not complain about unknown options if ignore_unknown_options is enabled' do expect { Mustermann::Pattern.new("", foo: :bar, ignore_unknown_options: true) }.not_to raise_error end end describe :respond_to? do subject(:pattern) { Mustermann::Pattern.new("") } it { should_not respond_to(:expand) } it { should_not respond_to(:to_templates) } it { expect { pattern.expand } .to raise_error(NotImplementedError) } it { expect { pattern.to_templates } .to raise_error(NotImplementedError) } end describe :== do example { Mustermann::Pattern.new('/foo') .should be == Mustermann::Pattern.new('/foo') } example { Mustermann::Pattern.new('/foo') .should_not be == Mustermann::Pattern.new('/bar') } example { Mustermann::Sinatra.new('/foo') .should be == Mustermann::Sinatra.new('/foo') } example { Mustermann::Rails.new('/foo') .should_not be == Mustermann::Sinatra.new('/foo') } end describe :eql? do example { Mustermann::Pattern.new('/foo') .should be_eql Mustermann::Pattern.new('/foo') } example { Mustermann::Pattern.new('/foo') .should_not be_eql Mustermann::Pattern.new('/bar') } example { Mustermann::Sinatra.new('/foo') .should be_eql Mustermann::Sinatra.new('/foo') } example { Mustermann::Rails.new('/foo') .should_not be_eql Mustermann::Sinatra.new('/foo') } end describe :equal? do example { Mustermann::Pattern.new('/foo') .should be_equal Mustermann::Pattern.new('/foo') } example { Mustermann::Pattern.new('/foo') .should_not be_equal Mustermann::Pattern.new('/bar') } example { Mustermann::Sinatra.new('/foo') .should be_equal Mustermann::Sinatra.new('/foo') } example { Mustermann::Rails.new('/foo') .should_not be_equal Mustermann::Sinatra.new('/foo') } end end mustermann-1.1.1/mustermann/spec/regexp_based_spec.rb000066400000000000000000000004251360365612700230270ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/regexp_based' describe Mustermann::RegexpBased do it 'raises a NotImplementedError when used directly' do expect { Mustermann::RegexpBased.new("") === "" }.to raise_error(NotImplementedError) end end mustermann-1.1.1/mustermann/spec/regular_spec.rb000066400000000000000000000140631360365612700220430ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'timeout' require 'mustermann/regular' describe Mustermann::Regular do extend Support::Pattern pattern '' do it { should match('') } it { should_not match('/') } it { should_not respond_to(:expand) } it { should_not respond_to(:to_templates) } end pattern '/' do it { should match('/') } it { should_not match('/foo') } end pattern '/foo' do it { should match('/foo') } it { should_not match('/bar') } it { should_not match('/foo.bar') } end pattern '/foo/bar' do it { should match('/foo/bar') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } end pattern '/(?.*)' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo.bar') .capturing foo: 'foo.bar' } it { should match('/%0Afoo') .capturing foo: '%0Afoo' } it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' } end context 'with Regexp::EXTENDED' do let(:pattern) { %r{ \/compare\/ # match any URL beginning with \/compare\/ (.+) # extract the full path (including any directories) \/ # match the final slash ([^.]+) # match the first SHA1 \.{2,3} # match .. or ... (.+) # match the second SHA1 }x } example { expect { Timeout.timeout(1){ Mustermann::Regular.new(pattern) }}.not_to raise_error } it { expect(Mustermann::Regular.new(pattern)).to match('/compare/foo/bar..baz') } end describe :check_achnors do context 'raises on anchors' do example { expect { Mustermann::Regular.new('^foo') }.to raise_error(Mustermann::CompileError) } example { expect { Mustermann::Regular.new('foo$') }.to raise_error(Mustermann::CompileError) } example { expect { Mustermann::Regular.new('\Afoo') }.to raise_error(Mustermann::CompileError) } example { expect { Mustermann::Regular.new('foo\Z') }.to raise_error(Mustermann::CompileError) } example { expect { Mustermann::Regular.new('foo\z') }.to raise_error(Mustermann::CompileError) } example { expect { Mustermann::Regular.new(/^foo/) }.to raise_error(Mustermann::CompileError) } example { expect { Mustermann::Regular.new(/foo$/) }.to raise_error(Mustermann::CompileError) } example { expect { Mustermann::Regular.new(/\Afoo/) }.to raise_error(Mustermann::CompileError) } example { expect { Mustermann::Regular.new(/foo\Z/) }.to raise_error(Mustermann::CompileError) } example { expect { Mustermann::Regular.new(/foo\z/) }.to raise_error(Mustermann::CompileError) } example { expect { Mustermann::Regular.new('[^f]') }.not_to raise_error } example { expect { Mustermann::Regular.new('\\\A') }.not_to raise_error } example { expect { Mustermann::Regular.new('[[:digit:]]') }.not_to raise_error } example { expect { Mustermann::Regular.new(/[^f]/) }.not_to raise_error } example { expect { Mustermann::Regular.new(/\\A/) }.not_to raise_error } example { expect { Mustermann::Regular.new(/[[:digit:]]/) }.not_to raise_error } end context 'with check_anchors disabled' do example { expect { Mustermann::Regular.new('^foo', check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new('foo$', check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new('\Afoo', check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new('foo\Z', check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new('foo\z', check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new(/^foo/, check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new(/foo$/, check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new(/\Afoo/, check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new(/foo\Z/, check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new(/foo\z/, check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new('[^f]', check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new('\\\A', check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new('[[:digit:]]', check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new(/[^f]/, check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new(/\\A/, check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new(/[[:digit:]]/, check_anchors: false) }.not_to raise_error } end end context "peeking" do subject(:pattern) { Mustermann::Regular.new("(?[^/]+)") } describe :peek_size do example { pattern.peek_size("foo bar/blah") .should be == "foo bar".size } example { pattern.peek_size("foo%20bar/blah") .should be == "foo%20bar".size } example { pattern.peek_size("/foo bar") .should be_nil } end describe :peek_match do example { pattern.peek_match("foo bar/blah") .to_s .should be == "foo bar" } example { pattern.peek_match("foo%20bar/blah") .to_s .should be == "foo%20bar" } example { pattern.peek_match("/foo bar") .should be_nil } end describe :peek_params do example { pattern.peek_params("foo bar/blah") .should be == [{"name" => "foo bar"}, "foo bar".size] } example { pattern.peek_params("foo%20bar/blah") .should be == [{"name" => "foo bar"}, "foo%20bar".size] } example { pattern.peek_params("/foo bar") .should be_nil } end end end mustermann-1.1.1/mustermann/spec/simple_match_spec.rb000066400000000000000000000005111360365612700230400ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/simple_match' describe Mustermann::SimpleMatch do subject { Mustermann::SimpleMatch.new('example') } its(:to_s) { should be == 'example' } its(:names) { should be == [] } its(:captures) { should be == [] } example { subject[1].should be == nil } end mustermann-1.1.1/mustermann/spec/sinatra_spec.rb000066400000000000000000000736771360365612700220630ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/sinatra' describe Mustermann::Sinatra do extend Support::Pattern pattern '' do it { should match('') } it { should_not match('/') } it { should generate_template('') } it { should respond_to(:expand) } it { should respond_to(:to_templates) } end pattern '/' do it { should match('/') } it { should_not match('/foo') } end pattern '/foo' do it { should match('/foo') } it { should_not match('/bar') } it { should_not match('/foo.bar') } end pattern '/foo/bar' do it { should match('/foo/bar') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } end pattern '/foo\/bar' do it { should match('/foo/bar') } it { should match('/foo%2Fbar') } it { should match('/foo%2fbar') } end pattern '/:foo' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo.bar') .capturing foo: 'foo.bar' } it { should match('/%0Afoo') .capturing foo: '%0Afoo' } it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should generate_template('/{foo}') } end pattern '/föö' do it { should match("/f%C3%B6%C3%B6") } end pattern "/:foo/:bar" do it { should match('/foo/bar') .capturing foo: 'foo', bar: 'bar' } it { should match('/foo.bar/bar.foo') .capturing foo: 'foo.bar', bar: 'bar.foo' } it { should match('/user@example.com/name') .capturing foo: 'user@example.com', bar: 'name' } it { should match('/10.1/te.st') .capturing foo: '10.1', bar: 'te.st' } it { should match('/10.1.2/te.st') .capturing foo: '10.1.2', bar: 'te.st' } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } example { pattern.params('/bar/foo').should be == {"foo" => "bar", "bar" => "foo"} } example { pattern.params('').should be_nil } it { should generate_template('/{foo}/{bar}') } end pattern "/{foo}/{bar}" do it { should match('/foo/bar') .capturing foo: 'foo', bar: 'bar' } it { should match('/foo.bar/bar.foo') .capturing foo: 'foo.bar', bar: 'bar.foo' } it { should match('/user@example.com/name') .capturing foo: 'user@example.com', bar: 'name' } it { should match('/10.1/te.st') .capturing foo: '10.1', bar: 'te.st' } it { should match('/10.1.2/te.st') .capturing foo: '10.1.2', bar: 'te.st' } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } example { pattern.params('/bar/foo').should be == {"foo" => "bar", "bar" => "foo"} } example { pattern.params('').should be_nil } it { should generate_template('/{foo}/{bar}') } end pattern '/hello/:person' do it { should match('/hello/Frank').capturing person: 'Frank' } it { should generate_template('/hello/{person}') } end pattern '/hello/{person}' do it { should match('/hello/Frank').capturing person: 'Frank' } it { should generate_template('/hello/{person}') } end pattern '/?:foo?/?:bar?' do it { should match('/hello/world') .capturing foo: 'hello', bar: 'world' } it { should match('/hello') .capturing foo: 'hello', bar: nil } it { should match('/') .capturing foo: nil, bar: nil } it { should match('') .capturing foo: nil, bar: nil } it { should expand(foo: 'hello') .to('/hello/') } it { should expand(foo: 'hello', bar: 'world') .to('/hello/world') } it { should expand(bar: 'world') .to('//world') } it { should expand .to('//') } it { should_not expand(baz: '') } it { should_not match('/hello/world/') } it { should generate_templates("", "/", "//", "//{bar}", "/{bar}", "/{foo}", "/{foo}/", "/{foo}/{bar}", "/{foo}{bar}", "{bar}", "{foo}", "{foo}/", "{foo}/{bar}", "{foo}{bar}") } end pattern '/:foo_bar' do it { should match('/hello').capturing foo_bar: 'hello' } it { should generate_template('/{foo_bar}') } end pattern '/{foo.bar}' do it { should match('/hello').capturing :"foo.bar" => 'hello' } it { should generate_template('/{foo.bar}') } end pattern '/*' do it { should match('/') .capturing splat: '' } it { should match('/foo') .capturing splat: 'foo' } it { should match('/foo/bar') .capturing splat: 'foo/bar' } it { should generate_template('/{+splat}') } example { pattern.params('/foo').should be == {"splat" => ["foo"]} } end pattern '/{+splat}' do it { should match('/') .capturing splat: '' } it { should match('/foo') .capturing splat: 'foo' } it { should match('/foo/bar') .capturing splat: 'foo/bar' } it { should generate_template('/{+splat}') } example { pattern.params('/foo').should be == {"splat" => ["foo"]} } end pattern '/*foo' do it { should match('/') .capturing foo: '' } it { should match('/foo') .capturing foo: 'foo' } it { should match('/foo/bar') .capturing foo: 'foo/bar' } it { should generate_template('/{+foo}') } example { pattern.params('/foo') .should be == {"foo" => "foo" } } example { pattern.params('/foo/bar') .should be == {"foo" => "foo/bar" } } end pattern '/{+foo}' do it { should match('/') .capturing foo: '' } it { should match('/foo') .capturing foo: 'foo' } it { should match('/foo/bar') .capturing foo: 'foo/bar' } it { should generate_template('/{+foo}') } example { pattern.params('/foo') .should be == {"foo" => "foo" } } example { pattern.params('/foo/bar') .should be == {"foo" => "foo/bar" } } end pattern '/*foo/*bar' do it { should match('/foo/bar') .capturing foo: 'foo', bar: 'bar' } it { should generate_template('/{+foo}/{+bar}') } end pattern '/{+foo}/{+bar}' do it { should match('/foo/bar') .capturing foo: 'foo', bar: 'bar' } it { should generate_template('/{+foo}/{+bar}') } end pattern '/:foo/*' do it { should match("/foo/bar/baz") .capturing foo: 'foo', splat: 'bar/baz' } it { should match("/foo/") .capturing foo: 'foo', splat: '' } it { should match('/h%20w/h%20a%20y') .capturing foo: 'h%20w', splat: 'h%20a%20y' } it { should_not match('/foo') } it { should generate_template('/{foo}/{+splat}') } example { pattern.params('/bar/foo').should be == {"splat" => ["foo"], "foo" => "bar"} } example { pattern.params('/bar/foo/f%20o').should be == {"splat" => ["foo/f o"], "foo" => "bar"} } end pattern '/{foo}/*' do it { should match("/foo/bar/baz") .capturing foo: 'foo', splat: 'bar/baz' } it { should match("/foo/") .capturing foo: 'foo', splat: '' } it { should match('/h%20w/h%20a%20y') .capturing foo: 'h%20w', splat: 'h%20a%20y' } it { should_not match('/foo') } it { should generate_template('/{foo}/{+splat}') } example { pattern.params('/bar/foo').should be == {"splat" => ["foo"], "foo" => "bar"} } example { pattern.params('/bar/foo/f%20o').should be == {"splat" => ["foo/f o"], "foo" => "bar"} } end pattern '/test$/' do it { should match('/test$/') } end pattern '/te+st/' do it { should match('/te+st/') } it { should_not match('/test/') } it { should_not match('/teest/') } end pattern "/path with spaces" do it { should match('/path%20with%20spaces') } it { should match('/path%2Bwith%2Bspaces') } it { should match('/path+with+spaces') } it { should generate_template('/path%20with%20spaces') } end pattern '/foo&bar' do it { should match('/foo&bar') } end pattern '/foo\{bar' do it { should match('/foo%7Bbar') } end pattern '/*/:foo/*/*' do it { should match('/bar/foo/bling/baz/boom') } it "should capture all splat parts" do match = pattern.match('/bar/foo/bling/baz/boom') match.captures.should be == ['bar', 'foo', 'bling', 'baz/boom'] match.names.should be == ['splat', 'foo'] end it 'should map to proper params' do pattern.params('/bar/foo/bling/baz/boom').should be == { "foo" => "foo", "splat" => ['bar', 'bling', 'baz/boom'] } end end pattern '/{+splat}/{foo}/{+splat}/{+splat}' do it { should match('/bar/foo/bling/baz/boom') } it "should capture all splat parts" do match = pattern.match('/bar/foo/bling/baz/boom') match.captures.should be == ['bar', 'foo', 'bling', 'baz/boom'] match.names.should be == ['splat', 'foo'] end it 'should map to proper params' do pattern.params('/bar/foo/bling/baz/boom').should be == { "foo" => "foo", "splat" => ['bar', 'bling', 'baz/boom'] } end end pattern '/test.bar' do it { should match('/test.bar') } it { should_not match('/test0bar') } end pattern '/:file.:ext' do it { should match('/pony.jpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%2Ejpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%2ejpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%E6%AD%A3%2Ejpg') .capturing file: 'pony%E6%AD%A3', ext: 'jpg' } it { should match('/pony%e6%ad%a3%2ejpg') .capturing file: 'pony%e6%ad%a3', ext: 'jpg' } it { should match('/pony正%2Ejpg') .capturing file: 'pony正', ext: 'jpg' } it { should match('/pony正%2ejpg') .capturing file: 'pony正', ext: 'jpg' } it { should match('/pony正..jpg') .capturing file: 'pony正.', ext: 'jpg' } it { should_not match('/.jpg') } end pattern '/(:a)x?' do it { should match('/a') .capturing a: 'a' } it { should match('/xa') .capturing a: 'xa' } it { should match('/axa') .capturing a: 'axa' } it { should match('/ax') .capturing a: 'a' } it { should match('/axax') .capturing a: 'axa' } it { should match('/axaxx') .capturing a: 'axax' } it { should generate_template('/{a}x') } it { should generate_template('/{a}') } end pattern '/:user(@:host)?' do it { should match('/foo@bar') .capturing user: 'foo', host: 'bar' } it { should match('/foo.foo@bar') .capturing user: 'foo.foo', host: 'bar' } it { should match('/foo@bar.bar') .capturing user: 'foo', host: 'bar.bar' } it { should generate_template('/{user}') } it { should generate_template('/{user}@{host}') } end pattern '/:file(.:ext)?' do it { should match('/pony') .capturing file: 'pony', ext: nil } it { should match('/pony.jpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%2Ejpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%2ejpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony.png.jpg') .capturing file: 'pony.png', ext: 'jpg' } it { should match('/pony.') .capturing file: 'pony.' } it { should_not match('/.jpg') } it { should generate_template('/{file}') } it { should generate_template('/{file}.{ext}') } it { should_not generate_template('/{file}.') } end pattern '/:id/test.bar' do it { should match('/3/test.bar') .capturing id: '3' } it { should match('/2/test.bar') .capturing id: '2' } it { should match('/2E/test.bar') .capturing id: '2E' } it { should match('/2e/test.bar') .capturing id: '2e' } it { should match('/%2E/test.bar') .capturing id: '%2E' } end pattern '/10/:id' do it { should match('/10/test') .capturing id: 'test' } it { should match('/10/te.st') .capturing id: 'te.st' } end pattern '/10.1/:id' do it { should match('/10.1/test') .capturing id: 'test' } it { should match('/10.1/te.st') .capturing id: 'te.st' } end pattern '/:foo.:bar/:id' do it { should match('/10.1/te.st') .capturing foo: "10", bar: "1", id: "te.st" } it { should match('/10.1.2/te.st') .capturing foo: "10.1", bar: "2", id: "te.st" } end pattern '/:a/:b.?:c?' do it { should match('/a/b') .capturing a: 'a', b: 'b', c: nil } it { should match('/a/b.c') .capturing a: 'a', b: 'b', c: 'c' } it { should match('/a.b/c') .capturing a: 'a.b', b: 'c', c: nil } it { should match('/a.b/c.d') .capturing a: 'a.b', b: 'c', c: 'd' } it { should_not match('/a.b/c.d/e') } end pattern '/:a(foo:b)?' do it { should match('/barfoobar') .capturing a: 'bar', b: 'bar' } it { should match('/barfoobarfoobar') .capturing a: 'barfoobar', b: 'bar' } it { should match('/bar') .capturing a: 'bar', b: nil } it { should_not match('/') } end pattern '/foo?' do it { should match('/fo') } it { should match('/foo') } it { should_not match('') } it { should_not match('/') } it { should_not match('/f') } it { should_not match('/fooo') } end pattern '/foo\?' do it { should match('/foo?') } it { should_not match('/foo\?') } it { should_not match('/fo') } it { should_not match('/foo') } it { should_not match('') } it { should_not match('/') } it { should_not match('/f') } it { should_not match('/fooo') } end pattern '/foo\\\?' do it { should match('/foo%5c') } it { should match('/foo') } it { should_not match('/foo\?') } it { should_not match('/fo') } it { should_not match('') } it { should_not match('/') } it { should_not match('/f') } it { should_not match('/fooo') } end pattern '/\(' do it { should match('/(') } end pattern '/\(?' do it { should match('/(') } it { should match('/') } end pattern '/(\()?' do it { should match('/(') } it { should match('/') } end pattern '/(\(\))?' do it { should match('/') } it { should match('/()') } it { should_not match('/(') } end pattern '/\(\)?' do it { should match('/(') } it { should match('/()') } it { should_not match('/') } end pattern '/\*' do it { should match('/*') } it { should_not match('/a') } end pattern '/\*/*' do it { should match('/*/b/c') } it { should_not match('/a/b/c') } end pattern '/\:foo' do it { should match('/:foo') } it { should_not match('/foo') } end pattern '/:fOO' do it { should match('/a').capturing fOO: 'a' } end pattern '/:_X' do it { should match('/a').capturing _X: 'a' } end pattern '/:f00' do it { should match('/a').capturing f00: 'a' } end pattern '/:foo(/:bar)?/:baz?' do it { should match('/foo/bar/baz').capturing foo: 'foo', bar: 'bar', baz: 'baz' } end pattern "/(foo|bar)" do it { should match("/foo") } it { should match("/bar") } it { should generate_template('/foo') } it { should generate_template('/bar') } end pattern "/(foo\\|bar)" do it { should match "/foo%7Cbar" } it { should generate_template "/foo%7Cbar" } it { should_not match("/foo") } it { should_not match("/bar") } it { should_not generate_template('/foo') } it { should_not generate_template('/bar') } end pattern "/(:a/:b|:c)" do it { should match("/foo") .capturing c: 'foo' } it { should match("/foo/bar") .capturing a: 'foo', b: 'bar' } it { should generate_template('/{a}/{b}') } it { should generate_template('/{c}') } it { should expand(a: 'foo', b: 'bar') .to('/foo/bar') } it { should expand(c: 'foo') .to('/foo') } it { should_not expand(a: 'foo', b: 'bar', c: 'baz') } end pattern "/:a/:b|:c" do it { should match("foo") .capturing c: 'foo' } it { should match("/foo/bar") .capturing a: 'foo', b: 'bar' } it { should generate_template('/{a}/{b}') } it { should generate_template('{c}') } it { should expand(a: 'foo', b: 'bar') .to('/foo/bar') } it { should expand(c: 'foo') .to('foo') } it { should_not expand(a: 'foo', b: 'bar', c: 'baz') } end pattern "/({foo}|{bar})", capture: { foo: /\d+/, bar: /\w+/ } do it { should match("/a") .capturing foo: nil, bar: 'a' } it { should match("/1234") .capturing foo: "1234", bar: nil } it { should_not match("/a/b") } end pattern "/{foo|bar}", capture: { foo: /\d+/, bar: /\w+/ } do it { should match("/a") .capturing foo: nil, bar: 'a' } it { should match("/1234") .capturing foo: "1234", bar: nil } it { should_not match("/a/b") } end pattern "/{foo|bar|baz}", capture: { foo: /\d+/, bar: /[ab]+/, baz: /[cde]+/ } do it { should match("/ab") .capturing foo: nil, bar: 'ab', baz: nil } it { should match("/1234") .capturing foo: "1234", bar: nil, baz: nil } it { should match("/ccddee") .capturing foo: nil, bar: nil, baz: "ccddee" } it { should_not match("/a/b") } end pattern '/:foo', capture: /\d+/ do it { should match('/1') .capturing foo: '1' } it { should match('/123') .capturing foo: '123' } it { should_not match('/') } it { should_not match('/foo') } end pattern '/:foo', capture: /\d+/ do it { should match('/1') .capturing foo: '1' } it { should match('/123') .capturing foo: '123' } it { should_not match('/') } it { should_not match('/foo') } end pattern '/:foo', capture: '1' do it { should match('/1').capturing foo: '1' } it { should_not match('/') } it { should_not match('/foo') } it { should_not match('/123') } end pattern '/:foo', capture: 'a.b' do it { should match('/a.b') .capturing foo: 'a.b' } it { should match('/a%2Eb') .capturing foo: 'a%2Eb' } it { should match('/a%2eb') .capturing foo: 'a%2eb' } it { should_not match('/ab') } it { should_not match('/afb') } it { should_not match('/a1b') } it { should_not match('/a.bc') } end pattern '/:foo(/:bar)?', capture: :alpha do it { should match('/abc') .capturing foo: 'abc', bar: nil } it { should match('/a/b') .capturing foo: 'a', bar: 'b' } it { should match('/a') .capturing foo: 'a', bar: nil } it { should_not match('/1/2') } it { should_not match('/a/2') } it { should_not match('/1/b') } it { should_not match('/1') } it { should_not match('/1/') } it { should_not match('/a/') } it { should_not match('//a') } end pattern '/:foo', capture: ['foo', 'bar', /\d+/] do it { should match('/1') .capturing foo: '1' } it { should match('/123') .capturing foo: '123' } it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should_not match('/') } it { should_not match('/baz') } it { should_not match('/foo1') } end pattern '/:foo:bar:baz', capture: { foo: :alpha, bar: /\d+/ } do it { should match('/ab123xy-1') .capturing foo: 'ab', bar: '123', baz: 'xy-1' } it { should match('/ab123') .capturing foo: 'ab', bar: '12', baz: '3' } it { should_not match('/123abcxy-1') } it { should_not match('/abcxy-1') } it { should_not match('/abc1') } end pattern '/:foo', capture: { foo: ['foo', 'bar', /\d+/] } do it { should match('/1') .capturing foo: '1' } it { should match('/123') .capturing foo: '123' } it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should_not match('/') } it { should_not match('/baz') } it { should_not match('/foo1') } end pattern '/:file(.:ext)?', capture: { ext: ['jpg', 'png'] } do it { should match('/pony') .capturing file: 'pony', ext: nil } it { should match('/pony.jpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%2Ejpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%2ejpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony.png') .capturing file: 'pony', ext: 'png' } it { should match('/pony%2Epng') .capturing file: 'pony', ext: 'png' } it { should match('/pony%2epng') .capturing file: 'pony', ext: 'png' } it { should match('/pony.png.jpg') .capturing file: 'pony.png', ext: 'jpg' } it { should match('/pony.jpg.png') .capturing file: 'pony.jpg', ext: 'png' } it { should match('/pony.gif') .capturing file: 'pony.gif', ext: nil } it { should match('/pony.') .capturing file: 'pony.', ext: nil } it { should_not match('.jpg') } end pattern '/:file:ext?', capture: { ext: ['.jpg', '.png', '.tar.gz'] } do it { should match('/pony') .capturing file: 'pony', ext: nil } it { should match('/pony.jpg') .capturing file: 'pony', ext: '.jpg' } it { should match('/pony.png') .capturing file: 'pony', ext: '.png' } it { should match('/pony.png.jpg') .capturing file: 'pony.png', ext: '.jpg' } it { should match('/pony.jpg.png') .capturing file: 'pony.jpg', ext: '.png' } it { should match('/pony.tar.gz') .capturing file: 'pony', ext: '.tar.gz' } it { should match('/pony.gif') .capturing file: 'pony.gif', ext: nil } it { should match('/pony.') .capturing file: 'pony.', ext: nil } it { should_not match('/.jpg') } end pattern '/:a(@:b)?', capture: { b: /\d+/ } do it { should match('/a') .capturing a: 'a', b: nil } it { should match('/a@1') .capturing a: 'a', b: '1' } it { should match('/a@b') .capturing a: 'a@b', b: nil } it { should match('/a@1@2') .capturing a: 'a@1', b: '2' } end pattern '/(:a)b?', greedy: false do it { should match('/ab').capturing a: 'a' } end pattern '/:file(.:ext)?', greedy: false do it { should match('/pony') .capturing file: 'pony', ext: nil } it { should match('/pony.jpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony.png.jpg') .capturing file: 'pony', ext: 'png.jpg' } end pattern '/auth/*', except: '/auth/login' do it { should match('/auth/admin') } it { should match('/auth/foobar') } it { should_not match('/auth/login') } end pattern '/:foo/:bar', except: '/:bar/20' do it { should match('/foo/bar').capturing foo: 'foo', bar: 'bar' } it { should_not match('/20/20') } end pattern '/foo?', uri_decode: false do it { should match('/foo') } it { should match('/fo') } it { should_not match('/foo?') } end pattern '/foo/bar', uri_decode: false do it { should match('/foo/bar') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } end pattern "/path with spaces", uri_decode: false do it { should match('/path with spaces') } it { should_not match('/path%20with%20spaces') } it { should_not match('/path%2Bwith%2Bspaces') } it { should_not match('/path+with+spaces') } end pattern "/path with spaces", space_matches_plus: false do it { should match('/path%20with%20spaces') } it { should_not match('/path%2Bwith%2Bspaces') } it { should_not match('/path+with+spaces') } end context 'invalid syntax' do example 'unexpected closing parenthesis' do expect { Mustermann::Sinatra.new('foo)bar') }. to raise_error(Mustermann::ParseError, 'unexpected ) while parsing "foo)bar"') end example 'missing closing parenthesis' do expect { Mustermann::Sinatra.new('foo(bar') }. to raise_error(Mustermann::ParseError, 'unexpected end of string while parsing "foo(bar"') end example 'missing unescaped closing parenthesis' do expect { Mustermann::Sinatra.new('foo(bar\)') }. to raise_error(Mustermann::ParseError, 'unexpected end of string while parsing "foo(bar\\\\)"') end example '? at beginning of route' do expect { Mustermann::Sinatra.new('?foobar') }. to raise_error(Mustermann::ParseError, 'unexpected ? while parsing "?foobar"') end example 'double ?' do expect { Mustermann::Sinatra.new('foo??bar') }. to raise_error(Mustermann::ParseError, 'unexpected ? while parsing "foo??bar"') end example 'dangling escape' do expect { Mustermann::Sinatra.new('foo\\') }. to raise_error(Mustermann::ParseError, 'unexpected end of string while parsing "foo\\\\"') end end context 'invalid capture names' do example 'empty name' do expect { Mustermann::Sinatra.new('/:/') }. to raise_error(Mustermann::CompileError, "capture name can't be empty: \"/:/\"") end example 'named splat' do expect { Mustermann::Sinatra.new('/:splat/') }. to raise_error(Mustermann::CompileError, "capture name can't be splat: \"/:splat/\"") end example 'named captures' do expect { Mustermann::Sinatra.new('/:captures/') }. to raise_error(Mustermann::CompileError, "capture name can't be captures: \"/:captures/\"") end example 'with capital letter' do expect { Mustermann::Sinatra.new('/:Foo/') }. to raise_error(Mustermann::CompileError, "capture name must start with underscore or lower case letter: \"/:Foo/\"") end example 'with integer' do expect { Mustermann::Sinatra.new('/:1a/') }. to raise_error(Mustermann::CompileError, "capture name must start with underscore or lower case letter: \"/:1a/\"") end example 'same name twice' do expect { Mustermann::Sinatra.new('/:foo(/:bar)?/:bar?') }. to raise_error(Mustermann::CompileError, "can't use the same capture name twice: \"/:foo(/:bar)?/:bar?\"") end end context 'Regexp compatibility' do describe :=== do example('non-matching') { Mustermann::Sinatra.new("/") .should_not be === '/foo' } example('matching') { Mustermann::Sinatra.new("/:foo") .should be === '/foo' } end describe :=~ do example('non-matching') { Mustermann::Sinatra.new("/") .should_not be =~ '/foo' } example('matching') { Mustermann::Sinatra.new("/:foo") .should be =~ '/foo' } context 'String#=~' do example('non-matching') { "/foo".should_not be =~ Mustermann::Sinatra.new("/") } example('matching') { "/foo".should be =~ Mustermann::Sinatra.new("/:foo") } end end describe :to_regexp do example('empty pattern') { Mustermann::Sinatra.new('').to_regexp.should be == /\A(?-mix:)\Z/ } context 'Regexp.try_convert' do example('empty pattern') { Regexp.try_convert(Mustermann::Sinatra.new('')).should be == /\A(?-mix:)\Z/ } end end end context 'Proc compatibility' do describe :to_proc do example { Mustermann::Sinatra.new("/").to_proc.should be_a(Proc) } example('non-matching') { Mustermann::Sinatra.new("/") .to_proc.call('/foo').should be == false } example('matching') { Mustermann::Sinatra.new("/:foo") .to_proc.call('/foo').should be == true } end end context "peeking" do subject(:pattern) { Mustermann::Sinatra.new(":name") } describe :peek_size do example { pattern.peek_size("foo bar/blah") .should be == "foo bar".size } example { pattern.peek_size("foo%20bar/blah") .should be == "foo%20bar".size } example { pattern.peek_size("/foo bar") .should be_nil } end describe :peek_match do example { pattern.peek_match("foo bar/blah") .to_s .should be == "foo bar" } example { pattern.peek_match("foo%20bar/blah") .to_s .should be == "foo%20bar" } example { pattern.peek_match("/foo bar") .should be_nil } end describe :peek_params do example { pattern.peek_params("foo bar/blah") .should be == [{"name" => "foo bar"}, "foo bar".size] } example { pattern.peek_params("foo%20bar/blah") .should be == [{"name" => "foo bar"}, "foo%20bar".size] } example { pattern.peek_params("/foo bar") .should be_nil } end end describe :| do let(:first) { Mustermann.new("a") } let(:second) { Mustermann.new("b") } subject(:composite) { first | second } context "with no capture names" do its(:class) { should be == Mustermann::Sinatra } its(:to_s) { should be == "a|b" } end context "only first has captures" do let(:first) { Mustermann.new(":a") } its(:class) { should be == Mustermann::Sinatra } its(:to_s) { should be == "{a}|b" } end context "only second has captures" do let(:second) { Mustermann.new(":b") } its(:class) { should be == Mustermann::Sinatra } its(:to_s) { should be == "a|{b}" } end context "both have captures" do let(:first) { Mustermann.new(":a") } let(:second) { Mustermann.new(":b") } its(:class) { should be == Mustermann::Composite } end context "options mismatch" do let(:second) { Mustermann.new(":b", greedy: false) } its(:class) { should be == Mustermann::Composite } end context "argument is a string" do let(:second) { ":b" } its(:class) { should be == Mustermann::Sinatra } its(:to_s) { should be == "a|\\:b" } end context "argument is an identity pattern" do let(:second) { Mustermann::Identity.new(":b") } its(:class) { should be == Mustermann::Sinatra } its(:to_s) { should be == "a|\\:b" } end context "argument is an identity pattern, but options mismatch" do let(:second) { Mustermann::Identity.new(":b", uri_decode: false) } its(:class) { should be == Mustermann::Composite } end end describe "native concatination" do subject { Mustermann.new(prefix) + Mustermann.new(pattern) } let(:prefix) { "/a" } let(:pattern) { "/:b(.:c)?" } it { should match("/a/b.json") } end end mustermann-1.1.1/mustermann/spec/to_pattern_spec.rb000066400000000000000000000046251360365612700225640ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/to_pattern' require 'delegate' describe Mustermann::ToPattern do context String do example { "".to_pattern .should be_a(Mustermann::Sinatra) } example { "".to_pattern(type: :rails) .should be_a(Mustermann::Rails) } end context Regexp do example { //.to_pattern .should be_a(Mustermann::Regular) } example { //.to_pattern(type: :rails) .should be_a(Mustermann::Regular) } end context Symbol do example { :foo.to_pattern .should be_a(Mustermann::Sinatra) } example { :foo.to_pattern(type: :rails) .should be_a(Mustermann::Sinatra) } end context Array do example { [:foo, :bar].to_pattern .should be_a(Mustermann::Composite) } example { [:foo, :bar].to_pattern(type: :rails) .should be_a(Mustermann::Composite) } end context Mustermann::Pattern do subject(:pattern) { Mustermann.new('') } example { pattern.to_pattern.should be == pattern } example { pattern.to_pattern(type: :rails).should be_a(Mustermann::Sinatra) } end context 'custom class' do let(:example_class) do Class.new do include Mustermann::ToPattern def to_s ":foo/:bar" end end end example { example_class.new.to_pattern .should be_a(Mustermann::Sinatra) } example { example_class.new.to_pattern(type: :rails) .should be_a(Mustermann::Rails) } example { Mustermann.new(example_class.new) .should be_a(Mustermann::Sinatra) } example { Mustermann.new(example_class.new, type: :rails) .should be_a(Mustermann::Rails) } end context 'primitive delegate' do let(:example_class) do Class.new(DelegateClass(Array)) do include Mustermann::ToPattern end end example { example_class.new([:foo, :bar]).to_pattern .should be_a(Mustermann::Composite) } example { example_class.new([:foo, :bar]).to_pattern(type: :rails) .should be_a(Mustermann::Composite) } end context 'primitive subclass' do let(:example_class) do Class.new(Array) do include Mustermann::ToPattern end end example { example_class.new([:foo, :bar]).to_pattern .should be_a(Mustermann::Composite) } example { example_class.new([:foo, :bar]).to_pattern(type: :rails) .should be_a(Mustermann::Composite) } end end mustermann-1.1.1/support/000077500000000000000000000000001360365612700154305ustar00rootroot00000000000000mustermann-1.1.1/support/lib/000077500000000000000000000000001360365612700161765ustar00rootroot00000000000000mustermann-1.1.1/support/lib/support.rb000066400000000000000000000003271360365612700202410ustar00rootroot00000000000000require 'support/env' require 'support/coverage' require 'support/expand_matcher' require 'support/generate_template_matcher' require 'support/match_matcher' require 'support/pattern' require 'support/scan_matcher' mustermann-1.1.1/support/lib/support/000077500000000000000000000000001360365612700177125ustar00rootroot00000000000000mustermann-1.1.1/support/lib/support/coverage.rb000066400000000000000000000007361360365612700220400ustar00rootroot00000000000000require 'simplecov' require 'coveralls' require 'support/projects' SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new( [ SimpleCov::Formatter::HTMLFormatter, Coveralls::SimpleCov::Formatter ] ) SimpleCov.start do project_name 'mustermann' minimum_coverage 100 coverage_dir '.coverage' add_filter "/spec/" add_filter "/support/" Support::Projects.each do |project| add_group project.sub('mustermann-', ''), "/#{project}/lib" end end mustermann-1.1.1/support/lib/support/env.rb000066400000000000000000000005251360365612700210310ustar00rootroot00000000000000if RUBY_VERSION < '2.1.0' $stderr.puts "needs Ruby 2.1.0, you're running #{RUBY_VERSION}" exit 1 end RUBY_ENGINE ||= 'ruby' ENV['RACK_ENV'] = 'test' require 'tool/warning_filter' $-w = true require 'rspec' require 'rspec/its' RSpec.configure do |config| config.expect_with :rspec do |c| c.syntax = [:should, :expect] end end mustermann-1.1.1/support/lib/support/expand_matcher.rb000066400000000000000000000013541360365612700232240ustar00rootroot00000000000000RSpec::Matchers.define :expand do |behavior = nil, **values| match do |pattern| @string ||= nil begin expanded = pattern.expand(behavior, values) rescue Exception false else @string ? @string == expanded : !!expanded end end chain :to do |string| @string = string end failure_message do |pattern| message = "expected %p to be expandable with %p" % [pattern, values] message << " (%p behavior)" % behavior if behavior expanded = pattern.expand(behavior, values) message << " and result in %p, but got %p" % [@string, expanded] if @string message end failure_message_when_negated do |pattern| "expected %p not to be expandable with %p" % [pattern, values] end endmustermann-1.1.1/support/lib/support/generate_template_matcher.rb000066400000000000000000000015151360365612700254310ustar00rootroot00000000000000RSpec::Matchers.define :generate_template do |template| match { |pattern| pattern.to_templates.include? template } failure_message do |pattern| "expected %p to generate template %p, but only generated %s" % [ pattern, template, pattern.to_templates.map(&:inspect).join(', ') ] end failure_message_when_negated do |pattern| "expected %p not to generate template %p" % [ pattern, template ] end end RSpec::Matchers.define :generate_templates do |*templates| match { |pattern| pattern.to_templates.sort == templates.sort } failure_message do |pattern| "expected %p to generate templates %p, but generated %p" % [ pattern, templates.sort, pattern.to_templates.sort ] end failure_message_when_negated do |pattern| "expected %p not to generate templates %p" % [ pattern, templates ] end endmustermann-1.1.1/support/lib/support/match_matcher.rb000066400000000000000000000020201360365612700230300ustar00rootroot00000000000000RSpec::Matchers.define :match do |expected| match do |actual| @captures ||= false match = actual.match(expected) match &&= @captures.all? { |k, v| match[k] == v } if @captures match end chain :capturing do |captures| @captures = captures end failure_message do |actual| require 'pp' match = actual.match(expected) if match key, value = @captures.detect { |k, v| match[k] != v } message = "expected %p to capture %p as %p when matching %p, but got %p\n\nRegular Expression:\n%p" % [ actual.to_s, key, value, expected, match[key], actual.regexp ] if actual.respond_to? :to_ast require 'mustermann/visualizer/tree_renderer' tree = Mustermann::Visualizer::TreeRenderer.render(actual) message << "\n\nAST:\n#{tree}" end message else "expected %p to match %p" % [ actual, expected ] end end failure_message_when_negated do |actual| "expected %p not to match %p" % [ actual.to_s, expected ] end endmustermann-1.1.1/support/lib/support/pattern.rb000066400000000000000000000024121360365612700217130ustar00rootroot00000000000000require 'timeout' module Support module Pattern extend RSpec::Matchers::DSL def pattern(pattern, options = nil, &block) description = "pattern %p" % pattern if options description << " with options %p" % [options] instance = subject_for(pattern, **options) else instance = subject_for(pattern) end context description do subject(:pattern, &instance) its(:to_s) { should be == pattern } its(:inspect) { should be == "#<#{described_class}:#{pattern.inspect}>" } its(:names) { should be_an(Array) } its(:to_templates) { should be == [pattern] } if described_class.name == "Mustermann::Template" example 'string should be immune to external change' do subject.to_s.replace "NOT THE PATTERN" subject.to_s.should be == pattern end instance_eval(&block) end end def subject_for(pattern, *args, **options) instance = Timeout.timeout(1) { described_class.new(pattern, *args, **options) } proc { instance } rescue Timeout::Error => error proc { raise Timeout::Error, "could not compile #{pattern.inspect} in time", error.backtrace } rescue Exception => error proc { raise error } end end endmustermann-1.1.1/support/lib/support/projects.rb000066400000000000000000000005441360365612700220730ustar00rootroot00000000000000module Support module Projects include Enumerable extend self def base File.expand_path('../../../..', __FILE__) end def projects @@projects ||= Dir.chdir(base) do Dir['mustermann*/*.gemspec'].map { |f| File.dirname(f) }.sort end end def each(&block) projects.each(&block) end end end mustermann-1.1.1/support/lib/support/scan_matcher.rb000066400000000000000000000035701360365612700226730ustar00rootroot00000000000000module Support module ScanMatcher extend RSpec::Matchers::DSL def scan(pattern, **options) give_scan_result(:scan, pattern, **options) end def check(pattern, **options) give_scan_result(:check, pattern, **options) end def scan_until(pattern, **options) give_scan_result(:scan_until, pattern, **options) end def check_until(pattern, **options) give_scan_result(:check_until, pattern, **options) end matcher :give_scan_result do |method_name, pattern, **options| def result_expectations @result_expectations ||= [] end def expect_result(description, expected, &block) result_expectations << Proc.new do |result| if !block.call(result) "expected %p to %s %p matching %s" % [ result.scanner, method_name, pattern, description ] end end end match do |scanner| scanned = scanner.public_send(method_name, pattern, **options) scanned and result_expectations.all? { |e| !e.call(scanned) } end chain(:matching_substring) do |substring| expected_result("the substring %p" % substring) { |r| r.to_s == substring } end chain(:matching_length) do |length| expected_result("%d characters" % length) { |r| r.length == length } end chain(:matching_params) do |params| expected_result("with params %p" % [params]) { |r| r.params == params } end failure_message do |scanner| if scanned = scanner.public_send(method_name, pattern, **options) message = result_expectations.inject(nil) { |m,e| m || e.call(scanned) } end message || "expected %p to %s %p" % [ scanner, method_name, pattern ] end failure_message_when_negated do |scanner| "expected %p not to %s %p" % [ scanner, method_name, pattern ] end end end endmustermann-1.1.1/support/support.gemspec000066400000000000000000000017021360365612700205110ustar00rootroot00000000000000$:.unshift File.expand_path("../../mustermann/lib", __FILE__) require "mustermann/version" Gem::Specification.new do |s| s.name = "support" s.version = "0.0.1" s.author = "Konstantin Haase" s.email = "konstantin.mailinglists@googlemail.com" s.homepage = "https://github.com/rkh/mustermann" s.summary = %q{support for mustermann development} s.require_path = 'lib' s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } s.add_dependency 'tool', '~> 0.2' s.add_dependency 'rspec' s.add_dependency 'rspec-its' s.add_dependency 'addressable' s.add_dependency 'sinatra', '~> 1.4' s.add_dependency 'rack-test' s.add_dependency 'rake' s.add_dependency 'yard' s.add_dependency 'redcarpet' s.add_dependency 'simplecov' s.add_dependency 'coveralls' end