/*
* This software and supporting documentation were developed by
*
* Siemens Corporate Technology
* Competence Center Knowledge Management and Business Transformation
* D-81730 Munich, Germany
*
* Authors (representing a really great team ;-) )
* Stefan B. Augustin, Thorbj�rn Hansen, Manfred Langen
*
* This software is Open Source under GNU General Public License (GPL).
* Read the text of this license in LICENSE.TXT
* or look at www.opensource.org/licenses/
*
* Once more we emphasize, that:
* THIS SOFTWARE IS MADE AVAILABLE, AS IS, WITHOUT ANY WARRANTY
* REGARDING THE SOFTWARE, ITS PERFORMANCE OR
* FITNESS FOR ANY PARTICULAR USE, FREEDOM FROM ANY COMPUTER DISEASES OR
* ITS CONFORMITY TO ANY SPECIFICATION. THE ENTIRE RISK AS TO QUALITY AND
* PERFORMANCE OF THE SOFTWARE IS WITH THE USER.
*
*/
// Template
// ************ package ******************************************************
package KFM.GUI.Templates;
// ************ imports ******************************************************
import java.io.*;
import java.lang.String;
import java.util.Properties;
import KFM.Exceptions.ProgrammerException;
import KFM.*;
import KFM.Language;
import KFM.RegExp;
import KFM.log.*;
import com.oroinc.text.regex.MatchResult; // Get rid of this when it is included in RegExp.
/** HTML Template which implements additional HTML elements that are executed on the server.
*
* <H2>Still to do</H2>
*
* <P>Since 2000-03-06, `write� and `writeToString� set `html=null�, hoping to reduce memory usage.
* Now only exactly one of `write� or `writeToString� may be called, and only once!</P>
*
* <P>Implement the observer. Each template must register at an observer so that the observer may call the
* template�s `clearCache�.</P>
*
* <P>When we extend this class�s functionality, we might need to register labels (`registerKfm� and
* `registerKfmIf�) and parse the template before modifying it (`parse�).</P>
*
* <H2>Supported element types</H2>
*
* <P>You can extend HTML with two kinds of element types: KfmIf and Kfm. Their BNF is as follows:
*
* <PRE>
* KfmIf := <"KfmIf" Label ">" Content "</KfmIf>"
* Kfm := "<Kfm" Label Attributes ">" Content "</KfmIf>"
* where
* Label := Attribute
* Attributes := ( Attribute "=" Quote Value Quote )*
* Quote := "\"" | "'"
* </PRE>
*
* Note: Since 99-11-24, we can quote with double or single quotes. Contrary to the case in HTML
* a single quote is not allowed inside double quotes, nor the other way around. (This only
* concerns KFM labels, not ordinary HTML tags.)
*
* The first attribute is the label, without a value. Following values must have values.
* A value must be quoted and may not contain a quote nor a ">", use the "&...;" escape syntax.</P>
*
* <P>IMPORTANT NOTE: Currently, the element types KfmIf and Kfm may not be nested!</P>
*
* <H2>Usage</H2>
*
* <P>First create a template. Every time you want to display the template, call `startWithLanguage. The
* first time you call `startWithLanguage� for any language, the template is loaded from file and cached.
* After that, the template is fetched from the cache. `startWithLanguage� will also copy the template into
* the working buffer.</P>
*
* <P>To evaluate the KfmIf elements in the working buffer, use `replaceKfmIf�. To evaluate the Kfm elements
* in the working buffer use `getKfmParam� to get the parameter values and `replaceKfm� to replace the
* element by the text you have calculated.</P>
*
* <P>Before 2000-03-06: You may use `toString� or `write� at any time to get the current working buffer as
* string or to write it to a stream. Since 2000-03-06, `write� and `writeToString� set `html=null�, hoping
* to reduce memory usage. Now only exactly one of `write� or `writeToString� may be called, and only
* once!</P>
*
* <A NAME="debug-behaviour"><H2>Debug behaviour</H2></A>
*
* <P>In debug mode, the `replace*� methods mark evaluated Kfm- and KfmIf-commands with comments, for
* example
* <CODE><KFM A B="b">Old content</KFM></CODE>
* is replaced by
* <CODE><!--KFM A B="b"-->New content<!--/KFM--></CODE>.</P>
* <P>In debug mode, The `check� method marks all left-over Kfm- and KfmIf-commands in red.</P>
*
* <H2>Definitions for HTML</H2>
*
* <P>See <A HREF="http://www.w3.org/TR/REC-html40/intro/sgmltut.html#h-3.2">SGML constructs used in
* HTML</A> from the W3Cs document for HTML for correct definitions of words. A summary follws.</P>
*
* <P>In the example <CODE><NAME ATTR="VALUE">CONTENT</NAME></CODE> we have the following:</P>
*
* <DL>
* <DT> element name<DD> <CODE>NAME</CODE>
* <DT> element type<DD> the type of the element, like types/classes/signatures in programming languages
* (which attributes it accepts etc.)
* <DT> element <DD> <CODE><NAME ATTR="VALUE">CONTENT</NAME></CODE>
* <DT> start tag <DD> <CODE><NAME ATTR="VALUE"></CODE>
* <DT> end tag <DD> <CODE></NAME></CODE>
* <DT> attribute <DD> <CODE>ATTR</CODE>
* <DT> value (of attribute <CODE>ATTR</CODE>)
* <DD> <CODE>VALUE</CODE> (without the quotes)
* <DT> content <DD> <CODE>CONTENT</CODE>
* </DL>
*
* <P>Convention: A Kfm element with label CMD is a Kfm-command CMD, a KfmIf element with label IF is a
* KfmIf-command IF.</P>
*
* <H2>Related classes</H2>
*
* @see IText
* see IText -- Internationalized Text.
*
* @see ComplexTemplate
* see ComplexTemplate -- Powerful Template with complex interface.
*
* <pre>
* ====================
* History:
* creation date: 99.02.26
* --------------------
* Changes:
* <none>
* </pre>
*
* @version 1.0 (99.02.27)
*
*/
public class Template {
//
// Constants
//
/**
* File extension of a template.
*/
public static final String templateExtension = ".kfmt.htm";
//
// Variables
//
/**
* current language.
*
*/
protected Language currentLanguage;
/**
* Name prefix.
*
* E.g. "NewProposal" for a template file
* "NewProposal." + Language + templateExtension
* for example "NewProposal.German.kfmt.htm".
*/
protected String namePrefix;
/**
* Directory that contains files with prefix `namePrefix�.
*/
protected String dir;
/**
* Working buffer (text that to be output by `write� or `toString�).
*
* Note: The type should be something that handles replacement more efficiently than a `String�. But the
* only candidate we know is `StringBuffer�, which does not allow replacement.
*/
protected String html;
/**
* If this template has already been loaded for a language, it is cached here.
*
* Use `Language.toString()� for keys, the property is the template as a string.
*/
protected Properties cache; // languagename -> template
/**
* Record debug behaviour vs release behaviour.
*
* See <A HREF="#debug-behaviour">debug behaviour</A> in header.
*/
protected boolean debug = false;
/**
* Is true iff `markCommandsLeft(true)� is running.
*
* Means that found Kfm- and KfmIf-commands should be marked, that is made visible and red (for debugging).
*/
protected boolean domark = false;
/** Log file. */
protected KFMLog mLog = null;
//
// Methods
//
/**
* Prepare a template without loading it.
*
* @param theNamePrefix E.g. "NewProposal" for a template file
* "NewProposal." + Language + templateExtension
* for example "NewProposal.German.kfmt.htm".
*/
public Template(/* TemplateObserver observer,*/ String theDir, String theNamePrefix)
{
namePrefix = theNamePrefix;
dir = theDir;
cache = new Properties();
}
/**
* Prepare a template without loading it. This constructor supporting new log concept.
*
* @param theNamePrefix E.g. "NewProposal" for a template file
* "NewProposal." + Language + templateExtension
* for example "NewProposal.German.kfmt.htm".
*/
public Template(/* TemplateObserver observer,*/ String theDir, String theNamePrefix, KFMLog aLog)
{
this(theDir, theNamePrefix);
mLog = aLog;
}
/**
* Clear cache with templates in different languages.
*/
public void clearCache () {
cache.clear();
}
/** for subclasses to initialize when the template is started to be used. */
public void init()
{
// @@@ nothing, all is done by startWithLanguage... please refactor
}
/**
* Instantiate working buffer, if neccessary load language specific template from file and cache it.
*
* On success, writes the template into the working buffer `html�. On fail, the working buffer `html� is
* not initialized. Note: In Java, a `String� is not modifyable. If you modify a `String� variable, you
* get a new object and the old object is not modified. That means that you can modify `html� without
* changing the contents in the cache.
*
* If the template is not found in the requested language, try to get it in German.
*
*@return Whether the template was found.
*/
public boolean startWithLanguage (Language language)
{
try {
if(! cache.containsKey(language.toString())) {
String templateFileName = dir + "/" + namePrefix + "." + language.toString() + templateExtension;
if (mLog == null) {
KFMSystem.log.info("Template::StartWithLanguage: Try to load file \""
+ templateFileName + "\" and put it into the cache.");
} else {
mLog.info("Template::StartWithLanguage: Try to load file \""
+ templateFileName + "\" and put it into the cache.");
}
//
// * Read from file.
//
// @@@ Should be in a convenience function somewhere.
BufferedReader in = new BufferedReader(new FileReader(templateFileName));
StringBuffer inBuf = new StringBuffer();
String line;
readline: while(true) {
line = in.readLine();
if(line == null) break readline;
inBuf.append(line);
inBuf.append('\n');
}
in.close();
cache.put(language.toString(),
"<!-- START TEMPLATE FILE " + templateFileName + "\n -->"
+ inBuf.toString()
+ "<!-- END TEMPLATE FILE " + templateFileName + "\n -->");
}
html = cache.getProperty(language.toString());
currentLanguage = language;
init();
return true /*success*/;
} catch(FileNotFoundException e) {
if(language.isGerman()) {
if (mLog == null) {
KFMSystem.log.info("Template::StartWithLanguage: "
+ "Couldn't load file in requested language German.");
} else {
mLog.info("Template::StartWithLanguage: "
+ "Couldn't load file in requested language German.");
}
// @@@ This is stupid. We should rethrow `FileNotFoundException�.
return false /*fail*/;
} else {
if (mLog == null) {
KFMSystem.log.info("Template::StartWithLanguage: Couldn't load file "
+ "in requested language, trying German ('de' and 'german').");
} else {
mLog.info("Template::StartWithLanguage: Couldn't load file "
+ "in requested language, trying German ('de' and 'german').");
}
if(startWithLanguage(new Language("de"))) {
return true;
} else if(startWithLanguage(new Language("german"))) {
return true;
} else {
if (mLog == null) {
KFMSystem.log.info("Template::StartWithLanguage: "
+ "Couldn't load file in requested German.");
} else {
mLog.info("Template::StartWithLanguage: "
+ "Couldn't load file in requested German.");
}
// @@@ This is stupid. We should rethrow `FileNotFoundException�.
return false /*fail*/;
}
}
} catch(IOException e) {
if (mLog == null) {
KFMSystem.log.error("Template::StartWithLanguage: IO-Error", e);
} else {
mLog.error("Template::StartWithLanguage: IO-Error", e);
}
return false /*fail*/;
}
}
/** Write working buffer to an OutputStream, and delete working buffer.
*
* We choose OutputStream because it is the parent class of ServletOutputStream.
*
* <P>Since 2000-03-06, `write� and `writeToString� set `html=null�, hoping to reduce memory usage.
* Now only exactly one of `write� or `writeToString� may be called, and only once!</P>
*/
public void write(OutputStream out) {
privateCheckHtml();
PrintWriter pwout = new PrintWriter(out);
pwout.print(html);
pwout.flush();
html = null;
}
/**
* @see write(OutputStream out)
*/
public void write(PrintWriter aWriter) {
privateCheckHtml();
aWriter.print(html);
aWriter.flush();
html = null;
}
/** Return working buffer as string, and delete working buffer.
*
* <P>Since 2000-03-06, `write� and `writeToString� set `html=null�, hoping to reduce memory usage.
* Now only exactly one of `write� or `writeToString� may be called, and only once!</P>
*
* This used to be called `toString�, but that was not a good name,
* because `toString� should not modify the object at all.
*/
public String writeToString() {
privateCheckHtml();
String tRet = html;
html = null;
return tRet;
}
/** Return working buffer as string.
*
* @deprecated May produce a memory leak, because `html� (which can get really big) might not be deleted.
* You may still use it for debugging, though.
*/
public String toString() {
if (mLog == null) {
KFMSystem.log.info("Warning: Deprecated method Template::toString called!");
} else {
mLog.info("Warning: Deprecated method Template::toString called!");
}
privateCheckHtml();
return html;
}
/**
* Helper that constructs a regexp for `<elemname label [params]>� with optional spaces.
*
*@param elemname Element name, e.g. "Kfm", "KfmIf".
*@param label Case insensitive label, e.g. "Date" in "<Kfm Date>". May be a regexp. Must not
* contain whitespace.
*
* Note: If you have to insert parenthesises here, use the `(?:...)� notation of Perl5 that means that
* the parenthesises do not define a match group. If you do not do that, you will have to change the
* references to match groups in every regexp that uses `cmdPattern�.
*/
protected static String cmdPattern(String elemname, String label) {
return "\\<\\s*" + elemname + "\\s+" + label +
"(?:\\>|\\s+[^>]*\\>)";
}
/**
* Evaluate all occurences of a KfmIf-command.
*
* Replace all elements of a KfmIf-command `label�, that is `<KfmIf label [params]>text</KfmIf>�, by
* `text� or empty string, depending on `keep�.
*
*@param label Case insensitive label, e.g. "Date" in "<Kfm Date>". May be a regexp. Must not contain
* whitespace.
*@return whether a replacement was performed.
*
* For debug behaviour, see <A HREF="#debug-behaviour">debug behaviour</A> in header.
* For mark behaviour, see ???.
*/
public boolean replaceAllKfmIf (String label, boolean keep)
{
privateCheckHtml();
boolean replacementOccured = false;
while(true) {
// Note: endtagM must be *after* starttagM.
MatchResult starttagM = RegExp.match(cmdPattern("KfmIf", label), html);
if(starttagM == null) {
return replacementOccured;
}
int offset = starttagM.endOffset(0);
String html2 = html.substring(offset);
MatchResult endtagM = RegExp.match("\\</KfmIf\\s*\\>", html2);
if(endtagM == null) {
return replacementOccured;
}
replacementOccured = true;
String replacement = (keep ? html.substring(starttagM.endOffset(0), endtagM.beginOffset(0) + offset) : "" );
if(domark) {
//D KFMSystem.log.detail("replaceAllKfmIf: mark!");
replacement = markTag(starttagM.group(0)) + replacement + markTag(endtagM.group(0));
}
if(debug) {
replacement = commentifyTag(starttagM.group(0)) + replacement + commentifyTag(endtagM.group(0));
}
//D KFMSystem.log.detail("Replace of Command KfmIf " + label + ": "
//D + html.substring(starttagM.endOffset(0), endtagM.beginOffset(0))
//D + " -> " + replacement);
html = html.substring(0, starttagM.beginOffset(0))
+ replacement
+ html2.substring(endtagM.endOffset(0));
}
}
/**
* Get a param of first occurence of a Kfm-command.
*
* Search for start tag of first Kfm-command `label�, that is `<Kfm label [params] param="value"
* [params]>� and return `value�. Return empty string when not found.
*
*@param label Case insensitive label, e.g. "Date" in "<Kfm Date>". May be a regexp. Must not
* contain whitespace.
*/
public String getKfmParam (String label, String param)
{
privateCheckHtml();
//D KFMSystem.log.detail("get: " + label + " " + param);
MatchResult starttagM = RegExp.match(cmdPattern("Kfm", label), html);
if(starttagM == null) {
//D KFMSystem.log.detail("label not found.");
return "";
}
MatchResult valueM = RegExp.match(
"\\s" + param + "\\s*=\\s*"
// This will allow something like "...', and does *not* allow embedded ">".
// Note that "group(1)" does not contain the quotes.
// + "[\"']([^>\"']*)[\"']"
// This will allow "...'..." and '..."...' and *does* allow embedded ">".
// Note that "group(1)" now contains the quotes.
+ "((?:\"[^\"]*\")|(?:'[^']*'))"
, starttagM.group(0));
if(valueM == null) {
//D KFMSystem.log.detail("param not found.");
return "";
}
// Now we must strip the quotes.
// return valueM.group(1);
String t = valueM.group(1);
t = t.replace('�', '>');
return t.substring(1, t.length()-1);
}
/**
* Evaluate first occurence of a Kfm-command.
*
* Replace element of first Kfm-command `label�, that is `<Kfm label [params]>text</Kfm>�, by `replace�.
*
* For debug behaviour, see <A HREF="#debug-behaviour">debug behaviour</A> in header.
* For mark behaviour, see ???.
*
*@param label Case insensitive label, e.g. "Date" in "<Kfm Date>". May be a regexp. Must not contain
* whitespace.
*@param replace Hack: iff null, keep the current content.
*
*@return whether a replacement was performed.
*/
public boolean replaceKfm (String label, String replace)
{
if(replace == null) {
// Legacy behaviour.
return replaceKfm(label, true, "", "");
} else {
return replaceKfm(label, false, replace, "");
}
}
/**
* Evaluate first occurence of a Kfm-command.
*
* Replace element of first Kfm-command `label�, that is `<Kfm label [params]>text</Kfm>�, by `replace�.
*
* For debug behaviour, see <A HREF="#debug-behaviour">debug behaviour</A> in header.
* For mark behaviour, see ???.
*
*@param label Case insensitive label, e.g. "Date" in "<Kfm Date>". May be a regexp. Must not contain
* whitespace.
*@param aKeep Keep the content?
*@param aBefore Before the content.
*@param aAfter After the content
*
*@return whether a replacement was performed.
*/
public boolean replaceKfm (String label, boolean aKeep, String aBefore, String aAfter)
{
privateCheckHtml();
// This regexp ensures that start tag is before end tag.
MatchResult elementM = null;
String p = "";
try {
p = "(" + cmdPattern("Kfm", label) + ")" /*group 1: start tag*/
// This seems very suspect (WARNING: it can cause StackOverflow Errors!):
// + "((?:.|\\n)*?)" /*group 2: content*/
// lets try: .*?
+ "(.*?)" /*group 2: content replaced*/
+ "(" + "\\</Kfm\\s*\\>" + ")"; /*group 3: end tag */
elementM = RegExp.match(p, html, false, true);
} catch(Error e) {
mLog.error(
"Template::replace,RegExp.match caused Error: " + e + "\n"
+ "Arguments: p = '" + p + "'\n"
+ "html = '" + html + "'");
return false;
}
final int starttagI = 1;
final int contentI = 2;
final int endtagI = 3;
if(elementM == null) return false;
String replacement = aBefore + (aKeep ? elementM.group(contentI) : "") + aAfter;
if(domark) {
//D KFMSystem.log.detail("replaceKfm: mark!");
replacement = markTag(elementM.group(starttagI))
+ replacement + markTag(elementM.group(endtagI));
}
if(debug) {
replacement = commentifyTag(elementM.group(starttagI))
+ replacement + commentifyTag(elementM.group(endtagI));
}
html = html.substring(0, elementM.beginOffset(0))
+ replacement
+ html.substring(elementM.endOffset(0));
//D KFMSystem.log.detail("Replace of Command Kfm " + label + ": " + elementM.group(0) + " -> " + replacement);
return true;
}
/**
* Evaluate all occurences of a Kfm-command.
*
* Replace all elements of a Kfm-command `label�, that is `<Kfm label [params]>text</Kfm>�, by `replace�.
* Only useful for Kfm-commands where you do not need to process parameters.
*
* For debug behaviour, see <A HREF="#debug-behaviour">debug behaviour</A> in header.
* For mark behaviour, see ???.
*
*@param label Case insensitive label, e.g. "Date" in "<Kfm Date>". May be a regexp. Must not contain
* whitespace.
*@param replace Hack: iff null, keep the current content.
*
*@return whether a replacement was performed.
*/
public boolean replaceAllKfm (String label, String replace)
{
if(replaceKfm(label, replace)) {
while(replaceKfm(label, replace)) {}
return true;
} else {
return false;
}
}
/**
* Evaluate all occurences of a Kfm-command.
*
* Replace all elements of a Kfm-command `label�, that is `<Kfm label [params]>text</Kfm>�, by `replace�.
* Only useful for Kfm-commands where you do not need to process parameters.
*
* For debug behaviour, see <A HREF="#debug-behaviour">debug behaviour</A> in header.
* For mark behaviour, see ???.
*
*@param label Case insensitive label, e.g. "Date" in "<Kfm Date>". May be a regexp. Must not contain
* whitespace.
*@param aKeep Keep the content?
*@param aBefore Before the content.
*@param aAfter After the content
*
*@return whether a replacement was performed.
*/
public boolean replaceAllKfm (String label, boolean aKeep, String aBefore, String aAfter)
{
if(replaceKfm(label, aKeep, aBefore, aAfter)) {
while(replaceKfm(label, aKeep, aBefore, aAfter)) {}
return true;
} else {
return false;
}
}
/**
* In a subclass `S�, define a method `execute� that does all replacements. This function can be called by all
* subclasses of `S�. I do not think we can give `execute� a definite signature.
*/
//
// Debugging functionality
//
private void privateCheckHtml()
{
if(html == null) {
throw new ProgrammerException(
"Class Template: html is null. "
+ "Either you used `write� and `writeToString� more than once, "
+ " or you forgot to call startWithLanguage for '" + dir + "/" + namePrefix);
}
}
/**
* Set template to debug behaviour or release behaviour.
*
* See <A HREF="#debug-behaviour">debug behaviour</A> in header.
*/
public void setDebug(boolean theDebug)
{
debug = theDebug;
}
/**
* Check whether there are Kfm- or KfmIf-commands left.
*/
public boolean checkCommandsLeft ()
{
return markCommandsLeft(false /*do not mark*/);
}
/**
* Check whether there are Kfm- or KfmIf-commands left and always mark them.
*
* @see markCommandsLeft
* see markCommandsLeft(boolean)
*/
public boolean markCommandsLeft ()
{
return markCommandsLeft(true /*do mark*/);
}
/**
* Check whether there are Kfm- or KfmIf-commands left and optionally mark them.
*
*@param mark True iff the Kfm- or KfmIf-commands left should be marked.
* Otherwise just compute the return value.
*
*@return True iff there are Kfm- or KfmIf-commands left.
*/
public boolean markCommandsLeft (boolean mark)
{
try {
boolean replacementOccured;
domark = mark;
// Note: "\\S+" matches any label, \\S is non-whitespace.
replacementOccured = replaceAllKfmIf("\\S+", true /*keep content*/);
replacementOccured |= replaceAllKfm("\\S+", null /*keep content*/);
return replacementOccured;
} finally {
domark = false;
}
}
/**
* Make a tag visible and red (for debugging).
*/
protected String markTag (String tag)
{
return "<FONT COLOR=red>" + Converter.quoteHtmlTags(tag) + "</FONT>";
}
/**
* Put a tag into comments (for debugging).
*/
protected String commentifyTag (String tag)
{
return "<!--" + Converter.quoteHtmlTags(tag) + "-->";
}
//
// Usage sample.
//
/**
* Sample of usage.
*/
public static void main (String args[])
throws FileNotFoundException, IOException
{
Template t = new Template("o:/KFM/java/src/KFM/GUI/Templates/", "sample-template");
t.setDebug(true);
//D KFMSystem.log.detail("Lese ein.");
t.startWithLanguage(Language.German);
//
// * Execute substitutions.
//
//D KFMSystem.log.detail("Ersetze.");
t.replaceAllKfmIf("IdeaExists", true);
t.replaceAllKfmIf("NoIdeaExists", false);
t.replaceAllKfm("Title", "xxx");
t.replaceAllKfm("Clients", "yyy");
// A loop like this will only work correctly when the HTML code is wellformed and follows our restrictions.
String x, y;
do {
x = t.getKfmParam("Point", "x");
y = t.getKfmParam("Point", "y");
} while(t.replaceKfm("Point", "(" + x + ", " + y + ")"));
t.markCommandsLeft(); // Should find the `<Kfm InsertFormTag>�.
//D KFMSystem.log.detail("Gebe aus.");
t.write(System.out);
//D KFMSystem.log.detail();
//D KFMSystem.log.detail("Fertig");
}
// Test `main� on this file:
// <!-- sample-template.German.kfmt.html -- >
//
// <KfmIf IdeaExists>
// <P>Ich bin heute ideenreich.</P>
// </KfmIf>
//
// <KfmIf NoIdeaExists>
// <P>Ich bin heute ideenlos.</P>
// </KfmIf>
//
// <P>Ein allgemeiner Punkt ist <Kfm Point x="a" y="b"></Kfm>.
// Ein falscher Punkt ist <Kfm Point x="0" y="1" z="2"></Kfm>.
// Ein richtiger Punkt ist <Kfm Point y="-y" x="-x"></Kfm>.
// </P>
//
// <Kfm InsertFormTag></Kfm> <!-- Expands to <FORM>. Not handled in this example. -->
// <TABLE>
// <TR><TD>�berschrift des bilateralen Vorschlags</TD>
// <TD><Kfm Title></Kfm></TD></TR>
// <TR><TD>Wer sind die potentiellen Kunden?</TD>
// <TD><Kfm Clients></Kfm></TD></TR>
// </TABLE>
// </FORM>
}