feat: add tab-based markdown and selection

This commit is contained in:
Anna 2020-11-09 16:31:28 -05:00
parent 178950bee9
commit c012fb5f77
8 changed files with 650 additions and 58 deletions

View File

@ -110,13 +110,15 @@ namespace XIVChat_Desktop {
// check if backlog or catch-up is needed
if (sameHost) {
// catch-up
var lastRealMessage = this.app.Window.Messages.FirstOrDefault(msg => msg.Channel != 0);
var lastRealMessage = this.app.Window.Messages.LastOrDefault(msg => msg.Channel != 0);
if (lastRealMessage != null) {
_backlogSequence += 1;
var catchUp = new ClientCatchUp(lastRealMessage.Timestamp);
await SecretMessage.SendSecretMessage(stream, handshake.Keys.tx, catchUp, this.cancel.Token);
}
} else if (this.app.Config.BacklogMessages > 0) {
// backlog
_backlogSequence += 1;
var backlogReq = new ClientBacklog {
Amount = this.app.Config.BacklogMessages,
};
@ -263,9 +265,7 @@ namespace XIVChat_Desktop {
case ServerOperation.Backlog:
var backlog = ServerBacklog.Decode(payload);
this.backlogSequence += 1;
var seq = this.backlogSequence;
var seq = _backlogSequence;
foreach (var msg in backlog.messages.ToList().Chunks(100)) {
msg.Reverse();
var array = msg.ToArray();
@ -282,7 +282,7 @@ namespace XIVChat_Desktop {
}
}
private int backlogSequence = -1;
private static int _backlogSequence = -1;
private void SetPlayerData(PlayerData? playerData) {
var visibility = playerData == null ? Visibility.Collapsed : Visibility.Visible;

View File

@ -0,0 +1,54 @@
using System.Windows;
using System.Windows.Controls;
using XIVChatCommon;
namespace XIVChat_Desktop.Controls {
public class MessageTextBlock : SelectableTextBlock {
public static readonly DependencyProperty MessageProperty = DependencyProperty.Register(
"Message",
typeof(ServerMessage),
typeof(MessageTextBlock),
new PropertyMetadata(null, PropertyChanged)
);
public ServerMessage? Message {
get => (ServerMessage)this.GetValue(MessageProperty);
set => this.SetValue(MessageProperty, value);
}
public static readonly DependencyProperty TabProperty = DependencyProperty.Register(
"Tab",
typeof(Tab),
typeof(MessageTextBlock),
new PropertyMetadata(null, PropertyChanged)
);
public Tab? Tab {
get => (Tab)this.GetValue(TabProperty);
set => this.SetValue(TabProperty, value);
}
public static void PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
// Clear current textBlock
if (!(d is MessageTextBlock textBlock)) {
return;
}
var message = textBlock.Message;
var tab = textBlock.Tab;
if (message == null || tab == null) {
return;
}
textBlock.ClearValue(TextBlock.TextProperty);
textBlock.Inlines.Clear();
// Create new formatted text
var lineHeight = textBlock.FontFamily.LineSpacing * textBlock.FontSize;
foreach (var inline in MessageFormatter.ChunksToTextBlock(lineHeight, message, tab)) {
textBlock.Inlines.Add(inline);
}
}
}
}

View File

@ -0,0 +1,69 @@
using System;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
namespace XIVChat_Desktop.Controls {
public class SelectableTextBlock : TextBlock {
static SelectableTextBlock() {
FocusableProperty.OverrideMetadata(typeof(SelectableTextBlock), new FrameworkPropertyMetadata(true));
TextEditorWrapper.RegisterCommandHandlers(typeof(SelectableTextBlock), true, true, true);
// remove the focus rectangle around the control
FocusVisualStyleProperty.OverrideMetadata(typeof(SelectableTextBlock), new FrameworkPropertyMetadata(null));
}
public SelectableTextBlock() {
TextEditorWrapper.CreateFor(this);
}
}
class TextEditorWrapper {
private static readonly Type TextEditorType = Type.GetType("System.Windows.Documents.TextEditor, PresentationFramework")!;
private static readonly PropertyInfo IsReadOnlyProp = TextEditorType.GetProperty("IsReadOnly", BindingFlags.Instance | BindingFlags.NonPublic)!;
private static readonly PropertyInfo TextViewProp = TextEditorType.GetProperty("TextView", BindingFlags.Instance | BindingFlags.NonPublic)!;
private static readonly MethodInfo RegisterMethod = TextEditorType.GetMethod(
"RegisterCommandHandlers",
BindingFlags.Static | BindingFlags.NonPublic,
null,
new[] {
typeof(Type), typeof(bool), typeof(bool), typeof(bool),
},
null
)!;
private static readonly Type TextContainerType = Type.GetType("System.Windows.Documents.ITextContainer, PresentationFramework")!;
private static readonly PropertyInfo TextContainerTextViewProp = TextContainerType.GetProperty("TextView")!;
private static readonly PropertyInfo TextContainerProp = typeof(TextBlock).GetProperty("TextContainer", BindingFlags.Instance | BindingFlags.NonPublic)!;
public static void RegisterCommandHandlers(Type controlType, bool acceptsRichContent, bool readOnly, bool registerEventListeners) {
RegisterMethod.Invoke(null, new object[] {
controlType, acceptsRichContent, readOnly, registerEventListeners,
});
}
public static void CreateFor(TextBlock tb) {
var textContainer = TextContainerProp.GetValue(tb);
var editor = new TextEditorWrapper(textContainer!, tb, false);
IsReadOnlyProp.SetValue(editor.editor, true);
TextViewProp.SetValue(editor.editor, TextContainerTextViewProp.GetValue(textContainer));
}
private readonly object editor;
private TextEditorWrapper(object textContainer, FrameworkElement uiScope, bool isUndoEnabled) {
this.editor = Activator.CreateInstance(
TextEditorType,
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.CreateInstance,
null,
new[] {
textContainer, uiScope, isUndoEnabled,
},
null
)!;
}
}
}

View File

@ -4,6 +4,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:XIVChat_Desktop"
xmlns:cc="clr-namespace:XIVChat_Desktop.Controls"
xmlns:ui="http://schemas.modernwpf.com/2019"
ui:WindowHelper.UseModernWindowStyle="True"
mc:Ignorable="d"
@ -53,7 +54,8 @@
<TabControl.ContentTemplate>
<DataTemplate>
<Grid d:DataContext="{d:DesignInstance local:Tab}">
<Grid x:Name="TabGrid"
d:DataContext="{d:DesignInstance local:Tab}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
@ -85,10 +87,11 @@
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock FontFamily="Global User Interface, /fonts/#XIV AXIS Std ATK"
TextWrapping="Wrap"
FontSize="{Binding App.Config.FontSize, ElementName=Main, UpdateSourceTrigger=PropertyChanged}"
local:MessageFormatter.FormattedText="{Binding .}" />
<cc:MessageTextBlock FontFamily="Global User Interface, /fonts/#XIV AXIS Std ATK"
TextWrapping="Wrap"
FontSize="{Binding App.Config.FontSize, ElementName=Main, UpdateSourceTrigger=PropertyChanged}"
Tab="{Binding DataContext, ElementName=TabGrid}"
Message="{Binding .}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

View File

@ -14,6 +14,7 @@
d:DataContext="{d:DesignInstance local:FiltersSelection}">
<Grid Margin="8">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
@ -24,11 +25,15 @@
ui:ControlHelper.PlaceholderText="Name"
ui:ControlHelper.Header="Name"
Text="{Binding Tab.Name}" />
<TabControl Grid.Row="1"
<CheckBox Grid.Row="1"
Margin="0,0,0,8"
Content="Process messages as Markdown"
IsChecked="{Binding Tab.ProcessMarkdown}" />
<TabControl Grid.Row="2"
x:Name="Tabs" />
<Button Margin="0,8,0,0"
Grid.Row="2"
Grid.Row="3"
Content="Save"
Click="Save_Click" />
</Grid>
</Window>
</Window>

424
XIVChat Desktop/Markdown.cs Normal file
View File

@ -0,0 +1,424 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Documents;
namespace XIVChat_Desktop {
public static class Markdown {
private readonly static char[] Delimiters = {
'*', '_', '~',
};
public static IEnumerable<Inline> MarkdownToInlines(string input) {
var nodes = ParseNodes(input);
return NodesToInlines(nodes);
}
private static ContainerNode ParseNodes(string input) {
var root = new ContainerNode();
DelimiterNode? run = null;
char? last = null;
var segment = new StringBuilder();
void AddTextRun() {
if (segment.Length == 0) {
return;
}
root.AddChild(new TextNode(root, segment.ToString()));
segment.Clear();
}
void ProcessExistingRun() {
// determine the flank
run.DetermineFlank();
// if flank is still unknown, this is an invalid run
var isUnknown = run.flank == DelimiterNode.Flank.Unknown;
// also if strikethrough does not contain two delimiters, this is an invalid run
var notLongEnough = run.character == '~' && run.length < 2;
if (isUnknown || notLongEnough) {
// return these characters to the string
for (var i = 0; i < run.length; i++) {
segment.Append(run.character);
}
// remove the run
goto RemoveRun;
}
// at this point, we're going to add the run, so also add the text if necessary
AddTextRun();
// add the run to the list
root.AddChild(run);
RemoveRun:
// remove the run from working area
run = null;
}
var chars = input.ToCharArray();
foreach (var c in chars) {
bool append = true;
// these characters can form delimiter runs
if (Delimiters.Contains(c)) {
// don't add this character to the text segment
append = false;
if (run != null && run.character != c) {
run.following = c;
ProcessExistingRun();
}
// create a run if necessary
run ??= new DelimiterNode(root, c) {
preceding = last,
length = 0,
originalLength = 0,
};
// increase the run's length
run.length += 1;
run.originalLength += 1;
// skip to the end
goto Finish;
}
// the last character was a delimiter but this character is not
// we know that this character is not a delimiter because of the goto
if (run != null && last != null && Delimiters.Contains((char)last)) {
// set the following character to this character
run.following = c;
ProcessExistingRun();
}
Finish:
last = c;
if (append) {
segment.Append(c);
}
}
// if we ended on a run, process it
if (run != null) {
ProcessExistingRun();
}
AddTextRun();
return root;
}
private static IEnumerable<Inline> NodesToInlines(MarkdownNodeWithChildren root) {
// FIXME: Ex. 436, 439, 448, 451 (escaping)
// https://spec.commonmark.org/0.29/#emphasis-and-strong-emphasis
var openersBottom = new Dictionary<int, Dictionary<char, LinkedListNode<DelimiterNode>?>>();
for (var i = 0; i < 3; i++) {
openersBottom[i] = new Dictionary<char, LinkedListNode<DelimiterNode>?>();
foreach (var c in Delimiters) {
openersBottom[i][c] = null;
}
}
var delims = new LinkedList<DelimiterNode>();
var node = root.Children.First;
while (node != null) {
if (node.Value is DelimiterNode dRun) {
delims.AddLast(dRun);
}
node = node.Next;
}
// no emphasis to process, so just return all nodes as-is
if (delims.Count == 0) {
return root.Children.Select(child => child.ToInline());
}
// move forward looking for closers
var closer = delims.First;
while (closer != null) {
if (!closer.Value.CanClose) {
closer = closer.Next;
continue;
}
// found first emphasis closer. now look back for first matching opener
var opener = closer.Previous;
var openerFound = false;
var bottom = openersBottom[closer.Value.originalLength % 3][closer.Value.character];
while (opener != null && opener != bottom) {
var oddMatch =
(closer.Value.CanOpen || opener.Value.CanClose) &&
closer.Value.originalLength % 3 != 0 &&
(opener.Value.originalLength + closer.Value.originalLength) % 3 == 0;
if (opener.Value.character == closer.Value.character && opener.Value.CanOpen && !oddMatch) {
openerFound = true;
break;
}
opener = opener.Previous;
}
var oldCloser = closer;
// emphasis and strikethrough
// https://spec.commonmark.org/0.29/#emphasis-and-strong-emphasis
// https://github.github.com/gfm/#strikethrough-extension-
if (Delimiters.Contains(closer.Value.character)) {
if (!openerFound) {
closer = closer.Next;
} else {
if (opener == null) {
throw new InvalidOperationException();
}
MarkdownNodeWithChildren emph;
var isStrike = closer.Value.character == '~';
if (isStrike) {
opener.Value.length -= 2;
closer.Value.length -= 2;
emph = new StrikethroughNode(root);
} else {
var useDelims = closer.Value.length >= 2 && opener.Value.length >= 2 ? 2 : 1;
opener.Value.length -= useDelims;
closer.Value.length -= useDelims;
emph = useDelims == 1 ? (MarkdownNodeWithChildren)new EmphasisNode(root) : new StrongNode(root);
}
var runsOpener = root.Children.Find(opener.Value);
var runsCloser = root.Children.Find(closer.Value);
var tmp = runsOpener!.Next;
while (tmp != null && tmp != runsCloser) {
emph.AddChild(tmp.Value);
var oldTmp = tmp;
tmp = tmp.Next;
root.Children.Remove(oldTmp);
if (oldTmp.Value is DelimiterNode dNode) {
delims.Remove(dNode);
}
}
root.Children.AddAfter(runsOpener, emph);
if (opener.Value.length == 0) {
delims.Remove(opener);
root.Children.Remove(runsOpener);
}
if (closer.Value.length == 0) {
closer = closer.Next;
delims.Remove(oldCloser);
root.Children.Remove(runsCloser!);
}
}
}
if (openerFound) {
continue;
}
openersBottom[oldCloser.Value.originalLength % 3][oldCloser.Value.character] = oldCloser.Previous;
if (!oldCloser.Value.CanOpen) {
delims.Remove(oldCloser);
}
}
return root.Children.Select(child => child.ToInline());
}
}
public abstract class MarkdownNode {
public MarkdownNode? Parent { get; set; }
protected MarkdownNode(MarkdownNode? parent = null) {
this.Parent = parent;
}
public abstract Inline ToInline();
}
public class TextNode : MarkdownNode {
public string Text { get; set; }
public TextNode(MarkdownNode parent, string text) : base(parent) {
this.Text = text;
}
public override Inline ToInline() => new Run(this.Text);
}
public class DelimiterNode : MarkdownNode {
public enum Flank {
Unknown,
Left,
Right,
Both,
}
public readonly char character;
public char? preceding;
public char? following;
public int originalLength;
public int length;
public Flank flank = Flank.Unknown;
public bool CanOpen => this.CalcCanOpen();
public bool CanClose => this.CalcCanClose();
public DelimiterNode(MarkdownNode parent, char character) : base(parent) {
this.character = character;
}
public override Inline ToInline() {
var text = new StringBuilder(this.length);
for (var i = 0; i < this.length; i++) {
text.Append(this.character);
}
return new Run(text.ToString());
}
private bool CalcCanOpen() {
switch (this.character) {
case '*' when (this.flank == Flank.Left || this.flank == Flank.Both):
case '_' when (this.flank == Flank.Left || (this.flank == Flank.Both && this.preceding?.IsAsciiPunctuation() == true)):
case '~' when (this.length >= 2 && (this.flank == Flank.Left || this.flank == Flank.Both)):
return true;
default:
return false;
}
}
private bool CalcCanClose() {
switch (this.character) {
case '*' when (this.flank == Flank.Right || this.flank == Flank.Both):
case '_' when (this.flank == Flank.Right || (this.flank == Flank.Both && this.following?.IsAsciiPunctuation() == true)):
case '~' when (this.length >= 2 && (this.flank == Flank.Right || this.flank == Flank.Both)):
return true;
default:
return false;
}
}
public void DetermineFlank() {
var followedByWhitespace = this.following?.IsWhitespace() ?? true;
var followedByPunctuation = this.following?.IsAsciiPunctuation() ?? false;
var precededByWhitespace = this.preceding?.IsWhitespace() ?? true;
var precededByPunctuation = this.preceding?.IsAsciiPunctuation() ?? false;
var isLeft = false;
var isRight = false;
// A left-flanking delimiter run is a delimiter run that is (1) not followed by Unicode whitespace, and either (2a) not followed by a punctuation character, or (2b) followed by a punctuation character and preceded by Unicode whitespace or a punctuation character. For purposes of this definition, the beginning and the end of the line count as Unicode whitespace.
if (!followedByWhitespace && (!followedByPunctuation || (followedByPunctuation && (precededByWhitespace || precededByPunctuation)))) {
isLeft = true;
}
// A right-flanking delimiter run is a delimiter run that is (1) not preceded by Unicode whitespace, and either (2a) not preceded by a punctuation character, or (2b) preceded by a punctuation character and followed by Unicode whitespace or a punctuation character. For purposes of this definition, the beginning and the end of the line count as Unicode whitespace.
if (!precededByWhitespace && (!precededByPunctuation || (precededByPunctuation && (followedByWhitespace || followedByPunctuation)))) {
isRight = true;
}
if (isLeft && isRight) {
this.flank = Flank.Both;
} else if (isLeft) {
this.flank = Flank.Left;
} else if (isRight) {
this.flank = Flank.Right;
}
}
}
public class EmphasisNode : MarkdownNodeWithChildren {
public EmphasisNode(MarkdownNode parent) : base(parent) { }
public override Inline ToInline() {
var span = new Span {
FontStyle = FontStyles.Italic,
};
span.Inlines.AddRange(this.Children.Select(child => child.ToInline()));
return span;
}
}
public class StrongNode : MarkdownNodeWithChildren {
public StrongNode(MarkdownNode parent) : base(parent) { }
public override Inline ToInline() {
var span = new Span {
FontWeight = FontWeights.Bold,
};
span.Inlines.AddRange(this.Children.Select(child => child.ToInline()));
return span;
}
}
public class StrikethroughNode : MarkdownNodeWithChildren {
public StrikethroughNode(MarkdownNode parent) : base(parent) { }
public override Inline ToInline() {
var span = new Span();
span.TextDecorations.Add(TextDecorations.Strikethrough);
span.Inlines.AddRange(this.Children.Select(child => child.ToInline()));
return span;
}
}
public abstract class MarkdownNodeWithChildren : MarkdownNode {
public LinkedList<MarkdownNode> Children { get; } = new LinkedList<MarkdownNode>();
protected MarkdownNodeWithChildren(MarkdownNode? parent) : base(parent) { }
public void AddChild(MarkdownNode node) {
node.Parent = this;
this.Children.AddLast(node);
}
/// <summary>
/// Replace this node with its children in its parent's children.
/// </summary>
public void PopUp() {
if (!(this.Parent is MarkdownNodeWithChildren parent)) {
return;
}
var us = parent.Children.Find(this);
if (us == null) {
return;
}
foreach (var child in this.Children) {
child.Parent = parent;
parent.Children.AddBefore(us, child);
parent.Children.Remove(us);
}
}
}
public class ContainerNode : MarkdownNodeWithChildren {
public ContainerNode(MarkdownNode? parent = null) : base(parent) { }
public override Inline ToInline() {
var span = new Span();
span.Inlines.AddRange(this.Children.Select(child => child.ToInline()));
return span;
}
}
}

View File

@ -7,6 +7,7 @@ using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using XIVChatCommon;
using Inline = System.Windows.Documents.Inline;
namespace XIVChat_Desktop {
public class MessageFormatter {
@ -16,8 +17,8 @@ namespace XIVChat_Desktop {
public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached(
"FormattedText",
typeof(ServerMessage),
typeof(MessageFormatter),
new PropertyMetadata(null, FormattedTextPropertyChanged)
typeof(MessageFormatter)
// new PropertyMetadata(null, FormattedTextPropertyChanged)
);
public static void SetFormattedText(DependencyObject textBlock, ServerMessage value) {
@ -28,23 +29,23 @@ namespace XIVChat_Desktop {
return (string)textBlock.GetValue(FormattedTextProperty);
}
private static void FormattedTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
// Clear current textBlock
if (!(d is TextBlock textBlock)) {
return;
}
// private static void FormattedTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
// // Clear current textBlock
// if (!(d is TextBlock textBlock)) {
// return;
// }
//
// textBlock.ClearValue(TextBlock.TextProperty);
// textBlock.Inlines.Clear();
//
// // Create new formatted text
// var lineHeight = textBlock.FontFamily.LineSpacing * textBlock.FontSize;
// foreach (var inline in ChunksToTextBlock(lineHeight, (ServerMessage)e.NewValue)) {
// textBlock.Inlines.Add(inline);
// }
// }
textBlock.ClearValue(TextBlock.TextProperty);
textBlock.Inlines.Clear();
// Create new formatted text
var lineHeight = textBlock.FontFamily.LineSpacing * textBlock.FontSize;
foreach (var inline in ChunksToTextBlock(lineHeight, (ServerMessage)e.NewValue)) {
textBlock.Inlines.Add(inline);
}
}
private static IEnumerable<Inline> ChunksToTextBlock(double lineHeight, ServerMessage message) {
public static IEnumerable<Inline> ChunksToTextBlock(double lineHeight, ServerMessage message, Tab tab) {
var elements = new List<Inline>();
var timestampString = message.Timestamp.ToLocalTime().ToString("t", CultureInfo.CurrentUICulture);
@ -65,36 +66,24 @@ namespace XIVChat_Desktop {
var brush = new SolidColorBrush(Color.FromArgb(a, r, g, b));
var style = textChunk.Italic ? FontStyles.Italic : FontStyles.Normal;
//var part = string.Empty;
//foreach (char c in textChunk.Content) {
// if (c >= '\ue000' && c <= '\uf8ff') {
// // private use
if (tab.ProcessMarkdown) {
var inlines = Markdown.MarkdownToInlines(textChunk.Content);
// // add existing text if necessary
// if (part.Length != 0) {
// elements.Add(new Run(part) {
// Foreground = brush,
// FontStyle = style,
// });
// part = string.Empty;
// }
foreach (var inline in inlines) {
inline.Foreground = brush;
if (inline.FontStyle == FontStyles.Normal) {
inline.FontStyle = style;
}
// // add private use segment with font
// elements.Add(new Run(c.ToString()) {
// Foreground = brush,
// FontStyle = style,
// FontFamily = new FontFamily(new Uri("pack://application:,,,/"), "/fonts/#XIV AXIS Std ATK"),
// });
// continue;
// }
elements.Add(inline);
}
} else {
elements.Add(new Run(textChunk.Content) {
Foreground = brush,
FontStyle = style,
});
}
// part += c;
//}
elements.Add(new Run(textChunk.Content) {
Foreground = brush,
FontStyle = style,
});
break;
case IconChunk iconChunk:
var bounds = GetBounds(iconChunk.index);

View File

@ -17,5 +17,53 @@ namespace XIVChat_Desktop {
public static void Dispatch(this DispatcherObject dispatcherObj, DispatcherPriority priority, Action action) {
dispatcherObj.Dispatcher.BeginInvoke(priority, action);
}
public static bool IsAsciiPunctuation(this char c) {
// 2.1 Characters and lines
// An ASCII punctuation character is !, ", #, $, %, &, ', (, ), *, +, ,, -, ., /, :, ;, <, =, >, ?, @, [, \, ], ^, _, `, {, |, }, or ~.
switch (c) {
case '!':
case '"':
case '#':
case '$':
case '%':
case '&':
case '\'':
case '(':
case ')':
case '*':
case '+':
case ',':
case '-':
case '.':
case '/':
case ':':
case ';':
case '<':
case '=':
case '>':
case '?':
case '@':
case '[':
case '\\':
case ']':
case '^':
case '_':
case '`':
case '{':
case '|':
case '}':
case '~':
return true;
}
return false;
}
public static bool IsWhitespace(this char c) {
// 2.1 Characters and lines
// A whitespace character is a space(U + 0020), tab(U + 0009), newline(U + 000A), line tabulation (U + 000B), form feed (U + 000C), or carriage return (U + 000D).
return c <= ' ' && (c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r');
}
}
}