[Ruby] Abstract Ruby Codegen (#562)

* Add AbstractRubyCodegen

* Refactor constructor

* Move escapeReservedWord() to AbstractRubyCodegen

* Move getTypeDeclaration() to AbstractRubyCodegen

* Move toDefaultValue() to AbstractRubyCodegen

* Move toVarName() to AbstractRubyCodegen

* Move toParamName() to AbstractRubyCodegen

* Move toOperationId() to AbstractRubyCodegen

* Move escapeQuotationMark() to AbstractRubyCodegen

* Move escapeUnsafeCharacters() to AbstractRubyCodegen

* Use super.escapeReservedWord()

* RubyClientCodegen extends AbstractRubyCodegen

* Add the differences with AbstractRubyCodegen to "reservedWords"

* cliOptions.clear() is not a language specific matter

- Rails, Sinatra requires cliOptions.clear()
- Ruby client doesn't requires that

* Remove duplicated statements with AbstractRubyCodegen

* Remove duplicated methods with AbstractRubyCodegen

* Merge toVarName() into AbstractRubyCodegen

* Merge getTypeDeclaration() into AbstractRubyCodegen

* Merge toDefaultValue() into AbstractRubyCodegen

* Update Ruby related samples

- bin/ruby-client-petstore.sh
- bin/ruby-on-rails-server-petstore.sh
- bin/ruby-sinatra-server-petstore.sh

* Remove unnecessary 'import'

* Avoid unnecessary HTML escaping
This commit is contained in:
Akihito Nakano
2018-07-17 10:08:42 +09:00
committed by William Cheng
parent a055dc0351
commit afb238814d
8 changed files with 213 additions and 390 deletions

View File

@@ -0,0 +1,162 @@
/*
* Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
* Copyright 2018 SmartBear Software
*
* 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 org.openapitools.codegen.languages;
import io.swagger.v3.oas.models.media.ArraySchema;
import io.swagger.v3.oas.models.media.Schema;
import org.openapitools.codegen.*;
import org.openapitools.codegen.utils.ModelUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
abstract class AbstractRubyCodegen extends DefaultCodegen implements CodegenConfig {
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractRubyCodegen.class);
AbstractRubyCodegen() {
super();
setReservedWordsLowerCase(
Arrays.asList(
"__FILE__", "and", "def", "end", "in", "or", "self", "unless", "__LINE__",
"begin", "defined?", "ensure", "module", "redo", "super", "until", "BEGIN",
"break", "do", "false", "next", "rescue", "then", "when", "END", "case",
"else", "for", "nil", "retry", "true", "while", "alias", "class", "elsif",
"if", "not", "return", "undef", "yield")
);
languageSpecificPrimitives.clear();
languageSpecificPrimitives.add("String");
languageSpecificPrimitives.add("Integer");
languageSpecificPrimitives.add("Float");
languageSpecificPrimitives.add("Date");
languageSpecificPrimitives.add("DateTime");
languageSpecificPrimitives.add("Array");
languageSpecificPrimitives.add("Hash");
languageSpecificPrimitives.add("File");
languageSpecificPrimitives.add("Object");
typeMapping.clear();
typeMapping.put("string", "String");
typeMapping.put("char", "String");
typeMapping.put("int", "Integer");
typeMapping.put("integer", "Integer");
typeMapping.put("long", "Integer");
typeMapping.put("short", "Integer");
typeMapping.put("float", "Float");
typeMapping.put("double", "Float");
typeMapping.put("number", "Float");
typeMapping.put("date", "Date");
typeMapping.put("DateTime", "DateTime");
typeMapping.put("array", "Array");
typeMapping.put("List", "Array");
typeMapping.put("map", "Hash");
typeMapping.put("object", "Object");
typeMapping.put("file", "File");
typeMapping.put("binary", "String");
typeMapping.put("ByteArray", "String");
typeMapping.put("UUID", "String");
}
@Override
public String escapeReservedWord(String name) {
if (this.reservedWordsMappings().containsKey(name)) {
return this.reservedWordsMappings().get(name);
}
return "_" + name;
}
@Override
public String getTypeDeclaration(Schema schema) {
if (ModelUtils.isArraySchema(schema)) {
Schema inner = ((ArraySchema) schema).getItems();
return getSchemaType(schema) + "<" + getTypeDeclaration(inner) + ">";
} else if (ModelUtils.isMapSchema(schema)) {
Schema inner = (Schema) schema.getAdditionalProperties();
return getSchemaType(schema) + "<String, " + getTypeDeclaration(inner) + ">";
}
return super.getTypeDeclaration(schema);
}
@Override
public String toDefaultValue(Schema p) {
if (ModelUtils.isIntegerSchema(p) || ModelUtils.isNumberSchema(p) || ModelUtils.isBooleanSchema(p)) {
if (p.getDefault() != null) {
return p.getDefault().toString();
}
} else if (ModelUtils.isStringSchema(p)) {
if (p.getDefault() != null) {
return "'" + escapeText((String) p.getDefault()) + "'";
}
}
return null;
}
@Override
public String toVarName(String name) {
// sanitize name
name = sanitizeName(name); // FIXME: a parameter should not be assigned. Also declare the methods parameters as 'final'.
// if it's all uppper case, convert to lower case
if (name.matches("^[A-Z_]*$")) {
name = name.toLowerCase();
}
// camelize (lower first character) the variable name
// petId => pet_id
name = underscore(name);
// for reserved word or word starting with number, append _
if (isReservedWord(name) || name.matches("^\\d.*")) {
name = escapeReservedWord(name);
}
return name;
}
@Override
public String toParamName(String name) {
// should be the same as variable name
return toVarName(name);
}
@Override
public String toOperationId(String operationId) {
// method name cannot use reserved keyword, e.g. return
if (isReservedWord(operationId)) {
String newOperationId = underscore("call_" + operationId);
LOGGER.warn(operationId + " (reserved word) cannot be used as method name. Renamed to " + newOperationId);
return newOperationId;
}
return underscore(operationId);
}
@Override
public String escapeQuotationMark(String input) {
// remove ' to avoid code injection
return input.replace("'", "");
}
@Override
public String escapeUnsafeCharacters(String input) {
return input.replace("=end", "=_end").replace("=begin", "=_begin");
}
}

View File

@@ -18,14 +18,11 @@
package org.openapitools.codegen.languages;
import org.openapitools.codegen.CliOption;
import org.openapitools.codegen.CodegenConfig;
import org.openapitools.codegen.CodegenConstants;
import org.openapitools.codegen.CodegenParameter;
import org.openapitools.codegen.CodegenProperty;
import org.openapitools.codegen.CodegenType;
import org.openapitools.codegen.DefaultCodegen;
import org.openapitools.codegen.SupportingFile;
import org.openapitools.codegen.utils.ModelUtils;
import java.io.File;
import java.util.Arrays;
@@ -37,7 +34,7 @@ import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RubyClientCodegen extends DefaultCodegen implements CodegenConfig {
public class RubyClientCodegen extends AbstractRubyCodegen {
private static final Logger LOGGER = LoggerFactory.getLogger(RubyClientCodegen.class);
public static final String GEM_NAME = "gemName";
public static final String MODULE_NAME = "moduleName";
@@ -89,21 +86,13 @@ public class RubyClientCodegen extends DefaultCodegen implements CodegenConfig {
// default HIDE_GENERATION_TIMESTAMP to true
hideGenerationTimestamp = Boolean.TRUE;
setReservedWordsLowerCase(
Arrays.asList(
// local variable names used in API methods (endpoints)
"local_var_path", "query_params", "header_params", "_header_accept", "_header_accept_result",
"_header_content_type", "form_params", "post_body", "auth_names",
// ruby reserved keywords
"__FILE__", "and", "def", "end", "in", "or", "self", "unless", "__LINE__",
"begin", "defined?", "ensure", "module", "redo", "super", "until", "BEGIN",
"break", "do", "false", "next", "rescue", "then", "when", "END", "case",
"else", "for", "nil", "retry", "true", "while", "alias", "class", "elsif",
"if", "not", "return", "undef", "yield")
);
// local variable names used in API methods (endpoints)
for (String word : Arrays.asList(
"local_var_path", "query_params", "header_params", "_header_accept", "_header_accept_result",
"_header_content_type", "form_params", "post_body", "auth_names")) {
reservedWords.add(word.toLowerCase());
}
typeMapping.clear();
languageSpecificPrimitives.clear();
// primitives in ruby lang
languageSpecificPrimitives.add("int");
@@ -111,37 +100,8 @@ public class RubyClientCodegen extends DefaultCodegen implements CodegenConfig {
languageSpecificPrimitives.add("map");
languageSpecificPrimitives.add("string");
// primitives in the typeMapping
languageSpecificPrimitives.add("String");
languageSpecificPrimitives.add("Integer");
languageSpecificPrimitives.add("Float");
languageSpecificPrimitives.add("Date");
languageSpecificPrimitives.add("DateTime");
languageSpecificPrimitives.add("BOOLEAN");
languageSpecificPrimitives.add("Array");
languageSpecificPrimitives.add("Hash");
languageSpecificPrimitives.add("File");
languageSpecificPrimitives.add("Object");
typeMapping.put("string", "String");
typeMapping.put("char", "String");
typeMapping.put("int", "Integer");
typeMapping.put("integer", "Integer");
typeMapping.put("long", "Integer");
typeMapping.put("short", "Integer");
typeMapping.put("float", "Float");
typeMapping.put("double", "Float");
typeMapping.put("number", "Float");
typeMapping.put("date", "Date");
typeMapping.put("DateTime", "DateTime");
typeMapping.put("boolean", "BOOLEAN");
typeMapping.put("array", "Array");
typeMapping.put("List", "Array");
typeMapping.put("map", "Hash");
typeMapping.put("object", "Object");
typeMapping.put("file", "File");
typeMapping.put("binary", "String");
typeMapping.put("ByteArray", "String");
typeMapping.put("UUID", "String");
// remove modelPackage and apiPackage added by default
Iterator<CliOption> itr = cliOptions.iterator();
@@ -308,14 +268,6 @@ public class RubyClientCodegen extends DefaultCodegen implements CodegenConfig {
return underscore(moduleName.replaceAll("[^\\w]+", ""));
}
@Override
public String escapeReservedWord(String name) {
if (this.reservedWordsMappings().containsKey(name)) {
return this.reservedWordsMappings().get(name);
}
return "_" + name;
}
@Override
public String apiFileFolder() {
return outputFolder + File.separator + libFolder + File.separator + gemName + File.separator + apiPackage.replace("/", File.separator);
@@ -346,34 +298,6 @@ public class RubyClientCodegen extends DefaultCodegen implements CodegenConfig {
return (outputFolder + "/" + modelDocPath).replace('/', File.separatorChar);
}
@Override
public String getTypeDeclaration(Schema schema) {
if (ModelUtils.isArraySchema(schema)) {
Schema inner = ((ArraySchema) schema).getItems();
return getSchemaType(schema) + "<" + getTypeDeclaration(inner) + ">";
} else if (ModelUtils.isMapSchema(schema)) {
Schema inner = (Schema) schema.getAdditionalProperties();
return getSchemaType(schema) + "<String, " + getTypeDeclaration(inner) + ">";
}
return super.getTypeDeclaration(schema);
}
@Override
public String toDefaultValue(Schema p) {
if (ModelUtils.isIntegerSchema(p) || ModelUtils.isNumberSchema(p) || ModelUtils.isBooleanSchema(p)) {
if (p.getDefault() != null) {
return p.getDefault().toString();
}
} else if (ModelUtils.isStringSchema(p)) {
if (p.getDefault() != null) {
return "'" + escapeText((String) p.getDefault()) + "'";
}
}
return null;
}
@Override
public String getSchemaType(Schema schema) {
String openAPIType = super.getSchemaType(schema);
@@ -394,33 +318,6 @@ public class RubyClientCodegen extends DefaultCodegen implements CodegenConfig {
return toModelName(type);
}
@Override
public String toVarName(String name) {
// sanitize name
name = sanitizeName(name); // FIXME: a parameter should not be assigned. Also declare the methods parameters as 'final'.
// if it's all uppper case, convert to lower case
if (name.matches("^[A-Z_]*$")) {
name = name.toLowerCase();
}
// camelize (lower first character) the variable name
// petId => pet_id
name = underscore(name);
// for reserved word or word starting with number, append _
if (isReservedWord(name) || name.matches("^\\d.*")) {
name = escapeReservedWord(name);
}
return name;
}
@Override
public String toParamName(String name) {
// should be the same as variable name
return toVarName(name);
}
@Override
public String toModelName(String name) {
name = sanitizeName(name); // FIXME: a parameter should not be assigned. Also declare the methods parameters as 'final'.
@@ -684,16 +581,4 @@ public class RubyClientCodegen extends DefaultCodegen implements CodegenConfig {
//
//return super.shouldOverwrite(filename) && !filename.endsWith("_spec.rb");
}
@Override
public String escapeQuotationMark(String input) {
// remove ' to avoid code injection
return input.replace("'", "");
}
@Override
public String escapeUnsafeCharacters(String input) {
return input.replace("=end", "=_end").replace("=begin", "=_begin");
}
}

View File

@@ -18,30 +18,19 @@
package org.openapitools.codegen.languages;
import java.text.SimpleDateFormat;
import java.util.Date;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.openapitools.codegen.CodegenConfig;
import org.openapitools.codegen.CodegenType;
import org.openapitools.codegen.DefaultCodegen;
import org.openapitools.codegen.SupportingFile;
import org.openapitools.codegen.utils.ModelUtils;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.media.*;
import io.swagger.v3.core.util.Yaml;
import java.io.File;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RubyOnRailsServerCodegen extends DefaultCodegen implements CodegenConfig {
public class RubyOnRailsServerCodegen extends AbstractRubyCodegen {
private static final Logger LOGGER = LoggerFactory.getLogger(RubyOnRailsServerCodegen.class);
private static final SimpleDateFormat MIGRATE_FILE_NAME_FORMAT = new SimpleDateFormat("yyyyMMddHHmmss");
@@ -87,33 +76,14 @@ public class RubyOnRailsServerCodegen extends DefaultCodegen implements CodegenC
embeddedTemplateDir = templateDir = "ruby-on-rails-server";
typeMapping.clear();
languageSpecificPrimitives.clear();
setReservedWordsLowerCase(
Arrays.asList(
"__FILE__", "and", "def", "end", "in", "or", "self", "unless", "__LINE__",
"begin", "defined?", "ensure", "module", "redo", "super", "until", "BEGIN",
"break", "do", "false", "next", "rescue", "then", "when", "END", "case",
"else", "for", "nil", "retry", "true", "while", "alias", "class", "elsif",
"if", "not", "return", "undef", "yield")
);
// In order to adapt to DB migrate script, overwrite typeMapping
typeMapping.put("string", "string");
typeMapping.put("char", "string");
typeMapping.put("int", "integer");
typeMapping.put("integer", "integer");
typeMapping.put("long", "integer");
typeMapping.put("short", "integer");
typeMapping.put("float", "float");
typeMapping.put("double", "decimal");
typeMapping.put("number", "float");
typeMapping.put("date", "date");
typeMapping.put("DateTime", "datetime");
typeMapping.put("boolean", "boolean");
typeMapping.put("binary", "string");
typeMapping.put("ByteArray", "string");
typeMapping.put("UUID", "string");
// remove modelPackage and apiPackage added by default
cliOptions.clear();
@@ -203,59 +173,11 @@ public class RubyOnRailsServerCodegen extends DefaultCodegen implements CodegenC
return "Generates a Ruby on Rails (v5) server library.";
}
@Override
public String escapeReservedWord(String name) {
if(this.reservedWordsMappings().containsKey(name)) {
return this.reservedWordsMappings().get(name);
}
return "_" + name;
}
@Override
public String apiFileFolder() {
return outputFolder + File.separator + apiPackage.replace("/", File.separator);
}
@Override
public String getTypeDeclaration(Schema p) {
if (ModelUtils.isArraySchema(p)) {
ArraySchema ap = (ArraySchema) p;
Schema inner = ap.getItems();
return getSchemaType(p) + "[" + getTypeDeclaration(inner) + "]";
} else if (ModelUtils.isMapSchema(p)) {
Schema inner = (Schema) p.getAdditionalProperties();
return getSchemaType(p) + "[string," + getTypeDeclaration(inner) + "]";
}
return super.getTypeDeclaration(p);
}
@Override
public String toDefaultValue(Schema p) {
return "null";
}
@Override
public String toVarName(String name) {
// replace - with _ e.g. created-at => created_at
name = name.replaceAll("-", "_"); // FIXME: a parameter should not be assigned. Also declare the methods parameters as 'final'.
// if it's all uppper case, convert to lower case
if (name.matches("^[A-Z_]*$")) {
name = name.toLowerCase();
}
// camelize (lower first character) the variable name
// petId => pet_id
name = underscore(name);
// for reserved word or word starting with number, append _
if (isReservedWord(name) || name.matches("^\\d.*")) {
name = escapeReservedWord(name);
}
return name;
}
@Override
public String getSchemaType(Schema p) {
String openAPIType = super.getSchemaType(p);
@@ -266,12 +188,6 @@ public class RubyOnRailsServerCodegen extends DefaultCodegen implements CodegenC
return "string";
}
@Override
public String toParamName(String name) {
// should be the same as variable name
return toVarName(name);
}
@Override
public String toModelName(String name) {
// model name cannot use reserved keyword, e.g. return
@@ -318,32 +234,9 @@ public class RubyOnRailsServerCodegen extends DefaultCodegen implements CodegenC
return camelize(name) + "Controller";
}
@Override
public String toOperationId(String operationId) {
// method name cannot use reserved keyword, e.g. return
if (isReservedWord(operationId)) {
String newOperationId = underscore("call_" + operationId);
LOGGER.warn(operationId + " (reserved word) cannot be used as method name. Renamed to " + newOperationId);
return newOperationId;
}
return underscore(operationId);
}
@Override
public Map<String, Object> postProcessSupportingFileData(Map<String, Object> objs) {
generateYAMLSpecFile(objs);
return super.postProcessSupportingFileData(objs);
}
@Override
public String escapeQuotationMark(String input) {
// remove ' to avoid code injection
return input.replace("'", "");
}
@Override
public String escapeUnsafeCharacters(String input) {
return input.replace("=end", "=_end").replace("=begin", "=_begin");
}
}

View File

@@ -17,29 +17,18 @@
package org.openapitools.codegen.languages;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.openapitools.codegen.CodegenConfig;
import org.openapitools.codegen.CodegenType;
import org.openapitools.codegen.DefaultCodegen;
import org.openapitools.codegen.SupportingFile;
import org.openapitools.codegen.utils.ModelUtils;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.media.*;
import io.swagger.v3.core.util.Yaml;
import java.io.File;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RubySinatraServerCodegen extends DefaultCodegen implements CodegenConfig {
public class RubySinatraServerCodegen extends AbstractRubyCodegen {
private static final Logger LOGGER = LoggerFactory.getLogger(RubySinatraServerCodegen.class);
@@ -58,35 +47,6 @@ public class RubySinatraServerCodegen extends DefaultCodegen implements CodegenC
apiTemplateFiles.put("api.mustache", ".rb");
embeddedTemplateDir = templateDir = "ruby-sinatra-server";
typeMapping.clear();
languageSpecificPrimitives.clear();
setReservedWordsLowerCase(
Arrays.asList(
"__FILE__", "and", "def", "end", "in", "or", "self", "unless", "__LINE__",
"begin", "defined?", "ensure", "module", "redo", "super", "until", "BEGIN",
"break", "do", "false", "next", "rescue", "then", "when", "END", "case",
"else", "for", "nil", "retry", "true", "while", "alias", "class", "elsif",
"if", "not", "return", "undef", "yield")
);
languageSpecificPrimitives.add("int");
languageSpecificPrimitives.add("array");
languageSpecificPrimitives.add("map");
languageSpecificPrimitives.add("string");
languageSpecificPrimitives.add("DateTime");
typeMapping.put("long", "int");
typeMapping.put("integer", "int");
typeMapping.put("Array", "array");
typeMapping.put("String", "string");
typeMapping.put("List", "array");
typeMapping.put("map", "map");
//TODO binary should be mapped to byte array
// mapped to String as a workaround
typeMapping.put("binary", "string");
typeMapping.put("UUID", "string");
// remove modelPackage and apiPackage added by default
cliOptions.clear();
}
@@ -122,32 +82,11 @@ public class RubySinatraServerCodegen extends DefaultCodegen implements CodegenC
return "Generates a Ruby Sinatra server library.";
}
@Override
public String escapeReservedWord(String name) {
if (this.reservedWordsMappings().containsKey(name)) {
return this.reservedWordsMappings().get(name);
}
return "_" + name;
}
@Override
public String apiFileFolder() {
return outputFolder + File.separator + apiPackage.replace("/", File.separator);
}
@Override
public String getTypeDeclaration(Schema p) {
if (ModelUtils.isArraySchema(p)) {
ArraySchema ap = (ArraySchema) p;
Schema inner = ap.getItems();
return getSchemaType(p) + "[" + getTypeDeclaration(inner) + "]";
} else if (ModelUtils.isMapSchema(p)) {
Schema inner = (Schema) p.getAdditionalProperties();
return getSchemaType(p) + "[string," + getTypeDeclaration(inner) + "]";
}
return super.getTypeDeclaration(p);
}
@Override
public String getSchemaType(Schema p) {
String openAPIType = super.getSchemaType(p);
@@ -166,39 +105,6 @@ public class RubySinatraServerCodegen extends DefaultCodegen implements CodegenC
return type;
}
@Override
public String toDefaultValue(Schema p) {
return "null";
}
@Override
public String toVarName(String name) {
// replace - with _ e.g. created-at => created_at
name = name.replaceAll("-", "_"); // FIXME: a parameter should not be assigned. Also declare the methods parameters as 'final'.
// if it's all uppper case, convert to lower case
if (name.matches("^[A-Z_]*$")) {
name = name.toLowerCase();
}
// camelize (lower first character) the variable name
// petId => pet_id
name = underscore(name);
// for reserved word or word starting with number, append _
if (isReservedWord(name) || name.matches("^\\d.*")) {
name = escapeReservedWord(name);
}
return name;
}
@Override
public String toParamName(String name) {
// should be the same as variable name
return toVarName(name);
}
@Override
public String toModelName(String name) {
// model name cannot use reserved keyword, e.g. return
@@ -243,32 +149,9 @@ public class RubySinatraServerCodegen extends DefaultCodegen implements CodegenC
return camelize(name) + "Api";
}
@Override
public String toOperationId(String operationId) {
// method name cannot use reserved keyword, e.g. return
if (isReservedWord(operationId)) {
String newOperationId = underscore("call_" + operationId);
LOGGER.warn(operationId + " (reserved word) cannot be used as method name. Renamed to " + newOperationId);
return newOperationId;
}
return underscore(operationId);
}
@Override
public Map<String, Object> postProcessSupportingFileData(Map<String, Object> objs) {
generateYAMLSpecFile(objs);
return super.postProcessSupportingFileData(objs);
}
@Override
public String escapeQuotationMark(String input) {
// remove ' to avoid code injection
return input.replace("'", "");
}
@Override
public String escapeUnsafeCharacters(String input) {
return input.replace("=end", "=_end").replace("=begin", "=_begin");
}
}