一、APT概述
我们在前面的java注解详解一文中已经讲过,可以在运行时利用反射机制运行处理注解。其实,我们还可以在编译时处理注解,这就是不得不说官方为我们提供的注解处理工具APT (Annotation
Processing Tool )。
APT用来在编译时期扫描处理源代码中的注解信息,我们可以根据注解信息生成一些文件,比如Java文件。利用APT为我们生成的Java代码,实现冗余的代码功能,这样就减少手动的代码输入,提升了编码效率,而且使源代码看起来更清晰简洁。
从Java5开始,JDK就自带了注解处理器APT,不过从近几年开始APT才真正的流行起来,这要得益于Android上各种主流库都用了APT来实现,比如Dagger、ButterKnife、AndroidAnnotation、EventBus等。因为我本身工作中经常用到上面这些框架,为了更深入了解这些框架的实现过程,因此想利用APT技术实现自己的编译时注解。
二、实现目标
在Android开发中我们经常要编写如下冗余的代码:
Button button = (Button) findViewById(R.id.button1); Button button2 = (Button) findViewById(R.id.button2); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) {} }); button2.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) {} });
编写上面这些冗余代码不但浪费时间,而且一定程度上造成源代码冗余复杂。那么为了解决这种问题,我们可以利用注解和APT工具生成一个代理类(ProxyClass),让这个代理类帮助我们实现上面这些冗余的代码。
首先我们通过自定义的注解注解要处理的元素:
public class MainActivity extends AppCompatActivity { @ViewById(R.id.tv) TextView textView; @ViewById(R.id.btn) Button button; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ProxyTool.bind(this); } @Click({R.id.tv, R.id.btn}) public void myClick(View view) { //Click }
然后我们利用APT生成MainActivity
的代理类 MainActivity$$Proxy
,让该类帮我们实现冗余的功能:
public class MainActivity$$Proxy implements IProxy<MainActivity> { @Override public void inject(final MainActivity target, View root) { target.button = (Button)(root.findViewById(R.id.btn)); target.textView = (TextView)(root.findViewById(R.id.tv)); View.OnClickListener listener; listener = new View.OnClickListener() { @Override public void onClick(View view) { target.myClick(view); } } ; (root.findViewById(R.id.btn)).setOnClickListener(listener); (root.findViewById(R.id.tv)).setOnClickListener(listener); } }
OK,我们接下来就要实现上面的功能。
三、 项目框架
我把我们的注解框架命名为ProxyTool,把该框架分为四个模块,前三个为核心模块:
- proxytool-api:框架api模块,供使用者调用,Android Library类型模块
- proxytool-annotations:自定义注解模块,Java类型模块
- proxytool-compiler:注解处理器模块,用于处理注解并生成文件,Java类型模块
- proxytool-sample:示例Demo模块,Android工程类型模块
其中这四个模块的依赖关系如下:
proxytool-api依赖proxytool-annotations模块。
proxytool-compiler依赖proxytool-annotations模块。
proxytool-sample模块依赖proxytool-api模块。
有人也许会问,为什么不可以将这写模块写在一起呢?
因为注解处理模块器proxytool-compiler只在我们编译过程中需要使用到,在APP运行阶段就不需要使用该模块了。所以在发布APP时,我们就不必把注解处理器模块打包进来,以免造成程序臃肿,所以把proxytool-compiler模块单独拿出来。同时注解处理模块和api模块都需要使用到自定义注解模块,所以就需要把自定义注解模块单独拿出来。这样为何需要分成三个模块的原因也就一目了然了,其实butterfnife框架也是这样分的。
四、自定义注解模块
首先我们在自定义注解模块中定义两个注解类型,分别用于绑定View的id和注册View的点击事件:
/** * Bind a field to the view for the specified ID */ @Retention(RetentionPolicy.SOURCE) @Target(ElementType.FIELD) public @interface ViewById { int value(); }
/** * Bind a method to an android.view.View.OnClickListener on the view for each ID specified. */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface OnClick { public int[] value(); }
ViewById
中的value
用于接收注解该View的id值,OnClick
中的value
数组用于接收一组View的id值,这些view会被注册点击响应事件。因为这些注册只在编译时有需要用到,程序运行时就不再需要了,所以我们把这些注解定义成编译时保留(RetentionPolicy.SOURCE
)即可。限于篇幅的考虑,下面的介绍中我只对ViewById
注解做处理,OnClick
注解处理也是类似的。
五、注解处理器模块
创建好自定义注解后,我们就需要利用java提供的注解处理器根据自定义的注解来生成代理类了,该模块需要依赖其他三个模块:
compile 'com.squareup:javapoet:1.7.0' compile 'com.google.auto.service:auto-service:1.0-rc2' compile project(':proxytool-annotations')
- javapoet是square公司出的一个帮助我们非常方便生成java代码文件的第三方库,避免我们手动拼接字符串的麻烦。
- auto-service是google公司出的第三方库,主要用于注解处理器,可以自动帮我们生成META-INF 配置信息。
- 注解处理器要根据自定义注解进行解析,所以也需要依赖该模块。
让我们看一下注解处理器的API。所有的注解处理器都必须继承AbstractProcessor
,如下所示:
/** * 注解处理器 */ @AutoService(Processor.class) public class ProxyToolProcessor extends AbstractProcessor { private Filer mFiler; //文件相关工具类 private Elements mElementUtils; //元素相关的工具类 private Messager mMessager; //日志相关的工具类 /** * 处理器的初始化方法,可以获取相关的工具类 */ @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); mFiler = processingEnv.getFiler(); mElementUtils = processingEnv.getElementUtils(); mMessager = processingEnv.getMessager(); } /** * 处理器的主方法,用于扫描处理注解,生成java文件 */ @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { ... return false; } /** * 指定哪些注解应该被注解处理器注册 */ @Override public Set<String> getSupportedOptions() { Set<String> types = new LinkedHashSet<>(); types.add(ViewById.class.getName()); types.add(OnClick.class.getName()); return types; } /** * 用来指定你使用的 java 版本 */ @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } }
@AutoService(Processor.class)
属于auto-service库,可以自动生成META-INF/services/javax.annotation.processing.Processor文件(该文件是所有注解处理器都必须定义的),免去了我们手动配置的麻烦。init(ProcessingEnvironment processingEnvironment)
在处理器初始化的调用,通过processingEnv
参数我们可以拿到一些实用的工具类Elements
,Messager
和Filer
。我们在后面将会使用到它们。Elements
,一个用来处理Element
的工具类。Messager
,一个用来输出日志信息的工具类。Filer
、如这个类的名字所示,你可以使用这个类来创建文件。process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)
这是注解处理器的主方法,你可以在这个方法里面编码实现扫描,处理注解,生成 java 文件。getSupportedAnnotationTypes()
在这个方法里面你必须指定哪些注解应该被注解处理器注册。它的返回值是一个String集合,包含了你的注解处理器想要处理的注解类型的全限定名。getSupportedSourceVersion()
用来指定你使用的 java 版本,通常我们返回SourceVersion.latestSupported()
即可。
@SupportedSourceVersion(SourceVersion.RELEASE_7) @SupportedAnnotationTypes({ "proxytool.ViewById", "proxytool.OnClick" }) public class MyProcessor extends AbstractProcessor { @Override public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { return false; } @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); } }
但是考虑到兼容性问题,建议还是重写getSupportedSourceVersion
方法和getSupportedAnnotationTypes
方法
Element元素
在继续讲解处理器之前,我们必须先明白Elment
元素这个概念。在注解处理器中,我们扫描 java 源文件,源代码中的每一部分都是Element
的一个特定类型。换句话说:Element
代表程序中的元素,比如说
包,类,方法。在下面的例子中,我将添加注释来说明这个问题:
package com.example; //PackageElement public class Foo { // TypeElement private int a; // VariableElement private Foo other; // VariableElement public Foo() {} // ExecuteableElement public void setA( // ExecuteableElement int newA // TypeElement ) { } }
ExecuteableElement
:可以表示一个普通方法、构造方法、初始化方法(静态和实例)。PackageElement
:代表一个包名。TypeElement
:代表一个类、接口。VariableElement
:代表一个字段、枚举常量、方法或构造方法的参数、本地变量、或异常参数等。Element
:上述所有元素的父接口,代表源码中的每一个元素。
在注解处理器世界中,整个java代码被结构化了。我们需要像解析XML文件一样去解析整个源代码。Element
就像XML解析器中的DOM元素,你可以通过如下两个方法获取该元素的子元素和父元素:
Element getEnclosingElement(); //获取父元素 List<? extends Element> getEnclosedElements(); //获取子元素
比如你有如下的一个类
public class Foo { private int a; // VariableElement private Foo other; // VariableElement public Foo() {} // ExecuteableElement }
成员变量a
通过getEnclosingElement
方法返回的是的父元素是类Foo
,类Foo
通过getEnclosedElements
返回的子元素就包括成员变量a
、成员变量other
以及构造方法Foo()
。关于这两个方法更多的解释,可以参见官方文档。
实现process方法
1. 收集注解信息
注解处理器中,最核心的方法就是process()
,在这里你可以扫描和处理注解,并生成java文件。首先我们扫描所有被@ViewById
注解的元素:
@Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { //处理被ViewById注解的元素 for (Element element : roundEnv.getElementsAnnotatedWith(ViewById.class)) { if (!isValid(ViewById.class, "fields", element)) { return true; } parseViewById(element); } ...
通roundEnvironment.getElementsAnnotatedWith(ViewById.class)
方法返回一个被@ViewById
注解的Element
类型的元素列表。注意这里是Element
列表,而不是类列表,Element
可以包括类、方法、变量等。
2. 匹配准则
接下来我们需要对这个元素做进一步的检查,保证被注解的元素是符合规范的。如果使用者不按规范随意注解元素的话,程序是无法正常运行的。所以我们需要执行isVaid
方法用于检测被注解元素的合法性:
private boolean isValid(Class<? extends Annotation> annotationClass, String targetThing, Element element) { boolean isVaild = true; //获取变量的所在的父元素,肯能是类、接口、枚举 TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); //父元素的全限定名 String qualifiedName = enclosingElement.getQualifiedName().toString(); // 所在的类不能是private或static修饰 Set<Modifier> modifiers = element.getModifiers(); if (modifiers.contains(PRIVATE) || modifiers.contains(STATIC)) { error(element, "@%s %s must not be private or static. (%s.%s)", annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(), element.getSimpleName()); isVaild = false; } // 父元素必须是类,而不能是接口或枚举 if (enclosingElement.getKind() != ElementKind.CLASS) { error(enclosingElement, "@%s %s may only be contained in classes. (%s.%s)", annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(), element.getSimpleName()); isVaild = false; } //不能在Android框架层注解 if (qualifiedName.startsWith("android.")) { error(element, "@%s-annotated class incorrectly in Android framework package. (%s)", annotationClass.getSimpleName(), qualifiedName); return false; } //不能在java框架层注解 if (qualifiedName.startsWith("java.")) { error(element, "@%s-annotated class incorrectly in Java framework package. (%s)", annotationClass.getSimpleName(), qualifiedName); return false; } return isVaild; } private void error(Element e, String msg, Object... args) { mMessager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args), e); }
在isValid
方法中,它检查被注解的元素是否符合规则:
- 被注解元素的父元素必须是个类,而不能是接口、枚举。
- 被注解元素的父元素必须是非private 和 非static修饰。
- 被注解的元素只能注解非框架层元素。
这里只是简单的列出几个是否符合注解规范的条件,更严格的判断条件还需要大家来完善。
3. 错误处理
不知道大家有没有发现,在error
方法中利用了Messager
(init
方法中获取到的工具类)来处理错误信息。Messager
为注解处理器提供了一种报告错误消息,警告信息和其他消息的方式。它不是注解处理器开发者的日志工具。Messager
是用来给那些使用了你的注解处理器的第三方开发者显示信息的。
其中非常重要的是Kind.ERROR
级别信息,因为这种消息类型是用来表明我们的注解处理器在处理过程中出错了。有可能是第三方开发者误使用了我们的@ViewById
注解(比如,使用@ViewById
注解了一个接口中的变量)。这个概念与传统的
java 应用程序有一点区别。传统的 java 应用程序出现了错误,你可以抛出一个异常。如果你在process()
中抛出了一个异常,那 jvm 就会崩溃。注解处理器的使用者将会得到一个从 javac 给出的非常难懂的异常错误信息。因为它包含了注解处理器的堆栈信息。因此注解处理器提供了Messager
类。它能打印漂亮的错误信息,而且你可以链接到引起这个错误的元素上。回到process中:
@Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { //处理被ViewById注解的元素 for (Element element : roundEnvironment.getElementsAnnotatedWith(ViewById.class)) { if (!isValid(ViewById.class, "fields", element)) { return true; } parseViewById(element); } ... }
为了能够获取Messager
显示的信息,非常重要的是注解处理器必须不崩溃地完成运行。这就是我们在调用error()
后,跳出isValid
,执行return
的原因。如果我们在这里没有返回的话,
trueprocess()
就会继续运行,因为messager.printMessage(
并不会终止进程。
Diagnostic.Kind.ERROR)
4. 数据模型
一旦isValid
方法检查通过,那么就表示该注解元素是可以使用的,因此我们继续执行parseViewById
方法,把这些元素封装成model,供后面生成Java文件时使用。
private Map<String, ProxyClass> mProxyClassMap = new HashMap<>(); /** * 处理ViewById注解 * * @param element */ private void parseViewById(Element element) { ProxyClass proxyClass = getProxyClass(element); //把被注解的view对象封装成一个model,放入代理类的集合中 FieldViewBinding bindView = new FieldViewBinding(element); proxyClass.add(bindView); } /** * 生成或获取注解元素所对应的ProxyClass类 */ private ProxyClass getProxyClass(Element element) { //被注解的变量所在的类 TypeElement classElement = (TypeElement) element.getEnclosingElement(); String qualifiedName = classElement.getQualifiedName().toString(); ProxyClass proxyClass = mProxyClassMap.get(qualifiedName); if (proxyClass == null) { //生成每个宿主类所对应的代理类,后面用于生产java文件 proxyClass = new ProxyClass(classElement, mElementUtils); mProxyClassMap.put(qualifiedName, proxyClass); } return proxyClass; }
parseViewById(Element element)
:在该方法中,首先需要通过getProxyClass
方法获取一个ProxyClass
类型的对象。ProxyClass
代表了该注解元素所对应的类元素,这里我们利用面向对象的思想进行了封装。然后我们把被注解的元素也封装成一个FieldViewBinding类型的model,并放入到ProxyClass中。getProxyClass(Element element)
:该方法主要是生成或获取注解元素所对应的类,。你可以在getProxyClass
方法中看到,利用getEnclosingElement
方法获取了该注解元素的父元素,也就是该注解元素的所在的类。我们把每一个类元素TypeElement
都封装成了ProxyClass,并保存在HashMap中。
上面这两个方法的作用,主要是把注解的元素和注解元素所在的类都封装成了model,并存储起来,供我们后面生成Java源码时使用。下面表示了两个model类:
FieldViewBinding
类:
public class FieldViewBinding { /** 注解元素*/ private VariableElement mElement; /** 资源id*/ private int mResId; /** 变量名*/ private String mVariableName; /**变量类型*/ private TypeMirror mTypeMirror; public FieldViewBinding(Element element) { mElement = (VariableElement) element; ViewById viewById = element.getAnnotation(ViewById.class); //资源id mResId = viewById.value(); //变量名 mVariableName = element.getSimpleName().toString(); //变量类型 mTypeMirror = element.asType(); } public VariableElement getElement() { return mElement;} public int getResId() { return mResId;} public String getVariableName() {return mVariableName;} public TypeMirror getTypeMirror() {return mTypeMirror;} }
public class ProxyClass { /**类元素 */ public TypeElement mTypeElement; /**元素相关的辅助类*/ private Elements mElementUtils; /** FieldViewBinding类型的集合*/ private Set<FieldViewBinding> bindViews = new HashSet<>(); public ProxyClass(TypeElement mTypeElement, Elements mElementUtils) { this.mTypeElement = mTypeElement; this.mElementUtils = mElementUtils; } public void add(FieldViewBinding bindView) { bindViews.add(bindView); } /** * 用于生成代理类 */ public JavaFile generateProxy() { ... } }
ProxyClass
类中的generateProxy()方法是用于生成每个类所对应的代理类,比如类MainActivity
就会生成类MainActivity$$Proxy
,该方法生成的详细过程会后面再讲。
文件的生成
既然我们已经收集到了注解元素和注解元素所在的类,那么我们就需要为每个类生成一个全新的代理类,在代理类中执行那些冗余的代码操作。继续回到process方法中:
@Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { ... //为每个宿主类生成所对应的代理类 for (ProxyClass proxyClass_ : mProxyClassMap.values()) { try { proxyClass_.generateProxy().writeTo(mFiler); } catch (IOException e) { error(null, e.getMessage()); } } mProxyClassMap.clear(); return true; }
现在既然已经收集到了每个注解元素所对应的类,那么我们就需要为每个类生成所对应的代理类,遍历所有的类元素集合mProxyClassMap
通过ProxyClass
的generateProxy()
方法来生成Java源码:
ProxyClass##generateProxy()
//proxytool.IProxy public static final ClassName IPROXY = ClassName.get("proxytool.api", "IProxy"); //android.view.View public static final ClassName VIEW = ClassName.get("android.view", "View"); //生成代理类的后缀名 public static final String SUFFIX = "$$Proxy"; /** * 用于生成代理类 */ public JavaFile generateProxy() { //生成public void inject(final T target, View root)方法 MethodSpec.Builder injectMethodBuilder = MethodSpec.methodBuilder("inject") .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .addParameter(TypeName.get(mTypeElement.asType()), "target", Modifier.FINAL) .addParameter(VIEW, "root"); //在inject方法中,添加我们的findViewById逻辑 for (FieldViewBinding model : bindViews) { // find views injectMethodBuilder.addStatement("target.$N = ($T)(root.findViewById($L))", model.getVariableName(), ClassName.get(model.getTypeMirror()), model.getResId()); } // 添加以$$Proxy为后缀的类 TypeSpec finderClass = TypeSpec.classBuilder(mTypeElement.getSimpleName() + SUFFIX) .addModifiers(Modifier.PUBLIC) //添加父接口 .addSuperinterface(ParameterizedTypeName.get(IPROXY, TypeName.get(mTypeElement.asType()))) //把inject方法添加到该类中 .addMethod(injectMethodBuilder.build()) .build(); //添加包名 String packageName = mElementUtils.getPackageOf(mTypeElement).getQualifiedName().toString(); //生成Java文件 return JavaFile.builder(packageName, finderClass).build(); }
上面我们使用了javapoet来帮助我们生成Java源码,免去手动拼接字符串的麻烦。生成过程很简单,看注释就明白了。
不知道大家发现了没有循环结束后我们还执行了mProxyClassMap.clear()
,原因就在于process()
可以被多次调用,因为新生成的Java文件很可能包括@ViewById
注解,所以process方法会多次执行直到没有生成该注解为止。所以我们应该清空之前的数据,避免生成重复的代理类。
六、API模块
既然已经生成了代理类,那么我还需要提供API供使用者访问该代理类,供在Activity、Fragment、View中如下使用:
//Activity ProxyTool.bind(this); //Fragment ProxyTool.bind(this, view); //View ProxyTool.bind(this);
在ProxyTool
的bind
方法中我们需要为需要为不同的目标(比如
Activity、Fragment 和 View 等)提供重载的注入方法,这些方法最终都调用createBinding
方法:
public class ProxyTool { //Activity @UiThread public static void bind(@NonNull Activity target) { View sourceView = target.getWindow().getDecorView(); createBinding(target, sourceView); } //View @UiThread public static void bind(@NonNull View target) { createBinding(target, target); } //Fragment @UiThread public static void bind(@NonNull Object target, @NonNull View source) { createBinding(target, source); } public static final String SUFFIX = "$$Proxy"; public static void createBinding(@NonNull Object target, @NonNull View root) { try { //生成类名+后缀名的代理类,并执行注入操作 Class<?> targetClass = target.getClass(); Class<?> proxyClass = Class.forName(targetClass.getName() + SUFFIX); IProxy proxy = (IProxy) proxyClass.newInstance(); proxy.inject(target, root); } catch (Exception e) { e.printStackTrace(); } } }
- 我们重载了三个
bind
方法用于接收的不同目标(Activity、Fragment 和 View 等),target
参数表示注解元素所在的类,root
参数表示要查找的View的,因为Activity和View本身即是target也是root,所以只要一个参数即可,而Fragment中类和View是分离的,所以需要两个参数。 - 所有bind方法最终调用了
createBinding
方法,在该方法中我们通过targetClass.getName()
,拼接成代理类的全限定名,然后生成代理类的实例,并执行代理类的
+ SUFFIXinject
方法,该方法中执行的就是findViewById的操作。
这里我们还需要注意一点,所有生成的代理类都默认实现IProxy
接口:
public interface IProxy<T> { /** * @param target 所在的类 * @param root 查找 View 的地方 */ public void inject(final T target, View root); }
该接口定义了inject方法,代理类中需要在该方法中实现具体的注入逻辑。代理类的生成和inject方法的实现,都是在注解处理器模块中进行处理,具体的生成过程都在文件的生成
章节中,这里就不再讲。
七、项目中的使用
上面三个核心模块都已经介绍完了,现在让我们在具体的项目中使用吧。
首先在整个工程的build.gradle中添加如下:
dependencies { ... classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' }
然后在自己module的build.gradle中添加插件和依赖,如下所示:
apply plugin: 'com.neenbedankt.android-apt' ... dependencies { ... compile project(':proxytool-api') apt project(':proxytool-compiler') }
然后我们在项目中使用该框架的注解和API:
public class MainActivity extends AppCompatActivity { @ViewById(R.id.btnOne) Button btnOne; @ViewById(R.id.btnTwo) Button btnTwo; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ProxyTool.bind(this); } @OnClick({R.id.btnOne,R.id.btnTwo}) public void myClick(View view){ btnOne.setText("111111"); btnTwo.setText("222222"); } }
执行Make Project操作后,在build/generated/source/apt/debug/下就会生成所对应的代理类MainActivity$$Proxy
:
public class MainActivity$$Proxy implements IProxy<MainActivity> { @Override public void inject(final MainActivity target, View root) { target.btnTwo = (Button)(root.findViewById(2131427414)); target.btnOne = (Button)(root.findViewById(2131427413)); View.OnClickListener listener; listener = new View.OnClickListener() { @Override public void onClick(View view) { target.myClick(view); } } ; (root.findViewById(2131427413)).setOnClickListener(listener); (root.findViewById(2131427414)).setOnClickListener(listener); } }
八、总结
限于篇幅的考虑,上面只介绍了如何对ViewById
注解进行处理,OnClick
注解处理也是类似的。整个项目的地址:https://github.com/maofan4041/ProxyTool
参考:
ANNOTATION PROCESSING 101
Annotation-Processing-Tool详解
Android 利用 APT 技术在编译期生成代码
Android 如何编写基于编译时注解的项目
万能的APT!编译时注解的妙用
</pre><pre class="prettyprint" name="code" style="white-space: nowrap; word-wrap: break-word; box-sizing: border-box; position: relative; overflow-y: hidden; overflow-x: auto; margin-top: 0px; margin-bottom: 1.1em; font-family: "Source Code Pro", monospace; padding: 5px 5px 5px 60px; font-size: 14px; line-height: 1.45; word-break: break-all; color: rgb(51, 51, 51); background-color: rgba(128, 128, 128, 0.0470588); border: 1px solid rgba(128, 128, 128, 0.0745098); border-radius: 0px;">