001    /*
002     * Copyright 2004-2006 Geert Bevin <gbevin[remove] at uwyn dot com>
003     * Distributed under the terms of either:
004     * - the common development and distribution license (CDDL), v1.0; or
005     * - the GNU Lesser General Public License, v2.1 or later
006     * $Id: XhtmlRenderer.java 3108 2006-03-13 18:03:00Z gbevin $
007     */
008    package com.uwyn.jhighlight.renderer;
009    
010    import java.io.*;
011    
012    import com.uwyn.jhighlight.JHighlightVersion;
013    import com.uwyn.jhighlight.highlighter.ExplicitStateHighlighter;
014    import com.uwyn.jhighlight.tools.ExceptionUtils;
015    import com.uwyn.jhighlight.tools.StringUtils;
016    import java.net.URL;
017    import java.net.URLConnection;
018    import java.util.Iterator;
019    import java.util.Map;
020    import java.util.Properties;
021    import java.util.logging.Logger;
022    
023    /**
024     * Provides an abstract base class to perform source code to XHTML syntax
025     * highlighting.
026     *
027     * @author Geert Bevin (gbevin[remove] at uwyn dot com)
028     * @version $Revision: 3108 $
029     * @since 1.0
030     */
031    public abstract class XhtmlRenderer implements Renderer
032    {
033            /**
034             * Transforms source code that's provided through an
035             * <code>InputStream</code> to highlighted syntax in XHTML and writes it
036             * back to an <code>OutputStream</code>.
037             * <p>If the highlighting has to become a fragment, no CSS styles will be
038             * generated.
039             * <p>For complete documents, there's a collection of default styles that
040             * will be included. It's possible to override these by changing the
041             * provided <code>jhighlight.properties</code> file. It's best to look at
042             * this file in the JHighlight archive and modify the styles that are
043             * there already.
044             *
045             * @param name The name of the source file.
046             * @param in The input stream that provides the source code that needs to
047             * be transformed.
048             * @param out The output stream to which to resulting XHTML should be
049             * written.
050             * @param encoding The encoding that will be used to read and write the
051             * text.
052             * @param fragment <code>true</code> if the generated XHTML should be a
053             * fragment; or <code>false</code> if it should be a complete page
054             * @see #highlight(String, String, String, boolean)
055             * @since 1.0
056             */
057            public void highlight(String name, InputStream in, OutputStream out, String encoding, boolean fragment)
058            throws IOException
059            {
060                    ExplicitStateHighlighter highlighter = getHighlighter();
061                    
062                    Reader isr;
063                    Writer osw;
064                    if (null == encoding)
065                    {
066                            isr = new InputStreamReader(in);
067                            osw = new OutputStreamWriter(out);
068                    }
069                    else
070                    {
071                            isr = new InputStreamReader(in, encoding);
072                            osw = new OutputStreamWriter(out, encoding);
073                    }
074                    
075                    BufferedReader r = new BufferedReader(isr);
076                    BufferedWriter w = new BufferedWriter(osw);
077                    
078                    if (fragment)
079                    {
080                            w.write(getXhtmlHeaderFragment(name));
081                    }
082                    else
083                    {
084                            w.write(getXhtmlHeader(name));
085                    }
086                    
087                    String line;
088                    String token;
089                    int length;
090                    int style;
091                    String css_class;
092                    int previous_style = 0;
093                    boolean newline = false;
094                    while ((line = r.readLine()) != null)
095                    {
096                            line += "\n";
097                            line = StringUtils.convertTabsToSpaces(line, 4);
098                            
099                            // should be optimized by reusing a custom LineReader class
100                            Reader lineReader = new StringReader(line);
101                            highlighter.setReader(lineReader);
102                            int index = 0;
103                            while (index < line.length())
104                            {
105                                    style = highlighter.getNextToken();
106                                    length = highlighter.getTokenLength();
107                                    token = line.substring(index, index + length);
108                                    
109                                    if (style != previous_style ||
110                                            newline)
111                                    {
112                                            css_class = getCssClass(style);
113                                            
114                                            if (css_class != null)
115                                            {
116                                                    if (previous_style != 0 && !newline)
117                                                    {
118                                                            w.write("</span>");
119                                                    }
120                                                    w.write("<span class=\"" + css_class + "\">");
121                                                    
122                                                    previous_style = style;
123                                            }
124                                    }
125                                    newline = false;                                        
126                                    w.write(StringUtils.replace(StringUtils.encodeHtml(StringUtils.replace(token, "\n", "")), " ", "&nbsp;"));
127                                    
128                                    index += length;
129                            }
130                            
131                            w.write("</span><br />\n");
132                            newline = true;
133                    }
134                    
135                    if (!fragment) w.write(getXhtmlFooter());
136                    
137                    w.flush();
138                    w.close();
139            }
140            
141            /**
142             * Transforms source code that's provided through a
143             * <code>String</code> to highlighted syntax in XHTML and returns it
144             * as a <code>String</code>.
145             * <p>If the highlighting has to become a fragment, no CSS styles will be
146             * generated.
147             *
148             * @param name The name of the source file.
149             * @param in The input string that provides the source code that needs to
150             * be transformed.
151             * @param encoding The encoding that will be used to read and write the
152             * text.
153             * @param fragment <code>true</code> if the generated XHTML should be a
154             * fragment; or <code>false</code> if it should be a complete page
155             * or <code>false</code> if it should be a complete document
156             * @return the highlighted source code as XHTML in a string
157             * @see #highlight(String, InputStream, OutputStream, String, boolean)
158             * @since 1.0
159             */
160            public String highlight(String name, String in, String encoding, boolean fragment)
161            throws IOException
162            {
163                    ByteArrayOutputStream out = new ByteArrayOutputStream();
164                    highlight(name, new StringBufferInputStream(in), out, encoding, fragment);
165                    return out.toString(encoding);
166            }
167                    
168            /**
169             * Returns a map of all the CSS styles that the renderer requires,
170             * together with default definitions for them.
171             *
172             * @return The map of CSS styles.
173             * @since 1.0
174             */
175            protected abstract Map getDefaultCssStyles();
176            
177            /**
178             * Looks up the CSS class identifier that corresponds to the syntax style.
179             *
180             * @param style The syntax style.
181             * @return The requested CSS class identifier; or
182             * <p><code>null</code> if the syntax style isn't supported.
183             * @since 1.0
184             */
185            protected abstract String getCssClass(int style);
186            
187            /**
188             * Returns the language-specific highlighting lexer that should be used
189             *
190             * @return The requested highlighting lexer.
191             * @since 1.0
192             */
193            protected abstract ExplicitStateHighlighter getHighlighter();
194            
195            /**
196             * Returns all the CSS class definitions that should appear within the
197             * <code>style</code> XHTML tag.
198             * <p>This should support all the classes that the
199             * <code>getCssClass(int)</code> method returns.
200             *
201             * @return The CSS class definitions
202             * @see #getCssClass(int)
203             * @since 1.0
204             */
205            protected String getCssClassDefinitions()
206            {
207                    StringBuffer css = new StringBuffer();
208                    
209                    Properties properties = new Properties();
210                    
211                    URL jhighlighter_props = getClass().getClassLoader().getResource("jhighlight.properties");
212                    if (jhighlighter_props != null)
213                    {
214                            try
215                            {
216                                    URLConnection connection = jhighlighter_props.openConnection();
217                                    connection.setUseCaches(false);
218                                    InputStream is = connection.getInputStream();
219                                    
220                                    try
221                                    {
222                                            properties.load(is);
223                                    }
224                                    finally
225                                    {
226                                            is.close();
227                                    }
228                            }
229                            catch (IOException e)
230                            {
231                                    Logger.getLogger("com.uwyn.jhighlight").warning("Error while reading the '" + jhighlighter_props.toExternalForm() + "' resource, using default CSS styles.\n" + ExceptionUtils.getExceptionStackTrace(e));
232                            }
233                    }
234                    
235                    Iterator it = getDefaultCssStyles().entrySet().iterator();
236                    Map.Entry entry;
237                    while (it.hasNext())
238                    {
239                            entry = (Map.Entry)it.next();
240                            
241                            String key = (String)entry.getKey();
242                            
243                            css.append(key);
244                            css.append(" {\n");
245                            
246                            if (properties.containsKey(key))
247                            {
248                                    css.append(properties.get(key));
249                            }
250                            else
251                            {
252                                    css.append(entry.getValue());
253                            }
254                            
255                            css.append("\n}\n");
256                    }
257                    
258                    return css.toString();
259            }
260            
261            /**
262             * Returns the XHTML header that preceedes the highlighted source code.
263             * <p>It will integrate the CSS class definitions and use the source's
264             * name to indicate in XHTML which file has been highlighted.
265             *
266             * @param name The name of the source file.
267             * @return The constructed XHTML header.
268             * @since 1.0
269             */
270            protected String getXhtmlHeader(String name)
271            {
272                    if (null == name)
273                    {
274                            name = "";
275                    }
276                    
277                    return
278                            "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n" +
279                            "                      \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n" +
280                            "<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\" lang=\"en\">\n" +
281                            "<head>\n" +
282                            "    <meta http-equiv=\"content-type\" content=\"text/html; charset=ISO-8859-1\" />\n" +
283                            "    <meta name=\"generator\" content=\"JHighlight v"+JHighlightVersion.getVersion()+" (http://jhighlight.dev.java.net)\" />\n" +
284                            "    <title>" + StringUtils.encodeHtml(name) + "</title>\n" +
285                            "    <link rel=\"Help\" href=\"http://jhighlight.dev.java.net\" />\n" +
286                            "    <style type=\"text/css\">\n" +
287                            getCssClassDefinitions() +
288                            "    </style>\n" +
289                            "</head>\n" +
290                            "<body>\n" +
291                            "<h1>" + StringUtils.encodeHtml(name) + "</h1>" +
292                            "<code>";
293            }
294            
295            /**
296             * Returns the XHTML header that preceedes the highlighted source code for
297             * a fragment.
298             *
299             * @param name The name of the source file.
300             * @return The constructed XHTML header.
301             * @since 1.0
302             */
303            protected String getXhtmlHeaderFragment(String name)
304            {
305                    if (null == name)
306                    {
307                            name = "";
308                    }
309                    
310                    return "<!-- "+name+" : generated by JHighlight v"+JHighlightVersion.getVersion()+" (http://jhighlight.dev.java.net) -->\n";
311            }
312            
313            /**
314             * Returns the XHTML footer that nicely finishes the file after the
315             * highlighted source code.
316             *
317             * @return The requested XHTML footer.
318             * @since 1.0
319             */
320            protected String getXhtmlFooter()
321            {
322                    return "</code>\n</body>\n</html>\n";
323                    
324            }
325    }