|
4 | 4 |
|
5 | 5 | namespace ApplicationUtility; |
6 | 6 |
|
7 | | -// TODO: abstract out StringBuilder into a class which will have the same API but will be able |
8 | | -// to either write to a StringBuilder or render directly to the console, with color (if |
9 | | -// supported) |
10 | 7 | class MarkdownDocument |
11 | 8 | { |
12 | | - List<MarkdownElement> elements = new (); |
| 9 | + const int DefaultLineWidth = 100; |
| 10 | + |
| 11 | + readonly int lineWidth; |
| 12 | + readonly StringBuilder doc = new (); |
| 13 | + readonly Stack<int> indent = new (); |
13 | 14 |
|
14 | | - public bool IsEmpty => elements.Count == 0; |
| 15 | + public string Text => doc.ToString (); |
15 | 16 |
|
16 | | - public MarkdownHeading AddHeading (uint level, string text) |
| 17 | + public MarkdownDocument (int lineWidth = DefaultLineWidth) |
17 | 18 | { |
18 | | - var heading = new MarkdownHeading (level, text); |
19 | | - elements.Add (heading); |
20 | | - return heading; |
| 19 | + this.lineWidth = lineWidth >= 0 ? lineWidth : DefaultLineWidth; |
| 20 | + ResetIndent (); |
21 | 21 | } |
22 | 22 |
|
23 | | - public static MarkdownTextSpan CreateNewLine () => new MarkdownTextSpan (Environment.NewLine); |
24 | | - public static MarkdownParagraph CreateParagraph () => new MarkdownParagraph (); |
25 | | - public static MarkdownTextSpan CreateText (string text, bool bold = false) => new MarkdownTextSpan (text) { Bold = bold }; |
| 23 | + public MarkdownDocument AddHeading (uint level, string text) |
| 24 | + { |
| 25 | + // Headings don't break on `lineWidth`... |
| 26 | + if (doc.Length > 0) { |
| 27 | + AddNewline (); |
| 28 | + } |
| 29 | + |
| 30 | + // ...and they always start at column 0... |
| 31 | + doc.Append ('#', (int)(level == 0 ? 1 : level)); |
| 32 | + doc.Append (' '); |
| 33 | + AppendText (text, breakLine: false); |
| 34 | + AddNewline (); |
| 35 | + |
| 36 | + // ...and they reset the indent |
| 37 | + ResetIndent (); |
26 | 38 |
|
27 | | - public void AddNewLine () |
| 39 | + return this; |
| 40 | + } |
| 41 | + |
| 42 | + public MarkdownDocument AddText (string text, MarkdownTextStyle style = MarkdownTextStyle.Plain, bool addIndent = true) |
28 | 43 | { |
29 | | - elements.Add (CreateNewLine ()); |
| 44 | + AppendText (text, style, breakLine: true, addIndent); |
| 45 | + return this; |
30 | 46 | } |
31 | 47 |
|
32 | | - public MarkdownPresenter Render (bool toConsole, bool useColor, bool renderPlainText) |
| 48 | + public MarkdownDocument AddNewline (int count = 1) |
33 | 49 | { |
34 | | - var presenter = new MarkdownPresenter (toConsole, useColor, renderPlainText); |
35 | | - if (IsEmpty) { |
36 | | - return presenter; |
| 50 | + if (count < 1) { |
| 51 | + return this; |
37 | 52 | } |
38 | 53 |
|
39 | | - foreach (MarkdownElement element in elements) { |
40 | | - RenderSafe (presenter, element, renderPlainText); |
| 54 | + for (int i = 0; i < count; i++) { |
| 55 | + doc.AppendLine (); |
41 | 56 | } |
42 | 57 |
|
43 | | - return presenter; |
| 58 | + return this; |
44 | 59 | } |
45 | 60 |
|
46 | | - void RenderSafe (MarkdownPresenter presenter, MarkdownElement element, bool plain) |
| 61 | + int AppendIndent () |
47 | 62 | { |
48 | | - try { |
49 | | - Render (presenter, element, plain); |
50 | | - } catch (Exception ex) { |
51 | | - Log.Warning ($"Failed to render element {element}.", ex); |
| 63 | + int indent = GetIndent (); |
| 64 | + if (indent > 0) { |
| 65 | + doc.Append (' ', indent); |
52 | 66 | } |
| 67 | + |
| 68 | + return indent; |
53 | 69 | } |
54 | 70 |
|
55 | | - void RenderChildren (MarkdownPresenter presenter, MarkdownContainerElement element, bool plain, |
56 | | - Action<MarkdownElement>? beforeElement = null, Action<MarkdownElement>? afterElement = null) |
| 71 | + void AppendText (string text, MarkdownTextStyle style = MarkdownTextStyle.Plain, bool breakLine = true, bool addIndent = true) |
57 | 72 | { |
58 | | - if (element.Children == null || element.Children.Count == 0) { |
| 73 | + int indent; |
| 74 | + |
| 75 | + if (addIndent) { |
| 76 | + indent = AppendIndent (); |
| 77 | + } else { |
| 78 | + indent = GetIndent (); |
| 79 | + } |
| 80 | + |
| 81 | + if (!breakLine) { |
| 82 | + doc.Append (text); |
59 | 83 | return; |
60 | 84 | } |
61 | 85 |
|
62 | | - foreach (MarkdownElement child in element.Children) { |
63 | | - beforeElement?.Invoke (child); |
64 | | - RenderSafe (presenter, child, plain); |
65 | | - afterElement?.Invoke (child); |
| 86 | + // TODO: implement breaking the line at the last whitespace character before maximum line width. |
| 87 | + // Indent is included in calculations. |
| 88 | + doc.Append (text); |
| 89 | + } |
| 90 | + |
| 91 | + public MarkdownDocument BeginList () |
| 92 | + { |
| 93 | + doc.AppendLine (); |
| 94 | + SetNewIndent (2); |
| 95 | + return this; |
| 96 | + } |
| 97 | + |
| 98 | + public MarkdownDocument StartListItem (string? text = null, MarkdownTextStyle style = MarkdownTextStyle.Plain) |
| 99 | + { |
| 100 | + AppendIndent (); |
| 101 | + doc.Append ("* "); |
| 102 | + |
| 103 | + if (!String.IsNullOrEmpty (text)) { |
| 104 | + AppendText (text, style, addIndent: false); |
66 | 105 | } |
| 106 | + |
| 107 | + return this; |
67 | 108 | } |
68 | 109 |
|
69 | | - void Render (MarkdownPresenter presenter, MarkdownElement element, bool plain) |
| 110 | + public MarkdownDocument EndListItem (bool appendLine = true) |
70 | 111 | { |
71 | | - if (element is MarkdownTextSpan textSpan) { |
72 | | - Render (presenter, textSpan, plain); |
73 | | - } else if (element is MarkdownHeading section) { |
74 | | - Render (presenter, section, plain); |
75 | | - } else if (element is MarkdownParagraph para) { |
76 | | - Render (presenter, para, plain); |
77 | | - } else if (element is MarkdownList list) { |
78 | | - Render (presenter, list, plain); |
79 | | - } else { |
80 | | - throw new InvalidOperationException ($"Internal error: Markdown element {element.GetType ()} not supported when rendering."); |
| 112 | + if (appendLine) { |
| 113 | + doc.AppendLine (); |
81 | 114 | } |
| 115 | + return this; |
82 | 116 | } |
83 | 117 |
|
84 | | - void Render (MarkdownPresenter presenter, MarkdownList list, bool plain) |
| 118 | + public MarkdownDocument AddListItem (string? text = null, MarkdownTextStyle style = MarkdownTextStyle.Plain, bool appendLine = true) |
85 | 119 | { |
86 | | - presenter.AddNewLine (); |
87 | | - RenderChildren ( |
88 | | - presenter, |
89 | | - list, |
90 | | - plain, |
91 | | - beforeElement: (MarkdownElement element) => { |
92 | | - if (element is MarkdownList) { |
93 | | - return; |
94 | | - } |
95 | | - presenter.Append (" * "); |
96 | | - }, |
97 | | - afterElement: (MarkdownElement _) => presenter.AddNewLine () |
98 | | - ); |
99 | | - presenter.AddNewLine (); |
100 | | - presenter.AddNewLine (); |
| 120 | + StartListItem (text, style); |
| 121 | + EndListItem (appendLine); |
| 122 | + return this; |
101 | 123 | } |
102 | 124 |
|
103 | | - void Render (MarkdownPresenter presenter, MarkdownParagraph para, bool plain) |
| 125 | + public MarkdownDocument EndList () |
104 | 126 | { |
105 | | - RenderChildren (presenter, para, plain); |
106 | | - int newLines = para.GetNumberOfNewLinesNeeded (); |
107 | | - if (newLines > 0) { |
108 | | - for (int i = 0; i < newLines; i++) { |
109 | | - presenter.AddNewLine (); |
110 | | - } |
111 | | - } |
| 127 | + RestorePreviousIndent (); |
| 128 | + return this; |
112 | 129 | } |
113 | 130 |
|
114 | | - void Render (MarkdownPresenter presenter, MarkdownHeading section, bool plain) |
| 131 | + int GetIndent () => indent.Peek (); |
| 132 | + |
| 133 | + int SetNewIndent (int delta = 2) |
115 | 134 | { |
116 | | - presenter.Append ('#', (int)section.Level); |
117 | | - presenter.Append (' '); |
118 | | - presenter.Append (section.Text); |
119 | | - presenter.AddNewLine (); |
120 | | - presenter.AddNewLine (); |
| 135 | + int newIndent = GetIndent () + delta; |
| 136 | + indent.Push (newIndent); |
| 137 | + return newIndent; |
| 138 | + } |
121 | 139 |
|
122 | | - RenderChildren (presenter, section, plain); |
| 140 | + int RestorePreviousIndent () |
| 141 | + { |
| 142 | + indent.Pop (); |
| 143 | + return indent.Peek (); |
123 | 144 | } |
124 | 145 |
|
125 | | - void Render (MarkdownPresenter presenter, MarkdownTextSpan textSpan, bool plain) |
| 146 | + void ResetIndent () |
126 | 147 | { |
127 | | - if (textSpan.IsEmpty) { |
128 | | - return; |
129 | | - } |
| 148 | + indent.Clear (); |
| 149 | + indent.Push (0); |
| 150 | + } |
| 151 | +} |
130 | 152 |
|
131 | | - RenderSpan (textSpan); |
132 | | - if (textSpan.Fragments == null) { |
133 | | - return; |
134 | | - } |
| 153 | +// FIXME: replace all of this with https://www.nuget.org/packages/Microsoft.PowerShell.MarkdownRender |
| 154 | +// |
| 155 | +// TODO: abstract out StringBuilder into a class which will have the same API but will be able |
| 156 | +// to either write to a StringBuilder or render directly to the console, with color (if |
| 157 | +// supported) |
| 158 | +// class MarkdownDocumentOld |
| 159 | +// { |
| 160 | +// List<MarkdownElement> elements = new (); |
135 | 161 |
|
136 | | - foreach (MarkdownTextSpan fragment in textSpan.Fragments) { |
137 | | - RenderSpan (fragment); |
138 | | - } |
| 162 | +// public bool IsEmpty => elements.Count == 0; |
139 | 163 |
|
140 | | - void RenderSpan (MarkdownTextSpan span) |
141 | | - { |
142 | | - string? text = span.RemoveTailWhitespace ? span.Text?.TrimEnd () : span.Text; |
| 164 | +// public MarkdownHeading AddHeading (uint level, string text) |
| 165 | +// { |
| 166 | +// var heading = new MarkdownHeading (level, text); |
| 167 | +// elements.Add (heading); |
| 168 | +// return heading; |
| 169 | +// } |
143 | 170 |
|
144 | | - presenter.Append (span.Text, span.Style); |
145 | | - } |
146 | | - } |
| 171 | +// public static MarkdownTextSpan CreateNewLine () => new MarkdownTextSpan (Environment.NewLine); |
| 172 | +// public static MarkdownParagraph CreateParagraph () => new MarkdownParagraph (); |
| 173 | +// public static MarkdownTextSpan CreateText (string text, bool bold = false) => new MarkdownTextSpan (text) { Bold = bold }; |
147 | 174 |
|
148 | | - void AddNewLine (StringBuilder sb) => sb.Append (Environment.NewLine); |
149 | | -} |
| 175 | +// public void AddNewLine () |
| 176 | +// { |
| 177 | +// elements.Add (CreateNewLine ()); |
| 178 | +// } |
| 179 | + |
| 180 | +// public MarkdownPresenter Render (bool toConsole, bool useColor, bool renderPlainText) |
| 181 | +// { |
| 182 | +// var presenter = new MarkdownPresenter (toConsole, useColor, renderPlainText); |
| 183 | +// if (IsEmpty) { |
| 184 | +// return presenter; |
| 185 | +// } |
| 186 | + |
| 187 | +// foreach (MarkdownElement element in elements) { |
| 188 | +// RenderSafe (presenter, element, renderPlainText); |
| 189 | +// } |
| 190 | + |
| 191 | +// return presenter; |
| 192 | +// } |
| 193 | + |
| 194 | +// void RenderSafe (MarkdownPresenter presenter, MarkdownElement element, bool plain) |
| 195 | +// { |
| 196 | +// try { |
| 197 | +// Render (presenter, element, plain); |
| 198 | +// } catch (Exception ex) { |
| 199 | +// Log.Warning ($"Failed to render element {element}.", ex); |
| 200 | +// } |
| 201 | +// } |
| 202 | + |
| 203 | +// void RenderChildren (MarkdownPresenter presenter, MarkdownContainerElement element, bool plain, |
| 204 | +// Action<MarkdownElement>? beforeElement = null, Action<MarkdownElement>? afterElement = null) |
| 205 | +// { |
| 206 | +// if (element.Children == null || element.Children.Count == 0) { |
| 207 | +// return; |
| 208 | +// } |
| 209 | + |
| 210 | +// foreach (MarkdownElement child in element.Children) { |
| 211 | +// beforeElement?.Invoke (child); |
| 212 | +// RenderSafe (presenter, child, plain); |
| 213 | +// afterElement?.Invoke (child); |
| 214 | +// } |
| 215 | +// } |
| 216 | + |
| 217 | +// void Render (MarkdownPresenter presenter, MarkdownElement element, bool plain) |
| 218 | +// { |
| 219 | +// if (element is MarkdownTextSpan textSpan) { |
| 220 | +// Render (presenter, textSpan, plain); |
| 221 | +// } else if (element is MarkdownHeading section) { |
| 222 | +// Render (presenter, section, plain); |
| 223 | +// } else if (element is MarkdownParagraph para) { |
| 224 | +// Render (presenter, para, plain); |
| 225 | +// } else if (element is MarkdownList list) { |
| 226 | +// Render (presenter, list, plain); |
| 227 | +// } else { |
| 228 | +// throw new InvalidOperationException ($"Internal error: Markdown element {element.GetType ()} not supported when rendering."); |
| 229 | +// } |
| 230 | +// } |
| 231 | + |
| 232 | +// void Render (MarkdownPresenter presenter, MarkdownList list, bool plain) |
| 233 | +// { |
| 234 | +// presenter.AddNewLine (); |
| 235 | +// RenderChildren ( |
| 236 | +// presenter, |
| 237 | +// list, |
| 238 | +// plain, |
| 239 | +// beforeElement: (MarkdownElement element) => { |
| 240 | +// if (element is MarkdownList) { |
| 241 | +// return; |
| 242 | +// } |
| 243 | +// presenter.Append (" * "); |
| 244 | +// }, |
| 245 | +// afterElement: (MarkdownElement _) => presenter.AddNewLine () |
| 246 | +// ); |
| 247 | +// presenter.AddNewLine (); |
| 248 | +// presenter.AddNewLine (); |
| 249 | +// } |
| 250 | + |
| 251 | +// void Render (MarkdownPresenter presenter, MarkdownParagraph para, bool plain) |
| 252 | +// { |
| 253 | +// RenderChildren (presenter, para, plain); |
| 254 | +// int newLines = para.GetNumberOfNewLinesNeeded (); |
| 255 | +// if (newLines > 0) { |
| 256 | +// for (int i = 0; i < newLines; i++) { |
| 257 | +// presenter.AddNewLine (); |
| 258 | +// } |
| 259 | +// } |
| 260 | +// } |
| 261 | + |
| 262 | +// void Render (MarkdownPresenter presenter, MarkdownHeading section, bool plain) |
| 263 | +// { |
| 264 | +// presenter.Append ('#', (int)section.Level); |
| 265 | +// presenter.Append (' '); |
| 266 | +// presenter.Append (section.Text); |
| 267 | +// presenter.AddNewLine (); |
| 268 | +// presenter.AddNewLine (); |
| 269 | + |
| 270 | +// RenderChildren (presenter, section, plain); |
| 271 | +// } |
| 272 | + |
| 273 | +// void Render (MarkdownPresenter presenter, MarkdownTextSpan textSpan, bool plain) |
| 274 | +// { |
| 275 | +// if (textSpan.IsEmpty) { |
| 276 | +// return; |
| 277 | +// } |
| 278 | + |
| 279 | +// RenderSpan (textSpan); |
| 280 | +// if (textSpan.Fragments == null) { |
| 281 | +// return; |
| 282 | +// } |
| 283 | + |
| 284 | +// foreach (MarkdownTextSpan fragment in textSpan.Fragments) { |
| 285 | +// RenderSpan (fragment); |
| 286 | +// } |
| 287 | + |
| 288 | +// void RenderSpan (MarkdownTextSpan span) |
| 289 | +// { |
| 290 | +// string? text = span.RemoveTailWhitespace ? span.Text?.TrimEnd () : span.Text; |
| 291 | + |
| 292 | +// presenter.Append (span.Text, span.Style); |
| 293 | +// } |
| 294 | +// } |
| 295 | + |
| 296 | +// void AddNewLine (StringBuilder sb) => sb.Append (Environment.NewLine); |
| 297 | +// } |
0 commit comments