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

org.fxmisc.richtext.model.ReadOnlyStyledDocument Maven / Gradle / Ivy

There is a newer version: 0.11.3
Show newest version
package org.fxmisc.richtext.model;

import static org.reactfx.util.Either.*;
import static org.reactfx.util.Tuples.*;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.BinaryOperator;
import java.util.function.UnaryOperator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.reactfx.collection.MaterializedListModification;
import org.reactfx.util.BiIndex;
import org.reactfx.util.Either;
import org.reactfx.util.FingerTree;
import org.reactfx.util.FingerTree.NonEmptyFingerTree;
import org.reactfx.util.ToSemigroup;
import org.reactfx.util.Tuple2;
import org.reactfx.util.Tuple3;

public final class ReadOnlyStyledDocument implements StyledDocument {

    private static class Summary {
        private final int paragraphCount;
        private final int charCount;

        public Summary(int paragraphCount, int charCount) {
            assert paragraphCount > 0;
            assert charCount >= 0;

            this.paragraphCount = paragraphCount;
            this.charCount = charCount;
        }

        public int length() {
            return charCount + paragraphCount - 1;
        }
    }

    private static  ToSemigroup, Summary> summaryProvider() {
        return new ToSemigroup, Summary>() {

            @Override
            public Summary apply(Paragraph p) {
                return new Summary(1, p.length());
            }

            @Override
            public Summary reduce(Summary left, Summary right) {
                return new Summary(
                        left.paragraphCount + right.paragraphCount,
                        left.charCount + right.charCount);
            }
        };

    }

    private static final Pattern LINE_TERMINATOR = Pattern.compile("\r\n|\r|\n");

    private static final BiFunction> NAVIGATE =
            (s, i) -> i <= s.length() ? left(i) : right(i - (s.length() + 1));

    public static  ReadOnlyStyledDocument fromString(String str, PS paragraphStyle, S style) {
        Matcher m = LINE_TERMINATOR.matcher(str);

        int n = 1;
        while(m.find()) ++n;
        List> res = new ArrayList<>(n);

        int start = 0;
        m.reset();
        while(m.find()) {
            String s = str.substring(start, m.start());
            res.add(new Paragraph<>(paragraphStyle, s, style));
            start = m.end();
        }
        String last = str.substring(start);
        res.add(new Paragraph<>(paragraphStyle, last, style));

        return new ReadOnlyStyledDocument<>(res);
    }

    public static  ReadOnlyStyledDocument from(StyledDocument doc) {
        if(doc instanceof ReadOnlyStyledDocument) {
            return (ReadOnlyStyledDocument) doc;
        } else {
            return new ReadOnlyStyledDocument<>(doc.getParagraphs());
        }
    }

    public static  Codec> codec(Codec pCodec, Codec tCodec) {
        return new Codec>() {
            private final Codec>> codec = Codec.listCodec(paragraphCodec(pCodec, tCodec));

            @Override
            public String getName() {
                return "application/richtextfx-styled-document<" + tCodec.getName() + ";" + pCodec.getName() + ">";
            }

            @Override
            public void encode(DataOutputStream os, StyledDocument doc) throws IOException {
                codec.encode(os, doc.getParagraphs());
            }

            @Override
            public StyledDocument decode(DataInputStream is) throws IOException {
                return new ReadOnlyStyledDocument<>(codec.decode(is));
            }

        };
    }

    private static  Codec> paragraphCodec(Codec pCodec, Codec tCodec) {
        return new Codec>() {
            private final Codec>> segmentsCodec = Codec.listCodec(styledTextCodec(tCodec));

            @Override
            public String getName() {
                return "paragraph<" + tCodec.getName() + ";" + pCodec.getName() + ">";
            }

            @Override
            public void encode(DataOutputStream os, Paragraph p) throws IOException {
                pCodec.encode(os, p.getParagraphStyle());
                segmentsCodec.encode(os, p.getSegments());
            }

            @Override
            public Paragraph decode(DataInputStream is) throws IOException {
                PS paragraphStyle = pCodec.decode(is);
                List> segments = segmentsCodec.decode(is);
                return new Paragraph<>(paragraphStyle, segments);
            }
        };
    }

    private static  Codec> styledTextCodec(Codec styleCodec) {
        return new Codec>() {

            @Override
            public String getName() {
                return "styledtext<" + styleCodec.getName() + ">";
            }

            @Override
            public void encode(DataOutputStream os, StyledText t) throws IOException {
                STRING_CODEC.encode(os, t.getText());
                styleCodec.encode(os, t.getStyle());
            }

            @Override
            public StyledText decode(DataInputStream is) throws IOException {
                String text = STRING_CODEC.decode(is);
                S style = styleCodec.decode(is);
                return new StyledText<>(text, style);
            }

        };
    }


    private final NonEmptyFingerTree, Summary> tree;

    private String text = null;
    private List> paragraphs = null;

    private ReadOnlyStyledDocument(NonEmptyFingerTree, Summary> tree) {
        this.tree = tree;
    }

    ReadOnlyStyledDocument(List> paragraphs) {
        this.tree =
                FingerTree.mkTree(paragraphs, summaryProvider()).caseEmpty().unify(
                        emptyTree -> { throw new AssertionError("Unreachable code"); },
                        neTree -> neTree);
    }

    @Override
    public int length() {
        return tree.getSummary().length();
    }

    @Override
    public String getText() {
        if(text == null) {
            String[] strings = getParagraphs().stream()
                    .map(Paragraph::getText)
                    .toArray(n -> new String[n]);
            text = String.join("\n", strings);
        }
        return text;
    }

    public int getParagraphCount() {
        return tree.getLeafCount();
    }

    public Paragraph getParagraph(int index) {
        return tree.getLeaf(index);
    }

    @Override
    public List> getParagraphs() {
        if(paragraphs == null) {
            paragraphs = tree.asList();
        }
        return paragraphs;
    }

    @Override
    public Position position(int major, int minor) {
        return new Pos(major, minor);
    }

    @Override
    public Position offsetToPosition(int offset, Bias bias) {
        return position(0, 0).offsetBy(offset, bias);
    }

    public Tuple2, ReadOnlyStyledDocument> split(int position) {
        return tree.locate(NAVIGATE, position).map(this::split);
    }

    public Tuple2, ReadOnlyStyledDocument> split(
            int row, int col) {
        return tree.splitAt(row).map((l, p, r) -> {
            Paragraph p1 = p.trim(col);
            Paragraph p2 = p.subSequence(col);
            ReadOnlyStyledDocument doc1 = new ReadOnlyStyledDocument<>(l.append(p1));
            ReadOnlyStyledDocument doc2 = new ReadOnlyStyledDocument<>(r.prepend(p2));
            return t(doc1, doc2);
        });
    }

    @Override
    public ReadOnlyStyledDocument concat(StyledDocument other) {
        return concat0(other, Paragraph::concat);
    }

    private ReadOnlyStyledDocument concatR(StyledDocument other) {
        return concat0(other, Paragraph::concatR);
    }

    private ReadOnlyStyledDocument concat0(StyledDocument other, BinaryOperator> parConcat) {
        int n = tree.getLeafCount() - 1;
        Paragraph p0 = tree.getLeaf(n);
        Paragraph p1 = other.getParagraphs().get(0);
        Paragraph p = parConcat.apply(p0, p1);
        NonEmptyFingerTree, Summary> tree1 = tree.updateLeaf(n, p);
        FingerTree, Summary> tree2 = (other instanceof ReadOnlyStyledDocument)
                ? ((ReadOnlyStyledDocument) other).tree.split(1)._2
                : FingerTree.mkTree(other.getParagraphs().subList(1, other.getParagraphs().size()), summaryProvider());
        return new ReadOnlyStyledDocument<>(tree1.join(tree2));
    }

    @Override
    public StyledDocument subSequence(int start, int end) {
        return split(end)._1.split(start)._2;
    }

    public Tuple3, RichTextChange, MaterializedListModification>> replace(
            int from, int to, ReadOnlyStyledDocument replacement) {
        return replace(from, to, x -> replacement);
    }

    Tuple3, RichTextChange, MaterializedListModification>> replace(
            int from, int to, UnaryOperator> f) {
        BiIndex start = tree.locate(NAVIGATE, from);
        BiIndex end = tree.locate(NAVIGATE, to);
        return replace(start, end, f);
    }

    Tuple3, RichTextChange, MaterializedListModification>> replace(
            BiIndex start, BiIndex end, UnaryOperator> f) {
        int pos = tree.getSummaryBetween(0, start.major).map(s -> s.length() + 1).orElse(0) + start.minor;

        List> removedPars =
                getParagraphs().subList(start.major, end.major + 1);

        return end.map(this::split).map((l0, r) -> {
            return start.map(l0::split).map((l, removed) -> {
                ReadOnlyStyledDocument replacement = f.apply(removed);
                ReadOnlyStyledDocument doc = l.concatR(replacement).concat(r);
                RichTextChange change = new RichTextChange<>(pos, removed, replacement);
                List> addedPars = doc.getParagraphs().subList(start.major, start.major + replacement.getParagraphCount());
                MaterializedListModification> parChange =
                        MaterializedListModification.create(start.major, removedPars, addedPars);
                return t(doc, change, parChange);
            });
        });
    }

    Tuple3, RichTextChange, MaterializedListModification>> replaceParagraph(
            int parIdx, UnaryOperator> f) {
        return replace(
                new BiIndex(parIdx, 0),
                new BiIndex(parIdx, tree.getLeaf(parIdx).length()),
                doc -> doc.mapParagraphs(f));
    }

    ReadOnlyStyledDocument mapParagraphs(UnaryOperator> f) {
        int n = tree.getLeafCount();
        List> pars = new ArrayList<>(n);
        for(int i = 0; i < n; ++i) {
            pars.add(f.apply(tree.getLeaf(i)));
        }
        return new ReadOnlyStyledDocument<>(pars);
    }

    @Override
    public String toString() {
        return getParagraphs()
                .stream()
                .map(Paragraph::toString)
                .reduce((p1, p2) -> p1 + "\n" + p2)
                .orElse("");
    }

    @Override
    public final boolean equals(Object other) {
        if(other instanceof StyledDocument) {
            StyledDocument that = (StyledDocument) other;
            return Objects.equals(this.getParagraphs(), that.getParagraphs());
        } else {
            return false;
        }
    }

    @Override
    public final int hashCode() {
        return getParagraphs().hashCode();
    }


    private class Pos implements Position {

        private final int major;
        private final int minor;

        private Pos(int major, int minor) {
            this.major = major;
            this.minor = minor;
        }

        @Override
        public String toString() {
            return "(" + major + ", " + minor + ")";
        }

        @Override
        public boolean sameAs(Position other) {
            return getTargetObject() == other.getTargetObject()
                    && major == other.getMajor()
                    && minor == other.getMinor();
        }

        @Override
        public TwoDimensional getTargetObject() {
            return ReadOnlyStyledDocument.this;
        }

        @Override
        public int getMajor() {
            return major;
        }

        @Override
        public int getMinor() {
            return minor;
        }

        @Override
        public Position clamp() {
            if(major == tree.getLeafCount() - 1) {
                int elemLen = tree.getLeaf(major).length();
                if(minor < elemLen) {
                    return this;
                } else {
                    return new Pos(major, elemLen-1);
                }
            } else {
                return this;
            }
        }

        @Override
        public Position offsetBy(int offset, Bias bias) {
            return tree.locateProgressively(s -> s.charCount + s.paragraphCount, toOffset() + offset)
                    .map(Pos::new);
        }

        @Override
        public int toOffset() {
            if(major == 0) {
                return minor;
            } else {
                return tree.getSummaryBetween(0, major).get().length() + 1 + minor;
            }
        }
    }
}