- Initial commit.

This commit is contained in:
Alexander Tsvyashchenko 2010-01-11 14:38:44 +02:00
commit e085eb821c
11 changed files with 540 additions and 0 deletions

20
COPYRIGHT.txt Normal file
View File

@ -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.

212
README.txt Normal file
View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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 &gt; 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, &quot;#{RAILS_ROOT}/cache&quot;<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>

View File

@ -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 %>

View File

@ -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 %>

View File

@ -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;
}

View File

@ -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

43
init.rb Normal file
View File

@ -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

2
routes.rb Normal file
View File

@ -0,0 +1,2 @@
connect 'wiki_external_filter/:macro/:name', :controller => 'wiki_external_filter', :action => 'filter', :macro => /\S+/