package de.duehl.basics.text.html.generation;

/*
 * Copyright 2024 Christian Dühl. All rights reserved.
 *
 * This program is free software. You can redistribute it and/or
 * modify it under the same terms as perl:
 *
 * general:  http://dev.perl.org/licenses/
 * GPL:      http://dev.perl.org/licenses/gpl1.html
 * artistic: http://dev.perl.org/licenses/artistic.html
 */

import de.duehl.basics.collections.CollectionsHelper;
import de.duehl.basics.text.Text;
import de.duehl.basics.text.html.generation.tools.Utf8MetaExchanger;
import de.duehl.basics.text.xml.NamedXmlParameter;
import de.duehl.basics.text.xml.generation.XmlBuilder;

import static de.duehl.basics.text.html.generation.HtmlHeaderType.*;

import java.util.ArrayList;
import java.util.List;

/**
 * Diese Klasse stellt einen StringBuilder zum Erstellen von HTML-Seiten dar, welcher die
 * Einrücktiefe selbst verwaltet.
 *
 * Hier sind nur solche Methoden enthalten, die eine externe CSS-Datei erwarten oder ganz ohne CSS
 * auskommen.
 *
 * @version 1.01     2024-07-10
 * @author Christian Dühl
 */

public class HtmlBuilder extends XmlBuilder {

    static final String n = Text.LINE_BREAK;

    private static final String TOP_LINK_NAME = "top";

    static final String CSS = ""
            + "<style>" + n
            + "    body {" + n
            + "        background-color:#E6E6FF;" + n
            + "        color:#000096;" + n
            + "        font-family:Verdana, sans-serif;" + n
            + "        font-size:14pt;" + n
            + "        margin-left:3%;" + n
            + "        margin-right:3%;" + n
            + "        color:#000096;" + n
            + "    }" + n
            + "    p {" + n
            + "        font-size:1em;" + n
            + "        font-family:Verdana, sans-serif;" + n
            + "        color:#000096;" + n
            + "    }" + n
            + "    h1 {" + n
            + "        text-align: left;" + n
            + "        font-size: 36pt;" + n
            + "        font-weight: bold;" + n
            + "        color:#000096;" + n
            + "        margin-top:25;" + n
            + "    }" + n
            + "    h1#id1 {" + n
            + "        margin-top:0;" + n
            + "    }" + n
            + "    h2 {" + n
            + "        text-align: left;" + n
            + "        font-size: 24pt;" + n
            + "        font-weight: bold;" + n
            + "        color:#000096;" + n
            + "        margin-bottom:8;" + n
            + "        margin-top:15;" + n
            + "    }" + n
            + "    h3 {" + n
            + "        font-size: 20pt;" + n
            + "        color:#000096;" + n
            + "        margin-bottom:0;" + n
            + "    }" + n
            + "    h4 {" + n
            + "        font-size: 18pt;" + n
            + "        color:#000096;" + n
            + "        margin-bottom:0;" + n
            + "    }" + n
            + "    h5 {" + n
            + "        font-size: 16pt;" + n
            + "        color:#000096;" + n
            + "        margin-bottom:0;" + n
            + "    }" + n
            + "    h6 {" + n
            + "        font-size: 15pt;" + n
            + "        color:#000096;" + n
            + "        margin-bottom:0;" + n
            + "    }" + n
            + "    .monospace {" + n
            + "        font-family: monospace;" + n
            + "    }" + n
            + "</style>"
            ;

    protected int numberOfH1 = 0;
    protected int numberOfH2 = 0;
    protected int numberOfH3 = 0;
    protected int numberOfH4 = 0;
    protected int numberOfH5 = 0;
    protected int numberOfH6 = 0;

    protected List<HtmlHeader> headers;

    private boolean hasContents;

    /**
     * Gibt an, ob die Links zum Seitenanfang über einer neuen H1-Überschrift angezeigt werden
     * sollen.
     */
    private boolean showTopLink;

    /**
     * Gibt an, ob die Inhaltsverzeichnisse mit Hilfe von Namen anstelle von IDs erzeugt werden.
     * Damit gab es in der Darstellung innerhalb von Swing Probleme. Außerhalb funktionieren die
     * IDs wunderbar.
     */
    private boolean createContentWithNames;

    /**
     * Gibt an, ob auch vor dem Anfang einer neuen Überschrift vom Typ H2 ein Link zum Anfang
     * angezeigt werden soll.
     */
    private boolean appendTopLinksToH2;

    /** Konstruktor. */
    public HtmlBuilder() {
        headers = new ArrayList<>();
        hasContents = false;
        showTopLink = true;
        createContentWithNames = false;
        appendTopLinksToH2 = false;
    }

    /**
     * Erzeugt die Inhaltsverzeichnisse mit Hilfe von Namen anstelle von IDs. Damit gab es in der
     * Darstellung innerhalb von Swing Probleme. Außerhalb funktionieren die IDs wunderbar.
     */
    public HtmlBuilder createContentWithNames() {
        createContentWithNames = true;
        return this;
    }

    /** Unterdrückt die Links zum Seitenanfang über einer neuen H1-Überschrift. */
    public HtmlBuilder hideTopLinks() {
        showTopLink = false;
        return this;
    }

    /** Erhöht das Level der Einrückung. */
    @Override
    public HtmlBuilder increaseIndentationLevel() {
        super.increaseIndentationLevel();
        return this;
    }

    /** Verringert das Level der Einrückung. */
    @Override
    public HtmlBuilder decreaseIndentationLevel() {
        super.decreaseIndentationLevel();
        return this;
    }

    /** Setzt das Level der Einrückung auf 0. */
    @Override
    public HtmlBuilder setIndentationLevelToZero() {
        super.setIndentationLevelToZero();
        return this;
    }

    /**
     * Gibt an, dass keine komplette HTML-Seite erzeugt werden soll, sondern ein Bereich innerhalb
     * des Bodies.
     */
    public HtmlBuilder createHtmlPartInBody() {
        setIndentationLevel(2); // html und body
        return this;
    }

    /** Löscht den Inhalt des HtmlBuilders. */
    @Override
    public HtmlBuilder clear() {
        super.clear();
        return this;
    }

    /** Fügt beliebigen Text hinzu. */
    @Override
    public HtmlBuilder append(String text) {
        super.append(text);
        return this;
    }

    /** Fügt beliebigen Text hinzu. Dieser wird passend eingerückt.*/
    @Override
    public HtmlBuilder appendIndented(String text) {
        super.appendIndented(text);
        return this;
    }

    /** Fügt beliebigen Text und einen Zeilenumbruch hinzu. Der Text wird passend eingerückt. */
    @Override
    public HtmlBuilder appendLn(String text) {
        super.appendLn(text);
        return this;
    }

    /**
     * Fügt die übergebenen Zeilen (der String wird an Zeilenumbrüchen dafür aufgetrennt) passend
     * eingerückt hinzu. Dafür sollte der übergebene Text in der kleinsten Einrückung die
     * Einrücktiefe null aufweisen, damit alles richtig aussieht.
     */
    public HtmlBuilder appendMultipleLines(String linesToSplit) {
        String lines = Text.removeLineBreakAtEndIfEndsWith(linesToSplit);
        List<String> list = Text.splitByLineBreaks(lines);
        return appendMultipleLines(list);
    }

    /**
     * Fügt die übergebenen Zeilen passend eingerückt hinzu. Dafür sollten die übergebenen Zeilen
     * in der kleinsten Einrückung die Einrücktiefe null aufweisen, damit alles richtig aussieht.
     */
    @Override
    public HtmlBuilder appendMultipleLines(String ... lines) {
        super.appendMultipleLines(lines);
        return this;
    }

    /**
     * Fügt die übergebenen Zeilen passend eingerückt hinzu. Dafür sollten die übergebenen Zeilen
     * in der kleinsten Einrückung die Einrücktiefe null aufweisen, damit alles richtig aussieht.
     */
    @Override
    public HtmlBuilder appendMultipleLines(List<String> lines) {
        super.appendMultipleLines(lines);
        return this;
    }

    /** Fügt eine Zahl hinzu. */
    @Override
    public HtmlBuilder append(int number) {
        super.append(number);
        return this;
    }

    /** Fügt eine Zahl hinzu. Diese wird passend eingerückt. */
    @Override
    public HtmlBuilder appendIndented(int number) {
        super.appendIndented(number);
        return this;
    }

    /** Fügt einen Zeilenumbruch im Dokument, aber nicht im HTML hinzu. */
    @Override
    public HtmlBuilder appendLineBreak() {
        super.appendLineBreak();
        return this;
    }

    /** Fügt einen Zeilenumbruch im HTML und im Dokument hinzu. */
    public HtmlBuilder appendHtmlLineBreak() {
        append("<br />");
        appendLineBreak();
        return this;
    }

    /**
     * Fügt einen Kopf für HTML 5 hinzu.
     *
     * @param pageTitle
     *            Titel der Seite.
     */
    public HtmlBuilder appendHtml5HeadWithTitle(String pageTitle) {
        appendUpperHeadPartHtml5(pageTitle);
        appendLowerHeadPart();
        return this;
    }

    /**
     * Fügt einen Kopf für HTML 5 hinzu.
     *
     * @param pageTitle
     *            Titel der Seite.
     */
    public HtmlBuilder appendHtml5HeadWithTitleUtf8(String pageTitle) {
        appendUpperHeadPartHtml5(pageTitle);
        appendMetaUtf8();
        appendLowerHeadPart();
        return this;
    }

    /**
     * Fügt einen Kopf für HTML 5 hinzu.
     *
     * @param pageTitle
     *            Titel der Seite.
     * @param cssFilename
     *            Name der CSS-Datei.
     */
    public HtmlBuilder appendHtml5HeadWithTitleAndCssFilename(String pageTitle, String cssFilename) {
        appendUpperHeadPartHtml5(pageTitle);
        appendMetaCssFilename(cssFilename);
        appendLowerHeadPart();
        return this;
    }

    /**
     * Fügt einen Kopf für HTML 5 hinzu.
     *
     * @param pageTitle
     *            Titel der Seite.
     * @param cssFilename
     *            Name der CSS-Datei.
     * @param javaScript
     *            JavaScript, welches in den Kopf eingebettet wird. Hier kann auch sonstiges
     *            "fertiges" HTML mitgegeben werden, das im Kopf-Bereich eingefügt werden soll.
     *            Dafür sollte der übergebene Text in der kleinsten Einrückung die Einrücktiefe
     *            null aufweisen, damit alles richtig aussieht.
     */
    public HtmlBuilder appendHtml5HeadWithTitleCssFilenameAndJavaScript(String pageTitle,
            String cssFilename, String javaScript) {
        appendUpperHeadPartHtml5(pageTitle);
        appendMetaCssFilename(cssFilename);
        appendMultipleLines(javaScript);
        appendLowerHeadPart();
        return this;
    }

    /**
     * Fügt einen Kopf für HTML 5 hinzu.
     *
     * @param pageTitle
     *            Titel der Seite.
     * @param cssFilenames
     *            Liste mit Namen von CSS-Dateien.
     * @param javaScript
     *            JavaScript, welches in den Kopf eingebettet wird. Hier kann auch sonstiges
     *            "fertiges" HTML mitgegeben werden, das im Kopf-Bereich eingefügt werden soll.
     *            Dafür sollte der übergebene Text in der kleinsten Einrückung die Einrücktiefe
     *            null aufweisen, damit alles richtig aussieht.
     */
    public HtmlBuilder appendHtml5HeadWithTitleMultipleCssFilenameAndJavaScript(String pageTitle,
            List<String> cssFilenames, String javaScript) {
        appendUpperHeadPartHtml5(pageTitle);
        for (String cssFilename : cssFilenames) {
            appendMetaCssFilename(cssFilename);
        }
        appendMultipleLines(javaScript);
        appendLowerHeadPart();
        return this;
    }

    /**
     * Fügt einen Kopf für HTML 5 hinzu.
     *
     * @param pageTitle
     *            Titel der Seite.
     * @param cssFilenames
     *            Liste mit Namen von CSS-Dateien.
     * @param additionalLines
     *            Weitere Zeilen, welche in den Kopf eingebettet werden. Hier kann auch sonstiges
     *            "fertiges" HTML mitgegeben werden, das im Kopf-Bereich eingefügt werden soll.
     *            Dafür sollte der übergebene Text in der kleinsten Einrückung die Einrücktiefe
     *            null aufweisen, damit alles richtig aussieht.
     */
    public HtmlBuilder appendHtml5HeadWithTitleMultipleCssFilenameAndAdditionalLines(
            String pageTitle, List<String> cssFilenames, List<String> additionalLines) {
        appendUpperHeadPartHtml5(pageTitle);
        for (String cssFilename : cssFilenames) {
            appendMetaCssFilename(cssFilename);
        }
        appendMultipleLines(additionalLines);
        appendLowerHeadPart();
        return this;
    }

    /**
     * Fügt einen Kopf für HTML 5 hinzu.
     *
     * @param pageTitle
     *            Titel der Seite.
     * @param cssFilenames
     *            Liste mit Namen von CSS-Dateien.
     * @param additionalLines
     *            Weitere Zeilen, welche in den Kopf eingebettet werden. Hier kann auch sonstiges
     *            "fertiges" HTML mitgegeben werden, das im Kopf-Bereich eingefügt werden soll.
     *            Dafür sollte der übergebene Text in der kleinsten Einrückung die Einrücktiefe
     *            null aufweisen, damit alles richtig aussieht.
     */
    public HtmlBuilder appendHtml5HeadWithTitleMultipleCssFilenameAndAdditionalLinesUtf8(
            String pageTitle, List<String> cssFilenames, List<String> additionalLines) {
        appendUpperHeadPartHtml5(pageTitle);
        appendMetaUtf8();
        for (String cssFilename : cssFilenames) {
            appendMetaCssFilename(cssFilename);
        }
        appendMultipleLines(additionalLines);
        appendLowerHeadPart();
        return this;
    }

    /**
     * Fügt einen Kopf für HTML 5 hinzu.
     *
     * @param pageTitle
     *            Titel der Seite.
     * @param javaScript
     *            JavaScript, welches in den Kopf eingebettet wird. Hier kann auch sonstiges
     *            "fertiges" HTML mitgegeben werden, das im Kopf-Bereich eingefügt werden soll.
     *            Dafür sollte der übergebene Text in der kleinsten Einrückung die Einrücktiefe
     *            null aufweisen, damit alles richtig aussieht.
     * @param cssFilenames
     *            Mehrere Namen von CSS-Dateien.
     */
    public HtmlBuilder appendHtml5HeadWithTitleJavaScriptAndMultipleCssFilenames(String pageTitle,
            String javaScript, String ... cssFilenames) {
        appendUpperHeadPartHtml5(pageTitle);
        for (String cssFilename : cssFilenames) {
            appendMetaCssFilename(cssFilename);
        }
        appendMultipleLines(javaScript);
        appendLowerHeadPart();
        return this;
    }

    /**
     * Fügt einen Kopf mit selbst erzeugtem Standard-CSS für body, p und die Überschriften hinzu.
     *
     * @param pageTitle
     *            Titel der Seite.
     */
    public HtmlBuilder appendHtml5HeadWithOwnCss(String pageTitle) {
        return appendHtml5HeadWithOwnCss(pageTitle, CSS);
    }

    /**
     * Fügt einen Kopf mit selbst erzeugtem Standard-CSS für body, p und die Überschriften hinzu,
     * wobei eine Metainformation ergänzt wird, dass der Inhalt in UTF-8 vorliegt.
     *
     * @param pageTitle
     *            Titel der Seite.
     */
    public HtmlBuilder appendHtml5HeadWithOwnCssUtf8(String pageTitle) {
        return appendHtml5HeadWithOwnCssUtf8(pageTitle, CSS);
    }

    /**
     * Fügt einen Kopf mit selbst erzeugtem CSS für body, p und die Überschriften hinzu.
     *
     * @param pageTitle
     *            Titel der Seite.
     * @param extendedCss
     *            Fügt diesen CSS-Abschnitt am Ende des in den Kopf der HTML-Seite einzufügenden
     *            Standard-CSS ein.
     */
    public HtmlBuilder appendHtml5HeadWithOwnExtendedCss(String pageTitle, String extendedCss) {
        String css = extendStandardCss(extendedCss);
        return appendHtml5HeadWithOwnCss(pageTitle, css);
    }

    /**
     * Fügt einen Kopf mit selbst erzeugtem CSS für body, p und die Überschriften hinzu, wobei eine
     * Metainformation ergänzt wird, dass der Inhalt in UTF-8 vorliegt.
     *
     * @param pageTitle
     *            Titel der Seite.
     * @param extendedCss
     *            Fügt diesen CSS-Abschnitt am Ende des in den Kopf der HTML-Seite einzufügenden
     *            Standard-CSS ein.
     */
    public HtmlBuilder appendHtml5HeadWithOwnExtendedCssUtf8(String pageTitle,
            String extendedCss) {
        String css = extendStandardCss(extendedCss);
        return appendHtml5HeadWithOwnCssUtf8(pageTitle, css);
    }

    static String extendStandardCss(String extendedCss) {
        int closingStyleTagIndex = CSS.indexOf("</style>");
        if (-1 == closingStyleTagIndex) {
            throw new RuntimeException("Es wurde kein </style> im Standard-CSS gefunden!");
        }
        return CSS.substring(0, closingStyleTagIndex)
                + extendedCss + n
                + CSS.substring(closingStyleTagIndex);
    }

    /** Erzeugt eine CSS-Erweiterung, welche die Fontgröße setzt. */
    public static String createBodyFontSizeInPointCssExtension(int point) {
        return ""
                + "    body {" + n
                + "        font-size:" + point + "pt;" + n
                + "    }" //+ n
                ;
    }

    /**
     * Fügt einen Kopf mit selbst erzeugtem CSS für body, p und die Überschriften hinzu.
     *
     * @param pageTitle
     *            Titel der Seite.
     * @param css
     *            In den Kopf der HTML-Seite einzufügendes CSS.
     */
    public HtmlBuilder appendHtml5HeadWithOwnCss(String pageTitle, String css) {
        boolean useUtf8 = false;
        return appendHtml5HeadWithOwnCssInternal(pageTitle, useUtf8, css);
    }

    /**
     * Fügt einen Kopf mit selbst erzeugtem CSS für body, p und die Überschriften hinzu, wobei eine
     * Metainformation ergänzt wird, dass der Inhalt in UTF-8 vorliegt.
     *
     * @param pageTitle
     *            Titel der Seite.
     * @param css
     *            In den Kopf der HTML-Seite einzufügendes CSS.
     */
    public HtmlBuilder appendHtml5HeadWithOwnCssUtf8(String pageTitle, String css) {
        boolean useUtf8 = true;
        return appendHtml5HeadWithOwnCssInternal(pageTitle, useUtf8, css);
    }

    /**
     * Fügt einen Kopf mit selbst erzeugtem CSS für body, p und die Überschriften hinzu.
     *
     * @param pageTitle
     *            Titel der Seite.
     * @param useUtf8
     *            Gibt an, ob eine Metainformation ergänzt wird, dass der Inhalt in UTF-8 vorliegt.
     * @param css
     *            In den Kopf der HTML-Seite einzufügendes CSS.
     */
    private HtmlBuilder appendHtml5HeadWithOwnCssInternal(String pageTitle, boolean useUtf8,
            String css) {
        appendUpperHeadPartHtml5(pageTitle);
        if (useUtf8) {
            appendMetaUtf8();
        }
        appendMultipleLines(css);
        appendLowerHeadPart();
        return this;
    }

    /**
     * Erzeugt den Anfang des HTML5-Kopfes.
     *
     * @param pageTitle
     *            Titel der Seite.
     */
    protected final HtmlBuilder appendUpperHeadPartHtml5(String pageTitle) {
        appendLn("<!DOCTYPE html>");

        // Neu 10.01.2020:
        //appendOpeningTag("html");
        NamedXmlParameter langDe = new NamedXmlParameter("lang", "de");
        appendOpeningTagWithParameters("html", langDe);

        appendOpeningTag("head");

        // Neu 10.01.2020:
        //appendMetaUtf8();  // ausgebaut 04.02.2020, dafür gibt es extra Methoden!

        appendInTag("title", pageTitle);
        return this;
    }

    /** Ergänzt eine Metainformation, dass der Inhalt in UTF-8 vorliegt. */
    protected HtmlBuilder appendMetaUtf8() {
        //appendLn("<meta http-equiv=\"content-Type\" content=\"text/html; charset=utf-8\">");
        //appendLn("<meta charset=\"utf-8\">");
        appendLn(Utf8MetaExchanger.UTF8_META_SHORT);

        return this;
        /*
         * Siehe auch:
         *     https://www.w3.org/International/questions/qa-html-encoding-declarations.de
         */
    }

    /**
     * Ergänzt eine Metainformation zur einzubindenden CSS-Datei.
     *
     * @param cssFilename
     *            Name der CSS-Datei.
     */
    private HtmlBuilder appendMetaCssFilename(String cssFilename) {
        appendLn("<link href=\"" + cssFilename + "\" rel=\"stylesheet\" type=\"text/css\" />");
        return this;
    }

    /** Erzeugt den Abschluss des HTML-Kopfes. */
    protected final HtmlBuilder appendLowerHeadPart() {
        appendClosingTag("head");
        appendLineBreak();
        appendLineBreak();
        appendOpeningTag("body");
        return this;
    }

    /**
     * Fügt einen Kopf mit der CSS-Datei format.css im gleichen Verzeichnis hinzu.
     *
     * Das macht natürlich nur Sinn, wenn der Inhalt des Builders anschließend gespeichert wird.
     *
     * @param pageTitle
     *            Titel der Seite.
     */
    public HtmlBuilder appendHtml5HeadWithFormatCssInSameDirectory(String pageTitle) {
        boolean useUtf8 = false;
        appendHtml5HeadWithFormatCssInSameDirectoryInternal(pageTitle, useUtf8);
        return this;
    }

    /**
     * Fügt einen Kopf mit der CSS-Datei format.css im gleichen Verzeichnis hinzu, wobei eine
     * Metainformation ergänzt wird, dass der Inhalt in UTF-8 vorliegt.
     *
     * Das macht natürlich nur Sinn, wenn der Inhalt des Builders anschließend gespeichert wird.
     *
     * @param pageTitle
     *            Titel der Seite.
     */
    public HtmlBuilder appendHtml5HeadWithFormatCssInSameDirectoryUtf8(String pageTitle) {
        boolean useUtf8 = true;
        appendHtml5HeadWithFormatCssInSameDirectoryInternal(pageTitle, useUtf8);
        return this;
    }

    /**
     * Fügt einen Kopf mit der CSS-Datei format.css im gleichen Verzeichnis hinzu.
     *
     * Das macht natürlich nur Sinn, wenn der Inhalt des Builders anschließend gespeichert wird.
     *
     * @param pageTitle
     *            Titel der Seite.
     * @param useUtf8
     *            Gibt an, ob eine Metainformation ergänzt wird, dass der Inhalt in UTF-8 vorliegt.
     */
    private HtmlBuilder appendHtml5HeadWithFormatCssInSameDirectoryInternal(String pageTitle,
            boolean useUtf8) {
        appendUpperHeadPartHtml5(pageTitle);
        if (useUtf8) {
            appendMetaUtf8();
        }
        appendFormatInSameDirectoryCssToHead();
        appendLowerHeadPart();
        return this;
    }

    /**
     * Fügt einen Kopf mit der CSS-Datei format.css im darüberliegendem Verzeichnis hinzu.
     *
     * Das macht natürlich nur Sinn, wenn der Inhalt des Builders anschließend gespeichert wird.
     *
     * @param pageTitle
     *            Titel der Seite.
     */
    public HtmlBuilder appendHtml5HeadWithFormatCssInUpperDirectory(String pageTitle) {
        boolean useUtf8 = false;
        appendHtml5HeadWithFormatCssInUpperDirectoryInternal(pageTitle, useUtf8);
        return this;
    }

    /**
     * Fügt einen Kopf mit der CSS-Datei format.css im darüberliegendem Verzeichnis hinzu, wobei
     * eine Metainformation ergänzt wird, dass der Inhalt in UTF-8 vorliegt.
     *
     * Das macht natürlich nur Sinn, wenn der Inhalt des Builders anschließend gespeichert wird.
     *
     * @param pageTitle
     *            Titel der Seite.
     */
    public HtmlBuilder appendHtml5HeadWithFormatCssInUpperDirectoryUtf8(String pageTitle) {
        boolean useUtf8 = true;
        appendHtml5HeadWithFormatCssInUpperDirectoryInternal(pageTitle, useUtf8);
        return this;
    }

    /**
     * Fügt einen Kopf mit der CSS-Datei format.css im darüberliegendem Verzeichnis hinzu.
     *
     * Das macht natürlich nur Sinn, wenn der Inhalt des Builders anschließend gespeichert wird.
     *
     * @param pageTitle
     *            Titel der Seite.
     * @param useUtf8
     *            Gibt an, ob eine Metainformation ergänzt wird, dass der Inhalt in UTF-8 vorliegt.
     */
    private HtmlBuilder appendHtml5HeadWithFormatCssInUpperDirectoryInternal(String pageTitle,
            boolean useUtf8) {
        appendUpperHeadPartHtml5(pageTitle);
        if (useUtf8) {
            appendMetaUtf8();
        }
        appendFormatInUpperDirectoryCssToHead();
        appendLowerHeadPart();
        return this;
    }

    /** Ergänzt den Hinweis auf die CSS-Datei format.css im gleichen Verzeichnis. */
    private HtmlBuilder appendFormatInSameDirectoryCssToHead() {
        appendLn("<link href=\"format.css\" rel=\"stylesheet\" type=\"text/css\" />");
        return this;
    }

    /** Ergänzt den Hinweis auf die CSS-Datei format.css im darüberliegendem Verzeichnis. */
    private HtmlBuilder appendFormatInUpperDirectoryCssToHead() {
        appendLn("<link href=\"../format.css\" rel=\"stylesheet\" type=\"text/css\" />");
        return this;
    }

    /** Schreibt einen öffnenden div-Tag in die Ausgabe. */
    public HtmlBuilder appendOpeningDiv() {
        appendOpeningTag("div");
        return this;
    }

    /**
     * Schreibt einen öffnenden div-Tag mit CSS-Klasse in die Ausgabe.
     *
     * @param cssClass
     *            CSS-Klasse des div-Tags.
     */
    public HtmlBuilder appendOpeningDivWithClass(String cssClass) {
        appendOpeningTagWithClass("div", cssClass);
        return this;
    }

    /**
     * Schreibt einen öffnenden div-Tag mit CSS-ID in die Ausgabe.
     *
     * @param id
     *            ID des div-Tags.
     */
    public HtmlBuilder appendOpeningDivWithId(String id) {
        appendOpeningTagWithId("div", id);
        return this;
    }

    /**
     * Schreibt einen öffnenden div-Tag mit CSS-ID und CSS-Klasse in die Ausgabe.
     *
     * @param id
     *            ID des div-Tags.
     * @param cssClass
     *            CSS-Klasse des div-Tags.
     */
    public HtmlBuilder appendOpeningDivWithIdAndClass(String id, String cssClass) {
        appendOpeningTagWithIdAndClass("div", id, cssClass);
        return this;
    }

    /** Schreibt einen schließenden div-Tag in die Ausgabe. */
    public HtmlBuilder appendClosingDiv() {
        appendClosingTag("div");
        return this;
    }

    /** Schreibt einen öffnenden ol-Tag in die Ausgabe. */
    public HtmlBuilder appendOpeningOl() {
        appendOpeningTag("ol");
        return this;
    }

    /**
     * Schreibt einen öffnenden ol-Tag in die Ausgabe, der bei der angegebenen Nummer beginnt.
     *
     * @param min
     *            Erste Zahl in der Aufzählung.
     */
    public HtmlBuilder appendOpeningOlStart(int min) {
        appendOpeningTagWithParameters("ol", "start=\"" + min + "\"");
        return this;
    }

    /** Schreibt einen schließenden ol-Tag in die Ausgabe. */
    public HtmlBuilder appendClosingOl() {
        appendClosingTag("ol");
        return this;
    }

    /** Schreibt einen öffnenden ul-Tag in die Ausgabe. */
    public HtmlBuilder appendOpeningUl() {
        appendOpeningTag("ul");
        return this;
    }

    /** Schreibt einen schließenden ul-Tag in die Ausgabe. */
    public HtmlBuilder appendClosingUl() {
        appendClosingTag("ul");
        return this;
    }

    /** Schreibt einen öffnenden li-Tag in die Ausgabe. */
    public HtmlBuilder appendOpeningLi() {
        appendOpeningTag("li");
        return this;
    }

    /** Schreibt einen schließenden li-Tag in die Ausgabe. */
    public HtmlBuilder appendClosingLi() {
        appendClosingTag("li");
        return this;
    }

    /**
     * Schreibt einen einzeiligen li-Tag in die Ausgabe.
     *
     * @param text
     *            Inhalt des li-Tags.
     */
    public HtmlBuilder appendLi(String text) {
        appendInTag("li", text);
        return this;
    }

    /**
     * Schreibt einen einzeiligen li-Tag in die Ausgabe.
     *
     * @param number
     *            Inhalt des li-Tags in Form einer Zahl.
     */
    public HtmlBuilder appendLi(int number) {
        appendLi(Integer.toString(number));
        return this;
    }

    /** Schreibt einen öffnenden table-Tag in die Ausgabe. */
    public HtmlBuilder appendOpeningTable() {
        appendOpeningTag("table");
        return this;
    }

    /**
     * Schreibt einen öffnenden table-Tag in die Ausgabe.
     *
     * @param borderWidth
     *            Breite des Rahmens um die Tabelle.
     */
    public HtmlBuilder appendOpeningTableWithBorderWidth(int borderWidth) {
        appendOpeningTagWithParameters("table", "border=\"" + borderWidth + "\"");
        return this;
    }

    /**
     * Schreibt einen öffnenden table-Tag mit Rahmen mit spezieller CSS-Klasse in die Ausgabe.
     *
     * @param cssClass
     *            Name der CSS-Klasse.
     */
    public HtmlBuilder appendOpeningTableWithClass(String cssClass) {
        appendOpeningTagWithClass("table", cssClass);
        return this;
    }

    /**
     * Schreibt einen öffnenden table-Tag mit Rahmen mit spezieller CSS-ID in die Ausgabe.
     *
     * @param id
     *            Name der CSS-ID.
     */
    public HtmlBuilder appendOpeningTableWithId(String id) {
        appendOpeningTagWithId("table", id);
        return this;
    }

    /** Schreibt einen schließenden table-Tag in die Ausgabe. */
    public HtmlBuilder appendClosingTable() {
        appendClosingTag("table");
        return this;
    }

    /** Schreibt einen öffnenden thead-Tag in die Ausgabe. */
    public HtmlBuilder appendOpeningThead() {
        appendOpeningTag("thead");
        return this;
    }

    /** Schreibt einen schließenden thead-Tag in die Ausgabe. */
    public HtmlBuilder appendClosingThead() {
        appendClosingTag("thead");
        return this;
    }

    /** Schreibt einen öffnenden tbody-Tag in die Ausgabe. */
    public HtmlBuilder appendOpeningTbody() {
        appendOpeningTag("tbody");
        return this;
    }

    /** Schreibt einen schließenden tbody-Tag in die Ausgabe. */
    public HtmlBuilder appendClosingTbody() {
        appendClosingTag("tbody");
        return this;
    }

    /** Schreibt einen öffnenden tfoot-Tag in die Ausgabe. */
    public HtmlBuilder appendOpeningTfoot() {
        appendOpeningTag("tfoot");
        return this;
    }

    /** Schreibt einen schließenden tfoot-Tag in die Ausgabe. */
    public HtmlBuilder appendClosingTfoot() {
        appendClosingTag("tfoot");
        return this;
    }

    /** Schreibt einen öffnenden tr-Tag in die Ausgabe. */
    public HtmlBuilder appendOpeningTr() {
        appendOpeningTag("tr");
        return this;
    }

    /** Schreibt einen öffnenden tr-Tag mit eigenem CSS-Style in die Ausgabe. */
    public HtmlBuilder appendOpeningTrWithClass(String cssClass) {
        appendOpeningTagWithClass("tr", cssClass);
        return this;
    }

    /** Schreibt einen schließenden tr-Tag in die Ausgabe. */
    public HtmlBuilder appendClosingTr() {
        appendClosingTag("tr");
        return this;
    }

    /** Schreibt einen öffnenden th-Tag in die Ausgabe. */
    public HtmlBuilder appendOpeningTh() {
        appendOpeningTag("th");
        return this;
    }

    /** Schreibt einen linksbündigen öffnenden th-Tag in die Ausgabe. */
    public HtmlBuilder appendOpeningLeftAlignedTh() {
        appendOpeningTagWithParameters("th", "align=\"left\"");
        return this;
    }

    /** Schreibt einen schließenden th-Tag in die Ausgabe. */
    public HtmlBuilder appendClosingTh() {
        appendClosingTag("th");
        return this;
    }

    /**
     * Schreibt einen th-Tag mit Inhalt in die Ausgabe.
     *
     * @param headerText
     *            Text der in den th-Tag kommt.
     */
    public HtmlBuilder appendTh(String headerText) {
        appendInTag("th", headerText);
        return this;
    }

    /**
     * Schreibt einen th-Tag mit Inhalt und der angegebenen Klasse in die
     * Ausgabe.
     *
     * @param headerText
     *            Text der in den th-Tag kommt.
     * @param cssClass
     *            CSS-Klasse.
     */
    public HtmlBuilder appendTh(String headerText, String cssClass) {
        appendInTagWithClass("th", headerText, cssClass);
        return this;
    }

    /**
     * Schreibt einen linksbündigen th-Tag mit Inhalt in die Ausgabe.
     *
     * @param text
     *            Text der in den th-Tag kommt.
     */
    public HtmlBuilder appendLeftAlignedTh(String text) {
        appendInTagWithParameters("th", text, "align=\"left\"");
        return this;
    }

    /**
     * Schreibt einen linksbündigen th-Tag mit Inhalt und spezieller CSS-Klasse in die Ausgabe.
     *
     * @param text
     *            Text der in den th-Tag kommt.
     * @param cssClass
     *            Name der CSS-Klasse.
     */
    public HtmlBuilder appendLeftAlignedThWithClass(String text, String cssClass) {
        appendInTagWithParameters("th", text, "class=\"" + cssClass + "\"", "align=\"left\"");
        return this;
    }

    /**
     * Schreibt einen rechtsbündigen th-Tag mit Inhalt in die Ausgabe.
     *
     * @param text
     *            Text der in den th-Tag kommt.
     */
    public HtmlBuilder appendRightAlignedTh(String text) {
        appendInTagWithParameters("th", text, "align=\"right\"");
        return this;
    }

    /** Schreibt einen öffnenden td-Tag in die Ausgabe. */
    public HtmlBuilder appendOpeningTd() {
        appendOpeningTag("td");
        return this;
    }

    /** Schreibt einen schließenden td-Tag in die Ausgabe. */
    public HtmlBuilder appendClosingTd() {
        appendClosingTag("td");
        return this;
    }

    /**
     * Schreibt einen td-Tag mit Inhalt in die Ausgabe.
     *
     * @param dataText
     *            Text der in den td-Tag kommt.
     */
    public HtmlBuilder appendTd(String dataText) {
        appendInTag("td", dataText);
        return this;
    }

    /**
     * Schreibt einen td-Tag mit Inhalt in die Ausgabe.
     *
     * @param dataText
     *            Text der in den td-Tag kommt.
     * @param cssClass
     *            Name der CSS-Klasse.
     */
    public HtmlBuilder appendTd(String dataText, String cssClass) {
        appendInTagWithClass("td", dataText, cssClass);
        return this;
    }

    /**
     * Schreibt einen rechtsbündigen td-Tag mit Inhalt in die Ausgabe.
     *
     * @param text
     *            Text der in den th-Tag kommt.
     */
    public HtmlBuilder appendRightAlignedTd(String text) {
        appendInTagWithParameters("td", text, "align=\"right\"");
        return this;
    }

    /**
     * Schreibt einen td-Tag mit Inhalt in die Ausgabe.
     *
     * @param number
     *            Zahl die in den td-Tag kommt.
     */
    public HtmlBuilder appendTd(int number) {
        appendTd(Integer.toString(number));
        return this;
    }

    /**
     * Fügt der Tabelle eine Zeile mit den übergebenen Überschriften hinzu. Die Tabellenzeile (tr)
     * wird mit angelegt.
     */
    public HtmlBuilder appendHeaderRow(List<String> headers) {
        appendOpeningTr();
        for (String header : headers) {
            appendTh(header);
        }
        appendClosingTr();
        return this;
    }

    /**
     * Fügt der Tabelle eine Zeile mit den übergebenen Überschriften hinzu. Die Tabellenzeile (tr)
     * wird mit angelegt.
     */
    public HtmlBuilder appendHeaderRow(String... headers) {
        List<String> list = CollectionsHelper.stringArrayToList(headers);
        return appendHeaderRow(list);
    }

    /**
     * Fügt der Tabelle eine linksbündige Zeile mit den übergebenen Überschriften hinzu. Die
     * Tabellenzeile (tr) wird mit angelegt.
     */
    public HtmlBuilder appendLeftAlignedHeaderRow(List<String> headers) {
        appendOpeningTr();
        for (String header : headers) {
            appendLeftAlignedTh(header);
        }
        appendClosingTr();
        return this;
    }

    /**
     * Fügt der Tabelle eine linksbündige Zeile mit den übergebenen Überschriften hinzu. Die
     * Tabellenzeile (tr) wird mit angelegt.
     */
    public HtmlBuilder appendLeftAlignedHeaderRow(String... headers) {
        List<String> list = CollectionsHelper.stringArrayToList(headers);
        return appendLeftAlignedHeaderRow(list);
    }

    /**
     * Fügt der Tabelle eine Zeile mit den übergebenen Datenfeldern hinzu. Die Tabellenzeile (tr)
     * wird mit angelegt.
     */
    public HtmlBuilder appendDataRow(List<String> headers) {
        appendOpeningTr();
        for (String header : headers) {
            appendTd(header);
        }
        appendClosingTr();
        return this;
    }

    /**
     * Fügt der Tabelle eine Zeile mit den übergebenen Datenfeldern hinzu. Die Tabellenzeile (tr)
     * wird mit angelegt.
     */
    public HtmlBuilder appendDataRow(String... headers) {
        List<String> list = CollectionsHelper.stringArrayToList(headers);
        return appendDataRow(list);
    }

    /** Fügt einen öffnenden blockquote-Tag hinzu. */
    public HtmlBuilder appendOpeningQuote() {
        appendOpeningTag("blockquote");
        return this;
    }

    /** Fügt einen schließenden blockquote-Tag hinzu. */
    public HtmlBuilder appendClosingQuote() {
        appendClosingTag("blockquote");
        return this;
    }

    /** Fügt einen öffnenden blockquote-Tag hinzu. */
    public HtmlBuilder appendOpeningBlockquote() {
        return appendOpeningQuote();
    }

    /** Fügt einen schließenden blockquote-Tag hinzu. */
    public HtmlBuilder appendClosingBlockquote() {
        return appendClosingQuote();
    }

    /**
     * Fügt eine Überschrift erster Ordnung hinzu.
     *
     * @param text
     *            Überschrift.
     */
    public HtmlBuilder appendH1(String text) {
        ++numberOfH1;
        return appendHeader(H1, text);
    }

    /**
     * Fügt eine Überschrift zweiter Ordnung hinzu.
     *
     * @param text
     *            Überschrift.
     */
    public HtmlBuilder appendH2(String text) {
        ++numberOfH2;
        return appendHeader(H2, text);
    }

    /**
     * Fügt eine Überschrift dritter Ordnung hinzu.
     *
     * @param text
     *            Überschrift.
     */
    public HtmlBuilder appendH3(String text) {
        ++numberOfH3;
        return appendHeader(H3, text);
    }

    /**
     * Fügt eine Überschrift vierter Ordnung hinzu.
     *
     * @param text
     *            Überschrift.
     */
    public HtmlBuilder appendH4(String text) {
        ++numberOfH4;
        return appendHeader(H4, text);
    }

    /**
     * Fügt eine Überschrift fünfter Ordnung hinzu.
     *
     * @param text
     *            Überschrift.
     */
    public HtmlBuilder appendH5(String text) {
        ++numberOfH5;
        return appendHeader(H5, text);
    }

    /**
     * Fügt eine Überschrift sechster Ordnung hinzu.
     *
     * @param text
     *            Überschrift.
     */
    public HtmlBuilder appendH6(String text) {
        ++numberOfH6;
        return appendHeader(H6, text);
    }

    private HtmlBuilder appendHeader(HtmlHeaderType headerType, String text) {
        appendTopLinkIfNeccessary(headerType);
        String id = generateHeaderId(headerType);
        appendHeaderAndInsertLineBreakIfNotFirstContent(headerType, text, id);

        String tag = headerType.getHtmlTagName();
        if (createContentWithNames) {
            String anker = "<a name=\"" + id + "\"/>";
            String textWithAnker = anker + text;
            appendInTag(tag, textWithAnker);
            // <h2><a name="intro"/>Entstehungsgeschichte</h2>
            // <h2><a name="id1_1"/>Alle Vokabeln</h2>
        }
        else {
            appendInTagWithId(tag, text, id);
        }

        return this;
    }

    protected final void appendHeaderAndInsertLineBreakIfNotFirstContent(HtmlHeaderType headerType,
            String text, String id) {
        headers.add(new HtmlHeader(headerType, text, id));

        if (length() != getLengthAtLastIncrease()) {
            appendLineBreak();
        }
    }

    protected final void appendTopLinkIfNeccessary(HtmlHeaderType headerType) {
        if (headerType == H1 && numberOfH1 > 1 && showTopLink) {
            appendToTopLink();
        }
        else if (headerType == H2 && numberOfH2 > 1 && showTopLink && appendTopLinksToH2) {
            appendToTopLink();
        }
    }

    /** Fügt einen Link zum Seitenanfang hinzu. */
    public HtmlBuilder appendToTopLink() {
        appendLink("#" + TOP_LINK_NAME, "Zum Seitenanfang");
        return this;
    }

    protected final String generateHeaderId(HtmlHeaderType headerType) {
        String id;

        if (headerType == H1) {
            id = "id" + numberOfH1;
        }
        else if (headerType == H2) {
            id = "id" + numberOfH1 + "_" + numberOfH2;
        }
        else if (headerType == H3) {
            id = "id" + numberOfH1 + "_" + numberOfH2 + "_" + numberOfH3;
        }
        else if (headerType == H4) {
            id = "id" + numberOfH1 + "_" + numberOfH2 + "_" + numberOfH3 + "_" + numberOfH4;
        }
        else if (headerType == H5) {
            id = "id" + numberOfH1 + "_" + numberOfH2 + "_" + numberOfH3 + "_" + numberOfH4 + "_"
                    + numberOfH5;
        }
        else if (headerType == H6) {
            id = "id" + numberOfH1 + "_" + numberOfH2 + "_" + numberOfH3 + "_" + numberOfH4 + "_"
                    + numberOfH5 + "_" + numberOfH6;
        }
        else {
            throw new RuntimeException("Unknown header type '" + headerType + "'!");
        }

        return id;
    }

    /**
     * Fügt einen Link hinzu.
     *
     * @param url
     *            URL des Links.
     * @param description
     *            Beschreibung des Links.
     */
    public HtmlBuilder appendLink(String url, String description) {
        appendInTagWithParameters("a", description, "href=\"" + url + "\"");
        return this;
    }

    /**
     * Fügt einen Link hinzu.
     *
     * @param url
     *            URL des Links.
     * @param description
     *            Beschreibung des Links.
     * @param cssClass
     *            CSS-Klasse des Links.
     */
    public HtmlBuilder appendLinkWithClass(String url, String description, String cssClass) {
        appendInTagWithParameters("a", description,
                "class=\"" + cssClass + "\"",
                "href=\"" + url + "\"");
        return this;
    }

    /**
     * Fügt einen Link hinzu.
     *
     * @param url
     *            URL des Links.
     * @param description
     *            Beschreibung des Links.
     * @param cssClass
     *            CSS-Klasse des Links.
     * @param onClick
     *            Befehl der beim Klick ausgeführt wird.
     */
    public HtmlBuilder appendLinkWithClassAndScript(String url, String description, String cssClass,
            String onClick) {
        appendInTagWithParameters("a", description,
                "class=\"" + cssClass + "\"",
                "href=\"" + url + "\"",
                "onclick=\"" + onClick + "\"");
        return this;
    }

    /**
     * Fügt einen Link ohne URL hinzu.
     *
     * @param description
     *            Beschreibung des Links.
     * @param cssClass
     *            CSS-Klasse des Links.
     * @param onClick
     *            Befehl der beim Klick ausgeführt wird.
     */
    public HtmlBuilder appendLinkWithoutUrlWithClassAndScript(String description, String cssClass,
            String onClick) {
        appendInTagWithParameters("a", description,
                "class=\"" + cssClass + "\"",
                "onclick=\"" + onClick + "\"");
        return this;
    }

    /**
     * Fügt einen Link ohne passende Einrückung hinzu.
     *
     * @param url
     *            URL des Links.
     * @param description
     *            Beschreibung des Links.
     */
    private HtmlBuilder appendLinkWithoutIndentation(String url, String description) {
        appendInTagWithParametersWithoutIndentation("a", description, "href=\"" + url + "\"");
        appendSpaces();
        appendHtmlLineBreak();
        return this;
    }

    /**
     * Schreibt einen Paragraphen mit dem übergebenen Text in die Ausgabe.
     *
     * @param text
     *            Text innerhalb des p-Tags.
     */
    public HtmlBuilder appendP(String text) {
        appendInTag("p", text);
        return this;
    }

    /**
     * Schreibt einen Paragraphen mit dem übergebenen Text und der übergebenen CSS-Klasse in die
     * Ausgabe.
     *
     * @param text
     *            Text innerhalb des p-Tags.
     * @param cssClass
     *            CSS-Klasse des p-Tags.
     */
    public HtmlBuilder appendPWithClass(String text, String cssClass) {
        appendInTagWithClass("p", text, cssClass);
        return this;
    }

    /** Schreibt einen öffnenden p-Tag in die Ausgabe. */
    public HtmlBuilder appendOpeningP() {
        appendOpeningTag("p");
        return this;
    }

    /**
     * Schreibt einen öffnenden p-Tag mit CSS-Klasse in die Ausgabe.
     *
     * @param cssClass
     *            CSS-Klasse.
     */
    public HtmlBuilder appendOpeningP(String cssClass) {
        appendOpeningTagWithClass("p", cssClass);
        return this;
    }

    /** Schreibt einen schließenden p-Tag in die Ausgabe. */
    public HtmlBuilder appendClosingP() {
        appendClosingTag("p");
        return this;
    }

    /**
     * Schreibt einen Paragraphen mit dem übergebenen Text in einem Monospace-Font in die Ausgabe.
     * Dafür muss es im CSS eine Klasse "monospace" geben.
     *
     * @param text
     *            Text innerhalb des p-Tags.
     */
    public HtmlBuilder appendPTT(String text) {
        appendInTagWithParameters("p", text, "class=\"monospace\"");
        return this;
    }

    /**
     * Fügt einen Text umgeben von einem Tag (öffnender Tag vorn, schließender Tag hinten) ein.
     *
     * @param tag
     *            Name des Tags.
     * @param text
     *            Text innerhalb des Tags.
     */
    @Override
    public HtmlBuilder appendInTag(String tag, String text) {
        super.appendInTag(tag, text);
        return this;
    }

    /**
     * Fügt einen Text umgeben von einem Tag (öffnender Tag vorn, schließender Tag hinten) ein.
     *
     * @param tag
     *            Name des Tags.
     * @param text
     *            Text innerhalb des Tags.
     * @param cssClass
     *            CSS-Klasse.
     */
    protected HtmlBuilder appendInTagWithClass(String tag, String text, String cssClass) {
        appendInTagWithParameters(tag, text, "class=\"" + cssClass + "\"");
        return this;
    }

    /**
     * Fügt einen Text umgeben von einem Tag (öffnender Tag vorn, schließender Tag hinten) ein.
     *
     * @param tag
     *            Name des Tags.
     * @param text
     *            Text innerhalb des Tags.
     * @param cssId
     *            CSS-ID.
     */
    protected HtmlBuilder appendInTagWithId(String tag, String text, String cssId) {
        appendInTagWithParameters(tag, text, "id=\"" + cssId + "\"");
        return this;
    }

    /**
     * Fügt einen Text umgeben von einem Tag (öffnender Tag vorn, schließender Tag hinten) ein.
     *
     * @param tag
     *            Name des Tags.
     * @param text
     *            Text innerhalb des Tags.
     * @param cssId
     *            CSS-ID.
     */
    protected HtmlBuilder appendInTagWithName(String tag, String text, String name) {
        appendInTagWithParameters(tag, text, "name=\"" + name + "\"");
        return this;
    }

    /**
     * Fügt einen Text umgeben von einem Tag (öffnender Tag vorn, schließender Tag hinten) ein,
     * samt Parameter für den Tag.
     *
     * @param tag
     *            Name des Tags.
     * @param text
     *            Text innerhalb des Tags.
     * @param params
     *            Parameter die angefügt werden.
     */
    protected HtmlBuilder appendInTagWithParameters(String tag, String text, String ... params) {
        appendSpaces();
        appendInTagWithParametersWithoutIndentation(tag, text, params);
        return this;
    }

    /**
     * Fügt einen Text umgeben von einem Tag (öffnender Tag vorn, schließender Tag hinten) ein,
     * samt Parameter für den Tag, ohne passende Einrückung.
     *
     * @param tag
     *            Name des Tags.
     * @param text
     *            Text innerhalb des Tags.
     * @param params
     *            Parameter die angefügt werden.
     */
    private HtmlBuilder appendInTagWithParametersWithoutIndentation(String tag, String text,
            String ... params) {
        append("<");
        append(tag);
        for (String param : params) {
            append(" ").append(param);
        }
        append(">");

        append(text);

        append("</");
        append(tag);
        append(">");

        appendLineBreak();
        return this;
    }

    /**
     * Fügt einen Tag ohne Inhalt (öffnender Tag vorn, am Ende ein />) ein, samt Parameter für den
     * Tag.
     *
     * @param tag
     *            Name des Tags.
     * @param params
     *            Parameter die angefügt werden.
     */
    public HtmlBuilder appendInTagWithParametersWithoutText(String tag, String ... params) {
        appendSpaces();
        append("<");
        append(tag);
        for (String param : params) {
            append(" ").append(param);
        }
        append("/>");

        appendLineBreak();
        return this;
    }

    /**
     * Fügt einen Tag ohne Inhalt (öffnender Tag vorn, am Ende ein />) ein.
     *
     * @param tag
     *            Name des Tags.
     */
    public HtmlBuilder appendInTagWithoutText(String tag) {
        appendSpaces();
        append("<");
        append(tag);
        append(" />");

        appendLineBreak();
        return this;
    }

    /** Erzeugt den Abschluss des HTML-Dokuments mit schließendem body- und html-Tag. */
    public HtmlBuilder appendFoot() {
        appendLineBreak();

        if (hasContents) {
            appendToTopLink();
        }

        appendClosingTag("body");
        appendClosingTag("html");

        return this;
    }

    /** Fügt eine Trennlinie mit etwas Luft nach oben und unten in den Report ein. */
    public HtmlBuilder appendSeparatoringLine() {
        appendP("");
        appendP("");
        appendLine();
        appendP("");
        return this;
    }

    /** Fügt eine Trennlinie hinzu. */
    public HtmlBuilder appendLine() {
        appendInTagWithoutText("hr");
        return this;
    }

    /**
     * Fügt den Inhalt eines StringBuilders hinzu. Damit richtig eingerückt wird sollte der Text
     * die kleinste Einrücktiefe 0 aufweisen.
     */
    public HtmlBuilder append(StringBuilder otherBuilder) {
        appendMultipleLines(otherBuilder.toString());
        return this;
    }

    /**
     * Fügt den Inhalt eines anderen HtmlBuilders hinzu. Damit richtig eingerückt wird sollte der
     * Text die kleinste Einrücktiefe 0 aufweisen.
     */
    public HtmlBuilder append(HtmlBuilder otherBuilder) {
        appendMultipleLines(otherBuilder.toString());
        return this;
    }

    /** Fügt einen nicht als HTML interpretieren Abschnitt hinzu. */
    public HtmlBuilder appendPre(String text) {
        appendInTag("pre", text);
        return this;
    }

    /** Fügt einen öffnenden pre-Tag hinzu. */
    public HtmlBuilder appendOpeningPre() {
        appendOpeningTag("pre");
        return this;
    }

    /** Fügt einen schließenden pre-Tag hinzu. */
    public HtmlBuilder appendClosingPre() {
        appendClosingTag("pre");
        return this;
    }

    /** Fügt ein Inhaltsverzeichnis oben in das HTML-Dokument ein. */
    public HtmlBuilder insertContent() {
        boolean withTopLinkAnker = false;
        return insertContent(withTopLinkAnker);
    }

    /** Fügt ein Inhaltsverzeichnis oben in das HTML-Dokument ein. */
    public HtmlBuilder insertContentWithTopLinkAnker() {
        boolean withTopLinkAnker = true;
        return insertContent(withTopLinkAnker);
    }

    /** Fügt ein Inhaltsverzeichnis oben in das HTML-Dokument ein. */
    private HtmlBuilder insertContent(boolean withTopLinkAnker) {
        String body = "<body>";
        int bodyStartIndex = indexOf(body);
        if (bodyStartIndex == -1) {
            throw new RuntimeException("Kein Body-Tag im Html gefunden!");
        }

        int bodyEndIndex = bodyStartIndex + body.length();

        String contents;
        if (withTopLinkAnker) {
            contents = buildContents(withTopLinkAnker);
        }
        else {
            contents = buildContents();
        }
        insert(bodyEndIndex, contents);
        hasContents = true;
        return this;
    }

    /** Fügt ein Inhaltsverzeichnis unten in das HTML-Dokument ein. */
    public HtmlBuilder insertContentAtEnd() {
        String body = "</body>";
        int bodyStartIndex = indexOf(body);
        if (bodyStartIndex == -1) {
            throw new RuntimeException("Kein schließendes Body-Tag im Html gefunden!");
        }

        insert(bodyStartIndex, buildContents() + "    ");
        hasContents = true;
        return this;
    }

    private String buildContents() {
        boolean withTopLinkAnker = false;
        return buildContents(withTopLinkAnker);
    }

    private String buildContents(boolean withTopLinkAnker) {
        HtmlBuilder contentBuilder = new HtmlBuilder();

        String title = "Inhaltsverzeichnis";
        if (withTopLinkAnker) {
            title = createTopLinkAnker() + title;
        }

        contentBuilder
                .createHtmlPartInBody()
                .appendLineBreak()
                .appendInTag("h1", title); // bewusst nicht appendH1(title)  !

        for (HtmlHeader header : headers) {
            contentBuilder
                    .appendIndented(header.getContentIndentation())
                    .appendLinkWithoutIndentation("#" + header.getId(), header.getText());
        }

        return contentBuilder.toString();
    }

    /** Fügt einen vertikalen Abstand hinzu. */
    public HtmlBuilder appendVerticalSpace() {
        appendInTag("p", "&nbsp;");
        return this;
    }

    /**
     * Fügt eine öffnende Form mit dem angegebenen Namen und der Url hinzu, welche die Ergebnisse
     * im gleichen Tab anzeigt.
     */
    public HtmlBuilder appendOpeningForm(String nameOfTheForm, String url) {
        boolean openSeachesInNewTab = false;
        return appendOpeningFormInternal(nameOfTheForm, url, openSeachesInNewTab);
    }

    /**
     * Fügt eine öffnende Form mit dem angegebenen Namen und der Url hinzu, welche die Ergebnisse
     * in einem neuen Tab anzeigt.
     *
     * @param nameOfTheForm
     *            Name des Formulars.
     * @param url
     *            URL an die die Daten der Form geschickt werden.
     */
    public HtmlBuilder appendOpeningFormOpeningInNewTab(String nameOfTheForm, String url) {
        boolean openSeachesInNewTab = true;
        return appendOpeningFormInternal(nameOfTheForm, url, openSeachesInNewTab);
    }

    private HtmlBuilder appendOpeningFormInternal(String nameOfTheForm, String url,
            boolean openSeachesInNewTab) {
        List<String> parameters = CollectionsHelper.buildListFrom(
                "name=\"" + nameOfTheForm + "\"",
                "action=\"" + url + "\"",
                "accept-charset=\"UTF-8\"",
                "method=\"post\"");
        if (openSeachesInNewTab) {
            parameters.add("target=\"_blank\"");
        }
        appendOpeningTagWithParameters("form", parameters);
        return this;
    }

    /**
     * Fügt ein namenloses öffnendes Formular mit CSS-Klasse hinzu.
     *
     * @param url
     *            URL an die die Daten des Formulars geschickt werden.
     * @param cssClass
     *            Die CSS-Klasse des Formulars.
     */
    public HtmlBuilder appendOpenNamelessForm(String url, String cssClass) {
        appendOpeningTagWithParameters("form",
                "class=\"" + cssClass + "\"",
                "action=\"" + url + "\"",
                "accept-charset=\"UTF-8\"",
                "method=\"post\"");
        return this;
    }

    /**
     * Fügt ein namenloses öffnendes Formular mit CSS-Klasse und mit enctype="multipart/form-data"
     * für den Upload von Dateien hinzu.
     *
     * @param url
     *            URL an die die Daten des Formulars geschickt werden.
     * @param cssClass
     *            Die CSS-Klasse des Formulars.
     */
    public HtmlBuilder appendOpenUpdateForm(String url, String cssClass) {
        appendOpeningTagWithParameters("form",
                "class=\"" + cssClass + "\"",
                "action=\"" + url + "\"",
                "accept-charset=\"UTF-8\"",
                "method=\"post\"",
                "enctype=\"multipart/form-data\"");
        return this;
    }

    /** Fügt eine schließende Form hinzu. */
    public HtmlBuilder appendClosingForm() {
        appendClosingTag("form");
        return this;
    }

    /**
     * Fügt den Beginn eines fieldsets in einer Form hinzu, dazu wird eine Legende mit dem
     * übergebenen Titel erzeugt.
     */
    public HtmlBuilder appendOpeningFieldset(String title) {
        appendOpeningTag("fieldset");
        appendInTag("legend", title);
        return this;
    }

    /** Fügt das Ende eines fieldsets in einer Form hinzu. */
    public HtmlBuilder appendClosingFieldset() {
        appendClosingTag("fieldset");
        return this;
    }

    /**
     * Fügt einen Button mit einem Typ und einem Text zu einer Form hinzu.
     *
     * @param type
     *            Art des Buttons.
     * @param text
     *            Beschriftung des Buttons.
     */
    public HtmlBuilder appendButtonWithType(String type, String text) {
        appendInTagWithParameters("button", text,
                "type=\"" + type + "\"");
        return this;
    }

    /**
     * Fügt einen Button mit einem Typ, weiteren Parametern und einem Text zu einer Form hinzu.
     *
     * @param type
     *            Art des Buttons.
     * @param text
     *            Beschriftung des Buttons.
     * @param additionalParameters
     *            Weitere Parameter
     */
    public HtmlBuilder appendButtonWithTypeAndAdditionalParams(String type, String text,
            String ... additionalParameters) {
        String[] params = CollectionsHelper.appendInfrontOfArray("type=\"" + type + "\"",
                additionalParameters);
        appendInTagWithParameters("button", text, params);
        return this;
    }

    /**
     * Fügt einen Label zu einem Formular hinzu.
     *
     * @param field
     *            Das Feld zu dem der Label erzeugt wird.
     * @param fieldTitle
     *            Die Beschriftung des Labels.
     */
    public HtmlBuilder appendLabel(String field, String fieldTitle) {
        appendInTagWithParameters("label", fieldTitle,
                "for=\"" + field + "\"");
        return this;
    }

    /**
     * Fügt einen Label zu einem Formular hinzu.
     *
     * @param field
     *            Das Feld zu dem der Label erzeugt wird.
     * @param fieldTitle
     *            Die Beschriftung des Labels.
     * @param cssClass
     *            Die CSS-Klasse des Labels.
     */
    public HtmlBuilder appendLabelWithClass(String field, String fieldTitle, String cssClass) {
        appendInTagWithParameters("label", fieldTitle,
                "class=\"" + cssClass + "\"",
                "for=\"" + field + "\"");
        return this;
    }

    /**
     * Fügt ein Inputfeld zu einem Formular hinzu.
     *
     * @param field
     *            Das betreffende Feld.
     */
    public HtmlBuilder appendInput(String field) {
        appendInTagWithParametersWithoutText("input",
                "id=\"" + field + "\"",
                "name=\"" + field + "\"");
        return this;
    }

    /**
     * Fügt ein Inputfeld zu einem Formular hinzu.
     *
     * @param field
     *            Das betreffende Feld.
     * @param cssClass
     *            Die CSS-Klasse des Input-Feldes.
     */
    public HtmlBuilder appendInputWithClass(String field, String cssClass) {
        appendInTagWithParametersWithoutText("input",
                "class=\"" + cssClass + "\"",
                "id=\"" + field + "\"",
                "name=\"" + field + "\"");
        return this;
    }

    /**
     * Fügt ein Inputfeld zu einem Formular hinzu.
     *
     * @param field
     *            Das betreffende Feld.
     * @param cssClass
     *            Die CSS-Klasse des Input-Feldes.
     */
    public HtmlBuilder appendInputWithValue(String field, String value) {
        appendInTagWithParametersWithoutText("input",
                "id=\"" + field + "\"",
                "name=\"" + field + "\"",
                "value=\"" + value + "\"");
        return this;
    }

    /**
     * Fügt ein Inputfeld mit vorbelegtem Wert zu einem Formular hinzu.
     *
     * @param field
     *            Das betreffende Feld.
     * @param cssClass
     *            Die CSS-Klasse des Input-Feldes.
     * @param value
     *            Vorbelegter Wert.
     */
    public HtmlBuilder appendInputWithClassAndValue(String field, String cssClass, String value) {
        appendInTagWithParametersWithoutText("input",
                "class=\"" + cssClass + "\"",
                "id=\"" + field + "\"",
                "name=\"" + field + "\"",
                "value=\"" + value + "\"");
        return this;
    }

    /**
     * Fügt ein Pflicht-Inputfeld mit vorbelegtem Wert zu einem Formular hinzu.
     *
     * @param field
     *            Das betreffende Feld.
     * @param cssClass
     *            Die CSS-Klasse des Input-Feldes.
     * @param value
     *            Vorbelegter Wert.
     */
    public HtmlBuilder appendRequiredInputWithClassAndValue(String field, String cssClass,
            String value) {
        appendInTagWithParametersWithoutText("input",
                "class=\"" + cssClass + "\"",
                "id=\"" + field + "\"",
                "name=\"" + field + "\"",
                "value=\"" + value + "\"",
                "required");
        return this;
    }

    /**
     * Fügt ein Inputfeld mit angezeigtem Platzhalter, solange der Benutzer keinen Wert eingibt, zu
     * einem Formular hinzu.
     *
     * @param field
     *            Das betreffende Feld.
     * @param cssClass
     *            Die CSS-Klasse des Input-Feldes.
     * @param placeholder
     *            Platzhalter der hellgrau angezeigt wird, solange der Benutzer nichts eingibt.
     */
    public HtmlBuilder appendInputWithClassAndPlaceholder(String field, String cssClass,
            String placeholder) {
        appendInTagWithParametersWithoutText("input",
                "class=\"" + cssClass + "\"",
                "id=\"" + field + "\"",
                "name=\"" + field + "\"",
                "placeholder=\"" + placeholder + "\"");
        return this;
    }

    /**
     * Fügt ein Pflicht-Inputfeld mit angezeigtem Platzhalter, solange der Benutzer keinen Wert
     * eingibt, zu einem Formular hinzu.
     *
     * @param field
     *            Das betreffende Feld.
     * @param cssClass
     *            Die CSS-Klasse des Input-Feldes.
     * @param placeholder
     *            Platzhalter der hellgrau angezeigt wird, solange der Benutzer nichts eingibt.
     */
    public HtmlBuilder appendRequiredInputWithClassAndPlaceholder(String field, String cssClass,
            String placeholder) {
        appendInTagWithParametersWithoutText("input",
                "class=\"" + cssClass + "\"",
                "id=\"" + field + "\"",
                "name=\"" + field + "\"",
                "placeholder=\"" + placeholder + "\"",
                "required");
        return this;
    }

    /**
     * Fügt ein Pflicht-Inputfeld mit angezeigtem Platzhalter, solange der Benutzer keinen Wert
     * eingibt, und einem Pattern, dem der eingegebene Text entsprechen muss, zu einem Formular
     * hinzu.
     *
     * Achtung, auf das Pattern darf man sich nicht zu sehr verlassen, es gibt immer Mittel und
     * Wege dieses zu umgehen, man sollte die Erfüllung auf jeden Fall serverseitig überprüfen!
     *
     * @param field
     *            Das betreffende Feld.
     * @param cssClass
     *            Die CSS-Klasse des Input-Feldes.
     * @param placeholder
     *            Platzhalter der hellgrau angezeigt wird, solange der Benutzer nichts eingibt.
     * @param pattern
     *            Regulärer Ausdruck der festlegt, wie der eingegebene Wert beschaffen sein darf.
     */
    public HtmlBuilder appendRequiredInputWithClassPlaceholderAndPattern(String field,
            String cssClass, String placeholder, String pattern) {
        appendInTagWithParametersWithoutText("input",
                "class=\"" + cssClass + "\"",
                "id=\"" + field + "\"",
                "name=\"" + field + "\"",
                "placeholder=\"" + placeholder + "\"",
                "pattern=\"" + pattern + "\"",
                "required");
        return this;
    }

    /**
     * Fügt ein Pflicht-Passwort-Inputfeld, das mindestens die angegebene Anzahl an Zeichen
     * enthalten muss, mit angezeigtem Platzhalter, solange der Benutzer keinen Wert eingibt, zu
     * einem Formular hinzu.
     *
     * Achtung, auf die Minimallänge darf man sich nicht zu sehr verlassen, es gibt immer Mittel
     * und Wege dieses zu umgehen, man sollte die Erfüllung auf jeden Fall serverseitig überprüfen!
     *
     * @param field
     *            Das betreffende Feld.
     * @param cssClass
     *            Die CSS-Klasse des Input-Feldes.
     * @param placeholder
     *            Platzhalter der hellgrau angezeigt wird, solange der Benutzer nichts eingibt.
     * @param minLength
     *            Die Anzahl Zeichen, die das Passwort mindestens lang sein muss.
     */
    public HtmlBuilder appendPasswordInputWithClassPlaceholderAndMinLength(String field,
            String cssClass, String placeholder, int minLength) {
        appendInTagWithParametersWithoutText("input",
                "class=\"" + cssClass + "\"",
                "id=\"" + field + "\"",
                "name=\"" + field + "\"",
                "type=\"password\"",
                "placeholder=\"" + placeholder + "\"",
                "minlength=\"" + minLength + "\"");
        return this;
    }

    /**
     * Fügt ein Pflicht-Passwort-Inputfeld, das mindestens die angegebene Anzahl an Zeichen
     * enthalten muss, mit angezeigtem Platzhalter, solange der Benutzer keinen Wert eingibt, zu
     * einem Formular hinzu.
     *
     * Achtung, auf die Minimallänge darf man sich nicht zu sehr verlassen, es gibt immer Mittel
     * und Wege dieses zu umgehen, man sollte die Erfüllung auf jeden Fall serverseitig überprüfen!
     *
     * @param field
     *            Das betreffende Feld.
     * @param cssClass
     *            Die CSS-Klasse des Input-Feldes.
     * @param placeholder
     *            Platzhalter der hellgrau angezeigt wird, solange der Benutzer nichts eingibt.
     * @param minLength
     *            Die Anzahl Zeichen, die das Passwort mindestens lang sein muss.
     */
    public HtmlBuilder appendRequiredPasswordInputWithClassPlaceholderAndMinLength(String field,
            String cssClass, String placeholder, int minLength) {
        appendInTagWithParametersWithoutText("input",
                "class=\"" + cssClass + "\"",
                "id=\"" + field + "\"",
                "name=\"" + field + "\"",
                "type=\"password\"",
                "placeholder=\"" + placeholder + "\"",
                "minlength=\"" + minLength + "\"",
                "required");
        return this;
    }

    /**
     * Fügt ein E-Mail-Inputfeld mit angezeigtem Platzhalter, solange der Benutzer keinen Wert
     * eingibt, zu einem Formular hinzu.
     *
     * @param field
     *            Das betreffende Feld.
     * @param cssClass
     *            Die CSS-Klasse des Input-Feldes.
     * @param placeholder
     *            Platzhalter der hellgrau angezeigt wird, solange der Benutzer nichts eingibt.
     */
    public HtmlBuilder appendEmailInputWithClassPlaceholder(String field,
            String cssClass, String placeholder) {
        appendInTagWithParametersWithoutText("input",
                "class=\"" + cssClass + "\"",
                "id=\"" + field + "\"",
                "name=\"" + field + "\"",
                "type=\"email\"",
                "placeholder=\"" + placeholder + "\"");
        return this;
    }

    /**
     * Fügt ein Pflicht-E-Mail-Inputfeld mit angezeigtem Platzhalter, solange der Benutzer keinen
     * Wert eingibt, zu einem Formular hinzu.
     *
     * @param field
     *            Das betreffende Feld.
     * @param cssClass
     *            Die CSS-Klasse des Input-Feldes.
     * @param placeholder
     *            Platzhalter der hellgrau angezeigt wird, solange der Benutzer nichts eingibt.
     */
    public HtmlBuilder appendRequiredEmailInputWithClassAndPlaceholder(String field,
            String cssClass, String placeholder) {
        appendInTagWithParametersWithoutText("input",
                "class=\"" + cssClass + "\"",
                "id=\"" + field + "\"",
                "name=\"" + field + "\"",
                "type=\"email\"",
                "placeholder=\"" + placeholder + "\"",
                "required");
        return this;
    }

    /**
     * Fügt ein Pflicht-E-Mail-Inputfeld mit einem vorbelegten Wert zu einem Formular hinzu.
     *
     * @param field
     *            Das betreffende Feld.
     * @param cssClass
     *            Die CSS-Klasse des Input-Feldes.
     * @param value
     *            Vorbelegter Wert des Feldes.
     */
    public HtmlBuilder appendRequiredEmailInputWithClassAndValue(String field, String cssClass,
            String value) {
        appendInTagWithParametersWithoutText("input",
                "class=\"" + cssClass + "\"",
                "id=\"" + field + "\"",
                "name=\"" + field + "\"",
                "type=\"email\"",
                "value=\"" + value + "\"",
                "required");
        return this;
    }

    /**
     * Fügt ein Input-Feld in Form einer Checkbox hinzu.
     *
     * @param field
     *            Das Feld zu dem der Label erzeugt wird.
     * @param selected
     *            Gibt an ob die CheckBox ausgewählt sein soll.
     */
    public HtmlBuilder appendInputCheckbox(String field, boolean selected) {
        String[] params = {
                "id=\"" + field + "\"",
                "type=\"checkbox\"",
                "name=\"" + field + "\""
                };
        if (selected) {
            params = CollectionsHelper.appendAtEndOfArray(params, "checked");
        }
        appendInTagWithParametersWithoutText("input", params);
        return this;
    }

    /**
     * Fügt eine CheckBox in einem zweispaltigen Layout hinzu.
     *
     * @param field
     *            Das betreffende Feld.
     * @param selected
     *            Gibt an, ob die Checkbox ausgewählt ist.
     * @param jsCall
     *            JavaScript, das bei onclick ausgeführt werden soll.
     */
    public HtmlBuilder appendInputCheckboxWithOnClickScript(String field, boolean selected,
            String jsCall) {
        String[] params = {
                "id=\"" + field + "\"",
                "type=\"checkbox\"",
                "name=\"" + field + "\"",
                "onclick=\"javascript: " + jsCall + "\""
                };
        if (selected) {
            params = CollectionsHelper.appendAtEndOfArray(params, "checked");
        }
        appendInTagWithParametersWithoutText("input", params);
        return this;
    }

    /**
     * Fügt ein Input-Feld in Form eines RadioButtons hinzu.
     *
     * @param field
     *            Das Feld zu dem der Label erzeugt wird.
     * @param buttonGroupName
     *            Der Name der Gruppe, zu der dieser RadioButton gehört.
     * @param selected
     *            Gibt an ob die CheckBox ausgewählt sein soll.
     */
    public HtmlBuilder appendInputRadioButton(String field, String buttonGroupName,
            boolean selected) {
        String[] params = {
                "id=\"" + field + "\"",
                "type=\"radio\"",
                "name=\"" + buttonGroupName + "\"",
                "value=\"" + field + "\""
                };
        if (selected) {
            params = CollectionsHelper.appendAtEndOfArray(params, "checked");
        }
        appendInTagWithParametersWithoutText("input", params);
        return this;
    }

    /**
     * Fügt ein Label und einen Spinner hinzu.
     *
     * @param field
     *            Das betreffende Feld.
     * @param fieldTitle
     *            Beschriftung des Labels.
     * @param min
     *            Minimaler erlaubter Wert.
     * @param max
     *            Maximaler erlaubter Wert.
     * @param step
     *            Schrittweite mit den Pfeilen des Spinners.
     * @param value
     *            Vorbelegter Wert.
     * @param cssLabelClass
     *            CSS-Klasse des Labels.
     * @param cssSpinnerClass
     *            CSS-Klasse des Spinners.
     */
    public HtmlBuilder appendLabelAndSpinner(String field, String fieldTitle, int min, int max,
            int step, String value, String cssLabelClass, String cssSpinnerClass) {
        appendOpeningP();
        appendLabelWithClass(field, fieldTitle, cssLabelClass);
        appendSpinner(field, min, max, step, value, cssSpinnerClass);
        appendClosingP();
        return this;
    }

    /**
     * Fügt ein Label und einen Spinner hinzu.
     *
     * @param field
     *            Das betreffende Feld.
     * @param min
     *            Minimaler erlaubter Wert.
     * @param max
     *            Maximaler erlaubter Wert.
     * @param step
     *            Schrittweite mit den Pfeilen des Spinners.
     * @param value
     *            Vorbelegter Wert.
     * @param cssClass
     *            CSS-Klasse des Spinners.
     */
    public HtmlBuilder appendSpinner(String field, int min, int max, int step, String value,
            String cssClass) {
        appendInTagWithParametersWithoutText("input",
                "id=\"" + field + "\"",
                "type=\"number\"",
                "name=\"" + field + "\"",
                "min=\"" + min + "\"",
                "max=\"" + max + "\"",
                "step=\"" + step + "\"",
                "value=\"" + value + "\"",
                "class=\"" + cssClass + "\""
                );
        return this;
    }

    /**
     * Fügt ein verstecktes Feld zu einem Formular hinzu.
     *
     * @param name
     *            Name des versteckten Feldes.
     * @param value
     *            Wert des Feldes.
     */
    public HtmlBuilder appendHiddenField(String name, String value) {
        appendInTagWithParametersWithoutText("input",
                "type=\"hidden\"",
                "name=\"" + name + "\"",
                "value=\"" + value + "\"");
        return this;
    }

    /**
     * Fügt einen Button zum wirklichen Leeren der Eingabefelder zu einem Formular hinzu.
     *
     * @param buttonName
     *            Name des Buttons.
     * @param formName
     *            Name des Formulars.
     */
    public HtmlBuilder appendReallyClearFieldsOfFormButton(String buttonName, String formName) {
        return appendReallyClearFieldsOfFormButton(buttonName, formName, new ArrayList<>());
    }

    /**
     * Fügt einen Button zum wirklichen Leeren der Eingabefelder zu einem Formular hinzu.
     *
     * @param buttonName
     *            Name des Buttons.
     * @param formName
     *            Name des Formulars.
     * @param idsToIgnore
     *            Auflistung von IDs der Felder, welche nicht geleert werden sollen.
     */
    public HtmlBuilder appendReallyClearFieldsOfFormButton(String buttonName, String formName,
            String ... idsToIgnore) {
        return appendReallyClearFieldsOfFormButton(buttonName, formName,
                CollectionsHelper.stringArrayToList(idsToIgnore));
    }

    /**
     * Fügt einen Button zum wirklichen Leeren der Eingabefelder zu einem Formular hinzu.
     *
     * @param buttonName
     *            Name des Buttons.
     * @param formName
     *            Name des Formulars.
     * @param idsToIgnore
     *            Liste mit den IDs der Felder, welche nicht geleert werden sollen.
     */
    public HtmlBuilder appendReallyClearFieldsOfFormButton(String buttonName, String formName,
            List<String> idsToIgnore) {
        List<String> lines = generateReallyClearFieldsScriptStart();
        lines.addAll(generateReallyClearFieldsScriptPart(idsToIgnore));
        lines.addAll(generateReallyClearFieldsScriptEnd());
        appendMultipleLines(lines);

        appendButtonWithTypeAndAdditionalParams("button",
                "Felder leeren",
                "name=\"" + buttonName + "\"",
                "onClick=\"clearAllFieldsInFirstForm(" + formName + ")\""
                );

        return this;
    }

    private List<String> generateReallyClearFieldsScriptStart() {
        return CollectionsHelper.buildListFrom(
                //"<script language=\"javascript\" type=\"text/javascript\">", deprecated in HTML5
                "<script>",
                "    function clearAllFieldsInFirstForm(form) {",
                "        for (i = 0; i < form.elements.length; ++i) {"
                );
    }

    private List<String> generateReallyClearFieldsScriptPart(List<String> idsToIgnore) {
        List<String> lines = CollectionsHelper.buildListFrom(
                "            if (form.elements[i].type == 'text'",
                "                    || form.elements[i].type == 'textarea') {"
                );
        if (idsToIgnore.isEmpty()) {
            lines.add(
                    "                form.elements[i].value = '';"
                    );
        }
        else {
            StringBuilder builder = new StringBuilder();
            builder.append("form.elements[i].id != '" + idsToIgnore.get(0) + "'");
            for (int index = 1; index < idsToIgnore.size(); ++index) {
                builder
                        .append(Text.LINE_BREAK)
                        .append("                                && ")
                        .append("form.elements[i].id != '" + idsToIgnore.get(index) + "'");
            }
            String ifClause = builder.toString();
            lines.add("                if (" + ifClause + ") {");
            lines.add("                    form.elements[i].value = '';");
            lines.add("                }");
        }
        lines.add("            }");
        return lines;
    }

    private List<String> generateReallyClearFieldsScriptEnd() {
        return CollectionsHelper.buildListFrom(
                "        }",
                "    }",
                "</script>"
                );
    }

    /**
     * Fügt einen Button zum wirklichen Leeren der Eingabefelder und bestimmter Checkboxen zu einem
     * Formular hinzu.
     *
     * @param buttonName
     *            Name des Buttons.
     * @param formName
     *            Name des Formulars.
     * @param checkboxIdsToClear
     *            Liste mit den IDs der Checkboxen, welche deselektiert werden sollen.
     * @param idsToIgnore
     *            Liste mit den IDs der Felder, welche nicht geleert werden sollen.
     */
    public HtmlBuilder appendReallyClearFieldsAndCheckbuttonsOfFormButton(String buttonName,
            String formName, List<String> checkboxIdsToClear, List<String> idsToIgnore) {
        List<String> lines = generateReallyClearFieldsScriptStart();
        lines.addAll(generateReallyClearFieldsScriptPart(idsToIgnore));
        lines.addAll(generateReallyClearCheckboxesScriptPart(checkboxIdsToClear));
        lines.addAll(generateReallyClearFieldsScriptEnd());
        appendMultipleLines(lines);

        appendButtonWithTypeAndAdditionalParams("button",
                "Felder leeren",
                "name=\"" + buttonName + "\"",
                "onClick=\"clearAllFieldsInFirstForm(" + formName + ")\""
                );

        return this;
    }

    private List<String> generateReallyClearCheckboxesScriptPart(List<String> checkboxIdsToClear) {
        if (checkboxIdsToClear.isEmpty()) {
            return new ArrayList<>();
        }

        List<String> lines = CollectionsHelper.buildListFrom(
                "            if (form.elements[i].type == 'checkbox') {"
                );

        StringBuilder builder = new StringBuilder();
        builder.append("form.elements[i].id == '" + checkboxIdsToClear.get(0) + "'");
        for (int index = 1; index < checkboxIdsToClear.size(); ++index) {
            builder
                    .append(Text.LINE_BREAK)
                    .append("                                || ")
                    .append("form.elements[i].id == '" + checkboxIdsToClear.get(index) + "'");
        }
        String ifClause = builder.toString();
        lines.add("                if (" + ifClause + ") {");
        lines.add("                    form.elements[i].checked = false;");
        lines.add("                }");

        lines.add("            }");
        return lines;
    }

    /**
     * Fügt ein Eingabefeld mit einem dazugehörigen Label zu einem Formular hinzu.
     *
     * @param field
     *            Das betreffende Feld.
     * @param fieldTitle
     *            Beschriftung des Labels.
     */
    public HtmlBuilder appendLabelAndInput(String field, String fieldTitle) {
        appendOpeningP();
        appendLabel(field, fieldTitle);
        appendInput(field);
        appendClosingP();
        return this;
    }

    /**
     * Fügt ein Eingabefeld mit einem dazugehörigen Label mit einem vorbelegten Wert zu einem
     * Formular hinzu.
     *
     * @param field
     *            Das betreffende Feld.
     * @param fieldTitle
     *            Beschriftung des Labels.
     * @param value
     *            Vorbelegter Wert des Feldes.
     */
    public HtmlBuilder appendLabelAndInputWithValue(String field, String fieldTitle,
            String value) {
        appendOpeningP();
        appendLabel(field, fieldTitle);
        appendInputWithValue(field, value);
        appendClosingP();
        return this;
    }

    /**
     * Fügt ein Label und eine CheckBox in einem zweispaltigen Layout hinzu.
     *
     * @param field
     *            Das betreffende Feld.
     * @param fieldTitle
     *            Beschriftung des Labels.
     * @param selected
     *            Gibt an, ob die Checkbox ausgewählt ist.
     * @param cssLabelClass
     *            CSS-Klasse des Labels.
     */
    public HtmlBuilder appendLabelAndCheckBox(String field, String fieldTitle,
            boolean selected, String cssLabelClass) {
        appendOpeningP();
        appendLabelWithClass(field, fieldTitle, cssLabelClass);
        appendInputCheckbox(field, selected);
        appendClosingP();
        return this;
    }

    /**
     * Fügt ein Label und eine CheckBox in einem zweispaltigen Layout hinzu.
     *
     * @param field
     *            Das betreffende Feld.
     * @param fieldTitle
     *            Beschriftung des Labels.
     * @param selected
     *            Gibt an, ob die Checkbox ausgewählt ist.
     * @param jsCall
     *            JavaScript, das bei onclick ausgeführt werden soll.
     * @param cssLabelClass
     *            CSS-Klasse des Labels.
     */
    public HtmlBuilder appendLabelAndCheckBoxWithOnClickScript(String field, String fieldTitle,
            boolean selected, String jsCall, String cssLabelClass) {
        appendOpeningP();
        appendLabelWithClass(field, fieldTitle, cssLabelClass);
        appendInputCheckboxWithOnClickScript(field, selected, jsCall);
        appendClosingP();
        return this;
    }

    /**
     * Fügt ein Label und einen RadioButton in einem zweispaltigen Layout hinzu.
     *
     * @param field
     *            Das betreffende Feld.
     * @param buttonGroupName
     *            Name der Gruppe zu der dieser RadioButton gehört.
     * @param fieldTitle
     *            Beschriftung des Labels.
     * @param selected
     *            Gibt an, ob der RadioButton ausgewählt ist.
     * @param cssLabelClass
     *            CSS-Klasse des Labels.
     */
    public HtmlBuilder appendLabelAndRadioButton(String field, String buttonGroupName,
            String fieldTitle, boolean selected, String cssLabelClass) {
        appendOpeningP();
        appendLabelWithClass(field, fieldTitle, cssLabelClass);
        appendInputRadioButton(field, buttonGroupName, selected);
        appendClosingP();
        return this;
    }

    /**
     * Fügt Form-Buttons zum Absenden und dem zurücksetzen der Feldinhalte hinzu.
     *
     * @param submitTitle
     *            Beschriftung des Buttons zum Abschicken des Formulars.
     */
    public HtmlBuilder appendFormButtons(String submitTitle) {
        appendOpeningP();
        appendButtonWithType("reset", "Eingaben zurücksetzen");
        appendButtonWithType("submit", submitTitle);
        appendClosingP();
        return this;
    }

    /** Fügt ein JavaScript hinzu, welches den Fokus auf das übergebene Feld setzt. */
    public HtmlBuilder appendFocusSettingJavaScript(String field) {
        appendMultipleLines(
                //"<script language=\"javascript\" type=\"text/javascript\">", deprecated in HTML5
                "<script>",
                "    document.getElementById(\"" + field + "\").focus();",
                "</script>");
        return this;
    }

    /**
     * Erzeugt ein Script um die Passworte in zwei Feldern auf Gleichheit zu überprüfen. Bei nicht
     * übereinstimmenden Passworten wird ein Hinweis angezeigt.
     *
     * @param field1
     *            Erstes Passwort-Feld.
     * @param field2
     *            Zweites Passwort-Feld.
     */
    public HtmlBuilder appendPasswordVerificationJavaScript(String field1, String field2) {
        appendMultipleLines(
                //"<script language=\"javascript\" type=\"text/javascript\">", deprecated in HTML5
                "<script>",
                "    var password1 = document.getElementById('" + field1 + "');",
                "    var password2 = document.getElementById('" + field2 + "');",
                "    var checkPasswordValidity = function () {",
                "        if (password1.value != password2.value) {",
                "            password2.setCustomValidity('Passwörter müssen übereinstimmen!');",
                "        }",
                "        else {",
                "            password2.setCustomValidity('');",
                "        }",
                "    };",
                "    password1.addEventListener('change', checkPasswordValidity);",
                "    password2.addEventListener('change', checkPasswordValidity);",
                "</script>"
                );
        return this;
    }

    /**
     * Fügt ein Script hinzu, mit dem ein Http-Post-Request abgeschickt werden kann.
     *
     * Z.B. nach einem Klick einer Tabellenspalte, siehe appendOnRowClickJavaScript()
     */
    public HtmlBuilder appendSendPostJavaScript() {
        appendMultipleLines(
                //"<script language=\"javascript\" type=\"text/javascript\">", deprecated in HTML5
                "<script>",
                "    function post(path, params, method) {",
                "        method = method || \"POST\"; // Set method to post by default if not "
                 + "specified.",
                "",
                "        var form = document.createElement(\"form\");",
                "        form.setAttribute(\"method\", method);",
                "        form.setAttribute(\"action\", path);",
                "        /*",
                "         * Es geht hier auch",
                "         *     form.setAttribute(\"target\", \"_blank\");",
                "         * um die neue Seite in einem eigenen Tab anzuzeigen.",
                "         */",
                "",
                "        for (var key in params) {",
                "            if (params.hasOwnProperty(key)) {",
                "                var hiddenField = document.createElement(\"input\");",
                "                hiddenField.setAttribute(\"type\", \"hidden\");",
                "                hiddenField.setAttribute(\"name\", key);",
                "                hiddenField.setAttribute(\"value\", params[key]);",
                "",
                "                form.appendChild(hiddenField);",
                "            }",
                "        }",
                "",
                "        document.body.appendChild(form);",
                "        form.submit();",
                "    }",
                "</script>");
        return this;
    }

    /**
     * Fügt ein Script zum Sortieren einer Tabelle hinzu.
     *
     * @param tableId
     *            CSS-Id der Tabelle.
     */
    public HtmlBuilder appendTableSortingJavaScript(String tableId) {
        appendMultipleLines(
                //"<script language=\"javascript\" type=\"text/javascript\">", deprecated in HTML5
                "<script>",
                "    \"use strict\";",
                "    var tableSort = function (tab) {",
                "            var titel = tab.getElementsByTagName(\"thead\")[0]",
                "                    .getElementsByTagName(\"tr\")[0]."
                        +                      "getElementsByTagName(\"th\");",
                "            var tbdy = tab.getElementsByTagName(\"tbody\")[0];",
                "            var tz = tbdy.rows;",
                "            var nzeilen = tz.length;",
                "            if (nzeilen == 0) {",
                "                return;",
                "            }",
                "            var nspalten = tz[0].cells.length;",
                "            var arr = new Array(nzeilen);",
                "            var sortiert = -1;",
                "            var sorttype = new Array(nspalten);",
                "            var sortbuttonStyle = document.createElement('style'); "
                        +                                          "// Stylesheet für Button im TH",
                "            sortbuttonStyle.innerText =",
                "                '.sortbutton { width:100%; height:100%; border: none; "
                        +            "background-color: transparent; font: inherit; "
                        +            "color: inherit; text-align: inherit; padding: 0; "
                        +            "cursor: pointer; } "
                        +        ".sortbutton::-moz-focus-inner { margin: -1px; "
                        +            "border-width: 1px; padding: 0; }';",
                "            document.head.appendChild(sortbuttonStyle);",
                "            var initTableHead = function (sp) { // Kopfzeile vorbereiten",
                "                var b = document.createElement(\"button\");",
                "                b.type = \"button\";",
                "                b.className = \"sortbutton\";",
                "                b.innerHTML = titel[sp].innerHTML;",
                "                b.addEventListener(\"click\", function () {",
                "                    tsort(sp);",
                "                }, false);",
                "                titel[sp].innerHTML = \"\";",
                "                titel[sp].appendChild(b);",
                "            }",
                "            var getData = function (ele, s) {",
                "                    var val = ele.textContent; // Nicht im IE8",
                "                    if (!isNaN(val) && val.search(/[0-9]/) != -1) return val;",
                "                    var n = val.replace(\",\", \".\");",
                "                    if (!isNaN(n) && n.search(/[0-9]/) != -1) return n;",
                "                    sorttype[s] = \"s\"; // String",
                "                    return val;",
                "                } // getData",
                "            var vglFkt_s = function (a, b) {",
                "                    var as = a[sortiert],",
                "                        bs = b[sortiert];",
                "                    if (as > bs) {",
                "                        return 1;",
                "                    }",
                "                    else {",
                "                        return -1;",
                "                    }",
                "                } // vglFkt_s",
                "            var vglFkt_n = function (a, b) {",
                "                    return parseFloat(a[sortiert]) - parseFloat(b[sortiert]);",
                "                } // vglFkt_n",
                "            var tsort = function (sp) {",
                "                    if (sp == sortiert) {",
                "                        // Tabelle ist schon nach dieser Spalte sortiert, also",
                "                        // nur Reihenfolge umdrehen:",
                "                        arr.reverse();",
                "                    }",
                "                    else { // Sortieren",
                "                        sortiert = sp;",
                "                        if (sorttype[sp] == \"n\") {",
                "                            arr.sort(vglFkt_n);",
                "                        }",
                "                        else {",
                "                            arr.sort(vglFkt_s);",
                "                        }",
                "                    }",
                "                    for (var z = 0; z < nzeilen; z++) {",
                "                        tbdy.appendChild(arr[z][nspalten]); "
                        +                              "// Sortierte Daten zurückschreiben",
                "                    }",
                "                } // tsort",
                "            // Kopfzeile vorbereiten",
                "            for (var i = 0; i < titel.length; i++) {",
                "                initTableHead(i);",
                "            }",
                "            // Array mit Info, wie Spalte zu sortieren ist, vorbelegen",
                "            for (var s = 0; s < nspalten; s++) {",
                "                sorttype[s] = \"n\";",
                "            }",
                "            // Tabelleninhalt in ein Array kopieren",
                "            for (var z = 0; z < nzeilen; z++) {",
                "                var zelle = tz[z].getElementsByTagName(\"td\"); // cells;",
                "                arr[z] = new Array(nspalten + 1);",
                "                arr[z][nspalten] = tz[z];",
                "                for (var s = 0; s < nspalten; s++) {",
                "                    var zi = getData(zelle[s], s);",
                "                    arr[z][s] = zi;",
                "                    // zelle[s].innerHTML += \"<br>\"+zi+\"<br>\"+sorttype[s]; "
                        +                     "// zum Debuggen",
                "                }",
                "            }",
                "        } // tableSort",
                "    var initTableSort = function () {",
                "            var sort_Table = document.querySelectorAll(\"table#" + tableId + "\");",
                "            for (var i = 0; i < sort_Table.length; i++) {",
                "                new tableSort(sort_Table[i]);",
                "            }",
                "        } // initTable",
                "    if (window.addEventListener) {",
                "        window.addEventListener(\"DOMContentLoaded\", initTableSort, false); "
                        +              "// nicht im IE8",
                "    }",
                "</script>");
        return this;
    }

    /**
     * Fügt ein Script zum Sortieren einer Tabelle hinzu, das danach die Einfärbung des
     * Hintergrunds der Zeilen der Tabelle passend vornimmt, wenn diese per CSS-Klasse erfolgt.
     *
     * @param tableId
     *            CSS-Id der Tabelle.
     * @param titleForSameRowColor
     *            Der String muss in der Titelzeile der Tabelle als Überschrift der Spalte
     *            eingetragen sein, nach der entschieden wird, ob zwei aufeinander folgende Spalten
     *            die gleiche Hintergrundfarbe bekommen sollen. Zum Beispiel "HUP".
     * @param cssClassForOddRows
     *            Die CSS-Klasse für die ungeraden Zeilen.
     * @param cssClassForEvenRows
     *            Die CSS-Klasse für die geraden Zeilen.
     */
    public HtmlBuilder appendTableSortingJavaScriptWithRowBackgroundColorAdjustment(
            String tableId, String titleForSameRowColor, String cssClassForOddRows,
            String cssClassForEvenRows) {
        appendMultipleLines(
                //"<script language=\"javascript\" type=\"text/javascript\">", deprecated in HTML5
                "<script>",
                "    \"use strict\";",
                "    var tableSort = function (tab) {",
                "            var titel = tab.getElementsByTagName(\"thead\")[0]",
                "                    .getElementsByTagName(\"tr\")[0]."
                        +                      "getElementsByTagName(\"th\");",
                "",
                "            var hupIndex = -1;",
                "            for (var t = 0; t < titel.length; t++) {",
                "                var tinner = titel[t];",
                "                var tinnerX = tinner.innerHTML;",
                "                if (0 == tinnerX.localeCompare(\"" + titleForSameRowColor + "\")) {",
                "                    hupIndex = t;",
                "                }",
                "            }",
                "",
                "            var tbdy = tab.getElementsByTagName(\"tbody\")[0];",
                "            var tz = tbdy.rows;",
                "            var nzeilen = tz.length;",
                "            if (nzeilen == 0) {",
                "                return;",
                "            }",
                "            var nspalten = tz[0].cells.length;",
                "            var arr = new Array(nzeilen);",
                "            var sortiert = -1;",
                "            var sorttype = new Array(nspalten);",
                "            var sortbuttonStyle = document.createElement('style'); "
                        +                                          "// Stylesheet für Button im TH",
                "            sortbuttonStyle.innerText =",
                "                '.sortbutton { width:100%; height:100%; border: none; "
                        +            "background-color: transparent; font: inherit; "
                        +            "color: inherit; text-align: inherit; padding: 0; "
                        +            "cursor: pointer; } "
                        +        ".sortbutton::-moz-focus-inner { margin: -1px; "
                        +            "border-width: 1px; padding: 0; }';",
                "            document.head.appendChild(sortbuttonStyle);",
                "            var initTableHead = function (sp) { // Kopfzeile vorbereiten",
                "                var b = document.createElement(\"button\");",
                "                b.type = \"button\";",
                "                b.className = \"sortbutton\";",
                "                b.innerHTML = titel[sp].innerHTML;",
                "                b.addEventListener(\"click\", function () {",
                "                    tsort(sp);",
                "                }, false);",
                "                titel[sp].innerHTML = \"\";",
                "                titel[sp].appendChild(b);",
                "            }",
                "            var getData = function (ele, s) {",
                "                    var val = ele.textContent; // Nicht im IE8",
                "                    if (!isNaN(val) && val.search(/[0-9]/) != -1) return val;",
                "                    var n = val.replace(\",\", \".\");",
                "                    if (!isNaN(n) && n.search(/[0-9]/) != -1) return n;",
                "                    sorttype[s] = \"s\"; // String",
                "                    return val;",
                "                } // getData",
                "            var vglFkt_s = function (a, b) {",
                "                    var as = a[sortiert],",
                "                        bs = b[sortiert];",
                "                    if (as > bs) {",
                "                        return 1;",
                "                    }",
                "                    else {",
                "                        return -1;",
                "                    }",
                "                } // vglFkt_s",
                "            var vglFkt_n = function (a, b) {",
                "                    return parseFloat(a[sortiert]) - parseFloat(b[sortiert]);",
                "                } // vglFkt_n",
                "            var tsort = function (sp) {",
                "                    if (sp == sortiert) {",
                "                        // Tabelle ist schon nach dieser Spalte sortiert, also",
                "                        // nur Reihenfolge umdrehen:",
                "                        arr.reverse();",
                "                    }",
                "                    else { // Sortieren",
                "                        sortiert = sp;",
                "                        if (sorttype[sp] == \"n\") {",
                "                            arr.sort(vglFkt_n);",
                "                        }",
                "                        else {",
                "                            arr.sort(vglFkt_s);",
                "                        }",
                "                    }",
                "                    for (var z = 0; z < nzeilen; z++) {",
                "                        tbdy.appendChild(arr[z][nspalten]); "
                        +                              "// Sortierte Daten zurückschreiben",
                "                    }",
                "",
                "                    if (-1 < hupIndex) {",
                "                        var oldHup = \"\";",
                "                        var rowClass = \"" + cssClassForOddRows + "\";",
                "                        for (var z = 0; z < nzeilen; z++) {",
                "                            var row = tz[z];",
                "                            var hupCell = row.cells[hupIndex];",
                "                            var hup = hupCell.innerHTML;",
                "",
                "                            var classList = row.classList;",
                "                            if (classList.contains(\"" + cssClassForEvenRows + "\")) {",
                "                                classList.remove(\"" + cssClassForEvenRows + "\");",
                "                            }",
                "                            if (classList.contains(\"" + cssClassForOddRows + "\")) {",
                "                                classList.remove(\"" + cssClassForOddRows + "\");",
                "                            }",
                "",
                "                            //console.log(\"z=\", z, \", hup=\", hup, \", "
                            + "oldHup=\", oldHup, \", diff=\", hup.localeCompare(oldHup));",
                "                            if (0 != hup.localeCompare(oldHup)) {",
                "                                if (0 == rowClass.localeCompare(\""
                            + cssClassForOddRows + "\")) {",
                "                                    rowClass = \"" + cssClassForEvenRows + "\";",
                "                                }",
                "                                else {",
                "                                    rowClass = \"" + cssClassForOddRows + "\";",
                "                                }",
                "                            }",
                "                            classList.add(rowClass);",
                "                            oldHup = hup;",
                "                        }",
                "                    }",
                "",
                "                } // tsort",
                "            // Kopfzeile vorbereiten",
                "            for (var i = 0; i < titel.length; i++) {",
                "                initTableHead(i);",
                "            }",
                "            // Array mit Info, wie Spalte zu sortieren ist, vorbelegen",
                "            for (var s = 0; s < nspalten; s++) {",
                "                sorttype[s] = \"n\";",
                "            }",
                "            // Tabelleninhalt in ein Array kopieren",
                "            for (var z = 0; z < nzeilen; z++) {",
                "                var zelle = tz[z].getElementsByTagName(\"td\"); // cells;",
                "                arr[z] = new Array(nspalten + 1);",
                "                arr[z][nspalten] = tz[z];",
                "                for (var s = 0; s < nspalten; s++) {",
                "                    var zi = getData(zelle[s], s);",
                "                    arr[z][s] = zi;",
                "                    // zelle[s].innerHTML += \"<br>\"+zi+\"<br>\"+sorttype[s]; "
                        +                     "// zum Debuggen",
                "                }",
                "            }",
                "        } // tableSort",
                "    var initTableSort = function () {",
                "            var sort_Table = document.querySelectorAll(\"table#" + tableId + "\");",
                "            for (var i = 0; i < sort_Table.length; i++) {",
                "                new tableSort(sort_Table[i]);",
                "            }",
                "        } // initTable",
                "    if (window.addEventListener) {",
                "        window.addEventListener(\"DOMContentLoaded\", initTableSort, false); "
                        +              "// nicht im IE8",
                "    }",
                "</script>");
        return this;
    }

    /**
     * Fügt einen öffnenden Tag der Form <tag> hinzu.
     *
     * @param tag
     *            Der zu öffnende Tag.
     */
    @Override
    public HtmlBuilder appendOpeningTag(String tag) {
        super.appendOpeningTag(tag);
        return this;
    }

    /**
     * Fügt einen öffnenden Tag der Form <tag> hinzu.
     *
     * @param tag
     *            Der zu öffnende Tag.
     * @param cssClass
     *            Die CSS-Klasse des zu öffnenden Tags.
     */
    public HtmlBuilder appendOpeningTagWithClass(String tag, String cssClass) {
        appendLn("<" + tag + " class=\"" + cssClass + "\">");
        increaseIndentationLevel();
        return this;
    }

    /**
     * Fügt einen öffnenden Tag der Form <tag id="..."> hinzu.
     *
     * @param tag
     *            Der zu öffnende Tag.
     * @param id
     *            Die ID des zu öffnenden Tags.
     */
    public HtmlBuilder appendOpeningTagWithId(String tag, String id) {
        appendLn("<" + tag + " id=\"" + id + "\">");
        increaseIndentationLevel();
        return this;
    }

    /**
     * Fügt einen öffnenden Tag der Form <tag id="..." class="..."> hinzu.
     *
     * @param tag
     *            Der zu öffnende Tag.
     * @param id
     *            Die ID des zu öffnenden Tags.
     * @param cssClass
     *            Die CSS-Klasse des zu öffnenden Tags.
     */
    public HtmlBuilder appendOpeningTagWithIdAndClass(String tag, String id, String cssClass) {
        appendLn("<" + tag + " id=\"" + id + "\" class=\"" + cssClass + "\">");
        increaseIndentationLevel();
        return this;
    }

    /**
     * Fügt einen öffnenden Tag der Form <tag param1 param2 ... > hinzu.
     *
     * @param tag
     *            Der zu öffnende Tag.
     * @param parameters
     *            Parameter die angefügt werden.
     */
    @Override
    public HtmlBuilder appendOpeningTagWithParameters(String tag, String ... params) {
        super.appendOpeningTagWithParameters(tag, params);
        return this;
    }

    /**
     * Fügt einen öffnenden Tag der Form <tag param1 param2 ... > hinzu.
     *
     * @param tag
     *            Der zu öffnende Tag.
     * @param parameters
     *            Parameter die angefügt werden.
     */
    @Override
    public HtmlBuilder appendOpeningTagWithParameters(String tag, List<String> params) {
        super.appendOpeningTagWithParameters(tag, params);
        return this;
    }

    /**
     * Fügt einen schließenden Tag der Form <tag> hinzu.
     *
     * @param tag
     *            Der zu schließende Tag.
     */
    @Override
    public HtmlBuilder appendClosingTag(String tag) {
        super.appendClosingTag(tag);
        return this;
    }

    /**
     * Fügt einen Text in einem Span mit der angegebenen CSS-Klasse hinzu.
     *
     * @param text
     *            Text innerhalb des Spans.
     * @param cssClass
     *            Die CSS-Klasse des Spans.
     */
    public HtmlBuilder appendSpanWithClass(String text, String cssClass) {
        appendInTagWithClass("span", text, cssClass);
        return this;
    }

    /**
     * Fügt einen Text in einem Span mit der angegebenen CSS-ID hinzu.
     *
     * @param text
     *            Text innerhalb des Spans.
     * @param cssId
     *            Die CSS-ID des Spans.
     */
    public HtmlBuilder appendSpanWithId(String text, String cssId) {
        appendInTagWithId("span", text, cssId);
        return this;
    }

    /** Fügt einen öffnenden span-Tag hinzu. */
    public HtmlBuilder appendOpeningSpan() {
        appendOpeningTag("span");
        return this;
    }

    /**
     * Fügt einen öffnenden span-Tag mit ID hinzu.
     *
     * @param id
     *            Die ID des zu öffnenden span-Tags.
     */
    public HtmlBuilder appendOpeningSpanWithId(String id) {
        appendOpeningTagWithId("span", id);
        return this;
    }

    /**
     * Fügt einen öffnenden span-Tag mit CSS-Klasse hinzu.
     *
     * @param cssClass
     *            Die CSS-Klasse des Spans.
     */
    public HtmlBuilder appendOpeningSpanWithClass(String cssClass) {
        appendOpeningTagWithClass("span", cssClass);
        return this;
    }

    /**
     * Fügt einen öffnenden span-Tag mit ID und CSS-Klasse hinzu.
     *
     * @param id
     *            Die ID des zu öffnenden span-Tags.
     */
    public HtmlBuilder appendOpeningSpanWithIdAndClass(String id, String cssClass) {
        appendOpeningTagWithIdAndClass("span", id, cssClass);
        return this;
    }

    /** Fügt einen schließenden span-Tag hinzu. */
    public HtmlBuilder appendClosingSpan() {
        appendClosingTag("span");
        return this;
    }

    /** Fügt einen öffnenden header-Tag hinzu. */
    public HtmlBuilder appendOpeningHeader() {
        appendOpeningTag("header");
        return this;
    }

    /** Fügt einen schließenden header-Tag hinzu. */
    public HtmlBuilder appendClosingHeader() {
        appendClosingTag("header");
        return this;
    }

    /** Fügt einen öffnenden main-Tag hinzu. */
    public HtmlBuilder appendOpeningMain() {
        appendOpeningTag("main");
        return this;
    }

    /** Fügt einen schließenden main-Tag hinzu. */
    public HtmlBuilder appendClosingMain() {
        appendClosingTag("main");
        return this;
    }

    /** Fügt einen öffnenden footer-Tag hinzu. */
    public HtmlBuilder appendOpeningFooter() {
        appendOpeningTag("footer");
        return this;
    }

    /** Fügt einen schließenden footer-Tag hinzu. */
    public HtmlBuilder appendClosingFooter() {
        appendClosingTag("footer");
        return this;
    }

    /** Fügt einen iFrame mit Quellcode hinzu. */
    public HtmlBuilder appendFrameWithSource(String sourceCode) {
        String src = sourceCode.replace("'", "\"");

        appendIndented("<");
        append("iframe");
        append(" ").append("srcdoc='");
        appendLineBreak();
        increaseIndentationLevel();
        increaseIndentationLevel();
        appendMultipleLines(src);
        decreaseIndentationLevel();
        appendIndented("' ").append("sandbox=\"\"");
        append(">");
        appendLineBreak();

        appendClosingTag("iframe");
        return this;
    }

    /** Fügt einen iFrame mit Quellcode und einer CSS-Klasse für den iFrame hinzu. */
    public HtmlBuilder appendFrameWithSource(String cssClass, String sourceCode) {
        String src = sourceCode.replace("'", "\"");

        appendIndented("<");
        append("iframe");
        append(" ").append("class=\"").append(cssClass).append("\"");
        append(" ").append("srcdoc='");
        appendLineBreak();
        increaseIndentationLevel();
        increaseIndentationLevel();
        appendMultipleLines(src);
        decreaseIndentationLevel();
        appendIndented("' ").append("sandbox=\"\"");
        append(">");
        appendLineBreak();

        appendClosingTag("iframe");
        return this;
    }

    /**
     * Fügt ein JavaScript onRowClick() hinzu, welches aufgerufen wird, wenn auf REihen von
     * Tabellen geklickt wird.
     */
    public HtmlBuilder appendOnRowClickJavaScript() {
        appendMultipleLines(
                //"<script language=\"javascript\" type=\"text/javascript\">", deprecated in HTML5
                "<script>",
                "    function onRowClick(tableId, callback) {",
                "        var table = document.getElementById(tableId);",
                "        var rows = table.getElementsByTagName(\"tr\");",
                "        for (var index = 0; index < rows.length; index++) {",
                "            var rowTable = function (row) {",
                "                return function () {",
                "                    callback(row);",
                "                };",
                "            };",
                "            table.rows[index].onclick = rowTable(table.rows[index]);",
                "        }",
                "    };",
                "</script>");
        return this;
    }

    /**
     * Fügt ein JavaScript switch_maximum_hits_field() hinzu, welches dazu dient, ein Eingabefeld
     * abhängig vom Zustand einer Checkbox zu deaktivieren
     */
    public HtmlBuilder appendSwitchMaximumHitsFieldJavaScript() {
        appendMultipleLines(
                //"<script language=\"javascript\" type=\"text/javascript\">", deprecated in HTML5
                "<script>",
                "    function switch_maximum_hits_field(idMaxHits, idCheckBox) {",
                "        inputField = document.getElementById(idMaxHits);",
                "        checkbox = document.getElementById(idCheckBox);",
                "        if (checkbox.checked) {",
                "            inputField.disabled = true;",
                "        }",
                "        else {",
                "            inputField.disabled = false;",
                "        }",
                "    }",
                "</script>");
        return this;
    }

    /** Fügt einen Anker hinzu. */
    public HtmlBuilder createAnchor(String name) {
        appendMultipleLines("<a name=\"" + name + "\"/>");
        return this;
    }

    /**
     * Legt fest, dass auch vor dem Anfang einer neuen Überschrift vom Typ H2 ein Link zum Anfang
     * angezeigt werden soll.
     */
    public HtmlBuilder appendTopLinksToH2() {
        appendTopLinksToH2 = true;
        return this;
    }

    /** Fügt einen Anker hinzu, zu dem die TopLinks springen. */
    public HtmlBuilder appendTopAnker() {
        String anker = "<p>" + createTopLinkAnker() + "</p>";
        appendIndented(anker);
        return this;
    }

    private String createTopLinkAnker() {
        return "<a name=\"" + TOP_LINK_NAME + "\"/>";
    }

}
