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

com.google.api.tools.framework.snippet.Doc Maven / Gradle / Ivy

There is a newer version: 0.0.8
Show newest version
/*
 * Copyright (C) 2016 Google Inc.
 *
 * 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.google.api.tools.framework.snippet;

import com.google.common.base.CharMatcher;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;

import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;

import javax.annotation.Nullable;

/**
 * A Java implementation of a Wadler-Lindig pretty printer.
 *
 * 

The pretty printer takes documents formed using a small set of operators, and formats them as * strings suitable for display in a fixed number of columns. * *

A pretty-printer document (or {@code Doc}) is an immutable object that represents a piece of * formatted text. A programmer can use the combinators {@link #EMPTY} and {@link #text text()} * to construct documents, and can concatenate documents with {@link #append append()}. For example: *

 * append(text("Good"), text("Morning"), text("World"))
 * 
* pretty-prints as: *
 * GoodMorningWorld
 * 
* *

Line breaking is controlled using the {@link #BREAK} and {@link #group group()} combinators. * Within a {@link #group group()} combinator, all breaks are printed using a common breaking * policy, described using a {@link GroupKind}. For example: *

 * group(HORIZONTAL, append(text("Good"), BREAK, text("Morning"), BREAK, text("World"))
 * 
* pretty-prints as *
 * Good Morning World
 * 
* whereas *
 * group(VERTICAL, append(text("Good"), BREAK, text("Morning"), BREAK, text("World"))
 * 
* pretty-prints as: *
 * Good
 * Morning
 * World
 * 
* *

A key reason for the pretty-printer's expressive power is the automatic group kind * ({@link GroupKind#AUTO}), which allows us to specify pretty-printing alternatives. Breaks in an * automatic group are all printed as spaces, if the result would not overflow a single line, or as * newlines, otherwise; the pretty-printer makes the appropriate choice at layout time depending on * the space available. * *

The combinators {@link #nest nest()}, {@link #align align()}, and {@link #hang hang()} * control indentation. For instance, the {@link #nest nest()} combinator increases the indentation * on new-lines by a number of columns. For example: *

 * nest(2, group(VERTICAL, append(text("Good"), BREAK, text("Morning"), BREAK, text("World")))
 * 
* pretty-prints as: *
 * Good
 *   Morning
 *   World
 * 
* *

Many methods of this class come in two variants: *

    *
  • a static method, written {@code Doc.group(xyz)}, and *
  • an instance method, written {@code xyz.group()}. *
* The two may be used interchangeably, although the prefix (static) version usually leads to more * readable code. * *

Wadler's original pretty printer is implemented in Haskell [1]. The original Wadler version * relies on lazy evaluation, which Java does not have, so this implementation is based on Lindig's * strict version in Objective Caml [2]. * *

References: *

    *
  • [1] Philip Wadler. "A Prettier Printer". Journal of Functional Programming, 1999. *
  • [2] Christian Lindig. "Strictly Pretty". *
* *

This library is thread-safe. * */ public abstract class Doc { /** Size of the default indentation step in characters. */ public static final int DEFAULT_INDENT = 2; /** The default number of columns to format for if no width is specified. */ public static final int DEFAULT_WIDTH = 80; // Document combinators. /** * Returns a document representing the literal text {@code text}. Assumes that there are no * newline characters in the representation. (If there are new-line characters the pretty-printer * will print them as-is, usually leading to poorly formatted output.) For example, * {@code text("hello")} prints as {@code "hello"}. */ public static Doc text(String text) { return new Text(text); } /** The empty document, which prints as {@code ""} (the empty string). */ public static final Doc EMPTY = text(""); // Convenience constants for common punctuation characters. public static final Doc COMMA = text(","); public static final Doc LBRACKET = text("["); public static final Doc RBRACKET = text("]"); public static final Doc LPAREN = text("("); public static final Doc RPAREN = text(")"); public static final Doc LBRACE = text("{"); public static final Doc RBRACE = text("}"); public static final Doc LANGLE = text("<"); public static final Doc RANGLE = text(">"); public static final Doc SEMI = Doc.text(";"); private static final Doc COMMA_AND_BREAK = Doc.text(",").add(breakWith(" ")); private static final Doc SPACE = Doc.text(" "); // White space (excluding breaks) generated by the pretty printer. This // is currently just a simple space. private static final CharMatcher WHITESPACE = CharMatcher.anyOf(" "); /** Applies the {@code text()} combinator to a sequence of strings. */ public static List texts(Iterable strings) { List results = Lists.newArrayList(); for (String s : strings) { results.add(text(s)); } return results; } /** Applies the {@code text()} combinator to a sequence of strings. */ public static List texts(String... strings) { return texts(Arrays.asList(strings)); } /** * Splits the string {@code input} up into words, and places each word into a literal * {@link #text text()} document, separated by {@link #BREAK} objects. */ private static final Splitter TO_WORDS = Splitter.on(CharMatcher.breakingWhitespace()).omitEmptyStrings(); public static Doc words(String input) { return join(texts(TO_WORDS.split(input))); } /** * Returns a break, which will be printed according to the policy of the closest enclosing group. * *

The {@code representation} argument describes how the break should be printed in horizontal * mode; the representation is ignored in vertical mode. Assumes that there are no newline * characters in the representation. For example: *

    *
  • {@code group(HORIZONTAL, breakWith("-"))} prints as {@code "-"} *
  • {@code group(VERTICAL, breakWith("-"))} prints as {@code "\n"} *
*/ public static Doc breakWith(String representation) { return new Break(representation); } /** * The default break, which is printed either as a space in horizontal mode, or as a newline in * vertical mode. For example: *
    *
  • {@code group(HORIZONTAL, BREAK)} prints as {@code " "} *
  • {@code group(VERTICAL, BREAK)} prints as {@code "\n"} *
*/ public static final Doc BREAK = breakWith(" "); /** * A break that is printed either as the empty string in horizontal mode, or as a newline in * vertical mode. *
    *
  • {@code group(HORIZONTAL, SOFT_BREAK)} prints as {@code ""} *
  • {@code group(VERTICAL, SOFT_BREAK)} prints as {@code "\n"} *
*/ public static final Doc SOFT_BREAK = breakWith(""); /** * Appends document {@code that} to the document, with no breaks between documents. For example, * {@code text("a").add(text("b c"))} prints as {@code "ab c"}. */ public Doc add(Doc that) { if (this == Doc.EMPTY) { return that; } if (that == Doc.EMPTY) { return this; } return new Concat(this, that); } /** Appends a list of documents, with no breaks between documents. */ public static Doc append(Doc... docs) { return append(Arrays.asList(docs)); } /** Appends a list of documents, with no breaks between documents. */ public static Doc append(Iterable docs) { Doc output = EMPTY; for (Doc doc : docs) { output = output.add(doc); } return output; } /** * Appends documents, placing the separator {@code separator} between adjacent documents. * For example, *
   * joinWith(append(COMMA, BREAK), text("a"), text("b"), text("c")))
   * 
* prints as {@code "a, b, c"} in horizontal mode, or as {@code "a,\nb,\nc"} in vertical mode. */ public static Doc joinWith(Doc separator, Doc... docs) { return joinWith(separator, Arrays.asList(docs)); } /** Appends documents, placing the separator {@code separator} between adjacent documents. */ public static Doc joinWith(Doc separator, Iterable docs) { Doc output = EMPTY; boolean first = true; for (Doc doc : docs) { if (!first) { output = output.add(separator); } output = output.add(doc); first = false; } return output; } /** Appends documents, placing a {@link #BREAK} between adjacent documents. */ public static Doc join(Doc... docs) { return joinWith(BREAK, Arrays.asList(docs)); } /** Appends documents, placing a {@link #BREAK} between adjacent documents. */ public static Doc join(Iterable docs) { return joinWith(BREAK, docs); } /** Describes how the breaks within a {@link #group group()} should be printed. */ public enum GroupKind { /** Breaks are printed as spaces. */ HORIZONTAL, /** Breaks are printed as newlines. */ VERTICAL, /** * The breaks in the group are all printed as spaces, if the result would not overflow a single * line, or as newlines, otherwise. */ FILL, /** * Each break is printed as a space, if that would not lead to overflow, or as a newline, * otherwise. A different decision is made for each break. */ AUTO } /** * Encloses document {@code child} in a group. Breaks are formatted according to the policy of the * closest enclosing group. * *

For example, consider formatting the document *

   * group(p, join(break(), text("xx"), text("yy"), text("zz"), text("ww")))
   * 
* under different policies {@code p} for different column widths. * * Under policy {@link GroupKind#HORIZONTAL}, all breaks in a group are printed as spaces, * regardless of whether that leads to an overflow: *
    *
  • Width 3: {@code "xx yy zz ww"} *
  • Width 80: {@code "xx yy zz ww"} *
* * Under policy {@link GroupKind#VERTICAL}, all breaks are printed as newlines: *
    *
  • Width 3: {@code "xx\nyy\nzz\nww"} *
  • Width 80: {@code "xx\nyy\nzz\nww"} *
* * Under policy {@link GroupKind#AUTO}, all breaks in the group are printed as spaces if the * result would not overflow a line, or as newlines, otherwise: *
    *
  • Width 3: {@code "xx\nyy\nzz\nww"} *
  • Width 80: {@code "xx yy zz ww"} *
* * Under policy {@link GroupKind#FILL}, each break is printed as a space if that would not lead to * overflow, or as a newline, otherwise. Unlike {@link GroupKind#AUTO}, a separate decision is * made for each break. *
    *
  • Width 3: {@code "xx\nyy\nzz\nww"} *
  • Width 5: {@code "xx yy\nzz ww"} *
  • Width 80: {@code "xx yy zz ww"} *
*/ public static Doc group(GroupKind kind, Doc child) { return new Group(child, kind); } /** Encloses document {@code child} in an {@link GroupKind#AUTO} group. */ public static Doc group(Doc child) { return group(GroupKind.AUTO, child); } /** Encloses document {@code child} in an {@link GroupKind#VERTICAL} group. */ public static Doc vgroup(Doc child) { return group(GroupKind.VERTICAL, child); } /** Encloses document {@code child} in an {@link GroupKind#HORIZONTAL} group. */ public static Doc hgroup(Doc child) { return group(GroupKind.HORIZONTAL, child); } /** Encloses document {@code child} in an {@link GroupKind#FILL} group. */ public static Doc fgroup(Doc child) { return group(GroupKind.FILL, child); } /** Encloses the document in an {@link GroupKind#AUTO} group. */ public Doc group() { return group(this); } /** Encloses the document in an {@link GroupKind#VERTICAL} group. */ public Doc vgroup() { return vgroup(this); } /** Encloses the document in an {@link GroupKind#HORIZONTAL} group. */ public Doc hgroup() { return hgroup(this); } /** Encloses the document in an {@link GroupKind#FILL} group. */ public Doc fgroup() { return fgroup(this); } /** Encloses the document in an group of a given {@code kind}. */ public Doc group(GroupKind kind) { return group(kind, this); } /** * Increases the indentation level of document {@code child} by {@code indent} columns. * *

After each vertical break, a number of spaces equal to the current indentation level is * added. For example: *

   * nest(2, group(VERTICAL, join(BREAK, text("x"), text("y"), text("z"))))
   * 
* prints as: *
   * x
   *   y
   *   z
   * 
*

Note that indentation is only relevant for vertical breaks; for example *

   * nest(2, group(HORIZONTAL, join(BREAK, text("x"), text("y"), text("z"))))
   * 
* prints as: *
   * x y z
   * 
* Indentation is cumulative, for example: *
   * nest(2, nest(2, group(VERTICAL,
   *                       join(BREAK, text("x"), text("y"), text("z")))))
   * 
* prints as: *
   * x
   *     y
   *     z
   * 
*/ public static Doc nest(int indent, Doc child) { return new Nest(indent, child); } /** Increases the indentation level of the document by {@code indent} columns. */ public Doc nest(int indent) { return nest(indent, this); } /** Increase the indentation level of document {@code child} by {@link #DEFAULT_INDENT} spaces. */ public static Doc nest(Doc child) { return new Nest(DEFAULT_INDENT, child); } /** Increase the indentation level of the document by {@link #DEFAULT_INDENT} spaces. */ public Doc nest() { return nest(this); } /** * Aligns breaks in document {@code child} at the current column. For example: *
   * append(text("alist = ["),
   *        align(join(append(text(","), BREAK),
   *                   text("x"), text("y"), text("z"))),
   *        text("]"))
   * 
* prints in horizontal mode as: *
   * alist = [x, y, z]
   * 
* and prints in vertical mode as: *
   * alist = [x,
   *          y,
   *          z]
   * 
*/ public static Doc align(Doc child) { return new Align(child); } /** Aligns breaks in the document at the current column. */ public Doc align() { return align(this); } /** * Performs a hanging indentation of document {@code child} with an indent of {@code indent} * spaces. For example *
   * append(text("alist = ["),
   *        hang(join(append(text(","), BREAK),
   *                  text("x"), text("y"), text("z"))),
   *        text("]"))
   * 
* prints in vertical mode as: *
   * alist = [x,
   *            y,
   *            z]
   * 
* {@code hang(indent, child)} is a shorthand for {@code align(nest(indent, child))}. */ public static Doc hang(int indent, Doc child) { return nest(indent, child).align(); } /** * Performs a hanging indentation of document {@code child} with an indent of * {@link #DEFAULT_INDENT} spaces. */ public static Doc hang(Doc child) { return hang(DEFAULT_INDENT, child); } /** Performs a hanging indentation of the document with an indent of {@code indent} spaces. */ public Doc hang(int indent) { return hang(indent, this); } /** * Performs a hanging indentation of the document with an indent of {@link #DEFAULT_INDENT} * spaces. */ public Doc hang() { return hang(this); } /** * Sets the indentation level of document {@code child} to {@code indent} columns. */ public static Doc indentAt(int indent, Doc child) { return new IndentAt(indent, child); } /** Sets the indentation level of the document to {@code indent} columns. */ public Doc indentAt(int indent) { return indentAt(indent, this); } /** * Wraps document {@code child} in brackets. * For example, {@code brackets(text("x"))} prints as {@code "[x]"}. */ public static Doc brackets(Doc child) { return LBRACKET.add(child).add(RBRACKET); } /** Wraps the document in brackets. */ public Doc brackets() { return brackets(this); } /** * Wraps document {@code child} in parentheses. * For example, {@code parens(text("x"))} prints as {@code "(x)"}. */ public static Doc parens(Doc child) { return LPAREN.add(child).add(RPAREN); } /** Wraps the document in parentheses. */ public Doc parens() { return parens(this); } /** * Wraps document {@code child} in braces. * For example, {@code braces(text("x"))} prints as {@code "{x}"}. */ public static Doc braces(Doc child) { return LBRACE.add(child).add(RBRACE); } /** Wraps the document in braces. */ public Doc braces() { return braces(this); } /** * Wraps document {@code child} in angle brackets. * For example, {@code angles(text("x"))} prints as {@code ""}. */ public static Doc angles(Doc child) { return LANGLE.add(child).add(RANGLE); } /** Wraps the document in angle brackets. */ public Doc angles() { return angles(this); } /** * ANSI color codes. */ public enum AnsiColor { RED("\033[31m"), MAGENTA("\033[35m"), YELLOW("\033[33m"), DEFAULT(""), RESET("\033[0m"); private final String code; private AnsiColor(String code) { this.code = code; } public String code() { return code; } } /** * Colors document {@code child} using the specified ANSI color code. * * TODO(user): nested colors do not work. */ public static Doc color(AnsiColor c, Doc child) { return (new Text(c.code(), 0)) .add(child) .add(new Text(AnsiColor.RESET.code(), 0)); } /** Colors the document using the specified ANSI color code. */ public Doc color(AnsiColor c) { return color(c, this); } /** Appends a separator to all documents except the last one in a list. */ public static List punctuate(Doc separator, List docs) { List output = Lists.newArrayList(); for (int i = 0; i < docs.size(); i++) { if (i < docs.size() - 1) { output.add(docs.get(i).add(separator)); } else { output.add(docs.get(i)); } } return output; } /** * Returns a builder for a block which breaks vertically as *
   *   header
   *     line
   *     line
   *     ...
   *   [footer]
   * 
*/ public static BlockBuilder blockBuilder(Doc header, int indent) { return new BlockBuilder(header, indent); } /** * Returns a builder for a block with indentation of 2. */ public static BlockBuilder blockBuilder(Doc header) { return new BlockBuilder(header, 2); } /** * A builder for block-like constructs. */ public static class BlockBuilder { private final Doc header; private final int indent; private final List lines = Lists.newArrayList(); private BlockBuilder(Doc header, int indent) { this.header = header; this.indent = indent; } public BlockBuilder add(Doc line) { lines.add(line); return this; } public BlockBuilder addAll(Iterable lines) { for (Doc line : lines) { this.lines.add(line); } return this; } public Doc build(@Nullable Doc footer) { // Build group which breaks vertically as // header // line // line // ... Doc headerAndLinesGroup = header.add(Doc.BREAK).add(Doc.join(lines)).vgroup().nest(indent); // Add footer if given underneath the header/lines group // header // line // ... // footer if (footer != null) { return Doc.join(headerAndLinesGroup, footer).vgroup(); } return headerAndLinesGroup; } public Doc build() { return build(null); } } /** * Build a function invocation as *
   *   function ( arg1, BREAK arg2, BREAK ... argn )
   * 
* i.e. a break is only allowed after the first argument and before the last argument. */ public static Doc invocation(int indent, Doc function, Iterable arguments) { return function.add(Doc.parens(Doc.joinWith(COMMA_AND_BREAK, arguments).group().nest(indent))); } /** * Build a function invocation with indentation of 4 for arguments. */ public static Doc invocation(Doc function, Iterable arguments) { return invocation(4, function, arguments); } /** * Build a binary operator invocation as *
   *   left BREAK operator SPACE right
   * 
*/ public static Doc binary(int indent, Doc left, Doc operator, Doc right) { return left.add(Doc.BREAK).add(operator).add(SPACE).add(right).group().nest(indent); } /** * Builds a binary with indentation of 4. */ public static Doc binary(Doc left, Doc operator, Doc right) { return binary(4, left, operator, right); } /** * Determines whether the document is empty except of whitespace. */ public abstract boolean isWhitespace(); /** * A layout mode describes how to break a group, and is either HORIZONTAL, VERTICAL, or FILL. * Layout modes are only used as part of the layout state machine; AUTO is never used as a layout * state. */ private enum LayoutMode { HORIZONTAL, VERTICAL, FILL } /** * An agendum represents an entry in the stack of documents that need to be fitted or formatted. */ private static class Agendum { final int indentation; final LayoutMode mode; final Doc doc; Agendum(int indentation, LayoutMode mode, Doc doc) { this.indentation = indentation; this.mode = mode; this.doc = doc; } } /** * Pretty prints the document into a {@link StringBuilder}, formatted for {@code width} columns. */ public void prettyPrint(StringBuilder builder, int width) { Deque agenda = new ArrayDeque(); agenda.push(new Agendum(0, LayoutMode.HORIZONTAL, this)); int consumed = 0; while (!agenda.isEmpty()) { Agendum agendum = agenda.pop(); consumed = agendum.doc.format(builder, agenda, width, agendum.indentation, consumed, agendum.mode); } } /** * Pretty prints the document into a {@link StringBuilder}, formatted for {@code DEFAULT_WIDTH} * columns. */ public void prettyPrint(StringBuilder builder) { prettyPrint(builder, DEFAULT_WIDTH); } /** * Pretty prints the document, formatted for {@code width} columns. Returns the result as a * string. */ public String prettyPrint(int width) { StringBuilder builder = new StringBuilder(); prettyPrint(builder, width); return builder.toString(); } /** * Pretty prints the document, formatted for {@code DEFAULT_WIDTH} columns. Returns the result as * a string. */ public String prettyPrint() { return prettyPrint(DEFAULT_WIDTH); } @Override public String toString() { return prettyPrint(); } /* Everything below this line is internal to the pretty printer. * * Internally, there are six ways to form a pretty-printer document. * Concat: the concatenation of two documents * Text: a literal string * Nest: increase the nesting level of an inner document * Align: align breaks to the current column * Break: a break, which is printed either as a space or a line break * Group: a group is a unit in which all breaks should be printed according * to a common breaking policy. */ /** The state manipulated by the fits() method. */ private static class FitsState { /** Did the fits() method reach a line break or overflow the line? */ boolean done; /** How many columns remain on the current line? */ int width; FitsState(int width) { this.width = width; this.done = false; } } /** * Determines if this document fits in {@code state.width} columns, up to the next newline. * Updates {@code state.width} to the new number of columns, and sets {@code state.done} to true * if a newline is encountered or the line overflows. */ protected abstract void fits(LayoutMode mode, FitsState state); /** * Determines whether the documents in {@code agenda} fit in {@code width} columns, up to the next * newline. */ private static boolean agendaFits(Deque agenda, int width) { FitsState state = new FitsState(width); Iterator iterator = agenda.iterator(); while (iterator.hasNext() && !state.done && state.width >= 0) { Agendum agendum = iterator.next(); agendum.doc.fits(agendum.mode, state); } return state.width >= 0; } /** * Pretty-print a document into a {@link StringBuilder}. Updates the agenda, and returns an * updated number of consumed characters. * * @param agenda the document layout worklist. * @param width number of columns in the target layout (e.g., 80) * @param indentation current number of columns of indentation. * @param consumed number of columns already used in the current line. * @param mode the current layout mode. * @return an updated number of consumed characters. */ protected abstract int format(StringBuilder builder, Deque agenda, int width, int indentation, int consumed, LayoutMode mode); /** The concatenation of two documents. */ private static class Concat extends Doc { /** The first document. */ final Doc left; /** The second document. */ final Doc right; Concat(Doc left, Doc right) { this.left = left; this.right = right; } @Override protected void fits(LayoutMode mode, FitsState state) { left.fits(mode, state); if (!state.done) { right.fits(mode, state); } } @Override protected int format(StringBuilder builder, Deque agenda, int width, int indentation, int consumed, LayoutMode mode) { agenda.push(new Agendum(indentation, mode, right)); agenda.push(new Agendum(indentation, mode, left)); return consumed; } @Override public boolean isWhitespace() { // Concat tree grows on the left (implemented in Doc.add method), therefore to shorten the // recursion for the below AND operation, we should check the right node first. return right.isWhitespace() && left.isWhitespace(); } } /** Literal text. */ private static class Text extends Doc { /** The literal text to print. */ final String contents; /** * The width to use in size computations. By default this is simply the width of the text; * however when printing invisible characters (such as ANSI color codes) this may be set to * something different. */ final int length; Text(String contents) { this.contents = contents; this.length = contents.length(); } Text(String contents, int length) { this.contents = contents; this.length = length; } @Override protected void fits(LayoutMode mode, FitsState state) { state.width -= length; state.done = state.width < 0; } @Override protected int format(StringBuilder builder, Deque agenda, int width, int indentation, int consumed, LayoutMode mode) { builder.append(contents); return consumed + length; } @Override public boolean isWhitespace() { return WHITESPACE.matchesAllOf(contents); } } /** Increase indentation level. */ private static class Nest extends Doc { /** The increase in indentation. */ final int indent; /** The child document to indent. */ final Doc child; Nest(int indent, Doc child) { this.indent = indent; this.child = child; } @Override protected void fits(LayoutMode mode, FitsState state) { child.fits(mode, state); } @Override protected int format(StringBuilder builder, Deque agenda, int width, int indentation, int consumed, LayoutMode mode) { agenda.push(new Agendum(indentation + indent, mode, child)); return consumed; } @Override public boolean isWhitespace() { return child.isWhitespace(); } } /** Align breaks to the current column */ private static class Align extends Doc { /** The child document to align. */ final Doc child; Align(Doc child) { this.child = child; } @Override protected void fits(LayoutMode mode, FitsState state) { child.fits(mode, state); } @Override protected int format(StringBuilder builder, Deque agenda, int width, int indentation, int consumed, LayoutMode mode) { agenda.push(new Agendum(consumed, mode, child)); return consumed; } @Override public boolean isWhitespace() { return child.isWhitespace(); } } /** A break. */ private static class Break extends Doc { /** The representation of the break in horizontal mode. Ignored in vertical mode. */ final String representation; Break(String representation) { this.representation = representation; } @Override protected void fits(LayoutMode mode, FitsState state) { switch (mode) { case HORIZONTAL: state.width -= representation.length(); state.done = state.width < 0; break; default: state.done = true; } } @Override protected int format(StringBuilder builder, Deque agenda, int width, int indentation, int consumed, LayoutMode mode) { boolean horizontal; switch (mode) { case HORIZONTAL: horizontal = true; break; case VERTICAL: horizontal = false; break; default: horizontal = agendaFits(agenda, width - consumed - representation.length()); } if (horizontal) { builder.append(representation); return consumed + representation.length(); } else { // Drop trailing WS int i = builder.length() - 1; while (i >= 0 && WHITESPACE.matches(builder.charAt(i))) { builder.deleteCharAt(i--); } builder.append(String.format("%n")); builder.append(Strings.repeat(" ", indentation)); return indentation; } } @Override public boolean isWhitespace() { return representation.trim().length() == 0; } } /** A group, in which Breaks share a common breaking policy */ private static class Group extends Doc { /** * The contents of this Group. This Group's breaking policy controls the layout of any Breaks in * the child document for which this Group is the closest enclosing Group. */ final Doc child; /** The kind (breaking policy) for this group. */ final GroupKind kind; Group(Doc child, GroupKind kind) { this.child = child; this.kind = kind; } @Override protected void fits(LayoutMode mode, FitsState state) { child.fits(LayoutMode.HORIZONTAL, state); } @Override protected int format(StringBuilder builder, Deque agenda, int width, int indentation, int consumed, LayoutMode mode) { switch (kind) { case HORIZONTAL: agenda.push(new Agendum(indentation, LayoutMode.HORIZONTAL, child)); break; case VERTICAL: agenda.push(new Agendum(indentation, LayoutMode.VERTICAL, child)); break; case FILL: agenda.push(new Agendum(indentation, LayoutMode.FILL, child)); break; case AUTO: // Try horizontal first; if that doesn't fit, then use vertical. agenda.push(new Agendum(indentation, LayoutMode.HORIZONTAL, child)); if (!agendaFits(agenda, width - consumed)) { agenda.pop(); agenda.push(new Agendum(indentation, LayoutMode.VERTICAL, child)); } break; } return consumed; } @Override public boolean isWhitespace() { return child.isWhitespace(); } } /** Set a constant indentation level. */ private static class IndentAt extends Doc { /** The indentation level. */ final int indent; /** The child document to indent. */ final Doc child; IndentAt(int indent, Doc child) { this.indent = indent; this.child = child; } @Override protected void fits(LayoutMode mode, FitsState state) { child.fits(mode, state); } @Override protected int format(StringBuilder builder, Deque agenda, int width, int indentation, int consumed, LayoutMode mode) { agenda.push(new Agendum(indent, mode, child)); return consumed + indent; } @Override public boolean isWhitespace() { return child.isWhitespace(); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy