/********************************************************************++
Copyright (c) Microsoft Corporation. All rights reserved.
--********************************************************************/
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Text;
using System.Globalization;
using System.Management.Automation.Internal;
namespace Microsoft.PowerShell.Commands.Internal.Format
{
///
/// writer class to handle Complex Object formatting
///
internal sealed class ComplexWriter
{
///
/// initialization method to be called before any other operation
///
/// LineOutput interfaces to write to
/// number of columns used to write out
internal void Initialize(LineOutput lineOutput, int numberOfTextColumns)
{
_lo = lineOutput;
_textColumns = numberOfTextColumns;
}
///
/// Writes a string
///
///
internal void WriteString(string s)
{
_indentationManager.Clear();
AddToBuffer(s);
WriteToScreen();
}
///
/// it interprets a list of format value tokens and outputs it
///
/// list of FormatValue tokens to interpret
internal void WriteObject(List formatValueList)
{
// we always start with no indentation
_indentationManager.Clear();
foreach (FormatEntry fe in formatValueList)
{
// operate on each directive inside the list,
// carrying the indentation from invocation to invocation
GenerateFormatEntryDisplay(fe, 0);
}
// make sure that, if we have pending text in the buffer it gets flushed
WriteToScreen();
}
///
/// operate on a single entry
///
/// entry to process
/// current depth of recursion
private void GenerateFormatEntryDisplay(FormatEntry fe, int currentDepth)
{
foreach (object obj in fe.formatValueList)
{
FormatEntry feChild = obj as FormatEntry;
if (feChild != null)
{
if (currentDepth < maxRecursionDepth)
{
if (feChild.frameInfo != null)
{
// if we have frame information, we need to push it on the
// indentation stack
using (_indentationManager.StackFrame(feChild.frameInfo))
{
GenerateFormatEntryDisplay(feChild, currentDepth + 1);
}
}
else
{
// no need here of activating an indentation stack frame
GenerateFormatEntryDisplay(feChild, currentDepth + 1);
}
}
continue;
}
if (obj is FormatNewLine)
{
this.WriteToScreen();
continue;
}
FormatTextField ftf = obj as FormatTextField;
if (ftf != null)
{
this.AddToBuffer(ftf.text);
continue;
}
FormatPropertyField fpf = obj as FormatPropertyField;
if (fpf != null)
{
this.AddToBuffer(fpf.propertyValue);
}
}
}
///
/// add a string to the current buffer, waiting for a FlushBuffer()
///
/// string to add to buffer
private void AddToBuffer(string s)
{
_stringBuffer.Append(s);
}
///
/// write to the output interface
///
private void WriteToScreen()
{
int leftIndentation = _indentationManager.LeftIndentation;
int rightIndentation = _indentationManager.RightIndentation;
int firstLineIndentation = _indentationManager.FirstLineIndentation;
// VALIDITY CHECKS:
// check the useful ("active") width
int usefulWidth = _textColumns - rightIndentation - leftIndentation;
if (usefulWidth <= 0)
{
// fatal error, there is nothing to write to the device
// just clear the buffer and return
_stringBuffer = new StringBuilder();
}
// check indentation or hanging is not larger than the active width
int indentationAbsoluteValue = (firstLineIndentation > 0) ? firstLineIndentation : -firstLineIndentation;
if (indentationAbsoluteValue >= usefulWidth)
{
// valu too big, we reset it to zero
firstLineIndentation = 0;
}
// compute the first line indentation or hanging
int firstLineWidth = _textColumns - rightIndentation - leftIndentation;
int followingLinesWidth = firstLineWidth;
if (firstLineIndentation >= 0)
{
// the first line has an indentation
firstLineWidth -= firstLineIndentation;
}
else
{
// the first line is hanging
followingLinesWidth += firstLineIndentation;
}
//error checking on invalid values
// generate the lines using the computed widths
StringCollection sc = StringManipulationHelper.GenerateLines(_lo.DisplayCells, _stringBuffer.ToString(),
firstLineWidth, followingLinesWidth);
// compute padding
int firstLinePadding = leftIndentation;
int followingLinesPadding = leftIndentation;
if (firstLineIndentation >= 0)
{
// the first line has an indentation
firstLinePadding += firstLineIndentation;
}
else
{
// the first line is hanging
followingLinesPadding -= firstLineIndentation;
}
// now write the lines on the screen
bool firstLine = true;
foreach (string s in sc)
{
if (firstLine)
{
firstLine = false;
_lo.WriteLine(StringManipulationHelper.PadLeft(s, firstLinePadding));
}
else
{
_lo.WriteLine(StringManipulationHelper.PadLeft(s, followingLinesPadding));
}
}
_stringBuffer = new StringBuilder();
}
///
/// helper object to manage the frame-based indentation and margins
///
private IndentationManager _indentationManager = new IndentationManager();
///
/// buffer to accumulate partially constructed text
///
private StringBuilder _stringBuffer = new StringBuilder();
///
/// interface to write to
///
private LineOutput _lo;
///
/// number of columns for the output device
///
private int _textColumns;
private const int maxRecursionDepth = 50;
}
internal sealed class IndentationManager
{
private sealed class IndentationStackFrame : IDisposable
{
internal IndentationStackFrame(IndentationManager mgr)
{
_mgr = mgr;
}
public void Dispose()
{
if (_mgr != null)
{
_mgr.RemoveStackFrame();
}
}
private IndentationManager _mgr;
}
internal void Clear()
{
_frameInfoStack.Clear();
}
internal IDisposable StackFrame(FrameInfo frameInfo)
{
IndentationStackFrame frame = new IndentationStackFrame(this);
_frameInfoStack.Push(frameInfo);
return frame;
}
private void RemoveStackFrame()
{
_frameInfoStack.Pop();
}
internal int RightIndentation
{
get
{
return ComputeRightIndentation();
}
}
internal int LeftIndentation
{
get
{
return ComputeLeftIndentation();
}
}
internal int FirstLineIndentation
{
get
{
if (_frameInfoStack.Count == 0)
return 0;
return _frameInfoStack.Peek().firstLine;
}
}
private int ComputeRightIndentation()
{
int val = 0;
foreach (FrameInfo fi in _frameInfoStack)
{
val += fi.rightIndentation;
}
return val;
}
private int ComputeLeftIndentation()
{
int val = 0;
foreach (FrameInfo fi in _frameInfoStack)
{
val += fi.leftIndentation;
}
return val;
}
private Stack _frameInfoStack = new Stack();
}
///
/// Result of GetWords
///
internal struct GetWordsResult
{
internal string Word;
internal string Delim;
}
///
/// collection of helper functions for string formatting
///
internal sealed class StringManipulationHelper
{
private static readonly char s_softHyphen = '\u00AD';
private static readonly char s_hardHyphen = '\u2011';
private static readonly char s_nonBreakingSpace = '\u00A0';
private static Collection s_cultureCollection = new Collection();
static StringManipulationHelper()
{
s_cultureCollection.Add("en"); // English
s_cultureCollection.Add("fr"); // French
s_cultureCollection.Add("de"); // German
s_cultureCollection.Add("it"); // Italian
s_cultureCollection.Add("pt"); // Portuguese
s_cultureCollection.Add("es"); // Spanish
}
///
/// Breaks a string into a collection of words
/// TODO: we might be able to improve this function in the future
/// so that we do not break paths etc.
///
/// input string
/// a collection of words
private static IEnumerable GetWords(string s)
{
StringBuilder sb = new StringBuilder();
GetWordsResult result = new GetWordsResult();
for (int i = 0; i < s.Length; i++)
{
// Soft hyphen = \u00AD - Should break, and add a hyphen if needed. If not needed for a break, hyphen should be absent
if (s[i] == ' ' || s[i] == '\t' || s[i] == s_softHyphen)
{
result.Word = sb.ToString();
sb.Clear();
result.Delim = new String(s[i], 1);
yield return result;
}
// Non-breaking space = \u00A0 - ideally shouldn't wrap
// Hard hyphen = \u2011 - Should not break
else if (s[i] == s_hardHyphen || s[i] == s_nonBreakingSpace)
{
result.Word = sb.ToString();
sb.Clear();
result.Delim = String.Empty;
yield return result;
}
else
{
sb.Append(s[i]);
}
}
result.Word = sb.ToString();
result.Delim = String.Empty;
yield return result;
}
internal static StringCollection GenerateLines(DisplayCells displayCells, string val, int firstLineLen, int followingLinesLen)
{
if (s_cultureCollection.Contains(CultureInfo.CurrentCulture.TwoLetterISOLanguageName))
{
return GenerateLinesWithWordWrap(displayCells, val, firstLineLen, followingLinesLen);
}
else
{
return GenerateLinesWithoutWordWrap(displayCells, val, firstLineLen, followingLinesLen);
}
}
private static StringCollection GenerateLinesWithoutWordWrap(DisplayCells displayCells, string val, int firstLineLen, int followingLinesLen)
{
StringCollection retVal = new StringCollection();
if (string.IsNullOrEmpty(val))
{
// if null or empty, just add and we are done
retVal.Add(val);
return retVal;
}
// break string on newlines and process each line separately
string[] lines = SplitLines(val);
for (int k = 0; k < lines.Length; k++)
{
if (lines[k] == null || displayCells.Length(lines[k]) <= firstLineLen)
{
// we do not need to split further, just add
retVal.Add(lines[k]);
continue;
}
// the string does not fit, so we have to wrap around on multiple lines
// for each of these lines in the string, the first line will have
// a (potentially) different length (indentation or hanging)
// for each line, start a new state
SplitLinesAccumulator accumulator = new SplitLinesAccumulator(retVal, firstLineLen, followingLinesLen);
int offset = 0; // offset into the line we are splitting
while (true)
{
// acquire the current active display line length (it can very from call to call)
int currentDisplayLen = accumulator.ActiveLen;
// determine if the current tail would fit or not
// for the remaining part of the string, determine its display cell count
int currentCellsToFit = displayCells.Length(lines[k], offset);
// determine if we fit into the line
int excessCells = currentCellsToFit - currentDisplayLen;
if (excessCells > 0)
{
// we are not at the end of the string, select a sub string
// that would fit in the remaining display length
int charactersToAdd = displayCells.GetHeadSplitLength(lines[k], offset, currentDisplayLen);
if (charactersToAdd <= 0)
{
// corner case: we have a two cell character and the current
// display length is one.
// add a single cell arbitrary character instead of the original
// one and keep going
charactersToAdd = 1;
accumulator.AddLine("?");
}
else
{
// of the given length, add it to the accumulator
accumulator.AddLine(lines[k].Substring(offset, charactersToAdd));
}
// increase the offset by the # of characters added
offset += charactersToAdd;
}
else
{
// we reached the last (partial) line, we add it all
accumulator.AddLine(lines[k].Substring(offset));
break;
}
}
}
return retVal;
}
private sealed class SplitLinesAccumulator
{
internal SplitLinesAccumulator(StringCollection retVal, int firstLineLen, int followingLinesLen)
{
_retVal = retVal;
_firstLineLen = firstLineLen;
_followingLinesLen = followingLinesLen;
}
internal void AddLine(string s)
{
if (!_addedFirstLine)
{
_addedFirstLine = true;
}
_retVal.Add(s);
}
internal int ActiveLen
{
get
{
if (_addedFirstLine)
return _followingLinesLen;
return _firstLineLen;
}
}
private StringCollection _retVal;
private bool _addedFirstLine;
private int _firstLineLen;
private int _followingLinesLen;
}
private static StringCollection GenerateLinesWithWordWrap(DisplayCells displayCells, string val, int firstLineLen, int followingLinesLen)
{
StringCollection retVal = new StringCollection();
if (string.IsNullOrEmpty(val))
{
// if null or empty, just add and we are done
retVal.Add(val);
return retVal;
}
// break string on newlines and process each line separately
string[] lines = SplitLines(val);
for (int k = 0; k < lines.Length; k++)
{
if (lines[k] == null || displayCells.Length(lines[k]) <= firstLineLen)
{
// we do not need to split further, just add
retVal.Add(lines[k]);
continue;
}
int spacesLeft = firstLineLen;
int lineWidth = firstLineLen;
bool firstLine = true;
StringBuilder singleLine = new StringBuilder();
foreach (GetWordsResult word in GetWords(lines[k]))
{
string wordToAdd = word.Word;
// Handle soft hyphen
if (word.Delim == s_softHyphen.ToString())
{
int wordWidthWithHyphen = displayCells.Length(wordToAdd) + displayCells.Length(s_softHyphen.ToString());
// Add hyphen only if necessary
if (wordWidthWithHyphen == spacesLeft)
{
wordToAdd += "-";
}
}
else
{
if (!String.IsNullOrEmpty(word.Delim))
{
wordToAdd += word.Delim;
}
}
int wordWidth = displayCells.Length(wordToAdd);
// Handle zero width
if (lineWidth == 0)
{
if (firstLine)
{
firstLine = false;
lineWidth = followingLinesLen;
}
if (lineWidth == 0)
{
break;
}
spacesLeft = lineWidth;
}
// Word is wider than a single line
if (wordWidth > lineWidth)
{
foreach (char c in wordToAdd)
{
char charToAdd = c;
int charWidth = displayCells.Length(c);
// corner case: we have a two cell character and the current
// display length is one.
// add a single cell arbitrary character instead of the original
// one and keep going
if (charWidth > lineWidth)
{
charToAdd = '?';
charWidth = 1;
}
if (charWidth > spacesLeft)
{
retVal.Add(singleLine.ToString());
singleLine.Clear();
singleLine.Append(charToAdd);
if (firstLine)
{
firstLine = false;
lineWidth = followingLinesLen;
}
spacesLeft = lineWidth - charWidth;
}
else
{
singleLine.Append(charToAdd);
spacesLeft -= charWidth;
}
}
}
else
{
if (wordWidth > spacesLeft)
{
retVal.Add(singleLine.ToString());
singleLine.Clear();
singleLine.Append(wordToAdd);
if (firstLine)
{
firstLine = false;
lineWidth = followingLinesLen;
}
spacesLeft = lineWidth - wordWidth;
}
else
{
singleLine.Append(wordToAdd);
spacesLeft -= wordWidth;
}
}
}
retVal.Add(singleLine.ToString());
}
return retVal;
}
///
/// split a multiline string into an array of strings
/// by honoring both \n and \r\n
///
/// string to split
/// string array with the values
internal static string[] SplitLines(string s)
{
if (string.IsNullOrEmpty(s))
return new string[1] { s };
StringBuilder sb = new StringBuilder();
foreach (char c in s)
{
if (c != '\r')
sb.Append(c);
}
return sb.ToString().Split(s_newLineChar);
}
#if false
internal static string StripNewLines (string s)
{
if (string.IsNullOrEmpty (s))
return s;
string[] lines = SplitLines (s);
if (lines.Length == 0)
return null;
if (lines.Length == 1)
return lines[0];
StringBuilder sb = new StringBuilder ();
for (int k = 0; k < lines.Length; k++)
{
if (k == 0)
sb.Append (lines[k]);
else
sb.Append (" " + lines[k]);
}
return sb.ToString ();
}
#endif
internal static string TruncateAtNewLine(string s)
{
if (string.IsNullOrEmpty(s))
return s;
int lineBreak = s.IndexOfAny(s_lineBreakChars);
if (lineBreak < 0)
return s;
return s.Substring(0, lineBreak) + PSObjectHelper.ellipses;
}
internal static string PadLeft(string val, int count)
{
return StringUtil.Padding(count) + val;
}
private static readonly char[] s_newLineChar = new char[] { '\n' };
private static readonly char[] s_lineBreakChars = new char[] { '\n', '\r' };
}
}