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

com.vladsch.flexmark.formatter.FormatterUtils 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.formatter;

import com.vladsch.flexmark.ast.*;
import com.vladsch.flexmark.parser.ListOptions;
import com.vladsch.flexmark.parser.Parser;
import com.vladsch.flexmark.util.ast.*;
import com.vladsch.flexmark.util.data.DataKey;
import com.vladsch.flexmark.util.data.MutableDataHolder;
import com.vladsch.flexmark.util.data.NullableDataKey;
import com.vladsch.flexmark.util.format.MarkdownParagraph;
import com.vladsch.flexmark.util.format.TrackedOffset;
import com.vladsch.flexmark.util.format.TrackedOffsetList;
import com.vladsch.flexmark.util.format.options.BlockQuoteMarker;
import com.vladsch.flexmark.util.format.options.ListSpacing;
import com.vladsch.flexmark.util.misc.CharPredicate;
import com.vladsch.flexmark.util.misc.Pair;
import com.vladsch.flexmark.util.misc.Utils;
import com.vladsch.flexmark.util.sequence.BasedSequence;
import com.vladsch.flexmark.util.sequence.LineAppendable;
import com.vladsch.flexmark.util.sequence.LineInfo;
import com.vladsch.flexmark.util.sequence.RepeatedSequence;
import com.vladsch.flexmark.util.sequence.builder.SequenceBuilder;
import com.vladsch.flexmark.util.sequence.mappers.SpaceMapper;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.vladsch.flexmark.util.format.options.ListSpacing.*;
import static com.vladsch.flexmark.util.sequence.BasedSequence.NULL;
import static com.vladsch.flexmark.util.sequence.LineAppendable.F_TRIM_LEADING_WHITESPACE;
import static com.vladsch.flexmark.util.sequence.LineAppendable.F_TRIM_TRAILING_WHITESPACE;

public class FormatterUtils {

    final public static DataKey LIST_ITEM_NUMBER = new DataKey<>("LIST_ITEM_NUMBER", 0);
    final public static DataKey FIRST_LIST_ITEM_CHILD = new DataKey<>("FIRST_LIST_ITEM_CHILD", false); // Set to true for first block list item child of an empty list item
    final public static Function> NULL_PADDING = sequence -> Pair.of(0, 0);
    final public static DataKey>> LIST_ALIGN_NUMERIC = new DataKey<>("LIST_ITEM_NUMBER", NULL_PADDING); // function takes ordered marker and returns Pair LeftPad,RightPad
    final public static NullableDataKey LIST_ITEM_SPACING = new NullableDataKey<>("LIST_ITEM_SPACING");

    public static String getBlockLikePrefix(BlockQuoteLike node, NodeFormatterContext context, BlockQuoteMarker blockQuoteMarkers, @NotNull BasedSequence prefix) {
        String prefixChars = node.getOpeningMarker().toString();
        String usePrefix;
        boolean compactPrefix = false;

        switch (blockQuoteMarkers) {
            case AS_IS:
                if (node.getFirstChild() != null) {
                    usePrefix = node.getChars().baseSubSequence(node.getOpeningMarker().getStartOffset(), node.getFirstChild().getStartOffset()).toString();
                } else {
                    usePrefix = prefixChars;
                }
                break;

            case ADD_COMPACT:
                usePrefix = prefixChars.trim();
                break;

            case ADD_COMPACT_WITH_SPACE:
                compactPrefix = true;
                usePrefix = prefixChars.trim() + " ";
                break;

            case ADD_SPACED:
                usePrefix = prefixChars.trim() + " ";
                break;

            default:
                throw new IllegalStateException("Unexpected value: " + blockQuoteMarkers);
        }

        // create a combined prefix, compact if needed
        CharPredicate quoteLikePrefixPredicate = context.getBlockQuoteLikePrefixPredicate();

        String combinedPrefix = prefix.toString();
        if (compactPrefix && combinedPrefix.endsWith(" ") && combinedPrefix.length() >= 2 && quoteLikePrefixPredicate.test(combinedPrefix.charAt(combinedPrefix.length() - 2))) {
            combinedPrefix = combinedPrefix.substring(0, combinedPrefix.length() - 1) + usePrefix;
        } else {
            combinedPrefix += usePrefix;
        }

        return combinedPrefix;
    }

    @SuppressWarnings("SameParameterValue")
    public static CharSequence stripSoftLineBreak(CharSequence chars, CharSequence spaceChar) {
        StringBuffer sb = null;
        Matcher matcher = Pattern.compile("\\s*(?:\r\n|\r|\n)\\s*").matcher(chars);
        while (matcher.find()) {
            if (sb == null) sb = new StringBuffer();
            matcher.appendReplacement(sb, spaceChar.toString());
        }
        if (sb != null) {
            matcher.appendTail(sb);
            return sb;
        }
        return chars;
    }

    @NotNull
    public static String getActualAdditionalPrefix(BasedSequence contentChars, MarkdownWriter markdown) {
        String prefix;
        int parentPrefix = markdown.getPrefix().length();
        int column = contentChars.baseColumnAtStart();

        prefix = RepeatedSequence.repeatOf(" ", Utils.minLimit(0, column - parentPrefix)).toString();
        return prefix;
    }

    @NotNull
    public static String getAdditionalPrefix(BasedSequence fromChars, BasedSequence toChars) {
        String prefix;
        int parentPrefix = fromChars.getStartOffset();
        int column = toChars.getStartOffset();

        prefix = RepeatedSequence.repeatOf(" ", Utils.minLimit(0, column - parentPrefix)).toString();
        return prefix;
    }

    public static BasedSequence getSoftLineBreakSpan(Node node) {
        if (node == null) return NULL;

        Node lastNode = node;
        Node nextNode = node.getNext();

        while (nextNode != null && !(nextNode instanceof SoftLineBreak)) {
            lastNode = nextNode;
            nextNode = nextNode.getNext();
        }

        return Node.spanningChars(node.getChars(), lastNode.getChars());
    }

    public static void appendWhiteSpaceBetween(
            MarkdownWriter markdown,
            Node prev,
            Node next,
            boolean preserve,
            boolean collapse,
            boolean collapseToEOL
    ) {
        if (next != null && prev != null && (preserve || collapse)) {
            appendWhiteSpaceBetween(markdown, prev.getChars(), next.getChars(), preserve, collapse, collapseToEOL);
        }
    }

    public static void appendWhiteSpaceBetween(
            MarkdownWriter markdown,
            BasedSequence prev,
            BasedSequence next,
            boolean preserve,
            boolean collapse,
            boolean collapseToEOL
    ) {
        if (next != null && prev != null && (preserve || collapse)) {
            if (prev.getEndOffset() <= next.getStartOffset()) {
                BasedSequence sequence = prev.baseSubSequence(prev.getEndOffset(), next.getStartOffset());
                if (!sequence.isEmpty() && sequence.isBlank()) {
                    if (!preserve) {
                        if (collapseToEOL && sequence.indexOfAny(CharPredicate.ANY_EOL) != -1) {
                            markdown.append('\n');
                        } else {
                            markdown.append(' ');
                        }
                    } else {
                        // need to set pre-formatted or spaces after eol are ignored assuming prefixes are used
                        int saved = markdown.getOptions();
                        markdown.setOptions(saved & ~F_TRIM_LEADING_WHITESPACE);
                        markdown.append(sequence);
                        markdown.setOptions(saved);
                    }
                }
            } else {
                // nodes reversed due to children being rendered before the parent
            }
        }
    }

    public static void renderList(ListBlock node, NodeFormatterContext context, MarkdownWriter markdown) {
        if (context.isTransformingText()) {
            // CAUTION: during translation no formatting should be done
            context.renderChildren(node);
        } else {
            ArrayList itemList = new ArrayList<>();
            Node item = node.getFirstChild();
            while (item != null) {
                itemList.add(item);
                item = item.getNext();
            }
            renderList(node, context, markdown, itemList);
        }
    }

    public static void renderList(ListBlock node, NodeFormatterContext context, MarkdownWriter markdown, List itemList) {
        FormatterOptions formatterOptions = context.getFormatterOptions();
        if (formatterOptions.listAddBlankLineBefore && !node.isOrDescendantOfType(ListItem.class)) {
            markdown.blankLine();
        }

        Document document = context.getDocument();
        ListSpacing listSpacing = LIST_ITEM_SPACING.get(document);
        int listItemNumber = LIST_ITEM_NUMBER.get(document);
        int startingNumber = node instanceof OrderedList ? formatterOptions.listRenumberItems && formatterOptions.listResetFirstItemNumber ? 1 : ((OrderedList) node).getStartNumber() : 1;
        Function> listAlignNumeric = LIST_ALIGN_NUMERIC.get(document);
        document.set(LIST_ITEM_NUMBER, startingNumber);

        ListSpacing itemSpacing = null;
        switch (formatterOptions.listSpacing) {
            case AS_IS:
                break;
            case LOOSE:
                itemSpacing = LOOSE;
                break;
            case TIGHT:
                itemSpacing = ListSpacing.TIGHT;
                break;
            case LOOSEN:
                itemSpacing = hasLooseItems(itemList) ? LOOSE : ListSpacing.TIGHT;
                break;
            case TIGHTEN:
                itemSpacing = hasLooseItems(itemList) ? AS_IS : ListSpacing.TIGHT;
                break;
        }

        document.remove(LIST_ALIGN_NUMERIC);

        if (!formatterOptions.listAlignNumeric.isNoChange() && node instanceof OrderedList) {
            int maxLen = Integer.MIN_VALUE;
            int minLen = Integer.MAX_VALUE;
            int i = startingNumber;
            for (Node item : itemList) {
                if (!formatterOptions.listRemoveEmptyItems || item.hasChildren() && item.getFirstChildAnyNot(BlankLine.class) != null) {
                    int length = formatterOptions.listRenumberItems ? Integer.toString(i).length() + 1 : ((ListItem) item).getOpeningMarker().length();
                    maxLen = Math.max(maxLen, length);
                    minLen = Math.min(minLen, length);
                    i++;
                }
            }

            if (maxLen != minLen) {
                int finalMaxLen = maxLen;
                document.set(LIST_ALIGN_NUMERIC, formatterOptions.listAlignNumeric.isLeft()
                        ? sequence -> Pair.of(0, Math.min(4, Math.max(0, finalMaxLen - sequence.length())))
                        : sequence -> Pair.of(Math.min(4, Math.max(0, finalMaxLen - sequence.length())), 0)
                );
            }
        }

        document.set(LIST_ITEM_SPACING, itemSpacing == LOOSE && (listSpacing == null || listSpacing == LOOSE) ? LOOSE : itemSpacing);
        for (Node item : itemList) {
            if (itemSpacing == LOOSE && (listSpacing == null || listSpacing == LOOSE)) {
                markdown.blankLine();
            }
            context.render(item);
        }
        document.set(LIST_ITEM_SPACING, listSpacing);
        document.set(LIST_ITEM_NUMBER, listItemNumber);
        document.set(LIST_ALIGN_NUMERIC, listAlignNumeric);

        if (!node.isOrDescendantOfType(ListItem.class)) {
            markdown.tailBlankLine();
        }
    }

    @SuppressWarnings("WeakerAccess")
    public static void renderLooseParagraph(Paragraph node, NodeFormatterContext context, MarkdownWriter markdown) {
        markdown.blankLine();
        renderLooseItemParagraph(node, context, markdown);
    }

    public static boolean isFollowedByBlankLine(@Nullable Node node) {
        while (node != null) {
            if (node.getNextAnyNot(HtmlCommentBlock.class, HtmlInnerBlockComment.class, HtmlInlineComment.class) instanceof BlankLine) return true;
            if (node.getNextAnyNot(BlankLine.class, HtmlCommentBlock.class, HtmlInnerBlockComment.class, HtmlInlineComment.class) != null) return false;
            node = node.getParent();
        }
        return false;
    }

    public static boolean isNotLastItem(@Nullable Node node) {
        while (node != null && !(node instanceof Document)) {
            if (node.getNextAnyNot(BlankLine.class, HtmlCommentBlock.class, HtmlInnerBlockComment.class, HtmlInlineComment.class) != null) return true;
            node = node.getParent();
        }
        return false;
    }

    public static boolean isLastOfItem(@Nullable Node node) {
        return node != null && node.getNextAnyNot(BlankLine.class, HtmlCommentBlock.class, HtmlInnerBlockComment.class, HtmlInlineComment.class) == null;
    }

    @SuppressWarnings("WeakerAccess")
    public static void renderLooseItemParagraph(Paragraph node, NodeFormatterContext context, MarkdownWriter markdown) {
        renderTextBlockParagraphLines(node, context, markdown);
        Block parent = node.getParent();
        if (parent instanceof ListItem) {
            if (context.getFormatterOptions().blankLinesInAst) {
                boolean addBlankLine;
                boolean canAddTailBlankLine = !((ParagraphContainer) parent).isParagraphEndWrappingDisabled(node);

                ListItem listItem = (ListItem) parent;
                switch (context.getFormatterOptions().listSpacing) {
                    case LOOSEN:
                        addBlankLine = parent.getParent() instanceof ListBlock && ((ListBlock) parent.getParent()).isLoose() && hasLooseItems(parent.getParent().getChildren())
                                && (isFollowedByBlankLine(node) && isNotLastItem(parent)
                                || !listItem.isOwnTight()
                                || listItem.isItemParagraph(node) && parent.getFirstChild() != null && parent.getFirstChild().getNext() != null)
                        ;
                        break;

                    case LOOSE:
                        addBlankLine = true;
                        break;

                    case TIGHTEN:
                        addBlankLine = canAddTailBlankLine && (listItem.isItemParagraph(node) ? isFollowedByBlankLine(node) && isNotLastItem(node) : isNotLastItem(node));
                        break;

                    case AS_IS:
                        addBlankLine = isFollowedByBlankLine(node) && isNotLastItem(parent);
                        break;

                    case TIGHT:
                    default:
                        addBlankLine = false;
                        break;
                }

                if (addBlankLine) {
                    markdown.tailBlankLine();
                }
            } else {
                if (context.getFormatterOptions().listSpacing != TIGHTEN || parent.getNext() != null) {
                    markdown.tailBlankLine();
                }
            }
        } else {
            markdown.tailBlankLine();
        }
    }

    static boolean hasLooseItems(Iterable itemList) {
        for (Node item : itemList) {
            if (item instanceof ListItem) {
                if (!((ListItem) item).isOwnTight() && item.getNext() != null) {
                    return true;
                }
            }
        }
        return false;
    }

    public static void renderListItem(
            ListItem node,
            NodeFormatterContext context,
            MarkdownWriter markdown,
            ListOptions listOptions,
            BasedSequence markerSuffix,
            boolean addBlankLineLooseItems
    ) {
        FormatterOptions options = context.getFormatterOptions();
        boolean savedFirstListItemChild = FIRST_LIST_ITEM_CHILD.get(context.getDocument());

        if (context.isTransformingText()) {
            BasedSequence openingMarker = node.getOpeningMarker();
            String itemContentPrefix;
            String itemContentSpacer;
            String prefix;
            String additionalPrefix = getActualAdditionalPrefix(openingMarker, markdown);

            if (node.getFirstChild() == null) {
                // TEST: not sure if this works, need an empty list item with no children to test
                int count = openingMarker.length() + (listOptions.isItemContentAfterSuffix() ? markerSuffix.length() : 0) + 1;
                itemContentPrefix = RepeatedSequence.repeatOf(' ', count).toString();
                prefix = additionalPrefix + itemContentPrefix;

                itemContentSpacer = " ";
            } else {
                BasedSequence childContent = node.getFirstChild().getChars();
                itemContentPrefix = getAdditionalPrefix(markerSuffix.isEmpty() ? openingMarker : markerSuffix, childContent);
                prefix = additionalPrefix + itemContentPrefix;

                itemContentSpacer = getAdditionalPrefix(markerSuffix.isEmpty() ? openingMarker.getEmptySuffix() : markerSuffix.getEmptySuffix(), childContent);
            }

            markdown.pushPrefix().addPrefix(prefix, true);
            markdown.append(additionalPrefix).append(openingMarker);

            if (!markerSuffix.isEmpty()) {
                String markerSuffixIndent = getAdditionalPrefix(openingMarker.getEmptySuffix(), markerSuffix);
                markdown.append(markerSuffixIndent).append(markerSuffix);
            }

            markdown.append(itemContentSpacer);

            // if have no item text and followed by eol then add EOL
            if (!(node.getFirstChild() instanceof Paragraph)) {
                if (node.getFirstChild() == null) {
                    if (!savedFirstListItemChild) {
                        markdown.append("\n");
                    }
                } else {
                    int posEOL = node.endOfLine(openingMarker.getEndOffset());
                    if (posEOL < node.getFirstChild().getStartOffset()) {
                        // output EOL
                        markdown.append("\n");
                    }
                }
            }

            context.renderChildren(node);
            markdown.popPrefix();
        } else {
            if (options.listRemoveEmptyItems && !(node.hasChildren() && node.getFirstChildAnyNot(BlankLine.class) != null)) {
                return;
            }

            CharSequence useOpeningMarker = node.getOpeningMarker();
            if (node.isOrderedItem()) {
                char delimiter = useOpeningMarker.charAt(useOpeningMarker.length() - 1);
                CharSequence number = useOpeningMarker.subSequence(0, useOpeningMarker.length() - 1);

                switch (options.listNumberedMarker) {
                    case ANY:
                        break;
                    case DOT:
                        delimiter = '.';
                        break;
                    case PAREN:
                        delimiter = ')';
                        break;
                    default:
                        throw new IllegalStateException("Missing case for ListNumberedMarker " + options.listNumberedMarker.name());
                }

                Document document = context.getDocument();

                if (options.listRenumberItems) {
                    Integer itemNumber = LIST_ITEM_NUMBER.get(document);
                    useOpeningMarker = String.format(Locale.US, "%d%c", itemNumber++, delimiter);
                    document.set(LIST_ITEM_NUMBER, itemNumber);
                } else {
                    useOpeningMarker = String.format("%s%c", number, delimiter);
                }

                Pair padding = LIST_ALIGN_NUMERIC.get(document).apply(useOpeningMarker);
                if (padding.getFirst() > 0) useOpeningMarker = RepeatedSequence.ofSpaces(padding.getFirst()).toString() + useOpeningMarker.toString();
                if (padding.getSecond() > 0) useOpeningMarker = useOpeningMarker.toString() + RepeatedSequence.ofSpaces(padding.getSecond()).toString();
            } else {
                if (node.canChangeMarker()) {
                    switch (options.listBulletMarker) {
                        case ANY:
                            break;
                        case DASH:
                            useOpeningMarker = "-";
                            break;
                        case ASTERISK:
                            useOpeningMarker = "*";
                            break;
                        case PLUS:
                            useOpeningMarker = "+";
                            break;
                        default:
                            throw new IllegalStateException("Missing case for ListBulletMarker " + options.listBulletMarker.name());
                    }
                }
            }

            // NOTE: if list item content after suffix is set in the parser, then sub-items are indented after suffix
            //    otherwise only the item's lazy continuation for the paragraph can be indented after suffix, child items are normally indented
            int itemContinuationCount = (listOptions.isItemContentAfterSuffix() || options.listsItemContentAfterSuffix ? markerSuffix.length() : 0);
            int continuationCount = useOpeningMarker.length() + (listOptions.isItemContentAfterSuffix() ? markerSuffix.length() : 0) + 1;
            CharSequence additionalItemPrefix = options.itemContentIndent ? RepeatedSequence.repeatOf(' ', itemContinuationCount)
                    : "";

            CharSequence childPrefix = options.itemContentIndent ? RepeatedSequence.repeatOf(' ', continuationCount)
                    : RepeatedSequence.repeatOf(" ", listOptions.getItemIndent()).toString();

            BasedSequence openingMarker = node.getOpeningMarker();
            BasedSequence replacedOpenMarker = openingMarker.getBuilder().append(openingMarker.getEmptyPrefix()).append(useOpeningMarker).append(openingMarker.getEmptySuffix()).toSequence();

            markdown.pushOptions()
                    .preserveSpaces()
                    .append(replacedOpenMarker)
                    .append(' ')
                    .append(markerSuffix)
                    .popOptions();

            markdown.pushPrefix().addPrefix(childPrefix, true);

            Node childNode = node.getFirstChild();
            if (childNode != null && node.getFirstChildAnyNot(BlankLine.class) != null) {
                markdown.pushPrefix().addPrefix(additionalItemPrefix, true);
                // NOTE: depends on first child
                FIRST_LIST_ITEM_CHILD.set(context.getDocument(), true);
                context.render(childNode);
                FIRST_LIST_ITEM_CHILD.set(context.getDocument(), false);
                markdown.popPrefix();

                while (true) {
                    childNode = childNode.getNext();
                    if (childNode == null) break;
                    context.render(childNode);
                }

                if (addBlankLineLooseItems && (node.isLoose() && context.getFormatterOptions().listSpacing == LOOSEN || context.getFormatterOptions().listSpacing == LOOSE)) {
                    markdown.tailBlankLine();
                }
            } else {
                // NOTE: empty list item
                if (node.isLoose()) {
                    markdown.tailBlankLine();
                } else {
                    if (!savedFirstListItemChild) {
                        markdown.line();
                    }
                }
            }
            markdown.popPrefix();
        }

        FIRST_LIST_ITEM_CHILD.set(context.getDocument(), savedFirstListItemChild);
    }

    @SuppressWarnings("WeakerAccess")
    public static void renderTextBlockParagraphLines(Node node, NodeFormatterContext context, MarkdownWriter markdown) {
        if (context.isTransformingText()) {
            context.translatingSpan((context1, writer) -> context1.renderChildren(node));
            markdown.line();
        } else {
            FormatterOptions formatterOptions = context.getFormatterOptions();
            if (formatterOptions.rightMargin > 0) {
                MutableDataHolder subContextOptions = context.getOptions().toMutable().set(Formatter.KEEP_SOFT_LINE_BREAKS, true).set(Formatter.KEEP_HARD_LINE_BREAKS, true);
                SequenceBuilder builder = context.getDocument().getChars().getBuilder();
                NodeFormatterContext subContext = context.getSubContext(subContextOptions, builder.getBuilder());
                MarkdownWriter subContextMarkdown = subContext.getMarkdown();
                subContextMarkdown.removeOptions(F_TRIM_TRAILING_WHITESPACE);
                subContext.renderChildren(node);

                BasedSequence nodeLessEol = node.getChars().trimEOL();
                BasedSequence trailingSpaces = node.getChars().trimmedEnd();
                if (trailingSpaces.isNotEmpty() && !subContextMarkdown.endsWithEOL()) {
                    // add these so our tracked offsets at end of paragraph after whitespaces are not outside the sequence
                    subContextMarkdown.append(trailingSpaces);
                }
                subContextMarkdown.line();
                subContextMarkdown.appendToSilently(builder, 0, -1);

                BasedSequence paragraphChars = builder.toSequence();
                BasedSequence altParagraphChars = builder.toSequence(context.getTrackedSequence());
                boolean haveAltSequence = paragraphChars != altParagraphChars;
                int startOffset;
                int endOffset;

                TrackedOffsetList trackedOffsets = context.getTrackedOffsets();
                if (haveAltSequence) {
                    // NOTE: this is only needed to find offset in trackedOffsets for the paragraph
                    BasedSequence charsLessEol = altParagraphChars.trimEnd();
                    startOffset = charsLessEol.getStartOffset();
                    int endOffsetDelta = nodeLessEol.countTrailingWhitespace() - charsLessEol.countTrailingWhitespace();
                    endOffset = charsLessEol.getEndOffset() + endOffsetDelta + 1;
                } else {
                    startOffset = nodeLessEol.getStartOffset();
                    endOffset = nodeLessEol.getEndOffset();
                }

                TrackedOffsetList paragraphTrackedOffsets = trackedOffsets.getTrackedOffsets(startOffset, endOffset);

                MarkdownParagraph formatter = new MarkdownParagraph(paragraphChars, altParagraphChars, formatterOptions.charWidthProvider);
                formatter.setOptions(context.getOptions());

                formatter.setWidth(formatterOptions.rightMargin - markdown.getPrefix().length());
                formatter.setKeepSoftBreaks(false);
                formatter.setKeepHardBreaks(formatterOptions.keepHardLineBreaks);
                formatter.setRestoreTrackedSpaces(context.isRestoreTrackedSpaces());
                formatter.setFirstIndent(NULL);
                formatter.setIndent(NULL);

                // adjust first line width, based on change in prefix after the first line EOL
                formatter.setFirstWidthOffset(-markdown.column() + markdown.getAfterEolPrefixDelta());

                if (formatterOptions.applySpecialLeadInHandlers) {
                    formatter.setLeadInHandlers(Parser.SPECIAL_LEAD_IN_HANDLERS.get(context.getDocument()));
                }

                for (TrackedOffset trackedOffset : paragraphTrackedOffsets) {
                    formatter.addTrackedOffset(trackedOffset);
                }

                BasedSequence wrappedText = formatter.wrapText().toMapped(SpaceMapper.fromNonBreakSpace);
                int startLine = markdown.getLineCount();
                int firstLineOffset = markdown.column();
                markdown.pushOptions().preserveSpaces().append(wrappedText).line().popOptions();

                if (!paragraphTrackedOffsets.isEmpty()) {
                    LineInfo startLineInfo = markdown.getLineInfo(startLine);

                    for (TrackedOffset trackedOffset : paragraphTrackedOffsets) {
                        if (trackedOffset.isResolved()) {
                            int offsetIndex = trackedOffset.getIndex();
                            @NotNull Pair lineColumn = wrappedText.lineColumnAtIndex(offsetIndex);
                            int trackedLine = lineColumn.getFirst();
                            LineInfo lineInfo = markdown.getLineInfo(startLine + trackedLine);

                            int lengthOffset = startLineInfo.sumLength - startLineInfo.length;
                            int prefixDelta = lineInfo.sumPrefixLength - startLineInfo.sumPrefixLength + startLineInfo.prefixLength;
                            int delta = firstLineOffset + lengthOffset + prefixDelta;
                            trackedOffset.setIndex(offsetIndex + delta);
//                            System.out.println(String.format("Wrap Resolved %d -> %d + %d = %d in line[%d]: '%s'", trackedOffset.getOffset(), offsetIndex, delta, offsetIndex + delta, lineInfo.index + 1, line.getBuilder().append(line).toStringWithRanges(true)));
//                            System.out.println(String.format("      delta: %d = firstLineOffset: %d + lengthOffset: %d + prefixDelta: %d", delta, firstLineOffset, lengthOffset, prefixDelta));
//                            System.out.println(String.format("      startLineInfo: %s", lineInfo));
//                            System.out.println(String.format("      lineInfo: %s", lineInfo));
//                        } else {
//                            System.out.println(String.format("Wrap Unresolved %d", trackedOffset.getOffset()));
                        }
                    }
                }
            } else {
                context.renderChildren(node);
                markdown.line();
            }
        }
    }

    public static void renderBlockQuoteLike(BlockQuoteLike node, NodeFormatterContext context, MarkdownWriter markdown) {
        FormatterOptions formatterOptions = context.getFormatterOptions();

        String combinedPrefix = getBlockLikePrefix(node, context, formatterOptions.blockQuoteMarkers, markdown.getPrefix());

        markdown.pushPrefix();

        if (!FIRST_LIST_ITEM_CHILD.get(context.getDocument())) {
            if (formatterOptions.blockQuoteBlankLines) {
                markdown.blankLine();
            }
            markdown.setPrefix(combinedPrefix, false);
        } else {
            String firstPrefix = getBlockLikePrefix(node, context, formatterOptions.blockQuoteMarkers, NULL);
            markdown.pushOptions().removeOptions(LineAppendable.F_WHITESPACE_REMOVAL).append(firstPrefix).popOptions();
            markdown.setPrefix(combinedPrefix, true);
        }

        int lines = markdown.getLineCountWithPending();
        context.renderChildren((Node) node);
        markdown.popPrefix();

        if (formatterOptions.blockQuoteBlankLines && (lines < markdown.getLineCountWithPending() && !FIRST_LIST_ITEM_CHILD.get(context.getDocument())))
            markdown.tailBlankLine();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy