| //////////////////////////////////////////////////////////////////////////////// |
| // |
| // 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; |
| } |
| } |