/*
* Copyright 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.template.soy.jssrc.dsl;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.template.soy.jssrc.dsl.CodeChunk.WithValue;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Set;
import javax.annotation.Nullable;
/**
* Helper class to keep track of state during a single call to {@link CodeChunk#getCode()},
* including the initial statements that have already been formatted
* and the current indentation level.
*/
final class FormattingContext implements AutoCloseable {
private final StringBuilder buf;
private final int initialSize;
private Scope curScope = new Scope(null /* parent */, false /* emitClosingBrace */);
private String curIndent;
private boolean nextAppendShouldStartNewLine = false;
FormattingContext() {
this(0 /* startingIndent */);
}
/** @param startingIndent The number of columns to consider the "baseline" indentation level. */
FormattingContext(int startingIndent) {
curIndent = Strings.repeat(" ", startingIndent);
buf = new StringBuilder(curIndent);
initialSize = curIndent.length();
}
FormattingContext append(String stuff) {
maybeIndent();
buf.append(stuff);
return this;
}
FormattingContext append(char c) {
maybeIndent();
buf.append(c);
return this;
}
/**
* Writes the initial statements for the {@code chunk} to the buffer. This is the only allowed
* direct caller of {@link CodeChunk#doFormatInitialStatements}.
*/
FormattingContext appendInitialStatements(CodeChunk chunk) {
if (shouldFormat(chunk)) {
chunk.doFormatInitialStatements(this);
}
return this;
}
/** Writes the output expression for the {@code value} to the buffer. */
FormattingContext appendOutputExpression(WithValue value) {
value.doFormatOutputExpr(this);
return this;
}
/** Writes all code for the {@code chunk} to the buffer. */
FormattingContext appendAll(CodeChunk chunk) {
appendInitialStatements(chunk);
if (chunk instanceof CodeChunk.WithValue
// Skip Composites and Declarations to prevent a spurious trailing variable name.
// TODO(brndn): migrate these classes to be CodeChunks (not CodeChunk.WithValues) and
// remove this logic.
&& !(chunk instanceof Composite)
&& !(chunk instanceof Declaration)) {
appendOutputExpression((CodeChunk.WithValue) chunk);
append(";");
endLine();
}
return this;
}
private boolean shouldFormat(CodeChunk chunk) {
boolean shouldFormat = !curScope.alreadyFormatted(chunk);
if (shouldFormat) {
curScope.formatted.add(chunk);
}
return shouldFormat;
}
FormattingContext enterBlock() {
maybeIndent();
buf.append('{');
curIndent = curIndent + " ";
endLine();
curScope = new Scope(curScope, true /* emitClosingBrace */);
return this;
}
/**
* For use only by {@link Switch#doFormatInitialStatements}. It's not an error for bodies of case
* clauses to be brace-delimited, but it is slightly less readable, so omit them.
*/
FormattingContext enterCaseBody() {
maybeIndent();
curIndent = curIndent + " ";
endLine();
curScope = new Scope(curScope, false /* emitClosingBrace */);
return this;
}
FormattingContext endLine() {
// To prevent spurious trailing whitespace, don't actually write the newline
// until the next call to append().
nextAppendShouldStartNewLine = true;
return this;
}
/**
* If this is the first call to {@link #append} since the last {@link #endLine},
* writes the newline and leading indentation.
*/
private void maybeIndent() {
if (nextAppendShouldStartNewLine) {
buf.append('\n').append(curIndent);
nextAppendShouldStartNewLine = false;
}
}
@Override
public String toString() {
return isEmpty() ? "" : buf.toString();
}
boolean isEmpty() {
return buf.length() == initialSize;
}
@Override
public void close() {
boolean emitClosingBrace = curScope.emitClosingBrace;
curScope = Preconditions.checkNotNull(curScope.parent);
Preconditions.checkState(!curIndent.isEmpty());
curIndent = curIndent.substring(2);
endLine();
if (emitClosingBrace) {
append('}');
}
}
/**
* Returns a FormattingContext representing the concatenation of this FormattingContext with
* {@code other}. For use only by {@link CodeChunk#getCode(int, OutputContext)}.
*/
FormattingContext concat(FormattingContext other) {
if (isEmpty()) {
return other;
} else if (other.isEmpty()) {
return this;
} else {
curIndent = ""; // don't serialize trailing whitespace in front of the next FormattingContext.
return append(other.toString());
}
}
/**
* {@link FormattingContext} needs to keep track of the conditional nesting structure
* in order to avoid, for example, formatting the initial statements of a code chunk
* in one branch and referencing the chunk in another. The scopes form a simple tree,
* built and torn down by {@link #enterBlock()} and {@link #close()} respectively.
* {@link FormattingContext#curScope} points to the current tip of the tree.
*/
private static final class Scope {
final Set<CodeChunk> formatted =
Collections.<CodeChunk>newSetFromMap(new IdentityHashMap<CodeChunk, Boolean>());
@Nullable
final Scope parent;
final boolean emitClosingBrace;
Scope(@Nullable Scope parent, boolean emitClosingBrace) {
this.parent = parent;
this.emitClosingBrace = emitClosingBrace;
}
boolean alreadyFormatted(CodeChunk chunk) {
return formatted.contains(chunk) || (parent != null && parent.alreadyFormatted(chunk));
}
}
}