blob: a0b5dccd6715b82b7035db60d12d5485087f2684 [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You 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 org.apache.royale.formatter;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.io.IOUtils;
import org.apache.royale.compiler.internal.parsing.mxml.MXMLToken;
import org.apache.royale.compiler.internal.parsing.mxml.MXMLTokenizer;
import org.apache.royale.compiler.parsing.IMXMLToken;
import org.apache.royale.compiler.parsing.MXMLTokenTypes;
import org.apache.royale.compiler.problems.ICompilerProblem;
import org.apache.royale.compiler.problems.UnexpectedExceptionProblem;
import org.apache.royale.formatter.internal.BaseTokenFormatter;
public class MXMLTokenFormatter extends BaseTokenFormatter {
private static final int TOKEN_TYPE_EXTRA = 999999;
private static final Pattern SCRIPT_START_PATTERN = Pattern.compile("<((?:mx|fx):(Script|Metadata))");
private static final String FORMATTER_TAG_OFF = "@formatter:off";
private static final String FORMATTER_TAG_ON = "@formatter:on";
public MXMLTokenFormatter(FormatterSettings settings) {
super(settings);
}
private int indent;
private int numRequiredNewLines;
private boolean requiredSpace;
private boolean inOpenTag;
private boolean inCloseTag;
private boolean skipFormatting;
private String attributeIndent;
private IMXMLToken prevToken;
private IMXMLToken prevTokenOrExtra;
private IMXMLToken token;
private IMXMLToken nextToken;
private List<ElementStackItem> elementStack;
public String format(String filePath, String text, Collection<ICompilerProblem> problems) {
if (problems == null) {
problems = new ArrayList<ICompilerProblem>();
}
StringReader textReader = new StringReader(text);
MXMLTokenizer mxmlTokenizer = new MXMLTokenizer();
IMXMLToken[] originalTokens = null;
try {
originalTokens = mxmlTokenizer.getTokens(textReader);
} finally {
IOUtils.closeQuietly(textReader);
IOUtils.closeQuietly(mxmlTokenizer);
}
if (mxmlTokenizer.hasTokenizationProblems()) {
problems.addAll(mxmlTokenizer.getTokenizationProblems());
}
if (!settings.ignoreProblems && hasErrors(problems)) {
return text;
}
List<IMXMLToken> tokens = insertExtraMXMLTokens(originalTokens, text);
try {
return parseTokens(filePath, text, tokens, problems);
} catch (Exception e) {
if (problems != null) {
System.err.println(e);
e.printStackTrace(System.err);
problems.add(new UnexpectedExceptionProblem(e));
}
return text;
}
}
private String parseTokens(String filePath, String text, List<IMXMLToken> tokens, Collection<ICompilerProblem> problems) throws Exception {
indent = 0;
numRequiredNewLines = 0;
requiredSpace = false;
inOpenTag = false;
inCloseTag = false;
skipFormatting = false;
attributeIndent = "";
prevToken = null;
prevTokenOrExtra = null;
token = null;
nextToken = null;
elementStack = new ArrayList<ElementStackItem>();
StringBuilder builder = new StringBuilder();
for (int i = 0; i < tokens.size(); i++) {
token = tokens.get(i);
nextToken = null;
if (i < (tokens.size() - 1)) {
nextToken = tokens.get(i + 1);
}
if (token.getType() == TOKEN_TYPE_EXTRA) {
if (skipFormatting) {
builder.append(token.getText());
} else {
if (i == (tokens.size() - 1)) {
// if the last token is whitespace, include at most one
// new line, but strip the rest
numRequiredNewLines = Math.min(1, Math.max(0, countNewLinesInExtra(token)));
appendNewLines(builder, numRequiredNewLines);
break;
}
numRequiredNewLines = Math.max(numRequiredNewLines, countNewLinesInExtra(token));
}
prevTokenOrExtra = token;
continue;
} else if (token.getType() == MXMLTokenTypes.TOKEN_WHITESPACE) {
if (skipFormatting) {
builder.append(token.getText());
} else {
if (elementStack.isEmpty() || !elementStack.get(elementStack.size() - 1).containsText) {
numRequiredNewLines = Math.max(numRequiredNewLines, countNewLinesInExtra(token));
} else {
// if the parent element contains text, treat whitespace
// the same as text, and don't reformat it
// text is never reformatted because some components use it
// without collapsing whitespace, and developers would be
// confused if whitespace that they deliberately added were
// to be removed
builder.append(token.getText());
}
if (i == (tokens.size() - 1)) {
// if the last token is whitespace, include at most one
// new line, but strip the rest
numRequiredNewLines = Math.min(1, numRequiredNewLines);
appendNewLines(builder, numRequiredNewLines);
}
}
continue;
} else if (token.getType() == MXMLTokenTypes.TOKEN_OPEN_TAG_START
&& SCRIPT_START_PATTERN.matcher(token.getText()).matches()) {
if (prevToken != null && numRequiredNewLines > 0) {
appendNewLines(builder, numRequiredNewLines);
}
StringBuilder scriptBuilder = new StringBuilder();
scriptBuilder.append(token.getText());
boolean inScriptCloseTag = false;
while (i < (tokens.size() - 1)) {
i++;
token = tokens.get(i);
scriptBuilder.append(token.getText());
if (token.getType() == MXMLTokenTypes.TOKEN_CLOSE_TAG_START) {
inScriptCloseTag = true;
} else if (inScriptCloseTag && token.getType() == MXMLTokenTypes.TOKEN_TAG_END) {
break;
}
}
if (problems == null) {
// we need to know if there were problems because it means that we
// need to return the original, unformatted text
problems = new ArrayList<ICompilerProblem>();
}
builder.append(formatMXMLScriptElement(filePath, token.getLine(), scriptBuilder.toString(), problems));
if (hasErrors(problems)) {
return text;
}
prevToken = token;
prevTokenOrExtra = token;
requiredSpace = false;
numRequiredNewLines = 1;
continue;
}
// characters that must appear before the token
switch (token.getType()) {
case MXMLTokenTypes.TOKEN_OPEN_TAG_START: {
inOpenTag = true;
// if the parent contains text, children should be the same
boolean containsText = !elementStack.isEmpty()
&& elementStack.get(elementStack.size() - 1).containsText;
elementStack.add(new ElementStackItem(token, token.getText().substring(1), containsText));
break;
}
case MXMLTokenTypes.TOKEN_CLOSE_TAG_START: {
if (elementStack.isEmpty() || !elementStack.get(elementStack.size() - 1).containsText) {
indent = decreaseIndent(indent);
}
inCloseTag = true;
break;
}
case MXMLTokenTypes.TOKEN_NAME: {
requiredSpace = true;
break;
}
}
if (!skipFormatting && prevToken != null) {
if (numRequiredNewLines > 0) {
appendNewLines(builder, numRequiredNewLines);
appendIndent(builder, indent);
if (attributeIndent.length() > 0) {
builder.append(attributeIndent);
}
} else if (requiredSpace) {
builder.append(' ');
}
}
// include the token's own text
// no token gets reformatted before being appended
// whitespace is the only special case, but that's not handled here
builder.append(token.getText());
// characters that must appear after the token
requiredSpace = false;
numRequiredNewLines = 0;
switch (token.getType()) {
case MXMLTokenTypes.TOKEN_PROCESSING_INSTRUCTION: {
numRequiredNewLines = Math.max(numRequiredNewLines, 1);
break;
}
case MXMLTokenTypes.TOKEN_CLOSE_TAG_START: {
if (nextToken != null && nextToken.getType() != MXMLTokenTypes.TOKEN_TAG_END
&& nextToken.getType() != MXMLTokenTypes.TOKEN_EMPTY_TAG_END
&& nextToken.getType() != TOKEN_TYPE_EXTRA) {
requiredSpace = true;
}
if (elementStack.isEmpty()) {
// something is very wrong!
return text;
}
String elementName = token.getText().substring(2);
ElementStackItem elementItem = elementStack.remove(elementStack.size() - 1);
if (!elementName.equals(elementItem.elementName)) {
// there's a unclosed tag with a different name somewhere
return text;
}
break;
}
case MXMLTokenTypes.TOKEN_OPEN_TAG_START: {
if (nextToken != null && nextToken.getType() != MXMLTokenTypes.TOKEN_TAG_END
&& nextToken.getType() != MXMLTokenTypes.TOKEN_EMPTY_TAG_END) {
attributeIndent = getAttributeIndent(token);
if (nextToken.getType() != TOKEN_TYPE_EXTRA) {
requiredSpace = true;
}
}
break;
}
case MXMLTokenTypes.TOKEN_TAG_END: {
if (inOpenTag) {
ElementStackItem element = elementStack.get(elementStack.size() - 1);
if (!element.containsText) {
element.containsText = elementContainsText(tokens, i + 1, element.token);
}
if (elementStack.isEmpty() || !elementStack.get(elementStack.size() - 1).containsText) {
indent = increaseIndent(indent);
}
} else {
if (elementStack.isEmpty() || !elementStack.get(elementStack.size() - 1).containsText) {
numRequiredNewLines = Math.max(numRequiredNewLines, 1);
}
}
inOpenTag = false;
attributeIndent = "";
inCloseTag = false;
break;
}
case MXMLTokenTypes.TOKEN_EMPTY_TAG_END: {
if (inOpenTag) {
elementStack.remove(elementStack.size() - 1);
} else {
if (elementStack.isEmpty() || !elementStack.get(elementStack.size() - 1).containsText) {
numRequiredNewLines = Math.max(numRequiredNewLines, 1);
}
}
inOpenTag = false;
// no need to change nested indent after this tag
// however, we may need to remove attribute indent
attributeIndent = "";
// we shouldn't find an empty close tag, but clear flag anyway
inCloseTag = false;
break;
}
case MXMLTokenTypes.TOKEN_STRING: {
if (inOpenTag && settings.mxmlInsertNewLineBetweenAttributes && nextToken != null
&& nextToken.getType() != MXMLTokenTypes.TOKEN_TAG_END
&& nextToken.getType() != MXMLTokenTypes.TOKEN_EMPTY_TAG_END) {
numRequiredNewLines = Math.max(numRequiredNewLines, 1);
}
break;
}
case MXMLTokenTypes.TOKEN_COMMENT: {
String tokenText = token.getText();
String trimmed = tokenText.substring(4, tokenText.length() - 3).trim();
if (!skipFormatting && FORMATTER_TAG_OFF.equals(trimmed)) {
skipFormatting = true;
} else if (skipFormatting && FORMATTER_TAG_ON.equals(trimmed)) {
skipFormatting = false;
}
break;
}
}
prevToken = token;
prevTokenOrExtra = token;
}
return builder.toString();
}
private String formatMXMLScriptElement(String filePath, int line, String text,
Collection<ICompilerProblem> problems) {
String indent = "\t";
if (settings.insertSpaces) {
indent = "";
for (int i = 0; i < settings.tabSize; i++) {
indent += " ";
}
}
StringBuilder builder = new StringBuilder();
Pattern scriptPattern = Pattern.compile(
"^<((?:mx|fx):(\\w+))>\\s*(<!\\[CDATA\\[)?((?:.|(?:\\r?\\n))*?)(?:\\]\\]>)?\\s*<\\/(?:mx|fx):(?:\\w+)>$");
Matcher scriptMatcher = scriptPattern.matcher(text);
if (!scriptMatcher.matches()) {
return text;
}
if (problems == null) {
// we need to know if there were problems because it means that we
// need to return the original, unformatted text
problems = new ArrayList<ICompilerProblem>();
}
String scriptTagText = scriptMatcher.group(1);
String scriptTagName = scriptMatcher.group(2);
String cdataText = scriptMatcher.group(3);
String scriptText = scriptMatcher.group(4);
boolean requireCdata = cdataText != null || "Script".equals(scriptTagName);
ASTokenFormatter asFormatter = new ASTokenFormatter(settings);
String formattedScriptText = asFormatter.format(filePath + "@Script[" + line + "]", scriptText, problems);
if (!settings.ignoreProblems && hasErrors(problems)) {
return text;
}
if (formattedScriptText.length() > 0) {
String[] formattedLines = formattedScriptText.split("\n");
String lineIndent = requireCdata ? (indent + indent + indent) : (indent + indent);
for (int i = 0; i < formattedLines.length; i++) {
formattedLines[i] = lineIndent + formattedLines[i];
}
formattedScriptText = String.join("\n", formattedLines);
}
builder.append(indent);
builder.append("<");
builder.append(scriptTagText);
builder.append(">\n");
if (requireCdata) {
builder.append(indent);
builder.append(indent);
builder.append("<![CDATA[\n");
}
if (formattedScriptText.length() > 0) {
builder.append(formattedScriptText);
builder.append("\n");
}
if (requireCdata) {
builder.append(indent);
builder.append(indent);
builder.append("]]>\n");
}
builder.append(indent);
builder.append("</");
builder.append(scriptTagText);
builder.append(">");
return builder.toString();
}
private boolean elementContainsText(List<IMXMLToken> tokens, int startIndex, IMXMLToken openTagToken) {
ArrayList<IMXMLToken> elementStack = new ArrayList<IMXMLToken>();
elementStack.add(openTagToken);
for (int i = startIndex; i < tokens.size(); i++) {
IMXMLToken token = tokens.get(i);
switch (token.getType()) {
case MXMLTokenTypes.TOKEN_TEXT: {
if (elementStack.size() == 1) {
return true;
}
break;
}
case MXMLTokenTypes.TOKEN_OPEN_TAG_START: {
elementStack.add(token);
break;
}
case MXMLTokenTypes.TOKEN_EMPTY_TAG_END: {
elementStack.remove(elementStack.size() - 1);
if (elementStack.size() == 0) {
return false;
}
break;
}
case MXMLTokenTypes.TOKEN_CLOSE_TAG_START: {
elementStack.remove(elementStack.size() - 1);
if (elementStack.size() == 0) {
return false;
}
break;
}
}
}
return false;
}
private int countNewLinesInExtra(IMXMLToken token) {
if (token == null
|| (token.getType() != MXMLTokenTypes.TOKEN_WHITESPACE && token.getType() != TOKEN_TYPE_EXTRA)) {
return 0;
}
int numNewLinesInWhitespace = 0;
String whitespace = token.getText();
int index = -1;
while ((index = whitespace.indexOf('\n', index + 1)) != -1) {
numNewLinesInWhitespace++;
}
return numNewLinesInWhitespace;
}
private String getAttributeIndent(IMXMLToken openTagToken) {
if (!settings.mxmlAlignAttributes) {
return getIndent();
}
int indentSize = openTagToken.getText().length() + 1;
String result = "";
while (indentSize >= settings.tabSize) {
result += getIndent();
indentSize -= settings.tabSize;
}
for (int i = 0; i < indentSize; i++) {
result += " ";
}
return result;
}
private List<IMXMLToken> insertExtraMXMLTokens(IMXMLToken[] originalTokens, String text) {
ArrayList<IMXMLToken> tokens = new ArrayList<IMXMLToken>();
IMXMLToken prevToken = null;
for (IMXMLToken token : originalTokens) {
if (prevToken != null) {
int start = prevToken.getEnd();
int end = token.getStart();
if (end > start) {
String tokenText = text.substring(start, end);
MXMLToken extraToken = new MXMLToken(TOKEN_TYPE_EXTRA, start, end, prevToken.getLine(),
prevToken.getColumn() + end - start, tokenText);
extraToken.setEndLine(token.getLine());
extraToken.setEndLine(token.getColumn());
tokens.add(extraToken);
}
}
tokens.add(token);
prevToken = token;
}
if (prevToken != null) {
int start = prevToken.getEnd();
int end = text.length();
if (end > start) {
String tokenText = text.substring(start, end);
MXMLToken extraToken = new MXMLToken(TOKEN_TYPE_EXTRA, start, end, prevToken.getLine(),
prevToken.getColumn() + end - start, tokenText);
extraToken.setEndLine(prevToken.getLine());
extraToken.setEndLine(prevToken.getColumn());
tokens.add(extraToken);
}
}
return tokens;
}
private static class ElementStackItem {
public ElementStackItem(IMXMLToken token, String elementName, boolean containsText) {
this.token = token;
this.elementName = elementName;
this.containsText = containsText;
}
public IMXMLToken token;
public String elementName;
public boolean containsText = false;
}
}