public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken) { using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true)) { var index = 1; foreach (var trackEvent in info.TrackEvents) { cancellationToken.ThrowIfCancellationRequested(); writer.WriteLine(index.ToString(CultureInfo.InvariantCulture)); writer.WriteLine(@"{0:hh\:mm\:ss\,fff} --> {1:hh\:mm\:ss\,fff}", TimeSpan.FromTicks(trackEvent.StartPositionTicks), TimeSpan.FromTicks(trackEvent.EndPositionTicks)); var text = trackEvent.Text; // TODO: Not sure how to handle these text = Regex.Replace(text, @"\\N", " ", RegexOptions.IgnoreCase); writer.WriteLine(text); writer.WriteLine(string.Empty); index++; } } }
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken) { using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true)) { writer.WriteLine("WEBVTT"); writer.WriteLine(string.Empty); foreach (var trackEvent in info.TrackEvents) { cancellationToken.ThrowIfCancellationRequested(); TimeSpan startTime = TimeSpan.FromTicks(trackEvent.StartPositionTicks); TimeSpan endTime = TimeSpan.FromTicks(trackEvent.EndPositionTicks); // make sure the start and end times are different and seqential if (endTime.TotalMilliseconds <= startTime.TotalMilliseconds) { endTime = startTime.Add(TimeSpan.FromMilliseconds(1)); } writer.WriteLine(@"{0:hh\:mm\:ss\.fff} --> {1:hh\:mm\:ss\.fff}", startTime, endTime); var text = trackEvent.Text; // TODO: Not sure how to handle these text = Regex.Replace(text, @"\\n", " ", RegexOptions.IgnoreCase); writer.WriteLine(text); writer.WriteLine(string.Empty); } } }
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken) { using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true)) { var json = _json.SerializeToString(info); writer.Write(json); } }
public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken) { var trackInfo = new SubtitleTrackInfo(); using ( var reader = new StreamReader(stream)) { string line; while ((line = reader.ReadLine()) != null) { cancellationToken.ThrowIfCancellationRequested(); if (string.IsNullOrWhiteSpace(line)) { continue; } var subEvent = new SubtitleTrackEvent {Id = line}; line = reader.ReadLine(); if (string.IsNullOrWhiteSpace(line)) { continue; } var time = Regex.Split(line, @"[\t ]*-->[\t ]*"); subEvent.StartPositionTicks = GetTicks(time[0]); var endTime = time[1]; var idx = endTime.IndexOf(" ", StringComparison.Ordinal); if (idx > 0) endTime = endTime.Substring(0, idx); subEvent.EndPositionTicks = GetTicks(endTime); var multiline = new List<string>(); while ((line = reader.ReadLine()) != null) { if (string.IsNullOrEmpty(line)) { break; } multiline.Add(line); } subEvent.Text = string.Join(ParserValues.NewLine, multiline); subEvent.Text = subEvent.Text.Replace(@"\N", ParserValues.NewLine, StringComparison.OrdinalIgnoreCase); subEvent.Text = Regex.Replace(subEvent.Text, @"\{(\\[\w]+\(?([\w\d]+,?)+\)?)+\}", string.Empty, RegexOptions.IgnoreCase); subEvent.Text = Regex.Replace(subEvent.Text, "<", "<", RegexOptions.IgnoreCase); subEvent.Text = Regex.Replace(subEvent.Text, ">", ">", RegexOptions.IgnoreCase); subEvent.Text = Regex.Replace(subEvent.Text, "<(\\/?(font|b|u|i|s))((\\s+(\\w|\\w[\\w\\-]*\\w)(\\s*=\\s*(?:\\\".*?\\\"|'.*?'|[^'\\\">\\s]+))?)+\\s*|\\s*)(\\/?)>", "<$1$3$7>", RegexOptions.IgnoreCase); trackInfo.TrackEvents.Add(subEvent); } } return trackInfo; }
public void TestParse() { var expectedSubs = new SubtitleTrackInfo { TrackEvents = new List<SubtitleTrackEvent> { new SubtitleTrackEvent { Id = "1", StartPositionTicks = 24000000, EndPositionTicks = 72000000, Text = "Senator, we're <br />making our final <br />approach into Coruscant." }, new SubtitleTrackEvent { Id = "2", StartPositionTicks = 97100000, EndPositionTicks = 133900000, Text = "Very good, Lieutenant." }, new SubtitleTrackEvent { Id = "3", StartPositionTicks = 150400000, EndPositionTicks = 180400000, Text = "It's <br />a <br />trap!" } } }; var sut = new SsaParser(); var stream = File.OpenRead(@"MediaEncoding\Subtitles\TestSubtitles\data.ssa"); var result = sut.Parse(stream, CancellationToken.None); Assert.IsNotNull(result); Assert.AreEqual(expectedSubs.TrackEvents.Count,result.TrackEvents.Count); for (int i = 0; i < expectedSubs.TrackEvents.Count; i++) { Assert.AreEqual(expectedSubs.TrackEvents[i].Id, result.TrackEvents[i].Id); Assert.AreEqual(expectedSubs.TrackEvents[i].StartPositionTicks, result.TrackEvents[i].StartPositionTicks); Assert.AreEqual(expectedSubs.TrackEvents[i].EndPositionTicks, result.TrackEvents[i].EndPositionTicks); Assert.AreEqual(expectedSubs.TrackEvents[i].Text, result.TrackEvents[i].Text); } }
public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken) { var trackInfo = new SubtitleTrackInfo(); var eventIndex = 1; using (var reader = new StreamReader(stream)) { string line; while (reader.ReadLine() != "[Events]") {} var headers = ParseFieldHeaders(reader.ReadLine()); while ((line = reader.ReadLine()) != null) { cancellationToken.ThrowIfCancellationRequested(); if (string.IsNullOrWhiteSpace(line)) { continue; } if(line.StartsWith("[")) break; if(string.IsNullOrEmpty(line)) continue; var subEvent = new SubtitleTrackEvent { Id = eventIndex.ToString(_usCulture) }; eventIndex++; var sections = line.Substring(10).Split(','); subEvent.StartPositionTicks = GetTicks(sections[headers["Start"]]); subEvent.EndPositionTicks = GetTicks(sections[headers["End"]]); subEvent.Text = string.Join(",", sections.Skip(headers["Text"])); RemoteNativeFormatting(subEvent); subEvent.Text = subEvent.Text.Replace("\\n", ParserValues.NewLine, StringComparison.OrdinalIgnoreCase); subEvent.Text = Regex.Replace(subEvent.Text, @"\{(\\[\w]+\(?([\w\d]+,?)+\)?)+\}", string.Empty, RegexOptions.IgnoreCase); trackInfo.TrackEvents.Add(subEvent); } } return trackInfo; }
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken) { // Example: https://github.com/zmalltalker/ttml2vtt/blob/master/data/sample.xml // Parser example: https://github.com/mozilla/popcorn-js/blob/master/parsers/parserTTML/popcorn.parserTTML.js using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true)) { writer.WriteLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>"); writer.WriteLine("<tt xmlns=\"http://www.w3.org/ns/ttml\" xmlns:tts=\"http://www.w3.org/2006/04/ttaf1#styling\" lang=\"no\">"); writer.WriteLine("<head>"); writer.WriteLine("<styling>"); writer.WriteLine("<style id=\"italic\" tts:fontStyle=\"italic\" />"); writer.WriteLine("<style id=\"left\" tts:textAlign=\"left\" />"); writer.WriteLine("<style id=\"center\" tts:textAlign=\"center\" />"); writer.WriteLine("<style id=\"right\" tts:textAlign=\"right\" />"); writer.WriteLine("</styling>"); writer.WriteLine("</head>"); writer.WriteLine("<body>"); writer.WriteLine("<div>"); foreach (var trackEvent in info.TrackEvents) { var text = trackEvent.Text; text = Regex.Replace(text, @"\\n", "<br/>", RegexOptions.IgnoreCase); writer.WriteLine("<p begin=\"{0}\" dur=\"{1}\">{2}</p>", trackEvent.StartPositionTicks, (trackEvent.EndPositionTicks - trackEvent.StartPositionTicks), text); } writer.WriteLine("</div>"); writer.WriteLine("</body>"); writer.WriteLine("</tt>"); } }
private void FilterEvents(SubtitleTrackInfo track, long startPositionTicks, long endTimeTicks, bool preserveTimestamps) { // Drop subs that are earlier than what we're looking for track.TrackEvents = track.TrackEvents .SkipWhile(i => (i.StartPositionTicks - startPositionTicks) < 0 || (i.EndPositionTicks - startPositionTicks) < 0) .ToArray(); if (endTimeTicks > 0) { track.TrackEvents = track.TrackEvents .TakeWhile(i => i.StartPositionTicks <= endTimeTicks) .ToArray(); } if (!preserveTimestamps) { foreach (var trackEvent in track.TrackEvents) { trackEvent.EndPositionTicks -= startPositionTicks; trackEvent.StartPositionTicks -= startPositionTicks; } } }
/// <inheritdoc /> public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken) { using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true)) { var trackEvents = info.TrackEvents; var timeFormat = @"hh\:mm\:ss\.ff"; // Write ASS header writer.WriteLine("[Script Info]"); writer.WriteLine("Title: Jellyfin transcoded ASS subtitle"); writer.WriteLine("ScriptType: v4.00+"); writer.WriteLine(); writer.WriteLine("[V4+ Styles]"); writer.WriteLine("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding"); writer.WriteLine("Style: Default,Arial,20,&H00FFFFFF,&H00FFFFFF,&H19333333,&H910E0807,0,0,0,0,100,100,0,0,0,1,0,2,10,10,10,1"); writer.WriteLine(); writer.WriteLine("[Events]"); writer.WriteLine("Format: Layer, Start, End, Style, Text"); for (int i = 0; i < trackEvents.Count; i++) { cancellationToken.ThrowIfCancellationRequested(); var trackEvent = trackEvents[i]; var startTime = TimeSpan.FromTicks(trackEvent.StartPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture); var endTime = TimeSpan.FromTicks(trackEvent.EndPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture); var text = Regex.Replace(trackEvent.Text, @"\n", "\\n", RegexOptions.IgnoreCase); writer.WriteLine( "Dialogue: 0,{0},{1},Default,{2}", startTime, endTime, text); } } }
public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken) { var trackInfo = new SubtitleTrackInfo(); using (var reader = new StreamReader(stream)) { bool eventsStarted = false; string[] format = "Marked, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text".Split(','); int indexLayer = 0; int indexStart = 1; int indexEnd = 2; int indexStyle = 3; int indexName = 4; int indexEffect = 8; int indexText = 9; int lineNumber = 0; var header = new StringBuilder(); string line; while ((line = reader.ReadLine()) != null) { cancellationToken.ThrowIfCancellationRequested(); lineNumber++; if (!eventsStarted) { header.AppendLine(line); } if (line.Trim().ToLower() == "[events]") { eventsStarted = true; } else if (!string.IsNullOrEmpty(line) && line.Trim().StartsWith(";")) { // skip comment lines } else if (eventsStarted && line.Trim().Length > 0) { string s = line.Trim().ToLower(); if (s.StartsWith("format:")) { if (line.Length > 10) { format = line.ToLower().Substring(8).Split(','); for (int i = 0; i < format.Length; i++) { if (format[i].Trim().ToLower() == "layer") { indexLayer = i; } else if (format[i].Trim().ToLower() == "start") { indexStart = i; } else if (format[i].Trim().ToLower() == "end") { indexEnd = i; } else if (format[i].Trim().ToLower() == "text") { indexText = i; } else if (format[i].Trim().ToLower() == "effect") { indexEffect = i; } else if (format[i].Trim().ToLower() == "style") { indexStyle = i; } } } } else if (!string.IsNullOrEmpty(s)) { string text = string.Empty; string start = string.Empty; string end = string.Empty; string style = string.Empty; string layer = string.Empty; string effect = string.Empty; string name = string.Empty; string[] splittedLine; if (s.StartsWith("dialogue:")) { splittedLine = line.Substring(10).Split(','); } else { splittedLine = line.Split(','); } for (int i = 0; i < splittedLine.Length; i++) { if (i == indexStart) { start = splittedLine[i].Trim(); } else if (i == indexEnd) { end = splittedLine[i].Trim(); } else if (i == indexLayer) { layer = splittedLine[i]; } else if (i == indexEffect) { effect = splittedLine[i]; } else if (i == indexText) { text = splittedLine[i]; } else if (i == indexStyle) { style = splittedLine[i]; } else if (i == indexName) { name = splittedLine[i]; } else if (i > indexText) { text += "," + splittedLine[i]; } } try { var p = new SubtitleTrackEvent(); p.StartPositionTicks = GetTimeCodeFromString(start); p.EndPositionTicks = GetTimeCodeFromString(end); p.Text = GetFormattedText(text); trackInfo.TrackEvents.Add(p); } catch { } } } } //if (header.Length > 0) //subtitle.Header = header.ToString(); //subtitle.Renumber(1); } return(trackInfo); }
public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken) { var trackInfo = new SubtitleTrackInfo(); List <SubtitleTrackEvent> trackEvents = new List <SubtitleTrackEvent>(); using (var reader = new StreamReader(stream)) { string line; while ((line = reader.ReadLine()) != null) { cancellationToken.ThrowIfCancellationRequested(); if (string.IsNullOrWhiteSpace(line)) { continue; } var subEvent = new SubtitleTrackEvent { Id = line }; line = reader.ReadLine(); if (string.IsNullOrWhiteSpace(line)) { continue; } var time = Regex.Split(line, @"[\t ]*-->[\t ]*"); if (time.Length < 2) { // This occurs when subtitle text has an empty line as part of the text. // Need to adjust the break statement below to resolve this. _logger.LogWarning("Unrecognized line in srt: {0}", line); continue; } subEvent.StartPositionTicks = GetTicks(time[0]); var endTime = time[1]; var idx = endTime.IndexOf(" ", StringComparison.Ordinal); if (idx > 0) { endTime = endTime.Substring(0, idx); } subEvent.EndPositionTicks = GetTicks(endTime); var multiline = new List <string>(); while ((line = reader.ReadLine()) != null) { if (string.IsNullOrEmpty(line)) { break; } multiline.Add(line); } subEvent.Text = string.Join(ParserValues.NewLine, multiline); subEvent.Text = subEvent.Text.Replace(@"\N", ParserValues.NewLine, StringComparison.OrdinalIgnoreCase); subEvent.Text = Regex.Replace(subEvent.Text, @"\{(?:\\\d?[\w.-]+(?:\([^\)]*\)|&H?[0-9A-Fa-f]+&|))+\}", string.Empty, RegexOptions.IgnoreCase); subEvent.Text = Regex.Replace(subEvent.Text, "<", "<", RegexOptions.IgnoreCase); subEvent.Text = Regex.Replace(subEvent.Text, ">", ">", RegexOptions.IgnoreCase); subEvent.Text = Regex.Replace(subEvent.Text, "<(\\/?(font|b|u|i|s))((\\s+(\\w|\\w[\\w\\-]*\\w)(\\s*=\\s*(?:\\\".*?\\\"|'.*?'|[^'\\\">\\s]+))?)+\\s*|\\s*)(\\/?)>", "<$1$3$7>", RegexOptions.IgnoreCase); trackEvents.Add(subEvent); } } trackInfo.TrackEvents = trackEvents.ToArray(); return(trackInfo); }
public void TestParse() { var expectedSubs = new SubtitleTrackInfo { TrackEvents = new List<SubtitleTrackEvent> { new SubtitleTrackEvent { Id = "1", StartPositionTicks = 24000000, EndPositionTicks = 52000000, Text = "[Background Music Playing]" }, new SubtitleTrackEvent { Id = "2", StartPositionTicks = 157120000, EndPositionTicks = 173990000, Text = "Oh my god, Watch out!<br />It's coming!!" }, new SubtitleTrackEvent { Id = "3", StartPositionTicks = 257120000, EndPositionTicks = 303990000, Text = "[Bird noises]" }, new SubtitleTrackEvent { Id = "4", StartPositionTicks = 310000000, EndPositionTicks = 319990000, Text = "This text is <font color=\"red\">RED</font> and has not been positioned." }, new SubtitleTrackEvent { Id = "5", StartPositionTicks = 320000000, EndPositionTicks = 329990000, Text = "This is a<br />new line, as is<br />this" }, new SubtitleTrackEvent { Id = "6", StartPositionTicks = 330000000, EndPositionTicks = 339990000, Text = "This contains nested <b>bold, <i>italic, <u>underline</u> and <s>strike-through</s></u></i></b> HTML tags" }, new SubtitleTrackEvent { Id = "7", StartPositionTicks = 340000000, EndPositionTicks = 349990000, Text = "Unclosed but <b>supported HTML tags are left in, SSA italics aren't" }, new SubtitleTrackEvent { Id = "8", StartPositionTicks = 350000000, EndPositionTicks = 359990000, Text = "<ggg>Unsupported</ggg> HTML tags are escaped and left in, even if <hhh>not closed." }, new SubtitleTrackEvent { Id = "9", StartPositionTicks = 360000000, EndPositionTicks = 369990000, Text = "Multiple SSA tags are stripped" }, new SubtitleTrackEvent { Id = "10", StartPositionTicks = 370000000, EndPositionTicks = 379990000, Text = "Greater than (<) and less than (>) are shown" } } }; var sut = new SrtParser(); var stream = File.OpenRead(@"MediaEncoding\Subtitles\TestSubtitles\unit.srt"); var result = sut.Parse(stream, CancellationToken.None); Assert.IsNotNull(result); Assert.AreEqual(expectedSubs.TrackEvents.Count,result.TrackEvents.Count); for (int i = 0; i < expectedSubs.TrackEvents.Count; i++) { Assert.AreEqual(expectedSubs.TrackEvents[i].Id, result.TrackEvents[i].Id); Assert.AreEqual(expectedSubs.TrackEvents[i].StartPositionTicks, result.TrackEvents[i].StartPositionTicks); Assert.AreEqual(expectedSubs.TrackEvents[i].EndPositionTicks, result.TrackEvents[i].EndPositionTicks); Assert.AreEqual(expectedSubs.TrackEvents[i].Text, result.TrackEvents[i].Text); } }
public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken) { var trackInfo = new SubtitleTrackInfo(); using (var reader = new StreamReader(stream)) { bool eventsStarted = false; string[] format = "Marked, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text".Split(','); int indexLayer = 0; int indexStart = 1; int indexEnd = 2; int indexStyle = 3; int indexName = 4; int indexEffect = 8; int indexText = 9; int lineNumber = 0; var header = new StringBuilder(); string line; while ((line = reader.ReadLine()) != null) { cancellationToken.ThrowIfCancellationRequested(); lineNumber++; if (!eventsStarted) header.AppendLine(line); if (line.Trim().ToLower() == "[events]") { eventsStarted = true; } else if (!string.IsNullOrEmpty(line) && line.Trim().StartsWith(";")) { // skip comment lines } else if (eventsStarted && line.Trim().Length > 0) { string s = line.Trim().ToLower(); if (s.StartsWith("format:")) { if (line.Length > 10) { format = line.ToLower().Substring(8).Split(','); for (int i = 0; i < format.Length; i++) { if (format[i].Trim().ToLower() == "layer") indexLayer = i; else if (format[i].Trim().ToLower() == "start") indexStart = i; else if (format[i].Trim().ToLower() == "end") indexEnd = i; else if (format[i].Trim().ToLower() == "text") indexText = i; else if (format[i].Trim().ToLower() == "effect") indexEffect = i; else if (format[i].Trim().ToLower() == "style") indexStyle = i; } } } else if (!string.IsNullOrEmpty(s)) { string text = string.Empty; string start = string.Empty; string end = string.Empty; string style = string.Empty; string layer = string.Empty; string effect = string.Empty; string name = string.Empty; string[] splittedLine; if (s.StartsWith("dialogue:")) splittedLine = line.Substring(10).Split(','); else splittedLine = line.Split(','); for (int i = 0; i < splittedLine.Length; i++) { if (i == indexStart) start = splittedLine[i].Trim(); else if (i == indexEnd) end = splittedLine[i].Trim(); else if (i == indexLayer) layer = splittedLine[i]; else if (i == indexEffect) effect = splittedLine[i]; else if (i == indexText) text = splittedLine[i]; else if (i == indexStyle) style = splittedLine[i]; else if (i == indexName) name = splittedLine[i]; else if (i > indexText) text += "," + splittedLine[i]; } try { var p = new SubtitleTrackEvent(); p.StartPositionTicks = GetTimeCodeFromString(start); p.EndPositionTicks = GetTimeCodeFromString(end); p.Text = GetFormattedText(text); trackInfo.TrackEvents.Add(p); } catch { } } } } //if (header.Length > 0) //subtitle.Header = header.ToString(); //subtitle.Renumber(1); } return trackInfo; }
public void TestWrite() { var infoSubs = new SubtitleTrackInfo { TrackEvents = new List<SubtitleTrackEvent> { new SubtitleTrackEvent { Id = "1", StartPositionTicks = 24000000, EndPositionTicks = 52000000, Text = "[Background Music Playing]" }, new SubtitleTrackEvent { Id = "2", StartPositionTicks = 157120000, EndPositionTicks = 173990000, Text = "Oh my god, Watch out!<br />It's coming!!" }, new SubtitleTrackEvent { Id = "3", StartPositionTicks = 257120000, EndPositionTicks = 303990000, Text = "[Bird noises]" }, new SubtitleTrackEvent { Id = "4", StartPositionTicks = 310000000, EndPositionTicks = 319990000, Text = "This text is <font color=\"red\">RED</font> and has not been positioned." }, new SubtitleTrackEvent { Id = "5", StartPositionTicks = 320000000, EndPositionTicks = 329990000, Text = "This is a<br />new line, as is<br />this" }, new SubtitleTrackEvent { Id = "6", StartPositionTicks = 330000000, EndPositionTicks = 339990000, Text = "This contains nested <b>bold, <i>italic, <u>underline</u> and <s>strike-through</s></u></i></b> HTML tags" }, new SubtitleTrackEvent { Id = "7", StartPositionTicks = 340000000, EndPositionTicks = 349990000, Text = "Unclosed but <b>supported HTML tags are left in, SSA italics aren't" }, new SubtitleTrackEvent { Id = "8", StartPositionTicks = 350000000, EndPositionTicks = 359990000, Text = "<ggg>Unsupported</ggg> HTML tags are escaped and left in, even if <hhh>not closed." }, new SubtitleTrackEvent { Id = "9", StartPositionTicks = 360000000, EndPositionTicks = 369990000, Text = "Multiple SSA tags are stripped" }, new SubtitleTrackEvent { Id = "10", StartPositionTicks = 370000000, EndPositionTicks = 379990000, Text = "Greater than (<) and less than (>) are shown" } } }; var sut = new VttWriter(); if(File.Exists("testVTT.vtt")) File.Delete("testVTT.vtt"); using (var file = File.OpenWrite("testVTT.vtt")) { sut.Write(infoSubs, file, CancellationToken.None); } var result = File.ReadAllText("testVTT.vtt"); var expectedText = File.ReadAllText(@"MediaEncoding\Subtitles\TestSubtitles\expected.vtt"); Assert.AreEqual(expectedText, result); }