olelo 0.9.0

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 (282) hide show
  1. data/.gitignore +10 -0
  2. data/.gitmodules +3 -0
  3. data/Gemfile +3 -0
  4. data/README.markdown +104 -0
  5. data/Rakefile +92 -0
  6. data/bin/olelo +5 -0
  7. data/config.ru +86 -0
  8. data/config/aspects.rb +256 -0
  9. data/config/config.yml.default +154 -0
  10. data/config/initializers/00-mime_types.rb +29 -0
  11. data/config/initializers/01-slim.rb +2 -0
  12. data/config/interwiki.yml +11 -0
  13. data/doc/AUTHORS +7 -0
  14. data/doc/LICENSE +22 -0
  15. data/lib/olelo.rb +39 -0
  16. data/lib/olelo/application.rb +294 -0
  17. data/lib/olelo/attributes.rb +285 -0
  18. data/lib/olelo/config.rb +88 -0
  19. data/lib/olelo/extensions.rb +252 -0
  20. data/lib/olelo/helper.rb +290 -0
  21. data/lib/olelo/hooks.rb +142 -0
  22. data/lib/olelo/html_safe.rb +29 -0
  23. data/lib/olelo/initializer.rb +76 -0
  24. data/lib/olelo/locale.rb +63 -0
  25. data/lib/olelo/locale.yml +284 -0
  26. data/lib/olelo/menu.rb +101 -0
  27. data/lib/olelo/middleware/blacklist.rb +25 -0
  28. data/lib/olelo/middleware/degrade_mime_type.rb +19 -0
  29. data/lib/olelo/middleware/flash.rb +97 -0
  30. data/lib/olelo/middleware/force_encoding.rb +41 -0
  31. data/lib/olelo/page.rb +266 -0
  32. data/lib/olelo/patch.rb +311 -0
  33. data/lib/olelo/plugin.rb +188 -0
  34. data/lib/olelo/repository.rb +225 -0
  35. data/lib/olelo/routing.rb +223 -0
  36. data/lib/olelo/templates.rb +30 -0
  37. data/lib/olelo/user.rb +132 -0
  38. data/lib/olelo/util.rb +233 -0
  39. data/lib/olelo/version.rb +3 -0
  40. data/lib/olelo/virtualfs.rb +161 -0
  41. data/lib/rack/olelo_patches.rb +33 -0
  42. data/lib/rack/relative_redirect.rb +44 -0
  43. data/lib/rack/static_cache.rb +93 -0
  44. data/lib/yard/addons.rb +1 -0
  45. data/lib/yard/addons/hook_handler.rb +25 -0
  46. data/lib/yard/addons/override_tag.rb +14 -0
  47. data/lib/yard/addons/route_handler.rb +33 -0
  48. data/lib/yard/addons/sanitize_anchor.rb +16 -0
  49. data/olelo.gemspec +31 -0
  50. data/plugins/aspects/changelog.rb +45 -0
  51. data/plugins/aspects/documentbrowser.rb +57 -0
  52. data/plugins/aspects/download.rb +11 -0
  53. data/plugins/aspects/highlight.rb +8 -0
  54. data/plugins/aspects/image.rb +41 -0
  55. data/plugins/aspects/imageinfo.rb +64 -0
  56. data/plugins/aspects/locale.yml +60 -0
  57. data/plugins/aspects/main.rb +199 -0
  58. data/plugins/aspects/pageinfo.rb +37 -0
  59. data/plugins/aspects/source.rb +6 -0
  60. data/plugins/aspects/subpages.rb +44 -0
  61. data/plugins/aspects/text.rb +9 -0
  62. data/plugins/blog/blog.css +1 -0
  63. data/plugins/blog/blog.scss +37 -0
  64. data/plugins/blog/locale.yml +12 -0
  65. data/plugins/blog/main.rb +85 -0
  66. data/plugins/editor/locale.yml +18 -0
  67. data/plugins/editor/markup/main.rb +3 -0
  68. data/plugins/editor/markup/script.js +10 -0
  69. data/plugins/editor/markup/script/00-jquery.textselection.js +267 -0
  70. data/plugins/editor/markup/script/01-olelo.markupeditor.js +116 -0
  71. data/plugins/editor/markup/script/init.js +10 -0
  72. data/plugins/editor/preview.rb +52 -0
  73. data/plugins/editor/recaptcha.rb +56 -0
  74. data/plugins/filters/creole.rb +37 -0
  75. data/plugins/filters/disposition.rb +9 -0
  76. data/plugins/filters/editsection.rb +67 -0
  77. data/plugins/filters/fix_img_tag.rb +16 -0
  78. data/plugins/filters/html_wrapper.rb +12 -0
  79. data/plugins/filters/interwiki.rb +19 -0
  80. data/plugins/filters/link_classifier.rb +26 -0
  81. data/plugins/filters/locale.yml +15 -0
  82. data/plugins/filters/main.rb +202 -0
  83. data/plugins/filters/markdown_nowiki.rb +15 -0
  84. data/plugins/filters/numbering.xsl +93 -0
  85. data/plugins/filters/orgmode.rb +6 -0
  86. data/plugins/filters/rubypants.rb +6 -0
  87. data/plugins/filters/s5/main.rb +32 -0
  88. data/plugins/filters/s5/s5.xsl +118 -0
  89. data/plugins/filters/tilt.rb +17 -0
  90. data/plugins/filters/toc.rb +50 -0
  91. data/plugins/filters/xhtml2latex.xsl +232 -0
  92. data/plugins/filters/xslt.rb +22 -0
  93. data/plugins/gallery/gallery.css +1 -0
  94. data/plugins/gallery/gallery.scss +28 -0
  95. data/plugins/gallery/main.rb +34 -0
  96. data/plugins/misc/fancybox/images/blank.gif +0 -0
  97. data/plugins/misc/fancybox/images/fancy_close.png +0 -0
  98. data/plugins/misc/fancybox/images/fancy_loading.png +0 -0
  99. data/plugins/misc/fancybox/images/fancy_nav_left.png +0 -0
  100. data/plugins/misc/fancybox/images/fancy_nav_right.png +0 -0
  101. data/plugins/misc/fancybox/images/fancy_shadow_e.png +0 -0
  102. data/plugins/misc/fancybox/images/fancy_shadow_n.png +0 -0
  103. data/plugins/misc/fancybox/images/fancy_shadow_ne.png +0 -0
  104. data/plugins/misc/fancybox/images/fancy_shadow_nw.png +0 -0
  105. data/plugins/misc/fancybox/images/fancy_shadow_s.png +0 -0
  106. data/plugins/misc/fancybox/images/fancy_shadow_se.png +0 -0
  107. data/plugins/misc/fancybox/images/fancy_shadow_sw.png +0 -0
  108. data/plugins/misc/fancybox/images/fancy_shadow_w.png +0 -0
  109. data/plugins/misc/fancybox/images/fancy_title_left.png +0 -0
  110. data/plugins/misc/fancybox/images/fancy_title_main.png +0 -0
  111. data/plugins/misc/fancybox/images/fancy_title_over.png +0 -0
  112. data/plugins/misc/fancybox/images/fancy_title_right.png +0 -0
  113. data/plugins/misc/fancybox/images/fancybox-x.png +0 -0
  114. data/plugins/misc/fancybox/images/fancybox-y.png +0 -0
  115. data/plugins/misc/fancybox/images/fancybox.png +0 -0
  116. data/plugins/misc/fancybox/jquery.fancybox.css +1 -0
  117. data/plugins/misc/fancybox/jquery.fancybox.scss +323 -0
  118. data/plugins/misc/fancybox/main.rb +4 -0
  119. data/plugins/misc/fancybox/script.js +37 -0
  120. data/plugins/misc/fancybox/script/00-jquery.mousewheel.js +84 -0
  121. data/plugins/misc/fancybox/script/01-jquery.easing.js +205 -0
  122. data/plugins/misc/fancybox/script/02-jquery.fancybox.js +1156 -0
  123. data/plugins/misc/fancybox/script/init.js +18 -0
  124. data/plugins/misc/system.rb +192 -0
  125. data/plugins/misc/variables.rb +29 -0
  126. data/plugins/misc/webdav.rb +45 -0
  127. data/plugins/repositories/git_grep.rb +69 -0
  128. data/plugins/repositories/gitrb_repository.rb +204 -0
  129. data/plugins/repositories/locale.yml +12 -0
  130. data/plugins/repositories/rugged_repository.rb +454 -0
  131. data/plugins/security/acl.rb +57 -0
  132. data/plugins/security/basic_auth.rb +21 -0
  133. data/plugins/security/locale.yml +21 -0
  134. data/plugins/security/persistent_login.rb +32 -0
  135. data/plugins/security/portal.rb +28 -0
  136. data/plugins/security/private_wiki.rb +24 -0
  137. data/plugins/security/readonly_wiki.rb +25 -0
  138. data/plugins/security/stack.rb +20 -0
  139. data/plugins/security/yamlfile.rb +66 -0
  140. data/plugins/tags/code.rb +6 -0
  141. data/plugins/tags/footnotes.rb +35 -0
  142. data/plugins/tags/gist-embed.css +123 -0
  143. data/plugins/tags/gist.rb +13 -0
  144. data/plugins/tags/html.rb +57 -0
  145. data/plugins/tags/include.rb +20 -0
  146. data/plugins/tags/main.rb +353 -0
  147. data/plugins/tags/math.rb +117 -0
  148. data/plugins/tags/scripting.rb +64 -0
  149. data/plugins/tags/sort.rb +7 -0
  150. data/plugins/tags/tabs.rb +20 -0
  151. data/plugins/treeview/images/collapsed.png +0 -0
  152. data/plugins/treeview/images/expanded.png +0 -0
  153. data/plugins/treeview/images/menu.png +0 -0
  154. data/plugins/treeview/images/tree.png +0 -0
  155. data/plugins/treeview/images/wait.gif +0 -0
  156. data/plugins/treeview/main.rb +16 -0
  157. data/plugins/treeview/script.js +5 -0
  158. data/plugins/treeview/script/00-jquery.treeview.js +164 -0
  159. data/plugins/treeview/script/init.js +25 -0
  160. data/plugins/treeview/treeview.css +1 -0
  161. data/plugins/treeview/treeview.scss +113 -0
  162. data/plugins/utils/assets.rb +74 -0
  163. data/plugins/utils/cache.rb +53 -0
  164. data/plugins/utils/image_magick.rb +34 -0
  165. data/plugins/utils/pygments.css +1 -0
  166. data/plugins/utils/pygments.rb +50 -0
  167. data/plugins/utils/pygments.scss +83 -0
  168. data/plugins/utils/semaphore.rb +50 -0
  169. data/plugins/utils/shell.rb +45 -0
  170. data/plugins/utils/store.rb +315 -0
  171. data/plugins/utils/worker.rb +36 -0
  172. data/plugins/utils/xml.rb +29 -0
  173. data/static/images/favicon.png +0 -0
  174. data/static/script.js +267 -0
  175. data/static/script/00-json2.js +486 -0
  176. data/static/script/01-jstorage.js +217 -0
  177. data/static/script/02-jquery.js +9440 -0
  178. data/static/script/03-jquery.ui.core.js +337 -0
  179. data/static/script/04-jquery.ui.widget.js +502 -0
  180. data/static/script/05-jquery.ui.position.js +517 -0
  181. data/static/script/06-jquery.ui.menu.js +609 -0
  182. data/static/script/07-jquery.ui.autocomplete.js +601 -0
  183. data/static/script/08-olelo.i18n.js +37 -0
  184. data/static/script/09-olelo.unsaved.js +68 -0
  185. data/static/script/10-olelo.historytable.js +40 -0
  186. data/static/script/11-olelo.pagination.js +18 -0
  187. data/static/script/13-olelo.tabwidget.js +57 -0
  188. data/static/script/14-olelo.timeago.js +70 -0
  189. data/static/script/15-olelo.underliner.js +31 -0
  190. data/static/script/16-olelo.ui.combobox.js +32 -0
  191. data/static/script/init.js +48 -0
  192. data/static/themes/atlantis/constants.scss +15 -0
  193. data/static/themes/atlantis/iehacks.scss +38 -0
  194. data/static/themes/atlantis/images/actions/delete.png +0 -0
  195. data/static/themes/atlantis/images/actions/edit.png +0 -0
  196. data/static/themes/atlantis/images/actions/history.png +0 -0
  197. data/static/themes/atlantis/images/actions/home.png +0 -0
  198. data/static/themes/atlantis/images/actions/move.png +0 -0
  199. data/static/themes/atlantis/images/actions/new.png +0 -0
  200. data/static/themes/atlantis/images/actions/page.png +0 -0
  201. data/static/themes/atlantis/images/bg/button.png +0 -0
  202. data/static/themes/atlantis/images/bg/container.png +0 -0
  203. data/static/themes/atlantis/images/bg/content.png +0 -0
  204. data/static/themes/atlantis/images/bg/footer.png +0 -0
  205. data/static/themes/atlantis/images/bg/header.jpg +0 -0
  206. data/static/themes/atlantis/images/bg/header.orig.jpg +0 -0
  207. data/static/themes/atlantis/images/bg/header_gray.jpg +0 -0
  208. data/static/themes/atlantis/images/bug.png +0 -0
  209. data/static/themes/atlantis/images/filetypes/7z.png +0 -0
  210. data/static/themes/atlantis/images/filetypes/_archive.png +0 -0
  211. data/static/themes/atlantis/images/filetypes/_audio.png +0 -0
  212. data/static/themes/atlantis/images/filetypes/_code.png +0 -0
  213. data/static/themes/atlantis/images/filetypes/_linux.png +0 -0
  214. data/static/themes/atlantis/images/filetypes/_picture.png +0 -0
  215. data/static/themes/atlantis/images/filetypes/_video.png +0 -0
  216. data/static/themes/atlantis/images/filetypes/bz2.png +0 -0
  217. data/static/themes/atlantis/images/filetypes/doc.png +0 -0
  218. data/static/themes/atlantis/images/filetypes/flac.png +0 -0
  219. data/static/themes/atlantis/images/filetypes/gz.png +0 -0
  220. data/static/themes/atlantis/images/filetypes/html.png +0 -0
  221. data/static/themes/atlantis/images/filetypes/java.png +0 -0
  222. data/static/themes/atlantis/images/filetypes/jpg.png +0 -0
  223. data/static/themes/atlantis/images/filetypes/midi.png +0 -0
  224. data/static/themes/atlantis/images/filetypes/mp3.png +0 -0
  225. data/static/themes/atlantis/images/filetypes/ogg.png +0 -0
  226. data/static/themes/atlantis/images/filetypes/pdf.png +0 -0
  227. data/static/themes/atlantis/images/filetypes/php.png +0 -0
  228. data/static/themes/atlantis/images/filetypes/png.png +0 -0
  229. data/static/themes/atlantis/images/filetypes/ppt.png +0 -0
  230. data/static/themes/atlantis/images/filetypes/psd.png +0 -0
  231. data/static/themes/atlantis/images/filetypes/rar.png +0 -0
  232. data/static/themes/atlantis/images/filetypes/rb.png +0 -0
  233. data/static/themes/atlantis/images/filetypes/sh.png +0 -0
  234. data/static/themes/atlantis/images/filetypes/tar.png +0 -0
  235. data/static/themes/atlantis/images/filetypes/txt.png +0 -0
  236. data/static/themes/atlantis/images/filetypes/wma.png +0 -0
  237. data/static/themes/atlantis/images/filetypes/xls.png +0 -0
  238. data/static/themes/atlantis/images/filetypes/zip.png +0 -0
  239. data/static/themes/atlantis/images/folder.png +0 -0
  240. data/static/themes/atlantis/images/folder_open.png +0 -0
  241. data/static/themes/atlantis/images/loading.gif +0 -0
  242. data/static/themes/atlantis/images/loading.xcf +0 -0
  243. data/static/themes/atlantis/images/not_found.png +0 -0
  244. data/static/themes/atlantis/images/page.png +0 -0
  245. data/static/themes/atlantis/images/search.png +0 -0
  246. data/static/themes/atlantis/layout.scss +115 -0
  247. data/static/themes/atlantis/menu.scss +99 -0
  248. data/static/themes/atlantis/print.scss +129 -0
  249. data/static/themes/atlantis/screen.scss +495 -0
  250. data/static/themes/atlantis/style.css +3 -0
  251. data/static/themes/lib/autocomplete.scss +39 -0
  252. data/static/themes/lib/headlines.scss +10 -0
  253. data/static/themes/lib/horizontal-list.scss +31 -0
  254. data/static/themes/lib/patch.scss +88 -0
  255. data/static/themes/lib/reset.scss +114 -0
  256. data/static/themes/lib/rounded.scss +46 -0
  257. data/static/themes/lib/shadow.scss +14 -0
  258. data/test/config_test.rb +28 -0
  259. data/test/factory_test.rb +29 -0
  260. data/test/hash_extensions_test.rb +16 -0
  261. data/test/helper.rb +38 -0
  262. data/test/hooks_test.rb +85 -0
  263. data/test/object_extensions_test.rb +20 -0
  264. data/test/page_test.rb +168 -0
  265. data/test/request_test.rb +166 -0
  266. data/test/string_extensions_test.rb +32 -0
  267. data/test/templates_test.rb +39 -0
  268. data/test/util_test.rb +71 -0
  269. data/views/changes.slim +22 -0
  270. data/views/compare.slim +8 -0
  271. data/views/delete.slim +9 -0
  272. data/views/deleted.slim +2 -0
  273. data/views/edit.slim +65 -0
  274. data/views/error.slim +6 -0
  275. data/views/history.slim +20 -0
  276. data/views/layout.slim +38 -0
  277. data/views/login.slim +37 -0
  278. data/views/move.slim +10 -0
  279. data/views/not_found.slim +6 -0
  280. data/views/profile.slim +26 -0
  281. data/views/show.slim +9 -0
  282. metadata +488 -0
@@ -0,0 +1,13 @@
1
+ description 'Tag to embed github gist'
2
+ export_scripts 'gist-embed.css'
3
+ require 'open-uri'
4
+
5
+ Tag.define :gist, :requires => :id do |context, attrs|
6
+ if attrs['id'] =~ /^\d+$/
7
+ body = open("https://gist.github.com/#{attrs['id']}.json").read
8
+ gist = JSON.parse(body)
9
+ gist['div'].gsub(' ', ' ')
10
+ else
11
+ raise ArgumentError, 'Invalid gist id'
12
+ end
13
+ end
@@ -0,0 +1,57 @@
1
+ description 'Safe html tags'
2
+
3
+ HTML_TAGS = [
4
+ [:a, {:optional => %w(href title)}],
5
+ [:img, {:autoclose => true, :optional => %w(src alt title)}],
6
+ [:br, {:autoclose => true}],
7
+ [:i],
8
+ [:u],
9
+ [:b],
10
+ [:pre],
11
+ [:kbd],
12
+ # provided by syntax highlighter
13
+ # [:code, :optional => %w(lang)]
14
+ [:cite],
15
+ [:strong],
16
+ [:em],
17
+ [:ins],
18
+ [:sup],
19
+ [:sub],
20
+ [:del],
21
+ [:table],
22
+ [:tr],
23
+ [:td, {:optional => %w(colspan rowspan)}],
24
+ [:th],
25
+ [:ol, {:optional => %w(start)}],
26
+ [:ul],
27
+ [:li],
28
+ [:p],
29
+ [:h1],
30
+ [:h2],
31
+ [:h3],
32
+ [:h4],
33
+ [:h5],
34
+ [:h6],
35
+ [:blockquote, {:optional => %w(cite)}],
36
+ [:div, {:optional => %w(style)}],
37
+ [:span, {:optional => %w(style)}],
38
+ [:video, {:optional => %w(autoplay controls height width loop preload src poster)}],
39
+ [:audio, {:optional => %w(autoplay controls loop preload src)}]
40
+ ]
41
+
42
+ HTML_TAGS.each do |name, options|
43
+ options ||= {}
44
+ if options.delete(:autoclose)
45
+ Tag.define name, options do |context, attrs|
46
+ attrs = attrs.map {|(k,v)| %{#{k}="#{escape_html v}"} }.join
47
+ "<#{name}#{attrs.blank? ? '' : ' '+attrs}/>"
48
+ end
49
+ else
50
+ Tag.define name, options do |context, attrs, content|
51
+ attrs = attrs.map {|(k,v)| %{#{k}="#{escape_html v}"} }.join
52
+ content = subfilter(context.subcontext, content)
53
+ content.gsub!(/(\A<p[^>]*>)|(<\/p>\Z)/, '')
54
+ "<#{name}#{attrs.blank? ? '' : ' '+attrs}>#{content}</#{name}>"
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,20 @@
1
+ description 'Include tags'
2
+
3
+ Tag.define :include, :optional => '*', :requires => :page, :limit => 10, :description => 'Include page' do |context, attrs|
4
+ path = attrs['page']
5
+ path = context.page.path/'..'/path if !path.starts_with? '/'
6
+ if page = Page.find(path, context.page.tree_version)
7
+ Aspects::Aspect.find!(page, :name => attrs['aspect'], :layout => true).
8
+ call(context.subcontext(:params => attrs.merge(:included => true), :page => page), page)
9
+ else
10
+ %{<a href="#{escape_html build_path(path, :action => :new)}">#{escape_html :create_page.t(:page => path)}</a>}
11
+ end
12
+ end
13
+
14
+ Tag.define :includeonly, :immediate => true, :description => 'Text which is shown only if included' do |context, attrs, content|
15
+ nested_tags(context.subcontext, content) if context.params[:included]
16
+ end
17
+
18
+ Tag.define :noinclude, :immediate => true, :description => 'Text which is not included' do |context, attrs, content|
19
+ nested_tags(context.subcontext, content) if !context.params[:included]
20
+ end
@@ -0,0 +1,353 @@
1
+ description 'Extends wiki text with custom xml tags'
2
+ dependencies 'filters'
3
+
4
+ # Simple XML tag parser based on regular expressions
5
+ class TagSoupParser
6
+ include Util
7
+
8
+ NAME = /[\-\w]+(?:\:[\-\w]+)?/
9
+ QUOTED_VALUE = /"[^"]*"|'[^']*'/
10
+ UNQUOTED_VALUE = /(?:[^\s'"\/>]|\/+[^'"\/>])+/
11
+ QUOTED_ATTR = /(#{NAME})=(#{QUOTED_VALUE})/
12
+ UNQUOTED_ATTR = /(#{NAME})=(#{UNQUOTED_VALUE})/
13
+ BOOL_ATTR = /(#{NAME})/
14
+ ATTRIBUTE = /\A\s*(#{QUOTED_ATTR}|#{UNQUOTED_ATTR}|#{BOOL_ATTR})/
15
+
16
+ # enabled_tags must be a hash of tag names
17
+ # that will be recognized by the parser.
18
+ # Hash values must be true for autoclosing tags and false otherwise.
19
+ # Other tags are ignored.
20
+ def initialize(enabled_tags, content)
21
+ @enabled_tags, @content = enabled_tags, content
22
+ @output = ''
23
+ @parsed = nil
24
+ end
25
+
26
+ # Parse the content and call the block
27
+ # for every recognized tag.
28
+ # The block gets two arguments,
29
+ # the attribute hash and the content of the tag.
30
+ # Another instance of the parser has to parse the content to support nested tags.
31
+ def parse(&block)
32
+ while @content =~ /<(#{NAME})/
33
+ @output << $`
34
+ @content = $'
35
+ name = $1.downcase
36
+ if @enabled_tags.include?(name)
37
+ @name = name
38
+ @parsed = $&
39
+ parse_tag(&block)
40
+ else
41
+ # unknown tag, continue parsing after it
42
+ @output << $&
43
+ end
44
+ end
45
+ @output << @content
46
+ end
47
+
48
+ private
49
+
50
+ # Parse the attribute list
51
+ # Allowed attribute formats
52
+ # name="value"
53
+ # name='value'
54
+ # name=value (no space, ' or " allowed in value)
55
+ # name (for boolean values)
56
+ def parse_attributes
57
+ @attrs = Hash.with_indifferent_access
58
+ while @content =~ ATTRIBUTE
59
+ @content = $'
60
+ @parsed << $&
61
+ match = $&
62
+ case match
63
+ when QUOTED_ATTR
64
+ @attrs[$1] = unescape_html($2[1...-1])
65
+ when UNQUOTED_ATTR
66
+ @attrs[$1] = unescape_html($2)
67
+ when BOOL_ATTR
68
+ @attrs[$1] = $1
69
+ end
70
+ end
71
+ end
72
+
73
+ # Parse a tag after the beginning "<@name"
74
+ def parse_tag
75
+ parse_attributes
76
+
77
+ case @content
78
+ when /\A\s*(\/?)>/
79
+ @content = $'
80
+ @parsed << $&
81
+ @output << yield(@name, @attrs, !$1.blank? || @enabled_tags[@name] ? '' : get_content)
82
+ else
83
+ # Tag which begins with <name but has no >.
84
+ # Ignore this and continue parsing after it.
85
+ @output << @parsed
86
+ end
87
+ end
88
+
89
+ # Collect the inner content of the tag
90
+ def get_content
91
+ stack = [@name]
92
+ text = ''
93
+ while !stack.empty?
94
+ case @content
95
+ # Tag begins
96
+ when /\A<(#{NAME})/
97
+ @content = $'
98
+ text << $&
99
+ stack << $1
100
+ # Tag ends
101
+ when /\A<\/(#{NAME})>/
102
+ @content = $'
103
+ if i = stack.rindex($1.downcase)
104
+ stack = stack[0...i]
105
+ text << $& if !stack.empty?
106
+ else
107
+ text << $&
108
+ end
109
+ # Text till the next tag beginning
110
+ when /\A[^<]+/
111
+ text << $&
112
+ @content = $'
113
+ # Suprious <
114
+ when /\A</
115
+ text << '<'
116
+ @content = $'
117
+ # No closing tag
118
+ else
119
+ text << @content
120
+ @content = ''
121
+ break
122
+ end
123
+ end
124
+ text
125
+ end
126
+ end
127
+
128
+ class Tag < Filters::NestingFilter
129
+ @@tags = {}
130
+
131
+ def self.tags
132
+ @@tags
133
+ end
134
+
135
+ # Define a tag which is executed by the tag filter
136
+ #
137
+ # Supported options:
138
+ # * :limit - Limit tag count
139
+ # * :optional - List of optional attributes (* to allow arbitrary)
140
+ # * :requires - List of required attributes
141
+ # * :dynamic - Dynamic tags are uncached, the content is generated
142
+ # on the fly everytime the page is rendered.
143
+ # Warning: Dynamic tags introduce a large overhead!
144
+ # * :immediate - Replace tag immediately with generated content.
145
+ # This means BEFORE the execution of the subfilter.
146
+ # Immediate tags can generate wiki text which is then parsed by the subfilter.
147
+ # The default behaviour is that tags are not immediate.
148
+ # The content is not parsed by the subfilter, this is useful for html generation.
149
+ # * :description - Tag description, by default the plugin description
150
+ # * :namespace - Namespace of the tag, by default the plugin name
151
+ #
152
+ # Tags are added as methods to this filter. This means every method
153
+ # of this class can be called from the tag block.
154
+ # Dynamic tags are an exception. They are executed later from the layout hook.
155
+ def self.define(name, options = {}, &block)
156
+ raise 'Dynamic tag cannot be immediate' if options[:dynamic] && options[:immediate]
157
+ raise 'Tag must take two or three arguments' if block.arity != 2 && block.arity != 3
158
+ # Find the plugin which provided this tag.
159
+ plugin = Plugin.for(block)
160
+ options.merge!(:name => name.to_s, :plugin => plugin, :autoclose => block.arity == 2,
161
+ :optional => Set.new([*options[:optional]].compact.flatten.map(&:to_s)),
162
+ :requires => Set.new([*options[:requires]].compact.flatten.map(&:to_s)))
163
+ options[:description] ||= plugin.description
164
+ options[:namespace] ||= plugin.path.split('/').last
165
+ tag = TagInfo.new(options)
166
+ @@tags[tag.full_name] = @@tags[tag.name] = tag
167
+ if tag.dynamic
168
+ klass = Class.new
169
+ klass.class_eval do
170
+ include PageHelper
171
+ include Templates
172
+ define_method(:call, &block)
173
+ end
174
+ tag.dynamic = klass
175
+ else
176
+ define_method("TAG #{tag.full_name}", &block)
177
+ end
178
+ end
179
+
180
+ # Configure the tag filter
181
+ # Options:
182
+ # * :enable - Whitelist of tags to enable
183
+ # * :disable - Blacklist of tags to disable
184
+ # * :static - Execute dynamic tags only once
185
+ #
186
+ # Examples:
187
+ # :enable => %w(html:* include) Enables all tags in the html namespace and the include tag.
188
+ def configure(options)
189
+ super
190
+ list = @options[:enable] ? tag_list(*@options[:enable]) : @@tags.keys
191
+ list -= tag_list(*@options[:disable]) if @options[:disable]
192
+ @enabled_tags = list.inject({}) do |hash, name|
193
+ hash[name] = @@tags[name].autoclose
194
+ hash
195
+ end
196
+ @static = options[:static]
197
+ end
198
+
199
+ # Parse nested tags. This method can be called from tag blocks.
200
+ def nested_tags(context, content)
201
+ context[:tag_level] ||= 0
202
+ context[:tag_level] += 1
203
+ return 'Maximum tag nesting exceeded' if context[:tag_level] > MAX_RECURSION
204
+ result = TagSoupParser.new(@enabled_tags, content).parse do |name, attrs, text|
205
+ process_tag(name, attrs, text, context)
206
+ end
207
+ context[:tag_level] -= 1
208
+ result
209
+ end
210
+
211
+ # Execute the subfilter on content. Tags are also evaluated.
212
+ def subfilter(context, content)
213
+ super(context, nested_tags(context, content))
214
+ end
215
+
216
+ # Main filter method
217
+ def filter(context, content)
218
+ @protected_tags = []
219
+ @protection_prefix = "TAG#{object_id}X"
220
+ @protection_suffix = 'X'
221
+ replace_protected_tags(subfilter(context, content))
222
+ end
223
+
224
+ private
225
+
226
+ def tag_list(*list)
227
+ @@tags.select do |name, tag|
228
+ list.include?(tag.name) ||
229
+ list.include?(tag.full_name) ||
230
+ list.include?("#{tag.namespace}:*")
231
+ end.map(&:last)
232
+ end
233
+
234
+ MAX_RECURSION = 100
235
+ BLOCK_ELEMENTS = %w(style script address blockquote div h1 h2 h3 h4 h5 h6 ul p ol pre table hr br)
236
+ BLOCK_ELEMENT_REGEX = /<(#{BLOCK_ELEMENTS.join('|')})/
237
+
238
+ class TagInfo
239
+ attr_accessor :name, :namespace, :limit, :optional, :requires,
240
+ :autoclose, :immediate, :dynamic, :description, :plugin
241
+
242
+ def full_name
243
+ "#{namespace}:#{name}"
244
+ end
245
+
246
+ def initialize(options)
247
+ options.each_pair {|k,v| send("#{k}=", v) }
248
+ end
249
+ end
250
+
251
+ def process_tag(name, attrs, content, context)
252
+ tag = @@tags[name]
253
+ name = tag.full_name
254
+
255
+ tag_counter = context[:tag_counter] ||= {}
256
+ tag_counter[name] ||= 0
257
+ tag_counter[name] += 1
258
+
259
+ raise 'Tag limit exceeded' if tag.limit && tag_counter[name] > tag.limit
260
+
261
+ required = tag.requires.find {|a| !attrs.include?(a) }
262
+ raise %{Attribute "#{required}" is required} if required
263
+
264
+ if !tag.optional.include?('*')
265
+ invalid = attrs.keys.find {|a| !tag.requires.include?(a) && !tag.optional.include?(a) }
266
+ raise %{Attribute "#{invalid}" is invalid} if invalid
267
+ end
268
+
269
+ args = [context, attrs, content]
270
+ args.pop if tag.autoclose
271
+
272
+ content =
273
+ if tag.dynamic
274
+ if @static
275
+ tag.dynamic.new.call(*args).to_s
276
+ else
277
+ args[0] = name
278
+ %{DYNTAG(#{encode64 Marshal.dump(args)})}
279
+ end
280
+ else
281
+ send("TAG #{name}", *args).to_s
282
+ end
283
+
284
+ if tag.immediate
285
+ content
286
+ else
287
+ @protected_tags << content
288
+ "#{@protection_prefix}#{@protected_tags.length-1}#{@protection_suffix}"
289
+ end
290
+ rescue Exception => ex
291
+ Olelo.logger.error ex.message
292
+ "#{name} - #{ex.message}"
293
+ end
294
+
295
+ def replace_protected_tags(content)
296
+ # Protected elements can be nested into each other
297
+ MAX_RECURSION.times do
298
+ break if !content.gsub!(/#{@protection_prefix}(\d+)#{@protection_suffix}/) do
299
+ element = @protected_tags[$1.to_i]
300
+
301
+ # Remove unwanted <p>-tags around block-level-elements
302
+ prefix = $`
303
+ if element =~ BLOCK_ELEMENT_REGEX
304
+ count = prefix.scan('<p>').size - prefix.scan('</p>').size
305
+ count > 0 ? '</p>' + element + '<p>' : element
306
+ else
307
+ element
308
+ end
309
+ end
310
+ content.gsub!(%r{<p>\s*</p>}, '')
311
+ end
312
+ content
313
+ end
314
+ end
315
+
316
+ # Evaluate and replace all dynamic tags on the page
317
+ Application.hook :render, 2000 do |name, xml, layout|
318
+ no_cache = false
319
+ xml.gsub!(/DYNTAG\(([^\)]+)\)/) do
320
+ no_cache = true
321
+ begin
322
+ name, attrs, content = Marshal.load(decode64($1))
323
+ raise 'Invalid dynamic tag' unless Hash === attrs && Tag.tags[name] && Tag.tags[name].dynamic
324
+ begin
325
+ context = Aspects::Context.new(:page => page, :params => params, :request => request, :response => response)
326
+ if content
327
+ raise 'Invalid dynamic tag' unless String === content
328
+ Tag.tags[name].dynamic.new.call(context, attrs, content).to_s
329
+ else
330
+ Tag.tags[name].dynamic.new.call(context, attrs).to_s
331
+ end
332
+ rescue Exception => ex
333
+ Olelo.logger.debug ex
334
+ "#{name} - #{escape_html ex.message}"
335
+ end
336
+ rescue Exception => ex
337
+ Olelo.logger.error ex
338
+ ''
339
+ end
340
+ end
341
+ cache_control(:no_cache => true) if no_cache
342
+ end
343
+
344
+ Filters::Filter.register :tag, Tag, :description => 'Process extension tags'
345
+
346
+ Tag.define :nowiki, :description => 'Disable tag and wikitext filtering' do |context, attrs, content|
347
+ escape_html(content)
348
+ end
349
+
350
+ Tag.define :notags, :description => 'Disable tag processing', :immediate => true do |context, attrs, content|
351
+ content
352
+ end
353
+
@@ -0,0 +1,117 @@
1
+ description 'Math tag for LaTeX rendering'
2
+ dependencies 'utils/image_magick'
3
+
4
+ class MathRenderer
5
+ include Util
6
+ extend Factory
7
+
8
+ def self.instance
9
+ @instance ||= create(Config['math_renderer']) || create('latex')
10
+ end
11
+
12
+ def self.create(name)
13
+ registry[name].new
14
+ rescue Exception => ex
15
+ Olelo.logger.warn "Failed to initialize math renderer #{name}: #{ex.message}"
16
+ end
17
+ end
18
+
19
+ class RitexRenderer < MathRenderer
20
+ def initialize
21
+ require 'ritex'
22
+ end
23
+
24
+ def render(code, display)
25
+ Ritex::Parser.new.parse(code)
26
+ end
27
+
28
+ register 'ritex', RitexRenderer
29
+ end
30
+
31
+ class ItexRenderer < MathRenderer
32
+ def initialize
33
+ `itex2MML --version`
34
+ end
35
+
36
+ def render(code, display)
37
+ Shell.itex2MML(display == 'block' ? '--display' : '--inline').run(code.strip)
38
+ end
39
+
40
+ register 'itex', ItexRenderer
41
+ end
42
+
43
+ class BlahtexMLRenderer < MathRenderer
44
+ def initialize
45
+ `blahtex`
46
+ end
47
+
48
+ def render(code, display)
49
+ content = Shell.blahtex('--mathml').run(code.strip)
50
+ content =~ %r{<mathml>(.*)</mathml>}m
51
+ '<mathml xmlns="http://www.w3.org/1998/Math/MathML" display="' + display + '">' + $1.to_s + '</mathml>'
52
+ end
53
+
54
+ register 'blahtexml', BlahtexMLRenderer
55
+ end
56
+
57
+ class BlahtexImageRenderer < MathRenderer
58
+ include PageHelper
59
+
60
+ def initialize
61
+ `blahtex`
62
+ FileUtils.mkpath(Config['blahtex_directory'])
63
+ end
64
+
65
+ def render(code, display)
66
+ content = Shell.blahtex('--png', '--png-directory', Config['blahtex_directory']).run(code.strip)
67
+ if content =~ %r{<md5>(.*)</md5>}m
68
+ path = build_path "_/blahtex/#{$1}.png"
69
+ %{<img src="#{escape_html path}" alt="#{escape_html code}" class="math #{display}"/>}
70
+ elsif content.include?('error') && content =~ %r{<message>(.*)</message>}
71
+ raise $1
72
+ end
73
+ end
74
+
75
+ register 'blahteximage', BlahtexImageRenderer
76
+ end
77
+
78
+ class GoogleRenderer < MathRenderer
79
+ def render(code, display)
80
+ %{<img src="http://chart.apis.google.com/chart?cht=tx&amp;chl=#{escape code}" alt="#{escape_html code}" class="math #{display}"/>}
81
+ end
82
+
83
+ register 'google', GoogleRenderer
84
+ end
85
+
86
+ class LaTeXRenderer < MathRenderer
87
+ def render(code, display)
88
+ display == 'inline' ? "\\(#{escape_html(code)}\\)" : "\\[#{escape_html(code)}\\]"
89
+ end
90
+
91
+ register 'latex', LaTeXRenderer
92
+ register 'mathjax', LaTeXRenderer
93
+ end
94
+
95
+ Tag.define :math, :optional => :display do |context, attrs, code|
96
+ raise('Limits exceeded') if code.size > 10240
97
+ MathRenderer.instance.render(code, attrs['display'] == 'block' ? 'block' : 'inline')
98
+ end
99
+
100
+ class ::Olelo::Application
101
+ hook :script do
102
+ if page && Config['math_renderer'] == 'mathjax'
103
+ %{<script type="text/javascript" src="http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"/>}
104
+ end
105
+ end
106
+
107
+ get '/_/blahtex/:name', :name => /[\w\.]+/ do
108
+ begin
109
+ response['Content-Type'] = 'image/png'
110
+ file = File.join(Config['blahtex_directory'], params[:name])
111
+ response['Content-Length'] ||= File.stat(file).size.to_s
112
+ halt BlockFile.open(file, 'rb')
113
+ rescue => ex
114
+ ImageMagick.label(ex.message)
115
+ end
116
+ end
117
+ end