diff --git a/Cyjb.Markdown/Cyjb.Markdown.csproj b/Cyjb.Markdown/Cyjb.Markdown.csproj
index dd5c301..2e09584 100644
--- a/Cyjb.Markdown/Cyjb.Markdown.csproj
+++ b/Cyjb.Markdown/Cyjb.Markdown.csproj
@@ -32,15 +32,15 @@
-
+
True
-
+
True
all
compile; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/Cyjb.Markdown/ParseBlock/BlockLine.cs b/Cyjb.Markdown/ParseBlock/BlockLine.cs
new file mode 100644
index 0000000..2d31ba7
--- /dev/null
+++ b/Cyjb.Markdown/ParseBlock/BlockLine.cs
@@ -0,0 +1,361 @@
+using System.Text;
+using Cyjb.Collections;
+using Cyjb.Markdown.Utils;
+using Cyjb.Text;
+
+namespace Cyjb.Markdown.ParseBlock;
+
+///
+/// 表示块的行。
+///
+internal sealed class BlockLine
+{
+ ///
+ /// 代码缩进长度。
+ ///
+ public const int CodeIndent = 4;
+
+ ///
+ /// 行的文本。
+ ///
+ private readonly BlockText text = new();
+ ///
+ /// 行定位器。
+ ///
+ private readonly LineLocator locator;
+ ///
+ /// 是否已计算缩进信息。
+ ///
+ private bool hasIndent;
+ ///
+ /// 缩进的起始位置。
+ ///
+ private int indentStart = -1;
+ ///
+ /// 缩进结束位置。
+ ///
+ private int indentEnd;
+ ///
+ /// 缩进缩进文本。
+ ///
+ private StringView indentText;
+ ///
+ /// 缩进原始起始位置。
+ ///
+ private int indentOriginalStart;
+ ///
+ /// 缩进起始列号。
+ ///
+ private int indentStartColumn;
+ ///
+ /// 缩进结束列号。
+ ///
+ private int indentEndColumn;
+
+ ///
+ /// 使用指定的行定位器初始化 类的新实例。
+ ///
+ /// 行定位器。
+ internal BlockLine(LineLocator locator)
+ {
+ this.locator = locator;
+ }
+
+ ///
+ /// 返回指定索引的行列位置。
+ ///
+ /// 要检查行列位置的索引。
+ /// 指定索引的行列位置。
+ public LinePosition GetPosition(int index)
+ {
+ return locator.GetPosition(index);
+ }
+
+ ///
+ /// 获取当前缩进宽度。
+ ///
+ public int Indent
+ {
+ get
+ {
+ GetIndent();
+ return indentEndColumn - indentStartColumn;
+ }
+ }
+ ///
+ /// 是否是段落可以跳过的缩进。
+ ///
+ public bool ParagraphSkippable { get; set; }
+ ///
+ /// 获取是否是代码缩进。
+ ///
+ public bool IsCodeIndent => Indent >= CodeIndent;
+ ///
+ /// 获取行的起始位置。
+ ///
+ public int Start => hasIndent ? indentStart : text.Start;
+ ///
+ /// 获取行的结束位置。
+ ///
+ public int End => text.End;
+ ///
+ /// 获取行的范围。
+ ///
+ public TextSpan Span
+ {
+ get
+ {
+ if (hasIndent)
+ {
+ return new TextSpan(indentStart, text.End);
+ }
+ else
+ {
+ return text.Span;
+ }
+ }
+ }
+
+ public BlockText BlockText => text;
+
+ ///
+ /// 获取行是否是空的。
+ ///
+ internal bool IsEmpty => text.Tokens.Count == 0;
+
+ ///
+ /// 获取词法单元列表。
+ ///
+ internal IReadOnlyList> Tokens => text.Tokens;
+
+ ///
+ /// 返回当前行是否是空的或只包含空白。
+ ///
+ /// 要检查的起始词法单元索引。
+ public bool IsBlank(int start = 0)
+ {
+ int count = text.Tokens.Count;
+ for (int i = start; i < count; i++)
+ {
+ BlockKind kind = text.Tokens[i].Kind;
+ if (kind != BlockKind.Indent && kind != BlockKind.NewLine)
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ ///
+ /// 跳过指定个数的空白。
+ ///
+ /// 要跳过的空白个数。
+ public void SkipIndent(int count)
+ {
+ GetIndent();
+ indentStartColumn += count;
+ if (indentStartColumn >= indentEndColumn)
+ {
+ indentStartColumn = indentEndColumn;
+ indentStart = indentEnd;
+ return;
+ }
+ int column;
+ // 由于 Tab 可能对应多列,因此需要找到首个 index 使得 column(index)≤startColumn。
+ for (; indentStart < indentEnd; indentStart++)
+ {
+ column = locator.GetPosition(indentStart).Column;
+ if (column == indentStartColumn)
+ {
+ return;
+ }
+ else if (column > indentStartColumn)
+ {
+ indentStart--;
+ return;
+ }
+ }
+ // 避免 end 的列位置超出 startColumn
+ column = locator.GetPosition(indentStart).Column;
+ if (column > indentStartColumn)
+ {
+ indentStart--;
+ }
+ }
+
+ ///
+ /// 跳过起始空白。
+ ///
+ public void SkipIndent()
+ {
+ GetIndent();
+ indentStartColumn = indentEndColumn;
+ indentStart = indentEnd;
+ }
+
+ ///
+ /// 跳过当前行。
+ ///
+ public void Skip()
+ {
+ text.Clear();
+ hasIndent = true;
+ indentStart = indentOriginalStart = indentEnd = 0;
+ indentText = StringView.Empty;
+ indentStartColumn = indentEndColumn = 0;
+ }
+
+ ///
+ /// 将当前行添加到指定块文本。
+ ///
+ /// 要添加到的块文本。
+ public void AppendTo(BlockText text)
+ {
+ // 添加缩进。
+ if (hasIndent && Indent > 0)
+ {
+ text.Add(new Token(BlockKind.Indent, GetIndentText(), new TextSpan(indentStart, indentEnd)));
+ }
+ // 添加剩余词法单元。
+ this.text.AppendTo(text);
+ }
+
+ ///
+ /// 将当前行添加到指定字符串。
+ ///
+ /// 字符串构造器。
+ public void AppendTo(StringBuilder builder)
+ {
+ // 添加缩进。
+ if (hasIndent && Indent > 0)
+ {
+ builder.Append(GetIndentText().AsSpan());
+ }
+ // 添加剩余词法单元。
+ text.AppendTo(builder);
+ }
+
+ ///
+ /// 清除行的数据。
+ ///
+ public void Clear()
+ {
+ text.Clear();
+ hasIndent = false;
+ ParagraphSkippable = true;
+ }
+
+ ///
+ /// 添加新的词法单元。
+ ///
+ /// 要添加的词法单元。
+ internal void Add(Token token)
+ {
+ text.Add(token);
+ }
+
+ ///
+ /// 获取缩进信息。
+ ///
+ private void GetIndent()
+ {
+ if (hasIndent)
+ {
+ return;
+ }
+ hasIndent = true;
+ if (text.Tokens.Count == 0)
+ {
+ indentStart = indentOriginalStart = indentEnd = 0;
+ indentText = StringView.Empty;
+ indentStartColumn = indentEndColumn = 0;
+ return;
+ }
+ Token token = text.PeekFront();
+ if (token.Kind == BlockKind.Indent)
+ {
+ // 是缩进,提取相关信息。
+ text.PopFront();
+ indentStart = token.Span.Start;
+ indentEnd = token.Span.End;
+ indentOriginalStart = indentStart;
+ indentText = token.Text;
+ indentStartColumn = locator.GetPosition(indentStart).Column;
+ indentEndColumn = locator.GetPosition(indentEnd).Column;
+ }
+ else
+ {
+ // 其它,使用空缩进。
+ indentStart = indentOriginalStart = indentEnd = token.Span.Start;
+ indentText = StringView.Empty;
+ indentStartColumn = indentEndColumn = 0;
+ }
+ }
+
+ ///
+ /// 返回开始处的下一词法单元,但不将其消费。
+ ///
+ /// 开始处的下一词法单元。
+ internal Token PeekFront()
+ {
+ return text.PeekFront();
+ }
+
+ ///
+ /// 读取并返回开始处的下一词法单元。
+ ///
+ /// 开始处的下一词法单元。
+ internal Token PopFront()
+ {
+ Token token = text.PopFront();
+ // 读取词法单元后,需要重新检查缩进、文本和位置信息。
+ hasIndent = false;
+ return token;
+ }
+
+
+ ///
+ /// 返回当前对象的字符串视图表示形式。
+ ///
+ /// 当前对象的字符串视图表示形式。
+ public StringView ToStringView()
+ {
+ if (!hasIndent || Indent == 0)
+ {
+ return text.ToStringView();
+ }
+ StringBuilder sb = StringBuilderPool.Rent(text.Length);
+ sb.Append(GetIndentText().AsSpan());
+ text.AppendTo(sb);
+ string result = sb.ToString();
+ StringBuilderPool.Return(sb);
+ return result;
+ }
+
+ ///
+ /// 返回剩余的缩进文本。
+ ///
+ /// 缩进文本。
+ private StringView GetIndentText()
+ {
+ int column = locator.GetPosition(indentStart).Column;
+ if (column == indentStartColumn)
+ {
+ return indentText[(indentStart - indentOriginalStart)..];
+ }
+ else
+ {
+ // 当前是部分 Tab,需要使用空格补齐 column(start) 到 startColumn 的位置。
+ column = locator.GetPosition(indentStart + 1).Column;
+ using ValueList result = new(stackalloc char[ValueList.StackallocCharSizeLimit]);
+ result.Add(' ', column - indentStartColumn);
+ int idx = indentStart + 1 - indentOriginalStart;
+ // 存在 Tab 时,可能会出现列数超出字符数的场景。
+ if (idx < indentText.Length)
+ {
+ result.Add(indentText.AsSpan(idx));
+ }
+ return result.ToString();
+ }
+ }
+}
diff --git a/Cyjb.Markdown/ParseBlock/BlockParser.cs b/Cyjb.Markdown/ParseBlock/BlockParser.cs
index 7c40f67..ef609b5 100644
--- a/Cyjb.Markdown/ParseBlock/BlockParser.cs
+++ b/Cyjb.Markdown/ParseBlock/BlockParser.cs
@@ -130,7 +130,7 @@ public BlockParser(TextReader text, ParseOptions? options)
public Document Parse()
{
Token token;
- BlockText line = new(locator);
+ BlockLine line = new(locator);
while (true)
{
// 清除行的旧数据。
@@ -138,7 +138,7 @@ public Document Parse()
// 添加所有换行符前的 Token。
while (!(token = tokenizer.Read()).IsEndOfFile)
{
- line.AddToken(token);
+ line.Add(token);
if (token.Kind == BlockKind.NewLine)
{
ParseLine(line);
@@ -204,7 +204,7 @@ public Document Parse()
/// 解析指定行。
///
/// 要解析的行。
- private void ParseLine(BlockText line)
+ private void ParseLine(BlockLine line)
{
int lineStart = line.Start;
// 栈底总是 document,总是可以接受任何行,因此总是跳过。
@@ -243,7 +243,7 @@ private void ParseLine(BlockText line)
break;
}
processor.IsNeedReplaced = false;
- token = line.Peek();
+ token = line.PeekFront();
currentInlineProcessors.Clear();
if (token.Kind == BlockKind.NewLine)
{
@@ -306,7 +306,7 @@ private void ParseLine(BlockText line)
if (!line.IsBlank())
{
// 为行添加一个新的段落。
- processor = new ParagraphProcessor(options);
+ processor = new ParagraphProcessor(this);
AddProcessor(processor, lineStart);
// 需要跳过段落的起始缩进。
if (line.ParagraphSkippable)
diff --git a/Cyjb.Markdown/ParseBlock/BlockText.cs b/Cyjb.Markdown/ParseBlock/BlockText.cs
index a38d93f..5502cec 100644
--- a/Cyjb.Markdown/ParseBlock/BlockText.cs
+++ b/Cyjb.Markdown/ParseBlock/BlockText.cs
@@ -1,275 +1,282 @@
using System.Text;
using Cyjb.Collections;
+using Cyjb.Markdown.Utils;
using Cyjb.Text;
namespace Cyjb.Markdown.ParseBlock;
///
-/// 块的文本。
+/// 表示块的文本。
///
internal sealed class BlockText
{
- ///
- /// 代码缩进长度。
- ///
- public const int CodeIndent = 4;
-
///
/// 词法单元队列。
///
- private readonly ListQueue> tokens = new();
- ///
- /// 行定位器。
- ///
- private readonly LineLocator locator;
- ///
- /// 行的起始位置。
- ///
- private int start = -1;
- ///
- /// 行的结束位置。
- ///
- private int end;
+ private readonly Deque> tokens = new();
///
- /// 映射的文本。
+ /// 文本的长度。
///
- private MappedText? text;
- ///
- /// 是否已计算缩进信息。
- ///
- private bool hasIndent;
- ///
- /// 缩进结束位置。
- ///
- private int indentEnd;
- ///
- /// 缩进缩进文本。
- ///
- private StringView indentText;
- ///
- /// 缩进原始起始位置。
- ///
- private int indentOriginalStart;
- ///
- /// 缩进起始列号。
- ///
- private int indentStartColumn;
- ///
- /// 缩进结束列号。
- ///
- private int indentEndColumn;
+ private int length;
///
- /// 使用指定的行定位器初始化 类的新实例。
+ /// 获取文本的起始位置。
///
- /// 行定位器。
- internal BlockText(LineLocator locator)
+ public int Start
{
- this.locator = locator;
+ get
+ {
+ if (tokens.TryPeekFront(out var token))
+ {
+ return token.Span.Start;
+ }
+ else
+ {
+ return 0;
+ }
+ }
}
-
///
- /// 返回指定索引的行列位置。
+ /// 获取文本的结束位置。
///
- /// 要检查行列位置的索引。
- /// 指定索引的行列位置。
- public LinePosition GetPosition(int index)
+ public int End
{
- return locator.GetPosition(index);
+ get
+ {
+ if (tokens.TryPeekBack(out var token))
+ {
+ return token.Span.End;
+ }
+ else
+ {
+ return 0;
+ }
+ }
}
-
///
- /// 获取当前缩进宽度。
+ /// 获取文本的范围。
///
- public int Indent
+ public TextSpan Span
{
get
{
- GetIndent();
- return indentEndColumn - indentStartColumn;
+ int count = tokens.Count;
+ if (count == 0)
+ {
+ return new TextSpan();
+ }
+ else if (count == 1)
+ {
+ return tokens.PeekFront().Span;
+ }
+ else
+ {
+ return new TextSpan(tokens.PeekFront().Span.Start, tokens.PeekBack().Span.End);
+ }
}
}
+
///
- /// 是否是段落可以跳过的缩进。
- ///
- public bool ParagraphSkippable { get; set; }
- ///
- /// 获取是否是代码缩进。
+ /// 获取文本的长度。
///
- public bool IsCodeIndent => Indent >= CodeIndent;
+ public int Length => length;
///
- /// 获取行的起始位置。
+ /// 获取词法单元列表。
///
- public int Start => hasIndent ? start : start;
+ public IReadOnlyList> Tokens => tokens;
+
///
- /// 获取行的结束位置。
+ /// 检查是否是单行文本。
///
- public int End => end;
+ /// 如果是单行文本,会返回 true;否则返回 false。
+ public bool IsSingleLine()
+ {
+ int count = tokens.Count;
+ for (int i = 0; i < count; i++)
+ {
+ if (tokens[i].Kind == BlockKind.NewLine)
+ {
+ return i == count - 1;
+ }
+ ReadOnlySpan span = tokens[i].Text;
+ int idx = span.IndexOf('\n');
+ if (idx >= 0 && idx < span.Length - 1)
+ {
+ return false;
+ }
+ }
+ return true;
+ }
///
- /// 获取当前行的文本。
+ /// 返回指定索引的文本。
///
- public MappedText Text
+ /// 要检查的索引。
+ /// 指定索引的文本。
+ public char this[int index]
{
get
{
- if (text == null)
+ int count = tokens.Count;
+ for (int i = 0; i < count; i++)
{
- List texts = new();
- int length = 0;
- int lastLength = 0;
- int lastMappedIndex = 0;
- TextSpanBuilder spanBuilder = new();
- List> maps = new();
- // 添加缩进。
- if (hasIndent && Indent > 0)
+ StringView text = tokens[i].Text;
+ int textLen = text.Length;
+ if (index >= textLen)
{
- lastMappedIndex = start;
- maps.Add(new Tuple(0, lastMappedIndex));
- spanBuilder.Add(lastMappedIndex);
- StringView text = GetIndentText();
- lastLength = text.Length;
- length += lastLength;
- texts.Add(text);
+ index -= textLen;
}
- // 添加剩余词法单元。
- bool isFirst = true;
- int count = tokens.Count;
- for (int i = 0; i < count; i++)
+ else
{
- var token = tokens[i];
- // 首个缩进可能会存在 Tab 部分替换为空格的情况,因此之后的词法单元也需要添加索引。
- if (isFirst)
- {
- int offset = token.Span.Start - lastMappedIndex;
- if (lastLength != offset)
- {
- maps.Add(new Tuple(lastLength, offset));
- }
- isFirst = false;
- }
- lastMappedIndex = token.Span.Start;
- lastLength = token.Text.Length;
- length += lastLength;
- if (texts.Count > 0 && texts[^1].TryConcat(token.Text, out var concated))
- {
- texts[^1] = concated;
- }
- else
- {
- texts.Add(token.Text);
- }
- spanBuilder.Add(token.Span);
+ return text[index];
}
- text = new MappedText(texts, length, spanBuilder.GetSpan(), maps);
}
- return text;
+ return SourceReader.InvalidCharacter;
}
}
///
- /// 获取行是否是空的。
+ /// 移除起始位置的多个字符。
///
- internal bool IsEmpty => tokens.Count == 0;
-
- ///
- /// 获取词法单元列表。
- ///
- internal IReadOnlyList> Tokens => tokens;
-
- ///
- /// 返回当前行是否是空的或只包含空白。
- ///
- /// 要检查的起始词法单元索引。
- public bool IsBlank(int start = 0)
+ /// 要移除的字符个数。
+ public void RemoteStart(int length)
{
- int count = tokens.Count;
- for (int i = start; i < count; i++)
+ this.length -= length;
+ while (tokens.TryPeekFront(out var token))
{
- BlockKind kind = tokens[i].Kind;
- if (kind != BlockKind.Indent && kind != BlockKind.NewLine)
+ StringView text = token.Text;
+ int textLen = text.Length;
+ if (length >= textLen)
{
- return false;
+ length -= textLen;
+ tokens.PopFront();
+ }
+ else
+ {
+ token.Text = text.Substring(length);
+ token.Span = token.Span with
+ {
+ Start = token.Span.Start + length,
+ };
+ break;
}
}
- return true;
}
///
- /// 跳过指定个数的空白。
+ /// 移除结束位置的多个字符。
///
- /// 要跳过的空白个数。
- public void SkipIndent(int count)
+ /// 要移除的字符个数。
+ public void RemoteEnd(int length)
{
- GetIndent();
- indentStartColumn += count;
- if (indentStartColumn >= indentEndColumn)
+ this.length -= length;
+ while (tokens.TryPeekBack(out var token))
{
- indentStartColumn = indentEndColumn;
- start = indentEnd;
- text = null;
- return;
- }
- int column;
- // 由于 Tab 可能对应多列,因此需要找到首个 index 使得 column(index)≤startColumn。
- for (; start < indentEnd; start++)
- {
- column = locator.GetPosition(start).Column;
- if (column == indentStartColumn)
+ StringView text = token.Text;
+ length -= text.Length;
+ if (length >= 0)
{
- return;
+ tokens.PopBack();
}
- else if (column > indentStartColumn)
+ else
{
- start--;
- return;
+ token.Text = text[..-length];
+ token.Span = token.Span with
+ {
+ End = token.Span.Start - length,
+ };
+ break;
}
}
- // 避免 end 的列位置超出 startColumn
- column = locator.GetPosition(start).Column;
- if (column > indentStartColumn)
- {
- start--;
- }
- text = null;
}
///
- /// 跳过全部空白。
+ /// 移除起始空白。
///
- public void SkipIndent()
+ /// 如果移除了任何起始空白,则返回 true;否则返回 false。
+ public bool TrimStart()
{
- GetIndent();
- indentStartColumn = indentEndColumn;
- start = indentEnd;
- text = null;
+ int diff = 0;
+ while (tokens.TryPeekFront(out var token))
+ {
+ StringView text = token.Text;
+ int textLen = text.Length;
+ text = text.TrimStart(MarkdownUtil.WhitespaceChars);
+ textLen -= text.Length;
+ if (textLen == 0)
+ {
+ break;
+ }
+ diff += textLen;
+ if (text.IsEmpty)
+ {
+ tokens.PopFront();
+ }
+ else
+ {
+ token.Text = text;
+ token.Span = token.Span with
+ {
+ Start = token.Span.Start + textLen,
+ };
+ break;
+ }
+ }
+ if (diff == 0)
+ {
+ return false;
+ }
+ length -= diff;
+ return true;
}
///
- /// 跳过当前行。
+ /// 移除结尾空白。
///
- public void Skip()
+ /// 如果移除了任何结尾空白,则返回 true;否则返回 false。
+ public bool TrimEnd(bool trimNewLine = true)
{
- tokens.Clear();
- hasIndent = true;
- start = indentOriginalStart = indentEnd = end;
- indentText = StringView.Empty;
- indentStartColumn = indentEndColumn = 0;
- text = null;
+ int diff = 0;
+ char[] chars = trimNewLine ? MarkdownUtil.WhitespaceChars : MarkdownUtil.WhitespaceCharsWithoutNewLine;
+ while (tokens.TryPeekBack(out var token))
+ {
+ StringView text = token.Text;
+ int textLen = text.Length;
+ text = text.TrimEnd(chars);
+ textLen -= text.Length;
+ if (textLen == 0)
+ {
+ break;
+ }
+ diff += textLen;
+ if (text.IsEmpty)
+ {
+ tokens.PopBack();
+ }
+ else
+ {
+ token.Text = text;
+ token.Span = token.Span with
+ {
+ End = token.Span.Start + text.Length,
+ };
+ break;
+ }
+ }
+ if (diff == 0)
+ {
+ return false;
+ }
+ length -= diff;
+ return true;
}
///
- /// 将当前行添加到指定字符串。
+ /// 将当前文本添加到指定字符串。
///
/// 字符串构造器。
public void AppendTo(StringBuilder builder)
{
- // 添加缩进。
- if (hasIndent && Indent > 0)
- {
- builder.Append(GetIndentText().AsSpan());
- }
- // 添加剩余词法单元。
int count = tokens.Count;
for (int i = 0; i < count; i++)
{
@@ -277,119 +284,190 @@ public void AppendTo(StringBuilder builder)
}
}
- ///
- /// 清除行的数据。
- ///
- public void Clear()
- {
- tokens.Clear();
- hasIndent = false;
- text = null;
- start = -1;
- end = 0;
- ParagraphSkippable = true;
- }
-
///
/// 添加新的词法单元。
///
/// 要添加的词法单元。
- internal void AddToken(Token token)
+ /// 是否可以连接字符串视图。
+ public void Add(Token token, bool canConcat = false)
{
- if (start < 0)
+ if (length == 0)
{
- start = token.Span.Start;
+ canConcat = false;
}
- end = token.Span.End;
- tokens.Enqueue(token);
+ length += token.Text.Length;
+ if (canConcat)
+ {
+ var lastToken = tokens.PeekBack();
+ if (lastToken.Text.TryConcat(token.Text, out var concated))
+ {
+ lastToken.Text = concated;
+ lastToken.Span = lastToken.Span with
+ {
+ End = token.Span.End,
+ };
+ return;
+ }
+ }
+ tokens.PushBack(token);
}
///
- /// 获取缩进信息。
+ /// 添加新的词法单元的一部分。
///
- private void GetIndent()
+ /// 要添加的词法单元。
+ /// 要添加的起始索引。
+ /// 要添加的长度。
+ public void Add(Token token, int start, int length)
{
- if (hasIndent)
- {
- return;
- }
- hasIndent = true;
- if (tokens.Count == 0)
+ if (length <= 0)
{
- start = indentOriginalStart = indentEnd = end;
- indentText = StringView.Empty;
- indentStartColumn = indentEndColumn = 0;
return;
}
- Token token = tokens.Peek();
- if (token.Kind == BlockKind.Indent)
+ StringView text = token.Text;
+ if (length == text.Length)
{
- // 是缩进,提取相关信息。
- tokens.Dequeue();
- text = null;
- start = token.Span.Start;
- indentEnd = token.Span.End;
- indentOriginalStart = start;
- indentText = token.Text;
- indentStartColumn = locator.GetPosition(start).Column;
- indentEndColumn = locator.GetPosition(indentEnd).Column;
+ Add(token);
}
else
{
- // 其它,使用空缩进。
- start = indentOriginalStart = indentEnd = token.Span.Start;
- indentText = StringView.Empty;
- indentStartColumn = indentEndColumn = 0;
+ int spanStart = token.Span.Start + start;
+ int spanEnd = spanStart + length;
+ tokens.PushBack(new Token(token.Kind, text.Slice(start, length), new TextSpan(spanStart, spanEnd)));
+ this.length += length;
}
}
///
- /// 返回下一词法单元,但不将其消费。
+ /// 返回开始处的下一词法单元,但不将其消费。
+ ///
+ /// 开始处的下一词法单元。
+ public Token PeekFront()
+ {
+ return tokens.PeekFront();
+ }
+
+ ///
+ /// 读取并返回开始处的下一词法单元。
+ ///
+ /// 开始处的下一词法单元。
+ public Token PopFront()
+ {
+ Token token = tokens.PopFront();
+ length -= token.Text.Length;
+ return token;
+ }
+
+ ///
+ /// 返回末尾处的下一词法单元,但不将其消费。
///
- /// 下一词法单元。
- internal Token Peek()
+ /// 末尾处的下一词法单元。
+ public Token PeekBack()
{
- return tokens.Peek();
+ return tokens.PeekBack();
}
///
- /// 读取并返回下一词法单元。
+ /// 读取并返回末尾处的下一词法单元。
///
- /// 下一词法单元。
- internal Token Read()
+ /// 末尾处的下一词法单元。
+ public Token PopBack()
{
- Token token = tokens.Dequeue();
- // 读取词法单元后,需要重新检查缩进、文本和位置信息。
- hasIndent = false;
- text = null;
- start = token.Span.End;
+ Token token = tokens.PopBack();
+ length -= token.Text.Length;
return token;
}
///
- /// 返回剩余的缩进文本。
+ /// 清除所有文本。
+ ///
+ public void Clear()
+ {
+ tokens.Clear();
+ length = 0;
+ }
+
+ ///
+ /// 将当前文本的内容添加到指定块文本。
///
- /// 缩进文本。
- private StringView GetIndentText()
+ /// 要添加到的块文本。
+ public void AppendTo(BlockText text)
{
- int column = locator.GetPosition(start).Column;
- if (column == indentStartColumn)
+ int count = tokens.Count;
+ for (int i = 0; i < count; i++)
{
- return indentText[(start - indentOriginalStart)..];
+ text.Add(tokens[i], true);
}
- else
+ }
+
+ ///
+ /// 读取位置映射。
+ ///
+ /// 要写入的位置映射表。
+ public void GetLocationMap(LocationMap locMap)
+ {
+ int count = tokens.Count;
+ int index = 0;
+ for (int i = 0; i < count; i++)
{
- // 当前是部分 Tab,需要使用空格补齐 column(start) 到 startColumn 的位置。
- column = locator.GetPosition(start + 1).Column;
- using ValueList result = new(stackalloc char[ValueList.StackallocCharSizeLimit]);
- result.Add(' ', column - indentStartColumn);
- int idx = start + 1 - indentOriginalStart;
- // 存在 Tab 时,可能会出现列数超出字符数的场景。
- if (idx < indentText.Length)
- {
- result.Add(indentText.AsSpan(idx));
- }
- return result.ToString();
+ var token = tokens[i];
+ locMap.Add(index, token.Span.Start);
+ index += token.Text.Length;
+ }
+ }
+
+ ///
+ /// 创建当前文本的副本。
+ ///
+ /// 当前文本的副本。
+ public BlockText Clone()
+ {
+ BlockText result = new();
+ int count = tokens.Count;
+ result.tokens.EnsureCapacity(count);
+ result.length = length;
+ for (int i = 0; i < count; i++)
+ {
+ result.tokens.PushBack(tokens[i]);
+ }
+ return result;
+ }
+
+ ///
+ /// 返回当前对象的字符串视图表示形式。
+ ///
+ /// 当前对象的字符串视图表示形式。
+ public StringView ToStringView()
+ {
+ int count = tokens.Count;
+ if (count == 0)
+ {
+ return StringView.Empty;
+ }
+ else if (count == 1)
+ {
+ return tokens[0].Text;
+ }
+ // 优先连接字符串视图。
+ StringView view = tokens[0].Text;
+ int i = 1;
+ for (; i < count && view.TryConcat(tokens[i].Text, out var newView); i++)
+ {
+ view = newView;
+ }
+ if (i >= count)
+ {
+ return view;
+ }
+ // 存在无法连接的字符串,改为使用 StringBuilder 拼接。
+ StringBuilder text = StringBuilderPool.Rent(length);
+ text.Append(view.AsSpan());
+ for (; i < count; i++)
+ {
+ text.Append(tokens[i].Text.AsSpan());
}
+ string result = text.ToString();
+ StringBuilderPool.Return(text);
+ return result;
}
}
diff --git a/Cyjb.Markdown/ParseBlock/MappedText.cs b/Cyjb.Markdown/ParseBlock/MappedText.cs
deleted file mode 100644
index 77a6f02..0000000
--- a/Cyjb.Markdown/ParseBlock/MappedText.cs
+++ /dev/null
@@ -1,455 +0,0 @@
-using Cyjb.Collections;
-using Cyjb.Markdown.Utils;
-using Cyjb.Text;
-
-namespace Cyjb.Markdown.ParseBlock;
-
-///
-/// 表示可以映射到源码的文本。
-///
-internal sealed class MappedText
-{
- ///
- /// 文本。
- ///
- private readonly List texts;
- ///
- /// 文本的长度。
- ///
- private int length;
- ///
- /// 文本的范围。
- ///
- private TextSpan span;
- ///
- /// 源码映射表。
- ///
- private readonly List> maps;
- ///
- /// 位置映射器。
- ///
- private LocationMap? locMap;
-
- ///
- /// 使用指定的文本和映射信息初始化 类的新实例。
- ///
- /// 文本。
- /// 文本的长度。
- /// 文本的范围。
- /// 映射关系。
- internal MappedText(List texts, int length, TextSpan span, List> maps)
- {
- this.texts = texts;
- this.length = length;
- this.span = span;
- this.maps = maps;
- }
-
- ///
- /// 获取源码映射表。
- ///
- internal List> Maps => maps;
-
- ///
- /// 获取文本的文本范围。
- ///
- public TextSpan Span => span;
-
- ///
- /// 获取文本的长度。
- ///
- public int Length => length;
-
- ///
- /// 获取行是否是空的(不包含任何字符)。
- ///
- public bool IsEmpty => length == 0;
-
- ///
- /// 获取行是否是空白的(只包含空格或 Tab)。
- ///
- public bool IsBlank
- {
- get
- {
- foreach (StringView text in texts)
- {
- if (!text.AsSpan().IsBlank())
- {
- return false;
- }
- }
- return true;
- }
- }
-
- ///
- /// 获取指定索引的文本。
- ///
- /// 要检查的索引。
- /// 指定索引的文本。
- public char this[int index]
- {
- get
- {
- foreach (StringView text in texts)
- {
- if (index >= text.Length)
- {
- index -= text.Length;
- }
- else
- {
- return text[index];
- }
- }
- return SourceReader.InvalidCharacter;
- }
- }
-
- ///
- /// 返回指定字符索引在映射后的索引。
- ///
- /// 要检查的字符索引。
- /// 在映射后的索引。
- public int GetMappedIndex(int index)
- {
- locMap ??= new LocationMap(maps, LocationMapType.Offset);
- return locMap.MapLocation(index);
- }
-
- ///
- /// 移除起始位置的多个字符。
- ///
- /// 要移除的字符个数。
- public void RemoteStart(int count)
- {
- if (count < 0 || count > length)
- {
- throw CommonExceptions.ArgumentCountOutOfRange(count);
- }
- RemoteTextStart(count);
- RemoteMapStart(count);
- length -= count;
- int spanStart = span.Start + count;
- span = new TextSpan(spanStart, spanStart + length);
- }
-
- ///
- /// 移除结束位置的多个字符。
- ///
- /// 要移除的字符个数。
- public void RemoteEnd(int count)
- {
- if (count < 0 || count > length)
- {
- throw CommonExceptions.ArgumentCountOutOfRange(count);
- }
- RemoteTextEnd(count);
- length -= count;
- span = new TextSpan(span.Start, span.Start + length);
- }
-
- ///
- /// 移除起始空白。
- ///
- /// 如果移除了任何起始空白,则返回 true;否则返回 false。
- public bool TrimStart()
- {
- int diff = 0;
- int i;
- for (i = 0; i < texts.Count; i++)
- {
- StringView text = texts[i];
- int len = text.Length;
- text = text.TrimStart(MarkdownUtil.WhitespaceChars);
- if (text.Length == len)
- {
- break;
- }
- len -= text.Length;
- diff += len;
- if (!text.IsEmpty)
- {
- texts[i] = text;
- break;
- }
- }
- if (diff == 0)
- {
- return false;
- }
- texts.RemoveRange(0, i);
- RemoteMapStart(diff);
- length -= diff;
- span = new TextSpan(span.Start + diff, span.End);
- locMap = null;
- return true;
- }
-
- ///
- /// 移除结尾空白。
- ///
- /// 如果移除了任何结尾空白,则返回 true;否则返回 false。
- public bool TrimEnd()
- {
- int diff = 0;
- for (int i = texts.Count - 1; i >= 0; i--)
- {
- StringView span = texts[i];
- int len = span.Length;
- span = span.TrimEnd(MarkdownUtil.WhitespaceChars);
- if (span.Length == len)
- {
- break;
- }
- diff += len - span.Length;
- if (span.IsEmpty)
- {
- texts.RemoveAt(i);
- }
- else
- {
- texts[i] = texts[i][..span.Length];
- break;
- }
- }
- if (diff == 0)
- {
- return false;
- }
- length -= diff;
- span = new TextSpan(span.Start, span.Start + length);
- return true;
- }
-
- ///
- /// 返回当前文本中是否包含指定字符。
- ///
- /// 要检查的字符。
- /// 如果当前文本中包含指定字符,返回 true;否则返回 false。
- public bool Contains(char ch)
- {
- foreach (StringView text in texts)
- {
- if (text.Contains(ch))
- {
- return true;
- }
- }
- return false;
- }
-
- ///
- /// 返回指定的索引处开始,指定长度的文本。
- ///
- /// 开始切片的索引。
- /// 切片所需的长度。
- /// 从 开始长为 的文本。
- /// 小于零。
- /// +
- /// 小于零或大于文本长度。
- public MappedText Slice(int start, int length)
- {
- if (start < 0)
- {
- throw CommonExceptions.ArgumentNegative(start);
- }
- if (length < 0 || start + length > this.length)
- {
- throw CommonExceptions.ArgumentCountOutOfRange(length);
- }
- int originStart = start;
- int originLength = length;
- List newTexts = new();
- foreach (StringView text in texts)
- {
- if (start >= text.Length)
- {
- start -= text.Length;
- }
- else
- {
- int end = start + length;
- if (end > text.Length)
- {
- newTexts.Add(text[start..]);
- length -= text.Length - start;
- start = 0;
- }
- else
- {
- newTexts.Add(text[start..end]);
- break;
- }
- }
- }
- int spanStart = span.Start + originStart;
- TextSpan newSpan = new(spanStart, spanStart + originLength);
- return new MappedText(newTexts, originLength, newSpan, GetMaps(start, length));
- }
-
- ///
- /// 将当前文本添加到指定字符串后。
- ///
- /// 要添加到的字符串。
- /// 添加的起始索引。
- public void AppendTo(ref ValueList list, int startIndex = 0)
- {
- foreach (StringView text in texts)
- {
- if (startIndex >= text.Length)
- {
- startIndex -= text.Length;
- }
- else
- {
- list.Add(text[startIndex..].AsSpan());
- startIndex = 0;
- }
- }
- }
-
- ///
- /// 返回当前对象的字符串表示形式。
- ///
- /// 当前对象的字符串表示形式。
- public override string ToString()
- {
- ValueList text = new(stackalloc char[ValueList.StackallocCharSizeLimit]);
- AppendTo(ref text);
- string result = text.ToString();
- text.Dispose();
- return result;
- }
-
- ///
- /// 移除文本起始位置指定个数的字符。
- ///
- /// 要移除的字符个数。
- private void RemoteTextStart(int count)
- {
- int i;
- for (i = 0; i < texts.Count; i++)
- {
- StringView text = texts[i];
- if (count >= text.Length)
- {
- count -= text.Length;
- }
- else
- {
- break;
- }
- }
- if (i > 0)
- {
- texts.RemoveRange(0, i);
- if (texts.Count == 0)
- {
- return;
- }
- }
- texts[0] = texts[0].Substring(count);
- }
-
- ///
- /// 移除文本结束位置指定个数的字符。
- ///
- /// 要移除的字符个数。
- private void RemoteTextEnd(int count)
- {
- for (int i = texts.Count - 1; i >= 0; i--)
- {
- StringView text = texts[i];
- int len = text.Length;
- if (count >= len)
- {
- count -= len;
- texts.RemoveAt(i);
- }
- else
- {
- texts[i] = text.Substring(0, len - count);
- break;
- }
- }
- }
-
- ///
- /// 移除映射关系起始位置指定个数的字符。
- ///
- /// 要移除的字符个数。
- private void RemoteMapStart(int count)
- {
- int i;
- int mapCount = 0;
- for (i = 0; i < maps.Count; i++)
- {
- Tuple item = maps[i];
- if (count >= item.Item1)
- {
- count -= item.Item1;
- mapCount += item.Item2;
- }
- else
- {
- break;
- }
- }
- if (i > 0)
- {
- maps.RemoveRange(0, i);
- }
- if (maps.Count > 0)
- {
- maps[0] = new Tuple(maps[0].Item1 - count, maps[1].Item2 + mapCount);
- }
- else
- {
- maps.Add(new Tuple(0, mapCount + count));
- }
- }
-
- ///
- /// 返回从指定位置开始指定长度的映射信息。
- ///
- /// 文本的起始位置。
- /// 映射信息的长度。
- /// 相应的映射信息。
- private List> GetMaps(int start, int count)
- {
- if (start == 0)
- {
- return maps;
- }
- List> newMaps = new();
- int mapCount = 0;
- int index = start;
- int mappedIndex = start;
- int i;
- for (i = 0; i < maps.Count; i++)
- {
- Tuple tuple = maps[i];
- if (start >= tuple.Item1)
- {
- start -= tuple.Item1;
- mapCount += tuple.Item2;
- }
- else
- {
- (index, mappedIndex) = maps[i];
- break;
- }
- }
- newMaps.Add(new Tuple(index - start, mappedIndex + mapCount));
- // 后续直到 length 之前的都可以直接复制过去。
- count -= start;
- for (; i < maps.Count && count > 0; i++)
- {
- count -= maps[i].Item1;
- newMaps.Add(maps[i]);
- }
- return newMaps;
- }
-}
diff --git a/Cyjb.Markdown/ParseBlock/Processors/ATXHeadingProcessor.cs b/Cyjb.Markdown/ParseBlock/Processors/ATXHeadingProcessor.cs
index 0d4599e..3f0f9da 100644
--- a/Cyjb.Markdown/ParseBlock/Processors/ATXHeadingProcessor.cs
+++ b/Cyjb.Markdown/ParseBlock/Processors/ATXHeadingProcessor.cs
@@ -1,3 +1,4 @@
+using System.Runtime.CompilerServices;
using Cyjb.Markdown.ParseInline;
using Cyjb.Markdown.Syntax;
using Cyjb.Markdown.Utils;
@@ -23,7 +24,7 @@ internal sealed class ATXHeadingProcessor : BlockProcessor
///
/// ATX 标题的文本。
///
- private readonly MappedText text;
+ private readonly BlockText text;
///
/// 使用ATX 标题的起始位置和文本初始化 类的新实例。
@@ -32,7 +33,7 @@ internal sealed class ATXHeadingProcessor : BlockProcessor
/// ATX 标题的深度。
/// ATX 标题的文本。
/// ATX 标题的属性。
- private ATXHeadingProcessor(int start, int depth, MappedText text, HtmlAttributeList? attrs)
+ private ATXHeadingProcessor(int start, int depth, BlockText text, HtmlAttributeList? attrs)
: base(MarkdownKind.Heading)
{
heading = new Heading(depth, new TextSpan(start, start));
@@ -46,14 +47,14 @@ private ATXHeadingProcessor(int start, int depth, MappedText text, HtmlAttribute
///
/// 获取当前块是否需要解析行内节点。
///
- public override bool NeedParseInlines => !text.IsEmpty;
+ public override bool NeedParseInlines => text.Length > 0;
///
/// 尝试将当前节点延伸到下一行。
///
/// 要检查的行。
/// 当前节点是否可以延伸到下一行。
- public override BlockContinue TryContinue(BlockText line)
+ public override BlockContinue TryContinue(BlockLine line)
{
return BlockContinue.None;
}
@@ -66,7 +67,7 @@ public override BlockContinue TryContinue(BlockText line)
/// 如果存在有效的节点,则返回节点本身。否则返回 null。
public override Node? CloseNode(int end, BlockParser parser)
{
- HeadingUtils.ProcessHeading(parser, heading, LinkUtil.NormalizeLabel(text.ToString()));
+ HeadingUtils.ProcessHeading(parser, heading, LinkUtil.NormalizeLabel(text));
heading.Span = new TextSpan(heading.Span.Start, end);
return heading;
}
@@ -77,7 +78,7 @@ public override BlockContinue TryContinue(BlockText line)
/// 行内节点的解析器。
public override void ParseInline(InlineParser parser)
{
- parser.Parse(Enumerable.Repeat(text, 1), heading.Children);
+ parser.Parse(text, heading.Children);
}
///
@@ -92,36 +93,68 @@ private sealed class BlockFactory : IBlockFactory
/// 要检查的行。
/// 当前匹配到的块处理器。
/// 如果能够开始当前块的解析,则返回解析器序列。否则返回空序列。
- public IEnumerable TryStart(BlockParser parser, BlockText line, BlockProcessor matchedProcessor)
+ public IEnumerable TryStart(BlockParser parser, BlockLine line, BlockProcessor matchedProcessor)
{
if (line.IsCodeIndent)
{
yield break;
}
line.SkipIndent();
- HtmlAttributeList? attrs = line.Peek().Value as HtmlAttributeList;
- MappedText text = line.Text;
- // 计算标题的深度。
- int depth = 0;
- for (; depth < text.Length && text[depth] == '#'; depth++) ;
+ BlockText text = line.BlockText;
+ int start = text.Start;
+ var token = text.PeekFront();
+ HtmlAttributeList? attrs = token.Value as HtmlAttributeList;
+ // 计算标题的深度,标题一定在同一个 Token 内。
+ int depth = GetHeadingDepth(token.Text);
text.RemoteStart(depth);
// 忽略内容前后的空白
text.TrimStart();
text.TrimEnd();
// 检查闭合 #
- int end = text.Length;
- for (; end > 0 && text[end - 1] == '#'; end--) ;
- if (end < text.Length)
+ if (text.Tokens.Count > 0)
+ {
+ TrimEndingSharp(text);
+ }
+ yield return new ATXHeadingProcessor(start, depth, text.Clone(), attrs);
+ }
+
+ ///
+ /// 计算标题的深度。
+ ///
+ /// 标题的文本。
+ /// 标题的深度。
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int GetHeadingDepth(StringView text)
+ {
+ ReadOnlySpan span = text;
+ return span.Length - span.TrimStart('#').Length;
+ }
+
+ ///
+ /// 移除标题的结束 # 符号。
+ ///
+ /// 标题的文本。
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void TrimEndingSharp(BlockText text)
+ {
+ ReadOnlySpan span = text.PeekBack().Text;
+ int sharpCount = span.Length - span.TrimEnd('#').Length;
+ if (sharpCount == 0)
+ {
+ return;
+ }
+ if (sharpCount == span.Length)
+ {
+ // 最后一个 Token 被全部消费。
+ text.PopBack();
+ text.TrimEnd();
+ }
+ else if (MarkdownUtil.IsWhitespace(span[span.Length - sharpCount - 1]))
{
// 要求闭合 # 前包含空格或 Tab。
- if (end == 0 || (end > 0 && MarkdownUtil.IsWhitespace(text[end - 1])))
- {
- text.RemoteEnd(text.Length - end);
- // 忽略结尾 # 前的空白。
- text.TrimEnd();
- }
+ text.RemoteEnd(sharpCount);
+ text.TrimEnd();
}
- yield return new ATXHeadingProcessor(line.Start, depth, text, attrs);
}
}
}
diff --git a/Cyjb.Markdown/ParseBlock/Processors/BlockProcessor.cs b/Cyjb.Markdown/ParseBlock/Processors/BlockProcessor.cs
index 47161d9..306b3f3 100644
--- a/Cyjb.Markdown/ParseBlock/Processors/BlockProcessor.cs
+++ b/Cyjb.Markdown/ParseBlock/Processors/BlockProcessor.cs
@@ -67,23 +67,23 @@ public void NeedReplace()
}
///
- /// 获取当前激活的段落的行。
+ /// 获取当前激活的段落的文本。
///
- /// 当前激活的段落的行,如果激活的节点不是段落,则返回 null。
- public virtual IList? ParagraphLines => null;
+ /// 当前激活的段落的文本,如果激活的节点不是段落,则返回 null。
+ public virtual BlockText? ParagraphText => null;
///
/// 尝试将当前节点延伸到下一行。
///
/// 要检查的行。
/// 当前节点是否可以延伸到下一行。
- public abstract BlockContinue TryContinue(BlockText line);
+ public abstract BlockContinue TryContinue(BlockLine line);
///
/// 添加一个新行。
///
/// 新添加的行。
- public virtual void AddLine(BlockText line) { }
+ public virtual void AddLine(BlockLine line) { }
///
/// 返回当前节点是否可以包含指定类型的子节点。
diff --git a/Cyjb.Markdown/ParseBlock/Processors/BlockquoteProcessor.cs b/Cyjb.Markdown/ParseBlock/Processors/BlockquoteProcessor.cs
index 04acd9d..59903d2 100644
--- a/Cyjb.Markdown/ParseBlock/Processors/BlockquoteProcessor.cs
+++ b/Cyjb.Markdown/ParseBlock/Processors/BlockquoteProcessor.cs
@@ -37,7 +37,7 @@ private BlockquoteProcessor(int start) : base(MarkdownKind.Blockquote)
///
/// 要检查的行。
/// 当前节点是否可以延伸到下一行。
- public override BlockContinue TryContinue(BlockText line)
+ public override BlockContinue TryContinue(BlockLine line)
{
return CheckQuoteStart(line) ? BlockContinue.Continue : BlockContinue.None;
}
@@ -81,15 +81,15 @@ public override void AddNode(Node node)
///
/// 要检查的行。
/// 如果找到了块引用起始标记,则为 true;否则为 false。
- private static bool CheckQuoteStart(BlockText line)
+ private static bool CheckQuoteStart(BlockLine line)
{
- if (line.IsCodeIndent || line.Peek().Kind != BlockKind.QuoteStart)
+ if (line.IsCodeIndent || line.PeekFront().Kind != BlockKind.QuoteStart)
{
return false;
}
else
{
- line.Read();
+ line.PopFront();
// > 之后允许跳过一个空格。
line.SkipIndent(1);
return true;
@@ -108,9 +108,9 @@ private sealed class BlockFactory : IBlockFactory
/// 要检查的行。
/// 当前匹配到的块处理器。
/// 如果能够开始当前块的解析,则返回解析器序列。否则返回空序列。
- public IEnumerable TryStart(BlockParser parser, BlockText line, BlockProcessor matchedProcessor)
+ public IEnumerable TryStart(BlockParser parser, BlockLine line, BlockProcessor matchedProcessor)
{
- int start = line.Peek().Span.Start;
+ int start = line.PeekFront().Span.Start;
if (CheckQuoteStart(line))
{
yield return new BlockquoteProcessor(start);
diff --git a/Cyjb.Markdown/ParseBlock/Processors/CustomContainerProcessor.cs b/Cyjb.Markdown/ParseBlock/Processors/CustomContainerProcessor.cs
index ff488b1..a626797 100644
--- a/Cyjb.Markdown/ParseBlock/Processors/CustomContainerProcessor.cs
+++ b/Cyjb.Markdown/ParseBlock/Processors/CustomContainerProcessor.cs
@@ -55,13 +55,13 @@ private CustomContainerProcessor(int start, int fenceLength, string? info, HtmlA
///
/// 要检查的行。
/// 当前节点是否可以延伸到下一行。
- public override BlockContinue TryContinue(BlockText line)
+ public override BlockContinue TryContinue(BlockLine line)
{
if (!line.IsCodeIndent)
{
- Token token = line.Peek();
+ Token token = line.PeekFront();
if (token.Kind == BlockKind.CustomContainerFence &&
- MarkdownUtil.GetFenceLength(token.Text.AsSpan()) >= fenceLength)
+ MarkdownUtil.GetFenceLength(token.Text) >= fenceLength)
{
return BlockContinue.Closed;
}
@@ -112,7 +112,7 @@ private sealed class BlockFactory : IBlockFactory
/// 要检查的行。
/// 当前匹配到的块处理器。
/// 如果能够开始当前块的解析,则返回解析器序列。否则返回空序列。
- public IEnumerable TryStart(BlockParser parser, BlockText line, BlockProcessor matchedProcessor)
+ public IEnumerable TryStart(BlockParser parser, BlockLine line, BlockProcessor matchedProcessor)
{
if (line.IsCodeIndent)
{
diff --git a/Cyjb.Markdown/ParseBlock/Processors/DocumentProcessor.cs b/Cyjb.Markdown/ParseBlock/Processors/DocumentProcessor.cs
index 7273016..6200a33 100644
--- a/Cyjb.Markdown/ParseBlock/Processors/DocumentProcessor.cs
+++ b/Cyjb.Markdown/ParseBlock/Processors/DocumentProcessor.cs
@@ -28,7 +28,7 @@ public DocumentProcessor() : base(MarkdownKind.Document) { }
///
/// 要检查的行。
/// 当前节点是否可以延伸到下一行。
- public override BlockContinue TryContinue(BlockText line)
+ public override BlockContinue TryContinue(BlockLine line)
{
return BlockContinue.Continue;
}
diff --git a/Cyjb.Markdown/ParseBlock/Processors/FencedCodeBlockProcessor.cs b/Cyjb.Markdown/ParseBlock/Processors/FencedCodeBlockProcessor.cs
index 4ae4341..0e0d916 100644
--- a/Cyjb.Markdown/ParseBlock/Processors/FencedCodeBlockProcessor.cs
+++ b/Cyjb.Markdown/ParseBlock/Processors/FencedCodeBlockProcessor.cs
@@ -18,7 +18,7 @@ internal class FencedCodeBlockProcessor : BlockProcessor
///
/// 代码的文本。
///
- private readonly StringBuilder builder = StringBuilderPool.Rent(16);
+ private readonly StringBuilder builder = StringBuilderPool.Rent(64);
///
/// 代码块。
///
@@ -67,13 +67,13 @@ private FencedCodeBlockProcessor(int start,
///
/// 要检查的行。
/// 当前节点是否可以延伸到下一行。
- public override BlockContinue TryContinue(BlockText line)
+ public override BlockContinue TryContinue(BlockLine line)
{
if (!line.IsCodeIndent)
{
- Token token = line.Peek();
+ Token token = line.PeekFront();
if (token.Kind == BlockKind.CodeFence && token.Text[0] == fence &&
- MarkdownUtil.GetFenceLength(token.Text.AsSpan()) >= fenceLength)
+ MarkdownUtil.GetFenceLength(token.Text) >= fenceLength)
{
return BlockContinue.Closed;
}
@@ -87,7 +87,7 @@ public override BlockContinue TryContinue(BlockText line)
/// 添加一个新行。
///
/// 新添加的行。
- public override void AddLine(BlockText line)
+ public override void AddLine(BlockLine line)
{
line.AppendTo(builder);
}
@@ -118,7 +118,7 @@ private sealed class BlockFactory : IBlockFactory
/// 要检查的行。
/// 当前匹配到的块处理器。
/// 如果能够开始当前块的解析,则返回解析器序列。否则返回空序列。
- public IEnumerable TryStart(BlockParser parser, BlockText line, BlockProcessor matchedProcessor)
+ public IEnumerable TryStart(BlockParser parser, BlockLine line, BlockProcessor matchedProcessor)
{
if (line.IsCodeIndent)
{
diff --git a/Cyjb.Markdown/ParseBlock/Processors/FootnoteProcessor.cs b/Cyjb.Markdown/ParseBlock/Processors/FootnoteProcessor.cs
index 070c1f6..cc1610e 100644
--- a/Cyjb.Markdown/ParseBlock/Processors/FootnoteProcessor.cs
+++ b/Cyjb.Markdown/ParseBlock/Processors/FootnoteProcessor.cs
@@ -45,13 +45,13 @@ private FootnoteProcessor(int start, int end, string label)
///
/// 要检查的行。
/// 当前节点是否可以延伸到下一行。
- public override BlockContinue TryContinue(BlockText line)
+ public override BlockContinue TryContinue(BlockLine line)
{
if (line.IsCodeIndent || line.IsBlank())
{
// 缩进会被认为是脚注的一部分,这时要吃掉 4 个缩进。
// 空白行也是脚注的一部分。
- line.SkipIndent(BlockText.CodeIndent);
+ line.SkipIndent(BlockLine.CodeIndent);
return BlockContinue.Continue;
}
else
@@ -109,13 +109,15 @@ private sealed class BlockFactory : IBlockFactory
/// 要检查的行。
/// 当前匹配到的块处理器。
/// 如果能够开始当前块的解析,则返回解析器序列。否则返回空序列。
- public IEnumerable TryStart(BlockParser parser, BlockText line, BlockProcessor matchedProcessor)
+ public IEnumerable TryStart(BlockParser parser, BlockLine line, BlockProcessor matchedProcessor)
{
if (line.IsCodeIndent)
{
yield break;
}
- Token token = line.Read();
+ // 提前保存结束位置,避免行内文本被清空后无法找到正确的结束位置。
+ int end = line.End;
+ Token token = line.PopFront();
// 跳过之后的空白。
line.SkipIndent();
// 如果达到了行尾,那么消费整行,确保在空脚注时能够拿到正确的结束位置。
@@ -123,7 +125,11 @@ public IEnumerable TryStart(BlockParser parser, BlockText line,
{
line.Skip();
}
- yield return new FootnoteProcessor(token.Span.Start, line.Start, ((StringView)token.Value!).ToString());
+ else
+ {
+ end = line.Start;
+ }
+ yield return new FootnoteProcessor(token.Span.Start, end, ((StringView)token.Value!).ToString());
}
}
}
diff --git a/Cyjb.Markdown/ParseBlock/Processors/HtmlBlockProcessor.cs b/Cyjb.Markdown/ParseBlock/Processors/HtmlBlockProcessor.cs
index e8c6b42..46485ac 100644
--- a/Cyjb.Markdown/ParseBlock/Processors/HtmlBlockProcessor.cs
+++ b/Cyjb.Markdown/ParseBlock/Processors/HtmlBlockProcessor.cs
@@ -49,7 +49,7 @@ private HtmlBlockProcessor(int start, HtmlInfo info)
///
/// 要检查的行。
/// 当前节点是否可以延伸到下一行。
- public override BlockContinue TryContinue(BlockText line)
+ public override BlockContinue TryContinue(BlockLine line)
{
if (finished)
{
@@ -70,7 +70,7 @@ public override BlockContinue TryContinue(BlockText line)
/// 添加一个新行。
///
/// 新添加的行。
- public override void AddLine(BlockText line)
+ public override void AddLine(BlockLine line)
{
int start = builder.Length;
line.AppendTo(builder);
@@ -102,13 +102,13 @@ private sealed class BlockFactory : IBlockFactory
/// 要检查的行。
/// 当前匹配到的块处理器。
/// 如果能够开始当前块的解析,则返回解析器序列。否则返回空序列。
- public IEnumerable TryStart(BlockParser parser, BlockText line, BlockProcessor matchedProcessor)
+ public IEnumerable TryStart(BlockParser parser, BlockLine line, BlockProcessor matchedProcessor)
{
if (line.IsCodeIndent)
{
yield break;
}
- HtmlInfo info = (HtmlInfo)line.Peek().Value!;
+ HtmlInfo info = (HtmlInfo)line.PeekFront().Value!;
if (!info.CanInterruptParagraph && (parser.ActivatedProcessor.Kind == MarkdownKind.Paragraph ||
parser.ActivatedProcessor.CanLazyContinuation))
{
diff --git a/Cyjb.Markdown/ParseBlock/Processors/IBlockFactory.cs b/Cyjb.Markdown/ParseBlock/Processors/IBlockFactory.cs
index 8841b2e..93c8aa3 100644
--- a/Cyjb.Markdown/ParseBlock/Processors/IBlockFactory.cs
+++ b/Cyjb.Markdown/ParseBlock/Processors/IBlockFactory.cs
@@ -12,5 +12,5 @@ internal interface IBlockFactory
/// 要检查的行。
/// 当前匹配到的块处理器。
/// 如果能够开始当前块的解析,则返回解析器序列。否则返回空序列。
- IEnumerable TryStart(BlockParser parser, BlockText line, BlockProcessor matchedProcessor);
+ IEnumerable TryStart(BlockParser parser, BlockLine line, BlockProcessor matchedProcessor);
}
diff --git a/Cyjb.Markdown/ParseBlock/Processors/IndentedCodeBlockProcessor.cs b/Cyjb.Markdown/ParseBlock/Processors/IndentedCodeBlockProcessor.cs
index f8a8d02..c0ea20f 100644
--- a/Cyjb.Markdown/ParseBlock/Processors/IndentedCodeBlockProcessor.cs
+++ b/Cyjb.Markdown/ParseBlock/Processors/IndentedCodeBlockProcessor.cs
@@ -16,7 +16,7 @@ internal class IndentedCodeBlockProcessor : BlockProcessor
/// 块级语法分析器。
/// 要检查的行。
/// 新的块处理器数组,若未能成功解析,则返回空数组。
- public static IEnumerable TryStart(BlockParser parser, BlockText line)
+ public static IEnumerable TryStart(BlockParser parser, BlockLine line)
{
// 缩进代码块不会中断段落。
if (line.IsCodeIndent && !line.IsBlank() &&
@@ -25,7 +25,7 @@ public static IEnumerable TryStart(BlockParser parser, BlockText
// 代码块的起始位置包含缩进位置。
int start = line.Start;
// 跳过空白部分。
- line.SkipIndent(BlockText.CodeIndent);
+ line.SkipIndent(BlockLine.CodeIndent);
yield return new IndentedCodeBlockProcessor(start, line.End);
}
}
@@ -63,19 +63,19 @@ private IndentedCodeBlockProcessor(int start, int end) : base(MarkdownKind.CodeB
///
/// 要检查的行。
/// 当前节点是否可以延伸到下一行。
- public override BlockContinue TryContinue(BlockText line)
+ public override BlockContinue TryContinue(BlockLine line)
{
if (line.IsCodeIndent)
{
// 跳过空白部分。
- line.SkipIndent(BlockText.CodeIndent);
+ line.SkipIndent(BlockLine.CodeIndent);
end = line.End;
return BlockContinue.Continue;
}
else if (line.IsBlank())
{
// 跳过空白部分,但暂时不计入结尾。
- line.SkipIndent(BlockText.CodeIndent);
+ line.SkipIndent(BlockLine.CodeIndent);
return BlockContinue.Continue;
}
else
@@ -88,7 +88,7 @@ public override BlockContinue TryContinue(BlockText line)
/// 添加一个新行。
///
/// 新添加的行。
- public override void AddLine(BlockText line)
+ public override void AddLine(BlockLine line)
{
line.AppendTo(builder);
if (!line.IsBlank())
diff --git a/Cyjb.Markdown/ParseBlock/Processors/LinkDefinitionParser.cs b/Cyjb.Markdown/ParseBlock/Processors/LinkDefinitionParser.cs
index 5efa00c..94b0406 100644
--- a/Cyjb.Markdown/ParseBlock/Processors/LinkDefinitionParser.cs
+++ b/Cyjb.Markdown/ParseBlock/Processors/LinkDefinitionParser.cs
@@ -131,49 +131,46 @@ public LinkDefinitionParser(ParseOptions options, Action clearLines)
/// 解析指定的行。
///
/// 行的文本。
- public void Parse(MappedText text)
+ /// 行的文本范围。
+ public void Parse(StringView text, TextSpan span)
{
- ValueList list = new(stackalloc char[ValueList.StackallocCharSizeLimit]);
- text.AppendTo(ref list);
- ReadOnlySpan textSpan = list.AsSpan();
+ ReadOnlySpan textSpan = text;
while (!textSpan.IsEmpty)
{
bool success = false;
switch (state)
{
case State.StartDefinition:
- success = ParseStartDefinition(ref textSpan, text.Span);
+ success = ParseStartDefinition(ref textSpan, span);
break;
case State.Label:
success = ParseLabel(ref textSpan);
break;
case State.Destination:
- success = ParseDestination(ref textSpan, text.Span);
+ success = ParseDestination(ref textSpan, span);
break;
case State.StartTitleOrAttr:
success = ParseStartTitleOrAttr(ref textSpan);
break;
case State.Title:
- success = ParseTitle(ref textSpan, text.Span);
+ success = ParseTitle(ref textSpan, span);
break;
case State.StartAttributes:
success = ParseStartAttribute(ref textSpan);
break;
case State.Attributes:
- success = ParseAttributes(ref textSpan, text.Span);
+ success = ParseAttributes(ref textSpan, span);
break;
case State.AttributeValue:
- success = ParseAttributeValue(ref textSpan, text.Span);
+ success = ParseAttributeValue(ref textSpan, span);
break;
}
if (!success)
{
state = State.Failed;
- list.Dispose();
return;
}
}
- list.Dispose();
}
///
diff --git a/Cyjb.Markdown/ParseBlock/Processors/ListItemProcessor.cs b/Cyjb.Markdown/ParseBlock/Processors/ListItemProcessor.cs
index c9851ed..c9640d1 100644
--- a/Cyjb.Markdown/ParseBlock/Processors/ListItemProcessor.cs
+++ b/Cyjb.Markdown/ParseBlock/Processors/ListItemProcessor.cs
@@ -71,7 +71,7 @@ public bool? Checked
///
/// 要检查的行。
/// 当前节点是否可以延伸到下一行。
- public override BlockContinue TryContinue(BlockText line)
+ public override BlockContinue TryContinue(BlockLine line)
{
if (line.IsBlank())
{
diff --git a/Cyjb.Markdown/ParseBlock/Processors/ListProcessor.cs b/Cyjb.Markdown/ParseBlock/Processors/ListProcessor.cs
index cdaf3e0..f071f77 100644
--- a/Cyjb.Markdown/ParseBlock/Processors/ListProcessor.cs
+++ b/Cyjb.Markdown/ParseBlock/Processors/ListProcessor.cs
@@ -81,7 +81,7 @@ public void MarkLoose()
///
/// 要检查的行。
/// 当前节点是否可以延伸到下一行。
- public override BlockContinue TryContinue(BlockText line)
+ public override BlockContinue TryContinue(BlockLine line)
{
if (line.IsBlank())
{
@@ -183,16 +183,16 @@ private sealed class BlockFactory : IBlockFactory
/// 要检查的行。
/// 当前匹配到的块处理器。
/// 如果能够开始当前块的解析,则返回解析器序列。否则返回空序列。
- public IEnumerable TryStart(BlockParser parser, BlockText line, BlockProcessor matchedProcessor)
+ public IEnumerable TryStart(BlockParser parser, BlockLine line, BlockProcessor matchedProcessor)
{
if (line.IsCodeIndent)
{
yield break;
}
- Token token = line.Peek();
+ Token token = line.PeekFront();
ListStyleType styleType = (ListStyleType)token.Value!;
bool hasContent = !line.IsBlank(1);
- if (matchedProcessor.ParagraphLines?.Count > 0)
+ if (matchedProcessor.ParagraphText?.Length > 0)
{
// 空列表不能中断段落。
if (!hasContent)
@@ -208,10 +208,10 @@ public IEnumerable TryStart(BlockParser parser, BlockText line,
int itemStart = token.Span.Start;
// 找到内容相对列表项起始的缩进宽度。
int indentAfterMarker = line.Indent + token.Text.Length;
- line.Read();
+ line.PopFront();
int contentIndent = indentAfterMarker + line.Indent;
// 如果没有内容或者是代码段,那么认为内容缩进是列表项后一个字符位置。
- if (!hasContent || contentIndent - indentAfterMarker > BlockText.CodeIndent)
+ if (!hasContent || contentIndent - indentAfterMarker > BlockLine.CodeIndent)
{
contentIndent = indentAfterMarker + 1;
// 只跳过 marker 后的一个空白。
@@ -315,14 +315,14 @@ private static int ParseRomain(ReadOnlySpan text)
/// 要检查的行。
/// 如果当前行包含任务列表项,根据是否勾选返回 true 或 false;
/// 如果不包含任务列表项,返回 null。
- private static bool? CheckTaskListItem(BlockText line)
+ private static bool? CheckTaskListItem(BlockLine line)
{
// 检查包含任务列表项标志
if (line.Indent >= 4 || line.IsBlank())
{
return null;
}
- Token token = line.Peek();
+ Token token = line.PeekFront();
if (token.Kind != BlockKind.TaskListItemMarker)
{
return null;
@@ -340,7 +340,7 @@ private static int ParseRomain(ReadOnlySpan text)
return null;
}
// 消费掉任务列表项标志。
- line.Read();
+ line.PopFront();
// 消费掉任务列表项标志后的一个空白。
line.SkipIndent(1);
// 后续的段落不要跳过这里的空白。
diff --git a/Cyjb.Markdown/ParseBlock/Processors/MathBlockProcessor.cs b/Cyjb.Markdown/ParseBlock/Processors/MathBlockProcessor.cs
index 4065114..0d4aaec 100644
--- a/Cyjb.Markdown/ParseBlock/Processors/MathBlockProcessor.cs
+++ b/Cyjb.Markdown/ParseBlock/Processors/MathBlockProcessor.cs
@@ -60,11 +60,11 @@ private MathBlockProcessor(int start, int fenceLength, int indent, string? info,
///
/// 要检查的行。
/// 当前节点是否可以延伸到下一行。
- public override BlockContinue TryContinue(BlockText line)
+ public override BlockContinue TryContinue(BlockLine line)
{
if (!line.IsCodeIndent)
{
- Token token = line.Peek();
+ Token token = line.PeekFront();
if (token.Kind == BlockKind.MathFence &&
MarkdownUtil.GetFenceLength(token.Text) >= fenceLength)
{
@@ -80,7 +80,7 @@ public override BlockContinue TryContinue(BlockText line)
/// 添加一个新行。
///
/// 新添加的行。
- public override void AddLine(BlockText line)
+ public override void AddLine(BlockLine line)
{
line.AppendTo(builder);
}
@@ -111,7 +111,7 @@ private sealed class BlockFactory : IBlockFactory
/// 要检查的行。
/// 当前匹配到的块处理器。
/// 如果能够开始当前块的解析,则返回解析器序列。否则返回空序列。
- public IEnumerable TryStart(BlockParser parser, BlockText line, BlockProcessor matchedProcessor)
+ public IEnumerable TryStart(BlockParser parser, BlockLine line, BlockProcessor matchedProcessor)
{
if (line.IsCodeIndent)
{
diff --git a/Cyjb.Markdown/ParseBlock/Processors/ParagraphProcessor.cs b/Cyjb.Markdown/ParseBlock/Processors/ParagraphProcessor.cs
index b822640..62076c3 100644
--- a/Cyjb.Markdown/ParseBlock/Processors/ParagraphProcessor.cs
+++ b/Cyjb.Markdown/ParseBlock/Processors/ParagraphProcessor.cs
@@ -1,3 +1,4 @@
+using System.Text;
using Cyjb.Markdown.ParseInline;
using Cyjb.Markdown.Syntax;
using Cyjb.Text;
@@ -16,7 +17,7 @@ internal sealed class ParagraphProcessor : BlockProcessor
///
/// 段落包含的行。
///
- private readonly List lines = new();
+ private readonly BlockText text = new();
///
/// 代码块的起始位置。
///
@@ -29,16 +30,18 @@ internal sealed class ParagraphProcessor : BlockProcessor
/// 链接定义的解析器。
///
private readonly LinkDefinitionParser linkDefinitionParser;
+ private bool needClearLines = false;
///
/// 使用指定的解析选项初始化 类的新实例。
///
- /// 解析选项。
- public ParagraphProcessor(ParseOptions options) : base(MarkdownKind.Paragraph)
+ /// 块级语法分析器。
+ public ParagraphProcessor(BlockParser parser) : base(MarkdownKind.Paragraph)
{
- linkDefinitionParser = new LinkDefinitionParser(options, () =>
+ linkDefinitionParser = new LinkDefinitionParser(parser.Options, () =>
{
- lines.Clear();
+ text.Clear();
+ needClearLines = true;
start = -1;
trimStart = true;
});
@@ -64,14 +67,14 @@ public ParagraphProcessor(ParseOptions options) : base(MarkdownKind.Paragraph)
/// 获取当前激活的段落的行。
///
/// 当前激活的段落的行,如果激活的节点不是段落,则返回 null。
- public override IList? ParagraphLines => lines;
+ public override BlockText? ParagraphText => text;
///
/// 尝试将当前节点延伸到下一行。
///
/// 要检查的行。
/// 当前节点是否可以延伸到下一行。
- public override BlockContinue TryContinue(BlockText line)
+ public override BlockContinue TryContinue(BlockLine line)
{
return line.IsBlank() ? BlockContinue.None : BlockContinue.Continue;
}
@@ -80,7 +83,7 @@ public override BlockContinue TryContinue(BlockText line)
/// 添加一个新行。
///
/// 新添加的行。
- public override void AddLine(BlockText line)
+ public override void AddLine(BlockLine line)
{
// 在之前的文本被识别为链接声明后,需要移除新的行首空白。
if (trimStart)
@@ -92,11 +95,17 @@ public override void AddLine(BlockText line)
{
start = line.Start;
}
- MappedText text = line.Text;
- lines.Add(text);
if (linkDefinitionParser.CanContinue)
{
- linkDefinitionParser.Parse(text);
+ linkDefinitionParser.Parse(line.ToStringView(), line.Span);
+ }
+ if (needClearLines == true)
+ {
+ needClearLines = false;
+ }
+ else
+ {
+ line.AppendTo(text);
}
}
@@ -109,13 +118,13 @@ public override void AddLine(BlockText line)
public override Node? CloseNode(int end, BlockParser parser)
{
AddDefinitions(parser);
- if (lines.Count == 0)
+ if (text.Length == 0)
{
// 没有有效的行。
return null;
}
// 移除尾行后的空白。
- lines[^1].TrimEnd();
+ text.TrimEnd();
paragraph.Span = new TextSpan(start, end);
return paragraph;
}
@@ -143,6 +152,6 @@ public void AddDefinitions(BlockParser parser)
/// 行内节点的解析器。
public override void ParseInline(InlineParser parser)
{
- parser.Parse(lines, paragraph.Children);
+ parser.Parse(text, paragraph.Children);
}
}
diff --git a/Cyjb.Markdown/ParseBlock/Processors/SetextHeadingProcessor.cs b/Cyjb.Markdown/ParseBlock/Processors/SetextHeadingProcessor.cs
index 047951f..e9e59e9 100644
--- a/Cyjb.Markdown/ParseBlock/Processors/SetextHeadingProcessor.cs
+++ b/Cyjb.Markdown/ParseBlock/Processors/SetextHeadingProcessor.cs
@@ -1,4 +1,3 @@
-using System.Text;
using Cyjb.Collections;
using Cyjb.Markdown.ParseInline;
using Cyjb.Markdown.Syntax;
@@ -25,7 +24,7 @@ internal sealed class SetextHeadingProcessor : BlockProcessor
///
/// Setext 标题的文本。
///
- private readonly IList text;
+ private readonly BlockText text;
///
/// 使用 Setext 标题的起始位置和文本初始化 类的新实例。
@@ -34,7 +33,7 @@ internal sealed class SetextHeadingProcessor : BlockProcessor
/// Setext 标题的深度。
/// Setext 标题的文本。
/// Setext 标题的属性。
- private SetextHeadingProcessor(int start, int depth, IList text, HtmlAttributeList? attrs)
+ private SetextHeadingProcessor(int start, int depth, BlockText text, HtmlAttributeList? attrs)
: base(MarkdownKind.Heading)
{
heading = new Heading(depth, new TextSpan(start, start));
@@ -55,7 +54,7 @@ private SetextHeadingProcessor(int start, int depth, IList text, Htm
///
/// 要检查的行。
/// 当前节点是否可以延伸到下一行。
- public override BlockContinue TryContinue(BlockText line)
+ public override BlockContinue TryContinue(BlockLine line)
{
return BlockContinue.None;
}
@@ -68,8 +67,7 @@ public override BlockContinue TryContinue(BlockText line)
/// 如果存在有效的节点,则返回节点本身。否则返回 null。
public override Node? CloseNode(int end, BlockParser parser)
{
- string label = LinkUtil.NormalizeLabel(string.Join("", text.Select(text => text.ToString())));
- HeadingUtils.ProcessHeading(parser, heading, label);
+ HeadingUtils.ProcessHeading(parser, heading, LinkUtil.NormalizeLabel(text));
heading.Span = new TextSpan(heading.Span.Start, end);
return heading;
}
@@ -95,79 +93,54 @@ private sealed class BlockFactory : IBlockFactory
/// 要检查的行。
/// 当前匹配到的块处理器。
/// 如果能够开始当前块的解析,则返回解析器序列。否则返回空序列。
- public IEnumerable TryStart(BlockParser parser, BlockText line, BlockProcessor matchedProcessor)
+ public IEnumerable TryStart(BlockParser parser, BlockLine line, BlockProcessor matchedProcessor)
{
// 要求 Setext 标签之前是段落,而且包含有效内容。
- IList? lines;
- if (line.IsCodeIndent || (lines = matchedProcessor.ParagraphLines) == null ||
- lines.Count == 0)
+ BlockText? text;
+ if (line.IsCodeIndent || (text = matchedProcessor.ParagraphText) == null ||
+ text.Length == 0)
{
yield break;
}
// 需要将之前的段落关闭。
matchedProcessor.NeedReplace();
- int depth = line.Peek().Text[0] == '=' ? 1 : 2;
+ int depth = line.PeekFront().Text[0] == '=' ? 1 : 2;
// 移除尾行后的空白。
- lines[^1].TrimEnd();
+ text.TrimEnd();
HtmlAttributeList? attrs = null;
// 尝试解析属性。
if (parser.Options.UseHeaderAttributes)
{
- attrs = ParseAttributes(lines);
- // 移除尾行后的空白。
- lines[^1].TrimEnd();
+ attrs = ParseAttributes(text);
+ // 移除尾行后的空白,注意不要移除换行本身。
+ text.TrimEnd(false);
}
- yield return new SetextHeadingProcessor(lines[0].Span.Start, depth, lines, attrs);
+ yield return new SetextHeadingProcessor(text.Start, depth, text, attrs);
}
///
/// 尝试从行中解析属性。
///
- /// 要检查的行。
+ /// 要检查的文本。
/// 解析得到的属性列表,或者 null 表示解析失败。
- public static HtmlAttributeList? ParseAttributes(IList lines)
+ public static HtmlAttributeList? ParseAttributes(BlockText text)
{
// 最后一个字符是 }
- string text = lines[^1].ToString();
if (text.Length == 0 || text[^1] != '}')
{
return null;
}
// 找到最后一个未被引号扩起来的 {,且要求是未转义的。
- int lineIdx = lines.Count - 1;
- int startIdx = -1;
- for (; lineIdx >= 0; lineIdx--)
- {
- text = lines[lineIdx].ToString();
- startIdx = MarkdownUtil.FindAttributeStart(text);
- if (startIdx == -2)
- {
- // -2 表示找到了 { 但不能用作属性起始。
- return null;
- }
- else if (startIdx == -1)
- {
- // -1 表示未找到 {。
- continue;
- }
- break;
- }
+ int startIdx = MarkdownUtil.FindAttributeStart(text);
if (startIdx < 0)
{
// 未找到起始 {。
return null;
}
ValueList list = new(stackalloc char[ValueList.StackallocCharSizeLimit]);
- for (int i = lineIdx; i < lines.Count; i++)
+ for (int i = startIdx; i < text.Length; i++)
{
- if (i == lineIdx)
- {
- lines[i].AppendTo(ref list, startIdx);
- }
- else
- {
- lines[i].AppendTo(ref list);
- }
+ list.Add(text[i]);
}
ReadOnlySpan span = list.AsSpan();
HtmlAttributeList? attrs = MarkdownUtil.ParseAttributes(ref span);
@@ -175,12 +148,7 @@ public IEnumerable TryStart(BlockParser parser, BlockText line,
{
list.Dispose();
// 移除行中不需要的部分。
- for (int i = lines.Count - 1; i > lineIdx; i--)
- {
- lines.RemoveAt(i);
- }
- MappedText lastLine = lines[^1];
- lastLine.RemoteEnd(lastLine.Length - startIdx);
+ text.RemoteEnd(text.Length - startIdx);
}
else
{
diff --git a/Cyjb.Markdown/ParseBlock/Processors/TableProcessor.cs b/Cyjb.Markdown/ParseBlock/Processors/TableProcessor.cs
index 6e725f1..3a74de3 100644
--- a/Cyjb.Markdown/ParseBlock/Processors/TableProcessor.cs
+++ b/Cyjb.Markdown/ParseBlock/Processors/TableProcessor.cs
@@ -1,6 +1,3 @@
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Text;
using Cyjb.Markdown.ParseInline;
using Cyjb.Markdown.Syntax;
using Cyjb.Markdown.Utils;
@@ -28,19 +25,45 @@ internal sealed class TableProcessor : BlockProcessor
private readonly List cellInfos = new();
///
- /// 使用表格的对齐信息和标题行初始化 类的新实例。
+ /// 使用表格的分隔符信息和标题行初始化 类的新实例。
///
- /// 对齐信息。
+ /// 表格分隔符。
/// 标题行。
- private TableProcessor(List aligns, MappedText heading)
+ private TableProcessor(StringView[] delimiters, BlockText heading)
: base(MarkdownKind.Table)
{
- TableRow row = ParseRow(heading);
- int start = heading.Span.Start;
+ TableRow row = ParseRow(heading.Span, heading);
+ int start = heading.Start;
table = new Table(row, new TextSpan(start, start));
- for (int i = 0; i < aligns.Count; i++)
+ ParseDelimiters(delimiters);
+ }
+
+ ///
+ /// 解析分隔符。
+ ///
+ /// 要解析的分隔符。
+ private void ParseDelimiters(StringView[] delimiters)
+ {
+ for (int i = 0; i < delimiters.Length; i++)
{
- table.Aligns[i] = aligns[i];
+ StringView cellText = delimiters[i].Trim(MarkdownUtil.WhitespaceChars);
+ TableAlign align = TableAlign.None;
+ if (cellText.StartsWith(':'))
+ {
+ align = TableAlign.Left;
+ }
+ if (cellText.EndsWith(':'))
+ {
+ if (align == TableAlign.Left)
+ {
+ align = TableAlign.Center;
+ }
+ else
+ {
+ align = TableAlign.Right;
+ }
+ }
+ table.Aligns[i] = align;
}
}
@@ -63,7 +86,7 @@ private TableProcessor(List aligns, MappedText heading)
///
/// 要检查的行。
/// 当前节点是否可以延伸到下一行。
- public override BlockContinue TryContinue(BlockText line)
+ public override BlockContinue TryContinue(BlockLine line)
{
return line.IsBlank() ? BlockContinue.None : BlockContinue.Continue;
}
@@ -72,9 +95,9 @@ public override BlockContinue TryContinue(BlockText line)
/// 添加一个新行。
///
/// 新添加的行。
- public override void AddLine(BlockText line)
+ public override void AddLine(BlockLine line)
{
- table.Children.Add(ParseRow(line.Text));
+ table.Children.Add(ParseRow(line.Span, line.BlockText));
}
///
@@ -104,74 +127,85 @@ public override void ParseInline(InlineParser parser)
///
/// 解析指定行。
///
+ /// 行的文本范围。
/// 要解析的文本。
/// 解析后的表格行。
- private TableRow ParseRow(MappedText text)
+ private TableRow ParseRow(TextSpan rowSpan, BlockText text)
{
- TextSpan rowSpan = text.Span;
- text.TrimStart();
+ // 行首的空白都已当作缩进来处理,因此这里不需要 TrimStart。
text.TrimEnd();
List cells = new();
- List texts = new();
- int spanStart = 0;
- int start = 0;
- int len = text.Length;
- for (int i = 0; i < len; i++)
+ BlockText cellText = new();
+ int cellStart = text.Start;
+ int cellEnd = text.End;
+ bool escaped = false;
+ int count = text.Tokens.Count;
+ for (int i = 0; i < count; i++)
{
- char ch = text[i];
- switch (ch)
+ int startIdx = 0;
+ var token = text.Tokens[i];
+ ReadOnlySpan textSpan = token.Text;
+ int j = 0;
+ if (i == 0 && textSpan[0] == '|')
+ {
+ // 首个 | 总是会当作前导竖划线看待,不计入内容。
+ startIdx = 1;
+ j++;
+ }
+ for (; j < textSpan.Length; j++)
{
- case '\\':
- if (i + 1 < len)
+ char ch = textSpan[j];
+ if (ch == '\\')
+ {
+ escaped = !escaped;
+ }
+ else if (ch == '|')
+ {
+ if (escaped)
{
- if (text[i + 1] == '|')
+ // 需要特殊处理竖划线,转义后的竖划线不会产生新单元格,
+ // 但在后续解析行级元素时,需要当作一个竖划线看待,不会包含前面的转义字符。
+ // 特别是在 `\|` 场景,会被当作 `|` 解析。
+ // 所以在拼接字符串时,将 \ 忽略掉。
+ if (startIdx < j - 1)
{
- // 需要特殊处理竖划线,转义后的竖划线不会产生新单元格,
- // 但在后续解析行级元素时,需要当作一个竖划线看待,不会包含前面的转义字符。
- // 特别是在 `\|` 场景,会被当作 `|` 解析。
- // 所以在拼接字符串时,将 \ 忽略掉。
- if (i > start)
- {
- texts.Add(text[start..i]);
- }
- start = i + 1;
- i++;
+ cellText.Add(token, startIdx, j - 1 - startIdx);
}
- i++;
- }
- break;
- case '|':
- if (i == 0)
- {
- // 首个 | 总是会当作前导竖划线看待,不计入内容。
- start = i + 1;
+ startIdx = j;
+ escaped = false;
}
else
{
- if (i > start)
+ if (j > startIdx)
{
- texts.Add(text[start..i]);
+ cellText.Add(token, startIdx, j - startIdx);
}
// 单元格总是包含结束 | 的。
- TextSpan span = new(text.GetMappedIndex(spanStart), text.GetMappedIndex(i + 1));
- CellInfo info = new(span, texts);
+ int curEnd = token.Span.Start + j + 1;
+ TextSpan span = new(cellStart, curEnd);
+ CellInfo info = new(span, cellText);
cells.Add(info.Cell);
cellInfos.Add(info);
- spanStart = start = i + 1;
- texts = new List();
+ cellStart = curEnd;
+ startIdx = j + 1;
+ cellText = new BlockText();
}
- break;
+ }
+ else
+ {
+ escaped = false;
+ }
+ }
+ // 添加当前 Token 的剩余文本。
+ if (startIdx < textSpan.Length)
+ {
+ cellText.Add(token, startIdx, textSpan.Length - startIdx);
}
}
// 添加可能的最后一个单元格。
- if (texts.Count > 0 || spanStart < len)
+ if (cellText.Length > 0 || cellStart < cellEnd)
{
- if (start < len)
- {
- texts.Add(text[start..len]);
- }
- TextSpan span = new(text.GetMappedIndex(spanStart), text.GetMappedIndex(len));
- CellInfo info = new(span, texts);
+ CellInfo info = new(new TextSpan(cellStart, cellEnd), cellText);
cells.Add(info.Cell);
cellInfos.Add(info);
}
@@ -190,21 +224,21 @@ private sealed class CellInfo
///
/// 映射文本列表。
///
- private readonly List text;
+ private readonly BlockText text;
///
/// 使用指定的文本范围和映射文本初始化 类的新实例。
///
/// 单元格的文本范围。
- /// 映射文本列表。
- public CellInfo(TextSpan span, List text)
+ /// 块文本。
+ public CellInfo(TextSpan span, BlockText text)
{
Cell = new TableCell(span);
// 需要移除文本的前后空白。
- if (text.Count > 0)
+ if (text.Length > 0)
{
- text[0].TrimStart();
- text[^1].TrimEnd();
+ text.TrimStart();
+ text.TrimEnd();
}
this.text = text;
}
@@ -215,7 +249,7 @@ public CellInfo(TextSpan span, List text)
/// 行内节点的解析器。
public void ParseInline(InlineParser parser)
{
- if (text.Count > 0)
+ if (text.Length > 0)
{
parser.Parse(text, Cell.Children);
}
@@ -234,19 +268,21 @@ private sealed class BlockFactory : IBlockFactory
/// 要检查的行。
/// 当前匹配到的块处理器。
/// 如果能够开始当前块的解析,则返回解析器序列。否则返回空序列。
- public IEnumerable TryStart(BlockParser parser, BlockText line, BlockProcessor matchedProcessor)
+ public IEnumerable TryStart(BlockParser parser, BlockLine line, BlockProcessor matchedProcessor)
{
// 要求分割行之前是段落,而且包含且只包含一行。
- IList? lines;
- if (line.IsCodeIndent || (lines = matchedProcessor.ParagraphLines) == null ||
- lines.Count != 1)
+ BlockText? text;
+ if (line.IsCodeIndent || (text = matchedProcessor.ParagraphText) == null ||
+ !text.IsSingleLine())
{
yield break;
}
- MappedText heading = lines[0];
- List aligns = ParseDelimiterRow(line.Peek().Text.ToString());
+ // 语法分析时已确保分割行的内容是有效的,这里直接 split 即可。
+ // 忽略前后空白,忽略空的子字符串来忽略最外侧的竖划线。
+ StringView[] delimiters = line.PeekFront().Text.Trim(MarkdownUtil.WhitespaceChars)
+ .Split('|', StringSplitOptions.RemoveEmptyEntries);
// 标题行与分割行必须具有相同的单元格数。
- if (aligns.Count != CountCell(heading))
+ if (delimiters.Length != CountCell(text))
{
yield break;
}
@@ -254,7 +290,7 @@ public IEnumerable TryStart(BlockParser parser, BlockText line,
matchedProcessor.NeedReplace();
// 跳过当前行。
line.Skip();
- yield return new TableProcessor(aligns, heading);
+ yield return new TableProcessor(delimiters, text);
}
///
@@ -262,76 +298,55 @@ public IEnumerable TryStart(BlockParser parser, BlockText line,
///
/// 要解析的文本。
/// 单元格的个数。
- private static int CountCell(MappedText text)
+ private static int CountCell(BlockText text)
{
- int count = 0;
- ReadOnlySpan str = text.ToString();
- MarkdownUtil.Trim(ref str);
- int start = 0;
- int len = str.Length;
- for (int i = 0; i < len; i++)
+ int cellCount = 0;
+ bool escaped = false;
+ bool hasContent = false;
+ int end = text.Tokens.Count - 1;
+ for (int i = 0; i <= end; i++)
{
- char ch = str[i];
- switch (ch)
+ ReadOnlySpan textSpan = text.Tokens[i].Text;
+ textSpan = textSpan.TrimEnd(MarkdownUtil.Whitespace);
+ int j = 0;
+ if (i == 0 && textSpan[0] == '|')
+ {
+ // 首个 | 总是会当作前导竖划线看待,不计入内容。
+ j++;
+ hasContent = true;
+ }
+ for (; j < textSpan.Length; j++)
{
- case '\\':
- if (i + 1 < len)
+ char ch = textSpan[j];
+ if (ch == '\\')
+ {
+ escaped = !escaped;
+ }
+ else if (ch == '|')
+ {
+ if (escaped)
{
- i++;
+ escaped = false;
}
- break;
- case '|':
- if (i > 0)
+ else
{
- // 首个 | 总是会当作前导竖划线看待,不计入内容。
- count++;
- start = i + 1;
+ cellCount++;
+ hasContent = false;
}
- break;
- }
- }
- // 添加可能的最后一个单元格。
- if (start < len)
- {
- count++;
- }
- return count;
- }
-
- ///
- /// 解析分割行。
- ///
- /// 要解析的文本。
- /// 表格的对齐。
- private static List ParseDelimiterRow(string text)
- {
- // 语法分析时已确保分割行的内容是有效的,这里直接 split 即可。
- // 忽略前后空白,忽略空的子字符串来忽略最外侧的竖划线。
- IEnumerable cells = MarkdownUtil.Trim(text)
- .Split('|', StringSplitOptions.RemoveEmptyEntries)
- .Select(cell => MarkdownUtil.Trim(cell));
- List columns = new();
- foreach (string str in cells)
- {
- TableAlign align = TableAlign.None;
- if (str.StartsWith(':'))
- {
- align = TableAlign.Left;
- }
- if (str.EndsWith(':'))
- {
- if (align == TableAlign.Left)
- {
- align = TableAlign.Center;
}
else
{
- align = TableAlign.Right;
+ escaped = false;
+ hasContent = true;
}
}
- columns.Add(align);
}
- return columns;
+ // 添加可能的最后一个单元格。
+ if (hasContent)
+ {
+ cellCount++;
+ }
+ return cellCount;
}
}
}
diff --git a/Cyjb.Markdown/ParseBlock/Processors/ThematicBreakProcessor.cs b/Cyjb.Markdown/ParseBlock/Processors/ThematicBreakProcessor.cs
index 74418a7..9d28cac 100644
--- a/Cyjb.Markdown/ParseBlock/Processors/ThematicBreakProcessor.cs
+++ b/Cyjb.Markdown/ParseBlock/Processors/ThematicBreakProcessor.cs
@@ -32,7 +32,7 @@ private ThematicBreakProcessor(int start) : base(MarkdownKind.ThematicBreak)
///
/// 要检查的行。
/// 当前节点是否可以延伸到下一行。
- public override BlockContinue TryContinue(BlockText line)
+ public override BlockContinue TryContinue(BlockLine line)
{
return BlockContinue.None;
}
@@ -60,13 +60,13 @@ private sealed class BlockFactory : IBlockFactory
/// 要检查的行。
/// 当前匹配到的块处理器。
/// 如果能够开始当前块的解析,则返回处理器序列。否则返回空序列。
- public IEnumerable TryStart(BlockParser parser, BlockText line, BlockProcessor matchedProcessor)
+ public IEnumerable TryStart(BlockParser parser, BlockLine line, BlockProcessor matchedProcessor)
{
if (line.IsCodeIndent)
{
yield break;
}
- Token token = line.Peek();
+ Token token = line.PeekFront();
if (token.Kind == BlockKind.DashLine)
{
// 需要确保 DashLine 的长度至少为 3。
diff --git a/Cyjb.Markdown/ParseInline/InlineParser.cs b/Cyjb.Markdown/ParseInline/InlineParser.cs
index 015aa47..871e110 100644
--- a/Cyjb.Markdown/ParseInline/InlineParser.cs
+++ b/Cyjb.Markdown/ParseInline/InlineParser.cs
@@ -1,5 +1,4 @@
using System.Diagnostics.CodeAnalysis;
-using Cyjb.Collections;
using Cyjb.Compilers.Lexers;
using Cyjb.Markdown.ParseBlock;
using Cyjb.Markdown.Syntax;
@@ -53,7 +52,7 @@ internal sealed class InlineParser
///
/// 位置映射关系。
///
- private LocationMap locationMap;
+ private readonly LocationMap locationMap = new();
#pragma warning disable CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑声明为可以为 null。
@@ -86,26 +85,14 @@ internal InlineParser(IReadOnlyDictionary linkDefines,
///
/// 从指定文本解析行级节点。
///
- /// 要解析的文本列表。
+ /// 要解析的块文本。
/// 子节点列表。
- public void Parse(IEnumerable texts, NodeList children)
+ public void Parse(BlockText text, NodeList children)
{
// 将文本拼接成源码流。
- int sumCount = 0;
- foreach (MappedText text in texts)
- {
- sumCount += text.Length;
- }
- ValueList list = sumCount <= ValueList.StackallocCharSizeLimit
- ? new(stackalloc char[sumCount])
- : new(sumCount);
- foreach (MappedText text in texts)
- {
- text.AppendTo(ref list);
- }
- reader = new SourceReader(new StringReader(list.ToString()));
- list.Dispose();
- locationMap = new LocationMap(GetMaps(texts), LocationMapType.Offset);
+ reader = new SourceReader(text.ToStringView());
+ locationMap.Clear();
+ text.GetLocationMap(locationMap);
// 将词法单元重新映射成源码位置。
tokenizer.Load(reader);
tokenizer.SharedContext = this;
@@ -474,41 +461,6 @@ internal bool TryGetLinkDefine(string label, [MaybeNullWhen(false)] out LinkDefi
return reader.ReadBlock(info.StartMark.Index, endIndex - info.StartMark.Index).ToString();
}
- ///
- /// 从指定文本中提取映射信息。
- ///
- /// 文本序列。
- /// 提取的映射信息。
- private static IEnumerable> GetMaps(IEnumerable texts)
- {
- int lastMappedIndex = 0;
- foreach (MappedText text in texts)
- {
- int length = text.Length;
- int curLen = 0;
- bool isFirst = true;
- foreach (Tuple map in text.Maps)
- {
- var (index, mappedIndex) = map;
- curLen += index;
- if (curLen >= length)
- {
- break;
- }
- if (isFirst)
- {
- mappedIndex -= lastMappedIndex;
- isFirst = false;
- }
- yield return new Tuple(index, mappedIndex);
- lastMappedIndex += mappedIndex;
- }
- int restLen = length - curLen;
- yield return new Tuple(restLen, restLen);
- lastMappedIndex += restLen;
- }
- }
-
///
/// 添加字面量节点。
///
diff --git a/Cyjb.Markdown/Syntax/TableAlignList.cs b/Cyjb.Markdown/Syntax/TableAlignList.cs
index 157eeb0..8f4ff43 100644
--- a/Cyjb.Markdown/Syntax/TableAlignList.cs
+++ b/Cyjb.Markdown/Syntax/TableAlignList.cs
@@ -1,4 +1,3 @@
-using System.Xml.Linq;
using Cyjb.Collections.ObjectModel;
namespace Cyjb.Markdown.Syntax;
diff --git a/Cyjb.Markdown/Utils/LinkUtil.cs b/Cyjb.Markdown/Utils/LinkUtil.cs
index 60b9116..40bd4db 100644
--- a/Cyjb.Markdown/Utils/LinkUtil.cs
+++ b/Cyjb.Markdown/Utils/LinkUtil.cs
@@ -1,6 +1,7 @@
using System.Text;
using System.Text.RegularExpressions;
using Cyjb.Collections;
+using Cyjb.Markdown.ParseBlock;
namespace Cyjb.Markdown.Utils;
@@ -28,6 +29,43 @@ public static void CheckLabel(string label)
}
}
+ ///
+ /// 标准化链接标签。
+ ///
+ /// 要标准化的链接标签。
+ /// 标准化后的标签。
+ public static string NormalizeLabel(BlockText label)
+ {
+ using ValueList text = label.Length <= ValueList.StackallocCharSizeLimit
+ ? new ValueList(stackalloc char[label.Length])
+ : new ValueList(label.Length);
+ int count = label.Tokens.Count;
+ bool isWhitespace = true;
+ for (int i = 0; i < count; i++)
+ {
+ ReadOnlySpan span = label.Tokens[i].Text;
+ for (int j = 0; j < span.Length; j++)
+ {
+ char ch = span[j];
+ // 将中间的连续空白合并成一个。
+ if (MarkdownUtil.IsWhitespace(ch))
+ {
+ if (!isWhitespace)
+ {
+ isWhitespace = true;
+ text.Add(' ');
+ }
+ }
+ else
+ {
+ isWhitespace = false;
+ text.Add(UnicodeCaseFolding.GetCaseFolding(ch));
+ }
+ }
+ }
+ return text.ToString();
+ }
+
///
/// 标准化链接标签。
///
diff --git a/Cyjb.Markdown/Utils/MarkdownUtil.Attributes.cs b/Cyjb.Markdown/Utils/MarkdownUtil.Attributes.cs
index 0dceef9..4da13f8 100644
--- a/Cyjb.Markdown/Utils/MarkdownUtil.Attributes.cs
+++ b/Cyjb.Markdown/Utils/MarkdownUtil.Attributes.cs
@@ -1,4 +1,5 @@
using Cyjb.Collections;
+using Cyjb.Markdown.ParseBlock;
using Cyjb.Markdown.Syntax;
namespace Cyjb.Markdown.Utils;
@@ -41,6 +42,37 @@ public static int FindAttributeStart(ReadOnlySpan text)
return -1;
}
+ ///
+ /// 找到属性的起始 { 字符。
+ ///
+ /// 要检查的文本。
+ /// 起始 { 字符的索引;如果未找到则返回 -1;
+ /// 如果找到了 { 字符但不能用作属性起始,则返回 -2。
+ public static int FindAttributeStart(BlockText text)
+ {
+ for (int i = text.Length - 1; i >= 0; i--)
+ {
+ char ch = text[i];
+ if (ch == '{')
+ {
+ // 要求 { 是未转义的。
+ if (text.IsEscaped(i))
+ {
+ return -2;
+ }
+ else
+ {
+ return i;
+ }
+ }
+ else if (ch == '"' || ch == '\'')
+ {
+ for (i--; i >= 0 && text[i] != ch; i--) ;
+ }
+ }
+ return -1;
+ }
+
///
/// 从文本中解析属性。
///
diff --git a/Cyjb.Markdown/Utils/MarkdownUtil.Fence.cs b/Cyjb.Markdown/Utils/MarkdownUtil.Fence.cs
index a0373cd..303a321 100644
--- a/Cyjb.Markdown/Utils/MarkdownUtil.Fence.cs
+++ b/Cyjb.Markdown/Utils/MarkdownUtil.Fence.cs
@@ -9,20 +9,15 @@ internal static partial class MarkdownUtil
///
/// 返回分隔符的长度。
///
- /// 要检查的字符串。
+ /// 要检查的字符串视图。
/// 分隔符的长度。
- public static int GetFenceLength(ReadOnlySpan text)
+ public static int GetFenceLength(StringView text)
{
- char fence = text[0];
+ ReadOnlySpan span = text;
+ char fence = span[0];
// 在词法分析中已确保分隔符长度至少为 2。
int i = 2;
- for (; i < text.Length; i++)
- {
- if (text[i] != fence)
- {
- return i;
- }
- }
+ for (; i < span.Length && span[i] == fence; i++) ;
return i;
}
@@ -37,7 +32,7 @@ public static int GetFenceLength(ReadOnlySpan text)
/// 分隔符的长度。
/// 分隔符的信息。
/// 分隔符的属性。
- public static void ParseFenceStart(BlockParser parser, BlockText line, out int start, out int indent,
+ public static void ParseFenceStart(BlockParser parser, BlockLine line, out int start, out int indent,
out char fenceChar, out int fenceLength,
out string? info, out HtmlAttributeList? attrs)
{
@@ -47,13 +42,12 @@ public static void ParseFenceStart(BlockParser parser, BlockText line, out int s
indent = line.Indent;
line.SkipIndent();
// 解析自定义容器的信息。
- Token token = line.Peek();
+ Token token = line.PeekFront();
fenceChar = token.Text[0];
- fenceLength = GetFenceLength(token.Text.AsSpan());
+ fenceLength = GetFenceLength(token.Text);
if (token.Kind is BlockKind.CodeFenceStart or BlockKind.MathFenceStart or BlockKind.CustomContainerFenceStart)
{
- ReadOnlySpan text = token.Text.AsSpan(fenceLength);
- Trim(ref text);
+ ReadOnlySpan text = token.Text.AsSpan(fenceLength).Trim(MarkdownUtil.Whitespace);
info = text.Unescape();
if (info.Length == 0)
{
diff --git a/Cyjb.Markdown/Utils/MarkdownUtil.Unescape.cs b/Cyjb.Markdown/Utils/MarkdownUtil.Unescape.cs
index 2042956..c05d34f 100644
--- a/Cyjb.Markdown/Utils/MarkdownUtil.Unescape.cs
+++ b/Cyjb.Markdown/Utils/MarkdownUtil.Unescape.cs
@@ -1,5 +1,6 @@
using System.Globalization;
using Cyjb.Collections;
+using Cyjb.Markdown.ParseBlock;
namespace Cyjb.Markdown.Utils;
@@ -65,6 +66,30 @@ public static bool IsEscaped(this ReadOnlySpan text, int idx, int start =
return (slashCount & 1) == 1;
}
+ ///
+ /// 返回指定块文本中,指定索引的字符是否是被转义的。
+ ///
+ /// 要检查的块文本。
+ /// 要检查的字符位置。
+ /// 要检查的起始索引。
+ /// 如果指定索引的字符是被转义的,则返回 true;否则返回 false。
+ public static bool IsEscaped(this BlockText text, int idx, int start = 0)
+ {
+ int slashCount = 0;
+ for (int i = idx - 1; i >= start; i--)
+ {
+ if (text[i] == '\\')
+ {
+ slashCount++;
+ }
+ else
+ {
+ break;
+ }
+ }
+ return (slashCount & 1) == 1;
+ }
+
///
/// 返回指定字符序列中,指定字符首次出现(非转义)的索引。
///
diff --git a/Cyjb.Markdown/Utils/MarkdownUtil.cs b/Cyjb.Markdown/Utils/MarkdownUtil.cs
index ca57ded..889477d 100644
--- a/Cyjb.Markdown/Utils/MarkdownUtil.cs
+++ b/Cyjb.Markdown/Utils/MarkdownUtil.cs
@@ -13,7 +13,12 @@ internal static partial class MarkdownUtil
///
/// Markdown 的空白字符。
///
- public static readonly char[] WhitespaceChars = new char[]{ ' ', '\t', '\r', '\n' };
+ public static readonly char[] WhitespaceChars = new char[] { ' ', '\t', '\r', '\n' };
+
+ ///
+ /// Markdown 的空白字符。
+ ///
+ public static readonly char[] WhitespaceCharsWithoutNewLine = new char[] { ' ', '\t' };
///
/// 返回指定字符是否表示 Markdown 空白。
@@ -106,39 +111,4 @@ public static bool Trim(ref ReadOnlySpan text)
text = text.TrimStart(Whitespace).TrimEnd(Whitespace);
return text.Length < len;
}
-
- ///
- /// 移除指定文本的起始和尾随空白。
- ///
- /// 要移除起始和尾随空白的文本。
- /// 移除了起始和尾随空白后的文本。
- public static string Trim(string text)
- {
- ReadOnlySpan span = text;
- if (Trim(ref span))
- {
- return span.ToString();
- }
- else
- {
- return text;
- }
- }
-
- ///
- /// 获取当前文本是否是空白的(只包含空格、Tab、\r 或 \n)。
- ///
- /// 要检查的文本。
- /// 如果当前文本是空白的,则返回 true;否则返回 false。
- public static bool IsBlank(this ReadOnlySpan text)
- {
- for (int i = 0; i < text.Length; i++)
- {
- if (!IsWhitespace(text[i]))
- {
- return false;
- }
- }
- return true;
- }
}