- Initial commit.
This commit is contained in:
commit
e085eb821c
|
@ -0,0 +1,20 @@
|
|||
Redmine Wiki External Filter plugin that allows to filter macro blocks
|
||||
arguments using external program and display results on wiki.
|
||||
|
||||
Copyright (C) 2010 Alexander Tsvyashchenko, http://www.ndl.kiev.ua
|
||||
Based on wiki_latex_plugin by Nils Israel <info@nils-israel.net>
|
||||
Based on wiki_graphviz_plugin by tckz <at.tckz@gmail.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
@ -0,0 +1,212 @@
|
|||
Redmine Wiki External Filter Plugin
|
||||
===================================
|
||||
Copyright (C) 2010 Alexander Tsvyashchenko,
|
||||
http://www.ndl.kiev.ua - see COPYRIGHT.txt
|
||||
|
||||
Overview
|
||||
========
|
||||
|
||||
This plugin allows defining macros that process macro argument using
|
||||
external filter program and render its result in Redmine wiki.
|
||||
|
||||
For every filter two macros are defined: <macro> and <macro>_include.
|
||||
The first one directly processes its argument using filter, while the
|
||||
second one assumes its argument is wiki page name, so it reads that wiki page
|
||||
content and processes it using the filter.
|
||||
|
||||
Macros already bundled with current release are listed below, but adding new
|
||||
ones is typically as easy as adding several lines in plugin config file.
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
See [Installing a plugin](http://www.redmine.org/wiki/redmine/Plugins) on
|
||||
Redmine site.
|
||||
|
||||
Additionally, copy wiki_external_filter.yml from config folder of plugin
|
||||
directory to the config folder of your redmine installation.
|
||||
|
||||
After installation it's **strongly recommended** to go to plugin settings and
|
||||
configure caching. Note that RoR file-based caching suggested by default does
|
||||
not implement proper cache expiration: you should either setup a cron task to
|
||||
clean cache or do it manually from time to time.
|
||||
|
||||
To successfully use macros with complex argument expressions, it's necessary
|
||||
to patch core Redmine components as follows:
|
||||
* [Change MACROS_RE](http://www.redmine.org/issues/3061) regexp not to stop
|
||||
too early - in the issue description textile wiki formatter is mentioned,
|
||||
but in fact this change should be done for whatever wiki formatter you use.
|
||||
* [Change arguments parsing](http://www.redmine.org/boards/3/topics/4987#message-9854) - again, should be done for whatever wiki formatter you use.
|
||||
* Additionally, for some of the formatters escaping should be avoided for
|
||||
macro arguments.
|
||||
|
||||
Specific filters installation instructions are below.
|
||||
|
||||
Prefedined Macros
|
||||
=================
|
||||
|
||||
plantuml
|
||||
--------
|
||||
|
||||
[PlantUML](http://plantuml.sourceforge.net/) is a tool to render UML diagrams
|
||||
from their textual representation. It's assumed that it can be invoked via
|
||||
wrapper /usr/bin/plantuml, here's its example content:
|
||||
|
||||
#!/bin/bash
|
||||
/usr/bin/java -Djava.io.tmpdir=/var/tmp -jar /usr/share/plantuml/lib/plantuml.jar ${@}
|
||||
|
||||
Result is rendered as PNG file. SVG support seems to be under development for
|
||||
PlantUML but so far looks like it's still unusable.
|
||||
|
||||
Example of usage:
|
||||
|
||||
{{plantuml(
|
||||
Alice -> Bob: Authentication Request
|
||||
alt successful case
|
||||
Bob -> Alice: Authentication Accepted
|
||||
else some kind of failure
|
||||
Bob -> Alice: Authentication Failure
|
||||
opt
|
||||
loop 1000 times
|
||||
Alice -> Bob: DNS Attack
|
||||
end
|
||||
end
|
||||
else Another type of failure
|
||||
Bob -> Alice: Please repeat
|
||||
end
|
||||
)}}
|
||||
|
||||
graphviz
|
||||
--------
|
||||
[Graphviz](http://www.graphviz.org/) is a tool for graph-like structures
|
||||
visualization. It's assumed that it can be called as /usr/bin/dot.
|
||||
|
||||
Result is rendered as SVG file.
|
||||
|
||||
Example of usage:
|
||||
|
||||
{{graphviz(
|
||||
digraph finite_state_machine {
|
||||
rankdir=LR;
|
||||
size="8,5"
|
||||
node [shape = doublecircle]; LR_0 LR_3 LR_4 LR_8;
|
||||
node [shape = circle];
|
||||
LR_0 -> LR_2 [ label = "SS(B)" ];
|
||||
LR_0 -> LR_1 [ label = "SS(S)" ];
|
||||
LR_1 -> LR_3 [ label = "S($end)" ];
|
||||
LR_2 -> LR_6 [ label = "SS(b)" ];
|
||||
LR_2 -> LR_5 [ label = "SS(a)" ];
|
||||
LR_2 -> LR_4 [ label = "S(A)" ];
|
||||
LR_5 -> LR_7 [ label = "S(b)" ];
|
||||
LR_5 -> LR_5 [ label = "S(a)" ];
|
||||
LR_6 -> LR_6 [ label = "S(b)" ];
|
||||
LR_6 -> LR_5 [ label = "S(a)" ];
|
||||
LR_7 -> LR_8 [ label = "S(b)" ];
|
||||
LR_7 -> LR_5 [ label = "S(a)" ];
|
||||
LR_8 -> LR_6 [ label = "S(b)" ];
|
||||
LR_8 -> LR_5 [ label = "S(a)" ];
|
||||
}
|
||||
)}}
|
||||
|
||||
ritex
|
||||
-----
|
||||
|
||||
Combination of [Ritex: a Ruby WebTeX to MathML converter](http://ritex.rubyforge.org/) and [SVGMath](http://www.grigoriev.ru/svgmath/) that takes WebTeX
|
||||
formula specification as input and produces SVG file as output.
|
||||
|
||||
Both ritex and SVGMath require some patches/wrappers.
|
||||
|
||||
Additionally working installation of xmllint from libxml2 with configured
|
||||
MathML catalog is required: for Gentoo use [this ebuild](http://bugs.gentoo.org/194501).
|
||||
|
||||
Example of usage:
|
||||
|
||||
{{ritex(
|
||||
G(y) = \left\{\array{ 1 - e^{-\lambda x} & \text{ if } y \geq 0 \\ 0 & \text{ if } y < 0 }\right.
|
||||
)}}
|
||||
|
||||
fortune
|
||||
-------
|
||||
|
||||
[Fortune](http://en.wikipedia.org/wiki/Fortune_(Unix)) is a simple program
|
||||
that displays a random message from a database of quotations.
|
||||
|
||||
Not strictly a filter on its own (as it does not require any input), but it
|
||||
plays nice with external filtering approach and is fun to use, hence it's here
|
||||
;-)
|
||||
|
||||
Example of usage:
|
||||
|
||||
{{fortune}}
|
||||
|
||||
Writing new macros
|
||||
==================
|
||||
|
||||
New macros can easily be added via wiki_external_filter.yml config file.
|
||||
|
||||
Wiki external filter uses standard Unix approach for filtering: input is fed
|
||||
to the command via stdin and output is read on stdout. If command return
|
||||
status is zero, content type is assumed to be of content_type specified in
|
||||
config, otherwise it's assumed it's plain error text.
|
||||
|
||||
You can use prolog/epilog config parameters to add standard text before/after
|
||||
actual macro content passed to filter.
|
||||
|
||||
Additionally, cache_seconds parameter specifies the number of seconds command
|
||||
output result should be cached, use zero to disable caching for this
|
||||
particular command.
|
||||
|
||||
Macro argument is de-escaped via CGI.unescapeHTML call prior to being fed to
|
||||
filter.
|
||||
|
||||
The way filter output is visualized is controlled via
|
||||
app/views/wiki_external_filter/macro_*.html.erb files.
|
||||
|
||||
By default all 'image/*' content types are rendered as <img>, 'text/plain'
|
||||
is included in resulting HTML page with escaping, all '*/*xml*'
|
||||
and '*/*html*' content types are included directly into output page without
|
||||
escaping, all other content types are embedded as <object>.
|
||||
|
||||
Current bugs/issues
|
||||
===================
|
||||
|
||||
1. Redmine core requires patching to get things work. In fact, the whole
|
||||
wiki formatting design as of now seems to be quite messy.
|
||||
2. FireFox has broken implementation of SVG rendering: <img> tag for SVG
|
||||
inclusion does not work: see the [related bug](https://bugzilla.mozilla.org/show_bug.cgi?id=276431) dated back to 2004 (sic!).
|
||||
WebKit browsers, on the other hand, are too fragile with <object> SVG
|
||||
embedding, having problems with resulting image size.
|
||||
Considering I do not use FireFox but use WebKit-based browsers - guess
|
||||
which route I've chosen ;-)
|
||||
If you insist on making it work for FireFox (and breaking things for
|
||||
WebKit-based browsers) - copy <object> embedding code in the views
|
||||
templates under the `when /image\/svg\+xml/ then` clause (put it before
|
||||
generic 'image' case).
|
||||
Alternatively, you can use some JavaScript-based trickery to use different
|
||||
embedding ways depending on browser version.
|
||||
Of course, IE does not support SVG at all, but who really uses it these
|
||||
days anyway? ;-)
|
||||
3. For formula support, theoretically ritex alone is sufficient if you have
|
||||
MathML-capable browser, however in practice there are too many issues with
|
||||
this approach: for example Firefox (actually the onlt MathML-capable
|
||||
browser so far, it seems) requires specific DOCTYPE additions that Redmine
|
||||
currently lacks; additionally, Redmine emits text/html, while Firefox
|
||||
expects text/xml in order to parse MathML. Changing content type alone is
|
||||
not sufficient as Redmine HTML output does not pass more strict checks
|
||||
required for XML output. Hence, the double conversion (WebTeX to MathML
|
||||
and then MathML to SVG) is necessary. Once (if ever?) MathML support
|
||||
matures in other browser, possibly this can be revisited.
|
||||
4. SVGs could have been embedded into HTML page directly (thus allowing to use
|
||||
redmine links there) but I'm afraid there are similar problems
|
||||
as with MathML embedding attempts.
|
||||
5. RoR caching support is a mess: no way to expire old files from file-based
|
||||
cache??? Are you joking???
|
||||
|
||||
Additional info
|
||||
===============
|
||||
|
||||
1. Somewhat similar plugins (although with narrower scope) are [graphviz plugin](http://github.com/tckz/redmine-wiki_graphviz_plugin) and [latex plugin](http://www.redmine.org/boards/3/topics/4987).
|
||||
Graphviz functionality is mostly covered by current version of
|
||||
wiki_external_filter. Latex is not, but only due to the fact I do not have
|
||||
latex installed nor currently have a need in that: adding macro that
|
||||
performs latex filtering should be trivial.
|
|
@ -0,0 +1,19 @@
|
|||
|
||||
class WikiExternalFilterController < ApplicationController
|
||||
|
||||
include WikiExternalFilterHelper
|
||||
|
||||
def filter
|
||||
name = params[:name]
|
||||
macro = params[:macro]
|
||||
config = load_config
|
||||
cache_key = self.construct_cache_key(macro, name)
|
||||
content = read_fragment cache_key
|
||||
|
||||
if (content)
|
||||
send_data content, :type => config[macro]['content_type'], :disposition => 'inline'
|
||||
else
|
||||
render_404
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,120 @@
|
|||
require 'digest/sha2'
|
||||
|
||||
module WikiExternalFilterHelper
|
||||
|
||||
def load_config
|
||||
unless @config
|
||||
config_file = "#{RAILS_ROOT}/config/wiki_external_filter.yml"
|
||||
unless File.exists?(config_file)
|
||||
raise "Config not found: #{config_file}"
|
||||
end
|
||||
@config = YAML.load_file(config_file)[RAILS_ENV]
|
||||
end
|
||||
@config
|
||||
end
|
||||
|
||||
def has_macro(macro)
|
||||
config = load_config
|
||||
config.key?(macro)
|
||||
end
|
||||
|
||||
module_function :load_config, :has_macro
|
||||
|
||||
def construct_cache_key(macro, name)
|
||||
['wiki_external_filter', macro, name].join("/")
|
||||
end
|
||||
|
||||
def build(text, macro, info)
|
||||
|
||||
name = Digest::SHA256.hexdigest(text)
|
||||
result = {}
|
||||
content = nil
|
||||
cache_key = nil
|
||||
expires = 0
|
||||
|
||||
if info.key?('cache_seconds')
|
||||
expires = info['cache_seconds']
|
||||
else
|
||||
expires = Setting.plugin_wiki_external_filter['cache_seconds'].to_i
|
||||
end
|
||||
|
||||
if expires > 0
|
||||
cache_key = self.construct_cache_key(macro, name)
|
||||
content = read_fragment cache_key, :expires_in => expires.seconds
|
||||
end
|
||||
|
||||
if content
|
||||
result[:source] = text
|
||||
result[:content] = content
|
||||
result[:content_type] = info['content_type']
|
||||
RAILS_DEFAULT_LOGGER.debug "from cache: #{name}"
|
||||
else
|
||||
result = self.build_forced(text, info)
|
||||
if result[:status]
|
||||
if expires > 0
|
||||
write_fragment cache_key, result[:content], :expires_in => expires.seconds
|
||||
RAILS_DEFAULT_LOGGER.debug "cache saved: #{name}"
|
||||
end
|
||||
else
|
||||
raise "Error applying external filter: #{result[:content]}"
|
||||
end
|
||||
end
|
||||
|
||||
result[:name] = name
|
||||
result[:macro] = macro
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
def build_forced(text, info)
|
||||
|
||||
result = {}
|
||||
|
||||
RAILS_DEFAULT_LOGGER.debug "executing command: #{info['command']}"
|
||||
|
||||
content = IO.popen(info['command'], 'r+b') { |f|
|
||||
f.write info[:prolog] if info.key?(:prolog)
|
||||
f.write CGI.unescapeHTML(text)
|
||||
f.write info[:epilog] if info.key?(:epilog)
|
||||
f.close_write
|
||||
f.read
|
||||
}
|
||||
|
||||
RAILS_DEFAULT_LOGGER.info("child status: sig=#{$?.termsig}, exit=#{$?.exitstatus}")
|
||||
|
||||
result[:content] = content
|
||||
result[:content_type] = info['content_type']
|
||||
result[:source] = text
|
||||
result[:status] = $?.exitstatus == 0
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
def render_tag(result)
|
||||
render_to_string :template => 'wiki_external_filter/macro_inline', :layout => false, :locals => result
|
||||
end
|
||||
|
||||
def render_block(result, wiki_name)
|
||||
result = result.dup
|
||||
result[:wiki_name] = wiki_name
|
||||
render_to_string :template => 'wiki_external_filter/macro_block', :layout => false, :locals => result
|
||||
end
|
||||
|
||||
class Macro
|
||||
def initialize(view, source, macro, info)
|
||||
@view = view
|
||||
@view.controller.extend(WikiExternalFilterHelper)
|
||||
source.gsub!(/<br \/>/, "")
|
||||
source.gsub!(/<\/?p>/, "")
|
||||
@result = @view.controller.build(source, macro, info)
|
||||
end
|
||||
|
||||
def render()
|
||||
@view.controller.render_tag(@result)
|
||||
end
|
||||
|
||||
def render_block(wiki_name)
|
||||
@view.controller.render_block(@result, wiki_name)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,21 @@
|
|||
<fieldset>
|
||||
<legend>Caching Settings</legend>
|
||||
<p>
|
||||
<label>Cache expiration time</label><%= text_field_tag 'settings[cache_seconds]', @settings['cache_seconds'] %> seconds
|
||||
</p>
|
||||
<p>
|
||||
Enter zero to disable caching. Make sure to configure fragment_cache_store if you use the value > 0.
|
||||
</p>
|
||||
<p>
|
||||
Configuration example for ActiveSupport::Cache::FileStore, config/environments/production.rb file:<br/>
|
||||
<br/>
|
||||
...<br/>
|
||||
config.action_controller.fragment_cache_store = :file_store, "#{RAILS_ROOT}/cache"<br/>
|
||||
...
|
||||
</p>
|
||||
<p>
|
||||
Current cache settings are:<br/>
|
||||
ActionController::Base.cache_configured? = <%= h ActionController::Base.cache_configured? ? "true" : "false" %><br/>
|
||||
ActionController::Base.fragment_cache_store = <%= h ActionController::Base.fragment_cache_store.inspect %>
|
||||
</p>
|
||||
</fieldset>
|
|
@ -0,0 +1,29 @@
|
|||
<div class='externalfilterblock'>
|
||||
<%
|
||||
case content_type
|
||||
when /image\// then
|
||||
%>
|
||||
<img src='<%= url_for(:controller => 'wiki_external_filter', :action => 'filter', :macro => macro, :name => name) %>' alt="<%= h source %>" />
|
||||
<%
|
||||
when /text\/plain/ then
|
||||
%>
|
||||
<pre><%= h content %></pre>
|
||||
<%
|
||||
when /\/.*(x|ht)ml/ then
|
||||
%>
|
||||
<div><%= content %></div>
|
||||
<%
|
||||
else
|
||||
%>
|
||||
<object class='externalfilterinline' name='<%= name %>' data='<%= url_for(:controller => 'wiki_external_filter', :action => 'filter', :macro => macro, :name => name) %>' type='<%= content_type %>' title="<%= h source %>">
|
||||
<embed name='<%= name %>-2' src='<%= url_for(:controller => 'wiki_external_filter', :action => 'filter', :macro => macro, :name => name) %>' type='<%= content_type %>' title="<%= h source %>" />
|
||||
</object>
|
||||
<%
|
||||
end
|
||||
%>
|
||||
<br/>
|
||||
<span class='wiki_page'>Goto source: [[<%= wiki_name %>]]</span>
|
||||
</div>
|
||||
<% content_for :header_tags do %>
|
||||
<%= stylesheet_link_tag "wiki_external_filter.css", :plugin => "wiki_external_filter", :media => :all %>
|
||||
<% end %>
|
|
@ -0,0 +1,25 @@
|
|||
<%
|
||||
case content_type
|
||||
when /image\// then
|
||||
%>
|
||||
<img class='externalfilterinline' src='<%= url_for(:controller => 'wiki_external_filter', :action => 'filter', :macro => macro, :name => name) %>' alt="<%= h source %>" />
|
||||
<%
|
||||
when /text\/plain/ then
|
||||
%>
|
||||
<pre class='externalfilterinline'><%= h content %></pre>
|
||||
<%
|
||||
when /\/.*(x|ht)ml/ then
|
||||
%>
|
||||
<div class='externalfilterinline'><%= content %></div>
|
||||
<%
|
||||
else
|
||||
%>
|
||||
<object class='externalfilterinline' name='<%= name %>' data='<%= url_for(:controller => 'wiki_external_filter', :action => 'filter', :macro => macro, :name => name) %>' type='<%= content_type %>' title="<%= h source %>">
|
||||
<embed name='<%= name %>-2' src='<%= url_for(:controller => 'wiki_external_filter', :action => 'filter', :macro => macro, :name => name) %>' type='<%= content_type %>' title="<%= h source %>" />
|
||||
</object>
|
||||
<%
|
||||
end
|
||||
%>
|
||||
<% content_for :header_tags do %>
|
||||
<%= stylesheet_link_tag "wiki_external_filter.css", :plugin => "wiki_external_filter", :media => :all %>
|
||||
<% end %>
|
|
@ -0,0 +1,18 @@
|
|||
.externalfilterinline{
|
||||
position: relative;
|
||||
bottom: -0.2em;
|
||||
}
|
||||
|
||||
.externalfilterblock img{
|
||||
display: block;
|
||||
}
|
||||
|
||||
.externalfilterblock .wiki_page{
|
||||
font-size: 80%;
|
||||
border-top: 1px solid #ccc;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.externalfilterblock .wiki_page a{
|
||||
color: #999;
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
development: &development
|
||||
plantuml:
|
||||
description: "Constructs UML diagram image from its textual description in PlantUML language, see http://plantuml.sourceforge.net"
|
||||
command: "/usr/bin/plantuml -pipe"
|
||||
content_type: "image/png"
|
||||
prolog: "@startuml"
|
||||
epilog: "@enduml"
|
||||
graphviz:
|
||||
description: "Constructs graph image from its textual description in DOT language, see http://www.graphviz.org"
|
||||
command: "/usr/bin/dot -Tsvg"
|
||||
content_type: "image/svg+xml"
|
||||
ritex:
|
||||
description: "Converts WebTeX expression to MathML, see http://ritex.rubyforge.org/"
|
||||
command: "(echo '<!DOCTYPE math PUBLIC \"-//W3C//DTD MathML 2.0//EN\" \"http://www.w3.org/Math/DTD/mathml2/mathml2.dtd\">'; /usr/bin/ritex) | xmllint --noent --nonet --catalogs --loaddtd - | /usr/bin/math2svg"
|
||||
content_type: "image/svg+xml"
|
||||
# For MathML-compliant browsers and when Redmine is fully XML-compliant.
|
||||
# ritex:
|
||||
# description: "Converts WebTeX expression to MathML, see http://ritex.rubyforge.org/"
|
||||
# command: "/usr/bin/ritex"
|
||||
# content_type: "application/xhtml+xml"
|
||||
fortune:
|
||||
description: "Prints a random, hopefully interesting, adage, see http://en.wikipedia.org/wiki/Fortune_(Unix)"
|
||||
command: "/usr/bin/fortune"
|
||||
cache_seconds: 0
|
||||
content_type: "text/plain"
|
||||
|
||||
test:
|
||||
type: mock
|
||||
|
||||
production:
|
||||
<<: *development
|
|
@ -0,0 +1,43 @@
|
|||
require 'redmine'
|
||||
|
||||
RAILS_DEFAULT_LOGGER.info 'Starting wiki_external_filter plugin for Redmine'
|
||||
|
||||
Redmine::Plugin.register :wiki_external_filter do
|
||||
name 'Wiki External Filter Plugin'
|
||||
author 'Alexander Tsvyashchenko'
|
||||
description 'Processes given text using external command and renders its output'
|
||||
author_url 'http://www.ndl.kiev.ua'
|
||||
version '0.0.1'
|
||||
settings :default => {'cache_seconds' => '0'}, :partial => 'wiki_external_filter/settings'
|
||||
|
||||
config = WikiExternalFilterHelper.load_config
|
||||
RAILS_DEFAULT_LOGGER.debug "Config: #{config.inspect}"
|
||||
|
||||
config.keys.each do |name|
|
||||
RAILS_DEFAULT_LOGGER.info "Registering #{name} macro with wiki_external_filter"
|
||||
Redmine::WikiFormatting::Macros.register do
|
||||
info = config[name]
|
||||
desc info['description']
|
||||
macro name do |wiki_content_obj, args|
|
||||
m = WikiExternalFilterHelper::Macro.new(self, args.to_s, name, info)
|
||||
m.render
|
||||
end
|
||||
|
||||
# code borrowed from wiki latex plugin
|
||||
# code borrowed from wiki template macro
|
||||
desc info['description']
|
||||
macro (name + "_include").to_sym do |obj, args|
|
||||
page = Wiki.find_page(args.to_s, :project => @project)
|
||||
raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
|
||||
|
||||
@included_wiki_pages ||= []
|
||||
raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title)
|
||||
@included_wiki_pages << page.title
|
||||
m = WikiExternalFilterHelper::Macro.new(self, page.content.text, name, info)
|
||||
@included_wiki_pages.pop
|
||||
m.render_block(args.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
Reference in New Issue