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

com.zeoflow.jx.file.LineWrapper Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2021 ZeoFlow SRL
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.zeoflow.jx.file;

import java.io.IOException;

import static com.zeoflow.jx.file.Util.checkNotNull;

/**
 * Implements soft line wrapping on an appendable. To use, append characters using {@link #append}
 * or soft-wrapping spaces using {@link #wrappingSpace}.
 */
final class LineWrapper
{

    private final RecordingAppendable out;
    private final String indent;
    private final int columnLimit;
    /**
     * Characters written since the last wrapping space that haven't yet been flushed.
     */
    private final StringBuilder buffer = new StringBuilder();
    private boolean closed;
    /**
     * The number of characters since the most recent newline. Includes both out and the buffer.
     */
    private int column = 0;

    /**
     * -1 if we have no buffering; otherwise the number of {@code indent}s to write after wrapping.
     */
    private int indentLevel = -1;

    /**
     * Null if we have no buffering; otherwise the type to pass to the next call to {@link #flush}.
     */
    private FlushType nextFlush;

    LineWrapper(Appendable out, String indent, int columnLimit)
    {
        checkNotNull(out, "out == null");
        this.out = new RecordingAppendable(out);
        this.indent = indent;
        this.columnLimit = columnLimit;
    }

    /**
     * @return the last emitted char or {@link Character#MIN_VALUE} if nothing emitted yet.
     */
    char lastChar()
    {
        return out.lastChar;
    }

    /**
     * Emit {@code s}. This may be buffered to permit line wraps to be inserted.
     */
    void append(String s) throws IOException
    {
        if (closed) throw new IllegalStateException("closed");

        if (nextFlush != null)
        {
            int nextNewline = s.indexOf('\n');

            // If s doesn't cause the current line to cross the limit, buffer it and return. We'll decide
            // whether or not we have to wrap it later.
            if (nextNewline == -1 && column + s.length() <= columnLimit)
            {
                buffer.append(s);
                column += s.length();
                return;
            }

            // Wrap if appending s would overflow the current line.
            boolean wrap = nextNewline == -1 || column + nextNewline > columnLimit;
            flush(wrap ? FlushType.WRAP : nextFlush);
        }

        out.append(s);
        int lastNewline = s.lastIndexOf('\n');
        column = lastNewline != -1
                ? s.length() - lastNewline - 1
                : column + s.length();
    }

    /**
     * Emit either a space or a newline character.
     */
    void wrappingSpace(int indentLevel) throws IOException
    {
        if (closed) throw new IllegalStateException("closed");

        if (this.nextFlush != null) flush(nextFlush);
        column++; // Increment the column even though the space is deferred to next call to flush().
        this.nextFlush = FlushType.SPACE;
        this.indentLevel = indentLevel;
    }

    /**
     * Emit a newline character if the line will exceed it's limit, otherwise do nothing.
     */
    void zeroWidthSpace(int indentLevel) throws IOException
    {
        if (closed) throw new IllegalStateException("closed");

        if (column == 0) return;
        if (this.nextFlush != null) flush(nextFlush);
        this.nextFlush = FlushType.EMPTY;
        this.indentLevel = indentLevel;
    }

    /**
     * Flush any outstanding text and forbid future writes to this line wrapper.
     */
    void close() throws IOException
    {
        if (nextFlush != null) flush(nextFlush);
        closed = true;
    }

    /**
     * Write the space followed by any buffered text that follows it.
     */
    private void flush(FlushType flushType) throws IOException
    {
        switch (flushType)
        {
            case WRAP:
                out.append('\n');
                for (int i = 0; i < indentLevel; i++)
                {
                    out.append(indent);
                }
                column = indentLevel * indent.length();
                column += buffer.length();
                break;
            case SPACE:
                out.append(' ');
                break;
            case EMPTY:
                break;
            default:
                throw new IllegalArgumentException("Unknown FlushType: " + flushType);
        }

        out.append(buffer);
        buffer.delete(0, buffer.length());
        indentLevel = -1;
        nextFlush = null;
    }

    private enum FlushType
    {
        WRAP, SPACE, EMPTY
    }

    /**
     * A delegating {@link Appendable} that records info about the chars passing through it.
     */
    static final class RecordingAppendable implements Appendable
    {

        private final Appendable delegate;

        char lastChar = Character.MIN_VALUE;

        RecordingAppendable(Appendable delegate)
        {
            this.delegate = delegate;
        }

        @Override
        public Appendable append(CharSequence csq) throws IOException
        {
            int length = csq.length();
            if (length != 0)
            {
                lastChar = csq.charAt(length - 1);
            }
            return delegate.append(csq);
        }

        @Override
        public Appendable append(CharSequence csq, int start, int end) throws IOException
        {
            CharSequence sub = csq.subSequence(start, end);
            return append(sub);
        }

        @Override
        public Appendable append(char c) throws IOException
        {
            lastChar = c;
            return delegate.append(c);
        }

    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy