view src/nabble/view/web/template/HtmlListNamespace.java @ 0:7ecd1a4ef557

add content
author Franklin Schmidt <fschmidt@gmail.com>
date Thu, 21 Mar 2019 19:15:52 -0600
parents
children
line wrap: on
line source

package nabble.view.web.template;

import fschmidt.html.Html;
import fschmidt.html.HtmlCdata;
import fschmidt.html.HtmlScript;
import fschmidt.html.HtmlStyle;
import fschmidt.html.HtmlTag;
import fschmidt.html.HtmlTextContainer;
import fschmidt.util.java.HtmlUtils;
import fschmidt.util.java.ObjectUtils;
import nabble.model.FileUpload;
import nabble.model.Message;
import nabble.model.ModelHome;
import nabble.model.Node;
import nabble.model.User;
import nabble.naml.compiler.Command;
import nabble.naml.compiler.CommandSpec;
import nabble.naml.compiler.IPrintWriter;
import nabble.naml.compiler.Interpreter;
import nabble.naml.compiler.Namespace;
import nabble.naml.compiler.ScopedInterpreter;
import nabble.naml.namespaces.ListSequence;
import nabble.view.lib.Jtp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import java.util.regex.Pattern;


@Namespace (
	name = "html_list",
	global = true
)
public final class HtmlListNamespace extends ListSequence<Object> {
	private static final Logger logger = LoggerFactory.getLogger(HtmlListNamespace.class);

	private final Message.Source source;
	private final Message.Format format;

	public HtmlListNamespace(Html list,Message.Source source, Message.Format  format) {
		super(list);
		this.source = source;
		this.format = format;
	}

	public String toString() {
		return ObjectUtils.join(elements);
	}

	public String toMailText() {
		return htmlToTextMail2(elements);
	}

	public static final CommandSpec process_raw_tags = CommandSpec.NO_OUTPUT;

	@Command public void process_raw_tags(IPrintWriter out,Interpreter interp) {
		processRaw(elements);
	}

	public static final CommandSpec process_cdata_tags = CommandSpec.NO_OUTPUT;

	@Command public void process_cdata_tags(IPrintWriter out,Interpreter interp) {
		processCDATA(elements);
	}

	public static final CommandSpec process_quotes = CommandSpec.NO_OUTPUT()
		.scopedParameters("wrote")
		.optionalParameters("max_quoted_lines")
		.build()
	;

	@Command public void process_quotes(IPrintWriter out,ScopedInterpreter<AuthorWroteNamespace> interp) {
		int maxQuotedLines = interp.getArgAsInt("max_quoted_lines",10);
		processQuotes(elements,maxQuotedLines, interp);
	}

	public static final CommandSpec process_quotes_as_text = CommandSpec.NO_OUTPUT()
		.scopedParameters("wrote")
		.build()
	;

	@Command public void process_quotes_as_text(IPrintWriter out,ScopedInterpreter<AuthorWroteNamespace> interp) {
		// In order to avoid complexity, the algorithm should process
		// just one type of line breaks. Since the desired output is TEXT,
		// the chosen line break is CRLF. Thus we have to convert <br>
		// into CRLF below.
		if (format == Message.Format.HTML) {
			convertBRIntoCRLF(elements);
		}
		// All lines breaks are CRLF now...
		processQuotesText(elements, 0, interp);
	}

	private void convertBRIntoCRLF(List<Object> elements) {
		for (int i = 0; i < elements.size(); i++) {
			Object o = elements.get(i);
			if (o instanceof HtmlTag && ((HtmlTag) o).getName().equals("br"))
				elements.set(i, "\n");
			else if (o instanceof String) {
				elements.set(i, ((String)o).replaceAll("\r?\n", ""));
			}
		}
	}

	public static final CommandSpec process_email = CommandSpec.NO_OUTPUT;

	@Command public void process_email(IPrintWriter out,Interpreter interp) {
		processEmail(elements, source);
	}

	public static final CommandSpec set_target_to_top = CommandSpec.NO_OUTPUT;

	@Command public void set_target_to_top(IPrintWriter out,Interpreter interp) {
		setTargetToTop(elements);
	}

	public static final CommandSpec add_nofollow = CommandSpec.NO_OUTPUT()
		.optionalParameters("accept_rel_follow")
		.build()
	;

	@Command public void add_nofollow(IPrintWriter out,Interpreter interp) {
		boolean acceptRelFollow = interp.getArgAsBoolean("accept_rel_follow", false);
		addNofollow(elements, acceptRelFollow);
	}

	public static final CommandSpec process_smilies = CommandSpec.NO_OUTPUT;

	@Command public void process_smilies(IPrintWriter out,Interpreter interp) {
		processSmilies(elements,true);
	}

	public static final CommandSpec process_smilies_as_text = CommandSpec.NO_OUTPUT;

	@Command public void process_smilies_as_text(IPrintWriter out,Interpreter interp) {
		processSmilies(elements,false);
	}

	public static final CommandSpec process_file_tags = CommandSpec.NO_OUTPUT;

	@Command public void process_file_tags(IPrintWriter out,Interpreter interp) {
		FileUpload.processFileTags(elements,source);
	}



	// security

	public static final CommandSpec disable_banned_tags = CommandSpec.NO_OUTPUT()
		.dotParameter("banned_tags")
		.optionalParameters("remove")
		.build()
	;

	@Command public void disable_banned_tags(IPrintWriter out,Interpreter interp) {
		String[] tags = splitAndTrim(interp.getArgString("banned_tags"));
		boolean remove = interp.getArgAsBoolean("remove", format.isMail());
		disableBannedTags(elements,remove,tags);
	}

	public static final CommandSpec disable_invalid_urls = CommandSpec.NO_OUTPUT()
		.dotParameter("url_attributes")
		.build()
	;

	@Command public void disable_invalid_urls(IPrintWriter out,Interpreter interp) {
		String[] attrs = splitAndTrim(interp.getArgString("url_attributes"));
		disableInvalidUrls(elements,false,attrs);
	}

	public static final CommandSpec disable_javascript_urls = CommandSpec.NO_OUTPUT()
		.dotParameter("url_attributes")
		.build()
	;

	@Command public void disable_javascript_urls(IPrintWriter out,Interpreter interp) {
		String[] attrs = splitAndTrim(interp.getArgString("url_attributes"));
		disableJavascriptUrls(elements,false,attrs);
	}

	public static final CommandSpec disable_on_event = CommandSpec.NO_OUTPUT;

	@Command public void disable_on_event(IPrintWriter out,Interpreter interp) {
		disableOnEvent(elements,false);
	}

	public static final CommandSpec disable_scripts = CommandSpec.NO_OUTPUT;

	@Command public void disable_scripts(IPrintWriter out,Interpreter interp) {
		disableScripts(elements,false);
	}

	public static final CommandSpec disable_style_blocks = CommandSpec.NO_OUTPUT()
		.optionalParameters("remove")
		.build()
	;

	@Command public void disable_style_blocks(IPrintWriter out,Interpreter interp) {
		boolean remove = interp.getArgAsBoolean("remove", format.isMail());
		disableStyleBlocks(elements,remove);
	}


	static String[] splitAndTrim(String s) {
		final String[] a = s.split(",");
		for( int i=0; i<a.length; i++ ) {
			a[i] = a[i].trim();
		}
		return a;
	}




	public static final CommandSpec process_tag = CommandSpec.NO_OUTPUT()
		.parameters("tag")
		.scopedParameters("do")
		.dotParameter("do")
		.build()
	;

	@Command public void process_tag(IPrintWriter out,ScopedInterpreter<TagNamespace> interp) {
		String tagName = interp.getArgString("tag");
		for( ListIterator<Object> i = elements.listIterator(); i.hasNext(); ) {
			Object curr = i.next();
			if( curr instanceof HtmlTag ) {
				HtmlTag tag = (HtmlTag)curr;
				if( tag.getName().equals(tagName) ) {
					TagNamespace ns = new TagNamespace(tag);
					String replacement = interp.getArgString(ns,"do");
					i.set(replacement);
				}
			}
		}
	}

	public static final CommandSpec process_text = CommandSpec.NO_OUTPUT()
		.parameters("text")
		.dotParameter("replacement")
		.build()
	;

	@Command public void process_text(IPrintWriter out,Interpreter interp) {
		String text = interp.getArgString("text");
		String replacement = interp.getArgString("replacement");
		for( ListIterator<Object> i = elements.listIterator(); i.hasNext(); ) {
			Object curr = i.next();
			if( curr instanceof String ) {
				String s = (String) curr;
				if (text.equals(s)) {
					i.set(replacement);
				}
			}
		}
	}

	@Namespace (
		name = "html_tag",
		global = false,
		transparent = true
	)
	public static final class TagNamespace {
		private final HtmlTag tag;

		private TagNamespace(HtmlTag tag) {
			this.tag = tag;
		}

		@Command public void tag_as_string(IPrintWriter out,Interpreter interp) {
			out.print(tag);
		}

		public static final CommandSpec tag_attribute = new CommandSpec.Builder()
			.dotParameter("name")
			.build()
		;

		@Command public void tag_attribute(IPrintWriter out,Interpreter interp) {
			out.print( HtmlTag.unquote(tag.getAttributeValue(interp.getArgString("name"))) );
		}


		public static final CommandSpec tag_has_attribute = new CommandSpec.Builder()
			.dotParameter("name")
			.build()
		;

		@Command public void tag_has_attribute(IPrintWriter out,Interpreter interp) {
			out.print( tag.getAttributeValue(interp.getArgString("name")) != null );
		}

	}


	public static final CommandSpec process_embed = new CommandSpec.Builder()
		.dotParameter("regex")
		.build()
	;

	@Command public void process_embed(IPrintWriter out,Interpreter interp) {
		Pattern ptn = Pattern.compile( interp.getArgString("regex") );
		for( ListIterator<Object> i = elements.listIterator(); i.hasNext(); ) {
			Object curr = i.next();
			if( curr instanceof HtmlTag ) {
				HtmlTag tag = (HtmlTag)curr;
				if( tag.getName().equals("nabble_embed") ) {
					if( tag.isEmpty() ) {  // no good
						i.set( new Embedded(HtmlUtils.htmlEncode(tag.toString())) );
					} else {
						List<Object> list = new ArrayList<Object>();
						while(true) {
							i.remove();
							if( !i.hasNext() ) {  // no closing tag
								list.add(0,tag);
								elements.add( new Embedded(HtmlUtils.htmlEncode(ObjectUtils.join(list))) );
								return;
							}
							curr = i.next();
							if( curr instanceof HtmlTag ) {
								HtmlTag tag2 = (HtmlTag)curr;
								if( tag2.getName().equals("/nabble_embed") )  // done
									break;
							}
							list.add(curr);
						}
						String text = ObjectUtils.join(list);
						if( ptn.matcher(text).matches() ) {  // ok
							i.set( new Embedded(text) );
						} else {  // not ok
							list.add(0,tag);
							list.add(curr);
							i.set( new Embedded(HtmlUtils.htmlEncode(ObjectUtils.join(list))) );
						}
					}
				}
			}
		}
	}

	private static class Embedded {
		private final String text;

		Embedded(String text) {
			this.text = text;
		}

		public String toString() {
			return text;
		}

	}



	// from MessageFormatImpls


	private static void processRaw(List<Object> list) {
		for( ListIterator<Object> i=list.listIterator(); i.hasNext(); ) {
			Object o = i.next();
			if( o instanceof HtmlTextContainer ) {
				HtmlTextContainer container = (HtmlTextContainer)o;
				if( container.startTag.getName().equalsIgnoreCase("raw") ) {
					HtmlTag preTag = new HtmlTag(container.startTag);
					preTag.setName("pre");
					i.set( new Embedded(
						preTag.toString() + HtmlUtils.htmlEncode(container.text) + "</pre>"
					) );
				}
			}
		}
	}

	private static void processCDATA(List<Object> list) {
		for( ListIterator<Object> i=list.listIterator(); i.hasNext(); ) {
			Object o = i.next();
			if (o instanceof HtmlCdata) {
				HtmlCdata container = (HtmlCdata) o;
				container.toString();
				i.set("<pre>" + HtmlUtils.htmlEncode("<![CDATA[" + container.text + "]]>") + "</pre>");
			}
		}
	}


	private static final HtmlTag blockquote = new HtmlTag("blockquote");
	private static final HtmlTag _blockquote = new HtmlTag("/blockquote");
	private static final HtmlTag divQuote = new HtmlTag("div");
	private static final HtmlTag divQuoteAuthor = new HtmlTag("div");
	private static final HtmlTag divQuoteMessage = new HtmlTag("div");
	private static final HtmlTag divQuoteMessageHidden = new HtmlTag("div");
	private static final HtmlTag _div = new HtmlTag("/div");
	static {
		blockquote.setAttribute("class",HtmlTag.quote("quote dark-border-color"));
		divQuote.setAttribute("class",HtmlTag.quote("quote light-border-color"));
		divQuoteAuthor.setAttribute("class",HtmlTag.quote("quote-author"));
		divQuoteAuthor.setAttribute("style",HtmlTag.quote("font-weight: bold;"));
		divQuoteMessage.setAttribute("class",HtmlTag.quote("quote-message"));
		divQuoteMessageHidden.setAttribute("class",HtmlTag.quote("quote-message shrinkable-quote"));
	}


	@Namespace (
		name = "html_list_author_wrote",
		global = true
	)
	public static class AuthorWroteNamespace {
		private final String authorStr;

		private AuthorWroteNamespace(String authorStr) {
			this.authorStr = authorStr;
		}

		@Command public void author(IPrintWriter out,Interpreter interp) {
			out.print(authorStr);
		}
	}

	private static class QuoteInfo {
		final int i;
		final int nBreaks;

		QuoteInfo(int i,int nBreaks) {
			this.i = i;
			this.nBreaks = nBreaks;
		}
	}

	private static final Set<String> breakingTags = new HashSet<String>(Arrays.asList(
		"br",
		"div",
		"p"
	));

	private static void processQuotes(List<Object> list, int maxQuotedLines, ScopedInterpreter<AuthorWroteNamespace> interp) {
		processQuotes(list,maxQuotedLines,0, interp);
	}

	private static QuoteInfo processQuotes(List<Object> list,int maxQuotedLines,int start, ScopedInterpreter<AuthorWroteNamespace> interp) {
		int nBreaks = 0;
		int n = list.size();
		for( int i=start; i<n; i++ ) {
			Object o = list.get(i);
			if( !(o instanceof HtmlTag) )
				continue;
			HtmlTag tag = (HtmlTag)o;
			String tagName = tag.getName().toLowerCase();
			if( tagName.equals("/quote") )
				return new QuoteInfo(i,nBreaks);
			if( tagName.equals("quote") ) {
				QuoteInfo qi = processQuotes(list,maxQuotedLines,i+1, interp);
				if( qi==null ) {
					list.set(i,HtmlUtils.htmlEncode(tag.toString()));
					return null;
				}
				int closeQuote = qi.i;
				nBreaks += qi.nBreaks + 1;
				int fromEnd = list.size() - closeQuote;

				list.remove(i);
				nBreaks -= removeBr(list,i);
				list.add(i++,blockquote);
				list.add(i++,divQuote);
				list.add(i++,"\n");
				String author = HtmlTag.unquote(tag.getAttributeValue("author"));
				if( author != null ) {
					list.add(i++,divQuoteAuthor);
					list.add(i++,interp.getArgString(new AuthorWroteNamespace(author),"wrote"));
					list.add(i++,_div);
					list.add(i++,"\n");
				}
				list.add(i++, qi.nBreaks < maxQuotedLines ? divQuoteMessage : divQuoteMessageHidden );
//				list.add(i++, divQuoteMessage );

				i = list.size() - fromEnd;
				int brs = removeBrUp(list,i-1);
				nBreaks -= brs;
				i -= brs;
				list.remove(i);
				nBreaks -= removeBr(list,i);
				list.add(i++,_div);
				list.add(i++,"\n");
				list.add(i++,_div);
				list.add(i++,_blockquote);
				list.add(i++,"\n");
				i--;

				n = list.size();
			}
			else if( breakingTags.contains(tagName) ) {
				nBreaks++;
			}
		}
		return null;
	}


	private static final HtmlTag _a = new HtmlTag("/a");

	private static void processEmail(List<Object> list,Message.Source src) {
		int count = 0;
		int n = list.size() - 3 + 1;
		for( int i=0; i<n; i++ ) {
			Object o = list.get(i);
			if( !(o instanceof HtmlTag) )
				continue;
			HtmlTag tag = (HtmlTag)o;
			if( !tag.getName().toLowerCase().equals("email") )
				continue;
			o = list.get(i+1);
			if( !(o instanceof String) )
				continue;
			String email = (String)o;
			o = list.get(i+2);
			if( !(o instanceof HtmlTag) )
				continue;
			HtmlTag endTag = (HtmlTag)o;
			if( !endTag.getName().toLowerCase().equals("/email") )
				continue;
			list.remove(i);
			list.remove(i);
			list.remove(i);
			HtmlTag a = new HtmlTag("a");
			StringBuilder href = new StringBuilder();
			href.append( "/user/SendEmail.jtp" );
			if( src==null || src instanceof Message.TempSource) {
				href.append( "?type=email&email=" ).append( HtmlUtils.urlEncode(email) );
			} else {
				if( src instanceof Node ) {
					Node node = (Node)src;
					href.append( "?type=node&node=" ).append( node.getId() );
				} else if( src instanceof User ) {
					User user = (User)src;
					href.append( "?type=sig&user=" ).append( user.getId() );
				} else {
					throw new RuntimeException("src="+src);
				}
				href.append( "&i=" ).append( count++ );
			}
			a.setAttribute("href",HtmlTag.quote(href.toString()));
			a.setAttribute("target","\"_top\"");
			a.setAttribute("rel","\"nofollow\"");
			list.add(i++,a);
			list.add(i++,ModelHome.hideEmail(email));
			list.add(i,_a);
		}
	}


	private static void setTargetToTop(List<Object> list) {
		for( Object o : list ) {
			if( o instanceof HtmlTag ) {
				HtmlTag tag = (HtmlTag)o;
				if( tag.getName().toLowerCase().equals("a") ) {
					if( tag.getAttributeValue("target") == null ) {
						tag.setAttribute("target","\"_top\"");
					}
				}
			}
		}
	}

	private static void addNofollow(List<Object> list, boolean acceptRelFollow) {
		for( Object o : list ) {
			if( o instanceof HtmlTag ) {
				HtmlTag tag = (HtmlTag)o;
				String tagName = tag.getName().toLowerCase();
				if (tagName.equals("a")) {
					if (acceptRelFollow && "\"follow\"".equals(tag.getAttributeValue("rel")))
						continue;
					tag.setAttribute("rel","\"nofollow\"");
					tag.setAttribute("link","\"external\"");
				}
			}
		}
	}



	// from HtmlSecurity

	private static void disable(ListIterator<Object> i,Object curr,boolean removeViolation) {
		if( removeViolation ) {
			i.remove();
		} else {
			i.set(HtmlUtils.htmlEncode(curr.toString()));
		}
	}

	public static void disableBannedTags(List<Object> html,boolean removeViolation,String... tagNames) {
		Set<String> tagNameSet = new HashSet<String>();
		for( String tagName : tagNames ) {
			tagNameSet.add(tagName);
			tagNameSet.add("/"+tagName);
		}
		for( ListIterator<Object> i = html.listIterator(); i.hasNext(); ) {
			Object curr = i.next();
			if( curr instanceof HtmlTag ) {
				HtmlTag tag = (HtmlTag)curr;
				if( tagNameSet.contains(tag.getName().toLowerCase()) ) {
					disable(i,tag,removeViolation);
				}
			}
		}
	}

	private static final URL baseUrl;
	static {
		try {
			baseUrl = new URL("https://www.nabble.com/");  // any valid URL is fine here
		} catch(MalformedURLException e) {
			logger.error("",e);
			System.exit(-1);
			throw new RuntimeException(e);
		}
	}

	private static void disableInvalidUrls(List<Object> html,boolean removeViolation,String... parameters) {
		for( ListIterator<Object> i = html.listIterator(); i.hasNext(); ) {
			Object curr = i.next();
			if( curr instanceof HtmlTag ) {
				HtmlTag tag = (HtmlTag)curr;
				for (String attrName: parameters) {
					String val = HtmlTag.unquote(tag.getAttributeValue(attrName));
					if (val != null) {
						try {
							new URL(baseUrl,val).toURI();
						} catch(MalformedURLException e) {
							disable(i,tag,removeViolation);
							break;
						} catch(URISyntaxException e) {
							disable(i,tag,removeViolation);
							break;
						}
					}
				}
			}
		}
	}

	private static void disableJavascriptUrls(List<Object> html,boolean removeViolation,String... parameters) {
		for( ListIterator<Object> i = html.listIterator(); i.hasNext(); ) {
			Object curr = i.next();
			if( curr instanceof HtmlTag ) {
				HtmlTag tag = (HtmlTag)curr;
				for (String attrName: parameters) {
					String val = HtmlTag.unquote(tag.getAttributeValue(attrName));
					if (val != null && val.toLowerCase().startsWith("javascript:")) {
						disable(i,tag,removeViolation);
						break;
					}
				}
			}
		}
	}

	private static void disableOnEvent(List<Object> html,boolean removeViolation) {
		for( ListIterator<Object> i = html.listIterator(); i.hasNext(); ) {
			Object curr = i.next();
			if( curr instanceof HtmlTag ) {
				HtmlTag tag = (HtmlTag)curr;
				for( String attrName : tag.getAttributeNames() ) {
					if (HtmlTag.unquote(attrName).toLowerCase().startsWith("on")) {
						disable(i,tag,removeViolation);
						break;
					}
				}
			}
		}
	}

	private static final Pattern urchin = Pattern.compile("\\s*_uacct\\s*=\\s*[\"'][^\"']+[\"']\\s*;\\s*urchinTracker\\(\\);\\s*",Pattern.MULTILINE);

	private static void disableScripts(List<Object> html,boolean removeViolation) {
		for( ListIterator<Object> i = html.listIterator(); i.hasNext(); ) {
			Object curr = i.next();
			if( curr instanceof HtmlScript ) {
				HtmlScript script = (HtmlScript)curr;
				String src = HtmlTag.unquote(script.startTag.getAttributeValue("src"));
				if( src != null ) {
					if( script.text.trim().length()==0 ) {
						if( src.equals("http://www.google-analytics.com/urchin.js") )
							continue;
					}
				} else {
					if( urchin.matcher(script.text).matches() )
						continue;
				}
				disable(i,script,removeViolation);
			}
		}
	}

	private static void disableStyleBlocks(List<Object> html,boolean removeViolation) {
		for( ListIterator<Object> i = html.listIterator(); i.hasNext(); ) {
			Object curr = i.next();
			if( curr instanceof HtmlStyle ) {
				disable(i,curr,removeViolation);
			}
		}
	}


	private static String htmlToTextMail2(List<Object> html) {
	    StringBuilder buf = new StringBuilder();
	    for (int i = 0; i < html.size(); i++) {
			Object next = html.get(i);
	        if( next instanceof String ) {
	            buf.append(next);
	        } else if( next instanceof HtmlTag) {
	        	HtmlTag tag = (HtmlTag)next;
				String tagName = tag.getName().toLowerCase();
	        	String separator = Message.htmlSeparators.get(tagName);
	        	if (separator!=null && buf.length()>0 && buf.charAt(buf.length()-1)!='\n')
	        		buf.append(separator);
	        	if (tagName.equals("img")) {
	        		String src = tag.getAttributeValue("src");
	        		if (src!=null) {
	        			buf.append('<');
	        			buf.append(HtmlTag.unquote(src));
	        			buf.append("> ");
	        		}
	        	}
	        	else if (tagName.equals("a")) {
	        		String src = tag.getAttributeValue("href");
	        		if (src!=null) {
						String anchorText = html.get(i+1).toString();
						buf.append(anchorText.trim());
						buf.append(" <");
	        			buf.append(HtmlTag.unquote(src));
	        			buf.append("> ");
						i++;
	        		}
	        	}
	        }
	    }
	    Message.wrapQuoteText(buf);
	    return buf.toString();
	}


	private static int processQuotesText(List<Object> list, int start, ScopedInterpreter<AuthorWroteNamespace> interp) {
		int n = list.size();
		for( int i=start; i<n; i++ ) {
			Object o = list.get(i);
			if( !(o instanceof HtmlTag) )
				continue;
			HtmlTag tag = (HtmlTag)o;
			String tagName = tag.getName().toLowerCase();
			if( tagName.equals("/quote") )
				return i;
			if( tagName.equals("quote") ) {
				int closeQuote = processQuotesText(list, i+1, interp);
				if( closeQuote == -1 )
					return -1;
				int fromEnd = list.size() - closeQuote;

				list.remove(i);
				removeCRLF(list, i, 0);
				if (i > 0)
					list.add(i++,"\n");

				String author = HtmlTag.unquote(tag.getAttributeValue("author"));
				if( author != null ) {
					list.add(i++,interp.getArgString(new AuthorWroteNamespace(author),"wrote"));
				}
				int begin = i;
				i = list.size() - fromEnd;
				removeCRLFUp(list, i-1, -1);
				list.remove(i);
				removeCRLF(list, i, 0);
				for (int j=begin;j<i;j++)
					quoteText(list,j);

				list.add(i, "\n");
				n = list.size();
			}
		}
		return -1;
	}

	private static void quoteText(List<Object> list,int i) {
		Object obj = list.get(i);
		if( !(obj instanceof String) )
			return;
		String s = (String) obj;
		if (s.length() == 0)
			return;

		removeCRLFUp(list, i-1, -1);
		s = CRLF_UP_PTN.matcher(s).replaceAll("");

		if (!s.startsWith("\n") && !s.startsWith("\r\n") )
			s = "\n" + s;

		s = s.replaceAll("\n", "\n> ");
		s = s.replaceAll("\n> >", "\n>>");

		list.set(i, s + '\n');
		removeCRLF(list, i+1, +1);
	}

	private static final Pattern CRLF_PTN = Pattern.compile("^(\\r?\\n){1}");
	private static final Pattern CRLF_UP_PTN = Pattern.compile("(\\r?\\n){1}$");
	private static final Pattern BR_PTN = Pattern.compile("^(\\s*<br/?>){1,2}");
	private static final Pattern BR_UP_PTN = Pattern.compile("(<br/?>\\s*){1,2}$");

	private static void removeCRLF(List<Object> list, int i, int increment) {
		Object obj = i >= 0 && i < list.size()? list.get(i) : null;
		if (obj instanceof String) {
			String s = (String) obj;
			while (s.length() == 0 && increment != 0) {
				i += increment;
				obj = i >= 0 && i < list.size()? list.get(i) : null;
				if (obj == null || !(obj instanceof String))
					return;
				s = (String) obj;
			}
			s = CRLF_PTN.matcher(s).replaceFirst("");
			list.set(i, s);
		}
	}

	private static void removeCRLFUp(List<Object> list, int i, int increment) {
		Object obj = i >= 0 && i < list.size()? list.get(i) : null;
		if (obj instanceof String) {
			String s = (String) obj;
			while (s.length() == 0 && increment != 0) {
				i += increment;
				obj = i >= 0 && i < list.size()? list.get(i) : null;
				if (obj == null || !(obj instanceof String))
					return;
				s = (String) obj;
			}
			s = CRLF_UP_PTN.matcher(s).replaceFirst("");
			list.set(i, s);
		}
	}

	private static int removeBr(List<Object> list,int i) {
		return removeBr(list,i,0);
	}

	private static int removeBr(List<Object> list,int i,int count) {
		if( count==2 )
			return count;
		if( i >= list.size() )
			return count;
		Object obj = list.get(i);
		if( obj instanceof String ) {
			String s = (String)obj;
			s = BR_PTN.matcher(s).replaceAll("");
			list.set(i,s);
		} else if( obj instanceof HtmlTag ) {
			HtmlTag tag = (HtmlTag)obj;
			if( tag.getName().equalsIgnoreCase("br") ) {
				list.remove(i);
				return removeBr(list,i,count+1);
			}
		}
		return count;
	}

	private static int removeBrUp(List<Object> list,int i) {
		return removeBrUp(list,i,0);
	}

	private static int removeBrUp(List<Object> list,int i,int count) {
		if( count==2 )
			return count;
		if( i < 0 )
			return count;
		Object obj = list.get(i);
		if( obj instanceof String ) {
			String s = (String)obj;
			s = BR_UP_PTN.matcher(s).replaceAll("");
			list.set(i,s);
		} else if( obj instanceof HtmlTag ) {
			HtmlTag tag = (HtmlTag)obj;
			if( tag.getName().equalsIgnoreCase("br") ) {
				list.remove(i);
				return removeBrUp(list,i-1,count+1);
			}
		}
		return count;
	}


//	private static final String smiliesDir = "http://" + Jtp.getDefaultHost() + "/images/smiley/";
	private static final String smiliesDir = "/images/smiley/";

	private static void processSmilies(List<Object> list,boolean isHtml) {
		for( ListIterator<Object> i=list.listIterator(); i.hasNext(); ) {
			Object o = i.next();
			if( o instanceof HtmlTag ) {
				HtmlTag tag = (HtmlTag)o;
				if( tag.getName().toLowerCase().equals("smiley") ) {
					if( isHtml ) {
						String s = HtmlTag.unquote(tag.getAttributeValue("image"));
						if( s != null ) {
							HtmlTag img = new HtmlTag("img class='smiley' src='"+smiliesDir+s+"' /");
							i.set(img);
						}
					}
				}
			}
		}
	}


}