diff --git a/docs/assets/images/options.jpg b/docs/assets/images/options.jpg index a26207f1..608e98c3 100644 Binary files a/docs/assets/images/options.jpg and b/docs/assets/images/options.jpg differ diff --git a/docs/help/options.md b/docs/help/options.md index a2a8ef68..1daf9971 100644 --- a/docs/help/options.md +++ b/docs/help/options.md @@ -14,12 +14,12 @@ Options that enable or disable the collection of anonymous app usage information The options dialog allows you to select a color theme, light or dark, or configure the specific colors used in each type of node as well as the font used and the background color. A drop down arrow on each color lets you pick from different color palettes. You can also customize the font that is used in the XML Notepad tree view. ### Editor -The editor to use if the XML file is invalid -and cannot be opened by XML Notepad. +The editor to use if the XML file is invalid and cannot be opened by XML Notepad. ### Formatting -You can also configure the formatting options that are used when you save an XML file, or turn off formatting altogether. You can also configure the -TreeView indentation level in pixels. +You can also configure the formatting options that are used when you save an XML file, or turn off formatting altogether. Preserve Whitepsace also controls how the file is opened, when true you will +see all the whitespace nodes in the document, which are used also when the file is saved. +You can also configure the TreeView indentation level in pixels. ### Language Specify which language annotations to pick from associated XSD schemas. @@ -27,13 +27,18 @@ Specify which language annotations to pick from associated XSD schemas. ### Long Lines How to deal with editing of long lines. +### Schema Options +Controls whether the tree view shows special information with the nodes in the tree. +It will promote the text of the specially named child nodes to the text of the parent +node in the tree. + ### Settings Location Where to store these settings. See [Settings](settings.md) for more information. ### Updates -You can also configure the auto-update mechanism associated with +These settings configure the auto-update mechanism associated with the ClickOnce installer. If the "Enable updates" field is true, then XML Notepad will ping the specified "Update Location" for an "Updates.xml" file to see if a new version of XML Notepad is available. See [Updates](updates.md) for more information on how this works. ### Validation @@ -51,3 +56,4 @@ on documents that have no `Resources\XmlNote.ico XmlNotepad.Program $(MyKeyFile) - - + + + + v4.8 3.5 false - + + ..\..\publish\ true Disk @@ -37,6 +40,8 @@ Chris Lovett readme.htm true + 0 + 2.9.0.8 false true true @@ -68,7 +73,8 @@ 50D58171E20BB6188B199ACA7C20A26DA0DFBF35 - + + true @@ -104,10 +110,10 @@ - - ..\packages\System.Security.AccessControl.5.0.0\lib\net461\System.Security.AccessControl.dll - True - + + ..\packages\System.Security.AccessControl.5.0.0\lib\net461\System.Security.AccessControl.dll + True + ..\packages\System.Security.Principal.Windows.5.0.0\lib\net461\System.Security.Principal.Windows.dll @@ -172,7 +178,8 @@ FormSearch.cs - + + @@ -301,64 +308,80 @@ False - - + + + + Include True File False - - + + + + Include True File False - - + + + + Include True File False - - + + + + Include True File False - - + + + + Include True File False - - + + + + Include True File False - - + + + + Include True File False - - + + + + Include True File diff --git a/src/Application/FormMain.cs b/src/Application/FormMain.cs index 95bb9a4d..3d03bf44 100644 --- a/src/Application/FormMain.cs +++ b/src/Application/FormMain.cs @@ -23,7 +23,7 @@ namespace XmlNotepad /// /// Summary description for Form1. /// - public partial class FormMain : Form, ISite + public partial class FormMain : Form, ISite, ITrustService { private readonly UndoManager _undoManager; private Settings _settings = new Settings(); @@ -179,7 +179,7 @@ public FormMain(SettingsLocation location) private void DispatchAction(Action action) { - if (!this.Disposing) + if (!this.Disposing && !this.closing) { ISynchronizeInvoke si = (ISynchronizeInvoke)this; if (si.InvokeRequired) @@ -505,6 +505,7 @@ protected override void OnClosing(CancelEventArgs e) return; } } + this.closing = true; this._delayedActions.Close(); SaveConfig(); base.OnClosing(e); @@ -567,6 +568,7 @@ protected override void OnLayout(LayoutEventArgs levent) /// protected override void Dispose(bool disposing) { + this.closing = true; if (disposing) { if (components != null) @@ -2927,6 +2929,7 @@ private void goToLineToolStripMenuItem_Click(object sender, EventArgs e) TextBox xPos; TextBox yPos; TextBox status; + private bool closing; internal void ShowMousePosition() { @@ -2977,6 +2980,19 @@ private void Panel_MouseMove(object sender, MouseEventArgs e) xPos.Text = e.X.ToString(); yPos.Text = e.Y.ToString(); } + + public async System.Threading.Tasks.Task CanTrustUrl(Uri location) + { + bool result = false; + this.Invoke(new Action(() => + { + result = MessageBox.Show(this, SR.XslScriptCodePrompt, SR.XslScriptCodeCaption, + MessageBoxButtons.YesNo, MessageBoxIcon.Exclamation) == DialogResult.Yes; + } + )); + await System.Threading.Tasks.Task.CompletedTask; + return result; + } #endregion } diff --git a/src/Application/FormOptions.cs b/src/Application/FormOptions.cs index 80ff4d77..e373f894 100644 --- a/src/Application/FormOptions.cs +++ b/src/Application/FormOptions.cs @@ -214,6 +214,7 @@ public class UserSettings private int _indentLevel; private IndentChar _indentChar; private string _newLineChars; + private bool _preserveWhitespace; private string _language; private int _maximumLineLength; private int _maximumValueLength; @@ -252,6 +253,7 @@ public UserSettings(Settings s) _indentLevel = this._settings.GetInteger("IndentLevel"); _indentChar = (IndentChar)this._settings["IndentChar"]; _newLineChars = this._settings.GetString("NewLineChars"); + _preserveWhitespace = this._settings.GetBoolean("PreserveWhitespace"); _language = this._settings.GetString("Language"); _settingsLocation = this._settings.GetLocation(); _schemaAwareText = this._settings.GetBoolean("SchemaAwareText"); @@ -348,6 +350,7 @@ public void Apply() this._settings["IndentLevel"] = _indentLevel; this._settings["IndentChar"] = _indentChar; this._settings["NewLineChars"] = _newLineChars; + this._settings["PreserveWhitespace"] = _preserveWhitespace; this._settings["NoByteOrderMark"] = _noByteOrderMark; this._settings.SetLocation(_settingsLocation); @@ -765,6 +768,21 @@ public string NewLineChars } } + [SRCategory("FormatCategory")] + [LocDisplayName("PreserveWhitespace")] + [SRDescription("PreserveWhitespaceDescription")] + public bool PreserveWhitespace + { + get + { + return this._preserveWhitespace; + } + set + { + this._preserveWhitespace = value; + } + } + [SRCategory("FormatCategory")] [LocDisplayName("NoByteOrderMark")] [SRDescription("NoByteOrderMarkDescription")] diff --git a/src/Model/DelayedAction.cs b/src/Model/DelayedAction.cs index ac710830..91609752 100644 --- a/src/Model/DelayedAction.cs +++ b/src/Model/DelayedAction.cs @@ -144,7 +144,6 @@ internal void OnDelayTimerTick(object state) { try { - Debug.WriteLine("invoking delayed action: " + this.name); a(); } catch (Exception ex) diff --git a/src/Model/DomLoader.cs b/src/Model/DomLoader.cs index 5d53535f..884dc302 100644 --- a/src/Model/DomLoader.cs +++ b/src/Model/DomLoader.cs @@ -179,6 +179,7 @@ public XmlDocument Load(XmlReader r) this._lineInfos = new List(); this._firstRealLine = null; this._doc = new XmlDocument(); + this._doc.PreserveWhitespace = Settings.Instance.GetBoolean("PreserveWhitespace"); this._doc.XmlResolver = Settings.Instance.Resolver; this._doc.Schemas.XmlResolver = Settings.Instance.Resolver; SetLoading(this._doc, true); @@ -206,7 +207,7 @@ void SetLoading(XmlDocument doc, bool flag) private void LoadDocument() { - bool preserveWhitespace = false; + bool preserveWhitespace = this._doc.PreserveWhitespace; XmlReader r = this._reader; XmlNode parent = this._doc; XmlElement element; diff --git a/src/Model/Settings.cs b/src/Model/Settings.cs index 6b517b35..5ad64b5f 100644 --- a/src/Model/Settings.cs +++ b/src/Model/Settings.cs @@ -932,6 +932,7 @@ public void SetDefaults() this["IndentLevel"] = 2; this["IndentChar"] = IndentChar.Space; this["NewLineChars"] = Settings.EscapeNewLines("\r\n"); + this["PreserveWhitespace"] = false; this["Language"] = ""; this["NoByteOrderMark"] = false; diff --git a/src/Model/XInclude.cs b/src/Model/XInclude.cs index e89f1694..87bc1f64 100644 --- a/src/Model/XInclude.cs +++ b/src/Model/XInclude.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Xml; namespace XmlNotepad @@ -16,6 +17,9 @@ public class XmlIncludeReader : XmlReader, IXmlLineInfo private Stack _stack = new Stack(); private Stack _baseUris = new Stack(); private Uri _baseUri; + private bool _cancelled; + private long _position; + private long _size; public const string XIncludeNamespaceUri = "http://www.w3.org/2001/XInclude"; @@ -27,6 +31,15 @@ public static XmlIncludeReader CreateIncludeReader(string url, XmlReaderSettings return r; } + public void Cancel() + { + this._cancelled = true; + } + + public long Position => _position; + + public long Size => _size; + // [cjl] dead code removal //public static XmlIncludeReader CreateIncludeReader(Stream stream, XmlReaderSettings settings, string baseUri) { // XmlIncludeReader r = new XmlIncludeReader(); @@ -55,9 +68,25 @@ public static XmlIncludeReader CreateIncludeReader(XmlDocument doc, XmlReaderSet r._reader = new XmlNodeReader(doc); r._settings = settings; r._baseUri = new Uri(baseUri); + r._size = CountNodes(doc); + r._position = 0; return r; } + static long CountNodes(XmlNode node) + { + long count = 1; + for (var child = node.FirstChild; child != null; child = child.NextSibling) + { + count++; + if (child.HasChildNodes) + { + count += CountNodes(child); + } + } + return count; + } + public override XmlNodeType NodeType { get { return _reader.NodeType; } @@ -197,6 +226,12 @@ public override bool MoveToElement() /// we have reached the end of the top level document. public override bool Read() { + if (_cancelled) + { + throw new System.Threading.Tasks.TaskCanceledException(); + } + _position++; + bool rc = _reader.Read(); pop: while (!rc && _stack.Count > 0) @@ -262,6 +297,7 @@ bool ExpandInclude() _stack.Push(_reader); _baseUris.Push(resolved); _reader = new XmlNodeReader(include); + _size += CountNodes(include); return _reader.Read(); // initialize reader to first node in document. } } @@ -274,6 +310,7 @@ bool ExpandInclude() _stack.Push(_reader); _reader = new XmlNodeReader(fallback); _reader.Read(); // initialize reader + _size += CountNodes(fallback); return _reader.Read(); // consume fallback start tag. } else diff --git a/src/Model/XmlCache.cs b/src/Model/XmlCache.cs index e3f3690b..c3d18c29 100644 --- a/src/Model/XmlCache.cs +++ b/src/Model/XmlCache.cs @@ -227,6 +227,7 @@ public XmlReaderSettings GetReaderSettings() settings.DtdProcessing = this._settings.GetBoolean("IgnoreDTD") ? DtdProcessing.Ignore : DtdProcessing.Parse; settings.CheckCharacters = false; settings.XmlResolver = Settings.Instance.Resolver; + settings.IgnoreWhitespace = false; return settings; } diff --git a/src/XmlNotepad/AsyncXslt.cs b/src/XmlNotepad/AsyncXslt.cs new file mode 100644 index 00000000..6f15a660 --- /dev/null +++ b/src/XmlNotepad/AsyncXslt.cs @@ -0,0 +1,651 @@ +using System; +using System.Threading.Tasks; +using System.Xml.Xsl; +using System.Xml; +using System.IO; +using System.Drawing; +using System.Threading; +using SysTask = System.Threading.Tasks.Task; +using System.Diagnostics; +using System.Runtime.Remoting.Contexts; +using System.Windows.Forms; +using System.Collections.Generic; +using SR = XmlNotepad.StringResources; +using System.ComponentModel; +using System.Xml.XPath; + +namespace XmlNotepad +{ + + internal class AsyncXsltContext + { + public Uri baseUri; + public XmlDocument document; + // The xslt file to use + public string xsltfilename; + // Output file name hint and updated to real output path when finished. + public string outpath; + // Whether output name is non-negotiable. + public bool userSpecifiedOutput; + // whether DOM has this.inner.CanRead; + + public override bool CanSeek => this.inner.CanSeek; + + public override bool CanWrite => this.inner.CanWrite; + + public override long Length + { + get + { + if (!disposed) + { + lastLength = this.inner.Length; + } + return lastLength; + } + } + + public override long Position + { + get + { + if (!disposed) + { + lastPosition = this.inner.Position; + } + return lastPosition; + } + set => this.inner.Position = value; + } + + public override void Flush() + { + this.inner.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (cancelled) + { + throw new OperationCanceledException(); + } + return this.inner.Read(buffer, offset, count); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return this.inner.Seek(offset, origin); + } + + public override void SetLength(long value) + { + this.inner.SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + this.inner.Write(buffer, offset, count); + if (this.EstimatedSize < this.Position) + { + this.EstimatedSize = this.Position * 2; + } + if (cancelled) + { + disposed = true; + throw new OperationCanceledException(); + } + } + } + + internal class AsyncXslt + { + private XslCompiledTransform _xslt; + private XmlDocument _xsltdoc; + private XslCompiledTransform _defaultss; + private Uri _xsltUri; + private DateTime _loaded; + private string _tempFile; + private bool _usingDefaultXslt; + private Settings _settings; + AsyncXsltContext _context; + private readonly IDictionary _trusted = new Dictionary(); + private ITrustService _trustService; + + public AsyncXslt(ISite site) + { + _settings = Settings.Instance; + _trustService = (ITrustService)site.GetService(typeof(ITrustService)); + } + + public void Close() + { + if (_context != null) + { + _context.Cancel(); + } + this.CleanupTempFile(); + } + + public bool UsingDefaultXslt => this._usingDefaultXslt; + + public void ResetDefaultStyleSheet() + { + _defaultss = null; + } + + /// + /// Run an XSLT transform and show the results. + /// + /// The info for the transform + public async System.Threading.Tasks.Task TransformDocumentAsync(AsyncXsltContext context) + { + this.CleanupTempFile(); + this._context = context; + await SysTask.Run(RunTransform); + return context.outpath; + } + + async System.Threading.Tasks.Task RunTransform() + { + Uri resolved = null; + string outpath = this._context.outpath; + XmlDocument context = this._context.document; + this._context.running = true; + bool trustRetry = true; + while (trustRetry) + { + trustRetry = false; + try + { + XslCompiledTransform transform; + if (string.IsNullOrEmpty(this._context.xsltfilename)) + { + transform = GetDefaultStylesheet(); + this._usingDefaultXslt = true; + if (this._settings.GetBoolean("DisableDefaultXslt")) + { + context = new XmlDocument(); + context.LoadXml("Default styling of your XML documents is disabled in your Options"); + } + } + else + { + resolved = new Uri(_context.baseUri, this._context.xsltfilename); + if (resolved != this._xsltUri || IsModified()) + { + _xslt = new XslCompiledTransform(); + this._loaded = DateTime.Now; + var settings = new XsltSettings(true, this._context.enableScripts); + settings.EnableScript = (_trusted.ContainsKey(resolved)); + var rs = new XmlReaderSettings(); + rs.DtdProcessing = this._context.ignoreDTD ? DtdProcessing.Ignore : DtdProcessing.Parse; + rs.XmlResolver = this._context.resolver; + using (XmlReader r = XmlReader.Create(resolved.AbsoluteUri, rs)) + { + _xslt.Load(r, settings, this._context.resolver); + } + + // the XSLT DOM is also handy to have around for GetOutputMethod + this._xsltdoc = new XmlDocument(); + this._xsltdoc.Load(resolved.AbsoluteUri); + } + transform = _xslt; + this._usingDefaultXslt = false; + } + + if (string.IsNullOrEmpty(outpath)) + { + if (!_context.disableOutputFile) + { + if (!string.IsNullOrEmpty(this._context.xsltfilename)) + { + outpath = this.GetXsltOutputFileName(this._context.xsltfilename); + } + else + { + // default stylesheet produces html + this._tempFile = outpath = GetWritableFileName("DefaultXsltOutput.htm"); + } + } + } + else if (!_context.userSpecifiedOutput) + { + var ext = GetDefaultOutputExtension(); + var basePath = Path.Combine(Path.GetDirectoryName(outpath), Path.GetFileNameWithoutExtension(outpath)); + outpath = basePath + ext; + outpath = GetWritableFileName(outpath); + } + else + { + outpath = GetWritableFileName(outpath); + } + + if (null != transform) + { + var dir = Path.GetDirectoryName(outpath); + if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + + var settings = new XmlReaderSettings(); + settings.XmlResolver = this._context.resolver; + settings.DtdProcessing = this._context.ignoreDTD ? DtdProcessing.Ignore : DtdProcessing.Parse; + var xmlReader = XmlIncludeReader.CreateIncludeReader(context, settings, _context.baseUri.AbsoluteUri); + this._context.reader = xmlReader; + if (string.IsNullOrEmpty(outpath)) + { + using (StringWriter writer = new StringWriter()) + { + transform.Transform(xmlReader, null, writer); + this._xsltUri = resolved; + this._context.output = writer.ToString(); + } + } + else + { + bool noBom = false; + Settings appSettings = this._settings; + if (appSettings != null) + { + noBom = (bool)appSettings["NoByteOrderMark"]; + } + if (noBom) + { + // cache to an inmemory stream so we can strip the BOM. + using (MemoryStream ms = new MemoryStream()) + { + transform.Transform(xmlReader, null, ms); + ms.Seek(0, SeekOrigin.Begin); + EncodingHelpers.WriteFileWithoutBOM(ms, outpath); + } + } + else + { + if (this._context.estimatedOutputSize == 0) + { + this._context.estimatedOutputSize = xmlReader.Size * 4; + } + using (FileStream writer = new FileStream(outpath, FileMode.OpenOrCreate, FileAccess.Write)) + { + var wrapper = new ProgressiveStream(writer, this._context.estimatedOutputSize); + this._context.writer = wrapper; + Stopwatch watch = new Stopwatch(); + watch.Start(); + transform.Transform(xmlReader, null, wrapper); + watch.Stop(); + this._context.info = new PerformanceInfo(); + this._context.info.XsltMilliseconds = watch.ElapsedMilliseconds; + Debug.WriteLine("Transform in {0} milliseconds", watch.ElapsedMilliseconds); + this._xsltUri = resolved; + writer.Flush(); + } + this._context.estimatedOutputSize = new FileInfo(outpath).Length; + } + } + } + } + catch (System.Xml.Xsl.XsltException x) + { + if (x.Message.Contains("XsltSettings")) + { + if (!_trusted.ContainsKey(resolved)) + { + if (await this._trustService.CanTrustUrl(resolved)) + { + _trusted[resolved] = true; + trustRetry = true; + continue; + } + } + } + WriteError(x); + } + catch (Exception x) + { + WriteError(x); + } + } + + this._context.outpath = outpath; + this._context.running = false; + } + + bool IsModified() + { + if (this._xsltUri.IsFile) + { + string path = this._xsltUri.LocalPath; + DateTime lastWrite = File.GetLastWriteTime(path); + return this._loaded < lastWrite; + } + return false; + } + + + private string GetXsltOutputFileName(string xsltfilename) + { + // pick a good default filename ... this means we need to know the and unfortunately + // XslCompiledTransform doesn't give us that so we need to get it outselves. + var ext = GetDefaultOutputExtension(); + string outpath = null; + if (string.IsNullOrEmpty(xsltfilename)) + { + var basePath = Path.GetFileNameWithoutExtension(this._context.baseUri.GetComponents(UriComponents.Path, UriFormat.SafeUnescaped)); + outpath = basePath + "_output" + ext; + } + else + { + outpath = Path.GetFileNameWithoutExtension(xsltfilename) + "_output" + ext; + } + return GetWritableFileName(outpath); + } + + public string GetDefaultOutputExtension(string customFileName = null) + { + string ext = ".xml"; + try + { + if (this._xsltdoc == null) + { + string path = customFileName; + var resolved = new Uri(_context.baseUri, path); + this._xsltdoc = new XmlDocument(); + this._xsltdoc.Load(resolved.AbsoluteUri); + } + var method = GetOutputMethod(this._xsltdoc); + if (method.ToLower() == "html") + { + ext = ".htm"; + } + else if (method.ToLower() == "text") + { + ext = ".txt"; + } + } + catch (Exception ex) + { + Debug.WriteLine("XsltControl.GetDefaultOutputExtension exception " + ex.Message); + } + return ext; + } + + string GetOutputMethod(XmlDocument xsltdoc) + { + var ns = xsltdoc.DocumentElement.NamespaceURI; + string method = "xml"; // the default. + var mgr = new XmlNamespaceManager(xsltdoc.NameTable); + mgr.AddNamespace("xsl", ns); + XmlElement e = xsltdoc.SelectSingleNode("//xsl:output", mgr) as XmlElement; + if (e != null) + { + var specifiedMethod = e.GetAttribute("method"); + if (!string.IsNullOrEmpty(specifiedMethod)) + { + return specifiedMethod; + } + } + + // then we need to figure out the default method which is xml unless there's an html element here + foreach (XmlNode node in xsltdoc.DocumentElement.ChildNodes) + { + if (node is XmlElement child) + { + if (string.IsNullOrEmpty(child.NamespaceURI) && string.Compare(child.LocalName, "html", StringComparison.OrdinalIgnoreCase) == 0) + { + return "html"; + } + else + { + // might be an so look inside these too... + foreach (XmlNode subnode in child.ChildNodes) + { + if (subnode is XmlElement grandchild) + { + if ((string.IsNullOrEmpty(grandchild.NamespaceURI) || grandchild.NamespaceURI.Contains("xhtml")) + && string.Compare(grandchild.LocalName, "html", StringComparison.OrdinalIgnoreCase) == 0) + { + return "html"; + } + } + } + } + } + } + return method; + } + + private void WriteError(Exception e) + { + using (StringWriter writer = new StringWriter()) + { + writer.WriteLine("

"); + writer.WriteLine(SR.TransformErrorCaption); + writer.WriteLine("

"); + while (e != null) + { + writer.WriteLine(e.Message); + e = e.InnerException; + } + _context.output = writer.ToString(); + } + } + + private string GetWritableFileName(string fileName) + { + try + { + if (string.IsNullOrEmpty(fileName)) + { + fileName = GetXsltOutputFileName(null); + } + + // if the fileName is a full path then honor that request. + Uri uri = new Uri(fileName, UriKind.RelativeOrAbsolute); + var resolved = new Uri(this._context.baseUri, uri); + + // If the XML file is from HTTP then put XSLT output in the %TEMP% folder. + if (resolved.Scheme != "file") + { + uri = new Uri(Path.GetTempPath()); + this._tempFile = new Uri(uri, fileName).LocalPath; + return this._tempFile; + } + + string path = resolved.LocalPath; + if (resolved == this._context.baseUri) + { + // can't write to the same location as the XML file or we will lose the XML file! + path = Path.Combine(Path.GetDirectoryName(path), Path.GetFileNameWithoutExtension(path) + "_output" + Path.GetExtension(path)); + } + + var dir = Path.GetDirectoryName(path); + if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + + // make sure we can write to the location. + using (FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) + { + var test = System.Text.UTF8Encoding.UTF8.GetBytes("test"); + fs.Write(test, 0, test.Length); + } + + return path; + } + catch (Exception ex) + { + Debug.WriteLine("XsltControl.GetWritableBaseUri exception " + ex.Message); + } + + // We don't have write permissions? + Uri baseUri = new Uri(Path.GetTempPath()); + this._tempFile = new Uri(baseUri, fileName).LocalPath; + return this._tempFile; + } + + void CleanupTempFile() + { + if (!string.IsNullOrEmpty(_tempFile) && File.Exists(_tempFile)) + { + try + { + File.Delete(_tempFile); + } + catch { } + } + this._tempFile = null; + } + + XslCompiledTransform GetDefaultStylesheet() + { + if (_defaultss != null) + { + return _defaultss; + } + using (Stream stream = this.GetType().Assembly.GetManifestResourceStream(_context.defaultSSResource)) + { + if (null != stream) + { + using (StreamReader sr = new StreamReader(stream)) + { + string html = null; + html = GetDefaultStyles(sr.ReadToEnd()); + + using (XmlReader reader = XmlReader.Create(new StringReader(html))) + { + XslCompiledTransform t = new XslCompiledTransform(); + t.Load(reader); + _defaultss = t; + } + // the XSLT DOM is also handy to have around for GetOutputMethod + stream.Seek(0, SeekOrigin.Begin); + this._xsltdoc = new XmlDocument(); + this._xsltdoc.Load(stream); + } + } + else + { + throw new Exception(string.Format("You have a build problem: resource '{0} not found", _context.defaultSSResource)); + } + } + return _defaultss; + } + + string GetDefaultStyles(string html) + { + var font = (string)this._settings["FontFamily"]; + var fontSize = (double)this._settings["FontSize"]; + html = html.Replace("$FONT_FAMILY", font != null ? font : "Consolas, Courier New"); + html = html.Replace("$FONT_SIZE", font != null ? fontSize + "pt" : "10pt"); + + var theme = (ColorTheme)_settings["Theme"]; + var colors = (ThemeColors)_settings[theme == ColorTheme.Light ? "LightColors" : "DarkColors"]; + html = html.Replace("$BACKGROUND_COLOR", GetHexColor(colors.ContainerBackground)); + html = html.Replace("$ATTRIBUTE_NAME_COLOR", GetHexColor(colors.Attribute)); + html = html.Replace("$ATTRIBUTE_VALUE_COLOR", GetHexColor(colors.Text)); + html = html.Replace("$PI_COLOR", GetHexColor(colors.PI)); + html = html.Replace("$TEXT_COLOR", GetHexColor(colors.Text)); + html = html.Replace("$COMMENT_COLOR", GetHexColor(colors.Comment)); + html = html.Replace("$ELEMENT_COLOR", GetHexColor(colors.Element)); + html = html.Replace("$MARKUP_COLOR", GetHexColor(colors.Markup)); + html = html.Replace("$SIDENOTE_COLOR", GetHexColor(colors.EditorBackground)); + html = html.Replace("$OUTPUT_TIP_DISPLAY", this._context.hasDefaultXsltOutput ? "none" : "block"); + return html; + } + + string GetHexColor(Color c) + { + return System.Drawing.ColorTranslator.ToHtml(c); + } + + } +} diff --git a/src/XmlNotepad/FormTransformProgress.Designer.cs b/src/XmlNotepad/FormTransformProgress.Designer.cs new file mode 100644 index 00000000..d55da15e --- /dev/null +++ b/src/XmlNotepad/FormTransformProgress.Designer.cs @@ -0,0 +1,100 @@ +namespace XmlNotepad +{ + partial class FormTransformProgress + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); + this.buttonCancel = new System.Windows.Forms.Button(); + this.progressBar1 = new System.Windows.Forms.ProgressBar(); + this.tableLayoutPanel1.SuspendLayout(); + this.SuspendLayout(); + // + // tableLayoutPanel1 + // + this.tableLayoutPanel1.AutoSize = true; + this.tableLayoutPanel1.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; + this.tableLayoutPanel1.ColumnCount = 2; + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); + this.tableLayoutPanel1.Controls.Add(this.buttonCancel, 1, 2); + this.tableLayoutPanel1.Controls.Add(this.progressBar1, 0, 1); + this.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill; + this.tableLayoutPanel1.Location = new System.Drawing.Point(0, 0); + this.tableLayoutPanel1.Name = "tableLayoutPanel1"; + this.tableLayoutPanel1.RowCount = 3; + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 45F)); + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 55F)); + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanel1.Size = new System.Drawing.Size(528, 156); + this.tableLayoutPanel1.TabIndex = 0; + // + // buttonCancel + // + this.buttonCancel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.buttonCancel.AutoSize = true; + this.buttonCancel.Location = new System.Drawing.Point(450, 123); + this.buttonCancel.Name = "buttonCancel"; + this.buttonCancel.Padding = new System.Windows.Forms.Padding(3); + this.buttonCancel.Size = new System.Drawing.Size(75, 29); + this.buttonCancel.TabIndex = 0; + this.buttonCancel.Text = "&Cancel"; + this.buttonCancel.UseVisualStyleBackColor = true; + this.buttonCancel.Click += new System.EventHandler(this.buttonCancel_Click); + // + // progressBar1 + // + this.tableLayoutPanel1.SetColumnSpan(this.progressBar1, 2); + this.progressBar1.Dock = System.Windows.Forms.DockStyle.Top; + this.progressBar1.Location = new System.Drawing.Point(20, 54); + this.progressBar1.Margin = new System.Windows.Forms.Padding(20, 0, 20, 0); + this.progressBar1.Name = "progressBar1"; + this.progressBar1.Size = new System.Drawing.Size(488, 23); + this.progressBar1.TabIndex = 1; + // + // FormTransformProgress + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(528, 156); + this.Controls.Add(this.tableLayoutPanel1); + this.Name = "FormTransformProgress"; + this.Text = "Xslt Transform Progress"; + this.tableLayoutPanel1.ResumeLayout(false); + this.tableLayoutPanel1.PerformLayout(); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1; + private System.Windows.Forms.Button buttonCancel; + private System.Windows.Forms.ProgressBar progressBar1; + } +} \ No newline at end of file diff --git a/src/XmlNotepad/FormTransformProgress.cs b/src/XmlNotepad/FormTransformProgress.cs new file mode 100644 index 00000000..5e74767b --- /dev/null +++ b/src/XmlNotepad/FormTransformProgress.cs @@ -0,0 +1,35 @@ +using System; +using System.Windows.Forms; + +namespace XmlNotepad +{ + public partial class FormTransformProgress : Form + { + public FormTransformProgress() + { + InitializeComponent(); + } + + public void SetProgress(int min, int max, int value) + { + if (max == 0) + { + this.progressBar1.Style = ProgressBarStyle.Marquee; + } + else + { + this.progressBar1.Style = ProgressBarStyle.Continuous; + this.progressBar1.Minimum = min; + this.progressBar1.Maximum = max; + this.progressBar1.Value = value; + } + } + + + private void buttonCancel_Click(object sender, EventArgs e) + { + this.DialogResult = DialogResult.Cancel; + this.Hide(); + } + } +} diff --git a/src/XmlNotepad/FormTransformProgress.resx b/src/XmlNotepad/FormTransformProgress.resx new file mode 100644 index 00000000..1af7de15 --- /dev/null +++ b/src/XmlNotepad/FormTransformProgress.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/src/XmlNotepad/StringResources.Designer.cs b/src/XmlNotepad/StringResources.Designer.cs index 85fd0beb..b66b8313 100644 --- a/src/XmlNotepad/StringResources.Designer.cs +++ b/src/XmlNotepad/StringResources.Designer.cs @@ -1128,6 +1128,24 @@ public class StringResources { } } + /// + /// Looks up a localized string similar to Preserve Whitespace. + /// + public static string PreserveWhitespace { + get { + return ResourceManager.GetString("PreserveWhitespace", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Preserve all whitespace in XML document so you can see the Whitespace nodes in the tree.. + /// + public static string PreserveWhitespaceDescription { + get { + return ResourceManager.GetString("PreserveWhitespaceDescription", resourceCulture); + } + } + /// /// Looks up a localized string similar to The target file is read only, would you like to overwrite '{0}'?. /// diff --git a/src/XmlNotepad/StringResources.resx b/src/XmlNotepad/StringResources.resx index fec22367..c4440726 100644 --- a/src/XmlNotepad/StringResources.resx +++ b/src/XmlNotepad/StringResources.resx @@ -962,4 +962,12 @@ Error: {0} Hide identical Property Grid caption + + Preserve Whitespace + Property Grid caption + + + Preserve all whitespace in XML document so you can see the Whitespace nodes in the tree. + Property Grid description + \ No newline at end of file diff --git a/src/XmlNotepad/XmlNotepad.csproj b/src/XmlNotepad/XmlNotepad.csproj index f9f965b6..300a33f3 100644 --- a/src/XmlNotepad/XmlNotepad.csproj +++ b/src/XmlNotepad/XmlNotepad.csproj @@ -104,12 +104,19 @@ Code + Code + + Form + + + FormTransformProgress.cs + @@ -161,6 +168,9 @@ XsltViewer.cs + + FormTransformProgress.cs + NodeTextView.cs Designer diff --git a/src/XmlNotepad/XsltControl.cs b/src/XmlNotepad/XsltControl.cs index 06727625..6df5f211 100644 --- a/src/XmlNotepad/XsltControl.cs +++ b/src/XmlNotepad/XsltControl.cs @@ -1,15 +1,13 @@ using Microsoft.Web.WebView2.Core; using System; -using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Drawing; using System.IO; using System.Runtime.InteropServices; +using System.Threading.Tasks; using System.Windows.Forms; using System.Xml; -using System.Xml.Xsl; -using SR = XmlNotepad.StringResources; namespace XmlNotepad { @@ -18,29 +16,26 @@ public class WebView2Exception : Exception public WebView2Exception(string msg) : base(msg) { } } + public interface ITrustService + { + Task CanTrustUrl(Uri location); + } + public partial class XsltControl : UserControl { private readonly Stopwatch _urlWatch = new Stopwatch(); private string _html; private string _fileName; - private DateTime _loaded; private Uri _baseUri; private PerformanceInfo _info = null; - private XslCompiledTransform _xslt; - private XmlDocument _xsltdoc; - private XslCompiledTransform _defaultss; - private Uri _xsltUri; private ISite _site; private XmlUrlResolver _resolver; private Settings _settings; - private string _defaultSSResource = "XmlNotepad.DefaultSS.xslt"; - private readonly IDictionary _trusted = new Dictionary(); private bool _webInitialized; private bool _webView2Initialized; private bool _webView2Supported; - private string _tempFile; - private bool _usingDefaultXslt; - private bool _hasXsltOutput; // whether DOM has WebBrowserException; @@ -64,20 +59,8 @@ public void OnClosed() // This serves 2 purposes, it reclaims memory while XSLT output is not visible // and it clears the Find dialog so it does not float over the XmlTreeView. this.Display(""); - CleanupTempFile(); - } - - void CleanupTempFile() - { - if (!string.IsNullOrEmpty(_tempFile) && File.Exists(_tempFile)) - { - try - { - File.Delete(_tempFile); - } - catch { } - } - this._tempFile = null; + this.StopAsyncTransform(); + _asyncXslt.Close(); } private async void EnsureCoreWebView2(CoreWebView2Environment environment) @@ -312,7 +295,6 @@ Uri GetBaseUri() private void Display(string content) { - CleanupTempFile(); if (content != this._html && _webInitialized) { _urlWatch.Reset(); @@ -376,28 +358,23 @@ private void RaiseBrowserException(Exception e) public string BrowserVersion { get; set; } + private string _defaultSSResource = "XmlNotepad.DefaultSS.xslt"; + public string DefaultStylesheetResource { get { return this._defaultSSResource; } set { this._defaultSSResource = value; } } - public bool HasXsltOutput - { - get => _hasXsltOutput; - set - { - if (value != _hasXsltOutput) - { - _defaultss = null; - _hasXsltOutput = value; - } - } - } + // An override for hasxsltoutput. + public bool? HasXsltOutput { get; set; } public void SetSite(ISite site) { this._site = site; + + _asyncXslt = new AsyncXslt(site); + IServiceProvider sp = (IServiceProvider)site; this._resolver = new XmlProxyResolver(sp); this._settings = (Settings)sp.GetService(typeof(Settings)); @@ -426,8 +403,8 @@ private void OnSettingsChanged(object sender, string name) } else if (name == "Font" || name == "Theme" || name == "Colors" || name == "LightColors" || name == "DarkColors") { - _defaultss = null; - if (this._usingDefaultXslt) + this._asyncXslt.ResetDefaultStyleSheet(); + if (this._asyncXslt.UsingDefaultXslt) { string id = this.Handle.ToString(); // make sure action is unique to this control instance since we have 2! _settings.DelayedActions.StartDelayedAction("Transform" + id, UpdateTransform, TimeSpan.FromMilliseconds(50)); @@ -439,7 +416,7 @@ private void UpdateTransform() { if (_previousTransform != null) { - DisplayXsltResults(_previousTransform.document, _previousTransform.xsltfilename, _previousTransform.outpath, + _ = DisplayXsltResults(_previousTransform.document, _previousTransform.xsltfilename, _previousTransform.outpath, _previousTransform.userSpecifiedOutput); } } @@ -456,263 +433,125 @@ public Uri ResolveRelativePath(string filename) } } - class Context - { - public XmlDocument document; - public string xsltfilename; - public string outpath; - public bool userSpecifiedOutput; - } - - Context _previousTransform; + AsyncXsltContext _previousTransform; /// /// Run an XSLT transform and show the results. /// - /// The document to transform + /// The document to transform /// The xslt file to use /// Output file name hint. /// Whether output name is non-negotiable. - /// The output file name or null if DisableOutputFile is true - public string DisplayXsltResults(XmlDocument context, string xsltfilename, string outpath = null, bool userSpecifiedOutput = false) + /// Whether document has path to use for xslt output file + public async Task DisplayXsltResults(XmlDocument doc, string xsltfilename, string outpath = null, bool userSpecifiedOutput = false, bool hasDefaultXsltOutput = false) { - if (!this._webInitialized) + if (!this._webInitialized || this._asyncXslt == null) { return null; } - _previousTransform = new Context() + if (_previousTransform != null) + { + _previousTransform.Cancel(); + } + + if (HasXsltOutput.HasValue) + { + hasDefaultXsltOutput = HasXsltOutput.Value; + } + + + var context = new AsyncXsltContext() { - document = context, + document = doc, xsltfilename = xsltfilename, outpath = outpath, - userSpecifiedOutput = userSpecifiedOutput + userSpecifiedOutput = userSpecifiedOutput, + hasDefaultXsltOutput = hasDefaultXsltOutput, + defaultSSResource = this._defaultSSResource, + baseUri = this.GetBaseUri(), + ignoreDTD = this.IgnoreDTD, + enableScripts = this.EnableScripts, + disableOutputFile = this.DisableOutputFile, + resolver = this._resolver, + }; - - this.CleanupTempFile(); - Uri resolved = null; - try + if (_previousTransform != null && _previousTransform.xsltfilename == xsltfilename) { - XslCompiledTransform transform; - if (string.IsNullOrEmpty(xsltfilename)) - { - transform = GetDefaultStylesheet(); - this._usingDefaultXslt = true; - if (this._settings.GetBoolean("DisableDefaultXslt")) - { - context = new XmlDocument(); - context.LoadXml("Default styling of your XML documents is disabled in your Options"); - } - } - else - { - resolved = new Uri(_baseUri, xsltfilename); - if (resolved != this._xsltUri || IsModified()) - { - _xslt = new XslCompiledTransform(); - this._loaded = DateTime.Now; - var settings = new XsltSettings(true, this.EnableScripts); - settings.EnableScript = (_trusted.ContainsKey(resolved)); - var rs = new XmlReaderSettings(); - rs.DtdProcessing = this.IgnoreDTD ? DtdProcessing.Ignore : DtdProcessing.Parse; - rs.XmlResolver = _resolver; - using (XmlReader r = XmlReader.Create(resolved.AbsoluteUri, rs)) - { - _xslt.Load(r, settings, _resolver); - } + context.estimatedOutputSize = _previousTransform.estimatedOutputSize; + } + _previousTransform = context; - // the XSLT DOM is also handy to have around for GetOutputMethod - this._xsltdoc = new XmlDocument(); - this._xsltdoc.Load(resolved.AbsoluteUri); - } - transform = _xslt; - this._usingDefaultXslt = false; - } + this._settings.DelayedActions.StartDelayedAction("SlowTransformProgress", + OnSlowTransform, TimeSpan.FromSeconds(1)); - if (string.IsNullOrEmpty(outpath)) - { - if (!DisableOutputFile) - { - if (!string.IsNullOrEmpty(xsltfilename)) - { - outpath = this.GetXsltOutputFileName(xsltfilename); - } - else - { - // default stylesheet produces html - this._tempFile = outpath = GetWritableFileName("DefaultXsltOutput.htm"); - } - } - } - else if (!userSpecifiedOutput) - { - var ext = GetDefaultOutputExtension(); - var basePath = Path.Combine(Path.GetDirectoryName(outpath), Path.GetFileNameWithoutExtension(outpath)); - outpath = basePath + ext; - outpath = GetWritableFileName(outpath); - } - else + var path = await this._asyncXslt.TransformDocumentAsync(_previousTransform); + + StopAsyncTransform(); + + if (context == this._previousTransform) + { + if (!string.IsNullOrEmpty(context.output)) { - outpath = GetWritableFileName(outpath); + Display(context.output); } - - if (null != transform) + else if (!string.IsNullOrEmpty(context.outpath)) { - var dir = Path.GetDirectoryName(outpath); - if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) + if (File.Exists(context.outpath)) { - Directory.CreateDirectory(dir); - } - - var settings = new XmlReaderSettings(); - settings.XmlResolver = new XmlProxyResolver(this._site); - settings.DtdProcessing = this.IgnoreDTD ? DtdProcessing.Ignore : DtdProcessing.Parse; - var xmlReader = XmlIncludeReader.CreateIncludeReader(context, settings, GetBaseUri().AbsoluteUri); - if (string.IsNullOrEmpty(outpath)) - { - using (StringWriter writer = new StringWriter()) - { - transform.Transform(xmlReader, null, writer); - this._xsltUri = resolved; - Display(writer.ToString()); - } + var size = new FileInfo(context.outpath).Length; + Debug.WriteLine($"Display {context.outpath} ({size})"); + DisplayFile(context.outpath); } else { - bool noBom = false; - Settings appSettings = (Settings)this._site.GetService(typeof(Settings)); - if (appSettings != null) - { - noBom = (bool)appSettings["NoByteOrderMark"]; - } - if (noBom) - { - // cache to an inmemory stream so we can strip the BOM. - using (MemoryStream ms = new MemoryStream()) - { - transform.Transform(xmlReader, null, ms); - ms.Seek(0, SeekOrigin.Begin); - EncodingHelpers.WriteFileWithoutBOM(ms, outpath); - } - } - else - { - using (FileStream writer = new FileStream(outpath, FileMode.OpenOrCreate, FileAccess.Write)) - { - Stopwatch watch = new Stopwatch(); - watch.Start(); - transform.Transform(xmlReader, null, writer); - watch.Stop(); - this._info = new PerformanceInfo(); - this._info.XsltMilliseconds = watch.ElapsedMilliseconds; - Debug.WriteLine("Transform in {0} milliseconds", watch.ElapsedMilliseconds); - this._xsltUri = resolved; - } - } - - DisplayFile(outpath); - } - } - } - catch (System.Xml.Xsl.XsltException x) - { - if (x.Message.Contains("XsltSettings")) - { - if (!_trusted.ContainsKey(resolved) && - MessageBox.Show(this, SR.XslScriptCodePrompt, SR.XslScriptCodeCaption, - MessageBoxButtons.YesNo, MessageBoxIcon.Exclamation) == DialogResult.Yes) - { - _trusted[resolved] = true; - return DisplayXsltResults(context, xsltfilename, outpath); + Debug.WriteLine($"Display {context.outpath} (FILE NOT FOUND)"); } } - WriteError(x); - } - catch (Exception x) - { - WriteError(x); } - - return outpath; + return path; } - private string GetXsltOutputFileName(string xsltfilename) + + private void StopAsyncTransform() { - // pick a good default filename ... this means we need to know the and unfortunately - // XslCompiledTransform doesn't give us that so we need to get it outselves. - var ext = GetDefaultOutputExtension(); - string outpath = null; - if (string.IsNullOrEmpty(xsltfilename)) + this._settings.DelayedActions.CancelDelayedAction("SlowTransformProgress"); + if (this._progress != null) { - var basePath = Path.GetFileNameWithoutExtension(this._baseUri.GetComponents(UriComponents.Path, UriFormat.SafeUnescaped)); - outpath = basePath + "_output" + ext; + this._progress.Close(); + this._progress = null; } - else - { - outpath = Path.GetFileNameWithoutExtension(xsltfilename) + "_output" + ext; - } - return GetWritableFileName(outpath); + } - private string GetWritableFileName(string fileName) + private void OnSlowTransform() { - try + if (_previousTransform != null && _previousTransform.running) { - if (string.IsNullOrEmpty(fileName)) + this._progress = new FormTransformProgress(); + this._progress.SetProgress(0, (int)this._previousTransform.Size, (int)this._previousTransform.Position); + this._settings.DelayedActions.StartDelayedAction("UpdateTransformProgress", UpdateTransformProgress, TimeSpan.FromMilliseconds(30)); + if (this._progress.ShowDialog() == DialogResult.Cancel) { - fileName = GetXsltOutputFileName(null); + this._previousTransform.Cancel(); } - - // if the fileName is a full path then honor that request. - Uri uri = new Uri(fileName, UriKind.RelativeOrAbsolute); - var resolved = new Uri(this._baseUri, uri); - - // If the XML file is from HTTP then put XSLT output in the %TEMP% folder. - if (resolved.Scheme != "file") - { - uri = new Uri(Path.GetTempPath()); - this._tempFile = new Uri(uri, fileName).LocalPath; - return this._tempFile; - } - - string path = resolved.LocalPath; - if (resolved == this._baseUri) - { - // can't write to the same location as the XML file or we will lose the XML file! - path = Path.Combine(Path.GetDirectoryName(path), Path.GetFileNameWithoutExtension(path) + "_output" + Path.GetExtension(path)); - } - - var dir = Path.GetDirectoryName(path); - if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) - { - Directory.CreateDirectory(dir); - } - - // make sure we can write to the location. - using (FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) - { - var test = System.Text.UTF8Encoding.UTF8.GetBytes("test"); - fs.Write(test, 0, test.Length); - } - - return path; } - catch (Exception ex) + } + + private void UpdateTransformProgress() + { + if (this._progress != null) { - Debug.WriteLine("XsltControl.GetWritableBaseUri exception " + ex.Message); + this._progress.SetProgress(0, (int)this._previousTransform.Size, (int)this._previousTransform.Position); + this._settings.DelayedActions.StartDelayedAction("UpdateTransformProgress", UpdateTransformProgress, TimeSpan.FromMilliseconds(30)); } - - // We don't have write permissions? - Uri baseUri = new Uri(Path.GetTempPath()); - this._tempFile = new Uri(baseUri, fileName).LocalPath; - return this._tempFile; } public string GetOutputFileFilter(string customFileName = null) { // return something like this: // XML files (*.xml)|*.xml|XSL files (*.xsl)|*.xsl|XSD files (*.xsd)|*.xsd|All files (*.*)|*.* - var ext = GetDefaultOutputExtension(customFileName); + var ext = this._asyncXslt.GetDefaultOutputExtension(customFileName); switch (ext) { case ".xml": @@ -725,170 +564,6 @@ public string GetOutputFileFilter(string customFileName = null) } - public string GetDefaultOutputExtension(string customFileName = null) - { - string ext = ".xml"; - try - { - if (this._xsltdoc == null) - { - string path = customFileName; - var resolved = new Uri(_baseUri, path); - this._xsltdoc = new XmlDocument(); - this._xsltdoc.Load(resolved.AbsoluteUri); - } - var method = GetOutputMethod(this._xsltdoc); - if (method.ToLower() == "html") - { - ext = ".htm"; - } - else if (method.ToLower() == "text") - { - ext = ".txt"; - } - } - catch (Exception ex) - { - Debug.WriteLine("XsltControl.GetDefaultOutputExtension exception " + ex.Message); - } - return ext; - } - - string GetOutputMethod(XmlDocument xsltdoc) - { - var ns = xsltdoc.DocumentElement.NamespaceURI; - string method = "xml"; // the default. - var mgr = new XmlNamespaceManager(xsltdoc.NameTable); - mgr.AddNamespace("xsl", ns); - XmlElement e = xsltdoc.SelectSingleNode("//xsl:output", mgr) as XmlElement; - if (e != null) - { - var specifiedMethod = e.GetAttribute("method"); - if (!string.IsNullOrEmpty(specifiedMethod)) - { - return specifiedMethod; - } - } - - // then we need to figure out the default method which is xml unless there's an html element here - foreach (XmlNode node in xsltdoc.DocumentElement.ChildNodes) - { - if (node is XmlElement child) - { - if (string.IsNullOrEmpty(child.NamespaceURI) && string.Compare(child.LocalName, "html", StringComparison.OrdinalIgnoreCase) == 0) - { - return "html"; - } - else - { - // might be an so look inside these too... - foreach (XmlNode subnode in child.ChildNodes) - { - if (subnode is XmlElement grandchild) - { - if ((string.IsNullOrEmpty(grandchild.NamespaceURI) || grandchild.NamespaceURI.Contains("xhtml")) - && string.Compare(grandchild.LocalName, "html", StringComparison.OrdinalIgnoreCase) == 0) - { - return "html"; - } - } - } - } - } - } - return method; - } - - private void WriteError(Exception e) - { - using (StringWriter writer = new StringWriter()) - { - writer.WriteLine("

"); - writer.WriteLine(SR.TransformErrorCaption); - writer.WriteLine("

"); - while (e != null) - { - writer.WriteLine(e.Message); - e = e.InnerException; - } - Display(writer.ToString()); - } - } - - string GetHexColor(Color c) - { - return System.Drawing.ColorTranslator.ToHtml(c); - } - - string GetDefaultStyles(string html) - { - var font = (string)this._settings["FontFamily"]; - var fontSize = (double)this._settings["FontSize"]; - html = html.Replace("$FONT_FAMILY", font != null ? font : "Consolas, Courier New"); - html = html.Replace("$FONT_SIZE", font != null ? fontSize + "pt" : "10pt"); - - var theme = (ColorTheme)_settings["Theme"]; - var colors = (ThemeColors)_settings[theme == ColorTheme.Light ? "LightColors" : "DarkColors"]; - html = html.Replace("$BACKGROUND_COLOR", GetHexColor(colors.ContainerBackground)); - html = html.Replace("$ATTRIBUTE_NAME_COLOR", GetHexColor(colors.Attribute)); - html = html.Replace("$ATTRIBUTE_VALUE_COLOR", GetHexColor(colors.Text)); - html = html.Replace("$PI_COLOR", GetHexColor(colors.PI)); - html = html.Replace("$TEXT_COLOR", GetHexColor(colors.Text)); - html = html.Replace("$COMMENT_COLOR", GetHexColor(colors.Comment)); - html = html.Replace("$ELEMENT_COLOR", GetHexColor(colors.Element)); - html = html.Replace("$MARKUP_COLOR", GetHexColor(colors.Markup)); - html = html.Replace("$SIDENOTE_COLOR", GetHexColor(colors.EditorBackground)); - html = html.Replace("$OUTPUT_TIP_DISPLAY", this.HasXsltOutput ? "none" : "block"); - return html; - } - - XslCompiledTransform GetDefaultStylesheet() - { - if (_defaultss != null) - { - return _defaultss; - } - using (Stream stream = this.GetType().Assembly.GetManifestResourceStream(this._defaultSSResource)) - { - if (null != stream) - { - using (StreamReader sr = new StreamReader(stream)) - { - string html = null; - html = GetDefaultStyles(sr.ReadToEnd()); - - using (XmlReader reader = XmlReader.Create(new StringReader(html))) - { - XslCompiledTransform t = new XslCompiledTransform(); - t.Load(reader); - _defaultss = t; - } - // the XSLT DOM is also handy to have around for GetOutputMethod - stream.Seek(0, SeekOrigin.Begin); - this._xsltdoc = new XmlDocument(); - this._xsltdoc.Load(stream); - } - } - else - { - throw new Exception(string.Format("You have a build problem: resource '{0} not found", this._defaultSSResource)); - } - } - return _defaultss; - } - - bool IsModified() - { - if (this._xsltUri.IsFile) - { - string path = this._xsltUri.LocalPath; - DateTime lastWrite = File.GetLastWriteTime(path); - return this._loaded < lastWrite; - } - return false; - } - - private Guid cmdGuid = new Guid("ED016940-BD5B-11CF-BA4E-00C04FD70816"); private enum OLECMDEXECOPT diff --git a/src/XmlNotepad/XsltViewer.cs b/src/XmlNotepad/XsltViewer.cs index ddb6a721..6da7956d 100644 --- a/src/XmlNotepad/XsltViewer.cs +++ b/src/XmlNotepad/XsltViewer.cs @@ -102,30 +102,59 @@ private void OnOutputFileNameKeyDown(object sender, KeyEventArgs e) } } - public void DisplayXsltResults() + private bool IsValidPath(string path) { - string xpath = this.SourceFileName.Text.Trim(); + try + { + Uri uri = new Uri(path, UriKind.RelativeOrAbsolute); + if (uri.IsAbsoluteUri && uri.Scheme == Uri.UriSchemeFile) + { + string valid = System.IO.Path.GetFullPath(uri.LocalPath); + } + else + { + string valid = System.IO.Path.GetFullPath(path); + } + return true; + } + catch (Exception ex) + { + MessageBox.Show(ex.Message, "Invalid Path", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + return false; + } + + public async void DisplayXsltResults() + { + string xpath = this.SourceFileName.Text.Trim().Trim('"'); if (!string.IsNullOrEmpty(xpath) && this._xsltFiles != null) { + if (!IsValidPath(xpath)) + { + return; + } Uri uri = this.xsltControl.ResolveRelativePath(xpath); if (uri != null) { this._xsltFiles.AddRecentFile(uri); } } - string output = this.OutputFileName.Text.Trim(); + string output = this.OutputFileName.Text.Trim().Trim('"'); if (string.IsNullOrWhiteSpace(output)) { _userSpecifiedOutput = false; } - bool hasXsltOutput = !string.IsNullOrEmpty(this._model.XsltDefaultOutput); - if (!_userSpecifiedOutput && hasXsltOutput) + else if (!IsValidPath(output)) + { + return; + } + bool hasDefaultXsltOutput = !string.IsNullOrEmpty(this._model.XsltDefaultOutput); + if (!_userSpecifiedOutput && hasDefaultXsltOutput) { output = this._model.XsltDefaultOutput; } - this.xsltControl.HasXsltOutput = hasXsltOutput; - output = this.xsltControl.DisplayXsltResults(this._model.Document, xpath, output, _userSpecifiedOutput); + output = await this.xsltControl.DisplayXsltResults(this._model.Document, xpath, output, _userSpecifiedOutput, hasDefaultXsltOutput); if (!string.IsNullOrWhiteSpace(output)) { this.OutputFileName.Text = MakeRelative(output);