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) /// 要检查的行。 /// 如果当前行包含任务列表项,根据是否勾选返回 truefalse; /// 如果不包含任务列表项,返回 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; - } }