|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text;
namespace System.Xml.Xsl.XsltOld
{
internal abstract class SequentialOutput : IRecordOutput
{
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_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";
private static readonly char[] s_TextValueFind = new char[] { s_Ampersand, s_GreaterThan, s_LessThan };
private static readonly string[] s_TextValueReplace = new string[] { s_EnAmpersand, s_EnGreaterThan, s_EnLessThan };
private static readonly char[] s_XmlAttributeValueFind = new char[] { s_Ampersand, s_GreaterThan, s_LessThan, s_Quote, s_NewLine, s_Return };
private static readonly string[] s_XmlAttributeValueReplace = new string[] { s_EnAmpersand, s_EnGreaterThan, s_EnLessThan, s_EnQuote, s_EnNewLine, s_EnReturn };
// Instance members
private readonly Processor _processor;
protected Encoding? encoding;
private ArrayList? _outputCache;
private bool _firstLine = true;
private bool _secondRoot;
// Cached Output properties:
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;
[MemberNotNull(nameof(_output))]
private void CacheOuptutProps(XsltOutput output)
{
_output = output;
_isXmlOutput = _output.Method == XsltOutput.OutputMethod.Xml;
_isHtmlOutput = _output.Method == XsltOutput.OutputMethod.Html;
_cdataElements = _output.CDataElements;
_indentOutput = _output.Indent;
_outputDoctype = _output.DoctypeSystem != null || (_isHtmlOutput && _output.DoctypePublic != null);
_outputXmlDecl = _isXmlOutput && !_output.OmitXmlDeclaration && !_omitXmlDeclCalled;
}
//
// Constructor
//
internal SequentialOutput(Processor processor)
{
_processor = processor;
CacheOuptutProps(processor.Output);
}
public void OmitXmlDecl()
{
_omitXmlDeclCalled = true;
_outputXmlDecl = false;
}
//
// Particular outputs
//
private void WriteStartElement(RecordBuilder record)
{
Debug.Assert(record.MainNode.NodeType == XmlNodeType.Element);
BuilderInfo mainNode = record.MainNode;
HtmlElementProps? htmlProps = null;
if (_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 (_isXmlOutput)
{
if (mainNode.Depth == 0)
{
if (
_secondRoot && (
_output.DoctypeSystem != null ||
_output.Standalone
)
)
{
throw XsltException.Create(SR.Xslt_MultipleRoots);
}
_secondRoot = true;
}
}
if (_outputDoctype)
{
WriteDoctype(mainNode);
_outputDoctype = false;
}
if (_cdataElements != null && _cdataElements.Contains(new XmlQualifiedName(mainNode.LocalName, mainNode.NamespaceURI)) && _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(!_isHtmlOutput || mainNode.Prefix != null, "Html can't have abbreviated 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(_output.MediaType);
Write("; charset=");
Write(this.encoding!.WebName);
Write("\">");
}
}
private 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);
}
}
private 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);
}
}
}
private void WriteCDataSection(string value)
{
Write(s_CDataBegin);
WriteCData(value);
Write(s_CDataEnd);
}
private void WriteDoctype(BuilderInfo mainNode)
{
Debug.Assert(_outputDoctype, "It supposed to check this condition before actual call");
Debug.Assert(_output.DoctypeSystem != null || (_isHtmlOutput && _output.DoctypePublic != null), "We set outputDoctype == true only if");
Indent(0);
Write(s_DocType);
if (_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);
}
private void WriteXmlDeclaration()
{
Debug.Assert(_outputXmlDecl, "It supposed to check this condition before actual call");
Debug.Assert(_isXmlOutput && !_output.OmitXmlDeclaration, "We set outputXmlDecl == true only if");
_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);
}
private void WriteProcessingInstruction(RecordBuilder record)
{
Indent(record);
WriteProcessingInstruction(record.MainNode);
}
private void WriteProcessingInstruction(BuilderInfo node)
{
Write(s_LessThanQuestion);
WriteName(node.Prefix, node.LocalName);
Write(s_Space);
Write(node.Value);
if (_isHtmlOutput)
{
Write(s_GreaterThan);
}
else
{
Write(s_QuestionGreaterThan);
}
}
private void WriteEndElement(RecordBuilder record)
{
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.Equals("html", node.LocalName, StringComparison.OrdinalIgnoreCase))
{
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 (_processor.SetDefaultOutput(method))
{
CacheOuptutProps(_processor.Output);
}
return true;
}
private void CacheRecord(RecordBuilder record)
{
_outputCache ??= new ArrayList();
_outputCache.Add(record.MainNode.Clone());
}
private void OutputCachedRecords()
{
if (_outputCache == null)
{
return;
}
for (int record = 0; record < _outputCache.Count; record++)
{
Debug.Assert(_outputCache[record] is BuilderInfo);
BuilderInfo info = (BuilderInfo)_outputCache[record]!;
OutputRecord(info);
}
_outputCache = null;
}
private void OutputRecord(RecordBuilder record)
{
BuilderInfo mainNode = record.MainNode;
if (_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 (_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(_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 (_utf8Encoding == null)
{
_utf8Encoding = Encoding.UTF8;
_byteBuffer = new byte[_utf8Encoding.GetMaxByteCount(1)];
}
int bytes = _utf8Encoding.GetBytes(value, i - 1, 1, _byteBuffer!, 0);
for (int j = 0; j < bytes; j++)
{
Write("%");
Write(((uint)_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 really 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 && string.Equals(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 (_isHtmlOutput)
{
WriteHtmlAttributeValue(attrValue);
}
else
{
WriteXmlAttributeValue(attrValue);
}
Write(s_Quote);
}
}
private void Indent(RecordBuilder record)
{
if (!record.Manager.CurrentElementScope.Mixed)
{
Indent(record.MainNode.Depth);
}
}
private void Indent(int depth)
{
if (_firstLine)
{
if (_indentOutput)
{
_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();
}
}
|