/// <summary> /// Converts internal urls to public ones in a given html fragment /// </summary> /// <param name="html"></param> /// <param name="converters"></param> /// <returns></returns> internal static string ConvertInternalUrlsToPublic(string html, IEnumerable <IInternalUrlConverter> converters) { var convertersMap = GetConvertersMap(_ => _); if (!convertersMap.Any()) { return(html); } // Urls, generated in UserControl-s may still have "~/" as a prefix foreach (var urlPrefix in convertersMap.Keys) { string rawUrlPrefix = "~/" + urlPrefix; string resolvedUrlPrefix = ResolvePrefix(urlPrefix); html = UrlUtils.ReplaceUrlPrefix(html, rawUrlPrefix, resolvedUrlPrefix); } StringBuilder result = null; var urlsToConvert = new List <UrlToConvert> (); foreach (var pair in convertersMap) { string internalPrefix = ResolvePrefix(pair.Key); var converter = pair.Value; // Bracket encoding fix string prefixToSearch = internalPrefix; if (prefixToSearch.EndsWith("(", StringComparison.Ordinal)) { prefixToSearch = prefixToSearch.Substring(0, internalPrefix.Length - 1); } urlsToConvert.AddRange(UrlUtils.FindUrlsInHtml(html, prefixToSearch).Select(match => new UrlToConvert(match, internalPrefix, converter))); } // Sorting the offsets by descending, so we can replace urls in that order by not affecting offsets of not yet processed urls urlsToConvert.Sort((a, b) => - a.Match.Index.CompareTo(b.Match.Index)); int lastReplacementIndex = int.MaxValue; var urlSpace = new UrlSpace(); var measurements = new Dictionary <string, Measurement>(); var convertionCache = new Dictionary <string, string>(); foreach (var urlToConvert in urlsToConvert) { UrlUtils.UrlMatch urlMatch = urlToConvert.Match; if (urlMatch.Index == lastReplacementIndex) { continue; } string internalUrlPrefix = urlToConvert.UrlPrefix; string internalUrl = urlMatch.Value; string publicUrl; if (!convertionCache.TryGetValue(internalUrl, out publicUrl)) { string decodedInternalUrl = internalUrl.Replace("%28", "(").Replace("%29", ")").Replace("&", "&"); if (!decodedInternalUrl.StartsWith(internalUrlPrefix)) { continue; } var converter = urlToConvert.Converter; MeasureConvertionPerformance(measurements, converter, () => { publicUrl = urlToConvert.Converter.ToPublicUrl(decodedInternalUrl, urlSpace); }); if (publicUrl == null) { convertionCache.Add(internalUrl, null); continue; } // Encoding xml attribute value publicUrl = publicUrl.Replace("&", "&"); convertionCache.Add(internalUrl, publicUrl); } else { if (internalUrl == null) { continue; } } if (result == null) { result = new StringBuilder(html); } result.Remove(urlMatch.Index, urlMatch.Value.Length); result.Insert(urlMatch.Index, publicUrl); lastReplacementIndex = urlMatch.Index; } foreach (var measurement in measurements.Values) { Profiler.AddSubMeasurement(measurement); } return(result != null?result.ToString() : html); }