| /* |
| JSPWiki - a JSP-based WikiWiki clone. |
| |
| Copyright (C) 2001-2002 Janne Jalkanen (Janne.Jalkanen@iki.fi) |
| |
| 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| */ |
| package com.ecyrd.jspwiki; |
| |
| import java.io.*; |
| import java.util.*; |
| import java.text.*; |
| |
| import org.apache.log4j.Category; |
| import org.apache.oro.text.*; |
| import org.apache.oro.text.regex.*; |
| |
| import com.ecyrd.jspwiki.plugin.PluginManager; |
| import com.ecyrd.jspwiki.plugin.PluginException; |
| import com.ecyrd.jspwiki.attachment.AttachmentManager; |
| import com.ecyrd.jspwiki.attachment.Attachment; |
| import com.ecyrd.jspwiki.providers.ProviderException; |
| |
| /** |
| * Handles conversion from Wiki format into fully featured HTML. |
| * This is where all the magic happens. It is CRITICAL that this |
| * class is tested, or all Wikis might die horribly. |
| * <P> |
| * The output of the HTML has not yet been validated against |
| * the HTML DTD. However, it is very simple. |
| * |
| * @author Janne Jalkanen |
| */ |
| |
| public class TranslatorReader extends Reader |
| { |
| public static final int READ = 0; |
| public static final int EDIT = 1; |
| private static final int EMPTY = 2; // Empty message |
| private static final int LOCAL = 3; |
| private static final int LOCALREF = 4; |
| private static final int IMAGE = 5; |
| private static final int EXTERNAL = 6; |
| private static final int INTERWIKI = 7; |
| private static final int IMAGELINK = 8; |
| private static final int IMAGEWIKILINK = 9; |
| public static final int ATTACHMENT = 10; |
| private static final int ATTACHMENTIMAGE = 11; |
| |
| /** Allow this many characters to be pushed back in the stream. */ |
| private static final int PUSHBACK_BUFFER_SIZE = 8; |
| private PushbackReader m_in; |
| |
| private StringReader m_data = new StringReader(""); |
| |
| private static Category log = Category.getInstance( TranslatorReader.class ); |
| |
| //private boolean m_iscode = false; |
| private boolean m_isbold = false; |
| private boolean m_isitalic = false; |
| private boolean m_isTypedText = false; |
| private boolean m_istable = false; |
| private boolean m_isPre = false; |
| private boolean m_isdefinition = false; |
| private int m_listlevel = 0; |
| private int m_numlistlevel = 0; |
| |
| /** Tag that gets closed at EOL. */ |
| private String m_closeTag = null; |
| |
| private WikiEngine m_engine; |
| private WikiContext m_context; |
| |
| /** Optionally stores internal wikilinks */ |
| private ArrayList m_localLinkMutatorChain = new ArrayList(); |
| private ArrayList m_externalLinkMutatorChain = new ArrayList(); |
| private ArrayList m_attachmentLinkMutatorChain = new ArrayList(); |
| |
| /** Keeps image regexp Patterns */ |
| private ArrayList m_inlineImagePatterns; |
| |
| private PatternMatcher m_inlineMatcher = new Perl5Matcher(); |
| |
| private ArrayList m_linkMutators = new ArrayList(); |
| |
| /** |
| * This property defines the inline image pattern. It's current value |
| * is jspwiki.translatorReader.inlinePattern |
| */ |
| public static final String PROP_INLINEIMAGEPTRN = "jspwiki.translatorReader.inlinePattern"; |
| |
| /** If true, consider CamelCase hyperlinks as well. */ |
| public static final String PROP_CAMELCASELINKS = "jspwiki.translatorReader.camelCaseLinks"; |
| |
| /** If true, all hyperlinks are translated as well, regardless whether they |
| are surrounded by brackets. */ |
| public static final String PROP_PLAINURIS = "jspwiki.translatorReader.plainUris"; |
| |
| /** If true, all outward links (external links) have a small link image appended. */ |
| public static final String PROP_USEOUTLINKIMAGE = "jspwiki.translatorReader.useOutlinkImage"; |
| |
| /** If set to "true", allows using raw HTML within Wiki text. Be warned, |
| this is a VERY dangerous option to set - never turn this on in a publicly |
| allowable Wiki, unless you are absolutely certain of what you're doing. */ |
| public static final String PROP_ALLOWHTML = "jspwiki.translatorReader.allowHTML"; |
| |
| /** If true, then considers CamelCase links as well. */ |
| private boolean m_camelCaseLinks = false; |
| |
| /** If true, consider URIs that have no brackets as well. */ |
| // FIXME: Currently reserved, but not used. |
| private boolean m_plainUris = false; |
| |
| /** If true, all outward links use a small link image. */ |
| private boolean m_useOutlinkImage = true; |
| |
| /** If true, allows raw HTML. */ |
| private boolean m_allowHTML = false; |
| |
| private PatternMatcher m_matcher = new Perl5Matcher(); |
| private PatternCompiler m_compiler = new Perl5Compiler(); |
| private Pattern m_camelCasePtrn; |
| |
| /** |
| * The default inlining pattern. Currently "*.png" |
| */ |
| public static final String DEFAULT_INLINEPATTERN = "*.png"; |
| |
| /** |
| * These characters constitute word separators when trying |
| * to find CamelCase links. |
| */ |
| private static final String WORD_SEPARATORS = ",.|:;+=&"; |
| |
| /** |
| * @param engine The WikiEngine this reader is attached to. Is |
| * used to figure out of a page exits. |
| */ |
| |
| // FIXME: TranslatorReaders should be pooled for better performance. |
| public TranslatorReader( WikiContext context, Reader in ) |
| { |
| PatternCompiler compiler = new GlobCompiler(); |
| ArrayList compiledpatterns = new ArrayList(); |
| |
| m_in = new PushbackReader( new BufferedReader( in ), |
| PUSHBACK_BUFFER_SIZE ); |
| m_engine = context.getEngine(); |
| m_context = context; |
| |
| Collection ptrns = getImagePatterns( m_engine ); |
| |
| // |
| // Make them into Regexp Patterns. Unknown patterns |
| // are ignored. |
| // |
| for( Iterator i = ptrns.iterator(); i.hasNext(); ) |
| { |
| try |
| { |
| compiledpatterns.add( compiler.compile( (String)i.next() ) ); |
| } |
| catch( MalformedPatternException e ) |
| { |
| log.error("Malformed pattern in properties: ", e ); |
| } |
| } |
| |
| m_inlineImagePatterns = compiledpatterns; |
| |
| try |
| { |
| m_camelCasePtrn = m_compiler.compile( "^([[:^alnum:]]*|\\~)([[:upper:]]+[[:lower:]]+[[:upper:]]+[[:alnum:]]*)[[:^alnum:]]*$" ); |
| } |
| catch( MalformedPatternException e ) |
| { |
| log.fatal("Internal error: Someone put in a faulty pattern.",e); |
| throw new InternalWikiException("Faulty camelcasepattern in TranslatorReader"); |
| } |
| |
| // |
| // Set the properties. |
| // |
| Properties props = m_engine.getWikiProperties(); |
| |
| m_camelCaseLinks = TextUtil.getBooleanProperty( props, |
| PROP_CAMELCASELINKS, |
| m_camelCaseLinks ); |
| m_plainUris = TextUtil.getBooleanProperty( props, |
| PROP_PLAINURIS, |
| m_plainUris ); |
| m_useOutlinkImage = TextUtil.getBooleanProperty( props, |
| PROP_USEOUTLINKIMAGE, |
| m_useOutlinkImage ); |
| m_allowHTML = TextUtil.getBooleanProperty( props, |
| PROP_ALLOWHTML, |
| m_allowHTML ); |
| } |
| |
| /** |
| * Adds a hook for processing link texts. This hook is called |
| * when the link text is written into the output stream, and |
| * you may use it to modify the text. It does not affect the |
| * actual link, only the user-visible text. |
| * |
| * @param mutator The hook to call. Null is safe. |
| */ |
| public void addLinkTransmutator( StringTransmutator mutator ) |
| { |
| if( mutator != null ) |
| { |
| m_linkMutators.add( mutator ); |
| } |
| } |
| |
| /** |
| * Adds a hook for processing local links. The engine |
| * transforms both non-existing and existing page links. |
| * |
| * @param mutator The hook to call. Null is safe. |
| */ |
| public void addLocalLinkHook( StringTransmutator mutator ) |
| { |
| if( mutator != null ) |
| { |
| m_localLinkMutatorChain.add( mutator ); |
| } |
| } |
| |
| /** |
| * Adds a hook for processing external links. This includes |
| * all http:// ftp://, etc. links, including inlined images. |
| * |
| * @param mutator The hook to call. Null is safe. |
| */ |
| public void addExternalLinkHook( StringTransmutator mutator ) |
| { |
| if( mutator != null ) |
| { |
| m_externalLinkMutatorChain.add( mutator ); |
| } |
| } |
| |
| /** |
| * Adds a hook for processing attachment links. |
| * |
| * @param mutator The hook to call. Null is safe. |
| */ |
| public void addAttachmentLinkHook( StringTransmutator mutator ) |
| { |
| if( mutator != null ) |
| { |
| m_attachmentLinkMutatorChain.add( mutator ); |
| } |
| } |
| |
| /** |
| * Figure out which image suffixes should be inlined. |
| * @return Collection of Strings with patterns. |
| */ |
| |
| protected static Collection getImagePatterns( WikiEngine engine ) |
| { |
| Properties props = engine.getWikiProperties(); |
| ArrayList ptrnlist = new ArrayList(); |
| |
| for( Enumeration e = props.propertyNames(); e.hasMoreElements(); ) |
| { |
| String name = (String) e.nextElement(); |
| |
| if( name.startsWith( PROP_INLINEIMAGEPTRN ) ) |
| { |
| String ptrn = props.getProperty( name ); |
| |
| ptrnlist.add( ptrn ); |
| } |
| } |
| |
| if( ptrnlist.size() == 0 ) |
| { |
| ptrnlist.add( DEFAULT_INLINEPATTERN ); |
| } |
| |
| return ptrnlist; |
| } |
| |
| /** |
| * Returns link name, if it exists; otherwise it returns null. |
| */ |
| private String linkExists( String page ) |
| { |
| return m_engine.getFinalPageName( page ); |
| } |
| |
| /** |
| * Calls a transmutator chain. |
| * |
| * @param list Chain to call |
| * @param text Text that should be passed to the mutate() method |
| * of each of the mutators in the chain. |
| * @return The result of the mutation. |
| */ |
| |
| private String callMutatorChain( Collection list, String text ) |
| { |
| if( list == null || list.size() == 0 ) |
| { |
| return text; |
| } |
| |
| for( Iterator i = list.iterator(); i.hasNext(); ) |
| { |
| StringTransmutator m = (StringTransmutator) i.next(); |
| |
| text = m.mutate( m_context, text ); |
| } |
| |
| return text; |
| } |
| |
| /** |
| * Write a HTMLized link depending on its type. |
| * The link mutator chain is processed. |
| * |
| * @param type Type of the link. |
| * @param link The actual link. |
| * @param text The user-visible text for the link. |
| */ |
| public String makeLink( int type, String link, String text ) |
| { |
| String result; |
| |
| if( text == null ) text = link; |
| |
| // Make sure we make a link name that can be accepted |
| // as a valid URL. |
| |
| String encodedlink = m_engine.encodeName( link ); |
| |
| if( encodedlink.length() == 0 ) |
| { |
| type = EMPTY; |
| } |
| |
| text = callMutatorChain( m_linkMutators, text ); |
| |
| switch(type) |
| { |
| case READ: |
| result = "<A CLASS=\"wikipage\" HREF=\""+m_engine.getViewURL(link)+"\">"+text+"</A>"; |
| break; |
| |
| case EDIT: |
| result = "<U>"+text+"</U><A HREF=\""+m_engine.getEditURL(link)+"\">?</A>"; |
| break; |
| |
| case EMPTY: |
| result = "<U>"+text+"</U>"; |
| break; |
| |
| // |
| // These two are for local references - footnotes and |
| // references to footnotes. |
| // We embed the page name (or whatever WikiContext gives us) |
| // to make sure the links are unique across Wiki. |
| // |
| case LOCALREF: |
| result = "<A CLASS=\"footnoteref\" HREF=\"#ref-"+ |
| m_context.getPage().getName()+"-"+ |
| link+"\">["+text+"]</A>"; |
| break; |
| |
| case LOCAL: |
| result = "<A CLASS=\"footnote\" NAME=\"ref-"+ |
| m_context.getPage().getName()+"-"+ |
| link.substring(1)+"\">["+text+"]</A>"; |
| break; |
| |
| // |
| // With the image, external and interwiki types we need to |
| // make sure nobody can put in Javascript or something else |
| // annoying into the links themselves. We do this by preventing |
| // a haxor from stopping the link name short with quotes in |
| // fillBuffer(). |
| // |
| case IMAGE: |
| result = "<IMG CLASS=\"inline\" SRC=\""+link+"\" ALT=\""+text+"\" />"; |
| break; |
| |
| case IMAGELINK: |
| result = "<A HREF=\""+text+"\"><IMG CLASS=\"inline\" SRC=\""+link+"\" /></A>"; |
| break; |
| |
| case IMAGEWIKILINK: |
| String pagelink = m_engine.getViewURL(text); |
| result = "<A CLASS=\"wikipage\" HREF=\""+pagelink+"\"><IMG CLASS=\"inline\" SRC=\""+link+"\" ALT=\""+text+"\" /></A>"; |
| break; |
| |
| case EXTERNAL: |
| result = "<A CLASS=\"external\" HREF=\""+link+"\">"+text+"</A>"; |
| break; |
| |
| case INTERWIKI: |
| result = "<A CLASS=\"interwiki\" HREF=\""+link+"\">"+text+"</A>"; |
| break; |
| |
| case ATTACHMENT: |
| result = "<a class=\"attachment\" href=\""+m_engine.getBaseURL()+ |
| "attach?page="+link+"\">"+text+"</a>"+ |
| "<a href=\""+m_engine.getBaseURL()+"PageInfo.jsp?page="+link+ |
| "\"><img src=\"images/attachment_small.png\" border=\"0\" /></a>"; |
| break; |
| |
| default: |
| result = ""; |
| break; |
| } |
| |
| return result; |
| } |
| |
| |
| /** |
| * Cleans a Wiki name. |
| * <P> |
| * [ This is a link ] -> ThisIsALink |
| * |
| * @param link Link to be cleared. Null is safe, and causes this to return null. |
| * @return A cleaned link. |
| * |
| * @since 2.0 |
| */ |
| public static String cleanLink( String link ) |
| { |
| StringBuffer clean = new StringBuffer(); |
| |
| if( link == null ) return null; |
| |
| // |
| // Compress away all whitespace and capitalize |
| // all words in between. |
| // |
| |
| StringTokenizer st = new StringTokenizer( link, " -" ); |
| |
| while( st.hasMoreTokens() ) |
| { |
| StringBuffer component = new StringBuffer(st.nextToken()); |
| |
| component.setCharAt(0, Character.toUpperCase( component.charAt(0) ) ); |
| |
| // |
| // We must do this, because otherwise compiling on JDK 1.4 causes |
| // a downwards incompatibility to JDK 1.3. |
| // |
| clean.append( component.toString() ); |
| } |
| |
| // |
| // Remove non-alphanumeric characters that should not |
| // be put inside WikiNames. Note that all valid |
| // Unicode letters are considered okay for WikiNames. |
| // It is the problem of the WikiPageProvider to take |
| // care of actually storing that information. |
| // |
| |
| for( int i = 0; i < clean.length(); i++ ) |
| { |
| if( !(Character.isLetterOrDigit(clean.charAt(i)) || |
| clean.charAt(i) == '_' || |
| clean.charAt(i) == '.') ) |
| { |
| clean.deleteCharAt(i); |
| --i; // We just shortened this buffer. |
| } |
| } |
| |
| return clean.toString(); |
| } |
| |
| /** |
| * Figures out if a link is an off-site link. This recognizes |
| * the most common protocols by checking how it starts. |
| */ |
| private boolean isExternalLink( String link ) |
| { |
| return link.startsWith("http:") || link.startsWith("ftp:") || |
| link.startsWith("https:") || link.startsWith("mailto:") || |
| link.startsWith("news:") || link.startsWith("file:"); |
| } |
| |
| /** |
| * Matches the given link to the list of image name patterns |
| * to determine whether it should be treated as an inline image |
| * or not. |
| */ |
| private boolean isImageLink( String link ) |
| { |
| for( Iterator i = m_inlineImagePatterns.iterator(); i.hasNext(); ) |
| { |
| if( m_inlineMatcher.matches( link, (Pattern) i.next() ) ) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Returns true, if the argument contains a number, otherwise false. |
| * In a quick test this is roughly the same speed as Integer.parseInt() |
| * if the argument is a number, and roughly ten times the speed, if |
| * the argument is NOT a number. |
| */ |
| |
| private boolean isNumber( String s ) |
| { |
| if( s == null ) return false; |
| |
| if( s.length() > 1 && s.charAt(0) == '-' ) |
| s = s.substring(1); |
| |
| for( int i = 0; i < s.length(); i++ ) |
| { |
| if( !Character.isDigit(s.charAt(i)) ) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Checks for the existence of a traditional style CamelCase link. |
| * <P> |
| * We separate all white-space -separated words, and feed it to this |
| * routine to find if there are any possible camelcase links. |
| * For example, if "word" is "__HyperLink__" we return "HyperLink". |
| * |
| * @param word A phrase to search in. |
| * @return The match within the phrase. Returns null, if no CamelCase |
| * hyperlink exists within this phrase. |
| */ |
| private String checkForCamelCaseLink( String word ) |
| { |
| PatternMatcherInput input; |
| |
| input = new PatternMatcherInput( word ); |
| |
| if( m_matcher.contains( input, m_camelCasePtrn ) ) |
| { |
| MatchResult res = m_matcher.getMatch(); |
| |
| int start = res.beginOffset(2); |
| int end = res.endOffset(2); |
| |
| String link = res.group(2); |
| String matchedLink; |
| |
| if( res.group(1) != null ) |
| { |
| if( res.group(1).equals("~") || |
| res.group(1).indexOf('[') != -1 ) |
| { |
| // Delete the (~) from beginning. |
| // We'll make '~' the generic kill-processing-character from |
| // now on. |
| return null; |
| } |
| } |
| |
| return link; |
| } // if match |
| |
| return null; |
| } |
| |
| /** |
| * When given a link to a WikiName, we just return |
| * a proper HTML link for it. The local link mutator |
| * chain is also called. |
| */ |
| private String makeCamelCaseLink( String wikiname ) |
| { |
| String matchedLink; |
| String link; |
| |
| callMutatorChain( m_localLinkMutatorChain, wikiname ); |
| |
| if( (matchedLink = linkExists( wikiname )) != null ) |
| { |
| link = makeLink( READ, matchedLink, wikiname ); |
| } |
| else |
| { |
| link = makeLink( EDIT, wikiname, wikiname ); |
| } |
| |
| return link; |
| } |
| |
| |
| /** |
| * Image links are handled differently: |
| * 1. If the text is a WikiName of an existing page, |
| * it gets linked. |
| * 2. If the text is an external link, then it is inlined. |
| * 3. Otherwise it becomes an ALT text. |
| * |
| * @param reallink The link to the image. |
| * @param link Link text portion, may be a link to somewhere else. |
| * @param hasLinkText If true, then the defined link had a link text available. |
| * This means that the link text may be a link to a wiki page, |
| * or an external resource. |
| */ |
| |
| private String handleImageLink( String reallink, String link, boolean hasLinkText ) |
| { |
| String possiblePage = cleanLink( link ); |
| String matchedLink; |
| String res = ""; |
| |
| if( isExternalLink( link ) && hasLinkText ) |
| { |
| res = makeLink( IMAGELINK, reallink, link ); |
| } |
| else if( (matchedLink = linkExists( possiblePage )) != null && |
| hasLinkText ) |
| { |
| // System.out.println("Orig="+link+", Matched: "+matchedLink); |
| callMutatorChain( m_localLinkMutatorChain, possiblePage ); |
| |
| res = makeLink( IMAGEWIKILINK, reallink, link ); |
| } |
| else |
| { |
| res = makeLink( IMAGE, reallink, link ); |
| } |
| |
| return res; |
| } |
| |
| /** |
| * If outlink images are turned on, returns a link to the outward |
| * linking image. |
| */ |
| private final String outlinkImage() |
| { |
| if( m_useOutlinkImage ) |
| { |
| return "<img class=\"outlink\" src=\""+m_engine.getBaseURL()+"images/out.png\" alt=\"\" />"; |
| } |
| |
| return ""; |
| } |
| |
| /** |
| * Gobbles up all hyperlinks that are encased in square brackets. |
| */ |
| private String handleHyperlinks( String link ) |
| { |
| StringBuffer sb = new StringBuffer(); |
| String reallink; |
| int cutpoint; |
| |
| // |
| // Start with plugin links. |
| // |
| if( PluginManager.isPluginLink( link ) ) |
| { |
| String included; |
| try |
| { |
| included = m_engine.getPluginManager().execute( m_context, link ); |
| } |
| catch( PluginException e ) |
| { |
| log.error( "Failed to insert plugin", e ); |
| log.error( "Root cause:",e.getRootThrowable() ); |
| included = "<FONT COLOR=\"#FF0000\">Plugin insertion failed: "+e.getMessage()+"</FONT>"; |
| } |
| |
| sb.append( included ); |
| |
| return sb.toString(); |
| } |
| |
| link = TextUtil.replaceEntities( link ); |
| |
| if( (cutpoint = link.indexOf('|')) != -1 ) |
| { |
| reallink = link.substring( cutpoint+1 ).trim(); |
| link = link.substring( 0, cutpoint ); |
| } |
| else |
| { |
| reallink = link.trim(); |
| } |
| |
| int interwikipoint = -1; |
| |
| // |
| // Yes, we now have the components separated. |
| // link = the text the link should have |
| // reallink = the url or page name. |
| // |
| // In many cases these are the same. [link|reallink]. |
| // |
| if( VariableManager.isVariableLink( link ) ) |
| { |
| String value; |
| |
| try |
| { |
| value = m_engine.getVariableManager().parseAndGetValue( m_context, link ); |
| } |
| catch( NoSuchVariableException e ) |
| { |
| value = "<FONT COLOR=\"#FF0000\">"+e.getMessage()+"</FONT>"; |
| } |
| catch( IllegalArgumentException e ) |
| { |
| value = "<FONT COLOR=\"#FF0000\">"+e.getMessage()+"</FONT>"; |
| } |
| |
| sb.append( value ); |
| } |
| else if( isExternalLink( reallink ) ) |
| { |
| // It's an external link, out of this Wiki |
| |
| callMutatorChain( m_externalLinkMutatorChain, reallink ); |
| |
| if( isImageLink( reallink ) ) |
| { |
| sb.append( handleImageLink( reallink, link, (cutpoint != -1) ) ); |
| } |
| else |
| { |
| sb.append( makeLink( EXTERNAL, reallink, link ) ); |
| sb.append( outlinkImage() ); |
| } |
| } |
| else if( (interwikipoint = reallink.indexOf(":")) != -1 ) |
| { |
| // It's an interwiki link |
| // InterWiki links also get added to external link chain |
| // after the links have been resolved. |
| |
| // FIXME: There is an interesting issue here: We probably should |
| // URLEncode the wikiPage, but we can't since some of the |
| // Wikis use slashes (/), which won't survive URLEncoding. |
| // Besides, we don't know which character set the other Wiki |
| // is using, so you'll have to write the entire name as it appears |
| // in the URL. Bugger. |
| |
| String extWiki = reallink.substring( 0, interwikipoint ); |
| String wikiPage = reallink.substring( interwikipoint+1 ); |
| |
| String urlReference = m_engine.getInterWikiURL( extWiki ); |
| |
| if( urlReference != null ) |
| { |
| urlReference = TextUtil.replaceString( urlReference, "%s", wikiPage ); |
| callMutatorChain( m_externalLinkMutatorChain, urlReference ); |
| |
| sb.append( makeLink( INTERWIKI, urlReference, link ) ); |
| |
| if( isExternalLink(urlReference) ) |
| { |
| sb.append( outlinkImage() ); |
| } |
| } |
| else |
| { |
| sb.append( link+" <FONT COLOR=\"#FF0000\">(No InterWiki reference defined in properties for Wiki called '"+extWiki+"'!)</FONT>"); |
| } |
| } |
| else if( reallink.startsWith("#") ) |
| { |
| // It defines a local footnote |
| sb.append( makeLink( LOCAL, reallink, link ) ); |
| } |
| else if( isNumber( reallink ) ) |
| { |
| // It defines a reference to a local footnote |
| sb.append( makeLink( LOCALREF, reallink, link ) ); |
| } |
| else |
| { |
| // |
| // Internal wiki link, but is it an attachment link? |
| // |
| String attachment = findAttachment( reallink ); |
| if( attachment != null ) |
| { |
| callMutatorChain( m_attachmentLinkMutatorChain, attachment ); |
| |
| if( isImageLink( reallink ) ) |
| { |
| attachment = m_engine.getBaseURL()+"attach?page="+attachment; |
| sb.append( handleImageLink( attachment, link, (cutpoint != -1) ) ); |
| } |
| else |
| { |
| sb.append( makeLink( ATTACHMENT, attachment, link ) ); |
| } |
| } |
| else |
| { |
| // It's an internal Wiki link |
| reallink = cleanLink( reallink ); |
| |
| callMutatorChain( m_localLinkMutatorChain, reallink ); |
| |
| String matchedLink; |
| if( (matchedLink = linkExists( reallink )) != null ) |
| { |
| sb.append( makeLink( READ, matchedLink, link ) ); |
| } |
| else |
| { |
| sb.append( makeLink( EDIT, reallink, link ) ); |
| } |
| } |
| } |
| |
| return sb.toString(); |
| } |
| |
| private String findAttachment( String link ) |
| { |
| AttachmentManager mgr = m_engine.getAttachmentManager(); |
| WikiPage currentPage = m_context.getPage(); |
| Attachment att = null; |
| |
| /* |
| System.out.println("Finding attachment of page "+currentPage.getName()); |
| System.out.println("With name "+link); |
| */ |
| |
| try |
| { |
| att = mgr.getAttachmentInfo( m_context, link ); |
| } |
| catch( ProviderException e ) |
| { |
| log.warn("Finding attachments failed: ",e); |
| return null; |
| } |
| |
| if( att != null ) |
| { |
| return att.getName(); |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Closes all annoying lists and things that the user might've |
| * left open. |
| */ |
| private String closeAll() |
| { |
| StringBuffer buf = new StringBuffer(); |
| |
| if( m_isbold ) |
| { |
| buf.append("</B>"); |
| m_isbold = false; |
| } |
| |
| if( m_isitalic ) |
| { |
| buf.append("</I>"); |
| m_isitalic = false; |
| } |
| |
| if( m_isTypedText ) |
| { |
| buf.append("</TT>"); |
| m_isTypedText = false; |
| } |
| |
| for( ; m_listlevel > 0; m_listlevel-- ) |
| { |
| buf.append( "</UL>\n" ); |
| } |
| |
| for( ; m_numlistlevel > 0; m_numlistlevel-- ) |
| { |
| buf.append( "</OL>\n" ); |
| } |
| |
| if( m_isPre ) |
| { |
| buf.append("</PRE>\n"); |
| m_isPre = false; |
| } |
| |
| if( m_istable ) |
| { |
| buf.append( "</TABLE>" ); |
| m_istable = false; |
| } |
| |
| return buf.toString(); |
| } |
| |
| /** |
| * Counts how many consecutive characters of a certain type exists on the line. |
| * @param line String of chars to check. |
| * @param startPos Position to start reading from. |
| * @param char Character to check for. |
| */ |
| private int countChar( String line, int startPos, char c ) |
| { |
| int count; |
| |
| for( count = 0; (startPos+count < line.length()) && (line.charAt(count+startPos) == c); count++ ); |
| |
| return count; |
| } |
| |
| /** |
| * Returns a new String that has char c n times. |
| */ |
| private String repeatChar( char c, int n ) |
| { |
| StringBuffer sb = new StringBuffer(); |
| for( int i = 0; i < n; i++ ) sb.append(c); |
| |
| return sb.toString(); |
| } |
| |
| private int nextToken() |
| throws IOException |
| { |
| return m_in.read(); |
| } |
| |
| /** |
| * Push back any character to the current input. Does not |
| * push back a read EOF, though. |
| */ |
| private void pushBack( int c ) |
| throws IOException |
| { |
| if( c != -1 ) |
| { |
| m_in.unread( c ); |
| } |
| } |
| |
| private String handleBackslash() |
| throws IOException |
| { |
| int ch = nextToken(); |
| |
| if( ch == '\\' ) |
| { |
| int ch2 = nextToken(); |
| |
| if( ch2 == '\\' ) |
| { |
| return "<BR clear=\"all\" />"; |
| } |
| |
| pushBack( ch2 ); |
| |
| return "<BR />"; |
| } |
| |
| pushBack( ch ); |
| |
| return "\\"; |
| } |
| |
| private String handleUnderscore() |
| throws IOException |
| { |
| int ch = nextToken(); |
| String res = "_"; |
| |
| if( ch == '_' ) |
| { |
| res = m_isbold ? "</B>" : "<B>"; |
| m_isbold = !m_isbold; |
| } |
| else |
| { |
| pushBack( ch ); |
| } |
| |
| return res; |
| } |
| |
| /** |
| * For example: italics. |
| */ |
| private String handleApostrophe() |
| throws IOException |
| { |
| int ch = nextToken(); |
| String res = "'"; |
| |
| if( ch == '\'' ) |
| { |
| res = m_isitalic ? "</I>" : "<I>"; |
| m_isitalic = !m_isitalic; |
| } |
| else |
| { |
| m_in.unread( ch ); |
| } |
| |
| return res; |
| } |
| |
| private String handleOpenbrace() |
| throws IOException |
| { |
| int ch = nextToken(); |
| String res = "{"; |
| |
| if( ch == '{' ) |
| { |
| int ch2 = nextToken(); |
| |
| if( ch2 == '{' ) |
| { |
| res = "<PRE>"; |
| m_isPre = true; |
| } |
| else |
| { |
| pushBack( ch2 ); |
| |
| res = "<TT>"; |
| m_isTypedText = true; |
| } |
| } |
| else |
| { |
| pushBack( ch ); |
| } |
| |
| return res; |
| } |
| |
| /** |
| * Handles both }} and }}} |
| */ |
| private String handleClosebrace() |
| throws IOException |
| { |
| String res = "}"; |
| |
| int ch2 = nextToken(); |
| |
| if( ch2 == '}' ) |
| { |
| int ch3 = nextToken(); |
| |
| if( ch3 == '}' ) |
| { |
| if( m_isPre ) |
| { |
| m_isPre = false; |
| res = "</PRE>"; |
| } |
| else |
| { |
| res = "}}}"; |
| } |
| } |
| else |
| { |
| pushBack( ch3 ); |
| |
| if( !m_isPre ) |
| { |
| res = "</TT>"; |
| m_isTypedText = false; |
| } |
| else |
| { |
| pushBack( ch2 ); |
| } |
| } |
| } |
| else |
| { |
| pushBack( ch2 ); |
| } |
| |
| return res; |
| } |
| |
| private String handleDash() |
| throws IOException |
| { |
| int ch = nextToken(); |
| |
| if( ch == '-' ) |
| { |
| int ch2 = nextToken(); |
| |
| if( ch2 == '-' ) |
| { |
| int ch3 = nextToken(); |
| |
| if( ch3 == '-' ) |
| { |
| // Empty away all the rest of the dashes. |
| // Do not forget to return the first non-match back. |
| while( (ch = nextToken()) == '-' ); |
| |
| pushBack(ch); |
| return "<HR />"; |
| } |
| |
| pushBack( ch3 ); |
| } |
| pushBack( ch2 ); |
| } |
| |
| pushBack( ch ); |
| |
| return "-"; |
| } |
| |
| private String handleHeading() |
| throws IOException |
| { |
| StringBuffer buf = new StringBuffer(); |
| |
| int ch = nextToken(); |
| |
| if( ch == '!' ) |
| { |
| int ch2 = nextToken(); |
| |
| if( ch2 == '!' ) |
| { |
| buf.append("<H2>"); |
| m_closeTag = "</H2>"; |
| } |
| else |
| { |
| buf.append( "<H3>" ); |
| m_closeTag = "</H3>"; |
| pushBack( ch2 ); |
| } |
| } |
| else |
| { |
| buf.append( "<H4>" ); |
| m_closeTag = "</H4>"; |
| pushBack( ch ); |
| } |
| |
| return buf.toString(); |
| } |
| |
| /** |
| * Reads the stream until the next EOL or EOF. |
| */ |
| private StringBuffer readUntilEOL() |
| throws IOException |
| { |
| int ch; |
| StringBuffer buf = new StringBuffer(); |
| |
| while( true ) |
| { |
| ch = nextToken(); |
| |
| if( ch == -1 || ch == '\n' ) |
| break; |
| |
| buf.append( (char) ch ); |
| } |
| |
| return buf; |
| } |
| |
| private int countChars( PushbackReader in, char c ) |
| throws IOException |
| { |
| int count = 0; |
| int ch; |
| |
| while( (ch = in.read()) != -1 ) |
| { |
| if( (char)ch == c ) |
| { |
| count++; |
| } |
| else |
| { |
| in.unread( ch ); |
| break; |
| } |
| } |
| |
| return count; |
| } |
| |
| private String handleUnorderedList() |
| throws IOException |
| { |
| StringBuffer buf = new StringBuffer(); |
| |
| if( m_listlevel > 0 ) |
| { |
| buf.append("</LI>\n"); |
| } |
| |
| int numBullets = countChars( m_in, '*' ) + 1; |
| |
| if( numBullets > m_listlevel ) |
| { |
| for( ; m_listlevel < numBullets; m_listlevel++ ) |
| buf.append("<UL>\n"); |
| } |
| else if( numBullets < m_listlevel ) |
| { |
| for( ; m_listlevel > numBullets; m_listlevel-- ) |
| buf.append("</UL>\n"); |
| } |
| |
| buf.append("<LI>"); |
| |
| return buf.toString(); |
| } |
| |
| private String handleOrderedList() |
| throws IOException |
| { |
| StringBuffer buf = new StringBuffer(); |
| |
| if( m_numlistlevel > 0 ) |
| { |
| buf.append("</LI>\n"); |
| } |
| |
| int numBullets = countChars( m_in, '#' ) + 1; |
| |
| if( numBullets > m_numlistlevel ) |
| { |
| for( ; m_numlistlevel < numBullets; m_numlistlevel++ ) |
| buf.append("<OL>\n"); |
| } |
| else if( numBullets < m_numlistlevel ) |
| { |
| for( ; m_numlistlevel > numBullets; m_numlistlevel-- ) |
| buf.append("</OL>\n"); |
| } |
| |
| buf.append("<LI>"); |
| |
| return buf.toString(); |
| |
| } |
| |
| private String handleDefinitionList() |
| throws IOException |
| { |
| if( !m_isdefinition ) |
| { |
| m_isdefinition = true; |
| |
| m_closeTag = "</DD>\n</DL>"; |
| |
| return "<DL>\n<DT>"; |
| } |
| |
| return ";"; |
| } |
| |
| private String handleOpenbracket() |
| throws IOException |
| { |
| StringBuffer sb = new StringBuffer(); |
| int ch; |
| |
| while( (ch = nextToken()) == '[' ) |
| { |
| sb.append( (char)ch ); |
| } |
| |
| pushBack( ch ); |
| |
| if( sb.length() > 0 ) |
| { |
| return sb.toString(); |
| } |
| |
| while( true ) |
| { |
| ch = nextToken(); |
| |
| if( ch == -1 || ch == ']' ) |
| break; |
| |
| sb.append( (char) ch ); |
| } |
| |
| if( ch == -1 ) |
| { |
| log.info("Warning: unterminated link detected!"); |
| return sb.toString(); |
| } |
| |
| return handleHyperlinks( sb.toString() ); |
| } |
| |
| private String handleBar( boolean newLine ) |
| throws IOException |
| { |
| StringBuffer sb = new StringBuffer(); |
| |
| if( !m_istable && !newLine ) |
| { |
| return "|"; |
| } |
| |
| if( newLine ) |
| { |
| if( !m_istable ) |
| { |
| sb.append("<TABLE CLASS=\"wikitable\" BORDER=\"1\">\n"); |
| m_istable = true; |
| } |
| |
| sb.append("<TR>"); |
| m_closeTag = "</TD></TR>"; |
| } |
| |
| int ch = nextToken(); |
| |
| if( ch == '|' ) |
| { |
| if( !newLine ) |
| { |
| sb.append("</TH>"); |
| } |
| sb.append("<TH>"); |
| m_closeTag = "</TH></TR>"; |
| } |
| else |
| { |
| if( !newLine ) |
| { |
| sb.append("</TD>"); |
| } |
| sb.append("<TD>"); |
| pushBack( ch ); |
| } |
| |
| return sb.toString(); |
| } |
| |
| /** |
| * Generic escape of next character or entity. |
| */ |
| private String handleTilde() |
| throws IOException |
| { |
| int ch = nextToken(); |
| |
| if( ch == '|' ) |
| return "|"; |
| |
| if( Character.isUpperCase( (char) ch ) ) |
| { |
| return String.valueOf( (char)ch ); |
| } |
| |
| // No escape. |
| pushBack( ch ); |
| |
| return "~"; |
| } |
| |
| private void fillBuffer() |
| throws IOException |
| { |
| StringBuffer buf = new StringBuffer(); |
| StringBuffer word = null; |
| int previousCh = -2; |
| int start = 0; |
| |
| boolean quitReading = false; |
| boolean newLine = true; // FIXME: not true if reading starts in middle of buffer |
| |
| while(!quitReading) |
| { |
| int ch = nextToken(); |
| String s = null; |
| |
| // |
| // Check if we're actually ending the preformatted mode. |
| // We still must do an entity transformation here. |
| // |
| if( m_isPre ) |
| { |
| if( ch == '}' ) |
| { |
| buf.append( handleClosebrace() ); |
| } |
| else if( ch == '<' ) |
| { |
| buf.append("<"); |
| } |
| else if( ch == '>' ) |
| { |
| buf.append(">"); |
| } |
| else if( ch == -1 ) |
| { |
| quitReading = true; |
| } |
| else |
| { |
| buf.append( (char)ch ); |
| } |
| |
| continue; |
| } |
| |
| // |
| // CamelCase detection, a non-trivial endeavour. |
| // We keep track of all white-space separated entities, which we |
| // hereby refer to as "words". We then check for an existence |
| // of a CamelCase format text string inside the "word", and |
| // if one exists, we replace it with a proper link. |
| // |
| |
| if( m_camelCaseLinks ) |
| { |
| // Quick parse of start of a word boundary. |
| |
| if( word == null && |
| (Character.isWhitespace( (char)previousCh ) || |
| WORD_SEPARATORS.indexOf( (char)previousCh ) != -1 || |
| newLine ) && |
| !Character.isWhitespace( (char) ch ) ) |
| { |
| word = new StringBuffer(); |
| } |
| |
| // Are we currently tracking a word? |
| if( word != null ) |
| { |
| // |
| // Check for the end of the word. |
| // |
| |
| if( Character.isWhitespace( (char)ch ) || |
| ch == -1 || |
| WORD_SEPARATORS.indexOf( (char) ch ) != -1 ) |
| { |
| String potentialLink = word.toString(); |
| |
| String camelCase = checkForCamelCaseLink(potentialLink); |
| |
| if( camelCase != null ) |
| { |
| // System.out.println("Buffer is "+buf); |
| |
| // System.out.println(" Replacing "+camelCase+" with proper link."); |
| start = buf.toString().lastIndexOf( camelCase ); |
| buf.replace(start, |
| start+camelCase.length(), |
| makeCamelCaseLink(camelCase) ); |
| |
| // System.out.println(" Resulting with "+buf); |
| } |
| |
| // We've ended a word boundary, so time to reset. |
| word = null; |
| } |
| else |
| { |
| // This should only be appending letters and digits. |
| word.append( (char)ch ); |
| } // if end of word |
| } // if word's not null |
| |
| // Always set the previous character to test for word starts. |
| previousCh = ch; |
| |
| } // if m_camelCaseLinks |
| |
| // |
| // Check if any lists need closing down. |
| // |
| |
| if( newLine && ch != '*' && ch != ' ' && m_listlevel > 0 ) |
| { |
| buf.append("</LI>\n"); |
| for( ; m_listlevel > 0; m_listlevel-- ) |
| { |
| buf.append("</UL>\n"); |
| } |
| } |
| |
| if( newLine && ch != '#' && ch != ' ' && m_numlistlevel > 0 ) |
| { |
| buf.append("</LI>\n"); |
| for( ; m_numlistlevel > 0; m_numlistlevel-- ) |
| { |
| buf.append("</OL>\n"); |
| } |
| } |
| |
| if( newLine && ch != '|' && m_istable ) |
| { |
| buf.append("</TABLE>\n"); |
| m_istable = false; |
| m_closeTag = null; |
| } |
| |
| // |
| // Now, check the incoming token. |
| // |
| switch( ch ) |
| { |
| case '\r': |
| // DOS linefeeds we forget |
| s = null; |
| break; |
| |
| case '\n': |
| // |
| // Close things like headings, etc. |
| // |
| if( m_closeTag != null ) |
| { |
| buf.append( m_closeTag ); |
| m_closeTag = null; |
| } |
| |
| m_isdefinition = false; |
| |
| if( newLine ) |
| { |
| // Paragraph change. |
| |
| buf.append("<P>\n"); |
| } |
| else |
| { |
| buf.append("\n"); |
| newLine = true; |
| } |
| |
| break; |
| |
| case '\\': |
| s = handleBackslash(); |
| break; |
| |
| case '_': |
| s = handleUnderscore(); |
| break; |
| |
| case '\'': |
| s = handleApostrophe(); |
| break; |
| |
| case '{': |
| s = handleOpenbrace(); |
| break; |
| |
| case '}': |
| s = handleClosebrace(); |
| break; |
| |
| case '-': |
| s = handleDash(); |
| break; |
| |
| case '!': |
| if( newLine ) |
| { |
| s = handleHeading(); |
| } |
| else |
| { |
| s = "!"; |
| } |
| break; |
| |
| case ';': |
| if( newLine ) |
| { |
| s = handleDefinitionList(); |
| } |
| else |
| { |
| s = ";"; |
| } |
| break; |
| |
| case ':': |
| if( m_isdefinition ) |
| { |
| s = "</DT><DD>"; |
| m_isdefinition = false; |
| } |
| else |
| { |
| s = ":"; |
| } |
| break; |
| |
| case '[': |
| s = handleOpenbracket(); |
| break; |
| |
| case '*': |
| if( newLine ) |
| { |
| s = handleUnorderedList(); |
| } |
| else |
| { |
| s = "*"; |
| } |
| break; |
| |
| case '#': |
| if( newLine ) |
| { |
| s = handleOrderedList(); |
| } |
| else |
| { |
| s = "#"; |
| } |
| break; |
| |
| case '|': |
| s = handleBar( newLine ); |
| break; |
| |
| case '<': |
| s = m_allowHTML ? "<" : "<"; |
| break; |
| |
| case '>': |
| s = m_allowHTML ? ">" : ">"; |
| break; |
| |
| case '\"': |
| s = m_allowHTML ? "\"" : """; |
| break; |
| |
| /* |
| case '&': |
| s = "&"; |
| break; |
| */ |
| case '~': |
| s = handleTilde(); |
| break; |
| |
| case -1: |
| if( m_closeTag != null ) |
| { |
| buf.append( m_closeTag ); |
| m_closeTag = null; |
| } |
| quitReading = true; |
| break; |
| |
| default: |
| buf.append( (char)ch ); |
| newLine = false; |
| break; |
| } |
| |
| if( s != null ) |
| { |
| buf.append( s ); |
| newLine = false; |
| } |
| |
| } |
| |
| m_data = new StringReader( buf.toString() ); |
| } |
| |
| |
| public int read() |
| throws IOException |
| { |
| int val = m_data.read(); |
| |
| if( val == -1 ) |
| { |
| fillBuffer(); |
| val = m_data.read(); |
| |
| if( val == -1 ) |
| { |
| m_data = new StringReader( closeAll() ); |
| |
| val = m_data.read(); |
| } |
| } |
| |
| return val; |
| } |
| |
| public int read( char[] buf, int off, int len ) |
| throws IOException |
| { |
| return m_data.read( buf, off, len ); |
| } |
| |
| public boolean ready() |
| throws IOException |
| { |
| log.debug("ready ? "+m_data.ready() ); |
| if(!m_data.ready()) |
| { |
| fillBuffer(); |
| } |
| |
| return m_data.ready(); |
| } |
| |
| public void close() |
| { |
| } |
| } |