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 3106 2006-03-13 17:53:50Z gbevin $
007 */
008package com.uwyn.jhighlight.renderer;
009
010import java.io.*;
011
012import com.uwyn.jhighlight.JHighlightVersion;
013import com.uwyn.jhighlight.highlighter.ExplicitStateHighlighter;
014import com.uwyn.jhighlight.tools.ExceptionUtils;
015import com.uwyn.jhighlight.tools.StringUtils;
016import java.net.URL;
017import java.net.URLConnection;
018import java.util.Iterator;
019import java.util.Map;
020import java.util.Properties;
021import 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: 3106 $
029 * @since 1.0
030 */
031public 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}