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", "")), " ", " "));
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 }