d7832ba262
Added the ability to rename existing pages. [[!redirects Some Page Name]] redirects Wikilinks [[Some Page Name]] to the current page (assuming "Some Page Name" does not exist). Real pages trump redirects (though this may change, depending on user feedback).
212 lines
6.3 KiB
Ruby
212 lines
6.3 KiB
Ruby
require 'cgi'
|
|
require 'chunks/engines'
|
|
require 'chunks/category'
|
|
require_dependency 'chunks/include'
|
|
require_dependency 'chunks/redirect'
|
|
require_dependency 'chunks/wiki'
|
|
require_dependency 'chunks/literal'
|
|
require 'chunks/nowiki'
|
|
require 'sanitizer'
|
|
|
|
|
|
# Wiki content is just a string that can process itself with a chain of
|
|
# actions. The actions can modify wiki content so that certain parts of
|
|
# it are protected from being rendered by later actions.
|
|
#
|
|
# When wiki content is rendered, it can be interrogated to find out
|
|
# which chunks were rendered. This means things like categories, wiki
|
|
# links, can be determined.
|
|
#
|
|
# Exactly how wiki content is rendered is determined by a number of
|
|
# settings that are optionally passed in to a constructor. The current
|
|
# options are:
|
|
# * :engine
|
|
# => The structural markup engine to use (Textile, Markdown, RDoc)
|
|
# * :engine_opts
|
|
# => A list of options to pass to the markup engines (safe modes, etc)
|
|
# * :pre_engine_actions
|
|
# => A list of render actions or chunks to be processed before the
|
|
# markup engine is applied. By default this is:
|
|
# Category, Include, URIChunk, WikiChunk::Link, WikiChunk::Word
|
|
# * :post_engine_actions
|
|
# => A list of render actions or chunks to apply after the markup
|
|
# engine. By default these are:
|
|
# Literal::Pre, Literal::Tags
|
|
# * :mode
|
|
# => How should the content be rendered? For normal display (show),
|
|
# publishing (:publish) or export (:export)?
|
|
|
|
module ChunkManager
|
|
attr_reader :chunks_by_type, :chunks_by_id, :chunks, :chunk_id
|
|
|
|
ACTIVE_CHUNKS = [ NoWiki, Category, Redirect, WikiChunk::Link,
|
|
WikiChunk::Word ]
|
|
|
|
HIDE_CHUNKS = [ Literal::Pre, Literal::Tags, Literal::Math ]
|
|
|
|
MASK_RE = {
|
|
ACTIVE_CHUNKS => Chunk::Abstract.mask_re(ACTIVE_CHUNKS),
|
|
HIDE_CHUNKS => Chunk::Abstract.mask_re(HIDE_CHUNKS)
|
|
}
|
|
|
|
def init_chunk_manager
|
|
@chunks_by_type = Hash.new
|
|
Chunk::Abstract::derivatives.each{|chunk_type|
|
|
@chunks_by_type[chunk_type] = Array.new
|
|
}
|
|
@chunks_by_id = Hash.new
|
|
@chunks = []
|
|
@chunk_id = 0
|
|
end
|
|
|
|
def add_chunk(c)
|
|
@chunks_by_type[c.class] << c
|
|
@chunks_by_id[c.object_id] = c
|
|
@chunks << c
|
|
@chunk_id += 1
|
|
end
|
|
|
|
def delete_chunk(c)
|
|
@chunks_by_type[c.class].delete(c)
|
|
@chunks_by_id.delete(c.object_id)
|
|
@chunks.delete(c)
|
|
end
|
|
|
|
def merge_chunks(other)
|
|
other.chunks.each{|c| add_chunk(c)}
|
|
end
|
|
|
|
def scan_chunkid(text)
|
|
text.scan(MASK_RE[ACTIVE_CHUNKS]){|a| yield a[0] }
|
|
end
|
|
|
|
def find_chunks(chunk_type)
|
|
@chunks.select { |chunk| chunk.kind_of?(chunk_type) and chunk.rendered? }
|
|
end
|
|
|
|
end
|
|
|
|
# A simplified version of WikiContent. Useful to avoid recursion problems in
|
|
# WikiContent.new
|
|
class WikiContentStub < String
|
|
|
|
attr_reader :web, :options
|
|
include ChunkManager
|
|
|
|
def initialize(content, web, options)
|
|
super(content)
|
|
@web = web
|
|
@options = options
|
|
init_chunk_manager
|
|
end
|
|
|
|
# Detects the mask strings contained in the text of chunks of type chunk_types
|
|
# and yields the corresponding chunk ids
|
|
# example: content = "chunk123categorychunk <pre>chunk456categorychunk</pre>"
|
|
# inside_chunks(Literal::Pre) ==> yield 456
|
|
def inside_chunks(chunk_types)
|
|
chunk_types.each{|chunk_type| chunk_type.apply_to(self) }
|
|
|
|
chunk_types.each{|chunk_type| @chunks_by_type[chunk_type].each{|hide_chunk|
|
|
scan_chunkid(hide_chunk.text){|id| yield id }
|
|
}
|
|
}
|
|
end
|
|
end
|
|
|
|
class WikiContent < String
|
|
|
|
include ChunkManager
|
|
include Sanitizer
|
|
|
|
DEFAULT_OPTS = {
|
|
:active_chunks => ACTIVE_CHUNKS,
|
|
:hide_chunks => HIDE_CHUNKS,
|
|
:engine => Engines::MarkdownMML,
|
|
:engine_opts => [],
|
|
:mode => :show
|
|
}.freeze
|
|
|
|
attr_reader :web, :options, :revision, :not_rendered, :pre_rendered
|
|
|
|
# Create a new wiki content string from the given one.
|
|
# The options are explained at the top of this file.
|
|
def initialize(revision, url_generator, options = {})
|
|
@revision = revision
|
|
@url_generator = url_generator
|
|
@web = @revision.page.web
|
|
|
|
@options = DEFAULT_OPTS.dup.merge(options)
|
|
@options[:engine] = Engines::MAP[@web.markup]
|
|
@options[:engine_opts] = [:filter_html, :filter_styles] if @web.safe_mode?
|
|
@options[:active_chunks] = (ACTIVE_CHUNKS - [WikiChunk::Word] ) if @web.brackets_only?
|
|
@options[:hide_chunks] = (HIDE_CHUNKS - [Literal::Math] ) unless
|
|
[Engines::MarkdownMML, Engines::MarkdownPNG].include?(@options[:engine])
|
|
|
|
@not_rendered = @pre_rendered = nil
|
|
|
|
super(@revision.content)
|
|
init_chunk_manager
|
|
build_chunks
|
|
@not_rendered = String.new(self)
|
|
end
|
|
|
|
# Call @web.page_link using current options.
|
|
def page_link(web_name, name, text, link_type)
|
|
web = Web.find_by_name(web_name) || Web.find_by_address(web_name) || @web
|
|
@options[:link_type] = (link_type || :show)
|
|
@url_generator.make_link(name, web, text, @options)
|
|
end
|
|
|
|
def build_chunks
|
|
# create and mask Includes and "active_chunks" chunks
|
|
NoWiki.apply_to(self) if @options[:active_chunks].include?(NoWiki)
|
|
Include.apply_to(self)
|
|
@options[:active_chunks].each{|chunk_type| chunk_type.apply_to(self) unless chunk_type == NoWiki}
|
|
|
|
# Handle hiding contexts like "pre" and "code" etc..
|
|
# The markup (textile, rdoc etc) can produce such contexts with its own syntax.
|
|
# To reveal them, we work on a copy of the content.
|
|
# The copy is rendered and used to detect the chunks that are inside protecting context
|
|
# These chunks are reverted on the original content string.
|
|
|
|
copy = WikiContentStub.new(self, @web, @options)
|
|
@options[:engine].apply_to(copy)
|
|
|
|
copy.inside_chunks(@options[:hide_chunks]) do |id|
|
|
@chunks_by_id[id.to_i].revert
|
|
end
|
|
end
|
|
|
|
def pre_render!
|
|
unless @pre_rendered
|
|
@chunks_by_type[Include].each{|chunk| chunk.unmask }
|
|
@pre_rendered = String.new(self)
|
|
end
|
|
@pre_rendered
|
|
end
|
|
|
|
def render!
|
|
pre_render!
|
|
@options[:engine].apply_to(self)
|
|
# unmask in one go. $~[1] is the chunk id
|
|
gsub!(MASK_RE[ACTIVE_CHUNKS]) do
|
|
chunk = @chunks_by_id[$~[1].to_i]
|
|
if chunk.nil?
|
|
# if we match a chunkmask that existed in the original content string
|
|
# just keep it as it is
|
|
$~[0]
|
|
else
|
|
chunk.unmask_text
|
|
end
|
|
end
|
|
self.replace xhtml_sanitize(self)
|
|
end
|
|
|
|
def page_name
|
|
@revision.page.name
|
|
end
|
|
|
|
end
|
|
|