diff --git a/OneMore/Commands/Edit/ConvertMarkdownCommand.cs b/OneMore/Commands/Edit/ConvertMarkdownCommand.cs index 6c481de5c3..65d71436fb 100644 --- a/OneMore/Commands/Edit/ConvertMarkdownCommand.cs +++ b/OneMore/Commands/Edit/ConvertMarkdownCommand.cs @@ -68,6 +68,8 @@ public override async Task Execute(params object[] args) var text = reader.ReadTextFrom(paragraphs, allContent); text = Regex.Replace(text, @"
([\n\r]+)", "$1"); + text = Regex.Replace(text, @"\<*input\s+type*=*\""checkbox\""\s+unchecked\s+[a-zA-Z *]*\/\>", "[ ]"); + text = Regex.Replace(text, @"\<*input\s+type*=*\""checkbox\""\s+checked\s+[a-zA-Z *]*\/\>", "[x]"); var body = OneMoreDig.ConvertMarkdownToHtml(filepath, text); diff --git a/OneMore/Commands/File/ImportCommand.cs b/OneMore/Commands/File/ImportCommand.cs index dbbd58b1e5..28b8de37b9 100644 --- a/OneMore/Commands/File/ImportCommand.cs +++ b/OneMore/Commands/File/ImportCommand.cs @@ -11,7 +11,8 @@ namespace River.OneMoreAddIn.Commands using System; using System.Drawing; using System.IO; - using System.Threading; + using System.Text.RegularExpressions; + using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using System.Xml.Linq; @@ -653,6 +654,8 @@ private async Task ImportMarkdownFile(string filepath, CancellationToken t page.Title = Path.GetFileNameWithoutExtension(filepath); var container = page.EnsureContentContainer(); + body = Regex.Replace(body, @"\<*input\s+type*=*\""checkbox\""\s+unchecked\s+[a-zA-Z *]*\/\>", "[ ]"); + body = Regex.Replace(body, @"\<*input\s+type*=*\""checkbox\""\s+checked\s+[a-zA-Z *]*\/\>", "[x]"); container.Add(new XElement(ns + "HTMLBlock", new XElement(ns + "Data", @@ -675,6 +678,7 @@ private async Task ImportMarkdownFile(string filepath, CancellationToken t converter = new MarkdownConverter(page); converter.RewriteHeadings(); + converter.RewriteTodo(); logger.WriteLine($"updating..."); logger.WriteLine(page.Root); diff --git a/OneMore/Commands/File/Markdown/MarkdownConverter.cs b/OneMore/Commands/File/Markdown/MarkdownConverter.cs index 3b5fc2bf1f..c05485ab0b 100644 --- a/OneMore/Commands/File/Markdown/MarkdownConverter.cs +++ b/OneMore/Commands/File/Markdown/MarkdownConverter.cs @@ -7,6 +7,7 @@ namespace River.OneMoreAddIn.Commands using River.OneMoreAddIn.Models; using River.OneMoreAddIn.Styles; using System.Collections.Generic; + using System.Globalization; using System.Linq; using System.Text.RegularExpressions; using System.Xml.Linq; @@ -51,6 +52,28 @@ public void RewriteHeadings() { RewriteHeadings(outline.Descendants(ns + "OE")); } + + // added header spacing specific to markdown + var quickstyles = page.Root.Elements(ns + "QuickStyleDef"); + foreach (var quickstyle in quickstyles) + { + var name = quickstyle.Attribute("name").Value; + if (name.Equals("h1") || name.Equals("h2")) + { + replaceAtributes(quickstyle, spaceBefore: 0.8, spaceAfter: 0.5); + } + else + if (name.Equals("h3") || name.Equals("h4")) + { + replaceAtributes(quickstyle, spaceBefore: 0.3, spaceAfter: 0.3); + } + } + void replaceAtributes(XElement quickstyle, double spaceBefore, double spaceAfter) + { + quickstyle.SetAttributeValue("spaceBefore", spaceBefore.ToString("####0.00", CultureInfo.InvariantCulture)); + quickstyle.SetAttributeValue("spaceAfter", spaceAfter.ToString("####0.00", CultureInfo.InvariantCulture)); + } + } @@ -141,8 +164,22 @@ public MarkdownConverter RewriteHeadings(IEnumerable paragraphs) } + /// + /// Applies standard OneNote styling to all recognizable headings in all Outlines + /// on the page + /// + public void RewriteTodo() + { + foreach (var outline in page.BodyOutlines) + { + RewriteTodo(outline.Descendants(ns + "OE")); + } + } + + /// /// Tag current line with To Do tag if beginning with [ ] or [x] + /// Also :TAGS: will be handled here /// All other :emojis: should be translated inline by Markdig /// /// @@ -158,21 +195,46 @@ public MarkdownConverter RewriteTodo(IEnumerable paragraphs) { var cdata = run.GetCData(); var wrapper = cdata.GetWrapper(); + if (wrapper.FirstNode is XText) + { + cdata.Value = wrapper.GetInnerXml(); + } + while (wrapper.FirstNode is not XText && wrapper.FirstNode is not null) + { + wrapper = (XElement)wrapper.FirstNode; + } if (wrapper.FirstNode is XText text) { var match = boxpattern.Match(text.Value); + // special treatment of todo tag if (match.Success) { text.Value = text.Value.Substring(match.Length); + var org = text.Value; + var completed = match.Groups["x"].Value == "x"; + text.Value = text.Value.Replace((completed ? "[x]" : "[ ]"), ""); + cdata.Value = cdata.Value.Replace(org, text.Value); // ensure TagDef exists - var index = page.AddTagDef("3", "To Do", 4); - - // inject tag prior to run - run.AddBeforeSelf(new Tag(index, match.Groups["x"].Value == "x")); + page.SetTag(paragraph, tagSymbol: "3", tagStatus:completed,tagName:"todo"); + } + else + { + // look for all other tags + foreach (var t in MarkdownEmojis.taglist) + { + // check for other tags + if (text.Value.Contains(t.name)) + { + var org = text.Value; + text.Value = text.Value.Replace(t.name, ""); + cdata.Value = cdata.Value.Replace(org, text.Value); + // ensure TagDef exists + page.SetTag(paragraph, tagSymbol: t.id, tagStatus: false, tagName: t.topic, tagType: t.type); + break; + } + } - // update run text - cdata.Value = wrapper.GetInnerXml(); } } } diff --git a/OneMore/Commands/File/Markdown/MarkdownEmojis.cs b/OneMore/Commands/File/Markdown/MarkdownEmojis.cs new file mode 100644 index 0000000000..93f2eff2c3 --- /dev/null +++ b/OneMore/Commands/File/Markdown/MarkdownEmojis.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; + +namespace River.OneMoreAddIn.Commands +{ + public static class MarkdownEmojis + { + public static List<(string name, string id, string topic, int type)> taglist = new List<(string name, string id, string topic, int type)> + { +// (":todo:", "3", "todo" , 0), + (":question:", "6", "question" , 0), + (":star:", "13", "important", 0 ), + (":exclamation:", "17", "critical", 0), + (":phone:", "18", "phone", 0), + (":bulb:", "21", "idea", 0), + (":house:", "23", "address", 0), + (":three:", "33", "three", 0), + (":zero:", "39", "zero", 0), + (":two:", "51", "two", 0), + (":arrow_right:", "59", "main agenda item", 0), + (":one:", "70", "one", 0), + (":information_desk_person:","94", "discuss person a/b", 21), + (":bellsymbol:", "97", "bellsymbol", 0), + (":busts_in_silhouette:", "116", "busts_in_silhouette", 0), + (":bell:", "117", "bell", 0), + (":letter:", "118", "letter", 0), + (":musical_note:", "121", "musical_note", 0), + (":secret:", "131", "idea", 0), + (":book:", "132", "book", 0), + (":movie_camera:", "133", "movie_camera", 0), + (":zap:", "140", "lightning_bolt", 0), + (":o:", "1", "default", 0) + }; + + + } +} diff --git a/OneMore/Commands/File/Markdown/MarkdownWriter.cs b/OneMore/Commands/File/Markdown/MarkdownWriter.cs index 7b0207107b..197cded54f 100644 --- a/OneMore/Commands/File/Markdown/MarkdownWriter.cs +++ b/OneMore/Commands/File/Markdown/MarkdownWriter.cs @@ -34,18 +34,32 @@ private sealed class Context // accent enclosure char, asterisk* or backquote` public string Accent; } + // helper class to pass parameter + private sealed class PrefixClass + { + public string indents = string.Empty; + public string tags = string.Empty; + public string bullets = string.Empty; + public string tablelistid = string.Empty; + public bool justclosed = false; + + public PrefixClass() + { + } + } // Note that if pasting md text directly into OneNote, there's no good way to indent text // and prevent OneNote from auto-formatting. Closest alt is to use a string of nbsp's // but that conflicts with other directives like headings and list numbering. One way is // to substitute indentations (e.g., OEChildren) with the blockquote directive instead. - private const string Indent = " "; //">"; //    "; + private const string Indent = " "; //">"; //    "; private const string Quote = ">"; private readonly Page page; private readonly XNamespace ns; private readonly List"); + } } - WriteTable(element); + if (contained) + { + var tableindex = nestedtables.Count() + 1; + nestedtables.Add((element, tableindex)); + writer.Write(prefix.indents + "[nested-table" + tableindex + "](#nested-table" + tableindex + ")"); + } + else + { + WriteTable(element, prefix); + while (nestedtables.Count() != 0) + { + var nestedtable = nestedtables.First(); + writer.WriteLine(prefix.indents + "
"); + writer.WriteLine(prefix.indents + "" + "Nested Table " + nestedtable.index + ""); + WriteTable(nestedtable.container, prefix); + writer.WriteLine(prefix.indents + "
"); + nestedtables.RemoveAt(0); + } + } + if (bordersVisible.Equals("true")) + { + writer.Write(prefix.indents + ""); + } + // Write extra line + writer.WriteLine(); break; } } @@ -301,17 +380,7 @@ private void Write(XElement container, private Context DetectQuickStyle(XElement element) { - // quickStyleIndex could be on T, OE, or OEChildren, Outline, Page - // so ascend until we find one... - - int index = -1; - while (element is not null && - !element.GetAttributeValue("quickStyleIndex", out index, -1)) - { - element = element.Parent; - } - - if (index >= 0) + if (element.GetAttributeValue("quickStyleIndex", out int index)) { var context = new Context { @@ -337,40 +406,39 @@ private Context DetectQuickStyle(XElement element) } - private void Stylize(string prefix) + private string Stylize() { - writer.Write(prefix); - if (contexts.Count == 0) return; + var styleprefix = ""; + if (contexts.Count == 0) return ""; var context = contexts.Peek(); var quick = quickStyles.First(q => q.Index == context.QuickStyleIndex); switch (quick.Name) { case "PageTitle": - case "h1": - writer.Write("# "); - break; - - case "h2": writer.Write("## "); break; - case "h3": writer.Write("### "); break; - case "h4": writer.Write("#### "); break; - case "h5": writer.Write("##### "); break; - case "h6": writer.Write("###### "); break; - case "blockquote": writer.Write("> "); break; + case "h1": styleprefix = ("# "); break; + case "h2": styleprefix = ("## "); break; + case "h3": styleprefix = ("### "); break; + case "h4": styleprefix = ("#### "); break; + case "h5": styleprefix = ("##### "); break; + case "h6": styleprefix = ("###### "); break; + case "blockquote": styleprefix = ("> "); break; // cite and code are both block-scope style, on the OE - case "cite": writer.Write("*"); break; - case "code": writer.Write("`"); break; - //case "p": logger.Write(Environment.NewLine); break; + case "cite": styleprefix = ("*"); break; + case "code": styleprefix = ("`"); break; + //case "p": lstyleprefix = (Environment.NewLine); break; } + return styleprefix; } - private void WriteTag(XElement element) + private string WriteTag(XElement element, bool contained) { var symbol = page.Root.Elements(ns + "TagDef") .Where(e => e.Attribute("index").Value == element.Attribute("index").Value) .Select(e => int.Parse(e.Attribute("symbol").Value)) .FirstOrDefault(); - + var retValue = ""; + var tagSymbol = MarkdownEmojis.taglist.Find(x => x.id == symbol.ToString()); switch (symbol) { case 3: // to do @@ -381,31 +449,19 @@ private void WriteTag(XElement element) case 94: // discuss person a/b case 95: // discuss manager var check = element.Attribute("completed").Value == "true" ? "x" : " "; - writer.Write($"[{check}] "); - break; + retValue = contained + ? @"" + : ($"[{check}] "); - case 6: writer.Write(":question: "); break; // question - case 13: writer.Write(":star: "); break; // important - case 17: writer.Write(":exclamation: "); break; // critical - case 18: writer.Write(":phone: "); break; // phone - case 21: writer.Write(":bulb: "); break; // idea - case 23: writer.Write(":house: "); break; // address - case 33: writer.Write(":three: "); break; // three - case 39: writer.Write(":zero: "); break; // zero - case 51: writer.Write(":two: "); break; // two - case 70: writer.Write(":one: "); break; // one - case 118: writer.Write(":mailbox: "); break; // contact - case 121: writer.Write(":musical_note: "); break; // music to listen to - case 131: writer.Write(":secret: "); break; // password - case 133: writer.Write(":movie_camera: "); break; // movie to see - case 132: writer.Write(":book: "); break; // book to read - case 140: writer.Write(":zap: "); break; // lightning bolt - default: writer.Write(":o: "); break; // big red circle + break; + default: retValue = tagSymbol.name + " "; + break; } + return retValue; } - private void WriteText(XCData cdata, bool startOfLine) + private void WriteText(XCData cdata, bool startOfLine, bool contained) { cdata.Value = cdata.Value .Replace("
", " ") // usually followed by NL so leave it there @@ -428,7 +484,15 @@ private void WriteText(XCData cdata, bool startOfLine) span.ReplaceWith(new XText(text)); } - foreach (var anchor in wrapper.Elements("a")) + // escape directives + var raw = wrapper.GetInnerXml() + .Replace("<", "\\<") + .Replace("|", "\\|") + .Replace("à", "→ ") // right arrow + .Replace("\n", contained ? "
" : "\n"); // newlines in tables + + // replace links with <> to allow special characters and hence place if after escape directives + foreach (var anchor in wrapper.Elements("a").ToList()) { var href = anchor.Attribute("href")?.Value; if (!string.IsNullOrEmpty(href)) @@ -440,21 +504,21 @@ private void WriteText(XCData cdata, bool startOfLine) } else { - anchor.ReplaceWith(new XText($"[{anchor.Value}]({href})")); + anchor.ReplaceWith(new XText($"[{anchor.Value}](<{href}>)")); } } } - // escape directives - var raw = wrapper.GetInnerXml() - .Replace("<", "\\<") - .Replace("|", "\\|"); - if (startOfLine && raw.Length > 0 && raw.StartsWith("#")) { writer.Write("\\"); } + if (startOfLine && raw.Length > 0) + { + raw += " "; // add extra space to end of line + } + logger.Debug($"text [{raw}]"); writer.Write(raw); } @@ -480,7 +544,7 @@ private void WriteImage(XElement element) image.Save(filename, ImageFormat.Png); #endif - var imgPath = Path.Combine(attachmentFolder, name); + var imgPath = Path.Combine(attachmentFolder, name).Replace("\\", "/").Replace(" ", "%20"); writer.Write($"![Image-{imageCounter}]({imgPath})"); } else @@ -545,19 +609,16 @@ private void WriteFile(XElement element) } - private void WriteTable(XElement element) + private void WriteTable(XElement element, PrefixClass prefix) { #region WriteRow(TableRow row) void WriteRow(TableRow row) { - writer.Write("| "); + writer.Write(prefix.indents + "| "); foreach (var cell in row.Cells) { - cell.Root - .Element(ns + "OEChildren") - .Elements(ns + "OE") - .ForEach(e => Write(e, contained: true)); - + PrefixClass nestedprefix = new PrefixClass(); + Write(cell.Root, nestedprefix, contained: true); writer.Write(" | "); } writer.WriteLine(); @@ -566,14 +627,15 @@ void WriteRow(TableRow row) var table = new Table(element); - // table needs a blank line before it + // table needs a blank line before it, even 2nd one sometimes needed + writer.WriteLine(); writer.WriteLine(); var rows = table.Rows; // header - - - - - - - - - - - - - - - - - - - - if (table.HasHeaderRow && rows.Any()) + if ((table.HasHeaderRow && rows.Any()) || rows.Count() == 1) { // use first row data as header WriteRow(rows.First()); @@ -593,7 +655,7 @@ void WriteRow(TableRow row) // separator - - - - - - - - - - - - - - - - - - writer.Write("|"); + writer.Write(prefix.indents + "| "); for (int i = 0; i < table.ColumnCount; i++) { writer.Write(" :--- |"); diff --git a/OneMore/Commands/File/Markdown/OneMoreDigExtensions.cs b/OneMore/Commands/File/Markdown/OneMoreDigExtensions.cs index 42ab906fd7..ce83ef85f9 100644 --- a/OneMore/Commands/File/Markdown/OneMoreDigExtensions.cs +++ b/OneMore/Commands/File/Markdown/OneMoreDigExtensions.cs @@ -5,13 +5,31 @@ namespace River.OneMoreAddIn.Commands { using Markdig; - + using Markdig.Extensions.Emoji; + using System.Collections.Generic; + using System.Linq; internal static class OneMoreDigExtensions { + public static MarkdownPipelineBuilder UseOneMoreExtensions( this MarkdownPipelineBuilder pipeline) { + var emojiDic = EmojiMapping.GetDefaultEmojiShortcodeToUnicode(); + var emojiDicNew = new Dictionary(); + foreach (var mappings in emojiDic) + { + var tagName = MarkdownEmojis.taglist.FirstOrDefault(x => x.name.Equals(mappings.Key)).name; + if (tagName.IsNullOrEmpty()) + { + emojiDicNew.Add(mappings.Key,mappings.Value); + } + } + var DefaultEmojisAndSmileysMapping = new EmojiMapping( + emojiDicNew, EmojiMapping.GetDefaultSmileyToEmojiShortcode()); + // var emojiMapping = EmojiMapping.DefaultEmojisAndSmileysMapping; + pipeline.Extensions.Add(new EmojiExtension(DefaultEmojisAndSmileysMapping)); + pipeline.Extensions.Add(new OneMoreDigExtension()); return pipeline; } diff --git a/OneMore/Models/Page.cs b/OneMore/Models/Page.cs index 0c7b0102bc..b91584bda8 100644 --- a/OneMore/Models/Page.cs +++ b/OneMore/Models/Page.cs @@ -122,7 +122,6 @@ public void OptimizeForSave(bool keep) public bool IsValid => Root is not null; - /// /// Gets the namespace used to create new elements for the page /// @@ -277,6 +276,55 @@ public void AddTagDef(TagDef tagdef) Root.AddFirst(tagdef); } + /// + /// Extended version from RemindCommand to handle also non Todo Tags + /// + public string SetTag(XElement paragraph, string tagSymbol, string tagName, int tagType = 0, bool tagStatus = false) + { + var index = this.GetTagDefIndex(tagSymbol); + if (index == null) + { + index = this.AddTagDef(tagSymbol, tagName, tagType); + } + + var tag = paragraph.Elements(Namespace + "Tag") + .FirstOrDefault(e => e.Attribute("index").Value == index); + + if (tag == null) + { + // tags must be ordered by index even within their containing paragraph + // so take all, remove from paragraph, append, sort, re-add... + + var tags = paragraph.Elements(Namespace + "Tag").ToList(); + tags.ForEach(t => t.Remove()); + + // synchronize tag with reminder + var completed = tagStatus == true + ? "true" : "false"; + + tag = new XElement(Namespace + "Tag", + new XAttribute("index", index), + new XAttribute("completed", completed), + new XAttribute("disabled", "false") + ); + + tags.Add(tag); + + paragraph.AddFirst(tags.OrderBy(t => t.Attribute("index").Value)); + } + else + { + // synchronize tag with reminder + var tcompleted = tag.Attribute("completed").Value == "true"; + var rcompleted = tagStatus; + if (tcompleted != rcompleted) + { + tag.Attribute("completed").Value = rcompleted ? "true" : "false"; + } + } + return index; + } + /// /// Apply the given quick style mappings to all descendents of the specified outline. diff --git a/OneMore/OneMore.csproj b/OneMore/OneMore.csproj index de9e00477f..01138c4bec 100644 --- a/OneMore/OneMore.csproj +++ b/OneMore/OneMore.csproj @@ -181,6 +181,7 @@ + Form