diff --git a/README.md b/README.md index c013be8..9f79349 100644 --- a/README.md +++ b/README.md @@ -1 +1,2 @@ -# WpfTex \ No newline at end of file +# WpfTex + diff --git a/WpfTex/LatexEditor/LatexDocument.cs b/WpfTex/LatexEditor/LatexDocument.cs index 153d89b..501c8ad 100644 --- a/WpfTex/LatexEditor/LatexDocument.cs +++ b/WpfTex/LatexEditor/LatexDocument.cs @@ -1,148 +1,15 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Linq; using System.Text; -using System.Text.RegularExpressions; using System.Threading.Tasks; -using System.Windows; -using System.Windows.Markup; using System.Windows.Media; -using LatexEditor.Fonts; +using System.Windows.Media.TextFormatting; namespace LatexEditor { - [ContentProperty("Content")] - public class LatexDocument : FrameworkElement + public class LatexDocument { - public static readonly DependencyProperty ContentProperty = - DependencyProperty.Register("Content", typeof(string), typeof(LatexDocument), - new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.AffectsRender)); - [Category("LaTeX")] - [Description("LaTeX markup")] - public string Content - { - get => (string)GetValue(ContentProperty); - set => SetValue(ContentProperty, value); - } - - public static readonly DependencyProperty FontSizeProperty = - DependencyProperty.Register("FontSize", typeof(double), typeof(LatexDocument), - new FrameworkPropertyMetadata(20d, FrameworkPropertyMetadataOptions.AffectsRender)); - - [Category("LaTeX")] - [Description("LaTeX font scaling")] - public double FontSize - { - get => (double)GetValue(FontSizeProperty); - set => SetValue(FontSizeProperty, value); - } - - protected List Parse(string latex) - { - var x = 0d; - var y = 0d; - var font = CmFont.SerifItalic; - var size = FontSize; - - var glyphs = new List(latex.Length); - - var state = new Stack(); - state.Push("-"); - - while (latex.Length > 0) - { - var s = state.Peek(); - - if (s == "cmd") - { - state.Pop(); - var m = Regex.Match(latex, @"([a-z]+)(?=[^a-z]|$)", RegexOptions.IgnoreCase); - if (m.Index == 0) - { - var cmd = latex.Substring(0, m.Groups[1].Length); - switch (cmd) - { - case "it": - font = CmFont.SerifItalic; - break; - case "sym": - font = CmFont.Symbols; - break; - case "rm": - font = CmFont.Serif; - break; - case "sup": - size = FontSize * 0.4; - y = FontSize * (1 - 0.4); - break; - case "sub": - size = FontSize * 0.5; - y = -FontSize * 0.5 * 0.5; - break; - default: - continue; - } - latex = latex.Substring(m.Length).TrimStart(' '); - } - else - { - latex = latex.Substring(1); - } - } - else - { - if (latex.StartsWith("\\")) - { - latex = latex.Substring(1); - - state.Push("cmd"); - } - else - { - var chr = latex[0]; - latex = latex.Substring(1); - - var gtf = font.GlyphTypeface(); - var point = new Point(x, y); - var gi = new GlyphInfo(font, chr, size, point); - x += gtf.AdvanceWidths[gi.Index] * size; - glyphs.Add(gi); - } - } - } - - return glyphs; - } - - protected override void OnRender(DrawingContext dc) - { - var text = Content; - - if (string.IsNullOrEmpty(text)) return; - - var glyphInfoList = Parse(text); - - var gtfGroups = glyphInfoList.GroupBy(gi => gi.Gtf); - foreach (var gtfGroup in gtfGroups) - { - var gtf = gtfGroup.Key; - - var sizeGroups = gtfGroup.GroupBy(gi => gi.Size); - foreach (var sizeGroup in sizeGroups) - { - var size = sizeGroup.Key; - - var glyphs = sizeGroup.Select(gi => gi.Index).ToList(); - var advanceWidths = new double[glyphs.Count]; - var offsets = sizeGroup.Select(gi => gi.Offset).ToList(); - var gr = new GlyphRun(gtf, 0, false, size, - glyphs, new Point(0, FontSize), advanceWidths, - offsets, null, null, null, null, null); - dc.DrawGlyphRun(Brushes.Black, gr); - } - } - } } } diff --git a/WpfTex/LatexEditor/LatexEditor.csproj b/WpfTex/LatexEditor/LatexEditor.csproj index 75d3341..6881c74 100644 --- a/WpfTex/LatexEditor/LatexEditor.csproj +++ b/WpfTex/LatexEditor/LatexEditor.csproj @@ -34,6 +34,9 @@ 4 + + ..\packages\JetBrains.Annotations.10.2.1\lib\net\JetBrains.Annotations.dll + @@ -66,12 +69,17 @@ + MainWindow.xaml Code + + + + Code @@ -87,6 +95,7 @@ ResXFileCodeGenerator + Designer Resources.Designer.cs @@ -150,6 +159,7 @@ + SettingsSingleFileGenerator Settings.Designer.cs diff --git a/WpfTex/LatexEditor/LatexViewer.cs b/WpfTex/LatexEditor/LatexViewer.cs new file mode 100644 index 0000000..073fa6f --- /dev/null +++ b/WpfTex/LatexEditor/LatexViewer.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Markup; +using System.Windows.Media; +using LatexEditor.Fonts; +using LatexEditor.Parser; + +namespace LatexEditor +{ + [ContentProperty("Content")] + public class LatexViewer : FrameworkElement + { + public static readonly DependencyProperty ContentProperty = + DependencyProperty.Register("Content", typeof(string), typeof(LatexViewer), + new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.AffectsRender)); + + [Category("LaTeX")] + [Description("LaTeX markup")] + public string Content + { + get => (string) GetValue(ContentProperty); + set => SetValue(ContentProperty, value); + } + + public static readonly DependencyProperty FontSizeProperty = + DependencyProperty.Register("FontSize", typeof(double), typeof(LatexViewer), + new FrameworkPropertyMetadata(20d, FrameworkPropertyMetadataOptions.AffectsRender)); + + [Category("LaTeX")] + [Description("LaTeX font scaling")] + public double FontSize + { + get => (double) GetValue(FontSizeProperty); + set => SetValue(FontSizeProperty, value); + } + + protected List Parse(string latex) + { + var x = 0d; + var y = 0d; + var font = CmFont.SerifItalic; + var size = FontSize; + + var glyphs = new List(latex.Length); + + var state = new Stack(); + state.Push("-"); + + while (latex.Length > 0) + { + var s = state.Peek(); + + if (s == "cmd") + { + state.Pop(); + var m = Regex.Match(latex, @"([a-z]+)(?=[^a-z]|$)", RegexOptions.IgnoreCase); + if (m.Index == 0) + { + var cmd = latex.Substring(0, m.Groups[1].Length); + switch (cmd) + { + case "it": + font = CmFont.SerifItalic; + break; + case "sym": + font = CmFont.Symbols; + break; + case "rm": + font = CmFont.Serif; + break; + case "sup": + size = FontSize * 0.4; + y = FontSize * (1 - 0.4); + break; + case "sub": + size = FontSize * 0.5; + y = -FontSize * 0.5 * 0.5; + break; + default: + continue; + } + latex = latex.Substring(m.Length).TrimStart(' '); + } + else + { + latex = latex.Substring(1); + } + } + else + { + if (latex.StartsWith("\\")) + { + latex = latex.Substring(1); + + state.Push("cmd"); + } + else + { + var chr = latex[0]; + latex = latex.Substring(1); + + var gtf = font.GlyphTypeface(); + var point = new Point(x, y); + var gi = new GlyphInfo(font, chr, size, point); + x += gtf.AdvanceWidths[gi.Index] * size; + glyphs.Add(gi); + } + } + } + + return glyphs; + } + + protected override void OnRender(DrawingContext dc) + { + Debug.WriteLine(dc, nameof(LatexViewer)); + Debug.WriteLine(dc.GetType(), nameof(LatexViewer)); + + if (string.IsNullOrEmpty(Content)) return; + + var glyphInfoList = LatexParser.Parse(Content); + + var gtfGroups = glyphInfoList.GroupBy(gi => gi.Gtf); + foreach (var gtfGroup in gtfGroups) + { + var gtf = gtfGroup.Key; + + var sizeGroups = gtfGroup.GroupBy(gi => gi.Size); + foreach (var sizeGroup in sizeGroups) + { + var size = sizeGroup.Key; + + var glyphs = sizeGroup.Select(gi => gi.Index).ToList(); + var advanceWidths = new double[glyphs.Count]; + var offsets = sizeGroup.Select(gi => gi.Offset).ToList(); + var gr = new GlyphRun(gtf, 0, false, size, + glyphs, new Point(0, FontSize), advanceWidths, + offsets, null, null, null, null, null); + + dc.DrawGlyphRun(Brushes.Black, gr); + } + } + } + } +} \ No newline at end of file diff --git a/WpfTex/LatexEditor/MainWindow.xaml b/WpfTex/LatexEditor/MainWindow.xaml index ec81942..31004be 100644 --- a/WpfTex/LatexEditor/MainWindow.xaml +++ b/WpfTex/LatexEditor/MainWindow.xaml @@ -7,8 +7,22 @@ mc:Ignorable="d" Title="MainWindow" Height="350" Width="525"> - - \sym h \it x, y \sym i - + + + A \ \alpha \quad N \ \nu \\ + B \ \beta \quad \Xi \ \xi \\ + \Gamma \ \gamma \quad O \ \omicron \\ + \Delta \ \delta \quad \Pi \ \pi \\ + E \ \epsilon \quad P \ \rho \\ + Z \ \zeta \quad \Sigma \ \sigma \\ + H \ \eta \quad T \ \tau \\ + \Theta \ \theta \quad Y \ \upsilon \\ + I \ \iota \quad \Phi \ \phi \\ + K \ \kappa \quad X \ \chi \\ + \Lambda \ \lambda \quad \Psi \ \psi \\ + M \ \mu \quad \Omega \ \omega + + + \ No newline at end of file diff --git a/WpfTex/LatexEditor/Parser/LatexParser.cs b/WpfTex/LatexEditor/Parser/LatexParser.cs new file mode 100644 index 0000000..8121c64 --- /dev/null +++ b/WpfTex/LatexEditor/Parser/LatexParser.cs @@ -0,0 +1,125 @@ +using System.Collections.Generic; +using System.Windows; +using LatexEditor.Fonts; + +namespace LatexEditor.Parser +{ + public static class LatexParser + { + private static readonly Lexer LatexLexer = new Lexer() + { + new TokenDescriptor("whitespace", @"\s+"), + new TokenDescriptor("escape", @"\\([\#\$\%\^\&_\{\}\~\\])", 1), + new TokenDescriptor("command", @"\\([^\d\s\\]+|[\ ])", 1), + new TokenDescriptor("command", @"[\#\$\%\^\&_]"), + new TokenDescriptor("number", @"\d"), + new TokenDescriptor("open", @"\{"), + new TokenDescriptor("close", @"\}"), + new TokenDescriptor("letter", @"[\S]"), // nearly catch-all for uncaptured symbols + }; + + private static readonly Dictionary greekLetters = new Dictionary + { + ["alpha"] = 0x03b1, + ["beta"] = 0x03b2, + ["gamma"] = 0x03b3, + ["delta"] = 0x03b4, + ["epsilon"] = 0x03b5, + ["zeta"] = 0x03b6, + ["eta"] = 0x03b7, + ["theta"] = 0x03b8, + ["iota"] = 0x03b9, + ["kappa"] = 0x03ba, + ["lambda"] = 0x03bb, + ["mu"] = 0x03bc, + ["nu"] = 0x03bd, + ["xi"] = 0x03be, + ["omicron"] = 0x03bf, + ["pi"] = 0x03c0, + ["rho"] = 0x03c1, + ["sigma"] = 0x03c2, + ["tau"] = 0x03c4, + ["upsilon"] = 0x03c5, + ["phi"] = 0x03c6, + ["chi"] = 0x03c7, + ["psi"] = 0x03c8, + ["omega"] = 0x03c9, + + ["Gamma"] = 0x0393, + ["Delta"] = 0x0394, + ["Theta"] = 0x0398, + ["Lambda"] = 0x039b, + ["Xi"] = 0x039e, + ["Pi"] = 0x03a0, + ["Sigma"] = 0x03a3, + ["Phi"] = 0x03a6, + ["Psi"] = 0x03a8, + ["Omega"] = 0x03a9, + }; + + private static readonly Dictionary spaces = new Dictionary + { + [","] = 0.16666, + ["!"] = -0.16666, + [">"] = .3, + [":"] = .3, + [";"] = .4, + [" "] = .5, + ["quad"] = 1, + ["qquad"] = 4, + }; + + public static IEnumerable Parse(string latex) => Parse(LatexLexer.Tokenize(latex)); + + private static IEnumerable Parse(IEnumerable tokens) + { + var tokenQ = new Queue(tokens); + var glyphs = new List(); + + const double size = 20; + var x = 0d; + var y = 0d; + + void AddGlyph(CmFont font, int c) + { + glyphs.Add(new GlyphInfo(font, c, size, new Point(x * size, y * size))); + var gtf = font.GlyphTypeface(); + var idx = gtf.CharacterToGlyphMap[c]; + x += gtf.AdvanceWidths[idx]; + } + + while (tokenQ.Count > 0) + { + var token = tokenQ.Dequeue(); + var val = token.Value; + switch (token.TokenName) + { + case "letter": + AddGlyph(CmFont.SerifItalic, val[0]); + break; + case "number": + AddGlyph(CmFont.Serif, val[0]); + break; + case "command": + if (greekLetters.ContainsKey(val)) + AddGlyph(CmFont.SerifItalic, greekLetters[val]); + else if (spaces.ContainsKey(val)) + x += spaces[val]; + else + foreach (var c in token.CapturedString) + AddGlyph(CmFont.SerifItalic, c); + break; + case "escape": + if (val == "\\") + { + x = 0; + y -= 1; + } + break; + } + } + + return glyphs; + } + } +} \ No newline at end of file diff --git a/WpfTex/LatexEditor/Parser/Lexer.cs b/WpfTex/LatexEditor/Parser/Lexer.cs new file mode 100644 index 0000000..17a4fbe --- /dev/null +++ b/WpfTex/LatexEditor/Parser/Lexer.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net.NetworkInformation; + +namespace LatexEditor.Parser +{ + + public class Lexer : IEnumerable + { + public List TokenDescriptors { get; } + + public Lexer() : this(new List()) + { + } + + public Lexer(List tokens) + { + TokenDescriptors = tokens; + } + + public IEnumerable Tokenize(string text) + { + while (!string.IsNullOrEmpty(text)) + { + Token cap = null; + foreach (var ltd in TokenDescriptors) + if (ltd.TryBreakString(ref text, out cap)) + break; + + if (cap == null) + text = text.Substring(1); + else + yield return cap; + } + } + + public void Add(TokenDescriptor td) => TokenDescriptors.Add(td); + + public IEnumerator GetEnumerator() + { + return TokenDescriptors.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable) TokenDescriptors).GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/WpfTex/LatexEditor/Parser/Token.cs b/WpfTex/LatexEditor/Parser/Token.cs new file mode 100644 index 0000000..de330a2 --- /dev/null +++ b/WpfTex/LatexEditor/Parser/Token.cs @@ -0,0 +1,21 @@ +namespace LatexEditor.Parser +{ + public class Token + { + public string TokenName { get; } + public string Value { get; } + public string CapturedString { get; } + + public Token(string tokenName, string value, string capturedString) + { + TokenName = tokenName; + Value = value; + CapturedString = capturedString; + } + + public override string ToString() + { + return $"\"{Value}\" ({TokenName})"; + } + } +} \ No newline at end of file diff --git a/WpfTex/LatexEditor/Parser/TokenDescriptor.cs b/WpfTex/LatexEditor/Parser/TokenDescriptor.cs new file mode 100644 index 0000000..b94ad53 --- /dev/null +++ b/WpfTex/LatexEditor/Parser/TokenDescriptor.cs @@ -0,0 +1,34 @@ +using System.Text.RegularExpressions; +using JetBrains.Annotations; + +namespace LatexEditor.Parser +{ + public class TokenDescriptor + { + public string TokenName { get; } + public int CaptureGroup { get; } + public Regex Regex { get; } + + public TokenDescriptor(string tokenName, [RegexPattern, NotNull] string pattern, int captureGroup = 0) + { + TokenName = tokenName; + CaptureGroup = captureGroup; + Regex = new Regex("^" + pattern); + } + + public bool TryBreakString(ref string text, out Token capture) + { + var match = Regex.Match(text); + + if (!match.Success) + { + capture = null; + return false; + } + + capture = new Token(TokenName, match.Groups[CaptureGroup].Value, match.Value); + text = text.Substring(match.Length); + return true; + } + } +} \ No newline at end of file diff --git a/WpfTex/LatexEditor/Properties/Resources.Designer.cs b/WpfTex/LatexEditor/Properties/Resources.Designer.cs index 0fe7719..25b6b0e 100644 --- a/WpfTex/LatexEditor/Properties/Resources.Designer.cs +++ b/WpfTex/LatexEditor/Properties/Resources.Designer.cs @@ -8,10 +8,10 @@ // //------------------------------------------------------------------------------ -namespace LatexEditor.Properties -{ - - +namespace LatexEditor.Properties { + using System; + + /// /// A strongly-typed resource class, for looking up localized strings, etc. /// @@ -22,48 +22,40 @@ namespace LatexEditor.Properties [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources - { - + internal class Resources { + private static global::System.Resources.ResourceManager resourceMan; - + private static global::System.Globalization.CultureInfo resourceCulture; - + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() - { + internal Resources() { } - + /// /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager - { - get - { - if ((resourceMan == null)) - { + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("LatexEditor.Properties.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; } } - + /// /// Overrides the current thread's CurrentUICulture property for all /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture - { - get - { + internal static global::System.Globalization.CultureInfo Culture { + get { return resourceCulture; } - set - { + set { resourceCulture = value; } } diff --git a/WpfTex/LatexEditor/Properties/Settings.settings b/WpfTex/LatexEditor/Properties/Settings.settings index 033d7a5..8e2caba 100644 --- a/WpfTex/LatexEditor/Properties/Settings.settings +++ b/WpfTex/LatexEditor/Properties/Settings.settings @@ -1,7 +1,7 @@  - + \ No newline at end of file diff --git a/WpfTex/LatexEditor/packages.config b/WpfTex/LatexEditor/packages.config new file mode 100644 index 0000000..b26fcb6 --- /dev/null +++ b/WpfTex/LatexEditor/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file