app/models/wiki_content.rb in instiki-0.9.2 vs app/models/wiki_content.rb in instiki-0.10.0

- old
+ new

@@ -1,105 +1,207 @@ -require 'cgi' -require 'chunks/engines' -require 'chunks/category' -require 'chunks/include' -require 'chunks/wiki' -require 'chunks/literal' -require 'chunks/uri' -require 'chunks/nowiki' - -# 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 (:display), -# publishing (:publish) or export (:export)? -# -# AUTHOR: Mark Reid <mark @ threewordslong . com> -# CREATED: 15th May 2004 -# UPDATED: 22nd May 2004 -class WikiContent < String - - PRE_ENGINE_ACTIONS = [ NoWiki, Category, Include, URIChunk, WikiChunk::Link, WikiChunk::Word ] - POST_ENGINE_ACTIONS = [ Literal::Pre, Literal::Tags ] - DEFAULT_OPTS = { - :pre_engine_actions => PRE_ENGINE_ACTIONS, - :post_engine_actions => POST_ENGINE_ACTIONS, - :engine => Engines::Textile, - :engine_opts => [], - :mode => [:display] - } - - attr_reader :web, :options, :rendered - - # Create a new wiki content string from the given one. - # The options are explained at the top of this file. - def initialize(revision, options = {}) - @revision = revision - @web = @revision.page.web - - # Deep copy of DEFAULT_OPTS to ensure that changes to PRE/POST_ENGINE_ACTIONS stay local - @options = Marshal.load(Marshal.dump(DEFAULT_OPTS)).update(options) - @options[:engine] = Engines::MAP[@web.markup] || Engines::Textile - @options[:engine_opts] = (@web.safe_mode ? [:filter_html, :filter_styles] : []) - - @options[:pre_engine_actions].delete(WikiChunk::Word) if @web.brackets_only - - super(@revision.content) - - begin - render!(@options[:pre_engine_actions] + [@options[:engine]] + @options[:post_engine_actions]) - rescue => e - @rendered = e.message - end - end - - # Call @web.page_link using current options. - def page_link(name, text) - @web.make_link(name, text, @options) - end - - # Find all the chunks of the given types - def find_chunks(chunk_type) - rendered.select { |chunk| chunk.kind_of?(chunk_type) } - end - - # Render this content using the specified actions. - def render!(chunk_types) - @chunks = [] - chunk_types.each { |chunk_type| self.apply_type!(chunk_type) } - - @rendered = @chunks.map { |chunk| chunk.unmask(self) }.compact - (@chunks - @rendered).each { |chunk| chunk.revert(self) } - end - - # Find all the chunks of the given type in this content - # Each time the type's pattern is matched, create a new - # chunk for it, and replace the occurance of the chunk - # in this content with its mask. - def apply_type!(chunk_type) - self.gsub!( chunk_type.pattern ) do |match| - @chunks << chunk_type.new($~) - @chunks.last.mask(self) - end - end -end \ No newline at end of file +require 'cgi' +require 'chunks/engines' +require 'chunks/category' +require 'chunks/include' +require 'chunks/wiki' +require 'chunks/literal' +require 'chunks/uri' +require 'chunks/nowiki' + +# 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)? +# +# AUTHOR: Mark Reid <mark @ threewordslong . com> +# CREATED: 15th May 2004 +# UPDATED: 22nd May 2004 + +module ChunkManager + attr_reader :chunks_by_type, :chunks_by_id, :chunks, :chunk_id + + ACTIVE_CHUNKS = [ NoWiki, Category, WikiChunk::Link, URIChunk, LocalURIChunk, + WikiChunk::Word ] + + HIDE_CHUNKS = [ Literal::Pre, Literal::Tags ] + + 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.id] = c + @chunks << c + @chunk_id += 1 + end + + def delete_chunk(c) + @chunks_by_type[c.class].delete(c) + @chunks_by_id.delete(c.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 + + # for testing and WikiContentStub; we need a page_id even if we have no page + def page_id + 0 + end + +end + +# A simplified version of WikiContent. Useful to avoid recursion problems in +# WikiContent.new +class WikiContentStub < String + attr_reader :options + include ChunkManager + def initialize(content, options) + super(content) + @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 + + DEFAULT_OPTS = { + :active_chunks => ACTIVE_CHUNKS, + :engine => Engines::Textile, + :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, options = {}) + @revision = revision + @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 + + super(@revision.content) + init_chunk_manager + build_chunks + @not_rendered = String.new(self) + end + + # Call @web.page_link using current options. + def page_link(name, text, link_type) + @options[:link_type] = (link_type || :show) + @web.make_link(name, text, @options) + end + + def build_chunks + # create and mask Includes and "active_chunks" chunks + Include.apply_to(self) + @options[:active_chunks].each{|chunk_type| chunk_type.apply_to(self)} + + # 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, @options) + @options[:engine].apply_to(copy) + + copy.inside_chunks(HIDE_CHUNKS) do |id| + @chunks_by_id[id].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]){ + if chunk = @chunks_by_id[$~[1]] + chunk.unmask_text + # if we match a chunkmask that existed in the original content string + # just keep it as it is + else + $~[0] + end} + self + end + + def page_name + @revision.page.name + end + + def page_id + @revision.page.id + end + +end