瀏覽代碼

- Added bytecode implementation interface & support for it in the expression compiler

- Replaced StringUtils.unescape with a simpler implementation
- Added BuiltinTypeMembers to allow the compiler control over its tags
Stan Hebben 6 年之前
父節點
當前提交
6c510eb26b

+ 34
- 0
CodeModel/src/main/java/org/openzen/zenscript/codemodel/type/member/BuiltinTypeMembers.java 查看文件

@@ -0,0 +1,34 @@
1
+/*
2
+ * To change this license header, choose License Headers in Project Properties.
3
+ * To change this template file, choose Tools | Templates
4
+ * and open the template in the editor.
5
+ */
6
+package org.openzen.zenscript.codemodel.type.member;
7
+
8
+import org.openzen.zenscript.codemodel.Modifiers;
9
+import org.openzen.zenscript.codemodel.expression.ConstantIntExpression;
10
+import org.openzen.zenscript.codemodel.member.CasterMember;
11
+import org.openzen.zenscript.codemodel.member.builtin.ConstantGetterMember;
12
+import org.openzen.zenscript.codemodel.type.BasicTypeID;
13
+import org.openzen.zenscript.shared.CodePosition;
14
+
15
+/**
16
+ *
17
+ * @author Hoofdgebruiker
18
+ */
19
+public class BuiltinTypeMembers {
20
+	public static final ConstantGetterMember INT_GET_MIN_VALUE = new ConstantGetterMember("MIN_VALUE", position -> new ConstantIntExpression(position, Integer.MIN_VALUE));
21
+	public static final ConstantGetterMember INT_GET_MAX_VALUE = new ConstantGetterMember("MAX_VALUE", position -> new ConstantIntExpression(position, Integer.MAX_VALUE));
22
+	
23
+	public static final CasterMember INT_TO_BYTE = new CasterMember(CodePosition.BUILTIN, 0, BasicTypeID.BYTE);
24
+	public static final CasterMember INT_TO_SBYTE = new CasterMember(CodePosition.BUILTIN, 0, BasicTypeID.SBYTE);
25
+	public static final CasterMember INT_TO_SHORT = new CasterMember(CodePosition.BUILTIN, 0, BasicTypeID.SHORT);
26
+	public static final CasterMember INT_TO_USHORT = new CasterMember(CodePosition.BUILTIN, 0, BasicTypeID.USHORT);
27
+	public static final CasterMember INT_TO_UINT = new CasterMember(CodePosition.BUILTIN, Modifiers.MODIFIER_IMPLICIT, BasicTypeID.UINT);
28
+	public static final CasterMember INT_TO_LONG = new CasterMember(CodePosition.BUILTIN, Modifiers.MODIFIER_IMPLICIT, BasicTypeID.LONG);
29
+	public static final CasterMember INT_TO_ULONG = new CasterMember(CodePosition.BUILTIN, Modifiers.MODIFIER_IMPLICIT, BasicTypeID.ULONG);
30
+	public static final CasterMember INT_TO_FLOAT = new CasterMember(CodePosition.BUILTIN, Modifiers.MODIFIER_IMPLICIT, BasicTypeID.FLOAT);
31
+	public static final CasterMember INT_TO_DOUBLE = new CasterMember(CodePosition.BUILTIN, Modifiers.MODIFIER_IMPLICIT, BasicTypeID.DOUBLE);
32
+	public static final CasterMember INT_TO_CHAR = new CasterMember(CodePosition.BUILTIN, 0, BasicTypeID.CHAR);
33
+	public static final CasterMember INT_TO_STRING = new CasterMember(CodePosition.BUILTIN, Modifiers.MODIFIER_IMPLICIT, BasicTypeID.STRING);
34
+}

+ 4
- 0
CodeModel/src/main/java/org/openzen/zenscript/codemodel/type/member/DefinitionMemberGroup.java 查看文件

@@ -225,6 +225,10 @@ public class DefinitionMemberGroup {
225 225
 	
226 226
 	public Expression call(CodePosition position, TypeScope scope, Expression target, CallArguments arguments, boolean allowStaticUsage) {
227 227
 		ICallableMember method = selectMethod(position, scope, arguments, true, allowStaticUsage);
228
+		for (int i = 0; i < arguments.arguments.length; i++) {
229
+			arguments.arguments[i] = arguments.arguments[i].castImplicit(position, scope, method.getHeader().parameters[i].type);
230
+		}
231
+		
228 232
 		FunctionHeader instancedHeader = method.getHeader().withGenericArguments(scope.getTypeRegistry(), arguments.typeArguments);
229 233
 		return method.call(position, target, instancedHeader, arguments);
230 234
 	}

+ 13
- 13
CodeModel/src/main/java/org/openzen/zenscript/codemodel/type/member/TypeMemberBuilder.java 查看文件

@@ -270,20 +270,20 @@ public class TypeMemberBuilder implements ITypeVisitor<Void> {
270 270
 		registerArithmeticOperations(LONG, LONG);
271 271
 		registerArithmeticOperations(FLOAT, FLOAT);
272 272
 		registerArithmeticOperations(DOUBLE, DOUBLE);
273
-		members.addGetter(new ConstantGetterMember("MIN_VALUE", position -> new ConstantIntExpression(position, Integer.MIN_VALUE)), TypeMemberPriority.SPECIFIED);
274
-		members.addGetter(new ConstantGetterMember("MAX_VALUE", position -> new ConstantIntExpression(position, Integer.MAX_VALUE)), TypeMemberPriority.SPECIFIED);
273
+		members.addGetter(BuiltinTypeMembers.INT_GET_MIN_VALUE, TypeMemberPriority.SPECIFIED);
274
+		members.addGetter(BuiltinTypeMembers.INT_GET_MAX_VALUE, TypeMemberPriority.SPECIFIED);
275 275
 		
276
-		members.addCaster(new CasterMember(CodePosition.BUILTIN, 0, BasicTypeID.BYTE), TypeMemberPriority.SPECIFIED);
277
-		members.addCaster(new CasterMember(CodePosition.BUILTIN, 0, BasicTypeID.SBYTE), TypeMemberPriority.SPECIFIED);
278
-		members.addCaster(new CasterMember(CodePosition.BUILTIN, 0, BasicTypeID.SHORT), TypeMemberPriority.SPECIFIED);
279
-		members.addCaster(new CasterMember(CodePosition.BUILTIN, 0, BasicTypeID.USHORT), TypeMemberPriority.SPECIFIED);
280
-		members.addCaster(new CasterMember(CodePosition.BUILTIN, Modifiers.MODIFIER_IMPLICIT, BasicTypeID.UINT), TypeMemberPriority.SPECIFIED);
281
-		members.addCaster(new CasterMember(CodePosition.BUILTIN, Modifiers.MODIFIER_IMPLICIT, BasicTypeID.LONG), TypeMemberPriority.SPECIFIED);
282
-		members.addCaster(new CasterMember(CodePosition.BUILTIN, Modifiers.MODIFIER_IMPLICIT, BasicTypeID.ULONG), TypeMemberPriority.SPECIFIED);
283
-		members.addCaster(new CasterMember(CodePosition.BUILTIN, Modifiers.MODIFIER_IMPLICIT, BasicTypeID.FLOAT), TypeMemberPriority.SPECIFIED);
284
-		members.addCaster(new CasterMember(CodePosition.BUILTIN, Modifiers.MODIFIER_IMPLICIT, BasicTypeID.DOUBLE), TypeMemberPriority.SPECIFIED);
285
-		members.addCaster(new CasterMember(CodePosition.BUILTIN, 0, BasicTypeID.CHAR), TypeMemberPriority.SPECIFIED);
286
-		members.addCaster(new CasterMember(CodePosition.BUILTIN, Modifiers.MODIFIER_IMPLICIT, BasicTypeID.STRING), TypeMemberPriority.SPECIFIED);
276
+		members.addCaster(BuiltinTypeMembers.INT_TO_BYTE, TypeMemberPriority.SPECIFIED);
277
+		members.addCaster(BuiltinTypeMembers.INT_TO_SBYTE, TypeMemberPriority.SPECIFIED);
278
+		members.addCaster(BuiltinTypeMembers.INT_TO_SHORT, TypeMemberPriority.SPECIFIED);
279
+		members.addCaster(BuiltinTypeMembers.INT_TO_USHORT, TypeMemberPriority.SPECIFIED);
280
+		members.addCaster(BuiltinTypeMembers.INT_TO_UINT, TypeMemberPriority.SPECIFIED);
281
+		members.addCaster(BuiltinTypeMembers.INT_TO_LONG, TypeMemberPriority.SPECIFIED);
282
+		members.addCaster(BuiltinTypeMembers.INT_TO_ULONG, TypeMemberPriority.SPECIFIED);
283
+		members.addCaster(BuiltinTypeMembers.INT_TO_FLOAT, TypeMemberPriority.SPECIFIED);
284
+		members.addCaster(BuiltinTypeMembers.INT_TO_DOUBLE, TypeMemberPriority.SPECIFIED);
285
+		members.addCaster(BuiltinTypeMembers.INT_TO_CHAR, TypeMemberPriority.SPECIFIED);
286
+		members.addCaster(BuiltinTypeMembers.INT_TO_STRING, TypeMemberPriority.SPECIFIED);
287 287
 	}
288 288
 
289 289
 	private void visitUInt() {

+ 16
- 0
JavaBytecodeCompiler/src/main/java/org/openzen/zenscript/javabytecode/JavaBytecodeImplementation.java 查看文件

@@ -0,0 +1,16 @@
1
+/*
2
+ * To change this license header, choose License Headers in Project Properties.
3
+ * To change this template file, choose Tools | Templates
4
+ * and open the template in the editor.
5
+ */
6
+package org.openzen.zenscript.javabytecode;
7
+
8
+import org.openzen.zenscript.javabytecode.compiler.JavaWriter;
9
+
10
+/**
11
+ *
12
+ * @author Hoofdgebruiker
13
+ */
14
+public interface JavaBytecodeImplementation {
15
+	public void compile(JavaWriter writer);
16
+}

+ 19
- 0
JavaBytecodeCompiler/src/main/java/org/openzen/zenscript/javabytecode/JavaCompiler.java 查看文件

@@ -12,6 +12,7 @@ import org.objectweb.asm.Opcodes;
12 12
 import org.openzen.zenscript.codemodel.HighLevelDefinition;
13 13
 import org.openzen.zenscript.codemodel.ScriptBlock;
14 14
 import org.openzen.zenscript.codemodel.statement.Statement;
15
+import org.openzen.zenscript.codemodel.type.member.BuiltinTypeMembers;
15 16
 import org.openzen.zenscript.javabytecode.compiler.JavaStatementVisitor;
16 17
 import org.openzen.zenscript.javabytecode.compiler.JavaWriter;
17 18
 import org.openzen.zenscript.shared.SourceFile;
@@ -21,6 +22,24 @@ import org.openzen.zenscript.shared.SourceFile;
21 22
  * @author Hoofdgebruiker
22 23
  */
23 24
 public class JavaCompiler {
25
+	static {
26
+		JavaClassInfo jInteger = new JavaClassInfo("java/lang/Integer");
27
+		BuiltinTypeMembers.INT_GET_MIN_VALUE.setTag(JavaFieldInfo.class, new JavaFieldInfo(jInteger, "MIN_VALUE", "I"));
28
+		BuiltinTypeMembers.INT_GET_MAX_VALUE.setTag(JavaFieldInfo.class, new JavaFieldInfo(jInteger, "MAX_VALUE", "I"));
29
+		
30
+		BuiltinTypeMembers.INT_TO_BYTE.setTag(JavaBytecodeImplementation.class, writer -> writer.i2b());
31
+		BuiltinTypeMembers.INT_TO_SBYTE.setTag(JavaBytecodeImplementation.class, writer -> writer.i2b());
32
+		BuiltinTypeMembers.INT_TO_SHORT.setTag(JavaBytecodeImplementation.class, writer -> writer.i2s());
33
+		BuiltinTypeMembers.INT_TO_USHORT.setTag(JavaBytecodeImplementation.class, writer -> writer.i2s());
34
+		BuiltinTypeMembers.INT_TO_UINT.setTag(JavaBytecodeImplementation.class, writer -> {});
35
+		BuiltinTypeMembers.INT_TO_LONG.setTag(JavaBytecodeImplementation.class, writer -> writer.i2l());
36
+		BuiltinTypeMembers.INT_TO_ULONG.setTag(JavaBytecodeImplementation.class, writer -> writer.i2l());
37
+		BuiltinTypeMembers.INT_TO_FLOAT.setTag(JavaBytecodeImplementation.class, writer -> writer.i2f());
38
+		BuiltinTypeMembers.INT_TO_DOUBLE.setTag(JavaBytecodeImplementation.class, writer -> writer.i2d());
39
+		BuiltinTypeMembers.INT_TO_CHAR.setTag(JavaBytecodeImplementation.class, writer -> writer.i2s());
40
+		BuiltinTypeMembers.INT_TO_STRING.setTag(JavaMethodInfo.class, new JavaMethodInfo(jInteger, "toString", "(I)Ljava/lang/String;", true));
41
+	}
42
+	
24 43
 	private final JavaModule target;
25 44
 	private final List<String> scriptBlockNames = new ArrayList<>();
26 45
 	private final ClassWriter scriptsClassWriter;

+ 6
- 0
JavaBytecodeCompiler/src/main/java/org/openzen/zenscript/javabytecode/JavaMethodInfo.java 查看文件

@@ -13,10 +13,16 @@ public class JavaMethodInfo {
13 13
 	public final JavaClassInfo javaClass;
14 14
 	public final String name;
15 15
 	public final String signature;
16
+	public final boolean isStatic;
16 17
 	
17 18
 	public JavaMethodInfo(JavaClassInfo javaClass, String name, String signature) {
19
+		this(javaClass, name, signature, false);
20
+	}
21
+	
22
+	public JavaMethodInfo(JavaClassInfo javaClass, String name, String signature, boolean isStatic) {
18 23
 		this.javaClass = javaClass;
19 24
 		this.name = name;
20 25
 		this.signature = signature;
26
+		this.isStatic = isStatic;
21 27
 	}
22 28
 }

+ 18
- 5
JavaBytecodeCompiler/src/main/java/org/openzen/zenscript/javabytecode/compiler/JavaExpressionVisitor.java 查看文件

@@ -1,6 +1,7 @@
1 1
 package org.openzen.zenscript.javabytecode.compiler;
2 2
 
3 3
 import org.openzen.zenscript.codemodel.expression.*;
4
+import org.openzen.zenscript.javabytecode.JavaBytecodeImplementation;
4 5
 import org.openzen.zenscript.javabytecode.JavaFieldInfo;
5 6
 import org.openzen.zenscript.javabytecode.JavaMethodInfo;
6 7
 
@@ -29,20 +30,32 @@ public class JavaExpressionVisitor implements ExpressionVisitor<Void> {
29 30
 
30 31
     @Override
31 32
     public Void visitCall(CallExpression expression) {
32
-
33 33
         expression.target.accept(this);
34 34
         for (Expression argument : expression.arguments.arguments) {
35 35
             argument.accept(this);
36 36
         }
37 37
 		
38
+		JavaBytecodeImplementation implementation = expression.member.getTag(JavaBytecodeImplementation.class);
39
+		if (implementation != null) {
40
+			implementation.compile(javaWriter);
41
+			return null;
42
+		}
43
+		
38 44
 		JavaMethodInfo methodInfo = expression.member.getTag(JavaMethodInfo.class);
39 45
 		if (methodInfo == null)
40 46
 			throw new IllegalStateException("Call target has no method info!");
41 47
 		
42
-        javaWriter.invokeVirtual(
43
-				methodInfo.javaClass.internalClassName,
44
-				methodInfo.name,
45
-				methodInfo.signature);
48
+		if (methodInfo.isStatic) {
49
+			javaWriter.invokeStatic(
50
+					methodInfo.javaClass.internalClassName,
51
+					methodInfo.name,
52
+					methodInfo.signature);
53
+		} else {
54
+			javaWriter.invokeVirtual(
55
+					methodInfo.javaClass.internalClassName,
56
+					methodInfo.name,
57
+					methodInfo.signature);
58
+		}
46 59
 		
47 60
         return null;
48 61
     }

+ 3
- 3
Parser/src/main/java/org/openzen/zenscript/parser/expression/ParsedExpression.java 查看文件

@@ -29,7 +29,7 @@ import org.openzen.zenscript.parser.type.IParsedType;
29 29
 import org.openzen.zenscript.shared.CodePosition;
30 30
 import org.openzen.zenscript.shared.CompileException;
31 31
 import org.openzen.zenscript.shared.CompileExceptionCode;
32
-import static org.openzen.zenscript.shared.StringUtils.unescapeString;
32
+import static org.openzen.zenscript.shared.StringUtils.unescape;
33 33
 
34 34
 /**
35 35
  *
@@ -315,7 +315,7 @@ public abstract class ParsedExpression {
315 315
 						indexString2 = parser.optional(T_STRING_DQ);
316 316
 					
317 317
 					if (indexString2 != null) {
318
-						base = new ParsedExpressionMember(position, base, unescapeString(indexString2.content), Collections.emptyList());
318
+						base = new ParsedExpressionMember(position, base, unescape(indexString2.content), Collections.emptyList());
319 319
 					} else {
320 320
 						ZSToken last = parser.next();
321 321
 						throw new ParseException(last, "Invalid expression, last token: " + last.content);
@@ -364,7 +364,7 @@ public abstract class ParsedExpression {
364 364
 			case T_STRING_DQ:
365 365
 				return new ParsedExpressionString(
366 366
 						position,
367
-						unescapeString(parser.next().content));
367
+						unescape(parser.next().content));
368 368
 			case T_IDENTIFIER: {
369 369
 				String name = parser.next().content;
370 370
 				List<IParsedType> genericParameters = IParsedType.parseGenericParameters(parser);

+ 1
- 0
ScriptingExample/scripts/helloworld.zs 查看文件

@@ -1 +1,2 @@
1 1
 println("Hello world!");
2
+println(5);

+ 286
- 360
Shared/src/main/java/org/openzen/zenscript/shared/StringUtils.java 查看文件

@@ -5,390 +5,316 @@
5 5
  */
6 6
 package org.openzen.zenscript.shared;
7 7
 
8
+import java.io.IOException;
9
+import java.io.InputStream;
10
+import java.util.Collection;
11
+import java.util.HashMap;
12
+import java.util.Map;
13
+import java.util.Properties;
14
+import java.util.logging.Level;
15
+import java.util.logging.Logger;
16
+import java.util.regex.Pattern;
17
+
8 18
 /**
9 19
  *
10 20
  * @author Hoofdgebruiker
11 21
  */
12 22
 public class StringUtils {
23
+	private static final Map<String, CharacterEntity> NAMED_CHARACTER_ENTITIES;
24
+	private static final Pattern MATCH_ACCENTS = Pattern.compile("\\p{M}");
25
+	
26
+	static
27
+	{
28
+		NAMED_CHARACTER_ENTITIES = new HashMap<>();
29
+		
30
+		Properties properties = new Properties();
31
+		try {
32
+			InputStream input = String.class.getResourceAsStream("/org/openzen/zenscript/shared/characterEntities.properties");
33
+			if (input != null)
34
+				properties.load(input);
35
+			else
36
+				System.out.println("Warning: could not load character entities");
37
+		} catch (IOException ex) {
38
+			Logger.getLogger(StringUtils.class.getName()).log(Level.SEVERE, null, ex);
39
+		}
40
+		
41
+		for (Object okey : properties.keySet()) {
42
+			String key = okey.toString();
43
+			char value = (char) Integer.parseInt(properties.getProperty(key));
44
+			CharacterEntity entity = new CharacterEntity(key, value);
45
+			NAMED_CHARACTER_ENTITIES.put(entity.stringValue, entity);
46
+		}
47
+	}
13 48
 	
14 49
 	/**
50
+	 * Left pads (prefixes) a string with characters until it reaches the given string
51
+	 * length. Does not do anything if the string length &gt;= given length.
15 52
 	 * 
16
-	 * unescape_perl_string()
17
-	 * 
18
-	 * Tom Christiansen <tchrist@perl.com> Sun Nov 28 12:55:24 MST 2010
19
-	 * 
20
-	 * It's completely ridiculous that there's no standard unescape_java_string
21
-	 * function. Since I have to do the damn thing myself, I might as well make
22
-	 * it halfway useful by supporting things Java was too stupid to consider in
23
-	 * strings:
24
-	 * 
25
-	 * => "?" items are additions to Java string escapes but normal in Java
26
-	 * regexes
27
-	 * 
28
-	 * => "!" items are also additions to Java regex escapes
29
-	 * 
30
-	 * Standard singletons: ?\a ?\e \f \n \r \t
31
-	 * 
32
-	 * NB: \b is unsupported as backspace so it can pass-through to the regex
33
-	 * translator untouched; I refuse to make anyone doublebackslash it as
34
-	 * doublebackslashing is a Java idiocy I desperately wish would die out.
35
-	 * There are plenty of other ways to write it:
53
+	 * @param value value to be padded
54
+	 * @param length desired string length
55
+	 * @param c padding character
56
+	 * @return padded string
57
+	 */
58
+	public static String lpad(String value, int length, char c)
59
+	{
60
+		if (value.length() >= length)
61
+			return value;
62
+		
63
+		return times(c, length - value.length()) + value;
64
+	}
65
+	
66
+	/**
67
+	 * Right pads (suffixes) a string with characters until it reaches the given
68
+	 * string length. Does not do anything if the string length &gt;= given length.
36 69
 	 * 
37
-	 * \cH, \12, \012, \x08 \x{8}, \u0008, \U00000008
70
+	 * @param value value to be padded
71
+	 * @param length desired string length
72
+	 * @param c padding character
73
+	 * @return padded string
74
+	 */
75
+	public static String rpad(String value, int length, char c)
76
+	{
77
+		if (value.length() >= length)
78
+			return value;
79
+		
80
+		return value + times(c, length - value.length());
81
+	}
82
+	
83
+	/**
84
+	 * Constructs a string with count times the given character.
38 85
 	 * 
39
-	 * Octal escapes: \0 \0N \0NN \N \NN \NNN Can range up to !\777 not \377
86
+	 * @param c filling character
87
+	 * @param count character count
88
+	 * @return string value
89
+	 */
90
+	public static String times(char c, int count)
91
+	{
92
+		char[] value = new char[count];
93
+		for (int i = 0; i < count; i++) {
94
+			value[i] = c;
95
+		}
96
+		return new String(value);
97
+	}
98
+	
99
+	/**
100
+	 * Unescapes a string escaped in one of following ways:
40 101
 	 * 
41
-	 * TODO: add !\o{NNNNN} last Unicode is 4177777 maxint is 37777777777
102
+	 * <ul>
103
+	 * <li>A string escaped with single quotes (<code>'Hello "my" world'</code>)</li>
104
+	 * <li>A string escaped with double quotes (<code>"Hello 'my' world"</code>)</li>
105
+	 * <li>A near-literal string (<code>@"C:\Program Files\"</code>) in which escape sequences
106
+	 * aren't processed but the " character cannot occur</li>
107
+	 * </ul>
42 108
 	 * 
43
-	 * Control chars: ?\cX Means: ord(X) ^ ord('@')
109
+	 * The following escape sequences are recognized:
110
+	 * <ul>
111
+	 * <li>\\</li>
112
+	 * <li>\'</li>
113
+	 * <li>\"</li>
114
+	 * <li>\&amp;namedCharacterEntity; (note that although redundant, \&amp;#ddd; and \&amp;#xXXXX; are also allowed)</li>
115
+	 * <li>\t</li>
116
+	 * <li>\n</li>
117
+	 * <li>\r</li>
118
+	 * <li>\b</li>
119
+	 * <li>\f</li>
120
+	 * <li>\&amp;uXXXX for unicode character points</li>
121
+	 * </ul>
44 122
 	 * 
45
-	 * Old hex escapes: \xXX unbraced must be 2 xdigits
123
+	 * @param escapedString escaped string
124
+	 * @return unescaped string
125
+	 */
126
+	public static String unescape(String escapedString)
127
+	{
128
+		if (escapedString.length() < 2)
129
+			throw new IllegalArgumentException("String is not quoted");
130
+		
131
+		boolean isLiteral = escapedString.charAt(0) == '@';
132
+		if (isLiteral)
133
+			escapedString = escapedString.substring(1);
134
+		
135
+		if (escapedString.charAt(0) != '"' && escapedString.charAt(0) != '\'')
136
+			throw new IllegalArgumentException("String is not quoted");
137
+		
138
+		char quoteCharacter = escapedString.charAt(0);
139
+		if (escapedString.charAt(escapedString.length() - 1) != quoteCharacter)
140
+			throw new IllegalArgumentException("Unbalanced quotes");
141
+		
142
+		if (isLiteral)
143
+			return escapedString.substring(1, escapedString.length() - 1);
144
+		
145
+		StringBuilder result = new StringBuilder(escapedString.length() - 2);
146
+		
147
+		for (int i = 1; i < escapedString.length() - 1; i++) {
148
+			if (escapedString.charAt(i) == '\\') {
149
+				if (i >= escapedString.length() - 1)
150
+					throw new IllegalArgumentException("Unfinished escape sequence");
151
+				
152
+				switch (escapedString.charAt(i + 1)) {
153
+					case '\\': i++; result.append('\\'); break;
154
+					case '&':
155
+						CharacterEntity characterEntity = readCharacterEntity(escapedString, i + 1);
156
+						i += characterEntity.stringValue.length() + 2;
157
+						result.append(characterEntity.charValue);
158
+						break;
159
+					case 't': i++; result.append('\t'); break;
160
+					case 'r': i++; result.append('\r'); break;
161
+					case 'n': i++; result.append('\n'); break;
162
+					case 'b': i++; result.append('\b'); break;
163
+					case 'f': i++; result.append('\f'); break;
164
+					case '"': i++; result.append('\"'); break;
165
+					case '\'': i++; result.append('\''); break;
166
+					case 'u':
167
+						if (i >= escapedString.length() - 5)
168
+							throw new IllegalArgumentException("Unfinished escape sequence");
169
+						int hex0 = readHexCharacter(escapedString.charAt(i + 2));
170
+						int hex1 = readHexCharacter(escapedString.charAt(i + 3));
171
+						int hex2 = readHexCharacter(escapedString.charAt(i + 4));
172
+						int hex3 = readHexCharacter(escapedString.charAt(i + 5));
173
+						i += 5;
174
+						result.append((hex0 << 12) | (hex1 << 8) | (hex2 << 4) | hex3);
175
+					default:
176
+						throw new IllegalArgumentException("Illegal escape sequence");
177
+				}
178
+			}
179
+			else
180
+				result.append(escapedString.charAt(i));
181
+		}
182
+		
183
+		return result.toString();
184
+	}
185
+	
186
+	/**
187
+	 * Escapes special characters in the given string, including ". (but not ').
188
+	 * Adds opening and closing quotes.
46 189
 	 * 
47
-	 * Perl hex escapes: !\x{XXX} braced may be 1-8 xdigits NB: proper Unicode
48
-	 * never needs more than 6, as highest valid codepoint is 0x10FFFF, not
49
-	 * maxint 0xFFFFFFFF
190
+	 * @param value value to be escaped
191
+	 * @param quote character (' or ")
192
+	 * @param escapeUnicode true to escape any non-ascii value, false to leave them be
193
+	 * @return escaped value
194
+	 */
195
+	public static String escape(String value, char quote, boolean escapeUnicode)
196
+	{
197
+		StringBuilder output = new StringBuilder();
198
+		output.append(quote);
199
+		for (char c : value.toCharArray()) {
200
+			switch (c) {
201
+				case '"': if (quote == '"') output.append("\\\""); break;
202
+				case '\'': if (quote == '\'') output.append("\\\'"); break;
203
+				case '\n': output.append("\\n"); break;
204
+				case '\r': output.append("\\r"); break;
205
+				case '\t': output.append("\\t"); break;
206
+				default:
207
+					if (escapeUnicode && c > 127) {
208
+						output.append("\\u");
209
+						output.append(lpad(Integer.toHexString(c), 4, '0'));
210
+					} else {
211
+						output.append(c);
212
+					}
213
+			}
214
+		}
215
+		
216
+		output.append(quote);
217
+		return output.toString();
218
+	}
219
+	
220
+	/**
221
+	 * Reads a single hex digit and converts it to a value 0-15.
50 222
 	 * 
51
-	 * Lame Java escape: \[IDIOT JAVA PREPROCESSOR]uXXXX must be exactly 4
52
-	 * xdigits;
223
+	 * @param hex hex digit
224
+	 * @return converted value
225
+	 */
226
+	public static int readHexCharacter(char hex)
227
+	{
228
+		if (hex >= '0' && hex <= '9')
229
+			return hex - '0';
230
+		
231
+		if (hex >= 'A' && hex <= 'F')
232
+			return hex - 'A' + 10;
233
+		
234
+		if (hex >= 'a' && hex <= 'f')
235
+			return hex - 'a' + 10;
236
+		
237
+		throw new IllegalArgumentException("Illegal hex character: " + hex);
238
+	}
239
+	
240
+	/**
241
+	 * Retrieves all official named character entities.
53 242
 	 * 
54
-	 * I can't write XXXX in this comment where it belongs because the damned
55
-	 * Java Preprocessor can't mind its own business. Idiots!
243
+	 * @return named character entities
244
+	 */
245
+	public static Collection<CharacterEntity> getNamedCharacterEntities()
246
+	{
247
+		return NAMED_CHARACTER_ENTITIES.values();
248
+	}
249
+	
250
+	/**
251
+	 * Reads a single character entity (formatted as &amp;characterEntity;) at the
252
+	 * given string offset.
56 253
 	 * 
57
-	 * Lame Python escape: !\UXXXXXXXX must be exactly 8 xdigits
254
+	 * The following formats are supported:
255
+	 * <ul>
256
+	 * <li>&amp;namedCharacterEntity;</li>
257
+	 * <li>&amp;#ddd</li>
258
+	 * <li>&amp;#xXXXX</li>
259
+	 * </ul>
58 260
 	 * 
59
-	 * TODO: Perl translation escapes: \Q \U \L \E \[IDIOT JAVA PREPROCESSOR]u
60
-	 * \l These are not so important to cover if you're passing the result to
61
-	 * Pattern.compile(), since it handles them for you further downstream. Hm,
62
-	 * what about \[IDIOT JAVA PREPROCESSOR]u?
261
+	 * The returned value includes the character entity, without the enclosing
262
+	 * &amp; and ; characters.
63 263
 	 * 
64
-	 * @param oldstr
65
-	 * @return
264
+	 * @param str string value to search in
265
+	 * @param offset offset to look at
266
+	 * @return character entity
267
+	 * @throws IllegalArgumentException if the given string does not contain a
268
+	 *	valid character entity at the given position
66 269
 	 */
67
-	public static String unescapeString(String oldstr) {
68
-		if ((oldstr.charAt(0) != '"' || oldstr.charAt(oldstr.length() - 1) != '"')
69
-				&& (oldstr.charAt(0) != '\'' || oldstr.charAt(oldstr.length() - 1) != '\'')) {
70
-			// TODO: error
71
-			// throw new TweakerExecuteException("Not a valid string constant: "
72
-			// + oldstr);
73
-		}
74
-		oldstr = oldstr.substring(1, oldstr.length() - 1);
75
-
76
-		/*
77
-		 * In contrast to fixing Java's broken regex charclasses, this one need
78
-		 * be no bigger, as unescaping shrinks the string here, where in the
79
-		 * other one, it grows it.
80
-		 */
81
-
82
-		StringBuilder newstr = new StringBuilder(oldstr.length());
83
-
84
-		boolean saw_backslash = false;
85
-
86
-		for (int i = 0; i < oldstr.length(); i++) {
87
-			int cp = oldstr.codePointAt(i);
88
-			if (oldstr.codePointAt(i) > Character.MAX_VALUE) {
89
-				i++;
90
-				/**** WE HATES UTF-16! WE HATES IT FOREVERSES!!! ****/
91
-			}
92
-
93
-			if (!saw_backslash) {
94
-				if (cp == '\\') {
95
-					saw_backslash = true;
96
-				} else {
97
-					newstr.append(Character.toChars(cp));
98
-				}
99
-				continue; /* switch */
100
-			}
101
-
102
-			if (cp == '\\') {
103
-				saw_backslash = false;
104
-				newstr.append('\\');
105
-				continue; /* switch */
106
-			}
107
-
108
-			switch (cp) {
109
-
110
-				case 'r':
111
-					newstr.append('\r');
112
-					break; /* switch */
113
-
114
-				case 'n':
115
-					newstr.append('\n');
116
-					break; /* switch */
117
-
118
-				case 'f':
119
-					newstr.append('\f');
120
-					break; /* switch */
121
-
122
-				/* PASS a \b THROUGH!! */
123
-				case 'b':
124
-					newstr.append("\\b");
125
-					break; /* switch */
126
-
127
-				case 't':
128
-					newstr.append('\t');
129
-					break; /* switch */
130
-
131
-				case 'a':
132
-					newstr.append('\007');
133
-					break; /* switch */
134
-
135
-				case 'e':
136
-					newstr.append('\033');
137
-					break; /* switch */
138
-
139
-				/*
140
-				 * A "control" character is what you get when you xor its
141
-				 * codepoint with '@'==64. This only makes sense for ASCII, and
142
-				 * may not yield a "control" character after all.
143
-				 * 
144
-				 * Strange but true: "\c{" is ";", "\c}" is "=", etc.
145
-				 */
146
-				case 'c': {
147
-					if (++i == oldstr.length()) {
148
-						// TODO: error
149
-						// throw new TweakerExecuteException("trailing \\c");
150
-					}
151
-					cp = oldstr.codePointAt(i);
152
-					/*
153
-					 * don't need to grok surrogates, as next line blows them up
154
-					 */
155
-					if (cp > 0x7f) {
156
-						// TODO: error
157
-						// throw new TweakerExecuteException(
158
-						// "expected ASCII after \\c");
159
-					}
160
-					newstr.append(Character.toChars(cp ^ 64));
161
-					break; /* switch */
162
-				}
163
-
164
-				case '8':
165
-				case '9':
166
-					// TODO: error
167
-					// throw new TweakerExecuteException("illegal octal digit");
168
-					/* NOTREACHED */
169
-
170
-					/*
171
-					 * may be 0 to 2 octal digits following this one so back up
172
-					 * one for fallthrough to next case; unread this digit and
173
-					 * fall through to next case.
174
-					 */
175
-				case '1':
176
-				case '2':
177
-				case '3':
178
-				case '4':
179
-				case '5':
180
-				case '6':
181
-				case '7':
182
-					--i;
183
-					/* FALLTHROUGH */
184
-
185
-					/*
186
-					 * Can have 0, 1, or 2 octal digits following a 0 this
187
-					 * permits larger values than octal 377, up to octal 777.
188
-					 */
189
-				case '0': {
190
-					if (i + 1 == oldstr.length()) {
191
-						/* found \0 at end of string */
192
-						newstr.append(Character.toChars(0));
193
-						break; /* switch */
194
-					}
195
-					i++;
196
-					int digits = 0;
197
-					int j;
198
-					for (j = 0; j <= 2; j++) {
199
-						if (i + j == oldstr.length()) {
200
-							break; /* for */
201
-						}
202
-						/* safe because will unread surrogate */
203
-						int ch = oldstr.charAt(i + j);
204
-						if (ch < '0' || ch > '7') {
205
-							break; /* for */
206
-						}
207
-						digits++;
208
-					}
209
-					if (digits == 0) {
210
-						--i;
211
-						newstr.append('\0');
212
-						break; /* switch */
213
-					}
214
-					int value = 0;
215
-					try {
216
-						value = Integer
217
-							.parseInt(oldstr.substring(i, i + digits), 8);
218
-					} catch (NumberFormatException nfe) {
219
-						// TODO: error
220
-						// throw new TweakerExecuteException(
221
-						// "invalid octal value for \\0 escape");
222
-					}
223
-					newstr.append(Character.toChars(value));
224
-					i += digits - 1;
225
-					break; /* switch */
226
-				} /* end case '0' */
227
-
228
-				case 'x': {
229
-					if (i + 2 > oldstr.length()) {
230
-						// TODO: error
231
-						// throw new TweakerExecuteException(
232
-						// "string too short for \\x escape");
233
-					}
234
-					i++;
235
-					boolean saw_brace = false;
236
-					if (oldstr.charAt(i) == '{') {
237
-						/* ^^^^^^ ok to ignore surrogates here */
238
-						i++;
239
-						saw_brace = true;
240
-					}
241
-					int j;
242
-					for (j = 0; j < 8; j++) {
243
-
244
-						if (!saw_brace && j == 2) {
245
-							break; /* for */
246
-						}
247
-
248
-						/*
249
-						 * ASCII test also catches surrogates
250
-						 */
251
-						int ch = oldstr.charAt(i + j);
252
-						if (ch > 127) {
253
-							// TODO: error
254
-							// throw new TweakerExecuteException(
255
-							// "illegal non-ASCII hex digit in \\x escape");
256
-						}
257
-
258
-						if (saw_brace && ch == '}') {
259
-							break; /* for */
260
-						}
261
-
262
-						if (!((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F'))) {
263
-							// TODO: error
264
-							// throw new TweakerExecuteException(String.format(
265
-							// "illegal hex digit #%d '%c' in \\x", ch, ch));
266
-						}
267
-
268
-					}
269
-					if (j == 0) {
270
-						// TODO: error
271
-						// throw new TweakerExecuteException(
272
-						// "empty braces in \\x{} escape");
273
-					}
274
-					int value = 0;
275
-					try {
276
-						value = Integer.parseInt(oldstr.substring(i, i + j), 16);
277
-					} catch (NumberFormatException nfe) {
278
-						// TODO: error
279
-						// throw new TweakerExecuteException(
280
-						// "invalid hex value for \\x escape");
281
-					}
282
-					newstr.append(Character.toChars(value));
283
-					if (saw_brace) {
284
-						j++;
285
-					}
286
-					i += j - 1;
287
-					break; /* switch */
288
-				}
289
-
290
-				case 'u': {
291
-					if (i + 4 > oldstr.length()) {
292
-						// TODO: error
293
-						// throw new TweakerExecuteException(
294
-						// "string too short for \\u escape");
295
-					}
296
-					i++;
297
-					int j;
298
-					for (j = 0; j < 4; j++) {
299
-						/* this also handles the surrogate issue */
300
-						if (oldstr.charAt(i + j) > 127) {
301
-							// TODO: error
302
-							// throw new TweakerExecuteException(
303
-							// "illegal non-ASCII hex digit in \\u escape");
304
-						}
305
-					}
306
-					int value = 0;
307
-					try {
308
-						value = Integer.parseInt(oldstr.substring(i, i + j), 16);
309
-					} catch (NumberFormatException nfe) {
310
-						// TODO: error
311
-						// throw new TweakerExecuteException(
312
-						// "invalid hex value for \\u escape");
313
-					}
314
-					newstr.append(Character.toChars(value));
315
-					i += j - 1;
316
-					break; /* switch */
317
-				}
318
-
319
-				case 'U': {
320
-					if (i + 8 > oldstr.length()) {
321
-						// TODO: error
322
-						// throw new TweakerExecuteException(
323
-						// "string too short for \\U escape");
324
-					}
325
-					i++;
326
-					int j;
327
-					for (j = 0; j < 8; j++) {
328
-						/* this also handles the surrogate issue */
329
-						if (oldstr.charAt(i + j) > 127) {
330
-							// TODO: error
331
-							// throw new TweakerExecuteException(
332
-							// "illegal non-ASCII hex digit in \\U escape");
333
-						}
334
-					}
335
-					int value = 0;
336
-					try {
337
-						value = Integer.parseInt(oldstr.substring(i, i + j), 16);
338
-					} catch (NumberFormatException nfe) {
339
-						// TODO: error
340
-						// throw new TweakerExecuteException(
341
-						// "invalid hex value for \\U escape");
342
-					}
343
-					newstr.append(Character.toChars(value));
344
-					i += j - 1;
345
-					break; /* switch */
346
-				}
347
-
348
-				default:
349
-					newstr.append('\\');
350
-					newstr.append(Character.toChars(cp));
351
-					/*
352
-					 * say(String.format(
353
-					 * "DEFAULT unrecognized escape %c passed through", cp));
354
-					 */
355
-					break; /* switch */
356
-
270
+	public static CharacterEntity readCharacterEntity(String str, int offset)
271
+	{
272
+		if (offset + 3 >= str.length())
273
+			throw new IllegalArgumentException("Not a proper character entity");
274
+		if (str.charAt(offset) != '&')
275
+			throw new IllegalArgumentException("Not a proper character entity");
276
+		
277
+		int semi = str.indexOf(';', offset);
278
+		if (semi < 0)
279
+			throw new IllegalArgumentException("Not a proper character entity");
280
+		
281
+		String entity = str.substring(offset + 1, semi);
282
+		if (entity.isEmpty())
283
+			throw new IllegalArgumentException("Not a proper character entity");
284
+		
285
+		if (NAMED_CHARACTER_ENTITIES.containsKey(entity))
286
+			return NAMED_CHARACTER_ENTITIES.get(entity);
287
+		
288
+		if (entity.charAt(0) == '#') {
289
+			if (entity.length() < 2)
290
+				throw new IllegalArgumentException("Not a proper character entity");
291
+			
292
+			if (str.charAt(1) == 'x') {
293
+				// hex character entity
294
+				if (entity.length() != 7)
295
+					throw new IllegalArgumentException("Not a proper character entity");
296
+				
297
+				int ivalue = Integer.parseInt(entity.substring(2), 16);
298
+				return new CharacterEntity(entity, (char) ivalue);
299
+			} else {
300
+				// decimal character entity
301
+				int ivalue = Integer.parseInt(entity.substring(1));
302
+				return new CharacterEntity(entity, (char) ivalue);
357 303
 			}
358
-			saw_backslash = false;
359
-		}
360
-
361
-		/* weird to leave one at the end */
362
-		if (saw_backslash) {
363
-			newstr.append('\\');
364 304
 		}
365 305
 		
366
-		String result = newstr.toString();
367
-		return result;
306
+		throw new IllegalArgumentException("Not a valid named character entity");
368 307
 	}
369
-
370
-	/*
371
-	 * Return a string "U+XX.XXX.XXXX" etc, where each XX set is the xdigits of
372
-	 * the logical Unicode code point. No bloody brain-damaged UTF-16 surrogate
373
-	 * crap, just true logical characters.
374
-	 */
375
-	private static String uniplus(String s) {
376
-		if (s.length() == 0) {
377
-			return "";
378
-		}
379
-		/* This is just the minimum; sb will grow as needed. */
380
-		StringBuilder sb = new StringBuilder(2 + 3 * s.length());
381
-		sb.append("U+");
382
-		for (int i = 0; i < s.length(); i++) {
383
-			sb.append(String.format("%X", s.codePointAt(i)));
384
-			if (s.codePointAt(i) > Character.MAX_VALUE) {
385
-				i++;
386
-				/**** WE HATES UTF-16! WE HATES IT FOREVERSES!!! ****/
387
-			}
388
-			if (i + 1 < s.length()) {
389
-				sb.append(".");
390
-			}
308
+	
309
+	public static class CharacterEntity
310
+	{
311
+		public char charValue;
312
+		public String stringValue;
313
+		
314
+		public CharacterEntity(String stringValue, char charValue)
315
+		{
316
+			this.charValue = charValue;
317
+			this.stringValue = stringValue;
391 318
 		}
392
-		return sb.toString();
393 319
 	}
394 320
 }

+ 266
- 0
src/main/resources/org/openzen/zenscript/shared/characterEntities.properties 查看文件

@@ -0,0 +1,266 @@
1
+quot=34
2
+amp=38
3
+apos=39
4
+lt=60
5
+gt=62
6
+
7
+nbsp=160
8
+iexcl=161
9
+cent=162
10
+pound=163
11
+curren=164
12
+yen=165
13
+brvbar=166
14
+sect=167
15
+uml=168
16
+copy=169
17
+ordf=170
18
+laquo=171
19
+not=172
20
+shy=173
21
+reg=174
22
+macr=175
23
+deg=176
24
+plusmn=177
25
+sup2=178
26
+sup3=179
27
+acute=180
28
+micro=181
29
+para=182
30
+middot=183
31
+cedil=184
32
+sup1=185
33
+ordm=186
34
+raquo=187
35
+frac14=188
36
+frac12=189
37
+frac34=190
38
+iquest=191
39
+
40
+Agrave=192
41
+Aacute=193
42
+Acirc=194
43
+Atilde=195
44
+Auml=196
45
+Aring=197
46
+AElig=198
47
+Ccedil=199
48
+Egrave=200
49
+Eacute=201
50
+Ecirc=202
51
+Euml=203
52
+lgrave=204
53
+lacute=205
54
+lcirc=206
55
+luml=207
56
+ETH=208
57
+Ntilde=209
58
+Ograve=210
59
+Oacute=211
60
+Ocirc=212
61
+Otilde=213
62
+Ouml=214
63
+times=215
64
+Oslash=216
65
+Ugrave=217
66
+Uacute=218
67
+Ucirc=219
68
+Uuml=220
69
+Yacute=221
70
+THORN=222
71
+szlig=223
72
+agrave=224
73
+aacute=225
74
+acirc=226
75
+atilde=227
76
+auml=228
77
+aring=229
78
+aelig=230
79
+ccedil=231
80
+egrave=232
81
+eacute=233
82
+ecirc=234
83
+euml=235
84
+igrave=236
85
+iacute=237
86
+icirc=238
87
+iuml=239
88
+eth=240
89
+ntilde=241
90
+ograve=242
91
+oacute=243
92
+ocirc=244
93
+otilde=245
94
+ouml=246
95
+divide=247
96
+oslash=248
97
+ugrave=249
98
+uacute=250
99
+ucirc=251
100
+uuml=252
101
+yacute=253
102
+thorn=254
103
+yuml=255
104
+
105
+OElig=338
106
+oelig=339
107
+Scaron=352
108
+scaron=353
109
+Yuml=376
110
+
111
+fnof=402
112
+
113
+circ=710
114
+tilde=732
115
+
116
+Alpha=913
117
+Beta=914
118
+Gamma=915
119
+Delta=916
120
+Epsilon=917
121
+Zeta=918
122
+Eta=919
123
+Theta=920
124
+Iota=921
125
+Kappa=922
126
+Lambda=923
127
+Mu=924
128
+Nu=925
129
+Xi=926
130
+Omicron=927
131
+Pi=928
132
+Rho=929
133
+Sigma=931
134
+Tau=932
135
+Upsilon=933
136
+Phi=934
137
+Chi=935
138
+Psi=936
139
+Omega=937
140
+
141
+alpha=945
142
+beta=946
143
+gamma=947
144
+delta=948
145
+epsilon=949
146
+zeta=950
147
+eta=951
148
+theta=952
149
+iota=953
150
+kappa=954
151
+lambda=955
152
+mu=956
153
+nu=957
154
+xi=958
155
+omicron=959
156
+pi=960
157
+rho=961
158
+sigmaf=962
159
+sigma=963
160
+tau=964
161
+upsilon=965
162
+phi=966
163
+chi=967
164
+psi=968
165
+omega=969
166
+thetasym=977
167
+upsih=978
168
+piv=982
169
+
170
+ensp=8194
171
+emsp=8195
172
+thinsp=8201
173
+zwnj=8204
174
+zwj=8205
175
+lrm=8206
176
+rlm=8207
177
+ndash=8211
178
+mdash=8212
179
+lsquo=8216
180
+rsquo=8217
181
+sbquo=8218
182
+ldquo=8220
183
+rdquo=8221
184
+bdquo=8222
185
+dagger=8224
186
+Dagger=8225
187
+bull=8226
188
+hellip=8230
189
+permil=8240
190
+prime=8242
191
+Prime=8243
192
+lsaquo=8249
193
+rsaquo=8250
194
+oline=8254
195
+frasl=8260
196
+
197
+euro=8364
198
+
199
+image=8465
200
+weierp=8472
201
+real=8476
202
+trade=8482
203
+alefsym=8501
204
+
205
+larr=8592
206
+uarr=8593
207
+rarr=8594
208
+darr=8595
209
+harr=8596
210
+crarr=8629
211
+lArr=8656
212
+uArr=8657
213
+rArr=8658
214
+dArr=8659
215
+hArr=8660
216
+
217
+forall=8704
218
+part=8706
219
+exist=8707
220
+empty=8709
221
+nabla=8711
222
+isin=8712
223
+notin=8713
224
+ni=8715
225
+prod=8719
226
+sum=8721
227
+minus=8722
228
+lowast=8727
229
+radic=8730
230
+prop=8733
231
+infin=8734
232
+ang=8736
233
+and=8743
234
+or=8744
235
+cap=8745
236
+cup=8746
237
+int=8747
238
+there4=8756
239
+sim=8764
240
+cong=8773
241
+asymp=8776
242
+ne=8800
243
+equiv=8801
244
+le=8804
245
+ge=8805
246
+sub=8834
247
+sup=8835
248
+nsub=8836
249
+sube=8838
250
+supe=8839
251
+oplus=8853
252
+otimes=8855
253
+perp=8869
254
+sdot=8901
255
+
256
+lceil=8968
257
+rceil=8969
258
+lfloor=8970
259
+rfloor=8971
260
+lang=9001
261
+rang=9002
262
+loz=9674
263
+spades=9824
264
+clubs=8927
265
+hearts=9829
266
+diams=9830

Loading…
取消
儲存