├── spec ├── dummy │ ├── public │ │ ├── favicon.ico │ │ ├── robots.txt │ │ ├── 422.html │ │ ├── 404.html │ │ └── 500.html │ ├── .rvmrc │ ├── app │ │ ├── views │ │ │ ├── layouts │ │ │ │ ├── sample.en.erb │ │ │ │ ├── sample.ru.erb │ │ │ │ ├── articles.html.erb │ │ │ │ └── application.html.erb │ │ │ ├── shared │ │ │ │ └── _first.html.erb │ │ │ └── articles │ │ │ │ └── show.html.erb │ │ ├── helpers │ │ │ └── application_helper.rb │ │ ├── assets │ │ │ ├── images │ │ │ │ └── rails.png │ │ │ ├── stylesheets │ │ │ │ └── application.css │ │ │ └── javascripts │ │ │ │ └── application.js │ │ ├── controllers │ │ │ ├── admin │ │ │ │ └── articles_controller.rb │ │ │ └── application_controller.rb │ │ └── models │ │ │ └── article.rb │ ├── config │ │ ├── initializers │ │ │ ├── puffer_pages.rb │ │ │ ├── mime_types.rb │ │ │ ├── backtrace_silencers.rb │ │ │ ├── session_store.rb │ │ │ ├── secret_token.rb │ │ │ ├── wrap_parameters.rb │ │ │ └── inflections.rb │ │ ├── environment.rb │ │ ├── boot.rb │ │ ├── locales │ │ │ └── en.yml │ │ ├── database.yml │ │ ├── environments │ │ │ ├── development.rb │ │ │ ├── pg_test.rb │ │ │ ├── test.rb │ │ │ └── production.rb │ │ ├── routes.rb │ │ └── application.rb │ ├── config.ru │ ├── db │ │ ├── migrate │ │ │ ├── 20111117081813_create_articles.rb │ │ │ ├── 20130118071519_add_translations.rb │ │ │ ├── 20130118071518_add_locales_to_pages.rb │ │ │ ├── 20130118071517_add_handler_to_page_parts.rb │ │ │ ├── 20130118071513_create_layouts.rb │ │ │ ├── 20130118071514_create_snippets.rb │ │ │ ├── 20130118071515_create_origins.rb │ │ │ ├── 20130118071512_create_page_parts.rb │ │ │ ├── 20130118071511_create_pages.rb │ │ │ └── 20130118071516_migrate_to_uuid.rb │ │ └── seeds.rb │ ├── Rakefile │ └── script │ │ └── rails ├── fabricators │ ├── articles_fabricator.rb │ ├── origin_fabricator.rb │ ├── snippets_fabricator.rb │ ├── page_parts_fabricator.rb │ ├── pages_fabricator.rb │ └── layouts_fabricator.rb ├── data │ ├── broken.json │ ├── import.json │ ├── unlocalized.json │ └── localized.json ├── lib │ ├── handlers │ │ ├── base_spec.rb │ │ └── yaml_spec.rb │ ├── liquid │ │ ├── tags │ │ │ ├── scope_spec.rb │ │ │ ├── image_spec.rb │ │ │ ├── partials_spec.rb │ │ │ ├── include_spec.rb │ │ │ └── cache_spec.rb │ │ ├── backend_spec.rb │ │ ├── interpolation_spec.rb │ │ └── tags_spec.rb │ ├── core_spec.rb │ ├── handlers_spec.rb │ ├── page_drop_spec.rb │ ├── rspec │ │ └── matchers │ │ │ └── render_page_spec.rb │ └── pagenator_spec.rb ├── models │ └── puffer_pages │ │ ├── page_part_spec.rb │ │ ├── layout_spec.rb │ │ ├── snippet_spec.rb │ │ ├── localable_spec.rb │ │ └── renderable_spec.rb ├── requests │ └── origins_requests_spec.rb ├── controllers │ └── pages_controller_spec.rb └── spec_helper.rb ├── .rvmrc ├── lib ├── puffer_pages │ ├── version.rb │ ├── rspec.rb │ ├── backends │ │ ├── controllers │ │ │ ├── layouts_base.rb │ │ │ ├── snippets_base.rb │ │ │ ├── origins_base.rb │ │ │ └── pages_base.rb │ │ └── models │ │ │ ├── layout.rb │ │ │ ├── snippet.rb │ │ │ ├── mixins │ │ │ ├── importable.rb │ │ │ ├── localable.rb │ │ │ └── translatable.rb │ │ │ ├── origin.rb │ │ │ └── page_part.rb │ ├── liquid │ │ ├── tags │ │ │ ├── helper.rb │ │ │ ├── javascript.rb │ │ │ ├── partials.rb │ │ │ ├── scope.rb │ │ │ ├── yield.rb │ │ │ ├── include.rb │ │ │ ├── render.rb │ │ │ ├── super.rb │ │ │ ├── image.rb │ │ │ ├── array.rb │ │ │ ├── assets.rb │ │ │ ├── url.rb │ │ │ ├── translate.rb │ │ │ └── cache.rb │ │ ├── backend.rb │ │ ├── tracker.rb │ │ ├── page_drop.rb │ │ └── file_system.rb │ ├── helpers.rb │ ├── handlers │ │ ├── base.rb │ │ └── yaml.rb │ ├── extensions │ │ ├── core.rb │ │ ├── renderer.rb │ │ ├── context.rb │ │ └── pagenator.rb │ ├── rspec │ │ ├── view_rendering.rb │ │ ├── matchers.rb │ │ └── matchers │ │ │ └── render_page.rb │ ├── backends.rb │ ├── globalize │ │ └── migrator.rb │ ├── renderer.rb │ ├── engine.rb │ ├── handlers.rb │ ├── log_subscriber.rb │ └── migrations.rb └── puffer_pages.rb ├── .rspec ├── app ├── components │ ├── codemirror_component.rb │ ├── page_parts_component.rb │ ├── handlers_component.rb │ ├── render │ │ └── _tree_page.html.erb │ ├── handlers │ │ └── form.html.erb │ ├── page_parts │ │ ├── _page_part.html.erb │ │ └── form.html.erb │ └── codemirror │ │ └── form.html.erb ├── models │ └── puffer_pages │ │ ├── page.rb │ │ ├── origin.rb │ │ ├── layout.rb │ │ ├── snippet.rb │ │ └── page_part.rb ├── controllers │ ├── admin │ │ ├── pages_controller.rb │ │ ├── layouts_controller.rb │ │ ├── origins_controller.rb │ │ └── snippets_controller.rb │ └── pages_controller.rb ├── assets │ ├── stylesheets │ │ └── puffer │ │ │ ├── codemirror │ │ │ ├── ambiance-mobile.css │ │ │ ├── neat.css │ │ │ ├── elegant.css │ │ │ ├── cobalt.css │ │ │ ├── eclipse.css │ │ │ ├── night.css │ │ │ ├── monokai.css │ │ │ ├── erlang-dark.css │ │ │ ├── blackboard.css │ │ │ ├── rubyblue.css │ │ │ ├── vibrant-ink.css │ │ │ ├── twilight.css │ │ │ ├── lesser-dark.css │ │ │ └── xq-dark.css │ │ │ └── puffer_pages.css │ └── javascripts │ │ └── puffer │ │ ├── liquid.js │ │ ├── codemirror │ │ ├── yaml.js │ │ └── htmlmixed.js │ │ ├── matchbrackets.js │ │ └── multiplex.js └── helpers │ └── puffer_pages_helper.rb ├── config ├── routes.rb └── locales │ └── en.yml ├── gemfiles ├── Gemfile.rails-3-1 ├── Gemfile.rails-3-2 └── Gemfile.rails-head ├── .gitignore ├── db └── migrate │ ├── 20130118064524_add_locales_to_pages.rb │ ├── 20130110144030_add_handler_to_page_parts.rb │ ├── 20090506102004_create_layouts.rb │ ├── 20090510121824_create_snippets.rb │ ├── 20120812100913_create_origins.rb │ ├── 20090504132337_create_page_parts.rb │ ├── 20090422092419_create_pages.rb │ └── 20120924120226_migrate_to_uuid.rb ├── Gemfile ├── .travis.yml ├── Rakefile ├── MIT-LICENSE ├── Guardfile ├── puffer_pages.gemspec └── README.md /spec/dummy/public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rvmrc: -------------------------------------------------------------------------------- 1 | rvm use 1.9.3@puffer_pages --create 2 | -------------------------------------------------------------------------------- /spec/dummy/.rvmrc: -------------------------------------------------------------------------------- 1 | rvm use 1.9.3@puffer_pages --create 2 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/sample.en.erb: -------------------------------------------------------------------------------- 1 | sample.en.erb -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/sample.ru.erb: -------------------------------------------------------------------------------- 1 | sample.ru.erb -------------------------------------------------------------------------------- /spec/dummy/app/views/shared/_first.html.erb: -------------------------------------------------------------------------------- 1 | shared/first content -------------------------------------------------------------------------------- /spec/dummy/app/views/articles/show.html.erb: -------------------------------------------------------------------------------- 1 | <%= @article.title %> -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/articles.html.erb: -------------------------------------------------------------------------------- 1 | app - <%= yield %> - app -------------------------------------------------------------------------------- /lib/puffer_pages/version.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | VERSION = "0.5.1" 3 | end -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | #--format documentation 3 | --backtrace 4 | --order random 5 | -------------------------------------------------------------------------------- /spec/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/components/codemirror_component.rb: -------------------------------------------------------------------------------- 1 | class CodemirrorComponent < BaseComponent 2 | end 3 | -------------------------------------------------------------------------------- /app/models/puffer_pages/page.rb: -------------------------------------------------------------------------------- 1 | class PufferPages::Page < PufferPages::Backends::Page 2 | end 3 | -------------------------------------------------------------------------------- /app/models/puffer_pages/origin.rb: -------------------------------------------------------------------------------- 1 | class PufferPages::Origin < PufferPages::Backends::Origin 2 | end 3 | -------------------------------------------------------------------------------- /app/controllers/admin/pages_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::PagesController < PufferPages::PagesBase 2 | unloadable 3 | end 4 | -------------------------------------------------------------------------------- /app/models/puffer_pages/layout.rb: -------------------------------------------------------------------------------- 1 | class PufferPages::Layout < PufferPages::Backends::Layout 2 | translatable :body 3 | end 4 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | PufferPages::Engine.routes.draw do 2 | match '(*path)' => 'pages#index', :as => 'puffer_page' 3 | end 4 | -------------------------------------------------------------------------------- /app/controllers/admin/layouts_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::LayoutsController < PufferPages::LayoutsBase 2 | unloadable 3 | end 4 | -------------------------------------------------------------------------------- /app/controllers/admin/origins_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::OriginsController < PufferPages::OriginsBase 2 | unloadable 3 | end 4 | -------------------------------------------------------------------------------- /app/controllers/admin/snippets_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::SnippetsController < PufferPages::SnippetsBase 2 | unloadable 3 | end 4 | -------------------------------------------------------------------------------- /app/models/puffer_pages/snippet.rb: -------------------------------------------------------------------------------- 1 | class PufferPages::Snippet < PufferPages::Backends::Snippet 2 | translatable :body 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/images/rails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puffer/puffer_pages/HEAD/spec/dummy/app/assets/images/rails.png -------------------------------------------------------------------------------- /app/models/puffer_pages/page_part.rb: -------------------------------------------------------------------------------- 1 | class PufferPages::PagePart < PufferPages::Backends::PagePart 2 | translatable :body 3 | end 4 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | puffer_pages: 3 | inherited_layout: inherited (%{name}) 4 | tree_page: 5 | view: view 6 | -------------------------------------------------------------------------------- /app/components/page_parts_component.rb: -------------------------------------------------------------------------------- 1 | class PagePartsComponent < Puffer::Component::Base 2 | def form 3 | render 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/puffer_pages.rb: -------------------------------------------------------------------------------- 1 | PufferPages.setup do |config| 2 | config.localize = ENV['LOCALIZE'] != 'false' 3 | config.access_token = 'token' 4 | end 5 | -------------------------------------------------------------------------------- /app/components/handlers_component.rb: -------------------------------------------------------------------------------- 1 | class HandlersComponent < SelectComponent 2 | 3 | private 4 | 5 | def select_options 6 | PufferPages::Handlers.select 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails-3-1: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec path: '../' 4 | 5 | gem 'puffer', github: 'puffer/puffer' 6 | gem 'rails', '~> 3.1.0' 7 | gem 'activeuuid' 8 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails-3-2: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec path: '../' 4 | 5 | gem 'puffer', github: 'puffer/puffer' 6 | gem 'rails', '~> 3.2.0' 7 | gem 'activeuuid' 8 | -------------------------------------------------------------------------------- /spec/fabricators/articles_fabricator.rb: -------------------------------------------------------------------------------- 1 | Fabricator(:article) do 2 | title { Forgery::LoremIpsum.word(random: true) } 3 | body { Forgery::LoremIpsum.sentence(random: true) } 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Dummy::Application 5 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails-head: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec path: '../' 4 | 5 | gem 'puffer', github: 'puffer/puffer' 6 | gem 'rails', github: 'rails/rails' 7 | gem 'activeuuid' 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | pkg/ 4 | spec/dummy/db/*.sqlite3 5 | spec/dummy/log/*.log 6 | spec/dummy/tmp/ 7 | *.orig 8 | .idea 9 | tmp/ 10 | Gemfile.lock 11 | .DS_Store 12 | .rbx/ 13 | -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | Dummy::Application.initialize! 6 | -------------------------------------------------------------------------------- /app/assets/stylesheets/puffer/codemirror/ambiance-mobile.css: -------------------------------------------------------------------------------- 1 | .cm-s-ambiance.CodeMirror { 2 | -webkit-box-shadow: none; 3 | -moz-box-shadow: none; 4 | -o-box-shadow: none; 5 | box-shadow: none; 6 | } 7 | -------------------------------------------------------------------------------- /spec/fabricators/origin_fabricator.rb: -------------------------------------------------------------------------------- 1 | Fabricator(:origin, class_name: :'puffer_pages/origin') do 2 | name { sequence(:name) { |i| "origin_#{i}" } } 3 | host { 'http://localhost:3000' } 4 | token { SecureRandom.hex } 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | # Set up gems listed in the Gemfile. 4 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) 5 | 6 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) 7 | -------------------------------------------------------------------------------- /spec/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-Agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /spec/data/broken.json: -------------------------------------------------------------------------------- 1 | { 2 | "layouts": [{ 3 | "created_at": "2013-12-23T16:23:00Z", 4 | "id": "404DF5860F01492EB470711FE3BE4535", 5 | "name": "application", 6 | "strange_attribute": "Haha, I'm strange!" 7 | }] 8 | } -------------------------------------------------------------------------------- /db/migrate/20130118064524_add_locales_to_pages.rb: -------------------------------------------------------------------------------- 1 | class AddLocalesToPages < ActiveRecord::Migration 2 | def self.up 3 | add_column :pages, :locales, :text 4 | end 5 | 6 | def self.down 7 | remove_column :pages, :locales 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /app/controllers/pages_controller.rb: -------------------------------------------------------------------------------- 1 | class PagesController < ApplicationController 2 | unloadable 3 | 4 | def index 5 | page = PufferPages::Page.find_page(request.path_info) 6 | render puffer_page: page, content_type: page.try(:content_type) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/components/render/_tree_page.html.erb: -------------------------------------------------------------------------------- 1 | <%= tree_page.name %> — <%= "/" + tree_page.location.to_s %> 2 | <%= tree_page.status %> 3 | <%= link_to t('.view'), puffer_pages.puffer_page_path(tree_page.to_location), :target => '_blank' if tree_page.status.published? %> 4 | -------------------------------------------------------------------------------- /db/migrate/20130110144030_add_handler_to_page_parts.rb: -------------------------------------------------------------------------------- 1 | class AddHandlerToPageParts < ActiveRecord::Migration 2 | def self.up 3 | add_column :page_parts, :handler, :string 4 | end 5 | 6 | def self.down 7 | remove_column :page_parts, :handler 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/puffer_pages/rspec.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Rspec 3 | 4 | end 5 | end 6 | 7 | RSpec.configure do |config| 8 | config.include PufferPages::Rspec::Matchers 9 | end 10 | 11 | RSpec::Rails::ControllerExampleGroup.send :include, PufferPages::Rspec::ViewRendering 12 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20111117081813_create_articles.rb: -------------------------------------------------------------------------------- 1 | class CreateArticles < ActiveRecord::Migration 2 | def change 3 | create_table :articles do |t| 4 | t.string :title 5 | t.string :slug 6 | t.text :body 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20130118071519_add_translations.rb: -------------------------------------------------------------------------------- 1 | class AddTranslations < ActiveRecord::Migration 2 | def self.up 3 | PufferPages::Migrations.create_translation_tables! 4 | end 5 | 6 | def self.down 7 | PufferPages::Migrations.drop_translation_table! 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | # Add your own tasks in files placed in lib/tasks ending in .rake, 3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 4 | 5 | require File.expand_path('../config/application', __FILE__) 6 | 7 | Dummy::Application.load_tasks 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem 'puffer', github: 'puffer/puffer' 6 | gem 'rails' 7 | 8 | group :test do 9 | gem 'guard' 10 | gem 'guard-rspec' 11 | gem 'rb-inotify', require: false 12 | gem 'rb-fsevent', require: false 13 | gem 'rb-fchange', require: false 14 | end 15 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= stylesheet_link_tag "application" %> 6 | <%= javascript_include_tag "application" %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/dummy/script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../../config/application', __FILE__) 5 | require File.expand_path('../../config/boot', __FILE__) 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /app/components/handlers/form.html.erb: -------------------------------------------------------------------------------- 1 | <%= component_fields_for @record do |f| %> 2 |
3 | <%= f.label field %><%= f.select field, @options, { include_blank: false }, field.input_options %> 4 |
5 | <%= @record.errors[field.name.to_sym].first %> 6 |
7 |
8 | <% end %> -------------------------------------------------------------------------------- /spec/dummy/app/controllers/admin/articles_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::ArticlesController < Puffer::Base 2 | 3 | setup do 4 | group :articles 5 | end 6 | 7 | index do 8 | field :title 9 | field :slug 10 | field :body 11 | end 12 | 13 | form do 14 | field :title 15 | field :body 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20130118071518_add_locales_to_pages.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from puffer_pages (originally 20130118064524) 2 | class AddLocalesToPages < ActiveRecord::Migration 3 | def self.up 4 | add_column :pages, :locales, :text 5 | end 6 | 7 | def self.down 8 | remove_column :pages, :locales 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 1.9.3 3 | - 2.0.0 4 | - rbx-19mode 5 | 6 | gemfile: 7 | - Gemfile 8 | - gemfiles/Gemfile.rails-3-1 9 | - gemfiles/Gemfile.rails-3-2 10 | - gemfiles/Gemfile.rails-head 11 | 12 | env: 13 | - LOCALIZE=true 14 | - LOCALIZE=false 15 | 16 | matrix: 17 | allow_failures: 18 | - gemfile: gemfiles/Gemfile.rails-head -------------------------------------------------------------------------------- /db/migrate/20090506102004_create_layouts.rb: -------------------------------------------------------------------------------- 1 | class CreateLayouts < ActiveRecord::Migration 2 | def self.up 3 | create_table :layouts do |t| 4 | t.string :name 5 | t.text :body 6 | 7 | t.timestamps 8 | end 9 | 10 | add_index :layouts, :name 11 | end 12 | 13 | def self.down 14 | drop_table :layouts 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/dummy/app/models/article.rb: -------------------------------------------------------------------------------- 1 | class Article < ActiveRecord::Base 2 | 3 | validates :title, :presence => true 4 | validates :slug, :presence => true, :uniqueness => true 5 | 6 | before_validation :sluggify 7 | 8 | def sluggify 9 | self.slug = title.parameterize 10 | end 11 | 12 | def to_param 13 | slug 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20130118071517_add_handler_to_page_parts.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from puffer_pages (originally 20130110144030) 2 | class AddHandlerToPageParts < ActiveRecord::Migration 3 | def self.up 4 | add_column :page_parts, :handler, :string 5 | end 6 | 7 | def self.down 8 | remove_column :page_parts, :handler 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/lib/handlers/base_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe PufferPages::Handlers::Base do 4 | subject { described_class.new(:html) } 5 | let(:page_part) { Fabricate :main, body: 'just {{type}}' } 6 | 7 | context do 8 | specify { subject.process(page_part, drops: { type: 'plain html' }).should == 'just plain html' } 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20090510121824_create_snippets.rb: -------------------------------------------------------------------------------- 1 | class CreateSnippets < ActiveRecord::Migration 2 | def self.up 3 | create_table :snippets do |t| 4 | t.string :name 5 | t.text :body 6 | 7 | t.timestamps 8 | end 9 | 10 | add_index :snippets, :name 11 | end 12 | 13 | def self.down 14 | drop_table :snippets 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/puffer_pages/backends/controllers/layouts_base.rb: -------------------------------------------------------------------------------- 1 | class PufferPages::LayoutsBase < Puffer::Base 2 | setup do 3 | group :pages 4 | model_name :'puffer_pages/layout' 5 | end 6 | 7 | index do 8 | field :name 9 | end 10 | 11 | form do 12 | field :name 13 | field :body, type: :codemirror, mode: 'text/x-liquid-html' 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20120812100913_create_origins.rb: -------------------------------------------------------------------------------- 1 | class CreateOrigins < ActiveRecord::Migration 2 | def self.up 3 | create_table :origins do |t| 4 | t.string :name 5 | t.string :host 6 | t.string :path 7 | t.string :token 8 | 9 | t.timestamps 10 | end 11 | end 12 | 13 | def self.down 14 | drop_table :origins 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/puffer_pages/backends/controllers/snippets_base.rb: -------------------------------------------------------------------------------- 1 | class PufferPages::SnippetsBase < Puffer::Base 2 | setup do 3 | group :pages 4 | model_name :'puffer_pages/snippet' 5 | end 6 | 7 | index do 8 | field :name 9 | end 10 | 11 | form do 12 | field :name 13 | field :body, type: :codemirror, mode: 'text/x-liquid-html' 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/dummy/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) 7 | # Mayor.create(name: 'Emanuel', city: cities.first) 8 | -------------------------------------------------------------------------------- /spec/models/puffer_pages/page_part_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | require 'spec_helper' 3 | 4 | describe PufferPages::PagePart do 5 | it { should be_a(PufferPages::Backends::Mixins::Renderable) } 6 | 7 | describe "validations" do 8 | it { should validate_presence_of(:name) } 9 | 10 | describe do 11 | it { should validate_uniqueness_of(:name) } 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/puffer_pages/liquid/tags/helper.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Liquid 3 | module Tags 4 | 5 | class Helper < ::Liquid::Tag 6 | def render(context) 7 | context.registers[:tracker].register("<%= #{@tag_name} %>") 8 | end 9 | end 10 | 11 | end 12 | end 13 | end 14 | 15 | Liquid::Template.register_tag('csrf_meta_tags', PufferPages::Liquid::Tags::Helper) 16 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll automatically include all the stylesheets available in this directory 3 | * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at 4 | * the top of the compiled file, but it's generally better to create a new file per style scope. 5 | * = require_self 6 | * = require_tree . 7 | */ -------------------------------------------------------------------------------- /app/components/page_parts/_page_part.html.erb: -------------------------------------------------------------------------------- 1 | <%= form.fields_for field.name, page_part, :child_index => child_index do |page_part_builder| %> 2 | <% field.children.each do |field| %> 3 | <%= field.render :form, parent_controller, page_part_builder.object, :builder => page_part_builder %> 4 | <% end %> 5 | <%= page_part_builder.hidden_field :id, :data => { :acts => 'id' } if page_part_builder.object.persisted? %> 6 | <% end %> 7 | -------------------------------------------------------------------------------- /db/migrate/20090504132337_create_page_parts.rb: -------------------------------------------------------------------------------- 1 | class CreatePageParts < ActiveRecord::Migration 2 | def self.up 3 | create_table :page_parts do |t| 4 | t.string :name 5 | t.text :body 6 | t.integer :page_id 7 | 8 | t.timestamps 9 | end 10 | 11 | add_index :page_parts, :name 12 | add_index :page_parts, :page_id 13 | end 14 | 15 | def self.down 16 | drop_table :page_parts 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/puffer_pages/liquid/tags/javascript.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Liquid 3 | module Tags 4 | 5 | class Javascript < ::Liquid::Block 6 | def render(context) 7 | context.registers[:tracker].register("<%= javascript_tag do %>#{super}<% end %>") 8 | end 9 | end 10 | 11 | end 12 | end 13 | end 14 | 15 | Liquid::Template.register_tag('javascript', PufferPages::Liquid::Tags::Javascript) 16 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20130118071513_create_layouts.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from puffer_pages (originally 20090506102004) 2 | class CreateLayouts < ActiveRecord::Migration 3 | def self.up 4 | create_table :layouts do |t| 5 | t.string :name 6 | t.text :body 7 | 8 | t.timestamps 9 | end 10 | 11 | add_index :layouts, :name 12 | end 13 | 14 | def self.down 15 | drop_table :layouts 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | 4 | around_filter :locale_context 5 | 6 | def locale_context &block 7 | params[:locale].present? ? I18n.with_locale(params[:locale], &block) : block.call 8 | end 9 | 10 | def has_puffer_access? namespace 11 | true 12 | end 13 | 14 | def current_puffer_user 15 | nil 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20130118071514_create_snippets.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from puffer_pages (originally 20090510121824) 2 | class CreateSnippets < ActiveRecord::Migration 3 | def self.up 4 | create_table :snippets do |t| 5 | t.string :name 6 | t.text :body 7 | 8 | t.timestamps 9 | end 10 | 11 | add_index :snippets, :name 12 | end 13 | 14 | def self.down 15 | drop_table :snippets 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into including all the files listed below. 2 | // Add new JavaScript/Coffee code in separate files in this directory and they'll automatically 3 | // be included in the compiled file accessible from http://example.com/assets/application.js 4 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 5 | // the compiled file. 6 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20130118071515_create_origins.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from puffer_pages (originally 20120812100913) 2 | class CreateOrigins < ActiveRecord::Migration 3 | def self.up 4 | create_table :origins do |t| 5 | t.string :name 6 | t.string :host 7 | t.string :path 8 | t.string :token 9 | 10 | t.timestamps 11 | end 12 | end 13 | 14 | def self.down 15 | drop_table :origins 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/puffer_pages/liquid/backend.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Liquid 3 | class Backend 4 | include ::I18n::Backend::Simple::Implementation 5 | include ::I18n::Backend::Pluralization 6 | 7 | def load_translations; end 8 | def store_translations(*args); end 9 | def initialized?; true; end 10 | 11 | def translations 12 | Contextuality.page_translations || {} 13 | end 14 | 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Dummy::Application.config.session_store :cookie_store, key: '_dummy_session' 4 | 5 | # Use the database for sessions instead of the cookie-based default, 6 | # which shouldn't be used to store highly confidential information 7 | # (create the session table with "rails generate session_migration") 8 | # Dummy::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /spec/models/puffer_pages/layout_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | require 'spec_helper' 3 | 4 | describe PufferPages::Layout do 5 | describe "validations" do 6 | it { should validate_presence_of(:name) } 7 | it { should validate_uniqueness_of(:name)} 8 | end 9 | 10 | describe "#find_layout" do 11 | let!(:layout) { Fabricate :layout, :name => 'main' } 12 | 13 | specify { PufferPages::Layout.find_layout('main').should == layout} 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/models/puffer_pages/snippet_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | require 'spec_helper' 3 | 4 | describe PufferPages::Snippet do 5 | describe "validations" do 6 | it { should validate_presence_of(:name) } 7 | it { should validate_uniqueness_of(:name) } 8 | end 9 | 10 | describe "#find_snippet" do 11 | let!(:snippet) { Fabricate :snippet, :name => 'main' } 12 | 13 | specify { PufferPages::Snippet.find_snippet('main').should == snippet} 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/puffer_pages/helpers.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Helpers 3 | def puffer_pages_context 4 | drops = assigns.each_with_object({}) do |(key, value), result| 5 | result[key] = value if value.respond_to?(:to_liquid) 6 | end 7 | registers = assigns.each_with_object({}) do |(key, value), result| 8 | result[key] = value 9 | end 10 | 11 | { drops: drops, registers: registers.merge!(:controller => controller) } 12 | end 13 | end 14 | end -------------------------------------------------------------------------------- /spec/lib/liquid/tags/scope_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe PufferPages::Liquid::Tags::Scope do 4 | let(:klass) do 5 | Class.new do 6 | include PufferPages::Backends::Mixins::Renderable 7 | 8 | def render *args 9 | render_template *args 10 | end 11 | end 12 | end 13 | let(:template) do 14 | klass.new 15 | end 16 | 17 | specify { template.render("{% scope foo: 'hello' %}{{ foo }}{% endscope %}").should == 'hello' } 18 | end 19 | -------------------------------------------------------------------------------- /lib/puffer_pages/handlers/base.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Handlers 3 | class Base 4 | attr_reader :type 5 | 6 | def initialize type 7 | @type = type 8 | end 9 | 10 | def process renderable, context = nil 11 | renderable.render context 12 | end 13 | 14 | def codemirror_mode 15 | 'text/x-liquid-html' 16 | end 17 | end 18 | end 19 | end 20 | 21 | PufferPages::Handlers.register PufferPages::Handlers::Base, :html 22 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20130118071512_create_page_parts.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from puffer_pages (originally 20090504132337) 2 | class CreatePageParts < ActiveRecord::Migration 3 | def self.up 4 | create_table :page_parts do |t| 5 | t.string :name 6 | t.text :body 7 | t.integer :page_id 8 | 9 | t.timestamps 10 | end 11 | 12 | add_index :page_parts, :name 13 | add_index :page_parts, :page_id 14 | end 15 | 16 | def self.down 17 | drop_table :page_parts 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | Dummy::Application.config.secret_token = '8cf6d9f23949d20957da43a2b5b54c77d84a52592120f14e54969949efef0508ad8cb263ce3949a78ebff63b3143b237e26bc2d147609f831dc57204cecd2561' 8 | -------------------------------------------------------------------------------- /spec/lib/liquid/tags/image_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe PufferPages::Liquid::Tags::Image do 4 | describe 'snippet' do 5 | let!(:root) { Fabricate :root } 6 | 7 | specify { root.render("{% image 'i.png' %}").should == "<%= image_tag 'i.png', {} %>" } 8 | specify { root.render("{% image 'i.png', alt:'txt' %}").should == "<%= image_tag 'i.png', {\"alt\"=>\"txt\"} %>" } 9 | specify { root.render("{% image 'i.png', alt:var %}", var: 'txt').should == "<%= image_tag 'i.png', {\"alt\"=>\"txt\"} %>" } 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/fabricators/snippets_fabricator.rb: -------------------------------------------------------------------------------- 1 | Fabricator(:snippet, class_name: :'puffer_pages/snippet') do 2 | name { Forgery::LoremIpsum.word(random: true) } 3 | body { Forgery::LoremIpsum.sentence(random: true) } 4 | if PufferPages.localize 5 | body_translations do 6 | (I18n.available_locales - [I18n.locale]).each_with_object({}) do |locale, result| 7 | result[locale] = Forgery::LoremIpsum.sentence(random: true) 8 | end 9 | end 10 | end 11 | end 12 | 13 | Fabricator(:custom, from: :snippet) do 14 | name { "custom" } 15 | end 16 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # Disable root element in JSON by default. 12 | ActiveSupport.on_load(:active_record) do 13 | self.include_root_in_json = false 14 | end 15 | -------------------------------------------------------------------------------- /spec/lib/core_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Core' do 4 | 5 | describe 'arrange' do 6 | 7 | it 'should generate proper hash' do 8 | @root = Fabricate :page, :layout_name => 'foo_layout' 9 | @foo = Fabricate :page, :slug => 'foo', :parent => @root 10 | @bar = Fabricate :page, :slug => 'bar', :parent => @foo 11 | @baz = Fabricate :page, :slug => 'baz', :parent => @root 12 | 13 | @root.reload.self_and_descendants.all.arranged.should == @root.reload.self_and_descendants.arrange 14 | end 15 | 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /app/assets/stylesheets/puffer/codemirror/neat.css: -------------------------------------------------------------------------------- 1 | .cm-s-neat span.cm-comment { color: #a86; } 2 | .cm-s-neat span.cm-keyword { line-height: 1em; font-weight: bold; color: blue; } 3 | .cm-s-neat span.cm-string { color: #a22; } 4 | .cm-s-neat span.cm-builtin { line-height: 1em; font-weight: bold; color: #077; } 5 | .cm-s-neat span.cm-special { line-height: 1em; font-weight: bold; color: #0aa; } 6 | .cm-s-neat span.cm-variable { color: black; } 7 | .cm-s-neat span.cm-number, .cm-s-neat span.cm-atom { color: #3a3; } 8 | .cm-s-neat span.cm-meta {color: #555;} 9 | .cm-s-neat span.cm-link { color: #3a3; } 10 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | # 12 | # These inflection rules are supported but not enabled by default: 13 | # ActiveSupport::Inflector.inflections do |inflect| 14 | # inflect.acronym 'RESTful' 15 | # end 16 | -------------------------------------------------------------------------------- /lib/puffer_pages/liquid/tracker.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Liquid 3 | class Tracker 4 | 5 | def initialize 6 | @ids = [] 7 | end 8 | 9 | def register content 10 | @ids << Digest::MD5.hexdigest(SecureRandom.uuid) 11 | content.gsub(/<%/, "<#{@ids.last}%").gsub(/%>/, "%#{@ids.last}>") 12 | end 13 | 14 | def cleanup content 15 | ids = @ids.join('|') 16 | content = content.gsub(/<%/, "<%").gsub(/%>/, "%>")# unless PufferPages.allow_erb 17 | content.gsub(/<(#{ids})%/, "<%").gsub(/%(#{ids})>/, "%>") 18 | end 19 | 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/puffer_pages/extensions/core.rb: -------------------------------------------------------------------------------- 1 | Array.class_eval do 2 | def arranged 3 | arranged = ActiveSupport::OrderedHash.new 4 | insertion_points = [arranged] 5 | depth = 0 6 | each do |node| 7 | next if node.depth > depth && insertion_points.last.keys.last && node.parent_id != insertion_points.last.keys.last.id 8 | insertion_points.push insertion_points.last.values.last if node.depth > depth 9 | (depth - node.depth).times { insertion_points.pop } if node.depth < depth 10 | insertion_points.last.merge! node => ActiveSupport::OrderedHash.new 11 | depth = node.depth 12 | end 13 | arranged 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/fabricators/page_parts_fabricator.rb: -------------------------------------------------------------------------------- 1 | Fabricator(:page_part, class_name: :'puffer_pages/page_part') do 2 | name { Forgery::LoremIpsum.word } 3 | body { |attrs| "PagePart: `#{attrs[:name]}`" } 4 | if PufferPages.localize 5 | body_translations do 6 | (I18n.available_locales - [I18n.locale]).each_with_object({}) do |locale, result| 7 | result[locale] = Forgery::LoremIpsum.sentence(random: true) 8 | end 9 | end 10 | end 11 | end 12 | 13 | Fabricator(:main, from: :page_part) do 14 | name { PufferPages.primary_page_part_name } 15 | end 16 | 17 | Fabricator(:sidebar, from: :page_part) do 18 | name { 'sidebar' } 19 | end 20 | -------------------------------------------------------------------------------- /spec/fabricators/pages_fabricator.rb: -------------------------------------------------------------------------------- 1 | Fabricator(:page, class_name: :'puffer_pages/page') do 2 | name { Forgery::LoremIpsum.sentence(random: true) } 3 | slug '' 4 | status {'published'} 5 | 6 | after_build do |page| 7 | page.page_parts.each do |page_part| 8 | if page_part.body == "PagePart: `#{page_part.name}`" 9 | page_part.body = "PagePart: `#{page_part.name}`, Page: `#{page.location}`" 10 | end 11 | end 12 | end 13 | end 14 | 15 | Fabricator(:root, from: :page) do 16 | slug '' 17 | layout_name 'application' 18 | end 19 | 20 | Fabricator(:foo_root, from: :page) do 21 | slug '' 22 | layout_name 'foo_layout' 23 | end 24 | -------------------------------------------------------------------------------- /app/assets/stylesheets/puffer/codemirror/elegant.css: -------------------------------------------------------------------------------- 1 | .cm-s-elegant span.cm-number, .cm-s-elegant span.cm-string, .cm-s-elegant span.cm-atom {color: #762;} 2 | .cm-s-elegant span.cm-comment {color: #262; font-style: italic; line-height: 1em;} 3 | .cm-s-elegant span.cm-meta {color: #555; font-style: italic; line-height: 1em;} 4 | .cm-s-elegant span.cm-variable {color: black;} 5 | .cm-s-elegant span.cm-variable-2 {color: #b11;} 6 | .cm-s-elegant span.cm-qualifier {color: #555;} 7 | .cm-s-elegant span.cm-keyword {color: #730;} 8 | .cm-s-elegant span.cm-builtin {color: #30a;} 9 | .cm-s-elegant span.cm-error {background-color: #fdd;} 10 | .cm-s-elegant span.cm-link {color: #762;} 11 | -------------------------------------------------------------------------------- /db/migrate/20090422092419_create_pages.rb: -------------------------------------------------------------------------------- 1 | class CreatePages < ActiveRecord::Migration 2 | def self.up 3 | create_table :pages do |t| 4 | t.string :name 5 | t.string :slug 6 | t.string :location 7 | t.text :title 8 | t.text :description 9 | t.text :keywords 10 | t.string :layout_name 11 | t.string :status 12 | 13 | t.integer :parent_id 14 | t.integer :lft 15 | t.integer :rgt 16 | t.integer :depth, :default => 0 17 | 18 | t.timestamps 19 | end 20 | 21 | add_index :pages, :slug 22 | add_index :pages, :location 23 | end 24 | 25 | def self.down 26 | drop_table :pages 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/puffer_pages/rspec/view_rendering.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Rspec 3 | module ViewRendering 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | before do 8 | unless render_views? 9 | dummy_page = PufferPages::Page.new 10 | dummy_page.stub(:inherited_layout) { '' } 11 | dummy_page.stub(:dummy_page?) { true } 12 | controller.stub(:_puffer_page_for) do |location, scope| 13 | dummy_page.stub(:location) { PufferPages::Page::normalize_path(location) } 14 | dummy_page 15 | end 16 | end 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | require 'rubygems' 3 | begin 4 | require 'bundler/setup' 5 | rescue LoadError 6 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 7 | end 8 | 9 | require 'rake' 10 | require 'rdoc/task' 11 | 12 | require 'rspec/core' 13 | require 'rspec/core/rake_task' 14 | 15 | RSpec::Core::RakeTask.new(:spec) 16 | 17 | task :default => :spec 18 | 19 | Rake::RDocTask.new(:rdoc) do |rdoc| 20 | rdoc.rdoc_dir = 'rdoc' 21 | rdoc.title = 'Puffer Pages' 22 | rdoc.options << '--line-numbers' << '--inline-source' 23 | rdoc.rdoc_files.include('README.rdoc') 24 | rdoc.rdoc_files.include('lib/**/*.rb') 25 | end 26 | 27 | require "bundler/gem_tasks" 28 | -------------------------------------------------------------------------------- /lib/puffer_pages/liquid/tags/partials.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Liquid 3 | module Tags 4 | 5 | class Partials < Include 6 | 7 | private 8 | 9 | def _read_template_from_file_system(context) 10 | file_system = context.registers[:file_system] || Liquid::Template.file_system 11 | template_name = "#{@tag_name.pluralize}/#{context[@template_name]}" 12 | 13 | file_system.read_template_file(template_name, context) 14 | end 15 | end 16 | 17 | end 18 | end 19 | end 20 | 21 | Liquid::Template.register_tag('snippet', PufferPages::Liquid::Tags::Partials) 22 | Liquid::Template.register_tag('layout', PufferPages::Liquid::Tags::Partials) 23 | -------------------------------------------------------------------------------- /lib/puffer_pages/handlers/yaml.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Handlers 3 | class Yaml < Base 4 | def process renderable, context = nil 5 | renderable.self_and_ancestors.where(handler: 'yaml').reverse.each_with_object({}) do |renderable, result| 6 | load_arguments = [renderable.render(context)] 7 | load_arguments.push renderable.name if YAML.method(:load).arity == -2 8 | hash = YAML.load *load_arguments 9 | result.deep_merge! hash 10 | end 11 | end 12 | 13 | def codemirror_mode 14 | 'text/x-liquid-yaml' 15 | end 16 | end 17 | end 18 | end 19 | 20 | PufferPages::Handlers.register PufferPages::Handlers::Yaml, :yaml 21 | -------------------------------------------------------------------------------- /lib/puffer_pages/backends.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Backends 3 | autoload :Snippet, 'puffer_pages/backends/models/snippet' 4 | autoload :Layout, 'puffer_pages/backends/models/layout' 5 | autoload :Page, 'puffer_pages/backends/models/page' 6 | autoload :PagePart, 'puffer_pages/backends/models/page_part' 7 | autoload :Origin, 'puffer_pages/backends/models/origin' 8 | 9 | module Mixins 10 | autoload :Renderable, 'puffer_pages/backends/models/mixins/renderable' 11 | autoload :Importable, 'puffer_pages/backends/models/mixins/importable' 12 | autoload :Translatable, 'puffer_pages/backends/models/mixins/translatable' 13 | autoload :Localable, 'puffer_pages/backends/models/mixins/localable' 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20130118071511_create_pages.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from puffer_pages (originally 20090422092419) 2 | class CreatePages < ActiveRecord::Migration 3 | def self.up 4 | create_table :pages do |t| 5 | t.string :name 6 | t.string :slug 7 | t.string :location 8 | t.text :title 9 | t.text :description 10 | t.text :keywords 11 | t.string :layout_name 12 | t.string :status 13 | 14 | t.integer :parent_id 15 | t.integer :lft 16 | t.integer :rgt 17 | t.integer :depth, :default => 0 18 | 19 | t.timestamps 20 | end 21 | 22 | add_index :pages, :slug 23 | add_index :pages, :location 24 | end 25 | 26 | def self.down 27 | drop_table :pages 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/puffer_pages/globalize/migrator.rb: -------------------------------------------------------------------------------- 1 | require 'globalize' 2 | 3 | module PufferPages 4 | module Globalize 5 | class Migrator < ::Globalize::ActiveRecord::Migration::Migrator 6 | def create_translation_table 7 | connection.create_table(translations_table_name, id: false) do |t| 8 | t.uuid :id, primary_key: true 9 | t.uuid "#{table_name.sub(/^#{table_name_prefix}/, '').singularize}_id" 10 | t.string :locale 11 | fields.each do |name, options| 12 | if options.is_a? Hash 13 | t.column name, options.delete(:type), options 14 | else 15 | t.column name, options 16 | end 17 | end 18 | t.timestamps 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/helpers/puffer_pages_helper.rb: -------------------------------------------------------------------------------- 1 | module PufferPagesHelper 2 | unloadable 3 | 4 | def possible_layouts 5 | inherited_layout + (application_layouts + puffer_layouts).uniq.sort 6 | end 7 | 8 | def application_layouts 9 | Dir.glob("#{view_paths.first}/layouts/[^_]*").flatten.map {|path| File.basename(path).gsub(/\..*$/, '')}.uniq 10 | end 11 | 12 | def puffer_layouts 13 | PufferPages::Layout.order(:name).all.map(&:name) 14 | end 15 | 16 | def inherited_layout 17 | record.inherited_layout_name && !record.root? ? [[t('puffer_pages.inherited_layout', :name => record.inherited_layout_name), '']] : [] 18 | end 19 | 20 | def possible_statuses 21 | PufferPages::Page.statuses 22 | end 23 | 24 | def tree_page 25 | render :partial => 'tree_page', :object => record 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/fabricators/layouts_fabricator.rb: -------------------------------------------------------------------------------- 1 | Fabricator(:layout, class_name: :'puffer_pages/layout') do 2 | name { Forgery::LoremIpsum.word(random: true) } 3 | body { Forgery::LoremIpsum.sentence(random: true) } 4 | if PufferPages.localize 5 | body_translations do 6 | (I18n.available_locales - [I18n.locale]).each_with_object({}) do |locale, result| 7 | result[locale] = Forgery::LoremIpsum.sentence(random: true) 8 | end 9 | end 10 | end 11 | end 12 | 13 | Fabricator(:application, from: :layout) do 14 | name { "application" } 15 | end 16 | 17 | Fabricator(:foo_layout, from: :layout) do 18 | name { "foo_layout" } 19 | body { "foo_layout {{self.slug}}" } 20 | end 21 | 22 | Fabricator(:bar_layout, from: :layout) do 23 | name { "bar_layout" } 24 | body { "bar_layout {{self.slug}}" } 25 | end 26 | -------------------------------------------------------------------------------- /lib/puffer_pages/renderer.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | class Renderer < ActionView::TemplateRenderer 3 | def render(context, options) 4 | @view = context 5 | super 6 | end 7 | 8 | def determine_template(options) 9 | @view.assign(@view.assigns.merge!('puffer_page' => options[:puffer_page])) 10 | 11 | super 12 | rescue ActionView::MissingTemplate 13 | options[:text] = '' 14 | super 15 | end 16 | 17 | def find_layout(layout, keys) 18 | layout = "<%= render(:inline => @puffer_page.render(puffer_pages_context), 19 | :layout => @puffer_page.layout_for_render) %>" 20 | 21 | handler = ActionView::Template.handler_for_extension("erb") 22 | ActionView::Template.new(layout, "puffer pages layout wrapper", handler, :locals => keys) 23 | end 24 | end 25 | end -------------------------------------------------------------------------------- /spec/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The change you wanted was rejected.

23 |

Maybe you tried to change something you didn't have access to.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/puffer_pages/liquid/tags/scope.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Liquid 3 | module Tags 4 | 5 | class Scope < ::Liquid::Block 6 | def initialize(tag_name, markup, tokens) 7 | @attributes = {} 8 | markup.scan(::Liquid::TagAttributes) do |key, value| 9 | @attributes[key] = value 10 | end 11 | 12 | super 13 | end 14 | 15 | def render(context) 16 | context.stack do 17 | @attributes.each do |key, value| 18 | context[key] = context[value] 19 | end 20 | 21 | super 22 | end 23 | end 24 | end 25 | 26 | end 27 | end 28 | end 29 | 30 | Liquid::Template.register_tag('scope', PufferPages::Liquid::Tags::Scope) 31 | Liquid::Template.register_tag('context', PufferPages::Liquid::Tags::Scope) 32 | -------------------------------------------------------------------------------- /spec/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The page you were looking for doesn't exist.

23 |

You may have mistyped the address or the page may have moved.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

We're sorry, but something went wrong.

23 |

We've been notified about this issue and we'll take a look at it shortly.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/puffer_pages/liquid/tags/yield.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Liquid 3 | module Tags 4 | 5 | class Yield < ::Liquid::Tag 6 | Syntax = /^(#{::Liquid::QuotedFragment})/ 7 | 8 | def initialize(tag_name, markup, tokens) 9 | if markup =~ Syntax 10 | @name = $1 11 | elsif markup.blank? 12 | @name = nil 13 | else 14 | raise SyntaxError.new("Syntax Error in 'yield' - Valid syntax: yield [content_name]") 15 | end 16 | 17 | super 18 | end 19 | 20 | def render(context) 21 | context.registers[:tracker].register(@name ? 22 | "<%= yield :'#{@name}' %>" : 23 | "<%= yield %>") 24 | end 25 | end 26 | 27 | end 28 | end 29 | end 30 | 31 | Liquid::Template.register_tag('yield', PufferPages::Liquid::Tags::Yield) 32 | -------------------------------------------------------------------------------- /lib/puffer_pages/engine.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | class Engine < Rails::Engine 3 | engine_name :puffer_pages 4 | 5 | config.autoload_paths << File.join(root, 'lib') 6 | config.puffer_pages = PufferPages.config 7 | 8 | initializer 'puffer_pages.install_i18n_backend', after: 'build_middleware_stack' do 9 | if PufferPages.install_i18n_backend 10 | I18n.backend = I18n::Backend::Chain.new(PufferPages.i18n_backend, I18n.backend) 11 | end 12 | end 13 | 14 | initializer 'puffer_pages.initialize_cache', after: 'initialize_cache' do 15 | PufferPages.cache_store = config.puffer_pages.cache_store || ::Rails.cache 16 | PufferPages.config.perform_caching = config.action_controller.perform_caching 17 | end 18 | 19 | ActiveSupport.on_load(:action_view) do 20 | ActionView::Base.send :include, PufferPages::Helpers 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/puffer_pages/handlers.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Handlers 3 | class HandlerMissing < Exception; end 4 | 5 | def self.handlers 6 | @handlers ||= HashWithIndifferentAccess.new 7 | end 8 | 9 | def self.register klass, *types 10 | handlers.merge! Hash[types.flatten.map { |type| [type, klass.new(type)] }] 11 | end 12 | 13 | def self.process type, *args 14 | raise HandlerMissing.new("Can not find handler for '#{type}' type") unless handlers[type] 15 | handlers[type].process(*args) 16 | end 17 | 18 | def self.select 19 | handlers.values.map { |handler| [ 20 | I18n.t("puffer_pages.handlers.#{handler.type}"), 21 | handler.type, 22 | { 'data-codemirror-mode' => handler.codemirror_mode } 23 | ] } 24 | end 25 | end 26 | end 27 | 28 | require 'puffer_pages/handlers/base' 29 | require 'puffer_pages/handlers/yaml' 30 | -------------------------------------------------------------------------------- /lib/puffer_pages/backends/controllers/origins_base.rb: -------------------------------------------------------------------------------- 1 | class PufferPages::OriginsBase < Puffer::Base 2 | skip_before_filter :require_puffer_user, only: :export 3 | 4 | member do 5 | get :import 6 | end 7 | 8 | collection do 9 | get :export, display: false 10 | end 11 | 12 | def import 13 | @record = resource.member 14 | @record.import! 15 | redirect_to admin_origins_path 16 | end 17 | 18 | def export 19 | if params[:token] == PufferPages.access_token 20 | render json: resource.model.export_json 21 | else 22 | render nothing: true, status: 401 23 | end 24 | end 25 | 26 | setup do 27 | group :pages 28 | model_name :'puffer_pages/origin' 29 | end 30 | 31 | index do 32 | field :name 33 | field :uri 34 | field :token 35 | end 36 | 37 | form do 38 | field :name 39 | field :host 40 | field :path 41 | field :token 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/puffer_pages/backends/models/layout.rb: -------------------------------------------------------------------------------- 1 | class PufferPages::Backends::Layout < ActiveRecord::Base 2 | include ActiveUUID::UUID 3 | include PufferPages::Backends::Mixins::Renderable 4 | include PufferPages::Backends::Mixins::Importable 5 | include PufferPages::Backends::Mixins::Translatable 6 | self.abstract_class = true 7 | self.table_name = :layouts 8 | 9 | attr_protected 10 | 11 | validates_presence_of :name 12 | validates_uniqueness_of :name 13 | 14 | def self.find_layout(name) 15 | where(:name => name).first 16 | end 17 | 18 | def render *args 19 | _, context = normalize_render_options *args 20 | render_template body, context, additional_render_options 21 | end 22 | 23 | def additional_render_options 24 | { environment: { processed: self } } 25 | end 26 | 27 | def i18n_scope 28 | [:layouts, name.to_sym] 29 | end 30 | 31 | def i18n_defaults 32 | [] 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/puffer_pages/backends/models/snippet.rb: -------------------------------------------------------------------------------- 1 | class PufferPages::Backends::Snippet < ActiveRecord::Base 2 | include ActiveUUID::UUID 3 | include PufferPages::Backends::Mixins::Renderable 4 | include PufferPages::Backends::Mixins::Importable 5 | include PufferPages::Backends::Mixins::Translatable 6 | self.abstract_class = true 7 | self.table_name = :snippets 8 | 9 | attr_protected 10 | 11 | validates_presence_of :name 12 | validates_uniqueness_of :name 13 | 14 | def self.find_snippet(name) 15 | where(:name => name).first 16 | end 17 | 18 | def render *args 19 | _, context = normalize_render_options *args 20 | render_template body, context, additional_render_options 21 | end 22 | 23 | def additional_render_options 24 | { environment: { processed: self } } 25 | end 26 | 27 | def i18n_scope 28 | [:snippets, name.to_sym] 29 | end 30 | 31 | def i18n_defaults 32 | [] 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3-ruby (not necessary on OS X Leopard) 3 | development: 4 | adapter: sqlite3 5 | database: db/development.sqlite3 6 | pool: 5 7 | timeout: 5000 8 | 9 | # development: 10 | # adapter: mysql2 11 | # database: puffer_pages_development 12 | # usename: root 13 | # encoding: utf8 14 | 15 | # Warning: The database defined as "test" will be erased and 16 | # re-generated from your development database when you run "rake". 17 | # Do not set this db to the same as development or production. 18 | test: 19 | adapter: sqlite3 20 | database: ":memory:" 21 | pool: 5 22 | timeout: 5000 23 | 24 | pg_test: 25 | adapter: postgresql 26 | hostname: localhost 27 | database: puffer_pages 28 | schema_search_path: public 29 | encoding: utf8 30 | 31 | production: 32 | adapter: sqlite3 33 | database: db/development.sqlite3 34 | pool: 5 35 | timeout: 5000 36 | -------------------------------------------------------------------------------- /spec/lib/liquid/backend_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe PufferPages::Liquid::Backend do 4 | let(:backend) { described_class.new } 5 | let(:translations) { { en: { hello: 'PufferPages world' } } } 6 | 7 | specify { backend.translations == {} } 8 | 9 | specify do 10 | contextualize(page_translations: translations) do 11 | backend.translations 12 | end.should == translations 13 | end 14 | 15 | specify do 16 | contextualize(page_translations: translations) do 17 | backend.translate(:en, 'hello') 18 | end.should == 'PufferPages world' 19 | end 20 | 21 | context 'backend installation' do 22 | context 'fallbacks' do 23 | specify do 24 | contextualize(page_translations: translations) do 25 | I18n.with_locale :ru do 26 | I18n.translate('hello') 27 | end 28 | end.should == 'PufferPages world' 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/puffer_pages/extensions/renderer.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Extensions 3 | module Renderer 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | alias_method_chain :render, :puffer_pages 8 | end 9 | 10 | def render_with_puffer_pages(context, options) 11 | if options.key?(:puffer_page) && options[:puffer_page].is_a?(PufferPages::Page) 12 | render_puffer_page(context, options) 13 | else 14 | render_without_puffer_pages(context, options) 15 | end 16 | end 17 | 18 | def render_puffer_page(context, options) 19 | _puffer_page_renderer.render(context, options) 20 | end 21 | 22 | private 23 | 24 | def _puffer_page_renderer #:nodoc: 25 | @_puffer_page_renderer ||= PufferPages::Renderer.new(@lookup_context) 26 | end 27 | end 28 | end 29 | end 30 | 31 | ActionView::Renderer.send :include, PufferPages::Extensions::Renderer -------------------------------------------------------------------------------- /lib/puffer_pages/log_subscriber.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | class LogSubscriber < ActiveSupport::LogSubscriber 3 | def render_page event 4 | message = " PufferPages: rendered page /#{event.payload[:subject].location} #{duration(event)}" 5 | info message 6 | end 7 | 8 | def render_page_part event 9 | message = " PufferPages: rendered page_part #{event.payload[:subject].name} #{duration(event)}" 10 | debug message 11 | end 12 | 13 | def render_layout event 14 | message = " PufferPages: rendered layout #{event.payload[:subject].name} #{duration(event)}" 15 | debug message 16 | end 17 | 18 | def render_snippet event 19 | message = " PufferPages: rendered snippet #{event.payload[:subject].name} #{duration(event)}" 20 | debug message 21 | end 22 | 23 | def duration event 24 | '(%.1fms)' % event.duration 25 | end 26 | end 27 | end 28 | 29 | PufferPages::LogSubscriber.attach_to :puffer_pages 30 | -------------------------------------------------------------------------------- /app/assets/javascripts/puffer/liquid.js: -------------------------------------------------------------------------------- 1 | CodeMirror.defineMode("text/x-liquid-html", function(config) { 2 | return CodeMirror.multiplexingMode( 3 | CodeMirror.getMode(config, "text/html"), 4 | { 5 | open: "{{", close: "}}", 6 | mode: CodeMirror.getMode(config, "text/x-liquid-variable"), 7 | delimStyle: "tag" 8 | }, 9 | { 10 | open: "{%", close: "%}", 11 | mode: CodeMirror.getMode(config, "text/x-liquid-tag"), 12 | delimStyle: "tag" 13 | } 14 | ); 15 | }); 16 | 17 | CodeMirror.defineMode("text/x-liquid-yaml", function(config) { 18 | return CodeMirror.multiplexingMode( 19 | CodeMirror.getMode(config, "text/x-yaml"), 20 | { 21 | open: "{{", close: "}}", 22 | mode: CodeMirror.getMode(config, "text/x-liquid-variable"), 23 | delimStyle: "tag" 24 | }, 25 | { 26 | open: "{%", close: "%}", 27 | mode: CodeMirror.getMode(config, "text/x-liquid-tag"), 28 | delimStyle: "tag" 29 | } 30 | ); 31 | }); 32 | -------------------------------------------------------------------------------- /lib/puffer_pages/extensions/context.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Extensions 3 | module Liquid 4 | module Context 5 | extend ActiveSupport::Concern 6 | 7 | included do 8 | alias_method_chain :resolve, :interpolation 9 | end 10 | 11 | def resolve_with_interpolation key 12 | if key.is_a? Symbol 13 | scope = @scopes.detect { |s| s.key? key } 14 | scope ||= @environments.detect { |s| s.key? key } 15 | scope[key] if scope 16 | else 17 | resolved = resolve_without_interpolation key 18 | if resolved.is_a?(String) && key =~ /^"(.*)"$/ 19 | resolved.gsub!(/\#\{(.*?)\}/) do 20 | ::Liquid::Variable.new($1).render(self) 21 | end 22 | end 23 | resolved 24 | end 25 | end 26 | end 27 | end 28 | end 29 | end 30 | 31 | Liquid::Context.send :include, PufferPages::Extensions::Liquid::Context 32 | -------------------------------------------------------------------------------- /lib/puffer_pages/rspec/matchers.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Rspec 3 | module Matchers 4 | 5 | end 6 | end 7 | end 8 | 9 | require 'puffer_pages/rspec/matchers/render_page' 10 | 11 | RSpec::Rails::ControllerExampleGroup.class_eval do 12 | def puffer_pages_render 13 | @puffer_pages_render 14 | end 15 | 16 | def self.included_with_puffer_pages base 17 | base.class_eval do 18 | around(:each) do |example| 19 | @puffer_pages_render = {} 20 | ActiveSupport::Notifications.subscribe('render_page.puffer_pages') do |name, start, finish, id, payload| 21 | @puffer_pages_render[payload[:subject]] ||= [] 22 | @puffer_pages_render[payload[:subject]].push payload 23 | end 24 | example.run 25 | ActiveSupport::Notifications.unsubscribe('render_page.puffer_pages') 26 | end 27 | end 28 | included_without_puffer_pages base 29 | end 30 | 31 | singleton_class.alias_method_chain :included, :puffer_pages 32 | end 33 | -------------------------------------------------------------------------------- /lib/puffer_pages/liquid/tags/include.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Liquid 3 | module Tags 4 | 5 | class Include < ::Liquid::Include 6 | def render(context) 7 | source = _read_template_from_file_system(context) 8 | variable = context[@variable_name || @template_name[1..-2]] 9 | 10 | context.stack do 11 | @attributes.each do |key, value| 12 | context[key] = context[value] 13 | end 14 | 15 | if variable.is_a?(Array) 16 | variable.collect do |variable| 17 | context[@template_name[1..-2]] = variable 18 | source.render(context) 19 | end 20 | else 21 | context[@template_name[1..-2]] = variable 22 | source.render(context) 23 | end 24 | end 25 | end 26 | end 27 | 28 | end 29 | end 30 | end 31 | 32 | Liquid::Template.register_tag('include', PufferPages::Liquid::Tags::Include) 33 | -------------------------------------------------------------------------------- /lib/puffer_pages/liquid/tags/render.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Liquid 3 | module Tags 4 | 5 | class Render < ::Liquid::Tag 6 | Syntax = /^(#{::Liquid::QuotedFragment})/ 7 | 8 | def initialize(tag_name, markup, tokens) 9 | if markup =~ Syntax 10 | @path = $1 11 | else 12 | raise SyntaxError.new("Syntax Error in 'render' - Valid syntax: render path") 13 | end 14 | 15 | super 16 | end 17 | 18 | def render(context) 19 | path = context[@path] 20 | context.registers[:tracker].register("<%= 21 | old_formats = formats 22 | begin 23 | self.formats = old_formats | [:html] 24 | render '#{path}' 25 | ensure 26 | self.formats = old_formats 27 | end 28 | %>") 29 | end 30 | end 31 | 32 | end 33 | end 34 | end 35 | 36 | Liquid::Template.register_tag('render', PufferPages::Liquid::Tags::Render) 37 | -------------------------------------------------------------------------------- /lib/puffer_pages/liquid/tags/super.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Liquid 3 | module Tags 4 | 5 | class Super < ::Liquid::Include 6 | def initialize(tag_name, markup, tokens) 7 | @attributes = {} 8 | markup.scan(::Liquid::TagAttributes) do |key, value| 9 | @attributes[key] = value 10 | end 11 | end 12 | 13 | def render(context) 14 | source = _read_template_from_file_system(context) 15 | 16 | context.stack do 17 | @attributes.each do |key, value| 18 | context[key] = context[value] 19 | end 20 | 21 | source.render(context) 22 | end 23 | end 24 | 25 | private 26 | def _read_template_from_file_system(context) 27 | file_system = context.registers[:file_system] || Liquid::Template.file_system 28 | file_system.read_template_file(:super, context) 29 | end 30 | end 31 | 32 | end 33 | end 34 | end 35 | 36 | Liquid::Template.register_tag('super', PufferPages::Liquid::Tags::Super) 37 | -------------------------------------------------------------------------------- /lib/puffer_pages/liquid/tags/image.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Liquid 3 | module Tags 4 | 5 | class Image < ::Liquid::Tag 6 | Syntax = /^(#{::Liquid::QuotedFragment})/ 7 | 8 | def initialize(tag_name, markup, tokens) 9 | if markup =~ Syntax 10 | @path = $1 11 | else 12 | raise SyntaxError.new("Syntax Error in 'image' - Valid syntax: image '/path/to/image'") 13 | end 14 | 15 | @attributes = {} 16 | markup.scan(::Liquid::TagAttributes) do |key, value| 17 | @attributes[key] = value 18 | end 19 | 20 | super 21 | end 22 | 23 | def render(context) 24 | attributes = {} 25 | @attributes.each do |key, value| 26 | attributes[key] = context[value] 27 | end 28 | 29 | context.registers[:tracker].register("<%= image_tag #{@path}, #{attributes} %>") 30 | end 31 | end 32 | 33 | end 34 | end 35 | end 36 | 37 | Liquid::Template.register_tag('image', PufferPages::Liquid::Tags::Image) 38 | -------------------------------------------------------------------------------- /spec/lib/liquid/interpolation_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Interpolation' do 4 | let!(:root) { Fabricate :root } 5 | 6 | context 'double quotes' do 7 | let!(:layout) { Fabricate :application, body: '{% assign var = "#{foo} interpolation" %}{{ var }}' } 8 | specify { root.render('foo' => 'bar').should == 'bar interpolation' } 9 | end 10 | 11 | context 'single quotes' do 12 | let!(:layout) { Fabricate :application, body: '{% assign var = \'#{foo} interpolation\' %}{{ var }}' } 13 | specify { root.render('foo' => 'bar').should == '#{foo} interpolation' } 14 | end 15 | 16 | context 'filters' do 17 | let!(:layout) { Fabricate :application, body: '{% assign var = "#{foo | capitalize} interpolation" %}{{ var }}' } 18 | specify { root.render('foo' => 'bar').should == 'Bar interpolation' } 19 | end 20 | 21 | context 'filters with parameters' do 22 | let!(:layout) { Fabricate :application, body: '{% assign var = "#{foo | minus: 1} interpolation" %}{{ var }}' } 23 | specify { root.render('foo' => 4).should == '3 interpolation' } 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2011 YOURNAME 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/puffer_pages/liquid/tags/array.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Liquid 3 | module Tags 4 | 5 | class Array < ::Liquid::Tag 6 | Syntax = /^(#{::Liquid::VariableSignature}+)\s*=\s*(.*)\s*/ 7 | 8 | def initialize(tag_name, markup, tokens) 9 | if markup =~ Syntax 10 | @variable_name = $1 11 | @items = variables_from_string($2) 12 | else 13 | raise SyntaxError.new("Syntax Error in 'array' - Valid syntax: array array_name = item[, item ...]") 14 | end 15 | 16 | super 17 | end 18 | 19 | def render(context) 20 | context[@variable_name] = @items.map { |item| context[item] } 21 | '' 22 | end 23 | 24 | private 25 | 26 | def variables_from_string(markup) 27 | markup.split(',').map do |var| 28 | var.strip =~ /\s*(#{::Liquid::QuotedFragment})\s*/ 29 | $1 ? $1 : nil 30 | end.compact 31 | end 32 | 33 | end 34 | 35 | end 36 | end 37 | end 38 | 39 | Liquid::Template.register_tag('array', PufferPages::Liquid::Tags::Array) 40 | -------------------------------------------------------------------------------- /app/assets/stylesheets/puffer/codemirror/cobalt.css: -------------------------------------------------------------------------------- 1 | .cm-s-cobalt.CodeMirror { background: #002240; color: white; } 2 | .cm-s-cobalt div.CodeMirror-selected { background: #b36539 !important; } 3 | .cm-s-cobalt .CodeMirror-gutters { background: #002240; border-right: 1px solid #aaa; } 4 | .cm-s-cobalt .CodeMirror-linenumber { color: #d0d0d0; } 5 | .cm-s-cobalt .CodeMirror-cursor { border-left: 1px solid white !important; } 6 | 7 | .cm-s-cobalt span.cm-comment { color: #08f; } 8 | .cm-s-cobalt span.cm-atom { color: #845dc4; } 9 | .cm-s-cobalt span.cm-number, .cm-s-cobalt span.cm-attribute { color: #ff80e1; } 10 | .cm-s-cobalt span.cm-keyword { color: #ffee80; } 11 | .cm-s-cobalt span.cm-string { color: #3ad900; } 12 | .cm-s-cobalt span.cm-meta { color: #ff9d00; } 13 | .cm-s-cobalt span.cm-variable-2, .cm-s-cobalt span.cm-tag { color: #9effff; } 14 | .cm-s-cobalt span.cm-variable-3, .cm-s-cobalt span.cm-def { color: white; } 15 | .cm-s-cobalt span.cm-error { color: #9d1e15; } 16 | .cm-s-cobalt span.cm-bracket { color: #d8d8d8; } 17 | .cm-s-cobalt span.cm-builtin, .cm-s-cobalt span.cm-special { color: #ff9e59; } 18 | .cm-s-cobalt span.cm-link { color: #845dc4; } 19 | -------------------------------------------------------------------------------- /app/assets/stylesheets/puffer/codemirror/eclipse.css: -------------------------------------------------------------------------------- 1 | .cm-s-eclipse span.cm-meta {color: #FF1717;} 2 | .cm-s-eclipse span.cm-keyword { line-height: 1em; font-weight: bold; color: #7F0055; } 3 | .cm-s-eclipse span.cm-atom {color: #219;} 4 | .cm-s-eclipse span.cm-number {color: #164;} 5 | .cm-s-eclipse span.cm-def {color: #00f;} 6 | .cm-s-eclipse span.cm-variable {color: black;} 7 | .cm-s-eclipse span.cm-variable-2 {color: #0000C0;} 8 | .cm-s-eclipse span.cm-variable-3 {color: #0000C0;} 9 | .cm-s-eclipse span.cm-property {color: black;} 10 | .cm-s-eclipse span.cm-operator {color: black;} 11 | .cm-s-eclipse span.cm-comment {color: #3F7F5F;} 12 | .cm-s-eclipse span.cm-string {color: #2A00FF;} 13 | .cm-s-eclipse span.cm-string-2 {color: #f50;} 14 | .cm-s-eclipse span.cm-error {color: #f00;} 15 | .cm-s-eclipse span.cm-qualifier {color: #555;} 16 | .cm-s-eclipse span.cm-builtin {color: #30a;} 17 | .cm-s-eclipse span.cm-bracket {color: #cc7;} 18 | .cm-s-eclipse span.cm-tag {color: #170;} 19 | .cm-s-eclipse span.cm-attribute {color: #00c;} 20 | .cm-s-eclipse span.cm-link {color: #219;} 21 | 22 | .cm-s-eclipse .CodeMirror-matchingbracket { 23 | border:1px solid grey; 24 | color:black !important;; 25 | } 26 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | guard :rspec do 5 | watch(%r{^spec/.+_spec\.rb$}) 6 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } 7 | watch('spec/spec_helper.rb') { "spec" } 8 | 9 | # Rails example 10 | watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 11 | watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } 12 | watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] } 13 | watch(%r{^spec/support/(.+)\.rb$}) { "spec" } 14 | watch('config/routes.rb') { "spec/routing" } 15 | watch('app/controllers/application_controller.rb') { "spec/controllers" } 16 | 17 | # Capybara features specs 18 | watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/features/#{m[1]}_spec.rb" } 19 | 20 | # Turnip features and steps 21 | watch(%r{^spec/acceptance/(.+)\.feature$}) 22 | watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' } 23 | end 24 | 25 | -------------------------------------------------------------------------------- /lib/puffer_pages/migrations.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Migrations 3 | def self.create_translation_tables! options = {} 4 | unless PufferPages.localize 5 | puts "WARN: Translation tables creation skip. Set `PufferPages.localize = true` to perform it" 6 | return 7 | end 8 | options = options.reverse_merge migrate_data: true 9 | 10 | [PufferPages::PagePart, PufferPages::Layout, PufferPages::Snippet].each do |model| 11 | model.create_translation_table!({ 12 | body: { type: :text } 13 | }, options) 14 | puts "-- Created translation table for #{model} with #{options}" 15 | end 16 | end 17 | 18 | def self.drop_translation_tables! options = {} 19 | unless PufferPages.localize 20 | puts "WARN: Translation tables dropping skip. Set `PufferPages.localize = true` to perform it" 21 | return 22 | end 23 | options = options.reverse_merge migrate_data: true 24 | 25 | [PufferPages::PagePart, PufferPages::Layout, PufferPages::Snippet].each do |model| 26 | model.drop_translation_table! options 27 | puts "-- Dropped translation table for #{model} with #{options}" 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/assets/stylesheets/puffer/codemirror/night.css: -------------------------------------------------------------------------------- 1 | /* Loosely based on the Midnight Textmate theme */ 2 | 3 | .cm-s-night.CodeMirror { background: #0a001f; color: #f8f8f8; } 4 | .cm-s-night div.CodeMirror-selected { background: #447 !important; } 5 | .cm-s-night .CodeMirror-gutters { background: #0a001f; border-right: 1px solid #aaa; } 6 | .cm-s-night .CodeMirror-linenumber { color: #f8f8f8; } 7 | .cm-s-night .CodeMirror-cursor { border-left: 1px solid white !important; } 8 | 9 | .cm-s-night span.cm-comment { color: #6900a1; } 10 | .cm-s-night span.cm-atom { color: #845dc4; } 11 | .cm-s-night span.cm-number, .cm-s-night span.cm-attribute { color: #ffd500; } 12 | .cm-s-night span.cm-keyword { color: #599eff; } 13 | .cm-s-night span.cm-string { color: #37f14a; } 14 | .cm-s-night span.cm-meta { color: #7678e2; } 15 | .cm-s-night span.cm-variable-2, .cm-s-night span.cm-tag { color: #99b2ff; } 16 | .cm-s-night span.cm-variable-3, .cm-s-night span.cm-def { color: white; } 17 | .cm-s-night span.cm-error { color: #9d1e15; } 18 | .cm-s-night span.cm-bracket { color: #8da6ce; } 19 | .cm-s-night span.cm-comment { color: #6900a1; } 20 | .cm-s-night span.cm-builtin, .cm-s-night span.cm-special { color: #ff9e59; } 21 | .cm-s-night span.cm-link { color: #845dc4; } 22 | -------------------------------------------------------------------------------- /spec/lib/handlers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe PufferPages::Handlers do 4 | before(:all) { @handlers = described_class.handlers } 5 | after(:all) { described_class.instance_variable_set(:@handlers, @handlers) } 6 | 7 | before { subject.instance_variable_set(:@handlers, nil) } 8 | 9 | context do 10 | let(:klass) do 11 | Class.new do 12 | attr_accessor :type 13 | def initialize type 14 | @type = type 15 | end 16 | 17 | def process *args 18 | "processed #{@type}, #{args.first}" 19 | end 20 | 21 | def codemirror_mode 22 | 'mode' 23 | end 24 | end 25 | end 26 | before { subject.register klass, :html, :json } 27 | 28 | specify { subject.handlers[:html].should be_a klass } 29 | specify { subject.handlers[:json].should be_a klass } 30 | specify { subject.select.should == [ 31 | ['translation missing: en.puffer_pages.handlers.html', :html, {"data-codemirror-mode" => "mode"}], 32 | ['translation missing: en.puffer_pages.handlers.json', :json, {"data-codemirror-mode" => "mode"}] 33 | ] } 34 | specify { subject.process('html', 'object').should == 'processed html, object' } 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/requests/origins_requests_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "Origins controller" do 4 | let(:token) { "token" } 5 | 6 | describe "#import" do 7 | let(:json) { File.new('spec/data/import.json').read } 8 | let(:origin) { Fabricate :origin } 9 | 10 | before { FakeWeb.register_uri :get, origin.uri, :body => json, :content_length => json.length } 11 | before { get "/admin/origins/#{origin.id}/import" } 12 | 13 | specify { PufferPages::Layout.count.should == 2 } 14 | specify { PufferPages::Page.count.should == 1 } 15 | specify { PufferPages::Snippet.count.should == 1 } 16 | specify { PufferPages::PagePart.count.should == 2 } 17 | end 18 | 19 | describe "#export" do 20 | context "with a valid token" do 21 | before { get "/admin/origins/export", :token => "token" } 22 | 23 | subject { ActiveSupport::JSON.decode response.body } 24 | 25 | %w(layouts pages snippets).each do |key| 26 | it { should have_key(key) } 27 | end 28 | end 29 | 30 | context "with an invalid token" do 31 | before { get "/admin/origins/export", :token => "bad" } 32 | 33 | specify { response.body.should be_blank } 34 | specify { response.status.should == 401 } 35 | end 36 | end 37 | end -------------------------------------------------------------------------------- /app/assets/stylesheets/puffer/codemirror/monokai.css: -------------------------------------------------------------------------------- 1 | /* Based on Sublime Text's Monokai theme */ 2 | 3 | .cm-s-monokai.CodeMirror {background: #272822; color: #f8f8f2;} 4 | .cm-s-monokai div.CodeMirror-selected {background: #49483E !important;} 5 | .cm-s-monokai .CodeMirror-gutters {background: #272822; border-right: 0px;} 6 | .cm-s-monokai .CodeMirror-linenumber {color: #d0d0d0;} 7 | .cm-s-monokai .CodeMirror-cursor {border-left: 1px solid #f8f8f0 !important;} 8 | 9 | .cm-s-monokai span.cm-comment {color: #75715e;} 10 | .cm-s-monokai span.cm-atom {color: #ae81ff;} 11 | .cm-s-monokai span.cm-number {color: #ae81ff;} 12 | 13 | .cm-s-monokai span.cm-property, .cm-s-monokai span.cm-attribute {color: #a6e22e;} 14 | .cm-s-monokai span.cm-keyword {color: #f92672;} 15 | .cm-s-monokai span.cm-string {color: #e6db74;} 16 | 17 | .cm-s-monokai span.cm-variable {color: #a6e22e;} 18 | .cm-s-monokai span.cm-variable-2 {color: #9effff;} 19 | .cm-s-monokai span.cm-def {color: #fd971f;} 20 | .cm-s-monokai span.cm-error {background: #f92672; color: #f8f8f0;} 21 | .cm-s-monokai span.cm-bracket {color: #f8f8f2;} 22 | .cm-s-monokai span.cm-tag {color: #f92672;} 23 | .cm-s-monokai span.cm-link {color: #ae81ff;} 24 | 25 | .cm-s-monokai .CodeMirror-matchingbracket { 26 | text-decoration: underline; 27 | color: white !important; 28 | } 29 | -------------------------------------------------------------------------------- /app/assets/stylesheets/puffer/codemirror/erlang-dark.css: -------------------------------------------------------------------------------- 1 | .cm-s-erlang-dark.CodeMirror { background: #002240; color: white; } 2 | .cm-s-erlang-dark div.CodeMirror-selected { background: #b36539 !important; } 3 | .cm-s-erlang-dark .CodeMirror-gutters { background: #002240; border-right: 1px solid #aaa; } 4 | .cm-s-erlang-dark .CodeMirror-linenumber { color: #d0d0d0; } 5 | .cm-s-erlang-dark .CodeMirror-cursor { border-left: 1px solid white !important; } 6 | 7 | .cm-s-erlang-dark span.cm-atom { color: #845dc4; } 8 | .cm-s-erlang-dark span.cm-attribute { color: #ff80e1; } 9 | .cm-s-erlang-dark span.cm-bracket { color: #ff9d00; } 10 | .cm-s-erlang-dark span.cm-builtin { color: #eeaaaa; } 11 | .cm-s-erlang-dark span.cm-comment { color: #7777ff; } 12 | .cm-s-erlang-dark span.cm-def { color: #ee77aa; } 13 | .cm-s-erlang-dark span.cm-error { color: #9d1e15; } 14 | .cm-s-erlang-dark span.cm-keyword { color: #ffee80; } 15 | .cm-s-erlang-dark span.cm-meta { color: #50fefe; } 16 | .cm-s-erlang-dark span.cm-number { color: #ffd0d0; } 17 | .cm-s-erlang-dark span.cm-operator { color: #dd1111; } 18 | .cm-s-erlang-dark span.cm-string { color: #3ad900; } 19 | .cm-s-erlang-dark span.cm-tag { color: #9effff; } 20 | .cm-s-erlang-dark span.cm-variable { color: #50fe50; } 21 | .cm-s-erlang-dark span.cm-variable-2 { color: #ee00ee; } 22 | -------------------------------------------------------------------------------- /spec/lib/handlers/yaml_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe PufferPages::Handlers::Yaml do 4 | subject { described_class.new(:yaml) } 5 | 6 | let!(:root) { Fabricate :root, name: 'root', page_parts: [ancestors.last] } 7 | let!(:first) { Fabricate :page, slug: 'first', parent: root, page_parts: [ancestors.first] } 8 | let!(:second) { Fabricate :page, slug: 'second', parent: first, page_parts: [page_part] } 9 | let(:page_part) { 10 | Fabricate.build :main, handler: 'yaml', body: YAML.dump(config: { value: 42, array: [1, 2], hash: { foo: 1 } }) 11 | } 12 | let(:ancestors) { 13 | [ 14 | Fabricate.build(:main, handler: 'yaml', body: YAML.dump(config: { array: [4, 5], hash: { bar: 2 } })), 15 | Fabricate.build(:main, handler: 'yaml', body: YAML.dump(config: { value: 33, hash: { bar: 1, baz: 3 } })) 16 | ] 17 | } 18 | 19 | context do 20 | specify { subject.process(page_part).should == 21 | { config: { value: 42, array: [1, 2], hash: { foo: 1, bar: 2, baz: 3 } } } } 22 | end 23 | 24 | context 'error yaml' do 25 | let(:page_part) { 26 | Fabricate.build :main, handler: 'yaml', body: " key: value\n key:value\n key:value" 27 | } 28 | 29 | specify do 30 | expect { subject.process(page_part).should }. 31 | to raise_exception Psych::SyntaxError 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/assets/stylesheets/puffer/codemirror/blackboard.css: -------------------------------------------------------------------------------- 1 | /* Port of TextMate's Blackboard theme */ 2 | 3 | .cm-s-blackboard.CodeMirror { background: #0C1021; color: #F8F8F8; } 4 | .cm-s-blackboard .CodeMirror-selected { background: #253B76 !important; } 5 | .cm-s-blackboard .CodeMirror-gutters { background: #0C1021; border-right: 0; } 6 | .cm-s-blackboard .CodeMirror-linenumber { color: #888; } 7 | .cm-s-blackboard .CodeMirror-cursor { border-left: 1px solid #A7A7A7 !important; } 8 | 9 | .cm-s-blackboard .cm-keyword { color: #FBDE2D; } 10 | .cm-s-blackboard .cm-atom { color: #D8FA3C; } 11 | .cm-s-blackboard .cm-number { color: #D8FA3C; } 12 | .cm-s-blackboard .cm-def { color: #8DA6CE; } 13 | .cm-s-blackboard .cm-variable { color: #FF6400; } 14 | .cm-s-blackboard .cm-operator { color: #FBDE2D;} 15 | .cm-s-blackboard .cm-comment { color: #AEAEAE; } 16 | .cm-s-blackboard .cm-string { color: #61CE3C; } 17 | .cm-s-blackboard .cm-string-2 { color: #61CE3C; } 18 | .cm-s-blackboard .cm-meta { color: #D8FA3C; } 19 | .cm-s-blackboard .cm-error { background: #9D1E15; color: #F8F8F8; } 20 | .cm-s-blackboard .cm-builtin { color: #8DA6CE; } 21 | .cm-s-blackboard .cm-tag { color: #8DA6CE; } 22 | .cm-s-blackboard .cm-attribute { color: #8DA6CE; } 23 | .cm-s-blackboard .cm-header { color: #FF6400; } 24 | .cm-s-blackboard .cm-hr { color: #AEAEAE; } 25 | .cm-s-blackboard .cm-link { color: #8DA6CE; } 26 | -------------------------------------------------------------------------------- /lib/puffer_pages/backends/models/mixins/importable.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Backends 3 | module Mixins 4 | module Importable 5 | extend ActiveSupport::Concern 6 | 7 | module ClassMethods 8 | def export_json 9 | all 10 | end 11 | 12 | def import_destroy 13 | destroy_all 14 | end 15 | 16 | def import_json json 17 | data = json.is_a?(String) ? ActiveSupport::JSON.decode(json) : json.map(&:stringify_keys!) 18 | 19 | import_destroy 20 | 21 | data.each do |attributes| 22 | associations = attributes.keys.each_with_object({}) do |attribute, hsh| 23 | if scoped.reflect_on_association(attribute.to_sym) 24 | hsh[attribute] = attributes.delete(attribute) 25 | end 26 | end 27 | 28 | attributes = attributes.with_indifferent_access 29 | record = scoped.create!(attributes) do |record| 30 | record.id = attributes[:id] if attributes[:id].present? 31 | end 32 | 33 | associations.each do |association, attributes| 34 | record.send(association).import_json(attributes) 35 | end 36 | end 37 | end 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/assets/stylesheets/puffer/codemirror/rubyblue.css: -------------------------------------------------------------------------------- 1 | .cm-s-rubyblue { font:13px/1.4em Trebuchet, Verdana, sans-serif; } /* - customized editor font - */ 2 | 3 | .cm-s-rubyblue.CodeMirror { background: #112435; color: white; } 4 | .cm-s-rubyblue div.CodeMirror-selected { background: #38566F !important; } 5 | .cm-s-rubyblue .CodeMirror-gutters { background: #1F4661; border-right: 7px solid #3E7087; } 6 | .cm-s-rubyblue .CodeMirror-linenumber { color: white; } 7 | .cm-s-rubyblue .CodeMirror-cursor { border-left: 1px solid white !important; } 8 | 9 | .cm-s-rubyblue span.cm-comment { color: #999; font-style:italic; line-height: 1em; } 10 | .cm-s-rubyblue span.cm-atom { color: #F4C20B; } 11 | .cm-s-rubyblue span.cm-number, .cm-s-rubyblue span.cm-attribute { color: #82C6E0; } 12 | .cm-s-rubyblue span.cm-keyword { color: #F0F; } 13 | .cm-s-rubyblue span.cm-string { color: #F08047; } 14 | .cm-s-rubyblue span.cm-meta { color: #F0F; } 15 | .cm-s-rubyblue span.cm-variable-2, .cm-s-rubyblue span.cm-tag { color: #7BD827; } 16 | .cm-s-rubyblue span.cm-variable-3, .cm-s-rubyblue span.cm-def { color: white; } 17 | .cm-s-rubyblue span.cm-error { color: #AF2018; } 18 | .cm-s-rubyblue span.cm-bracket { color: #F0F; } 19 | .cm-s-rubyblue span.cm-link { color: #F4C20B; } 20 | .cm-s-rubyblue span.CodeMirror-matchingbracket { color:#F0F !important; } 21 | .cm-s-rubyblue span.cm-builtin, .cm-s-rubyblue span.cm-special { color: #FF9D00; } 22 | -------------------------------------------------------------------------------- /lib/puffer_pages/liquid/tags/assets.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Liquid 3 | module Tags 4 | 5 | class Assets < ::Liquid::Tag 6 | Syntax = /^(#{::Liquid::QuotedFragment}+)/ 7 | 8 | def initialize(tag_name, markup, tokens) 9 | if markup =~ Syntax 10 | @paths = variables_from_string(markup) 11 | else 12 | raise SyntaxError.new("Syntax Error in '#{tag_name}' - Valid syntax: #{tag_name} path [, path, path ...]") 13 | end 14 | 15 | super 16 | end 17 | 18 | def render(context) 19 | paths = @paths.map {|path| "'#{context[path]}'" }.join(', ') 20 | 21 | erb = case @tag_name 22 | when 'javascripts' 23 | "<%= javascript_include_tag #{paths} %>" 24 | when 'stylesheets' 25 | "<%= stylesheet_link_tag #{paths} %>" 26 | end 27 | 28 | context.registers[:tracker].register(erb) 29 | end 30 | 31 | private 32 | 33 | def variables_from_string(markup) 34 | markup.split(',').map do |var| 35 | var.strip =~ /\s*(#{::Liquid::QuotedFragment})\s*/ 36 | $1 ? $1 : nil 37 | end.compact 38 | end 39 | 40 | end 41 | 42 | end 43 | end 44 | end 45 | 46 | Liquid::Template.register_tag('javascripts', PufferPages::Liquid::Tags::Assets) 47 | Liquid::Template.register_tag('stylesheets', PufferPages::Liquid::Tags::Assets) 48 | -------------------------------------------------------------------------------- /spec/controllers/pages_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe PagesController do 4 | 5 | before { @routes = PufferPages::Engine.routes } 6 | 7 | describe "GET index" do 8 | render_views 9 | 10 | let!(:layout) { Fabricate :application } 11 | let!(:root) { Fabricate :root } 12 | let!(:first) { Fabricate :page, slug: 'first', parent: root } 13 | let!(:second) { Fabricate :page, slug: 'second.css', parent: first } 14 | 15 | describe "proper page rendering" do 16 | it { should render_page root } 17 | specify do 18 | get :index, path: 'first' 19 | response.should render_page first 20 | response.should be_ok 21 | end 22 | specify do 23 | get :index, path: 'first/second.css' 24 | response.should render_page second 25 | response.content_type.should == 'text/css' 26 | response.should be_ok 27 | end 28 | specify do 29 | expect { get :index, path: 'first/second' }.to raise_error PufferPages::MissedPage 30 | response.should_not render_page 31 | response.should_not be_not_found 32 | end 33 | end 34 | 35 | describe "layout loaded from filesystem" do 36 | let!(:root) { Fabricate :root, layout_name: 'sample' } 37 | 38 | context do 39 | specify do 40 | get :index 41 | response.should render_template 'layouts/sample' 42 | end 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /app/components/page_parts/form.html.erb: -------------------------------------------------------------------------------- 1 | <%= component_fields_for @record do |f| %> 2 | 23 | <% end %> 24 | -------------------------------------------------------------------------------- /app/assets/stylesheets/puffer/codemirror/vibrant-ink.css: -------------------------------------------------------------------------------- 1 | /* Taken from the popular Visual Studio Vibrant Ink Schema */ 2 | 3 | .cm-s-vibrant-ink.CodeMirror { background: black; color: white; } 4 | .cm-s-vibrant-ink .CodeMirror-selected { background: #35493c !important; } 5 | 6 | .cm-s-vibrant-ink .CodeMirror-gutters { background: #002240; border-right: 1px solid #aaa; } 7 | .cm-s-vibrant-ink .CodeMirror-linenumber { color: #d0d0d0; } 8 | .cm-s-vibrant-ink .CodeMirror-cursor { border-left: 1px solid white !important; } 9 | 10 | .cm-s-vibrant-ink .cm-keyword { color: #CC7832; } 11 | .cm-s-vibrant-ink .cm-atom { color: #FC0; } 12 | .cm-s-vibrant-ink .cm-number { color: #FFEE98; } 13 | .cm-s-vibrant-ink .cm-def { color: #8DA6CE; } 14 | .cm-s-vibrant-ink span.cm-variable-2, .cm-s-cobalt span.cm-tag { color: #FFC66D } 15 | .cm-s-vibrant-ink span.cm-variable-3, .cm-s-cobalt span.cm-def { color: #FFC66D } 16 | .cm-s-vibrant-ink .cm-operator { color: #888; } 17 | .cm-s-vibrant-ink .cm-comment { color: gray; font-weight: bold; } 18 | .cm-s-vibrant-ink .cm-string { color: #A5C25C } 19 | .cm-s-vibrant-ink .cm-string-2 { color: red } 20 | .cm-s-vibrant-ink .cm-meta { color: #D8FA3C; } 21 | .cm-s-vibrant-ink .cm-error { border-bottom: 1px solid red; } 22 | .cm-s-vibrant-ink .cm-builtin { color: #8DA6CE; } 23 | .cm-s-vibrant-ink .cm-tag { color: #8DA6CE; } 24 | .cm-s-vibrant-ink .cm-attribute { color: #8DA6CE; } 25 | .cm-s-vibrant-ink .cm-header { color: #FF6400; } 26 | .cm-s-vibrant-ink .cm-hr { color: #AEAEAE; } 27 | .cm-s-vibrant-ink .cm-link { color: blue; } 28 | -------------------------------------------------------------------------------- /app/assets/stylesheets/puffer/codemirror/twilight.css: -------------------------------------------------------------------------------- 1 | .cm-s-twilight.CodeMirror { background: #141414; color: #f7f7f7; } /**/ 2 | .cm-s-twilight .CodeMirror-selected { background: #323232 !important; } /**/ 3 | 4 | .cm-s-twilight .CodeMirror-gutters { background: #222; border-right: 1px solid #aaa; } 5 | .cm-s-twilight .CodeMirror-linenumber { color: #aaa; } 6 | .cm-s-twilight .CodeMirror-cursor { border-left: 1px solid white !important; } 7 | 8 | .cm-s-twilight .cm-keyword { color: #f9ee98; } /**/ 9 | .cm-s-twilight .cm-atom { color: #FC0; } 10 | .cm-s-twilight .cm-number { color: #ca7841; } /**/ 11 | .cm-s-twilight .cm-def { color: #8DA6CE; } 12 | .cm-s-twilight span.cm-variable-2, .cm-s-twilight span.cm-tag { color: #607392; } /**/ 13 | .cm-s-twilight span.cm-variable-3, .cm-s-twilight span.cm-def { color: #607392; } /**/ 14 | .cm-s-twilight .cm-operator { color: #cda869; } /**/ 15 | .cm-s-twilight .cm-comment { color:#777; font-style:italic; font-weight:normal; } /**/ 16 | .cm-s-twilight .cm-string { color:#8f9d6a; font-style:italic; } /**/ 17 | .cm-s-twilight .cm-string-2 { color:#bd6b18 } /*?*/ 18 | .cm-s-twilight .cm-meta { background-color:#141414; color:#f7f7f7; } /*?*/ 19 | .cm-s-twilight .cm-error { border-bottom: 1px solid red; } 20 | .cm-s-twilight .cm-builtin { color: #cda869; } /*?*/ 21 | .cm-s-twilight .cm-tag { color: #997643; } /**/ 22 | .cm-s-twilight .cm-attribute { color: #d6bb6d; } /*?*/ 23 | .cm-s-twilight .cm-header { color: #FF6400; } 24 | .cm-s-twilight .cm-hr { color: #AEAEAE; } 25 | .cm-s-twilight .cm-link { color:#ad9361; font-style:italic; text-decoration:none; } /**/ 26 | 27 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Log error messages when you accidentally call methods on nil. 10 | config.whiny_nils = true 11 | 12 | # Show full error reports and disable caching 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger 20 | config.active_support.deprecation = :log 21 | 22 | # Only use best-standards-support built into browsers 23 | config.action_dispatch.best_standards_support = :builtin 24 | 25 | # Raise exception on mass assignment protection for Active Record models 26 | # config.active_record.mass_assignment_sanitizer = :strict 27 | 28 | # Log the query plan for queries taking more than this (works 29 | # with SQLite, MySQL, and PostgreSQL) 30 | # config.active_record.auto_explain_threshold_in_seconds = 0.5 31 | 32 | # Do not compress assets 33 | config.assets.compress = false 34 | 35 | # Expands the lines which load the assets 36 | config.assets.debug = false 37 | 38 | config.puffer_pages.raise_errors = true 39 | end 40 | -------------------------------------------------------------------------------- /spec/lib/liquid/tags/partials_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe PufferPages::Liquid::Tags::Partials do 4 | describe 'snippet' do 5 | let!(:root) { Fabricate :root } 6 | let!(:custom) { Fabricate :custom } 7 | 8 | specify { root.render("{% snippet 'custom' %}").should == custom.body } 9 | specify { root.render("{% assign snippet = 'custom' %}{% snippet snippet %}").should == custom.body } 10 | 11 | context do 12 | let!(:custom) { Fabricate :custom, body: "{{ custom }}" } 13 | specify { root.render("{% snippet 'custom' with 'hello' %}").should == 'hello' } 14 | end 15 | 16 | context do 17 | let!(:custom) { Fabricate :custom, body: "{{ variable }}" } 18 | specify { root.render("{% snippet 'custom', variable: 'hello' %}").should == 'hello' } 19 | end 20 | end 21 | 22 | describe 'layout' do 23 | let!(:root) { Fabricate :root } 24 | let!(:application) { Fabricate :application } 25 | 26 | specify { root.render("{% layout 'application' %}").should == application.body } 27 | specify { root.render("{% assign layout = 'application' %}{% layout layout %}").should == application.body } 28 | 29 | context do 30 | let!(:application) { Fabricate :application, body: "{{ application }}" } 31 | specify { root.render("{% layout 'application' with 'hello' %}").should == 'hello' } 32 | end 33 | 34 | context do 35 | let!(:application) { Fabricate :application, body: "{{ variable }}" } 36 | specify { root.render("{% layout 'application', variable: 'hello' %}").should == 'hello' } 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # Configure Rails Envinronment 2 | ENV["RAILS_ENV"] ||= "test" 3 | 4 | require File.expand_path("../dummy/config/environment.rb", __FILE__) 5 | require "rails/test_help" 6 | require "rspec/rails" 7 | require "puffer_pages/rspec" 8 | 9 | Bundler.require :development 10 | 11 | ActionMailer::Base.delivery_method = :test 12 | ActionMailer::Base.perform_deliveries = true 13 | ActionMailer::Base.default_url_options[:host] = "test.com" 14 | 15 | Rails.backtrace_cleaner.remove_silencers! 16 | 17 | # ActiveRecord::Base.logger = Logger.new(STDOUT) 18 | # Run any available migration 19 | ActiveRecord::Migrator.migrate File.expand_path("../dummy/db/migrate/", __FILE__) 20 | 21 | if ActiveRecord::Base.connection.respond_to? :schema_cache 22 | ActiveRecord::Base.connection.schema_cache.clear! 23 | end 24 | 25 | # Load support files 26 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 27 | Dir["#{File.dirname(__FILE__)}/fabricators/**/*.rb"].each { |f| require f } 28 | 29 | RSpec.configure do |config| 30 | # Remove this line if you don't want RSpec's should and should_not 31 | # methods or matchers 32 | require 'rspec/expectations' 33 | config.include RSpec::Matchers 34 | 35 | # == Mock Framework 36 | config.mock_with :rspec 37 | 38 | config.use_transactional_fixtures = false 39 | 40 | config.before(:suite) do 41 | DatabaseCleaner.clean_with(:truncation) 42 | DatabaseCleaner.strategy = :transaction 43 | end 44 | 45 | config.before(:each) do 46 | DatabaseCleaner.start 47 | end 48 | 49 | config.after(:each) do 50 | DatabaseCleaner.clean 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/puffer_pages/backends/models/origin.rb: -------------------------------------------------------------------------------- 1 | class PufferPages::Backends::Origin < ActiveRecord::Base 2 | self.abstract_class = true 3 | self.table_name = :origins 4 | 5 | attr_protected 6 | 7 | validates_presence_of :name, :host, :token 8 | validates_uniqueness_of :name 9 | 10 | def path 11 | read_attribute(:path).presence || PufferPages.export_path 12 | end 13 | 14 | def uri 15 | parsed_host.merge URI::Generic.build(:path => path, :query => { :token => token }.to_query) 16 | end 17 | 18 | def import! 19 | import_json import_data 20 | end 21 | 22 | def import_data 23 | Net::HTTP.get(uri) 24 | rescue 25 | raise PufferPages::ImportFailed.new("Could not connect to `#{name}` (#{uri})") 26 | end 27 | 28 | def import_json json 29 | data = json.is_a?(String) ? ActiveSupport::JSON.decode(json) : json 30 | ActiveRecord::Base.transaction do 31 | %w(layouts snippets pages).each do |table| 32 | klass = "puffer_pages/#{table}".classify.constantize 33 | klass.import_json data[table] 34 | end 35 | end 36 | end 37 | 38 | def self.export_json 39 | %w(layouts snippets pages).each_with_object({}) do |table, result| 40 | klass = "puffer_pages/#{table}".classify.constantize 41 | result[table] = klass.export_json 42 | end.as_json.to_json 43 | end 44 | 45 | private 46 | 47 | def parsed_host 48 | URI.parse([scheme, real_host].join('://')) 49 | end 50 | 51 | def real_host 52 | host.gsub(/\Ahttps?\:\/\//, '') 53 | end 54 | 55 | def scheme 56 | match = host.match(/\A(https?)\:\/\//) 57 | match ? match[1] : 'http' 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/lib/liquid/tags/include_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe PufferPages::Liquid::Tags::Include do 4 | describe 'include page_part' do 5 | let!(:root) { Fabricate :root, page_parts: [main, sidebar] } 6 | let!(:main) { Fabricate :main } 7 | let!(:sidebar) { Fabricate :sidebar } 8 | 9 | specify { root.render("{% include '#{PufferPages.primary_page_part_name}' %}").should == main.body } 10 | specify { root.render("{% assign sb = 'sidebar' %}{% include sb %}").should == sidebar.body } 11 | end 12 | 13 | describe 'include snippet' do 14 | let!(:root) { Fabricate :root } 15 | let!(:custom) { Fabricate :custom } 16 | 17 | specify { root.render("{% include 'snippets/custom' %}").should == custom.body } 18 | specify { root.render("{% assign snippet = 'snippets/custom' %}{% include snippet %}").should == custom.body } 19 | 20 | context do 21 | let!(:custom) { Fabricate :custom, body: "{{ variable }}" } 22 | specify { root.render("{% include 'snippets/custom', variable: 'hello' %}").should == 'hello' } 23 | end 24 | end 25 | 26 | describe 'include layout' do 27 | let!(:root) { Fabricate :root } 28 | let!(:application) { Fabricate :application } 29 | 30 | specify { root.render("{% include 'layouts/application' %}").should == application.body } 31 | specify { root.render("{% assign layout = 'layouts/application' %}{% include layout %}").should == application.body } 32 | 33 | context do 34 | let!(:application) { Fabricate :application, body: "{{ variable }}" } 35 | specify { root.render("{% include 'layouts/application', variable: 'hello' %}").should == 'hello' } 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/puffer_pages/backends/controllers/pages_base.rb: -------------------------------------------------------------------------------- 1 | class PufferPages::PagesBase < Puffer::TreeBase 2 | helper :puffer_pages 3 | 4 | setup do 5 | group :pages 6 | model_name :'puffer_pages/page' 7 | end 8 | 9 | tree do 10 | #field :name, :render => :tree_page 11 | field :name, render: -> { render :partial => 'tree_page', :object => record } 12 | end 13 | 14 | def new 15 | @record = resource.new_member 16 | if !@record.inherited_page_part(PufferPages.primary_page_part_name) 17 | @record.page_parts.build :name => PufferPages.primary_page_part_name 18 | end 19 | respond_with @record 20 | end 21 | 22 | index do 23 | field :name 24 | field :slug 25 | field :layout_name 26 | field :status 27 | end 28 | 29 | filter do 30 | field :name 31 | field :slug 32 | field :layout_name 33 | field :locales 34 | field 'page_parts.name' 35 | field 'page_parts.body' 36 | end 37 | 38 | form do 39 | field :parent_id, type: :hidden 40 | field :name 41 | field :slug 42 | field :layout_name, select: :possible_layouts, include_blank: false 43 | field :status, select: :possible_statuses, include_blank: false 44 | field :page_parts, type: :page_parts do 45 | field :handler, type: :handlers, include_blank: false, 46 | html: { 'data-codemirror-mode-select' => true } 47 | field :body, type: :codemirror, input_only: true, mode: 'text/x-liquid-html' 48 | field :name, type: :hidden, html: { data: { acts: 'name' } } 49 | field :_destroy, type: :hidden, html: { data: { acts: 'destroy' } } 50 | end 51 | field :locales, type: :codemirror, mode: 'text/x-yaml' 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /puffer_pages.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "puffer_pages/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "puffer_pages" 7 | s.version = PufferPages::VERSION 8 | s.authors = ["pyromaniac"] 9 | s.email = ["kinwizard@gmail.com"] 10 | s.homepage = "http://github.com/puffer/puffer_pages" 11 | s.summary = %q{Content Management System} 12 | s.description = %q{Puffer pages is integratable rails CMS with puffer admin interface} 13 | 14 | s.rubyforge_project = "puffer_pages" 15 | 16 | s.files = `git ls-files`.split("\n") 17 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 18 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 19 | s.require_paths = ["lib"] 20 | 21 | # specify any dependencies here; for example: 22 | s.add_runtime_dependency "rails", ">= 3.1" 23 | s.add_runtime_dependency "puffer" 24 | s.add_runtime_dependency "liquid" 25 | s.add_runtime_dependency "nested_set" 26 | s.add_runtime_dependency "activeuuid", ">= 0.4.0" 27 | s.add_runtime_dependency "contextuality" 28 | 29 | s.add_development_dependency "bcrypt-ruby" 30 | s.add_development_dependency "sqlite3" 31 | s.add_development_dependency "pg" 32 | s.add_development_dependency "mysql2" 33 | s.add_development_dependency "globalize3" 34 | s.add_development_dependency "rspec-rails" 35 | s.add_development_dependency "shoulda" 36 | s.add_development_dependency "database_cleaner" 37 | s.add_development_dependency "forgery" 38 | s.add_development_dependency "fabrication" 39 | s.add_development_dependency "fakeweb" 40 | s.add_development_dependency "timecop" 41 | end 42 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/pg_test.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Configure static asset server for tests with Cache-Control for performance 11 | config.serve_static_assets = true 12 | config.static_cache_control = "public, max-age=3600" 13 | 14 | # Log error messages when you accidentally call methods on nil 15 | config.whiny_nils = true 16 | 17 | # Show full error reports and disable caching 18 | config.consider_all_requests_local = true 19 | config.action_controller.perform_caching = false 20 | 21 | # Raise exceptions instead of rendering exception templates 22 | config.action_dispatch.show_exceptions = false 23 | 24 | # Disable request forgery protection in test environment 25 | config.action_controller.allow_forgery_protection = false 26 | 27 | # Tell Action Mailer not to deliver emails to the real world. 28 | # The :test delivery method accumulates sent emails in the 29 | # ActionMailer::Base.deliveries array. 30 | config.action_mailer.delivery_method = :test 31 | 32 | # Raise exception on mass assignment protection for Active Record models 33 | # config.active_record.mass_assignment_sanitizer = :strict 34 | 35 | # Print deprecation notices to the stderr 36 | config.active_support.deprecation = :stderr 37 | 38 | config.puffer_pages.raise_errors = true 39 | end 40 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Configure static asset server for tests with Cache-Control for performance 11 | config.serve_static_assets = true 12 | config.static_cache_control = "public, max-age=3600" 13 | 14 | # Log error messages when you accidentally call methods on nil 15 | config.whiny_nils = true 16 | 17 | # Show full error reports and disable caching 18 | config.consider_all_requests_local = true 19 | config.action_controller.perform_caching = false 20 | 21 | # Raise exceptions instead of rendering exception templates 22 | config.action_dispatch.show_exceptions = false 23 | 24 | # Disable request forgery protection in test environment 25 | config.action_controller.allow_forgery_protection = false 26 | 27 | # Tell Action Mailer not to deliver emails to the real world. 28 | # The :test delivery method accumulates sent emails in the 29 | # ActionMailer::Base.deliveries array. 30 | config.action_mailer.delivery_method = :test 31 | 32 | # Raise exception on mass assignment protection for Active Record models 33 | # config.active_record.mass_assignment_sanitizer = :strict 34 | 35 | # Print deprecation notices to the stderr 36 | config.active_support.deprecation = :stderr 37 | 38 | config.puffer_pages.raise_errors = true 39 | end 40 | -------------------------------------------------------------------------------- /app/components/codemirror/form.html.erb: -------------------------------------------------------------------------------- 1 | <%= component_fields_for @record do |f| %> 2 | <% unless field.options[:input_only] %> 3 |
4 | <%= f.label field %> 5 |
6 | <%= @record.errors[field.name.to_sym].first %> 7 |
8 |
9 | <% end %> 10 | <% if @record.respond_to? "#{field}_translations" %> 11 | 27 | <% else %> 28 |
29 | <%= f.text_area field, field.input_options.merge(data: { codemirror: { mode: field.options[:mode] || 'text/html' } }) %> 30 |
31 | <% end %> 32 | <% end %> 33 | -------------------------------------------------------------------------------- /lib/puffer_pages/liquid/page_drop.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Liquid 3 | class PageDrop < ::Liquid::Drop 4 | 5 | delegate *(PufferPages::Page.statuses.map {|s| "#{s}?"} << {:to => :page}) 6 | delegate :id, :slug, :created_at, :updated_at, :to => :page 7 | 8 | def initialize page 9 | @page = page 10 | end 11 | 12 | def name 13 | @context ? page.render(page.name, @context) : page.name 14 | end 15 | 16 | %w(parent root ancestors self_and_ancestors children self_and_children siblings 17 | self_and_siblings descendants, self_and_descendants).each do |attribute| 18 | define_method attribute do 19 | instance_variable_get("@#{attribute}") || 20 | instance_variable_set("@#{attribute}", page.send(attribute)) 21 | end 22 | end 23 | 24 | def path 25 | controller.puffer_pages.puffer_page_path page.to_location 26 | end 27 | 28 | def url 29 | controller.puffer_pages.puffer_page_url page.to_location 30 | end 31 | 32 | def current? 33 | current_page && page == current_page 34 | end 35 | 36 | def ancestor? 37 | current_page && page.is_ancestor_of?(current_page) 38 | end 39 | 40 | def == other 41 | page == other.send(:page) 42 | end 43 | 44 | def before_method name 45 | page_part = page.inherited_page_part(name) 46 | page_part.handle(@context) if page_part && @context 47 | end 48 | 49 | private 50 | attr_reader :page 51 | 52 | def current_page 53 | @current_page ||= @context.registers[:page] if @context 54 | end 55 | 56 | def controller 57 | @controller ||= @context.registers[:controller] if @context 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/data/import.json: -------------------------------------------------------------------------------- 1 | { 2 | "layouts": [ 3 | { 4 | "body":"Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", 5 | "created_at":"2012-10-10T12:30:09Z", 6 | "id":"2822BCAA21D24B3187C76C6C41E685F2", 7 | "name":"layout", 8 | "updated_at":"2012-10-10T12:30:09Z" 9 | }, 10 | { 11 | "body":"Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", 12 | "created_at":"2012-10-10T12:30:09Z","id":"520ACC91563B4F75816770C99038D67D", 13 | "name":"layout1","updated_at":"2012-10-10T12:30:09Z" 14 | } 15 | ], 16 | "snippets": [ 17 | { 18 | "body":"Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", 19 | "created_at":"2012-10-10T12:30:09Z", 20 | "id":"64AE0A360FD14E70AA0F9FB33C66D75B", 21 | "name":"lorem", 22 | "updated_at":"2012-10-10T12:30:09Z" 23 | } 24 | ], 25 | "pages": [ 26 | { 27 | "created_at":"2012-10-10T12:30:09Z", 28 | "id":"A080B50AF20B4F59B7008977201F1C1F", 29 | "layout_name":"layout", 30 | "name":"Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", 31 | "parent_id":null, 32 | "slug":null, 33 | "status":"published", 34 | "updated_at":"2012-10-10T12:30:09Z", 35 | "page_parts":[ 36 | { 37 | "body":null, 38 | "created_at":"2012-10-10T12:30:09Z", 39 | "id":"4B3086FFEBD4491F9D2401F324D49CBF", 40 | "name":"body", 41 | "page_id":"A080B50AF20B4F59B7008977201F1C1F", 42 | "updated_at":"2012-10-10T12:30:09Z" 43 | }, 44 | { 45 | "body":null, 46 | "created_at":"2012-10-10T12:30:09Z", 47 | "id":"F6423DC0AD074C9B979F6E90293817E4", 48 | "name":"main", 49 | "page_id":"A080B50AF20B4F59B7008977201F1C1F", 50 | "updated_at":"2012-10-10T12:30:09Z" 51 | } 52 | ] 53 | } 54 | ] 55 | } -------------------------------------------------------------------------------- /lib/puffer_pages/liquid/tags/url.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Liquid 3 | module Tags 4 | 5 | class Url < ::Liquid::Tag 6 | Syntax = /^(#{::Liquid::VariableSignature}+)\s*(.*)\s*/ 7 | 8 | def initialize(tag_name, markup, tokens) 9 | if markup =~ Syntax 10 | 11 | @helper_name, @arguments, @attributes = $1, [], {} 12 | 13 | @arguments = $2.split(',') 14 | attributes = @arguments.pop if @arguments.last && @arguments.last.strip =~ /(#{::Liquid::TagAttributes})/ 15 | 16 | @arguments = @arguments.map do |var| 17 | var.strip =~ /(#{::Liquid::QuotedFragment})/ 18 | $1 ? $1 : nil 19 | end.compact 20 | 21 | attributes.scan(::Liquid::TagAttributes) do |key, value| 22 | @attributes[key] = value 23 | end if attributes.present? 24 | else 25 | raise SyntaxError.new("Error in tag '#{tag_name}' - Valid syntax: #{tag_name} helper_name [object, object...] [, option:value option:value...]") 26 | end 27 | 28 | super 29 | end 30 | 31 | def render(context) 32 | key = context[@key] 33 | arguments = @arguments.map do |argument| 34 | argument = context[argument] 35 | argument.to_param if argument.is_a?(::Liquid::Drop) 36 | argument 37 | end 38 | attributes = @attributes.each_with_object({}) do |(name, value), result| 39 | result[name.to_sym] = context[value] 40 | end 41 | attributes.merge(:path_only => true) if @tag_name == 'path' 42 | 43 | context.registers[:controller].send("#{@helper_name}_#{@tag_name}", *arguments, attributes) 44 | end 45 | 46 | end 47 | 48 | end 49 | end 50 | end 51 | 52 | Liquid::Template.register_tag('path', PufferPages::Liquid::Tags::Url) 53 | Liquid::Template.register_tag('url', PufferPages::Liquid::Tags::Url) 54 | -------------------------------------------------------------------------------- /lib/puffer_pages/backends/models/mixins/localable.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Backends 3 | module Mixins 4 | module Localable 5 | extend ActiveSupport::Concern 6 | 7 | included do 8 | serialize :locales, Locales 9 | 10 | validate do 11 | errors[:locales] = locales.error unless locales.valid? 12 | end 13 | end 14 | 15 | def locales 16 | value = read_attribute(:locales) 17 | value.is_a?(Locales) ? value : Locales.new(value) 18 | end 19 | 20 | class Locales < ActiveSupport::HashWithIndifferentAccess 21 | def initialize source 22 | update (source.presence || {}).stringify_keys 23 | super() 24 | end 25 | 26 | def valid? 27 | !error 28 | end 29 | 30 | def error 31 | translations 32 | rescue ::SyntaxError => e 33 | e.message 34 | else 35 | nil 36 | end 37 | 38 | def translations 39 | @translations ||= Hash[map do |(locale, yaml)| 40 | load_arguments = [yaml] 41 | load_arguments.push "<#{locale}>" if YAML.method(:load).arity == -2 42 | result = YAML.load(*load_arguments).presence || {} 43 | raise ::SyntaxError.new("(<#{locale}>): Locale should be a hash") unless result.is_a?(Hash) 44 | [locale.to_sym, result.deep_symbolize_keys] 45 | end] 46 | end 47 | 48 | class << self 49 | def dump value 50 | value = value.is_a?(Locales) ? value : Locales.new(value) 51 | value ? YAML.dump(value.to_hash) : nil 52 | end 53 | 54 | def load value 55 | return unless value 56 | value = YAML.load(value) if value.is_a?(String) 57 | value.is_a?(Locales) ? value : Locales.new(value) 58 | end 59 | end 60 | end 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/puffer_pages/backends/models/page_part.rb: -------------------------------------------------------------------------------- 1 | class PufferPages::Backends::PagePart < ActiveRecord::Base 2 | include ActiveUUID::UUID 3 | include PufferPages::Backends::Mixins::Renderable 4 | include PufferPages::Backends::Mixins::Importable 5 | include PufferPages::Backends::Mixins::Translatable 6 | self.abstract_class = true 7 | self.table_name = :page_parts 8 | 9 | attr_protected 10 | 11 | default_scope ->{ includes :translations } if PufferPages.localize 12 | 13 | validates_presence_of :name 14 | validates_uniqueness_of :name, :scope => :page_id 15 | 16 | belongs_to :page, :class_name => '::PufferPages::Page', :inverse_of => :page_parts 17 | 18 | before_validation :defaultize_attributes 19 | def defaultize_attributes 20 | self.handler ||= 'html' 21 | end 22 | 23 | def main? 24 | name == PufferPages.primary_page_part_name 25 | end 26 | 27 | def parent 28 | ancestors.first 29 | end 30 | 31 | def ancestors 32 | page.ancestors_page_parts.where(name: name) 33 | end 34 | 35 | def self_and_ancestors 36 | page.self_and_ancestors_page_parts.where(name: name) 37 | end 38 | 39 | def render *args 40 | _, context = normalize_render_options *args 41 | render_template body, context, additional_render_options 42 | end 43 | 44 | def handle *args 45 | _, context = normalize_render_options *args 46 | PufferPages::Handlers.process handler || 'html', self, context 47 | end 48 | 49 | def additional_render_options 50 | { environment: { processed: self } } 51 | end 52 | 53 | def i18n_scope 54 | i18n_scope_for page.segments, :page_parts, name 55 | end 56 | 57 | def i18n_defaults 58 | page.segments.inject([]) do |memo, element| 59 | memo.push (memo.last || []).dup.push(element) 60 | end.unshift([]).inject([]) do |memo, segments| 61 | memo.unshift i18n_scope_for(segments) 62 | memo.unshift i18n_scope_for(segments, :page_parts, name) 63 | end 64 | end 65 | 66 | private 67 | 68 | def i18n_scope_for *segments 69 | [:pages, *segments.flatten.map(&:to_sym)] 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/puffer_pages/liquid/tags/translate.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Liquid 3 | module Tags 4 | 5 | class Translate < ::Liquid::Tag 6 | Syntax = /^(#{::Liquid::QuotedFragment})/ 7 | 8 | def initialize(tag_name, markup, tokens) 9 | if markup =~ Syntax 10 | @key = $1 11 | else 12 | raise SyntaxError.new("Syntax Error in 'translate' - Valid syntax: translate key") 13 | end 14 | 15 | @options = {} 16 | markup.scan(::Liquid::TagAttributes) do |key, value| 17 | @options[key.to_sym] = value 18 | end 19 | 20 | super 21 | end 22 | 23 | def render(context) 24 | key = context[@key] 25 | options = @options.each_with_object({}) do |(name, value), result| 26 | result[name] = context[value] unless I18n::RESERVED_KEYS.include?(name) 27 | end 28 | processed = context[:processed] 29 | 30 | if options[:count].is_a?(String) 31 | begin 32 | options[:count] = (options[:count] =~ /\./ ? Float(options[:count]) : Integer(options[:count])) 33 | rescue ArgumentError 34 | end 35 | end 36 | 37 | if processed && key.first == '.' 38 | I18n.translate i18n_key(processed, key.last(-1)), 39 | options.merge!(:default => i18n_defaults(processed, key.last(-1))) 40 | else 41 | I18n.translate key, options 42 | end 43 | end 44 | 45 | def i18n_key(processed, key) 46 | array_to_key processed.i18n_scope, key 47 | end 48 | 49 | def i18n_defaults(processed, key) 50 | processed.i18n_defaults.map { |default| array_to_key default, key } 51 | end 52 | 53 | def array_to_key *array 54 | array.flatten.map { |segment| segment.to_s.gsub(?., ?/) }.join(?.).to_sym 55 | end 56 | end 57 | 58 | end 59 | end 60 | end 61 | 62 | Liquid::Template.register_tag('translate', PufferPages::Liquid::Tags::Translate) 63 | Liquid::Template.register_tag('t', PufferPages::Liquid::Tags::Translate) 64 | -------------------------------------------------------------------------------- /app/assets/stylesheets/puffer/puffer_pages.css: -------------------------------------------------------------------------------- 1 | //= require puffer/codemirror 2 | //= require_tree ./codemirror 3 | //= require_self 4 | 5 | .cm-translation { 6 | color: #a11; 7 | border-bottom: 1px dashed #a11; 8 | } 9 | 10 | .cm-s-default { 11 | background: #fff; 12 | } 13 | 14 | .cm-liquid-tag { 15 | color: #0089b7; 16 | } 17 | 18 | .cm-liquid-variable { 19 | color: #0089b7; 20 | } 21 | 22 | .cm-tab { 23 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAYAAAAkuj5RAAAAAXNSR0IArs4c6QAAAGFJREFUSMft1LsRQFAQheHPowAKoACx3IgEKtaEHujDjORSgWTH/ZOdnZOcM/sgk/kFFWY0qV8foQwS4MKBCS3qR6ixBJvElOobYAtivseIE120FaowJPN75GMu8j/LfMwNjh4HUpwg4LUAAAAASUVORK5CYII=); 24 | background-position: right; 25 | background-repeat: no-repeat; 26 | } 27 | 28 | div.CodeMirror span.CodeMirror-matchingbracket { 29 | color: #e80000; 30 | } 31 | 32 | .handler { 33 | padding: 5px 10px; 34 | } 35 | 36 | .rui-tabs li { 37 | margin-bottom: 0; 38 | } 39 | 40 | .rui-tabs .rui-tabs-panel { 41 | padding: 0; 42 | border-left: 1px solid #ccc; 43 | border-right: 1px solid #ccc; 44 | height: 100%; 45 | } 46 | 47 | .rui-tabs .locales { 48 | border: 0; 49 | } 50 | 51 | .rui-tabs .locales .rui-tabs-panel { 52 | border: 0; 53 | } 54 | 55 | .codemirror_wrapper { 56 | border: 1px solid #CCC; 57 | } 58 | 59 | .rui-tabs-panel .codemirror_wrapper { 60 | border: 0; 61 | } 62 | 63 | *[data-fullscreen='true'] { 64 | background-color: #fff !important; 65 | display: block !important; 66 | position: fixed !important; 67 | top: 0 !important; 68 | left: 0 !important; 69 | width: 100% !important; 70 | height: 100% !important; 71 | z-index: 9999 !important; 72 | margin: 0 !important; 73 | padding: 0 !important; 74 | } 75 | 76 | *[data-fullscreen='true'] .codemirror_wrapper { 77 | height: 100% !important; 78 | } 79 | 80 | *[data-fullscreen='true'] .CodeMirror { 81 | height: 100% !important; 82 | } 83 | 84 | *[data-fullscreen='true'] .rui-tabs { 85 | height: 100% !important; 86 | } 87 | 88 | *[data-fullscreen='true'] .rui-tabs-panel { 89 | height: 100% !important; 90 | height: -webkit-calc(100% - 28px) !important; 91 | height: calc(100% - 28px) !important; 92 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/puffer/codemirror/lesser-dark.css: -------------------------------------------------------------------------------- 1 | /* 2 | http://lesscss.org/ dark theme 3 | Ported to CodeMirror by Peter Kroon 4 | */ 5 | .cm-s-lesser-dark { 6 | line-height: 1.3em; 7 | } 8 | .cm-s-lesser-dark { 9 | font-family: 'Bitstream Vera Sans Mono', 'DejaVu Sans Mono', 'Monaco', Courier, monospace !important; 10 | } 11 | 12 | .cm-s-lesser-dark.CodeMirror { background: #262626; color: #EBEFE7; text-shadow: 0 -1px 1px #262626; } 13 | .cm-s-lesser-dark div.CodeMirror-selected {background: #45443B !important;} /* 33322B*/ 14 | .cm-s-lesser-dark .CodeMirror-cursor { border-left: 1px solid white !important; } 15 | .cm-s-lesser-dark pre { padding: 0 8px; }/*editable code holder*/ 16 | 17 | div.CodeMirror span.CodeMirror-matchingbracket { color: #7EFC7E; }/*65FC65*/ 18 | 19 | .cm-s-lesser-dark .CodeMirror-gutters { background: #262626; border-right:1px solid #aaa; } 20 | .cm-s-lesser-dark .CodeMirror-linenumber { color: #777; } 21 | 22 | .cm-s-lesser-dark span.cm-keyword { color: #599eff; } 23 | .cm-s-lesser-dark span.cm-atom { color: #C2B470; } 24 | .cm-s-lesser-dark span.cm-number { color: #B35E4D; } 25 | .cm-s-lesser-dark span.cm-def {color: white;} 26 | .cm-s-lesser-dark span.cm-variable { color:#D9BF8C; } 27 | .cm-s-lesser-dark span.cm-variable-2 { color: #669199; } 28 | .cm-s-lesser-dark span.cm-variable-3 { color: white; } 29 | .cm-s-lesser-dark span.cm-property {color: #92A75C;} 30 | .cm-s-lesser-dark span.cm-operator {color: #92A75C;} 31 | .cm-s-lesser-dark span.cm-comment { color: #666; } 32 | .cm-s-lesser-dark span.cm-string { color: #BCD279; } 33 | .cm-s-lesser-dark span.cm-string-2 {color: #f50;} 34 | .cm-s-lesser-dark span.cm-meta { color: #738C73; } 35 | .cm-s-lesser-dark span.cm-error { color: #9d1e15; } 36 | .cm-s-lesser-dark span.cm-qualifier {color: #555;} 37 | .cm-s-lesser-dark span.cm-builtin { color: #ff9e59; } 38 | .cm-s-lesser-dark span.cm-bracket { color: #EBEFE7; } 39 | .cm-s-lesser-dark span.cm-tag { color: #669199; } 40 | .cm-s-lesser-dark span.cm-attribute {color: #00c;} 41 | .cm-s-lesser-dark span.cm-header {color: #a0a;} 42 | .cm-s-lesser-dark span.cm-quote {color: #090;} 43 | .cm-s-lesser-dark span.cm-hr {color: #999;} 44 | .cm-s-lesser-dark span.cm-link {color: #00c;} 45 | -------------------------------------------------------------------------------- /spec/models/puffer_pages/localable_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | require 'spec_helper' 3 | 4 | describe PufferPages::Backends::Mixins::Localable do 5 | let(:locales_class) { PufferPages::Backends::Mixins::Localable::Locales } 6 | 7 | context do 8 | let!(:page) { Fabricate.build :root } 9 | specify { page.locales.should == {} } 10 | specify { page.locales.should be_a locales_class } 11 | end 12 | 13 | context do 14 | let!(:page) { Fabricate :root } 15 | specify { page.locales.should == {} } 16 | specify { page.reload.locales.should == {} } 17 | specify { page.locales.should be_a locales_class } 18 | specify { page.reload.locales.should be_a locales_class } 19 | end 20 | 21 | context do 22 | let!(:page) { Fabricate :root, locales: { en: 'hello: world' } } 23 | specify { page.locales.should == { 'en' => 'hello: world' } } 24 | specify { page.reload.locales.should == { 'en' => 'hello: world' } } 25 | specify { page.locales.should be_a locales_class } 26 | specify { page.reload.locales.should be_a locales_class } 27 | end 28 | 29 | context do 30 | let!(:page) { Fabricate.build :root, locales: { en: 'Hello' } } 31 | specify { page.should be_invalid } 32 | specify { page.tap { |page| page.valid? }.errors[:locales].first.should == page.locales.error } 33 | end 34 | end 35 | 36 | describe PufferPages::Backends::Mixins::Localable::Locales do 37 | let(:valid) { 38 | described_class.new( 39 | en: YAML.dump(hello: 'World', bye: 'Hell'), 40 | de: YAML.dump('key' => 'value') 41 | ) 42 | } 43 | let(:invalid) { 44 | described_class.new( 45 | en: YAML.dump('hello') 46 | ) 47 | } 48 | describe '#translations' do 49 | specify { valid.translations.should == { en: { hello: 'World', bye: 'Hell' }, de: { key: 'value' } } } 50 | specify { expect { invalid.translations }.to raise_exception SyntaxError } 51 | end 52 | 53 | describe '#valid?' do 54 | specify { valid.should be_valid } 55 | specify { invalid.should_not be_valid } 56 | end 57 | 58 | describe '#valid?' do 59 | specify { valid.error.should be_false } 60 | specify { invalid.error.should =~ /Locale should be a hash/ } 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/puffer_pages/backends/models/mixins/translatable.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Backends 3 | module Mixins 4 | module Translatable 5 | extend ActiveSupport::Concern 6 | 7 | module ClassMethods 8 | def translatable *fields 9 | if PufferPages.localize 10 | translates *fields, fallbacks_for_empty_translations: true 11 | translation_class.send(:include, ActiveUUID::UUID) 12 | 13 | def self.globalize_migrator 14 | @globalize_migrator ||= PufferPages::Globalize::Migrator.new(self) 15 | end 16 | 17 | fields.each do |field| 18 | define_method "#{field}_translations" do 19 | result = translations.each_with_object(HashWithIndifferentAccess.new) do |translation, result| 20 | result[translation.locale] = translation.send(field) 21 | end 22 | globalize.stash.keys.each_with_object(result) do |locale, result| 23 | result[locale] = globalize.stash.read(locale, field) if globalize.stash.contains?(locale, field) 24 | end 25 | end 26 | 27 | define_method "#{field}_translations=" do |value| 28 | value.each do |(locale, value)| 29 | write_attribute(field, value, locale: locale) 30 | end 31 | end 32 | end 33 | 34 | define_method :serializable_hash_with_translations do |options = nil| 35 | options ||= {} 36 | except = Array.wrap(options[:except]) 37 | options[:except] = except + 38 | self.class.translated_attribute_names.map(&:to_s) 39 | methods = Array.wrap(options[:methods]) 40 | options[:methods] = methods + 41 | self.class.translated_attribute_names.map { |name| "#{name}_translations" } 42 | serializable_hash_without_translations options 43 | end 44 | 45 | alias_method_chain :serializable_hash, :translations 46 | end 47 | end 48 | end 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.routes.draw do 2 | 3 | namespace :admin do 4 | resources :pages 5 | resources :layouts 6 | resources :snippets 7 | resources :origins 8 | resources :articles 9 | end 10 | 11 | mount PufferPages::Engine => '/' 12 | 13 | # The priority is based upon order of creation: 14 | # first created -> highest priority. 15 | 16 | # Sample of regular route: 17 | # match 'products/:id' => 'catalog#view' 18 | # Keep in mind you can assign values other than :controller and :action 19 | 20 | # Sample of named route: 21 | # match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase 22 | # This route can be invoked with purchase_url(:id => product.id) 23 | 24 | # Sample resource route (maps HTTP verbs to controller actions automatically): 25 | # resources :products 26 | 27 | # Sample resource route with options: 28 | # resources :products do 29 | # member do 30 | # get 'short' 31 | # post 'toggle' 32 | # end 33 | # 34 | # collection do 35 | # get 'sold' 36 | # end 37 | # end 38 | 39 | # Sample resource route with sub-resources: 40 | # resources :products do 41 | # resources :comments, :sales 42 | # resource :seller 43 | # end 44 | 45 | # Sample resource route with more complex sub-resources 46 | # resources :products do 47 | # resources :comments 48 | # resources :sales do 49 | # get 'recent', :on => :collection 50 | # end 51 | # end 52 | 53 | # Sample resource route within a namespace: 54 | # namespace :admin do 55 | # # Directs /admin/products/* to Admin::ProductsController 56 | # # (app/controllers/admin/products_controller.rb) 57 | # resources :products 58 | # end 59 | 60 | # You can have the root of your site routed with "root" 61 | # just remember to delete public/index.html. 62 | # root :to => 'welcome#index' 63 | 64 | # See how all your routes lay out with "rake routes" 65 | 66 | # This is a legacy wild controller route that's not recommended for RESTful applications. 67 | # Note: This route will make all actions in every controller accessible via GET requests. 68 | # match ':controller(/:action(/:id(.:format)))' 69 | end 70 | -------------------------------------------------------------------------------- /lib/puffer_pages/liquid/file_system.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Liquid 3 | class FileSystem < ::Liquid::BlankFileSystem 4 | 5 | def read_template_file template_path, context 6 | source = case template_type(template_path) 7 | when :snippet then 8 | template_path = template_path.gsub(/^snippets\//, '') 9 | snippet = snippet(template_path) 10 | raise ::Liquid::FileSystemError, 11 | "No such snippet '#{template_path}' found" unless snippet 12 | snippet 13 | when :layout then 14 | template_path = template_path.gsub(/^layouts\//, '') 15 | layout = layout(template_path) 16 | raise ::Liquid::FileSystemError, 17 | "No such layout '#{template_path}' found" unless layout 18 | layout 19 | when :super then 20 | page_part = context[:processed] 21 | raise ::Liquid::FileSystemError, 22 | "Can not render super page_part outside the page_part context" unless page_part.is_a?(PufferPages::PagePart) 23 | parent_part = page_part.parent 24 | raise ::Liquid::FileSystemError, 25 | "No super page_part found for #{page_part.name}" unless parent_part 26 | parent_part 27 | when :page_part then 28 | page_part = context.registers[:page].inherited_page_part(template_path) 29 | raise ::Liquid::FileSystemError, 30 | "No such page_part '#{template_path}' found for current page" unless page_part 31 | page_part 32 | end 33 | 34 | source.respond_to?(:render) ? source : ::Liquid::Template.parse(source) 35 | end 36 | 37 | def template_type template_path 38 | return :super if template_path == :super 39 | return :layout if template_path.start_with? 'layouts/' 40 | return :snippet if template_path.start_with? 'snippets/' 41 | return :page_part 42 | end 43 | 44 | def snippet name 45 | @snippets_cache ||= {} 46 | @snippets_cache[name] ||= PufferPages::Snippet.find_snippet(name) 47 | end 48 | 49 | def layout name 50 | @layouts_cache ||= {} 51 | @layouts_cache[name] ||= PufferPages::Layout.find_layout(name) 52 | end 53 | 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /app/assets/stylesheets/puffer/codemirror/xq-dark.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2011 by MarkLogic Corporation 3 | Author: Mike Brevoort 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | */ 23 | .cm-s-xq-dark.CodeMirror { background: #0a001f; color: #f8f8f8; } 24 | .cm-s-xq-dark span.CodeMirror-selected { background: #a8f !important; } 25 | .cm-s-xq-dark .CodeMirror-gutters { background: #0a001f; border-right: 1px solid #aaa; } 26 | .cm-s-xq-dark .CodeMirror-linenumber { color: #f8f8f8; } 27 | .cm-s-xq-dark .CodeMirror-cursor { border-left: 1px solid white !important; } 28 | 29 | .cm-s-xq-dark span.cm-keyword {color: #FFBD40;} 30 | .cm-s-xq-dark span.cm-atom {color: #6C8CD5;} 31 | .cm-s-xq-dark span.cm-number {color: #164;} 32 | .cm-s-xq-dark span.cm-def {color: #FFF; text-decoration:underline;} 33 | .cm-s-xq-dark span.cm-variable {color: #FFF;} 34 | .cm-s-xq-dark span.cm-variable-2 {color: #EEE;} 35 | .cm-s-xq-dark span.cm-variable-3 {color: #DDD;} 36 | .cm-s-xq-dark span.cm-property {} 37 | .cm-s-xq-dark span.cm-operator {} 38 | .cm-s-xq-dark span.cm-comment {color: gray;} 39 | .cm-s-xq-dark span.cm-string {color: #9FEE00;} 40 | .cm-s-xq-dark span.cm-meta {color: yellow;} 41 | .cm-s-xq-dark span.cm-error {color: #f00;} 42 | .cm-s-xq-dark span.cm-qualifier {color: #FFF700;} 43 | .cm-s-xq-dark span.cm-builtin {color: #30a;} 44 | .cm-s-xq-dark span.cm-bracket {color: #cc7;} 45 | .cm-s-xq-dark span.cm-tag {color: #FFBD40;} 46 | .cm-s-xq-dark span.cm-attribute {color: #FFF700;} 47 | -------------------------------------------------------------------------------- /lib/puffer_pages/liquid/tags/cache.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Liquid 3 | module Tags 4 | 5 | class Cache < ::Liquid::Block 6 | TIME_FORMATS = { 7 | /\A([0-9\.]+)s?\Z/ => 1, 8 | /\A([0-9\.]+)m\Z/ => 60, 9 | /\A([0-9\.]+)h\Z/ => 60*60, 10 | /\A([0-9\.]+)d\Z/ => 60*60*24 11 | } 12 | 13 | def initialize(tag_name, markup, tokens) 14 | arguments = markup.split(?,) 15 | options = arguments.pop if arguments.last && arguments.last.strip =~ /(#{::Liquid::TagAttributes})/ 16 | 17 | @arguments = arguments.map do |var| 18 | var.strip =~ /(#{::Liquid::QuotedFragment})/ 19 | $1 ? $1 : nil 20 | end.compact 21 | 22 | @options = {} 23 | options.scan(::Liquid::TagAttributes) do |key, value| 24 | @options[key] = value 25 | end if options 26 | 27 | super 28 | end 29 | 30 | def render(context) 31 | arguments = @arguments.map { |value| context[value] } 32 | 33 | options = @options.each_with_object({}) do |(key, value), result| 34 | result[key] = context[value] 35 | end 36 | options = { 37 | expires_in: expires_in(options['expires_in']) 38 | }.reject { |k, v| v.nil? } 39 | 40 | key = cache_key arguments.unshift(context[:processed]) 41 | 42 | if cache? 43 | context.registers[:tracker].register(cache_store.fetch(key, options) do 44 | context.registers[:tracker].cleanup super 45 | end) 46 | else 47 | super 48 | end 49 | end 50 | 51 | def expires_in expiration 52 | fragments = expiration.to_s.split(' ').map(&:strip) 53 | times = fragments.map do |fragment| 54 | TIME_FORMATS.inject(nil) do |result, (format, multiplier)| 55 | break result if result 56 | match = fragment.match(format) 57 | (match[1].to_f * multiplier).round if match 58 | end 59 | end 60 | times.sum if times.present? && times.all? 61 | end 62 | 63 | def cache_key key 64 | ActiveSupport::Cache.expand_cache_key key.unshift('puffer_pages_cache') 65 | end 66 | 67 | def cache_store 68 | PufferPages.cache_store 69 | end 70 | 71 | def cache? 72 | PufferPages.config.perform_caching && cache_store 73 | end 74 | end 75 | 76 | end 77 | end 78 | end 79 | 80 | Liquid::Template.register_tag('cache', PufferPages::Liquid::Tags::Cache) 81 | -------------------------------------------------------------------------------- /lib/puffer_pages/extensions/pagenator.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Extensions 3 | module Pagenator # There is no error in module name 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | class_attribute :_puffer_pages_options 8 | self._puffer_pages_options = {:pagenated => false} 9 | end 10 | 11 | module ClassMethods 12 | def inherited(klass) 13 | super 14 | klass._puffer_pages_options = _puffer_pages_options.dup 15 | end 16 | 17 | def puffer_pages options = {} 18 | _puffer_pages_options[:pagenated] = true 19 | _puffer_pages_options[:only] = Array.wrap(options[:only]).map(&:to_s).presence 20 | _puffer_pages_options[:except] = Array.wrap(options[:except]).map(&:to_s).presence 21 | _puffer_pages_options[:scope] = options[:scope] 22 | end 23 | end 24 | 25 | def _normalize_options options 26 | super 27 | if (options.key?(:puffer_page) || _puffer_pages_action?) && options[:puffer_page] != false 28 | scope = options[:puffer_scope].presence || _puffer_pages_options[:scope].presence 29 | page = options.values_at(:puffer_page, :partial, :action, :file).delete_if(&:blank?).first 30 | options[:puffer_page] = _puffer_pages_template(page, scope) 31 | options[:layout] = 'puffer_page' 32 | end 33 | end 34 | 35 | private 36 | 37 | def _puffer_pages_action? 38 | if only = _puffer_pages_options[:only] 39 | only.include?(action_name) 40 | elsif except = _puffer_pages_options[:except] 41 | !except.include?(action_name) 42 | else 43 | _puffer_pages_options[:pagenated] 44 | end 45 | end 46 | 47 | def _puffer_pages_template suggest, scope = nil 48 | return suggest if suggest.is_a?(PufferPages::Page) 49 | 50 | scope = case scope 51 | when Proc 52 | scope.call(self) 53 | when String, Symbol 54 | send scope 55 | else 56 | scope 57 | end 58 | 59 | _puffer_page_for suggest, scope 60 | end 61 | 62 | def _puffer_page_for location, scope = nil 63 | location = location.presence || request.path_info 64 | formats = lookup_context.formats 65 | page = PufferPages::Page.controller_scope(scope).find_view_page(location, formats: formats) 66 | raise PufferPages::MissedPage.new(location, formats) unless page 67 | raise PufferPages::DraftPage.new(location, formats) if page.draft? 68 | page 69 | end 70 | 71 | end 72 | end 73 | end 74 | 75 | ActionController::Base.send :include, PufferPages::Extensions::Pagenator 76 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # Code is not reloaded between requests 5 | config.cache_classes = true 6 | 7 | # Full error reports are disabled and caching is turned on 8 | config.consider_all_requests_local = false 9 | config.action_controller.perform_caching = true 10 | 11 | # Disable Rails's static asset server (Apache or nginx will already do this) 12 | config.serve_static_assets = false 13 | 14 | # Compress JavaScripts and CSS 15 | config.assets.compress = true 16 | 17 | # Don't fallback to assets pipeline if a precompiled asset is missed 18 | config.assets.compile = false 19 | 20 | # Generate digests for assets URLs 21 | config.assets.digest = true 22 | 23 | # Defaults to Rails.root.join("public/assets") 24 | # config.assets.manifest = YOUR_PATH 25 | 26 | # Specifies the header that your server uses for sending files 27 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 28 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 29 | 30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 31 | # config.force_ssl = true 32 | 33 | # See everything in the log (default is :info) 34 | # config.log_level = :debug 35 | 36 | # Prepend all log lines with the following tags 37 | # config.log_tags = [ :subdomain, :uuid ] 38 | 39 | # Use a different logger for distributed setups 40 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 41 | 42 | # Use a different cache store in production 43 | # config.cache_store = :mem_cache_store 44 | 45 | # Enable serving of images, stylesheets, and JavaScripts from an asset server 46 | # config.action_controller.asset_host = "http://assets.example.com" 47 | 48 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) 49 | # config.assets.precompile += %w( search.js ) 50 | 51 | # Disable delivery errors, bad email addresses will be ignored 52 | # config.action_mailer.raise_delivery_errors = false 53 | 54 | # Enable threaded mode 55 | # config.threadsafe! 56 | 57 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 58 | # the I18n.default_locale when a translation can not be found) 59 | config.i18n.fallbacks = true 60 | 61 | # Send deprecation notices to registered listeners 62 | config.active_support.deprecation = :notify 63 | 64 | # Log the query plan for queries taking more than this (works 65 | # with SQLite, MySQL, and PostgreSQL) 66 | # config.active_record.auto_explain_threshold_in_seconds = 0.5 67 | end 68 | -------------------------------------------------------------------------------- /spec/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | Bundler.require :development 6 | 7 | require "puffer_pages" 8 | 9 | module Dummy 10 | class Application < Rails::Application 11 | # Settings in config/environments/* take precedence over those specified here. 12 | # Application configuration should go into files in config/initializers 13 | # -- all .rb files in that directory are automatically loaded. 14 | 15 | # Custom directories with classes and modules you want to be autoloadable. 16 | # config.autoload_paths += %W(#{config.root}/extras) 17 | 18 | # Only load the plugins named here, in the order given (default is alphabetical). 19 | # :all can be used as a placeholder for all plugins not explicitly named. 20 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 21 | 22 | # Activate observers that should always be running. 23 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 24 | 25 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 26 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 27 | # config.time_zone = 'Central Time (US & Canada)' 28 | 29 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 30 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 31 | config.i18n.default_locale = :en 32 | config.i18n.available_locales = [:ru, :en, :cn] 33 | config.i18n.fallbacks = true 34 | 35 | # Configure the default encoding used in templates for Ruby 1.9. 36 | config.encoding = "utf-8" 37 | 38 | # Configure sensitive parameters which will be filtered from the log file. 39 | config.filter_parameters += [:password] 40 | 41 | # Use SQL instead of Active Record's schema dumper when creating the database. 42 | # This is necessary if your schema can't be completely dumped by the schema dumper, 43 | # like if you have constraints or database-specific column types 44 | # config.active_record.schema_format = :sql 45 | 46 | # Enforce whitelist mode for mass assignment. 47 | # This will create an empty whitelist of attributes available for mass-assignment for all models 48 | # in your app. As such, your models will need to explicitly whitelist or blacklist accessible 49 | # parameters by using an attr_accessible or attr_protected declaration. 50 | # config.active_record.whitelist_attributes = true 51 | 52 | # Enable the asset pipeline 53 | config.assets.enabled = true 54 | 55 | # Version of your assets, change this if you want to expire all your assets 56 | config.assets.version = '1.0' 57 | 58 | config.puffer_pages.raise_errors = true 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /app/assets/javascripts/puffer/codemirror/yaml.js: -------------------------------------------------------------------------------- 1 | CodeMirror.defineMode("yaml", function() { 2 | 3 | var cons = ['true', 'false', 'on', 'off', 'yes', 'no']; 4 | var keywordRegex = new RegExp("\\b(("+cons.join(")|(")+"))$", 'i'); 5 | 6 | return { 7 | token: function(stream, state) { 8 | var ch = stream.peek(); 9 | var esc = state.escaped; 10 | state.escaped = false; 11 | /* comments */ 12 | if (ch == "#") { stream.skipToEnd(); return "comment"; } 13 | if (state.literal && stream.indentation() > state.keyCol) { 14 | stream.skipToEnd(); return "string"; 15 | } else if (state.literal) { state.literal = false; } 16 | if (stream.sol()) { 17 | state.keyCol = 0; 18 | state.pair = false; 19 | state.pairStart = false; 20 | /* document start */ 21 | if(stream.match(/---/)) { return "def"; } 22 | /* document end */ 23 | if (stream.match(/\.\.\./)) { return "def"; } 24 | /* array list item */ 25 | if (stream.match(/\s*-\s+/)) { return 'meta'; } 26 | } 27 | /* pairs (associative arrays) -> key */ 28 | if (!state.pair && stream.match(/^\s*([a-z0-9\._-])+(?=\s*:)/i)) { 29 | state.pair = true; 30 | state.keyCol = stream.indentation(); 31 | return "atom"; 32 | } 33 | if (state.pair && stream.match(/^:\s*/)) { state.pairStart = true; return 'meta'; } 34 | 35 | /* inline pairs/lists */ 36 | if (stream.match(/^(\{|\}|\[|\])/)) { 37 | if (ch == '{') 38 | state.inlinePairs++; 39 | else if (ch == '}') 40 | state.inlinePairs--; 41 | else if (ch == '[') 42 | state.inlineList++; 43 | else 44 | state.inlineList--; 45 | return 'meta'; 46 | } 47 | 48 | /* list seperator */ 49 | if (state.inlineList > 0 && !esc && ch == ',') { 50 | stream.next(); 51 | return 'meta'; 52 | } 53 | /* pairs seperator */ 54 | if (state.inlinePairs > 0 && !esc && ch == ',') { 55 | state.keyCol = 0; 56 | state.pair = false; 57 | state.pairStart = false; 58 | stream.next(); 59 | return 'meta'; 60 | } 61 | 62 | /* start of value of a pair */ 63 | if (state.pairStart) { 64 | /* block literals */ 65 | if (stream.match(/^\s*(\||\>)\s*/)) { state.literal = true; return 'meta'; }; 66 | /* references */ 67 | if (stream.match(/^\s*(\&|\*)[a-z0-9\._-]+\b/i)) { return 'variable-2'; } 68 | /* numbers */ 69 | if (state.inlinePairs == 0 && stream.match(/^\s*-?[0-9\.\,]+\s?$/)) { return 'number'; } 70 | if (state.inlinePairs > 0 && stream.match(/^\s*-?[0-9\.\,]+\s?(?=(,|}))/)) { return 'number'; } 71 | /* keywords */ 72 | if (stream.match(keywordRegex)) { return 'keyword'; } 73 | } 74 | 75 | /* nothing found, continue */ 76 | state.pairStart = false; 77 | state.escaped = (ch == '\\'); 78 | stream.next(); 79 | return null; 80 | }, 81 | startState: function() { 82 | return { 83 | pair: false, 84 | pairStart: false, 85 | keyCol: 0, 86 | inlinePairs: 0, 87 | inlineList: 0, 88 | literal: false, 89 | escaped: false 90 | }; 91 | } 92 | }; 93 | }); 94 | 95 | CodeMirror.defineMIME("text/x-yaml", "yaml"); 96 | -------------------------------------------------------------------------------- /app/assets/javascripts/puffer/matchbrackets.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<"}; 3 | function findMatchingBracket(cm) { 4 | var cur = cm.getCursor(), line = cm.getLineHandle(cur.line), pos = cur.ch - 1; 5 | var match = (pos >= 0 && matching[line.text.charAt(pos)]) || matching[line.text.charAt(++pos)]; 6 | if (!match) return null; 7 | var forward = match.charAt(1) == ">", d = forward ? 1 : -1; 8 | var style = cm.getTokenAt({line: cur.line, ch: pos + 1}).type; 9 | 10 | var stack = [line.text.charAt(pos)], re = /[(){}[\]]/; 11 | function scan(line, lineNo, start) { 12 | if (!line.text) return; 13 | var pos = forward ? 0 : line.text.length - 1, end = forward ? line.text.length : -1; 14 | if (start != null) pos = start + d; 15 | for (; pos != end; pos += d) { 16 | var ch = line.text.charAt(pos); 17 | if (re.test(ch) && cm.getTokenAt({line: lineNo, ch: pos + 1}).type == style) { 18 | var match = matching[ch]; 19 | if (match.charAt(1) == ">" == forward) stack.push(ch); 20 | else if (stack.pop() != match.charAt(0)) return {pos: pos, match: false}; 21 | else if (!stack.length) return {pos: pos, match: true}; 22 | } 23 | } 24 | } 25 | for (var i = cur.line, found, e = forward ? Math.min(i + 100, cm.lineCount()) : Math.max(-1, i - 100); i != e; i+=d) { 26 | if (i == cur.line) found = scan(line, i, pos); 27 | else found = scan(cm.getLineHandle(i), i); 28 | if (found) break; 29 | } 30 | return {from: {line: cur.line, ch: pos}, to: found && {line: i, ch: found.pos}, match: found && found.match}; 31 | } 32 | 33 | function matchBrackets(cm, autoclear) { 34 | var found = findMatchingBracket(cm); 35 | if (!found) return; 36 | var style = found.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket"; 37 | var one = cm.markText(found.from, {line: found.from.line, ch: found.from.ch + 1}, 38 | {className: style}); 39 | var two = found.to && cm.markText(found.to, {line: found.to.line, ch: found.to.ch + 1}, 40 | {className: style}); 41 | var clear = function() { 42 | cm.operation(function() { one.clear(); two && two.clear(); }); 43 | }; 44 | if (autoclear) setTimeout(clear, 800); 45 | else return clear; 46 | } 47 | 48 | var currentlyHighlighted = null; 49 | function doMatchBrackets(cm) { 50 | cm.operation(function() { 51 | if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;} 52 | if (!cm.somethingSelected()) currentlyHighlighted = matchBrackets(cm, false); 53 | }); 54 | } 55 | 56 | CodeMirror.defineOption("matchBrackets", false, function(cm, val) { 57 | if (val) cm.on("cursorActivity", doMatchBrackets); 58 | else cm.off("cursorActivity", doMatchBrackets); 59 | }); 60 | 61 | CodeMirror.defineExtension("matchBrackets", function() {matchBrackets(this, true);}); 62 | CodeMirror.defineExtension("findMatchingBracket", function(){return findMatchingBracket(this);}); 63 | })(); 64 | -------------------------------------------------------------------------------- /spec/lib/page_drop_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe PufferPages::Liquid::PageDrop do 4 | 5 | def render_layout layout, current_page, page = nil 6 | current_page.render(layout, other: page) 7 | end 8 | 9 | context do 10 | let(:hash) { { 'hello' => '{{ self.name }}' } } 11 | 12 | let!(:root) { Fabricate :root, name: 'root' } 13 | let!(:first) { Fabricate :page, slug: 'first', parent: root } 14 | let!(:second) { Fabricate :page, slug: 'second', parent: first, page_parts: [main, sidebar] } 15 | let!(:css) { Fabricate :page, slug: 'page.css', parent: first } 16 | let!(:main) { Fabricate(:main) } 17 | let!(:sidebar) { Fabricate(:sidebar, handler: 'yaml', body: YAML.dump(hash)) } 18 | 19 | before do 20 | root.reload 21 | first.reload 22 | second.reload 23 | end 24 | 25 | specify { render_layout('{{ self.parent.name }}', second).should == first.name } 26 | specify { render_layout('{{ self.root.name }}', second).should == 'root' } 27 | 28 | specify { render_layout('{{ other.current? }}', first, first).should == 'true' } 29 | specify { render_layout('{{ other.current? }}', first, root).should == 'false' } 30 | specify { render_layout('{{ other.current? }}', first, second).should == 'false' } 31 | 32 | specify { render_layout('{{ other.ancestor? }}', first, first).should == 'false' } 33 | specify { render_layout('{{ other.ancestor? }}', first, root).should == 'true' } 34 | specify { render_layout('{{ other.ancestor? }}', first, second).should == 'false' } 35 | 36 | specify { render_layout('{% if self == other %}equal{% else %}not equal{% endif %}', first, first).should == 'equal' } 37 | specify { render_layout('{% if self == other %}equal{% else %}not equal{% endif %}', first, root).should == 'not equal' } 38 | specify { render_layout('{% if self == other %}equal{% else %}not equal{% endif %}', first, second).should == 'not equal' } 39 | 40 | specify { render_layout('{{ self.body }}', second).should == main.body } 41 | specify { render_layout('{{ self.sidebar.hello }}', second).should == second.name } 42 | 43 | context do 44 | include RSpec::Rails::RequestExampleGroup 45 | 46 | context 'url helpers' do 47 | let!(:application) { Fabricate :application, :body => '{{ self.path }} {{ self.url }}' } 48 | 49 | specify do 50 | get "/#{second.location}" 51 | response.body.should == '/first/second http://www.example.com/first/second' 52 | end 53 | end 54 | 55 | context 'render tag' do 56 | let!(:application) { Fabricate :application, :body => "{% render 'shared/first' %}" } 57 | 58 | specify do 59 | get "/#{second.location}" 60 | response.body.should == 'shared/first content' 61 | end 62 | end 63 | 64 | context 'render tag with other format' do 65 | let!(:application) { Fabricate :application, :body => "{% render 'shared/first' %}" } 66 | 67 | specify do 68 | get "/#{css.location}" 69 | response.body.should == 'shared/first content' 70 | end 71 | end 72 | end 73 | end 74 | 75 | end 76 | -------------------------------------------------------------------------------- /spec/lib/rspec/matchers/render_page_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Specs' do 4 | describe '#render_page' do 5 | include RSpec::Rails::ControllerExampleGroup 6 | 7 | let!(:layout) { Fabricate :application } 8 | let!(:root) { Fabricate :root } 9 | let!(:first) { Fabricate :page, slug: 'first', parent: root } 10 | let!(:second) { Fabricate :page, slug: 'second', parent: first } 11 | 12 | context 'no page specified' do 13 | context do 14 | controller do 15 | def index 16 | @page = PufferPages::Page.root 17 | render puffer_page: @page 18 | end 19 | end 20 | 21 | it { should render_page } 22 | specify do 23 | get :index 24 | response.should render_page 25 | end 26 | end 27 | 28 | context do 29 | controller do 30 | def index 31 | render nothing: true 32 | end 33 | end 34 | 35 | it { should_not render_page } 36 | specify do 37 | get :index 38 | response.should_not render_page 39 | end 40 | end 41 | end 42 | 43 | context 'with page instance' do 44 | controller do 45 | def index 46 | @page = PufferPages::Page.root 47 | render puffer_page: @page 48 | end 49 | 50 | def show 51 | @page = PufferPages::Page.find_page 'first' 52 | render puffer_page: @page 53 | end 54 | end 55 | 56 | it { should render_page root } 57 | it { should_not render_page first } 58 | it { should_not render_page second } 59 | specify do 60 | get :index 61 | response.should render_page root 62 | end 63 | specify do 64 | get :index 65 | response.should_not render_page first 66 | end 67 | specify do 68 | get :index 69 | response.should_not render_page second 70 | end 71 | specify do 72 | get :show, id: 42 73 | response.should_not render_page root 74 | end 75 | specify do 76 | get :show, id: 42 77 | response.should render_page first 78 | end 79 | specify do 80 | get :show, id: 42 81 | response.should_not render_page second 82 | end 83 | end 84 | 85 | context 'with drops' do 86 | controller do 87 | def index 88 | page = PufferPages::Page.root 89 | @count = 42 90 | @string = 'hello' 91 | @object = Object.new 92 | render puffer_page: page 93 | end 94 | end 95 | 96 | it { should render_page } 97 | it { should render_page.with_drops } 98 | it { should render_page.with_drops('count') } 99 | it { should render_page.with_drops('string' => 'hello') } 100 | it { should render_page.with_drops('string', 'count' => 42) } 101 | it { should render_page.with_drops { |drops| drops.keys.include?('string') } } 102 | it { should render_page.with_drops { |drops| drops['count'] > 40 } } 103 | it { should render_page.with_drops('count') { |drops| !drops.key?('object') } } 104 | it { should_not render_page.with_drops('object') } 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /app/assets/javascripts/puffer/codemirror/htmlmixed.js: -------------------------------------------------------------------------------- 1 | CodeMirror.defineMode("htmlmixed", function(config) { 2 | var htmlMode = CodeMirror.getMode(config, {name: "xml", htmlMode: true}); 3 | var jsMode = CodeMirror.getMode(config, "javascript"); 4 | var cssMode = CodeMirror.getMode(config, "css"); 5 | 6 | function html(stream, state) { 7 | var style = htmlMode.token(stream, state.htmlState); 8 | if (/(?:^|\s)tag(?:\s|$)/.test(style) && stream.current() == ">" && state.htmlState.context) { 9 | if (/^script$/i.test(state.htmlState.context.tagName)) { 10 | state.token = javascript; 11 | state.localState = jsMode.startState(htmlMode.indent(state.htmlState, "")); 12 | } 13 | else if (/^style$/i.test(state.htmlState.context.tagName)) { 14 | state.token = css; 15 | state.localState = cssMode.startState(htmlMode.indent(state.htmlState, "")); 16 | } 17 | } 18 | return style; 19 | } 20 | function maybeBackup(stream, pat, style) { 21 | var cur = stream.current(); 22 | var close = cur.search(pat), m; 23 | if (close > -1) stream.backUp(cur.length - close); 24 | else if (m = cur.match(/<\/?$/)) { 25 | stream.backUp(cur.length); 26 | if (!stream.match(pat, false)) stream.match(cur[0]); 27 | } 28 | return style; 29 | } 30 | function javascript(stream, state) { 31 | if (stream.match(/^<\/\s*script\s*>/i, false)) { 32 | state.token = html; 33 | state.localState = null; 34 | return html(stream, state); 35 | } 36 | return maybeBackup(stream, /<\/\s*script\s*>/, 37 | jsMode.token(stream, state.localState)); 38 | } 39 | function css(stream, state) { 40 | if (stream.match(/^<\/\s*style\s*>/i, false)) { 41 | state.token = html; 42 | state.localState = null; 43 | return html(stream, state); 44 | } 45 | return maybeBackup(stream, /<\/\s*style\s*>/, 46 | cssMode.token(stream, state.localState)); 47 | } 48 | 49 | return { 50 | startState: function() { 51 | var state = htmlMode.startState(); 52 | return {token: html, localState: null, mode: "html", htmlState: state}; 53 | }, 54 | 55 | copyState: function(state) { 56 | if (state.localState) 57 | var local = CodeMirror.copyState(state.token == css ? cssMode : jsMode, state.localState); 58 | return {token: state.token, localState: local, mode: state.mode, 59 | htmlState: CodeMirror.copyState(htmlMode, state.htmlState)}; 60 | }, 61 | 62 | token: function(stream, state) { 63 | return state.token(stream, state); 64 | }, 65 | 66 | indent: function(state, textAfter) { 67 | if (state.token == html || /^\s*<\//.test(textAfter)) 68 | return htmlMode.indent(state.htmlState, textAfter); 69 | else if (state.token == javascript) 70 | return jsMode.indent(state.localState, textAfter); 71 | else 72 | return cssMode.indent(state.localState, textAfter); 73 | }, 74 | 75 | electricChars: "/{}:", 76 | 77 | innerMode: function(state) { 78 | var mode = state.token == html ? htmlMode : state.token == javascript ? jsMode : cssMode; 79 | return {state: state.localState || state.htmlState, mode: mode}; 80 | } 81 | }; 82 | }, "xml", "javascript", "css"); 83 | 84 | CodeMirror.defineMIME("text/html", "htmlmixed"); 85 | -------------------------------------------------------------------------------- /spec/data/unlocalized.json: -------------------------------------------------------------------------------- 1 | { 2 | "layouts": [{ 3 | "created_at": "2013-12-23T16:23:00Z", 4 | "id": "404DF5860F01492EB470711FE3BE4535", 5 | "name": "application", 6 | "updated_at": "2013-12-23T16:23:00Z", 7 | "body": "Nulla suscipit ligula in lacus." 8 | }], 9 | "snippets": [{ 10 | "created_at": "2013-12-23T16:23:00Z", 11 | "id": "C647D698E0D64A99BC8AA0C0A0F0BAEF", 12 | "name": "lacus", 13 | "updated_at": "2013-12-23T16:23:00Z", 14 | "body": "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla dapibus dolor vel est." 15 | }], 16 | "pages": [{ 17 | "created_at": "2013-12-23T16:23:00Z", 18 | "id": "55E458FCA2BB4BA0B95AE99ACDDAF094", 19 | "layout_name": "application", 20 | "locales": {}, 21 | "name": "Nunc nisl.", 22 | "parent_id": null, 23 | "slug": null, 24 | "status": "published", 25 | "updated_at": "2013-12-23T16:23:00Z", 26 | "page_parts": [{ 27 | "created_at": "2013-12-23T16:23:00Z", 28 | "handler": "html", 29 | "id": "A0C54DBB446D4727903A2B6FA5D4C9E2", 30 | "name": "body", 31 | "page_id": "55E458FCA2BB4BA0B95AE99ACDDAF094", 32 | "updated_at": "2013-12-23T16:23:00Z", 33 | "body": "PagePart: `body`, Page: ``" 34 | }, { 35 | "created_at": "2013-12-23T16:23:00Z", 36 | "handler": "html", 37 | "id": "EA1DE54674D1430894134AA9FDB603F3", 38 | "name": "sidebar", 39 | "page_id": "55E458FCA2BB4BA0B95AE99ACDDAF094", 40 | "updated_at": "2013-12-23T16:23:00Z", 41 | "body": "PagePart: `sidebar`, Page: ``" 42 | }] 43 | }, { 44 | "created_at": "2013-12-23T16:23:00Z", 45 | "id": "58C3B16335C242E8B231E2B634D1B7EA", 46 | "layout_name": null, 47 | "locales": {}, 48 | "name": "Morbi non quam nec dui luctus rutrum.", 49 | "parent_id": "55E458FCA2BB4BA0B95AE99ACDDAF094", 50 | "slug": "first", 51 | "status": "published", 52 | "updated_at": "2013-12-23T16:23:00Z", 53 | "page_parts": [{ 54 | "created_at": "2013-12-23T16:23:00Z", 55 | "handler": "html", 56 | "id": "A0BC46E472FC45CB8C57F068685DEA3D", 57 | "name": "body", 58 | "page_id": "58C3B16335C242E8B231E2B634D1B7EA", 59 | "updated_at": "2013-12-23T16:23:00Z", 60 | "body": "PagePart: `body`, Page: `first`" 61 | }] 62 | }, { 63 | "created_at": "2013-12-23T16:23:00Z", 64 | "id": "C8D5E74CE293403DAB5FBB5A89BDE6C5", 65 | "layout_name": null, 66 | "locales": {}, 67 | "name": "Donec dapibus.", 68 | "parent_id": "58C3B16335C242E8B231E2B634D1B7EA", 69 | "slug": "second", 70 | "status": "published", 71 | "updated_at": "2013-12-23T16:23:00Z", 72 | "page_parts": [{ 73 | "created_at": "2013-12-23T16:23:00Z", 74 | "handler": "html", 75 | "id": "504F931E6E8C495699BA5D4C9D1FAD06", 76 | "name": "sidebar", 77 | "page_id": "C8D5E74CE293403DAB5FBB5A89BDE6C5", 78 | "updated_at": "2013-12-23T16:23:00Z", 79 | "body": "PagePart: `sidebar`, Page: `first/second`" 80 | }] 81 | }] 82 | } -------------------------------------------------------------------------------- /spec/lib/liquid/tags_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Tags' do 4 | 5 | def render_page(page, drops = {}) 6 | page.render({ self: page }.merge(drops)) 7 | end 8 | 9 | describe 'stylesheets' do 10 | 11 | it 'should render stylesheets with proper params' do 12 | @page = Fabricate :page, :layout_name => 'foo_layout' 13 | @layout = Fabricate :layout, :name => 'foo_layout', :body => "{% assign st = 'styles' %}{% stylesheets 'reset', st %}" 14 | render_page(@page).should == "<%= stylesheet_link_tag 'reset', 'styles' %>" 15 | end 16 | 17 | end 18 | 19 | describe 'javascripts' do 20 | 21 | it 'should render javascripts with proper params' do 22 | @page = Fabricate :page, :layout_name => 'foo_layout' 23 | @layout = Fabricate :layout, :name => 'foo_layout', :body => "{% assign ctrl = 'controls' %}{% javascripts 'prototype', ctrl %}" 24 | render_page(@page).should == "<%= javascript_include_tag 'prototype', 'controls' %>" 25 | end 26 | 27 | end 28 | 29 | describe 'javascript' do 30 | 31 | it 'should render javascript tag' do 32 | @page = Fabricate :page, :layout_name => 'foo_layout' 33 | @layout = Fabricate :layout, :name => 'foo_layout', :body => "{% javascript %}\nvar i = \"\";\ni = 2;\n{% endjavascript %}" 34 | render_page(@page).should == "<%= javascript_tag do %>\nvar i = \"\";\ni = 2;\n<% end %>" 35 | end 36 | 37 | end 38 | 39 | describe 'super' do 40 | let!(:root){ 41 | Fabricate :page, :layout_name => 'foo', :page_parts => [ 42 | Fabricate(:page_part, :name => 'sidebar', :body => "root sidebar {{var}}") 43 | ] 44 | } 45 | let!(:page){ 46 | Fabricate :page, :slug => 'page', :parent => root, :page_parts => [ 47 | Fabricate(:page_part, :name => 'sidebar', :body => "wrap {% super var:'hello' %} sidebar") 48 | ] 49 | } 50 | let!(:page2){ 51 | Fabricate :page, :slug => 'page2', :parent => page, :page_parts => [ 52 | Fabricate(:page_part, :name => 'sidebar', :body => "wrap2 {% super %} sidebar") 53 | ] 54 | } 55 | 56 | specify{page.render("{% include 'sidebar' %}").should == "wrap root sidebar hello sidebar"} 57 | specify{page2.render("{% include 'sidebar' %}").should == "wrap2 wrap root sidebar hello sidebar sidebar"} 58 | end 59 | 60 | describe 'array' do 61 | subject{Liquid::Template.parse("{% array arr = 'one', 2, var %}{{arr[0]}} {{arr[1]}} {{arr[2]}}")} 62 | specify{subject.render('var' => 'three').should == 'one 2 three'} 63 | end 64 | 65 | context 'url helpers' do 66 | include RSpec::Rails::ControllerExampleGroup 67 | 68 | controller{} 69 | 70 | def render template, variables = {} 71 | Liquid::Template.parse(template).render!(variables.stringify_keys, {:registers => {:controller => controller}}) 72 | end 73 | 74 | specify { render("{% url admin_pages %}").should == 'http://test.host/admin/pages' } 75 | specify { render("{% path admin_page 10 %}").should == '/admin/pages/10' } 76 | specify { render("{% path admin_pages key: 'value' %}").should == '/admin/pages?key=value' } 77 | specify { render("{% path admin_page 'haha' %}").should == '/admin/pages/haha' } 78 | specify { render("{% path admin_page var %}", { var: 'foo' }).should == '/admin/pages/foo' } 79 | specify { render("{% path admin_page 10, key: value %}", { value: 'hello' }).should == '/admin/pages/10?key=hello' } 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/puffer_pages/rspec/matchers/render_page.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | module Rspec 3 | module Matchers 4 | class RenderPage < ::RSpec::Matchers::BuiltIn::BaseMatcher 5 | attr_reader :scope, :page, :drops 6 | 7 | def initialize scope, page = nil 8 | @scope = scope 9 | @page = page 10 | @page = PufferPages::Page.find_page(page) if page.is_a?(String) 11 | @drops ||= { values: {}, names: [], manual: [] } 12 | end 13 | 14 | def matches? controller_or_request 15 | scope.get :index if controller_or_request.is_a?(ActionController::Base) 16 | 17 | rendered_page && (!with_page? || page_conformity) && (!with_drops? || drops_conformity) 18 | end 19 | 20 | def with_drops *drops, &block 21 | @drops[:values].merge! drops.extract_options! 22 | @drops[:names].concat(drops.flatten).uniq! 23 | @drops[:manual].push block if block 24 | self 25 | end 26 | 27 | def failure_message_for_should 28 | message = '' 29 | message << "expected action to render #{page_message}\n" 30 | message << "with drops #{drops_message}\n" if with_drops? 31 | if with_drops? && !drops_conformity 32 | rendered_drops = scope.puffer_pages_render[rendered_page].first[:drops] 33 | message << "but available drops are: #{PP.pp rendered_drops, ''}" 34 | else 35 | message << "but #{rendered_page ? "`/#{rendered_page.location}`" : 'nothing'} was rendered" 36 | end 37 | message 38 | end 39 | 40 | def failure_message_for_should_not 41 | "expected action not to render #{page_message} but `/#{rendered_page.location}` was rendered" 42 | end 43 | 44 | def description 45 | "render page #{page_message}" 46 | end 47 | 48 | private 49 | 50 | def page_message 51 | page ? "page `/#{page.location}`" : 'any page' 52 | end 53 | 54 | def drops_message 55 | [drops[:names], drops[:values]].delete_if(&:blank?).map(&:inspect).join ?, 56 | end 57 | 58 | def rendered_page 59 | @rendered_page ||= scope.puffer_pages_render.keys.first 60 | end 61 | 62 | def with_page? 63 | !!page 64 | end 65 | 66 | def page_conformity 67 | scope.puffer_pages_render[rendered_page] && 68 | scope.puffer_pages_render[rendered_page].count == 1 && 69 | proper_page_rendered 70 | end 71 | 72 | def proper_page_rendered 73 | if rendered_page.respond_to?(:dummy_page?) && rendered_page.dummy_page? 74 | rendered_page.location == page.location 75 | else 76 | rendered_page == page 77 | end 78 | end 79 | 80 | def with_drops? 81 | drops.values.any?(&:present?) 82 | end 83 | 84 | def drops_conformity 85 | rendered_drops = scope.puffer_pages_render[rendered_page].first[:drops] 86 | 87 | rendered_drops.keys | drops[:names] == rendered_drops.keys && 88 | drops[:values].all? { |(name, value)| rendered_drops[name] == value } && 89 | drops[:manual].all? { |block| block.call(rendered_drops) } 90 | end 91 | end 92 | 93 | def render_page page = nil 94 | RenderPage.new self, page 95 | end 96 | end 97 | end 98 | end -------------------------------------------------------------------------------- /lib/puffer_pages.rb: -------------------------------------------------------------------------------- 1 | module PufferPages 2 | include ActiveSupport::Configurable 3 | 4 | autoload :SnippetsBase, 'puffer_pages/backends/controllers/snippets_base' 5 | autoload :LayoutsBase, 'puffer_pages/backends/controllers/layouts_base' 6 | autoload :PagesBase, 'puffer_pages/backends/controllers/pages_base' 7 | autoload :OriginsBase, 'puffer_pages/backends/controllers/origins_base' 8 | 9 | class PufferPagesError < StandardError 10 | end 11 | 12 | class RenderError < PufferPagesError 13 | def initialize location, formats = [] 14 | @location, @formats = location, formats 15 | end 16 | 17 | def with_formats 18 | if @formats.present? 19 | "with formats `#{@formats.join('`, `')}`" 20 | else 21 | "with any format" 22 | end 23 | end 24 | 25 | def reason 26 | end 27 | 28 | def message 29 | "PufferPages can`t render page `#{@location}` #{with_formats} #{reason}" 30 | end 31 | end 32 | 33 | class DraftPage < RenderError 34 | def reason 35 | "because it`s draft" 36 | end 37 | end 38 | 39 | class MissedPage < RenderError 40 | def reason 41 | "because it`s missed" 42 | end 43 | end 44 | 45 | class ImportFailed < PufferPagesError 46 | end 47 | 48 | mattr_accessor :primary_page_part_name 49 | self.primary_page_part_name = 'body' 50 | 51 | mattr_accessor :single_section_page_path 52 | self.single_section_page_path = false 53 | 54 | mattr_accessor :localize 55 | self.localize = false 56 | 57 | mattr_accessor :access_token 58 | self.access_token = nil 59 | 60 | mattr_accessor :export_path 61 | self.export_path = '/admin/origins/export' 62 | 63 | mattr_accessor :install_i18n_backend 64 | self.install_i18n_backend = true 65 | 66 | config.raise_errors = false 67 | 68 | module ConfigMethods 69 | def setup 70 | yield self 71 | end 72 | 73 | def i18n_backend 74 | @i18n_backend ||= PufferPages::Liquid::Backend.new 75 | end 76 | 77 | def cache_store 78 | config.cache_store 79 | end 80 | 81 | def cache_store= store 82 | config.cache_store = ActiveSupport::Cache.lookup_store(store) 83 | end 84 | end 85 | 86 | extend ConfigMethods 87 | end 88 | 89 | require 'puffer' 90 | require 'liquid' 91 | require 'uuidtools' 92 | require 'activeuuid' 93 | require 'nested_set' 94 | require 'contextuality' 95 | 96 | require 'puffer_pages/helpers' 97 | require 'puffer_pages/engine' 98 | require 'puffer_pages/backends' 99 | require 'puffer_pages/handlers' 100 | require 'puffer_pages/migrations' 101 | require 'puffer_pages/log_subscriber' 102 | require 'puffer_pages/extensions/core' 103 | require 'puffer_pages/extensions/context' 104 | require 'puffer_pages/extensions/renderer' 105 | require 'puffer_pages/extensions/pagenator' 106 | 107 | require 'puffer_pages/liquid/tags/url' 108 | require 'puffer_pages/liquid/tags/cache' 109 | require 'puffer_pages/liquid/tags/yield' 110 | require 'puffer_pages/liquid/tags/array' 111 | require 'puffer_pages/liquid/tags/assets' 112 | require 'puffer_pages/liquid/tags/image' 113 | require 'puffer_pages/liquid/tags/helper' 114 | require 'puffer_pages/liquid/tags/render' 115 | require 'puffer_pages/liquid/tags/scope' 116 | require 'puffer_pages/liquid/tags/super' 117 | require 'puffer_pages/liquid/tags/include' 118 | require 'puffer_pages/liquid/tags/partials' 119 | require 'puffer_pages/liquid/tags/translate' 120 | require 'puffer_pages/liquid/tags/javascript' 121 | -------------------------------------------------------------------------------- /spec/models/puffer_pages/renderable_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | require 'spec_helper' 3 | 4 | describe PufferPages::Backends::Mixins::Renderable do 5 | let(:klass) do 6 | Class.new do 7 | include PufferPages::Backends::Mixins::Renderable 8 | end 9 | end 10 | subject { klass.new } 11 | 12 | def merge *args 13 | subject.send(:merge_context, *args) 14 | end 15 | 16 | describe "#normalize_render_options" do 17 | let(:string) { 'Hello ^^' } 18 | let(:object) { Object.new } 19 | let(:hash) { { a: 1 } } 20 | let(:another_hash) { { b: 2 } } 21 | let(:context) { ::Liquid::Context.new } 22 | 23 | def normalize *args 24 | subject.send(:normalize_render_options, *args) 25 | end 26 | 27 | specify { normalize(string, context, hash).should == [string, merge(context, hash)] } 28 | specify { normalize(object, context, hash).should == [object, merge(context, hash)] } 29 | specify { normalize(context, hash).should == [nil, merge(context, hash)] } 30 | specify { normalize(string, hash, another_hash).should == [string, merge(hash, another_hash)] } 31 | specify { normalize(hash, another_hash).should == [nil, merge(hash, another_hash)] } 32 | specify { normalize(string, context).should == [string, merge(context, {})] } 33 | specify { normalize(string, hash).should == [string, merge(hash, {})] } 34 | specify { normalize(context).should == [nil, context] } 35 | specify { normalize(hash).should == [nil, merge(hash, {})] } 36 | specify { expect { normalize(string, context, hash, 42).should }.to raise_error ArgumentError } 37 | end 38 | 39 | describe "#merge_context" do 40 | let(:hash) { { a: 1 } } 41 | let(:another_hash) { { registers: { b: 2 }, environment: { c: 3 } } } 42 | let(:context) { ::Liquid::Context.new } 43 | 44 | specify { merge(hash, {}).should == { drops: { 'a' => 1 }, environment: {}, registers: {} } } 45 | specify { merge(another_hash, {}).should == { drops: {}, environment: { c: 3 }, registers: { b: 2 } } } 46 | specify { merge(hash, another_hash).should == { drops: { 'a' => 1 }, environment: { c: 3 }, registers: { b: 2 } } } 47 | specify { merge(context, {}).should == context } 48 | specify { merge(context, hash)['a'].should == 1 } 49 | specify { merge(context, hash).registers.should == {} } 50 | specify { merge(context, another_hash).registers.should == { b: 2 } } 51 | end 52 | 53 | describe "#normalize_context_options" do 54 | let(:hash1) { { a: 1 } } 55 | let(:hash2) { { b: 2 } } 56 | let(:hash3) { { c: 3 } } 57 | 58 | def normalize *args 59 | subject.send(:normalize_context_options, *args) 60 | end 61 | 62 | specify { normalize(foo: hash1).should == { drops: { 'foo' => hash1 }, environment: {}, registers: {} } } 63 | specify { normalize(drops: { 'a' => 1 }, environment: hash2, registers: hash3).should == 64 | { drops: { 'a' => 1 }, environment: hash2, registers: hash3 } } 65 | specify { normalize(environment: hash1, foo: hash2).should == 66 | { drops: { 'environment' => hash1, 'foo' => hash2 }, environment: {}, registers: {} } } 67 | end 68 | 69 | describe "#render_template" do 70 | def render *args 71 | subject.send(:render_template, *args) 72 | end 73 | 74 | context 'with hash context' do 75 | specify { render("{{ hello }}", hello: 'Hello').should == 'Hello' } 76 | end 77 | 78 | context 'with object context' do 79 | let(:liquid_context) { ::Liquid::Context.new('hello' => 'Hello') } 80 | 81 | specify { render("{{ hello }}", liquid_context).should == 'Hello' } 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /app/assets/javascripts/puffer/multiplex.js: -------------------------------------------------------------------------------- 1 | CodeMirror.multiplexingMode = function(outer /*, others */) { 2 | // Others should be {open, close, mode [, delimStyle]} objects 3 | var others = Array.prototype.slice.call(arguments, 1); 4 | var n_others = others.length; 5 | 6 | function indexOf(string, pattern, from) { 7 | if (typeof pattern == "string") return string.indexOf(pattern, from); 8 | var m = pattern.exec(from ? string.slice(from) : string); 9 | return m ? m.index + from : -1; 10 | } 11 | 12 | return { 13 | startState: function() { 14 | return { 15 | outer: CodeMirror.startState(outer), 16 | innerActive: null, 17 | inner: null 18 | }; 19 | }, 20 | 21 | copyState: function(state) { 22 | return { 23 | outer: CodeMirror.copyState(outer, state.outer), 24 | innerActive: state.innerActive, 25 | inner: state.innerActive && CodeMirror.copyState(state.innerActive.mode, state.inner) 26 | }; 27 | }, 28 | 29 | token: function(stream, state) { 30 | if (!state.innerActive) { 31 | var cutOff = Infinity, oldContent = stream.string; 32 | for (var i = 0; i < n_others; ++i) { 33 | var other = others[i]; 34 | var found = indexOf(oldContent, other.open, stream.pos); 35 | if (found == stream.pos) { 36 | stream.match(other.open); 37 | state.innerActive = other; 38 | state.inner = CodeMirror.startState(other.mode, outer.indent ? outer.indent(state.outer, "") : 0); 39 | return other.delimStyle; 40 | } else if (found != -1 && found < cutOff) { 41 | cutOff = found; 42 | } 43 | } 44 | if (cutOff != Infinity) stream.string = oldContent.slice(0, cutOff); 45 | var outerToken = outer.token(stream, state.outer); 46 | if (cutOff != Infinity) stream.string = oldContent; 47 | return outerToken; 48 | } else { 49 | var curInner = state.innerActive, oldContent = stream.string; 50 | var found = indexOf(oldContent, curInner.close, stream.pos); 51 | if (found == stream.pos) { 52 | stream.match(curInner.close); 53 | state.innerActive = state.inner = null; 54 | return curInner.delimStyle; 55 | } 56 | if (found > -1) stream.string = oldContent.slice(0, found); 57 | var innerToken = curInner.mode.token(stream, state.inner); 58 | if (found > -1) stream.string = oldContent; 59 | var cur = stream.current(), found = cur.indexOf(curInner.close); 60 | if (found > -1) stream.backUp(cur.length - found); 61 | return innerToken; 62 | } 63 | }, 64 | 65 | indent: function(state, textAfter) { 66 | var mode = state.innerActive ? state.innerActive.mode : outer; 67 | if (!mode.indent) return CodeMirror.Pass; 68 | return mode.indent(state.innerActive ? state.inner : state.outer, textAfter); 69 | }, 70 | 71 | blankLine: function(state) { 72 | var mode = state.innerActive ? state.innerActive.mode : outer; 73 | if (mode.blankLine) { 74 | mode.blankLine(state.innerActive ? state.inner : state.outer); 75 | } 76 | if (!state.innerActive) { 77 | for (var i = 0; i < n_others; ++i) { 78 | var other = others[i]; 79 | if (other.open === "\n") { 80 | state.innerActive = other; 81 | state.inner = CodeMirror.startState(other.mode, mode.indent ? mode.indent(state.outer, "") : 0); 82 | } 83 | } 84 | } else if (mode.close === "\n") { 85 | state.innerActive = state.inner = null; 86 | } 87 | }, 88 | 89 | electricChars: outer.electricChars, 90 | 91 | innerMode: function(state) { 92 | return state.inner ? {state: state.inner, mode: state.innerActive.mode} : {state: state.outer, mode: outer}; 93 | } 94 | }; 95 | }; 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Code Climate](https://codeclimate.com/github/puffer/puffer_pages.png)](https://codeclimate.com/github/puffer/puffer_pages) 2 | [![Build Status](https://secure.travis-ci.org/puffer/puffer_pages.png)](http://travis-ci.org/puffer/puffer_pages) 3 | 4 | # PufferPages is lightweight but powerful rails >= 3.1 CMS 5 | 6 | Interface of PufferPages based on [puffer](https://github.com/puffer/puffer) 7 | 8 | ## Keyfeatures 9 | 10 | * Full rails integration. PufferPages is part of rails and you can different features related to pages in rails application directly 11 | * Flexibility. Puffer designed to be as flexible as possible, so you can create your own functionality easily. 12 | * Layouts. You can use rails layouts for pages and you can use pages as action layouts! 13 | 14 | ## Installation 15 | 16 | You can instal puffer as a gem: 17 |
gem install puffer_pages
18 | Or in Gemfile: 19 |
gem "puffer_pages"
20 | 21 | Next step is: 22 |
rake puffer_pages:install:migrations
23 | This will install PufferPages config file in your initializers, some css/js, controllers and migrations 24 |
rake db:migrate
25 | 26 | Nex step - adding routes: 27 |
 28 |   mount PufferPages::Engine => '/'
 29 | 
30 | 31 | To start working with admin interface, you need to add some routes like: 32 |
 33 | namespace :admin do
 34 |   resources :pages
 35 |   resources :layouts
 36 |   resources :snippets
 37 |   resources :origins
 38 | end
 39 | 
40 | 41 | ## Introduction 42 | PufferPages is radiant-like cms, so it has layouts, snippets and pages. 43 | PufferPages use liquid as template language. 44 | 45 | ## Pages 46 | Pages - tree-based structure of site. 47 | Every page has one or more page parts. 48 | 49 | ## PageParts 50 | Page_parts are the same as content_for block content in rails. You can insert current page page_patrs at layout. 51 | Also, page_parts are inheritable. It means, that if root has page_part named `sidebar`, all its children will have the same page_part until this page_part will be redefined. 52 | Every page part must have main page part, named by default `body`. You can configure main page part name in config/initializers/puffer_pages.rb 53 | 54 | ## Layouts 55 | Layout is page canvas, so you can draw page parts on it. 56 | You can use layouts from database or rails applcation layouts for pages. 57 | 58 | ### Rails application layouts 59 | For application layout page_part body will be inserted instead of SUDDENLY! <%= yield %> 60 | For yield with no params specified puffer will use page part with default page_part name. 61 | 62 | So, main page part is action view and other are partials. So easy. 63 | 64 | ## [Liquid](http://github.com/tobi/liquid/) 65 | 66 | ### Variables 67 | This variables accessible from every page: 68 | 69 | * self - current page reference. 70 |
{{ self.name }}
71 | self is an instance of page drop. View [this](https://github.com/puffer/puffer_pages/blob/master/lib/puffer_pages/liquid/page_drop.rb) to find list of possible page drop methods 72 | 73 | ### include 74 | `include` is standart liquid tag with puffer data model 'file_system' 75 | 76 | #### for page_parts 77 | Use include tag for current page page_parts inclusion: 78 |
{% include 'page_part_name' %}
79 | 80 | #### for snippets 81 | To include snippet use this path form: 82 |
{% include 'snippets/snippet_name' %}
83 | 84 | Usage example: 85 |
 86 |   {% include 'sidebar' %} # this will render 'sidebar' page_part
 87 |   {% assign navigation = 'snippets/navigation' %}
 88 |   {% include navigation %} # this will render 'navigation' snippet
 89 | 
90 | 91 | ### stylesheets, javascripts 92 |
{% stylesheets path [, path, path ...] %}
93 | Both tags syntax is equal 94 | Tags renders rail`s stylesheet_link_tag or javascript_include_tag. 95 | 96 | Usage example: 97 |
 98 |   {% assign ctrl = 'controls' %}
 99 |   {% javascripts 'prototype', ctrl %}
100 | 
101 | 102 | -------------------------------------------------------------------------------- /spec/lib/liquid/tags/cache_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe PufferPages::Liquid::Tags::Cache do 4 | describe '#expires_in' do 5 | subject { PufferPages::Liquid::Tags::Cache.allocate } 6 | 7 | specify { subject.expires_in('10').should == 10.seconds } 8 | specify { subject.expires_in('10s').should == 10.seconds } 9 | specify { subject.expires_in('10m').should == 10.minutes } 10 | specify { subject.expires_in('10h').should == 10.hours } 11 | specify { subject.expires_in('10h 10m 10s').should == 10.hours + 10.minutes + 10.seconds } 12 | specify { subject.expires_in(nil).should be_nil } 13 | specify { subject.expires_in('').should be_nil } 14 | specify { subject.expires_in('10blabla').should be_nil } 15 | specify { subject.expires_in('10blabla 10m').should be_nil } 16 | end 17 | 18 | describe '#render' do 19 | 20 | def cache_key *args 21 | ActiveSupport::Cache.expand_cache_key args.unshift('puffer_pages_cache') 22 | end 23 | 24 | let!(:root) { Fabricate :root } 25 | before { PufferPages.cache_store = :memory_store } 26 | before { PufferPages.config.stub(:perform_caching) { true } } 27 | 28 | context 'simple caching' do 29 | let(:entry) { ActiveSupport::Cache::Entry.new 'Bye' } 30 | 31 | specify do 32 | PufferPages.config.stub(:perform_caching) { false } 33 | PufferPages.cache_store.should_not_receive(:write) 34 | PufferPages.cache_store.should_not_receive(:read_entry) 35 | root.render("{% cache %}Hello{% endcache %}").should == 'Hello' 36 | end 37 | 38 | specify do 39 | PufferPages.cache_store.should_receive(:write).with( 40 | cache_key(root), 'Hello', {} 41 | ).and_call_original 42 | root.render("{% cache %}Hello{% endcache %}").should == 'Hello' 43 | end 44 | 45 | specify do 46 | PufferPages.cache_store.stub(:read_entry).with( 47 | cache_key(root), {} 48 | ) { entry } 49 | root.render("{% cache %}Hello{% endcache %}").should == entry.value 50 | end 51 | end 52 | 53 | context 'additional params' do 54 | specify do 55 | PufferPages.cache_store.should_receive(:write).with( 56 | cache_key(root), 'Hello', { expires_in: 60 } 57 | ).and_call_original 58 | root.render("{% cache expires_in: '1m' %}Hello{% endcache %}") 59 | end 60 | 61 | specify do 62 | PufferPages.cache_store.should_receive(:write).with( 63 | cache_key(root, 'additional_key'), 'Hello', { expires_in: 60 } 64 | ).and_call_original 65 | root.render("{% cache 'additional_key', expires_in: '1m' %}Hello{% endcache %}") 66 | end 67 | 68 | specify do 69 | PufferPages.cache_store.should_receive(:write).with( 70 | cache_key(root, 'key1', 'key2'), 'Hello', {} 71 | ).and_call_original 72 | root.render("{% cache 'key1', 'key2', expires_in: '' %}Hello{% endcache %}") 73 | end 74 | 75 | specify do 76 | PufferPages.cache_store.should_receive(:write).with( 77 | cache_key(root, 'additional_key'), 'Hello', { expires_in: 3600 } 78 | ).and_call_original 79 | root.render( 80 | "{% cache key, expires_in: expire %}Hello{% endcache %}", 81 | { expire: '1h', key: 'additional_key' } 82 | ) 83 | end 84 | end 85 | 86 | context 'proper cache key' do 87 | let!(:custom) { Fabricate :custom, body: "{% cache %}Hello{% endcache %}" } 88 | 89 | specify do 90 | PufferPages.cache_store.should_receive(:write).with( 91 | cache_key(custom), 'Hello', {} 92 | ).and_call_original 93 | root.render("{% snippet 'custom' %}") 94 | end 95 | end 96 | 97 | context 'erb tracker cleanup' do 98 | specify do 99 | root.render("{% cache %}{% yield %}{% endcache %}").should == '<%= yield %>' # write 100 | root.render("{% cache %}{% yield %}{% endcache %}").should == '<%= yield %>' # read 101 | end 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /db/migrate/20120924120226_migrate_to_uuid.rb: -------------------------------------------------------------------------------- 1 | class PreviousPage < ActiveRecord::Base 2 | acts_as_nested_set 3 | has_many :page_parts, :class_name => '::PreviousPagePart', :inverse_of => :page, :foreign_key => :page_id 4 | def import_attributes 5 | attributes.except *%w(id parent_id lft rgt depth location title description keywords) 6 | end 7 | end 8 | 9 | class PreviousPagePart < ActiveRecord::Base 10 | belongs_to :page, :class_name => '::PreviousPage', :inverse_of => :page_parts 11 | def import_attributes 12 | attributes.except *%w(id page_id) 13 | end 14 | end 15 | 16 | class PreviousLayout < ActiveRecord::Base 17 | def import_attributes 18 | attributes.except *%w(id) 19 | end 20 | end 21 | 22 | class PreviousSnippet < ActiveRecord::Base 23 | def import_attributes 24 | attributes.except *%w(id) 25 | end 26 | end 27 | 28 | class MigrateToUuid < ActiveRecord::Migration 29 | def up 30 | [:pages, :page_parts, :layouts, :snippets].each do |table| 31 | ActiveRecord::Base.connection.indexes(table).each do |index| 32 | remove_index table, :name => index.name 33 | end 34 | rename_table table, :"previous_#{table}" 35 | end 36 | 37 | create_table :pages, :id => false do |t| 38 | t.uuid :id, :primary_key => true 39 | t.string :name 40 | t.string :slug 41 | t.string :layout_name 42 | t.string :status 43 | t.uuid :parent_id 44 | t.integer :lft 45 | t.integer :rgt 46 | t.integer :depth, :default => 0 47 | t.string :location 48 | t.timestamps 49 | end 50 | 51 | create_table :page_parts, :id => false do |t| 52 | t.uuid :id, :primary_key => true 53 | t.string :name 54 | t.text :body 55 | t.uuid :page_id 56 | t.timestamps 57 | end 58 | 59 | create_table :layouts, :id => false do |t| 60 | t.uuid :id, :primary_key => true 61 | t.string :name 62 | t.text :body 63 | t.timestamps 64 | end 65 | 66 | create_table :snippets, :id => false do |t| 67 | t.uuid :id, :primary_key => true 68 | t.string :name 69 | t.text :body 70 | t.timestamps 71 | end 72 | 73 | add_index :pages, :slug 74 | add_index :pages, :location 75 | 76 | add_index :page_parts, :name 77 | add_index :page_parts, :page_id 78 | 79 | add_index :layouts, :name 80 | 81 | add_index :snippets, :name 82 | 83 | [PufferPages::Page, PufferPages::PagePart, PufferPages::Layout, PufferPages::Snippet, 84 | PreviousPage, PreviousPagePart, PreviousLayout, PreviousSnippet].each do |model| 85 | model.reset_column_information 86 | end 87 | 88 | puts "\nMigrating data" 89 | PufferPages::Page.transaction do 90 | import_child_pages PreviousPage.roots 91 | PreviousLayout.all.each { |layout| PufferPages::Layout.create!(layout.import_attributes); print '.' } 92 | PreviousSnippet.all.each { |snippet| PufferPages::Snippet.create!(snippet.import_attributes); print '.' } 93 | end 94 | puts "\n" 95 | 96 | [:pages, :page_parts, :layouts, :snippets].each do |table| 97 | drop_table :"previous_#{table}" 98 | end 99 | end 100 | 101 | def import_child_pages pages, new_parent = nil 102 | pages.each do |page| 103 | new_page = (new_parent ? new_parent.children : PufferPages::Page).create! page.import_attributes 104 | new_page.page_parts = page.page_parts.map do |page_part| 105 | print '.' 106 | PufferPages::PagePart.create! page_part.import_attributes 107 | end 108 | print '.' 109 | %w(title keywords description).each do |attribute| 110 | print '.' 111 | page_part = new_page.page_parts.detect { |page_part| page_part.name == attribute } 112 | if page_part 113 | page_part.update_attributes body: "#{page_part.body}\n--#{attribute} value--\n#{page.send(attribute)}" 114 | else 115 | new_page.page_parts.create! name: attribute, body: page.send(attribute) 116 | end 117 | end 118 | page.save 119 | import_child_pages page.children, new_page unless page.leaf? 120 | end 121 | end 122 | 123 | def down 124 | raise IrreversibleMigration 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20130118071516_migrate_to_uuid.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from puffer_pages (originally 20120924120226) 2 | class PreviousPage < ActiveRecord::Base 3 | acts_as_nested_set 4 | has_many :page_parts, :class_name => '::PreviousPagePart', :inverse_of => :page, :foreign_key => :page_id 5 | def import_attributes 6 | attributes.except *%w(id parent_id lft rgt depth location title description keywords) 7 | end 8 | end 9 | 10 | class PreviousPagePart < ActiveRecord::Base 11 | belongs_to :page, :class_name => '::PreviousPage', :inverse_of => :page_parts 12 | def import_attributes 13 | attributes.except *%w(id page_id) 14 | end 15 | end 16 | 17 | class PreviousLayout < ActiveRecord::Base 18 | def import_attributes 19 | attributes.except *%w(id) 20 | end 21 | end 22 | 23 | class PreviousSnippet < ActiveRecord::Base 24 | def import_attributes 25 | attributes.except *%w(id) 26 | end 27 | end 28 | 29 | class MigrateToUuid < ActiveRecord::Migration 30 | def up 31 | [:pages, :page_parts, :layouts, :snippets].each do |table| 32 | ActiveRecord::Base.connection.indexes(table).each do |index| 33 | remove_index table, :name => index.name 34 | end 35 | rename_table table, :"previous_#{table}" 36 | end 37 | 38 | create_table :pages, :id => false do |t| 39 | t.uuid :id, :primary_key => true 40 | t.string :name 41 | t.string :slug 42 | t.string :layout_name 43 | t.string :status 44 | t.uuid :parent_id 45 | t.integer :lft 46 | t.integer :rgt 47 | t.integer :depth, :default => 0 48 | t.string :location 49 | t.timestamps 50 | end 51 | 52 | create_table :page_parts, :id => false do |t| 53 | t.uuid :id, :primary_key => true 54 | t.string :name 55 | t.text :body 56 | t.uuid :page_id 57 | t.timestamps 58 | end 59 | 60 | create_table :layouts, :id => false do |t| 61 | t.uuid :id, :primary_key => true 62 | t.string :name 63 | t.text :body 64 | t.timestamps 65 | end 66 | 67 | create_table :snippets, :id => false do |t| 68 | t.uuid :id, :primary_key => true 69 | t.string :name 70 | t.text :body 71 | t.timestamps 72 | end 73 | 74 | add_index :pages, :slug 75 | add_index :pages, :location 76 | 77 | add_index :page_parts, :name 78 | add_index :page_parts, :page_id 79 | 80 | add_index :layouts, :name 81 | 82 | add_index :snippets, :name 83 | 84 | [PufferPages::Page, PufferPages::PagePart, PufferPages::Layout, PufferPages::Snippet, 85 | PreviousPage, PreviousPagePart, PreviousLayout, PreviousSnippet].each do |model| 86 | model.reset_column_information 87 | end 88 | 89 | puts "\nMigrating data" 90 | PufferPages::Page.transaction do 91 | import_child_pages PreviousPage.roots 92 | PreviousLayout.all.each { |layout| PufferPages::Layout.create!(layout.import_attributes); print '.' } 93 | PreviousSnippet.all.each { |snippet| PufferPages::Snippet.create!(snippet.import_attributes); print '.' } 94 | end 95 | puts "\n" 96 | 97 | [:pages, :page_parts, :layouts, :snippets].each do |table| 98 | drop_table :"previous_#{table}" 99 | end 100 | end 101 | 102 | def import_child_pages pages, new_parent = nil 103 | pages.each do |page| 104 | new_page = (new_parent ? new_parent.children : PufferPages::Page).create! page.import_attributes 105 | new_page.page_parts = page.page_parts.map do |page_part| 106 | print '.' 107 | PufferPages::PagePart.create! page_part.import_attributes 108 | end 109 | print '.' 110 | %w(title keywords description).each do |attribute| 111 | print '.' 112 | page_part = new_page.page_parts.detect { |page_part| page_part.name == attribute } 113 | if page_part 114 | page_part.update_attributes body: "#{page_part.body}\n--#{attribute} value--\n#{page.send(attribute)}" 115 | else 116 | new_page.page_parts.create! name: attribute, body: page.send(attribute) 117 | end 118 | end 119 | page.save 120 | import_child_pages page.children, new_page unless page.leaf? 121 | end 122 | end 123 | 124 | def down 125 | raise IrreversibleMigration 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /spec/lib/pagenator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe PufferPages::Extensions::Pagenator do 4 | context 'controller' do 5 | include RSpec::Rails::ControllerExampleGroup 6 | render_views 7 | 8 | let!(:foo_layout){Fabricate :foo_layout} 9 | let!(:root){Fabricate :foo_root} 10 | let!(:anonymous){Fabricate :page, :slug => 'anonymous', :name => 'foo', :parent => root} 11 | let!(:foo){Fabricate :page, :slug => 'foo', :parent => root} 12 | let!(:bar){Fabricate :page, :slug => 'bar', :parent => foo} 13 | 14 | let!(:bar_layout){Fabricate :bar_layout} 15 | let!(:root2){Fabricate :foo_root, :layout_name => 'bar_layout'} 16 | 17 | let!(:named){Fabricate :page, :slug => 'named', :name => 'foo', :parent => root} 18 | let!(:named2){Fabricate :page, :slug => 'named', :name => 'bar', :parent => root2} 19 | 20 | context do 21 | controller do 22 | puffer_pages 23 | def index; end 24 | end 25 | 26 | specify do 27 | get :index 28 | response.body.should == 'foo_layout anonymous' 29 | end 30 | end 31 | 32 | context do 33 | controller do 34 | puffer_pages :only => :index 35 | def index; end 36 | end 37 | 38 | specify do 39 | get :index 40 | response.body.should == 'foo_layout anonymous' 41 | end 42 | end 43 | 44 | context do 45 | controller do 46 | puffer_pages :except => :show 47 | def index; end 48 | end 49 | 50 | specify do 51 | get :index 52 | response.body.should == 'foo_layout anonymous' 53 | end 54 | end 55 | 56 | context do 57 | controller do 58 | puffer_pages :only => [:show] 59 | def index; end 60 | end 61 | 62 | specify{expect{get :index}.to raise_error} 63 | end 64 | 65 | context do 66 | controller do 67 | puffer_pages :except => [:index] 68 | def index; end 69 | end 70 | 71 | specify{expect{get :index}.to raise_error} 72 | end 73 | 74 | context do 75 | controller do 76 | puffer_pages 77 | def index 78 | render 'foo' 79 | end 80 | end 81 | 82 | specify do 83 | get :index 84 | response.body.should == 'foo_layout foo' 85 | end 86 | end 87 | 88 | context do 89 | controller do 90 | puffer_pages 91 | def index 92 | render PufferPages::Page.where(:slug => 'foo').first 93 | end 94 | end 95 | 96 | specify do 97 | get :index 98 | response.body.should == 'foo_layout foo' 99 | end 100 | end 101 | 102 | context do 103 | controller do 104 | puffer_pages 105 | def index 106 | render 'foo/bar' 107 | end 108 | end 109 | 110 | specify do 111 | get :index 112 | response.body.should == 'foo_layout bar' 113 | end 114 | end 115 | 116 | context do 117 | controller do 118 | puffer_pages 119 | def index 120 | render 'foo/bar' 121 | end 122 | end 123 | 124 | specify do 125 | get :index 126 | response.body.should == 'foo_layout bar' 127 | end 128 | end 129 | 130 | context do 131 | controller do 132 | puffer_pages :scope => {:name => 'foo'} 133 | def index 134 | render 'named' 135 | end 136 | end 137 | 138 | specify do 139 | get :index 140 | response.body.should == 'foo_layout named' 141 | end 142 | end 143 | 144 | context do 145 | controller do 146 | puffer_pages :scope => lambda {|conroller| 147 | {:name => 'bar'} 148 | } 149 | def index 150 | render 'named' 151 | end 152 | end 153 | 154 | specify do 155 | get :index 156 | response.body.should == 'bar_layout named' 157 | end 158 | end 159 | 160 | context do 161 | controller do 162 | puffer_pages scope: :puffer_scope 163 | def index 164 | render 'named' 165 | end 166 | 167 | def puffer_scope 168 | {name: 'bar'} 169 | end 170 | end 171 | 172 | specify do 173 | get :index 174 | response.body.should == 'bar_layout named' 175 | end 176 | end 177 | end 178 | end -------------------------------------------------------------------------------- /spec/data/localized.json: -------------------------------------------------------------------------------- 1 | { 2 | "layouts": [{ 3 | "created_at": "2013-12-23T16:23:00Z", 4 | "id": "404DF5860F01492EB470711FE3BE4535", 5 | "name": "application", 6 | "updated_at": "2013-12-23T16:23:00Z", 7 | "body_translations": { 8 | "en": "Nulla suscipit ligula in lacus.", 9 | "ru": "Morbi ut odio.", 10 | "cn": "Donec ut mauris eget massa tempor convallis." 11 | } 12 | }], 13 | "snippets": [{ 14 | "created_at": "2013-12-23T16:23:00Z", 15 | "id": "C647D698E0D64A99BC8AA0C0A0F0BAEF", 16 | "name": "lacus", 17 | "updated_at": "2013-12-23T16:23:00Z", 18 | "body_translations": { 19 | "en": "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla dapibus dolor vel est.", 20 | "ru": "Curabitur gravida nisi at nibh.", 21 | "cn": "Nam nulla." 22 | } 23 | }], 24 | "pages": [{ 25 | "created_at": "2013-12-23T16:23:00Z", 26 | "id": "55E458FCA2BB4BA0B95AE99ACDDAF094", 27 | "layout_name": "application", 28 | "locales": {}, 29 | "name": "Nunc nisl.", 30 | "parent_id": null, 31 | "slug": null, 32 | "status": "published", 33 | "updated_at": "2013-12-23T16:23:00Z", 34 | "page_parts": [{ 35 | "created_at": "2013-12-23T16:23:00Z", 36 | "handler": "html", 37 | "id": "A0C54DBB446D4727903A2B6FA5D4C9E2", 38 | "name": "body", 39 | "page_id": "55E458FCA2BB4BA0B95AE99ACDDAF094", 40 | "updated_at": "2013-12-23T16:23:00Z", 41 | "body_translations": { 42 | "en": "PagePart: `body`, Page: ``", 43 | "ru": "Nulla neque libero, convallis eget, eleifend luctus, ultricies eu, nibh.", 44 | "cn": "Fusce posuere felis sed lacus." 45 | } 46 | }, { 47 | "created_at": "2013-12-23T16:23:00Z", 48 | "handler": "html", 49 | "id": "EA1DE54674D1430894134AA9FDB603F3", 50 | "name": "sidebar", 51 | "page_id": "55E458FCA2BB4BA0B95AE99ACDDAF094", 52 | "updated_at": "2013-12-23T16:23:00Z", 53 | "body_translations": { 54 | "en": "PagePart: `sidebar`, Page: ``", 55 | "ru": "Mauris enim leo, rhoncus sed, vestibulum sit amet, cursus id, turpis.", 56 | "cn": "Vivamus vel nulla eget eros elementum pellentesque." 57 | } 58 | }] 59 | }, { 60 | "created_at": "2013-12-23T16:23:00Z", 61 | "id": "58C3B16335C242E8B231E2B634D1B7EA", 62 | "layout_name": null, 63 | "locales": {}, 64 | "name": "Morbi non quam nec dui luctus rutrum.", 65 | "parent_id": "55E458FCA2BB4BA0B95AE99ACDDAF094", 66 | "slug": "first", 67 | "status": "published", 68 | "updated_at": "2013-12-23T16:23:00Z", 69 | "page_parts": [{ 70 | "created_at": "2013-12-23T16:23:00Z", 71 | "handler": "html", 72 | "id": "A0BC46E472FC45CB8C57F068685DEA3D", 73 | "name": "body", 74 | "page_id": "58C3B16335C242E8B231E2B634D1B7EA", 75 | "updated_at": "2013-12-23T16:23:00Z", 76 | "body_translations": { 77 | "en": "PagePart: `body`, Page: `first`", 78 | "ru": "Nam ultrices, libero non mattis pulvinar, nulla pede ullamcorper augue, a suscipit nulla elit ac nulla.", 79 | "cn": "Etiam justo." 80 | } 81 | }] 82 | }, { 83 | "created_at": "2013-12-23T16:23:00Z", 84 | "id": "C8D5E74CE293403DAB5FBB5A89BDE6C5", 85 | "layout_name": null, 86 | "locales": {}, 87 | "name": "Donec dapibus.", 88 | "parent_id": "58C3B16335C242E8B231E2B634D1B7EA", 89 | "slug": "second", 90 | "status": "published", 91 | "updated_at": "2013-12-23T16:23:00Z", 92 | "page_parts": [{ 93 | "created_at": "2013-12-23T16:23:00Z", 94 | "handler": "html", 95 | "id": "504F931E6E8C495699BA5D4C9D1FAD06", 96 | "name": "sidebar", 97 | "page_id": "C8D5E74CE293403DAB5FBB5A89BDE6C5", 98 | "updated_at": "2013-12-23T16:23:00Z", 99 | "body_translations": { 100 | "en": "PagePart: `sidebar`, Page: `first/second`", 101 | "ru": "Vivamus in felis eu sapien cursus vestibulum.", 102 | "cn": "Aenean sit amet justo." 103 | } 104 | }] 105 | }] 106 | } --------------------------------------------------------------------------------