All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vladsch.flexmark.parser.internal.InlineParserImpl Maven / Gradle / Ivy

Go to download

Core of flexmark-java (implementation of CommonMark for parsing markdown and rendering to HTML)

There is a newer version: 0.64.8
Show newest version
package com.vladsch.flexmark.parser.internal;

import com.vladsch.flexmark.ast.*;
import com.vladsch.flexmark.ast.util.ReferenceRepository;
import com.vladsch.flexmark.ast.util.TextNodeConverter;
import com.vladsch.flexmark.parser.*;
import com.vladsch.flexmark.parser.block.CharacterNodeFactory;
import com.vladsch.flexmark.parser.block.ParagraphPreProcessor;
import com.vladsch.flexmark.parser.block.ParserState;
import com.vladsch.flexmark.parser.core.delimiter.AsteriskDelimiterProcessor;
import com.vladsch.flexmark.parser.core.delimiter.Bracket;
import com.vladsch.flexmark.parser.core.delimiter.Delimiter;
import com.vladsch.flexmark.parser.core.delimiter.UnderscoreDelimiterProcessor;
import com.vladsch.flexmark.parser.delimiter.DelimiterProcessor;
import com.vladsch.flexmark.util.ast.*;
import com.vladsch.flexmark.util.data.DataHolder;
import com.vladsch.flexmark.util.dependency.DependencyResolver;
import com.vladsch.flexmark.util.misc.CharPredicate;
import com.vladsch.flexmark.util.sequence.BasedSequence;
import com.vladsch.flexmark.util.sequence.Escaping;
import com.vladsch.flexmark.util.sequence.SegmentedSequence;
import com.vladsch.flexmark.util.sequence.SequenceUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;
import java.util.function.Function;
import java.util.regex.Matcher;

public class InlineParserImpl extends LightInlineParserImpl implements InlineParser, ParagraphPreProcessor {
    protected final BitSet originalSpecialCharacters;
    protected final BitSet delimiterCharacters;
    protected final Map delimiterProcessors;
    protected final LinkRefProcessorData linkRefProcessorsData;
    protected List linkRefProcessors = null;
    protected Map> inlineParserExtensions = null;
    protected List inlineParserExtensionFactories = null;
    protected LinkDestinationParser linkDestinationParser = null;

    // used to temporarily override handling of special characters by custom ParagraphPreProcessors
    protected BitSet specialCharacters;
    protected BitSet customCharacters = null;
    protected Map customSpecialCharacterFactoryMap = null;
    protected ArrayList customSpecialCharacterNodes = null;

    /**
     * Link references by ID, needs to be built up using parseReference before calling parse.
     */
    protected ReferenceRepository referenceRepository;

    /**
     * Top delimiter (emphasis, strong emphasis or custom emphasis). (Brackets are on a separate stack, different
     * from the algorithm described in the spec.)
     */
    protected Delimiter lastDelimiter;

    /**
     * Top opening bracket ([ or ![)).
     */
    private Bracket lastBracket;

    public InlineParserImpl(
            DataHolder options,
            BitSet specialCharacters,
            BitSet delimiterCharacters,
            Map delimiterProcessors,
            LinkRefProcessorData linkRefProcessorsData,
            List inlineParserExtensionFactories
    ) {
        super(options);
        this.delimiterProcessors = delimiterProcessors;
        this.linkRefProcessorsData = linkRefProcessorsData;
        this.delimiterCharacters = delimiterCharacters;
        this.originalSpecialCharacters = specialCharacters;
        this.specialCharacters = specialCharacters;
        this.inlineParserExtensionFactories = !inlineParserExtensionFactories.isEmpty() ? inlineParserExtensionFactories : null;

        if (this.options.useHardcodedLinkAddressParser) {
            this.linkDestinationParser = new LinkDestinationParser(this.options.linksAllowMatchedParentheses, this.options.spaceInLinkUrls, this.options.parseJekyllMacrosInUrls, this.options.intellijDummyIdentifier);
        }
    }

    @Override
    public void initializeDocument(@NotNull Document document) {
        this.document = document;
        this.referenceRepository = Parser.REFERENCES.get(document);

        linkRefProcessors = new ArrayList<>(linkRefProcessorsData.processors.size());
        for (LinkRefProcessorFactory factory : linkRefProcessorsData.processors) {
            linkRefProcessors.add(factory.apply(document));
        }

        // create custom processors
        if (inlineParserExtensionFactories != null) {
            Map> extensions = calculateInlineParserExtensions(document, inlineParserExtensionFactories);
            inlineParserExtensions = new HashMap<>(extensions.size());
            Map parserExtensionMap = new HashMap<>();
            for (Map.Entry> entry : extensions.entrySet()) {
                List extensionList = new ArrayList<>(entry.getValue().size());
                for (InlineParserExtensionFactory factory : entry.getValue()) {
                    InlineParserExtension parserExtension = parserExtensionMap.get(factory);
                    if (parserExtension == null) {
                        parserExtension = factory.apply(this);
                        parserExtensionMap.put(factory, parserExtension);
                    }
                    extensionList.add(parserExtension);
                }

                inlineParserExtensions.put(entry.getKey(), extensionList);

                // set it as special character
                specialCharacters.set(entry.getKey());
            }
        }
    }

    @Override
    public void finalizeDocument(@NotNull Document document) {
        assert this.referenceRepository == Parser.REFERENCES.get(document);

        if (inlineParserExtensions != null) {
            for (List extensionList : inlineParserExtensions.values()) {
                for (InlineParserExtension extension : extensionList) {
                    extension.finalizeDocument(this);
                }
            }
        }
    }

    @Override
    public Delimiter getLastDelimiter() {
        return lastDelimiter;
    }

    @Override
    public Bracket getLastBracket() {
        return lastBracket;
    }

    @Nullable
    @Override
    public List parseCustom(@NotNull BasedSequence input, @NotNull Node node, @NotNull BitSet customCharacters, @NotNull Map nodeFactoryMap) {
        this.customCharacters = customCharacters;
        this.specialCharacters.or(customCharacters);
        this.customSpecialCharacterFactoryMap = nodeFactoryMap;
        this.customSpecialCharacterNodes = null;
        parse(input, node);
        this.specialCharacters = this.originalSpecialCharacters;
        this.customSpecialCharacterFactoryMap = null;
        this.customCharacters = null;
        return this.customSpecialCharacterNodes;
    }

    /**
     * Parse content in block into inline children, using reference map to resolve references.
     */
    @Override
    public void parse(@NotNull BasedSequence content, @NotNull Node block) {
        this.block = block;
        this.input = content.trim();
        this.index = 0;
        this.lastDelimiter = null;
        this.lastBracket = null;

        boolean customOnly = (block instanceof DoNotDecorate);// || block.getAncestorOfType(DoNotDecorate.class) != null;

        boolean moreToParse;
        do {
            moreToParse = parseInline(customOnly);
        } while (moreToParse);

        processDelimiters(null);
        flushTextNode();

        if (!customOnly) {
            if (inlineParserExtensions != null) {
                for (List extensionList : inlineParserExtensions.values()) {
                    for (InlineParserExtension extension : extensionList) {
                        extension.finalizeBlock(this);
                    }
                }
            }
        }

        // merge nodes if needed
        mergeTextNodes(block.getFirstChild(), block.getLastChild());
    }

    @Override
    public void mergeTextNodes(@Nullable Node fromNode, @Nullable Node toNode) {
        Text first = null;
        Text last = null;

        Node node = fromNode;
        while (node != null) {
            if (node instanceof Text) {
                Text text = (Text) node;
                if (first == null) {
                    first = text;
                }
                last = text;
            } else {
                mergeIfNeeded(first, last);
                first = null;
                last = null;
            }

            if (node == toNode) {
                break;
            }

            node = node.getNext();
        }

        mergeIfNeeded(first, last);
    }

    @Override
    public void mergeIfNeeded(Text first, Text last) {
        if (first != null && last != null && first != last) {
            ArrayList sb = new ArrayList<>();
            sb.add(first.getChars());
            Node node = first.getNext();
            Node stop = last.getNext();
            while (node != stop) {
                sb.add(node.getChars());
                Node unlink = node;
                node = node.getNext();
                unlink.unlink();
            }
            BasedSequence literal = SegmentedSequence.create(first.getChars(), sb);
            first.setChars(literal);
        }
    }

    /*
     *  ParagraphPreProcessor implementation
     */
    @Override
    public int preProcessBlock(Paragraph block, ParserState state) {
        BasedSequence contentChars = block.getChars();

        // try parsing the beginning as link reference definitions:
        int leadingSpaces = contentChars.countLeading(CharPredicate.SPACE_TAB);
        int length = contentChars.length();

        while (leadingSpaces <= 3 && length > 3 + leadingSpaces && contentChars.charAt(leadingSpaces) == '[') {
            if (leadingSpaces > 0) {
                contentChars = contentChars.subSequence(leadingSpaces, length);
                length -= leadingSpaces;
            }

            int pos = parseReference(block, contentChars);
            if (pos == 0) break;
            contentChars = contentChars.subSequence(pos, length);
            length = contentChars.length();
            leadingSpaces = contentChars.countLeading(CharPredicate.SPACE_TAB);
        }

        return contentChars.getStartOffset() - block.getStartOffset();
    }

    /**
     * Attempt to parse a reference definition, modifying the internal reference map.
     *
     * @param block the block whose text is being parsed for references
     * @param s     sequence of the blocks characters
     * @return number of characters were parsed as a reference from the start of the sequence, {@code 0} if none
     */
    protected int parseReference(Block block, BasedSequence s) {
        this.input = s;
        this.index = 0;
        BasedSequence dest;
        BasedSequence title;
        int matchChars;
        int startIndex = index;

        // label:
        matchChars = parseLinkLabel();
        if (matchChars == 0) {
            return 0;
        }

        // colon:
        if (peek() != ':') {
            return 0;
        }

        BasedSequence rawLabel = input.subSequence(0, matchChars + 1);
        index++;

        // link url
        spnl();

        dest = parseLinkDestination();

        if (dest == null || dest.length() == 0) {
            return 0;
        }

        int beforeTitle = index;
        spnl();
        title = parseLinkTitle();
        if (title == null) {
            // rewind before spaces
            index = beforeTitle;
        }

        boolean atLineEnd = true;
        if (index != input.length() && match(myParsing.LINE_END) == null) {
            if (title == null) {
                atLineEnd = false;
            } else {
                // the potential title we found is not at the line end,
                // but it could still be a legal link reference if we
                // discard the title
                title = null;
                // rewind before spaces
                index = beforeTitle;
                // and instead check if the link URL is at the line end
                atLineEnd = match(myParsing.LINE_END) != null;
            }
        }

        if (!atLineEnd) {
            return 0;
        }

        String normalizedLabel = Escaping.normalizeReferenceChars(rawLabel, true);
        if (normalizedLabel.isEmpty()) {
            return 0;
        }

        Reference reference = new Reference(rawLabel, dest, title);

        // NOTE: whether first or last reference is kept is defined by the repository modify behavior setting
        // for CommonMark this is set in the initializeDocument() function of the inline parser
        referenceRepository.put(normalizedLabel, reference);

        block.insertBefore(reference);

        return index - startIndex;
    }

    /**
     * Parse the next inline element in subject, advancing input index.
     * On success, add the result to block's children and return true.
     * On failure, return false.
     *
     * @return false on failure true on success
     */
    protected boolean parseInline() {
        return parseInline(false);
    }

    protected boolean parseInline(boolean customOnly) {
        boolean res = false;

        char c = peek();
        if (c == SequenceUtils.NUL) {
            return false;
        }

        if (!customOnly && inlineParserExtensions != null) {
            List extensions = inlineParserExtensions.get(c);
            if (extensions != null) {
                for (InlineParserExtension extension : extensions) {
                    res = extension.parse(this);
                    if (res) return true;
                }
            }
        }

        if (customCharacters != null && customCharacters.get(c)) {
            res = processCustomCharacters();
            if (!res) {
                index++;
                // When we get here, it's only for a single special character that turned out to not have a special meaning.
                // So we shouldn't have a single surrogate here, hence it should be ok to turn it into a String.
                appendText(input.subSequence(index - 1, index));
            } else {
                // Issue: #376, need to clear any delimiter stack since whatever is not used is not a delimiter
                processDelimiters(null);
                this.lastDelimiter = null;
            }

            return true;
        }

        switch (c) {
            case '\r':
            case '\n':
                res = parseNewline();
                break;
            case '\\':
                res = parseBackslash();
                break;
            case '`':
                res = parseBackticks();
                break;
            case '[':
                res = parseOpenBracket();
                break;
            case '!':
                res = parseBang();
                break;
            case ']':
                res = parseCloseBracket();
                break;
            case '<': {
                // first we check custom special characters for < delimiters and only allow 2 consecutive ones to allow anchor links and HTML processing
                boolean isDelimiter = delimiterCharacters.get(c);
                if (isDelimiter && peek(1) == '<') {
                    DelimiterProcessor delimiterProcessor = delimiterProcessors.get(c);
                    res = parseDelimiters(delimiterProcessor, c);
                } else {
                    res = parseAutolink() || parseHtmlInline();
                }
            }
            break;
            case '&':
                res = parseEntity();
                break;
            default: {
                // first we check custom special characters
                boolean isDelimiter = delimiterCharacters.get(c);
                if (isDelimiter) {
                    DelimiterProcessor delimiterProcessor = delimiterProcessors.get(c);
                    res = parseDelimiters(delimiterProcessor, c);
                } else {
                    res = parseString();
                }
            }
            break;
        }

        if (!res) {
            index++;
            // When we get here, it's only for a single special character that turned out to not have a special meaning.
            // So we shouldn't have a single surrogate here, hence it should be ok to turn it into a String.
            appendText(input.subSequence(index - 1, index));
        }

        return true;
    }

    private boolean processCustomCharacters() {
        char c = peek();
        CharacterNodeFactory factory = customSpecialCharacterFactoryMap.get(c);
        if (factory == null) return false;

        Node node = factory.get();
        node.setChars(input.subSequence(index, index + 1));

        if (currentText != null) {
            BasedSequence prevText = SegmentedSequence.create(node.getChars(), currentText);
            currentText = null;

            // see if need to trim some off the end
            int pos = prevText.length();
            BasedSequence skipped = null;
            while (pos > 0 && factory.skipPrev(prevText.charAt(pos - 1))) pos--;
            if (pos < prevText.length()) {
                skipped = prevText.subSequence(pos);
                prevText = prevText.subSequence(0, pos);
            }

            block.appendChild(new Text(prevText));
            if (skipped != null && factory.wantSkippedWhitespace()) {
                block.appendChild(new WhiteSpace(skipped));
            }
        }

        appendNode(node);
        if (customSpecialCharacterNodes == null) customSpecialCharacterNodes = new ArrayList<>();
        customSpecialCharacterNodes.add(node);

        int pos = index + 1;
        do {
            index++;
            c = peek();
        }
        while (c != SequenceUtils.NUL && factory.skipNext(c));

        if (pos < index && factory.wantSkippedWhitespace()) {
            block.appendChild(new WhiteSpace(input.subSequence(pos, index)));
        }

        return true;
    }

    /**
     * Parse a newline. If it was preceded by two spaces, append a hard line break; otherwise a soft line break.
     *
     * @return true
     */
    @Override
    public boolean parseNewline() {
        boolean crLf = index < input.length() - 1 && input.charAt(index + 1) == '\n';
        int crLfDelta = crLf ? 1 : 0;
        index += 1 + crLfDelta;

        // We're gonna add a new node in any case and we need to check the last text node, so flush outstanding text.
        flushTextNode();

        Node lastChild = block.getLastChild();
        // Check previous text for trailing spaces.
        // The "endsWith" is an optimization to avoid an RE match in the common case.
        if (lastChild instanceof Text && (lastChild.getChars().endsWith(" "))) {
            Text text = (Text) lastChild;
            BasedSequence literal = text.getChars();
            Matcher matcher = myParsing.FINAL_SPACE.matcher(literal);
            int spaces = matcher.find() ? matcher.end() - matcher.start() : 0;
            appendNode(spaces >= 2 ? new HardLineBreak(input.subSequence(index - (options.hardLineBreakLimit ? 3 + crLfDelta : spaces + 1 + crLfDelta), index)) : new SoftLineBreak(input.subSequence(index - 1 - crLfDelta, index)));
            if (spaces > 0) {
                if (literal.length() > spaces) {
                    lastChild.setChars(literal.subSequence(0, literal.length() - spaces).trimEnd());
                } else {
                    lastChild.unlink();
                }
            }
        } else {
            appendNode(new SoftLineBreak(input.subSequence(index - 1 - crLfDelta, index)));
        }

        // gobble leading spaces in next line
        while (peek() == ' ') {
            index++;
        }
        return true;
    }

    /**
     * Parse a backslash-escaped special character, adding either the escaped  character, a hard line break
     * (if the backslash is followed by a newline), or a literal backslash to the block's children.
     *
     * @return true
     */
    protected boolean parseBackslash() {
        index++;
        if (peek() == '\n' || peek() == '\r') {
            int charsMatched = peek(1) == '\n' ? 2 : 1;
            appendNode(new HardLineBreak(input.subSequence(index - 1, index + charsMatched)));
            index += charsMatched;
        } else if (index < input.length() && myParsing.ESCAPABLE.matcher(input.subSequence(index, index + 1)).matches()) {
            appendText(input, index - 1, index + 1);
            index++;
        } else {
            appendText(input.subSequence(index - 1, index));
        }
        return true;
    }

    /**
     * Attempt to parse backticks, adding either a backtick code span or a literal sequence of backticks.
     *
     * @return true if matched backticks, false otherwise
     */
    protected boolean parseBackticks() {
        BasedSequence ticks = match(myParsing.TICKS_HERE);
        if (ticks == null) {
            return false;
        }
        int afterOpenTicks = index;
        BasedSequence matched;
        while ((matched = match(myParsing.TICKS)) != null) {
            if (matched.equals(ticks)) {
                int ticksLength = ticks.length();
                BasedSequence codeText = input.subSequence(afterOpenTicks, index - ticksLength);
                Code node = new Code(input.subSequence(afterOpenTicks - ticksLength, afterOpenTicks), codeText, input.subSequence(index - ticksLength, index));

                if (options.codeSoftLineBreaks) {
                    // add softbreaks to code ast
                    int length = codeText.length();
                    int lastPos = 0;
                    while (lastPos < length) {
                        int softBreak = codeText.indexOfAny(CharPredicate.ANY_EOL, lastPos);
                        int pos = softBreak == -1 ? length : softBreak;

                        Text textNode = new Text(codeText.subSequence(lastPos, pos));
                        node.appendChild(textNode);

                        lastPos = pos;
                        if (lastPos >= length) break;
                        if (codeText.charAt(lastPos) == '\r') {
                            lastPos++;
                            if (lastPos >= length) break;
                            if (codeText.charAt(lastPos) == '\n') lastPos++;
                        } else {
                            lastPos++;
                        }

                        if (lastPos >= length) break;

                        if (pos < lastPos) {
                            SoftLineBreak softLineBreak = new SoftLineBreak(codeText.subSequence(softBreak, lastPos));
                            node.appendChild(softLineBreak);
                        }
                    }
                } else {
                    Text textNode = new Text(codeText);
                    node.appendChild(textNode);
                }

                appendNode(node);
                return true;
            }
        }

        // If we got here, we didn't match a closing backtick sequence.
        index = afterOpenTicks;
        appendText(ticks);
        return true;
    }

    private static class DelimiterData {
        final int count;
        final boolean canClose;
        final boolean canOpen;

        DelimiterData(int count, boolean canOpen, boolean canClose) {
            this.count = count;
            this.canOpen = canOpen;
            this.canClose = canClose;
        }
    }

    /**
     * Attempt to parse delimiters like emphasis, strong emphasis or custom delimiters.
     *
     * @param delimiterProcessor delimiter processor instance
     * @param delimiterChar      delimiter character being processed
     * @return true if processed characters false otherwise
     */
    protected boolean parseDelimiters(DelimiterProcessor delimiterProcessor, char delimiterChar) {
        DelimiterData res = scanDelimiters(delimiterProcessor, delimiterChar);
        if (res == null) {
            return false;
        }
        int numDelims = res.count;
        int startIndex = index;

        index += numDelims;
        Text node = appendSeparateText(input.subSequence(startIndex, index));

        // Add entry to stack for this opener
        this.lastDelimiter = new Delimiter(input, node, delimiterChar, res.canOpen, res.canClose, this.lastDelimiter, startIndex);
        this.lastDelimiter.setNumDelims(numDelims);
        if (this.lastDelimiter.getPrevious() != null) {
            this.lastDelimiter.getPrevious().setNext(this.lastDelimiter);
        }
        return true;
    }

    /**
     * Add open bracket to delimiter stack and add a text node to block's children.
     *
     * @return true
     */
    protected boolean parseOpenBracket() {
        int startIndex = index;
        index++;

        Text node = appendSeparateText(input.subSequence(index - 1, index));

        // Add entry to stack for this opener
        addBracket(Bracket.link(input, node, startIndex, lastBracket, lastDelimiter));
        return true;
    }

    /**
     * If next character is [, and ! delimiter to delimiter stack and add a text node to block's children.
     * Otherwise just add a text node.
     *
     * @return true if processed characters false otherwise
     */
    protected boolean parseBang() {
        int startIndex = index;
        index++;
        if (peek() == '[') {
            index++;

            Text node = appendSeparateText(input.subSequence(index - 2, index));

            // Add entry to stack for this opener
            addBracket(Bracket.image(input, node, startIndex + 1, lastBracket, lastDelimiter));
        } else {
            appendText(input.subSequence(index - 1, index));
        }
        return true;
    }

    private void addBracket(Bracket bracket) {
        if (lastBracket != null) {
            lastBracket.setBracketAfter(true);
        }
        lastBracket = bracket;
    }

    private void removeLastBracket() {
        lastBracket = lastBracket.getPrevious();
    }

    static class ReferenceProcessorMatch {
        final public LinkRefProcessor processor;
        final public BasedSequence nodeChars;
        final public boolean wantExclamation;

        public ReferenceProcessorMatch(LinkRefProcessor processor, boolean wantExclamation, BasedSequence nodeChars) {
            this.processor = processor;
            this.nodeChars = nodeChars;
            this.wantExclamation = wantExclamation;
        }
    }

    private ReferenceProcessorMatch matchLinkRef(Bracket opener, int startIndex, int lookAhead, int nesting) {
        if (linkRefProcessorsData.nestingIndex.length == 0) return null;

        ReferenceProcessorMatch match = null;
        BasedSequence textNoBang = null;
        BasedSequence textWithBang = null;
        boolean wantBang;

        int iMax = linkRefProcessorsData.processors.size();
        int startProc = linkRefProcessorsData.nestingIndex[lookAhead + nesting];
        for (int i = startProc; i < iMax; i++) {
            LinkRefProcessor linkProcessor = linkRefProcessors.get(i);
            BasedSequence nodeChars;

            if (lookAhead + nesting < linkProcessor.getBracketNestingLevel()) break;

            wantBang = linkProcessor.getWantExclamationPrefix();

            // preview the link ref
            if (opener.isImage() && wantBang) {
                // this one has index off by one for the leading !
                if (textWithBang == null) textWithBang = input.subSequence(opener.getStartIndex() - 1 - lookAhead, startIndex + lookAhead);
                nodeChars = textWithBang;
            } else {
                if (wantBang && opener.getStartIndex() >= lookAhead + 1 && input.charAt(opener.getStartIndex() - 1 - lookAhead) == '!') {
                    if (textWithBang == null) textWithBang = input.subSequence(opener.getStartIndex() - 1 - lookAhead, startIndex + lookAhead);
                    nodeChars = textWithBang;
                } else {
                    if (textNoBang == null) textNoBang = input.subSequence(opener.getStartIndex() - lookAhead, startIndex + lookAhead);
                    nodeChars = textNoBang;
                }
            }

            if (linkProcessor.isMatch(nodeChars)) {
                match = new ReferenceProcessorMatch(linkProcessor, wantBang, nodeChars);
                break;
            }
        }
        return match;
    }

    /**
     * Try to match close bracket against an opening in the delimiter stack. Add either a link or image, or a
     * plain [ character, to block's children. If there is a matching delimiter, removeIndex it from the delimiter stack.
     * 

* Also handles custom link ref processing * * @return true */ protected boolean parseCloseBracket() { index++; int startIndex = index; int nestedBrackets; // look through stack of delimiters for a [ or ![ Bracket opener = this.lastBracket; if (opener == null) { // No matching opener, just return a literal. appendText(input.subSequence(index - 1, index)); return true; } if (!opener.isAllowed()) { // Matching opener but it's not allowed, just return a literal. appendText(input.subSequence(index - 1, index)); removeLastBracket(); return true; } nestedBrackets = 0; // Check to see if we have a link/image BasedSequence dest = null; BasedSequence title = null; BasedSequence ref = null; boolean isLinkOrImage = false; boolean refIsBare = false; ReferenceProcessorMatch linkRefProcessorMatch = null; boolean refIsDefined = false; BasedSequence linkOpener = BasedSequence.NULL; BasedSequence linkCloser = BasedSequence.NULL; BasedSequence bareRef = BasedSequence.NULL; BasedSequence imageUrlContent = null; // Inline link? int preSpaceIndex = index; // May need to skip spaces if (options.spaceInLinkElements && peek() == ' ') { sp(); } if (peek() == '(') { int savedIndex = index; linkOpener = input.subSequence(index, index + 1); index++; spnl(); if ((dest = parseLinkDestination()) != null) { if (options.parseMultiLineImageUrls && opener.isImage() && !dest.startsWith("<") && dest.endsWith("?") && spnlUrl()) { // possible multi-line image url int contentStart = index; int contentEnd = contentStart; BasedSequence multiLineTitle; while (true) { sp(); multiLineTitle = parseLinkTitle(); if (multiLineTitle != null) sp(); if (peek() == ')') { linkCloser = input.subSequence(index, index + 1); index++; imageUrlContent = input.subSequence(contentStart, contentEnd); title = multiLineTitle; isLinkOrImage = true; break; } BasedSequence restOfLine = toEOL(); if (restOfLine == null) break; contentEnd = index; } } else { spnl(); // title needs a whitespace before if (myParsing.WHITESPACE.matcher(input.subSequence(index - 1, index)).matches()) { title = parseLinkTitle(); spnl(); } // test for spaces in url making it invalid, otherwise anything else goes if (peek() == ')') { linkCloser = input.subSequence(index, index + 1); index++; isLinkOrImage = true; } else { // back out, no match index = savedIndex; } } } else { index = savedIndex; } } else { index = preSpaceIndex; } if (!isLinkOrImage) { // maybe reference link, need to see if it matches a custom processor or need to skip this reference because it will be processed on the next char // as something else, like a wiki link if (!options.matchLookaheadFirst) { linkRefProcessorMatch = matchLinkRef(opener, startIndex, 0, nestedBrackets); } if (linkRefProcessorMatch != null) { // have a match, then no look ahead for next matches } else { // need to figure out max nesting we should test based on what is max processor desire and max available // nested inner ones are always only [] int maxWanted = linkRefProcessorsData.maxNesting; int maxAvail = 0; if (maxWanted > nestedBrackets) { // need to see what is available Bracket nested = opener; while (nested.getPrevious() != null && nested.getStartIndex() == nested.getPrevious().getStartIndex() + 1 && peek(maxAvail) == ']') { nested = nested.getPrevious(); maxAvail++; if (maxAvail + nestedBrackets == maxWanted || nested.isImage()) break; } } for (int nesting = maxAvail + 1; nesting-- > 0; ) { linkRefProcessorMatch = matchLinkRef(opener, startIndex, nesting, nestedBrackets); if (linkRefProcessorMatch != null) { if (nesting > 0) { while (nesting-- > 0) { index++; lastBracket.getNode().unlink(); removeLastBracket(); } opener = lastBracket; } break; } } } if (linkRefProcessorMatch == null) { // See if there's a link label int beforeLabel = index; int labelLength = parseLinkLabel(); if (labelLength > 2) { ref = input.subSequence(beforeLabel, beforeLabel + labelLength); } else if (!opener.isBracketAfter()) { // Empty or missing second label can only be a reference if there's no unescaped bracket in it. bareRef = input.subSequence(beforeLabel, beforeLabel + labelLength); if (opener.isImage()) { // this one has index off by one for the leading ! ref = input.subSequence(opener.getStartIndex() - 1, startIndex); } else { ref = input.subSequence(opener.getStartIndex(), startIndex); } refIsBare = true; } if (ref != null) { String normalizedLabel = Escaping.normalizeReferenceChars(ref, true); if (referenceRepository.containsKey(normalizedLabel)) { BasedSequence sequence = input.subSequence(opener.getStartIndex(), startIndex); boolean containsLinks = containsLinkRefs(refIsBare ? ref : sequence, opener.getNode().getNext(), false); isLinkOrImage = !containsLinks; refIsDefined = true; } else { // need to test if we are cutting in the middle of some other delimiters matching, if we are not then we will make this into a tentative if (!opener.isStraddling(ref)) { // link ref, otherwise we will break // it is the innermost ref and is bare, if not bare then we treat it as a ref if (!refIsBare && peek() == '[') { int nextLength = parseLinkLabel(); if (nextLength > 0) { // not bare and not defined and followed by another [], roll back to before the label and make it just text index = beforeLabel; } else { // undefined ref, create a tentative one but only if does not contain any other link refs boolean containsLinks = containsLinkRefs(ref, opener.getNode().getNext(), null); if (!containsLinks) { refIsBare = true; isLinkOrImage = true; } } } else { // undefined ref, bare or followed by empty [], create a tentative link ref but only if does not contain any other link refs boolean containsLinks = containsLinkRefs(ref, opener.getNode().getNext(), null); if (!containsLinks) { isLinkOrImage = true; } } } } } } } if (isLinkOrImage || linkRefProcessorMatch != null) { // If we got here, open is a potential opener // Flush text now. We don't need to worry about combining it with adjacent text nodes, as we'll wrap it in a // link or image node. flushTextNode(); Node insertNode; boolean isImage = opener.isImage(); if (linkRefProcessorMatch != null) { if (!linkRefProcessorMatch.wantExclamation && isImage) { appendText(input.subSequence(opener.getStartIndex() - 1, opener.getStartIndex())); opener.getNode().setChars(opener.getNode().getChars().subSequence(1)); //opener.image = false; isImage = false; } insertNode = linkRefProcessorMatch.processor.createNode(linkRefProcessorMatch.nodeChars); } else { insertNode = ref != null ? isImage ? new ImageRef() : new LinkRef() : isImage ? new Image() : new Link(); } { Node node = opener.getNode().getNext(); while (node != null) { Node next = node.getNext(); insertNode.appendChild(node); node = next; } } if (linkRefProcessorMatch != null) { // may need to adjust children's text because some characters were part of the processor's opener/closer if (insertNode.hasChildren()) { BasedSequence original = insertNode.getChildChars(); BasedSequence text = linkRefProcessorMatch.processor.adjustInlineText(document, insertNode); // may need to remove some delimiters if they span across original and changed text boundary or if now they are outside text boundary Delimiter delimiter = lastDelimiter; while (delimiter != null) { Delimiter prevDelimiter = delimiter.getPrevious(); BasedSequence delimiterChars = delimiter.getInput().subSequence(delimiter.getStartIndex(), delimiter.getEndIndex()); if (original.containsAllOf(delimiterChars)) { if (!text.containsAllOf(delimiterChars) || !linkRefProcessorMatch.processor.allowDelimiters(delimiterChars, document, insertNode)) { // remove it removeDelimiterKeepNode(delimiter); } } delimiter = prevDelimiter; } if (!text.containsAllOf(original)) { // now need to truncate child text for (Node node : insertNode.getChildren()) { BasedSequence nodeChars = node.getChars(); if (text.containsSomeOf(nodeChars)) { if (!text.containsAllOf(nodeChars)) { // truncate the contents to intersection of node's chars and adjusted chars BasedSequence chars = text.intersect(nodeChars); node.setChars(chars); } } else { // remove the node node.unlink(); } } } } } appendNode(insertNode); if (insertNode instanceof RefNode) { // set up the parts RefNode refNode = (RefNode) insertNode; refNode.setReferenceChars(ref); if (refIsDefined) refNode.setDefined(true); if (!refIsBare) { refNode.setTextChars(input.subSequence(isImage ? opener.getStartIndex() - 1 : opener.getStartIndex(), startIndex)); } else if (!bareRef.isEmpty()) { refNode.setTextOpeningMarker(bareRef.subSequence(0, 1)); refNode.setTextClosingMarker(bareRef.endSequence(1)); } insertNode.setCharsFromContent(); } else if (insertNode instanceof InlineLinkNode) { // set url and title InlineLinkNode inlineLinkNode = (InlineLinkNode) insertNode; inlineLinkNode.setUrlChars(dest); inlineLinkNode.setTitleChars(title); inlineLinkNode.setLinkOpeningMarker(linkOpener); inlineLinkNode.setLinkClosingMarker(linkCloser); inlineLinkNode.setTextChars(isImage ? input.subSequence(opener.getStartIndex() - 1, startIndex) : input.subSequence(opener.getStartIndex(), startIndex)); if (imageUrlContent != null) { ((Image) insertNode).setUrlContent(imageUrlContent); } insertNode.setCharsFromContent(); } // Process delimiters such as emphasis inside link/image processDelimiters(opener.getPreviousDelimiter()); Node toRemove = opener.getNode(); removeLastBracket(); if (linkRefProcessorMatch != null) { linkRefProcessorMatch.processor.updateNodeElements(document, insertNode); } // Links within links are not allowed. We found this link, so there can be no other link around it. if (insertNode instanceof Link) { Bracket bracket = this.lastBracket; while (bracket != null) { if (!bracket.isImage()) { // Disallow link opener. It will still get matched, but will not result in a link. bracket.setAllowed(false); } bracket = bracket.getPrevious(); } if (options.linkTextPriorityOverLinkRef || !containsLinkRefs(insertNode, false)) { // collapse any link refs contained in this link, they are duds, link takes precedence collapseLinkRefChildren(insertNode, child -> child instanceof LinkRendered || child.isTentative(), true); toRemove.unlink(); } else { // if contains link ref then treat this as plain text // leave opener, add children as parent nodes and append closer insertNode.unlink(); block.takeChildren(insertNode); appendText(insertNode.baseSubSequence(((Link) insertNode).getTextClosingMarker().getStartOffset(), insertNode.getEndOffset())); } } else if (insertNode instanceof RefNode) { // have a link ref, collapse to text any tentative ones contained in it, they are duds collapseLinkRefChildren(insertNode, child -> child instanceof LinkRendered || child.isTentative(), true); toRemove.unlink(); } else { toRemove.unlink(); } return true; } else { // no link or image index = startIndex; appendText(input.subSequence(index - 1, index)); removeLastBracket(); return true; } } protected static boolean containsLinkRefs(Node node, Boolean isTentative) { Node next = node.getFirstChild(); while (next != null) { if (next instanceof LinkRendered && (isTentative == null || ((LinkRendered) next).isTentative() == isTentative)) { return true; } next = next.getNext(); } return false; } protected static boolean containsLinkRefs(BasedSequence nodeChars, Node next, Boolean isTentative) { int startOffset = nodeChars.getStartOffset(); int endOffset = nodeChars.getEndOffset(); while (next != null) { if (next instanceof LinkRefDerived && (isTentative == null || ((LinkRefDerived) next).isTentative() == isTentative) && !(next.getStartOffset() >= endOffset || next.getEndOffset() <= startOffset)) { return true; } next = next.getNext(); } return false; } protected static void collapseLinkRefChildren(Node node, Function isTentative, boolean trimFirstLastChild) { Node child = node.getFirstChild(); boolean hadCollapse = false; while (child != null) { Node nextChild = child.getNext(); if (child instanceof LinkRefDerived && (isTentative == null || isTentative.apply((LinkRefDerived) child))) { // need to collapse this one, moving its text contents to text collapseLinkRefChildren(child, isTentative, false); child.unlink(); TextNodeConverter list = new TextNodeConverter(child.getChars()); list.addChildrenOf(child); if (nextChild != null) { list.insertMergedBefore(nextChild); } else { list.appendMergedTo(node); } hadCollapse = true; } child = nextChild; } if (hadCollapse) { TextNodeConverter.mergeTextNodes(node); } if (trimFirstLastChild) { // trim first and last child text Node firstChild = node.getFirstChild(); Node lastChild = node.getLastChild(); if (firstChild == lastChild) { if (firstChild != null && !(firstChild instanceof DoNotTrim)) firstChild.setChars(firstChild.getChars().trim()); } else { if (firstChild != null && !(firstChild instanceof DoNotTrim)) firstChild.setChars(firstChild.getChars().trimStart()); if (lastChild != null && !(lastChild instanceof DoNotTrim)) lastChild.setChars(lastChild.getChars().trimEnd()); } } } /** * Attempt to parse link destination, * * @return the string or null if no match. */ @Override public BasedSequence parseLinkDestination() { BasedSequence res = match(myParsing.LINK_DESTINATION_ANGLES); if (res != null) { return res; } else { if (linkDestinationParser != null) { BasedSequence match = linkDestinationParser.parseLinkDestination(input, index); index += match.length(); return match; } else { boolean spaceInUrls = options.spaceInLinkUrls; if (options.linksAllowMatchedParentheses) { // allow matched parenthesis // fix for issue of stack overflow when parsing long input lines, by implementing non-recursive scan BasedSequence matched = match(myParsing.LINK_DESTINATION_MATCHED_PARENS); if (matched != null) { int openCount = 0; int iMax = matched.length(); for (int i = 0; i < iMax; i++) { char c = matched.charAt(i); if (c == '\\') { if (i + 1 < iMax && myParsing.ESCAPABLE.matcher(matched.subSequence(i + 1, i + 2)).matches()) { // escape i++; } } else if (c == '(') { openCount++; } else if (c == ')') { if (openCount == 0) { // truncate to this and leave ')' to be parsed index -= iMax - i; matched = matched.subSequence(0, i); break; } openCount--; } } return spaceInUrls ? matched.trimEnd(CharPredicate.SPACE) : matched; } return null; } else { // spec 0.27 compatibility BasedSequence matched = match(myParsing.LINK_DESTINATION); return matched != null && spaceInUrls ? matched.trimEnd(CharPredicate.SPACE) : matched; } } } } /** * Attempt to parse link title (sans quotes), * * @return the string or null if no match. */ @Override public BasedSequence parseLinkTitle() { BasedSequence title = match(myParsing.LINK_TITLE); // chop off quotes from title and unescape: return title; //Escaping.unescapeString(title.substring(1, title.length() - 1)); } /** * Attempt to parse a link label * * @return number of characters parsed. */ @Override public int parseLinkLabel() { BasedSequence m = match(myParsing.LINK_LABEL); return m == null ? 0 : m.length(); } /** * Attempt to parse an autolink (URL or email in pointy brackets). * * @return true if processed characters false otherwise */ @Override public boolean parseAutolink() { BasedSequence m; if ((m = match(myParsing.EMAIL_AUTOLINK)) != null) { MailLink node = new MailLink(m.subSequence(0, 1), m.subSequence(1, m.length() - 1), m.subSequence(m.length() - 1, m.length())); appendNode(node); return true; } else if ((m = match(myParsing.AUTOLINK)) != null) { AutoLink node = new AutoLink(m.subSequence(0, 1), m.subSequence(1, m.length() - 1), m.subSequence(m.length() - 1, m.length())); appendNode(node); return true; } else if (options.wwwAutoLinkElement && (m = match(myParsing.WWW_AUTOLINK)) != null) { AutoLink node = new AutoLink(m.subSequence(0, 1), m.subSequence(1, m.length() - 1), m.subSequence(m.length() - 1, m.length())); appendNode(node); return true; } else { return false; } } /** * Attempt to parse inline HTML. * * @return true if processed characters false otherwise */ @Override public boolean parseHtmlInline() { BasedSequence m = match(myParsing.HTML_TAG); if (m != null) { // separate HTML comment from herd HtmlInlineBase node; if (m.startsWith("")) { node = new HtmlInlineComment(m); } else { node = new HtmlInline(m); } appendNode(node); return true; } else { return false; } } /** * Attempt to parse an entity, return Entity object if successful. * * @return true if processed characters false otherwise */ @Override public boolean parseEntity() { BasedSequence m; if ((m = match(myParsing.ENTITY_HERE)) != null) { HtmlEntity node = new HtmlEntity(m); appendNode(node); return true; } else { return false; } } /** * Parse a run of ordinary characters, or a single character with a special meaning in markdown, as a plain string. * * @return true if processed characters false otherwise */ protected boolean parseString() { int begin = index; int length = input.length(); while (index != length) { if (specialCharacters.get(input.charAt(index))) { break; } index++; } if (begin != index) { appendText(input, begin, index); return true; } else { return false; } } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } /** * Scan a sequence of characters with code delimiterChar, and return information about the number of delimiters * and whether they are positioned such that they can open and/or close emphasis or strong emphasis. * * @param delimiterProcessor delimiter processor instance * @param delimiterChar delimiter character being scanned * @return information about delimiter run, or {@code null} */ protected DelimiterData scanDelimiters(DelimiterProcessor delimiterProcessor, char delimiterChar) { int startIndex = index; int delimiterCount = 0; while (peek() == delimiterChar) { delimiterCount++; index++; } if (delimiterCount < delimiterProcessor.getMinLength()) { index = startIndex; return null; } String before = startIndex == 0 ? SequenceUtils.EOL : String.valueOf(input.charAt(startIndex - 1)); char charAfter = peek(); String after = charAfter == SequenceUtils.NUL ? SequenceUtils.EOL : String.valueOf(charAfter); // We could be more lazy here, in most cases we don't need to do every match case. boolean beforeIsPunctuation; boolean afterIsPunctuation; boolean leftFlanking; boolean rightFlanking; boolean beforeIsWhitespace = myParsing.UNICODE_WHITESPACE_CHAR.matcher(before).matches(); boolean afterIsWhitespace = myParsing.UNICODE_WHITESPACE_CHAR.matcher(after).matches(); if (options.inlineDelimiterDirectionalPunctuations) { beforeIsPunctuation = myParsing.PUNCTUATION_OPEN.matcher(before).matches(); afterIsPunctuation = myParsing.PUNCTUATION_CLOSE.matcher(after).matches(); leftFlanking = !afterIsWhitespace && (!afterIsPunctuation || beforeIsWhitespace || beforeIsPunctuation); rightFlanking = !beforeIsWhitespace && (!beforeIsPunctuation || afterIsWhitespace || afterIsPunctuation); } else { beforeIsPunctuation = myParsing.PUNCTUATION.matcher(before).matches(); afterIsPunctuation = myParsing.PUNCTUATION.matcher(after).matches(); leftFlanking = !afterIsWhitespace && !(afterIsPunctuation && !beforeIsWhitespace && !beforeIsPunctuation); rightFlanking = !beforeIsWhitespace && !(beforeIsPunctuation && !afterIsWhitespace && !afterIsPunctuation); } boolean canOpen; boolean canClose; canOpen = delimiterChar == delimiterProcessor.getOpeningCharacter() && delimiterProcessor.canBeOpener(before, after, leftFlanking, rightFlanking, beforeIsPunctuation, afterIsPunctuation, beforeIsWhitespace, afterIsWhitespace); canClose = delimiterChar == delimiterProcessor.getClosingCharacter() && delimiterProcessor.canBeCloser(before, after, leftFlanking, rightFlanking, beforeIsPunctuation, afterIsPunctuation, beforeIsWhitespace, afterIsWhitespace); index = startIndex; if (canOpen || canClose || !delimiterProcessor.skipNonOpenerCloser()) { return new DelimiterData(delimiterCount, canOpen, canClose); } else { return null; } } @Override public void processDelimiters(Delimiter stackBottom) { Map openersBottom = new HashMap<>(); // find first closer above stackBottom: Delimiter closer = lastDelimiter; while (closer != null && closer.getPrevious() != stackBottom) { closer = closer.getPrevious(); } // move forward, looking for closers, and handling each while (closer != null) { char delimiterChar = closer.getDelimiterChar(); DelimiterProcessor delimiterProcessor = delimiterProcessors.get(delimiterChar); if (!closer.canClose() || delimiterProcessor == null) { closer = closer.getNext(); continue; } char openingDelimiterChar = delimiterProcessor.getOpeningCharacter(); // found delimiter closer. now look back for first matching opener: int useDelims = 0; boolean openerFound = false; boolean potentialOpenerFound = false; Delimiter opener = closer.getPrevious(); while (opener != null && opener != stackBottom && opener != openersBottom.get(delimiterChar)) { if (opener.canOpen() && opener.getDelimiterChar() == openingDelimiterChar) { potentialOpenerFound = true; useDelims = delimiterProcessor.getDelimiterUse(opener, closer); if (useDelims > 0) { openerFound = true; break; } } opener = opener.getPrevious(); } if (!openerFound) { if (!potentialOpenerFound) { // Set lower bound for future searches for openers. // Only do this when we didn't even have a potential // opener (one that matches the character and can open). // If an opener was rejected because of the number of // delimiters (e.g. because of the "multiple of 3" rule), // we want to consider it next time because the number // of delimiters can change as we continue processing. openersBottom.put(delimiterChar, closer.getPrevious()); if (!closer.canOpen()) { // We can remove a closer that can't be an opener, // once we've seen there's no matching opener: removeDelimiterKeepNode(closer); } } closer = closer.getNext(); continue; } // Remove number of used delimiters from stack and inline nodes. opener.setNumDelims(opener.getNumDelims() - useDelims); closer.setNumDelims(closer.getNumDelims() - useDelims); removeDelimitersBetween(opener, closer); //// The delimiter processor can re-parent the nodes between opener and closer, //// so make sure they're contiguous already. //mergeTextNodes(openerNode.getNext(), closerNode.getPrevious()); opener.setNumDelims(opener.getNumDelims() + useDelims); closer.setNumDelims(closer.getNumDelims() + useDelims); delimiterProcessor.process(opener, closer, useDelims); opener.setNumDelims(opener.getNumDelims() - useDelims); closer.setNumDelims(closer.getNumDelims() - useDelims); // No delimiter characters left to process, so we can remove delimiter and the now empty node. if (opener.getNumDelims() == 0) { removeDelimiterAndNode(opener); } else { // adjust number of characters in the node by keeping outer of numDelims opener.getNode().setChars(opener.getNode().getChars().subSequence(0, opener.getNumDelims())); } if (closer.getNumDelims() == 0) { Delimiter next = closer.getNext(); removeDelimiterAndNode(closer); closer = next; } else { // adjust number of characters in the node by keeping outer of numDelims BasedSequence chars = closer.getNode().getChars(); int length = chars.length(); closer.getNode().setChars(chars.subSequence(length - closer.getNumDelims(), length)); closer.setIndex(closer.getIndex() + useDelims); } } // removeIndex all delimiters while (lastDelimiter != null && lastDelimiter != stackBottom) { removeDelimiterKeepNode(lastDelimiter); } } @Override public void removeDelimitersBetween(@NotNull Delimiter opener, @NotNull Delimiter closer) { Delimiter delimiter = closer.getPrevious(); while (delimiter != null && delimiter != opener) { Delimiter previousDelimiter = delimiter.getPrevious(); removeDelimiterKeepNode(delimiter); delimiter = previousDelimiter; } } /** * Remove the delimiter and the corresponding text node. For used delimiters, e.g. `*` in `*foo*`. * * @param delim delimiter to remove */ @Override public void removeDelimiterAndNode(@NotNull Delimiter delim) { Text node = delim.getNode(); Text previousText = delim.getPreviousNonDelimiterTextNode(); Text nextText = delim.getNextNonDelimiterTextNode(); if (previousText != null && nextText != null) { // Merge adjacent text nodes previousText.setChars(input.baseSubSequence(previousText.getStartOffset(), nextText.getEndOffset())); nextText.unlink(); } node.unlink(); removeDelimiter(delim); } /** * Remove the delimiter but keep the corresponding node as text. For unused delimiters such as `_` in `foo_bar`. * * @param delim delimiter being processed */ @Override public void removeDelimiterKeepNode(@NotNull Delimiter delim) { Node node; DelimiterProcessor delimiterProcessor = delimiterProcessors.get(delim.getDelimiterChar()); node = delimiterProcessor != null ? delimiterProcessor.unmatchedDelimiterNode(this, delim) : null; if (node != null) { if (node != delim.getNode()) { // replace node delim.getNode().insertAfter(node); delim.getNode().unlink(); } } else { node = delim.getNode(); } Text previousText = delim.getPreviousNonDelimiterTextNode(); Text nextText = delim.getNextNonDelimiterTextNode(); if (node instanceof Text && (previousText != null || nextText != null)) { // Merge adjacent text nodes into one if (nextText != null && previousText != null) { node.setChars(input.baseSubSequence(previousText.getStartOffset(), nextText.getEndOffset())); previousText.unlink(); nextText.unlink(); } else if (previousText != null) { node.setChars(input.baseSubSequence(previousText.getStartOffset(), node.getEndOffset())); previousText.unlink(); } else { node.setChars(input.baseSubSequence(node.getStartOffset(), nextText.getEndOffset())); nextText.unlink(); } } removeDelimiter(delim); } @Override public void removeDelimiter(@NotNull Delimiter delim) { if (delim.getPrevious() != null) { delim.getPrevious().setNext(delim.getNext()); } if (delim.getNext() == null) { // top of stack this.lastDelimiter = delim.getPrevious(); } else { delim.getNext().setPrevious(delim.getPrevious()); } } static Map> calculateInlineParserExtensions(DataHolder options, List extensionFactories) { Map> extensionMap = new HashMap<>(); for (InlineParserExtensionFactory factory : extensionFactories) { CharSequence chars = factory.getCharacters(); for (int i = 0; i < chars.length(); i++) { char c = chars.charAt(i); List list = extensionMap.computeIfAbsent(c, k -> new ArrayList<>()); list.add(factory); } } Map> extensions = new HashMap<>(); for (Character c : extensionMap.keySet()) { List list = extensionMap.get(c); List resolvedList = DependencyResolver.resolveFlatDependencies(list, null, null); extensions.put(c, resolvedList); } return extensions; } public static BitSet calculateDelimiterCharacters(DataHolder options, Set characters) { BitSet bitSet = new BitSet(); for (Character character : characters) { bitSet.set(character); } return bitSet; } public static BitSet calculateSpecialCharacters(DataHolder options, BitSet delimiterCharacters) { BitSet bitSet = new BitSet(); bitSet.or(delimiterCharacters); bitSet.set('\r'); bitSet.set('\n'); bitSet.set('`'); bitSet.set('['); bitSet.set(']'); bitSet.set('\\'); bitSet.set('!'); bitSet.set('<'); bitSet.set('&'); return bitSet; } public static Map calculateDelimiterProcessors(DataHolder options, List delimiterProcessors) { Map map = new HashMap<>(); //addDelimiterProcessors(Arrays.asList(new AsteriskDelimiterProcessor(), new UnderscoreDelimiterProcessor()), map); if (Parser.ASTERISK_DELIMITER_PROCESSOR.get(options)) { addDelimiterProcessors(Collections.singletonList(new AsteriskDelimiterProcessor(Parser.STRONG_WRAPS_EMPHASIS.get(options))), map); } if (Parser.UNDERSCORE_DELIMITER_PROCESSOR.get(options)) { addDelimiterProcessors(Collections.singletonList(new UnderscoreDelimiterProcessor(Parser.STRONG_WRAPS_EMPHASIS.get(options))), map); } addDelimiterProcessors(delimiterProcessors, map); return map; } // nothing to add, this is for extensions. public static LinkRefProcessorData calculateLinkRefProcessors(DataHolder options, List linkRefProcessors) { if (linkRefProcessors.size() > 1) { List sortedLinkProcessors = new ArrayList<>(linkRefProcessors.size()); sortedLinkProcessors.addAll(linkRefProcessors); int[] maxNestingLevelRef = new int[] { 0 }; Collections.sort(sortedLinkProcessors, (p1, p2) -> { int lv1 = p1.getBracketNestingLevel(options); int lv2 = p2.getBracketNestingLevel(options); int maxLevel = maxNestingLevelRef[0]; if (maxLevel < lv1) maxLevel = lv1; if (maxLevel < lv2) maxLevel = lv2; maxNestingLevelRef[0] = maxLevel; if (lv1 == lv2) { // processors that want exclamation before the [ have higher priority if (!p1.getWantExclamationPrefix(options)) lv1++; if (!p2.getWantExclamationPrefix(options)) lv2++; } return Integer.compare(lv1, lv2); }); int maxNestingLevel = maxNestingLevelRef[0]; int maxReferenceLinkNesting = maxNestingLevel; int[] nestingLookup = new int[maxNestingLevel + 1]; maxNestingLevel = -1; int index = 0; for (LinkRefProcessorFactory linkProcessor : sortedLinkProcessors) { if (maxNestingLevel < linkProcessor.getBracketNestingLevel(options)) { maxNestingLevel = linkProcessor.getBracketNestingLevel(options); nestingLookup[maxNestingLevel] = index; if (maxNestingLevel == maxReferenceLinkNesting) break; } index++; } return new LinkRefProcessorData(sortedLinkProcessors, maxReferenceLinkNesting, nestingLookup); } else if (linkRefProcessors.size() > 0) { int maxNesting = linkRefProcessors.get(0).getBracketNestingLevel(options); int[] nestingLookup = new int[maxNesting + 1]; return new LinkRefProcessorData(linkRefProcessors, maxNesting, nestingLookup); } else { return new LinkRefProcessorData(linkRefProcessors, 0, new int[0]); } } private static void addDelimiterProcessors(List delimiterProcessors, Map map) { for (DelimiterProcessor delimiterProcessor : delimiterProcessors) { char opening = delimiterProcessor.getOpeningCharacter(); addDelimiterProcessorForChar(opening, delimiterProcessor, map); char closing = delimiterProcessor.getClosingCharacter(); if (opening != closing) { addDelimiterProcessorForChar(closing, delimiterProcessor, map); } } } private static void addDelimiterProcessorForChar(char delimiterChar, DelimiterProcessor toAdd, Map delimiterProcessors) { DelimiterProcessor existing = delimiterProcessors.put(delimiterChar, toAdd); if (existing != null) { if (existing.getClass() != toAdd.getClass()) { throw new IllegalArgumentException("Delimiter processor conflict with delimiter char '" + delimiterChar + "', existing " + existing.getClass().getCanonicalName() + ", added " + toAdd.getClass().getCanonicalName()); } else { // warning??? System.out.println("Delimiter processor for char '" + delimiterChar + "', added more than once " + existing.getClass().getCanonicalName()); } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy