public override bool Match(InlineProcessor processor, ref StringSlice slice) { var startPosition = slice.Start; // Go to escape character var c = slice.NextChar(); int line; int column; if (c.IsAsciiPunctuation()) { processor.Inline = new LiteralInline() { Content = new StringSlice(slice.Text, slice.Start, slice.Start), Span = { Start = processor.GetSourcePosition(startPosition, out line, out column) }, Line = line, Column = column, IsFirstCharacterEscaped = true, }; processor.Inline.Span.End = processor.Inline.Span.Start + 1; slice.SkipChar(); return(true); } // A backslash at the end of the line is a [hard line break]: if (c == '\n' || c == '\r') { var newLine = c == '\n' ? NewLine.LineFeed : NewLine.CarriageReturn; if (c == '\r' && slice.PeekChar() == '\n') { newLine = NewLine.CarriageReturnLineFeed; } var inline = new LineBreakInline() { IsHard = true, IsBackslash = true, Span = { Start = processor.GetSourcePosition(startPosition, out line, out column) }, Line = line, Column = column, }; processor.Inline = inline; if (processor.TrackTrivia) { inline.NewLine = newLine; } inline.Span.End = inline.Span.Start + 1; slice.SkipChar(); // Skip \n or \r alone if (newLine == NewLine.CarriageReturnLineFeed) { slice.SkipChar(); // Skip \r\n } return(true); } return(false); }
/// <summary> /// Parses a column header equivalent to the regexp: <code>\s*:\s*[delimiterChar]+\s*:\s*</code> /// </summary> /// <param name="slice">The text slice.</param> /// <param name="delimiterChar">The delimiter character (either `-` or `=`). If `\0`, it will detect the character (either `-` or `=`)</param> /// <param name="align">The alignment of the column.</param> /// <returns> /// <c>true</c> if parsing was successfull /// </returns> public static bool ParseColumnHeaderDetect(ref StringSlice slice, ref char delimiterChar, out TableColumnAlign?align) { align = null; slice.TrimStart(); var c = slice.CurrentChar; bool hasLeft = false; bool hasRight = false; if (c == ':') { hasLeft = true; slice.SkipChar(); } slice.TrimStart(); c = slice.CurrentChar; // if we want to automatically detect if (delimiterChar == '\0') { if (c == '=' || c == '-') { delimiterChar = c; } else { return(false); } } // We expect at least one `-` delimiter char if (slice.CountAndSkipChar(delimiterChar) == 0) { return(false); } slice.TrimStart(); c = slice.CurrentChar; if (c == ':') { hasRight = true; slice.SkipChar(); } slice.TrimStart(); align = hasLeft && hasRight ? TableColumnAlign.Center : hasRight ? TableColumnAlign.Right : hasLeft ? TableColumnAlign.Left : (TableColumnAlign?)null; return(true); }
public override bool Match(InlineProcessor processor, ref StringSlice slice) { // A tasklist is either // [ ] // or [x] or [X] if (!(processor.Block !.Parent is ListItemBlock listItemBlock)) { return(false); } var startingPosition = slice.Start; var c = slice.NextChar(); if (!c.IsSpace() && c != 'x' && c != 'X') { return(false); } if (slice.NextChar() != ']') { return(false); } // Skip last ] slice.SkipChar(); // Create the TaskList var taskItem = new TaskList() { Span = { Start = processor.GetSourcePosition(startingPosition, out int line, out int column) },
public override bool Match(InlineProcessor processor, ref StringSlice slice) { // Hard line breaks are for separating inline content within a block. Neither syntax for hard line breaks works at the end of a paragraph or other block element: if (!processor.Block !.IsParagraphBlock) { return(false); } var startPosition = slice.Start; var hasDoubleSpacesBefore = slice.PeekCharExtra(-1).IsSpace() && slice.PeekCharExtra(-2).IsSpace(); var newLine = NewLine.LineFeed; if (processor.TrackTrivia) { if (slice.CurrentChar == '\r') { if (slice.PeekChar() == '\n') { newLine = NewLine.CarriageReturnLineFeed; slice.SkipChar(); // Skip \n } else { newLine = NewLine.CarriageReturn; } } else { newLine = NewLine.LineFeed; } } else { if (slice.CurrentChar == '\r' && slice.PeekChar() == '\n') { slice.SkipChar(); // Skip \n } } slice.SkipChar(); // Skip \r or \n processor.Inline = new LineBreakInline { Span = { Start = processor.GetSourcePosition(startPosition, out int line, out int column) },
public override bool Match(InlineProcessor processor, ref StringSlice slice) { var match = slice.CurrentChar; if (slice.PeekCharExtra(-1) == match) { return(false); } var startPosition = slice.Start; // Match the opened sticks int openSticks = slice.CountAndSkipChar(match); int contentStart = slice.Start; int closeSticks = 0; char c = slice.CurrentChar; var builder = StringBuilderCache.Local(); // A backtick string is a string of one or more backtick characters (`) that is neither preceded nor followed by a backtick. // A code span begins with a backtick string and ends with a backtick string of equal length. // The contents of the code span are the characters between the two backtick strings, normalized in the following ways: // 1. line endings are converted to spaces. // 2. If the resulting string both begins AND ends with a space character, but does not consist entirely // of space characters, a single space character is removed from the front and back. // This allows you to include code that begins or ends with backtick characters, which must be separated by // whitespace from the opening or closing backtick strings. bool allSpace = true; var contentEnd = -1; while (c != '\0') { // Transform '\n' into a single space if (c == '\n') { c = ' '; } else if (c == '\r') { slice.SkipChar(); c = slice.CurrentChar; continue; } if (c == match) { contentEnd = slice.Start; closeSticks = slice.CountAndSkipChar(match); if (openSticks == closeSticks) { break; } allSpace = false; builder.Append(match, closeSticks); c = slice.CurrentChar; } else { builder.Append(c); if (c != ' ') { allSpace = false; } c = slice.NextChar(); } } bool isMatching = false; if (closeSticks == openSticks) { string content; // Remove one space from front and back if the string is not all spaces if (!allSpace && builder.Length > 2 && builder[0] == ' ' && builder[builder.Length - 1] == ' ') { content = builder.ToString(1, builder.Length - 2); } else { content = builder.ToString(); } int delimiterCount = Math.Min(openSticks, closeSticks); var spanStart = processor.GetSourcePosition(startPosition, out int line, out int column); var spanEnd = processor.GetSourcePosition(slice.Start - 1); processor.Inline = new CodeInline(content) { Delimiter = match, ContentWithTrivia = new StringSlice(slice.Text, contentStart, contentEnd - 1), Span = new SourceSpan(spanStart, spanEnd), Line = line, Column = column, DelimiterCount = delimiterCount, }; isMatching = true; } return(isMatching); }
public override bool Match(InlineProcessor processor, ref StringSlice slice) { // Only working on Paragraph block if (!(processor.Block is ParagraphBlock)) { return(false); } var c = slice.CurrentChar; // If we have not a delimiter on the first line of a paragraph, don't bother to continue // tracking other delimiters on following lines var tableState = processor.ParserStates[Index] as TableState; bool isFirstLineEmpty = false; var position = processor.GetSourcePosition(slice.Start, out int globalLineIndex, out int column); var localLineIndex = globalLineIndex - processor.LineIndex; if (tableState is null) { // A table could be preceded by an empty line or a line containing an inline // that has not been added to the stack, so we consider this as a valid // start for a table. Typically, with this, we can have an attributes {...} // starting on the first line of a pipe table, even if the first line // doesn't have a pipe if (processor.Inline != null && (localLineIndex > 0 || c == '\n' || c == '\r')) { return(false); } if (processor.Inline is null) { isFirstLineEmpty = true; } // Else setup a table processor tableState = new TableState(); processor.ParserStates[Index] = tableState; } if (c == '\n' || c == '\r') { if (!isFirstLineEmpty && !tableState.LineHasPipe) { tableState.IsInvalidTable = true; } tableState.LineHasPipe = false; lineBreakParser.Match(processor, ref slice); tableState.LineIndex++; if (!isFirstLineEmpty) { tableState.ColumnAndLineDelimiters.Add(processor.Inline !); tableState.EndOfLines.Add(processor.Inline !); } } else { processor.Inline = new PipeTableDelimiterInline(this) { Span = new SourceSpan(position, position), Line = globalLineIndex, Column = column, LocalLineIndex = localLineIndex }; var deltaLine = localLineIndex - tableState.LineIndex; if (deltaLine > 0) { tableState.IsInvalidTable = true; } tableState.LineHasPipe = true; tableState.LineIndex = localLineIndex; slice.SkipChar(); // Skip the `|` character tableState.ColumnAndLineDelimiters.Add(processor.Inline); } return(true); }
public override bool Match(InlineProcessor processor, ref StringSlice slice) { // The following methods are inspired by the "An algorithm for parsing nested emphasis and links" // at the end of the CommonMark specs. var c = slice.CurrentChar; var startPosition = processor.GetSourcePosition(slice.Start, out int line, out int column); bool isImage = false; if (c == '!') { isImage = true; c = slice.NextChar(); if (c != '[') { return(false); } } string? label; SourceSpan labelWithTriviaSpan = SourceSpan.Empty; switch (c) { case '[': // If this is not an image, we may have a reference link shortcut // so we try to resolve it here var saved = slice; SourceSpan labelSpan; // If the label is followed by either a ( or a [, this is not a shortcut if (processor.TrackTrivia) { if (LinkHelper.TryParseLabelTrivia(ref slice, out label, out labelSpan)) { labelWithTriviaSpan.Start = labelSpan.Start; // skip opening [ labelWithTriviaSpan.End = labelSpan.End; // skip closing ] if (!processor.Document.ContainsLinkReferenceDefinition(label)) { label = null; } } } else { if (LinkHelper.TryParseLabel(ref slice, out label, out labelSpan)) { if (!processor.Document.ContainsLinkReferenceDefinition(label)) { label = null; } } } slice = saved; // Else we insert a LinkDelimiter slice.SkipChar(); var labelWithTrivia = new StringSlice(slice.Text, labelWithTriviaSpan.Start, labelWithTriviaSpan.End); processor.Inline = new LinkDelimiterInline(this) { Type = DelimiterType.Open, Label = label, LabelWithTrivia = labelWithTrivia, LabelSpan = processor.GetSourcePositionFromLocalSpan(labelSpan), IsImage = isImage, Span = new SourceSpan(startPosition, processor.GetSourcePosition(slice.Start - 1)), Line = line, Column = column }; return(true); case ']': slice.SkipChar(); if (processor.Inline != null) { if (TryProcessLinkOrImage(processor, ref slice)) { return(true); } } // If we don’t find one, we return a literal slice node ]. // (Done after by the LiteralInline parser) return(false); } // We don't have an emphasis return(false); }
private bool TryProcessLinkOrImage(InlineProcessor inlineState, ref StringSlice text) { LinkDelimiterInline?openParent = inlineState.Inline !.FirstParentOfType <LinkDelimiterInline>(); if (openParent is null) { return(false); } // If we do find one, but it’s not active, // we remove the inactive delimiter from the stack, // and return a literal text node ]. if (!openParent.IsActive) { inlineState.Inline = new LiteralInline() { Content = new StringSlice("["), Span = openParent.Span, Line = openParent.Line, Column = openParent.Column, }; openParent.ReplaceBy(inlineState.Inline); return(false); } // If we find one and it’s active, // then we parse ahead to see if we have // an inline link/image, reference link/image, // compact reference link/image, // or shortcut reference link/image var parentDelimiter = openParent.Parent; var savedText = text; if (text.CurrentChar == '(') { if (inlineState.TrackTrivia) { if (LinkHelper.TryParseInlineLinkTrivia( ref text, out string?url, out SourceSpan unescapedUrlSpan, out string?title, out SourceSpan unescapedTitleSpan, out char titleEnclosingCharacter, out SourceSpan linkSpan, out SourceSpan titleSpan, out SourceSpan triviaBeforeLink, out SourceSpan triviaAfterLink, out SourceSpan triviaAfterTitle, out bool urlHasPointyBrackets)) { var wsBeforeLink = new StringSlice(text.Text, triviaBeforeLink.Start, triviaBeforeLink.End); var wsAfterLink = new StringSlice(text.Text, triviaAfterLink.Start, triviaAfterLink.End); var wsAfterTitle = new StringSlice(text.Text, triviaAfterTitle.Start, triviaAfterTitle.End); var unescapedUrl = new StringSlice(text.Text, unescapedUrlSpan.Start, unescapedUrlSpan.End); var unescapedTitle = new StringSlice(text.Text, unescapedTitleSpan.Start, unescapedTitleSpan.End); // Inline Link var link = new LinkInline() { TriviaBeforeUrl = wsBeforeLink, Url = HtmlHelper.Unescape(url), UnescapedUrl = unescapedUrl, UrlHasPointyBrackets = urlHasPointyBrackets, TriviaAfterUrl = wsAfterLink, Title = HtmlHelper.Unescape(title), UnescapedTitle = unescapedTitle, TitleEnclosingCharacter = titleEnclosingCharacter, TriviaAfterTitle = wsAfterTitle, IsImage = openParent.IsImage, LabelSpan = openParent.LabelSpan, UrlSpan = inlineState.GetSourcePositionFromLocalSpan(linkSpan), TitleSpan = inlineState.GetSourcePositionFromLocalSpan(titleSpan), Span = new SourceSpan(openParent.Span.Start, inlineState.GetSourcePosition(text.Start - 1)), Line = openParent.Line, Column = openParent.Column, }; openParent.ReplaceBy(link); // Notifies processor as we are creating an inline locally inlineState.Inline = link; // Process emphasis delimiters inlineState.PostProcessInlines(0, link, null, false); // If we have a link (and not an image), // we also set all [ delimiters before the opening delimiter to inactive. // (This will prevent us from getting links within links.) if (!openParent.IsImage) { MarkParentAsInactive(parentDelimiter); } link.IsClosed = true; return(true); } } else { if (LinkHelper.TryParseInlineLink(ref text, out string?url, out string?title, out SourceSpan linkSpan, out SourceSpan titleSpan)) { // Inline Link var link = new LinkInline() { Url = HtmlHelper.Unescape(url), Title = HtmlHelper.Unescape(title), IsImage = openParent.IsImage, LabelSpan = openParent.LabelSpan, UrlSpan = inlineState.GetSourcePositionFromLocalSpan(linkSpan), TitleSpan = inlineState.GetSourcePositionFromLocalSpan(titleSpan), Span = new SourceSpan(openParent.Span.Start, inlineState.GetSourcePosition(text.Start - 1)), Line = openParent.Line, Column = openParent.Column, }; openParent.ReplaceBy(link); // Notifies processor as we are creating an inline locally inlineState.Inline = link; // Process emphasis delimiters inlineState.PostProcessInlines(0, link, null, false); // If we have a link (and not an image), // we also set all [ delimiters before the opening delimiter to inactive. // (This will prevent us from getting links within links.) if (!openParent.IsImage) { MarkParentAsInactive(parentDelimiter); } link.IsClosed = true; return(true); } } text = savedText; } var labelSpan = SourceSpan.Empty; string? label = null; SourceSpan labelWithTrivia = SourceSpan.Empty; bool isLabelSpanLocal = true; bool isShortcut = false; LocalLabel localLabel = LocalLabel.Local; // Handle Collapsed links if (text.CurrentChar == '[') { if (text.PeekChar() == ']') { label = openParent.Label; labelSpan = openParent.LabelSpan; isLabelSpanLocal = false; localLabel = LocalLabel.Empty; text.SkipChar(); // Skip [ text.SkipChar(); // Skip ] } } else { localLabel = LocalLabel.None; label = openParent.Label; isShortcut = true; } if (label != null || LinkHelper.TryParseLabelTrivia(ref text, true, out label, out labelSpan)) { labelWithTrivia = new SourceSpan(labelSpan.Start, labelSpan.End); if (isLabelSpanLocal) { labelSpan = inlineState.GetSourcePositionFromLocalSpan(labelSpan); } if (ProcessLinkReference(inlineState, text, label !, labelWithTrivia, isShortcut, labelSpan, openParent, inlineState.GetSourcePosition(text.Start - 1), localLabel)) { // Remove the open parent openParent.Remove(); if (!openParent.IsImage) { MarkParentAsInactive(parentDelimiter); } return(true); } else if (text.CurrentChar != ']' && text.CurrentChar != '[') { return(false); } }
public override bool Match(InlineProcessor processor, ref StringSlice slice) { // We are matching the following characters: // // ' ‘ ’ ‘ ’ 'left-single-quote', 'right-single-quote' // '' “ ” “ ” 'left-double-quote', 'right-double-quote' // " “ ” “ ” 'left-double-quote', 'right-double-quote' // << >> « » « » 'left-angle-quote', 'right-angle-quote' // ... … … 'ellipsis' // Special case: – and — are handle as a PostProcess step to avoid conflicts with pipetables header separator row // -- – – 'ndash' // --- — — 'mdash' var pc = slice.PeekCharExtra(-1); var c = slice.CurrentChar; var openingChar = c; var startingPosition = slice.Start; // undefined first var type = (SmartyPantType)0; switch (c) { case '\'': type = SmartyPantType.Quote; // We will resolve them at the end of parsing all inlines if (slice.PeekChar() == '\'') { slice.SkipChar(); type = SmartyPantType.DoubleQuote; // We will resolve them at the end of parsing all inlines } break; case '"': type = SmartyPantType.DoubleQuote; break; case '<': if (slice.NextChar() == '<') { type = SmartyPantType.LeftAngleQuote; } break; case '>': if (slice.NextChar() == '>') { type = SmartyPantType.RightAngleQuote; } break; case '.': if (slice.NextChar() == '.' && slice.NextChar() == '.') { type = SmartyPantType.Ellipsis; } break; case '-': if (slice.NextChar() == '-') { var quotePants = GetOrCreateState(processor); quotePants.HasDash = true; return(false); } break; } // If it is not matched, early exit if (type == 0) { return(false); } // Skip char c = slice.NextChar(); CharHelper.CheckOpenCloseDelimiter(pc, c, false, out bool canOpen, out bool canClose); bool postProcess = false; switch (type) { case SmartyPantType.Quote: postProcess = true; if (canOpen && !canClose) { type = SmartyPantType.LeftQuote; } else if (!canOpen && canClose) { type = SmartyPantType.RightQuote; } else { return(false); } break; case SmartyPantType.DoubleQuote: postProcess = true; if (canOpen && !canClose) { type = SmartyPantType.LeftDoubleQuote; } else if (!canOpen && canClose) { type = SmartyPantType.RightDoubleQuote; } else { return(false); } break; case SmartyPantType.LeftAngleQuote: postProcess = true; if (!canOpen || canClose) { return(false); } break; case SmartyPantType.RightAngleQuote: postProcess = true; if (canOpen || !canClose) { return(false); } break; case SmartyPantType.Ellipsis: if (canOpen || !canClose) { return(false); } break; } // Create the SmartyPant inline var pant = new SmartyPant() { Span = { Start = processor.GetSourcePosition(startingPosition, out int line, out int column) },