dryml 1.1.0.pre0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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