├── .rspec ├── .gitignore ├── spec ├── api_spec.rb ├── renderer │ ├── document_spec.rb │ ├── blocks │ │ └── posts_spec.rb │ └── blocks_spec.rb ├── resource_spec.rb ├── spec_helper.rb ├── resource │ └── post_spec.rb ├── tumblargh_spec.rb ├── fixtures │ └── themes │ │ ├── tumblr-boilerplate.html │ │ ├── solstice.html │ │ └── fluid.html └── parser_spec.rb ├── Gemfile ├── lib ├── tumblargh │ ├── version.rb │ ├── resource │ │ ├── tag.rb │ │ ├── note.rb │ │ ├── user.rb │ │ ├── dialogue.rb │ │ ├── photo.rb │ │ ├── base.rb │ │ ├── blog.rb │ │ └── post.rb │ ├── node │ │ ├── literal.rb │ │ ├── block_end.rb │ │ ├── root.rb │ │ ├── base.rb │ │ ├── tag.rb │ │ ├── block_start.rb │ │ └── block.rb │ ├── renderer │ │ ├── literal.rb │ │ ├── blocks │ │ │ ├── answer.rb │ │ │ ├── photoset.rb │ │ │ ├── tags.rb │ │ │ ├── reblogs.rb │ │ │ ├── chat.rb │ │ │ ├── base.rb │ │ │ ├── navigation.rb │ │ │ ├── dates.rb │ │ │ ├── audio.rb │ │ │ ├── notes.rb │ │ │ └── posts.rb │ │ ├── tag.rb │ │ ├── document.rb │ │ ├── base.rb │ │ └── blocks.rb │ ├── resource.rb │ ├── node.rb │ ├── grammar.treetop │ ├── renderer.rb │ ├── api.rb │ ├── parser.rb │ └── grammar.rb ├── middleman │ └── extensions │ │ └── tumblargh.rb ├── rack │ └── tumblargh.rb └── tumblargh.rb ├── .travis.yml ├── examples ├── middleman_config.rb └── config.ru ├── CHANGELOG.md ├── Rakefile ├── tumblargh.gemspec ├── LICENSE ├── Gemfile.lock └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | pkg/ 3 | -------------------------------------------------------------------------------- /spec/api_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /lib/tumblargh/version.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | VERSION = '0.2.2'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.0.0 4 | - 1.9.3 5 | 6 | notifications: 7 | email: false 8 | -------------------------------------------------------------------------------- /lib/tumblargh/resource/tag.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Resource 3 | 4 | class Tag < Base 5 | end 6 | 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/tumblargh/resource/note.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Resource 3 | 4 | class Note < Base 5 | end 6 | 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/tumblargh/resource/user.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Resource 3 | 4 | class User < Base 5 | end 6 | 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /examples/middleman_config.rb: -------------------------------------------------------------------------------- 1 | require 'tumblargh' 2 | 3 | activate :tumblargh, 4 | api_key: 'API KEY', 5 | blog: 'staff.tumblr.com' 6 | 7 | -------------------------------------------------------------------------------- /lib/tumblargh/resource/dialogue.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Resource 3 | 4 | class Dialogue < Base 5 | end 6 | 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/tumblargh/node/literal.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Node 3 | 4 | class Literal < Base 5 | 6 | end 7 | 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/tumblargh/node/block_end.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Node 3 | 4 | class BlockEnd < Base 5 | 6 | end 7 | 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/tumblargh/renderer/literal.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Renderer 3 | class Literal < Base 4 | def render 5 | node[1] 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/tumblargh/node/root.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Node 3 | 4 | class Root < Base 5 | 6 | def to_tree 7 | elements.map(&:to_tree) 8 | end 9 | 10 | def to_s 11 | elements.map(&:to_s).join '' 12 | end 13 | 14 | end 15 | 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/tumblargh/resource/photo.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Resource 3 | 4 | class Photo < Base 5 | 6 | def photo_url(size=500) 7 | size = size.to_i 8 | 9 | res = alt_sizes.select do |p| 10 | p[:width] == size 11 | end 12 | 13 | res.empty? ? original_size[:url] : res.first[:url] 14 | end 15 | 16 | end 17 | 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/tumblargh/node/base.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Node 3 | 4 | class Base < Treetop::Runtime::SyntaxNode 5 | 6 | def type 7 | @type ||= self.class.name.split('::').last.to_sym 8 | end 9 | 10 | def to_tree 11 | [type, text_value] 12 | end 13 | 14 | def to_s 15 | text_value 16 | end 17 | 18 | end 19 | 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.2.2 2 | 3 | - Don't raise an exception on missing blocks, merely log a warning 4 | - Implement `{TagsAsClasses}` tag 5 | 6 | ## 0.2.1 7 | 8 | - Block arguments, such as `{block:JumpPagination length="5"}` are now supported 9 | 10 | ## 0.2.0 11 | 12 | - Middlman 3.0 support 13 | - Improved permalink page reliability 14 | - Chat/dialog post support 15 | 16 | ## 0.1.0 17 | 18 | Initial release 19 | -------------------------------------------------------------------------------- /examples/config.ru: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'rubygems' 3 | require 'bundler/setup' 4 | 5 | require 'tumblargh' 6 | 7 | Tumblargh::API::set_api_key 'YOUR_TUMBLR_API_KEY' 8 | 9 | map "/" do 10 | 11 | app = proc do |env| 12 | html = Tumblargh::render_file('my_theme.html', 'willw.tumblr.com') 13 | 14 | [200, { "Content-Type" => "text/html" }, html.lines] 15 | end 16 | 17 | run app 18 | end 19 | -------------------------------------------------------------------------------- /spec/renderer/document_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | 4 | describe Tumblargh::Renderer::Document do 5 | 6 | describe "permalink pages" do 7 | 8 | before do 9 | @parser = Tumblargh::Parser.new 10 | @parser.html = "" 11 | 12 | @document = Tumblargh::Renderer::Document.new(@parser.tree, nil, :permalink => true) 13 | end 14 | 15 | it "the document should know this is a permalink page" do 16 | @document.permalink?.should be_true 17 | end 18 | 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /lib/tumblargh/renderer/blocks/answer.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Renderer 3 | module Blocks 4 | class Answer < Post 5 | contextual_tag :question 6 | contextual_tag :answer 7 | 8 | def asker 9 | if asking_name == 'anonymous' 10 | 'Anonymous' 11 | else 12 | "#{asking_name}" 13 | end 14 | end 15 | 16 | def asker_portrait_url(size) 17 | end 18 | end 19 | end 20 | end 21 | end 22 | 23 | -------------------------------------------------------------------------------- /lib/tumblargh/resource.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Resource 3 | 4 | autoload :Base, 'tumblargh/resource/base' 5 | autoload :Blog, 'tumblargh/resource/blog' 6 | autoload :Dialogue, 'tumblargh/resource/dialogue' 7 | autoload :Note, 'tumblargh/resource/note' 8 | autoload :Photo, 'tumblargh/resource/photo' 9 | autoload :Post, 'tumblargh/resource/post' 10 | autoload :Tag, 'tumblargh/resource/tag' 11 | autoload :User, 'tumblargh/resource/user' 12 | 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/tumblargh/node.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Node 3 | 4 | autoload :Base, 'tumblargh/node/base' 5 | autoload :Block, 'tumblargh/node/block' 6 | autoload :BlockEnd, 'tumblargh/node/block_end' 7 | autoload :BlockStart, 'tumblargh/node/block_start' 8 | autoload :HtmlStyleTag, 'tumblargh/node/html_style_tag' 9 | autoload :Literal, 'tumblargh/node/literal' 10 | autoload :Root, 'tumblargh/node/root' 11 | autoload :Tag, 'tumblargh/node/tag' 12 | 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tumblargh::Resource do 4 | 5 | describe "using local data" do 6 | 7 | before do 8 | json_path = File.join(FIXTURE_PATH, "data", "staff.tumblr.com-2012-05-06", "posts.json") 9 | @json = ActiveSupport::JSON.decode(open(json_path).read)["response"] 10 | end 11 | 12 | it "should not have to fall back to the API" do 13 | Tumblargh::API.disable! 14 | 15 | blog = Tumblargh::Resource::Blog.new("staff.tumblr.com", @json) 16 | lambda { blog.posts }.should_not raise_error 17 | 18 | Tumblargh::API.enable! 19 | end 20 | 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | require 'open-uri' 3 | 4 | # if something??! 5 | # require 'simplecov' 6 | # SimpleCov.start 7 | # end 8 | 9 | $: << File.dirname(__FILE__) + '/../lib' 10 | 11 | require 'tumblargh' 12 | 13 | FIXTURE_PATH = File.join(File.dirname(__FILE__), 'fixtures') 14 | TUMBLR_API_KEY = '8QoLnQy4lP0rn6QHNYSDxmhZo0L6xelNmNosAVj703FNfLBhZQ' 15 | 16 | # Requires supporting files with custom matchers and macros, etc, 17 | # in ./support/ and its subdirectories. 18 | Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each {|f| require f} 19 | 20 | RSpec.configure do |config| 21 | 22 | config.color_enabled = true 23 | 24 | end 25 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | 4 | require 'rspec/core/rake_task' 5 | 6 | desc 'Default: run specs.' 7 | task :default => :spec 8 | 9 | desc "Run specs" 10 | RSpec::Core::RakeTask.new do |t| 11 | t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default. 12 | # Put spec opts in a file named .rspec in root 13 | end 14 | 15 | desc "Generate code coverage" 16 | RSpec::Core::RakeTask.new(:coverage) do |t| 17 | t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default. 18 | end 19 | 20 | desc "Open an irb session preloaded with this library" 21 | task :console do 22 | sh "irb -rubygems -I lib -r tumblargh.rb" 23 | end 24 | -------------------------------------------------------------------------------- /lib/tumblargh/node/tag.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Node 3 | 4 | class Tag < Base 5 | def type 6 | return @type if defined?(@type) 7 | 8 | n = name.split(':') 9 | if n.size == 2 10 | @type = "#{n.first.camelize.to_sym}Tag" 11 | 12 | if @type == 'BlockTag' 13 | raise ParserError, "There's an unclosed block somewhere near `#{name}`" 14 | end 15 | 16 | @type 17 | else 18 | super 19 | end 20 | end 21 | 22 | def name 23 | elements[1].text_value 24 | end 25 | 26 | def to_tree 27 | return [type, name] 28 | end 29 | 30 | end 31 | 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/tumblargh/renderer/blocks/photoset.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Renderer 3 | module Blocks 4 | 5 | class Photoset < Photo 6 | def should_render? 7 | context_post.type == 'photoset' && context_post.photos.size > 1 8 | end 9 | end 10 | 11 | # Rendered for each of the Photoset photos 12 | class Photos < Base 13 | def render 14 | if context.is_a? Resource::Photo 15 | super 16 | else 17 | context.photos.map do |photo| 18 | photo.context = self 19 | self.class.new(node, photo).render 20 | end.flatten.join('') 21 | end 22 | end 23 | end 24 | 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/tumblargh/node/block_start.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Node 3 | class BlockStart < Base 4 | 5 | def name 6 | str = elements[1].text_value 7 | "#{str[0].upcase}#{str[1..str.size]}" 8 | end 9 | 10 | def options 11 | arguments = elements[3] 12 | return {} if arguments.nil? || arguments.elements.nil? 13 | 14 | arguments.elements.inject({}) do |memo, node| 15 | nodes = node.elements.first.elements 16 | k = nodes[0].text_value 17 | v = nodes[3].text_value 18 | 19 | memo[k] = v 20 | memo 21 | end 22 | end 23 | 24 | def matching_end 25 | "{/block:#{name}}" 26 | end 27 | 28 | def to_tree 29 | return [type, name, options] 30 | end 31 | 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/tumblargh/renderer/tag.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Renderer 3 | class Tag < Base 4 | def render 5 | # {PhotoURL-500} becomes photo_url(500) 6 | tag, *args = node[1].split('-') 7 | context.send tag.underscore, *args 8 | end 9 | end 10 | 11 | class CustomTag < Tag 12 | def method_name 13 | self.class.name.demodulize.sub('Tag', '').downcase 14 | end 15 | 16 | def render 17 | context.send(method_name, node[1].split(':').last) 18 | end 19 | end 20 | 21 | class ColorTag < CustomTag 22 | end 23 | 24 | class ImageTag < CustomTag 25 | end 26 | 27 | class FontTag < CustomTag 28 | end 29 | 30 | class TextTag < CustomTag 31 | end 32 | 33 | class LangTag < CustomTag 34 | end 35 | 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/tumblargh/node/block.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Node 3 | class Block < Root 4 | 5 | def name 6 | # First node is BlockStart 7 | elements.first.name 8 | end 9 | 10 | def options 11 | elements.first.options 12 | end 13 | 14 | def to_tree 15 | ary = [type, name, options] 16 | 17 | # Second node is a Treetop SyntaxNode which holds 18 | # the rest of the block contents. Extra parse node 19 | # due to grouping in the block grammar 20 | elements[1].elements.each do |e| 21 | if e.respond_to?(:to_tree) 22 | ary << e.to_tree 23 | else 24 | raise ParserError, "Unknown node type '#{e.class.name}' in Block '#{name}'" 25 | end 26 | end 27 | 28 | ary 29 | end 30 | 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/tumblargh/renderer/blocks/tags.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Renderer 3 | module Blocks 4 | 5 | # Rendered for each of a post's tags. 6 | class Tags < Base 7 | def tag 8 | context.name 9 | end 10 | 11 | def url_safe_tag 12 | escape_url(tag) 13 | end 14 | 15 | def tag_url 16 | "/tagged/#{url_safe}" 17 | end 18 | 19 | def tag_url_chrono 20 | "#{tag_url}/chrono" 21 | end 22 | 23 | def render 24 | if context.is_a? Resource::Tag 25 | super 26 | else 27 | context.tags.map do |tag| 28 | tag.context = self 29 | self.class.new(node, tag).render 30 | end.flatten.join('') 31 | end 32 | end 33 | 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/renderer/blocks/posts_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tumblargh::Renderer::Blocks::Posts do 4 | 5 | before do 6 | json_path = File.join(FIXTURE_PATH, "data", "staff.tumblr.com-2012-05-06", "posts.json") 7 | @json = ActiveSupport::JSON.decode(open(json_path).read)["response"].with_indifferent_access 8 | end 9 | 10 | it "should render the correct amount of posts" do 11 | theme = "{block:Posts}!{/block:Posts}" 12 | 13 | result = Tumblargh.render_html(theme, @json) 14 | result.count("!").should eql @json[:posts].size 15 | end 16 | 17 | describe "{TagsAsClasses}" do 18 | let(:post) { Tumblargh::Resource::Post.new({ tags: ["TROLOLOL", "Herp derp"] }, nil) } 19 | 20 | subject { Tumblargh::Renderer::Blocks::Posts.new(nil, post) } 21 | 22 | it "should give the tags as class names" do 23 | subject.tags_as_classes.should eql "trololol herp_derp" 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/tumblargh/renderer/blocks/reblogs.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Renderer 3 | module Blocks 4 | # TODO: Impl. 5 | 6 | # Rendered if a post was reblogged from another post. 7 | class RebloggedFrom < Base 8 | def should_render? 9 | false 10 | end 11 | 12 | def reblog_parent_name 13 | 14 | end 15 | 16 | def reblog_parent_title 17 | 18 | end 19 | 20 | def reblog_parent_url 21 | 22 | end 23 | 24 | def reblog_parent_portrait_url(size) 25 | end 26 | 27 | def reblog_root_name 28 | 29 | end 30 | 31 | def reblog_root_title 32 | 33 | end 34 | 35 | def reblog_root_portrait_url(size) 36 | 37 | end 38 | 39 | end 40 | 41 | class NotReblog < Base 42 | def should_render? 43 | true 44 | end 45 | end 46 | 47 | end 48 | end 49 | end 50 | 51 | -------------------------------------------------------------------------------- /lib/tumblargh/resource/base.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Resource 3 | 4 | class Base 5 | 6 | # Needed by renderer for context propagation 7 | attr_accessor :context 8 | 9 | def initialize(attrs={}) 10 | self.attributes = attrs 11 | end 12 | 13 | attr_reader :attributes 14 | 15 | def attributes=(attrs) 16 | @attributes = attrs.with_indifferent_access 17 | end 18 | 19 | def method_missing(method_symbol, *arguments) 20 | method_name = method_symbol.to_s 21 | 22 | if method_name =~ /(=|\?)$/ 23 | case $1 24 | when "=" 25 | attributes[$`] = arguments.first 26 | when "?" 27 | attributes[$`] 28 | end 29 | else 30 | return attributes[method_name] if attributes.include?(method_name) 31 | 32 | # propagating renderer context 33 | return context.send(method_symbol, *arguments) unless context.nil? 34 | end 35 | end 36 | end 37 | 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/tumblargh/renderer/blocks/chat.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Renderer 3 | module Blocks 4 | 5 | class Lines < Base 6 | 7 | contextual_tag :label 8 | contextual_tag :name 9 | contextual_tag :line, :phrase 10 | 11 | # "odd" or "even" for each line of this post. 12 | # TODO Implementation 13 | def alt 14 | end 15 | 16 | 17 | # A unique identifying integer representing the user of the current line of this post. 18 | # TODO Implementation 19 | def user_number 20 | end 21 | 22 | def render 23 | if context.is_a? Resource::Dialogue 24 | super 25 | else 26 | context.dialogue.map do |l| 27 | l.context = self 28 | self.class.new(node, l).render 29 | end.flatten.join('') 30 | end 31 | end 32 | 33 | end 34 | 35 | class Label < Base 36 | 37 | should_render_unless_blank :label 38 | 39 | end 40 | 41 | end 42 | end 43 | end 44 | 45 | -------------------------------------------------------------------------------- /lib/tumblargh/renderer/blocks/base.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Renderer 3 | module Blocks 4 | class Base < Renderer::Base 5 | 6 | class << self 7 | attr_accessor :should_render_if 8 | alias_method :should_render_unless_blank, :should_render_if= 9 | alias_method :should_render_unless_empty, :should_render_if= 10 | end 11 | 12 | def should_render? 13 | if defined?(@should_render_if) 14 | val = send(@should_render_if) 15 | return !(val || val.nil? || (val.respond_to?(:blank?) ? val.blank? : val.empty?)) 16 | end 17 | 18 | true 19 | end 20 | 21 | def render 22 | return '' unless should_render? 23 | 24 | _, type, options, *nodes = node 25 | 26 | res = nodes.map do |n| 27 | renderer = Renderer.factory(n, self, options) 28 | renderer.render unless renderer.nil? 29 | end 30 | 31 | " #{ res.join('') } " 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/resource/post_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tumblargh::Resource::Post do 4 | 5 | before do 6 | json_path = File.join(FIXTURE_PATH, "data", "staff.tumblr.com-2012-05-06", "posts.json") 7 | @json = ActiveSupport::JSON.decode(open(json_path).read)["response"] 8 | @blog = Tumblargh::Resource::Blog.new("staff.tumblr.com", @json) 9 | @posts = @blog.posts 10 | end 11 | 12 | it "should have an instance of Time for its date attribute" do 13 | @posts.each do |post| 14 | post.date.should be_an_instance_of Time 15 | end 16 | end 17 | 18 | it "should always return an array of tags" do 19 | @posts.each do |post| 20 | post.tags.should be_an_instance_of Array 21 | end 22 | end 23 | 24 | it "should correctly parse the number of tags" do 25 | @posts.each do |post| 26 | post.tags.size.should eql post.attributes[:tags].size 27 | end 28 | end 29 | 30 | it "should have the correct tag names" do 31 | @posts.each do |post| 32 | post.tags.map(&:name).should == post.attributes[:tags] 33 | end 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /tumblargh.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/tumblargh/version', __FILE__) 3 | 4 | Gem::Specification.new do |s| 5 | s.name = 'tumblargh' 6 | s.version = Tumblargh::VERSION 7 | s.license = 'MIT' 8 | s.summary = 'Groan-less Tumblr theme development.' 9 | s.description = "Tumblargh aims to reduce suffering involved with building a theme by offering a way to fully develop, lint and test Tumblr themes locally, with real posts from any existing Tumblog." 10 | s.authors = ['Jason Webster'] 11 | s.email = 'jason@metalabdesign.com' 12 | s.homepage = 'http://github.com/jasonwebster/tumblargh' 13 | 14 | s.files = `git ls-files`.split("\n") 15 | s.require_paths = ['lib'] 16 | 17 | s.add_dependency 'activesupport', '>= 3.1' 18 | s.add_dependency 'api_cache' 19 | s.add_dependency 'nokogiri' 20 | s.add_dependency 'treetop' 21 | 22 | s.add_development_dependency 'autotest-standalone' 23 | s.add_development_dependency 'rake' 24 | s.add_development_dependency 'rspec' 25 | s.add_development_dependency 'simplecov' 26 | end 27 | 28 | -------------------------------------------------------------------------------- /lib/tumblargh/grammar.treetop: -------------------------------------------------------------------------------- 1 | grammar Tumblr 2 | 3 | rule root 4 | (block / tag / orphan / literal)* 5 | end 6 | 7 | rule block 8 | block_start 9 | (block / tag / orphan / literal)* 10 | block_end 11 | 12 | end 13 | 14 | rule block_start 15 | '{block:' block_name space? block_arguments? '}' space? 16 | end 17 | 18 | rule block_end 19 | '{/block:' block_name '}' space? 20 | end 21 | 22 | rule block_name 23 | [^\s}:;]+ 24 | end 25 | 26 | rule block_arguments 27 | block_argument+ 28 | end 29 | 30 | rule block_argument 31 | (([a-zA-Z0-9]+) '=' '"' ([a-zA-Z0-9]+) '"') space? 32 | end 33 | 34 | rule tag 35 | '{' tag_name '}' 36 | end 37 | 38 | rule tag_name 39 | ([a-zA-Z0-9]+ ':'? [^\n:/{};\[\]\(\)]*) 40 | end 41 | 42 | rule orphan 43 | '{' !'/' 44 | end 45 | 46 | rule literal 47 | [^{]+ 48 | end 49 | 50 | rule space 51 | [\s]+ 52 | end 53 | 54 | end 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Jason Webster, MetaLab Design Ltd. 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/middleman/extensions/tumblargh.rb: -------------------------------------------------------------------------------- 1 | require 'rack/tumblargh' 2 | 3 | module Middleman 4 | module Extensions 5 | module Tumblargh 6 | class << self 7 | 8 | def registered(app, options={}) 9 | ::Tumblargh::API::set_api_key(options[:api_key]) 10 | 11 | app.after_configuration do 12 | unless build? 13 | use ::Rack::Tumblargh, options 14 | end 15 | end 16 | 17 | end 18 | 19 | alias :included :registered 20 | end 21 | 22 | end 23 | end 24 | 25 | # So, page proxies don't support globs or regex matching anymore, due to how the 26 | # new Sitemap stuff works (at least as far as I can tell). This is what I came 27 | # up with as a workaround. 28 | module Sitemap 29 | class Store 30 | alias_method :orig_find_resource_by_destination_path, :find_resource_by_destination_path 31 | 32 | def find_resource_by_destination_path(request_path) 33 | request_path = "/index.html" if request_path.match(/^\/post\//) 34 | orig_find_resource_by_destination_path(request_path) 35 | end 36 | end 37 | end 38 | 39 | end 40 | -------------------------------------------------------------------------------- /lib/tumblargh/resource/blog.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Resource 3 | 4 | class Blog < Base 5 | 6 | attr_accessor :domain 7 | 8 | def initialize(domain, attrs=nil) 9 | @domain = domain 10 | self.attributes = attrs.nil? ? fetch : attrs 11 | end 12 | 13 | def attributes=(attrs) 14 | attrs = attrs.with_indifferent_access 15 | 16 | # We passed in result from /posts, or a local file 17 | if attrs.include?(:posts) && attrs.include?(:blog) 18 | self.posts = attrs[:posts] 19 | attrs.delete(:posts) 20 | 21 | self.attributes = attrs[:blog] 22 | else 23 | super(attrs) 24 | end 25 | 26 | @attributes 27 | end 28 | 29 | def fetch 30 | API.blog(domain) 31 | end 32 | 33 | def fetch! 34 | self.attributes = fetch 35 | end 36 | 37 | def posts 38 | @posts || self.posts = API.posts(domain) 39 | @posts # Whyyy??? Must I do this? 40 | end 41 | 42 | def posts=(ary) 43 | @posts = ary.map { |p| Post.new(p, self) } 44 | end 45 | 46 | end 47 | 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | tumblargh (0.2.2) 5 | activesupport (>= 3.1) 6 | api_cache 7 | nokogiri 8 | treetop 9 | 10 | GEM 11 | remote: https://rubygems.org/ 12 | specs: 13 | activesupport (3.2.14) 14 | i18n (~> 0.6, >= 0.6.4) 15 | multi_json (~> 1.0) 16 | api_cache (0.2.3) 17 | autotest-standalone (4.5.9) 18 | diff-lcs (1.1.3) 19 | i18n (0.6.5) 20 | mini_portile (0.5.1) 21 | multi_json (1.1.0) 22 | nokogiri (1.6.0) 23 | mini_portile (~> 0.5.0) 24 | polyglot (0.3.3) 25 | rake (0.9.2.2) 26 | rspec (2.8.0) 27 | rspec-core (~> 2.8.0) 28 | rspec-expectations (~> 2.8.0) 29 | rspec-mocks (~> 2.8.0) 30 | rspec-core (2.8.0) 31 | rspec-expectations (2.8.0) 32 | diff-lcs (~> 1.1.2) 33 | rspec-mocks (2.8.0) 34 | simplecov (0.6.1) 35 | multi_json (~> 1.0) 36 | simplecov-html (~> 0.5.3) 37 | simplecov-html (0.5.3) 38 | treetop (1.4.15) 39 | polyglot 40 | polyglot (>= 0.3.1) 41 | 42 | PLATFORMS 43 | ruby 44 | 45 | DEPENDENCIES 46 | autotest-standalone 47 | rake 48 | rspec 49 | simplecov 50 | tumblargh! 51 | -------------------------------------------------------------------------------- /lib/tumblargh/renderer.rb: -------------------------------------------------------------------------------- 1 | require 'cgi' 2 | 3 | require 'tumblargh/renderer/base' 4 | require 'tumblargh/renderer/blocks' 5 | require 'tumblargh/renderer/document' 6 | require 'tumblargh/renderer/literal' 7 | require 'tumblargh/renderer/tag' 8 | 9 | module Tumblargh 10 | module Renderer 11 | 12 | def self.factory(node, context, options = {}) 13 | args = [] 14 | base = node.first.to_s 15 | 16 | if base == 'Block' 17 | block_name = node[1] 18 | 19 | if block_name[0..1] == 'If' 20 | if block_name[2..4] == 'Not' 21 | args << block_name[5..block_name.size] 22 | block_name = 'InverseBoolean' 23 | else 24 | args << block_name[2..block_name.size] 25 | block_name = 'Boolean' 26 | end 27 | elsif n_post = block_name.match(/Post(\d+)/) 28 | block_name = 'NumberedPost' 29 | args << n_post[1].to_i 30 | end 31 | 32 | base = "Blocks::#{ block_name }" 33 | end 34 | 35 | klass_name = "Tumblargh::Renderer::#{ base }" 36 | begin 37 | klass = klass_name.constantize 38 | klass.new(node, context, options, *args) 39 | rescue NameError => e 40 | puts "WARNING: Unsupported block `#{ klass_name }`" 41 | end 42 | end 43 | 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/tumblargh/api.rb: -------------------------------------------------------------------------------- 1 | require 'active_support' 2 | require 'active_support/core_ext/hash/indifferent_access' 3 | require 'active_support/core_ext/object/to_query' 4 | require 'active_support/json' 5 | 6 | require 'api_cache' 7 | require 'open-uri' 8 | 9 | module Tumblargh 10 | module API 11 | 12 | API_ROOT = 'http://api.tumblr.com/v2/blog/' 13 | 14 | @enabled = true 15 | 16 | class << self 17 | 18 | attr_accessor :api_key 19 | alias_method :set_api_key, :api_key= 20 | 21 | def fetch(path, query={}) 22 | raise "API is disabled" unless enabled? 23 | 24 | query = query.merge(:api_key => api_key).to_query 25 | url = "#{API_ROOT}#{path}?#{query}" 26 | resp = APICache.get(url) { open(url).read } 27 | ActiveSupport::JSON.decode(resp)['response'] 28 | end 29 | 30 | def blog(domain) 31 | fetch("#{domain}/info")['blog'] 32 | end 33 | 34 | def posts(domain, query={}) 35 | fetch("#{domain}/posts")['posts'] 36 | end 37 | 38 | def notes(domain, query) 39 | query.merge!(:notes_info => 'true') 40 | fetch("#{domain}/posts", query)['posts'][0]['notes'] 41 | end 42 | 43 | 44 | def enable! 45 | @enabled = true 46 | end 47 | 48 | def disable! 49 | @enabled = false 50 | end 51 | 52 | def enabled? 53 | @enabled 54 | end 55 | 56 | end 57 | 58 | 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/tumblargh/renderer/document.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Renderer 3 | class Document < Base 4 | 5 | # Are we rendering a permalink page? 6 | def permalink? 7 | options[:permalink] == true 8 | end 9 | 10 | # TAGS ---------- 11 | contextual_tag :title 12 | contextual_tag :description 13 | 14 | def meta_description 15 | strip_html(description) 16 | strip_html(description) 17 | end 18 | 19 | def favicon 20 | # TODO 21 | '' 22 | end 23 | 24 | def rss 25 | "#{context.url}rss" 26 | end 27 | 28 | # Appearance options 29 | # http://www.tumblr.com/docs/en/custom_themes#appearance-options 30 | def color(key) 31 | custom_value_for_type :color, key 32 | end 33 | 34 | def font(key) 35 | custom_value_for_type :font, key 36 | end 37 | 38 | def image(key) 39 | custom_value_for_type :image, key 40 | end 41 | 42 | def text(key) 43 | custom_value_for_type :text, key 44 | end 45 | 46 | def boolean(key) 47 | custom_value_for_type :if, key 48 | end 49 | 50 | def custom_value_for_type(type, key) 51 | config[type][key] rescue raise "No appearance option for #{type}:#{key}" 52 | end 53 | 54 | # END TAGS ------ 55 | 56 | def render 57 | node.map do |n| 58 | renderer = Renderer.factory(n, self) 59 | renderer.render unless renderer.nil? 60 | end.flatten.join('') 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/rack/tumblargh.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | class Tumblargh 3 | 4 | def initialize(app, options={}) 5 | @app = app 6 | @options = options 7 | @options[:blog] = 'staff.tumblr.com' if @options[:blog].nil? 8 | end 9 | 10 | attr_reader :options 11 | 12 | def call(env) 13 | request = Rack::Request.new(env) 14 | 15 | ['/tweets.js', %r{/api.*}].each do |route| 16 | if request.path.match route 17 | url = "http://#{@options[:blog]}#{request.path}?#{request.query_string}" 18 | return [301, { "Location" => url }, []] 19 | end 20 | end 21 | 22 | status, headers, response = @app.call(env) 23 | 24 | if should_parse?(status, headers) 25 | content = response.respond_to?(:body) ? response.body : response 26 | render_opts = { :permalink => permalink?(env['PATH_INFO']) } 27 | 28 | headers.delete('Content-Length') 29 | response = Rack::Response.new( 30 | render(content, render_opts), 31 | status, 32 | headers 33 | ) 34 | response.finish 35 | response.to_a 36 | else 37 | [status, headers, response] 38 | end 39 | end 40 | 41 | private 42 | 43 | def permalink?(path) 44 | !! path.match(/^\/post\/\d+/) 45 | end 46 | 47 | def should_parse?(status, headers) 48 | status == 200 && 49 | headers["Content-Type"] && 50 | headers["Content-Type"].include?("text/html") 51 | end 52 | 53 | def render(content, opts) 54 | ::Tumblargh::render_html(content.first, options[:blog], opts) 55 | end 56 | 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/tumblargh/resource/post.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Resource 3 | 4 | class Post < Base 5 | 6 | def initialize(attrs, blog) 7 | @blog = blog 8 | super(attrs) 9 | end 10 | 11 | def type 12 | if @attributes[:type] == 'photo' 13 | photos.size > 1 ? 'photoset' : 'photo' 14 | else 15 | @attributes[:type] 16 | end 17 | end 18 | 19 | # Override method_missing so this does not propagate 20 | def title 21 | @attributes[:title] 22 | end 23 | 24 | def date 25 | @date ||= @attributes[:date].to_time 26 | end 27 | 28 | def photo_url(size=500) 29 | return nil if (photos.nil? || photos.empty?) 30 | photos.first.photo_url size 31 | end 32 | 33 | def video(size=500) 34 | return nil if (player.nil? || player.empty?) 35 | 36 | size = size.to_i 37 | 38 | res = player.select do |p| 39 | p[:width] == size 40 | end 41 | 42 | res.empty? ? nil : res.first[:embed_code] 43 | end 44 | 45 | def tags 46 | @tags ||= @attributes[:tags].map do |t| 47 | Tag.new({ :name => t }) 48 | end 49 | end 50 | 51 | def notes 52 | @notes || self.notes = API.notes(@blog.domain, :id => id) 53 | @notes 54 | end 55 | 56 | def notes=(ary) 57 | @notes = ary.map { |n| Note.new(n) } 58 | end 59 | 60 | def dialogue 61 | @dialogue ||= (@attributes[:dialogue] || []).map { |t| Dialogue.new(t) } 62 | end 63 | 64 | def photos 65 | @photos ||= (@attributes[:photos] || []).map { |p| Photo.new(p) } 66 | end 67 | 68 | end 69 | 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/tumblargh/renderer/base.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Renderer 3 | class Base 4 | 5 | class << self 6 | 7 | # Define a simple tag on the block. 8 | # Name being tag name, and optionally the attibute/method to call 9 | # on the context. If the second argument is left off, it'll just use the tag name. 10 | def contextual_tag(name, attribute=nil) 11 | class_eval do 12 | define_method name do 13 | context.send(attribute || name) 14 | end 15 | end 16 | end 17 | 18 | end 19 | 20 | attr_reader :node, :options 21 | attr_accessor :context 22 | 23 | alias_method :config, :options # Backwards compatibility with old Document rendere 24 | 25 | def initialize(node, context, options = {}) 26 | @node = node 27 | @context = context 28 | @options = options.with_indifferent_access 29 | end 30 | 31 | def context_post 32 | real_post = context 33 | while not real_post.is_a?(::Tumblargh::Resource::Post) 34 | real_post = real_post.context 35 | end 36 | 37 | real_post 38 | end 39 | 40 | def escape_html(str) 41 | CGI.escapeHTML(str) 42 | end 43 | 44 | def escape_url(url) 45 | CGI.escape(url) 46 | end 47 | 48 | def strip_html(str) 49 | str.gsub(/<\/?[^>]*>/, '') 50 | end 51 | 52 | def render 53 | end 54 | 55 | alias_method :to_s, :render 56 | 57 | def method_missing(method, *arguments) 58 | raise "Can't find anything to do with '#{method}'" if context.nil? 59 | context.send(method, *arguments) 60 | end 61 | 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/tumblargh/renderer/blocks/navigation.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Renderer 3 | module Blocks 4 | # TODO: Impl. 5 | # Rendered if there is a "previous" page (newer posts) to navigate to. 6 | class PreviousPage < Base 7 | def previous_page 8 | end 9 | end 10 | 11 | # Rendered if there is a "next" page (older posts) to navigate to. 12 | class NextPage < Base 13 | def next_page 14 | end 15 | end 16 | 17 | # Rendered if there is a "previous" or "next" page. 18 | class Pagination < Base 19 | end 20 | 21 | # undocumented?? 22 | class PermalinkPagination < Base 23 | end 24 | 25 | class PreviousPost < Base 26 | def previous_post 27 | end 28 | end 29 | 30 | class NextPost < Base 31 | def next_post 32 | end 33 | end 34 | 35 | # {block:JumpPagination length="5"} {/block:JumpPagination} 36 | # Rendered for each page greater than the current page minus one-half length up to current page plus one-half length. 37 | class JumpPagination < Base 38 | end 39 | 40 | # Rendered when jump page is not the current page. 41 | class JumpPage < Base 42 | def url 43 | end 44 | 45 | def page_number 46 | end 47 | end 48 | 49 | # Rendered when jump page is the current page. 50 | class CurrentPage < JumpPage 51 | end 52 | 53 | # Rendered if Submissions are enabled. 54 | class SubmissionsEnabled < Base 55 | end 56 | 57 | # Rendered if Submissions are enabled. 58 | class AskEnabled < Base 59 | def should_render? 60 | true 61 | end 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/tumblargh/renderer/blocks/dates.rb: -------------------------------------------------------------------------------- 1 | # require 'action_view/helpers/date_helper' 2 | 3 | module Tumblargh 4 | module Renderer 5 | module Blocks 6 | 7 | # Rendered for all posts. 8 | # Always wrap dates in this block so they will be properly hidden on non-post pages. 9 | class Date < Base 10 | 11 | MAP = { 12 | # Tumblr tag => strftime symbol 13 | :day_of_month => '-d', 14 | :day_of_month_with_zero => :d, 15 | :day_of_week => :A, 16 | :short_day_of_week => :a, 17 | :day_of_week_number => :u, 18 | :day_of_year => '-j', 19 | :week_of_year => '-V', 20 | :month => :B, 21 | :short_month => :b, 22 | :month_number => '-m', 23 | :month_number_with_zero => :m 24 | } 25 | 26 | MAP.each_pair do |tag, sym| 27 | define_method tag do 28 | context.date.strftime("%#{sym}") 29 | end 30 | end 31 | 32 | def day_of_month_suffix 33 | day_of_month.ordinalize 34 | end 35 | 36 | def year 37 | date.year 38 | end 39 | 40 | def short_year 41 | year.to_s[2..4] 42 | end 43 | 44 | def time_ago 45 | # ActionView::Helpers::DateHelper::time_ago_in_words(date) 46 | "1 day ago" 47 | end 48 | 49 | end 50 | 51 | # Rendered for posts that are the first to be listed for a given day. 52 | class NewDayDate < Base 53 | end 54 | 55 | # Rendered for subsequent posts listed for a given day. 56 | class SameDayDate < Base 57 | end 58 | 59 | end 60 | end 61 | end 62 | 63 | -------------------------------------------------------------------------------- /spec/tumblargh_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tumblargh do 4 | 5 | describe "specifying the source blog/data" do 6 | 7 | it "should work with local blog data from a file" do 8 | json_path = File.join(FIXTURE_PATH, "data", "staff.tumblr.com-2012-05-06", "posts.json") 9 | 10 | blog = Tumblargh.send(:create_blog, json_path) 11 | 12 | blog.should be_an_instance_of Tumblargh::Resource::Blog 13 | blog.title.should eql "Tumblr Staff" 14 | end 15 | 16 | it "should work when passing in a hash" do 17 | json_path = File.join(FIXTURE_PATH, "data", "staff.tumblr.com-2012-05-06", "posts.json") 18 | json = ActiveSupport::JSON.decode(open(json_path).read) 19 | 20 | blog = Tumblargh.send(:create_blog, json) 21 | 22 | blog.should be_an_instance_of Tumblargh::Resource::Blog 23 | blog.title.should eql "Tumblr Staff" 24 | end 25 | 26 | it "should work when passing in a Blog object" do 27 | json_path = File.join(FIXTURE_PATH, "data", "staff.tumblr.com-2012-05-06", "posts.json") 28 | json = ActiveSupport::JSON.decode(open(json_path).read)["response"] 29 | obj = Tumblargh::Resource::Blog.new("#{json["blog"]["name"]}.tumblr.com", json) 30 | 31 | blog = Tumblargh.send(:create_blog, obj) 32 | 33 | blog.should be_an_instance_of Tumblargh::Resource::Blog 34 | blog.title.should eql "Tumblr Staff" 35 | end 36 | 37 | it "should work by simply passing a tumblr domain" do 38 | Tumblargh::API.set_api_key TUMBLR_API_KEY 39 | 40 | blog = Tumblargh.send(:create_blog, "staff.tumblr.com") 41 | 42 | blog.should be_an_instance_of Tumblargh::Resource::Blog 43 | blog.title.should eql "Tumblr Staff" 44 | 45 | Tumblargh::API.set_api_key nil 46 | end 47 | 48 | end 49 | 50 | end 51 | -------------------------------------------------------------------------------- /lib/tumblargh.rb: -------------------------------------------------------------------------------- 1 | require 'active_support' 2 | require 'active_support/core_ext/hash/indifferent_access' 3 | require 'active_support/core_ext/string/conversions' 4 | require 'active_support/core_ext/time/conversions' 5 | require 'active_support/inflector' 6 | require 'tumblargh/version' 7 | 8 | module Tumblargh 9 | 10 | autoload :API, 'tumblargh/api' 11 | autoload :Node, 'tumblargh/node' 12 | autoload :Parser, 'tumblargh/parser' 13 | autoload :Renderer, 'tumblargh/renderer' 14 | autoload :Resource, 'tumblargh/resource' 15 | 16 | class << self 17 | 18 | attr_accessor :config 19 | 20 | def render_file(file, blog, options={}) 21 | render(:file, file, blog, options) 22 | end 23 | 24 | def render_html(string, blog, options={}) 25 | render(:html, string, blog, options) 26 | end 27 | 28 | private 29 | 30 | def render(setter, theme, blog, options) 31 | parser = Parser.new 32 | parser.send("#{setter}=", theme) 33 | 34 | blog = create_blog blog 35 | 36 | options = parser.options.merge(options) 37 | 38 | Renderer::Document.new(parser.tree, blog, options).render 39 | end 40 | 41 | def create_blog(blog) 42 | if blog.is_a? Resource::Blog 43 | blog 44 | elsif blog.is_a? Hash 45 | create_blog_from_hash blog 46 | elsif File.exists? blog 47 | json = ActiveSupport::JSON.decode(open(blog).read) 48 | create_blog_from_hash json 49 | else 50 | Resource::Blog.new(blog) 51 | end 52 | end 53 | 54 | def create_blog_from_hash(hash) 55 | hash = hash["response"] if hash.key? "response" 56 | Resource::Blog.new("#{hash["blog"]["name"]}.tumblr.com", hash) 57 | end 58 | 59 | end 60 | 61 | end 62 | 63 | if Kernel.const_defined? :Middleman 64 | require "middleman/extensions/tumblargh" 65 | Middleman::Extensions.register(:tumblargh, Middleman::Extensions::Tumblargh) 66 | end 67 | -------------------------------------------------------------------------------- /lib/tumblargh/parser.rb: -------------------------------------------------------------------------------- 1 | require 'treetop' 2 | require 'open-uri' 3 | require 'nokogiri' 4 | 5 | module Tumblargh 6 | class ParserError < StandardError 7 | end 8 | 9 | class Parser 10 | grammar_file = File.join(File.dirname(__FILE__), 'grammar') 11 | 12 | if File.exists?("#{grammar_file}.rb") 13 | require "#{grammar_file}.rb" 14 | else 15 | Treetop.load("#{grammar_file}.treetop") 16 | end 17 | 18 | @@parser = TumblrParser.new 19 | 20 | def initialize(template=nil) 21 | self.file = template 22 | end 23 | 24 | attr_reader :file 25 | 26 | def file=(file) 27 | @file = file 28 | @html = nil 29 | @structure = nil 30 | @tree = nil 31 | @config = nil 32 | end 33 | 34 | def html=(html) 35 | self.file = nil 36 | @html = html 37 | end 38 | 39 | def html 40 | @html ||= open(@file).read 41 | end 42 | 43 | def tree 44 | @tree ||= parse 45 | end 46 | 47 | def options 48 | @options ||= extract_options 49 | end 50 | 51 | def to_s 52 | parse unless @structure 53 | @structure.to_s 54 | end 55 | 56 | private 57 | 58 | def parse 59 | @structure = @@parser.parse(html) 60 | 61 | if @structure.nil? 62 | raise ParserError, @@parser.failure_reason 63 | end 64 | 65 | @tree = @structure.to_tree 66 | end 67 | 68 | def extract_options 69 | opts = {}.with_indifferent_access 70 | 71 | doc = Nokogiri::HTML(html) 72 | doc.css('meta[name*=":"]').each do |meta| 73 | type, variable = meta['name'].downcase.split(':') 74 | variable.gsub!(/\s/, '') 75 | 76 | default = meta['content'] 77 | 78 | default = case type 79 | when "if" 80 | default == "1" 81 | else 82 | default 83 | end 84 | 85 | opts[type] ||= {} 86 | opts[type][variable] = default 87 | end 88 | 89 | opts 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/tumblargh/renderer/blocks/audio.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Renderer 3 | module Blocks 4 | class Audio < Post 5 | 6 | def audio_player(color=:default) 7 | html = context.player 8 | 9 | return html if color == :default 10 | 11 | map = { :white => 'FFFFFF', :grey => 'CCCCCC', :black => '000000' } 12 | color = map[color] unless map[color].nil? 13 | 14 | html.gsub(/color=[A-Z]{6}/i, "color=#{color}") 15 | end 16 | 17 | def audio_player_white 18 | audio_player(:white) 19 | end 20 | 21 | def audio_player_grey 22 | audio_player(:grey) 23 | end 24 | 25 | def audio_player_black 26 | audio_player(:black) 27 | end 28 | 29 | # def raw_audio_url 30 | # context.player.match(/audio_file=([^&]+)/)[1] rescue nil 31 | # end 32 | 33 | contextual_tag :raw_audio_url, :audio_url 34 | contextual_tag :play_count, :plays 35 | 36 | # TODO 37 | def formatted_play_count 38 | play_count.to_s.reverse.scan(/.{1,3}/).join(',').reverse 39 | end 40 | 41 | def play_count_with_label 42 | num = formatted_play_count 43 | "#{num} play#{num == 1 ? '' : 's'}" 44 | end 45 | 46 | end 47 | 48 | class AlbumArt < Base 49 | should_render_unless_blank :album_art_url 50 | contextual_tag :album_art_url, :album_art 51 | end 52 | 53 | class Artist < Base 54 | should_render_unless_blank :artist 55 | contextual_tag :artist 56 | end 57 | 58 | class Album < Base 59 | should_render_unless_blank :album 60 | contextual_tag :album 61 | end 62 | 63 | class TrackName < Base 64 | should_render_unless_blank :track_name 65 | contextual_tag :track_name 66 | end 67 | 68 | class AudioEmbed < Base 69 | end 70 | 71 | class AudioPlayer < Base 72 | end 73 | 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/tumblargh/renderer/blocks/notes.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Renderer 3 | module Blocks 4 | 5 | # Rendered on permalink pages if this post has notes. 6 | class PostNotes < Base 7 | def should_render? 8 | context.permalink? && note_count > 0 9 | end 10 | 11 | def note_count 12 | context_post.note_count || 0 13 | end 14 | 15 | def post_notes 16 | buf = ['
    '] 17 | 18 | # TODO: Support notes with_commentary 19 | buf << context_post.notes.map do |note| 20 | classes = "note without_commentary #{note.type}" 21 | action = case note.type 22 | when 'like' 23 | 'liked this' 24 | when 'reblog' 25 | 'reblogged this from somewhere?' 26 | end 27 | 28 | <<-eos 29 |
  1. 30 | 31 | 32 | 33 | 34 | 35 | #{note.blog_name} 36 | #{action} 37 | 38 | 39 |
    40 |
  2. 41 | eos 42 | end.join("\n") 43 | 44 | buf << '
' 45 | 46 | buf.join '' 47 | end 48 | end 49 | 50 | # Rendered if this post has notes. 51 | # Always wrap note counts in this block so they will be properly hidden on non-post pages. 52 | class NoteCount < Base 53 | def should_render? 54 | note_count > 0 55 | end 56 | 57 | def note_count 58 | context_post.note_count || 0 59 | end 60 | 61 | def note_count_with_label 62 | count = note_count 63 | "#{ count } note#{ count == 1 ? '' : 's' }" 64 | end 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/tumblargh/renderer/blocks/posts.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Renderer 3 | module Blocks 4 | # Posts Loop 5 | # 6 | # {block:Posts} is executed once for each post. Some post related tags can 7 | # exist outside of a `type` block, such as {Title} or {Permalink}, so 8 | # they should be defined here 9 | class Posts < Base 10 | 11 | contextual_tag :post_id, :id 12 | contextual_tag :post_type, :type 13 | contextual_tag :title 14 | contextual_tag :caption 15 | 16 | def permalink 17 | url = context.post_url 18 | url.gsub(/^http:\/\/[^\/]+/, '') 19 | end 20 | 21 | def permalink? 22 | context.permalink? 23 | end 24 | 25 | def post_notes_url 26 | # http://bikiniatoll.tumblr.com/notes/1377511430/vqS0xw8sm 27 | "/notes/#{context.id}/" 28 | end 29 | 30 | def reblog_url 31 | "/reblog/#{context.reblog_key}" 32 | end 33 | 34 | # An HTML class-attribute friendly list of the post's tags. 35 | # Example: "humor office new_york_city" 36 | # 37 | # By default, an HTML friendly version of the source domain of imported 38 | # posts will be included. This may not behave as expected with feeds 39 | # like Del.icio.us that send their content URLs as their permalinks. 40 | # Example: "twitter_com", "digg_com", etc. 41 | # 42 | # The class-attribute "reblog" will be included automatically if the 43 | # post was reblogged from another post. 44 | def tags_as_classes 45 | context.tags.map do |tag| 46 | tag.name.titlecase.gsub(/\s+/, '').underscore.downcase 47 | end.join " " 48 | end 49 | 50 | def render 51 | if context.is_a? Resource::Post 52 | super 53 | else 54 | posts = permalink? ? [context.posts.first] : context.posts 55 | 56 | posts.map do |post| 57 | post.context = self 58 | self.class.new(node, post).render 59 | end.flatten.join('') 60 | end 61 | end 62 | 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tumblargh 2 | 3 | [![Build Status](https://travis-ci.org/jasonwebster/tumblargh.png?branch=master)](https://travis-ci.org/jasonwebster/tumblargh) 4 | 5 | ## Groan-less Tumblr theme development 6 | 7 | ### What is this thing, and why should I care? 8 | 9 | If you've ever had to build a Tumblr theme, you've probably cried out in pain 10 | while tweaking locally, copying, pasting into the theme editor, saving, switching 11 | tabs and finally refreshing and waiting for your tesing blog to reload. 12 | 13 | Tumblargh aims to reduce suffering involved with building a theme by offering 14 | a way to fully develop, lint and test Tumblr themes locally, with real posts 15 | from any existing Tumblog. 16 | 17 | ### Getting Started 18 | 19 | You'll need to get an OAuth consumer key for the Tumblr v2 API to use remote data 20 | with Tumblargh. Registration is simple enough, just go to http://www.tumblr.com/oauth/apps 21 | and fill out the form. Any time Tumblargh asks for your API key, it'll be the 22 | OAuth Consumer key provided there. 23 | 24 | #### Middleman 25 | 26 | The recommended way to use tumblargh is in conjuction with 27 | [Middleman](http://middlemanapp.com/). 28 | 29 | > Middleman is a static site generator based on Sinatra. Providing dozens of 30 | templating languages (Haml, Sass, Compass, Slim, CoffeeScript, and more). 31 | 32 | Tumblargh includes a simple Middleman extension that turns any Middleman project 33 | into a local Tumblr theme building machine. As of `0.2.0`, Tumblargh requires 34 | Middleman `>= 3.0`. 35 | 36 | Tumblargh will automatically parse any html files served by Middleman, and 37 | populate them with content from the Tumblr of your choosing. It will not 38 | parse any HTML during Middleman's build process. The output of `middleman build` 39 | is ready for use on your blog, or submission to the Tumblr theme store. 40 | 41 | To get up and running with Middleman, first create a new Middleman project: 42 | 43 | ``` 44 | $ middleman init MY_PROJECT_NAME 45 | ``` 46 | 47 | If one does not already exist, create a Gemfile and add the following as needed: 48 | 49 | ```ruby 50 | source "http://rubygems.org" 51 | 52 | gem 'middleman' 53 | gem 'tumblargh' 54 | ``` 55 | 56 | Run `bundle install`. 57 | 58 | The bare minimum setup in your Middleman config.rb is: 59 | 60 | ```ruby 61 | require 'tumblargh' 62 | 63 | activate :tumblargh, 64 | api_key: 'API KEY', # This is your OAuth consumer key 65 | blog: 'staff.tumblr.com' 66 | ``` 67 | 68 | It is highly recommended to run the Middleman server via `bundle exec`. 69 | 70 | #### Rack 71 | 72 | See `examples/config.ru` for a minimal Rack setup, ready to go with `rackup` or 73 | your Ruby server of choice. 74 | 75 | ### Known issues & planned features 76 | 77 | - Source attribution `{block:ContentSource}` 78 | - Your likes `{block:Likes}` 79 | - Twitter integration `{block:Twitter}` 80 | - Custom page support 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /spec/renderer/blocks_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | 4 | describe Tumblargh::Renderer::Blocks do 5 | 6 | def capture_stdout(&block) 7 | original_stdout = $stdout 8 | $stdout = fake = StringIO.new 9 | begin 10 | yield 11 | ensure 12 | $stdout = original_stdout 13 | end 14 | fake.string 15 | end 16 | 17 | let(:parser) { Tumblargh::Parser.new } 18 | 19 | describe "{block:PermalinkPage}" do 20 | before do 21 | parser.html = <<-eos 22 | {block:PermalinkPage} 23 | ERMAGERD PERMERLINK 24 | {/block:PermalinkPage} 25 | eos 26 | end 27 | 28 | it "should render its content when it is a permalink page" do 29 | document = Tumblargh::Renderer::Document.new(parser.tree, nil, :permalink => true) 30 | output = document.render 31 | output.should match(/ERMAGERD PERMERLINK/) 32 | end 33 | 34 | it "should NOT render its content when it is a permalink page" do 35 | document = Tumblargh::Renderer::Document.new(parser.tree, nil, :permalink => false) 36 | output = document.render 37 | output.should_not match(/ERMAGERD PERMERLINK/) 38 | end 39 | end 40 | 41 | describe "boolean blocks" do 42 | before do 43 | parser.html = <<-eos 44 | {block:IfSomethingOnByDefault} 45 | PASS_ON 46 | {/block:IfSomethingOnByDefault} 47 | 48 | {block:IfSomethingOffByDefault} 49 | FAIL_OFF 50 | {/block:IfSomethingOffByDefault} 51 | 52 | {block:IfNotSomethingOnByDefault} 53 | FAIL_INVERSE_ON 54 | {/block:IfNotSomethingOnByDefault} 55 | 56 | {block:IfNotSomethingOffByDefault} 57 | PASS_INVERSE_OFF 58 | {/block:IfNotSomethingOffByDefault} 59 | eos 60 | end 61 | 62 | let(:options) do 63 | { 64 | "if" => { 65 | "somethingonbydefault" => true, 66 | "somethingoffbydefault" => false 67 | } 68 | } 69 | end 70 | 71 | let(:output) { Tumblargh::Renderer::Document.new(parser.tree, nil, options).render } 72 | 73 | it "should respect the default settings" do 74 | output.should match(/PASS_ON/) 75 | output.should_not match(/FAIL_OFF/) 76 | end 77 | 78 | 79 | it "inverse (IfNot) blocks should work" do 80 | output.should_not match(/FAIL_INVERSE_ON/) 81 | output.should match(/PASS_INVERSE_OFF/) 82 | end 83 | end 84 | 85 | describe "using unsupported blocks" do 86 | before do 87 | parser.html = <<-eos 88 | {block:ThisIsDefinitelyNotABlock}{/block:ThisIsDefinitelyNotABlock} 89 | eos 90 | end 91 | 92 | it "should not raise an error" do 93 | expect { 94 | Tumblargh::Renderer::Document.new(parser.tree, nil, {}).render 95 | }.not_to raise_error 96 | end 97 | 98 | it "should output a warning" do 99 | out = capture_stdout { 100 | Tumblargh::Renderer::Document.new(parser.tree, nil, {}).render 101 | } 102 | 103 | out.should =~ /WARNING: Unsupported block `Tumblargh::Renderer::Blocks::ThisIsDefinitelyNotABlock`/ 104 | end 105 | 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /spec/fixtures/themes/tumblr-boilerplate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {Title}{block:PostSummary} — {PostSummary}{/block:PostSummary} 8 | {block:Description}{/block:Description} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 36 | 37 | 38 | 39 | 52 |
53 | {block:SearchPage} 54 |
55 |

{lang:Found SearchResultCount results for SearchQuery 2}

56 | {block:NoSearchResults}

{lang:No results for SearchQuery 2}

{/block:NoSearchResults} 57 |
58 | {/block:SearchPage} 59 | {block:TagPage}

{lang:TagResultCount posts tagged Tag 3}

{/block:TagPage} 60 | 61 | {block:Posts} 62 |
63 | {block:Date} 64 | {block:NewDayDate}

{DayOfWeek}, {Month} {DayOfMonth}, {Year}

{/block:NewDayDate} 65 | {block:SameDayDate}

{DayOfWeek}, {Month} {DayOfMonth}, {Year}

{/block:SameDayDate} 66 | {/block:Date} 67 | {block:Text} 68 |
69 | {block:Title}

{Title}

{/block:Title} 70 | {Body} 71 |
72 | {/block:Text} 73 | {block:Quote} 74 |
75 |
“{Quote}”
76 | {block:Source}

— {Source}

{/block:Source} 77 |
78 | {/block:Quote} 79 | {block:Link} 80 | 84 | {/block:Link} 85 | {block:Video} 86 |
87 | {Video-500} 88 | {block:Caption}
{Caption}
{/block:Caption} 89 |
90 | {/block:Video} 91 | {block:Audio} 92 |
93 | {block:AlbumArt}{/block:AlbumArt} 94 | {AudioPlayerBlack} 95 | {block:Caption}
{Caption}
{/block:Caption} 96 |
97 | {/block:Audio} 98 | {block:Photo} 99 |
100 | {LinkOpenTag}{PhotoAlt}{LinkCloseTag} 101 | {block:Caption}
{Caption}
{/block:Caption} 102 |
103 | {/block:Photo} 104 | {block:Photoset} 105 |
106 | {block:Photos} 107 | {PhotoAlt} 108 | {block:Caption}
{Caption}
{/block:Caption} 109 | {/block:Photos} 110 | {block:Caption}
{Caption}
{/block:Caption} 111 |
112 | {/block:Photoset} 113 | 114 | {block:Panorama} 115 |
116 | {LinkOpenTag}{PhotoAlt}{LinkCloseTag} 117 | {block:Caption}
{Caption}
{/block:Caption} 118 |
119 | {/block:Panorama} 120 | 121 | {block:Chat} 122 |
123 | {block:Title}

{Title}

{/block:Title} 124 |
    125 | {block:Lines} 126 |
  • 127 | {block:Label}{Label}{/block:Label} 128 | {Line} 129 |
  • 130 | {/block:Lines} 131 |
132 |
133 | {/block:Chat} 134 | {block:Answer} 135 |
136 | 137 | {Asker} 138 | {Question} 139 | {Answer} 140 |
141 | {/block:Answer} 142 | {block:IndexPage}

{/block:IndexPage} 143 |
144 | {/block:Posts} 145 | 146 | {block:PermalinkPagination} 147 | 151 | {/block:PermalinkPagination} 152 | 153 | {block:Pagination} 154 | 164 | {/block:Pagination} 165 | 166 |
167 | 168 | 169 | -------------------------------------------------------------------------------- /spec/parser_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | 4 | describe Tumblargh::Parser do 5 | subject { Tumblargh::Parser.new } 6 | 7 | describe "given invalid input" do 8 | it "should throw an error if there is an unclosed block" do 9 | subject.html = <<-eos 10 | {block:Text} 11 | {block:Title}

{Title}

{block:Title} 12 |

{Body}

13 | {/block:text} 14 | eos 15 | 16 | lambda { subject.tree }.should raise_error Tumblargh::ParserError 17 | end 18 | end 19 | 20 | describe "given something that kinda looks like a tag but isn't" do 21 | before do 22 | subject.html = "
{CustomCSS
" 23 | end 24 | 25 | it "should NOT throw an error if there is a malformed tag" do 26 | lambda { subject.tree }.should_not raise_error Tumblargh::ParserError 27 | end 28 | 29 | it "should generate the following tree" do 30 | subject.tree.should == [[:Literal, "
"], [:Literal, "{"], [:Literal, "CustomCSS
"]] 31 | end 32 | end 33 | 34 | describe "when given a bunch of css" do 35 | before do 36 | @input = <<-eos 37 | .container{*zoom:1;margin:auto;width:960px;max-width:100%;position:relative}.container:after{content:"";display:table;clear:both}.dot-sprite,#clients .nav .dots li,#clients .nav .dots li.active,#fullscreen_nav .nav .dots li,#fullscreen_nav .nav .dots li.active{background:url("../images/dot-sa49b299bc4.png") no-repeat}body.index{min-width:1000px}body.fullscreen{overflow-x:hidden}#clients{height:461px;overflow:visible;background:#fff url("../images/homepage-clients-bg.png") repeat-x 0 0px}#clients .frame{width:958px;height:448px;margin:0 auto;position:relative;-moz-border-radius:7px;-webkit-border-radius:7px;-o-border-radius:7px;-ms-border-radius:7px;-khtml-border-radius:7px;border-radius:7px;-moz-box-shadow:#000 0px 1px 2px;-webkit-box-shadow:#000 0px 1px 2px;-o-box-shadow:#000 0px 1px 2px;box-shadow:#000 0px 1px 2px;background:#262626} 38 | 39 | #title a{ 40 | text-decoration:none; 41 | color:#fff; 42 | font-weight:bold; 43 | text-shadow: #fff 0px 1px 0px; 44 | padding:0; 45 | display:block; 46 | } 47 | 48 | #title b {height:93px; 49 | width:36px; 50 | position: absolute; 51 | top:0px; 52 | } 53 | 54 | #title b.left { 55 | background: url(http://static.tumblr.com/xsp9wak/Shikloi1h/background-title.png) top left; 56 | left:-36px; } 57 | 58 | #title b.right {right:-36px;} 59 | eos 60 | 61 | subject.html = @input 62 | end 63 | 64 | it "should NOT throw an error" do 65 | lambda { subject.tree }.should_not raise_error Tumblargh::ParserError 66 | end 67 | 68 | it "its output should match its input" do 69 | subject.to_s.should eql @input 70 | end 71 | end 72 | 73 | describe "given a simple partial" do 74 | before do 75 | subject.html = <<-eos 76 | {block:Text} 77 | {block:Title}

{Title}

{/block:Title} 78 |

{Body}

79 | {/block:text} 80 | eos 81 | end 82 | 83 | it "should not contain any custom appearance options" do 84 | subject.options.empty?.should be_true 85 | end 86 | 87 | it "should contain the following tree" do 88 | expected = [ 89 | [:Literal, " "], 90 | [:Block, "Text", {}, 91 | [:Block, "Title", {}, 92 | [:Literal, "

"], 93 | [:Tag, "Title"], 94 | [:Literal, "

"] 95 | ], 96 | [:Literal, "

"], 97 | [:Tag, "Body"], 98 | [:Literal, "

\n "] 99 | ] 100 | ] 101 | 102 | # Yes, Array.== Array is a deep value based comparison 103 | subject.tree.should == expected 104 | end 105 | end 106 | 107 | describe "given jake's solstice theme" do 108 | before do 109 | subject.html = open(File.join(FIXTURE_PATH, "themes", "solstice.html")).read 110 | end 111 | 112 | it "should contain the correct appearance options" do 113 | subject.options.should == { 114 | "image" => { 115 | "background" => "" 116 | }, 117 | "if" => { 118 | "showpeopleifollow" => true, 119 | "showmyportraitphoto" => true, 120 | "showvialinkswithrebloggedposts" => true, 121 | "useclassicpaging" => false, 122 | "hidedisquscommentcountifzero" => false 123 | }, 124 | "text" => { 125 | "disqusshortname" => "", 126 | "googleanalyticsid" => "" 127 | } 128 | } 129 | end 130 | 131 | it "should contain exacty one Posts block" do 132 | matches = subject.tree.select do |n| 133 | n[0] == :Block && n[1] == "Posts" 134 | end 135 | 136 | matches.size.should eql 1 137 | end 138 | end 139 | 140 | describe "block arguments" do 141 | context "with a single argument" do 142 | before do 143 | subject.html = <<-eos 144 | {block:JumpPagination length="5"} 145 | {block:CurrentPage}{PageNumber}{/block:CurrentPage} 146 | {/block:JumpPagination} 147 | eos 148 | end 149 | 150 | it "should contain the following tree" do 151 | expected = [ 152 | [:Literal, " "], 153 | [:Block, "JumpPagination", { "length" => "5" }, 154 | [:Block, "CurrentPage", {}, 155 | [:Literal, ""], 156 | [:Tag, "PageNumber"], 157 | [:Literal, ""] 158 | ] 159 | ] 160 | ] 161 | 162 | subject.tree.should == expected 163 | end 164 | end 165 | 166 | context "with multiple arguments" do 167 | before do 168 | subject.html = <<-eos 169 | {block:Something length="5" offset="2" foo="bar"}{/block:Something} 170 | eos 171 | end 172 | 173 | it "should contain the following tree" do 174 | expected = [ 175 | [:Literal, " "], 176 | [:Block, "Something", { "length" => "5", "offset" => "2", "foo" => "bar" }] 177 | ] 178 | 179 | subject.tree.should == expected 180 | end 181 | end 182 | end 183 | 184 | describe "tumblr-boilerplate" do 185 | before do 186 | # https://github.com/davesantos/tumblr-boilerplate/master/tumblr.html 187 | subject.html = open(File.join(FIXTURE_PATH, "themes", "tumblr-boilerplate.html")).read 188 | end 189 | 190 | it "should contain exacty one Posts block" do 191 | matches = subject.tree.select do |n| 192 | n[0] == :Block && n[1] == "Posts" 193 | end 194 | 195 | matches.size.should eql 1 196 | end 197 | end 198 | end 199 | -------------------------------------------------------------------------------- /lib/tumblargh/renderer/blocks.rb: -------------------------------------------------------------------------------- 1 | module Tumblargh 2 | module Renderer 3 | module Blocks 4 | 5 | require 'tumblargh/renderer/blocks/base' 6 | 7 | class Description < Base 8 | def should_render? 9 | !description.blank? 10 | end 11 | end 12 | 13 | class NumberedPost < Base 14 | attr_reader :num 15 | 16 | def initialize(node, context, *args) 17 | @num = args[0] 18 | super(node, context) 19 | end 20 | 21 | def should_render? 22 | num == context.posts.index(context_post) + 1 23 | end 24 | end 25 | 26 | # Common post blocks 27 | class Title < Base 28 | def should_render? 29 | !(title.nil? || title.blank?) 30 | end 31 | end 32 | 33 | class Caption < Base 34 | def should_render? 35 | !(caption.nil? || caption.blank?) 36 | end 37 | end 38 | 39 | # Base post type 40 | class Post < Base 41 | def should_render? 42 | self.class.name.demodulize.downcase == context.type 43 | end 44 | end 45 | 46 | # Source block for Quote posts 47 | class Source < Base 48 | def should_render? 49 | !source.blank? 50 | end 51 | 52 | def source 53 | context.source 54 | end 55 | end 56 | 57 | class Text < Post 58 | end 59 | 60 | class Photo < Post 61 | def should_render? 62 | context_post.type == 'photo' && context_post.photos.size == 1 63 | end 64 | 65 | def photo_alt 66 | strip_html(context.caption) 67 | end 68 | end 69 | 70 | 71 | 72 | class Video < Photo 73 | end 74 | 75 | 76 | 77 | class Quote < Post 78 | def quote 79 | context.text 80 | end 81 | end 82 | 83 | class Chat < Post 84 | end 85 | 86 | class Link < Post 87 | end 88 | 89 | 90 | # Meta-block for Appearance booleans, like {block:IfSomething} 91 | class Boolean < Base 92 | attr_reader :variable 93 | 94 | def initialize(node, context, options = {}, *args) 95 | @variable = args[0] 96 | super(node, context, options) 97 | end 98 | 99 | def should_render? 100 | context.boolean(variable.downcase) 101 | end 102 | end 103 | 104 | class InverseBoolean < Boolean 105 | def should_render? 106 | ! super 107 | end 108 | end 109 | 110 | 111 | # Rendered on permalink pages. (Useful for displaying the current post's 112 | # title in the page title) 113 | class PostTitle < Base 114 | def should_render? 115 | false 116 | end 117 | 118 | def post_title 119 | # TODO: Implementation 120 | end 121 | end 122 | 123 | # Identical to {PostTitle}, but will automatically generate a summary 124 | # if a title doesn't exist. 125 | class PostSummary < PostTitle 126 | def post_summary 127 | # TODO: Implementation 128 | end 129 | end 130 | 131 | 132 | 133 | class ContentSource < Base 134 | def should_render? 135 | !source_title.nil? 136 | end 137 | 138 | contextual_tag :source_url 139 | contextual_tag :source_title 140 | 141 | # TODO: Impl. 142 | def black_logo_url 143 | end 144 | 145 | def logo_width 146 | end 147 | 148 | def logo_height 149 | end 150 | 151 | end 152 | 153 | class SourceLogo < Base 154 | # TODO: Impl. 155 | end 156 | 157 | class NoSourceLogo < SourceLogo 158 | def should_render? 159 | ! super 160 | end 161 | end 162 | 163 | class HasTags < Base 164 | def should_render? 165 | !(tags.nil? || tags.blank?) 166 | end 167 | end 168 | 169 | 170 | # Rendered on index (post) pages. 171 | class IndexPage < Base 172 | def should_render? 173 | ! context.permalink? 174 | end 175 | end 176 | 177 | # Rendered on post permalink pages. 178 | class PermalinkPage < Base 179 | def should_render? 180 | context.permalink? 181 | end 182 | end 183 | 184 | class SearchPage < Base 185 | def should_render? 186 | false 187 | end 188 | end 189 | 190 | class NoSearchResults < Base 191 | def should_render? 192 | false 193 | end 194 | end 195 | 196 | class TagPage < Base 197 | def should_render? 198 | false 199 | end 200 | end 201 | 202 | # Rendered if you have defined any custom pages. 203 | class HasPages < Base 204 | # TODO: Implementation 205 | 206 | def should_render? 207 | false 208 | end 209 | end 210 | 211 | # Rendered for each custom page. 212 | class Pages < Base 213 | # TODO: Implementation 214 | 215 | def should_render? 216 | false 217 | end 218 | 219 | # custom page url 220 | def url 221 | end 222 | 223 | # custom page name/label 224 | def label 225 | end 226 | 227 | end 228 | 229 | # Rendered if you have Twitter integration enabled. 230 | class Twitter < Base 231 | # TODO: Implementation 232 | 233 | def should_render? 234 | false 235 | end 236 | 237 | def twitter_username 238 | end 239 | 240 | end 241 | 242 | 243 | # {block:Likes} {/block:Likes} Rendered if you are sharing your likes. 244 | # {Likes} Standard HTML output of your likes. 245 | # {Likes limit="5"} Standard HTML output of your last 5 likes. 246 | # Maximum: 10 247 | # {Likes width="200"} Standard HTML output of your likes with Audio and Video players scaled to 200-pixels wide. 248 | # Scale images with CSS max-width or similar. 249 | # {Likes summarize="100"} Standard HTML output of your likes with text summarize to 100-characters. 250 | # Maximum: 250 251 | class Likes < Base 252 | # TODO: Implementation 253 | 254 | def should_render? 255 | false 256 | end 257 | 258 | def likes 259 | end 260 | 261 | end 262 | 263 | require 'tumblargh/renderer/blocks/answer' 264 | require 'tumblargh/renderer/blocks/audio' 265 | require 'tumblargh/renderer/blocks/chat' 266 | require 'tumblargh/renderer/blocks/dates' 267 | require 'tumblargh/renderer/blocks/navigation' 268 | require 'tumblargh/renderer/blocks/notes' 269 | require 'tumblargh/renderer/blocks/photoset' 270 | require 'tumblargh/renderer/blocks/posts' 271 | require 'tumblargh/renderer/blocks/reblogs' 272 | require 'tumblargh/renderer/blocks/tags' 273 | 274 | end 275 | end 276 | end 277 | -------------------------------------------------------------------------------- /lib/tumblargh/grammar.rb: -------------------------------------------------------------------------------- 1 | # Autogenerated from a Treetop grammar. Edits may be lost. 2 | 3 | 4 | module Tumblr 5 | include Treetop::Runtime 6 | 7 | def root 8 | @root ||= :root 9 | end 10 | 11 | def _nt_root 12 | start_index = index 13 | if node_cache[:root].has_key?(index) 14 | cached = node_cache[:root][index] 15 | if cached 16 | cached = SyntaxNode.new(input, index...(index + 1)) if cached == true 17 | @index = cached.interval.end 18 | end 19 | return cached 20 | end 21 | 22 | s0, i0 = [], index 23 | loop do 24 | i1 = index 25 | r2 = _nt_block 26 | if r2 27 | r1 = r2 28 | else 29 | r3 = _nt_tag 30 | if r3 31 | r1 = r3 32 | else 33 | r4 = _nt_orphan 34 | if r4 35 | r1 = r4 36 | else 37 | r5 = _nt_literal 38 | if r5 39 | r1 = r5 40 | else 41 | @index = i1 42 | r1 = nil 43 | end 44 | end 45 | end 46 | end 47 | if r1 48 | s0 << r1 49 | else 50 | break 51 | end 52 | end 53 | r0 = instantiate_node(Tumblargh::Node::Root,input, i0...index, s0) 54 | 55 | node_cache[:root][start_index] = r0 56 | 57 | r0 58 | end 59 | 60 | module Block0 61 | def block_start 62 | elements[0] 63 | end 64 | 65 | def block_end 66 | elements[2] 67 | end 68 | end 69 | 70 | def _nt_block 71 | start_index = index 72 | if node_cache[:block].has_key?(index) 73 | cached = node_cache[:block][index] 74 | if cached 75 | cached = SyntaxNode.new(input, index...(index + 1)) if cached == true 76 | @index = cached.interval.end 77 | end 78 | return cached 79 | end 80 | 81 | i0, s0 = index, [] 82 | r1 = _nt_block_start 83 | s0 << r1 84 | if r1 85 | s2, i2 = [], index 86 | loop do 87 | i3 = index 88 | r4 = _nt_block 89 | if r4 90 | r3 = r4 91 | else 92 | r5 = _nt_tag 93 | if r5 94 | r3 = r5 95 | else 96 | r6 = _nt_orphan 97 | if r6 98 | r3 = r6 99 | else 100 | r7 = _nt_literal 101 | if r7 102 | r3 = r7 103 | else 104 | @index = i3 105 | r3 = nil 106 | end 107 | end 108 | end 109 | end 110 | if r3 111 | s2 << r3 112 | else 113 | break 114 | end 115 | end 116 | r2 = instantiate_node(SyntaxNode,input, i2...index, s2) 117 | s0 << r2 118 | if r2 119 | r8 = _nt_block_end 120 | s0 << r8 121 | end 122 | end 123 | if s0.last 124 | r0 = instantiate_node(Tumblargh::Node::Block,input, i0...index, s0) 125 | r0.extend(Block0) 126 | else 127 | @index = i0 128 | r0 = nil 129 | end 130 | 131 | node_cache[:block][start_index] = r0 132 | 133 | r0 134 | end 135 | 136 | module BlockStart0 137 | def block_name 138 | elements[1] 139 | end 140 | 141 | end 142 | 143 | def _nt_block_start 144 | start_index = index 145 | if node_cache[:block_start].has_key?(index) 146 | cached = node_cache[:block_start][index] 147 | if cached 148 | cached = SyntaxNode.new(input, index...(index + 1)) if cached == true 149 | @index = cached.interval.end 150 | end 151 | return cached 152 | end 153 | 154 | i0, s0 = index, [] 155 | if has_terminal?('{block:', false, index) 156 | r1 = instantiate_node(SyntaxNode,input, index...(index + 7)) 157 | @index += 7 158 | else 159 | terminal_parse_failure('{block:') 160 | r1 = nil 161 | end 162 | s0 << r1 163 | if r1 164 | r2 = _nt_block_name 165 | s0 << r2 166 | if r2 167 | r4 = _nt_space 168 | if r4 169 | r3 = r4 170 | else 171 | r3 = instantiate_node(SyntaxNode,input, index...index) 172 | end 173 | s0 << r3 174 | if r3 175 | r6 = _nt_block_arguments 176 | if r6 177 | r5 = r6 178 | else 179 | r5 = instantiate_node(SyntaxNode,input, index...index) 180 | end 181 | s0 << r5 182 | if r5 183 | if has_terminal?('}', false, index) 184 | r7 = instantiate_node(SyntaxNode,input, index...(index + 1)) 185 | @index += 1 186 | else 187 | terminal_parse_failure('}') 188 | r7 = nil 189 | end 190 | s0 << r7 191 | if r7 192 | r9 = _nt_space 193 | if r9 194 | r8 = r9 195 | else 196 | r8 = instantiate_node(SyntaxNode,input, index...index) 197 | end 198 | s0 << r8 199 | end 200 | end 201 | end 202 | end 203 | end 204 | if s0.last 205 | r0 = instantiate_node(Tumblargh::Node::BlockStart,input, i0...index, s0) 206 | r0.extend(BlockStart0) 207 | else 208 | @index = i0 209 | r0 = nil 210 | end 211 | 212 | node_cache[:block_start][start_index] = r0 213 | 214 | r0 215 | end 216 | 217 | module BlockEnd0 218 | def block_name 219 | elements[1] 220 | end 221 | 222 | end 223 | 224 | def _nt_block_end 225 | start_index = index 226 | if node_cache[:block_end].has_key?(index) 227 | cached = node_cache[:block_end][index] 228 | if cached 229 | cached = SyntaxNode.new(input, index...(index + 1)) if cached == true 230 | @index = cached.interval.end 231 | end 232 | return cached 233 | end 234 | 235 | i0, s0 = index, [] 236 | if has_terminal?('{/block:', false, index) 237 | r1 = instantiate_node(SyntaxNode,input, index...(index + 8)) 238 | @index += 8 239 | else 240 | terminal_parse_failure('{/block:') 241 | r1 = nil 242 | end 243 | s0 << r1 244 | if r1 245 | r2 = _nt_block_name 246 | s0 << r2 247 | if r2 248 | if has_terminal?('}', false, index) 249 | r3 = instantiate_node(SyntaxNode,input, index...(index + 1)) 250 | @index += 1 251 | else 252 | terminal_parse_failure('}') 253 | r3 = nil 254 | end 255 | s0 << r3 256 | if r3 257 | r5 = _nt_space 258 | if r5 259 | r4 = r5 260 | else 261 | r4 = instantiate_node(SyntaxNode,input, index...index) 262 | end 263 | s0 << r4 264 | end 265 | end 266 | end 267 | if s0.last 268 | r0 = instantiate_node(Tumblargh::Node::BlockEnd,input, i0...index, s0) 269 | r0.extend(BlockEnd0) 270 | else 271 | @index = i0 272 | r0 = nil 273 | end 274 | 275 | node_cache[:block_end][start_index] = r0 276 | 277 | r0 278 | end 279 | 280 | def _nt_block_name 281 | start_index = index 282 | if node_cache[:block_name].has_key?(index) 283 | cached = node_cache[:block_name][index] 284 | if cached 285 | cached = SyntaxNode.new(input, index...(index + 1)) if cached == true 286 | @index = cached.interval.end 287 | end 288 | return cached 289 | end 290 | 291 | s0, i0 = [], index 292 | loop do 293 | if has_terminal?('\G[^\\s}:;]', true, index) 294 | r1 = true 295 | @index += 1 296 | else 297 | r1 = nil 298 | end 299 | if r1 300 | s0 << r1 301 | else 302 | break 303 | end 304 | end 305 | if s0.empty? 306 | @index = i0 307 | r0 = nil 308 | else 309 | r0 = instantiate_node(SyntaxNode,input, i0...index, s0) 310 | end 311 | 312 | node_cache[:block_name][start_index] = r0 313 | 314 | r0 315 | end 316 | 317 | def _nt_block_arguments 318 | start_index = index 319 | if node_cache[:block_arguments].has_key?(index) 320 | cached = node_cache[:block_arguments][index] 321 | if cached 322 | cached = SyntaxNode.new(input, index...(index + 1)) if cached == true 323 | @index = cached.interval.end 324 | end 325 | return cached 326 | end 327 | 328 | s0, i0 = [], index 329 | loop do 330 | r1 = _nt_block_argument 331 | if r1 332 | s0 << r1 333 | else 334 | break 335 | end 336 | end 337 | if s0.empty? 338 | @index = i0 339 | r0 = nil 340 | else 341 | r0 = instantiate_node(SyntaxNode,input, i0...index, s0) 342 | end 343 | 344 | node_cache[:block_arguments][start_index] = r0 345 | 346 | r0 347 | end 348 | 349 | module BlockArgument0 350 | end 351 | 352 | module BlockArgument1 353 | end 354 | 355 | def _nt_block_argument 356 | start_index = index 357 | if node_cache[:block_argument].has_key?(index) 358 | cached = node_cache[:block_argument][index] 359 | if cached 360 | cached = SyntaxNode.new(input, index...(index + 1)) if cached == true 361 | @index = cached.interval.end 362 | end 363 | return cached 364 | end 365 | 366 | i0, s0 = index, [] 367 | i1, s1 = index, [] 368 | s2, i2 = [], index 369 | loop do 370 | if has_terminal?('\G[a-zA-Z0-9]', true, index) 371 | r3 = true 372 | @index += 1 373 | else 374 | r3 = nil 375 | end 376 | if r3 377 | s2 << r3 378 | else 379 | break 380 | end 381 | end 382 | if s2.empty? 383 | @index = i2 384 | r2 = nil 385 | else 386 | r2 = instantiate_node(SyntaxNode,input, i2...index, s2) 387 | end 388 | s1 << r2 389 | if r2 390 | if has_terminal?('=', false, index) 391 | r4 = instantiate_node(SyntaxNode,input, index...(index + 1)) 392 | @index += 1 393 | else 394 | terminal_parse_failure('=') 395 | r4 = nil 396 | end 397 | s1 << r4 398 | if r4 399 | if has_terminal?('"', false, index) 400 | r5 = instantiate_node(SyntaxNode,input, index...(index + 1)) 401 | @index += 1 402 | else 403 | terminal_parse_failure('"') 404 | r5 = nil 405 | end 406 | s1 << r5 407 | if r5 408 | s6, i6 = [], index 409 | loop do 410 | if has_terminal?('\G[a-zA-Z0-9]', true, index) 411 | r7 = true 412 | @index += 1 413 | else 414 | r7 = nil 415 | end 416 | if r7 417 | s6 << r7 418 | else 419 | break 420 | end 421 | end 422 | if s6.empty? 423 | @index = i6 424 | r6 = nil 425 | else 426 | r6 = instantiate_node(SyntaxNode,input, i6...index, s6) 427 | end 428 | s1 << r6 429 | if r6 430 | if has_terminal?('"', false, index) 431 | r8 = instantiate_node(SyntaxNode,input, index...(index + 1)) 432 | @index += 1 433 | else 434 | terminal_parse_failure('"') 435 | r8 = nil 436 | end 437 | s1 << r8 438 | end 439 | end 440 | end 441 | end 442 | if s1.last 443 | r1 = instantiate_node(SyntaxNode,input, i1...index, s1) 444 | r1.extend(BlockArgument0) 445 | else 446 | @index = i1 447 | r1 = nil 448 | end 449 | s0 << r1 450 | if r1 451 | r10 = _nt_space 452 | if r10 453 | r9 = r10 454 | else 455 | r9 = instantiate_node(SyntaxNode,input, index...index) 456 | end 457 | s0 << r9 458 | end 459 | if s0.last 460 | r0 = instantiate_node(SyntaxNode,input, i0...index, s0) 461 | r0.extend(BlockArgument1) 462 | else 463 | @index = i0 464 | r0 = nil 465 | end 466 | 467 | node_cache[:block_argument][start_index] = r0 468 | 469 | r0 470 | end 471 | 472 | module Tag0 473 | def tag_name 474 | elements[1] 475 | end 476 | 477 | end 478 | 479 | def _nt_tag 480 | start_index = index 481 | if node_cache[:tag].has_key?(index) 482 | cached = node_cache[:tag][index] 483 | if cached 484 | cached = SyntaxNode.new(input, index...(index + 1)) if cached == true 485 | @index = cached.interval.end 486 | end 487 | return cached 488 | end 489 | 490 | i0, s0 = index, [] 491 | if has_terminal?('{', false, index) 492 | r1 = instantiate_node(SyntaxNode,input, index...(index + 1)) 493 | @index += 1 494 | else 495 | terminal_parse_failure('{') 496 | r1 = nil 497 | end 498 | s0 << r1 499 | if r1 500 | r2 = _nt_tag_name 501 | s0 << r2 502 | if r2 503 | if has_terminal?('}', false, index) 504 | r3 = instantiate_node(SyntaxNode,input, index...(index + 1)) 505 | @index += 1 506 | else 507 | terminal_parse_failure('}') 508 | r3 = nil 509 | end 510 | s0 << r3 511 | end 512 | end 513 | if s0.last 514 | r0 = instantiate_node(Tumblargh::Node::Tag,input, i0...index, s0) 515 | r0.extend(Tag0) 516 | else 517 | @index = i0 518 | r0 = nil 519 | end 520 | 521 | node_cache[:tag][start_index] = r0 522 | 523 | r0 524 | end 525 | 526 | module TagName0 527 | end 528 | 529 | def _nt_tag_name 530 | start_index = index 531 | if node_cache[:tag_name].has_key?(index) 532 | cached = node_cache[:tag_name][index] 533 | if cached 534 | cached = SyntaxNode.new(input, index...(index + 1)) if cached == true 535 | @index = cached.interval.end 536 | end 537 | return cached 538 | end 539 | 540 | i0, s0 = index, [] 541 | s1, i1 = [], index 542 | loop do 543 | if has_terminal?('\G[a-zA-Z0-9]', true, index) 544 | r2 = true 545 | @index += 1 546 | else 547 | r2 = nil 548 | end 549 | if r2 550 | s1 << r2 551 | else 552 | break 553 | end 554 | end 555 | if s1.empty? 556 | @index = i1 557 | r1 = nil 558 | else 559 | r1 = instantiate_node(SyntaxNode,input, i1...index, s1) 560 | end 561 | s0 << r1 562 | if r1 563 | if has_terminal?(':', false, index) 564 | r4 = instantiate_node(SyntaxNode,input, index...(index + 1)) 565 | @index += 1 566 | else 567 | terminal_parse_failure(':') 568 | r4 = nil 569 | end 570 | if r4 571 | r3 = r4 572 | else 573 | r3 = instantiate_node(SyntaxNode,input, index...index) 574 | end 575 | s0 << r3 576 | if r3 577 | s5, i5 = [], index 578 | loop do 579 | if has_terminal?('\G[^\\n:/{};\\[\\]\\(\\)]', true, index) 580 | r6 = true 581 | @index += 1 582 | else 583 | r6 = nil 584 | end 585 | if r6 586 | s5 << r6 587 | else 588 | break 589 | end 590 | end 591 | r5 = instantiate_node(SyntaxNode,input, i5...index, s5) 592 | s0 << r5 593 | end 594 | end 595 | if s0.last 596 | r0 = instantiate_node(SyntaxNode,input, i0...index, s0) 597 | r0.extend(TagName0) 598 | else 599 | @index = i0 600 | r0 = nil 601 | end 602 | 603 | node_cache[:tag_name][start_index] = r0 604 | 605 | r0 606 | end 607 | 608 | module Orphan0 609 | end 610 | 611 | def _nt_orphan 612 | start_index = index 613 | if node_cache[:orphan].has_key?(index) 614 | cached = node_cache[:orphan][index] 615 | if cached 616 | cached = SyntaxNode.new(input, index...(index + 1)) if cached == true 617 | @index = cached.interval.end 618 | end 619 | return cached 620 | end 621 | 622 | i0, s0 = index, [] 623 | if has_terminal?('{', false, index) 624 | r1 = instantiate_node(SyntaxNode,input, index...(index + 1)) 625 | @index += 1 626 | else 627 | terminal_parse_failure('{') 628 | r1 = nil 629 | end 630 | s0 << r1 631 | if r1 632 | i2 = index 633 | if has_terminal?('/', false, index) 634 | r3 = instantiate_node(SyntaxNode,input, index...(index + 1)) 635 | @index += 1 636 | else 637 | terminal_parse_failure('/') 638 | r3 = nil 639 | end 640 | if r3 641 | r2 = nil 642 | else 643 | @index = i2 644 | r2 = instantiate_node(SyntaxNode,input, index...index) 645 | end 646 | s0 << r2 647 | end 648 | if s0.last 649 | r0 = instantiate_node(Tumblargh::Node::Literal,input, i0...index, s0) 650 | r0.extend(Orphan0) 651 | else 652 | @index = i0 653 | r0 = nil 654 | end 655 | 656 | node_cache[:orphan][start_index] = r0 657 | 658 | r0 659 | end 660 | 661 | def _nt_literal 662 | start_index = index 663 | if node_cache[:literal].has_key?(index) 664 | cached = node_cache[:literal][index] 665 | if cached 666 | cached = SyntaxNode.new(input, index...(index + 1)) if cached == true 667 | @index = cached.interval.end 668 | end 669 | return cached 670 | end 671 | 672 | s0, i0 = [], index 673 | loop do 674 | if has_terminal?('\G[^{]', true, index) 675 | r1 = true 676 | @index += 1 677 | else 678 | r1 = nil 679 | end 680 | if r1 681 | s0 << r1 682 | else 683 | break 684 | end 685 | end 686 | if s0.empty? 687 | @index = i0 688 | r0 = nil 689 | else 690 | r0 = instantiate_node(Tumblargh::Node::Literal,input, i0...index, s0) 691 | end 692 | 693 | node_cache[:literal][start_index] = r0 694 | 695 | r0 696 | end 697 | 698 | def _nt_space 699 | start_index = index 700 | if node_cache[:space].has_key?(index) 701 | cached = node_cache[:space][index] 702 | if cached 703 | cached = SyntaxNode.new(input, index...(index + 1)) if cached == true 704 | @index = cached.interval.end 705 | end 706 | return cached 707 | end 708 | 709 | s0, i0 = [], index 710 | loop do 711 | if has_terminal?('\G[\\s]', true, index) 712 | r1 = true 713 | @index += 1 714 | else 715 | r1 = nil 716 | end 717 | if r1 718 | s0 << r1 719 | else 720 | break 721 | end 722 | end 723 | if s0.empty? 724 | @index = i0 725 | r0 = nil 726 | else 727 | r0 = instantiate_node(SyntaxNode,input, i0...index, s0) 728 | end 729 | 730 | node_cache[:space][start_index] = r0 731 | 732 | r0 733 | end 734 | 735 | end 736 | 737 | class TumblrParser < Treetop::Runtime::CompiledParser 738 | include Tumblr 739 | end 740 | 741 | -------------------------------------------------------------------------------- /spec/fixtures/themes/solstice.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 24 | 25 | 26 | 27 | 28 | 29 | {Title}{block:PostSummary} — {PostSummary}{/block:PostSummary}{block:SearchPage} — Search: {SearchQuery}{/block:SearchPage} 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 75 | 76 | 77 | 78 | 79 | 97 | 98 | 99 | 100 | 101 |
102 |
103 | 106 |
107 |
108 | 321 | 370 |
371 |
372 |
373 |
374 |
375 |
376 | {block:Twitter} 377 | 378 | {/block:Twitter} 379 | {block:IfGoogleAnalyticsID} 380 | 390 | {/block:IfGoogleAnalyticsID} 391 | 392 | -------------------------------------------------------------------------------- /spec/fixtures/themes/fluid.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {block:SearchPage}{lang:Search results for SearchQuery} - {/block:SearchPage}{Title} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 928 | 929 | 930 | 931 | 932 | 933 | 934 | 935 | 936 | 945 | 946 | 947 |
948 | 949 |
950 |
951 |
952 |
{block:Posts} 953 | {block:Text}
954 | 958 | {block:Title}

{Title}

{/block:Title} 959 |
{Body}
960 | {block:RebloggedFrom}Via {ReblogParentTitle}{/block:RebloggedFrom} 961 | {block:IfDisqusShortname}{block:Date}{/block:Date}{block:IfDisqusShortname} 962 |
963 |
{/block:Text} 964 | {block:Quote}
965 | 969 |

{Quote}

970 | {block:Source}– {Source}{/block:Source} 971 | {block:RebloggedFrom}Via {ReblogParentTitle}{/block:RebloggedFrom} 972 | {block:IfDisqusShortname}{block:Date}{/block:Date}{block:IfDisqusShortname} 973 |
974 |
{/block:Quote} 975 | {block:Link}
976 | 980 |
981 | {Name} 982 | {block:Description}
983 | {Description} 984 |
{/block:Description} 985 | {block:RebloggedFrom}Via {ReblogParentTitle}{/block:RebloggedFrom} 986 | {block:IfDisqusShortname}{block:Date}{/block:Date}{block:IfDisqusShortname} 987 |
988 |
989 |
990 |
{/block:Link} 991 | {block:Photo}
992 | 996 |
997 | {LinkOpenTag}{PhotoAlt}{LinkCloseTag} 998 | {block:RebloggedFrom}Via {ReblogParentTitle}{/block:RebloggedFrom} 999 | {block:IfDisqusShortname}{block:Date}{/block:Date}{block:IfDisqusShortname} 1000 | {block:HighRes} {lang:Zoom}{/block:HighRes} 1001 |
1002 |
1003 | {block:Caption}
1004 | {Caption} 1005 |
1006 |
{/block:Caption} 1007 |
1008 |
{/block:Photo} 1009 | {block:Audio}
1010 | 1014 |
1015 | {AudioPlayerBlack} 1016 | {block:Caption}
1017 | {Caption} 1018 |
{/block:Caption} 1019 | {block:RebloggedFrom}Via {ReblogParentTitle}{/block:RebloggedFrom} 1020 | {block:IfDisqusShortname}{block:Date}{/block:Date}{block:IfDisqusShortname} 1021 |
1022 |
1023 |
1024 |
{/block:Audio} 1025 | {block:Video}
1026 | 1030 |
1031 | {Video-500} 1032 |
1033 |
1034 | {block:Caption}
1035 | {Caption} 1036 |
1037 | {block:RebloggedFrom}Via {ReblogParentTitle}{/block:RebloggedFrom} 1038 | {block:IfDisqusShortname}{block:Date}{/block:Date}{block:IfDisqusShortname} 1039 |
1040 |
{/block:Caption} 1041 |
1042 |
{/block:Video} 1043 | {block:Chat}
1044 | 1048 | {block:Title}

{Title}

{/block:Title} 1049 |
    1050 | {block:Lines} 1051 |
  • 1052 | {block:Label} {Label}{/block:Label} 1053 | {Line} 1054 |
  • 1055 | {/block:Lines} 1056 |
1057 | {block:RebloggedFrom}Via {ReblogParentTitle}{/block:RebloggedFrom} 1058 | {block:IfDisqusShortname}{block:Date}{/block:Date}{block:IfDisqusShortname} 1059 |
1060 |
{/block:Chat} 1061 | {block:PermalinkPage} 1062 |
1063 | 1067 | {block:IfDisqusShortname} 1068 |
1069 |
1070 | 1071 | {/block:IfDisqusShortname} 1072 | {block:PostNotes} 1073 | {PostNotes} 1074 | {/block:PostNotes} 1075 |
1076 | {/block:PermalinkPage} 1077 | {/block:Posts}
1078 | 1105 |
1106 |
1107 |
1108 | {block:IndexPage} 1109 | 1110 |
{CurrentPage}{TotalPages}
1111 | {block:Pagination}
1112 | {block:PreviousPage} {lang:Previous page}{/block:PreviousPage} 1113 | {block:NextPage} {lang:Next page}{/block:NextPage} 1114 |
{/block:Pagination} 1115 | {/block:IndexPage} 1116 | To Tumblr, Love PixelUnion 1117 |
1118 |
1119 | {block:English}{/block:English} 1120 | 1121 | {block:IfDisqusShortname} 1122 | 1136 | {/block:IfDisqusShortname} 1137 | 1138 | --------------------------------------------------------------------------------