Fixed local source links being refused by Word and LibreOffice

This commit is contained in:
Thorsten Sommer committed 2026-09-15 17:24:11 +02:00
1 parent 91d0e742de
commit baa48c80e1
6 files changed
+125 -7

No files matched your search

@@ -204,6 +204,26 @@ public static class FileExportFormatExtensions
_ => WITHOUT_BYTE_ORDER_MARK,
};
/// <summary>
/// Determines whether a link into a local file may name the page it points at.
/// </summary>
/// <remarks>
/// A page is named by the fragment of the link, the way the PDF open parameters call for. A
/// browser and a PDF reader follow that and open the document on the page; Word and LibreOffice
/// take the fragment for part of the file name, look for a file which does not exist, and refuse
/// the link altogether. There the page is dropped, so the link at least opens the document --
/// which page it was stays in the title of the source. Verified on 2026-09-15 with LibreOffice
/// on an exported .odt. A format added later keeps the page unless it is known to stumble too.
/// </remarks>
/// <param name="format">The format.</param>
/// <returns>True, when a reader of this format follows such a link.</returns>
public static bool FollowsPageAnchors(this FileExportFormat format) => format switch
{
FileExportFormat.MICROSOFT_WORD or FileExportFormat.OPEN_DOCUMENT_TEXT => false,
_ => true,
};
/// <summary>
/// Returns the name Pandoc knows the format by.
/// </summary>
+1 -1
View File
@@ -118,7 +118,7 @@ public static class PandocExport
// We read the text before we ask for a path: when there is nothing to convert, the user
// should learn that right away instead of picking a file first and getting an error afterwards.
//
if (!markdownContent.TryGetExportMarkdown(out var markdownText))
if (!markdownContent.TryGetExportMarkdown(out var markdownText, format.FollowsPageAnchors()))
{
LOGGER.LogWarning("Cannot export the content as {ExportFormat}, because it carries no text.", format);
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("Only text messages can be exported.")));
@@ -138,8 +138,9 @@ public static partial class SourceExtensions
/// Converts a list of sources to a markdown-formatted string.
/// </summary>
/// <param name="sources">The list of sources to convert.</param>
/// <param name="keepPageAnchors">Whether a link into a local file may name its page; see the method below.</param>
/// <returns>A markdown-formatted string representing the sources.</returns>
public static string ToMarkdown(this IList<Source> sources)
public static string ToMarkdown(this IList<Source> sources, bool keepPageAnchors = true)
{
var sb = new StringBuilder();
foreach (var group in sources.GroupSources())
@@ -152,8 +153,9 @@ public static partial class SourceExtensions
foreach (var numberedSource in group.Sources)
{
var url = keepPageAnchors ? numberedSource.Source.URL : WithoutPageAnchor(numberedSource.Source.URL);
sb.Append($"- [{numberedSource.Number}] ");
AppendMarkdownLink(sb, numberedSource.Source.Title, numberedSource.Source.URL);
AppendMarkdownLink(sb, numberedSource.Source.Title, url);
sb.AppendLine();
}
}
@@ -161,6 +163,29 @@ public static partial class SourceExtensions
return sb.ToString();
}
/// <summary>
/// Takes the page off a link into a local file, for a reader which cannot follow it.
/// </summary>
/// <remarks>
/// Everything a local link carries in its fragment is dropped, not only a page: a chunk is no
/// use to any reader either, and what breaks such a link is the fragment itself rather than what
/// stands in it. A web address keeps its fragment untouched, because there the fragment is part
/// of the address and naming a section of a page is exactly what it is for.
/// </remarks>
/// <param name="url">The link of the source.</param>
/// <returns>The link without its fragment, or the link itself when it carries none.</returns>
private static string WithoutPageAnchor(string url)
{
if (string.IsNullOrWhiteSpace(url))
return url;
var cleanedUrl = url.Trim().Replace("\r", string.Empty).Replace("\n", string.Empty);
if (!Uri.TryCreate(cleanedUrl, UriKind.Absolute, out var absoluteUri) || !absoluteUri.IsFile || absoluteUri.Fragment.Length == 0)
return url;
return absoluteUri.GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Fragment, UriFormat.UriEscaped);
}
/// <summary>
/// Converts a list of sources to a markdown-formatted string, headed by a title of its own.
/// </summary>
@@ -171,10 +196,11 @@ public static partial class SourceExtensions
/// for this and the chat does not.
/// </remarks>
/// <param name="sources">The list of sources to convert.</param>
/// <param name="keepPageAnchors">Whether a link into a local file may name its page.</param>
/// <returns>A markdown-formatted string representing the sources, or an empty string when there are none.</returns>
public static string ToExportMarkdown(this IList<Source> sources)
public static string ToExportMarkdown(this IList<Source> sources, bool keepPageAnchors = true)
{
var sourcesMarkdown = sources.ToMarkdown();
var sourcesMarkdown = sources.ToMarkdown(keepPageAnchors);
if (string.IsNullOrWhiteSpace(sourcesMarkdown))
return string.Empty;