Fixed web searches failing when several pages were read at once (#972)
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Verify (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-09-14 17:45:35 +02:00
1 parent 80eccca999
commit f869122070
4 files changed
+215 -8

No files matched your search

+44 -6
View File
@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using System.Net;
using System.Net.Http.Headers;
using System.Net.Sockets;
@@ -14,18 +15,39 @@ public sealed class HTMLParser
private const int DEFAULT_MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
/// <summary>
/// The HTML to Markdown converter, built once from a fixed configuration.
/// The fixed configuration every HTML to Markdown conversion runs with.
/// </summary>
/// <remarks>
/// Shared rather than built per call: the configuration never changes, and one web search
/// converts a page per result.
/// This one is shared, because it is only ever read: a configuration holds no counters and no
/// collections which get written to. The converters reading it are not shared, see the pool
/// below.
/// </remarks>
private static readonly Converter MARKDOWN_CONVERTER = new(new Config
private static readonly Config MARKDOWN_CONFIG = new()
{
UnknownTags = Config.UnknownTagsOption.Bypass,
RemoveComments = true,
SmartHrefHandling = true,
});
};
/// <summary>
/// The converters not currently in use, kept so that the reflection in their constructor does
/// not run for every page.
/// </summary>
/// <remarks>
/// One converter per conversion rather than one for all of them: a converter tracks the
/// ancestors of the node it is at in state of its own, updates that state at every single node,
/// and does so without any synchronization. A web search converts up to four pages at the same
/// time, which let those conversions tear each other's ancestor lists apart — sometimes loudly,
/// as an index outside the bounds of an array, and sometimes quietly, as a list indented by the
/// depth another page happened to be at.<br/><br/>
/// Which converter gets which page does not matter, so the pool needs no key: that ancestor
/// state is entered and left in pairs around every node, which leaves it empty once a
/// conversion returns. Nothing of a page outlives its own conversion. A key would, in fact, do
/// harm — two conversions of the same page at the same time would share one converter again.
/// <br/><br/>
/// The pool holds no more converters than are ever converting at once, which is a handful.
/// </remarks>
private static readonly ConcurrentBag<Converter> CONVERTER_POOL = [];
/// <summary>
/// Loads a web page.
@@ -238,5 +260,21 @@ public sealed class HTMLParser
/// </summary>
/// <param name="html">The HTML content to parse.</param>
/// <returns>The converted Markdown content.</returns>
public static string ParseToMarkdown(string html) => MARKDOWN_CONVERTER.Convert(html);
/// <remarks>
/// The converter returns to the pool only after it converted without throwing, and that is
/// deliberately not done in a finally block: a conversion which throws leaves the ancestors it
/// entered behind, because the library does not unwind them itself. Such a converter would
/// count those ancestors into every page it is handed afterwards, so it is left to the garbage
/// collector rather than passed on.
/// </remarks>
public static string ParseToMarkdown(string html)
{
if (!CONVERTER_POOL.TryTake(out var converter))
converter = new Converter(MARKDOWN_CONFIG);
var markdown = converter.Convert(html);
CONVERTER_POOL.Add(converter);
return markdown;
}
}