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

Большаков Николай #242

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
41 changes: 37 additions & 4 deletions MarkdownSpec.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ __Выделенный двумя символами текст__ должен

__Непарные_ символы в рамках одного абзаца не считаются выделением.

За подчерками, начинающими выделение, должен следовать непробельный символ. Иначе эти_ подчерки_ не считаются выделением
За подчерками, начинающими выделение, должен следовать непробельный символ. Иначе эти_ подчерки_ не считаются выделением
и остаются просто символами подчерка.

Подчерки, заканчивающие выделение, должны следовать за непробельным символом. Иначе эти _подчерки _не считаются_ окончанием выделения
Подчерки, заканчивающие выделение, должны следовать за непробельным символом. Иначе эти _подчерки _не считаются_ окончанием выделения
и остаются просто символами подчерка.

В случае __пересечения _двойных__ и одинарных_ подчерков ни один из них не считается выделением.
Expand All @@ -66,8 +66,41 @@ __Непарные_ символы в рамках одного абзаца н

Таким образом

# Заголовок __с _разными_ символами__
\# Заголовок \_\_с \_разными_ символами__

превратится в:

\<h1>Заголовок \<strong>с \<em>разными\</em> символами\</strong>\</h1>
\<h1>Заголовок \<strong>с \<em>разными\</em> символами\</strong>\</h1>

# Ссылки

Текст формата \[название](ссылка) становится HTML тегом <a href = "ссылка">название</a> (\<a href = "ссылка">название\</a>).

Еще есть упрощенный формат без названия:

Текст в треугольных скобках \<ссылка> превращается в <a href = "ссылка">ссылка</a> (\<a href = "ссылка">ссылка\</a>).

# Изображения

Текст формата \!\[название](путь до изображения) становится HTML тегом <img src="путь до изображения" alt="название"> (\<img src="путь до изображения" alt="название">).

# Списки

Любое число, начинающееся с новой строки, со следующей за ним точкой, превращает идущий за ним текст до переноса строки в элемент нумерованного списка.
Несколько таких элементов собираются в список.

Например:

1. Один
2. Два
345. Три

Превращается в \<ol>\<li>Один\</li>\<li>Два\</li>\<li>Три\</li>\</ol>

Аналогично, стоящий в начале строки знак '-' выделяет элемент ненумерованного списка:

- Один
- Два
- Три

Превращается в \<ul>\<li>Один\</li>\<li>Два\</li>\<li>Три\</li>\</ul>
6 changes: 6 additions & 0 deletions cs/Markdown/IStringProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Markdown;

public interface IStringProcessor
{
public string Render(string str);
}
9 changes: 9 additions & 0 deletions cs/Markdown/Markdown.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
222 changes: 222 additions & 0 deletions cs/Markdown/Md.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
using Markdown.MdTagHandlers;
using Markdown.MdTags;
using Markdown.MdTags.Interfaces;
using System.Text;

namespace Markdown;

public class Md : IStringProcessor
{
private readonly ITagPairsFinder tagPairsFinder;
private readonly IPairTagsIntersectionHandler pairTagsIntersectionHandler;
private readonly IGroupTagInsertionsFinder groupTagInsertionsFinder;
private readonly IMdTag[] mdTags;

public Md() : this(
new TagPairsFinder(),
new PairTagsIntersectionHandler(),
new GroupTagInsertionsFinder(),
[
new UnderscoresBetweenNumbersIgnoreTag(),
new EscapeMdTag(),
new OrderedListItemMdTag(),
new UnorderedListItemMdTag(),
new HeaderMdTag(),
new ImageMdTag(),
new LinkMdTag(),
new BoldTextMdTag(),
new ItalicTextMdTag(),
])
{
}

internal Md(
ITagPairsFinder tagPairsFinder,
IPairTagsIntersectionHandler pairTagsIntersectionHandler,
IGroupTagInsertionsFinder groupTagInsertionsFinder,
params IMdTag[] tags)
{
ArgumentNullException.ThrowIfNull(tagPairsFinder);
ArgumentNullException.ThrowIfNull(pairTagsIntersectionHandler);
ArgumentNullException.ThrowIfNull(groupTagInsertionsFinder);

this.tagPairsFinder = tagPairsFinder;
this.pairTagsIntersectionHandler = pairTagsIntersectionHandler;
this.groupTagInsertionsFinder = groupTagInsertionsFinder;

tags ??= [];

foreach (var tag in tags)
{
ArgumentNullException.ThrowIfNull(tag);

if (!IsTagWithSupportedType(tag))
{
throw new ArgumentException($"Tag '{tag.GetType()}' is unsupported.");
}
}

mdTags = tags;
}

public string Render(string mdString)
{
ArgumentNullException.ThrowIfNull(mdString);

var tagsIndices = GetTagsIndices(mdString, mdTags)
.GroupBy(kv => kv.Key.TagType)
.ToDictionary(group => group.Key, group => group.ToDictionary());
var htmlReplacements = new Dictionary<int, SubstringReplacement>();

if (tagsIndices.TryGetValue(TagType.Normal, out var normalTagsIndices))
{
AddReplacementsRange(htmlReplacements, GetNormalTagsReplacements(mdString, normalTagsIndices));
}

if (tagsIndices.TryGetValue(TagType.Pair, out var pairTagsIndices))
{
AddReplacementsRange(htmlReplacements, GetPairTagsReplacements(mdString, pairTagsIndices));
}

if (tagsIndices.TryGetValue(TagType.FullLine, out var fullLineTagsIndices))
{
AddReplacementsRange(htmlReplacements, GetFullLineTagsReplacements(mdString, fullLineTagsIndices));
}

return GetHtmlString(mdString, htmlReplacements);
}

private bool IsTagWithSupportedType(IMdTag tag)
{
return tag.TagType switch
{
TagType.Normal => tag is IHtmlConverter and not IGroupedMdTag,
TagType.Pair => tag is IHtmlTagsPair,
TagType.FullLine => tag is IHtmlTagsPair,
TagType.Ignore => true,
_ => false,
};
}

private Dictionary<IMdTag, List<SubstringReplacement>> GetTagsIndices(string mdString, IEnumerable<IMdTag> tags)
{
var tagsIndices = tags.ToDictionary(tag => tag, _ => new List<SubstringReplacement>());

for (var i = 0; i < mdString.Length; i++)
{
foreach (var tag in tags)
{
if (tag.IsMdTag(mdString, i, out var tagLength))
{
if (tag.TagType != TagType.Ignore)
{
tagsIndices[tag].Add(new SubstringReplacement(i, tagLength));
}

i += tagLength - 1;
break;
}
}
}

return tagsIndices;
}

private void AddReplacementsRange(
Dictionary<int, SubstringReplacement> replacements,
IEnumerable<SubstringReplacement> replacementsToAdd)
{
foreach (var newReplacement in replacementsToAdd)
{
if (replacements.TryGetValue(newReplacement.Index, out var replacement))
{
var replacementsConcat = replacement.Replacement!.Contains('/')
? replacement.Replacement + newReplacement.Replacement
: newReplacement.Replacement + replacement.Replacement;
replacement = replacement with { Replacement = replacementsConcat };
}
else
{
replacement = newReplacement;
}

replacements[newReplacement.Index] = replacement;
}
}

private IEnumerable<SubstringReplacement> GetNormalTagsReplacements(
string mdString,
Dictionary<IMdTag, List<SubstringReplacement>> normalTagsPositions)
{
return normalTagsPositions
.SelectMany(kv => kv.Value
.Select(x => x with { Replacement = ((IHtmlConverter)kv.Key).GetHtmlString(mdString, x.Index) }));
}

private IEnumerable<SubstringReplacement> GetPairTagsReplacements(
string mdString,
Dictionary<IMdTag, List<SubstringReplacement>> pairTagsPositions)
{
var pairedTags = pairTagsPositions
.ToDictionary(kv => kv.Key, kv => tagPairsFinder.PairTags(mdString, kv.Value).ToList());
var tagPairs = pairTagsIntersectionHandler
.RemoveIntersections(pairedTags)
.ToDictionary(kv => (IHtmlTagsPair)kv.Key, kv => kv.Value.AsEnumerable());

return GetPairedTagsReplacements(mdString, tagPairs);
}

private IEnumerable<SubstringReplacement> GetFullLineTagsReplacements(
string mdString,
Dictionary<IMdTag, List<SubstringReplacement>> fullLineTagsPositions)
{
var pairedTags = fullLineTagsPositions
.ToDictionary(kv => (IHtmlTagsPair)kv.Key, kv => tagPairsFinder.PairFullLineTags(mdString, kv.Value));

return GetPairedTagsReplacements(mdString, pairedTags);
}

private IEnumerable<SubstringReplacement> GetPairedTagsReplacements(
string mdString,
Dictionary<IHtmlTagsPair, IEnumerable<TagReplacementsPair>> pairedTags)
{
var htmlReplacements = pairedTags
.SelectMany(kv => kv.Value
.SelectMany(pair => pair.GetHtmlReplacements(kv.Key).Flatten()));
var groupTagsHtmlInsertions = pairedTags
.Where(kv => kv.Key is IGroupedMdTag)
.SelectMany(kv => groupTagInsertionsFinder
.GetGroupTagReplacements(mdString, ((IGroupedMdTag)kv.Key).GroupHtmlTagsPair, kv.Value));

return htmlReplacements
.Concat(groupTagsHtmlInsertions);
}

private string GetHtmlString(string mdString, Dictionary<int, SubstringReplacement> htmlReplacements)
{
var sb = new StringBuilder();

for (var i = 0; i < mdString.Length; i++)
{
var hasReplacement = htmlReplacements.TryGetValue(i, out var replacement);

if (hasReplacement)
{
sb.Append(replacement.Replacement);
i += Math.Max(0, replacement.Length - 1);
}

if (!hasReplacement || hasReplacement && replacement.Length == 0)
{
sb.Append(mdString[i]);
}
}

if (htmlReplacements.TryGetValue(mdString.Length, out var lastReplacement))
{
sb.Append(lastReplacement.Replacement);
}

return sb.ToString();
}
}
39 changes: 39 additions & 0 deletions cs/Markdown/MdTagHandlers/GroupTagInsertionsFinder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using Markdown.MdTags.Interfaces;

namespace Markdown.MdTagHandlers;

internal class GroupTagInsertionsFinder : IGroupTagInsertionsFinder
{
public IEnumerable<SubstringReplacement> GetGroupTagReplacements(
string mdString,
IHtmlTagsPair groupTag,
IEnumerable<TagReplacementsPair> tagPairs)
{
using var enumerator = tagPairs.GetEnumerator();

if (!enumerator.MoveNext())
{
yield break;
}

var previous = enumerator.Current;
yield return new SubstringReplacement(previous.OpenTag.Index, 0, groupTag.HtmlOpenTag);

while (enumerator.MoveNext())
{
var previousEndIndex = previous.CloseTag.EndIndex;
var currentStartIndex = enumerator.Current.OpenTag.Index;
var lengthBetweenTags = currentStartIndex - previousEndIndex;

if (!TagSearchHelper.IsWhiteSpaceSubstring(mdString, previousEndIndex, lengthBetweenTags))
{
yield return new SubstringReplacement(previousEndIndex, 0, groupTag.HtmlCloseTag);
yield return new SubstringReplacement(currentStartIndex - 1, 0, groupTag.HtmlOpenTag);
}

previous = enumerator.Current;
}

yield return new SubstringReplacement(previous.CloseTag.EndIndex, 0, groupTag.HtmlCloseTag);
}
}
9 changes: 9 additions & 0 deletions cs/Markdown/MdTagHandlers/IGroupTagInsertionsFinder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Markdown.MdTags.Interfaces;

namespace Markdown.MdTagHandlers;

internal interface IGroupTagInsertionsFinder
{
public IEnumerable<SubstringReplacement> GetGroupTagReplacements(
string mdString, IHtmlTagsPair groupTag, IEnumerable<TagReplacementsPair> tagPairs);
}
9 changes: 9 additions & 0 deletions cs/Markdown/MdTagHandlers/IPairTagsIntersectionHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Markdown.MdTags.Interfaces;

namespace Markdown.MdTagHandlers;

internal interface IPairTagsIntersectionHandler
{
public Dictionary<IMdTag, List<TagReplacementsPair>> RemoveIntersections(
Dictionary<IMdTag, List<TagReplacementsPair>> tagsIndices);
}
8 changes: 8 additions & 0 deletions cs/Markdown/MdTagHandlers/ITagPairsFinder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Markdown.MdTagHandlers;

internal interface ITagPairsFinder
{
public IEnumerable<TagReplacementsPair> PairTags(string str, IEnumerable<SubstringReplacement> tagPositions);

public IEnumerable<TagReplacementsPair> PairFullLineTags(string str, IEnumerable<SubstringReplacement> tagPositions);
}
Loading