// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Globalization; using System.IO; using System.Management.Automation; using System.Text; // interfaces for host interaction namespace Microsoft.PowerShell.Commands.Internal.Format { /// /// Base class providing support for string manipulation. /// This class is a tear off class provided by the LineOutput class /// /// Assumptions (in addition to the assumptions made for LineOutput): /// - characters map to one or more character cells /// /// NOTE: we provide a base class that is valid for devices that have a /// 1:1 mapping between a UNICODE character and a display cell. /// internal class DisplayCells { internal virtual int Length(string str) { return Length(str, 0); } internal virtual int Length(string str, int offset) { int length = 0; foreach (char c in str) { length += LengthInBufferCells(c); } return length - offset; } internal virtual int Length(char character) { return 1; } internal virtual int GetHeadSplitLength(string str, int displayCells) { return GetHeadSplitLength(str, 0, displayCells); } internal virtual int GetHeadSplitLength(string str, int offset, int displayCells) { int len = str.Length - offset; return (len < displayCells) ? len : displayCells; } internal virtual int GetTailSplitLength(string str, int displayCells) { return GetTailSplitLength(str, 0, displayCells); } internal virtual int GetTailSplitLength(string str, int offset, int displayCells) { int len = str.Length - offset; return (len < displayCells) ? len : displayCells; } #region Helpers protected static int LengthInBufferCells(char c) { // The following is based on http://www.cl.cam.ac.uk/~mgk25/c/wcwidth.c // which is derived from https://www.unicode.org/Public/UCD/latest/ucd/EastAsianWidth.txt bool isWide = c >= 0x1100 && (c <= 0x115f || /* Hangul Jamo init. consonants */ c == 0x2329 || c == 0x232a || ((uint)(c - 0x2e80) <= (0xa4cf - 0x2e80) && c != 0x303f) || /* CJK ... Yi */ ((uint)(c - 0xac00) <= (0xd7a3 - 0xac00)) || /* Hangul Syllables */ ((uint)(c - 0xf900) <= (0xfaff - 0xf900)) || /* CJK Compatibility Ideographs */ ((uint)(c - 0xfe10) <= (0xfe19 - 0xfe10)) || /* Vertical forms */ ((uint)(c - 0xfe30) <= (0xfe6f - 0xfe30)) || /* CJK Compatibility Forms */ ((uint)(c - 0xff00) <= (0xff60 - 0xff00)) || /* Fullwidth Forms */ ((uint)(c - 0xffe0) <= (0xffe6 - 0xffe0))); // We can ignore these ranges because .Net strings use surrogate pairs // for this range and we do not handle surrogage pairs. // (c >= 0x20000 && c <= 0x2fffd) || // (c >= 0x30000 && c <= 0x3fffd) return 1 + (isWide ? 1 : 0); } /// /// Given a string and a number of display cells, it computes how many /// characters would fit starting from the beginning or end of the string. /// /// String to be displayed. /// Offset inside the string. /// Number of display cells. /// If true compute from the head (i.e. k++) else from the tail (i.e. k--). /// Number of characters that would fit. protected int GetSplitLengthInternalHelper(string str, int offset, int displayCells, bool head) { int filledDisplayCellsCount = 0; // number of cells that are filled in int charactersAdded = 0; // number of characters that fit int currCharDisplayLen; // scratch variable int k = (head) ? offset : str.Length - 1; int kFinal = (head) ? str.Length - 1 : offset; while (true) { if ((head && (k > kFinal)) || ((!head) && (k < kFinal))) { break; } // compute the cell number for the current character currCharDisplayLen = this.Length(str[k]); if (filledDisplayCellsCount + currCharDisplayLen > displayCells) { // if we added this character it would not fit, we cannot continue break; } // keep adding, we fit filledDisplayCellsCount += currCharDisplayLen; charactersAdded++; // check if we fit exactly if (filledDisplayCellsCount == displayCells) { // exact fit, we cannot add more break; } k = (head) ? (k + 1) : (k - 1); } return charactersAdded; } #endregion } /// /// Base class providing information about the screen device capabilities /// and used to write the output strings to the text output device. /// Each device supported will have to derive from it. /// Examples of supported devices are: /// * Screen Layout: it layers on top of Console and RawConsole /// * File: it layers on top of a TextWriter /// * In Memory text stream: it layers on top of an in memory buffer /// * Printer: it layers on top of a memory buffer then sent to a printer device /// /// Assumptions: /// - Fixed pitch font: layout done in terms of character cells /// - character cell layout not affected by bold, reverse screen, color, etc. /// - returned values might change from call to call if the specific underlying /// implementation allows window resizing. /// internal abstract class LineOutput { /// /// Whether the device requires full buffering of formatting /// objects before any processing. /// internal virtual bool RequiresBuffering { get { return false; } } /// /// Delegate the implementor of ExecuteBufferPlayBack should /// call to cause the playback to happen when ready to execute. /// internal delegate void DoPlayBackCall(); /// /// If RequiresBuffering = true, this call will be made to /// start the playback. /// internal virtual void ExecuteBufferPlayBack(DoPlayBackCall playback) { } /// /// The number of columns the current device has. /// internal abstract int ColumnNumber { get; } /// /// The number of rows the current device has. /// internal abstract int RowNumber { get; } /// /// Write a line to the output device. /// /// /// string to be written to the device /// internal abstract void WriteLine(string s); internal WriteStreamType WriteStream { get; set; } /// /// Handle the stop processing signal. /// Set a flag that will be checked during operations. /// internal void StopProcessing() { _isStopping = true; } private bool _isStopping; internal void CheckStopProcessing() { if (!_isStopping) return; throw new PipelineStoppedException(); } /// /// Return an instance of the display helper tear off. /// /// internal virtual DisplayCells DisplayCells { get { CheckStopProcessing(); // just return the default singleton implementation return _displayCellsDefault; } } /// /// Singleton used for the default implementation. /// NOTE: derived classes may chose to provide a different /// implementation by overriding. /// protected static DisplayCells _displayCellsDefault = new DisplayCells(); } /// /// Helper class to provide line breaking (based on device width) /// and embedded newline processing /// It needs to be provided with two callbacks for line processing. /// internal class WriteLineHelper { #region callbacks /// /// Delegate definition. /// /// String to write. internal delegate void WriteCallback(string s); /// /// Instance of the delegate previously defined /// for line that has EXACTLY this.ncols characters. /// private readonly WriteCallback _writeCall = null; /// /// Instance of the delegate previously defined /// for generic line, less that this.ncols characters. /// private readonly WriteCallback _writeLineCall = null; #endregion private readonly bool _lineWrap; /// /// Construct an instance, given the two callbacks /// NOTE: if the underlying device treats the two cases as the /// same, the same delegate can be passed twice. /// /// True if we require line wrapping. /// Delegate for WriteLine(), must ben non null. /// Delegate for Write(), if null, use the first parameter. /// Helper object for manipulating strings. internal WriteLineHelper(bool lineWrap, WriteCallback wlc, WriteCallback wc, DisplayCells displayCells) { if (wlc == null) throw PSTraceSource.NewArgumentNullException(nameof(wlc)); if (displayCells == null) throw PSTraceSource.NewArgumentNullException(nameof(displayCells)); _displayCells = displayCells; _writeLineCall = wlc; _writeCall = wc ?? wlc; _lineWrap = lineWrap; } /// /// Main entry point to process a line. /// /// String to process. /// Width of the device. internal void WriteLine(string s, int cols) { WriteLineInternal(s, cols); } /// /// Internal helper, needed because it might make recursive calls to itself. /// /// String to process. /// Width of the device. private void WriteLineInternal(string val, int cols) { if (string.IsNullOrEmpty(val)) { _writeLineCall(val); return; } // If the output is being redirected, then we don't break val if (!_lineWrap) { _writeCall(val); return; } // check for line breaks string[] lines = StringManipulationHelper.SplitLines(val); // process the substrings as separate lines for (int k = 0; k < lines.Length; k++) { // compute the display length of the string int displayLength = _displayCells.Length(lines[k]); if (displayLength < cols) { // NOTE: this is the case where where System.Console.WriteLine() would work just fine _writeLineCall(lines[k]); continue; } if (displayLength == cols) { // NOTE: this is the corner case where System.Console.WriteLine() cannot be called _writeCall(lines[k]); continue; } // the string does not fit, so we have to wrap around on multiple lines string s = lines[k]; while (true) { // the string is still too long to fit, write the first cols characters // and go back for more wraparound int splitLen = _displayCells.GetHeadSplitLength(s, cols); WriteLineInternal(s.Substring(0, splitLen), cols); // chop off the first fieldWidth characters, already printed s = s.Substring(splitLen); if (_displayCells.Length(s) <= cols) { // if we fit, print the tail of the string and we are done WriteLineInternal(s, cols); break; } } } } private readonly DisplayCells _displayCells; } /// /// Implementation of the ILineOutput interface accepting an instance of a /// TextWriter abstract class. /// internal class TextWriterLineOutput : LineOutput { #region ILineOutput methods /// /// Get the columns on the screen /// for files, it is settable at creation time. /// internal override int ColumnNumber { get { CheckStopProcessing(); return _columns; } } /// /// Get the # of rows on the screen: for files /// we return -1, meaning infinite. /// internal override int RowNumber { get { CheckStopProcessing(); return -1; } } /// /// Write a line by delegating to the writer underneath. /// /// internal override void WriteLine(string s) { CheckStopProcessing(); if (_suppressNewline) { _writer.Write(s); } else { _writer.WriteLine(s); } } #endregion /// /// Initialization of the object. It must be called before /// attempting any operation. /// /// TextWriter to write to. /// Max columns widths for the text. internal TextWriterLineOutput(TextWriter writer, int columns) { _writer = writer; _columns = columns; } /// /// Initialization of the object. It must be called before /// attempting any operation. /// /// TextWriter to write to. /// Max columns widths for the text. /// False to add a newline to the end of the output string, true if not. internal TextWriterLineOutput(TextWriter writer, int columns, bool suppressNewline) : this(writer, columns) { _suppressNewline = suppressNewline; } private readonly int _columns = 0; private readonly TextWriter _writer = null; private readonly bool _suppressNewline = false; } /// /// TextWriter to generate data for the Monad pipeline in a streaming fashion: /// the provided callback will be called each time a line is written. /// internal class StreamingTextWriter : TextWriter { #region tracer [TraceSource("StreamingTextWriter", "StreamingTextWriter")] private static readonly PSTraceSource s_tracer = PSTraceSource.GetTracer("StreamingTextWriter", "StreamingTextWriter"); #endregion tracer /// /// Create an instance by passing a delegate. /// /// Delegate to write to. /// Culture for this TextWriter. internal StreamingTextWriter(WriteLineCallback writeCall, CultureInfo culture) : base(culture) { if (writeCall == null) throw PSTraceSource.NewArgumentNullException(nameof(writeCall)); _writeCall = writeCall; } #region TextWriter overrides public override Encoding Encoding { get { return new UnicodeEncoding(); } } public override void WriteLine(string s) { _writeCall(s); } #endregion /// /// Delegate definition. /// /// String to write. internal delegate void WriteLineCallback(string s); /// /// Instance of the delegate previously defined. /// private readonly WriteLineCallback _writeCall = null; } }