dryml 1.1.0.pre0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES.txt ADDED
@@ -0,0 +1,9 @@
1
+ CHANGES:
2
+
3
+ - Hobo::Dryml moved to Dryml
4
+ - Hobo.static_tags moved to Dryml.static_tags
5
+ - core.dryml moved to dryml/taglibs
6
+ - generator_directories moved from constant to parameter to Dryml.enable
7
+ - APPLICATION_TAGLIB not necessarily loaded automatically
8
+ - generator input and output directors no longer hardcoded, must be passed to #enable
9
+ - a bunch of functions in hobo.rb moved to dryml.rb
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2006 Tom Locke
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,36 @@
1
+ # DRYML
2
+
3
+ DRYML is the Don't Repeat Yourself Markup Language. It uses an
4
+ XML-like syntax and is best at creating XHTML documents. It could be
5
+ used to create other forms of plain-text documents, but the syntax is
6
+ not optimized for that and you may end up with extra carriage returns.
7
+ (Which would be a bug, so please send test cases).
8
+
9
+ DRYML was created for the Hobo project, but this is an extraction from
10
+ that project and can be used separately.
11
+
12
+ # How to use with Rails but without Hobo
13
+
14
+ - install both HoboSupport and Dryml as a plugin or gem
15
+
16
+ - create an `application.dryml`
17
+
18
+ $ mkdir app/views/taglibs
19
+ $ touch app/views/taglibs/application.dryml
20
+
21
+ - create `config/initializers/dryml.rb`
22
+
23
+ require 'dryml'
24
+ require 'dryml/template'
25
+ require 'dryml/dryml_generator'
26
+ Dryml.enable
27
+
28
+ Now you can use templates that end in ".dryml". Such templates will
29
+ ignore layouts.
30
+
31
+ # How to use outside of Rails
32
+
33
+ Dryml.render("<html><%= this %></html>", {:this => something})
34
+
35
+ See the [rdoc](http://fixme/doc/Dryml.html#render-instance_method) for
36
+ more information.
data/Rakefile ADDED
@@ -0,0 +1,47 @@
1
+ require 'rake'
2
+ require 'rake/rdoctask'
3
+ require 'rake/testtask'
4
+
5
+ $:.unshift File.join(File.expand_path(File.dirname(__FILE__)), '/lib')
6
+ $:.unshift File.join(File.expand_path(File.dirname(__FILE__)), '/../hobosupport/lib')
7
+ require 'dryml' # to get VERSION
8
+
9
+ RUBY = File.join(Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name']).sub(/.*\s.*/m, '"\&"')
10
+ RUBYDOCTEST = ENV['RUBYDOCTEST'] || "#{RUBY} -S rubydoctest"
11
+
12
+ desc "Default Task"
13
+ task :default => [ :test ]
14
+
15
+ # --- Testing --- #
16
+
17
+ desc "Run all tests"
18
+ task :test do |t|
19
+ files=Dir['test/*.rdoctest'].map {|f| File.expand_path(f)}.join(' ')
20
+ exit(1) if !system("#{RUBYDOCTEST} #{files}")
21
+ end
22
+
23
+ # --- RDOC --- #
24
+
25
+ require 'yard'
26
+ YARD::Rake::YardocTask.new do |t|
27
+ t.files = ['lib/**/*.rb', 'README', 'LICENSE.txt', 'CHANGES.txt']
28
+ end
29
+
30
+ # --- Packaging and Rubyforge & gemcutter & github--- #
31
+
32
+ require 'jeweler'
33
+ Jeweler::Tasks.new do |gemspec|
34
+ gemspec.version = Dryml::VERSION
35
+ gemspec.name = "dryml"
36
+ gemspec.email = "tom@tomlocke.com"
37
+ gemspec.summary = "The web app builder for Rails"
38
+ gemspec.homepage = "http://hobocentral.net/"
39
+ gemspec.authors = ["Tom Locke"]
40
+ gemspec.rubyforge_project = "hobo"
41
+ gemspec.add_dependency("hobosupport", ["= #{Dryml::VERSION}"])
42
+ gemspec.add_dependency("actionpack", [">= 2.2.2"])
43
+ end
44
+ Jeweler::GemcutterTasks.new
45
+ Jeweler::RubyforgeTasks.new do |rubyforge|
46
+ rubyforge.doc_task = false
47
+ end
data/TODO.txt ADDED
@@ -0,0 +1,6 @@
1
+ - remove actionview requirement
2
+ - get Hobo working
3
+ - standalone generators
4
+ - get it working in Sinatra
5
+ - get it working in rtomayko/tilt
6
+ - get it working in Rails3
@@ -0,0 +1,140 @@
1
+ module Dryml
2
+
3
+ class DRYMLBuilder
4
+
5
+ def initialize(template)
6
+ @template = template
7
+ @build_instructions = nil # set to [] on the first add_build_instruction
8
+ @part_names = []
9
+ end
10
+
11
+ attr_reader :template, :environment
12
+
13
+ def template_path
14
+ template.template_path
15
+ end
16
+
17
+
18
+ def set_environment(environment)
19
+ @environment = environment
20
+ end
21
+
22
+
23
+ def ready?(mtime, d=false)
24
+ @build_instructions && @last_build_mtime && @last_build_mtime >= mtime
25
+ end
26
+
27
+
28
+ def start
29
+ @part_names.clear
30
+ @build_instructions = []
31
+ end
32
+
33
+
34
+ def add_build_instruction(type, params)
35
+ @build_instructions << params.merge(:type => type)
36
+ end
37
+
38
+
39
+ def add_part(name, src, line_num)
40
+ raise DrymlException.new("duplicate part: #{name}", template_path, line_num) if name.in?(@part_names)
41
+ add_build_instruction(:def, :src => src, :line_num => line_num)
42
+ @part_names << name
43
+ end
44
+
45
+
46
+ def <<(params)
47
+ @build_instructions << params
48
+ end
49
+
50
+
51
+ def render_page_source(src, local_names)
52
+ locals = local_names.map{|l| "#{l} = __local_assigns__[:#{l}];"}.join(' ')
53
+
54
+ ("def render_page(__page_this__, __local_assigns__); " +
55
+ "#{locals} new_object_context(__page_this__) do " +
56
+ src +
57
+ "; output_buffer; end; end")
58
+ end
59
+
60
+
61
+ def erb_process(erb_src, method_def=false)
62
+ trim_mode = ActionView::TemplateHandlers::ERB.erb_trim_mode
63
+ erb = ERB.new(erb_src, nil, trim_mode, "output_buffer")
64
+ src = erb.src.split(';')[1..-2].join(';')
65
+
66
+ if method_def
67
+ src.sub /^\s*def.*?\(.*?\)/, '\0 __in_erb_template=true; '
68
+ else
69
+ "__in_erb_template=true; " + src
70
+ end
71
+ end
72
+
73
+
74
+ def build(local_names, auto_taglibs, src_mtime)
75
+
76
+ auto_taglibs.each { |t| import_taglib(t) }
77
+
78
+ @build_instructions._?.each do |instruction|
79
+ name = instruction[:name]
80
+ case instruction[:type]
81
+ when :eval
82
+ @environment.class_eval(instruction[:src], template_path, instruction[:line_num])
83
+
84
+ when :def
85
+ src = erb_process(instruction[:src], true)
86
+ @environment.class_eval(src, template_path, instruction[:line_num])
87
+
88
+ when :render_page
89
+ method_src = render_page_source(erb_process(instruction[:src]), local_names)
90
+ @environment.compiled_local_names = local_names
91
+ @environment.class_eval(method_src, template_path, instruction[:line_num])
92
+
93
+ when :include
94
+ import_taglib(instruction)
95
+
96
+ when :module
97
+ import_module(name.constantize, instruction[:as])
98
+
99
+ when :set_theme
100
+ set_theme(name)
101
+
102
+ when :alias_method
103
+ @environment.send(:alias_method, instruction[:new], instruction[:old])
104
+
105
+ else
106
+ raise RuntimeError.new("DRYML: Unknown build instruction :#{instruction[:type]}, " +
107
+ "building #{template_path}")
108
+ end
109
+ end
110
+ @last_build_mtime = src_mtime
111
+ end
112
+
113
+
114
+ def import_taglib(options)
115
+ if options[:module]
116
+ import_module(options[:module].constantize, options[:as])
117
+ else
118
+ template_dir = File.dirname(template_path)
119
+ options = options.merge(:template_dir => template_dir)
120
+
121
+ taglib = Taglib.get(options)
122
+ taglib.import_into(@environment, options[:as])
123
+ end
124
+ end
125
+
126
+
127
+ def import_module(mod, as=nil)
128
+ raise NotImplementedError if as
129
+ @environment.send(:include, mod)
130
+ end
131
+
132
+
133
+ def set_theme(name)
134
+ if Hobo.current_theme.nil? or Hobo.current_theme == name
135
+ Hobo.current_theme = name
136
+ import_taglib(:src => "taglibs/themes/#{name}/#{name}")
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,155 @@
1
+ require 'rexml/xpath'
2
+
3
+ module Dryml
4
+
5
+ # DrymlDoc provides the facility to parse a directory tree of DRYML taglibs, building a collection of objects that provide metadata
6
+ module DrymlDoc
7
+
8
+ def self.load_taglibs(directory, taglib_class=DrymlDoc::Taglib)
9
+ dryml_files = Dir["#{directory}/**/*.dryml"]
10
+
11
+ dryml_files.map { |f| taglib_class.new(directory, f) }
12
+ end
13
+
14
+ CommentMethods = classy_module do
15
+
16
+ def comment_intro
17
+ comment && comment =~ /(.*?)^#/m ? $1 : comment
18
+ end
19
+
20
+
21
+ def comment_rest
22
+ comment && comment[comment_intro.length..-1]
23
+ end
24
+
25
+ %w(comment comment_intro comment_rest).each do |m|
26
+ class_eval "def #{m}_html; Maruku.new(#{m}).to_html.gsub(/&amp;/, '&'); end"
27
+ end
28
+
29
+ end
30
+
31
+ class Taglib
32
+
33
+ def initialize(home, filename, name=nil)
34
+ @name = name || filename.sub(/.dryml$/, '')[home.length+1..-1]
35
+ @doc = Dryml::Parser::Document.new(File.read(filename), filename)
36
+ parse_tag_defs
37
+ end
38
+
39
+ attr_reader :name, :doc, :tag_defs
40
+
41
+ def comment
42
+ first_node = doc[0][0]
43
+ doc.restore_erb_scriptlets(first_node.to_s.strip) if first_node.is_a?(REXML::Comment)
44
+ end
45
+
46
+ include CommentMethods
47
+
48
+ private
49
+
50
+ def tagdef_class
51
+ self.class.parent.const_get('TagDef')
52
+ end
53
+
54
+ def parse_tag_defs
55
+ @tag_defs = []
56
+ REXML::XPath.match(doc, '/*/*[@tag]').each { |node| @tag_defs << tagdef_class.new(self, node) }
57
+ end
58
+
59
+ end
60
+
61
+ class TagDef
62
+
63
+ def initialize(taglib, node)
64
+ @taglib = taglib
65
+ @node = node
66
+ end
67
+
68
+ attr_reader :taglib, :node
69
+ delegate :doc, :to => :taglib
70
+
71
+
72
+ def name
73
+ node.attributes['tag']
74
+ end
75
+
76
+ def source
77
+ doc.restore_erb_scriptlets(node.to_s).strip
78
+ end
79
+
80
+ # The contents of the XML comment, if any, immediately above the tag definition
81
+ def comment
82
+ @comment ||= begin
83
+ space = node.previous_sibling and
84
+ space.to_s.blank? && space.to_s.count("\n") == 1 and
85
+ comment_node = space.previous_sibling
86
+
87
+ if comment_node.is_a?(REXML::Comment)
88
+ doc.restore_erb_scriptlets(comment_node.to_s.strip)
89
+ end
90
+ end
91
+ end
92
+
93
+ include CommentMethods
94
+
95
+ def no_doc?
96
+ comment =~ /^nodoc\b/
97
+ end
98
+
99
+ # An array of the arrtibute names defined by this tag
100
+ def attributes
101
+ (node.attributes['attrs'] || "").split(/\s*,\s*/).where_not.blank?
102
+ end
103
+
104
+
105
+ # Returns a recursive array srtucture, where each item in the array is a pair: [parameter_name, sub_parameters]
106
+ # (sub-parameters is the same kind of structure)
107
+ def parameters(element=node)
108
+ result = []
109
+ element.elements.each do |e|
110
+ if (p = e.attributes['param'])
111
+ param_name = p == "&true" ? e.name : p
112
+ result << [param_name, parameters(e)]
113
+ else
114
+ result.concat(parameters(e))
115
+ end
116
+ end
117
+ result
118
+ end
119
+
120
+
121
+ # Is this the base definition of a polymorphic tag
122
+ def polymorphic?
123
+ node.attributes['polymorphic'].present?
124
+ end
125
+
126
+ # Is this an <extend>?
127
+ def extension?
128
+ node.name == "extend"
129
+ end
130
+
131
+
132
+ # The definition's 'for' attribute
133
+ def for_type
134
+ node.attributes['for']
135
+ end
136
+
137
+
138
+ # The name of the tag, if any, that this definition merges its parameters into
139
+ # That is, the tag with 'merge' or 'merge-params' declared
140
+ def merge_params
141
+ REXML::XPath.first(node, ".//*[@merge|@merge-params]")._?.name
142
+ end
143
+
144
+ # The name of the tag, if any, that this definition merges its attributes into
145
+ # That is, the tag with 'merge' or 'merge-attrs' declared
146
+ def merge_attrs
147
+ REXML::XPath.first(node, ".//*[@merge|@merge-attrs]")._?.name
148
+ end
149
+
150
+ end
151
+
152
+
153
+ end
154
+
155
+ end
@@ -0,0 +1,271 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'set'
3
+ require 'fileutils'
4
+
5
+ require 'action_controller/dispatcher'
6
+
7
+ module Dryml
8
+
9
+ class DrymlGenerator
10
+
11
+ HEADER = "<!-- AUTOMATICALLY GENERATED FILE - DO NOT EDIT -->\n\n"
12
+
13
+ class << self
14
+ attr_accessor :run_on_every_request
15
+ attr_accessor :output_directory
16
+ end
17
+
18
+ def self.enable(generator_directories = [], output_directory = nil)
19
+ @output_directory = output_directory
20
+ @output_directory ||= "#{RAILS_ROOT}/app/views/taglibs/auto" if defined? :RAILS_ROOT
21
+ @generator_directories = generator_directories
22
+
23
+ # Unfortunately the dispatcher callbacks don't give us the hook we need (after routes are reloaded)
24
+ # so we have to alias_method_chain
25
+ ActionController::Dispatcher.class_eval do
26
+
27
+ if respond_to? :reload_application
28
+ #Rails 2.3
29
+ class << self
30
+ def reload_application_with_dryml_generators
31
+ reload_application_without_dryml_generators
32
+ DrymlGenerator.run unless Dryml::DrymlGenerator.run_on_every_request == false || Rails.env.production?
33
+ end
34
+ alias_method_chain :reload_application, :dryml_generators
35
+ end
36
+ else
37
+ #Rails <= 2.2
38
+ def reload_application_with_dryml_generators
39
+ reload_application_without_dryml_generators
40
+ DrymlGenerator.run unless Dryml::DrymlGenerator.run_on_every_request == false || Rails.env.production?
41
+ end
42
+ alias_method_chain :reload_application, :dryml_generators
43
+ end
44
+ end
45
+ end
46
+
47
+ def self.run(generator_directories=nil, output_directory=nil)
48
+ @generator_directories ||= generator_directories
49
+ @output_directory ||= output_directory
50
+ @generator ||= DrymlGenerator.new(generator_directories || @generator_directories)
51
+ @generator.run
52
+ end
53
+
54
+
55
+ def initialize(generator_directories=nil)
56
+ @templates = {}
57
+ @digests = {}
58
+ generator_directories ||= Dryml::DrymlGenerator.generator_directories
59
+ load_templates(generator_directories)
60
+ end
61
+
62
+ attr_accessor :subsite
63
+
64
+
65
+ def load_templates(generator_directories)
66
+ generator_directories.each do |dir|
67
+ Dir["#{dir}/**/*.dryml.erb"].each do |f|
68
+ name = f[dir.length + 1..-11]
69
+ erb = File.read(f)
70
+ @templates[name] = ERB.new(erb, nil, '-').src
71
+
72
+ # Create output directories and parents as required
73
+ [nil, *Hobo.subsites].each do |s|
74
+ FileUtils.mkdir_p(File.dirname("#{output_dir s}/#{name}"))
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+
81
+ def run
82
+ # FIXME
83
+ # Ensure all view hints loaded before running
84
+ subsites = [nil]
85
+ if defined?(:Hobo)
86
+ Hobo::Model.all_models.*.view_hints
87
+ subsites += [*Hobo.subsites]
88
+ end
89
+
90
+ subsites.each { |s| run_for_subsite(s) }
91
+ end
92
+
93
+
94
+ def run_for_subsite(subsite)
95
+ self.subsite = subsite
96
+ @templates.each_pair do |name, src|
97
+ run_one(name, src)
98
+ end
99
+ end
100
+
101
+
102
+ def output_dir(s=subsite)
103
+ s ? "#{Dryml::DrymlGenerator.output_directory}/#{s}" : Dryml::DrymlGenerator.output_directory
104
+ end
105
+
106
+
107
+ def run_one(name, src)
108
+ dryml = instance_eval(src, name)
109
+ if dryml_changed?(name, dryml)
110
+ out = HEADER + dryml
111
+ File.open("#{output_dir}/#{name}.dryml", 'w') { |f| f.write(out) }
112
+ end
113
+ end
114
+
115
+
116
+ def dryml_changed?(name, dryml)
117
+ key = "#{subsite}/#{name}"
118
+ d = digest dryml
119
+ if d != @digests[key]
120
+ @digests[key] = d
121
+ true
122
+ else
123
+ false
124
+ end
125
+ end
126
+
127
+
128
+ def digest(s)
129
+ OpenSSL::Digest::SHA1.hexdigest(s)
130
+ end
131
+
132
+
133
+ # --- Helper methods for the templates --- #
134
+
135
+ attr_reader :controller
136
+
137
+
138
+ def controllers
139
+ Hobo::ModelController.all_controllers(subsite).sort_by &:name
140
+ end
141
+
142
+
143
+ def models
144
+ Hobo::Model.all_models.sort_by &:name
145
+ end
146
+
147
+ def each_controller
148
+ controllers.each do |controller|
149
+ @controller = controller
150
+ yield
151
+ end
152
+ @controller = nil
153
+ end
154
+
155
+
156
+ def each_model
157
+ models.each do |model|
158
+ @model = model
159
+ yield
160
+ end
161
+ @model = nil
162
+ end
163
+
164
+
165
+ def model
166
+ @model || @controller.model
167
+ end
168
+
169
+
170
+ def model_name(*options)
171
+ name = :plural.in?(options) ? model.view_hints.model_name_plural : model.view_hints.model_name
172
+ name = name.titleize.downcase if :lowercase.in?(options)
173
+ name = name.camelize if :camel.in?(options)
174
+ name
175
+ end
176
+
177
+ # escape single quotes and backslashes for use in a single
178
+ # quoted string
179
+ def sq_escape(s)
180
+ s.gsub(/[\\]/, "\\\\\\\\").gsub(/'/, "\\\\'")
181
+ end
182
+
183
+
184
+ def model_class
185
+ model.name.underscore.gsub('_', '-').gsub('/', '--')
186
+ end
187
+
188
+
189
+ def view_hints
190
+ model.view_hints
191
+ end
192
+
193
+
194
+ def through_collection_names(klass=model)
195
+ klass.reflections.values.select do |refl|
196
+ refl.macro == :has_many && refl.options[:through]
197
+ end.map {|x| x.options[:through]}
198
+ end
199
+
200
+
201
+ def linkable?(*args)
202
+ options = args.extract_options!
203
+ options[:subsite] = subsite
204
+ klass, action = if args.length == 1
205
+ [model, args.first]
206
+ else
207
+ args
208
+ end
209
+ Hobo::ModelRouter.linkable?(klass, action, options)
210
+ end
211
+
212
+
213
+ def sortable_collection?(collection, model=self.model)
214
+ # There's no perfect way to detect for this, given that acts_as_list
215
+ # does not provide any metadata to reflect on, but if the :order
216
+ # option is the same as the target classes position_column, that's a
217
+ # pretty safe bet
218
+ if defined? ActiveRecord::Acts::List::InstanceMethods
219
+ refl = model.reflections[collection]
220
+ klass = refl.klass
221
+ klass < ActiveRecord::Acts::List::InstanceMethods &&
222
+ klass.new.position_column == refl.options[:order].to_s
223
+ end
224
+ end
225
+
226
+
227
+ def standard_fields(*args)
228
+ klass = args.first.is_a?(Class) ? args.shift : model
229
+ extras = args
230
+
231
+ fields = klass.attr_order.*.to_s & klass.content_columns.*.name
232
+
233
+ fields -= %w{created_at updated_at created_on updated_on deleted_at} unless extras.include?(:include_timestamps)
234
+
235
+ bt = extras.include?(:belongs_to)
236
+ hm = extras.include?(:has_many)
237
+ klass.reflections.values.sort_by { |refl| refl.name.to_s }.map do |refl|
238
+ fields << refl.name.to_s if bt && refl.macro == :belongs_to
239
+ fields << refl.name.to_s if hm && refl.macro == :has_many && refl.options[:accessible]
240
+ end
241
+
242
+ fields.reject! { |f| model.never_show? f }
243
+ fields
244
+ end
245
+
246
+
247
+ def creators
248
+ defined?(model::Lifecycle) ? model::Lifecycle.publishable_creators : []
249
+ end
250
+
251
+ def transitions
252
+ defined?(model::Lifecycle) ? model::Lifecycle.publishable_transitions : []
253
+ end
254
+
255
+ def creator_names
256
+ creators.map { |c| c.name.to_s }
257
+ end
258
+
259
+ def transition_names
260
+ transitions.map { |t| t.name.to_s }.uniq
261
+ end
262
+
263
+
264
+ def a_or_an(word)
265
+ (word =~ /^[aeiou]/i ? "an " : "a ") + word
266
+ end
267
+
268
+ end
269
+
270
+ end
271
+
@@ -0,0 +1,13 @@
1
+ class Dryml::DrymlSupportController < ActionController::Base
2
+
3
+ def edit_source
4
+ dryml_editor = ENV['DRYML_EDITOR']
5
+ if dryml_editor
6
+ file = File.join(RAILS_ROOT, params[:file])
7
+ command = dryml_editor.sub(":file", file).sub(":line", params[:line])
8
+ system(command)
9
+ end
10
+ render :nothing => true
11
+ end
12
+
13
+ end