| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344 |
- namespace Terminal.Gui.Drivers;
- internal static class Osc8UrlLinker
- {
- internal readonly struct Options
- {
- internal readonly string [] _allowedSchemes;
- internal readonly bool _validateWithUri;
- private Options (string [] allowedSchemes, bool validateWithUri)
- {
- _allowedSchemes = allowedSchemes;
- _validateWithUri = validateWithUri;
- }
- public static Options CreateDefault ()
- {
- return new Options (
- allowedSchemes: ["http", "https", "ftp", "ftps"],
- validateWithUri: true
- );
- }
- }
- private static readonly Options _defaultOptions = Options.CreateDefault ();
- internal static StringBuilder WrapOsc8 (StringBuilder input)
- {
- return WrapOsc8 (input, _defaultOptions);
- }
- internal static StringBuilder WrapOsc8 (StringBuilder input, Options options)
- {
- if (input.Length == 0)
- {
- return input;
- }
- string text = input.ToString ();
- int len = text.Length;
- StringBuilder? result = null;
- int copyFrom = 0;
- int i = 0;
- while (i < len)
- {
- if (text [i] == '\x1B')
- {
- int escEnd = ParseEscapeSequence (text, i, len);
- if (result != null)
- {
- if (i > copyFrom)
- {
- result.Append (text, copyFrom, i - copyFrom);
- }
- result.Append (text, i, escEnd - i);
- copyFrom = escEnd;
- }
- i = escEnd;
- continue;
- }
- int segStart = i;
- int nextEsc = text.IndexOf ('\x1B', segStart);
- if (nextEsc < 0)
- {
- nextEsc = len;
- }
- bool changed;
- string processed = WrapPlainText (text, segStart, nextEsc, options, out changed);
- if (changed)
- {
- result ??= new StringBuilder (text.Length + 100);
- if (segStart > copyFrom)
- {
- result.Append (text, copyFrom, segStart - copyFrom);
- }
- result.Append (processed);
- copyFrom = nextEsc;
- }
- i = nextEsc;
- }
- if (result is null)
- {
- return input;
- }
- if (copyFrom < len)
- {
- result.Append (text, copyFrom, len - copyFrom);
- }
- return result;
- }
- private static int ParseEscapeSequence (string text, int start, int len)
- {
- int i = start;
- if (i + 1 >= len)
- {
- return len;
- }
- char c1 = text [i + 1];
- if (c1 == '[')
- {
- int j = i + 2;
- while (j < len)
- {
- char ch = text [j++];
- if (ch >= '@' && ch <= '~')
- {
- break;
- }
- }
- return j;
- }
- if (c1 == ']')
- {
- int j = i + 2;
- while (j < len)
- {
- char ch = text [j++];
- if (ch == '\x07')
- {
- break;
- }
- if (ch == '\x1B')
- {
- if (j < len && text [j] == '\\')
- {
- j++;
- break;
- }
- }
- }
- return j;
- }
- return Math.Min (i + 2, len);
- }
- private static string WrapPlainText (string full, int start, int endExclusive, Options options, out bool changed)
- {
- ReadOnlySpan<char> span = full.AsSpan (start, endExclusive - start);
- ReadOnlySpan<char> delimiter = "://".AsSpan ();
- int i = 0;
- int copyFrom = 0;
- StringBuilder? sb = null;
- while (i < span.Length)
- {
- int rel = span.Slice (i).IndexOf (delimiter, StringComparison.Ordinal);
- if (rel < 0)
- {
- break;
- }
- int delimAt = i + rel;
- int schemeEnd = delimAt;
- int schemeStart = schemeEnd - 1;
- while (schemeStart >= 0 && char.IsLetter (span [schemeStart]))
- {
- schemeStart--;
- }
- schemeStart++;
- if (schemeStart < 0 || schemeStart >= schemeEnd)
- {
- i = delimAt + delimiter.Length;
- continue;
- }
- ReadOnlySpan<char> scheme = span.Slice (schemeStart, schemeEnd - schemeStart);
- if (!IsAllowedScheme (scheme, options))
- {
- i = delimAt + delimiter.Length;
- continue;
- }
- int urlStart = schemeStart;
- int j = delimAt + delimiter.Length;
- while (j < span.Length && !IsUrlTerminator (span [j]))
- {
- j++;
- }
- // Trim punctuation from the end of the URL, but remember it so we can re-append it after the hyperlink
- int urlEnd = TrimTrailingPunctuation (span, urlStart, j);
- ReadOnlySpan<char> trailing = span.Slice (urlEnd, j - urlEnd);
- if (urlEnd <= (delimAt + delimiter.Length))
- {
- i = j;
- continue;
- }
- string candidate = span.Slice (urlStart, urlEnd - urlStart).ToString ();
- Uri? _;
- if (options._validateWithUri && !IsValidUrl (candidate, options, out _))
- {
- i = j;
- continue;
- }
- sb ??= new StringBuilder (span.Length + 64);
- if (urlStart > copyFrom)
- {
- sb.Append (span.Slice (copyFrom, urlStart - copyFrom));
- }
- // Preserve original candidate for link target and display
- string linkTarget = candidate;
- sb.Append (EscSeqUtils.OSC_StartHyperlink (linkTarget));
- sb.Append (candidate);
- sb.Append (EscSeqUtils.OSC_EndHyperlink ());
- // Re-append the trimmed punctuation/suffix (e.g., "!", ",", ")")
- if (!trailing.IsEmpty)
- {
- sb.Append (trailing);
- }
- copyFrom = j;
- i = j;
- }
- if (sb is null)
- {
- changed = false;
- return span.ToString ();
- }
- if (copyFrom < span.Length)
- {
- sb.Append (span.Slice (copyFrom));
- }
- changed = true;
- return sb.ToString ();
- }
- private static bool IsAllowedScheme (ReadOnlySpan<char> scheme, Options options)
- {
- for (int i = 0; i < options._allowedSchemes.Length; i++)
- {
- string s = options._allowedSchemes [i];
- if (scheme.Equals (s, StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- }
- return false;
- }
- private static bool IsUrlTerminator (char c)
- {
- return char.IsWhiteSpace (c) || c == '<' || c == '>' || c == '"' || c == '\'' || c == '\x1B';
- }
- private static int TrimTrailingPunctuation (ReadOnlySpan<char> span, int start, int end)
- {
- int e = end;
- while (e > start)
- {
- char c = span [e - 1];
- if (c is '.' or ',' or '!' or '?' or ';' or ':' or ']' or '}' or '"')
- {
- e--;
- }
- else
- {
- break;
- }
- }
- if (e > start && span [e - 1] == ')')
- {
- int opens = 0;
- int closes = 0;
- for (int k = start; k < e; k++)
- {
- if (span [k] == '(')
- {
- opens++;
- }
- else if (span [k] == ')')
- {
- closes++;
- }
- }
- while (e > start && closes > opens && span [e - 1] == ')')
- {
- e--;
- closes--;
- }
- }
- return e;
- }
- private static bool IsValidUrl (string candidate, Options options, out Uri? uri)
- {
- if (Uri.TryCreate (candidate, UriKind.Absolute, out uri))
- {
- for (int i = 0; i < options._allowedSchemes.Length; i++)
- {
- string s = options._allowedSchemes [i];
- if (uri.Scheme.Equals (s, StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- }
- }
- uri = null;
- return false;
- }
- }
|