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