Gliner/Preprocessing/GlinerTextPreprocessor.cs
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace GlinerPoc.Preprocessing;

/// <summary>
/// Exact port of gliner2's WhitespaceTokenSplitter (Phase 3.6): the GLiNER
/// word pre-processing stage that runs BEFORE subword tokenization.
///
/// Official pattern (Python re, VERBOSE | IGNORECASE):
///   (?:https?://[^\s]+|www\.[^\s]+)
///   |[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}
///   |@[a-z0-9_]+
///   |\w+(?:[-_]\w+)*
///   |\S
/// each yield is (token.lower(), start, end).
///
/// Python \w = Unicode alphanumerics (L*, Nd, Nl, No) + underscore; the C#
/// class [\p{L}\p{N}_] is the same set. Known documented divergence: Python
/// \s additionally matches the file-separator control range U+009C-009F
/// region characters that .NET \s does not (irrelevant for V1 English scope;
/// recorded in the architecture doc).
/// </summary>
public static class GlinerTextPreprocessor
{
	private static readonly Regex Pattern = new(
		"(?:https?://[^\\s]+|www\\.[^\\s]+)" +
		"|[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}" +
		"|@[a-zA-Z0-9_]+" +
		"|[" + "\\p{L}\\p{N}_" + "]+(?:[-_][" + "\\p{L}\\p{N}_" + "]+)*" +
		// Surrogate pairs must match as ONE token: Python \S operates on whole
		// code points, .NET \S on UTF-16 code units (emoji would split).
		"|(?:[\\uD800-\\uDBFF][\\uDC00-\\uDFFF])" +
		"|\\S",
		RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);

	/// <summary>
	/// Official context rule: append "." when the text does not end with
	/// '.', '!' or '?'; an empty context becomes ".".
	/// </summary>
	public static string ApplyContextRule( string text )
	{
		if ( string.IsNullOrEmpty( text ) )
		{
			return ".";
		}
		char last = text[^1];
		if ( last == '.' || last == '!' || last == '?' )
		{
			return text;
		}
		return text + ".";
	}

	/// <summary>Split into lowercased words exactly like the official splitter.</summary>
	public static List<string> SplitWords( string text, List<string> results = null )
	{
		results ??= new List<string>( 16 );
		foreach ( Match m in Pattern.Matches( text ) )
		{
			results.Add( m.Value.ToLowerInvariant() );
		}
		return results;
	}
}