alula 0.2.3 → 0.4.0b

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 (182) hide show
  1. data/.gitignore +2 -0
  2. data/Gemfile +7 -0
  3. data/Guardfile +9 -0
  4. data/Rakefile +12 -1
  5. data/VERSION +1 -1
  6. data/alula.gemspec +20 -4
  7. data/lib/alula/attachment_processor.rb +77 -0
  8. data/lib/alula/cdn.rb +30 -0
  9. data/lib/alula/cdns/edgecast.rb +16 -0
  10. data/lib/alula/cdns/hosts.rb +14 -0
  11. data/lib/alula/cli.rb +90 -39
  12. data/lib/alula/compressors.rb +22 -10
  13. data/lib/alula/config.rb +141 -0
  14. data/lib/alula/content.rb +113 -0
  15. data/lib/alula/contents/attachment.rb +8 -0
  16. data/lib/alula/contents/item.rb +409 -0
  17. data/lib/alula/contents/metadata.rb +73 -0
  18. data/lib/alula/contents/page.rb +9 -0
  19. data/lib/alula/contents/post.rb +32 -0
  20. data/lib/alula/context.rb +72 -0
  21. data/lib/alula/core_ext.rb +5 -0
  22. data/lib/alula/core_ext/environment.rb +20 -0
  23. data/lib/alula/core_ext/filter.rb +20 -0
  24. data/lib/alula/core_ext/filters/smilies.rb +36 -0
  25. data/lib/alula/core_ext/manifest.rb +30 -0
  26. data/lib/alula/core_ext/tag.rb +100 -0
  27. data/lib/alula/core_ext/tags/attachment.rb +28 -0
  28. data/lib/alula/core_ext/tags/blockquote.rb +21 -0
  29. data/lib/alula/core_ext/tags/image.rb +48 -0
  30. data/lib/alula/core_ext/tags/locale.rb +17 -0
  31. data/lib/alula/core_ext/tags/video.rb +103 -0
  32. data/lib/alula/generator.rb +31 -0
  33. data/lib/alula/generators/feedbuilder.rb +44 -0
  34. data/lib/alula/generators/paginate.rb +88 -0
  35. data/lib/alula/generators/sitemap.rb +26 -0
  36. data/lib/alula/helpers.rb +2 -0
  37. data/lib/alula/helpers/addons.rb +12 -0
  38. data/lib/alula/helpers/assets.rb +56 -0
  39. data/lib/alula/helpers/url_helpers.rb +16 -0
  40. data/lib/alula/plugin.rb +32 -0
  41. data/lib/alula/processor.rb +86 -0
  42. data/lib/alula/processors/dummy.rb +24 -0
  43. data/lib/alula/processors/image.rb +52 -0
  44. data/lib/alula/processors/magick.rb +83 -0
  45. data/lib/alula/processors/video.rb +97 -0
  46. data/lib/alula/processors/zencoder.rb +199 -0
  47. data/lib/alula/progress.rb +95 -0
  48. data/lib/alula/progressbar.rb +66 -0
  49. data/lib/alula/site.rb +331 -262
  50. data/lib/alula/storage.rb +46 -0
  51. data/lib/alula/storages/file_item.rb +43 -0
  52. data/lib/alula/storages/filestorage.rb +96 -0
  53. data/lib/alula/storages/item.rb +12 -0
  54. data/lib/alula/support/commonlogger.rb +30 -0
  55. data/lib/alula/theme.rb +70 -13
  56. data/lib/alula/theme/layout.rb +56 -0
  57. data/lib/alula/theme/view.rb +43 -0
  58. data/lib/alula/version.rb +1 -1
  59. data/locales/en.yml +9 -0
  60. data/locales/fi.yml +10 -0
  61. data/locales/l10n/ar.yml +199 -0
  62. data/locales/l10n/az.yml +199 -0
  63. data/locales/l10n/bg.yml +199 -0
  64. data/locales/l10n/bn-IN.yml +182 -0
  65. data/locales/l10n/bs.yml +242 -0
  66. data/locales/l10n/ca.yml +199 -0
  67. data/locales/l10n/cs.yml +198 -0
  68. data/locales/l10n/csb.yml +210 -0
  69. data/locales/l10n/cy.yml +199 -0
  70. data/locales/l10n/da.yml +199 -0
  71. data/locales/l10n/de-AT.yml +203 -0
  72. data/locales/l10n/de-CH.yml +203 -0
  73. data/locales/l10n/de.yml +203 -0
  74. data/locales/l10n/dsb.yml +215 -0
  75. data/locales/l10n/el.yml +199 -0
  76. data/locales/l10n/en-AU.yml +205 -0
  77. data/locales/l10n/en-CA.yml +214 -0
  78. data/locales/l10n/en-GB.yml +205 -0
  79. data/locales/l10n/en-IN.yml +205 -0
  80. data/locales/l10n/en-US.yml +205 -0
  81. data/locales/l10n/en.yml +205 -0
  82. data/locales/l10n/eo.yml +201 -0
  83. data/locales/l10n/es-AR.yml +205 -0
  84. data/locales/l10n/es-CL.yml +199 -0
  85. data/locales/l10n/es-CO.yml +205 -0
  86. data/locales/l10n/es-MX.yml +205 -0
  87. data/locales/l10n/es-PE.yml +181 -0
  88. data/locales/l10n/es-VE.yml +205 -0
  89. data/locales/l10n/es.yml +199 -0
  90. data/locales/l10n/et.yml +199 -0
  91. data/locales/l10n/eu.yml +199 -0
  92. data/locales/l10n/fa.yml +199 -0
  93. data/locales/l10n/fi.yml +199 -0
  94. data/locales/l10n/fr-CA.yml +207 -0
  95. data/locales/l10n/fr-CH.yml +207 -0
  96. data/locales/l10n/fr.yml +222 -0
  97. data/locales/l10n/fur.yml +199 -0
  98. data/locales/l10n/gl-ES.yml +178 -0
  99. data/locales/l10n/gsw-CH.yml +199 -0
  100. data/locales/l10n/he.yml +201 -0
  101. data/locales/l10n/hi-IN.yml +199 -0
  102. data/locales/l10n/hi.yml +199 -0
  103. data/locales/l10n/hr.yml +237 -0
  104. data/locales/l10n/hsb.yml +214 -0
  105. data/locales/l10n/hu.yml +199 -0
  106. data/locales/l10n/id.yml +200 -0
  107. data/locales/l10n/is.yml +213 -0
  108. data/locales/l10n/it.yml +205 -0
  109. data/locales/l10n/ja.yml +197 -0
  110. data/locales/l10n/kn.yml +199 -0
  111. data/locales/l10n/ko.yml +197 -0
  112. data/locales/l10n/lo.yml +186 -0
  113. data/locales/l10n/lt.yml +182 -0
  114. data/locales/l10n/lv.yml +215 -0
  115. data/locales/l10n/mk.yml +170 -0
  116. data/locales/l10n/mn.yml +205 -0
  117. data/locales/l10n/nb.yml +207 -0
  118. data/locales/l10n/nl.yml +199 -0
  119. data/locales/l10n/nn.yml +160 -0
  120. data/locales/l10n/pl.yml +221 -0
  121. data/locales/l10n/pt-BR.yml +207 -0
  122. data/locales/l10n/pt-PT.yml +207 -0
  123. data/locales/l10n/quotes.yml +24 -0
  124. data/locales/l10n/rm.yml +182 -0
  125. data/locales/l10n/ro.yml +199 -0
  126. data/locales/l10n/ru.yml +257 -0
  127. data/locales/l10n/sk.yml +213 -0
  128. data/locales/l10n/sl.yml +210 -0
  129. data/locales/l10n/sr-Latn.yml +170 -0
  130. data/locales/l10n/sr.yml +170 -0
  131. data/locales/l10n/sv-SE.yml +199 -0
  132. data/locales/l10n/sw.yml +197 -0
  133. data/locales/l10n/th.yml +173 -0
  134. data/locales/l10n/tl.yml +229 -0
  135. data/locales/l10n/tr.yml +199 -0
  136. data/locales/l10n/uk.yml +257 -0
  137. data/locales/l10n/vi.yml +201 -0
  138. data/locales/l10n/wo.yml +205 -0
  139. data/locales/l10n/zh-CN.yml +199 -0
  140. data/locales/l10n/zh-TW.yml +199 -0
  141. data/template/Gemfile.erb +14 -4
  142. data/template/README +16 -0
  143. data/template/config.yml.erb +42 -38
  144. data/test/fixtures/config_001_simple.yml +2 -0
  145. data/test/fixtures/config_002_l10n.yml +5 -0
  146. data/test/fixtures/pages/invalid-page.markdown +1 -0
  147. data/test/fixtures/pages/multilingual-page.markdown +20 -0
  148. data/test/fixtures/pages/section/subpage.markdown +5 -0
  149. data/test/fixtures/pages/simple-page.markdown +7 -0
  150. data/test/fixtures/posts/2012-07-02-invalid-post.markdown +1 -0
  151. data/test/fixtures/posts/2012-07-02-simple.markdown +7 -0
  152. data/test/fixtures/posts/2012-07-03-full-metadata.markdown +8 -0
  153. data/test/fixtures/posts/2012-07-03-multilingual-full-metadata.markdown +20 -0
  154. data/test/fixtures/theme/test/layouts/default.html.erb +1 -0
  155. data/test/fixtures/theme/test/views/page.html.erb +1 -0
  156. data/test/fixtures/theme/test/views/post.html.erb +1 -0
  157. data/test/minitest_helper.rb +14 -0
  158. data/test/test_config.rb +33 -0
  159. data/test/test_content.rb +30 -0
  160. data/test/test_metadata.rb +83 -0
  161. data/test/test_page.rb +81 -0
  162. data/test/test_post.rb +123 -0
  163. data/test/test_storage.rb +23 -0
  164. data/test/test_storage_file.rb +32 -0
  165. data/test/test_theme.rb +45 -0
  166. data/vendor/assets/images/favicon.png +0 -0
  167. data/vendor/assets/images/grey.gif +0 -0
  168. data/vendor/assets/javascripts/jquery.alula.js.coffee +16 -0
  169. data/vendor/{javascripts → assets/javascripts}/jquery.js +0 -0
  170. data/vendor/assets/javascripts/jquery.lazyload.js +210 -0
  171. data/vendor/assets/javascripts/lazyload.js.coffee +15 -0
  172. data/vendor/layouts/feed.xml.builder +19 -0
  173. data/vendor/layouts/sitemap.xml.builder +10 -0
  174. data/vendor/views/feed_post.html.haml +1 -0
  175. metadata +529 -50
  176. data/lib/alula.rb +0 -5
  177. data/lib/alula/assethelper.rb +0 -75
  178. data/lib/alula/plugins.rb +0 -23
  179. data/lib/alula/plugins/assets.rb +0 -82
  180. data/lib/alula/plugins/pagination.rb +0 -121
  181. data/lib/alula/rake_tasks.rb +0 -42
  182. data/lib/alula/tasks.rb +0 -2
@@ -0,0 +1,73 @@
1
+ module Alula
2
+ class Content
3
+ class Metadata
4
+ def initialize(default = {})
5
+ default.each do |key, value|
6
+ self.send("#{key}=", value)
7
+ end
8
+ end
9
+
10
+ def load(payload)
11
+ meta = YAML.load(payload)
12
+ meta.each do |key, value|
13
+ # Check for localisations
14
+ self.send("#{key}=", value)
15
+ end
16
+ end
17
+
18
+ def respond_to?(name)
19
+ # Accept all setters
20
+ return true if name.to_s =~ /=$/
21
+
22
+ return true if instance_variable_defined?("@#{name}")
23
+
24
+ super
25
+ end
26
+
27
+ def method_missing(meth, *args, &blk)
28
+ if meth[/=$/]
29
+ instance_variable_set("@#{meth[0..-2]}", *args)
30
+ else
31
+ # Localisation support
32
+ value = instance_variable_get("@#{meth}")
33
+ if value.kind_of?(Hash)
34
+ # Try environment & locale first
35
+ if value.has_key?(environment) and value[environment].kind_of?(Hash)
36
+ return value[environment][args[0]] if value[environment].has_key?(args[0])
37
+ return value[environment][base_locale] if value[environment].has_key?(base_locale)
38
+ return value[environment]
39
+ else
40
+ # Try locales
41
+ return value[args[0]] if value.has_key?(args[0])
42
+ return value[base_locale] if value.has_key?(base_locale)
43
+ return value[environment] if value.has_key?(environment)
44
+ end
45
+ end
46
+
47
+ value
48
+ end
49
+ end
50
+
51
+ # Custom accessors
52
+ def date=(date)
53
+ begin
54
+ if date.kind_of?(String)
55
+ @date = Time.parse(date)
56
+ else
57
+ @date = date
58
+ end
59
+ rescue ArgumentError => e
60
+ @date = nil
61
+ end
62
+ end
63
+
64
+ def languages(locale = nil)
65
+ if @title.kind_of?(Hash)
66
+ @title.keys
67
+ else
68
+ [base_locale]
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,9 @@
1
+ require 'alula/contents/item'
2
+
3
+ module Alula
4
+ class Content
5
+ class Page < Item
6
+ has_payload
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,32 @@
1
+ require 'alula/contents/item'
2
+
3
+ module Alula
4
+ class Content
5
+ class Post < Item
6
+ has_payload
7
+
8
+ def previous(locale = nil)
9
+ pos = self.navigation(locale).index(self)
10
+ if pos and pos < (self.navigation(locale).count - 1)
11
+ self.navigation(locale)[pos + 1]
12
+ else
13
+ nil
14
+ end
15
+ end
16
+
17
+ def next(locale = nil)
18
+ pos = self.navigation(locale).index(self)
19
+ if pos and pos > 0
20
+ self.navigation(locale)[pos - 1]
21
+ else
22
+ nil
23
+ end
24
+ end
25
+
26
+ def navigation(locale = nil)
27
+ locale ||= self.current_locale || self.site.config.locale
28
+ @navigation[locale] ||= self.site.content.posts.select { |item| item.languages.include?(locale) }
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,72 @@
1
+ require 'liquid/context'
2
+
3
+ module Alula
4
+ class Context
5
+ def initialize(default = {})
6
+ @data = {}
7
+ default.each do |key, value|
8
+ @data[key.to_s] = value
9
+ end
10
+ end
11
+
12
+ def []=(key, value)
13
+ key = key.to_s
14
+ if value.kind_of?(Hash)
15
+ value = Context.new value
16
+ end
17
+ @data[key] = value
18
+ end
19
+
20
+ def [](key)
21
+ key = key.to_s
22
+ if @data[key].kind_of?(Hash)
23
+ @data[key] = Context.new @data[key]
24
+ end
25
+ @data[key]
26
+ end
27
+
28
+ def data=(new_data)
29
+ @data = new_data
30
+ end
31
+
32
+ def respond_to?(name)
33
+ if name.to_s =~ /=$/ and @data.key?(name.to_s[0..-2])
34
+ true
35
+ elsif @data.key?(name)
36
+ true
37
+ else
38
+ super
39
+ end
40
+ end
41
+
42
+ def method_missing(meth, *args, &block)
43
+ if meth.to_s =~ /=$/
44
+ self.send(:[]=, meth.to_s[0..-2], *args)
45
+ elsif @data.key?(meth.to_s)
46
+ self.send(:[], meth.to_s)
47
+ else
48
+ nil
49
+ end
50
+ end
51
+
52
+ def to_liquid
53
+ @liquid ||= begin
54
+ liquid = Liquid::Context.new(
55
+ nil, # Environments
56
+ nil, # Outer Scope
57
+ {:proxy => self}, # Registers
58
+ true) # Rethrow errors
59
+ liquid.class.class_eval do
60
+ def method_missing(meth, *args, &blk)
61
+ if registers[:proxy].respond_to?(meth.to_s)
62
+ registers[:proxy].send(meth, *args)
63
+ else
64
+ super
65
+ end
66
+ end
67
+ end
68
+ @liquid = liquid
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,5 @@
1
+ require 'alula/core_ext/tag'
2
+ require 'alula/core_ext/filter'
3
+
4
+ require 'alula/core_ext/manifest'
5
+ require 'alula/core_ext/environment'
@@ -0,0 +1,20 @@
1
+ require 'sprockets'
2
+
3
+ module Alula
4
+ class Environment < Sprockets::Environment
5
+ def initialize
6
+ super
7
+
8
+ @used = []
9
+ end
10
+
11
+ def used
12
+ @used.uniq
13
+ end
14
+
15
+ def [](key)
16
+ @used << key
17
+ super
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ module Alula
2
+ class Filter
3
+ def self.register(name, klass); filters[name.to_s] = klass; end
4
+ def self.filters; @@filters ||= {}; end
5
+ def filters; self.class.filters; end
6
+
7
+ def self.load(name, options)
8
+ if filters[name]
9
+ filter = filters[name].new(options)
10
+ return filter
11
+ end
12
+ end
13
+
14
+ def initialize(options)
15
+ @options = options
16
+ end
17
+ end
18
+ end
19
+
20
+ Dir[File.join(File.dirname(__FILE__), "filters", "*.rb")].each {|f| require f}
@@ -0,0 +1,36 @@
1
+ module Alula
2
+ class Smilies < Filter
3
+ MAP = {
4
+ angel: %w{:angel: },
5
+ angry: %w{:angry: },
6
+ cool: %w{:cool: },
7
+ badguy: %w{:badguy: },
8
+ crying: %w{:crying: ;( ;-( },
9
+ exclamation: %w{:exclamation: },
10
+ idea: %w{:idea: },
11
+ kiss: %w{:kiss: :* :-* },
12
+ laugh: %w{:laugh: :D :-D },
13
+ monkey: %w{:monkey: },
14
+ plain: %w{:plain: :| :-| },
15
+ question: %w{:question: },
16
+ tongue: %w{:tongue: },
17
+ sad: %w{:sad: :( :-( },
18
+ grin: %w{:grin: },
19
+ smile: %w{:smile: :) :-) ^^ ^_^},
20
+ smirk: %w{:smirk: },
21
+ point_right: %w{:point_right: },
22
+ surprise: %w{:surprise: },
23
+ uncertain: %w{:uncertain: },
24
+ wink: %w{:wink: ;) ;-) },
25
+ worried: %w{:worried: },
26
+ }
27
+ SMILIES = Hash[*MAP.collect{|cls, keys| keys.collect{|key| [key, "<span class=\"smilies-#{cls}\">#{key}</span>"]}}.flatten]
28
+ SMILIES_RE = Regexp.new(SMILIES.keys.collect{|k| Regexp.escape(k)}.join("|"))
29
+
30
+ def process(content, locale)
31
+ content.gsub(SMILIES_RE, SMILIES)
32
+ end
33
+ end
34
+ end
35
+
36
+ Alula::Filter.register :smilies, Alula::Smilies
@@ -0,0 +1,30 @@
1
+ require 'sprockets/manifest'
2
+
3
+ module Alula
4
+ class Manifest < Sprockets::Manifest
5
+ attr_accessor :progress
6
+
7
+ def assets_with_tracking
8
+ @_assets ||= AssetTracker.new assets_without_tracking, @progress
9
+ end
10
+ alias_method :assets_without_tracking, :assets
11
+ alias_method :assets, :assets_with_tracking
12
+
13
+ private
14
+ class AssetTracker
15
+ def initialize(hash, progress_callback)
16
+ @hash = hash
17
+ @progress_cb = progress_callback
18
+ end
19
+
20
+ def [](key)
21
+ @hash[key]
22
+ end
23
+
24
+ def []=(key, value)
25
+ @hash[key] = value
26
+ @progress_cb.call if @progress_cb
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,100 @@
1
+ require 'liquid'
2
+ require 'dimensions'
3
+
4
+ module Alula
5
+ module LiquidExt
6
+ attr_reader :context
7
+
8
+ def self.included(base)
9
+ base.extend(ClassMethods)
10
+ end
11
+
12
+ module ClassMethods
13
+ def register(tagname, klass)
14
+ Liquid::Template.register_tag(tagname.to_s, klass)
15
+ end
16
+ end
17
+
18
+ def initialize(tagname, markup, tokens)
19
+ super
20
+
21
+ @tagname = tagname
22
+ @markup = markup.strip
23
+ @tokens = tokens
24
+
25
+ @info = {}
26
+ @options = {}
27
+
28
+ # Parse tag options
29
+ if m = /^([\"'][\S\. ]+[\"']|[\S\.]+)(.*)$/.match(@markup)
30
+ @source = m[1].gsub(/^['"]?([^'"]+)['"]?$/, '\1')
31
+ @source = "" if @source == '""'
32
+ @options["source"] = @source unless @source.empty?
33
+
34
+ if m[2]
35
+ m[2].scan(/(\S+):["]?((?:.(?!["]?\s+(?:\S+):|[>"]))+.)["]?/) do |name, value|
36
+ @options[name] = value
37
+ end
38
+ end
39
+ end
40
+
41
+ prepare if respond_to?("prepare")
42
+ end
43
+
44
+ def render(context)
45
+ @context = context
46
+ if respond_to?("content")
47
+ content
48
+ else
49
+ super
50
+ end
51
+ end
52
+
53
+ private
54
+ def info(source, type = nil)
55
+ @info["#{source}#{type.to_s}"] ||= begin
56
+ file = self.context.asset_path(attachment_path(source, type))
57
+ return Hashie::Mash.new if file.nil?
58
+
59
+ # info = MiniExiftool.new File.join self.context.storage.path(:public), file
60
+ info = Dimensions.dimensions(File.join(self.context.storage.path(:public), file))
61
+ info ||= begin
62
+ _info = MiniExiftool.new File.join(self.context.storage.path(:public), file)
63
+ [_info.imagewidth, _info.imageheight]
64
+ end
65
+ Hashie::Mash.new({
66
+ width: info[0],
67
+ height: info[1],
68
+ })
69
+ end
70
+ end
71
+
72
+ def attachment_path(source, type = nil)
73
+ name = (type.nil?) ? source : File.join(type.to_s, source)
74
+ self.context.attachments.mapping[name]
75
+ end
76
+
77
+ def attachment_url(source, type = nil)
78
+ asset_url(attachment_path(source, type))
79
+ end
80
+
81
+ def asset_url(name)
82
+ self.context.asset_url(name)
83
+ end
84
+
85
+ def hires_url(source, type)
86
+ hires_source = source.gsub(/(#{File.extname(source)})$/, '-hires\1')
87
+ attachment_url(hires_source, type)
88
+ end
89
+ end
90
+
91
+ class Tag < Liquid::Tag
92
+ include LiquidExt
93
+ end
94
+
95
+ class Block < Liquid::Block
96
+ include LiquidExt
97
+ end
98
+ end
99
+
100
+ Dir[File.join(File.dirname(__FILE__), "tags", "*.rb")].each {|f| require f}
@@ -0,0 +1,28 @@
1
+ module Alula
2
+ class AttachmentTag < Tag
3
+ def prepare
4
+ @classes = []
5
+
6
+ if m = /(["'])?([^"']+)\1?(?: (.+))?/.match(@markup)
7
+ @source = m[2]
8
+ @title = File.basename(@source)
9
+ options = m[3]
10
+ end
11
+
12
+ if options
13
+ options.scan(/(\S+):["]?((?:.(?!["]?\s+(?:\S+):|[>"]))+.)["]?/) do |name, value|
14
+ case name
15
+ when "title"
16
+ @title = value
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ def content
23
+ "<a href=\"#{attachment_url(@source)}\">#{@title}</a>"
24
+ end
25
+ end
26
+ end
27
+
28
+ Alula::Tag.register :attachment, Alula::AttachmentTag
@@ -0,0 +1,21 @@
1
+ module Alula
2
+ class BlockquoteTag < Block
3
+ def render(context)
4
+ quote = super
5
+
6
+ tag = "<blockquote>"
7
+ tag += quote
8
+ if @options["author"] or @source
9
+ tag += "<div class=\"source\">"
10
+ tag += " &mdash; " if @options["author"] and @source.empty?
11
+ tag += "<strong>#{@options["author"]}</strong>" if @options["author"]
12
+ tag += " &mdash; " if @options["author"] and !@source.empty?
13
+ tag += "<cite>#{@source}</cite>" if @source
14
+ tag += "</div>"
15
+ end
16
+ tag += "</blockquote>"
17
+ end
18
+ end
19
+ end
20
+
21
+ Alula::Tag.register :blockquote, Alula::BlockquoteTag