hobo 0.5.3

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.
Files changed (125) hide show
  1. data/LICENSE.txt +22 -0
  2. data/README.txt +18 -0
  3. data/bin/hobo +81 -0
  4. data/hobo_files/plugin/CHANGES.txt +963 -0
  5. data/hobo_files/plugin/LICENSE.txt +22 -0
  6. data/hobo_files/plugin/README +4 -0
  7. data/hobo_files/plugin/Rakefile +11 -0
  8. data/hobo_files/plugin/generators/hobo/hobo_generator.rb +37 -0
  9. data/hobo_files/plugin/generators/hobo/templates/application.dryml +2 -0
  10. data/hobo_files/plugin/generators/hobo/templates/guest.rb +31 -0
  11. data/hobo_files/plugin/generators/hobo_front_controller/USAGE +11 -0
  12. data/hobo_files/plugin/generators/hobo_front_controller/hobo_front_controller_generator.rb +90 -0
  13. data/hobo_files/plugin/generators/hobo_front_controller/templates/controller.rb +51 -0
  14. data/hobo_files/plugin/generators/hobo_front_controller/templates/functional_test.rb +18 -0
  15. data/hobo_files/plugin/generators/hobo_front_controller/templates/helper.rb +2 -0
  16. data/hobo_files/plugin/generators/hobo_front_controller/templates/index.dryml +43 -0
  17. data/hobo_files/plugin/generators/hobo_front_controller/templates/login.dryml +44 -0
  18. data/hobo_files/plugin/generators/hobo_front_controller/templates/search.dryml +18 -0
  19. data/hobo_files/plugin/generators/hobo_front_controller/templates/signup.dryml +45 -0
  20. data/hobo_files/plugin/generators/hobo_model/USAGE +26 -0
  21. data/hobo_files/plugin/generators/hobo_model/hobo_model_generator.rb +38 -0
  22. data/hobo_files/plugin/generators/hobo_model/templates/fixtures.yml +11 -0
  23. data/hobo_files/plugin/generators/hobo_model/templates/migration.rb +13 -0
  24. data/hobo_files/plugin/generators/hobo_model/templates/model.rb +24 -0
  25. data/hobo_files/plugin/generators/hobo_model/templates/unit_test.rb +10 -0
  26. data/hobo_files/plugin/generators/hobo_model_controller/USAGE +30 -0
  27. data/hobo_files/plugin/generators/hobo_model_controller/hobo_model_controller_generator.rb +43 -0
  28. data/hobo_files/plugin/generators/hobo_model_controller/templates/controller.rb +5 -0
  29. data/hobo_files/plugin/generators/hobo_model_controller/templates/functional_test.rb +18 -0
  30. data/hobo_files/plugin/generators/hobo_model_controller/templates/helper.rb +2 -0
  31. data/hobo_files/plugin/generators/hobo_model_controller/templates/view.rhtml +2 -0
  32. data/hobo_files/plugin/generators/hobo_rapid/hobo_rapid_generator.rb +51 -0
  33. data/hobo_files/plugin/generators/hobo_rapid/templates/hobo_rapid.js +436 -0
  34. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/default_mapping.rb +11 -0
  35. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/banner.gif +0 -0
  36. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/bkg_bodytop.gif +0 -0
  37. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/bkg_corner_01.gif +0 -0
  38. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/bkg_corner_02.gif +0 -0
  39. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/bkg_corner_03.gif +0 -0
  40. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/bkg_corner_04.gif +0 -0
  41. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/bkg_shadow_bottom.gif +0 -0
  42. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/bkg_shadow_left.gif +0 -0
  43. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/bkg_shadow_right.gif +0 -0
  44. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/bkg_shadow_top.gif +0 -0
  45. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/header_blue.gif +0 -0
  46. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/header_dblue.gif +0 -0
  47. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/header_green.gif +0 -0
  48. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/header_purple.gif +0 -0
  49. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/header_red.gif +0 -0
  50. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/logo.gif +0 -0
  51. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/spinner.gif +0 -0
  52. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/txt_list_img_dblue.gif +0 -0
  53. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/txt_list_img_green.gif +0 -0
  54. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/txt_list_img_purple.gif +0 -0
  55. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/txt_list_img_red.gif +0 -0
  56. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/window_corner_01.gif +0 -0
  57. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/window_corner_02.gif +0 -0
  58. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/window_corner_03.gif +0 -0
  59. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/window_corner_04.gif +0 -0
  60. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/window_shadow_bottom.gif +0 -0
  61. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/window_shadow_left.gif +0 -0
  62. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/window_shadow_right.gif +0 -0
  63. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/window_shadow_top.gif +0 -0
  64. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/stylesheets/application.css +390 -0
  65. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/views/application.dryml +104 -0
  66. data/hobo_files/plugin/generators/hobo_user_model/USAGE +26 -0
  67. data/hobo_files/plugin/generators/hobo_user_model/hobo_user_model_generator.rb +38 -0
  68. data/hobo_files/plugin/generators/hobo_user_model/templates/fixtures.yml +11 -0
  69. data/hobo_files/plugin/generators/hobo_user_model/templates/migration.rb +15 -0
  70. data/hobo_files/plugin/generators/hobo_user_model/templates/model.rb +58 -0
  71. data/hobo_files/plugin/generators/hobo_user_model/templates/unit_test.rb +10 -0
  72. data/hobo_files/plugin/init.rb +44 -0
  73. data/hobo_files/plugin/lib/action_view_extensions/base.rb +14 -0
  74. data/hobo_files/plugin/lib/active_record/has_many_association.rb +54 -0
  75. data/hobo_files/plugin/lib/active_record/has_many_through_association.rb +22 -0
  76. data/hobo_files/plugin/lib/active_record/table_definition.rb +34 -0
  77. data/hobo_files/plugin/lib/extensions.rb +245 -0
  78. data/hobo_files/plugin/lib/extensions/test_case.rb +130 -0
  79. data/hobo_files/plugin/lib/hobo.rb +353 -0
  80. data/hobo_files/plugin/lib/hobo/HtmlString +3 -0
  81. data/hobo_files/plugin/lib/hobo/authenticated_user.rb +106 -0
  82. data/hobo_files/plugin/lib/hobo/authentication_support.rb +108 -0
  83. data/hobo_files/plugin/lib/hobo/composite_model.rb +66 -0
  84. data/hobo_files/plugin/lib/hobo/controller.rb +134 -0
  85. data/hobo_files/plugin/lib/hobo/controller_helpers.rb +135 -0
  86. data/hobo_files/plugin/lib/hobo/core.rb +475 -0
  87. data/hobo_files/plugin/lib/hobo/define_tags.rb +56 -0
  88. data/hobo_files/plugin/lib/hobo/dryml.rb +161 -0
  89. data/hobo_files/plugin/lib/hobo/dryml/dryml_builder.rb +126 -0
  90. data/hobo_files/plugin/lib/hobo/dryml/tag_module.rb +9 -0
  91. data/hobo_files/plugin/lib/hobo/dryml/taglib.rb +57 -0
  92. data/hobo_files/plugin/lib/hobo/dryml/template.rb +586 -0
  93. data/hobo_files/plugin/lib/hobo/dryml/template_environment.rb +302 -0
  94. data/hobo_files/plugin/lib/hobo/dryml/template_handler.rb +19 -0
  95. data/hobo_files/plugin/lib/hobo/generator.rb +25 -0
  96. data/hobo_files/plugin/lib/hobo/html_string.rb +3 -0
  97. data/hobo_files/plugin/lib/hobo/lazy_hash.rb +28 -0
  98. data/hobo_files/plugin/lib/hobo/mapping_tags.rb +262 -0
  99. data/hobo_files/plugin/lib/hobo/markdown_string.rb +7 -0
  100. data/hobo_files/plugin/lib/hobo/model.rb +391 -0
  101. data/hobo_files/plugin/lib/hobo/model_controller.rb +676 -0
  102. data/hobo_files/plugin/lib/hobo/model_queries.rb +92 -0
  103. data/hobo_files/plugin/lib/hobo/model_support.rb +44 -0
  104. data/hobo_files/plugin/lib/hobo/password_string.rb +3 -0
  105. data/hobo_files/plugin/lib/hobo/predicate_dispatch.rb +78 -0
  106. data/hobo_files/plugin/lib/hobo/proc_binding.rb +32 -0
  107. data/hobo_files/plugin/lib/hobo/rapid.rb +447 -0
  108. data/hobo_files/plugin/lib/hobo/static_tags +92 -0
  109. data/hobo_files/plugin/lib/hobo/text.rb +3 -0
  110. data/hobo_files/plugin/lib/hobo/textile_string.rb +13 -0
  111. data/hobo_files/plugin/lib/hobo/undefined.rb +41 -0
  112. data/hobo_files/plugin/lib/hobo/undefined_access_error.rb +5 -0
  113. data/hobo_files/plugin/lib/hobo/where_fragment.rb +23 -0
  114. data/hobo_files/plugin/lib/rexml.rb +345 -0
  115. data/hobo_files/plugin/tags/core.dryml +6 -0
  116. data/hobo_files/plugin/tags/rapid.dryml +177 -0
  117. data/hobo_files/plugin/tags/rapid_editing.dryml +168 -0
  118. data/hobo_files/plugin/tags/rapid_navigation.dryml +95 -0
  119. data/hobo_files/plugin/tags/rapid_pages.dryml +175 -0
  120. data/hobo_files/plugin/tasks/environments.rake +19 -0
  121. data/hobo_files/plugin/tasks/hobo_tasks.rake +4 -0
  122. data/hobo_files/plugin/test/hobo_dryml_template_test.rb +7 -0
  123. data/hobo_files/plugin/test/hobo_test.rb +7 -0
  124. data/hobo_files/plugin/uninstall.rb +1 -0
  125. metadata +206 -0
@@ -0,0 +1,56 @@
1
+ module Hobo
2
+
3
+ module DefineTags
4
+
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ base.extend(PredicateDispatch::ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+
12
+ attr_reader :hobo_tag_blocks
13
+
14
+
15
+ def def_tag(name, *attrs_and_pred, &tagdef_block)
16
+ pred, attrs = if attrs_and_pred.first.is_a? Proc
17
+ [attrs_and_pred.first, attrs_and_pred[1..-1]]
18
+ else
19
+ [nil, attrs_and_pred]
20
+ end
21
+
22
+ name = name.to_s
23
+ @hobo_tag_blocks ||= HashWithIndifferentAccess.new
24
+ @hobo_tag_blocks[name] = tagdef_block
25
+ @hobo_tag_blocks["#{name}_predicate"] = pred if pred
26
+
27
+ safe_name = Dryml.unreserve(name)
28
+ locals = attrs.map{|a| Hobo::Dryml.unreserve(a)} + ["options"]
29
+
30
+ def_line = if pred
31
+ "defp :#{safe_name}, @hobo_tag_blocks['#{name}_predicate'] do |options, block|"
32
+ elsif predicate_method?(safe_name)
33
+ # be sure not to overwrite the predicate dispatch method
34
+ "defp :#{safe_name} do |options, block|"
35
+ else
36
+ "def #{safe_name}(options={}, &block)"
37
+ end
38
+
39
+ src = <<-END
40
+ #{def_line}
41
+ _tag_context(options, block) do |tagbody|
42
+ locals = _tag_locals(options, #{attrs.inspect}, [])
43
+ locals_hash = { :tagbody => tagbody };
44
+ #{locals.inspect}.each_with_index{|a, i| locals_hash[a] = locals[i]}
45
+ Hobo::ProcBinding.new(self, locals_hash).instance_eval(&#{self.name}.hobo_tag_blocks['#{name}'])
46
+ end
47
+ end
48
+ END
49
+ class_eval src, __FILE__, __LINE__
50
+ end
51
+
52
+ end
53
+
54
+ end
55
+
56
+ end
@@ -0,0 +1,161 @@
1
+ module Hobo::Dryml
2
+
3
+ class DrymlException < Exception; end
4
+
5
+ class AttributeExtensionString < String;
6
+ def drop_prefix; self[2..-1]; end
7
+ end
8
+
9
+ TagDef = Struct.new "TagDef", :name, :attrs, :proc
10
+
11
+ RESERVED_WORDS = %w{if for while do class else elsif unless case when module in}
12
+
13
+ EMPTY_PAGE = "[tag-page]"
14
+
15
+ @renderer_classes = {}
16
+
17
+ class << self
18
+
19
+ attr_accessor :last_if
20
+
21
+ def clear_cache
22
+ @renderer_classes = {}
23
+ @tag_page_renderer_class = nil
24
+ end
25
+
26
+ def render_tag(view, tag, options={})
27
+ renderer = empty_page_renderer(view)
28
+ renderer.render_tag(tag, options)
29
+ end
30
+
31
+
32
+ def empty_page_renderer(view)
33
+ page_renderer(view, [], EMPTY_PAGE)
34
+ end
35
+
36
+
37
+ def page_renderer(view, local_names=[], page=nil)
38
+ if RAILS_ENV == "development"
39
+ clear_cache
40
+ Taglib.clear_cache
41
+ end
42
+
43
+ prepare_view!(view)
44
+ if page == EMPTY_PAGE
45
+ @tag_page_renderer_class = make_renderer_class("", EMPTY_PAGE, local_names, [ApplicationHelper]) if
46
+ @tag_page_renderer_class.nil?
47
+ @tag_page_renderer_class.new(page, view)
48
+ else
49
+ page ||= view.instance_variable_get('@hobo_template_path')
50
+ template_path = "app/views/" + page + ".dryml"
51
+ src_file = File.new(File.join(RAILS_ROOT, template_path))
52
+ renderer_class = @renderer_classes[page]
53
+
54
+ # do we need to recompile?
55
+ if (!renderer_class or # nothing cached?
56
+ (local_names - renderer_class.compiled_local_names).any? or # any new local names?
57
+ renderer_class.load_time < src_file.mtime) # cache out of date?
58
+ renderer_class = make_renderer_class(src_file.read, template_path, local_names,
59
+ default_imports_for_view(view))
60
+ renderer_class.load_time = src_file.mtime
61
+ @renderer_classes[page] = renderer_class
62
+ end
63
+ renderer_class.new(page, view)
64
+ end
65
+ end
66
+
67
+
68
+ def prepare_view!(view)
69
+ # Not sure why this isn't done for me...
70
+ # There's probably a button to press round here somewhere
71
+ for var in %w(@flash @cookies @action_name @_session @_request @request_origin
72
+ @template @request @ignore_missing_templates @_headers @variables_added
73
+ @_flash @response @template_class
74
+ @_cookies @before_filter_chain_aborted @url
75
+ @_response @template_root @headers @_params @params @session)
76
+ unless @view.instance_variables.include?(var)
77
+ view.instance_variable_set(var, view.controller.instance_variable_get(var))
78
+ end
79
+ end
80
+
81
+ end
82
+
83
+
84
+ def default_imports_for_view(view)
85
+ [ApplicationHelper,
86
+ view.controller.class.name.sub(/Controller$/, "Helper").constantize]
87
+ end
88
+
89
+
90
+ def make_renderer_class(template_src, template_path, locals, imports)
91
+ renderer_class = Class.new(TemplateEnvironment)
92
+ compile_renderer_class(renderer_class, template_src, template_path, locals, imports)
93
+ renderer_class
94
+ end
95
+
96
+
97
+ def compile_renderer_class(renderer_class, template_src, template_path, locals, imports)
98
+ template = Template.new(template_src, renderer_class, template_path)
99
+ imports.each {|m| template.import_module(m)}
100
+
101
+ # the sum of all the names we've seen so far - eventually we'll be ready for all of 'em
102
+ all_local_names = renderer_class.compiled_local_names | locals
103
+
104
+ template.compile(all_local_names)
105
+ end
106
+
107
+
108
+ def unreserve(word)
109
+ word = word.to_s
110
+ if RESERVED_WORDS.include?(word)
111
+ word + "_"
112
+ else
113
+ word
114
+ end
115
+ end
116
+
117
+
118
+ def merge_tag_options(to, from)
119
+ res = to.merge({})
120
+ from.each_pair do |option, value|
121
+ res[option] = if value.is_a?(AttributeExtensionString) and to.has_key?(option)
122
+ "#{to[option]} #{value.drop_prefix}"
123
+ else
124
+ value
125
+ end
126
+ end
127
+ res
128
+ end
129
+
130
+
131
+ def hashify_options(options, merge_into=nil)
132
+ result = merge_into || options
133
+
134
+ options.each_pair do |key, val|
135
+ if key.is_a? Array
136
+ result.delete(key)
137
+ k = key.first
138
+
139
+ if key.length == 2 and key.last.is_a? Fixnum
140
+ hashify_options(val) if val.is_a?(Hash)
141
+ result[k] ||= []
142
+ result[k][key.last] = val
143
+ else
144
+ existing = options[k]
145
+ v = if key.length == 1
146
+ val.is_a?(Hash) ? hashify_options(val, existing) : val
147
+ else
148
+ hashify_options({key[1..-1] => val}, existing)
149
+ end
150
+ result[k] = v
151
+ end
152
+ else
153
+ hashify_options(val) if val.is_a?(Hash)
154
+ end
155
+ end
156
+ result
157
+ end
158
+
159
+ end
160
+
161
+ end
@@ -0,0 +1,126 @@
1
+ module Hobo::Dryml
2
+
3
+ class DRYMLBuilder
4
+
5
+ APPLICATION_TAGLIB = "hobolib/application"
6
+ CORE_TAGLIB = "plugins/hobo/tags/core"
7
+
8
+ def initialize(template_path)
9
+ @build_instructions = Array.new
10
+ @template_path = template_path
11
+ end
12
+
13
+
14
+ def set_environment(environment)
15
+ @environment = environment
16
+ end
17
+
18
+
19
+ def ready?(mtime)
20
+ !(@build_instructions.empty? || @last_build_time.nil? || mtime > @last_build_time)
21
+ end
22
+
23
+
24
+ def clear_instructions
25
+ @build_instructions.clear
26
+ end
27
+
28
+
29
+ def add_build_instruction(params)
30
+ @build_instructions << params
31
+ end
32
+
33
+
34
+ def <<(params)
35
+ @build_instructions << params
36
+ end
37
+
38
+
39
+ def get_render_page_source(src, local_names)
40
+ locals = local_names.map{|l| "#{l} = __local_assigns__[:#{l}];"}.join(' ')
41
+
42
+ ("def render_page(__page_this__, __local_assigns__); " +
43
+ "#{locals} new_object_context(__page_this__) do " +
44
+ src +
45
+ "; end + part_contexts_js; end")
46
+ end
47
+
48
+
49
+ def build(local_names, auto_taglibs)
50
+ if auto_taglibs
51
+ import_taglib(CORE_TAGLIB)
52
+ import_taglib(APPLICATION_TAGLIB)
53
+ Hobo::MappingTags.apply_standard_mappings(@environment)
54
+ end
55
+
56
+ @build_instructions.each do |build_instruction|
57
+ case build_instruction[:type]
58
+ when :def, :part
59
+ @environment.class_eval(build_instruction[:src], @template_path, build_instruction[:line_num])
60
+
61
+ when :render_page
62
+ method_src = get_render_page_source(build_instruction[:src], local_names)
63
+ @environment.compiled_local_names = local_names
64
+ @environment.class_eval(method_src, @template_path, build_instruction[:line_num])
65
+
66
+ when :taglib
67
+ import_taglib(build_instruction[:name], build_instruction[:as])
68
+
69
+ when :module
70
+ import_module(build_instruction[:name].constantize, build_instruction[:as])
71
+
72
+ when :set_theme
73
+ set_theme(build_instruction[:name])
74
+
75
+ when :alias_method
76
+ @environment.send(:alias_method, build_instruction[:new], build_instruction[:old])
77
+
78
+ else
79
+ raise HoboError.new("DRYML: Unknown build instruction type found when building #{@template_path}")
80
+ end
81
+ end
82
+ @last_build_time = Time.now
83
+ end
84
+
85
+
86
+ def expand_template_path(path)
87
+ base = if path.starts_with? "plugins"
88
+ "vendor/" + path
89
+ elsif path.include?("/")
90
+ "app/views/#{path}"
91
+ else
92
+ template_dir = File.dirname(@template_path)
93
+ "#{template_dir}/#{path}"
94
+ end
95
+ base + ".dryml"
96
+ end
97
+
98
+
99
+ def import_taglib(src_path, as=nil)
100
+ path = expand_template_path(src_path)
101
+ unless @template_path == path
102
+ taglib = Taglib.get(RAILS_ROOT + (path.starts_with?("/") ? path : "/" + path))
103
+ taglib.import_into(@environment, as)
104
+ end
105
+ end
106
+
107
+
108
+ def import_module(mod, as=nil)
109
+ raise NotImplementedError.new if as
110
+ @environment.send(:include, mod)
111
+ end
112
+
113
+
114
+ def set_theme(name)
115
+ if Hobo.current_theme.nil? or Hobo.current_theme == name
116
+ Hobo.current_theme = name
117
+ import_taglib("hobolib/themes/#{name}/application")
118
+ mapping_module = "#{name}_mapping"
119
+ if File.exists?(path = RAILS_ROOT + "/app/views/hobolib/themes/#{mapping_module}.rb")
120
+ load(path)
121
+ Hobo::MappingTags.apply_mappings(@environment)
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,9 @@
1
+ # To be extended, not included
2
+
3
+ # DELETE ME
4
+
5
+ module Hobo::Dryml::TagModule
6
+
7
+ attr_accessor :tag_defs
8
+
9
+ end
@@ -0,0 +1,57 @@
1
+ module Hobo::Dryml
2
+
3
+ class Taglib
4
+
5
+ @cache = {}
6
+
7
+ class << self
8
+
9
+ def get(path)
10
+ raise DrymlException, "No such taglib: #{path}" unless File.exists?(path)
11
+ file = File.new(path)
12
+
13
+ taglib = @cache[file.path]
14
+ if taglib
15
+ taglib.reload
16
+ else
17
+ taglib = Taglib.new(file)
18
+ @cache[file.path] = taglib
19
+ end
20
+ taglib
21
+ end
22
+
23
+ def clear_cache
24
+ @cache = {}
25
+ end
26
+
27
+ end
28
+
29
+ def initialize(file)
30
+ @file = file
31
+ load
32
+ end
33
+
34
+ def reload
35
+ load if @file.mtime > @last_load_time
36
+ end
37
+
38
+ def load
39
+ @module = Module.new
40
+ @file.rewind
41
+ template = Template.new(@file.read, @module, @file.path)
42
+ template.compile([], false)
43
+ @last_load_time = @file.mtime
44
+ end
45
+
46
+ def import_into(class_or_module, as)
47
+ if as
48
+ raise NotImplementedError
49
+ as_class = Class.new(TemplateEnvironment) { include @module }
50
+ class_or_module.send(:define_method, as) { @module }
51
+ else
52
+ class_or_module.send(:include, @module)
53
+ end
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,586 @@
1
+ require 'rexml/document'
2
+
3
+ module Hobo::Dryml
4
+
5
+ class Template
6
+
7
+ DRYML_NAME = "[a-zA-Z_][a-zA-Z0-9_]*"
8
+
9
+ @build_cache = {}
10
+
11
+ class << self
12
+ attr_reader :build_cache
13
+
14
+ def clear_build_cache
15
+ @build_cache.clear()
16
+ end
17
+ end
18
+
19
+ def initialize(src, environment, template_path)
20
+ @src = src
21
+
22
+ @environment = environment # a class or a module
23
+ @environment.send(:include, Hobo::PredicateDispatch)
24
+
25
+ @template_path = template_path.sub(/^#{Regexp.escape(RAILS_ROOT)}/, "")
26
+
27
+ @builder = Template.build_cache[@template_path] || DRYMLBuilder.new(@template_path)
28
+ @builder.set_environment(environment)
29
+
30
+ @last_element = nil
31
+ end
32
+
33
+ attr_reader :tags, :template_path
34
+
35
+ def compile(local_names=[], auto_taglibs=true)
36
+ now = Time.now
37
+
38
+ unless @template_path == EMPTY_PAGE
39
+ filename = RAILS_ROOT + (@template_path.starts_with?("/") ? @template_path : "/" + @template_path)
40
+ mtime = File.stat(filename).mtime
41
+ end
42
+
43
+ if mtime.nil? || !@builder.ready?(mtime)
44
+ @builder.clear_instructions
45
+ parsed = true
46
+ # parse the DRYML file creating a list of build instructions
47
+ if is_taglib?
48
+ process_src
49
+ else
50
+ create_render_page_method
51
+ end
52
+
53
+ # store build instructions in the cache
54
+ Template.build_cache[@template_path] = @builder
55
+ end
56
+
57
+ # compile the build instructions
58
+ @builder.build(local_names, auto_taglibs)
59
+
60
+ from_cache = (parsed ? '' : ' (from cache)')
61
+ logger.info(" DRYML: Compiled#{from_cache} #{template_path} in %.2fs" % (Time.now - now))
62
+ end
63
+
64
+ def create_render_page_method
65
+ erb_src = process_src
66
+
67
+ src = ERB.new(erb_src).src[("_erbout = '';").length..-1]
68
+
69
+ @builder.add_build_instruction(:type => :render_page, :src => src, :line_num => 1)
70
+ end
71
+
72
+
73
+ def is_taglib?
74
+ @environment.class == Module
75
+ end
76
+
77
+
78
+ def process_src
79
+ # Replace <%...%> scriptlets with xml-safe references into a hash of scriptlets
80
+ @scriptlets = {}
81
+ src = @src.gsub(/<%(.*?)%>/m) do
82
+ _, scriptlet = *Regexp.last_match
83
+ id = @scriptlets.size + 1
84
+ @scriptlets[id] = scriptlet
85
+ newlines = "\n" * scriptlet.count("\n")
86
+ "[![HOBO-ERB#{id}#{newlines}]!]"
87
+ end
88
+
89
+ @xmlsrc = "<dryml_page>" + src + "</dryml_page>"
90
+
91
+ @doc = REXML::Document.new(RexSource.new(@xmlsrc))
92
+
93
+ erb_src = restore_erb_scriptlets(children_to_erb(@doc.root))
94
+
95
+ erb_src
96
+ end
97
+
98
+
99
+ def restore_erb_scriptlets(src)
100
+ src.gsub(/\[!\[HOBO-ERB(\d+)\s*\]!\]/m) { "<%#{@scriptlets[Regexp.last_match[1].to_i]}%>" }
101
+ end
102
+
103
+
104
+ def erb_process(src)
105
+ ERB.new(restore_erb_scriptlets(src)).src
106
+ end
107
+
108
+
109
+ def children_to_erb(nodes)
110
+ nodes.map{|x| node_to_erb(x)}.join
111
+ end
112
+
113
+
114
+ def node_to_erb(node)
115
+ case node
116
+
117
+ # v important this comes before REXML::Text, as REXML::CData < REXML::Text
118
+ when REXML::CData
119
+ REXML::CData::START + node.to_s + REXML::CData::STOP
120
+
121
+ when REXML::Comment
122
+ REXML::Comment::START + node.to_s + REXML::Comment::STOP
123
+
124
+ when REXML::Text
125
+ node.to_s
126
+
127
+ when REXML::Element
128
+ element_to_erb(node)
129
+ end
130
+ end
131
+
132
+
133
+ def element_to_erb(el)
134
+ dryml_exception("badly placed parameter tag <#{el.name}>", el) if
135
+ el.name.starts_with?(":")
136
+
137
+ @last_element = el
138
+ case el.name
139
+
140
+ when "taglib"
141
+ taglib_element(el)
142
+ # return nothing - the import has no presence in the erb source
143
+ tag_newlines(el)
144
+
145
+ when "set_theme"
146
+ require_attribute(el, "name", /^#{DRYML_NAME}$/)
147
+ @builder.add_build_instruction(:type => :set_theme, :name => el.attributes['name'])
148
+
149
+ # return nothing - set_theme has no presence in the erb source
150
+ tag_newlines(el)
151
+
152
+ when "def"
153
+ def_element(el)
154
+
155
+ when "tagbody"
156
+ tagbody_element(el)
157
+
158
+ else
159
+ if el.name.not_in?(Hobo.static_tags) or
160
+ el.attributes['replace_option'] or el.attributes['content_option']
161
+ tag_call(el)
162
+ else
163
+ html_element_to_erb(el)
164
+ end
165
+ end
166
+ end
167
+
168
+
169
+ def taglib_element(el)
170
+ require_toplevel(el)
171
+ require_attribute(el, "as", /^#{DRYML_NAME}$/, true)
172
+ if el.attributes["src"]
173
+ @builder.add_build_instruction(:type => :taglib,
174
+ :name => el.attributes["src"],
175
+ :as => el.attributes["as"])
176
+ elsif el.attributes["module"]
177
+ @builder.add_build_instruction(:type => :module,
178
+ :name => el.attributes["module"],
179
+ :as => el.attributes["as"])
180
+ end
181
+ end
182
+
183
+
184
+ def import_module(mod, as=nil)
185
+ @builder.import_module(mod, as)
186
+ end
187
+
188
+
189
+ def def_element(el)
190
+ require_toplevel(el)
191
+ require_attribute(el, "tag", /^#{DRYML_NAME}$/)
192
+ require_attribute(el, "attrs", /^\s*#{DRYML_NAME}(\s*,\s*#{DRYML_NAME})*\s*$/, true)
193
+ require_attribute(el, "alias_of", /^#{DRYML_NAME}$/, true)
194
+ require_attribute(el, "alias_current", /^#{DRYML_NAME}$/, true)
195
+ require_attribute(el, "if", /^#{DRYML_NAME}$/, true)
196
+
197
+ name = el.attributes["tag"]
198
+
199
+ alias_of = el.attributes['alias_of']
200
+ alias_current = el.attributes['alias_current']
201
+
202
+ dryml_exception("def cannot have both alias_of and alias_current", el) if alias_of && alias_current
203
+ dryml_exception("def with alias_of must be empty", el) if alias_of and el.size > 0
204
+
205
+ if alias_of || alias_current
206
+ old_name = alias_current ? name : alias_of
207
+ new_name = alias_current ? alias_current : name
208
+
209
+ @builder.add_build_instruction(:type => :alias_method, :new => new_name.to_sym, :old => old_name.to_sym)
210
+ end
211
+
212
+ if alias_of
213
+ "<% #{tag_newlines(el)} %>"
214
+ else
215
+ attrspec = el.attributes["attrs"]
216
+ attr_names = attrspec ? attrspec.split(/\s*,\s*/) : []
217
+
218
+ invalids = attr_names & %w{obj attr this}
219
+ dryml_exception("invalid attrs in def: #{invalids * ', '}", el) unless invalids.empty?
220
+
221
+ create_tag_method(el, name.to_sym, attr_names.omap{to_sym})
222
+ end
223
+ end
224
+
225
+
226
+ def create_tag_method(el, name, attrs)
227
+ name = Hobo::Dryml.unreserve(name)
228
+
229
+ inner_tags = find_inner_tags(el)
230
+
231
+ # A statement to assign values to local variables named after the tag's attrs.
232
+ setup_locals = ( (attrs.map{|a| "#{Hobo::Dryml.unreserve(a)}, "} + ['options, inner_tag_options']).join +
233
+ " = _tag_locals(__options__, #{attrs.inspect}, #{inner_tags.inspect})" )
234
+
235
+ start = "_tag_context(__options__, __block__) do |tagbody| #{setup_locals}"
236
+
237
+ pred = el.attributes["if"]
238
+ pred = pred[1..-1] if pred && pred.starts_with?('#')
239
+
240
+ def_line = if pred
241
+ "defp :#{name}, (proc {|options| #{pred}}) do |__options__, __block__|"
242
+ elsif @environment.predicate_method?(name)
243
+ # be sure not to overwrite the predicate dispatch method
244
+ "defp :#{name} do |__options__, __block__|"
245
+ else
246
+ "def #{name}(__options__={}, &__block__)"
247
+ end
248
+
249
+ method_src = ( "<% #{def_line}; #{start} " +
250
+ # reproduce any line breaks in the start-tag so that line numbers are preserved
251
+ tag_newlines(el) + "%>" +
252
+ children_to_erb(el) +
253
+ "<% @output; end; end %>" )
254
+
255
+ logger.debug(restore_erb_scriptlets(method_src)) if el.attributes["hobo_debug_source"]
256
+
257
+ @builder.add_build_instruction(:type => :def, :src => erb_process(method_src), :line_num => element_line_num(el))
258
+
259
+ # keep line numbers matching up
260
+ "<% #{"\n" * method_src.count("\n")} %>"
261
+ end
262
+
263
+
264
+ def find_inner_tags(el)
265
+ el.map do |e|
266
+ if e.is_a?(REXML::Element)
267
+ name = e.attributes["content_option"] || e.attributes["replace_option"]
268
+ [(name if name && !is_code_attribute?(name))] + find_inner_tags(e)
269
+ else
270
+ []
271
+ end
272
+ end.flatten.compact
273
+ end
274
+
275
+
276
+ def tagbody_element(el)
277
+ dryml_exception("tagbody can only appear inside a <def>", el) unless
278
+ find_ancestor(el) {|e| e.name == 'def'}
279
+ dryml_exception("tagbody cannot appear inside a part", el) if
280
+ find_ancestor(el) {|e| e.attributes['part_id']}
281
+ tagbody_call(el)
282
+ end
283
+
284
+
285
+ def tagbody_call(el)
286
+ options = []
287
+ obj = el.attributes['obj']
288
+ attr = el.attributes['attr']
289
+ options << ":obj => #{attribute_to_ruby(obj)}" if obj
290
+ options << ":attr => #{attribute_to_ruby(attr)}" if attr
291
+ else_ = attribute_to_ruby(el.attributes['else'])
292
+ "<%= tagbody ? tagbody.call({ #{options * ', '} }) : #{else_} %>"
293
+ end
294
+
295
+
296
+ def part_element(el, content)
297
+ require_attribute(el, "part_id", /^#{DRYML_NAME}$/)
298
+ part_name = el.attributes['part_id']
299
+ dom_id = el.attributes['id'] || part_name
300
+
301
+ dryml_exception("dupplicate part name: #{part_name}", el) if
302
+ (part_name + "_part").in?(@environment.instance_methods)
303
+
304
+ part_src = "<% def #{part_name}_part #{tag_newlines(el)}; new_context do %>" +
305
+ content +
306
+ "<% end; end %>"
307
+ create_part(part_src, element_line_num(el))
308
+
309
+ newlines = "\n" * part_src.count("\n")
310
+ res = "<%= call_part(#{attribute_to_ruby(dom_id)}, :#{part_name}) #{newlines} %>"
311
+ res
312
+ end
313
+
314
+
315
+ def create_part(erb_src, line_num)
316
+ # Add a method to the part module for this template
317
+
318
+ @builder.add_build_instruction(:type => :part, :src => erb_process(erb_src), :line_num => line_num)
319
+ end
320
+
321
+
322
+ def tag_call(el)
323
+ require_attribute(el, "content_option", /#{DRYML_NAME}/, true)
324
+ require_attribute(el, "replace_option", /#{DRYML_NAME}/, true)
325
+
326
+ # find out if it's empty before removing any <:param_tags>
327
+ empty_el = el.size == 0
328
+
329
+ # gather <:param_tags>, and remove them from the dom
330
+ compiled_param_tags = compile_parameter_tags(el)
331
+
332
+ name = Hobo::Dryml.unreserve(el.name)
333
+ options = tag_options(el, compiled_param_tags)
334
+ newlines = tag_newlines(el)
335
+ replace_option = el.attributes["replace_option"]
336
+ content_option = el.attributes["content_option"]
337
+ dryml_exception("both replace_option and content_option given") if replace_option && content_option
338
+ call = if replace_option
339
+ replace_option = attribute_to_ruby(replace_option)
340
+ "call_replaceable_tag(:#{name}, #{options}, inner_tag_options[#{replace_option}.to_sym])"
341
+ elsif content_option
342
+ content_option = attribute_to_ruby(content_option)
343
+ "call_replaceable_content_tag(:#{name}, #{options}, inner_tag_options[#{content_option}.to_sym])"
344
+ else
345
+ "#{name}(#{options})"
346
+ end
347
+
348
+ part_id = el.attributes['part_id']
349
+ if empty_el
350
+ if part_id
351
+ "<span id='#{part_id}'>" + part_element(el, "<%= #{call} %>") + "</span>"
352
+ else
353
+ "<%= #{call} #{newlines}%>"
354
+ end
355
+ else
356
+ children = children_to_erb(el)
357
+ if part_id
358
+ id = el.attributes['id'] || part_id
359
+ "<span id='<%= #{attribute_to_ruby(id)} %>'>" +
360
+ part_element(el, "<% _erbout.concat(#{call} do %>#{children}<% end) %>") +
361
+ "</span>"
362
+ else
363
+ "<% _erbout.concat(#{call} do #{newlines}%>#{children}<% end) %>"
364
+ end
365
+ end
366
+ end
367
+
368
+
369
+ def compile_parameter_tags(el)
370
+ # The implementation of parameter tags is greatly complicated
371
+ # by the need to maintain line-number parity between the dryml source
372
+ # and generated erb source
373
+
374
+ last = el.children.reverse.ofind{is_a?(REXML::Element) && name.starts_with?(":")}
375
+ return "" if last.nil?
376
+ param_section = el.children[0..el.index(last)]
377
+
378
+ dryml_exception("invalid content before parameter tag", el) unless param_section.all? do |e|
379
+ (e.is_a?(REXML::Element) && e.name.starts_with?(":")) ||
380
+ (e.is_a?(REXML::Text) && e.to_s.blank?) ||
381
+ e.is_a?(REXML::Comment)
382
+ end
383
+
384
+ last = param_section.last
385
+ compiled = param_section.map do |e|
386
+ # REXML Bug - don't use el.remove (also removes other children that are == )
387
+ el.delete_at(el.index(e))
388
+ case e
389
+ when REXML::Element
390
+ array_index = begin
391
+ # If there are other param-tags with this
392
+ # name, the index of this one, else nil
393
+ same_name_params = param_section.select {|x| x.is_a?(REXML::Element) and x.name == e.name}
394
+ same_name_params.length > 1 && same_name_params.index(e)
395
+ end
396
+
397
+ param_name = attr_name_to_option_key(e.name[1..-1], array_index)
398
+
399
+ dryml_exception("duplicate attribute/parameter-tag #{param}", el) if
400
+ param_name.in?(el.attributes.keys)
401
+
402
+ param_value =
403
+ if e.has_attributes?
404
+ pairs = e.attributes.map do |n,v|
405
+ "#{attr_name_to_option_key(n)} => " + "#{attribute_to_ruby(v)}"
406
+ end
407
+ # If there is content to, that goes in the hash under the key :content
408
+ if e.size > 0
409
+ pairs << "#{tag_newlines(el)}:content => (capture do %>#{children_to_erb(e)}<%; end)"
410
+ end
411
+ "{" + pairs.join(",") + "}"
412
+ elsif e.size > 0
413
+ "#{tag_newlines(el)}(capture do %>#{children_to_erb(e)}<%; end)"
414
+ else
415
+ "''"
416
+ end
417
+ pair = "#{param_name} => #{param_value}"
418
+ pair << ", " unless e == last
419
+ pair
420
+
421
+ when REXML::Text
422
+ e.to_s
423
+
424
+ when REXML::Comment
425
+ REXML::Comment::START + node.to_s + REXML::Comment::STOP
426
+
427
+ end
428
+ end
429
+
430
+ compiled.join
431
+ end
432
+
433
+
434
+ def attr_name_to_option_key(name, array_index=nil)
435
+ parts = name.split(".").map { |p| ":#{p}" }
436
+ parts << array_index if array_index
437
+ if parts.length == 1
438
+ parts.first
439
+ else
440
+ "[" + parts.join(', ') + "]"
441
+ end
442
+ end
443
+
444
+
445
+ def tag_options(el, param_tags_compiled)
446
+ attributes = el.attributes
447
+
448
+ options = attributes.map do |n,v|
449
+ param_name = attr_name_to_option_key(n)
450
+ param_value = attribute_to_ruby(v)
451
+ "#{param_name} => #{param_value}"
452
+ end
453
+
454
+ options << param_tags_compiled unless param_tags_compiled.blank?
455
+ all = options.join(', ')
456
+
457
+ xattrs = attributes['xattrs']
458
+ if xattrs
459
+ extra_options = if xattrs.blank?
460
+ "options"
461
+ elsif xattrs.starts_with?("#")
462
+ xattrs[1..-1]
463
+ else
464
+ dryml_exception("invalid xattrs", el)
465
+ end
466
+ "{#{all}}.merge((#{extra_options}) || {})"
467
+ else
468
+ "{#{all}}"
469
+ end
470
+ end
471
+
472
+
473
+ def html_element_to_erb(el)
474
+ start_tag_src = el.instance_variable_get("@start_tag_source").
475
+ gsub(REXML::CData::START, "").gsub(REXML::CData::STOP, "")
476
+
477
+ xattrs = el.attributes["xattrs"]
478
+ if xattrs
479
+ attr_args = if xattrs.starts_with?('#')
480
+ xattrs[1..-1]
481
+ elsif xattrs.blank?
482
+ "options"
483
+ else
484
+ dryml_exception("invalid xattrs", el)
485
+ end
486
+ class_attr = el.attributes["class"]
487
+ if class_attr
488
+ raise HoboError.new("invalid class attribute with xattrs: '#{class_attr}'") if
489
+ class_attr =~ /'|\[!\[HOBO-ERB/
490
+
491
+ attr_args.concat(", '#{class_attr}'")
492
+ start_tag_src.sub!(/\s*class\s*=\s*('[^']*?'|"[^"]*?")/, "")
493
+ end
494
+ start_tag_src.sub!(/\s*xattrs\s*=\s*('[^']*?'|"[^"]*?")/, " <%= xattrs(#{attr_args}) %>")
495
+ end
496
+
497
+ if start_tag_src.ends_with?("/>")
498
+ start_tag_src
499
+ else
500
+ if el.attributes['part_id']
501
+ body = part_element(el, children_to_erb(el))
502
+ if el.attributes["id"]
503
+ # remove part_id, and eval the id attribute with an erb scriptlet
504
+ start_tag_src.sub!(/\s*part_id\s*=\s*('[^']*?'|"[^"]*?")/, "")
505
+ id_expr = attribute_to_ruby(el.attributes['id'])
506
+ start_tag_src.sub!(/id\s*=\s*('[^']*?'|"[^"]*?")/, "id='<%= #{id_expr} %>'")
507
+ else
508
+ # rename part_id to id
509
+ start_tag_src.sub!(/part_id\s*=\s*('[^']*?'|"[^"]*?")/, "id=\\1")
510
+ end
511
+ dryml_exception("multiple part ids", el) if start_tag_src.index("part_id=")
512
+
513
+
514
+ start_tag_src + body + "</#{el.name}>"
515
+ else
516
+ start_tag_src + children_to_erb(el) + "</#{el.name}>"
517
+ end
518
+ end
519
+ end
520
+
521
+ def attribute_to_ruby(attr)
522
+ dryml_exception("erb scriptlet in attribute of defined tag") if attr && attr.index("[![HOBO-ERB")
523
+ if attr.nil?
524
+ "nil"
525
+ elsif is_code_attribute?(attr)
526
+ "(#{attr[1..-1]})"
527
+ else
528
+ str = if not attr =~ /"/
529
+ '"' + attr + '"'
530
+ elsif not attr =~ /'/
531
+ "'#{attr}'"
532
+ else
533
+ dryml_exception("invalid quote(s) in attribute value")
534
+ end
535
+ attr.starts_with?("++") ? "attr_extension(#{str})" : str
536
+ end
537
+ end
538
+
539
+ def find_ancestor(el)
540
+ e = el.parent
541
+ until e.is_a? REXML::Document
542
+ return e if yield(e)
543
+ e = e.parent
544
+ end
545
+ return nil
546
+ end
547
+
548
+ def require_toplevel(el)
549
+ dryml_exception("<#{el.name}> can only be at the top level", el) if el.parent != @doc.root
550
+ end
551
+
552
+ def require_attribute(el, name, rx=nil, optional=false)
553
+ val = el.attributes[name]
554
+ if val
555
+ dryml_exception("invalid #{name}=\"#{val}\" attribute on <#{el.name}>", el) unless val =~ rx
556
+ else
557
+ dryml_exception("missing #{name} attribute on <#{el.name}>", el) unless optional
558
+ end
559
+ end
560
+
561
+ def dryml_exception(message, el=nil)
562
+ el ||= @last_element
563
+ raise DrymlException.new(message + " -- at #{template_path}:#{element_line_num(el)}")
564
+ end
565
+
566
+ def element_line_num(el)
567
+ offset = el.instance_variable_get("@source_offset")
568
+ line_no = @xmlsrc[0..offset].count("\n") + 1
569
+ end
570
+
571
+ def tag_newlines(el)
572
+ src = el.instance_variable_get("@start_tag_source")
573
+ "\n" * src.count("\n")
574
+ end
575
+
576
+ def is_code_attribute?(attr_value)
577
+ attr_value.starts_with?("#")
578
+ end
579
+
580
+ def logger
581
+ ActionController::Base.logger rescue nil
582
+ end
583
+
584
+ end
585
+
586
+ end