-
Notifications
You must be signed in to change notification settings - Fork 300
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Заколюкин Степан #230
base: master
Are you sure you want to change the base?
Заколюкин Степан #230
Changes from all commits
d996f7b
d12bd7e
b90ae8e
c6ce4db
9fecab8
fb761e9
534eaeb
25d5bc2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
using System.Collections; | ||
|
||
namespace Markdown; | ||
|
||
public class BinaryTree<T> : IEnumerable<T> | ||
where T : IComparable | ||
{ | ||
private TreeNode Root; | ||
public void Add(T key) | ||
{ | ||
if (Equals(Root, null)) { Root = new TreeNode(key, null); return; } | ||
|
||
var currentSubtree = Root; | ||
|
||
while (true) | ||
{ | ||
if (key.CompareTo(currentSubtree.Value) >= 0) | ||
{ | ||
currentSubtree.HeightOfRight++; | ||
if (currentSubtree.Right == null) { currentSubtree.Right = new TreeNode(key, currentSubtree); return; } | ||
else currentSubtree = currentSubtree.Right; | ||
} | ||
else | ||
{ | ||
currentSubtree.HeightOfLeft++; | ||
if (currentSubtree.Left == null) { currentSubtree.Left = new TreeNode(key, currentSubtree); return; } | ||
else currentSubtree = currentSubtree.Left; | ||
} | ||
} | ||
} | ||
|
||
public bool Contains(T key) | ||
{ | ||
var currentSubtree = Root; | ||
|
||
while (!Equals(currentSubtree, null)) | ||
{ | ||
if (key.CompareTo(currentSubtree.Value) == 0) | ||
return true; | ||
|
||
if (key.CompareTo(currentSubtree.Value) > 0) | ||
currentSubtree = currentSubtree.Right; | ||
else currentSubtree = currentSubtree.Left; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
public T this[int i] | ||
{ | ||
get | ||
{ | ||
if (Root.HeightOfRight + Root.HeightOfLeft < i || i < 0) | ||
throw new IndexOutOfRangeException(); | ||
|
||
var currentSubtree = Root; | ||
var index = 0; | ||
|
||
while (true) | ||
{ | ||
if (currentSubtree.HeightOfLeft + index == i) return currentSubtree.Value; | ||
else if (currentSubtree.HeightOfLeft + index > i) | ||
currentSubtree = currentSubtree.Left; | ||
else if (currentSubtree.HeightOfLeft < i) | ||
{ | ||
index += currentSubtree.HeightOfLeft + 1; | ||
currentSubtree = currentSubtree.Right; | ||
} | ||
} | ||
} | ||
} | ||
|
||
public IEnumerator<T> GetEnumerator() | ||
{ | ||
if (Root == null) yield break; | ||
|
||
foreach (var subtree in Root) | ||
yield return subtree.Value; | ||
} | ||
|
||
IEnumerator IEnumerable.GetEnumerator() | ||
{ | ||
return GetEnumerator(); | ||
} | ||
|
||
public class TreeNode : IEnumerable<TreeNode> | ||
{ | ||
public T Value; | ||
public int HeightOfLeft { get; set; } | ||
public int HeightOfRight { get; set; } | ||
|
||
public TreeNode Left, Right, Ancestor; | ||
|
||
public TreeNode(T value, TreeNode ancestor) | ||
{ | ||
Value = value; | ||
Ancestor = ancestor; | ||
} | ||
|
||
IEnumerator IEnumerable.GetEnumerator() | ||
{ | ||
return GetEnumerator(); | ||
} | ||
|
||
public IEnumerator<TreeNode> GetEnumerator() | ||
{ | ||
var treeNode = this; | ||
|
||
while (!Equals(treeNode.Left, null)) | ||
treeNode = treeNode.Left; | ||
|
||
while (true) | ||
{ | ||
yield return treeNode; | ||
|
||
if (treeNode.Right != null) | ||
{ | ||
foreach (var tree in treeNode.Right) | ||
yield return tree; | ||
} | ||
|
||
if (treeNode == this) break; | ||
|
||
treeNode = treeNode.Ancestor; | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<OutputType>Exe</OutputType> | ||
<TargetFramework>net8.0</TargetFramework> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="FluentAssertions" Version="6.12.2" /> | ||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" /> | ||
<PackageReference Include="NUnit" Version="4.2.2" /> | ||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" /> | ||
</ItemGroup> | ||
|
||
</Project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
using System.Text; | ||
|
||
namespace Markdown; | ||
|
||
internal class Md | ||
{ | ||
public string Render(string markdown) | ||
{ | ||
var markupSpecification = GetMarkupSpecification().ToArray(); | ||
|
||
return RemoveEscapingOfControlSubstrings(PerformTextFormatting(markdown, | ||
FindAllSubstringsForFormatting(markdown, markupSpecification)), markupSpecification); | ||
} | ||
|
||
private IEnumerable<TagReplacementSpecification> GetMarkupSpecification() | ||
{ | ||
var result = new List<TagReplacementSpecification>(); | ||
|
||
var invalidSubstring = new List<string> { "__" }; | ||
for (var digit = 1; digit < 10; digit++) | ||
invalidSubstring.Add(digit.ToString()); | ||
result.Add(new TagReplacementSpecification( | ||
invalidSubstring, | ||
"_", "<em>", | ||
null, null, | ||
["_ "], [ " _", "__" ])); | ||
|
||
result.Add(new TagReplacementSpecification([], | ||
"__", "<strong>")); | ||
|
||
result.Add(new SingleReplacementTagSpecification([], | ||
"# ", "<h1>")); | ||
|
||
return result; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Идея выделить правила обработки в отдельный объект тоже неплохая, я бы даже выделил этот метод в отдельный класс типа MdToHtmlSpecificationBuilder и вызывал его отсюда По хорошему видится такая структура: есть ISpecificationProvider с методом GetMarkupSpecification, который возвращает массив спецификаций, его реализует MdToHtmlSpecificationBuilder с кодом из этого метода, а Md имеет в конструкторе |
||
} | ||
|
||
private IEnumerable<TextFragment> FindAllSubstringsForFormatting(string text, | ||
IEnumerable<TagReplacementSpecification> markupSpecification) | ||
{ | ||
foreach (var tagSpecific in markupSpecification) | ||
{ | ||
foreach (var fragment in FindAllFragmentsHighlightedByTag(tagSpecific, text)) | ||
yield return fragment; | ||
} | ||
Comment on lines
+40
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Так мы ж всё равно для каждого нового тега дополнительный раз гоняем по тексту. Тогда алгоритм работает за O(textLength * tagsCount), то есть каждый новый тег будет чуть замедлять работу. Я думал цикл поиска будет выглядеть так:
Тогда тебе не понадобится (вроде бы 🌚) самописное дерево, а понадобится встроенный в систему Stack, в который ты будешь писать активные на конкретную итерацию контексты и смотреть по ним invalidSubstring И ещё не стоит забывать про обработку пересечений тегов типа There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Тогда ты за один прогон найдёшь все теги There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Есть ощущение, что упадет на вот таком случае: "__пересечения _двойных__ и одинарных_ подчерков" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. В спецификации к заданию сказано:
A здесь выделение должно сработать по твоему алгоритму |
||
} | ||
|
||
private IEnumerable<TextFragment> FindAllFragmentsHighlightedByTag( | ||
TagReplacementSpecification tagSpecific, string text) | ||
{ | ||
var index = 0; | ||
text = ".." + text; | ||
var lookingForOpenTag = true; | ||
|
||
for (var i = 0; i < text.Length - tagSpecific.InputOpeningTag.Length - 1; i++) | ||
{ | ||
var currentSubstring = text.Substring(i, tagSpecific.InputOpeningTag.Length + 2); | ||
|
||
if (lookingForOpenTag && currentSubstring.EndsWith(tagSpecific.InputOpeningTag) && | ||
i + tagSpecific.InputOpeningTag.Length + 3 < text.Length && | ||
tagSpecific.CheckOpeningTag(text.Substring(i, tagSpecific.InputOpeningTag.Length + 3))) | ||
{ | ||
index = i; | ||
lookingForOpenTag = false; | ||
} | ||
else if (!lookingForOpenTag && currentSubstring.EndsWith(tagSpecific.InputClosingTag) && | ||
tagSpecific.CheckClosingTag(currentSubstring)) | ||
{ | ||
lookingForOpenTag = true; | ||
var length = i + tagSpecific.InputOpeningTag.Length - index; | ||
|
||
if (length > 2 * tagSpecific.InputOpeningTag.Length) | ||
yield return new TextFragment(index, length, tagSpecific); | ||
} | ||
else | ||
{ | ||
lookingForOpenTag = tagSpecific.InvalidSubstringsInMarkup | ||
.Any(currentSubstring.EndsWith) || lookingForOpenTag; | ||
} | ||
} | ||
} | ||
|
||
private string PerformTextFormatting(string text, IEnumerable<TextFragment> fragments) | ||
{ | ||
if (!fragments.Any()) return text; | ||
|
||
var result = new StringBuilder(); | ||
var endOfLastReplacement = -1; | ||
|
||
foreach (var replacementOptions in GetSortedCollectionTags(fragments)) | ||
{ | ||
result.Append(text[(endOfLastReplacement + 1)..replacementOptions.StartIndex]); | ||
result.Append(replacementOptions.NewTag); | ||
endOfLastReplacement = replacementOptions.StartIndex + replacementOptions.OldTag.Length - 1; | ||
} | ||
|
||
if (endOfLastReplacement + 1 != text.Length) | ||
result.Append(text[(endOfLastReplacement + 1)..text.Length]); | ||
|
||
return result.ToString(); | ||
} | ||
|
||
private BinaryTree<TagReplacementOptions> GetSortedCollectionTags(IEnumerable<TextFragment> fragments) | ||
{ | ||
var result = new BinaryTree<TagReplacementOptions>(); | ||
|
||
foreach (var fragment in fragments) | ||
{ | ||
result.Add(new TagReplacementOptions( | ||
fragment.Specification.InputOpeningTag, | ||
fragment.Specification.OutputOpeningTag, | ||
fragment.StartIndex)); | ||
result.Add(new TagReplacementOptions( | ||
fragment.Specification.InputClosingTag, | ||
fragment.Specification.OutputClosingTag, | ||
fragment.StartIndex + fragment.Length - fragment.Specification.InputClosingTag.Length)); | ||
} | ||
|
||
return result; | ||
} | ||
|
||
private string RemoveEscapingOfControlSubstrings(string text, IEnumerable<TagReplacementSpecification> tags) | ||
{ | ||
foreach (var tag in tags) | ||
{ | ||
text = text.Replace('\\' + tag.InputOpeningTag, tag.InputClosingTag); | ||
if (tag.InputClosingTag != tag.InputOpeningTag) | ||
text = text.Replace('\\' + tag.InputClosingTag, tag.InputClosingTag); | ||
} | ||
|
||
return text.Replace(@"\\", "\\");; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
using FluentAssertions; | ||
using NUnit.Framework; | ||
|
||
namespace Markdown; | ||
|
||
|
||
[TestFixture] | ||
internal class MdTests | ||
{ | ||
private Md md; | ||
[SetUp] | ||
public void InitializeFild() | ||
{ | ||
md = new Md(); | ||
} | ||
|
||
[Test] | ||
public void Render_StringEmpty_NoExceptions() | ||
{ | ||
var lambda = () => md.Render(string.Empty); | ||
|
||
lambda.Should().NotThrow(); | ||
} | ||
|
||
[TestCase("_12_3", "_12_3")] | ||
[TestCase("_выделяется тегом_", "<em>выделяется тегом</em>")] | ||
[TestCase("эти_ подчерки_ не считаются выделением", "эти_ подчерки_ не считаются выделением", | ||
"За подчерками, начинающими выделение, должен следовать непробельный символ.")] | ||
[TestCase("_нач_але, и в сер_еди_не, и в кон_це._", "<em>нач</em>але, и в сер<em>еди</em>не, и в кон<em>це.</em>")] | ||
[TestCase("курсив в ра_зных сл_овах не работает", "курсив в ра_зных сл_овах не работает")] | ||
[TestCase("эти _подчерки _не считаются", "эти _подчерки _не считаются", | ||
"Подчерки, заканчивающие выделение, должны следовать за непробельным символом.")] | ||
public void Render_WrappedInSingleUnderscore_WrappedInTagEm(string markdown, string expected, string message = "") | ||
{ | ||
var actual = md.Render(markdown); | ||
|
||
actual.Should().Be(expected); | ||
} | ||
|
||
[TestCase("__выделяется тегом__", "<strong>выделяется тегом</strong>")] | ||
public void Render_WrappedInDoubleUnderscore_WrappedInTagEm(string markdown, string expected) | ||
{ | ||
var actual = md.Render(markdown); | ||
|
||
actual.Should().Be(expected); | ||
} | ||
|
||
[TestCase(@"\_текст\_", "_текст_")] | ||
[TestCase(@"\_\_не выделяется тегом\_\_", "__не выделяется тегом__")] | ||
[TestCase(@"\\_вот это будет выделено тегом_", @"\<em>вот это будет выделено тегом</em>")] | ||
[TestCase(@"Здесь сим\волы экранирования\ \должны остаться.\", @"Здесь сим\волы экранирования\ \должны остаться.\")] | ||
|
||
public void Render_EscapingCharacters_FormattingIsNotApplied(string markdown, string expected) | ||
{ | ||
var actual = md.Render(markdown); | ||
|
||
actual.Should().Be(expected); | ||
} | ||
|
||
[TestCase("Внутри __двойного выделения _одинарное_ тоже__ работает", | ||
"Внутри <strong>двойного выделения <em>одинарное</em> тоже</strong> работает")] | ||
[TestCase("внутри _одинарного __двойное__ не_ работает", "внутри <em>одинарного __двойное__ не</em> работает")] | ||
public void Render_NestedKeywords(string markdown, string expected) | ||
{ | ||
var actual = md.Render(markdown); | ||
|
||
actual.Should().Be(expected); | ||
} | ||
|
||
[TestCase("__Непарные_ символы", "__Непарные_ символы")] | ||
public void Render_UnpairedFormattingCharacters_FormattingIsNotApplied(string markdown, string expected) | ||
{ | ||
var actual = md.Render(markdown); | ||
|
||
actual.Should().Be(expected); | ||
} | ||
|
||
[TestCase("__пересечения _двойных__ и одинарных_ подчерков", "__пересечения _двойных__ и одинарных_ подчерков")] | ||
public void Render_IntersectionDoubleAndSingleUnderscores_FormattingIsNotHappening(string markdown, string expected) | ||
{ | ||
var actual = md.Render(markdown); | ||
|
||
actual.Should().Be(expected); | ||
} | ||
|
||
[TestCase("# Заголовок\n\r текст", "<h1>Заголовок</h1> текст")] | ||
public void Render_Heading_TurnsIntoTagH1(string markdown, string expected) | ||
{ | ||
var actual = md.Render(markdown); | ||
|
||
actual.Should().Be(expected); | ||
} | ||
|
||
[TestCase("# Заголовок __с _разными_ символами__\n\r", "<h1>Заголовок <strong>с <em>разными</em> символами</strong></h1>")] | ||
public void Render_HeadingWithDifferentKeyCharacters(string markdown, string expected) | ||
{ | ||
var actual = md.Render(markdown); | ||
|
||
actual.Should().Be(expected); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
namespace Markdown; | ||
|
||
public class SingleReplacementTagSpecification : TagReplacementSpecification | ||
{ | ||
public SingleReplacementTagSpecification(IEnumerable<string> invalidSubstringsInMarkup, | ||
string inputOpeningTag, string outputOpeningTag) | ||
: base(invalidSubstringsInMarkup, inputOpeningTag, outputOpeningTag, "\n\r") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Докопаюсь до \n\r: это для винды такой перенос. Для линуха: \n. В шарпе есть Evironment.NewLine, который в зависимости от системы разный |
||
{ | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Тут бы навыделять переменных:
Так легче читается