Skip to content

Commit

Permalink
1414 convert md multiple outlines (#1466)
Browse files Browse the repository at this point in the history
clean only updated Outlines
#1414
  • Loading branch information
stevencohn authored Jul 10, 2024
1 parent 27a00e3 commit 823a5f7
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 59 deletions.
95 changes: 76 additions & 19 deletions OneMore/Commands/Edit/ConvertMarkdownCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace River.OneMoreAddIn.Commands
{
using River.OneMoreAddIn.Models;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
Expand All @@ -31,33 +32,89 @@ public override async Task Execute(params object[] args)
AllContent = (page.SelectionScope != SelectionScope.Region)
};

var content = await editor.ExtractSelectedContent();
var paragraphs = content.Elements(ns + "OE").ToList();
IEnumerable<XElement> outlines;
if (page.SelectionScope != SelectionScope.Region)
{
// process all outlines
outlines = page.Root.Elements(ns + "Outline");
}
else
{
// process only the selected outline
outlines = new List<XElement>
{
page.Root.Elements(ns + "Outline")
.Descendants(ns + "T")
.First(e => e.Attributes().Any(a => a.Name == "selected" && a.Value == "all"))
.FirstAncestor(ns + "Outline")
};
}

var reader = new PageReader(page)
// process each outline in sequence. By scoping to an outline, PageReader/Editor
// can maintain positional context and scope updates to the outline

foreach (var outline in outlines)
{
ColumnDivider = "|",
TableSides = "|"
};
var range = new SelectionRange(outline);
range.Deselect();

var filepath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
var content = await editor.ExtractSelectedContent(outline);
var paragraphs = content.Elements(ns + "OE").ToList();

var text = reader.ReadTextFrom(paragraphs, page.SelectionScope != SelectionScope.Region);
var body = OneMoreDig.ConvertMarkdownToHtml(filepath, text);
var reader = new PageReader(page)
{
ColumnDivider = "|",
TableSides = "|"
};

editor.InsertAtAnchor(new XElement(ns + "HTMLBlock",
new XElement(ns + "Data",
new XCData($"<html><body>{body}</body></html>")
)
));
var filepath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());

await one.Update(page);
var text = reader.ReadTextFrom(paragraphs, page.SelectionScope != SelectionScope.Region);
var body = OneMoreDig.ConvertMarkdownToHtml(filepath, text);

editor.InsertAtAnchor(new XElement(ns + "HTMLBlock",
new XElement(ns + "Data",
new XCData($"<html><body>{body}</body></html>")
)
));
}

// find and convert headers based on styles
page = await one.GetPage(page.PageId, OneNote.PageDetail.Basic);
MarkdownConverter.RewriteHeadings(page);
MarkdownConverter.SpaceOutParagraphs(page, 12);
// temporarily collect all outline IDs
var outlineIDs = page.Root.Elements(ns + "Outline")
.Select(e => e.Attribute("objectID").Value)
// must ToList or Count comparison won't work!
.ToList();

// update will remove unmodified omHash outlines
await one.Update(page);

// identify only remaining untouched outlines by exclusion
var untouchedIDs = outlineIDs.Except(
page.Root.Elements(ns + "Outline").Select(e => e.Attribute("objectID").Value))
// must ToList or Count comparison won't work!
.ToList();

if (untouchedIDs.Count < outlineIDs.Count)
{
// Pass 2, cleanup...

// find and convert headers based on styles
page = await one.GetPage(page.PageId, OneNote.PageDetail.Basic);

var converter = new MarkdownConverter(page);

// only clean up new (modified) outlines
var touched = page.Root.Elements(ns + "Outline")
.Where(e => !untouchedIDs.Contains(e.Attribute("objectID").Value));

foreach (var outline in touched)
{
converter.RewriteHeadings(outline);
converter.SpaceOutParagraphs(outline, 12);
}

await one.Update(page);
}
}
}
}
10 changes: 8 additions & 2 deletions OneMore/Commands/File/ImportCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -585,13 +585,19 @@ private async Task ImportMarkdownFile(string filepath, CancellationToken token)
)
));

MarkdownConverter.RewriteHeadings(page);
var converter = new MarkdownConverter(page);
converter.RewriteHeadings();

await one.Update(page);

// Pass 2, cleanup...

// find and convert headers based on styles
page = await one.GetPage(pageId, OneNote.PageDetail.Basic);
MarkdownConverter.RewriteHeadings(page);

converter = new MarkdownConverter(page);
converter.RewriteHeadings();

await one.Update(page);

await one.NavigateTo(pageId);
Expand Down
126 changes: 92 additions & 34 deletions OneMore/Commands/File/Markdown/MarkdownConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,39 @@ private sealed class Candidate
}


public static void RewriteHeadings(Page page)
private readonly Page page;
private readonly XNamespace ns;
private readonly StyleAnalyzer analyzer;


public MarkdownConverter(Page page)
{
this.page = page;
ns = page.Namespace;

analyzer = new StyleAnalyzer(page.Root);
}


/// <summary>
/// Applies standard OneNote styling to all recognizable headings in all Outlines
/// on the page
/// </summary>
public void RewriteHeadings()
{
var analyzer = new StyleAnalyzer(page.Root);
var ns = page.Namespace;
foreach (var outline in page.Root.Elements("Outline"))
{
RewriteHeadings(outline);
}
}


page.Root.Elements(ns + "Outline")
/// <summary>
/// Applies standard OneNote styling all recognizable headings in the given Outline
/// </summary>
public void RewriteHeadings(XElement outline)
{
var headings = outline
.Descendants(ns + "OE")
// candidate headings imported from markdown should have exactly one text run
.Where(e => e.Elements(ns + "T").Count() == 1)
Expand All @@ -42,55 +69,61 @@ public static void RewriteHeadings(Page page)
return c;
})
// shouldn't happen but...
.Where(c => c.Key != null)
.ForEach(heading =>
{
// ensures quick style is declared if not already
var quick = page.GetQuickStyle((StandardStyles)heading.Key);

// nix inline style we added during phase 1 import
heading.Element.Attributes().Where(a => a.Name == "style").Remove();
// set heading quick style on OE
heading.Element.SetAttributeValue("quickStyleIndex", quick.Index);

var stylizer = new Stylizer(quick);

heading.Element
.Elements(ns + "T")
.ForEach(e =>
{
// set any additional css on text run such as italics
stylizer.ApplyStyle(e);
});
});
.Where(c => c.Key != null);

foreach (var heading in headings)
{
// ensures quick style is declared if not already
var quick = page.GetQuickStyle((StandardStyles)heading.Key);

// nix inline style we added during phase 1 import
heading.Element.Attributes().Where(a => a.Name == "style").Remove();
// set heading quick style on OE
heading.Element.SetAttributeValue("quickStyleIndex", quick.Index);

var stylizer = new Stylizer(quick);

heading.Element
.Elements(ns + "T")
.ForEach(e =>
{
// set any additional css on text run such as italics
stylizer.ApplyStyle(e);
});
}
}


private static StandardStyles? MatchHeading(Style style)
{
if (style.FontFamily == "Calibri")
{
if (style.FontSize == "20.0" && style.Color == Style.Automatic)
var standard = StandardStyles.PageTitle.GetDefaults();
if (style.FontSize == standard.FontSize && style.Color == Style.Automatic)
{
return StandardStyles.PageTitle;
}

if (style.FontSize == "16.0" && style.Color == "#1E4E79")
standard = StandardStyles.Heading1.GetDefaults();
if (style.FontSize == standard.FontSize && style.Color == standard.Color)
{
return StandardStyles.Heading1;
}

if (style.FontSize == "14.0" && style.Color == "#2E75B5")
standard = StandardStyles.Heading2.GetDefaults();
if (style.FontSize == standard.FontSize && style.Color == standard.Color)
{
return StandardStyles.Heading2;
}

if (style.FontSize == "12.0" && style.Color == "#5B9BD5")
standard = StandardStyles.Heading3.GetDefaults();
if (style.FontSize == standard.FontSize && style.Color == standard.Color)
{
return style.IsItalic ? StandardStyles.Heading4 : StandardStyles.Heading3;
}

if (style.FontSize == "11.0" && style.Color == "#2E75B5")
standard = StandardStyles.Heading5.GetDefaults();
if (style.Color == standard.Color)
{
return style.IsItalic ? StandardStyles.Heading6 : StandardStyles.Heading5;
}
Expand All @@ -100,14 +133,39 @@ public static void RewriteHeadings(Page page)
}


public static void SpaceOutParagraphs(Page page, float spaceAfter)
/// <summary>
/// Adds OneNote paragraph spacing in all Outlines on the page
/// </summary>
/// <param name="spaceAfter"></param>
public void SpaceOutParagraphs(float spaceAfter)
{
foreach (var outline in page.Root.Elements("Outline"))
{
SpaceOutParagraphs(outline, spaceAfter);
}
}


/// <summary>
/// Adds OneNote paragraph spacing in the given Outline
/// </summary>
/// <param name="spaceAfter"></param>
public void SpaceOutParagraphs(XElement outline, float spaceAfter)
{
var ns = page.Namespace;
var after = $"{spaceAfter:0.0}";

foreach (var item in page.Root
var last = outline.Descendants(ns + "OE").Last();

foreach (var item in outline
.Descendants(ns + "OE")
.Where(e => e.NextNode is not null))
.Where(e =>
// not the last paragraph in the Outline
e != last &&
// any paragraph that is not a List
((e.NextNode is not null && !e.Elements(ns + "List").Any()) ||
// any last item in a List
(e.NextNode is null && e.Elements(ns + "List").Any())
)))
{
item.SetAttributeValue("spaceAfter", after);
}
Expand Down
18 changes: 14 additions & 4 deletions OneMore/Models/PageEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,12 @@ public void InsertAtAnchor(XElement content)
/// Extracts all selected content as a single OEChildren element, preserving relative
/// indents, table content, etc. The selected content is removedd from the page.
/// </summary>
/// <param name="targetOutline">
/// Optional Outline element to process.
/// Default is to process all page content.
/// </param>
/// <returns>An OEChildren XElement</returns>
public async Task<XElement> ExtractSelectedContent()
public async Task<XElement> ExtractSelectedContent(XElement targetOutline = null)
{
var content = new XElement(ns + "OEChildren");
// new XAttribute(XNamespace.Xmlns + OneNote.Prefix, ns.ToString())
Expand All @@ -144,8 +148,13 @@ public async Task<XElement> ExtractSelectedContent()
// IMPORTANT: Within an OE, no more than exactly one OEContent element
// can be selected at a time!

var runs = page.Root.Elements(ns + "Outline")
.Descendants(ns + "OE")


var paragraphs = targetOutline is null
? page.Root.Elements(ns + "Outline").Descendants(ns + "OE")
: targetOutline.Descendants(ns + "OE");

var runs = paragraphs
.Elements()
.Where(e =>
// tables are handled via their cell contents
Expand Down Expand Up @@ -256,7 +265,8 @@ private async Task<List<Snippet>> ExtractSnippets(List<XElement> runs)
// to determine if this element should be discarded.
runs.Reverse();

foreach (var run in runs)
// List elements will be handled inline along with their associated content
foreach (var run in runs.Where(e => e.Name.LocalName != "List"))
{
var snippet = new Snippet
{
Expand Down
5 changes: 5 additions & 0 deletions OneMore/Styles/StandardStyles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ internal enum StandardStyles
}


/// <summary>
/// Defines the standard built-in OneNote styles as of v15
/// </summary>
internal static class StandardStylesExtensions
{
public static QuickStyleDef GetDefaults(this StandardStyles key)
Expand Down Expand Up @@ -71,6 +74,7 @@ public static QuickStyleDef GetDefaults(this StandardStyles key)
break;

case StandardStyles.PageTitle:
// this FontFamily is a customization
style.FontFamily = "Calibri Light";
style.FontSize = "20.0";
break;
Expand All @@ -86,6 +90,7 @@ public static QuickStyleDef GetDefaults(this StandardStyles key)
break;

case StandardStyles.Code:
// this FontFamily is a customization
style.FontFamily = "Consolas";
style.Ignored = true;
break;
Expand Down

0 comments on commit 823a5f7

Please sign in to comment.