Skip to content

Commit

Permalink
Merge pull request #1181 from stevencohn/1118-fix-footnote-refs-in-runs
Browse files Browse the repository at this point in the history
Fix addition of reference in run with existing references
  • Loading branch information
stevencohn authored Nov 23, 2023
2 parents a203e1d + f94e842 commit c2725d8
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 89 deletions.
188 changes: 106 additions & 82 deletions OneMore/Commands/References/FootnoteEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ namespace River.OneMoreAddIn
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml.Linq;
using Resx = River.OneMoreAddIn.Properties.Resources;
using Resx = Properties.Resources;


internal class FootnoteEditor
{
private const string FootnotesMeta = "omfootnotes";
private const string FootnoteMeta = "omfootnote";
private const string DividerContent = "divider";
private const string EmptyContent = "empty";
private const string RefreshStyle = "font-style:italic;font-size:9.0pt;color:#808080";
Expand Down Expand Up @@ -215,15 +216,15 @@ private async Task<string> WriteFootnoteText(string textId)
// find next footnote label
var label = (divider.NodesAfterSelf()
.OfType<XElement>().Elements(ns + "Meta")
.Where(e => e.Attribute("name").Value.Equals("omfootnote"))
.Where(e => e.Attribute("name").Value.Equals(FootnoteMeta))
.DefaultIfEmpty() // avoids null ref exception
.Max(e => e == null ? 0 : int.Parse(e.Attribute("content").Value))
+ 1).ToString();

// find last footnote (sibling) element after which new note is to be added
var last = divider.NodesAfterSelf()
.OfType<XElement>().Elements(ns + "Meta")
.LastOrDefault(e => e.Attribute("name").Value.Equals("omfootnote"));
.LastOrDefault(e => e.Attribute("name").Value.Equals(FootnoteMeta));

// divider is a valid precedesor sibling; otherwise last's Parent
last = last == null ? divider : last.Parent;
Expand Down Expand Up @@ -275,7 +276,7 @@ private async Task<string> WriteFootnoteText(string textId)
var note = new Paragraph(
new XAttribute("style", $"color:{color}"),
new XElement(ns + "Meta",
new XAttribute("name", "omfootnote"),
new XAttribute("name", FootnoteMeta),
new XAttribute("content", label)
),
new XElement(ns + "T",
Expand Down Expand Up @@ -313,7 +314,7 @@ private bool WriteFootnoteRef(string label)
// find the new footer by its label and get its new objectID
var noteId = page.Root.Descendants(ns + "Meta")
.Where(e =>
e.Attribute("name").Value.Equals("omfootnote") &&
e.Attribute("name").Value.Equals(FootnoteMeta) &&
e.Attribute("content").Value.Equals(label))
.Select(e => e.Parent.Attribute("objectID").Value)
.FirstOrDefault();
Expand Down Expand Up @@ -384,7 +385,9 @@ private bool WriteFootnoteRef(string label)
/// </summary>
public async Task RefreshLabels(bool updatePage = false)
{
var refs = FindSelectedReferences(page.Root.Descendants(ns + "T"), true);
var refs = FindSelectedReferences(
page.Root.Descendants(ns + "T").InDocumentOrder(),
true);

if (refs?.Any() != true)
{
Expand All @@ -394,7 +397,7 @@ public async Task RefreshLabels(bool updatePage = false)

// find all footnotes
var notes = page.Root.Descendants(ns + "Meta")
.Where(e => e.Attribute("name").Value.Equals("omfootnote"))
.Where(e => e.Attribute("name").Value.Equals(FootnoteMeta))
.Select(e => new
{
Element = e.Parent,
Expand All @@ -418,9 +421,7 @@ public async Task RefreshLabels(bool updatePage = false)
int count = 0;
for (int i = 0, label = 1; i < refs.Count; i++, label++)
{
var note = notes
.FirstOrDefault(n => n.Label == refs[i].Label);

var note = notes.Find(n => n.Label == refs[i].Label);
if (note == null)
{
// something is awry!
Expand Down Expand Up @@ -478,8 +479,9 @@ public async Task RefreshLabels(bool updatePage = false)
}

var previous = divider;
foreach (var note in notes)
for (var i = 0; i < notes.Count; i++)
{
var note = notes[i];
previous.AddAfterSelf(note.Element);
previous = note.Element;
}
Expand All @@ -494,40 +496,51 @@ public async Task RefreshLabels(bool updatePage = false)

private List<FootnoteReference> FindSelectedReferences(IEnumerable<XElement> roots, bool super)
{
var pattern = super
? @"vertical-align:super[;'""].*>\[(\d+)\]</span>"
var pattern = super
? @"vertical-align:super[;'""][^>]*>\[(\d+)\]</span>"
: @"\[(\d+)\]";

// find selected "[\d]" labels
var list = roots.DescendantNodes().OfType<XCData>()
var regex = new Regex(pattern);

// there could be multiple references in each text run...

// find selected "[\d]" labels in body of page
var data = roots.DescendantNodes().OfType<XCData>()
.Select(CData => new
{
CData,
match = Regex.Match(CData.Value, pattern)
matches = regex.Matches(CData.Value)
})
.Where(o => o.match.Success)
.Select(o => new FootnoteReference
.Where(o => o.matches.Count > 0);

var list = new List<FootnoteReference>();
foreach (var datum in data)
{
foreach (Match match in datum.matches)
{
CData = o.CData,
Label = int.Parse(o.match.Groups[1].Value),
Index = o.match.Groups[1].Index,
Length = o.match.Groups[1].Length
})
.ToList();
list.Add(new FootnoteReference
{
CData = datum.CData,
Label = int.Parse(match.Groups[1].Value),
Index = match.Groups[1].Index,
Length = match.Groups[1].Length
});
}
}

// find selected footnote text lines
// find selected footnote text lines in footer of page
foreach (var root in roots)
{
var meta = root.Parent.Elements(ns + "Meta")
.Where(e => e.Attribute("name").Value.Equals("omfootnote"))
.Where(e => e.Attribute("name").Value.Equals(FootnoteMeta))
.Select(e => new
{
CData = e.Parent.Element(ns + "T").DescendantNodes().OfType<XCData>().FirstOrDefault(),
Label = int.Parse(e.Attribute("content").Value)
})
.FirstOrDefault();

if ((meta != null) && !list.Any(e => e.Label == meta.Label))
if ((meta != null) && !list.Exists(e => e.Label == meta.Label))
{
var match = Regex.Match(meta.CData.Value, @"\[(\d+)\]");
if (match.Success)
Expand Down Expand Up @@ -556,82 +569,94 @@ private List<FootnoteReference> FindSelectedReferences(IEnumerable<XElement> roo
/// reference or a footnote text.
/// </summary>
/// <remarks>
/// A dialog is displayed if the cursor is not positioned over a footnote ref or text.
/// If the cursor is not positioned over a reference or text then display a message
/// asking the user to move the cursor.
/// </remarks>
public async Task RemoveFootnote()
{
// find all selected paragraph
var elements = page.Root.Elements(ns + "Outline")
.Where(e => e.Attributes("selected").Any())
.Descendants(ns + "T")
.Where(e => e.Attribute("selected")?.Value == "all");

if (elements?.Any() != true)
var cursor = page.GetTextCursor();
if (cursor == null ||
page.SelectionScope != SelectionScope.Empty)
{
logger.WriteLine($"{nameof(FootnoteEditor.RemoveFootnote)} could not find a selected outline");
logger.WriteLine("could not delete footnote, cursor not found");
SystemSounds.Exclamation.Play();
return;
}

// matches both context body refs and footer section text lines
var selections = FindSelectedReferences(elements, false);
if (selections?.Any() != true)
string label = null;
int index = -1;
int length;

var meta = cursor.Parent.Elements(ns + "Meta")
.FirstOrDefault(e => e.Attribute("name").Value == FootnoteMeta);

if (meta != null)
{
logger.WriteLine($"{nameof(FootnoteEditor.RemoveFootnote)} could not find a selected reference");
SystemSounds.Exclamation.Play();
return;
// cursor must be positioned on a footnote text item
label = meta.Attribute("content").Value;
logger.WriteLine($"found note [{label}]");
}

foreach (var selection in selections)
else if (page.SelectionSpecial) // URL?
{
var parent = selection.CData.Parent.Parent; // should be a one:OE
// cursor is on a hyperlink, check that it matches the [label] syntax
var match = Regex.Match(cursor.Value,
@"vertical-align:super[;'""][^>]*>\[(\d+)\]<\/span>");

var found = parent.Elements(ns + "Meta")
.Any(e => e.Attributes("name").Any(a => a.Value.Equals("omfootnote")));

if (found)
if (match.Success)
{
// found a footnote, so remove it and associated reference
label = match.Groups[1].Value;
index = match.Groups[1].Index;
length = match.Groups[1].Length;
logger.WriteLine($"found link is [{label}] @{index}..{length}");
}
}

parent.Remove();
if (string.IsNullOrWhiteSpace(label))
{
logger.WriteLine("could not delete footnote, cursor not positioned");
SystemSounds.Exclamation.Play();
return;
}

// associated reference
var nref = page.Root.Elements(ns + "Outline")
.Where(e => e.Attributes("selected").Any())
.DescendantNodes()
.OfType<XCData>()
.FirstOrDefault(c => Regex.IsMatch(
c.Value,
$@"vertical-align:super[;'""].*>\[{selection.Label}\]</span>"));
if (index < 0)
{
// find reference and remove it
var cdata = page.Root.Elements(ns + "Outline")
.DescendantNodes()
.OfType<XCData>()
.FirstOrDefault(c => Regex.IsMatch(
c.Value,
$@"vertical-align:super[;'""][^>]*>\[{label}\]<\/span>"));

if (nref != null)
{
RemoveReference(nref, selection.Label);
}
}
else
if (cdata != null)
{
// found a reference, so remove it and associated footnote

RemoveReference(selection.CData, selection.Label);
RemoveReference(cdata, label);
}

// associated footnote
var note = page.Root.Descendants(ns + "Meta")
.Where(e =>
e.Attribute("name").Value.Equals("omfootnote") &&
e.Attribute("content").Value.Equals(selection.Label.ToString()))
.Select(e => e.Parent)
.FirstOrDefault();
// found note, remove it
cursor.Parent.Remove();
}
else
{
// found reference, remove it
var cdata = cursor.DescendantNodes().OfType<XCData>().First();
RemoveReference(cdata, label);

note?.Remove();
}
// find note and remove it
page.Root.Descendants(ns + "Meta")
.Where(e =>
e.Attribute("name").Value.Equals(FootnoteMeta) &&
e.Attribute("content").Value.Equals(label))
.Select(e => e.Parent)
.FirstOrDefault()?
.Remove();
}

// make sure divider is set
_ = EnsureFootnoteFooter();

var remaining = divider.NodesAfterSelf().OfType<XElement>().Elements(ns + "Meta")
.Any(e => e.Attribute("name").Value.Equals("omfootnote"));
.Any(e => e.Attribute("name").Value.Equals(FootnoteMeta));

if (remaining)
{
Expand All @@ -647,7 +672,7 @@ public async Task RemoveFootnote()
e.Attribute("content").Value.Equals(EmptyContent))
.Select(e => e.Parent);

if (empties != null)
if (empties.Any())
{
foreach (var empty in empties.ToList())
{
Expand All @@ -666,11 +691,9 @@ public async Task RemoveFootnote()
/*
<a href="..."><span style='vertical-align:super'>[2]</span></a>
*/

private static void RemoveReference(XCData data, int label)
private static void RemoveReference(XCData data, string label)
{
var wrapper = data.GetWrapper();

var a = wrapper.Elements("a").Elements("span")
.Where(e =>
e.Attribute("style").Value.Contains("vertical-align:super") &&
Expand All @@ -684,6 +707,7 @@ private static void RemoveReference(XCData data, int label)
data.Value = wrapper.GetInnerXml();
}
}

#endregion Delete
}
}
13 changes: 6 additions & 7 deletions OneMore/Commands/Tagging/HashtagPageScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace River.OneMoreAddIn.Commands
using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web;
using System.Xml.Linq;
using Models;

Expand Down Expand Up @@ -160,14 +161,12 @@ private string GetPlainText(string text)
{
// normalize the text to be XML compliant...
var value = text.Replace("&nbsp;", " ");
value = Regex.Replace(value, @"\<\s*br\s*\>", "<br/>");
value = Regex.Replace(value, @"(\s)lang=([\w\-]+)([\s/>])", "$1lang=\"$2\"$3");
value = Regex.Replace(value, @"\<\s*br\s*\>", "\n");

// wrap and then extract Text nodes to filter out <spans>
return XElement.Parse($"<cdata>{value}</cdata>")
.DescendantNodes()
.OfType<XText>()
.Aggregate(string.Empty, (x, y) => $"{x} {y}");
var plain = Regex.Replace(value, @"\<[^>]+>", "");
plain = HttpUtility.HtmlDecode(plain);

return plain;
}
}
}

0 comments on commit c2725d8

Please sign in to comment.