From: Gavin Kistner Date: 2005-06-23T11:53:08+09:00 Subject: MiniQuiz : Renesting Nodes (OWLScratch) --Apple-Mail-3--545942923 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=US-ASCII; delsp=yes; format=flowed return unless bored? #MiniQuiz = do my 'work' for me. I'm writing a library (OWLScratch) for converting wiki markup into HTML. (The OWL part of the name is because it's derived from the markup used by OpenWiki, hence "OpenWiki Language"; the Scratch is because it's my own half-implemented flavor.) It's not nearly ready yet, but the core concepts are working. So far, it tokenizes the document into a series of hierarchically-nested nodes. If I wanted XML, I could make up my own schema and be done. But I want HTML. In OWLScratch, the following represents a nested list: * List item 1 * List item 2 * List item 2.1 * List item 2.2 * List item 3 * List item 3.1 * List item 3.1.1 Right now, that tokenizes into one node per line: List item 1 List item 2 List item 2.1 List item 2.2 List item 3 List item 3.1 List item 3.1.1 The challenge is that I need to be able to spin through the list and (should be possible in one pass) properly nest those as the HTML requires: I know this should be not-hard, but I just played a full game of Ultimate Frisbee, and I thought this might appeal to someone else. The full library (so far, v0.0.1 or so) is attached, but I can (and probably will have to) extend the Tag class to support DOM-like properties such as previous_sibling and next_sibling. And make reparenting a node properly remove it from the previous parent's @child_nodes collection. I'll speak more about OWLScratch in a few days, when I hope to have it ready for a really preliminary release. Oh, for extra credit - think about nested list types, as seen here: http://openwiki.com/ow.asp?HelpOnFormatting#h9 (My lists do not (currently) allow content of a single item to be manually wrapped onto multiple lines.) --Apple-Mail-3--545942923 Content-Transfer-Encoding: quoted-printable Content-Type: text/x-ruby-script; x-unix-mode=0644; name="OWLScratch.rb" Content-Disposition: attachment; filename=OWLScratch.rb require 'strscan'=0D =0D #todo - override html conversion per factory=0D #todo - pass after scanning to properly reparent lists.=0D #todo - but I also like the name OWLScribble...no bird-tie in, but more = fun=0D class OWLScratch=0D =0D class TagFactory=0D def self.by_type=0D @by_type ||=3D {}=0D end=0D =0D attr_reader :tag_name, :open_match, :close_match, = :open_requires_bol, :close_requires_bol, :autoclose, :type, = :allowed_type=0D def initialize( tag_name, options=3D{} )=0D @tag_name =3D tag_name=0D [ :open_match, :close_match,=0D :open_requires_bol, :close_requires_bol,=0D :allowed_type, :autoclose,=0D :text, :attrs, :setup, :type ].each{ |k|=0D self.instance_variable_set( :"@#{k}", = options[ k ] )=0D }=0D ( self.class.by_type[ @type ] ||=3D [] ) << self = if @type=0D end=0D =0D def match( ss )=0D return nil unless ( !@open_requires_bol || = ss.bol? ) && ss.scan( @open_match )=0D tag =3D Tag.new( @tag_name, self )=0D @setup.call( tag, ss ) if @setup=0D tag=0D end=0D end=0D =0D class Tag=0D attr_accessor :tag_name, :child_nodes, :attrs, = :parent_node=0D =0D def initialize( tag_name, owning_factory )=0D @tag_name =3D tag_name=0D @owning_factory =3D owning_factory=0D @child_nodes =3D [ ]=0D @attrs =3D { }=0D end=0D =0D def type=0D @owning_factory.type=0D end=0D =0D def close_match=0D @owning_factory.close_match=0D end =0D =0D def close_requires_bol?=0D @owning_factory.close_requires_bol=0D end=0D =0D def autoclose?=0D @owning_factory.autoclose=0D end=0D =0D def allowed_type=0D @owning_factory.allowed_type=0D end=0D =0D def append_child( node )=0D puts "#{self.inspect}.append_child( = #{node.inspect} )" if $DEBUG=0D @child_nodes << node=0D node.parent_node =3D self=0D node=0D end=0D =0D def << ( str )=0D last_child =3D @child_nodes.last=0D if TextNode =3D=3D=3D last_child=0D last_child << str=0D else=0D append_child( TextNode.new( str ) )=0D end=0D end=0D =0D def to_html=0D out =3D "<#{@tag_name}"=0D @attrs.each{ |k,v| out << " = #{k}=3D\"#{v.to_s.gsub( '""', '"' )}\"" }=0D if @child_nodes.empty?=0D out << ' />'=0D else=0D out << '>'=0D @child_nodes.each{ |node|=0D out << node.to_html=0D }=0D out << ""=0D end=0D out << "\n" unless @owning_factory.type =3D=3D = :inline || @owning_factory.type =3D=3D :td=0D out=0D end=0D =0D def inspect=0D "<#{@tag_name}:#{@type}:#{@allowed_type}>"=0D end=0D end=0D =0D class TextNode=0D attr_accessor :parent_node=0D =0D def initialize( node_value=3D'' )=0D @node_value =3D node_value=0D end=0D =0D def << ( str )=0D @node_value << str=0D end=0D =0D def inspect=0D ""=0D end=0D =0D def to_html=0D @node_value.htmlsafe!=0D end=0D =0D end=0D =0D TagFactory.new( :wiki_command,=0D :type =3D> :block,=0D :open_match =3D> = /##(TableOfContents|DEPRECATED|BALETED|IncludePage\(\s*(.+)\s*\))##/, = :open_requires_bol =3D> true,=0D :setup =3D> lambda{ |tag, ss|=0D tag.attrs[ :do ] =3D ss[1][ /[a-z]+/i ]=0D tag.attrs[ :param ] =3D ss[2] if ss[2]=0D },=0D :autoclose =3D> true=0D )=0D TagFactory.new( :heading,=0D :type =3D> :block,=0D :open_match =3D> /(=3D{1,6}) +(.+) +\1[ \t]*\n/, = :open_requires_bol =3D> true,=0D :setup =3D> lambda{ |tag, ss|=0D tag.attrs[ :level ] =3D ss[1].length=0D tag << ss[2]=0D },=0D :autoclose =3D> true=0D )=0D TagFactory.new( :hr,=0D :type =3D> :block,=0D :open_match =3D> /-{4,} *\n/, :open_requires_bol =3D> = true,=0D :autoclose =3D> true=0D )=0D TagFactory.new( :bullet,=0D :type =3D> :block,=0D :open_match =3D> /(( )+) ?\* /, :open_requires_bol =3D> = true,=0D :close_match =3D> /\n/,=0D :setup =3D> lambda{ |tag, ss| tag.attrs[ :level ] =3D = ss[ 1 ].length / 2 },=0D :allowed_type =3D> :inline=0D )=0D TagFactory.new( :numberlist,=0D :type =3D> :block,=0D :open_match =3D> /(( )+) ?\d+\. /, :open_requires_bol = =3D> true,=0D :close_match =3D> /\n/,=0D :setup =3D> lambda{ |tag,ss| tag.attrs[ :level ] =3D = ss[1].length / 2 },=0D :allowed_type =3D> :inline=0D )=0D TagFactory.new( :alphalist,=0D :type =3D> :block,=0D :open_match =3D> /(( )+) ?[a-z]\. /, :open_requires_bol = =3D> true,=0D :close_match =3D> /\n/,=0D :setup =3D> lambda{ |tag,ss| tag.attrs[ :level ] =3D = ss[1].length / 2 },=0D :allowed_type =3D> :inline=0D )=0D TagFactory.new( :dl,=0D :type =3D> :block,=0D :open_match =3D> /(?=3D( |\t)+; .+ : )/, = :open_requires_bol =3D> true,=0D :close_match =3D> /(?!( |\t)+; .+ : )/, = :close_requires_bol =3D> true,=0D :allowed_type =3D> :deflist=0D )=0D TagFactory.new( :pre,=0D :type =3D> :block,=0D :open_match =3D> /^([ \t]*)\{\{\{\n(.+?)\n\1\}\}\}\n/m, = :open_requires_bol =3D> true,=0D :close_match =3D> /\n/,=0D :setup =3D> lambda{ |tag,ss| tag << ss[2].gsub( = /^#{ss[1]}/, '' ) },=0D :autoclose =3D> true=0D )=0D TagFactory.new( :table,=0D :type =3D> :block,=0D :open_match =3D> /(?=3D\|\|)/, :open_requires_bol =3D> = true,=0D :close_match =3D> /(?=3D[^|])/, :close_requires_bol =3D> = true,=0D :allowed_type =3D> :table=0D )=0D TagFactory.new( :p,=0D :type =3D> :block,=0D :open_match =3D> /(( )+) ?: /, :open_requires_bol =3D> = true,=0D :close_match =3D> /\n/,=0D :setup =3D> lambda{ |tag,ss| tag.attrs[ :indent ] =3D = ss[1].length / 2 },=0D :allowed_type =3D> :inline=0D )=0D # The paragraph is the catch-all for blocks;=0D # it must appear after all other block factories=0D TagFactory.new( :p,=0D :type =3D> :block,=0D :open_match =3D> /(?=3D\S)/, :open_requires_bol =3D> = true,=0D :close_match =3D> /\n\n/,=0D :allowed_type =3D> :inline=0D )=0D =0D =0D TagFactory.new( :tr,=0D :type =3D> :table,=0D :open_match =3D> /(?=3D\|\|)/, :open_requires_bol =3D> = true,=0D :close_match =3D> /\|\|[ \t]*\n/,=0D :allowed_type =3D> :td=0D )=0D TagFactory.new( :td,=0D :type =3D> :td,=0D :open_match =3D> /((?:\|\|)+)\s*/,=0D :close_match =3D> /(?=3D\s*\|\|)/,=0D :setup =3D> lambda{ |tag,ss|=0D colspan =3D ss[1].length / 2=0D tag.attrs[ :colspan ] =3D colspan unless = colspan < 2=0D },=0D :allowed_type =3D> :inline=0D )=0D =0D =0D TagFactory.new( :dt,=0D :type =3D> :deflist,=0D :open_match =3D> /( |\t)+; /, :open_requires_bol =3D> = true,=0D :close_match =3D> / : /,=0D :allowed_type =3D> :inline=0D )=0D TagFactory.new( :dd,=0D :type =3D> :deflist,=0D :open_match =3D> /(?=3D.)/,=0D :close_match =3D> /\n/,=0D :allowed_type =3D> :inline=0D )=0D =0D =0D TagFactory.new( :b,=0D :type =3D> :inline,=0D :open_match =3D> /\*\*/, :close_match =3D> /\*\*/,=0D :allowed_type =3D> :inline=0D )=0D TagFactory.new( :i,=0D :type =3D> :inline,=0D :open_match =3D> /\/\//, :close_match =3D> /\/\//,=0D :allowed_type =3D> :inline=0D )=0D TagFactory.new( :strike,=0D :type =3D> :inline,=0D :open_match =3D> /--/, :close_match =3D> /--/,=0D :allowed_type =3D> :inline=0D )=0D TagFactory.new( :sup,=0D :type =3D> :inline,=0D :open_match =3D> /\^\^/, :close_match =3D> /\^\^/,=0D :allowed_type =3D> :inline=0D )=0D TagFactory.new( :sub,=0D :type =3D> :inline,=0D :open_match =3D> /__/, :close_match =3D> /__/,=0D :allowed_type =3D> :inline=0D )=0D TagFactory.new( :tt,=0D :type =3D> :inline,=0D :open_match =3D> /@@([^\n]+?)@@/,=0D :setup =3D> lambda{ |tag,ss| tag << ss[1] },=0D :autoclose =3D> true=0D )=0D TagFactory.new( :tt,=0D :type =3D> :inline,=0D :open_match =3D> /\{\{\{([^\n]+?)\}\}\}/,=0D :setup =3D> lambda{ |tag,ss| tag << ss[1] },=0D :autoclose =3D> true=0D )=0D TagFactory.new( :todo,=0D :type =3D> :inline,=0D :open_match =3D> /!!([a-z].+?)!!/i,=0D :setup =3D> lambda{ |tag,ss| tag << ss[1] },=0D :autoclose =3D> true=0D )=0D TagFactory.new( :a,=0D :type =3D> :inline,=0D :open_match =3D> /(?:HTTP|FTP|HTTPS):\/\/\S+/,=0D :setup =3D> lambda{ |tag,ss|=0D tag.attrs[ :href ] =3D ss[0]=0D tag << ss[0]=0D },=0D :autoclose =3D> true=0D )=0D TagFactory.new( :a,=0D :type =3D> :inline,=0D :open_match =3D> /\[((?:HTTP|FTP|HTTPS):\/\/\S+) = ([^\]]+)\]/,=0D :setup =3D> lambda{ |tag,ss|=0D tag.attrs[ :href ] =3D ss[1]=0D tag << ss[2]=0D },=0D :autoclose =3D> true=0D )=0D TagFactory.new( :wiki_link,=0D :type =3D> :inline,=0D :open_match =3D> = /[A-Z]{2,}[a-z][a-zA-Z]*|[A-Z][a-z]+[A-Z][a-zA-Z]*/,=0D :setup =3D> lambda{ |tag,ss|=0D tag.attrs[ :page ] =3D ss[0]=0D tag << ss[0].dewikiword=0D },=0D :autoclose =3D> true=0D )=0D TagFactory.new( :wiki_link,=0D :type =3D> :inline,=0D :open_match =3D> /\[\[([^\]\n]{2,}?)\]\]/,=0D :setup =3D> lambda{ |tag,ss|=0D tag.attrs[ :page ] =3D ss[0]=0D tag << ss[0]=0D },=0D :autoclose =3D> true=0D )=0D TagFactory.new( :wiki_link,=0D :type =3D> :inline,=0D :open_match =3D> = /\[([A-Z]{2,}[a-z][a-zA-Z]*|[A-Z][a-z]+[A-Z][a-zA-Z]*) ([^\]]+)\]/,=0D :setup =3D> lambda{ |tag,ss|=0D tag.attrs[ :page ] =3D ss[1]=0D tag << ss[2]=0D },=0D :autoclose =3D> true=0D )=0D =0D def initialize( owl_string )=0D @ss =3D StringScanner.new( owl_string )=0D =0D @root =3D Tag.new( :root, TagFactory.new( :root, :type = =3D> :root, :allowed_type =3D> :block ) )=0D @current =3D @root=0D =0D #todo - preparse de-html of invalid tags=0D while !@ss.eos?=0D puts "Step with @current =3D #{@current.inspect} = : #{(@ss.peek(20)+'...').inspect}" if $DEBUG=0D =0D # Keep popping off the current tag until we get = to the root,=0D # as long as the end criteria is met=0D while ( @current !=3D @root ) && = (!@current.close_requires_bol? || @ss.bol?) && @ss.scan( = @current.close_match ) =0D @current =3D @current.parent_node || = @root=0D end=0D =0D # No point in continuing if closing out tags = consumed the rest of the string=0D break if @ss.eos?=0D =0D # Look for a tag to open=0D tag =3D nil=0D TagFactory.by_type[ @current.allowed_type = ].each{ |factory|=0D if tag =3D factory.match( @ss )=0D @current.append_child( tag )=0D @current =3D tag unless = tag.autoclose?=0D break=0D end=0D }=0D next if tag #found one, start over=0D =0D # Couldn't find a valid tag at this spot=0D # so we need to eat some characters=0D if @ss.scan( = /~([A-Z]{2,}[a-z][a-zA-Z]*|[A-Z][a-z]+[A-Z][a-zA-Z]*|\[\[([^\]\n]{2,}?)\]\= ]|\[(?:[A-Z]{2,}[a-z][a-zA-Z]*|[A-Z][a-z]+[A-Z][a-zA-Z]*) [^\]]+\])/ )=0D= # Shove negated links directly as text=0D= consumed =3D @ss[1]=0D else=0D # Man, it would be nice if I could do a = lookahead here=0D # and consume more than a few characters = at a time!=0D =0D #Hopefully no opening or closing tags = are based on letters or changes in tab/spaces;=0D #if that's not true, the next line must = be foregone in favor of the (slow)=0D #one-char-at-a-time consumption=0D consumed =3D @ss.scan( /[a-z \t]+|./m )=0D= #consumed =3D @ss.scan( /[a-z]+|[ = \t]+|./m )=0D #consumed =3D @ss.getch=0D end=0D @current << consumed if @current.allowed_type =3D=3D= :inline=0D end=0D =0D end=0D =0D def to_s=0D out =3D ''=0D @root.child_nodes.each{ |el|=0D out << el.to_html=0D }=0D out=0D end=0D =0D end=0D =0D class String=0D def htmlsafe=0D self.dup.htmlsafe!=0D end=0D =0D def htmlsafe!=0D self.gsub!( /&/, '&' )=0D self.gsub!( //, '>' )=0D self=0D end=0D =0D def dewikiword=0D self.gsub( /([a-z])([A-Z])/, '\\1 \\2' )=0D end=0D end=0D =0D if __FILE__ =3D=3D $0=0D str =3D <