feat: add tab-based markdown and selection
This commit is contained in:
parent
178950bee9
commit
c012fb5f77
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)!;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue