Хотя использование «только» регулярных выражений возможно при использовании балансирующих групп, это довольно тяжелое вуду и по своей сути «хрупкое».Я предлагаю использовать регулярные выражения для поиска тегов открытия / закрытия (без попытки связать закрытие с открытием), пометить и собрать их в коллекцию (вероятно, в стек), а затем проанализировать их «вручную» (с помощью foreach).Таким образом, вы получаете лучшее из обоих: поиск тегов по регулярным выражениям и обработка их (и неправильно написанных) вручную.
class TagMatch
{
public string Tag { get; set; }
public Capture Capture { get; set; }
public readonly List<string> Substrings = new List<string>();
}
static void Main(string[] args)
{
var rx = new Regex(@"(?<OPEN>\[[A-Za-z]+?\])|(?<CLOSE>\[/[A-Za-z]+?\])|(?<TEXT>[^\[]+|\[)");
var str = "Lorem [AA]ipsum [BB]dolor sit [/BB]amet, [ consectetur ][/AA]adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
var matches = rx.Matches(str);
var recurse = new Stack<TagMatch>();
recurse.Push(new TagMatch { Tag = String.Empty });
foreach (Match match in matches)
{
var text = match.Groups["TEXT"];
TagMatch last;
if (text.Success)
{
last = recurse.Peek();
last.Substrings.Add(text.Value);
continue;
}
var open = match.Groups["OPEN"];
string tag;
if (open.Success)
{
tag = open.Value.Substring(1, open.Value.Length - 2);
recurse.Push(new TagMatch { Tag = tag, Capture = open.Captures[0] });
continue;
}
var close = match.Groups["CLOSE"];
tag = close.Value.Substring(2, close.Value.Length - 3);
last = recurse.Peek();
if (last.Tag == tag)
{
recurse.Pop();
var lastLast = recurse.Peek();
lastLast.Substrings.Add("**" + last.Tag + "**");
lastLast.Substrings.AddRange(last.Substrings);
lastLast.Substrings.Add("**/" + last.Tag + "**");
}
else
{
throw new Exception();
}
}
if (recurse.Count != 1)
{
throw new Exception();
}
var sb = new StringBuilder();
foreach (var str2 in recurse.Pop().Substrings)
{
sb.Append(str2);
}
var str3 = sb.ToString();
}
Это пример.Он чувствителен к регистру (но эту проблему легко решить).Он не обрабатывает «непарные» теги, потому что есть разные способы их обработки.Там, где вы найдете «бросить новое исключение», вам придется добавить свою обработку.Понятно, что это не «выпадающее» решение.Это всего лишь пример.По этой логике я не буду отвечать на вопросы типа «компилятор говорит мне, что мне нужно пространство имен» или «компилятор не может найти Regex».НО я буду более чем рад ответить на «сложные» вопросы, например, как можно сопоставить непарные теги или как добавить поддержку [AAA=bbb]
тегов
(2nd BIG EDIT)
Бахахахах!Я ДЕЙСТВИТЕЛЬНО знал, что группировка была способом сделать это!
// Some classes
class BaseTagMatch {
public Capture Capture;
public override string ToString()
{
return String.Format("{1}: {2} [{0}]", GetType(), Capture.Index, Capture.Value.ToString());
}
}
class BeginTag : BaseTagMatch
{
public int Index;
public Capture Options;
public EndTag EndTag;
}
class EndTag : BaseTagMatch {
public int Index;
public BeginTag BeginTag;
}
class Text : BaseTagMatch
{
}
class UnmatchedTag : BaseTagMatch
{
}
// The code
var pattern =
@"(?# line 01) ^" +
@"(?# line 02) (" +
// Non [ Text
@"(?# line 03) (?>(?<TEXT>[^\[]+))" +
@"(?# line 04) |" +
// Immediately closed tag [a/]
@"(?# line 05) (?>\[ (?<TAG> [A-Z]+ ) \x20* =? \x20* (?<TAG_OPTION>( (?<= = \x20*) ( (?! \x20* /\]) [^\[\]\r\n] )* )? ) (?<BEGIN_INNER_TEXT>)(?<END_INNER_TEXT>) \x20* /\] )" +
@"(?# line 06) |" +
// Matched open tag [a]
@"(?# line 07) \[ (?<TAG> (?<OPEN> [A-Z]+ ) ) \x20* =? \x20* (?<TAG_OPTION>( (?<= = \x20*) ( (?! \x20* \]) [^\[\]\r\n] )* )? ) \x20* \] (?<BEGIN_INNER_TEXT>)" +
@"(?# line 08) |" +
// Matched close tag [/a]
@"(?# line 09) (?>(?<END_INNER_TEXT>) \[/ \k<OPEN> \x20* \] (?<-OPEN>))" +
@"(?# line 10) |" +
// Unmatched open tag [a]
@"(?# line 11) (?>(?<UNMATCHED_TAG> \[ [A-Z]+ \x20* =? \x20* ( (?<= = \x20*) ( (?! \x20* \]) [^\[\]\r\n] )* )? \x20* \] ) )" +
@"(?# line 12) |" +
// Unmatched close tag [/a]
@"(?# line 13) (?>(?<UNMATCHED_TAG> \[/ [A-Z]+ \x20* \] ) )" +
@"(?# line 14) |" +
// Single [ of Text (unmatched by other patterns)
@"(?# line 15) (?>(?<TEXT>\[))" +
@"(?# line 16) )*" +
@"(?# line 17) (?(OPEN)(?!))" +
@"(?# line 18) $";
var rx = new Regex(pattern, RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase);
var match = rx.Match("[div=c:max max]asdf[p = 1 ] a [p=2] [b = p/pp /] [q/] \n[a]sd [/z] [ [/p]f[/p]asdffds[/DIV] [p][/p]");
////var tags = match.Groups["TAG"].Captures.OfType<Capture>().ToArray();
////var tagoptions = match.Groups["TAG_OPTION"].Captures.OfType<Capture>().ToArray();
////var begininnertext = match.Groups["BEGIN_INNER_TEXT"].Captures.OfType<Capture>().ToArray();
////var endinnertext = match.Groups["END_INNER_TEXT"].Captures.OfType<Capture>().ToArray();
////var text = match.Groups["TEXT"].Captures.OfType<Capture>().ToArray();
////var unmatchedtag = match.Groups["UNMATCHED_TAG"].Captures.OfType<Capture>().ToArray();
var tags = match.Groups["TAG"].Captures.OfType<Capture>().Select((p, ix) => new BeginTag { Capture = p, Index = ix, Options = match.Groups["TAG_OPTION"].Captures[ix] }).ToList();
Func<Capture, int, EndTag> func = (p, ix) =>
{
var temp = new EndTag { Capture = p, Index = ix, BeginTag = tags[ix] };
tags[ix].EndTag = temp;
return temp;
};
var endTags = match.Groups["END_INNER_TEXT"].Captures.OfType<Capture>().Select((p, ix) => func(p, ix));
var text = match.Groups["TEXT"].Captures.OfType<Capture>().Select((p, ix) => new Text { Capture = p });
var unmatchedTags = match.Groups["UNMATCHED_TAG"].Captures.OfType<Capture>().Select((p, ix) => new UnmatchedTag { Capture = p });
// Here you have all the tags and the inner text neatly ordered and ready to be recomposed in a StringBuilder.
var allTags = tags.Cast<BaseTagMatch>().Union(endTags).Union(text).Union(unmatchedTags).ToList();
allTags.Sort((p, q) => p.Capture.Index - q.Capture.Index);
foreach (var el in allTags)
{
var type = el.GetType();
if (type == typeof(BeginTag))
{
}
else if (type == typeof(EndTag))
{
}
else if (type == typeof(UnmatchedTag))
{
}
else
{
// Text
}
}
Соответствие тегов без учета регистра, игнорирует теги, закрытые неправильно, поддерживает немедленно закрытые теги ([BR/]
). И кто-то сказалс Regex это было невозможно ... Bwahahahahah!
TAG
, TAGOPTION
, BEGIN_INNER_TEXT
и END_INNER_TEXT
совпадают (они всегда имеют одинаковое количество элементов).TEXT
и UNMATCHED_TAG
не совпадают!TAG
и TAG_OPTION
являются авто-объяснительными (оба лишены бесполезных пробелов).Захваты BEGIN_INNER_TEXT
и END_INNER_TEXT
всегда пусты, но вы можете использовать их свойство Index
, чтобы увидеть, где начинаются / заканчиваются теги.UNMATCHED_TAG
содержит теги, которые были открыты, но не закрыты, или закрыты, но не опровергнуты.Он не содержит тегов, которые имеют неправильный формат (например, [123
]).
В конце я беру TAG
, END_INNER_TEXT
(чтобы увидеть, где заканчиваются теги), TEXT
и UNMATCHED_TAG
и отсортировать их по индексу.Затем вы можете взять allTags
, поместить его в foreach
и для каждого элемента проверить его тип.Легко :-): -)
В качестве небольшой заметки, Regex - RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase
.Первые два - чтобы было легче писать и читать, третий - семантический.[A]
совпадает с [/a]
.
Необходимые показания:
http://www.codeproject.com/KB/recipes/Nested_RegEx_explained.aspx http://www.codeproject.com/KB/recipes/RegEx_Balanced_Grouping.aspx