/**
* Copyright (c) 2012-2016 André Bargull
* Alle Rechte vorbehalten / All Rights Reserved. Use is subject to license terms.
*
* <https://github.com/anba/es6draft>
*/
package com.github.anba.es6draft.repl;
import static com.github.anba.es6draft.runtime.AbstractOperations.Get;
import static com.github.anba.es6draft.runtime.AbstractOperations.IsCallable;
import static com.github.anba.es6draft.runtime.AbstractOperations.ToFlatString;
import static com.github.anba.es6draft.runtime.AbstractOperations.ToUint32;
import java.util.HashSet;
import java.util.Iterator;
import java.util.regex.Pattern;
import com.github.anba.es6draft.runtime.ExecutionContext;
import com.github.anba.es6draft.runtime.internal.ScriptException;
import com.github.anba.es6draft.runtime.internal.Strings;
import com.github.anba.es6draft.runtime.objects.date.DateObject;
import com.github.anba.es6draft.runtime.objects.date.DatePrototype;
import com.github.anba.es6draft.runtime.objects.text.RegExpObject;
import com.github.anba.es6draft.runtime.objects.text.RegExpPrototype;
import com.github.anba.es6draft.runtime.types.BuiltinSymbol;
import com.github.anba.es6draft.runtime.types.Callable;
import com.github.anba.es6draft.runtime.types.Property;
import com.github.anba.es6draft.runtime.types.ScriptObject;
import com.github.anba.es6draft.runtime.types.Symbol;
import com.github.anba.es6draft.runtime.types.Type;
import com.github.anba.es6draft.runtime.types.builtins.ArrayObject;
/**
*
*/
public final class SourceBuilder {
private static final int MAX_STACK_DEPTH = 5;
private static final int MAX_OBJECT_PROPERTIES = 30;
private static final int MAX_ARRAY_PROPERTIES = 80;
private static final SourceBuilder INSTANCE = new SourceBuilder(false);
private final boolean colored;
private final int maxStackDepth;
private final int maxObjectProperties;
private final int maxArrayProperties;
SourceBuilder(boolean colored) {
this(colored, MAX_STACK_DEPTH, MAX_OBJECT_PROPERTIES, MAX_ARRAY_PROPERTIES);
}
SourceBuilder(boolean colored, int maxStackDepth, int maxObjectProperties, int maxArrayProperties) {
this.colored = colored;
this.maxStackDepth = maxStackDepth;
this.maxObjectProperties = maxObjectProperties;
this.maxArrayProperties = maxArrayProperties;
}
private enum AnsiAttribute {
Reset(0), Bold(1), Underline(4), Negative(7), NormalIntensity(22), UnderlineNone(24), Positive(27),
TextColor(30), DefaultTextColor(39), BackgroundColor(40), DefaultBackgroundColor(49), TextColorHi(90),
BackgroundColorHi(100);
final int code;
private AnsiAttribute(int code) {
this.code = code;
}
int color(AnsiColor color) {
return code + color.offset;
}
}
private enum AnsiColor {
Black(0), Red(1), Green(2), Yellow(3), Blue(4), Magenta(5), Cyan(6), White(7);
final int offset;
private AnsiColor(int offset) {
this.offset = offset;
}
}
private enum Style {/* @formatter:off */
Special(AnsiAttribute.TextColor.color(AnsiColor.Cyan), AnsiAttribute.DefaultTextColor),
Number(AnsiAttribute.TextColor.color(AnsiColor.Yellow), AnsiAttribute.DefaultTextColor),
Boolean(AnsiAttribute.TextColor.color(AnsiColor.Yellow), AnsiAttribute.DefaultTextColor),
Undefined(AnsiAttribute.TextColorHi.color(AnsiColor.Black), AnsiAttribute.DefaultTextColor),
Null(AnsiAttribute.Bold, AnsiAttribute.NormalIntensity),
String(AnsiAttribute.TextColor.color(AnsiColor.Green), AnsiAttribute.DefaultTextColor),
Symbol(AnsiAttribute.TextColor.color(AnsiColor.Green), AnsiAttribute.DefaultTextColor),
SIMD(AnsiAttribute.TextColor.color(AnsiColor.Yellow), AnsiAttribute.DefaultTextColor),
Date(AnsiAttribute.TextColor.color(AnsiColor.Magenta), AnsiAttribute.DefaultTextColor),
RegExp(AnsiAttribute.TextColor.color(AnsiColor.Red), AnsiAttribute.DefaultTextColor),
;
/* @formatter:on */
final int on, off;
private Style(AnsiAttribute on, AnsiAttribute off) {
this(on.code, off.code);
}
private Style(int on, AnsiAttribute off) {
this(on, off.code);
}
private Style(int on, int off) {
this.on = on;
this.off = off;
}
}
/**
* Returns the simple mode, source representation for {@code val}.
*
* @param cx
* the execution context
* @param val
* the value
* @return the source representation of the value
*/
public static String ToSource(ExecutionContext cx, Object val) {
return INSTANCE.toSource(cx, val);
}
/**
* Returns the source representation for {@code val}.
*
* @param cx
* the execution context
* @param val
* the value
* @return the source representation of the value
*/
String toSource(ExecutionContext cx, Object val) {
HashSet<ScriptObject> stack = new HashSet<>();
return toSource(cx, stack, val);
}
private String toSource(ExecutionContext cx, HashSet<ScriptObject> stack, Object value) {
if (Type.isObject(value)) {
ScriptObject objValue = Type.objectValue(value);
Object toSource = Get(cx, objValue, "toSource");
if (IsCallable(toSource)) {
return ToFlatString(cx, ((Callable) toSource).call(cx, objValue));
}
}
return format(source(cx, stack, value), style(stack, value));
}
private String format(String source, Style style) {
if (!colored || style == null) {
return source;
}
return String.format("\u001B[%dm%s\u001B[%d;%dm", style.on, source, AnsiAttribute.Reset.code, style.off);
}
private static Style style(HashSet<ScriptObject> stack, Object value) {
switch (Type.of(value)) {
case Undefined:
return Style.Undefined;
case Null:
return Style.Null;
case Boolean:
return Style.Boolean;
case String:
return Style.String;
case Number:
return Style.Number;
case Symbol:
return Style.Symbol;
case SIMD:
return Style.SIMD;
case Object:
default:
if (IsCallable(value)) {
return Style.Special;
}
if (stack.contains(value)) {
return Style.Special;
}
if (value instanceof DateObject) {
return Style.Date;
}
if (value instanceof RegExpObject) {
return Style.RegExp;
}
return null;
}
}
private String source(ExecutionContext cx, HashSet<ScriptObject> stack, Object value) {
switch (Type.of(value)) {
case Null:
return "null";
case Boolean:
return Type.booleanValue(value) ? "true" : "false";
case String:
return Strings.quote(Type.stringValue(value).toString());
case Symbol:
return Type.symbolValue(value).toString();
case Number:
return ToFlatString(cx, value);
case SIMD:
return Type.simdValue(value).toString();
case Object:
ScriptObject objValue = Type.objectValue(value);
if (IsCallable(objValue)) {
return ((Callable) objValue).toSource(cx);
}
if (stack.contains(objValue) || stack.size() > maxStackDepth) {
return "« ... »";
}
stack.add(objValue);
try {
if (objValue instanceof DateObject) {
return DatePrototype.Properties.toString(cx, value).toString();
} else if (objValue instanceof RegExpObject) {
return RegExpPrototype.Properties.toString(cx, value).toString();
} else if (objValue instanceof ArrayObject) {
return arrayToSource(cx, stack, objValue);
} else {
return objectToSource(cx, stack, objValue);
}
} finally {
stack.remove(objValue);
}
case Undefined:
default:
return "(void 0)";
}
}
private String arrayToSource(ExecutionContext cx, HashSet<ScriptObject> stack, ScriptObject array) {
long len = ToUint32(cx, Get(cx, array, "length"));
if (len <= 0) {
return "[]";
}
int viewLen = (int) Math.min(len, maxArrayProperties);
StringBuilder properties = new StringBuilder();
for (int index = 0; index < viewLen; ++index) {
String value = toSource(cx, stack, Get(cx, array, index));
properties.append(", ").append(value);
}
if (viewLen < len) {
properties.append(", [...]");
}
properties.append(" ]").setCharAt(0, '[');
return properties.toString();
}
private String objectToSource(ExecutionContext cx, HashSet<ScriptObject> stack, ScriptObject object) {
Iterator<?> keys = object.ownKeys(cx);
if (!keys.hasNext()) {
return "{}";
}
StringBuilder properties = new StringBuilder();
for (int i = 0; keys.hasNext() && i < maxObjectProperties;) {
Object k = keys.next();
String key = propertyKeyToSource(cx, k);
Property prop;
try {
prop = object.getOwnProperty(cx, k);
} catch (ScriptException e) {
continue;
}
if (!prop.isEnumerable()) {
continue;
}
String value;
if (prop.isDataDescriptor()) {
value = toSource(cx, stack, prop.getValue());
} else {
value = accessorToSource(prop);
}
properties.append(", ").append(key).append(": ").append(value);
i += 1;
}
if (keys.hasNext()) {
properties.append(", [...]");
}
properties.append(" }").setCharAt(0, '{');
return properties.toString();
}
private static final Pattern namePattern = Pattern
.compile("\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*");
private String propertyKeyToSource(ExecutionContext cx, Object key) {
if (key instanceof String) {
String s = (String) key;
if (namePattern.matcher(s).matches()) {
return s;
}
return format(Strings.quote(s), Style.String);
}
assert key instanceof Symbol;
return String.format("[%s]", format(symbolDescription(cx, (Symbol) key), Style.Symbol));
}
private static String symbolDescription(ExecutionContext cx, Symbol symbol) {
for (BuiltinSymbol builtin : BuiltinSymbol.values()) {
if (builtin != BuiltinSymbol.NONE && builtin.get() == symbol) {
return symbol.getDescription();
}
}
String registered = cx.getRealm().getSymbolRegistry().getKey(symbol);
if (registered != null) {
return String.format("Symbol.for(%s)", Strings.quote(registered));
}
String description = symbol.getDescription();
if (description != null) {
return String.format("Symbol(%s)", Strings.quote(description));
}
return "Symbol()";
}
private String accessorToSource(Property accessor) {
String description;
if (accessor.getGetter() != null && accessor.getSetter() != null) {
description = "[Getter/Setter]";
} else if (accessor.getGetter() != null) {
description = "[Getter]";
} else if (accessor.getSetter() != null) {
description = "[Setter]";
} else {
description = "[]";
}
return format(description, Style.Special);
}
}