Skip to content
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

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
128 changes: 128 additions & 0 deletions cs/Markdown/BinaryTrees.cs
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;
}
}
}
}
17 changes: 17 additions & 0 deletions cs/Markdown/Markdown.csproj
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>
132 changes: 132 additions & 0 deletions cs/Markdown/Md.cs
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);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Тут бы навыделять переменных:

        var markupSpecification = GetMarkupSpecification().ToArray();

        var fragments = FindAllSubstringsForFormatting(markdown, markupSpecification);
        var renderedString = PerformTextFormatting(markdown, fragments);

        return RemoveEscapingOfControlSubstrings(renderedString, 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;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Идея выделить правила обработки в отдельный объект тоже неплохая, я бы даже выделил этот метод в отдельный класс типа MdToHtmlSpecificationBuilder и вызывал его отсюда

По хорошему видится такая структура: есть ISpecificationProvider с методом GetMarkupSpecification, который возвращает массив спецификаций, его реализует MdToHtmlSpecificationBuilder с кодом из этого метода, а Md имеет в конструкторе Md(ISpecificationProvider specificationProvider) { /* тут задать приватное ридонли поле*/}, а метод GetMarkupSpecification вызывается внутри Render, и так ты сможешь в любой момент всунуть ему другую спецификацию

}

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
Copy link

@Inree Inree Dec 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Так мы ж всё равно для каждого нового тега дополнительный раз гоняем по тексту. Тогда алгоритм работает за O(textLength * tagsCount), то есть каждый новый тег будет чуть замедлять работу.

Я думал цикл поиска будет выглядеть так:

  • циклом гонимся по тексту
    • Нашли что какие-то теги из списка подходят к символу по индексу. Если только один, то норм, если больше , то берём тот, что длинее (то есть из одинарного и двойного прочерка берётся двойной)
    • Если в этот момент был открыт контекст другого тега (например, жирный тег найден в рамках заголовка), то смотрим, не соответствует ли он invalidSubstrings этого контекста. Соответствует - пропускаем
    • Если был открыт контекст этого же тега (то есть найден закрывающий, а ранее был найден открывающий), пишем, что всё норм, найден открывающий тут, закрывающий тут, а контекст отпускаем
    • Открываем этот контекст, чтобы на будущих итерациях закрыть
  • Добежали до конца текста - смотрим, какой контекст остался открыт (не нашлось закрытия), дописываем их в конец, чтобы вёрстка не поплыла

Тогда тебе не понадобится (вроде бы 🌚) самописное дерево, а понадобится встроенный в систему Stack, в который ты будешь писать активные на конкретную итерацию контексты и смотреть по ним invalidSubstring

И ещё не стоит забывать про обработку пересечений тегов типа #text _text \n text_ text text", в котором на какой-то итерации найдётся, что закрывается контекст, внутри которого ещё контекст. Тогда по идее надо просто дропать незакрытый контекст, потому что кто-то накосячил в тексте, не наша проблема. В примере первый курсив просто не будет считаться

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Тогда ты за один прогон найдёшь все теги

Copy link
Author

@StepanZakolukin StepanZakolukin Dec 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Если в этот момент был открыт контекст другого тега (например, жирный тег найден в рамках заголовка), то смотрим, не соответствует ли он invalidSubstrings этого контекста. Соответствует - пропускаем

Есть ощущение, что упадет на вот таком случае: "__пересечения _двойных__ и одинарных_ подчерков"

Copy link
Author

@StepanZakolukin StepanZakolukin Dec 2, 2024

Choose a reason for hiding this comment

The 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(@"\\", "\\");;
}
}
101 changes: 101 additions & 0 deletions cs/Markdown/MdTests.cs
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);
}
}
10 changes: 10 additions & 0 deletions cs/Markdown/SingleReplacementTagSpecification.cs
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")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Докопаюсь до \n\r: это для винды такой перенос. Для линуха: \n. В шарпе есть Evironment.NewLine, который в зависимости от системы разный

{
}
}
Loading