package de.infinityloop.upcast.exportfilters;


import java.io.*;
import java.util.*;

import javax.swing.*;

import org.xml.sax.Attributes;

import de.infinityloop.common.Constants;
import de.infinityloop.util.Pair;
import de.infinityloop.util.gui.ParagraphLayout;
import de.infinityloop.util.inout.DiskFile;
import de.infinityloop.util.style.CSSProperties;
import de.infinityloop.util.style.CSSPropertyFilter;

/**
 * Implementation of an XHTML 1.0 strict export filter, based on the API for
 * custom export filters.
 *
 * Status: alpha
 *
 * @author Christian Roth <cr@infinity-loop.de>
 * @version 1.0
 */

public class ExportFilter5 extends ExportFilterBase {

//
// Required configuration constants
//
  /** Filter version */
  protected static final int kFilterVersion = 0x0200; /* 2.0.0 */
  
//
// Constants
//
  /** indent 2 spaces per level */
  private static final int indentationFactor = 2; // indent 2 spaces per level

  // Parameter name constants
  /** Write TOC contents; boolean */
  private static final String kWriteTOCParamName = "WriteTOC";

  /** Allow completely empty cells in tables; boolean */
  public static final String kAllowEmptyCellsParamName = "AllowEmptyCells";
  
  /** Markup Revision tracking; boolean; */
  public static final String kRevisionTrackingParamName = "RevisionTracking";

  /** Form handling; String: discard | text | form */
  public static final String kFormHandlingParamName = "FormHandling";
    
  /* Form handling mode constants: */
  public static final int kFormDiscard = 0;
  public static final int kFormText = 1;
  public static final int kFormForm = 2;

//
// Local variables
//

  /** The writer output should be written to. For buffering ans tranfer
      reasons, the current writer will change during working on the document. */
  Writer w;
  
  /** The original writer passed as destination in startDocument() */
  Writer origWriter;

  /** counter of element nesting */
  int nesting = 0;

  /** true only directly after a newline has been written. Used for indentation tracking. */
  boolean previousNewline = false;

  /** If we are in a heading, XHTML won't allow us to use anything other
      than "inline"-Elements, so we need to keep track of that */
  protected boolean inHeading = false;
  
  /** If we are in a paragraph, this is true. Used to determine if we artificially
      must bracket <img>s by <p>...</p> if not currently in a paragraph */
  protected Stack inInline = new Stack();

  /** If true, no inline elements shall be written. Used with <a ...> tags,
      which render bad on MSIE if <span> tags are embedded. */
  protected boolean suppressInlines = false;

  // Footnote Handling
  /** Counts footnotes for automatic numbering */
  protected int footNoteCounter = 0;
  
  /** We collect the XHTML-rendered footnote contents in here and write that
      as chunk at the end of the document. */
  protected StringWriter footNotes = new StringWriter();

  // Index Handling
  /** This is an alphabetically sorted vector of index targets. Sort based on index term. */
  protected TreeMap indexEntries = new TreeMap();
  
  /** Counts index entries, used for building the reference name */
  protected int indexEntryRefId = 0;
  
  /** Target handling (collect while in <url>) */
  protected Stack targetStack = new Stack();
  
  /** is true when handling a <link> element */
  protected boolean inURL = false;
  
  /** For Annotations: these are rendered as comments, so we only want the text! */
  protected boolean inAnnotation = false;

  /** The hidden state stack */
  protected Stack hiddenStateStk = new Stack();
  
  /** The heading number stack. */
  protected Stack headingNumberStack = new Stack();

  /** The saved indentation level */
  protected int savedLevel = 0;

  /** The stack of open list(-tag)s */
  protected Stack openLists = new Stack();

  /** Set to true when writing character content */
  protected boolean contentSeen = false;

  /** Dummy stream for trashing contents */
  protected Stack writerStack = new Stack();

  /** Dummy stream for trashing contents */
  protected StringWriter devnull = new StringWriter();

  /** True, when title element has been written */
  protected boolean titleWritten = false;
    
  /** Holds the default selection for dropdown list */
  protected int dropDownDefault = -999;
  
  /** Holds delayed indextarget entries which only may occur in inlines (i.e. in headings, par, ...) */
  protected StringWriter indextargets = new StringWriter();
  
  /* -------------------------------
   * Pre-defined property filters
   * ------------------------------*/
  
  /** Property filter for element: <par> */
  CSSPropertyFilter parFilter = new CSSPropertyFilter( CSSPropertyFilter.kExcludeListed, new String [] { "-ilx-*", "list*", "widows", "orphans" } );

  /** Property filter for element: <...> */
  CSSPropertyFilter defaultFilter = new CSSPropertyFilter( CSSPropertyFilter.kExcludeListed, new String [] { "-ilx-*", "widows", "orphans" } );

  /** Property filter for element: <inline>, <gentext> */
  CSSPropertyFilter inlineFilter = new CSSPropertyFilter( CSSPropertyFilter.kExcludeListed, new String [] { "-ilx-*", "widows", "orphans" } );
  
  /** Property filter for element: <list> */
  CSSPropertyFilter listFilter = new CSSPropertyFilter( CSSPropertyFilter.kExcludeListed, new String [] { "-ilx-*", "widows", "orphans" } );
  

//
// Preferences Cache
//
  /** a boolean indicating whether we allow completely empty cells (true) or whether we should put a '&nbsp;' in them. */
  boolean prefAllowEmptyCells = false;

  /** true when wanting to create an inline CSS stylesheet; currently NOT SUPPORTED */
  private boolean prefInlineStylesheet = false;

  /** alternative DOCTYPE string to write; leave empty when no DOCTYPE desired */
  private String prefDoctypeString = "*";
  
  /** true when contents of TOC elements should be written */
  private boolean prefWriteTOC = false;

  /** holds the name of the stylesheet to reference */
  private String prefCSSRef = "";
  
  /** Markup revisions (inserted, deleted) */
  private boolean prefMarkupRevision = false;

  /** Include visual/layout information */
  private boolean prefVisual = false;
  
  /** Form handling mode */
  private int prefFormHandling = kFormText;
  private String prefFormURI = "";
  

// *******************************************************
// Overrides of base class methods
// *******************************************************

  /**
   * Initialize global instance variables. Called automatically
   * beofre starting a conversion.
   */
  public void reset() {
    
    super.reset();
    
    inHeading = false;
    
    inInline = new Stack();
    inInline.push( new Boolean( false ) ); // we start out in block
    
    suppressInlines = false;
    footNoteCounter = 0;
    footNotes = new StringWriter();
    indexEntries = new TreeMap();
    indexEntryRefId = 0;
    targetStack = new Stack();
    inURL = false;
    inAnnotation = false;
    headingNumberStack = new Stack();
    hiddenStateClear();
    hiddenStatePush( false ); // In the beginning, all is visible.
    openLists = new Stack();
    contentSeen = false;
    devnull = new StringWriter();
    writerStack = new Stack();
    nesting = 0;
    titleWritten = false;
    dropDownDefault = -999;

    indextargets = new StringWriter();
  }

  /**
   * Module class name
   */
  public String getModuleClassName() { return "XHTML 1.0 (strict)"; }

  /**
   * Returns the name under which this module/filter is referred to
   * in human scripting/BCF languages.
   */
  public String getIdentificationName() { return ExportFilter.kXHTMLFilterName; }

    
// *******************************************************
// Implementation of "callbacks"
// *******************************************************

  /**
   * returns a human readable string of the current configuration.
   */
  protected String getHumanReadableConfigurationDescription() {
    StringBuffer result = new StringBuffer();
    
    if( prefVisual )
      result.append( ", include layout" );
      
    if( prefWriteTOC )
      result.append( ", add <TOC> element contents" );
      
    if( prefInlineStylesheet )
      result.append( ", inline CSS stylesheet" );
    else if( prefCSSRef != null )
      result.append( ", reference stylesheet '" + prefCSSRef + "'" );
      
    return result.toString();
  }
  

  /**
   * Notification that preferences might have changed. Update cache from current values.
   */
  protected void updateConfiguration() {
    String tmp;
    
    /* Configuration of our caller. Since this gets called before our parent's
     * update code, we can override any user selection and "wrong" settings
     * the user may have coded by hand in the preferences file to our liking.
     */
    setParameter( "IncludeHiddenContents", new Boolean( true ) );

    prefWriteTOC = getParameterBoolean( kWriteTOCParamName, false );
    prefAllowEmptyCells = getParameterBoolean( kAllowEmptyCellsParamName, false );
    prefInlineStylesheet = getParameterBoolean( kInlineCSSStylesheetParamName, false );
    prefDoctypeString = getParameterString( kDOCTYPEDeclarationParamName, "*" );
    prefMarkupRevision = getParameterBoolean( kRevisionTrackingParamName, false );
    prefVisual = getParameterBoolean( kIncludeVisualElementsParamName, true );

    // Calculate stylesheet link destination
    String tmpCSSRef = getParameterString( kCSSRefParamName, "${il:srcbasename}.css" );
    if( tmpCSSRef.length() == 0 ) // The name must be at least one character long!
      prefCSSRef = null;
    else
      prefCSSRef = tmpCSSRef;

    if( prefInlineStylesheet )
      prefCSSRef = null; // We do not allow a link to the external stylesheet when writing an inline stylesheet
      
    tmp = getParameterString( kFormHandlingParamName, "text" );
    if( tmp.equals( "discard" ) ) {
      prefFormHandling = kFormDiscard;
      prefFormURI = "";
    } else if( tmp.equals( "text" ) ) {
      prefFormHandling = kFormText;
      prefFormURI = "";
    } else if( tmp.equals( "form" ) ) {
      prefFormHandling = kFormForm;
      prefFormURI = "";
    }


    gprefUseNamespace = false;
  }


  /**
   * Notification that the filter's preferences should be initialized. Called
   * directly after instantiation, e.g.
   */
  protected void initializeConfiguration() {
    setParameter( kWriteTOCParamName, new Boolean( false ) );
    setParameter( kAllowEmptyCellsParamName, new Boolean( false ) );
    setParameter( kInlineCSSStylesheetParamName, new Boolean( false ) );
    setParameter( kDOCTYPEDeclarationParamName, "*" );
    setParameter( kSuffixParamName, new String( ".html" ) );
    setParameter( kIncludeVisualElementsParamName, new Boolean( "true" ) );
    setParameter( kCSSRefParamName, new String( "${il:srcbasename}.css" ) );
    setParameter( kFormHandlingParamName, new String( "text" ) );

    // Configuration of our caller.
    setParameter( kIncludeHiddenContentsParamName, new Boolean( true ) );
    setParameter( kUnicodeTranslationMapParamName, new String( "upcast:html-map" ) );
  }

  /**
   * Start of a document traversal.
   */
  protected void startDocument( Writer destination, String destinationSystemID ) {
    w = origWriter = destination;
        
    if( prefCSSRef != null )
      prefCSSRef = resolveExpression( prefCSSRef );
    
  }

  /**
   * End of document traversal.
   */
  protected void endDocument() {
    ; // nothing to do here
  }

  /**
   * Element start.
   */
  protected void startElement( String uri, String localName, String qName, Attributes attr ) {
    
    /* We decide based on the local name.
     * It is guaranteed that this is always there and valid.
     */
    
    if( inAnnotation ) // Do not handle any elements, but only text content.
      return;
    
    try {
    
      if( localName.equals( "document" ) )
        handleDocumentTag( true, attr );
      else if( localName.equals( "documentinfo" ) )
        handleDocumentinfoTag( true, attr );
      else if( localName.equals( "property" ) )
        handlePropertyTag( true, attr );
      else if( localName.equals( "part" ) )
        handlePartTag( true, attr );
      else if( localName.equals( "section" ) )
        handleSectionTag( true, attr );
      else if( localName.equals( "heading" ) )
        handleHeadingTag( true, attr );
      else if( localName.equals( "par" ) )
        handleParTag( true, attr );
      else if( localName.equals( "inline" ) )
        handleInlineTag( true, attr );
      else if( localName.equals( "gentext" ) )
        handleGentextTag( true, attr );
      else if( localName.equals( "link" ) )
        handleLinkTag( true, attr );
      else if( localName.equals( "reference" ) )
        handleReferenceTag( true, attr );
      else if( localName.equals( "textbox" ) )
        handleTextboxTag( true, attr );
      else if( localName.equals( "image" ) )
        handleImageTag( true, attr );
      else if( localName.equals( "target" ) )
        handleTargetTag( true, attr );
      else if( localName.equals( "annotation" ) )
        handleAnnotationTag( true, attr );
      else if( localName.equals( "footnote" ) )
        handleFootnoteTag( true, attr );
      else if( localName.equals( "endnote" ) )
        handleEndnoteTag( true, attr );
      else if( localName.equals( "list" ) )
        handleListTag( true, attr );
      else if( localName.equals( "item" ) )
        handleItemTag( true, attr );
      else if( localName.equals( "table" ) )
        handleTableTag( true, attr );
      else if( localName.equals( "colgroup" ) )
        handleColgroupTag( true, attr );
      else if( localName.equals( "col" ) )
        handleColTag( true, attr );
      else if( localName.equals( "thead" ) )
        handleTheadTag( true, attr );
      else if( localName.equals( "tbody" ) )
        handleTbodyTag( true, attr );
      else if( localName.equals( "tr" ) )
        handleTrTag( true, attr );
      else if( localName.equals( "td" ) )
        handleTdTag( true, attr );
      else if( localName.equals( "indextarget" ) )
        handleIndextargetTag( true, attr );
      else if( localName.equals( "inserted" ) )
        handleInsertedTag( true, attr );
      else if( localName.equals( "deleted" ) )
        handleDeletedTag( true, attr );
      else if( localName.equals( "hidden" ) )
        handleHiddenTag( true, attr );
      else if( localName.equals( "pageheader" ) )
        handlePageheaderTag( true, attr );
      else if( localName.equals( "pagefooter" ) )
        handlePagefooterTag( true, attr );
      else if( localName.equals( "formtext" ) )
        handleFormtextTag( true, attr );
      else if( localName.equals( "formcheckbox" ) )
        handleFormcheckboxTag( true, attr );
      else if( localName.equals( "formdropdown" ) )
        handleFormdropdownTag( true, attr );
      else if( localName.equals( "formchoices" ) )
        handleChoicelistTag( true, attr );
      else if( localName.equals( "formchoice" ) )
        handleChoiceTag( true, attr );
      else if( localName.equals( "toc" ) )
        handleTocTag( true, attr );
      else if( localName.equals( "object" ) )
        handleObjectTag( true, attr );
      else if( localName.equals( "pagebreak" ) )
        handlePagebreakTag( true, attr );
      else if( localName.equals( "highlight" ) )
        handleHighlightTag( true, attr );
      else
        handleDiscard( true, attr );
      
    } catch( IOException e ) {
      logger.error( "IOException in startElement() [" + e.getMessage() + "]" );
    }
  }

  protected void endElement( String uri, String localName, String qName, Attributes attr ) {

    if( inAnnotation && !localName.equals( "annotation" ) ) // Do not handle any elements except for annotation end, which will reset the flag.
      return;

    try {
    
      if( localName.equals( "document" ) )
        handleDocumentTag( false, attr );
      else if( localName.equals( "documentinfo" ) )
        handleDocumentinfoTag( false, attr );
      else if( localName.equals( "property" ) )
        handlePropertyTag( false, attr );
      else if( localName.equals( "part" ) )
        handlePartTag( false, attr );
      else if( localName.equals( "section" ) )
        handleSectionTag( false, attr );
      else if( localName.equals( "heading" ) )
        handleHeadingTag( false, attr );
      else if( localName.equals( "par" ) )
        handleParTag( false, attr );
      else if( localName.equals( "inline" ) )
        handleInlineTag( false, attr );
      else if( localName.equals( "gentext" ) )
        handleGentextTag( false, attr );
      else if( localName.equals( "link" ) )
        handleLinkTag( false, attr );
      else if( localName.equals( "reference" ) )
        handleReferenceTag( false, attr );
      else if( localName.equals( "textbox" ) )
        handleTextboxTag( false, attr );
      else if( localName.equals( "image" ) )
        handleImageTag( false, attr );
      else if( localName.equals( "target" ) )
        handleTargetTag( false, attr );
      else if( localName.equals( "annotation" ) )
        handleAnnotationTag( false, attr );
      else if( localName.equals( "footnote" ) )
        handleFootnoteTag( false, attr );
      else if( localName.equals( "endnote" ) )
        handleEndnoteTag( false, attr );
      else if( localName.equals( "list" ) )
        handleListTag( false, attr );
      else if( localName.equals( "item" ) )
        handleItemTag( false, attr );
      else if( localName.equals( "table" ) )
        handleTableTag( false, attr );
      else if( localName.equals( "colgroup" ) )
        handleColgroupTag( false, attr );
      else if( localName.equals( "col" ) )
        handleColTag( false, attr );
      else if( localName.equals( "thead" ) )
        handleTheadTag( false, attr );
      else if( localName.equals( "tbody" ) )
        handleTbodyTag( false, attr );
      else if( localName.equals( "tr" ) )
        handleTrTag( false, attr );
      else if( localName.equals( "td" ) )
        handleTdTag( false, attr );
      else if( localName.equals( "indextarget" ) )
        handleIndextargetTag( false, attr );
      else if( localName.equals( "inserted" ) )
        handleInsertedTag( false, attr );
      else if( localName.equals( "deleted" ) )
        handleDeletedTag( false, attr );
      else if( localName.equals( "hidden" ) )
        handleHiddenTag( false, attr );
      else if( localName.equals( "pageheader" ) )
        handlePageheaderTag( false, attr );
      else if( localName.equals( "pagefooter" ) )
        handlePagefooterTag( false, attr );
      else if( localName.equals( "formtext" ) )
        handleFormtextTag( false, attr );
      else if( localName.equals( "formcheckbox" ) )
        handleFormcheckboxTag( false, attr );
      else if( localName.equals( "formdropdown" ) )
        handleFormdropdownTag( false, attr );
      else if( localName.equals( "formchoices" ) )
        handleChoicelistTag( false, attr );
      else if( localName.equals( "formchoice" ) )
        handleChoiceTag( false, attr );
      else if( localName.equals( "toc" ) )
        handleTocTag( false, attr );
      else if( localName.equals( "object" ) )
        handleObjectTag( false, attr );
      else if( localName.equals( "pagebreak" ) )
        handlePagebreakTag( false, attr );
      else if( localName.equals( "highlight" ) )
        handleHighlightTag( false, attr );
      else
        handleDiscard( false, attr );

    } catch( IOException e ) {
      logger.error( "IOException in startElement() [" + e.getMessage() + "]" );
    }
  }


  /**
   * Text. Using encodeForXMLText() ensures that quoting occurs the right way
   * and the UnicodeOutput.map takes effect.
   */
  protected void characters( String text ) {
    if( !hiddenStatePeek() ) {
      contentSeen = true;
      try {
        w.write( encodeForXMLText( text ) );
      } catch( Exception e ) {
        logger.error( "Error writing PCDATA: " + e.getMessage() );
      }
    }
  }


  /**
   * This is literal text to be written without any quoting taking place.
   */
  protected void literal( String text ) {
    if( ! hiddenStatePeek() ) {
      contentSeen = true;
      try {
        w.write( text );
      } catch( Exception e ) {
        logger.error( "Error writing literal text: " + e.getMessage() );
      }
    }
  }


// *************************************************************
// Private support methods
// *************************************************************

  /**
   * handles <document>
   */
  protected void handleDocumentTag( boolean opening, Attributes atts ) throws IOException {
    
    if( opening ) {
      // XML prolog
      writeXMLProlog( w ); nl(w);

      // DOCTYPE
      if( prefDoctypeString.equals( "*" ) /* default */) {
        w.write( "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">" ); nl(w);
      } else if( prefDoctypeString.trim().length() == 0 ) {
        ; // do not write
      } else {
        w.write( prefDoctypeString.trim() ); nl(w);
      }
      
      // <html>, namespaces, some <meta>s
      w.write( "<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"" + atts.getValue( "xml:lang" ) + "\" lang=\"" + atts.getValue( "xml:lang" ) + "\">" ); nl(w);
      w.write( "<head>" ); nl(w);
      indent();
      w.write( indentation() + "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=" + getParameterString( kOutputEncodingParamName, "utf-8" ).toLowerCase() + "\" />" ); nl(w);

      // Copyright header, creator
      w.write( indentation() + "<meta name=\"GENERATOR\" content=\"upCast/" + getApplicationVersionAsString() + " (Java; I) [infinity-loop]\" />" ); nl(w);
      w.write( indentation() + "<meta http-equiv=\"GENERATOR\" content=\"upCast/" + getApplicationVersionAsString() + " (Java; I) [infinity-loop]\" />" ); nl(w);

      /* The opening <body> tag is written in handleDocumentinfoTag() on closing, because
       * we still need to fetch some <meta> properties in handlePropertyTag().
       */
      
      outdent(); // To account for indenting for each <meta> tag in handlePropertyTag()
      
    } else /* closing */ {
    
      if( prefFormHandling == kFormForm ) { // We need to write an opening form tag
        outdent();
        w.write( indentation() + "</form>" );
      }

      nesting = 1;
      
      /* First, we write the collected footnotes after a <hr>:
       */
      if( footNotes.toString().trim().length() > 0 ) {
        w.write( indentation() + "<hr />" ); nl(w);
        
        w.write( indentation() + "<table border=\"0\" summary=\"contains document footnotes\">" ); nl(w);
        indent();
          w.write( footNotes.toString() );
        outdent();
        w.write( indentation() + "</table>" ); nl(w);
      }
      

      /* Next, we write the index (if there are any entries):
       */
      renderIndex();
        
      /* Finally, we close the document properly.
       */
      outdent();
      w.write( "</body>" ); nl(w);
      w.write( "</html>" ); nl(w);
    }

  }


  /**
   * handles <documentinfo>
   */
  protected void handleDocumentinfoTag( boolean opening, Attributes atts ) throws IOException {
  
    if( opening ) {
    
      ; // do nothing
      
    } else /* closing */ {
      
      // Write link to stylesheet (if any):
      if( prefCSSRef != null ) {
        indent();
          w.write( indentation() + "<link rel=\"STYLESHEET\" type=\"text/css\" href=\"" + encodeForXMLAttribute( new DiskFile( prefCSSRef ).toURLString() ) + "\" />" ); nl(w);
        outdent();
      }
      
      if( prefVisual && prefInlineStylesheet ) { // Write an inline CSS stylesheet
        indent();
          w.write( indentation() + "<style type=\"text/css\">" ); nl(w);
          w.write( "@media screen { body { padding: 8px; } }"); nl(w); // So that we have a visual pleasing margin at the left side in the browser. Taken from CSS21 HTML4 sample stylesheet.
          w.write( encodeForXMLText( calculateCSSStylesheet(  new CSSPropertyFilter( CSSPropertyFilter.kExcludeListed, new String [] { CSSProperties.kCustomCSSPrefix + "*" } ), true /* ".class" selector notation */, false /*no pagesize code*/ ) ) );
          w.write( "ol, ul { padding: 0mm; margin: 0mm; }"); nl(w); // many browsers use padding instead of margin for formatting lists in their default stylesheets, so we override it here to default values for rendering without problems.
          w.write( indentation() + "</style>" ); nl(w);
        outdent();
      }
      
      if( ! titleWritten ) {
        indent();
          w.write( indentation() + "<title>" + encodeForXMLText( resolveExpression( "Document title: ${il:srcbasename}" ) ) + "</title>" ); nl(w);
        outdent();
      }
        
      w.write( indentation() + "</head>" ); nl(w);
      // outdent();
      // indent();
      w.write( indentation() + "<body>" ); nl(w);
      indent(); // account for outdent() in opening <document> tag handler.
      
      if( prefFormHandling == kFormForm ) { // We need to write an opening form tag
        w.write( indentation() + "<form action=\"" + prefFormURI + "\">" );
      }
      
    }
  }
  
  
  /**
   * handles <property>
   */
  protected void handlePropertyTag( boolean opening, Attributes atts ) throws IOException {
    
    if( opening ) {
      String name = atts.getValue( "name" );
      String value = atts.getValue( "value" );
      
      indent();
              
      if( value != null ) {

        if( name.equals( Constants.kDIAuthorPropname ) ) {
          w.write( indentation() + "<meta name=\"Author\" content=\"" + encodeForXMLAttribute( value ) + "\" />" ); nl(w);
        } else if( name.equals( Constants.kDISubjectPropname ) ) {
          w.write( indentation() + "<meta name=\"Description\" content=\"" + encodeForXMLAttribute( value ) + "\" />" ); nl(w);
        } else if( name.equals( Constants.kDIKeywordsPropname ) ) {
          w.write( indentation() + "<meta name=\"Keywords\" content=\"" + encodeForXMLAttribute( value ) + "\" />" ); nl(w);
        } else if( name.equals( Constants.kDICategoryPropname ) ) {
          w.write( indentation() + "<meta name=\"Classification\" content=\"" + encodeForXMLAttribute( value ) + "\" />" ); nl(w);
        } else if( name.equals( Constants.kDIRevisionTimePropname ) ) {
          w.write( indentation() + "<meta name=\"lastChanged\" content=\"" + encodeForXMLAttribute( value ) + "\" />" ); nl(w);
        } else if( name.equals( Constants.kDITitlePropname ) ) {
          titleWritten = true;
          w.write( indentation() + "<title>" + encodeForXMLText( value ) + "</title>" ); nl(w);
        }
        
        // only write in comments:

        else if( name.equals( Constants.kDICompanyPropname ) ) {
          w.write( indentation() + "<!--  Company : " + encodeForXMLAttribute( value ) + " -->" ); nl(w);
        } else if( name.equals( Constants.kDIOperatorPropname ) ) {
          w.write( indentation() + "<!--  Operator: " + encodeForXMLAttribute( value ) + " -->" ); nl(w);
        } else if( name.equals( Constants.kDIManagerPropname ) ) {
          w.write( indentation() + "<!--  Manager : " + encodeForXMLAttribute( value ) + " -->" ); nl(w);
        } else if( name.equals( Constants.kDICommentPropname ) ) {
          w.write( indentation() + "<!--  Comment : " + encodeForXMLAttribute( value ) + " -->" ); nl(w);
        } else if( name.equals( Constants.kDIDoccommentPropname ) ) {
          w.write( indentation() + "<!--  Document Comment: " + encodeForXMLAttribute( value ) + " -->" ); nl(w);
        } else if( name.equals( Constants.kDIHlinkbasePropname ) ) {
          w.write( indentation() + "<!--  Linkbase: " + encodeForXMLAttribute( value ) + " -->" ); nl(w);
        }
      }
            
    } else /* closing */ {

      outdent();
      
    }
  }


  /**
   * handles <part>
   */
  protected void handlePartTag( boolean opening, Attributes atts ) throws IOException {
    
    // XHTML has no notion of parts -> skipped entirely.
    if( opening ) {
      ;
    } else /* closing */ {
      ;
    }
  }


  /**
   * handles <section>
   */
  protected void handleSectionTag( boolean opening, Attributes atts ) throws IOException {
    
    // Sections in XHTML are implicitly defined by corresponding <heading>s -> skipped.
    if( opening ) {
      ;
    } else /* closing */ {
      ;
    }
  }


  /**
   * handles <heading>
   */
  protected void handleHeadingTag( boolean opening, Attributes atts ) throws IOException {
    
    if( opening ) {
      indent();
      int level = Integer.parseInt( atts.getValue( "level" ) );
      level = Math.min( Math.max( level, 1 ), 6 ); // only 6 levels in XHTML: h1..h6
      headingNumberStack.push( new Integer( level ) );
      
      w.write( indentation() + "<h" + level + calcClassAtt( atts ) + calcStyleAtt( atts, parFilter ) + ">" );
      inHeading = true;
      
      flushDelayedInlines();

    } else /* closing */ {
      
      inHeading = false;
      w.write( "</h" + ((Integer)headingNumberStack.pop()).toString() + ">" ); nl(w);
      outdent();
    }
  }


  /**
   * handles <par>
   */
  protected void handleParTag( boolean opening, Attributes atts ) throws IOException {
    
    if( opening ) {


      if( ! ((Boolean)inInline.peek()).booleanValue() ) {
        indent();
        w.write( indentation() + "<p" + calcClassAtt( atts ) + calcStyleAtt( atts, parFilter ) + ">" );
      }
      inInline.push( new Boolean( true ) );

      flushDelayedInlines();

    } else /* closing */ {

      inInline.pop();

      if( ! ((Boolean)inInline.peek()).booleanValue() ) {
        w.write( "</p>" ); nl(w);
        outdent();
      } else {
        w.write( "<br />" );
      }

    }
  }


  /**
   * handles <inline>
   */
  protected void handleInlineTag( boolean opening, Attributes atts ) throws IOException {
    String calcatts = calcClassAtt( atts ) + calcStyleAtt( atts, inlineFilter );
    
    if( opening )
      flushDelayedInlines();

    
    if( calcatts.trim().length() > 0 ) {
      if( opening ) {
  
        w.write( "<span" + calcatts + ">" );
  
      } else /* closing */ {
  
        w.write( "</span>" );
  
      }
    }
  }


  /**
   * handles <highlight>
   */
  protected void handleHighlightTag( boolean opening, Attributes atts ) throws IOException {
    
    if( opening ) {
      String s = atts.getValue( "color" );
      
      flushDelayedInlines();

      w.write( "<span class=\"highlight\" style=\"background-color: " + ( (s != null && s.length() >= 4 ) ? s : "transparent" ) +  "\">" );

    } else /* closing */ {

      w.write( "</span>" );

    }
  }


  /**
   * handles <gentext>
   */
  protected void handleGentextTag( boolean opening, Attributes atts ) throws IOException {
    
    if( opening ) {
      
      flushDelayedInlines();

      // Let's make the gentext type a class name so that we are able to style it later if we want to.
      String s = atts.getValue( "type" );
      if( s != null && s.length() > 0 ) {
        s = " " + "class=\"" + s + "\"";
      }
      
      w.write( "<span" + s + calcStyleAtt( atts, inlineFilter ) + ">" );

    } else /* closing */ {

      w.write( "</span>" );

    }
  }


  /**
   * handles <link>
   */
  protected void handleLinkTag( boolean opening, Attributes atts ) throws IOException {
    
    if( opening ) {

      flushDelayedInlines();

      inURL = true;
      w.write( "<a href=\"" + encodeForXMLAttribute( atts.getValue( "xlink:href" ) ) + "\">" );
    
    } else /* closing */ {

      w.write( "</a>" );
      inURL = false;
        
      while( targetStack.size() != 0 ) {
        w.write( (String) targetStack.pop() );
      }
      
    }
  }


  /**
   * handles <reference>
   */
  protected void handleReferenceTag( boolean opening, Attributes atts ) throws IOException {
    
    if( opening ) {

      flushDelayedInlines();

      inURL = true;
      w.write( "<a href=\"" + encodeForXMLAttribute( atts.getValue( "xlink:href" ) ) + "\">" );

      
    } else /* closing */ {

      w.write( "</a>" );
      inURL = false;
        
      while( targetStack.size() != 0 ) {
        w.write( (String) targetStack.pop() );
      }
      
    }
  }


  /**
   * handles <textbox>
   */
  protected void handleTextboxTag( boolean opening, Attributes atts ) throws IOException {
    
    /* Textbox implementation: We make it either a <span> or a <div> (depending
     * on the position in the tree), give it a class value of "textbox" and style it
     * to be positioned floating and relative to its parent.
     */
    
    if( opening ) {
      CSSPropertyFilter f = new CSSPropertyFilter( CSSPropertyFilter.kExcludeListed, new String [] { "-ilx-*", "list*", "widows", "orphans", "display" } );

      if( ((Boolean)inInline.peek()).booleanValue() ) {
        flushDelayedInlines();
        w.write( "<span class=\"textbox\" style=\"display: inline-block; " + calcStyleAttValue( atts, f ) + "\">" );
      } else {
        indent();
        w.write( "<div class=\"textbox\" style=\"display: block; " + calcStyleAttValue( atts, f ) + "\">" );
      }
      
    } else /* closing */ {

      if( ((Boolean)inInline.peek()).booleanValue() ) {
        w.write( "</span>" );
      } else {
        w.write( indentation() + "</div>" ); nl(w);
        outdent();
      }

    }
  }


  /**
   * handles <image>
   */
  protected void handleImageTag( boolean opening, Attributes atts ) throws IOException {

    if( opening ) {
    
      if( !hiddenStatePeek() ) {
        if( ! ((Boolean)inInline.peek()).booleanValue()  && !inHeading ) {
          /* images can only be inline in blocks. Therefore, we need to create an artificial block
           * if we aren't in a block right now.
           */
          indent();
          w.write( indentation() + "<p>" );
        }
        
        CSSPropertyFilter f = new CSSPropertyFilter( CSSPropertyFilter.kExcludeListed,
          new String [] { "-ilx-*", "list*", "widows", "orphans", "display", "width", "height" } );

        flushDelayedInlines();

        w.write( "<img "
          + "src=\"" + encodeForXMLAttribute( atts.getValue( "xlink:href" )  )+ "\""
          + ( atts.getValue( "width_px" ) != null && atts.getValue( "width_px" ).length() > 0 ? " width=\"" + encodeForXMLAttribute( atts.getValue( "width_px" ) ) + "\"" : "" )
          + ( atts.getValue( "height_px" ) != null && atts.getValue( "height_px" ).length() > 0 ? " height=\"" + encodeForXMLAttribute( atts.getValue( "height_px" ) ) + "\"" : "" )
          + " alt=\"" + (atts.getValue( "description" ) != null ? encodeForXMLAttribute( atts.getValue( "description" ) ) : "Image: " + encodeForXMLAttribute( atts.getValue( "xlink:href" ) ) ) + "\""
          + " style=\"" + calcStyleAttValue( atts, f ) + "\""
          + "/>" );
      
        contentSeen = true;
        
        if( ! ((Boolean)inInline.peek()).booleanValue() && !inHeading ) {
          w.write( "</p>" ); nl(w);
          outdent();
        }
      }
      
    } else /* closing */ {

      ; // empty element: nothing to do here.

    }
  }


  /**
   * handles <pagebreak>: Skip, but write a comment
   */
  protected void handlePagebreakTag( boolean opening, Attributes atts ) throws IOException {
    
    if( opening ) {
      
      w.write( "<!-- HERE: forced page break in original document -->" );
        
    } else /* closing */ {

      ; // empty element, do nothing.
      
    }
  }

  
  /**
   * handles <target>
   */
  protected void handleTargetTag( boolean opening, Attributes atts ) throws IOException {
    
    if( opening ) {
    
      flushDelayedInlines();

      /* We encounter a target.
       * We translate it to HTML using <a name="..."></a>.
       */

      /* A target node doesn't have any children. We are safe by simply rendering its reference
       * into the HTML document.
       */
       
      /* NEW: IMPORTANT: We may not write targets within occurrences of url elements, because this would yield something like
       * <a href="...">some<a name="..." />content</a>, which is not valid XHTML. Therefore, we collect targets while in url elements
       * and write it immediately after closing the url element's <a> tag. That's about the best thing we can do...
       */
      if( inURL ) {
        targetStack.push( new String( "<a id=\"" + encodeForXMLAttribute( atts.getValue( "id" ) ) + "\" name=\"" + encodeForXMLAttribute( atts.getValue( "id" ) ) + "\"></a>" ) );
      } else {
        w.write( new String( "<a id=\"" + encodeForXMLAttribute( atts.getValue( "id" ) ) + "\" name=\"" + encodeForXMLAttribute( atts.getValue( "id" ) ) + "\"></a>" ) );
      }
    
    } else /* closing */ {

      ; // empty element: nothing to do here.

    }
  }


  /**
   * handles <annotation>
   */
  protected void handleAnnotationTag( boolean opening, Attributes atts ) throws IOException {
    
    if( opening ) {

      inInline.push( new Boolean( false ) );
      hiddenStatePush( false ); // Even if we are in a hidden element, we write the annotation to the document.

      // We make an XHTML comment from that!
      w.write( "<!-- " );
      inAnnotation = true;
      
    } else /* closing */ {

      inAnnotation = false;
      w.write( " -->" );

      hiddenStatePop();
      inInline.pop();
    }
  }


  /**
   * handles <endnote>
   * Does the same as footnote
   */
  protected void handleEndnoteTag( boolean opening, Attributes atts ) throws IOException {
    handleFootnoteTag( opening, atts );
  }

  /**
   * handles <footnote>
   */
  protected void handleFootnoteTag( boolean opening, Attributes atts ) throws IOException {
    
    if( opening ) {

      flushDelayedInlines();

      inInline.push( new Boolean( false ) );
      w.write( "<a id=\"fnref" + footNoteCounter + "\" name=\"fnref" + footNoteCounter + "\"></a>" +
                 (hiddenStatePeek() ? "" : "<a href=\"#fn" + footNoteCounter + "\"><sup>" + (footNoteCounter+1) + "</sup></a>") );
  
      writerStack.push( w );
      if( w != devnull ) {
        w = footNotes; // switch to the string writer for footnote collecting
      }
      
      savedLevel = nesting;
      nesting = 2;
      previousNewline = true;
      
        w.write( indentation() + "<tr>" ); nl(w);
        indent();
          w.write( indentation() + "<td valign=\"top\"><a id=\"fn" + footNoteCounter + "\" name=\"fn" + footNoteCounter + "\"></a><a href=\"#fnref" + footNoteCounter + "\">[" + (footNoteCounter+1) + "]</a></td><td valign=\"top\"><div class=\"footnote\">" );
      
    } else /* closing */ {
          
          w.write( indentation() + "</div></td>" ); nl(w);
        outdent();
        w.write( indentation() + "</tr>" ); nl(w);

      nesting = savedLevel;
      w = (Writer) writerStack.pop();
      
      ++footNoteCounter; // Increment for next footnote.
      inInline.pop();
    }
  }


  /**
   * handles <list>
   */
  protected void handleListTag( boolean opening, Attributes atts ) throws IOException {
    
    if( opening ) {
      String listtype = atts.getValue( "type" );
      boolean ordered = false;
      
      if( listtype != null && (listtype.equals( "disc" ) || listtype.equals( "none" ) ) ) {
        openLists.push( "ul" );
      } else {
        openLists.push( "ol" );
        ordered = true;
      }
        
      indent();
        w.write( indentation() + "<" + (String)openLists.peek() );
        String styleDecl = calcStyleAtt( atts, listFilter );
        
//        if( ordered ) {
//          try {
//            styleDecl = styleDecl.substring( 0, styleDecl.length() - 1) + "; counter-reset: item " + (Integer.parseInt( atts.getValue( "startat" ) ) - 1) + "\"";
//          } catch( Exception e ) { }
//        }
        
        w.write( styleDecl );
        w.write( ">"); nl(w);
      
    } else /* closing */ {

        w.write( indentation() + "</" + (String)openLists.peek() + ">"); nl(w);
        openLists.pop();
        
      outdent();
      
    }
  }


  /**
   * handles <item>
   */
  protected void handleItemTag( boolean opening, Attributes atts ) throws IOException {
    
    if( opening ) {
      
      indent();
//        w.write( indentation() + "<li style=\":before { content: counter(item) '. '; counter-increment: item\">" ); nl(w);
      w.write( indentation() + "<li>" ); nl(w);
        
    } else /* closing */ {

        w.write( indentation() + "</li>" ); nl(w);
      outdent();
      
    }
  }


  /**
   * handles <table>
   */
  protected void handleTableTag( boolean opening, Attributes atts ) throws IOException {
    
    if( opening ) {

      indent();
        w.write( indentation() + "<table" + (prefVisual ? "" : " border=\"1\""/*make border visible*/) + calcStyleAtt( atts, defaultFilter ) + ">" ); nl(w);

        if( false ) { // We no longer need to do this since we receive callbacks for colgroup and col as of upCast 5.1.1
          indent();
          
          /* Here, we write the special table props: COLGROUP, COL
           */
          String coldescr = atts.getValue( "tablecolwidths" ); // This is a mangled string.
          
          if( coldescr != null && coldescr.length() > 0 ) { // If coldescription available:
            w.write( indentation() + "<colgroup>" );
            
            /* We must extract the numbers from the coldescr string and write COL elements with
             * respective widths.
             */
            int index = 0;
            String colWidth;
            
            while( (colWidth = getMangledComponent( coldescr, index ) ) != null ) { // If null, this component was not found.
              w.write( "<col width=\"" + convertTwipsToPixels( Integer.parseInt( colWidth ) ) + "\" />" );
              ++index;
            }
            
            w.write( "</colgroup>" ); nl(w);
            outdent();
          }
        }
    
    } else /* closing */ {
    
        w.write( indentation() + "</table>" ); nl(w);
      outdent();
      
    }
  }


  /**
   * handles <colgroup>
   */
  protected void handleColgroupTag( boolean opening, Attributes atts ) throws IOException {
    
    if( opening ) {
      
      indent();
        w.write( indentation() + "<colgroup>" );
        
    } else /* closing */ {

        w.write( indentation() + "</colgroup>" ); nl(w);
      outdent();
      
    }
  }


  /**
   * handles <col>
   */
  protected void handleColTag( boolean opening, Attributes atts ) throws IOException {
    
    if( opening ) {
      
      w.write( "<col"
        + formatAttribute( atts, "width" )
        + "/>" );
        
    } else /* closing */ {

      ; // empty element, do nothing.
      
    }
  }


  /**
   * handles <thead>
   */
  protected void handleTheadTag( boolean opening, Attributes atts ) throws IOException {
    
    if( opening ) {
      
      indent();
        w.write( indentation() + "<thead>" ); nl(w);
        
    } else /* closing */ {

        w.write( indentation() + "</thead>" ); nl(w);
      outdent();
      
    }
  }


  /**
   * handles <tbody>
   */
  protected void handleTbodyTag( boolean opening, Attributes atts ) throws IOException {
    
    if( opening ) {
      
      indent();
        w.write( indentation() + "<tbody>" ); nl(w);
        
    } else /* closing */ {

        w.write( indentation() + "</tbody>" ); nl(w);
      outdent();
      
    }
  }


  /**
   * handles <tr>
   */
  protected void handleTrTag( boolean opening, Attributes atts ) throws IOException {
    
    if( opening ) {
      
      indent();
        w.write( indentation() + "<tr>" ); nl(w);
        
    } else /* closing */ {

        w.write( indentation() + "</tr>" ); nl(w);
      outdent();
      
    }
  }


  /**
   * handles <td>
   */
  protected void handleTdTag( boolean opening, Attributes atts ) throws IOException {
    
    if( opening ) {
      
      indent();
        w.write( indentation() + "<td" );
        
        if( atts.getValue( "valign" ) != null && atts.getValue( "valign" ).length() > 0 )
          w.write( formatAttribute( atts, "valign" ) );

        if( atts.getValue( "colspan" ) != null && atts.getValue( "colspan" ).length() > 0 )
          w.write( formatAttribute( atts, "colspan" ) );
        if( atts.getValue( "rowspan" ) != null && atts.getValue( "rowspan" ).length() > 0 )
          w.write( formatAttribute( atts, "rowspan" ) );
        
        w.write( calcStyleAtt( atts, defaultFilter ) );
        
        w.write( ">" ); nl(w);
        
        contentSeen = false;
        
    } else /* closing */ {

        // If preference to allow completey empty cells is not set: make cell non-empty.
        if( ! contentSeen && !prefAllowEmptyCells )
          w.write( encodeForXMLText( new Character( (char)160 /* nbsp */).toString() ) );
          
        w.write( indentation() + "</td>" ); nl(w);
      outdent();
      
    }
  }


  /**
   * handles <indextarget>
   */
  protected void handleIndextargetTag( boolean opening, Attributes atts ) throws IOException {
    
    if( opening ) {
    
      writerStack.push( w );
      w = indextargets; // since indextargets cannot be nested, a non-stacked variable switch is ok
      
      /* We encounter an index target. This contains the term defined here
       * as element attribute. For HTML, we mark this place with a target,
       * and collect the index term into a sorted array.
       */
      int indexId = addIndexEntry( atts );
      
      /* An index node doesn't have any children. We are safe by simply rendering its reference
       * into the HTML document.
       * TODO: as of 5.2.1, indextarget may have <see> as child node!
       */
      w.write( "<a id=\"index" + indexId + "\" name=\"index" + indexId + "\" />" );
    
    } else /* closing */ {
      
      w = (Writer)writerStack.pop(); // empty element: nothing to do.
      if( ((Boolean)inInline.peek()).booleanValue() == true ) {
        flushDelayedInlines();
      }
    }
  }


  /**
   * handles <inserted>
   */
  protected void handleInsertedTag( boolean opening, Attributes atts ) throws IOException {
    
    if( opening ) {
      
      flushDelayedInlines();
      
      if( prefMarkupRevision ) {
        w.write( "<ins title=\""
            + encodeForXMLAttribute( "inserted by: " + atts.getValue( "author" ) )
            + "\" datetime=\""
            + encodeForXMLAttribute( atts.getValue( "date" ) )
            + "\">" );
      }

    } else /* closing */ {

      if( prefMarkupRevision ) {
        w.write( "</ins>" );
      }
      
    }
  }


  /**
   * handles <deleted>
   */
  protected void handleDeletedTag( boolean opening, Attributes atts ) throws IOException {
    
    if( opening ) {
      
      flushDelayedInlines();

      if( prefMarkupRevision ) {
        w.write( "<del title=\""
            + encodeForXMLAttribute( "deleted by: " + atts.getValue( "author" ) )
            + "\" datetime=\""
            + encodeForXMLAttribute( atts.getValue( "date" ) )
            + "\">" );

      } else {
        writerStack.push( w );
        w = devnull; // trash contents
      }

    } else /* closing */ {

      if( prefMarkupRevision ) {
        w.write( "</del>" );
      } else {
        w = (Writer) writerStack.pop();
        devnull = new StringWriter(); // clear to free memory
      }

    }
  }


  /**
   * handles <hidden>
   */
  protected void handleHiddenTag( boolean opening, Attributes atts ) throws IOException {
    
    if( opening ) {

      hiddenStatePush( true );

    } else /* closing */ {

      hiddenStatePop();

    }
  }

  /**
   * handles <toc>
   */
  protected void handleTocTag( boolean opening, Attributes atts ) throws IOException {
    if( prefWriteTOC ) {
      ; //just skip this element, but process children.
    } else {
      handleDiscard( opening, atts );
    }
  }


  /**
   * handles <pageheader>
   */
  protected void handlePageheaderTag( boolean opening, Attributes atts ) throws IOException {
    handleDiscard( opening, atts );
  }
  /**
   * handles <pagefooter>
   */
  protected void handlePagefooterTag( boolean opening, Attributes atts ) throws IOException {
    handleDiscard( opening, atts );
  }
  
  /**
   * handles <formtext>
   */
  protected void handleFormtextTag( boolean opening, Attributes atts ) throws IOException {

    if( opening ) {

      switch( prefFormHandling ) {
        case kFormDiscard:
          handleDiscard( opening, atts );
          break;
        
        case kFormText:
          writerStack.push( w );
          break; // We write the contents
        
        case kFormForm: {
          writerStack.push( w );

          int maxChars;
          boolean editable;
          String altText;
          String name;
          String defaultValue;
          
          try { name = atts.getValue( "name" ); } catch( Exception e ) { name = null; }
          try { maxChars = Integer.parseInt( atts.getValue( "maxChars" ) ); } catch( Exception e ) { maxChars = -1; /* unlimited */ }
          try { editable = ! Boolean.valueOf( atts.getValue( "protected" ) ).booleanValue(); } catch( Exception e ) { editable = true; }
          try { altText = atts.getValue( "helpText" ); } catch( Exception e ) { altText = null; }
          try { altText = atts.getValue( "helpText" ); } catch( Exception e ) { altText = "Checkbox" + (name != null ? " " + name : ""); }
          

          w.write( "<input type=\"text\""
           + (name != null ? " name=\"" + encodeForXMLAttribute(name) + "\"" : " name=\"formtext\"" )
           + (maxChars > 0 ? " maxlength=\"" + maxChars + "\"" : "" )
           + (altText != null ? " alt=\"" + encodeForXMLAttribute(altText) + "\"" : "" )
           + (editable ? "" : " readonly=\"readonly\"" )
           + " />" ); // empty element!
           
          w = devnull; // trash contents
          break;
          }
        }

    } else /* closing */ {

      handleDiscard( opening, atts ); // Since we pushed the writer in all three cases, this does the corect job for all.

    }
  }


  /**
   * handles <formcheckbox>
   */
  protected void handleFormcheckboxTag( boolean opening, Attributes atts ) throws IOException {

    if( opening ) {

      switch( prefFormHandling ) {
        case kFormDiscard:
          handleDiscard( opening, atts );
          break;
        
        case kFormText: {
          writerStack.push( w );

          String selectedValue;
          try { selectedValue = atts.getValue( "selectedValue" ); } catch( Exception e ) { selectedValue = "0"; }

          w.write( (selectedValue.trim().equals("1") ? "&#10003;" : "&#10065;" ) ); // empty box or checkmark character
          }
          break;
        
        case kFormForm: {
          writerStack.push( w );

          boolean locked;
          String name;
          String defaultValue;
          String altText;
          
          try { locked = Boolean.valueOf( atts.getValue( "protected" ) ).booleanValue(); } catch( Exception e ) { locked = true; }
          try { name = atts.getValue( "name" ); } catch( Exception e ) { name = null; }
          try { defaultValue = atts.getValue( "defaultValue" ); } catch( Exception e ) { defaultValue = "0"; }
          try { altText = atts.getValue( "helpText" ); } catch( Exception e ) { altText = "Checkbox" + (name != null ? " " + name : ""); }
          

          w.write( "<input type=\"checkbox\""
           + (name != null ? " name=\"" + encodeForXMLAttribute(name) + "\"" : " name=\"formtext\"" )
           + (altText != null ? " alt=\"" + encodeForXMLAttribute(altText) + "\"" : "" )
           + (defaultValue.trim().equals("1") ? " checked=\"checked\"" : "" )
           + (!locked ? "" : " readonly=\"readonly\"" )
           + " />" ); // empty element!

          w = devnull; // trash contents
          break;
          }
        }

    } else /* closing */ {

      handleDiscard( opening, atts ); // Since we pushed the writer in all three cases, this does the corect job for all.

    }
  }


  /**
   * handles <formdropdown>
   */
  protected void handleFormdropdownTag( boolean opening, Attributes atts ) throws IOException {
    if( opening ) {

      switch( prefFormHandling ) {
        case kFormDiscard:
          handleDiscard( opening, atts );
          break;
        
        case kFormText:
          writerStack.push( w );
          try { dropDownDefault = Integer.parseInt( atts.getValue( "defaultValue" ) ); } catch( Exception e ) { dropDownDefault = -999; }
          break;
          
        case kFormForm: {
          writerStack.push( w );

          boolean locked;
          String name;
          String selectedValue;
          String altText;
          
          try { locked = Boolean.valueOf( atts.getValue( "protected" ) ).booleanValue(); } catch( Exception e ) { locked = true; }

          name = atts.getValue( "name" );

          try { dropDownDefault = Integer.parseInt( atts.getValue( "defaultValue" ) ); } catch( Exception e ) { dropDownDefault = -999; }

          try { altText = atts.getValue( "helpText" ).trim(); } catch( Exception e ) { altText = "Checkbox" + (name != null ? " " + name : ""); }

          w.write( indentation() + "<select alt=\"" + encodeForXMLAttribute(altText) + "\" name=\"" + encodeForXMLAttribute(name) + "\"" + (locked ? " readonly=\"readonly\"" : "" ) + ">" );
          break;
          }
        }

    } else /* closing */ {

      switch( prefFormHandling ) {
        case kFormForm: {
          w.write( indentation() + "</select>" );
          break;
          }
      }
      handleDiscard( opening, atts ); // Since we pushed the writer in all three cases, this does the corect job for all.

    }
  }

  /**
   * handles <choicelist>
   */
  protected void handleChoicelistTag( boolean opening, Attributes atts ) throws IOException {

    if( opening ) {

      nl(w);
      indent(); // do nothing. We do not make this an optgroup!

    } else /* closing */ {

      outdent(); // do nothing
      nl(w);

    }
  }
  /**
   * handles <choice>
   */
  protected void handleChoiceTag( boolean opening, Attributes atts ) throws IOException {

    if( opening ) {

      switch( prefFormHandling ) {
        case kFormDiscard:
          handleDiscard( opening, atts );
          break;
        
        case kFormText: {
          writerStack.push( w );
          int valIndex;
          try { valIndex = Integer.parseInt( atts.getValue( "index" ) ); } catch( Exception e ) { valIndex = -1; }
          
          if( valIndex != dropDownDefault )
            w = devnull; // don't writem except when it is the correct default
          }
          break;

        case kFormForm: {
          writerStack.push( w );
          int valIndex;
          try { valIndex = Integer.parseInt( atts.getValue( "index" ) ); } catch( Exception e ) { valIndex = -1; }

          w.write( indentation() + "<option" + (valIndex == dropDownDefault ? " selected=\"selected\"" : "" ) + ">" );
          break;
          }
      }

    } else /* closing */ {

      if( prefFormHandling == kFormForm ) {
        w.write( "</option>" ); nl(w);
      }
      handleDiscard( opening, atts ); // Since we pushed the writer in all three cases, this does the corect job for all.
    }
  }

  /**
   * handles the discarding of the whole contents of the element.
   */
  protected void handleDiscard( boolean opening, Attributes atts ) throws IOException {
    
    if( opening ) {
      
      flushDelayedInlines();

      writerStack.push( w );
      w = devnull; // trash contents

    } else /* closing */ {

      w = (Writer) writerStack.pop();
      devnull = new StringWriter(); // clear to free memory

    }
  }
  
  /**
   * handles <object> by entering the object so that any contained image object may still be rendered.
   */
  protected void handleObjectTag( boolean opening, Attributes atts ) throws IOException {
    
    if( opening ) {
      ;
    } else /* closing */ {
      ;
    }
  }

  protected void flushDelayedInlines() throws IOException {
    w.write( indextargets.toString() );
    indextargets = new StringWriter(); // clear...
  }



  /* "hidden" state stack handling
   */
  protected void hiddenStateClear() { hiddenStateStk = new Stack(); }
  protected void hiddenStatePush( boolean b ) { hiddenStateStk.push( new Boolean( b ) ); }
  protected boolean hiddenStatePeek() { try{ return ((Boolean)hiddenStateStk.peek()).booleanValue(); } catch(Exception e) { return false; /* default: not hidden! */ } }
  protected void hiddenStatePop() { try { hiddenStateStk.pop(); } catch( Exception e ) { hiddenStateClear(); } }
  
  
  /**
   * calculates the complete style attribute text for an element.
   */
  protected String calcStyleAtt( Attributes atts, CSSPropertyFilter filter ) {
    String s = atts.getValue( kDiffStyleAttrName );
    
    if( s != null && s.length() > 0 && prefVisual ) {
      s = filterPropertyString( s, filter );

      if( s != null && s.length() > 0 ) {
        return " " + "style=\"" + encodeForXMLAttribute( s ) + "\"";
      }
    }
    
    return "";
  }

  /**
   * calculates the complete style attribute text for an element.
   */
  protected String calcStyleAttValue( Attributes atts, CSSPropertyFilter filter ) {
    String s = atts.getValue( kDiffStyleAttrName );
    
    if( s != null && s.length() > 0 && prefVisual ) {
      s = filterPropertyString( s, filter );

      if( s != null && s.length() > 0 ) {
        return s;
      }
    }
    
    return "";
  }

  /**
   * calculates the class attribute text for an element.
   */
  protected String calcClassAtt( Attributes atts ) {
    String s = atts.getValue( kLogicalAttrName );
    
    if( s != null && s.length() > 0 ) {
      return " " + "class=\"" + encodeForClassAttribute( s ) + "\"";
    }
    
    return "";
  }
  
  
  /**
   * adds an index entry to the sorted set of indices. Builds the
   * entry based on the atributes passed.
   */
  protected int addIndexEntry( Attributes atts ) {
    StringBuffer sb = new StringBuffer();
    
    for( int i = 1; i <= 9; ++i ) {
      String s = atts.getValue( "entryLevel" + i );
      
      if( s != null && s.length() > 0 )
        sb.append( new Character( Constants.kNameManglingSeparator).toString() + s );
    }
    
    if( sb.toString().length() < 1 ) { /* No index attribute! */
      sb.append( new Character( Constants.kNameManglingSeparator).toString() + "(no index entry text specified)" );
    }

    String indexKey = sb.toString().substring( 1 ) /*discard leading kPrefpathSeparator*/;
    Stack valueStack = (Stack)indexEntries.get( indexKey );
    
    if( valueStack == null ) // This is a new combination of index/sub/sub... entries, not already in the table.
      valueStack = new Stack();
    
    valueStack.push( new Integer( ++indexEntryRefId ) );
    
    indexEntries.put( indexKey, valueStack );
    
    return indexEntryRefId;
  }
  
  /**
   * renders the collected index entries
   */
  protected void renderIndex() throws IOException {
    
    
    if( indexEntries.size() > 0 ) { // Only do it if there are actually any index entries.
  
      /* Index entries are sorted alphabetically. We
       * write them sequentially, breaking them up at every component.
       */
      
      // Separate it from previous contents by a horizontal rule.
      w.write( indentation() + "<hr />" ); nl(w);

      // Write "Index" headline
      w.write( indentation() + "<h1>Index</h1>" ); nl(w);
      
      Iterator iter = indexEntries.keySet().iterator();
      String lastEntry = "";
        
      w.write( indentation() + "<p>" ); nl(w);
      
      while( iter.hasNext() ) {
        String mangledEntry = (String)iter.next();
        Stack refIDs = (Stack)indexEntries.get( mangledEntry );
        
        syncIndexPath( lastEntry, mangledEntry );
        
        for( int i = 0; i < refIDs.size(); ++i ) {
          int refID = ((Integer)refIDs.elementAt( i )).intValue();
          w.write( encodeForXMLText( new Character((char)160/*nbsp*/).toString() ) + "<a href=\"#index" + refID + "\">" + (i+1) + "</a>" );
        }
        w.write( "<br />" );
        lastEntry = mangledEntry;
      }
      w.write( indentation() + "</p>" ); nl(w);
    }
  }
  
  
  /**
   * calculates the index entry path stack of the passed path s
   */
  private Stack calcIndexPathStack( String s ) {
    Stack nameSequence = new Stack();
    StringBuffer sb = new StringBuffer(32);
    
    for( int i = 0; i < s.length(); ++i ) {
      char c = s.charAt( i );
      
      switch( c ) {
        case Constants.kNameManglingSeparator:
          nameSequence.push( sb.toString() );
          sb = new StringBuffer( 32 );
          break;
          
        default:
          sb.append( c );
          break;
      }
    }
    
    if( sb.length() > 0 ) {
      nameSequence.push( sb.toString() );
    }
    
    return nameSequence;
  }
  
  /**
   * syncs the index name sequences: the first is the one we're at, the second one the one we'd like to be
   */
  private void syncIndexPath( String curPath, String destPath ) throws IOException {
    int icur = 0;
    int idest = 0;
    Stack cur = calcIndexPathStack( curPath );
    Stack dest = calcIndexPathStack( destPath );
    
    // Skip identical path segment
    while( cur.size() > icur && dest.size() > idest && cur.elementAt( icur ).equals( dest.elementAt( idest ) ) ) {
      ++icur;
      ++idest;
    }

    // Open the new path
    for( int i = idest; i < dest.size(); ++i ) {
      if( i == 0 ) { // Starting new entry from root:
        w.write( "</p>" ); nl(w);
        w.write( indentation() + "<p>" ); // We enclose each new root in a paragraph
      }

      w.write( indentation() + indentation( i * indentationFactor, encodeForXMLText( new Character( (char)160 /*nbsp*/).toString() ) ) + dest.elementAt( i ) ); // Keyword path segment

      if( i != dest.size() -1 )
        w.write( "<br />" ); nl(w);
    }
  }

// *************************************************************************
// UI Code
// *************************************************************************

  // Elements
  
  JCheckBox IncludeTOCCheckbox;
  JCheckBox AllowEmptyCellsCheckbox;
  JCheckBox InlineStylesheetCheckbox;
  JCheckBox IncludeVisualCheckbox;

  JComboBox FormHandlingPopup;
  
  JTextField DoctypeDeclField;
  JTextField CSSRefField;
  JTextField UnicodeTranslationMapField;
  JTextField CSSUnitMapField;
  
  // Labels
  JLabel DoctypeLabel;
  JLabel CSSRefLabel;
  JLabel UnicodeTranslationLabel;
  JLabel CSSUnitsLabel;
  JLabel OptionsLabel;
  JLabel FormHandlingLabel;

  // Components
  JPanel tabXHTMLContainer;
  JPanel tabAdvancedContainer;

  /**
   * gets called by framework to retrieve any additional editor dialog tabs (=JPanels) we might want to
   * add to the standard editor.
   */
   
  protected Stack getCustomConfigurationEditorTabs() {
    Stack editors = new Stack();

   /* Create Objects...
    */

   // Elements
//    IncludeTOCCheckbox = new JCheckBox( "Include <toc> element contents" );
    AllowEmptyCellsCheckbox = new JCheckBox( "Allow empty table cell elements" );
    InlineStylesheetCheckbox = new JCheckBox( "Add inline CSS stylesheet" );
//    IncludeVisualCheckbox = new JCheckBox( "Include layout information" );
    
    FormHandlingPopup = new JComboBox();
    FormHandlingPopup.addItem( "Discard" );           /* index 0 */
    FormHandlingPopup.addItem( "Render as Text" );    /* index 1 */
    FormHandlingPopup.addItem( "Create HTML Form" );  /* index 2 */

    DoctypeDeclField = new JTextField( 22 );
    CSSRefField = new JTextField( 16 );
    UnicodeTranslationMapField = new JTextField( 16 );
    CSSUnitMapField = new JTextField( 16 );
    
    // Labels
    DoctypeLabel = new JLabel( "DOCTYPE declaration:" );
    CSSRefLabel = new JLabel( "External stylesheet:" );
    UnicodeTranslationLabel = new JLabel( "Unicode translation map:" );
    CSSUnitsLabel = new JLabel( "CSS property unit table:" );
    OptionsLabel = new JLabel( "Options:" );
    FormHandlingLabel = new JLabel( "Form elements:" );
    
    // Components
    JPanel tabXHTMLContainer;
    JPanel tabExpertContainer;


    // Tooltips
//    IncludeTOCCheckbox.setToolTipText( "When checked, the <toc> (table of contents) element contents will be included" );
    AllowEmptyCellsCheckbox.setToolTipText( "When checked, empty cells will not be filled with a non-breaking space character" );
    InlineStylesheetCheckbox.setToolTipText( "When checked, an inline CSS stylesheet will be added to the document" );
//    IncludeVisualCheckbox.setToolTipText( "When checked, layout information is included in the output" );
    DoctypeDeclField.setToolTipText( "DOCTYPE declaration to use; leave empty to suppress or use '*' for default" );
    CSSRefField.setToolTipText( "Custom stylesheet to reference; leave empty to suppress or use '${il:srcbasename}.css' for default" );
    UnicodeTranslationMapField.setToolTipText( "Custom Unicode output map to use, use 'upcast:html-map' for default" );
    CSSUnitMapField.setToolTipText( "Custom CSS unit map to use, use 'upcast:default-map' for default" );
    FormHandlingPopup.setToolTipText( "Determines how Form elements should be handled" );

   // Components
   tabXHTMLContainer = new JPanel( new ParagraphLayout( 12, 12, 12, 16, 2, 2 ) );
   tabXHTMLContainer.add( OptionsLabel, ParagraphLayout.NEW_PARAGRAPH );
//   tabXHTMLContainer.add( IncludeVisualCheckbox );
   tabXHTMLContainer.add( InlineStylesheetCheckbox /*, ParagraphLayout.NEW_LINE*/ );
   tabXHTMLContainer.add( AllowEmptyCellsCheckbox, ParagraphLayout.NEW_LINE );
//   tabXHTMLContainer.add( IncludeTOCCheckbox, ParagraphLayout.NEW_LINE );
   tabXHTMLContainer.add( FormHandlingLabel, ParagraphLayout.NEW_PARAGRAPH );
   tabXHTMLContainer.add( FormHandlingPopup );
   tabXHTMLContainer.add( CSSRefLabel, ParagraphLayout.NEW_PARAGRAPH );
   tabXHTMLContainer.add( CSSRefField );

   tabExpertContainer = new JPanel( new ParagraphLayout( 12, 12, 12, 16, 2, 2 ) );
   tabExpertContainer.add( DoctypeLabel, ParagraphLayout.NEW_PARAGRAPH );
   tabExpertContainer.add( DoctypeDeclField );
   tabExpertContainer.add( UnicodeTranslationLabel, ParagraphLayout.NEW_PARAGRAPH );
   tabExpertContainer.add( UnicodeTranslationMapField );
   tabExpertContainer.add( CSSUnitsLabel, ParagraphLayout.NEW_PARAGRAPH );
   tabExpertContainer.add( CSSUnitMapField );
   
   editors.push( new Pair( "XHTML", tabXHTMLContainer ) );
   editors.push( new Pair( "Advanced", tabExpertContainer ) );

   // Init with current values:
//   IncludeTOCCheckbox.setSelected( getParameterBoolean( kWriteTOCParamName, false ) );
   AllowEmptyCellsCheckbox.setSelected( getParameterBoolean( kAllowEmptyCellsParamName, true ) );
   InlineStylesheetCheckbox.setSelected( getParameterBoolean( kInlineCSSStylesheetParamName, false ) );
//   IncludeVisualCheckbox.setSelected( getParameterBoolean( kIncludeVisualElementsParamName, true ) );

    String tmp = getParameterString( kFormHandlingParamName, "text" ); /* discard | text | form */
    if( tmp.equals( "discard" ) )
      FormHandlingPopup.setSelectedIndex( 0 );
    else if( tmp.equals( "text" ) )
      FormHandlingPopup.setSelectedIndex( 1 );
    else if( tmp.equals( "form" ) )
      FormHandlingPopup.setSelectedIndex( 2 );
    else
      FormHandlingPopup.setSelectedIndex( 1 );

   DoctypeDeclField.setText( getParameterString( kDOCTYPEDeclarationParamName, "*" ) );
   CSSRefField.setText( getParameterString( kCSSRefParamName, "${il:srcbasename}.css" ) );
   UnicodeTranslationMapField.setText( getParameterString( kUnicodeTranslationMapParamName, "upcast:html-map" ) );
   CSSUnitMapField.setText( getParameterString( kCSSPropertyUnitMapParamName, "upcast:default-map" ) );

/*
    if( getParamIncludeVisualCheckbox.isSelected() ) {
      InlineStylesheetCheckbox.setSelected( getParameterBoolean( kInlineCSSStylesheetParamName, false ) );
      InlineStylesheetCheckbox.setEnabled( true );
      CSSRefField.setEnabled( true );
    } else {
      InlineStylesheetCheckbox.setSelected( false );
      InlineStylesheetCheckbox.setEnabled( false );
      CSSRefField.setEnabled( false );
    }
*/
/*
   IncludeVisualCheckbox.addActionListener(new java.awt.event.ActionListener() {
    	public void actionPerformed(java.awt.event.ActionEvent e) {
    	  if( IncludeVisualCheckbox.isSelected() ) {
    	    InlineStylesheetCheckbox.setSelected( getParameterBoolean( kInlineCSSStylesheetParamName, false ) );
    	    InlineStylesheetCheckbox.setEnabled( true );
    	    CSSRefField.setEnabled( true );
    	  } else {
    	    InlineStylesheetCheckbox.setSelected( false );
    	    InlineStylesheetCheckbox.setEnabled( false );
    	    CSSRefField.setEnabled( false );
    	  }
    	}
    });
*/
   return editors;
  }

  /**
   * called after the dialog was dismissed. Use to update configuration.
   */
  protected void customConfigurationEditorDismissed( boolean ok ) {
    if( ok ) {
//      setParameter( kWriteTOCParamName, new Boolean( IncludeTOCCheckbox.isSelected() ) );
      setParameter( kAllowEmptyCellsParamName, new Boolean( AllowEmptyCellsCheckbox.isSelected() ) );
      setParameter( kInlineCSSStylesheetParamName, new Boolean( InlineStylesheetCheckbox.isSelected() ) );
//      setParameter( kIncludeVisualElementsParamName, new Boolean( IncludeVisualCheckbox.isSelected() ) );

      switch( FormHandlingPopup.getSelectedIndex() ) {
        case 0: setParameter( kFormHandlingParamName, "discard" ); break;
        default:
        case 1: setParameter( kFormHandlingParamName, "text" ); break;
        case 2: setParameter( kFormHandlingParamName, "form" ); break;
      }

      setParameter( kDOCTYPEDeclarationParamName, DoctypeDeclField.getText() );
      setParameter( kCSSRefParamName, CSSRefField.getText() );
      setParameter( kUnicodeTranslationMapParamName, UnicodeTranslationMapField.getText() );
      setParameter( kCSSPropertyUnitMapParamName, CSSUnitMapField.getText() );
      
    }
  }

}

