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

com.vladsch.flexmark.util.sequence.LineAppendableImpl Maven / Gradle / Ivy

There is a newer version: 4.15.102
Show newest version
package com.vladsch.flexmark.util.sequence;

import com.vladsch.flexmark.util.collection.iteration.Indexed;
import com.vladsch.flexmark.util.collection.iteration.IndexedItemIterable;
import com.vladsch.flexmark.util.collection.iteration.IndexedItemIterator;
import com.vladsch.flexmark.util.misc.BitFieldSet;
import com.vladsch.flexmark.util.misc.CharPredicate;
import com.vladsch.flexmark.util.misc.Pair;
import com.vladsch.flexmark.util.sequence.builder.ISequenceBuilder;
import com.vladsch.flexmark.util.sequence.builder.StringSequenceBuilder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Stack;

import static com.vladsch.flexmark.util.misc.Utils.*;
import static com.vladsch.flexmark.util.sequence.SequenceUtils.isBlank;
import static com.vladsch.flexmark.util.sequence.SequenceUtils.*;
import static java.lang.Integer.MAX_VALUE;

public class LineAppendableImpl implements LineAppendable {
    final private static char EOL = '\n';

    final private boolean passThrough;              // pass through mode for all operations to appendable without examination
    final private BitFieldSet options;

    // pre-formatted nesting level, while >0 all text is passed through as is and not monitored
    private int preFormattedNesting;
    private int preFormattedFirstLine;        // line which should be prefixed
    private int preFormattedFirstLineOffset;  // first line start of preformatted offset
    private int preFormattedLastLine;         // last line of preformatted text
    private int preFormattedLastLineOffset;   // last line end of preformatted offset

    // accumulated text and line information
    private ISequenceBuilder appendable;
    final ArrayList lines;  // line contents

    // indent level to use after the next \n and before text is appended
    private CharSequence prefix;                     // current prefix
    private CharSequence prefixAfterEol;             // next prefix after eol
    private CharSequence indentPrefix;               // indent prefix
    final private Stack prefixStack;
    final private Stack indentPrefixStack;

    // current line being accumulated
    private boolean allWhitespace;                          // all chars were whitespace
    private boolean lastWasWhitespace;                      // last char was whitespace
    private int eolOnFirstText;                             // append EOLs on first text
    final private ArrayList indentsOnFirstEol;    // append indents on first eol
    final private Stack optionStack = new Stack<>();
    int modificationCount;                          // mod count for iterable use

    public LineAppendableImpl(int formatOptions) {
        this(null, LineAppendable.toOptionSet(formatOptions));
    }

    public LineAppendableImpl(@Nullable Appendable builder, int formatOptions) {
        this(builder, LineAppendable.toOptionSet(formatOptions));
    }

    public LineAppendableImpl(@Nullable Appendable appendable, BitFieldSet formatOptions) {
        this.appendable = appendable instanceof ISequenceBuilder ? ((ISequenceBuilder) appendable).getBuilder()
                : appendable instanceof LineAppendable ? ((LineAppendable) appendable).getBuilder()
                : StringSequenceBuilder.emptyBuilder();

        options = formatOptions;
        passThrough = any(F_PASS_THROUGH);
        preFormattedNesting = 0;
        preFormattedFirstLine = -1;
        preFormattedLastLine = -1;
        allWhitespace = true;
        lastWasWhitespace = false;
        lines = new ArrayList<>();
        prefixStack = new Stack<>();
        indentPrefixStack = new Stack<>();
        prefix = BasedSequence.EMPTY;
        prefixAfterEol = BasedSequence.EMPTY;
        indentPrefix = BasedSequence.EMPTY;
        eolOnFirstText = 0;
        indentsOnFirstEol = new ArrayList<>();
    }

    @Override
    public @NotNull LineAppendable getEmptyAppendable() {
        return new LineAppendableImpl(this, getOptions());
    }

    @NotNull
    @Override
    public BitFieldSet getOptionSet() {
        return options;
    }

    @NotNull
    @Override
    public LineAppendable setOptions(int flags) {
        options.setAll(flags);
        return this;
    }

    @Override
    public @NotNull LineAppendable pushOptions() {
        optionStack.push(options.toInt());
        return this;
    }

    @Override
    public @NotNull LineAppendable popOptions() {
        if (optionStack.isEmpty()) {
            throw new IllegalStateException("Option stack is empty");
        }
        Integer mask = optionStack.pop();
        options.setAll(mask);
        return this;
    }

    @Override
    public @NotNull LineAppendable changeOptions(int addFlags, int removeFlags) {
        if ((addFlags & removeFlags) != 0) {
            throw new IllegalStateException(String.format("Add flags:%d and remove flags:%d overlap:%d", addFlags, removeFlags, addFlags & removeFlags));
        }
        options.orMask(addFlags);
        options.andNotMask(removeFlags);
        return this;
    }

    private boolean any(int flags) {
        return options.any(flags);
    }

    private boolean isConvertingTabs() {
        return any(F_CONVERT_TABS | F_COLLAPSE_WHITESPACE);
    }

    private boolean isTrimTrailingWhitespace() {
        return any(F_TRIM_TRAILING_WHITESPACE);
    }

    private boolean isTrimLeadingWhitespace() {
        return any(F_TRIM_LEADING_WHITESPACE);
    }

    private boolean isCollapseWhitespace() {
        return any(F_COLLAPSE_WHITESPACE);
    }

    @NotNull
    @Override
    public BasedSequence getIndentPrefix() {
        return BasedSequence.of(indentPrefix);
    }

    @NotNull
    @Override
    public LineAppendable setIndentPrefix(@Nullable CharSequence prefix) {
        indentPrefix = prefix == null ? BasedSequence.NULL : prefix;
        return this;
    }

    @Override
    public @NotNull BasedSequence getPrefix() {
        return BasedSequence.of(prefixAfterEol);
    }

    @Override
    public @NotNull BasedSequence getBeforeEolPrefix() {
        return BasedSequence.of(prefix);
    }

    @NotNull
    @Override
    public LineAppendable addPrefix(@NotNull CharSequence prefix, boolean afterEol) {
        if (!passThrough && prefix.length() != 0) {
            if (afterEol) {
                prefixAfterEol = LineAppendable.combinedPrefix(prefixAfterEol, prefix);
            } else {
                this.prefix = LineAppendable.combinedPrefix(prefixAfterEol, prefix);
                prefixAfterEol = this.prefix;
            }
        }
        return this;
    }

    public int getAfterEolPrefixDelta() {
        return prefixAfterEol.length() - prefix.length();
    }

    @NotNull
    @Override
    public LineAppendable setPrefix(@Nullable CharSequence prefix, boolean afterEol) {
        if (!passThrough) {
            if (afterEol) {
                prefixAfterEol = prefix == null ? BasedSequence.NULL : prefix;
            } else {
                this.prefix = prefix == null ? BasedSequence.NULL : prefix;
                prefixAfterEol = this.prefix;
            }
        }
        return this;
    }

    @NotNull
    @Override
    public LineAppendable indent() {
        if (!passThrough) {
            line();
            rawIndent();
        }
        return this;
    }

    private void rawIndent() {
        prefixStack.push(prefixAfterEol);
        prefix = LineAppendable.combinedPrefix(prefixAfterEol, indentPrefix);
        prefixAfterEol = prefix;
        indentPrefixStack.push(true);
    }

    private void rawUnIndent() {
        if (prefixStack.isEmpty()) throw new IllegalStateException("unIndent with an empty stack");
        if (!indentPrefixStack.peek()) throw new IllegalStateException("unIndent for an element added by pushPrefix(), use popPrefix() instead");

        prefix = prefixStack.pop();
        prefixAfterEol = prefix;
        indentPrefixStack.pop();
    }

    @NotNull
    @Override
    public LineAppendable unIndent() {
        if (!passThrough) {
            line();
            rawUnIndent();
        }
        return this;
    }

    @NotNull
    @Override
    public LineAppendable unIndentNoEol() {
        if (!passThrough) {
            if (endsWithEOL()) {
                rawUnIndent();
            } else {
                CharSequence prefix = this.prefix;
                rawUnIndent();
                prefixAfterEol = this.prefix;
                this.prefix = prefix;
            }
        }
        return this;
    }

    @NotNull
    @Override
    public LineAppendable pushPrefix() {
        if (!passThrough) {
            prefixStack.push(prefixAfterEol);
            indentPrefixStack.push(false);
        }
        return this;
    }

    @NotNull
    @Override
    public LineAppendable popPrefix(boolean afterEol) {
        if (!passThrough) {
            if (prefixStack.isEmpty()) throw new IllegalStateException("popPrefix with an empty stack");
            if (indentPrefixStack.peek()) throw new IllegalStateException("popPrefix for element added by indent(), use unIndent() instead");

            prefixAfterEol = prefixStack.pop();
            if (!afterEol) {
                prefix = prefixAfterEol;
            }
            indentPrefixStack.pop();
        }
        return this;
    }

    @NotNull
    LineInfo getLastLineInfo() {
        return lines.isEmpty() ? LineInfo.NULL : lines.get(lines.size() - 1);
    }

    private boolean isTrailingBlankLine() {
        return appendable.length() == 0 && getLastLineInfo().isBlankText();
    }

    int lastNonBlankLine(int endLine) {
        if (endLine > lines.size() && appendable.length() > 0 && !allWhitespace) {
            // have dangling text
            return lines.size();
        }

        int i = Math.min(lines.size(), endLine);
        while (i-- > 0) {
            LineInfo lineInfo = lines.get(i);
            if (!lineInfo.isBlankText()) break;
        }
        return i;
    }

    @Override
    public int getTrailingBlankLines(int endLine) {
        endLine = Math.min(lines.size(), endLine);
        return endLine - lastNonBlankLine(endLine) - 1;
    }

    @Override
    public boolean endsWithEOL() {
        return appendable.length() == 0 && getLastLineInfo().isNotNull();
    }

    private LineInfo getLineRange(int start, int end, CharSequence prefix) {
        assert start <= end;

        CharSequence sequence = appendable.toSequence();
        CharSequence eol = trimmedEOL(sequence);
        
        if (eol == null || eol.length() == 0) {
            eol = SequenceUtils.EOL;
        }

        // KLUDGE: end always has 1 EOL character removed, however, if there is a \r before \n then one more char needs to be removed from end of text
        CharSequence text = start == Range.NULL.getStart() && end == Range.NULL.getEnd() ? BasedSequence.NULL 
                : sequence.subSequence(start, Math.max(start, end - Math.max(0, eol.length() - 1)));

        if (start >= end) {
            prefix = SequenceUtils.trimEnd(prefix);
        }

        CharSequence line = appendable.getBuilder().append(prefix).append(text).append(eol).toSequence();

        LineInfo.Preformatted preformatted;

        if (preFormattedNesting > 0) {
            preformatted = preFormattedFirstLine == lines.size() ? LineInfo.Preformatted.FIRST : LineInfo.Preformatted.BODY;
        } else {
            preformatted = preFormattedFirstLine == lines.size() ? LineInfo.Preformatted.LAST : LineInfo.Preformatted.NONE;
        }

        return LineInfo.create(line, getLastLineInfo(), prefix.length(), text.length(), line.length(), isBlank(prefix), allWhitespace || text.length() == 0, preformatted);
    }

    private void resetBuilder() {
        appendable = appendable.getBuilder();
        allWhitespace = true;
        lastWasWhitespace = true;
    }

    private void addLineRange(int start, int end, CharSequence prefix) {
        lines.add(getLineRange(start, end, prefix));
        resetBuilder();
    }

    private void appendEol(@NotNull CharSequence eol) {
        appendable.append(eol);

        int endOffset = appendable.length();
        addLineRange(0, endOffset - eol.length(), prefix);
        eolOnFirstText = 0;

        rawIndentsOnFirstEol();
    }

    private void rawIndentsOnFirstEol() {
        prefix = prefixAfterEol;

        while (!indentsOnFirstEol.isEmpty()) {
            Runnable runnable = indentsOnFirstEol.remove(indentsOnFirstEol.size() - 1);
            rawIndent();
            runnable.run();
        }
    }

    private void appendEol(int count) {
        while (count-- > 0) {
            appendEol(BasedSequence.EOL);
        }
    }

    private boolean isPrefixed(int currentLine) {
        return any(F_PREFIX_PRE_FORMATTED) || (preFormattedFirstLine == currentLine || preFormattedNesting == 0 && preFormattedLastLine != currentLine);
    }

    /**
     * Returns text range if EOL was appended
     * 

* NOTE: if range == Range.NULL then no line would be added * * @return pair of line text range if EOL was added and prefix */ private Pair getRangePrefixAfterEol() { int startOffset = 0; int endOffset = appendable.length() + 1; int currentLine = lines.size(); boolean needPrefix = isPrefixed(currentLine); if (passThrough) { return new Pair<>(Range.of(startOffset, endOffset - 1), needPrefix ? prefix : BasedSequence.NULL); } else { if (allWhitespace && (preFormattedNesting == 0 && !(preFormattedFirstLine == currentLine || preFormattedLastLine == currentLine))) { if (!any(F_TRIM_LEADING_EOL) || !lines.isEmpty()) { return new Pair<>(Range.of(startOffset, endOffset - 1), prefix); } else { return new Pair<>(Range.NULL, BasedSequence.NULL); } } else { // apply options other than convert tabs which is done at time of appending if (isTrimTrailingWhitespace() && preFormattedNesting == 0) { if (allWhitespace) { startOffset = endOffset - 1; } else { endOffset -= countTrailingSpaceTab(appendable.toSequence(), endOffset - 1); } } if (preFormattedFirstLine == currentLine) { if (startOffset > preFormattedFirstLineOffset) startOffset = preFormattedFirstLineOffset; } if (preFormattedLastLine == currentLine) { if (endOffset < preFormattedLastLineOffset + 1) endOffset = preFormattedLastLineOffset + 1; } return new Pair<>(Range.of(startOffset, endOffset - 1), needPrefix ? prefix : BasedSequence.NULL); } } } /** * Returns text offset before EOL if EOL was issued * * @return would be offset after adding EOL - 1 */ private int offsetAfterEol() { Pair rangePrefixAfterEol = getRangePrefixAfterEol(); LineInfo lastLineInfo = getLastLineInfo(); if (rangePrefixAfterEol.getFirst().isNull()) { return lastLineInfo.sumLength; } else { Range range = rangePrefixAfterEol.getFirst(); CharSequence prefix = rangePrefixAfterEol.getSecond(); if (range.isEmpty() && prefix.length() != 0) { prefix = trimEnd(prefix); } return lastLineInfo.sumLength + rangePrefixAfterEol.getFirst().getSpan() + prefix.length(); } } private void doEolOnFirstTest() { if (eolOnFirstText > 0) { eolOnFirstText = 0; appendEol(BasedSequence.EOL); } } private void appendImpl(CharSequence s, int index) { char c = s.charAt(index); if (passThrough) { if (c == EOL) { appendEol(BasedSequence.EOL); } else { if (eolOnFirstText > 0) { eolOnFirstText = 0; appendEol(BasedSequence.EOL); } if (c != '\t' && c != ' ') allWhitespace = false; appendable.append(c); } } else { if (c == EOL) { Pair rangePrefixAfterEol = getRangePrefixAfterEol(); Range textRange = rangePrefixAfterEol.getFirst(); if (textRange.isNull()) { // nothing to add, just add EOL resetBuilder(); } else { // add EOL and line appendable.append(c); addLineRange(textRange.getStart(), textRange.getEnd(), rangePrefixAfterEol.getSecond()); } rawIndentsOnFirstEol(); } else { doEolOnFirstTest(); if (c == '\t') { if (preFormattedNesting == 0 && any(F_COLLAPSE_WHITESPACE)) { if (!lastWasWhitespace) { appendable.append(' '); lastWasWhitespace = true; } } else { if (any(F_CONVERT_TABS)) { int column = appendable.length(); int spaces = 4 - (column % 4); appendable.append(' ', spaces); } else { appendable.append(s, index, index + 1); } } } else { if (c == ' ') { if (preFormattedNesting == 0) { if (!any(F_TRIM_LEADING_WHITESPACE) || (appendable.length() != 0 && !allWhitespace)) { if (any(F_COLLAPSE_WHITESPACE)) { if (!lastWasWhitespace) { appendable.append(' '); } } else { appendable.append(' '); } } } else { appendable.append(s.subSequence(index, index + 1)); } lastWasWhitespace = true; } else { allWhitespace = false; lastWasWhitespace = false; appendable.append(s, index, index + 1); } } } } } private void appendImpl(CharSequence csq, int start, int end) { int i = start; while (i < end) { appendImpl(csq, i++); } } @NotNull @Override public LineAppendable append(@NotNull CharSequence csq) { if (csq.length() > 0) { appendImpl(csq, 0, csq.length()); } else { appendable.append(csq); } return this; } @NotNull @Override public ISequenceBuilder getBuilder() { return appendable.getBuilder(); } @NotNull @Override public LineAppendable append(@NotNull CharSequence csq, int start, int end) { if (start < end) { appendImpl(csq, start, end); } return this; } @NotNull @Override public LineAppendable append(char c) { appendImpl(Character.toString(c), 0); return this; } @NotNull public LineAppendable append(char c, int count) { append(RepeatedSequence.repeatOf(c, count)); return this; } @NotNull public LineAppendable repeat(@NotNull CharSequence csq, int count) { append(RepeatedSequence.repeatOf(csq, count)); return this; } @NotNull public LineAppendable repeat(@NotNull CharSequence csq, int start, int end, int count) { append(RepeatedSequence.repeatOf(csq.subSequence(start, end), count)); return this; } @NotNull @Override public LineAppendable line() { if (preFormattedNesting > 0 || appendable.length() != 0) { appendImpl(SequenceUtils.EOL, 0); } else { CharSequence savedPrefix = this.prefix; boolean hadRawIndents = !indentsOnFirstEol.isEmpty(); rawIndentsOnFirstEol(); if (hadRawIndents || savedPrefix.length() > 0 && this.prefix.length() == 0) { // IMPORTANT: add an option for behaviour of empty EOL and prefix reset // HACK: html converter expects prefix reset on empty EOL for indentation // formatter wants to preserve first indent, until real text is output this.prefix = savedPrefix; } } return this; } @NotNull @Override public LineAppendable lineWithTrailingSpaces(int count) { if (preFormattedNesting > 0 || appendable.length() != 0) { int options = this.options.toInt(); this.options.andNotMask(F_TRIM_TRAILING_WHITESPACE | F_COLLAPSE_WHITESPACE); if (count > 0) append(' ', count); appendImpl(SequenceUtils.EOL, 0); this.options.setAll(options); } return this; } @NotNull @Override public LineAppendable lineIf(boolean predicate) { if (predicate) line(); return this; } @NotNull @Override public LineAppendable blankLine() { line(); if (!lines.isEmpty() && !isTrailingBlankLine() || lines.isEmpty() && !any(F_TRIM_LEADING_EOL)) appendEol(BasedSequence.EOL); return this; } @NotNull @Override public LineAppendable blankLineIf(boolean predicate) { if (predicate) blankLine(); return this; } @NotNull @Override public LineAppendable blankLine(int count) { line(); if ((!any(F_TRIM_LEADING_EOL) || !lines.isEmpty())) { int addBlankLines = count - getTrailingBlankLines(lines.size()); appendEol(addBlankLines); } return this; } @NotNull @Override public LineAppendable lineOnFirstText(boolean value) { if (value) eolOnFirstText++; else if (eolOnFirstText > 0) eolOnFirstText--; return this; } @NotNull @Override public LineAppendable removeIndentOnFirstEOL(@NotNull Runnable listener) { indentsOnFirstEol.remove(listener); return this; } @NotNull @Override public LineAppendable addIndentOnFirstEOL(@NotNull Runnable listener) { indentsOnFirstEol.add(listener); return this; } @Override public int getLineCount() { return lines.size(); } @Override public int getLineCountWithPending() { return appendable.length() == 0 ? lines.size() : lines.size() + 1; } @Override public int column() { return appendable.length(); } @NotNull @Override public LineInfo getLineInfo(int lineIndex) { if (lineIndex == lines.size()) { if (appendable.length() == 0) { return LineInfo.NULL; } else { // create a dummy line info Pair rangePrefixAfterEol = getRangePrefixAfterEol(); Range textRange = rangePrefixAfterEol.getFirst(); if (textRange.isNull()) { return LineInfo.NULL; } else { return getLineRange(textRange.getStart(), textRange.getEnd(), rangePrefixAfterEol.getSecond()); } } } else { return lines.get(lineIndex); } } @Override public @NotNull BasedSequence getLine(int lineIndex) { return getLineInfo(lineIndex).getLine(); } @Override public int offset() { return getLastLineInfo().sumLength; } @Override public int offsetWithPending() { return offsetAfterEol(); } @Override public boolean isPendingSpace() { return appendable.length() > 0 && lastWasWhitespace; } @Override public int getPendingSpace() { if (lastWasWhitespace && appendable.length() != 0) { return SequenceUtils.countTrailingSpaceTab(appendable.toSequence()); } return 0; } @Override public int getPendingEOL() { if (appendable.length() == 0) { // use count of blank lines+1 return getTrailingBlankLines(lines.size()) + 1; } else { return 0; } } @Override public boolean isPreFormatted() { return preFormattedNesting > 0; } @NotNull @Override public LineAppendable openPreFormatted(boolean addPrefixToFirstLine) { if (preFormattedNesting == 0) { if (preFormattedFirstLine != lines.size()) { preFormattedFirstLine = lines.size(); preFormattedFirstLineOffset = appendable.length(); } } ++preFormattedNesting; return this; } @NotNull @Override public LineAppendable closePreFormatted() { if (preFormattedNesting <= 0) throw new IllegalStateException("closePreFormatted called with nesting == 0"); --preFormattedNesting; if (preFormattedNesting == 0 && !endsWithEOL()) { // this will be the last line of preformatted text preFormattedLastLine = lines.size(); preFormattedLastLineOffset = appendable.length(); } return this; } @Override public String toString() { StringBuilder out = new StringBuilder(); try { appendToNoLine(out, true, Integer.MAX_VALUE, Integer.MAX_VALUE, 0, Integer.MAX_VALUE); } catch (IOException ignored) { } return out.toString(); } @NotNull @Override public String toString(int maxBlankLines, int maxTrailingBlankLines, boolean withPrefixes) { StringBuilder out = new StringBuilder(); try { appendTo(out, withPrefixes, maxBlankLines, maxTrailingBlankLines, 0, MAX_VALUE); } catch (IOException ignored) { } return out.toString(); } @NotNull @Override public CharSequence toSequence(int maxBlankLines, int maxTrailingBlankLines, boolean withPrefixes) { ISequenceBuilder out = getBuilder(); try { appendTo(out, withPrefixes, maxBlankLines, maxTrailingBlankLines, 0, MAX_VALUE); } catch (IOException ignored) { } return out.toSequence(); } @Override public T appendTo(@NotNull T out, boolean withPrefixes, int maxBlankLines, int maxTrailingBlankLines, int startLine, int endLine) throws IOException { line(); return appendToNoLine(out, withPrefixes, maxBlankLines, maxTrailingBlankLines, startLine, endLine); } public T appendToNoLine(@NotNull T out, boolean withPrefixes, int maxBlankLines, int maxTrailingBlankLines, int startLine, int endLine) throws IOException { boolean tailEOL = maxTrailingBlankLines >= 0; maxBlankLines = Math.max(0, maxBlankLines); maxTrailingBlankLines = Math.max(0, maxTrailingBlankLines); int endLinePending = lines.size(); int iMax = min(getLineCountWithPending(), endLine); int lastNonBlankLine = lastNonBlankLine(iMax); int consecutiveBlankLines = 0; for (int i = startLine; i < iMax; i++) { LineInfo info = getLineInfo(i); boolean notDanglingLine = i < endLinePending; if (info.textLength == 0 && !info.isPreformatted()) { if (i > lastNonBlankLine) { // NOTE: these are tail blank lines if (consecutiveBlankLines < maxTrailingBlankLines) { consecutiveBlankLines++; if (withPrefixes) out.append(trimEnd(info.getPrefix())); if (notDanglingLine && (tailEOL || consecutiveBlankLines != maxTrailingBlankLines)) { out.append(EOL); } } } else { if (consecutiveBlankLines < maxBlankLines) { consecutiveBlankLines++; if (withPrefixes) out.append(trimEnd(info.getPrefix())); if (notDanglingLine) out.append(EOL); } } } else { consecutiveBlankLines = 0; if (notDanglingLine && (tailEOL || i < lastNonBlankLine || info.isPreformatted() && info.getPreformatted() != LineInfo.Preformatted.LAST)) { if (withPrefixes) out.append(info.lineSeq); else out.append(info.getText()); } else { if (withPrefixes) out.append(info.getLineNoEOL()); else out.append(info.getText()); } } } return out; } @NotNull @Override public LineAppendable append(@NotNull LineAppendable lineAppendable, int startLine, int endLine, boolean withPrefixes) { int iMax = Math.min(endLine, lineAppendable.getLineCountWithPending()); startLine = Math.max(0, startLine); for (int i = startLine; i < iMax; i++) { LineInfo info = lineAppendable.getLineInfo(i); BasedSequence text = info.getTextNoEOL(); BasedSequence prefix = withPrefixes ? info.getPrefix() : BasedSequence.NULL; CharSequence combinedPrefix = any(F_PREFIX_PRE_FORMATTED) || !info.isPreformatted() || info.getPreformatted() == LineInfo.Preformatted.FIRST ? LineAppendable.combinedPrefix(this.prefix, prefix) : prefix; appendable.append(text); allWhitespace = info.isBlankText(); lastWasWhitespace = info.textLength == 0 || CharPredicate.SPACE_TAB.test(text.safeCharAt(info.textLength - 1)); if (i < lineAppendable.getLineCount()) { // full line appendable.append(EOL); allWhitespace = info.isBlankText(); int endOffset = appendable.length(); addLineRange(0, endOffset - 1, combinedPrefix); } else { this.prefix = combinedPrefix; } } return this; } /** * Remove lines and return index from which line info must be recomputed * * @param startLine start line index to remove * @param endLine end line index to remove * @return index from which line info must be recomputed */ private int removeLinesRaw(int startLine, int endLine) { int useStartLine = minLimit(startLine, 0); int useEndLine = maxLimit(endLine, getLineCountWithPending()); if (useStartLine < useEndLine) { lines.subList(useStartLine, useEndLine).clear(); modificationCount++; // recompute lineInfo for lines at or after the deleted lines return useStartLine; } if (endLine >= getLineCountWithPending() && appendable.length() > 0) { // reset pending text resetBuilder(); } return lines.size(); } void recomputeLineInfo(int startLine) { // recompute lineInfo for lines at or after the deleted lines int iMax = lines.size(); startLine = Math.max(0, startLine); if (startLine < iMax) { LineInfo lastInfo = startLine - 1 >= 0 ? lines.get(startLine - 1) : LineInfo.NULL; for (int i = startLine; i < iMax; i++) { LineInfo info = lines.get(i); lastInfo = LineInfo.create(lastInfo, info); lines.set(i, lastInfo); if (!lastInfo.needAggregateUpdate(info)) break; } } } @NotNull @Override public LineAppendable removeLines(int startLine, int endLine) { int useStartLine = removeLinesRaw(startLine, endLine); recomputeLineInfo(useStartLine); return this; } @Override public LineAppendable removeExtraBlankLines(int maxBlankLines, int maxTrailingBlankLines, int startLine, int endLine) { maxBlankLines = Math.max(0, maxBlankLines); maxTrailingBlankLines = Math.max(0, maxTrailingBlankLines); int iMax = min(endLine, getLineCountWithPending()); int consecutiveBlankLines = 0; int maxConsecutiveBlankLines = maxTrailingBlankLines; int minRemovedLine = getLineCountWithPending(); for (int i = iMax; i-- > 0; ) { LineInfo info = getLineInfo(i); if (info.isBlankText() && !info.isPreformatted()) { if (consecutiveBlankLines >= maxConsecutiveBlankLines) { // remove the last blank line to stay consistent with what would be done when appendingTo minRemovedLine = removeLinesRaw(i + consecutiveBlankLines, i + consecutiveBlankLines + 1); } else { consecutiveBlankLines++; } } else { consecutiveBlankLines = 0; maxConsecutiveBlankLines = maxBlankLines; } } recomputeLineInfo(minRemovedLine); return this; } @Override public void setPrefixLength(int lineIndex, int prefixLength) { if (lineIndex == lines.size() && appendable.length() > 0) { line(); } LineInfo info = lines.get(lineIndex); CharSequence line = info.lineSeq; if (prefixLength < 0 || prefixLength >= line.length() - 1) throw new IllegalArgumentException(String.format("prefixLength %d is out of valid range [0, %d) for the line", prefixLength, line.length() - 1)); if (prefixLength != info.prefixLength) { CharSequence prefix = line.subSequence(0, prefixLength); LineInfo newInfo = LineInfo.create( info.lineSeq, lineIndex == 0 ? LineInfo.NULL : lines.get(lineIndex - 1), prefix.length(), info.prefixLength + info.textLength - prefixLength, info.length, isBlank(prefix), isBlank(line.subSequence(prefixLength, info.getTextEnd())), info.getPreformatted()); lines.set(lineIndex, newInfo); this.recomputeLineInfo(lineIndex + 1); } } private LineInfo createLineInfo(int lineIndex, @NotNull CharSequence prefix, @NotNull CharSequence content) { LineInfo prevInfo = lineIndex == 0 ? LineInfo.NULL : lines.get(lineIndex - 1); LineInfo info = lineIndex == lines.size() ? LineInfo.NULL : lines.get(lineIndex); CharSequence text = content; CharSequence eol = trimmedEOL(content); if (eol == null) eol = SequenceUtils.EOL; else text = text.subSequence(0, text.length() - eol.length()); if (text.length() == 0) { prefix = SequenceUtils.trimEnd(prefix); } assert !containsAny(text, CharPredicate.ANY_EOL) : String.format("Line text should not contain any EOL, text: %s", toVisibleWhitespaceString(text)); CharSequence line = appendable.getBuilder().append(prefix).append(text).append(eol).toSequence(); LineInfo.Preformatted preformatted = info.isNotNull() ? info.getPreformatted() : prevInfo.isPreformatted() && prevInfo.getPreformatted() != LineInfo.Preformatted.LAST ? LineInfo.Preformatted.BODY : LineInfo.Preformatted.NONE; return LineInfo.create( line, prevInfo, prefix.length(), text.length(), line.length(), isBlank(prefix), isBlank(text), preformatted ); } @Override public void setLine(int lineIndex, @NotNull CharSequence prefix, @NotNull CharSequence content) { if (lineIndex == lines.size() && appendable.length() > 0) { line(); } lines.set(lineIndex, createLineInfo(lineIndex, prefix, content)); this.recomputeLineInfo(lineIndex + 1); } @Override public void insertLine(int lineIndex, @NotNull CharSequence prefix, @NotNull CharSequence content) { lines.add(lineIndex, createLineInfo(lineIndex, prefix, content)); this.recomputeLineInfo(lineIndex + 1); } int tailBlankLinesToRemove(int endLine, int maxTrailingBlankLines) { return max(0, getTrailingBlankLines(endLine) - max(0, maxTrailingBlankLines)); } static class IndexedLineInfoProxy implements Indexed { final @NotNull LineAppendableImpl appendable; final int startLine; final int endLine; final int maxTrailingBlankLines; public IndexedLineInfoProxy(@NotNull LineAppendableImpl appendable, int maxTrailingBlankLines, int startLine, int endLine) { this.appendable = appendable; this.startLine = startLine; this.endLine = Math.min(endLine, appendable.getLineCountWithPending()); this.maxTrailingBlankLines = maxTrailingBlankLines; } @NotNull @Override public LineInfo get(int index) { if (index + startLine >= endLine) throw new IndexOutOfBoundsException(String.format("index %d is out of valid range [%d, %d)", index, startLine, endLine)); return appendable.getLineInfo(index + startLine); } @Override public void set(int index, @NotNull LineInfo item) { if (index + startLine >= endLine) throw new IndexOutOfBoundsException(String.format("index %d is out of valid range [%d, %d)", index, startLine, endLine)); appendable.lines.set(startLine + index, item); appendable.recomputeLineInfo(startLine + index + 1); } @Override public void removeAt(int index) { if (index + startLine >= endLine) throw new IndexOutOfBoundsException(String.format("index %d is out of valid range [%d, %d)", index, startLine, endLine)); appendable.removeLines(index + startLine, index + 1); } @Override public int size() { int removeBlankLines = appendable.tailBlankLinesToRemove(endLine, maxTrailingBlankLines); return Math.max(0, endLine - startLine - removeBlankLines); } @Override public int modificationCount() { return appendable.modificationCount; } } static class IndexedLineProxy implements Indexed { final @NotNull IndexedLineInfoProxy proxy; final boolean withPrefixes; public IndexedLineProxy(@NotNull IndexedLineInfoProxy proxy, boolean withPrefixes) { this.proxy = proxy; this.withPrefixes = withPrefixes; } @Override public BasedSequence get(int index) { if (proxy.maxTrailingBlankLines == -1 && index + 1 == proxy.size()) { return withPrefixes ? proxy.get(index).getLineNoEOL() : proxy.get(index).getTextNoEOL(); } else { return withPrefixes ? proxy.get(index).getLine() : proxy.get(index).getText(); } } @Override public void set(int index, BasedSequence item) { if (withPrefixes) { proxy.appendable.setLine(index + proxy.startLine, BasedSequence.NULL, item); } else { proxy.appendable.setLine(index + proxy.startLine, proxy.appendable.getLineInfo(index + proxy.startLine).getPrefix(), item); } } @Override public void removeAt(int index) { proxy.removeAt(index); } @Override public int size() { return proxy.size(); } @Override public int modificationCount() { return proxy.modificationCount(); } } @NotNull IndexedLineInfoProxy getIndexedLineInfoProxy(int maxTrailingBlankLines, int startLine, int endLine) { return new IndexedLineInfoProxy(this, maxTrailingBlankLines, startLine, endLine); } @NotNull IndexedLineProxy getIndexedLineProxy(int maxTrailingBlankLines, int startLine, int endLine, boolean withPrefixes) { return new IndexedLineProxy(getIndexedLineInfoProxy(maxTrailingBlankLines, startLine, endLine), withPrefixes); } @Override public @NotNull Iterator iterator() { return new IndexedItemIterator<>(getIndexedLineInfoProxy(MAX_VALUE, 0, getLineCount())); } @Override public @NotNull Iterable getLines(int maxTrailingBlankLines, int startLine, int endLine, boolean withPrefixes) { return new IndexedItemIterable<>(getIndexedLineProxy(maxTrailingBlankLines, startLine, endLine, withPrefixes)); } @Override public @NotNull Iterable getLinesInfo(int maxTrailingBlankLines, int startLine, int endLine) { return new IndexedItemIterable<>(getIndexedLineInfoProxy(maxTrailingBlankLines, startLine, endLine)); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy