From ea498cb9c50f66dabedfdfb5c6a9d2abfef058c8 Mon Sep 17 00:00:00 2001 From: Benjamin ter Kuile Date: Tue, 22 Apr 2014 18:42:02 +0200 Subject: [PATCH] Add user product info --- Gemfile | 4 +- Gemfile.lock | 137 +- .../modal_info_controller.js.coffee | 5 + .../controllers/table_controller.js.coffee | 4 + .../user/app/models/product.js.coffee | 1 + .../controller_modifications.js.coffee | 12 +- .../user/app/templates/modal_info.emblem | 5 + .../user/app/templates/table.emblem | 14 +- .../user/flat/application.js.coffee.erb | 1 + .../user/foundation/forms.css.sass | 2 +- .../foundation_and_overrides.css.sass | 2 + .../user/foundation/menu_main.css.sass | 4 +- .../foundation/product_categories.css.sass | 49 +- .../suppliers/products_controller.rb | 2 +- .../users/application_controller.rb | 1 + app/models/product.rb | 1 + app/serializers/product_serializer.rb | 2 +- app/views/layouts/user/foundation.html.slim | 2 +- app/views/suppliers/products/_form.html.slim | 11 +- app/views/suppliers/sections/_form.html.slim | 6 +- config/locales/supplier.en.yml | 3 + config/locales/supplier.nl.yml | 3 + .../users/get_product_information.feature | 12 + spec/acceptance_steps/global_product_steps.rb | 6 + .../users/order_products_steps.rb | 5 + vendor/assets/textile-js/textile-js.js | 1285 +++++++++++++++++ 26 files changed, 1474 insertions(+), 105 deletions(-) create mode 100644 app/assets/javascripts/user/app/controllers/modal_info_controller.js.coffee create mode 100644 app/assets/javascripts/user/app/templates/modal_info.emblem create mode 100644 spec/acceptance/users/get_product_information.feature create mode 100644 spec/acceptance_steps/global_product_steps.rb create mode 100644 vendor/assets/textile-js/textile-js.js diff --git a/Gemfile b/Gemfile index 0f98dc19..79ee3faf 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source 'https://rubygems.org' -gem 'rails', '4.0.4' +gem 'rails', '4.1.0' gem 'rack-cors', :require => 'rack/cors' # Bundle edge Rails instead: @@ -18,7 +18,7 @@ gem 'slim-rails' # Gems used only for assets and not required # in production environments by default. group :assets do - gem 'sass-rails', '~> 4.0.2' + gem 'sass-rails' #, '~> 4.0.2' gem 'coffee-rails' #, '~> 3.2.1' #gem 'twitter-bootstrap-rails' gem 'bootstrap-sass', '~>2.3' diff --git a/Gemfile.lock b/Gemfile.lock index 1884f2e5..e7f9fd6b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -34,15 +34,9 @@ PATH specs: cmtool (0.5.0) bourbon - coffee-script - devise - devise_simply_stored email_validator haml-rails jquery-rails - paperclip - rails (>= 3.2.2) - sass-rails slim-rails tinymce-rails @@ -57,36 +51,37 @@ PATH GEM remote: https://rubygems.org/ specs: - actionmailer (4.0.4) - actionpack (= 4.0.4) + actionmailer (4.1.0) + actionpack (= 4.1.0) + actionview (= 4.1.0) mail (~> 2.5.4) - actionpack (4.0.4) - activesupport (= 4.0.4) - builder (~> 3.1.0) - erubis (~> 2.7.0) + actionpack (4.1.0) + actionview (= 4.1.0) + activesupport (= 4.1.0) rack (~> 1.5.2) rack-test (~> 0.6.2) + actionview (4.1.0) + activesupport (= 4.1.0) + builder (~> 3.1) + erubis (~> 2.7.0) active_decorator (0.3.4) active_model_serializers (0.8.1) activemodel (>= 3.0) - activemodel (4.0.4) - activesupport (= 4.0.4) - builder (~> 3.1.0) - activerecord (4.0.4) - activemodel (= 4.0.4) - activerecord-deprecated_finders (~> 1.0.2) - activesupport (= 4.0.4) - arel (~> 4.0.0) - activerecord-deprecated_finders (1.0.3) - activesupport (4.0.4) + activemodel (4.1.0) + activesupport (= 4.1.0) + builder (~> 3.1) + activerecord (4.1.0) + activemodel (= 4.1.0) + activesupport (= 4.1.0) + arel (~> 5.0.0) + activesupport (4.1.0) i18n (~> 0.6, >= 0.6.9) - minitest (~> 4.2) - multi_json (~> 1.3) + json (~> 1.7, >= 1.7.7) + minitest (~> 5.1) thread_safe (~> 0.1) - tzinfo (~> 0.3.37) - addressable (2.3.5) - arel (4.0.2) - atomic (1.1.16) + tzinfo (~> 1.1) + addressable (2.3.6) + arel (5.0.1.20140414130214) barber (0.4.2) ember-source execjs @@ -97,10 +92,10 @@ GEM bcrypt (3.1.7) bootstrap-sass (2.3.2.2) sass (~> 3.2) - bourbon (3.1.8) - sass (>= 3.2.0) + bourbon (3.2.0) + sass (~> 3.2) thor - builder (3.1.4) + builder (3.2.2) capybara (2.2.1) mime-types (>= 1.16) nokogiri (>= 1.3.3) @@ -113,7 +108,7 @@ GEM chunky_png (1.3.0) climate_control (0.0.3) activesupport (>= 3.0) - cocaine (0.5.3) + cocaine (0.5.4) climate_control (>= 0.0.3, < 1.0) coderay (1.1.0) coffee-rails (4.0.1) @@ -123,15 +118,15 @@ GEM coffee-script-source execjs coffee-script-source (1.7.0) - compass (0.12.4) + compass (0.12.6) chunky_png (~> 1.2) fssm (>= 0.2.7) - sass (~> 3.2.17) + sass (~> 3.2.19) compass-rails (1.1.6) compass (>= 0.12.2) connection_pool (1.2.0) cookiejar (0.3.2) - couchbase (1.3.6) + couchbase (1.3.7) connection_pool (~> 1.0, >= 1.0.0) multi_json (~> 1.0) yaji (~> 0.3, >= 0.3.2) @@ -174,12 +169,12 @@ GEM handlebars-source jquery-rails (>= 1.0.17) railties (>= 3.1) - ember-source (1.4.0) + ember-source (1.5.0) handlebars-source (~> 1.0) emblem-rails (0.2.1) barber-emblem (~> 0.1.1) ember-rails (>= 0.14.0) - emblem-source (0.3.12) + emblem-source (0.3.15) erubis (2.7.0) eventmachine (1.0.3) execjs (2.0.2) @@ -203,7 +198,7 @@ GEM websocket-driver (>= 0.3.1) font-awesome-rails (4.0.3.1) railties (>= 3.2, < 5.0) - foundation-rails (5.2.1.0) + foundation-rails (5.2.2.0) railties (>= 3.1.0) sass (>= 3.2.0) fssm (0.2.10) @@ -220,14 +215,14 @@ GEM haml (>= 3.1, < 5.0) railties (>= 4.0.1) handlebars-source (1.3.0) - hashie (2.0.5) + hashie (2.1.1) hike (1.2.3) http_parser.rb (0.6.0) i18n (0.6.9) jquery-rails (3.1.0) railties (>= 3.0, < 5.0) thor (>= 0.14, < 2.0) - jquery-ui-rails (4.2.0) + jquery-ui-rails (4.2.1) railties (>= 3.2.16) js-routes (0.9.7) railties (>= 3.2) @@ -250,8 +245,8 @@ GEM mime-types (1.25.1) mini_magick (3.7.0) subexec (~> 0.2.1) - mini_portile (0.5.2) - minitest (4.7.5) + mini_portile (0.5.3) + minitest (5.3.3) multi_json (1.9.2) multi_xml (0.5.5) multipart-post (2.0.0) @@ -291,20 +286,22 @@ GEM rack-cors (0.2.9) rack-test (0.6.2) rack (>= 1.0) - rails (4.0.4) - actionmailer (= 4.0.4) - actionpack (= 4.0.4) - activerecord (= 4.0.4) - activesupport (= 4.0.4) + rails (4.1.0) + actionmailer (= 4.1.0) + actionpack (= 4.1.0) + actionview (= 4.1.0) + activemodel (= 4.1.0) + activerecord (= 4.1.0) + activesupport (= 4.1.0) bundler (>= 1.3.0, < 2.0) - railties (= 4.0.4) - sprockets-rails (~> 2.0.0) - railties (4.0.4) - actionpack (= 4.0.4) - activesupport (= 4.0.4) + railties (= 4.1.0) + sprockets-rails (~> 2.0) + railties (4.1.0) + actionpack (= 4.1.0) + activesupport (= 4.1.0) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rake (10.2.2) + rake (10.3.1) rest-client (1.6.7) mime-types (>= 1.16) rqrcode (0.4.2) @@ -325,15 +322,15 @@ GEM rspec-expectations (~> 2.14.0) rspec-mocks (~> 2.14.0) ruby-progressbar (1.4.2) - sass (3.2.18) - sass-rails (4.0.2) + sass (3.2.19) + sass-rails (4.0.3) railties (>= 4.0.0, < 5.0) sass (~> 3.2.0) sprockets (~> 2.8, <= 2.11.0) - sprockets-rails (~> 2.0.0) - simple_form (3.0.1) - actionpack (>= 4.0.0, < 4.1) - activemodel (>= 4.0.0, < 4.1) + sprockets-rails (~> 2.0) + simple_form (3.0.2) + actionpack (~> 4.0) + activemodel (~> 4.0) simplecov (0.8.2) docile (~> 1.1.0) multi_json @@ -342,10 +339,10 @@ GEM slim (2.0.2) temple (~> 0.6.6) tilt (>= 1.3.3, < 2.1) - slim-rails (2.1.2) - actionpack (>= 3.0, < 4.1) - activesupport (>= 3.0, < 4.1) - railties (>= 3.0, < 4.1) + slim-rails (2.1.4) + actionpack (>= 3.0, < 4.2) + activesupport (>= 3.0, < 4.2) + railties (>= 3.0, < 4.2) slim (~> 2.0) slop (3.5.0) sprockets (2.11.0) @@ -353,7 +350,7 @@ GEM multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) - sprockets-rails (2.0.1) + sprockets-rails (2.1.3) actionpack (>= 3.0) activesupport (>= 3.0) sprockets (~> 2.8) @@ -363,9 +360,8 @@ GEM daemons (>= 1.0.9) eventmachine (>= 1.0.0) rack (>= 1.0.0) - thor (0.19.0) - thread_safe (0.3.1) - atomic (>= 1.1.7, < 2) + thor (0.19.1) + thread_safe (0.3.3) tilt (1.4.1) tinymce-rails (4.0.19) railties (>= 3.1.1) @@ -375,7 +371,8 @@ GEM turnip (1.2.1) gherkin (>= 2.5) rspec (>= 2.0, < 4.0) - tzinfo (0.3.39) + tzinfo (1.1.0) + thread_safe (~> 0.1) uglifier (2.5.0) execjs (>= 0.3.0) json (>= 1.8.0) @@ -427,10 +424,10 @@ DEPENDENCIES pry-rails quiet_assets rack-cors - rails (= 4.0.4) + rails (= 4.1.0) rqrcode rspec-rails - sass-rails (~> 4.0.2) + sass-rails simple_form simplecov simply_stored! diff --git a/app/assets/javascripts/user/app/controllers/modal_info_controller.js.coffee b/app/assets/javascripts/user/app/controllers/modal_info_controller.js.coffee new file mode 100644 index 00000000..99cdc654 --- /dev/null +++ b/app/assets/javascripts/user/app/controllers/modal_info_controller.js.coffee @@ -0,0 +1,5 @@ +App.ModalInfoController = Ember.ObjectController.extend + actions: + close: -> + @get('model.cancel').call(@) if @get('model.cancel') + @send 'closeModal' diff --git a/app/assets/javascripts/user/app/controllers/table_controller.js.coffee b/app/assets/javascripts/user/app/controllers/table_controller.js.coffee index e5801ccd..ea1b3c21 100644 --- a/app/assets/javascripts/user/app/controllers/table_controller.js.coffee +++ b/app/assets/javascripts/user/app/controllers/table_controller.js.coffee @@ -28,3 +28,7 @@ App.TableController = Ember.ObjectController.extend @set 'join_request_sent', true # keeps the button deactivated toggleProductCategory: (product_category)-> product_category.set 'collapsed', not product_category.get('collapsed') + showProductDescription: (product)-> + @send 'openModal', 'modal_info', Ember.Object.create + title: product.get('name') + body: textile(product.get('description')) diff --git a/app/assets/javascripts/user/app/models/product.js.coffee b/app/assets/javascripts/user/app/models/product.js.coffee index 7ec7b997..ef2f667c 100644 --- a/app/assets/javascripts/user/app/models/product.js.coffee +++ b/app/assets/javascripts/user/app/models/product.js.coffee @@ -2,5 +2,6 @@ attr = DS.attr App.Product = DS.Model.extend name: attr 'string' price: attr 'number' + description: attr 'string' product_category: DS.belongsTo('product_category') product_orders: DS.hasMany('product_order') diff --git a/app/assets/javascripts/user/app/modifications/controller_modifications.js.coffee b/app/assets/javascripts/user/app/modifications/controller_modifications.js.coffee index 2acd0c4c..a6201d4e 100644 --- a/app/assets/javascripts/user/app/modifications/controller_modifications.js.coffee +++ b/app/assets/javascripts/user/app/modifications/controller_modifications.js.coffee @@ -6,6 +6,11 @@ ControllerExtensions = Ember.Mixin.create if jqXHR.status == 401 App.__container__.lookup('route:application').unauthorized() handler + showModal: (options={})-> + $(document).foundation('reflow') # needed (stupid!!!) + @confirm_cancel = options.cancel + @set 'controllers.application.modal.title', options.title if options.title + @set 'controllers.application.modal.content', options.content if options.content Ember.ArrayController.reopen ControllerExtensions Ember.Controller.reopen needs: ['application'] @@ -47,13 +52,6 @@ Ember.Controller.reopen ##@set 'controllers.application.confirm.content', options.content if options.content ##$('#confirm-modal').foundation('reveal', 'open') # this kills the ember actions ##$('#confirm-modal').css('visibility', 'visible').show() - showModal: (options={})-> - #this.container.lookup('view:modal', {title:'Test title'}) - #debugger - $(document).foundation('reflow') # needed (stupid!!!) - @confirm_cancel = options.cancel - @set 'controllers.application.modal.title', options.title if options.title - @set 'controllers.application.modal.content', options.content if options.content #$('#confirm-modal').foundation('reveal', 'open') #this kills the ember actions #$('#confirm-modal').css('visibility', 'visible').show() diff --git a/app/assets/javascripts/user/app/templates/modal_info.emblem b/app/assets/javascripts/user/app/templates/modal_info.emblem new file mode 100644 index 00000000..7e55aa31 --- /dev/null +++ b/app/assets/javascripts/user/app/templates/modal_info.emblem @@ -0,0 +1,5 @@ +modal-dialog action="close" + h3.flush--top= title + p==body + hr + button{action 'close'}= t 'modal.info.close' diff --git a/app/assets/javascripts/user/app/templates/table.emblem b/app/assets/javascripts/user/app/templates/table.emblem index dc87099b..22f97794 100644 --- a/app/assets/javascripts/user/app/templates/table.emblem +++ b/app/assets/javascripts/user/app/templates/table.emblem @@ -25,9 +25,16 @@ unless product_category.collapsed ul.product_category-products each product in product_category.products - li + li class="order-product-#{unbound product.id}" + if product.description + button.show-product-description{action showProductDescription product} + span + else + span.no-product-description a{action addProduct product}= product.name - span.right.currency=currency product.price + button.add-product-to-list{action addProduct product} + span + span.product-price.currency=currency product.price .large-6.columns= render 'product_orders' else .large12 @@ -39,5 +46,8 @@ ul.product_category-products each product in product_category.products li + if product.description + button.show-product-description{action showProductDescription product} + span span= product.name span.right.currency=currency product.price diff --git a/app/assets/javascripts/user/flat/application.js.coffee.erb b/app/assets/javascripts/user/flat/application.js.coffee.erb index 44f72490..67968b1a 100644 --- a/app/assets/javascripts/user/flat/application.js.coffee.erb +++ b/app/assets/javascripts/user/flat/application.js.coffee.erb @@ -9,6 +9,7 @@ #= require translations #= require js-routes #= require_directory . +#= require textile-js #= require_self # # (($) -> diff --git a/app/assets/stylesheets/user/foundation/forms.css.sass b/app/assets/stylesheets/user/foundation/forms.css.sass index 1a286502..af2fb428 100644 --- a/app/assets/stylesheets/user/foundation/forms.css.sass +++ b/app/assets/stylesheets/user/foundation/forms.css.sass @@ -1,4 +1,4 @@ -@import foundation_and_overrides +@import ./foundation_and_overrides .form-row @extend .row diff --git a/app/assets/stylesheets/user/foundation/foundation_and_overrides.css.sass b/app/assets/stylesheets/user/foundation/foundation_and_overrides.css.sass index 537ef495..a348de51 100644 --- a/app/assets/stylesheets/user/foundation/foundation_and_overrides.css.sass +++ b/app/assets/stylesheets/user/foundation/foundation_and_overrides.css.sass @@ -13,6 +13,8 @@ // $rem-base: 16px; // Allows the use of rem-calc() or lower-bound() in your settings +a.unused-class + display: inline-block @import "foundation/functions" // $experimental: true; diff --git a/app/assets/stylesheets/user/foundation/menu_main.css.sass b/app/assets/stylesheets/user/foundation/menu_main.css.sass index 8842a7c9..4109d9db 100644 --- a/app/assets/stylesheets/user/foundation/menu_main.css.sass +++ b/app/assets/stylesheets/user/foundation/menu_main.css.sass @@ -8,8 +8,8 @@ header.top-menu background-position: left bottom, right bottom background-image: image-url('theme1/button-bar-left.png'), image-url('theme1/button-bar-right.png') color: $green - padding-left: 49px - padding-right: 52px + padding-left: 48px + padding-right: 52px .menu-content background-color: white background-repeat: repeat-x diff --git a/app/assets/stylesheets/user/foundation/product_categories.css.sass b/app/assets/stylesheets/user/foundation/product_categories.css.sass index e16dbe08..b8965992 100644 --- a/app/assets/stylesheets/user/foundation/product_categories.css.sass +++ b/app/assets/stylesheets/user/foundation/product_categories.css.sass @@ -1,12 +1,37 @@ -@import font-awesome -ul.product_category-products - list-style: none -.product_category-title - cursor: pointer - .icon - @extend .fa - @extend .fa-arrow-down - color: #ccc - padding-right: 10px - &.collapsed - @extend .fa-arrow-right +//@import ./foundation_and_overrides +//@import font-awesome +//ul.product_category-products + //list-style: none +//.product_category-title + //cursor: pointer + //.icon + //@extend .fa + //@extend .fa-arrow-down + //color: #ccc + //padding-right: 10px + //&.collapsed + //@extend .fa-arrow-right +//.product_category-products + //.product-price + //float: right + //.show-product-description + //+button($bg: $secondary-color) + //+button-icon-only + //margin-right: 7px + //span + //@extend .fa + //@extend .fa-info + //@extend .fa-lg + //.no-product-description + //// Empty space to match the product description for layout + //display: inline-block + //width: 1.65rem + //.add-product-to-list + //+button($bg: $secondary-color) + //+button-icon-only + //float: right + //margin-left: 5px + //span + //@extend .fa + //@extend .fa-plus + //@extend .fa-lg diff --git a/app/controllers/suppliers/products_controller.rb b/app/controllers/suppliers/products_controller.rb index 50dea67f..d892ebf2 100644 --- a/app/controllers/suppliers/products_controller.rb +++ b/app/controllers/suppliers/products_controller.rb @@ -95,7 +95,7 @@ module Suppliers private def product_params - params.require(:product).permit(:name, :code, :price, product_category_ids: []) + params.require(:product).permit(:name, :code, :price, :description, product_category_ids: []) end end end diff --git a/app/controllers/users/application_controller.rb b/app/controllers/users/application_controller.rb index c52adea9..b2aaee78 100644 --- a/app/controllers/users/application_controller.rb +++ b/app/controllers/users/application_controller.rb @@ -1,6 +1,7 @@ module Users class ApplicationController < ::ApplicationController before_action :user_authentication, :unless => ->(c){ %w(obtain_token).include?(c.action_name) || c.request.format.symbol == :html } # , except: [:obtain_token, :index] + private def user_authentication diff --git a/app/models/product.rb b/app/models/product.rb index 389e0e2c..482cb516 100644 --- a/app/models/product.rb +++ b/app/models/product.rb @@ -5,6 +5,7 @@ class Product property :name property :code property :price, type: Float + property :description #belongs_to :product_category has_and_belongs_to_many :product_categories, storing_keys: false diff --git a/app/serializers/product_serializer.rb b/app/serializers/product_serializer.rb index ac108684..5eb2bbaf 100644 --- a/app/serializers/product_serializer.rb +++ b/app/serializers/product_serializer.rb @@ -1,4 +1,4 @@ class ProductSerializer < Qwaiter::Serializer embed :ids, include: true - attributes :name, :price + attributes :name, :price, :description end diff --git a/app/views/layouts/user/foundation.html.slim b/app/views/layouts/user/foundation.html.slim index add14897..e77fddf0 100644 --- a/app/views/layouts/user/foundation.html.slim +++ b/app/views/layouts/user/foundation.html.slim @@ -7,7 +7,7 @@ html lang="en" title Qwaiter = stylesheet_link_tag "user/foundation/application" = javascript_include_tag "vendor/modernizr" - /= javascript_include_tag 'http://connect.facebook.net/en_US/all.js' + = javascript_include_tag 'http://connect.facebook.net/en_US/all.js' = javascript_include_tag "user/flat/application" - if ENV['QWAITER_MOBILE_EXPORT'] == 'yes' javascript: diff --git a/app/views/suppliers/products/_form.html.slim b/app/views/suppliers/products/_form.html.slim index 62e5f50f..8488374a 100644 --- a/app/views/suppliers/products/_form.html.slim +++ b/app/views/suppliers/products/_form.html.slim @@ -1,20 +1,25 @@ = form_for [:suppliers, @product] do |f| = render 'error_messages', target: @product - .form-row + .form-row class=(f.object.errors[:name].any? ? 'error' : nil) .form-label = f.label :name, data: {t: 'attributes.product.name'} .form-field = f.text_field :name - .form-row + .form-row class=(f.object.errors[:code].any? ? 'error' : nil) .form-label = f.label :code, data: {t: 'attributes.product.code'} .form-field = f.text_field :code - .form-row + .form-row class=(f.object.errors[:price].any? ? 'error' : nil) .form-label = f.label :price, data: {t: 'attributes.product.price'} .form-field = f.text_field :price + .form-row class=(f.object.errors[:description].any? ? 'error' : nil) + .form-label + = f.label :description, data: {t: 'attributes.product.description'} + .form-field + = f.text_area :description /= f.input :name /= f.input :code /= f.input :price diff --git a/app/views/suppliers/sections/_form.html.slim b/app/views/suppliers/sections/_form.html.slim index bd9342c4..c456e397 100644 --- a/app/views/suppliers/sections/_form.html.slim +++ b/app/views/suppliers/sections/_form.html.slim @@ -1,17 +1,17 @@ -# DEPRICATED = form_for [:suppliers, @section], html: {class: 'form-horizontal' } do |f| = render 'error_messages', target: @section - .form-row class=(@section.errors[:title].any? ? 'error' : nil) + .form-row class=(f.object.errors[:title].any? ? 'error' : nil) .form-label = f.label :title .form-field = f.text_field :title - .form-row class=(@section.errors[:width].any? ? 'error' : nil) + .form-row class=(f.object.errors[:width].any? ? 'error' : nil) .form-label = f.label :width .form-field = f.number_field :width - .form-row class=(@section.errors[:height].any? ? 'error' : nil) + .form-row class=(f.object.errors[:height].any? ? 'error' : nil) .form-label = f.label :height .form-field diff --git a/config/locales/supplier.en.yml b/config/locales/supplier.en.yml index 4d57d84b..de21cbac 100644 --- a/config/locales/supplier.en.yml +++ b/config/locales/supplier.en.yml @@ -96,3 +96,6 @@ en: closed: 'Closed' datepicker: no_date: 'Pick a date' + modal: + info: + close: OK diff --git a/config/locales/supplier.nl.yml b/config/locales/supplier.nl.yml index b2bf7a4e..96779321 100644 --- a/config/locales/supplier.nl.yml +++ b/config/locales/supplier.nl.yml @@ -96,3 +96,6 @@ nl: closed: 'Afgesloten' datepicker: no_date: 'Selecteer een datum' + modal: + info: + close: OK diff --git a/spec/acceptance/users/get_product_information.feature b/spec/acceptance/users/get_product_information.feature new file mode 100644 index 00000000..a4658f59 --- /dev/null +++ b/spec/acceptance/users/get_product_information.feature @@ -0,0 +1,12 @@ +Feature: Getting product information during an order + + @javascript + Scenario: Happy flow + Given There is an open supplier with a menu + And the product 'Heineken beer' has description 'Brewed in Amsterdam' + And I am signed in as a user + #And I open the debugger + And I am on the user homepage + #When the user scans a table QR code + #And the user clicks on the more info button for the product with name 'Heineken beer' + #Then the page should have content 'Brewed in Amsterdam' diff --git a/spec/acceptance_steps/global_product_steps.rb b/spec/acceptance_steps/global_product_steps.rb new file mode 100644 index 00000000..e3915779 --- /dev/null +++ b/spec/acceptance_steps/global_product_steps.rb @@ -0,0 +1,6 @@ +step "the product :product_name has description :product_description" do |product_name, product_description| + product = @product && @product.name == product_name ? @product : Product.find_by_name(product_name) + product.description = product_description + product.save or raise "Cannot save product: #{product.errors.full_messages.to_sentence}" + @product ||= product +end diff --git a/spec/acceptance_steps/users/order_products_steps.rb b/spec/acceptance_steps/users/order_products_steps.rb index 9b1671aa..3ed49aa0 100644 --- a/spec/acceptance_steps/users/order_products_steps.rb +++ b/spec/acceptance_steps/users/order_products_steps.rb @@ -27,3 +27,8 @@ step "there is a signed in user with an active order" do step "I am signed in as a user" step "the user has an active order" end + +step "the user clicks on the more info button for the product with name :product_name" do |product_name| + product = @product && @product.name == product_name ? @product : Product.find_by_name(product_name) + find(".order-product-#{product.id} .show-product-description").click +end diff --git a/vendor/assets/textile-js/textile-js.js b/vendor/assets/textile-js/textile-js.js new file mode 100644 index 00000000..e759da57 --- /dev/null +++ b/vendor/assets/textile-js/textile-js.js @@ -0,0 +1,1285 @@ +/*** + * Textile parser for JavaScript + * + * Copyright (c) 2012 Borgar Þorsteinsson (MIT License). + * + */ +/*jshint + laxcomma:true + laxbreak:true + eqnull:true + loopfunc:true + sub:true +*/ +;(function(){ +"use strict"; + + /*** + * Regular Expression helper methods + * + * This provides the `re` object, which contains several helper + * methods for working with big regular expressions (soup). + * + */ + var re = { + _cache: {} + , pattern: { + 'punct': "[!-/:-@\\[\\\\\\]-`{-~]" + , 'space': '\\s' + } + , escape: function ( src ) { + return src.replace( /[\-\[\]\{\}\(\)\*\+\?\.\,\\\^\$\|\#\s]/g, "\\$&" ); + } + , collapse: function ( src ) { + return src.replace( /(?:#.*?(?:\n|$))/g, '' ) + .replace( /\s+/g, '' ) + ; + } + , expand_patterns: function ( src ) { + // TODO: provide escape for patterns: \[:pattern:] ? + return src.replace( /\[\:\s*(\w+)\s*\:\]/g, function ( m, k ) { + return ( k in re.pattern ) + ? re.expand_patterns( re.pattern[ k ] ) + : k + ; + }) + ; + } + , isRegExp: function ( r ) { + return Object.prototype.toString.call( r ) === "[object RegExp]"; + } + , compile: function ( src, flags ) { + if ( re.isRegExp( src ) ) { + if ( arguments.length === 1 ) { // no flags arg provided, use the RegExp one + flags = ( src.global ? 'g' : '' ) + + ( src.ignoreCase ? 'i' : '' ) + + ( src.multiline ? 'm' : '' ); + } + src = src.source; + } + // don't do the same thing twice + var ckey = src + ( flags || '' ); + if ( ckey in re._cache ) { return re._cache[ ckey ]; } + // allow classes + var rx = re.expand_patterns( src ); + // allow verbose expressions + if ( flags && /x/.test( flags ) ) { + rx = re.collapse( rx ); + } + // allow dotall expressions + if ( flags && /s/.test( flags ) ) { + rx = rx.replace( /([^\\])\./g, '$1[^\\0]' ); + } + // TODO: test if MSIE and add replace \s with [\s\u00a0] if it is? + // clean flags and output new regexp + flags = ( flags || '' ).replace( /[^gim]/g, '' ); + return ( re._cache[ ckey ] = new RegExp( rx, flags ) ); + } + }; + + + + + /*** + * JSONML helper methods - http://www.jsonml.org/ + * + * This provides the `JSONML` object, which contains helper + * methods for rendering JSONML to HTML. + * + * Note that the tag ! is taken to mean comment, this is however + * not specified in the JSONML spec. + * + */ + var JSONML = { + escape: function ( text, esc_quotes ) { + return text.replace( /&(?!(#\d{2,}|#x[\da-fA-F]{2,}|[a-zA-Z][a-zA-Z1-4]{1,6});)/g, "&" ) + .replace( //g, ">" ) + .replace( /"/g, esc_quotes ? """ : '"' ) + .replace( /'/g, esc_quotes ? "'" : "'" ) + ; + } + , toHTML: function ( jsonml ) { + + jsonml = jsonml.concat(); + + // basic case + if ( typeof jsonml === "string" ) { + return JSONML.escape( jsonml ); + } + + var tag = jsonml.shift() + , attributes = {} + , content = [] + , tag_attrs = "" + , a + ; + if ( jsonml.length && typeof jsonml[ 0 ] === "object" && !_isArray( jsonml[ 0 ] ) ) { + attributes = jsonml.shift(); + } + + while ( jsonml.length ) { + content.push( JSONML.toHTML( jsonml.shift() ) ); + } + + for ( a in attributes ) { + tag_attrs += ( attributes[ a ] == null ) + ? " " + a + : " " + a + '="' + JSONML.escape( attributes[ a ], true ) + '"' + ; + } + + // be careful about adding whitespace here for inline elements + if ( tag == "!" ) { + return ""; + } + else if ( tag === "img" || tag === "br" || tag === "hr" || tag === "input" ) { + return "<" + tag + tag_attrs + " />"; + } + else { + return "<" + tag + tag_attrs + ">" + content.join( "" ) + ""; + } + } + }; + + + // merge object b properties into obect a + function merge ( a, b ) { + for ( var k in b ) { + a[ k ] = b[ k ]; + } + return a; + } + + + var _isArray = Array.isArray || function ( a ) { return Object.prototype.toString.call(a) === '[object Array]'; }; + + /* expressions */ + re.pattern[ 'blocks' ] = '(?:b[qc]|div|notextile|pre|h[1-6]|fn\\d+|p|###)'; + re.pattern[ 'pba_class' ] = '\\([^\\)]+\\)'; + re.pattern[ 'pba_style' ] = '\\{[^\\}]+\\}'; + re.pattern[ 'pba_lang' ] = '\\[[^\\[\\]]+\\]'; + re.pattern[ 'pba_align' ] = '(?:<>|<|>|=)'; + re.pattern[ 'pba_pad' ] = '[\\(\\)]+'; + re.pattern[ 'pba_attr' ] = '(?:[:pba_class:]|[:pba_style:]|[:pba_lang:]|[:pba_align:]|[:pba_pad:])*'; + re.pattern[ 'url_punct' ] = '[.,«»″‹›!?]'; + re.pattern[ 'html_id' ] = '[a-zA-Z][a-zA-Z\\d:]*'; + re.pattern[ 'html_attr' ] = '(?:"[^"]+"|\'[^\']+\'|[^>\\s]+)'; + re.pattern[ 'tx_urlch' ] = '[\\w"$\\-_.+!*\'(),";\\/?:@=&%#{}|\\\\^~\\[\\]`]'; + re.pattern[ 'tx_cite' ] = ':((?:[^\\s()]|\\([^\\s()]+\\)|[()])+?)(?=[!-\\.:-@\\[\\\\\\]-`{-~]+(?:$|\\s)|$|\\s)'; + re.pattern[ 'ucaps' ] = "A-Z"+ + // Latin extended À-Þ + "\u00c0-\u00d6\u00d8-\u00de"+ + // Latin caps with embelishments and ligatures... + "\u0100\u0102\u0104\u0106\u0108\u010a\u010c\u010e\u0110\u0112\u0114\u0116\u0118\u011a\u011c\u011e\u0120\u0122\u0124\u0126\u0128\u012a\u012c\u012e\u0130\u0132\u0134\u0136\u0139\u013b\u013d\u013f"+ + "\u0141\u0143\u0145\u0147\u014a\u014c\u014e\u0150\u0152\u0154\u0156\u0158\u015a\u015c\u015e\u0160\u0162\u0164\u0166\u0168\u016a\u016c\u016e\u0170\u0172\u0174\u0176\u0178\u0179\u017b\u017d"+ + "\u0181\u0182\u0184\u0186\u0187\u0189-\u018b\u018e-\u0191\u0193\u0194\u0196-\u0198\u019c\u019d\u019f\u01a0\u01a2\u01a4\u01a6\u01a7\u01a9\u01ac\u01ae\u01af\u01b1-\u01b3\u01b5\u01b7\u01b8\u01bc"+ + "\u01c4\u01c7\u01ca\u01cd\u01cf\u01d1\u01d3\u01d5\u01d7\u01d9\u01db\u01de\u01e0\u01e2\u01e4\u01e6\u01e8\u01ea\u01ec\u01ee\u01f1\u01f4\u01f6-\u01f8\u01fa\u01fc\u01fe"+ + "\u0200\u0202\u0204\u0206\u0208\u020a\u020c\u020e\u0210\u0212\u0214\u0216\u0218\u021a\u021c\u021e\u0220\u0222\u0224\u0226\u0228\u022a\u022c\u022e\u0230\u0232\u023a\u023b\u023d\u023e"+ + "\u0241\u0243-\u0246\u0248\u024a\u024c\u024e"+ + "\u1e00\u1e02\u1e04\u1e06\u1e08\u1e0a\u1e0c\u1e0e\u1e10\u1e12\u1e14\u1e16\u1e18\u1e1a\u1e1c\u1e1e\u1e20\u1e22\u1e24\u1e26\u1e28\u1e2a\u1e2c\u1e2e\u1e30\u1e32\u1e34\u1e36\u1e38\u1e3a\u1e3c\u1e3e\u1e40"+ + "\u1e42\u1e44\u1e46\u1e48\u1e4a\u1e4c\u1e4e\u1e50\u1e52\u1e54\u1e56\u1e58\u1e5a\u1e5c\u1e5e\u1e60\u1e62\u1e64\u1e66\u1e68\u1e6a\u1e6c\u1e6e\u1e70\u1e72\u1e74\u1e76\u1e78\u1e7a\u1e7c\u1e7e"+ + "\u1e80\u1e82\u1e84\u1e86\u1e88\u1e8a\u1e8c\u1e8e\u1e90\u1e92\u1e94\u1e9e\u1ea0\u1ea2\u1ea4\u1ea6\u1ea8\u1eaa\u1eac\u1eae\u1eb0\u1eb2\u1eb4\u1eb6\u1eb8\u1eba\u1ebc\u1ebe"+ + "\u1ec0\u1ec2\u1ec4\u1ec6\u1ec8\u1eca\u1ecc\u1ece\u1ed0\u1ed2\u1ed4\u1ed6\u1ed8\u1eda\u1edc\u1ede\u1ee0\u1ee2\u1ee4\u1ee6\u1ee8\u1eea\u1eec\u1eee\u1ef0\u1ef2\u1ef4\u1ef6\u1ef8\u1efa\u1efc\u1efe"+ + "\u2c60\u2c62-\u2c64\u2c67\u2c69\u2c6b\u2c6d-\u2c70\u2c72\u2c75\u2c7e\u2c7f"+ + "\ua722\ua724\ua726\ua728\ua72a\ua72c\ua72e\ua732\ua734\ua736\ua738\ua73a\ua73c\ua73e"+ + "\ua740\ua742\ua744\ua746\ua748\ua74a\ua74c\ua74e\ua750\ua752\ua754\ua756\ua758\ua75a\ua75c\ua75e\ua760\ua762\ua764\ua766\ua768\ua76a\ua76c\ua76e\ua779\ua77b\ua77d\ua77e"+ + "\ua780\ua782\ua784\ua786\ua78b\ua78d\ua790\ua792\ua7a0\ua7a2\ua7a4\ua7a6\ua7a8\ua7aa"; + + var re_block = re.compile( /^([:blocks:])/ ) + , re_block_se = re.compile( /^[:blocks:]$/ ) + , re_block_normal = re.compile( /^(.*?)($|\n(?:\s*\n|$)+)/, 's' ) + , re_block_extended = re.compile( /^(.*?)($|\n+(?=[:blocks:][:pba_attr:]\.))/, 's' ) + , re_ruler = /^(\-\-\-+|\*\*\*+|___+)(\n\s+|$)/ + , re_list = re.compile( /^((?:[\t ]*[\#\*]+[:pba_attr:] .+?(?:\n|$))+)(\s*\n)?/ ) + , re_list_item = /^([\#\*]+)(.+?)(\n|$)/ + , re_table = re.compile( /^((?:table[:pba_attr:]\.\n)?(?:(?:[:pba_attr:]\.[^\n\S]*)?\|.*?\|[^\n\S]*(?:\n|$))+)([^\n\S]*\n)?/, 's' ) + , re_table_head = /^table(_?)([^\n]+)\.\s?\n/ + , re_table_row = re.compile( /^([:pba_attr:]\.[^\n\S]*)?\|(.*?)\|[^\n\S]*(\n|$)/, 's' ) + , re_fenced_phrase = /^\[(__?|\*\*?|\?\?|[\-\+\^~@%])([^\n]+)\1\]/ + , re_phrase = /^([\[\{]?)(__?|\*\*?|\?\?|[\-\+\^~@%])/ + , re_text = re.compile( /^.+?(?=[\\(\n*)/ ) + , re_html_tag = re.compile( /^<([:html_id:])((?:\s[^=\s\/]+(?:\s*=\s*[:html_attr:])?)+)?\s*(\/?)>(\n*)/ ) + , re_html_comment = re.compile( /^/, 's' ) + , re_html_end_tag = re.compile( /^<\/([:html_id:])([^>]*)>/ ) + , re_html_attr = re.compile( /^\s*([^=\s]+)(?:\s*=\s*("[^"]+"|'[^']+'|[^>\s]+))?/ ) + , re_entity = /&(#\d\d{2,}|#x[\da-fA-F]{2,}|[a-zA-Z][a-zA-Z1-4]{1,6});/ + + // glyphs + , re_dimsign = /([\d\.,]+['"]? ?)x( ?)(?=[\d\.,]['"]?)/g + , re_emdash = /(^|[\s\w])--([\s\w]|$)/g + , re_trademark = /(\b ?|\s|^)(?:\((?:TM|tm)\)|\[(?:TM|tm)\])/g + , re_registered = /(\b ?|\s|^)(?:\(R\)|\[R\])/gi + , re_copyright = /(\b ?|\s|^)(?:\(C\)|\[C\])/gi + , re_apostrophe = /(\w)\'(\w)/g + , re_double_prime = re.compile( /(\d*[\.,]?\d+)"(?=\s|$|[:punct:])/g ) + , re_single_prime = re.compile( /(\d*[\.,]?\d+)'(?=\s|$|[:punct:])/g ) + , re_closing_dquote = re.compile( /([^\s\[\(])"(?=$|\s|[:punct:])/g ) + , re_closing_squote = re.compile( /([^\s\[\(])'(?=$|\s|[:punct:])/g ) + + // pba + , re_pba_classid = /^\(([^\(\)\n]+)\)/ + , re_pba_padding_l = /^([\(]+)/ + , re_pba_padding_r = /^([\)]+)/ + , re_pba_align_blk = /^(<>|<|>|=)/ + , re_pba_align_img = /^(<|>|=)/ + , re_pba_valign = /^(~|\^|\-)/ + , re_pba_colspan = /^\\(\d+)/ + , re_pba_rowspan = /^\/(\d+)/ + , re_pba_styles = /^\{([^\}]*)\}/ + , re_pba_css = /^\s*([^:\s]+)\s*:\s*(.+)\s*$/ + , re_pba_lang = /^\[([^\[\]]+)\]/ + ; + + var phrase_convert = { + '*': 'strong' + , '**': 'b' + , '??': 'cite' + , '_': 'em' + , '__': 'i' + , '-': 'del' + , '%': 'span' + , '+': 'ins' + , '~': 'sub' + , '^': 'sup' + , '@': 'code' + }; + + // area, base, basefont, bgsound, br, col, command, embed, frame, hr, + // img, input, keygen, link, meta, param, source, track or wbr + var html_singletons = { + 'br': 1 + , 'hr': 1 + , 'img': 1 + , 'link': 1 + , 'meta': 1 + , 'wbr': 1 + , 'area': 1 + , 'param': 1 + , 'input': 1 + , 'option': 1 + , 'base': 1 + }; + + var pba_align_lookup = { + '<': 'left' + , '=': 'center' + , '>': 'right' + , '<>': 'justify' + }; + + var pba_valign_lookup = { + '~':'bottom' + , '^':'top' + , '-':'middle' + }; + + // HTML tags allowed in the document (root) level that trigger HTML parsing + var allowed_blocktags = { + 'p': 0 + , 'hr': 0 + , 'ul': 1 + , 'ol': 0 + , 'li': 0 + , 'div': 1 + , 'pre': 0 + , 'object': 1 + , 'script': 0 + , 'noscript': 0 + , 'blockquote': 1 + , 'notextile': 1 + }; + + + function ribbon ( feed ) { + var _slot = null + , org = feed + '' + , pos = 0 + ; + return { + save: function () { + _slot = pos; + } + , load: function () { + pos = _slot; + feed = org.slice( pos ); + } + , advance: function ( n ) { + pos += ( typeof n === 'string' ) ? n.length : n; + return ( feed = org.slice( pos ) ); + } + , lookbehind: function ( nchars ) { + nchars = nchars == null ? 1 : nchars; + return org.slice( pos - nchars, pos ); + } + , startsWith: function ( s ) { + return feed.substring(0, s.length) === s; + } + , valueOf: function(){ + return feed; + } + , toString: function(){ + return feed; + } + }; + } + + + function builder ( arr ) { + var _arr = _isArray( arr ) ? arr : []; + return { + add: function ( node ) { + if ( typeof node === 'string' && + typeof _arr[_arr.length - 1 ] === 'string' ) { + // join if possible + _arr[ _arr.length - 1 ] += node; + } + else if ( _isArray( node ) ) { + var f = node.filter(function(s){ return s !== undefined; }); + _arr.push( f ); + } + else if ( node ) { + _arr.push( node ); + } + return this; + } + , merge: function ( s ) { + for (var i=0,l=s.length; i

- user can still create nonsensical but "well-formed" markup + function parse_html ( src, whitelist_tags ) { + var org = src + '' + , list = [] + , root = list + , _stack = [] + , m + , oktag = whitelist_tags ? function ( tag ) { return tag in whitelist_tags; } : function () { return true; } + , tag + ; + src = (typeof src === 'string') ? ribbon( src ) : src; + // loop + do { + + if ( (m = re_html_comment.exec( src )) && oktag('!') ) { + src.advance( m[0] ); + list.push( [ '!', m[1] ] ); + } + + // end tag + else if ( (m = re_html_end_tag.exec( src )) && oktag(m[1]) ) { + tag = m[1]; + var junk = m[2]; + if ( _stack.length ) { + for (var i=_stack.length-1; i>=0; i--) { + var head = _stack[i]; + if ( head[0] === tag ) { + _stack.splice( i ); + list = _stack[ _stack.length - 1 ] || root; + break; + } + } + } + src.advance( m[0] ); + } + + // open/void tag + else if ( (m = re_html_tag.exec( src )) && oktag(m[1]) ) { + src.advance( m[0] ); + tag = m[1]; + var single = m[3] || m[1] in html_singletons + , tail = m[4] + , element = [ tag ] + ; + + // attributes + if ( m[2] ) { element.push( parse_html_attr( m[2] ) ); } + + // tag + if ( single ) { // single tag + // let us add the element and continue our quest... + list.push( element ); + if ( tail ) { list.push( tail ); } + } + else { // open tag + if ( tail ) { element.push( tail ); } + + // TODO: some things auto close other things: ,

  • ,

    , + // if ( tag === 'p' && _stack.length ) { + // var seek = /^(p)$/; + // for (var i=_stack.length-1; i>=0; i--) { + // var head = _stack[i]; + // if ( seek.test( head[0] ) /* === tag */ ) { + // //src.advance( m[0] ); + // _stack.splice( i ); + // list = _stack[i] || root; + // } + // } + // } + + // TODO: some elements can move parser into "text" mode + // style, xmp, iframe, noembed, noframe, textarea, title, script, noscript, plaintext + //if ( /^(script)$/.test( tag ) ) { } + + _stack.push( element ); + list.push( element ); + list = element; + + } + } + else { + + // no match, move by all "uninteresting" chars + m = /([^<]+|[^\0])/.exec( src ); + if ( m ) { + list.push( m[0] ); + } + src.advance( m ? m[0].length || 1 : 1 ); + + } + + } + while ( src.valueOf() ); + return root; + } + + /* attribute parser */ + + function parse_attr ( input, element, end_token ) { + /* + The attr bit causes massive problems for span elements when parens are used. + Parens are a total mess and, unsurprisingly, causes trip ups: + + RC: `_{display:block}(span) span (span)_` -> `(span) span (span)` + PHP: `_{display:block}(span) span (span)_` -> `(span) span (span)` + + PHP and RC seem to mostly solve this by not parsing a final attr parens on spans if the + following character is a non-space. I've duplicated that: Class/ID is not matched on spans + if it is followed by `end_token` or . + */ + input += ''; + if ( !input || element === 'notextile' ) { return undefined; } + + var m + , st = {} + , o = { 'style': st } + , remaining = input + , is_block = element === 'table' || element === 'td' || re_block_se.test( element ) // "in" test would be better but what about fn#.? + , is_img = element === 'img' + , is_phrase = !is_block && !is_img && element !== 'a' + , re_pba_align = ( is_img ) ? re_pba_align_img : re_pba_align_blk + ; + + do { + + if ( (m = re_pba_styles.exec( remaining )) ) { + m[1].split(';').forEach(function(p){ + var d = p.match( re_pba_css ); + if ( d ) { st[ d[1] ] = d[2]; } + }); + remaining = remaining.slice( m[0].length ); + continue; + } + + if ( (m = re_pba_lang.exec( remaining )) ) { + o['lang'] = m[1]; + remaining = remaining.slice( m[0].length ); + continue; + } + + if ( (m = re_pba_classid.exec( remaining )) ) { + var rm = remaining.slice( m[0].length ); + if ( + ( !rm && is_phrase ) || + ( end_token && (rm[0] === ' ' || end_token === rm.slice(0,end_token.length)) ) + ) { + m = null; + continue; + } + var bits = m[1].split( '#' ); + if ( bits[0] ) { o['class'] = bits[0]; } + if ( bits[1] ) { o['id'] = bits[1]; } + remaining = rm; + continue; + } + + if ( is_block ) { + if ( (m = re_pba_padding_l.exec( remaining )) ) { + st[ "padding-left" ] = ( m[1].length ) + "em"; + remaining = remaining.slice( m[0].length ); + continue; + } + if ( (m = re_pba_padding_r.exec( remaining )) ) { + st[ "padding-right" ] = ( m[1].length ) + "em"; + remaining = remaining.slice( m[0].length ); + continue; + } + } + + // only for blocks: + if ( is_img || is_block ) { + if ( (m = re_pba_align.exec( remaining )) ) { + var align = pba_align_lookup[ m[1] ]; + if ( is_img ) { + o[ 'align' ] = align; + } + else { + st[ 'text-align' ] = align; + } + remaining = remaining.slice( m[0].length ); + continue; + } + } + + // only for table cells + if ( element === 'td' || element === 'tr' ) { + if ( (m = re_pba_valign.exec( remaining )) ) { + st[ "vertical-align" ] = pba_valign_lookup[ m[1] ]; + remaining = remaining.slice( m[0].length ); + continue; + } + } + if ( element === 'td' ) { + if ( (m = re_pba_colspan.exec( remaining )) ) { + o[ "colspan" ] = m[1]; + remaining = remaining.slice( m[0].length ); + continue; + } + if ( (m = re_pba_rowspan.exec( remaining )) ) { + o[ "rowspan" ] = m[1]; + remaining = remaining.slice( m[0].length ); + continue; + } + } + + } + while ( m ); + + // collapse styles + var s = []; + for ( var v in st ) { s.push( v + ':' + st[v] ); } + if ( s.length ) { o.style = s.join(';'); } else { delete o.style; } + + return remaining == input + ? undefined + : [ input.length - remaining.length, o ] + ; + } + + + + /* glyph parser */ + + function parse_glyphs ( src ) { + if ( typeof src !== 'string' ) { return src; } + // NB: order is important here ... + return src + // arrow + .replace( /([^\-]|^)->/, '$1→' ) // arrow + // dimensions + .replace( re_dimsign, '$1×$2' ) // dimension sign + // ellipsis + .replace( /([^.]?)\.{3}/g, '$1…' ) // ellipsis + // dashes + .replace( re_emdash, '$1—$2' ) // em dash + .replace( /( )-( )/g, '$1–$2' ) // en dash + // legal marks + .replace( re_trademark, '$1™' ) // trademark + .replace( re_registered, '$1®' ) // registered + .replace( re_copyright, '$1©' ) // copyright + // double quotes + .replace( re_double_prime, '$1″' ) // double prime + .replace( re_closing_dquote, '$1”' ) // double closing quote + .replace( /"/g, '“' ) // double opening quote + // single quotes + .replace( re_single_prime, '$1′' ) // single prime + .replace( re_apostrophe, '$1’$2' ) // I'm an apostrophe + .replace( re_closing_squote, '$1’' ) // single closing quote + .replace( /'/g, '‘' ) + ; + } + + + /* list parser */ + + function parse_list ( src, options ) { + + src = ribbon( src.replace( /(^|\n)[\t ]+/, '$1' ) ); + var pad = function ( n ) { + var s = '\n'; + while ( n-- ) { s += '\t'; } + return s; + } + , stack = [] + , m + , s + ; + + while ( (m = re_list_item.exec( src )) ) { + + var item = [ 'li' ] + , pba = parse_attr( m[2], 'li' ) + ; + if ( pba ) { + m[2] = m[2].slice( pba[0] ); + pba = pba[1]; + } + + var dest_level = m[1].length + , type = m[1].substr(-1) === '#' ? 'ol' : 'ul' + , eqlev = stack.length === dest_level + , new_li = null + , lst + , par + , r + ; + // create nesting until we have correct level + while ( stack.length < dest_level ) { + lst = [ type, pad( stack.length + 1 ), (new_li = [ 'li' ]) ]; + par = stack[ stack.length - 1 ]; + if ( par ) { + par.li.push( pad( stack.length ) ); + par.li.push( lst ); + } + stack.push({ ul: lst, li: new_li }); + } + // remove nesting until we have correct level + while ( stack.length > dest_level ) { + r = stack.pop(); + r.ul.push( pad( stack.length ) ); + } + par = stack[ stack.length - 1 ]; + if ( !new_li ) { + par.ul.push( pad( stack.length ), item ); + par.li = item; + } + if ( pba ) { par.li.push( pba ); } + Array.prototype.push.apply( par.li, parse_inline( m[2].trim(), options ) ); + + src.advance( m[0] ); + } + + while ( stack.length ) { + s = stack.pop(); + s.ul.push( pad( stack.length ) ); + } + + return s.ul; + } + + + + /* table parser */ + + function parse_table ( src, options ) { + src = ribbon( src.trim() ); + var table = [ 'table' ] + , row + , inner + , pba + , more + , m + ; + + if ( (m = re_table_head.exec( src )) ) { + // parse and apply table attr + src.advance( m[0] ); + pba = parse_attr( m[2], 'table' ); + if ( pba ) { + table.push( pba[1] ); + } + } + + while ( (m = re_table_row.exec( src )) ) { + row = [ 'tr' ]; + + if ( m[1] && (pba = parse_attr( m[1], 'tr' )) ) { + // FIXME: requires "\.\s?" -- else what ? + row.push( pba[1] ); + } + + table.push( '\n\t', row ); + inner = ribbon( m[2] ); + + do { + inner.save(); + + // cell loop + var th = inner.startsWith( '_' ) + , cell = [ th ? 'th' : 'td' ] + ; + if ( th ) { + inner.advance( 1 ); + } + + pba = parse_attr( inner, 'td' ); + if ( pba ) { + inner.advance( pba[0] ); + cell.push( pba[1] ); // FIXME: don't do this if next text fails + } + + if ( pba || th ) { + var d = /^\.\s*/.exec( inner ); + if ( d ) { + inner.advance( d[0] ); + } + else { + cell = [ 'td' ]; + inner.load(); + } + } + + var mx = /^(==.*?==|[^\|])*/.exec( inner ); + cell = cell.concat( parse_inline( mx[0], options ) ); + row.push( '\n\t\t', cell ); + more = inner.valueOf().charAt( mx[0].length ) === '|'; + inner.advance( mx[0].length + 1 ); + + } + while ( more ); + + row.push( '\n\t' ); + + src.advance( m[0] ); + } + table.push( '\n' ); + return table; + + } + + + /* inline parser */ + + function parse_inline ( src, options ) { + + src = ribbon( src ); + var list = builder() + , m + , pba + ; + + // loop + do { + src.save(); + + // linebreak -- having this first keeps it from messing to much with other phrases + if ( src.startsWith( '\n' ) ) { + src.advance( 1 ); + + if ( options.breaks ) { + list.add( [ 'br' ] ); + } + list.add( '\n' ); + continue; + } + + // inline notextile + if ( (m = /^==(.*?)==/.exec( src )) ) { + src.advance( m[0] ); + list.add( m[1] ); + continue; + } + + // lookbehind => /([\s>.,"'?!;:])$/ + var behind = src.lookbehind( 1 ); + var boundary = !behind || /^[\s>.,"'?!;:()]$/.test( behind ); + // FIXME: need to test right boundary for phrases as well + if ( (m = re_phrase.exec( src )) && ( boundary || m[1] ) ) { + src.advance( m[0] ); + var tok = m[2] + , fence = m[1] + , phrase_type = phrase_convert[ tok ] + , code = phrase_type === 'code' + ; + if ( (pba = !code && parse_attr( src, phrase_type, tok )) ) { + src.advance( pba[0] ); + pba = pba[1]; + } + // FIXME: if we can't match the fence on the end, we should output fence-prefix as normal text + // seek end + var m_mid; + var m_end; + if ( fence === '[' ) { + m_mid = '^(.*?)'; + m_end = '(?:])'; + } + else if ( fence === '{' ) { + m_mid = '^(.*?)'; + m_end = '(?:})'; + } + else { + var t1 = re.escape( tok.charAt(0) ); + m_mid = ( code ) + ? '^(\\S+|\\S+.*?\\S)' + : '^([^\\s' + t1 + ']+|[^\\s' + t1 + '].*?\\S('+t1+'*))' + ; + m_end = '(?=$|[\\s.,"\'!?;:()«»„“”‚‘’])'; + } + var rx = re.compile( m_mid + '(' + re.escape( tok ) + ')' + m_end ); + if ( (m = rx.exec( src )) && m[1] ) { + src.advance( m[0] ); + if ( code ) { + list.add( [ phrase_type, m[1] ] ); + } + else { + list.add( [ phrase_type, pba ].concat( parse_inline( m[1], options ) ) ); + } + continue; + } + // else + src.load(); + } + + // image + if ( (m = re_image.exec( src )) || (m = re_image_fenced.exec( src )) ) { + src.advance( m[0] ); + + pba = m[1] && parse_attr( m[1], 'img' ); + var attr = pba ? pba[1] : { 'src':'' } + , img = [ 'img', attr ] + ; + attr.src = m[2]; + attr.alt = m[3] ? ( attr.title = m[3] ) : ''; + + if ( m[4] ) { // +cite causes image to be wraped with a link (or link_ref)? + // TODO: support link_ref for image cite + img = [ 'a', { 'href': m[4] }, img ]; + } + list.add( img ); + continue; + } + + // html comment + if ( (m = re_html_comment.exec( src )) ) { + src.advance( m[0] ); + list.add( [ '!', m[1] ] ); + continue; + } + // html tag + // TODO: this seems to have a lot of overlap with block tags... DRY? + if ( (m = re_html_tag.exec( src )) ) { + src.advance( m[0] ); + var tag = m[1] + , single = m[3] || m[1] in html_singletons + , element = [ tag ] + , tail = m[4] + ; + if ( m[2] ) { + element.push( parse_html_attr( m[2] ) ); + } + if ( single ) { // single tag + list.add( element ).add( tail ); + continue; + } + else { // need terminator + // gulp up the rest of this block... + var re_end_tag = re.compile( "^(.*?)()", 's' ); + if ( (m = re_end_tag.exec( src )) ) { + src.advance( m[0] ); + if ( tag === 'code' ) { + element.push( tail, m[1] ); + } + else if ( tag === 'notextile' ) { + list.merge( parse_inline( m[1], options ) ); + continue; + } + else { + element = element.concat( parse_inline( m[1], options ) ); + } + list.add( element ); + continue; + } + // end tag is missing, treat tag as normal text... + } + src.load(); + } + + // footnote + if ( (m = re_footnote.exec( src )) ) { + src.advance( m[0] ); + list.add( [ 'sup', { 'class': 'footnote', 'id': 'fnr' + m[1] }, + [ 'a', { href: '#fn' + m[1] }, m[1] ] + ] ); + continue; + } + + // caps / abbr + if ( (m = re_caps.exec( src )) ) { + src.advance( m[0] ); + var caps = [ 'span', { 'class': 'caps' }, m[1] ]; + if ( m[2] ) { + caps = [ 'acronym', { 'title': m[2] }, caps ]; // FIXME: use , not acronym! + } + list.add( caps ); + continue; + } + + // links + if ( (boundary && (m = re_link.exec( src ))) || (m = re_link_fenced.exec( src )) ) { + src.advance( m[0].length ); + var title = m[1].match( re_link_title ) + , inner = ( title ) ? m[1].slice( 0, m[1].length - title[0].length ) : m[1] + ; + if ( (pba = parse_attr( inner, 'a' )) ) { + inner = inner.slice( pba[0] ); + pba = pba[1]; + } + else { + pba = {}; + } + if ( title && !inner ) { inner = title[0]; title = ""; } + pba.href = m[2]; + if ( title ) { pba.title = title[1]; } + list.add( [ 'a', pba ].concat( parse_inline( inner.replace( /^(\.?\s*)/, '' ), options ) ) ); + continue; + } + + // no match, move by all "uninteresting" chars + m = /([a-zA-Z0-9,.':]+|\s+|[^\0])/.exec( src ); + if ( m ) { + list.add( m[0] ); + } + src.advance( m ? m[0].length || 1 : 1 ); + + } + while ( src.valueOf() ); + + return list.get().map( parse_glyphs ); + } + + + /* block parser */ + + function parse_blocks ( src, options ) { + + var list = builder() + , paragraph = function ( s, tag, pba, linebreak ) { + tag = tag || 'p'; + var out = []; + s.split( /\n\n+/ ).forEach(function( bit, i ) { + if ( tag === 'p' && /^\s/.test( bit ) ) { + // no-paragraphs + // WTF?: Why does Textile not allow linebreaks in spaced lines + bit = bit.replace( /\n[\t ]/g, ' ' ).trim(); + out = out.concat( parse_inline( bit, options ) ); + } + else { + if ( linebreak && i ) { out.push( linebreak ); } + out.push( pba ? [ tag, pba ].concat( parse_inline( bit, options ) ) + : [ tag ].concat( parse_inline( bit, options ) ) ); + } + }); + return out; + } + , link_refs = {} + , m + ; + src = ribbon( src.replace( /^( *\n)+/, '' ) ); + + // loop + while ( src.valueOf() ) { + src.save(); + + // link_ref -- this goes first because it shouldn't trigger a linebreak + if ( (m = re_link_ref.exec( src )) ) { + src.advance( m[0] ); + link_refs[ m[1] ] = m[2]; + continue; + } + + // add linebreak + list.linebreak(); + + // named block + if ( (m = re_block.exec( src )) ) { + src.advance( m[0] ); + var block_type = m[0] + , pba = parse_attr( src, block_type ) + ; + if ( pba ) { + src.advance( pba[0] ); + pba = pba[1]; + } + if ( (m = /\.(\.?)(?:\s|(?=:))/.exec( src )) ) { + // FIXME: this whole copy_pba seems rather strange? + // slurp rest of block + var extended = !!m[1]; + m = ( extended ? re_block_extended : re_block_normal ).exec( src.advance( m[0] ) ); + src.advance( m[0] ); + // bq | bc | notextile | pre | h# | fn# | p | ### + if ( block_type === 'bq' ) { + var cite, inner = m[1]; + if ( (m = /^:(\S+)\s+/.exec( inner )) ) { + if ( !pba ) { pba = {}; } + pba.cite = m[1]; + inner = inner.slice( m[0].length ); + } + // RedCloth adds all attr to both: this is bad because it produces duplicate IDs + list.add( [ 'blockquote', pba, '\n' ].concat( + paragraph( inner, 'p', copy_pba(pba, { 'cite':1, 'id':1 }), '\n' ) + ).concat(['\n']) ); + } + else if ( block_type === 'bc' ) { + var sub_pba = ( pba ) ? copy_pba(pba, { 'id':1 }) : null; + list.add( [ 'pre', pba, ( sub_pba ? [ 'code', sub_pba, m[1] ] : [ 'code', m[1] ] ) ] ); + } + else if ( block_type === 'notextile' ) { + list.merge( parse_html( m[1] ) ); + } + else if ( block_type === '###' ) { + // ignore the insides + } + else if ( block_type === 'pre' ) { + // I disagree with RedCloth, but agree with PHP here: + // "pre(foo#bar).. line1\n\nline2" prevents multiline preformat blocks + // ...which seems like the whole point of having an extended pre block? + list.add( [ 'pre', pba, m[1] ] ); + } + else if ( re_footnote_def.test( block_type ) ) { // footnote + // Need to be careful: RedCloth fails "fn1(foo#m). footnote" -- it confuses the ID + var fnid = block_type.replace( /\D+/g, '' ); + if ( !pba ) { pba = {}; } + pba['class'] = ( pba['class'] ? pba['class'] + ' ' : '' ) + 'footnote'; + pba['id'] = 'fn' + fnid; + list.add( [ "p", pba, [ 'a', { 'href': '#fnr' + fnid }, [ 'sup', fnid ] ], ' ' ].concat( parse_inline( m[1], options ) ) ); + } + else { // heading | paragraph + list.merge( paragraph( m[1], block_type, pba, '\n' ) ); + } + continue; + } + else { + src.load(); + } + } + + // HTML comment + if ( (m = re_html_comment.exec( src )) ) { + src.advance( m[0] + (/(?:\s*\n+)+/.exec( src ) || [])[0] ); + list.add( [ '!', m[1] ] ); + continue; + } + + // block HTML + if ( (m = re_html_tag_block.exec( src )) ) { + var tag = m[1] + , single = m[3] || tag in html_singletons + , tail = m[4] + ; + // Unsurprisingly, all Textile implementations I have tested have trouble parsing simple HTML: + // + // "
    a\n
    b\n
    c\n
    d" + // + // I simply match them here as there is no way anyone is using nested HTML today, or if they + // are, then this will at least output less broken HTML as redundant tags will get quoted. + + // Is block tag? ... + if ( tag in allowed_blocktags ) { + src.advance( m[0] ); + + var element = [ tag ]; + + if ( m[2] ) { + element.push( parse_html_attr( m[2] ) ); + } + + if ( single ) { // single tag + // let us add the element and continue our quest... + list.add( element ); + continue; + } + else { // block + + // gulp up the rest of this block... + var re_end_tag = re.compile( "^(.*?)(\\s*)()(\\s*)", 's' ); + if ( (m = re_end_tag.exec( src )) ) { + src.advance( m[0] ); + if ( tag === 'pre' ) { + element.push( tail ); + element = element.concat( parse_html( m[1].replace( /\n+$/, '' ), { 'code': 1 } ) ); + if ( m[2] ) { element.push( m[2] ); } + list.add( element ); + } + else if ( tag === 'notextile' ) { + element = parse_html( m[1].trim() ); + list.merge( element ); + } + else if ( tag === 'script' || tag === 'noscript' ) { + //element = parse_html( m[1].trim() ); + element.push( tail + m[1] ); + list.add( element ); + } + else { + // These strange (and unnecessary) linebreak tests are here to get the + // tests working perfectly. In reality, this doesn't matter one bit. + if ( /\n/.test( tail ) ) { element.push( '\n' ); } + if ( /\n/.test( m[1] ) ) { + element = element.concat( parse_blocks( m[1], options ) ); + } + else { + element = element.concat( parse_inline( m[1].replace( /^ +/, '' ), options ) ); + } + if ( /\n/.test( m[2] ) ) { element.push( '\n' ); } + + list.add( element ); + } + continue; + } + /*else { + // end tag is missing, treat tag as normal text... + }*/ + } + } + src.load(); + } + + // ruler + if ( (m = re_ruler.exec( src )) ) { + src.advance( m[0] ); + list.add( [ 'hr' ] ); + continue; + } + + // list + if ( (m = re_list.exec( src )) ) { + src.advance( m[0] ); + list.add( parse_list( m[0], options ) ); + continue; + } + + // table + if ( (m = re_table.exec( src )) ) { + src.advance( m[0] ); + list.add( parse_table( m[1], options ) ); + continue; + } + + // paragraph + m = re_block_normal.exec( src ); + list.merge( paragraph( m[1], 'p', undefined, "\n" ) ); + src.advance( m[0] ); + + } + + return list.get().map( fix_links, link_refs ); + } + + + // recurse the tree and swap out any "href" attributes + function fix_links ( jsonml ) { + if ( _isArray( jsonml ) ) { + if ( jsonml[0] === 'a' ) { // found a link + var attr = jsonml[1]; + if ( typeof attr === "object" && 'href' in attr && attr.href in this ) { + attr.href = this[ attr.href ]; + } + } + for (var i=1,l=jsonml.length; i by default + }; + textile.setOptions = textile.setoptions = function ( opt ) { + merge( textile.defaults, opt ); + return this; + }; + + + textile.parse = textile.convert = textile; + textile.html_parser = parse_html; + textile.jsonml = function ( txt, opt ) { + // get a throw-away copy of options + opt = merge( merge( {}, textile.defaults ), opt || {} ); + // parse and return tree + return [ 'html' ].concat( parse_blocks( txt, opt ) ); + }; + textile.serialize = JSONML.toHTML; + + if ( typeof module !== 'undefined' && module.exports ) { + module.exports = textile; + } + else { + this.textile = textile; + } + + +}).call(function() { + return this || (typeof window !== 'undefined' ? window : global); +}());