using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; using System.Text.RegularExpressions; namespace Clipman { internal static class LinkPresentation { internal const int MaximumUrlCharacters = 8192; private static readonly HashSet DocumentExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) { ".html", ".htm", ".php", ".asp", ".aspx", ".jsp", ".pdf", ".txt", ".md" }; private static readonly HashSet StructuralSegments = new HashSet(StringComparer.OrdinalIgnoreCase) { "p", "page", "view", "index", "default", "home" }; public static bool TryGetUri(ClipEntry entry, out Uri uri) { return TryGetUri(entry == null ? null : entry.Text, out uri); } public static bool TryGetUri(string text, out Uri uri) { uri = null; if (string.IsNullOrEmpty(text) || text.Length > MaximumUrlCharacters) return false; return LinkClassifier.TryGetLinkOnlyUri(text, out uri) && IsUrlWithinLimit(uri); } public static bool TryGetWebUri(ClipEntry entry, out Uri uri) { if (!TryGetUri(entry, out uri)) return false; return uri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) || uri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase); } internal static bool IsUrlWithinLimit(Uri uri) { return uri != null && (uri.OriginalString ?? string.Empty).Length <= MaximumUrlCharacters; } public static string DisplayText(ClipEntry entry) { Uri uri; if (!TryGetUri(entry, out uri)) return string.Empty; var destination = Destination(uri); var name = SanitizeLabel(entry.Name, 200); var label = name.Length > 0 ? name : OfflineLabel(uri); if (string.IsNullOrWhiteSpace(label) || string.Equals(label, destination, StringComparison.CurrentCultureIgnoreCase)) { return destination; } return label + "; " + destination; } public static string SearchText(ClipEntry entry) { Uri uri; if (!TryGetUri(entry, out uri)) return string.Empty; return string.Join(" ", new[] { SanitizeLabel(entry.Name, 200), OfflineLabel(uri), Destination(uri), (entry.Text ?? string.Empty).Trim() }.Where(value => !string.IsNullOrWhiteSpace(value)).ToArray()); } public static string TypeAheadText(ClipEntry entry) { Uri uri; if (!TryGetUri(entry, out uri)) return (entry == null ? string.Empty : entry.Text ?? string.Empty).TrimStart(); var name = SanitizeLabel(entry.Name, 200); return name.Length > 0 ? name : OfflineLabel(uri); } public static string Destination(Uri uri) { if (!IsUrlWithinLimit(uri)) return string.Empty; var host = DisplayHost(uri); var port = uri.IsDefaultPort ? string.Empty : ":" + uri.Port; var path = SafeUnescape(uri.AbsolutePath ?? string.Empty).Trim(); if (path == "/") path = string.Empty; if (path.Length > 90) path = path.Substring(0, 87) + "..."; return host + port + path; } public static string OfflineLabel(Uri uri) { if (!IsUrlWithinLimit(uri)) return string.Empty; var host = DisplayHost(uri); var rawSegments = (uri.AbsolutePath ?? string.Empty) .Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries) .Select(SafeUnescape) .Where(segment => !string.IsNullOrWhiteSpace(segment)) .ToList(); string numeric = null; for (var index = rawSegments.Count - 1; index >= 0; index--) { var segment = rawSegments[index].Trim(); if (Regex.IsMatch(segment, @"^\d+$", RegexOptions.CultureInvariant)) { if (numeric == null) numeric = segment; continue; } segment = RemoveDocumentExtension(segment); if (StructuralSegments.Contains(segment) || IsUuid(segment) || IsHighEntropy(segment)) continue; var readable = MakeReadable(segment); if (readable.Length == 0) continue; if (numeric != null) readable += " " + numeric; return readable; } return host; } private static string DisplayHost(Uri uri) { var host = SanitizeLabel(uri == null ? string.Empty : uri.DnsSafeHost, 255).TrimEnd('.'); if (host.StartsWith("www.", StringComparison.OrdinalIgnoreCase)) host = host.Substring(4); return host; } private static string RemoveDocumentExtension(string value) { if (string.IsNullOrEmpty(value)) return value ?? string.Empty; foreach (var extension in DocumentExtensions) { if (value.Length > extension.Length && value.EndsWith(extension, StringComparison.OrdinalIgnoreCase)) { return value.Substring(0, value.Length - extension.Length); } } return value; } private static string MakeReadable(string value) { if (string.IsNullOrWhiteSpace(value)) return string.Empty; var builder = new StringBuilder(value.Length); foreach (var character in value) { builder.Append(character == '-' || character == '_' ? ' ' : character); } var readable = SanitizeLabel(builder.ToString(), 100); if (readable.Length == 0) return string.Empty; if (readable.All(character => !char.IsLetter(character) || char.IsLower(character))) { readable = char.ToUpper(readable[0]) + readable.Substring(1); } return readable.Length > 100 ? readable.Substring(0, 97) + "..." : readable; } private static bool IsUuid(string value) { Guid ignored; return Guid.TryParse(value, out ignored); } private static bool IsHighEntropy(string value) { if (string.IsNullOrEmpty(value) || value.Length < 24) return false; var letters = value.Count(char.IsLetter); var digits = value.Count(char.IsDigit); var separators = value.Count(character => character == '-' || character == '_' || character == '.'); return letters > 5 && digits > 5 && separators <= 3; } private static string SafeUnescape(string value) { if (!string.IsNullOrEmpty(value) && value.Length > MaximumUrlCharacters) return string.Empty; string decoded; try { decoded = Uri.UnescapeDataString(value ?? string.Empty); } catch { decoded = value ?? string.Empty; } return SanitizeLabel(decoded, 512); } internal static string SanitizeLabel(string value, int maximumScalars) { if (string.IsNullOrEmpty(value) || maximumScalars < 1) return string.Empty; var builder = new StringBuilder(Math.Min(value.Length, maximumScalars * 2)); var scalars = 0; var pendingSpace = false; for (var index = 0; index < value.Length && scalars < maximumScalars;) { var character = value[index]; UnicodeCategory category; var width = 1; if (char.IsHighSurrogate(character)) { if (index + 1 >= value.Length || !char.IsLowSurrogate(value[index + 1])) { index++; continue; } category = CharUnicodeInfo.GetUnicodeCategory(value, index); width = 2; } else { if (char.IsLowSurrogate(character)) { index++; continue; } category = char.GetUnicodeCategory(character); } var codePoint = width == 2 ? char.ConvertToUtf32(value, index) : character; if (codePoint == 0xfffd || category == UnicodeCategory.Format || category == UnicodeCategory.Surrogate) { index += width; continue; } if (category == UnicodeCategory.Control || category == UnicodeCategory.LineSeparator || category == UnicodeCategory.ParagraphSeparator || category == UnicodeCategory.SpaceSeparator || (width == 1 && char.IsWhiteSpace(character))) { pendingSpace = builder.Length > 0; index += width; continue; } if (pendingSpace && builder.Length > 0) builder.Append(' '); builder.Append(value, index, width); pendingSpace = false; scalars++; index += width; } return builder.ToString(); } } }