Exemple #1
0
        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);
        }
Exemple #2
0
        /// <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) },
Exemple #4
0
        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) },
Exemple #5
0
        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);
        }
Exemple #6
0
        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);
        }
Exemple #7
0
        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);
        }
Exemple #8
0
        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);
                }
            }
Exemple #9
0
        public override bool Match(InlineProcessor processor, ref StringSlice slice)
        {
            // We are matching the following characters:
            //
            // '    ‘ ’     &lsquo; &rsquo;     'left-single-quote', 'right-single-quote'
            // ''   “ ”     &ldquo; &rdquo;     'left-double-quote', 'right-double-quote'
            // "    “ ”     &ldquo; &rdquo;     'left-double-quote', 'right-double-quote'
            // << >>    « »     &laquo; &raquo;     'left-angle-quote', 'right-angle-quote'
            // ...  …   &hellip;    'ellipsis'

            // Special case: &ndash; and &mdash; are handle as a PostProcess step to avoid conflicts with pipetables header separator row
            // --   –   &ndash;     'ndash'
            // ---  —   &mdash;     '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) },