536cd135cc
Former-commit-id: 5624ac747d633e885131e8349322922b6a59baaa
671 lines
24 KiB
C#
671 lines
24 KiB
C#
//------------------------------------------------------------------------------
|
|
// <copyright file="SequentialOutput.cs" company="Microsoft">
|
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
// </copyright>
|
|
// <owner current="true" primary="true">Microsoft</owner>
|
|
//------------------------------------------------------------------------------
|
|
|
|
namespace System.Xml.Xsl.XsltOld {
|
|
using Res = System.Xml.Utils.Res;
|
|
using System;
|
|
using System.Diagnostics;
|
|
using System.Xml;
|
|
using System.Text;
|
|
using System.Collections;
|
|
using System.Globalization;
|
|
|
|
internal abstract class SequentialOutput : RecordOutput {
|
|
private const char s_Colon = ':';
|
|
private const char s_GreaterThan = '>';
|
|
private const char s_LessThan = '<';
|
|
private const char s_Space = ' ';
|
|
private const char s_Quote = '\"';
|
|
private const char s_Semicolon = ';';
|
|
private const char s_NewLine = '\n';
|
|
private const char s_Return = '\r';
|
|
private const char s_Ampersand = '&';
|
|
private const string s_LessThanQuestion = "<?";
|
|
private const string s_QuestionGreaterThan = "?>";
|
|
private const string s_LessThanSlash = "</";
|
|
private const string s_SlashGreaterThan = " />";
|
|
private const string s_EqualQuote = "=\"";
|
|
private const string s_DocType = "<!DOCTYPE ";
|
|
private const string s_CommentBegin = "<!--";
|
|
private const string s_CommentEnd = "-->";
|
|
private const string s_CDataBegin = "<![CDATA[";
|
|
private const string s_CDataEnd = "]]>";
|
|
private const string s_VersionAll = " version=\"1.0\"";
|
|
private const string s_Standalone = " standalone=\"";
|
|
private const string s_EncodingStart = " encoding=\"";
|
|
private const string s_Public = "PUBLIC ";
|
|
private const string s_System = "SYSTEM ";
|
|
private const string s_Html = "html";
|
|
private const string s_QuoteSpace = "\" ";
|
|
private const string s_CDataSplit = "]]]]><![CDATA[>";
|
|
|
|
private const string s_EnLessThan = "<";
|
|
private const string s_EnGreaterThan = ">";
|
|
private const string s_EnAmpersand = "&";
|
|
private const string s_EnQuote = """;
|
|
private const string s_EnNewLine = "
";
|
|
private const string s_EnReturn = "
";
|
|
|
|
private const string s_EndOfLine = "\r\n";
|
|
|
|
static char[] s_TextValueFind = new char[] {s_Ampersand, s_GreaterThan, s_LessThan};
|
|
static string[] s_TextValueReplace = new string[] {s_EnAmpersand , s_EnGreaterThan , s_EnLessThan };
|
|
|
|
static char[] s_XmlAttributeValueFind = new char[] {s_Ampersand, s_GreaterThan, s_LessThan, s_Quote, s_NewLine, s_Return};
|
|
static string[] s_XmlAttributeValueReplace = new string[] {s_EnAmpersand , s_EnGreaterThan , s_EnLessThan , s_EnQuote , s_EnNewLine , s_EnReturn };
|
|
|
|
// Instance members
|
|
private Processor processor;
|
|
protected Encoding encoding;
|
|
private ArrayList outputCache;
|
|
private bool firstLine = true;
|
|
private bool secondRoot;
|
|
|
|
// Cached Output propertes:
|
|
private XsltOutput output;
|
|
private bool isHtmlOutput;
|
|
private bool isXmlOutput;
|
|
private Hashtable cdataElements;
|
|
private bool indentOutput;
|
|
private bool outputDoctype;
|
|
private bool outputXmlDecl;
|
|
private bool omitXmlDeclCalled;
|
|
|
|
// Uri Escaping:
|
|
private byte[] byteBuffer;
|
|
private Encoding utf8Encoding;
|
|
|
|
XmlCharType xmlCharType = XmlCharType.Instance;
|
|
|
|
private void CacheOuptutProps(XsltOutput output) {
|
|
this.output = output;
|
|
this.isXmlOutput = this.output.Method == XsltOutput.OutputMethod.Xml;
|
|
this.isHtmlOutput = this.output.Method == XsltOutput.OutputMethod.Html;
|
|
this.cdataElements = this.output.CDataElements;
|
|
this.indentOutput = this.output.Indent;
|
|
this.outputDoctype = this.output.DoctypeSystem != null || (this.isHtmlOutput && this.output.DoctypePublic != null);
|
|
this.outputXmlDecl = this.isXmlOutput && ! this.output.OmitXmlDeclaration && ! this.omitXmlDeclCalled;
|
|
}
|
|
|
|
//
|
|
// Constructor
|
|
//
|
|
internal SequentialOutput(Processor processor) {
|
|
this.processor = processor;
|
|
CacheOuptutProps(processor.Output);
|
|
}
|
|
|
|
public void OmitXmlDecl() {
|
|
this.omitXmlDeclCalled = true;
|
|
this.outputXmlDecl = false;
|
|
}
|
|
|
|
//
|
|
// Particular outputs
|
|
//
|
|
void WriteStartElement(RecordBuilder record) {
|
|
Debug.Assert(record.MainNode.NodeType == XmlNodeType.Element);
|
|
BuilderInfo mainNode = record.MainNode;
|
|
HtmlElementProps htmlProps = null;
|
|
if (this.isHtmlOutput) {
|
|
if (mainNode.Prefix.Length == 0) {
|
|
htmlProps = mainNode.htmlProps;
|
|
if (htmlProps == null && mainNode.search) {
|
|
htmlProps = HtmlElementProps.GetProps(mainNode.LocalName);
|
|
}
|
|
record.Manager.CurrentElementScope.HtmlElementProps = htmlProps;
|
|
mainNode.IsEmptyTag = false;
|
|
}
|
|
}
|
|
else if (this.isXmlOutput) {
|
|
if (mainNode.Depth == 0) {
|
|
if(
|
|
secondRoot && (
|
|
output.DoctypeSystem != null ||
|
|
output.Standalone
|
|
)
|
|
) {
|
|
throw XsltException.Create(Res.Xslt_MultipleRoots);
|
|
}
|
|
secondRoot = true;
|
|
}
|
|
}
|
|
|
|
if (this.outputDoctype) {
|
|
WriteDoctype(mainNode);
|
|
this.outputDoctype = false;
|
|
}
|
|
|
|
if (this.cdataElements != null && this.cdataElements.Contains(new XmlQualifiedName(mainNode.LocalName, mainNode.NamespaceURI)) && this.isXmlOutput) {
|
|
record.Manager.CurrentElementScope.ToCData = true;
|
|
}
|
|
|
|
Indent(record);
|
|
Write(s_LessThan);
|
|
WriteName(mainNode.Prefix, mainNode.LocalName);
|
|
|
|
WriteAttributes(record.AttributeList, record.AttributeCount, htmlProps);
|
|
|
|
|
|
if (mainNode.IsEmptyTag) {
|
|
Debug.Assert(! this.isHtmlOutput || mainNode.Prefix != null, "Html can't have abreviated elements");
|
|
Write(s_SlashGreaterThan);
|
|
}
|
|
else {
|
|
Write(s_GreaterThan);
|
|
}
|
|
|
|
if(htmlProps != null && htmlProps.Head) {
|
|
mainNode.Depth ++;
|
|
Indent(record);
|
|
mainNode.Depth --;
|
|
Write("<META http-equiv=\"Content-Type\" content=\"");
|
|
Write(this.output.MediaType);
|
|
Write("; charset=");
|
|
Write(this.encoding.WebName);
|
|
Write("\">");
|
|
}
|
|
}
|
|
|
|
void WriteTextNode(RecordBuilder record) {
|
|
BuilderInfo mainNode = record.MainNode;
|
|
OutputScope scope = record.Manager.CurrentElementScope;
|
|
|
|
scope.Mixed = true;
|
|
|
|
if(scope.HtmlElementProps != null && scope.HtmlElementProps.NoEntities) {
|
|
// script or stile
|
|
Write(mainNode.Value);
|
|
}
|
|
else if (scope.ToCData) {
|
|
WriteCDataSection(mainNode.Value);
|
|
}
|
|
else {
|
|
WriteTextNode(mainNode);
|
|
}
|
|
}
|
|
|
|
void WriteTextNode(BuilderInfo node) {
|
|
for (int i = 0; i < node.TextInfoCount; i ++) {
|
|
string text = node.TextInfo[i];
|
|
if (text == null) { // disableEscaping marker
|
|
i++;
|
|
Debug.Assert(i < node.TextInfoCount, "disableEscaping marker can't be last TextInfo record");
|
|
Write(node.TextInfo[i]);
|
|
} else {
|
|
WriteWithReplace(text, s_TextValueFind, s_TextValueReplace);
|
|
}
|
|
}
|
|
}
|
|
|
|
void WriteCDataSection(string value) {
|
|
Write(s_CDataBegin);
|
|
WriteCData(value);
|
|
Write(s_CDataEnd);
|
|
}
|
|
|
|
void WriteDoctype(BuilderInfo mainNode) {
|
|
Debug.Assert(this.outputDoctype == true, "It supposed to check this condition before actual call");
|
|
Debug.Assert(this.output.DoctypeSystem != null || (this.isHtmlOutput && this.output.DoctypePublic != null), "We set outputDoctype == true only if");
|
|
Indent(0);
|
|
Write(s_DocType);
|
|
if (this.isXmlOutput) {
|
|
WriteName(mainNode.Prefix, mainNode.LocalName);
|
|
}
|
|
else {
|
|
WriteName(string.Empty, "html");
|
|
}
|
|
Write(s_Space);
|
|
if (output.DoctypePublic != null) {
|
|
Write(s_Public);
|
|
Write(s_Quote);
|
|
Write(output.DoctypePublic);
|
|
Write(s_QuoteSpace);
|
|
}
|
|
else {
|
|
Write(s_System);
|
|
}
|
|
if (output.DoctypeSystem != null) {
|
|
Write(s_Quote);
|
|
Write(output.DoctypeSystem);
|
|
Write(s_Quote);
|
|
}
|
|
Write(s_GreaterThan);
|
|
}
|
|
|
|
void WriteXmlDeclaration() {
|
|
Debug.Assert(this.outputXmlDecl == true, "It supposed to check this condition before actual call");
|
|
Debug.Assert(this.isXmlOutput && ! this.output.OmitXmlDeclaration, "We set outputXmlDecl == true only if");
|
|
this.outputXmlDecl = false;
|
|
|
|
Indent(0);
|
|
Write(s_LessThanQuestion);
|
|
WriteName(string.Empty, "xml");
|
|
Write(s_VersionAll);
|
|
if (this.encoding != null) {
|
|
Write(s_EncodingStart);
|
|
Write(this.encoding.WebName);
|
|
Write(s_Quote);
|
|
}
|
|
if (output.HasStandalone) {
|
|
Write(s_Standalone);
|
|
Write(output.Standalone ? "yes" : "no");
|
|
Write(s_Quote);
|
|
}
|
|
Write(s_QuestionGreaterThan);
|
|
}
|
|
|
|
void WriteProcessingInstruction(RecordBuilder record) {
|
|
Indent(record);
|
|
WriteProcessingInstruction(record.MainNode);
|
|
}
|
|
|
|
void WriteProcessingInstruction(BuilderInfo node) {
|
|
Write(s_LessThanQuestion);
|
|
WriteName(node.Prefix, node.LocalName);
|
|
Write(s_Space);
|
|
Write(node.Value);
|
|
|
|
if(this.isHtmlOutput) {
|
|
Write(s_GreaterThan);
|
|
}
|
|
else {
|
|
Write(s_QuestionGreaterThan);
|
|
}
|
|
}
|
|
|
|
void WriteEndElement(RecordBuilder record) {
|
|
BuilderInfo node = record.MainNode;
|
|
HtmlElementProps htmlProps = record.Manager.CurrentElementScope.HtmlElementProps;
|
|
|
|
if(htmlProps != null && htmlProps.Empty) {
|
|
return;
|
|
}
|
|
|
|
Indent(record);
|
|
Write(s_LessThanSlash);
|
|
WriteName(record.MainNode.Prefix, record.MainNode.LocalName);
|
|
Write(s_GreaterThan);
|
|
}
|
|
|
|
//
|
|
// RecordOutput interface method implementation
|
|
//
|
|
|
|
public Processor.OutputResult RecordDone(RecordBuilder record) {
|
|
if (output.Method == XsltOutput.OutputMethod.Unknown) {
|
|
if (! DecideDefaultOutput(record.MainNode)) {
|
|
CacheRecord(record);
|
|
}
|
|
else {
|
|
OutputCachedRecords();
|
|
OutputRecord(record);
|
|
}
|
|
}
|
|
else {
|
|
OutputRecord(record);
|
|
}
|
|
|
|
record.Reset();
|
|
return Processor.OutputResult.Continue;
|
|
}
|
|
|
|
public void TheEnd() {
|
|
OutputCachedRecords();
|
|
Close();
|
|
}
|
|
|
|
private bool DecideDefaultOutput(BuilderInfo node) {
|
|
XsltOutput.OutputMethod method = XsltOutput.OutputMethod.Xml;
|
|
switch (node.NodeType) {
|
|
case XmlNodeType.Element:
|
|
if (node.NamespaceURI.Length == 0 && String.Compare("html", node.LocalName, StringComparison.OrdinalIgnoreCase) == 0) {
|
|
method = XsltOutput.OutputMethod.Html;
|
|
}
|
|
break;
|
|
case XmlNodeType.Text:
|
|
case XmlNodeType.Whitespace:
|
|
case XmlNodeType.SignificantWhitespace:
|
|
if (xmlCharType.IsOnlyWhitespace(node.Value)) {
|
|
return false;
|
|
}
|
|
method = XsltOutput.OutputMethod.Xml;
|
|
break;
|
|
default :
|
|
return false;
|
|
}
|
|
if(this.processor.SetDefaultOutput(method)) {
|
|
CacheOuptutProps(processor.Output);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private void CacheRecord(RecordBuilder record) {
|
|
if (this.outputCache == null) {
|
|
this.outputCache = new ArrayList();
|
|
}
|
|
|
|
this.outputCache.Add(record.MainNode.Clone());
|
|
}
|
|
|
|
private void OutputCachedRecords() {
|
|
if (this.outputCache == null) {
|
|
return;
|
|
}
|
|
|
|
for(int record = 0; record < this.outputCache.Count; record ++) {
|
|
Debug.Assert(this.outputCache[record] is BuilderInfo);
|
|
BuilderInfo info = (BuilderInfo) this.outputCache[record];
|
|
|
|
OutputRecord(info);
|
|
}
|
|
|
|
this.outputCache = null;
|
|
}
|
|
|
|
private void OutputRecord(RecordBuilder record) {
|
|
BuilderInfo mainNode = record.MainNode;
|
|
|
|
if(this.outputXmlDecl) {
|
|
WriteXmlDeclaration();
|
|
}
|
|
|
|
switch (mainNode.NodeType) {
|
|
case XmlNodeType.Element:
|
|
WriteStartElement(record);
|
|
break;
|
|
case XmlNodeType.Text:
|
|
case XmlNodeType.Whitespace:
|
|
case XmlNodeType.SignificantWhitespace:
|
|
WriteTextNode(record);
|
|
break;
|
|
case XmlNodeType.CDATA:
|
|
Debug.Fail("Should never get here");
|
|
break;
|
|
case XmlNodeType.EntityReference:
|
|
Write(s_Ampersand);
|
|
WriteName(mainNode.Prefix, mainNode.LocalName);
|
|
Write(s_Semicolon);
|
|
break;
|
|
case XmlNodeType.ProcessingInstruction:
|
|
WriteProcessingInstruction(record);
|
|
break;
|
|
case XmlNodeType.Comment:
|
|
Indent(record);
|
|
Write(s_CommentBegin);
|
|
Write(mainNode.Value);
|
|
Write(s_CommentEnd);
|
|
break;
|
|
case XmlNodeType.Document:
|
|
break;
|
|
case XmlNodeType.DocumentType:
|
|
Write(mainNode.Value);
|
|
break;
|
|
case XmlNodeType.EndElement:
|
|
WriteEndElement(record);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void OutputRecord(BuilderInfo node) {
|
|
if(this.outputXmlDecl) {
|
|
WriteXmlDeclaration();
|
|
}
|
|
|
|
Indent(0); // we can have only top level stuff here
|
|
|
|
switch (node.NodeType) {
|
|
case XmlNodeType.Element:
|
|
Debug.Fail("Should never get here");
|
|
break;
|
|
case XmlNodeType.Text:
|
|
case XmlNodeType.Whitespace:
|
|
case XmlNodeType.SignificantWhitespace:
|
|
WriteTextNode(node);
|
|
break;
|
|
case XmlNodeType.CDATA:
|
|
Debug.Fail("Should never get here");
|
|
break;
|
|
case XmlNodeType.EntityReference:
|
|
Write(s_Ampersand);
|
|
WriteName(node.Prefix, node.LocalName);
|
|
Write(s_Semicolon);
|
|
break;
|
|
case XmlNodeType.ProcessingInstruction:
|
|
WriteProcessingInstruction(node);
|
|
break;
|
|
case XmlNodeType.Comment:
|
|
Write(s_CommentBegin);
|
|
Write(node.Value);
|
|
Write(s_CommentEnd);
|
|
break;
|
|
case XmlNodeType.Document:
|
|
break;
|
|
case XmlNodeType.DocumentType:
|
|
Write(node.Value);
|
|
break;
|
|
case XmlNodeType.EndElement:
|
|
Debug.Fail("Should never get here");
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Internal helpers
|
|
//
|
|
|
|
private void WriteName(string prefix, string name) {
|
|
if (prefix != null && prefix.Length > 0) {
|
|
Write(prefix);
|
|
if (name != null && name.Length > 0) {
|
|
Write(s_Colon);
|
|
}
|
|
else {
|
|
return;
|
|
}
|
|
}
|
|
Write(name);
|
|
}
|
|
|
|
private void WriteXmlAttributeValue(string value) {
|
|
Debug.Assert(value != null);
|
|
WriteWithReplace(value, s_XmlAttributeValueFind, s_XmlAttributeValueReplace);
|
|
}
|
|
|
|
private void WriteHtmlAttributeValue(string value) {
|
|
Debug.Assert(value != null);
|
|
|
|
int length = value.Length;
|
|
int i = 0;
|
|
while(i < length) {
|
|
char ch = value[i];
|
|
i ++;
|
|
switch (ch) {
|
|
case '&':
|
|
if(i != length && value[i] == '{') { // &{ hasn't to be encoded in HTML output.
|
|
Write(ch);
|
|
}
|
|
else {
|
|
Write(s_EnAmpersand);
|
|
}
|
|
break;
|
|
case '"':
|
|
Write(s_EnQuote);
|
|
break;
|
|
default:
|
|
Write(ch);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void WriteHtmlUri(string value) {
|
|
Debug.Assert(value != null);
|
|
Debug.Assert(this.isHtmlOutput);
|
|
|
|
int length = value.Length;
|
|
int i = 0;
|
|
while(i < length) {
|
|
char ch = value[i];
|
|
i ++;
|
|
switch (ch) {
|
|
case '&':
|
|
if(i != length && value[i] == '{') { // &{ hasn't to be encoded in HTML output.
|
|
Write(ch);
|
|
}
|
|
else {
|
|
Write(s_EnAmpersand);
|
|
}
|
|
break;
|
|
case '"':
|
|
Write(s_EnQuote);
|
|
break;
|
|
case '\n':
|
|
Write(s_EnNewLine);
|
|
break;
|
|
case '\r':
|
|
Write(s_EnReturn);
|
|
break;
|
|
default:
|
|
if(127 < ch) {
|
|
if (this.utf8Encoding == null) {
|
|
this.utf8Encoding = Encoding.UTF8;
|
|
this.byteBuffer = new byte[utf8Encoding.GetMaxByteCount(1)];
|
|
}
|
|
int bytes = this.utf8Encoding.GetBytes(value, i - 1, 1, this.byteBuffer, 0);
|
|
for(int j = 0; j < bytes; j ++) {
|
|
Write("%");
|
|
Write(((uint)this.byteBuffer[j]).ToString("X2", CultureInfo.InvariantCulture));
|
|
}
|
|
}
|
|
else {
|
|
Write(ch);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void WriteWithReplace(string value, char[] find, string[] replace) {
|
|
Debug.Assert(value != null);
|
|
Debug.Assert(find.Length == replace.Length);
|
|
|
|
int length = value.Length;
|
|
int pos = 0;
|
|
|
|
while(pos < length) {
|
|
int newPos = value.IndexOfAny(find, pos);
|
|
if (newPos == -1) {
|
|
break; // not found;
|
|
}
|
|
// output clean leading part of the string
|
|
while (pos < newPos) {
|
|
Write(value[pos]);
|
|
pos ++;
|
|
}
|
|
// output replacement
|
|
char badChar = value[pos];
|
|
int i;
|
|
for(i = find.Length - 1; 0 <= i; i --) {
|
|
if(find[i] == badChar) {
|
|
Write(replace[i]);
|
|
break;
|
|
}
|
|
}
|
|
Debug.Assert(0 <= i, "find char wasn't realy find");
|
|
pos ++;
|
|
}
|
|
|
|
// output rest of the string
|
|
if(pos == 0) {
|
|
Write(value);
|
|
}
|
|
else {
|
|
while(pos < length) {
|
|
Write(value[pos]);
|
|
pos ++;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void WriteCData(string value) {
|
|
Debug.Assert(value != null);
|
|
Write(value.Replace(s_CDataEnd, s_CDataSplit));
|
|
}
|
|
|
|
private void WriteAttributes(ArrayList list, int count, HtmlElementProps htmlElementsProps) {
|
|
Debug.Assert(count <= list.Count);
|
|
for (int attrib = 0; attrib < count; attrib ++) {
|
|
Debug.Assert(list[attrib] is BuilderInfo);
|
|
BuilderInfo attribute = (BuilderInfo) list[attrib];
|
|
string attrValue = attribute.Value;
|
|
bool abr = false, uri = false; {
|
|
if(htmlElementsProps != null && attribute.Prefix.Length == 0) {
|
|
HtmlAttributeProps htmlAttrProps = attribute.htmlAttrProps;
|
|
if (htmlAttrProps == null && attribute.search) {
|
|
htmlAttrProps = HtmlAttributeProps.GetProps(attribute.LocalName);
|
|
}
|
|
if(htmlAttrProps != null) {
|
|
abr = htmlElementsProps.AbrParent && htmlAttrProps.Abr;
|
|
uri = htmlElementsProps.UriParent && ( htmlAttrProps.Uri ||
|
|
htmlElementsProps.NameParent && htmlAttrProps.Name
|
|
);
|
|
}
|
|
}
|
|
}
|
|
Write(s_Space);
|
|
WriteName(attribute.Prefix, attribute.LocalName);
|
|
if(abr && 0 == string.Compare(attribute.LocalName, attrValue, StringComparison.OrdinalIgnoreCase) ) {
|
|
// Since the name of the attribute = the value of the attribute,
|
|
// this is a boolean attribute whose value should be suppressed
|
|
continue;
|
|
}
|
|
Write(s_EqualQuote);
|
|
if(uri) {
|
|
WriteHtmlUri(attrValue);
|
|
}
|
|
else if(this.isHtmlOutput) {
|
|
WriteHtmlAttributeValue(attrValue);
|
|
}
|
|
else {
|
|
WriteXmlAttributeValue(attrValue);
|
|
}
|
|
Write(s_Quote);
|
|
}
|
|
}
|
|
|
|
void Indent(RecordBuilder record) {
|
|
if (! record.Manager.CurrentElementScope.Mixed) {
|
|
Indent(record.MainNode.Depth);
|
|
}
|
|
}
|
|
|
|
void Indent(int depth) {
|
|
if(this.firstLine) {
|
|
if (this.indentOutput) {
|
|
this.firstLine = false;
|
|
}
|
|
return; // preven leading CRLF
|
|
}
|
|
Write(s_EndOfLine);
|
|
for (int i = 2 * depth; 0 < i; i--) {
|
|
Write(" ");
|
|
}
|
|
}
|
|
|
|
//
|
|
// Abstract methods
|
|
internal abstract void Write(char outputChar);
|
|
internal abstract void Write(string outputText);
|
|
internal abstract void Close();
|
|
}
|
|
}
|