├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── co │ │ └── jasonwyatt │ │ └── srmldemoapp │ │ ├── App.java │ │ ├── MainActivity.java │ │ ├── TargetActivity.java │ │ └── WidgetActivity.java │ └── res │ ├── layout │ ├── activity_main.xml │ ├── activity_target.xml │ └── activity_widget.xml │ ├── menu │ └── main_menu.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── docs └── srml.gif ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ ├── AndroidManifest.xml │ ├── java │ │ └── co │ │ │ └── jasonwyatt │ │ │ └── srml │ │ │ └── PerformanceTest.java │ └── res │ │ └── values │ │ └── strings.xml │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── co │ │ └── jasonwyatt │ │ └── srml │ │ ├── DefaultSanitizer.java │ │ ├── DefaultTransformer.java │ │ ├── SRML.java │ │ ├── SRMLImageLoader.java │ │ ├── SRMLTextView.java │ │ ├── Sanitizer.java │ │ ├── Transformer.java │ │ ├── tags │ │ ├── BadParameterException.java │ │ ├── BadTagException.java │ │ ├── Bold.java │ │ ├── Code.java │ │ ├── Color.java │ │ ├── CouldNotCreateTagException.java │ │ ├── CouldNotRegisterTagException.java │ │ ├── DrawableTag.java │ │ ├── FontTag.java │ │ ├── IntentTag.java │ │ ├── Italic.java │ │ ├── Link.java │ │ ├── ParameterMissingException.java │ │ ├── ParameterizedTag.java │ │ ├── Strikethrough.java │ │ ├── StyledClickableSpan.java │ │ ├── Subscript.java │ │ ├── Superscript.java │ │ ├── Tag.java │ │ ├── TagFactory.java │ │ └── Underline.java │ │ └── utils │ │ ├── FontSpan.java │ │ ├── SafeString.java │ │ └── Utils.java │ └── test │ └── java │ └── co │ └── jasonwyatt │ └── srml │ ├── tags │ ├── IntentTagTest.java │ └── ParameterizedTagTest.java │ └── utils │ ├── SafeStringTest.java │ └── UtilsTest.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # Intellij 36 | *.iml 37 | .idea 38 | 39 | # Keystore files 40 | *.jks 41 | 42 | # Performance traces 43 | *.trace 44 | 45 | # Stupid stuff 46 | .DS_Store 47 | 48 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | 3 | android: 4 | components: 5 | - tools 6 | - platform-tools 7 | - build-tools-24.0.2 8 | - android-24 9 | - extra-android-m2repository 10 | - sys-img-armeabi-v7a-android-18 11 | 12 | jdk: 13 | - oraclejdk8 14 | 15 | #before_script: 16 | # # Create and start an emulator for instrumentation tests. 17 | # - echo no | android create avd --force -n test -t android-18 --abi armeabi-v7a 18 | # - emulator -avd test -no-audio -no-window & 19 | # - android-wait-for-emulator 20 | # - adb shell input keyevent 82 21 | 22 | script: 23 | - ./gradlew :library:build :library:test 24 | 25 | branches: 26 | except: 27 | - gh-pages 28 | 29 | notifications: 30 | email: false 31 | 32 | sudo: false 33 | 34 | cache: 35 | directories: 36 | - $HOME/.m2 37 | - $HOME/.gradle 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SRML [![Build Status](https://travis-ci.org/jasonwyatt/SRML.svg?branch=master)](https://travis-ci.org/jasonwyatt/SRML) [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-SRML-brightgreen.svg?style=flat)](http://android-arsenal.com/details/1/4672) [![](https://jitpack.io/v/jasonwyatt/SRML.svg)](https://jitpack.io/#jasonwyatt/SRML) 2 | 3 | SRML: "String Resource Markup Language" 4 | 5 | Mark up your Android string resources with an impressive suite of formatting tags. 6 | 7 | ![Preview](docs/srml.gif) 8 | 9 | ## SRML Tags 10 | 11 | * [Bold](../../wiki/Tags#bold) 12 | * [Italic](../../wiki/Tags#italic) 13 | * [Underline](../../wiki/Tags#underline) 14 | * [Strikethrough](../../wiki/Tags#strikethrough) 15 | * [Superscript](../../wiki/Tags#superscript) 16 | * [Subscript](../../wiki/Tags#subscript) 17 | * [Code](../../wiki/Tags#code) 18 | * [Color](../../wiki/Tags#color) (foreground and background) 19 | * [Font](../../wiki/Tags#font) 20 | * [Link](../../wiki/Tags#link) 21 | * [Intent](../../wiki/Tags#intent) 22 | * [Drawable](../../wiki/Tags#drawable) 23 | * ... or [create your own](../../wiki/Custom%20Tags) tags! 24 | 25 | ## Setup 26 | 27 | Add [jitpack.io](https://jitpack.io) to your root `build.gradle` at the end of `repositories`: 28 | 29 | ```groovy 30 | allprojects { 31 | repositories { 32 | ... 33 | maven { url "https://jitpack.io" } 34 | } 35 | } 36 | ``` 37 | 38 | Add SRML as a dependency to your app's `build.gradle`: 39 | 40 | ```groovy 41 | dependencies { 42 | compile 'com.github.jasonwyatt:SRML:0.6.0' 43 | } 44 | ``` 45 | 46 | ## How to use 47 | 48 | ```java 49 | // simple case 50 | SRML.getString(context, R.string.mystring); 51 | 52 | // parameterized strings 53 | SRML.getString(context, R.string.my_parameterized_string, firstArg, secondArg, ...); 54 | 55 | // quantity strings 56 | SRML.getQuantityString(context, R.plurals.my_plurals_resource, quantity, ...format args...); 57 | 58 | // String array resources 59 | SRML.getStringArray(context, R.array.my_string_array); 60 | ``` 61 | 62 | Your resources can be arbitrarily complex, involving multiple, nested tags. 63 | 64 | ### SRMLTextView 65 | 66 | For ease of use, you can use `SRMLTextView` in place of `TextView` objects in your layouts, and it will automatically mark-up any text passed to it via `setText()`. 67 | 68 | ## Contributing 69 | 70 | Fork the repository, and clone your fork locally. After you do that, follow these guidelines: 71 | 72 | * Make sure you're doing your work on the `develop` branch. 73 | * Make your changes/improvements. 74 | * Make unit tests for your changes/improvements (preferable) 75 | * Be sure both your unit tests *and* the ones already in the project pass. 76 | * Commit, push, and open a pull request. Be sure to reference any bugs or feature enhancements your work pertains to (if any) in the pull request. 77 | * Be ready to discuss your pull request and celebrate its acceptance! 78 | 79 | ## License 80 | 81 | ``` 82 | Copyright 2016 Jason Feinstein 83 | 84 | Licensed under the Apache License, Version 2.0 (the "License"); 85 | you may not use this file except in compliance with the License. 86 | You may obtain a copy of the License at 87 | 88 | http://www.apache.org/licenses/LICENSE-2.0 89 | 90 | Unless required by applicable law or agreed to in writing, software 91 | distributed under the License is distributed on an "AS IS" BASIS, 92 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 93 | See the License for the specific language governing permissions and 94 | limitations under the License. 95 | ``` 96 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 24 5 | buildToolsVersion "24.0.2" 6 | defaultConfig { 7 | applicationId "co.jasonwyatt.srmldemoapp" 8 | minSdkVersion 15 9 | targetSdkVersion 24 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | debug { 16 | debuggable true 17 | } 18 | 19 | release { 20 | debuggable true 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | } 26 | 27 | dependencies { 28 | compile fileTree(dir: 'libs', include: ['*.jar']) 29 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 30 | exclude group: 'com.android.support', module: 'support-annotations' 31 | }) 32 | compile 'com.android.support:appcompat-v7:24.2.1' 33 | testCompile 'junit:junit:4.12' 34 | 35 | compile 'com.squareup.picasso:picasso:2.5.2' 36 | compile project(':library') 37 | } 38 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/jason/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/java/co/jasonwyatt/srmldemoapp/App.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srmldemoapp; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import android.graphics.Bitmap; 6 | 7 | import com.squareup.picasso.Picasso; 8 | 9 | import java.io.IOException; 10 | 11 | import co.jasonwyatt.srml.SRML; 12 | import co.jasonwyatt.srml.SRMLImageLoader; 13 | 14 | public class App extends Application { 15 | @Override 16 | public void onCreate() { 17 | super.onCreate(); 18 | 19 | SRML.setImageLoader(new SRMLImageLoader() { 20 | @Override 21 | public Bitmap loadImage(Context context, String url) { 22 | try { 23 | return Picasso.with(context).load(url).get(); 24 | } catch (IOException e) { 25 | throw new RuntimeException(e); 26 | } 27 | } 28 | 29 | @Override 30 | public Bitmap loadImage(Context context, String url, int width, int height) { 31 | try { 32 | return Picasso.with(context).load(url).resize(width, height).get(); 33 | } catch (IOException e) { 34 | throw new RuntimeException(e); 35 | } 36 | } 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/co/jasonwyatt/srmldemoapp/MainActivity.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srmldemoapp; 2 | 3 | import android.content.Intent; 4 | import android.os.AsyncTask; 5 | import android.support.annotation.NonNull; 6 | import android.support.v7.app.AppCompatActivity; 7 | import android.os.Bundle; 8 | import android.text.method.LinkMovementMethod; 9 | import android.view.Menu; 10 | import android.view.MenuInflater; 11 | import android.view.MenuItem; 12 | import android.view.View; 13 | import android.view.ViewGroup; 14 | import android.widget.ArrayAdapter; 15 | import android.widget.ListView; 16 | import android.widget.TextView; 17 | 18 | import co.jasonwyatt.srml.SRML; 19 | import co.jasonwyatt.srml.utils.SafeString; 20 | 21 | public class MainActivity extends AppCompatActivity { 22 | private ListView mListView; 23 | 24 | @Override 25 | protected void onCreate(Bundle savedInstanceState) { 26 | super.onCreate(savedInstanceState); 27 | setContentView(R.layout.activity_main); 28 | 29 | mListView = (ListView) findViewById(R.id.activity_main); 30 | } 31 | 32 | @Override 33 | public boolean onOptionsItemSelected(MenuItem item) { 34 | if (item.getItemId() == R.id.widget_item) { 35 | Intent i = new Intent(this, WidgetActivity.class); 36 | i.setAction(Intent.ACTION_VIEW); 37 | startActivity(i); 38 | return true; 39 | } 40 | return super.onOptionsItemSelected(item); 41 | } 42 | 43 | @Override 44 | public boolean onCreateOptionsMenu(Menu menu) { 45 | MenuInflater inf = getMenuInflater(); 46 | inf.inflate(R.menu.main_menu, menu); 47 | return true; 48 | } 49 | 50 | @Override 51 | protected void onResume() { 52 | super.onResume(); 53 | final ArrayAdapter adapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1) { 54 | @NonNull 55 | @Override 56 | public View getView(int position, View convertView, @NonNull ViewGroup parent) { 57 | View v = super.getView(position, convertView, parent); 58 | if (v instanceof TextView) { 59 | ((TextView) v).setMovementMethod(LinkMovementMethod.getInstance()); 60 | } 61 | return v; 62 | } 63 | }; 64 | 65 | // Only doing this in the background because the string array contains an {{image}} tag with 66 | // a url, and we use picasso for image fetching, which balks when you try to load an image 67 | // on the main thread. 68 | new AsyncTask() { 69 | @Override 70 | protected void onPostExecute(CharSequence[] charSequences) { 71 | adapter.addAll(charSequences); 72 | adapter.add(SRML.getString(MainActivity.this, R.string.safestring_text, new SafeString("{{b}}bolded SafeString{{/b}}"))); 73 | } 74 | 75 | @Override 76 | protected CharSequence[] doInBackground(Void... params) { 77 | return SRML.getStringArray(MainActivity.this, R.array.test_strings); 78 | } 79 | }.execute(); 80 | mListView.setAdapter(adapter); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/co/jasonwyatt/srmldemoapp/TargetActivity.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srmldemoapp; 2 | 3 | import android.content.Intent; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.os.Bundle; 6 | import android.widget.TextView; 7 | 8 | import co.jasonwyatt.srml.SRML; 9 | 10 | public class TargetActivity extends AppCompatActivity { 11 | 12 | @Override 13 | protected void onCreate(Bundle savedInstanceState) { 14 | super.onCreate(savedInstanceState); 15 | setContentView(R.layout.activity_target); 16 | 17 | setTitle(R.string.another_activity_title); 18 | 19 | Intent i = getIntent(); 20 | TextView textView = (TextView) findViewById(R.id.text); 21 | if (i.hasExtra("text")) { 22 | textView.setText(SRML.getString(this, R.string.text_extra_sent, i.getStringExtra("text"))); 23 | } else { 24 | textView.setText(SRML.getString(this, R.string.no_text_extra_sent)); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/co/jasonwyatt/srmldemoapp/WidgetActivity.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srmldemoapp; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | import android.os.Bundle; 5 | import android.text.method.LinkMovementMethod; 6 | import android.widget.TextView; 7 | 8 | public class WidgetActivity extends AppCompatActivity { 9 | 10 | @Override 11 | protected void onCreate(Bundle savedInstanceState) { 12 | super.onCreate(savedInstanceState); 13 | setContentView(R.layout.activity_widget); 14 | 15 | ((TextView)findViewById(R.id.srml_text_view)).setMovementMethod(LinkMovementMethod.getInstance()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_target.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_widget.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 23 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/menu/main_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwyatt/SRML/60a2aac725fb7ed94aa9f8f8f74838c1be9f5b25/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwyatt/SRML/60a2aac725fb7ed94aa9f8f8f74838c1be9f5b25/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwyatt/SRML/60a2aac725fb7ed94aa9f8f8f74838c1be9f5b25/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwyatt/SRML/60a2aac725fb7ed94aa9f8f8f74838c1be9f5b25/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwyatt/SRML/60a2aac725fb7ed94aa9f8f8f74838c1be9f5b25/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | #FBB829 7 | #FCFBE3 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | SRML Demo App 3 | Another Activity 4 | No value for extra \"test\" 5 | Value for extra \"test\" = {{code}}%s{{/code}} 6 | Widgets 7 | From the {{code}}TextView{{/code}} JavaDocs:\n\nDisplays text to the user and optionally allows them to edit it. A {{code}}TextView{{/code}} is a complete text editor, however the basic class is configured to not allow editing\; see {{link url=https://developer.android.com/reference/android/widget/EditText.html underline=false}}EditText{{/link}} for a subclass that configures the text view for editing.\n\nTo allow users to copy some or all of the TextView\'s value and paste it somewhere else, set the XML attribute {{code}}{{link url=https://developer.android.com/reference/android/R.styleable.html#TextView_textIsSelectable underline=false}}android:textIsSelectable{{/link}}{{/code}} to \"true\" or call {{code}}{{link url=https://developer.android.com/reference/android/widget/TextView.html#setTextIsSelectable(boolean) underline=false}}setTextIsSelectable(true){{/link}}{{/code}}. The {{code}}{{color fg=#0A0}}textIsSelectable{{/color}}{{/code}} flag allows users to make selection gestures in the TextView, which in turn triggers the system\'s built-in copy/paste controls. 8 | SRMLTextView 9 | Here\'s some SafeString text: %s 10 | 11 | 12 | This is {{b}}a test{{/b}} of {{b}}bold{{/b}} parsing. 13 | This is {{i}}a test{{/i}} of {{i}}italic{{/i}} parsing. 14 | This is {{b}}{{i}}a test{{/i}}{{/b}} of {{b}}{{i}}bold/italic{{/i}}{{/b}} parsing. 15 | This is {{u}}a test{{/u}} of {{u}}underline{{/u}} parsing. 16 | This is {{strike}}a test{{/strike}} of {{strike}}strikethrough{{/strike}} parsing. 17 | Superscript{{sup}}is quite cool{{/sup}} 18 | Subscript{{sub}}is very chill{{/sub}} 19 | {{color fg=#33000000}}This{{/color}} is a {{color fg=#FF00FF}}test case{{/color}} of {{color fg=#F00}}colored{{/color}} text. 20 | {{color bg=#33000000}}This{{/color}} is a {{color bg=#FF00FF}}test case{{/color}} of {{color bg=#F00}}colored{{/color}} text backgrounds. 21 | Let\'s do some {{color fg=#fff bg=#AA000000}}white text on a black background{{/color}} 22 | Here is some {{color fg=R.color.heart_of_gold}}text{{/color}} that was {{color bg=R.color.vanilla_creme}}colored{{/color}} by resource id. 23 | {{link url=http://www.google.com color=#F0F underline=true}}This{{/link}} is a {{link url=http://www.bandcamp.com}}test case{{/link}} with a couple of links. 24 | Here\'s some code: {{code}}hello world{{/code}} 25 | Click {{intent class=co.jasonwyatt.srmldemoapp.TargetActivity color=#0F0 underline=false}}here{{/intent}} to launch another activity. 26 | Click {{intent class=co.jasonwyatt.srmldemoapp.TargetActivity x_text=`hello world`}}here{{/intent}} to launch another activity with an extra. 27 | Let\'s try an image by a resource id: {{drawable res=R.mipmap.ic_launcher width=16dp height=16dp /}} - sweet! 28 | Let\'s try an image from a url: {{drawable url=http://emojis.slackmojis.com/emojis/images/1457563042/312/doge.png /}} - sweet! 29 | 30 | {{b}}Lorem {{i}}ipsum{{/i}}{{/b}} dolor sit amet, consectetuer adipiscing elit. Phasellus hendrerit. 31 | Pellentesque aliquet nibh nec urna. In nisi neque, aliquet vel, dapibus id, mattis vel, 32 | nisi. {{color fg=#f00}}Sed{{/color}} {{color fg=#0f0}}pretium{{/color}}, {{color fg=#00f}}ligula{{/color}} sollicitudin laoreet viverra, tortor libero sodales leo, eget 33 | blandit nunc tortor eu nibh. Nullam mollis. Ut justo. Suspendisse potenti.\n\n 34 | 35 | {{drawable url=http://emojis.slackmojis.com/emojis/images/1450735852/239/charmander.png?1450735852 /}}\n\n 36 | 37 | Sed egestas, ante et vulputate volutpat, eros pede semper est, vitae luctus metus libero 38 | eu augue. {{color bg=#FFFBCC}}Morbi purus libero, faucibus adipiscing, commodo quis, gravida id, est.{{/color}} Sed 39 | lectus. Praesent {{link url=http://srml.jasonwyatt.co}}elementum hendrerit tortor{{/link}}. Sed semper lorem at felis. Vestibulum 40 | volutpat, lacus a ultrices sagittis, mi neque euismod dui, eu pulvinar nunc sapien 41 | ornare nisl. Phasellus pede arcu, dapibus eu, fermentum et, dapibus sed, urna. 42 | 43 | 44 | {{font style=bold|italic|underline letter_spacing=0.5 line_height=40sp fg=#555 size=20sp typeface=serif}}Here\'s some text formatted using the font tag with several attributes: style=bold|italic|underline, letter_spacing=0.5, line_height=40sp, fg=#555, typeface=serif{{/font}}\n\nIsn\'t it cool? 45 | 46 | 47 | {{font text_appearance=R.style.my_text_appearance}}Here\'s some text formatted using the font tag with a text appearance style{{/font}}\n\nIsn\'t it cool? 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.2.2' 9 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | 21 | task clean(type: Delete) { 22 | delete rootProject.buildDir 23 | } 24 | -------------------------------------------------------------------------------- /docs/srml.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwyatt/SRML/60a2aac725fb7ed94aa9f8f8f74838c1be9f5b25/docs/srml.gif -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwyatt/SRML/60a2aac725fb7ed94aa9f8f8f74838c1be9f5b25/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Dec 28 10:00:20 PST 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'com.github.dcendents.android-maven' 3 | 4 | group='com.github.jasonwyatt' 5 | 6 | android { 7 | compileSdkVersion 24 8 | buildToolsVersion "24.0.2" 9 | 10 | defaultConfig { 11 | minSdkVersion 15 12 | targetSdkVersion 24 13 | versionCode 1 14 | versionName "1.0" 15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 16 | } 17 | } 18 | 19 | dependencies { 20 | compile fileTree(dir: 'libs', include: ['*.jar']) 21 | compile 'com.android.support:support-annotations:24.2.1' 22 | 23 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 24 | exclude group: 'com.android.support', module: 'support-annotations' 25 | }) 26 | testCompile 'junit:junit:4.12' 27 | //noinspection GradleDynamicVersion 28 | testCompile "org.mockito:mockito-core:1.+" 29 | } 30 | 31 | // build a jar with source files 32 | task sourcesJar(type: Jar) { 33 | from android.sourceSets.main.java.srcDirs 34 | classifier = 'sources' 35 | } 36 | 37 | task javadoc(type: Javadoc) { 38 | failOnError false 39 | source = android.sourceSets.main.java.sourceFiles 40 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 41 | classpath += configurations.compile 42 | } 43 | 44 | // build a jar with javadoc 45 | task javadocJar(type: Jar, dependsOn: javadoc) { 46 | classifier = 'javadoc' 47 | from javadoc.destinationDir 48 | } 49 | 50 | artifacts { 51 | archives sourcesJar 52 | archives javadocJar 53 | } -------------------------------------------------------------------------------- /library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/jason/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /library/src/androidTest/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /library/src/androidTest/java/co/jasonwyatt/srml/PerformanceTest.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml; 2 | 3 | import android.content.Context; 4 | import android.os.Build; 5 | import android.support.annotation.RequiresApi; 6 | import android.support.test.InstrumentationRegistry; 7 | import android.support.test.runner.AndroidJUnit4; 8 | import android.text.Html; 9 | import android.util.Log; 10 | 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | 14 | import java.util.Locale; 15 | 16 | import static org.junit.Assert.assertTrue; 17 | 18 | /** 19 | * @author jason 20 | * 21 | * Performance tests. They fail if {@link SRML#getString} is slower than {@link Html#fromHtml}. 22 | */ 23 | @RunWith(AndroidJUnit4.class) 24 | public class PerformanceTest { 25 | private static final String TAG = PerformanceTest.class.getSimpleName(); 26 | 27 | @Test 28 | public void testSRMLvsHTMLLegacy() { 29 | int iterations = 100000; 30 | Context context = InstrumentationRegistry.getContext(); 31 | 32 | long startTime = System.currentTimeMillis(); 33 | for (int i = 0; i < iterations; i++) { 34 | SRML.getString(context, R.string.simple_srml_string); 35 | } 36 | long elapsedSRML = System.currentTimeMillis() - startTime; 37 | 38 | startTime = System.currentTimeMillis(); 39 | for (int i = 0; i < iterations; i++) { 40 | Html.fromHtml(context.getString(R.string.simple_html_string)); 41 | } 42 | long elapsedHTML = System.currentTimeMillis() - startTime; 43 | 44 | double srmlTimePer = elapsedSRML / (double) iterations; 45 | double htmlTimePer = elapsedHTML / (double) iterations; 46 | Log.i(TAG, String.format(Locale.US, "SRML.getString averaged %.2fms", srmlTimePer)); 47 | Log.i(TAG, String.format(Locale.US, "Html.fromHtml averaged %.2fms", htmlTimePer)); 48 | assertTrue("SRML.getString should be as fast, or faster than, Html.fromHtml.", srmlTimePer <= 2* htmlTimePer); 49 | } 50 | 51 | @Test 52 | public void testSRMLvsHTMLLegacyWithParam() { 53 | int iterations = 100000; 54 | Context context = InstrumentationRegistry.getContext(); 55 | 56 | long startTime = System.currentTimeMillis(); 57 | for (int i = 0; i < iterations; i++) { 58 | SRML.getString(context, R.string.simple_srml_string_with_param, Integer.toString(i)); 59 | } 60 | long elapsedSRML = System.currentTimeMillis() - startTime; 61 | 62 | startTime = System.currentTimeMillis(); 63 | for (int i = 0; i < iterations; i++) { 64 | Html.fromHtml(context.getString(R.string.simple_html_string_with_param, Integer.toString(i))); 65 | } 66 | long elapsedHTML = System.currentTimeMillis() - startTime; 67 | 68 | double srmlTimePer = elapsedSRML / (double) iterations; 69 | double htmlTimePer = elapsedHTML / (double) iterations; 70 | Log.i(TAG, String.format(Locale.US, "SRML.getString averaged %.2fms", srmlTimePer)); 71 | Log.i(TAG, String.format(Locale.US, "Html.fromHtml averaged %.2fms", htmlTimePer)); 72 | assertTrue("SRML.getString should be as fast, or faster than, Html.fromHtml.", srmlTimePer <= 2* htmlTimePer); 73 | } 74 | 75 | @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) 76 | @Test 77 | public void testSRMLvsHTMLLegacyWithDirtyParam() { 78 | int iterations = 100000; 79 | Context context = InstrumentationRegistry.getContext(); 80 | 81 | long startTime = System.currentTimeMillis(); 82 | for (int i = 0; i < iterations; i++) { 83 | SRML.getString(context, R.string.simple_srml_string_with_param, String.format(Locale.US, "{{b}}%d{{/b}}", i)); 84 | } 85 | long elapsedSRML = System.currentTimeMillis() - startTime; 86 | 87 | startTime = System.currentTimeMillis(); 88 | for (int i = 0; i < iterations; i++) { 89 | Html.fromHtml(context.getString(R.string.simple_html_string_with_param, Html.escapeHtml(String.format(Locale.US, "%d", i)))); 90 | } 91 | long elapsedHTML = System.currentTimeMillis() - startTime; 92 | 93 | double srmlTimePer = elapsedSRML / (double) iterations; 94 | double htmlTimePer = elapsedHTML / (double) iterations; 95 | Log.i(TAG, String.format(Locale.US, "SRML.getString averaged %.2fms", srmlTimePer)); 96 | Log.i(TAG, String.format(Locale.US, "Html.fromHtml averaged %.2fms", htmlTimePer)); 97 | assertTrue("SRML.getString should be as fast, or faster than, Html.fromHtml.", srmlTimePer <= 2* htmlTimePer); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /library/src/androidTest/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | SRML 3 | 4 | {{b}}This{{/b}} is bold, while {{i}}this{{/i}} is italic. 5 | 6 | This is bold, while this is italic. 8 | ]]> 9 | 10 | {{b}}This{{/b}} is bold, while {{i}}this{{/i}} is italic; here is a bolded parameter: {{b}}%s{{/b}}. 11 | 12 | This is bold, while this is italic; here is a bolded parameter: %s 14 | ]]> 15 | 16 | -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/DefaultSanitizer.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml; 2 | 3 | import java.util.regex.Matcher; 4 | import java.util.regex.Pattern; 5 | 6 | import co.jasonwyatt.srml.utils.SafeString; 7 | 8 | /** 9 | * @author jason 10 | * 11 | * Sanitizes parameters for parameterized strings, and unsanitizes them later. 12 | */ 13 | public class DefaultSanitizer implements Sanitizer { 14 | private static final Pattern SANITIZE_PATTERN = Pattern.compile("\\{{2}"); 15 | private static final Pattern DESANITIZE_PATTERN = Pattern.compile("\u0000{2}"); 16 | private static final String SANITIZE_REPLACEMENT = "\u0000\u0000"; 17 | private static final String DESANITIZE_REPLACEMENT = "{{"; 18 | 19 | public DefaultSanitizer() { 20 | // 21 | } 22 | 23 | @Override 24 | public Object[] sanitizeArgs(Object[] formatArgs) { 25 | if (formatArgs == null) { 26 | return null; 27 | } 28 | for (int i = 0; i < formatArgs.length; i++) { 29 | if (formatArgs[i] instanceof CharSequence && !(formatArgs[i] instanceof SafeString)) { 30 | Matcher m = SANITIZE_PATTERN.matcher((CharSequence) formatArgs[i]); 31 | formatArgs[i] = m.replaceAll(SANITIZE_REPLACEMENT); 32 | } 33 | } 34 | return formatArgs; 35 | } 36 | 37 | @Override 38 | public String desantitize(String s) { 39 | Matcher m = DESANITIZE_PATTERN.matcher(s); 40 | return m.replaceAll(DESANITIZE_REPLACEMENT); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/DefaultTransformer.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml; 2 | 3 | import android.content.Context; 4 | import android.text.SpannableStringBuilder; 5 | 6 | import java.util.Stack; 7 | import java.util.regex.Matcher; 8 | import java.util.regex.Pattern; 9 | 10 | import co.jasonwyatt.srml.tags.BadTagException; 11 | import co.jasonwyatt.srml.tags.Tag; 12 | import co.jasonwyatt.srml.tags.TagFactory; 13 | 14 | /** 15 | * The default implementation of {@link Transformer} for SRML. 16 | * @author jason 17 | */ 18 | public class DefaultTransformer implements Transformer { 19 | 20 | private static final Pattern TAG_PATTERN = Pattern.compile("\\{\\{(/)?(([-a-zA-Z]+)(\\s+[^\\}]+)?)\\}\\}", Pattern.CASE_INSENSITIVE); 21 | private final Sanitizer mSanitizer; 22 | private final TagFactory mTagFactory; 23 | 24 | public DefaultTransformer() { 25 | mSanitizer = new DefaultSanitizer(); 26 | mTagFactory = new TagFactory(); 27 | } 28 | 29 | @Override 30 | public CharSequence transform(Context context, String srmlString) { 31 | SpannableStringBuilder builder = new SpannableStringBuilder(); 32 | 33 | Matcher m = TAG_PATTERN.matcher(srmlString); 34 | Stack tags = new Stack<>(); 35 | int lastEnd = 0; 36 | while (m.find()) { 37 | builder.append(mSanitizer.desantitize(srmlString.substring(lastEnd, m.start()))); 38 | 39 | String tagDetails = m.group(2); 40 | String tagName = m.group(3); 41 | boolean isSelfClosed = tagDetails.endsWith("/"); 42 | if (m.group(1) != null) { 43 | // it's a closing tag. 44 | Tag stackTop = tags.isEmpty() ? null : tags.peek(); 45 | if (stackTop != null && stackTop.matchesClosingTag(tagName)) { 46 | stackTop.operate(context, builder, builder.length()); 47 | tags.pop(); 48 | } 49 | } else if (isSelfClosed) { 50 | Tag tag = mTagFactory.getTag(tagName, tagDetails.substring(0, tagDetails.length()-1), builder.length()); 51 | if (tag.canBeEmpty()) { 52 | appendNonbreaking(builder, tag.getRequiredSpacesWhenEmpty()); 53 | tag.operate(context, builder, builder.length()); 54 | } else { 55 | throw new BadTagException("Tag "+m.group(0)+" is not allowed to be self-closing."); 56 | } 57 | } else { 58 | // it's a new opening tag. 59 | tags.push(mTagFactory.getTag(tagName, tagDetails, builder.length())); 60 | } 61 | 62 | lastEnd = m.end(); 63 | } 64 | // get the rest of the string.. 65 | builder.append(mSanitizer.desantitize(srmlString.substring(lastEnd, srmlString.length()))); 66 | // clean out any remaining tags... 67 | while (!tags.isEmpty()) { 68 | Tag t = tags.pop(); 69 | t.operate(context, builder, builder.length()); 70 | } 71 | 72 | return builder; 73 | } 74 | 75 | void appendNonbreaking(SpannableStringBuilder builder, int spaces) { 76 | for (int i = 0; i < spaces; i++) { 77 | builder.append("\u00A0"); 78 | } 79 | } 80 | 81 | @Override 82 | public Sanitizer getSanitizer() { 83 | return mSanitizer; 84 | } 85 | 86 | @Override 87 | public TagFactory getTagFactory() { 88 | return mTagFactory; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/SRML.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.ArrayRes; 5 | import android.support.annotation.PluralsRes; 6 | import android.support.annotation.StringRes; 7 | 8 | import co.jasonwyatt.srml.tags.Tag; 9 | import co.jasonwyatt.srml.utils.Utils; 10 | 11 | /** 12 | * SRML stands for String Resource Markup Language. 13 | * 14 | * @author jason 15 | */ 16 | public final class SRML { 17 | private static Transformer sTransformer; 18 | 19 | static { 20 | configure(new DefaultTransformer(), null); 21 | } 22 | 23 | private SRML() { 24 | // no outside instantiation needed plz 25 | } 26 | 27 | /** 28 | * Supply a {@link Transformer} for SRML. 29 | * @param transformer The new transformer. 30 | * @param imageLoader An {@link SRMLImageLoader} for SRML. 31 | */ 32 | public static void configure(Transformer transformer, SRMLImageLoader imageLoader) { 33 | sTransformer = transformer; 34 | setImageLoader(imageLoader); 35 | } 36 | 37 | /** 38 | * Set an image loader for SRML. 39 | * @param imageLoader The new image loader. 40 | */ 41 | public static void setImageLoader(SRMLImageLoader imageLoader) { 42 | Utils.setImageLoader(imageLoader); 43 | } 44 | 45 | /** 46 | * Register a new {@link Tag} type with SRML. 47 | * @param name The tag's name, aka: the first part in a tag after the {{ or {{/ 48 | * @param tagClass The class for an implementation of {@link Tag} 49 | */ 50 | public static void registerTag(String name, Class tagClass) { 51 | sTransformer.getTagFactory().registerTag(name, tagClass); 52 | } 53 | 54 | /** 55 | * Analog of {@link Context#getString(int)} for SRML. 56 | * @param context Context to retrieve the string resource from. 57 | * @param resId String resource. 58 | * @return The templatized string. 59 | */ 60 | public static CharSequence getString(Context context, @StringRes int resId) { 61 | return getString(context, sTransformer, resId); 62 | } 63 | 64 | /** 65 | * Analog of {@link Context#getString(int, Object[])} for SRML. 66 | * @param context Context to retrieve the string resource from. 67 | * @param resId String resource. 68 | * @param formatArgs Format arguments for the string, passed along to {@link Context#getString(int, Object[]} 69 | * @return The templatized string. 70 | */ 71 | public static CharSequence getString(Context context, @StringRes int resId, Object... formatArgs) { 72 | return getString(context, sTransformer, resId, formatArgs); 73 | } 74 | 75 | public static CharSequence[] getStringArray(Context context, @ArrayRes int resId) { 76 | return getStringArray(context, sTransformer, resId); 77 | } 78 | 79 | public static CharSequence getQuantityString(Context context, @PluralsRes int resId, int quantity) { 80 | return getQuantityString(context, sTransformer, resId, quantity); 81 | } 82 | 83 | public static CharSequence getQuantityString(Context context, @PluralsRes int resId, int quantity, Object... formatArgs) { 84 | return getQuantityString(context, sTransformer, resId, quantity, formatArgs); 85 | } 86 | 87 | public static CharSequence getString(Context context, Transformer transformer, @StringRes int resId) { 88 | return transformer.transform(context, context.getString(resId)); 89 | } 90 | 91 | public static CharSequence getString(Context context, Transformer transformer, @StringRes int resId, Object... formatArgs) { 92 | return transformer.transform(context, context.getString(resId, transformer.getSanitizer().sanitizeArgs(formatArgs))); 93 | } 94 | 95 | public static CharSequence[] getStringArray(Context context, Transformer transformer, @ArrayRes int resId) { 96 | String[] strings = context.getResources().getStringArray(resId); 97 | CharSequence[] result = new CharSequence[strings.length]; 98 | for (int i = 0, len = result.length; i < len; i++) { 99 | result[i] = transformer.transform(context, strings[i]); 100 | } 101 | return result; 102 | } 103 | 104 | public static CharSequence getQuantityString(Context context, Transformer transformer, @PluralsRes int resId, int quantity) { 105 | return transformer.transform(context, context.getResources().getQuantityString(resId, quantity)); 106 | } 107 | 108 | public static CharSequence getQuantityString(Context context, Transformer transformer, @PluralsRes int resId, int quantity, Object... formatArgs) { 109 | return transformer.transform(context, context.getResources().getQuantityString(resId, quantity, transformer.getSanitizer().sanitizeArgs(formatArgs))); 110 | } 111 | 112 | /** 113 | * Mark up a string with SRML. 114 | * @param context Current context 115 | * @param str String to mark up. 116 | * @return Marked-up CharSequence. 117 | */ 118 | public static CharSequence markup(Context context, String str) { 119 | return sTransformer.transform(context, str); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/SRMLImageLoader.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | 6 | /** 7 | * Defines a class capable of loading a {@link Bitmap} from a url. Can be registered with SRML 8 | * via {@link SRML#setImageLoader(SRMLImageLoader)}. 9 | * 10 | * @author jason 11 | */ 12 | public interface SRMLImageLoader { 13 | /** 14 | * Load the image at the given url and return it as a Bitmap. 15 | * @param context The current context. 16 | * @param url The image's url. 17 | * @return A bitmap of the image. 18 | */ 19 | Bitmap loadImage(Context context, String url); 20 | 21 | /** 22 | * Load the image at the given url and return it as a Bitmap in the provided size.. 23 | * @param context The current context. 24 | * @param url The image's url. 25 | * @param width Requested width of the image. 26 | * @param height Requested height of the image. 27 | * @return A bitmap. 28 | */ 29 | Bitmap loadImage(Context context, String url, int width, int height); 30 | } 31 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/SRMLTextView.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml; 2 | 3 | import android.content.Context; 4 | import android.os.Build; 5 | import android.support.annotation.RequiresApi; 6 | import android.util.AttributeSet; 7 | import android.widget.TextView; 8 | 9 | /** 10 | * A simple extension to {@link TextView} which will automatically call out to SRML.getString when 11 | * {@link #setText(CharSequence)} is called 12 | * 13 | * @author jason 14 | */ 15 | 16 | public class SRMLTextView extends TextView { 17 | public SRMLTextView(Context context) { 18 | super(context); 19 | } 20 | 21 | public SRMLTextView(Context context, AttributeSet attrs) { 22 | super(context, attrs); 23 | } 24 | 25 | public SRMLTextView(Context context, AttributeSet attrs, int defStyleAttr) { 26 | super(context, attrs, defStyleAttr); 27 | } 28 | 29 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 30 | public SRMLTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 31 | super(context, attrs, defStyleAttr, defStyleRes); 32 | } 33 | 34 | @Override 35 | public void setText(CharSequence text, BufferType type) { 36 | super.setText(SRML.markup(getContext(), text.toString()), BufferType.SPANNABLE); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/Sanitizer.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml; 2 | 3 | import android.content.Context; 4 | 5 | /** 6 | * Defines a sanitizer for SRML. Responsible for sanitizing and de-sanitizing formatArgs passed to 7 | * {@link SRML#getString(Context, int, Object[])} and friends. 8 | * 9 | * @author jason 10 | */ 11 | public interface Sanitizer { 12 | /** 13 | * Sanitize the string arguments in formatArgs by replacing instances of our tag-start borders 14 | * with equal-length replacements, so they don't get picked up by the transformer. 15 | * 16 | * Args which are an instance of {@link co.jasonwyatt.srml.utils.SafeString} should not be 17 | * sanitized. 18 | */ 19 | Object[] sanitizeArgs(Object[] formatArgs); 20 | 21 | /** 22 | * Desanitize a chunk of text. 23 | */ 24 | String desantitize(String s); 25 | } 26 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/Transformer.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml; 2 | 3 | import android.content.Context; 4 | 5 | import co.jasonwyatt.srml.tags.TagFactory; 6 | 7 | /** 8 | * Defines an object which is capable of transforming a given SRML-marked-up String into a 9 | * styled/spanned CharSequence according to the tags used in the string. 10 | * @author jason 11 | */ 12 | public interface Transformer { 13 | CharSequence transform(Context context, String srmlString); 14 | 15 | Sanitizer getSanitizer(); 16 | 17 | /** 18 | * Get the {@link TagFactory} for the transformer. 19 | */ 20 | TagFactory getTagFactory(); 21 | } 22 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/tags/BadParameterException.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml.tags; 2 | 3 | /** 4 | * @author jason 5 | * 6 | * This exception can be thrown by a {@link ParameterizedTag} when a parameter is invalid for some reason. 7 | */ 8 | public class BadParameterException extends RuntimeException { 9 | public BadParameterException(String message) { 10 | super(message); 11 | } 12 | 13 | public BadParameterException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/tags/BadTagException.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml.tags; 2 | 3 | /** 4 | * @author jason 5 | * 6 | * Thrown when we could not find a tag class for the given tag text. 7 | */ 8 | public class BadTagException extends RuntimeException { 9 | public BadTagException(String message) { 10 | super(message); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/tags/Bold.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml.tags; 2 | 3 | import android.content.Context; 4 | import android.graphics.Typeface; 5 | import android.text.Spannable; 6 | import android.text.Spanned; 7 | import android.text.style.StyleSpan; 8 | 9 | /** 10 | * @author jason 11 | * 12 | * Tag for bolding text. 13 | */ 14 | class Bold extends Tag { 15 | static final String NAME = "b"; 16 | 17 | Bold(String tagStr, int taggedTextStart) { 18 | super(tagStr, taggedTextStart); 19 | } 20 | 21 | @Override 22 | public boolean matchesClosingTag(String tagName) { 23 | return NAME.equalsIgnoreCase(tagName); 24 | } 25 | 26 | @Override 27 | public void operate(Context context, Spannable builder, int taggedTextEnd) { 28 | builder.setSpan(new StyleSpan(Typeface.BOLD), getTaggedTextStart(), taggedTextEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/tags/Code.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml.tags; 2 | 3 | import android.content.Context; 4 | import android.text.Spannable; 5 | import android.text.Spanned; 6 | import android.text.style.TypefaceSpan; 7 | 8 | /** 9 | * @author jason 10 | * 11 | * Shows monospace text like source code. 12 | */ 13 | class Code extends Tag { 14 | static final String NAME = "code"; 15 | private static final String MONOSPACE = "monospace"; 16 | 17 | public Code(String tagStr, int taggedTextStart) { 18 | super(tagStr, taggedTextStart); 19 | } 20 | 21 | @Override 22 | public boolean matchesClosingTag(String tagName) { 23 | return NAME.equalsIgnoreCase(tagName); 24 | } 25 | 26 | @Override 27 | public void operate(Context context, Spannable builder, int taggedTextEnd) { 28 | builder.setSpan(new TypefaceSpan(MONOSPACE), getTaggedTextStart(), taggedTextEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/tags/Color.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml.tags; 2 | 3 | import android.content.Context; 4 | import android.text.Spannable; 5 | import android.text.Spanned; 6 | import android.text.style.BackgroundColorSpan; 7 | import android.text.style.ForegroundColorSpan; 8 | 9 | import co.jasonwyatt.srml.utils.Utils; 10 | 11 | /** 12 | * @author jason 13 | * 14 | * Allows you to color a chunk of text. 15 | * 16 | * Usage: {{color fg=#FF00FF}}my text{{/color}} -> Magenta "my text" 17 | */ 18 | 19 | class Color extends ParameterizedTag { 20 | static final String NAME = "color"; 21 | private static final String PARAM_COLOR_FG = "fg"; 22 | private static final String PARAM_COLOR_BG = "bg"; 23 | 24 | public Color(String tagStr, int taggedTextStart) { 25 | super(tagStr, taggedTextStart); 26 | } 27 | 28 | @Override 29 | public boolean matchesClosingTag(String tagName) { 30 | return NAME.equalsIgnoreCase(tagName); 31 | } 32 | 33 | @Override 34 | public void operate(Context context, Spannable builder, int taggedTextEnd) { 35 | String colorValue = getParam(PARAM_COLOR_FG); 36 | if (colorValue != null) { 37 | builder.setSpan(new ForegroundColorSpan(Utils.getColorInt(context, colorValue)), getTaggedTextStart(), taggedTextEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 38 | } 39 | 40 | colorValue = getParam(PARAM_COLOR_BG); 41 | if (colorValue != null) { 42 | builder.setSpan(new BackgroundColorSpan(Utils.getColorInt(context, colorValue)), getTaggedTextStart(), taggedTextEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/tags/CouldNotCreateTagException.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml.tags; 2 | 3 | /** 4 | * @author jason 5 | * Thrown by the {@link TagFactory} when a tag could not be instantiated. 6 | */ 7 | public class CouldNotCreateTagException extends RuntimeException { 8 | CouldNotCreateTagException(Exception e, String tagName) { 9 | super("Error instantiating Tag with name: "+tagName, e); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/tags/CouldNotRegisterTagException.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml.tags; 2 | 3 | /** 4 | * @author jason 5 | * 6 | * Thrown when we could not register a user-supplied Tag class. 7 | */ 8 | public class CouldNotRegisterTagException extends RuntimeException { 9 | CouldNotRegisterTagException(Throwable e, Class tagClass) { 10 | super("Could not register "+tagClass.getName(), e); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/tags/DrawableTag.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml.tags; 2 | 3 | import android.content.Context; 4 | import android.graphics.drawable.BitmapDrawable; 5 | import android.graphics.drawable.Drawable; 6 | import android.os.Build; 7 | import android.text.Spannable; 8 | import android.text.Spanned; 9 | import android.text.style.DynamicDrawableSpan; 10 | import android.text.style.ImageSpan; 11 | 12 | import co.jasonwyatt.srml.utils.Utils; 13 | 14 | /** 15 | * The DrawableTag allows you to embed images in string resources. 16 | * 17 | * {{drawable res=R.drawable.app_icon /}} 18 | * 19 | * This is a self-closable tag that can be empty, so note that you need the " /" before the closing 20 | * "}}" 21 | * 22 | * @author jason 23 | */ 24 | class DrawableTag extends ParameterizedTag { 25 | static final String NAME = "drawable"; 26 | private static final String PARAM_NAME_RES = "res"; 27 | private static final String PARAM_NAME_URL = "url"; 28 | private static final String PARAM_NAME_WIDTH = "width"; 29 | private static final String PARAM_NAME_HEIGHT = "height"; 30 | private static final String PARAM_NAME_ALIGNMENT = "align"; 31 | private static final String PARAM_VALUE_ALIGN_BASELINE = "baseline"; 32 | private static final String PARAM_VALUE_ALIGN_BOTTOM = "bottom"; 33 | 34 | /** 35 | * {@inheritDoc} 36 | * 37 | * @param tagStr 38 | * @param taggedTextStart 39 | */ 40 | public DrawableTag(String tagStr, int taggedTextStart) { 41 | super(tagStr, taggedTextStart); 42 | } 43 | 44 | @Override 45 | public boolean canBeEmpty() { 46 | return true; 47 | } 48 | 49 | @Override 50 | public int getRequiredSpacesWhenEmpty() { 51 | return 1; 52 | } 53 | 54 | @Override 55 | public boolean matchesClosingTag(String tagName) { 56 | return NAME.equalsIgnoreCase(tagName); 57 | } 58 | 59 | @Override 60 | public void operate(Context context, Spannable spannable, int taggedTextEnd) { 61 | spannable.setSpan(new ImageSpan(loadDrawable(context), getVerticalAlignment()), getTaggedTextStart(), taggedTextEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 62 | } 63 | 64 | private int getVerticalAlignment() { 65 | String alignmentRaw = getParam(PARAM_NAME_ALIGNMENT); 66 | if (alignmentRaw == null) { 67 | return DynamicDrawableSpan.ALIGN_BASELINE; 68 | } 69 | switch (alignmentRaw) { 70 | case PARAM_VALUE_ALIGN_BASELINE: 71 | return DynamicDrawableSpan.ALIGN_BASELINE; 72 | case PARAM_VALUE_ALIGN_BOTTOM: 73 | return DynamicDrawableSpan.ALIGN_BOTTOM; 74 | default: 75 | throw new BadParameterException("Invalid alignment param for tag: "+getTagStr()); 76 | } 77 | } 78 | 79 | private Drawable loadDrawable(Context context) { 80 | Drawable result = null; 81 | String resId = getParam(PARAM_NAME_RES); 82 | String url = getParam(PARAM_NAME_URL); 83 | int widthPixels = Utils.getPixelSize(context, getParam(PARAM_NAME_WIDTH)); 84 | int heightPixels = Utils.getPixelSize(context, getParam(PARAM_NAME_HEIGHT)); 85 | 86 | if (resId != null) { 87 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 88 | result = context.getResources().getDrawable(Utils.getIdentifier(context, resId), context.getTheme()); 89 | } else { 90 | //noinspection deprecation 91 | result = context.getResources().getDrawable(Utils.getIdentifier(context, resId)); 92 | } 93 | } else if (url != null) { 94 | result = new BitmapDrawable(context.getResources(), Utils.loadImage(context, url, widthPixels, heightPixels)); 95 | } 96 | 97 | if (result == null) { 98 | throw new BadParameterException("Could not load a drawable for tag: "+getTagStr()); 99 | } 100 | if (widthPixels > 0 && heightPixels > 0) { 101 | result.setBounds(0, 0, widthPixels, heightPixels); 102 | } else if (widthPixels > 0) { 103 | result.setBounds(0, 0, widthPixels, result.getIntrinsicHeight()); 104 | } else if (heightPixels > 0) { 105 | result.setBounds(0, 0, result.getIntrinsicWidth(), heightPixels); 106 | } else { 107 | result.setBounds(0, 0, result.getIntrinsicWidth(), result.getIntrinsicHeight()); 108 | } 109 | return result; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/tags/FontTag.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml.tags; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.Nullable; 5 | import android.text.Spannable; 6 | import android.text.Spanned; 7 | import android.text.TextUtils; 8 | import android.text.style.TextAppearanceSpan; 9 | 10 | import java.util.regex.Pattern; 11 | 12 | import co.jasonwyatt.srml.utils.FontSpan; 13 | import co.jasonwyatt.srml.utils.Utils; 14 | 15 | /** 16 | * A powerful tag to configure several attributes of the font. 17 | * @author jason 18 | */ 19 | class FontTag extends ParameterizedTag { 20 | static final String NAME = "font"; 21 | private static final String PARAM_COLOR_FG = "fg"; 22 | private static final String PARAM_COLOR_BG = "bg"; 23 | private static final String PARAM_TYPEFACE = "typeface"; 24 | private static final String PARAM_STYLE = "style"; 25 | private static final String PARAM_LETTER_SPACING = "letter_spacing"; 26 | private static final String PARAM_LINE_HEIGHT = "line_height"; 27 | private static final String PARAM_TEXT_APPEARANCE = "text_appearance"; 28 | private static final String PARAM_TEXT_SIZE = "size"; 29 | private static final Pattern STYLE_DIVIDER = Pattern.compile("\\|"); 30 | private static final String STYLE_UNDERLINE = "underline"; 31 | private static final String STYLE_BOLD = "bold"; 32 | private static final String STYLE_ITALIC = "italic"; 33 | private static final String STYLE_STRIKETHROUGH = "strike"; 34 | private static final String STYLE_SUPERSCRIPT = "super"; 35 | private static final String STYLE_SUBSCRIPT = "sub"; 36 | private int mStart; 37 | private int mEnd; 38 | 39 | FontTag(String tagStr, int taggedTextStart) { 40 | super(tagStr, taggedTextStart); 41 | } 42 | 43 | @Override 44 | public boolean matchesClosingTag(String tagName) { 45 | return NAME.equalsIgnoreCase(tagName); 46 | } 47 | 48 | @Override 49 | public void operate(Context context, Spannable spannable, int taggedTextEnd) { 50 | mStart = getTaggedTextStart(); 51 | mEnd = taggedTextEnd; 52 | 53 | String textAppearanceValue = getParam(PARAM_TEXT_APPEARANCE); 54 | if (!TextUtils.isEmpty(textAppearanceValue)) { 55 | applySpan(spannable, new TextAppearanceSpan(context, Utils.getIdentifier(context, textAppearanceValue))); 56 | } 57 | 58 | FontSpan.Builder span = new FontSpan.Builder(); 59 | 60 | String letterSpacingValue = getParam(PARAM_LETTER_SPACING); 61 | float letterSpacing; 62 | if (!TextUtils.isEmpty(letterSpacingValue)) { 63 | try { 64 | letterSpacing = Float.parseFloat(letterSpacingValue); 65 | } catch (NumberFormatException nfe) { 66 | throw new BadParameterException("Invalid letter_spacing value:"+letterSpacingValue, nfe); 67 | } 68 | if (letterSpacing < 0) { 69 | throw new BadParameterException("letter_spacing cannot be less than 0: "+getTagStr()); 70 | } 71 | span.spacing(letterSpacing); 72 | } 73 | 74 | String lineHeightValue = getParam(PARAM_LINE_HEIGHT); 75 | if (!TextUtils.isEmpty(lineHeightValue)) { 76 | span.lineHeight(Utils.getPixelSize(context, lineHeightValue)); 77 | } 78 | 79 | span.flags(getStyles(getParam(PARAM_STYLE))); 80 | 81 | 82 | String fgValue = getParam(PARAM_COLOR_FG); 83 | if (!TextUtils.isEmpty(fgValue)) { 84 | span.foregroundColor(Utils.getColorInt(context, fgValue)); 85 | } 86 | 87 | String bgValue = getParam(PARAM_COLOR_BG); 88 | if (!TextUtils.isEmpty(bgValue)) { 89 | span.backgroundColor(Utils.getColorInt(context, bgValue)); 90 | } 91 | 92 | String typefaceValue = getParam(PARAM_TYPEFACE); 93 | if (!TextUtils.isEmpty(typefaceValue)) { 94 | span.typeface(typefaceValue); 95 | } 96 | 97 | String sizeValue = getParam(PARAM_TEXT_SIZE); 98 | if (!TextUtils.isEmpty(sizeValue)) { 99 | span.textSize(Utils.getPixelSize(context, sizeValue)); 100 | } 101 | 102 | applySpan(spannable, span.build()); 103 | 104 | } 105 | 106 | private int getStyles(@Nullable String styleString) { 107 | int styleSpanValue = 0; 108 | if (TextUtils.isEmpty(styleString)) { 109 | return styleSpanValue; 110 | } 111 | 112 | String[] styles = STYLE_DIVIDER.split(styleString); 113 | boolean seenSuperSub = false; 114 | for (String style : styles) { 115 | switch (style) { 116 | case STYLE_BOLD: 117 | styleSpanValue |= FontSpan.FLAG_BOLD; 118 | break; 119 | case STYLE_ITALIC: 120 | styleSpanValue |= FontSpan.FLAG_ITALIC; 121 | break; 122 | case STYLE_UNDERLINE: 123 | styleSpanValue |= FontSpan.FLAG_UNDERLINE; 124 | break; 125 | case STYLE_STRIKETHROUGH: 126 | styleSpanValue |= FontSpan.FLAG_STRIKETHROUGH; 127 | break; 128 | case STYLE_SUPERSCRIPT: 129 | if (seenSuperSub) { 130 | throw new BadParameterException("\"super\" style requested, but \"sub\" or \"super\" was already in the {{font}} style: "+styleString); 131 | } 132 | styleSpanValue |= FontSpan.FLAG_SUPERSCRIPT; 133 | seenSuperSub = true; 134 | break; 135 | case STYLE_SUBSCRIPT: 136 | if (seenSuperSub) { 137 | throw new BadParameterException("\"sub\" style requested, but \"sub\" or \"super\" was already in the {{font}} style: "+styleString); 138 | } 139 | styleSpanValue |= FontSpan.FLAG_SUBSCRIPT; 140 | seenSuperSub = true; 141 | break; 142 | default: 143 | throw new BadParameterException("Invalid value for {{font}} style: "+style+" in "+styleString); 144 | } 145 | } 146 | 147 | return styleSpanValue; 148 | } 149 | 150 | private void applySpan(Spannable s, Object o) { 151 | s.setSpan(o, mStart, mEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/tags/IntentTag.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml.tags; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.support.annotation.NonNull; 7 | import android.support.annotation.Nullable; 8 | import android.text.Spannable; 9 | import android.text.Spanned; 10 | import android.view.View; 11 | 12 | import java.util.List; 13 | import java.util.regex.Matcher; 14 | import java.util.regex.Pattern; 15 | 16 | import co.jasonwyatt.srml.utils.Utils; 17 | 18 | /** 19 | * @author jason 20 | * 21 | * This tag can be used to launch an activity or a service. 22 | * 23 | * {{intent class=com.yourcompany.ActivityName action=android.intent.action.VIEW for_service=false x_extraKey=extraValue }} 24 | * 25 | * class 26 | * REQUIRED. 27 | * The fully-qualified name of the activity or service you intend to start. 28 | * for_service 29 | * OPTIONAL. 30 | * Denotes whether or not to call context.startService(intent) instead of 31 | * context.startActivity(intent) 32 | * action 33 | * OPTIONAL. 34 | * what you'd specify in intent.setAction(..) 35 | * x_[extraKey] 36 | * OPTIONAL. 37 | * Intent extras with [extraKey] names. They allow you to supply additional information about 38 | * how to parse and send the parameter values to the target. 39 | */ 40 | public class IntentTag extends ParameterizedTag { 41 | static final String NAME = "intent"; 42 | private static final Pattern EXTRAS_KEY_PATTERN = Pattern.compile("x_[^=]+"); 43 | private static final String TYPE_INT = "int"; 44 | private static final String TYPE_LONG = "long"; 45 | private static final String TYPE_CHAR = "char"; 46 | private static final String TYPE_FLOAT = "float"; 47 | private static final String TYPE_DOUBLE = "double"; 48 | private static final String TYPE_BYTE = "byte"; 49 | private static final String TYPE_SHORT = "short"; 50 | private static final String[] TYPES = { 51 | TYPE_INT, TYPE_LONG, TYPE_CHAR, TYPE_FLOAT, TYPE_DOUBLE, TYPE_SHORT, TYPE_BYTE, 52 | }; 53 | private static final Pattern EXTRAS_VALUE_CAST_PATTERN = Pattern.compile("((" + Utils.join("|", TYPES) + ")\\((-?[0-9]+(\\.[0-9]+)?|[^ ])\\))|(true|false)"); 54 | private static final String PARAM_CLASS = "class"; 55 | private static final String PARAM_FOR_SERVICE = "for_service"; 56 | private static final String PARAM_ACTION = "action"; 57 | String mTargetClass; 58 | String mAction; 59 | Bundle mExtras; 60 | boolean mIsForService; 61 | 62 | IntentTag(String tagStr, int taggedTextStart) { 63 | super(tagStr, taggedTextStart); 64 | parseParams(); 65 | } 66 | 67 | private void parseParams() { 68 | mTargetClass = getParam(PARAM_CLASS); 69 | if (mTargetClass == null || mTargetClass.length() == 0) { 70 | throw new BadParameterException("No class parameter specified at " + getTaggedTextStart()); 71 | } 72 | 73 | String forService = getParam(PARAM_FOR_SERVICE); 74 | mIsForService = forService != null && "true".equalsIgnoreCase(forService); 75 | 76 | mAction = getParam(PARAM_ACTION); 77 | List> extraParams = getParamsMatching(EXTRAS_KEY_PATTERN); 78 | if (!extraParams.isEmpty()) { 79 | mExtras = new Bundle(extraParams.size()); 80 | populateExtras(mExtras, extraParams); 81 | } else { 82 | mExtras = null; 83 | } 84 | } 85 | 86 | @Override 87 | public boolean matchesClosingTag(String tagName) { 88 | return NAME.equalsIgnoreCase(tagName); 89 | } 90 | 91 | @Override 92 | public void operate(Context context, Spannable builder, int taggedTextEnd) { 93 | IntentSpan span = new IntentSpan(mTargetClass, mAction, mExtras, mIsForService); 94 | span.useParams(context, this); 95 | builder.setSpan(span, getTaggedTextStart(), taggedTextEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 96 | } 97 | 98 | static void populateExtras(Bundle target, List> params) { 99 | for (Utils.Pair param : params) { 100 | // clip off the x_ to get the extra name 101 | String name = param.first.substring(2); 102 | parseAndSetExtra(target, name, param.second); 103 | } 104 | } 105 | 106 | static void parseAndSetExtra(Bundle target, String name, String value) { 107 | Matcher m = EXTRAS_VALUE_CAST_PATTERN.matcher(value); 108 | if (m.matches()) { 109 | String type = m.group(2); 110 | String rawValue = m.group(3); 111 | String booleanValue = m.group(5); 112 | if (type == null && booleanValue != null) { 113 | target.putBoolean(name, "true".equalsIgnoreCase(booleanValue)); 114 | return; 115 | } 116 | try { 117 | switch (type != null ? type : "other") { 118 | case TYPE_BYTE: 119 | target.putByte(name, Byte.parseByte(rawValue)); 120 | break; 121 | case TYPE_CHAR: 122 | try { 123 | target.putChar(name, (char) Integer.parseInt(rawValue)); 124 | } catch (NumberFormatException nfe) { 125 | target.putChar(name, rawValue.charAt(0)); 126 | } 127 | break; 128 | case TYPE_DOUBLE: 129 | target.putDouble(name, Double.parseDouble(rawValue)); 130 | break; 131 | case TYPE_FLOAT: 132 | target.putFloat(name, Float.parseFloat(rawValue)); 133 | break; 134 | case TYPE_INT: 135 | target.putInt(name, Integer.parseInt(rawValue)); 136 | break; 137 | case TYPE_LONG: 138 | target.putLong(name, Long.parseLong(rawValue)); 139 | break; 140 | case TYPE_SHORT: 141 | target.putShort(name, Short.parseShort(rawValue)); 142 | break; 143 | default: 144 | target.putString(name, value); 145 | } 146 | } catch (NumberFormatException | ClassCastException e) { 147 | throw new BadParameterException("Bad value for param \""+name+"\": "+value, e); 148 | } 149 | } else { 150 | target.putString(name, value); 151 | } 152 | } 153 | 154 | private static class IntentSpan extends StyledClickableSpan { 155 | private final boolean mIsForService; 156 | private final String mAction; 157 | private final String mTargetClass; 158 | private final Bundle mExtras; 159 | 160 | IntentSpan(@NonNull String targetClass, @Nullable String action, @Nullable Bundle extras, boolean isForService) { 161 | mTargetClass = targetClass; 162 | mAction = action; 163 | mExtras = extras; 164 | mIsForService = isForService; 165 | } 166 | 167 | @Override 168 | public void onClick(View widget) { 169 | Intent intent = new Intent(); 170 | intent.setClassName(widget.getContext(), mTargetClass); 171 | if (mAction != null) { 172 | intent.setAction(mAction); 173 | } 174 | if (mExtras != null) { 175 | intent.putExtras(mExtras); 176 | } 177 | 178 | if (mIsForService) { 179 | widget.getContext().startService(intent); 180 | } else { 181 | widget.getContext().startActivity(intent); 182 | } 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/tags/Italic.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml.tags; 2 | 3 | import android.content.Context; 4 | import android.graphics.Typeface; 5 | import android.text.Spannable; 6 | import android.text.Spanned; 7 | import android.text.style.StyleSpan; 8 | 9 | /** 10 | * @author jason 11 | * 12 | * For italicizing text. 13 | */ 14 | class Italic extends Tag { 15 | static final String NAME = "i"; 16 | 17 | Italic(String tagStr, int taggedTextStart) { 18 | super(tagStr, taggedTextStart); 19 | } 20 | 21 | @Override 22 | public boolean matchesClosingTag(String tagName) { 23 | return NAME.equalsIgnoreCase(tagName); 24 | } 25 | 26 | @Override 27 | public void operate(Context context, Spannable builder, int taggedTextEnd) { 28 | builder.setSpan(new StyleSpan(Typeface.ITALIC), getTaggedTextStart(), taggedTextEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/tags/Link.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml.tags; 2 | 3 | import android.content.ActivityNotFoundException; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.net.Uri; 7 | import android.provider.Browser; 8 | import android.text.Spannable; 9 | import android.text.Spanned; 10 | import android.text.style.URLSpan; 11 | import android.util.Log; 12 | import android.view.View; 13 | 14 | /** 15 | * @author jason 16 | * 17 | * The {{link url=http://.....}} tag creates a clickable link Spannable in the text. 18 | */ 19 | class Link extends ParameterizedTag { 20 | static final String NAME = "link"; 21 | static final String URL_PARAM_NAME = "url"; 22 | 23 | Link(String tagStr, int taggedTextStart) { 24 | super(tagStr, taggedTextStart); 25 | } 26 | 27 | @Override 28 | public boolean matchesClosingTag(String tagName) { 29 | return NAME.equalsIgnoreCase(tagName); 30 | } 31 | 32 | @Override 33 | public void operate(Context context, Spannable builder, int taggedTextEnd) { 34 | String url = getParam(URL_PARAM_NAME); 35 | LinkSpan span = new LinkSpan(url); 36 | span.useParams(context, this); 37 | builder.setSpan(span, getTaggedTextStart(), taggedTextEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 38 | } 39 | 40 | private static class LinkSpan extends StyledClickableSpan { 41 | private final String mUrl; 42 | 43 | private LinkSpan(String url) { 44 | mUrl = url; 45 | } 46 | 47 | @Override 48 | public void onClick(View widget) { 49 | Uri uri = Uri.parse(mUrl); 50 | Context context = widget.getContext(); 51 | Intent intent = new Intent(Intent.ACTION_VIEW, uri); 52 | intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()); 53 | try { 54 | context.startActivity(intent); 55 | } catch (ActivityNotFoundException e) { 56 | Log.w("LinkSpan", "Actvity was not found for intent, " + intent.toString()); 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/tags/ParameterMissingException.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml.tags; 2 | 3 | /** 4 | * @author jason 5 | * 6 | * This exception can be thrown by a {@link ParameterizedTag} when a required parameter is missing. 7 | */ 8 | public class ParameterMissingException extends RuntimeException { 9 | public ParameterMissingException(String message, Throwable cause) { 10 | super(message, cause); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/tags/ParameterizedTag.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml.tags; 2 | 3 | import android.os.Build; 4 | import android.util.ArrayMap; 5 | 6 | import java.util.HashMap; 7 | import java.util.LinkedList; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.regex.Matcher; 11 | import java.util.regex.Pattern; 12 | 13 | import co.jasonwyatt.srml.utils.Utils; 14 | 15 | /** 16 | * A ParameterizedTag is one which can contain an arbitrarily long list of parameters, defined as 17 | * name=value pairs after the tag name in the opening tag.

18 | * 19 | * Parameters can be of the following formats:
20 | *
    21 | *
  • param_name=param_value if you do not need spaces, or
  • 22 | *
  • param_name=`param value` if your param does need spaces
  • 23 | *
24 | * 25 | * @author jason 26 | */ 27 | public abstract class ParameterizedTag extends Tag { 28 | private static final Pattern PARAMS_PATTERN = Pattern.compile("\\s+(([a-zA-Z0-9_\\.]+)=(`((?:[^`]|\\s)*)`|([^\\s}]+)))"); 29 | private final Map mParams; 30 | 31 | /** 32 | * {@inheritDoc} 33 | */ 34 | public ParameterizedTag(String tagStr, int taggedTextStart) { 35 | super(tagStr, taggedTextStart); 36 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 37 | mParams = new ArrayMap<>(3); 38 | } else { 39 | mParams = new HashMap<>(3); 40 | } 41 | 42 | parseParams(tagStr, mParams); 43 | } 44 | 45 | /** 46 | * Get a particular parameter from the params list parsed from the original tag string. 47 | * @param name Name of the parameter. 48 | */ 49 | protected String getParam(String name) { 50 | return mParams.get(name); 51 | } 52 | 53 | /** 54 | * Get all parameters where the keys match the provided pattern. 55 | * @param pattern A {@link Pattern} to match against the param names. 56 | * @return List of {@link Utils.Pair} pairs, with the keys & values for the 57 | * parameters which match {@param pattern} 58 | */ 59 | protected List> getParamsMatching(Pattern pattern) { 60 | return getParamsMatching(pattern, mParams); 61 | } 62 | 63 | static void parseParams(String tagStr, Map out) { 64 | Matcher m = PARAMS_PATTERN.matcher(tagStr); 65 | while (m.find()) { 66 | String paramName = m.group(2); 67 | String paramValue = m.group(4); 68 | if (paramValue == null) { 69 | paramValue = m.group(5); 70 | } 71 | out.put(paramName, paramValue); 72 | } 73 | } 74 | 75 | static List> getParamsMatching(Pattern pattern, Map from) { 76 | List> result = new LinkedList<>(); 77 | 78 | for (Map.Entry entry : from.entrySet()) { 79 | String key = entry.getKey(); 80 | Matcher m = pattern.matcher(key); 81 | if (m.matches()) { 82 | result.add(new Utils.Pair<>(key, entry.getValue())); 83 | } 84 | } 85 | 86 | return result; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/tags/Strikethrough.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml.tags; 2 | 3 | import android.content.Context; 4 | import android.text.Spannable; 5 | import android.text.Spanned; 6 | import android.text.style.StrikethroughSpan; 7 | 8 | /** 9 | * @author jason 10 | * 11 | * For strikethroughs around text. 12 | */ 13 | class Strikethrough extends Tag { 14 | static final String NAME = "strike"; 15 | 16 | public Strikethrough(String tagStr, int taggedTextStart) { 17 | super(tagStr, taggedTextStart); 18 | } 19 | 20 | @Override 21 | public boolean matchesClosingTag(String tagName) { 22 | return NAME.equalsIgnoreCase(tagName); 23 | } 24 | 25 | @Override 26 | public void operate(Context context, Spannable builder, int taggedTextEnd) { 27 | builder.setSpan(new StrikethroughSpan(), getTaggedTextStart(), taggedTextEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/tags/StyledClickableSpan.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml.tags; 2 | 3 | import android.content.Context; 4 | import android.text.TextPaint; 5 | import android.text.style.ClickableSpan; 6 | 7 | import co.jasonwyatt.srml.utils.Utils; 8 | 9 | /** 10 | * Extension of {@link ClickableSpan} which allows the user to customize its appearance, rather than 11 | * simply always using linkColor and underlining. 12 | * 13 | * @author jason 14 | */ 15 | public abstract class StyledClickableSpan extends ClickableSpan { 16 | private static final String PARAM_COLOR = "color"; 17 | private static final String PARAM_UNDERLINE = "underline"; 18 | 19 | private boolean mUseDefaultColor = true; 20 | private int mColor = 0; 21 | private boolean mUnderlined = true; 22 | 23 | /** 24 | * Parse the COLOR and UNDERLINE params from the given parameterized tag and use them to 25 | * determine how to render. 26 | * @param context Current context (for color lookup) 27 | * @param tag The tag using this StyledClickableSpan 28 | */ 29 | protected void useParams(Context context, ParameterizedTag tag) { 30 | String colorValue = tag.getParam(PARAM_COLOR); 31 | String underline = tag.getParam(PARAM_UNDERLINE); 32 | 33 | if (colorValue != null && underline != null) { 34 | setColorAndUnderlined(Utils.getColorInt(context, colorValue), !"false".equalsIgnoreCase(underline)); 35 | } else if (colorValue != null) { 36 | setColorOnly(Utils.getColorInt(context, colorValue)); 37 | } else if (underline != null) { 38 | setUnderlinedOnly(!"false".equalsIgnoreCase(underline)); 39 | } 40 | } 41 | 42 | private void setDefaultStyle() { 43 | mUseDefaultColor = true; 44 | mColor = 0; 45 | mUnderlined = true; 46 | } 47 | 48 | private void setColorOnly(int color) { 49 | mUseDefaultColor = false; 50 | mColor = color; 51 | mUnderlined = true; 52 | } 53 | 54 | private void setUnderlinedOnly(boolean underlined) { 55 | mUseDefaultColor = true; 56 | mColor = 0; 57 | mUnderlined = underlined; 58 | } 59 | 60 | private void setColorAndUnderlined(int color, boolean underlined) { 61 | mUseDefaultColor = false; 62 | mColor = color; 63 | mUnderlined = underlined; 64 | } 65 | 66 | @Override 67 | public void updateDrawState(TextPaint ds) { 68 | ds.setColor(mUseDefaultColor ? ds.linkColor : mColor); 69 | ds.setUnderlineText(mUnderlined); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/tags/Subscript.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml.tags; 2 | 3 | import android.content.Context; 4 | import android.text.Spannable; 5 | import android.text.Spanned; 6 | import android.text.style.SubscriptSpan; 7 | 8 | /** 9 | * Wrapped-text becomes subscript. 10 | * @author jason 11 | */ 12 | 13 | class Subscript extends Tag { 14 | static final String NAME = "sub"; 15 | 16 | public Subscript(String tagStr, int taggedTextStart) { 17 | super(tagStr, taggedTextStart); 18 | } 19 | 20 | @Override 21 | public boolean matchesClosingTag(String tagName) { 22 | return NAME.equalsIgnoreCase(tagName); 23 | } 24 | 25 | @Override 26 | public void operate(Context context, Spannable spannable, int taggedTextEnd) { 27 | spannable.setSpan(new SubscriptSpan(), getTaggedTextStart(), taggedTextEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/tags/Superscript.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml.tags; 2 | 3 | import android.content.Context; 4 | import android.text.Spannable; 5 | import android.text.Spanned; 6 | import android.text.style.SuperscriptSpan; 7 | 8 | /** 9 | * Wrapped-text becomes superscriptized.. 10 | * @author jason 11 | */ 12 | 13 | class Superscript extends Tag { 14 | static final String NAME = "sup"; 15 | 16 | public Superscript(String tagStr, int taggedTextStart) { 17 | super(tagStr, taggedTextStart); 18 | } 19 | 20 | @Override 21 | public boolean matchesClosingTag(String tagName) { 22 | return NAME.equalsIgnoreCase(tagName); 23 | } 24 | 25 | @Override 26 | public void operate(Context context, Spannable spannable, int taggedTextEnd) { 27 | spannable.setSpan(new SuperscriptSpan(), getTaggedTextStart(), taggedTextEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/tags/Tag.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml.tags; 2 | 3 | import android.content.Context; 4 | import android.text.Spannable; 5 | 6 | import co.jasonwyatt.srml.Transformer; 7 | 8 | /** 9 | * Tags are the meat and potatoes of SRML. Everything you can use to mark up your string resources 10 | * extends from this class. If you wish to create your own tags you can extend this directly. If 11 | * you prefer to make a custom tag which accepts parameters, extend {@link ParameterizedTag} instead 12 | * and it'll do the heavy lifting for you regarding parsing. 13 | * 14 | * @author jason 15 | */ 16 | public abstract class Tag { 17 | private final String mTagStr; 18 | private final int mTaggedTextStart; 19 | 20 | /** 21 | * Create a new instance of {@link Tag} 22 | * @param tagStr The tag's contents (name and parameters). 23 | */ 24 | public Tag(String tagStr, int taggedTextStart) { 25 | mTagStr = tagStr; 26 | mTaggedTextStart = taggedTextStart; 27 | } 28 | 29 | /** 30 | * Get the whole contents of the opening tag which instigated creation of this {@link Tag} 31 | * instance. 32 | * @return The whole tag string. 33 | */ 34 | public String getTagStr() { 35 | return mTagStr; 36 | } 37 | 38 | /** 39 | * Get the starting character index (of the parsed string) of this tag. 40 | * @return Starting index. 41 | */ 42 | public int getTaggedTextStart() { 43 | return mTaggedTextStart; 44 | } 45 | 46 | /** 47 | * Whether or not this tag is allowed to be an empty, self-closing tag.

48 | * NOTE: If you return true, the {@link Transformer} will call 49 | * {@link #getRequiredSpacesWhenEmpty()} before it calls {@link #operate(Context, Spannable, int)} 50 | * 51 | * @return Whether the tag can be empty. 52 | */ 53 | public boolean canBeEmpty() { 54 | return false; 55 | } 56 | 57 | /** 58 | * When true is returned from {@link #canBeEmpty()}, this method will be called to 59 | * allow you to specify a number of non-breaking spaces required by the Tag. When 60 | * {@link #operate(Context, Spannable, int)} is called, it will be passed the value from 61 | * {@link #getTaggedTextStart()} plus the return value from this method. 62 | * 63 | * @return The number of required non-breaking spaces for the tag. 64 | */ 65 | public int getRequiredSpacesWhenEmpty() { 66 | return 0; 67 | } 68 | 69 | /** 70 | * Return whether or not the closing tag seen matches this tag.

71 | * 72 | * NOTE: Typically you only need to check {@param tagName} against the name you use for the tag. 73 | * @param tagName The name of the closing tag. 74 | * @return Whether or not that name matches this tag, if so, SRML will call 75 | * {@link #operate(Context, Spannable, int)}, passing the ending index where the closing tag was 76 | * found. 77 | */ 78 | public abstract boolean matchesClosingTag(String tagName); 79 | 80 | /** 81 | * Operate on the provided {@link Spannable}.

82 | * 83 | * You'll typically want to call {@link Spannable#setSpan(Object, int, int, int)} on it, 84 | * using {@link #getTaggedTextStart()} and {@param taggedTextEnd} as the start and end 85 | * parameters. 86 | * 87 | * @param context The context for the resource. 88 | * @param spannable The {@link Spannable} on which to operate. 89 | * @param taggedTextEnd The index in the final (parsed) string where the closing tag was seen. 90 | */ 91 | public abstract void operate(Context context, Spannable spannable, int taggedTextEnd); 92 | } 93 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/tags/TagFactory.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml.tags; 2 | 3 | import java.lang.reflect.Constructor; 4 | import java.lang.reflect.InvocationTargetException; 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | /** 9 | * @author jason 10 | * 11 | * Creates {@link Tag} instances according to the supplied tag name. 12 | */ 13 | public final class TagFactory { 14 | private Map> mConstructorMap = new HashMap<>(); 15 | public TagFactory() { 16 | // nothing... 17 | } 18 | 19 | public Tag getTag(String tagName, String tagText, int startPosition) { 20 | if (Bold.NAME.equalsIgnoreCase(tagName)) { 21 | return new Bold(tagText, startPosition); 22 | } 23 | if (Italic.NAME.equalsIgnoreCase(tagName)) { 24 | return new Italic(tagText, startPosition); 25 | } 26 | if (Underline.NAME.equalsIgnoreCase(tagName)) { 27 | return new Underline(tagText, startPosition); 28 | } 29 | if (Strikethrough.NAME.equalsIgnoreCase(tagName)) { 30 | return new Strikethrough(tagText, startPosition); 31 | } 32 | if (Color.NAME.equalsIgnoreCase(tagName)) { 33 | return new Color(tagText, startPosition); 34 | } 35 | if (Link.NAME.equalsIgnoreCase(tagName)) { 36 | return new Link(tagText, startPosition); 37 | } 38 | if (Code.NAME.equalsIgnoreCase(tagName)) { 39 | return new Code(tagText, startPosition); 40 | } 41 | if (IntentTag.NAME.equalsIgnoreCase(tagName)) { 42 | return new IntentTag(tagText, startPosition); 43 | } 44 | if (DrawableTag.NAME.equalsIgnoreCase(tagName)) { 45 | return new DrawableTag(tagText, startPosition); 46 | } 47 | if (Superscript.NAME.equalsIgnoreCase(tagName)) { 48 | return new Superscript(tagText, startPosition); 49 | } 50 | if (Subscript.NAME.equalsIgnoreCase(tagName)) { 51 | return new Subscript(tagText, startPosition); 52 | } 53 | if (FontTag.NAME.equalsIgnoreCase(tagName)) { 54 | return new FontTag(tagText, startPosition); 55 | } 56 | if (mConstructorMap.containsKey(tagName.toLowerCase())) { 57 | try { 58 | return mConstructorMap.get(tagName.toLowerCase()).newInstance(tagText, startPosition); 59 | } catch (InstantiationException e) { 60 | throw new CouldNotCreateTagException(e, tagName); 61 | } catch (IllegalAccessException e) { 62 | throw new CouldNotCreateTagException(e, tagName); 63 | } catch (InvocationTargetException e) { 64 | throw new CouldNotCreateTagException(e, tagName); 65 | } 66 | } 67 | throw new BadTagException("Could not create Tag for {{" + tagText + "}} at position:" + startPosition); 68 | } 69 | 70 | public void registerTag(String tagName, Class tagClass) { 71 | try { 72 | mConstructorMap.put(tagName.toLowerCase(), tagClass.getConstructor(String.class, int.class)); 73 | } catch (NoSuchMethodException e) { 74 | throw new CouldNotRegisterTagException(e, tagClass); 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/tags/Underline.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml.tags; 2 | 3 | import android.content.Context; 4 | import android.text.Spannable; 5 | import android.text.Spanned; 6 | import android.text.style.UnderlineSpan; 7 | 8 | /** 9 | * @author jason 10 | * 11 | * Tag for underlining text. 12 | */ 13 | class Underline extends Tag { 14 | static final String NAME = "u"; 15 | 16 | Underline(String tagStr, int taggedTextStart) { 17 | super(tagStr, taggedTextStart); 18 | } 19 | 20 | @Override 21 | public boolean matchesClosingTag(String tagName) { 22 | return NAME.equalsIgnoreCase(tagName); 23 | } 24 | 25 | @Override 26 | public void operate(Context context, Spannable builder, int taggedTextEnd) { 27 | builder.setSpan(new UnderlineSpan(), getTaggedTextStart(), taggedTextEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/utils/FontSpan.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml.utils; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.graphics.Typeface; 5 | import android.os.Build; 6 | import android.support.annotation.ColorInt; 7 | import android.text.TextPaint; 8 | import android.text.TextUtils; 9 | import android.text.style.MetricAffectingSpan; 10 | 11 | 12 | /** 13 | * This class is a powerful Span implementation which allows for configuring several aspects of the 14 | * text to display. It should be instantiated using a {@link Builder}. 15 | * 16 | * @author jason 17 | */ 18 | @SuppressLint("ParcelCreator") 19 | public class FontSpan extends MetricAffectingSpan { 20 | public static final int FLAG_BOLD = 1; 21 | public static final int FLAG_ITALIC = 2; 22 | public static final int FLAG_UNDERLINE = 4; 23 | public static final int FLAG_STRIKETHROUGH = 8; 24 | public static final int FLAG_SUPERSCRIPT = 16; 25 | public static final int FLAG_SUBSCRIPT = 32; 26 | private final int mFlags; 27 | private final float mSpacing; 28 | private final int mLineHeight; 29 | private final int mForegroundColor; 30 | private final boolean mHasForegroundColor; 31 | private final int mBackgroundColor; 32 | private final boolean mHasBackgroundColor; 33 | private final int mTextSize; 34 | private final String mTypeface; 35 | 36 | private FontSpan(Builder builder) { 37 | mSpacing = builder.mSpacing; 38 | mLineHeight = builder.mLineHeight; 39 | mFlags = builder.mFlags; 40 | mForegroundColor = builder.mForegroundColor; 41 | mHasForegroundColor = builder.mHasForegroundColor; 42 | mBackgroundColor = builder.mBackgroundColor; 43 | mHasBackgroundColor = builder.mHasBackgroundColor; 44 | mTextSize = builder.mTextSize; 45 | mTypeface = builder.mTypeface; 46 | } 47 | 48 | @Override 49 | public void updateMeasureState(TextPaint p) { 50 | apply(this, p); 51 | } 52 | 53 | @Override 54 | public void updateDrawState(TextPaint ds) { 55 | apply(this, ds); 56 | } 57 | 58 | private static void apply(FontSpan span, TextPaint ds) { 59 | if (!TextUtils.isEmpty(span.mTypeface)) { 60 | int oldStyle; 61 | Typeface old = ds.getTypeface(); 62 | if (old == null) { 63 | oldStyle = 0; 64 | } else { 65 | oldStyle = old.getStyle(); 66 | } 67 | 68 | Typeface tf = Typeface.create(span.mTypeface, oldStyle); 69 | int fake = oldStyle & ~tf.getStyle(); 70 | 71 | if ((fake & Typeface.BOLD) != 0) { 72 | ds.setFakeBoldText(true); 73 | } 74 | 75 | if ((fake & Typeface.ITALIC) != 0) { 76 | ds.setTextSkewX(-0.25f); 77 | } 78 | 79 | ds.setTypeface(tf); 80 | } 81 | 82 | if (span.mTextSize > 0) { 83 | ds.setTextSize(span.mTextSize); 84 | } 85 | 86 | if ((span.mFlags & FLAG_UNDERLINE) != 0) { 87 | ds.setUnderlineText(true); 88 | } 89 | if ((span.mFlags & FLAG_STRIKETHROUGH) != 0) { 90 | ds.setStrikeThruText(true); 91 | } 92 | if ((span.mFlags & FLAG_BOLD) != 0) { 93 | ds.setFakeBoldText(true); 94 | } 95 | if ((span.mFlags & FLAG_ITALIC) != 0) { 96 | ds.setTextSkewX(-0.25f); 97 | } 98 | if ((span.mFlags & FLAG_SUBSCRIPT) != 0) { 99 | ds.baselineShift -= (int) (ds.ascent() / 2); 100 | } else if ((span.mFlags & FLAG_SUPERSCRIPT) != 0) { 101 | ds.baselineShift += (int) (ds.ascent() / 2); 102 | } 103 | 104 | if (span.mSpacing >= 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 105 | ds.setLetterSpacing(span.mSpacing); 106 | } 107 | if (span.mLineHeight >= 0) { 108 | ds.baselineShift += (ds.getTextSize() - span.mLineHeight); 109 | } 110 | 111 | if (span.mHasForegroundColor) { 112 | ds.setColor(span.mForegroundColor); 113 | } 114 | if (span.mHasBackgroundColor) { 115 | ds.bgColor = span.mBackgroundColor; 116 | } 117 | } 118 | 119 | /** 120 | * Builder for {@link FontSpan}. Allows you to configure several attributes pertaining to the 121 | * style of text that is rendered. 122 | */ 123 | public static class Builder { 124 | private int mFlags = 0; 125 | private float mSpacing = -1; 126 | private int mLineHeight = -1; 127 | private int mForegroundColor = 0; 128 | private int mBackgroundColor = 0; 129 | private boolean mHasForegroundColor = false; 130 | private boolean mHasBackgroundColor = false; 131 | private int mTextSize = -1; 132 | private String mTypeface; 133 | 134 | public Builder() { 135 | 136 | } 137 | 138 | /** 139 | * Set the character spacing for the FontSpan. Zero is the default, values are ratios of ems. 140 | *

NOTE: This is only supported on Lollipop or higher.

141 | * @param spacing Spacing. 142 | * @return The builder. 143 | */ 144 | public Builder spacing(float spacing) { 145 | mSpacing = spacing; 146 | return this; 147 | } 148 | 149 | /** 150 | * Set the line height for the span (in pixels). 151 | * @param lineHeightPx New line height. 152 | * @return The builder. 153 | */ 154 | public Builder lineHeight(int lineHeightPx) { 155 | mLineHeight = lineHeightPx; 156 | return this; 157 | } 158 | 159 | /** 160 | * Set the flags for the FontSpan. Can be a bitwise or of any of the following values: 161 | *
    162 | *
  • {@link #FLAG_BOLD}
  • 163 | *
  • {@link #FLAG_ITALIC}
  • 164 | *
  • {@link #FLAG_UNDERLINE}
  • 165 | *
  • {@link #FLAG_STRIKETHROUGH}
  • 166 | *
  • {@link #FLAG_SUPERSCRIPT}*
  • 167 | *
  • {@link #FLAG_SUBSCRIPT}*
  • 168 | *
169 | *

* - Only one of {@link #FLAG_SUBSCRIPT} or {@link #FLAG_SUPERSCRIPT} may be used.

170 | * @param flags Flags to set. 171 | * @return The builder. 172 | */ 173 | public Builder flags(int flags) { 174 | mFlags = flags; 175 | return this; 176 | } 177 | 178 | /** 179 | * Sets the text foreground color. 180 | * @param foregroundColor The foreground color for the text. 181 | * @return The builder. 182 | */ 183 | public Builder foregroundColor(@ColorInt int foregroundColor) { 184 | mForegroundColor = foregroundColor; 185 | mHasForegroundColor = true; 186 | return this; 187 | } 188 | 189 | /** 190 | * Sets the text background color. 191 | * @param backgroundColor The background color for the text. 192 | * @return The builder. 193 | */ 194 | public Builder backgroundColor(@ColorInt int backgroundColor) { 195 | mBackgroundColor = backgroundColor; 196 | mHasBackgroundColor = true; 197 | return this; 198 | } 199 | 200 | /** 201 | * Sets the text size. 202 | * @param sizePixels The text size (in pixels) 203 | * @return The builder. 204 | */ 205 | public Builder textSize(int sizePixels) { 206 | mTextSize = sizePixels; 207 | return this; 208 | } 209 | 210 | /** 211 | * Sets the typeface string. 212 | * @param typeface New typeface identifier string. 213 | * @return The builder. 214 | */ 215 | public Builder typeface(String typeface) { 216 | mTypeface = typeface; 217 | return this; 218 | } 219 | 220 | /** 221 | * Construct a FontSpan from the builder. 222 | * @return A new {@link FontSpan} 223 | */ 224 | public FontSpan build() { 225 | return new FontSpan(this); 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/utils/SafeString.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml.utils; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import java.io.Serializable; 6 | 7 | /** 8 | * Simple wrapper of {@link String} which the {@link co.jasonwyatt.srml.Sanitizer} should look for, 9 | * and skip the escape process. 10 | * 11 | * @author jason 12 | */ 13 | 14 | public class SafeString implements CharSequence, Serializable { 15 | private final String mString; 16 | 17 | public SafeString(@NonNull String s) { 18 | mString = s; 19 | } 20 | 21 | public boolean isEmpty() { 22 | return mString.isEmpty(); 23 | } 24 | 25 | /** 26 | * Returns the length of this SafeString. The length is the number 27 | * of 16-bit chars in the sequence.

28 | * 29 | * @return the number of chars in this sequence 30 | */ 31 | @Override 32 | public int length() { 33 | return mString.length(); 34 | } 35 | 36 | /** 37 | * Returns the char value at the specified index. An index ranges from zero 38 | * to length() - 1. The first char value of the sequence is at 39 | * index zero, the next at index one, and so on, as for array 40 | * indexing.

41 | * 42 | *

If the char value specified by the index is a 43 | * surrogate, the surrogate 44 | * value is returned. 45 | * 46 | * @param index the index of the char value to be returned 47 | * 48 | * @return the specified char value 49 | * 50 | * @throws IndexOutOfBoundsException 51 | * if the index argument is negative or not less than 52 | * length() 53 | */ 54 | @Override 55 | public char charAt(int index) { 56 | return mString.charAt(index); 57 | } 58 | 59 | /** 60 | * Returns a new CharSequence that is a subsequence of this sequence. 61 | * The subsequence starts with the char value at the specified index and 62 | * ends with the char value at index end - 1. The length 63 | * (in chars) of the 64 | * returned sequence is end - start, so if start == end 65 | * then an empty sequence is returned.

66 | * 67 | * @param start the start index, inclusive 68 | * @param end the end index, exclusive 69 | * 70 | * @return the specified subsequence 71 | * 72 | * @throws IndexOutOfBoundsException 73 | * if start or end are negative, 74 | * if end is greater than length(), 75 | * or if start is greater than end 76 | */ 77 | @Override 78 | public CharSequence subSequence(int start, int end) { 79 | return mString.subSequence(start, end); 80 | } 81 | 82 | /** 83 | * Returns a string containing the characters in this sequence in the same 84 | * order as this sequence. The length of the string will be the length of 85 | * this sequence.

86 | * 87 | * @return a string consisting of exactly this sequence of characters 88 | */ 89 | @Override 90 | public String toString() { 91 | return mString; 92 | } 93 | 94 | /** 95 | * Whether or not the provided object matches this SafeString. 96 | * @param obj Other object. 97 | * @return Whether or not the other object is a SafeString and its inner string value matches 98 | * ours. 99 | */ 100 | @Override 101 | public boolean equals(Object obj) { 102 | if (obj == null || !(obj instanceof SafeString)) { 103 | return false; 104 | } 105 | SafeString other = (SafeString) obj; 106 | return other.mString.equals(mString); 107 | } 108 | 109 | /** 110 | * Get the hashCode for this SafeString. 111 | * @return The hashcode for the inner {@link String} value. 112 | */ 113 | @Override 114 | public int hashCode() { 115 | return mString.hashCode(); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /library/src/main/java/co/jasonwyatt/srml/utils/Utils.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml.utils; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.os.Build; 6 | import android.util.DisplayMetrics; 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | import java.util.regex.Matcher; 11 | import java.util.regex.Pattern; 12 | 13 | import co.jasonwyatt.srml.SRMLImageLoader; 14 | import co.jasonwyatt.srml.tags.BadParameterException; 15 | 16 | /** 17 | * @author jason 18 | * 19 | * General utilities. 20 | */ 21 | public class Utils { 22 | private static final Pattern SIZE_PATTERN = Pattern.compile("([0-9]+(\\.[0-9]+)?)(sp|dp|px)?", Pattern.CASE_INSENSITIVE); 23 | private static final Pattern COLOR_VALUE_PATTERN = Pattern.compile("^#([0-9a-f]+)$", Pattern.CASE_INSENSITIVE); 24 | private static final Map sIdentifierCache = new HashMap<>(100); 25 | private static SRMLImageLoader sImageLoader; 26 | 27 | private Utils() { 28 | // nothing... 29 | } 30 | 31 | /** 32 | * Join an array of {@link CharSequence} objects, with a given divider. 33 | */ 34 | public static String join(CharSequence divider, CharSequence[] parts) { 35 | // guess at a decent first size for the string builder.. 36 | StringBuilder sb = new StringBuilder(parts.length*6); 37 | 38 | for (CharSequence s : parts) { 39 | if (sb.length() > 0) { 40 | sb.append(divider); 41 | } 42 | sb.append(s); 43 | } 44 | 45 | return sb.toString(); 46 | } 47 | 48 | /** 49 | * Get the integer value for an identifier string like "R.drawable.app_icon". This method will 50 | * cache identifier values so later lookups are quick. 51 | * 52 | * @param context The context in which to search for the identifier. 53 | * @param identifierStr The identifier string. 54 | * @return The actual identifier value. 55 | */ 56 | public static int getIdentifier(Context context, String identifierStr) { 57 | if (sIdentifierCache.containsKey(identifierStr)) { 58 | return sIdentifierCache.get(identifierStr); 59 | } 60 | String[] parts = identifierStr.split("\\."); 61 | int identifier = context.getResources().getIdentifier(parts[2], parts[1], context.getPackageName()); 62 | sIdentifierCache.put(identifierStr, identifier); 63 | return identifier; 64 | } 65 | 66 | /** 67 | * Set the {@link SRMLImageLoader} to use when {@link #loadImage(Context, String, int, int)} is called. 68 | * @param imageLoader A new image loader. 69 | */ 70 | public static void setImageLoader(SRMLImageLoader imageLoader) { 71 | sImageLoader = imageLoader; 72 | } 73 | 74 | /** 75 | * Load a bitmap from a url. 76 | * @param context The current context. 77 | * @param url The url of the image to load. 78 | * @param width Requested width of the image. 79 | * @param height Requested height of the image. 80 | * @return The loaded image, or null if no {@link SRMLImageLoader} is associated via 81 | * {@link #setImageLoader(SRMLImageLoader)} 82 | */ 83 | public static Bitmap loadImage(Context context, String url, int width, int height) { 84 | if (sImageLoader == null) { 85 | throw new IllegalStateException("No SRMLImageLoader specified, please pass one to SRML.setImageLoader()."); 86 | } 87 | 88 | if (width >= 0 && height >= 0) { 89 | return sImageLoader.loadImage(context, url, width, height); 90 | } 91 | return sImageLoader.loadImage(context, url); 92 | } 93 | 94 | /** 95 | * Transform the provided size string into a pixel size.
96 | * Supported formats: 97 | *
    98 | *
  • "32dp"
  • 99 | *
  • "32sp"
  • 100 | *
  • "32px"
  • 101 | *
  • "32" (interpreted as pixels)
  • 102 | *
103 | * @param context The current context. 104 | * @param size String representation of the size. 105 | * @return The resolved size. 106 | */ 107 | public static int getPixelSize(Context context, String size) { 108 | if (size == null) { 109 | return -1; 110 | } 111 | Matcher m = SIZE_PATTERN.matcher(size); 112 | if (m.find()) { 113 | float value = Float.parseFloat(m.group(1)); 114 | String unit = m.group(3); 115 | DisplayMetrics dm = context.getResources().getDisplayMetrics(); 116 | if (unit == null) { 117 | return (int) value; 118 | } else if ("dp".equalsIgnoreCase(unit)) { 119 | return (int) (value * dm.density); 120 | } else if ("sp".equalsIgnoreCase(unit)) { 121 | return (int) (value * dm.scaledDensity); 122 | } else { 123 | return (int) value; 124 | } 125 | } 126 | return 0; 127 | } 128 | 129 | /** 130 | * Parses a color value hex string into its integer representation. If it's not a hex string, it 131 | * tries to interpret {@param colorValue} as a resource identifier. 132 | * 133 | * @throws {@link NumberFormatException} if {@param colorValue} was not a hex string. 134 | * @throws {@link BadParameterException} if the color value could not be parsed. 135 | * @return Color int value. 136 | */ 137 | public static int getColorInt(String colorValue) { 138 | Matcher m = COLOR_VALUE_PATTERN.matcher(colorValue); 139 | if (!m.find()) { 140 | throw new NumberFormatException(); 141 | } 142 | 143 | colorValue = m.group(1); 144 | int colorLength = colorValue.length(); 145 | 146 | if (colorLength == 3) { 147 | int raw = Integer.parseInt(colorValue, 16); 148 | // 0xFFFFFF 149 | int first = (raw & 0xF00) >> 8; 150 | int second = (raw & 0x0F0) >> 4; 151 | int third = raw & 0x00F; 152 | return (0xFF << 24) | (first << 20) | (first << 16) | (second << 12) | (second << 8) | (third << 4) | third; 153 | } else if (colorLength == 6) { 154 | return (0xFF << 24) | Integer.parseInt(colorValue, 16); 155 | } else if (colorLength == 8) { 156 | return (int) Long.parseLong(colorValue, 16); 157 | } 158 | throw new BadParameterException("could not parse color value: "+colorValue); 159 | } 160 | 161 | /** 162 | * Attempts to parse the string as a hex color string, if that fails, it will attempt to look it 163 | * up as a resource. 164 | * 165 | * @see #getColorInt(String) 166 | * 167 | * @param context Current context. 168 | * @param colorValueOrResource Color string or resource identifier. 169 | * @throws {@link BadParameterException} if the color value could not be parsed. 170 | * @return Color int value. 171 | */ 172 | public static int getColorInt(Context context, String colorValueOrResource) { 173 | try { 174 | return getColorInt(colorValueOrResource); 175 | } catch (NumberFormatException e) { 176 | // try by resource... 177 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 178 | return context.getResources().getColor(Utils.getIdentifier(context, colorValueOrResource), context.getTheme()); 179 | } else { 180 | //noinspection deprecation 181 | return context.getResources().getColor(Utils.getIdentifier(context, colorValueOrResource)); 182 | } 183 | } 184 | } 185 | 186 | public static class Pair { 187 | public final F first; 188 | public final S second; 189 | 190 | public Pair(F first, S second) { 191 | this.first = first; 192 | this.second = second; 193 | } 194 | } 195 | 196 | } 197 | -------------------------------------------------------------------------------- /library/src/test/java/co/jasonwyatt/srml/tags/IntentTagTest.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml.tags; 2 | 3 | import android.os.Bundle; 4 | 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | 8 | import static junit.framework.Assert.assertTrue; 9 | import static org.mockito.Mockito.mock; 10 | import static org.mockito.Mockito.verify; 11 | 12 | public class IntentTagTest { 13 | private Bundle mBundle; 14 | 15 | @Before 16 | public void setup() { 17 | mBundle = mock(Bundle.class); 18 | } 19 | 20 | @Test 21 | public void parseAndSetExtra_string() { 22 | IntentTag.parseAndSetExtra(mBundle, "key1", "testvalue"); 23 | verify(mBundle).putString("key1", "testvalue"); 24 | 25 | IntentTag.parseAndSetExtra(mBundle, "key2", ""); 26 | verify(mBundle).putString("key2", ""); 27 | } 28 | 29 | @Test 30 | public void parseAndSetExtra_unsupportedCast() { 31 | IntentTag.parseAndSetExtra(mBundle, "key", "coolclass(value)"); 32 | verify(mBundle).putString("key", "coolclass(value)"); 33 | } 34 | 35 | @Test 36 | public void parseAndSetExtra_boolean() { 37 | IntentTag.parseAndSetExtra(mBundle, "true", "true"); 38 | verify(mBundle).putBoolean("true", true); 39 | 40 | IntentTag.parseAndSetExtra(mBundle, "false", "false"); 41 | verify(mBundle).putBoolean("false", false); 42 | } 43 | 44 | @Test 45 | public void parseAndSetExtra_byte() { 46 | IntentTag.parseAndSetExtra(mBundle, "zero", "byte(0)"); 47 | verify(mBundle).putByte("zero", (byte) 0); 48 | 49 | IntentTag.parseAndSetExtra(mBundle, "one", "byte(1)"); 50 | verify(mBundle).putByte("one", (byte) 1); 51 | 52 | IntentTag.parseAndSetExtra(mBundle, "neg_one", "byte(-1)"); 53 | verify(mBundle).putByte("neg_one", (byte) -1); 54 | 55 | try { 56 | IntentTag.parseAndSetExtra(mBundle, "overflow", "byte(256)"); 57 | } catch (BadParameterException e) { 58 | assertTrue(e.getCause() instanceof NumberFormatException); 59 | } 60 | } 61 | 62 | @Test 63 | public void parseAndSetExtra_short() { 64 | IntentTag.parseAndSetExtra(mBundle, "zero", "short(0)"); 65 | verify(mBundle).putShort("zero", (short) 0); 66 | 67 | IntentTag.parseAndSetExtra(mBundle, "one", "short(1)"); 68 | verify(mBundle).putShort("one", (short) 1); 69 | 70 | IntentTag.parseAndSetExtra(mBundle, "neg_one", "short(-1)"); 71 | verify(mBundle).putShort("neg_one", (short) -1); 72 | 73 | try { 74 | IntentTag.parseAndSetExtra(mBundle, "overflow", "short(65536)"); 75 | } catch (BadParameterException e) { 76 | assertTrue(e.getCause() instanceof NumberFormatException); 77 | } 78 | } 79 | 80 | @Test 81 | public void parseAndSetExtra_int() { 82 | IntentTag.parseAndSetExtra(mBundle, "zero", "int(0)"); 83 | verify(mBundle).putInt("zero", 0); 84 | 85 | IntentTag.parseAndSetExtra(mBundle, "one", "int(1)"); 86 | verify(mBundle).putInt("one", 1); 87 | 88 | IntentTag.parseAndSetExtra(mBundle, "neg_one", "int(-1)"); 89 | verify(mBundle).putInt("neg_one", -1); 90 | 91 | try { 92 | IntentTag.parseAndSetExtra(mBundle, "overflow", "int(5000000000)"); 93 | } catch (BadParameterException e) { 94 | assertTrue(e.getCause() instanceof NumberFormatException); 95 | } 96 | } 97 | 98 | @Test 99 | public void parseAndSetExtra_long() { 100 | IntentTag.parseAndSetExtra(mBundle, "zero", "long(0)"); 101 | verify(mBundle).putLong("zero", 0); 102 | 103 | IntentTag.parseAndSetExtra(mBundle, "one", "long(1)"); 104 | verify(mBundle).putLong("one", 1); 105 | 106 | IntentTag.parseAndSetExtra(mBundle, "neg_one", "long(-1)"); 107 | verify(mBundle).putLong("neg_one", -1); 108 | 109 | try { 110 | IntentTag.parseAndSetExtra(mBundle, "overflow", "long(10000000000000000000)"); 111 | } catch (BadParameterException e) { 112 | assertTrue(e.getCause() instanceof NumberFormatException); 113 | } 114 | } 115 | 116 | @Test 117 | public void parseAndSetExtra_float() { 118 | IntentTag.parseAndSetExtra(mBundle, "zero", "float(0)"); 119 | verify(mBundle).putFloat("zero", 0); 120 | 121 | IntentTag.parseAndSetExtra(mBundle, "zero.zero", "float(0.0)"); 122 | verify(mBundle).putFloat("zero.zero", 0.0f); 123 | 124 | IntentTag.parseAndSetExtra(mBundle, "zero.five", "float(0.5)"); 125 | verify(mBundle).putFloat("zero.five", 0.5f); 126 | 127 | IntentTag.parseAndSetExtra(mBundle, "one", "float(1)"); 128 | verify(mBundle).putFloat("one", 1); 129 | 130 | IntentTag.parseAndSetExtra(mBundle, "neg_one", "float(-1)"); 131 | verify(mBundle).putFloat("neg_one", -1); 132 | } 133 | 134 | @Test 135 | public void parseAndSetExtra_double() { 136 | IntentTag.parseAndSetExtra(mBundle, "zero", "double(0)"); 137 | verify(mBundle).putDouble("zero", 0); 138 | 139 | IntentTag.parseAndSetExtra(mBundle, "zero.zero", "double(0.0)"); 140 | verify(mBundle).putDouble("zero.zero", 0.0); 141 | 142 | IntentTag.parseAndSetExtra(mBundle, "zero.five", "double(0.5)"); 143 | verify(mBundle).putDouble("zero.five", 0.5); 144 | 145 | IntentTag.parseAndSetExtra(mBundle, "one", "double(1)"); 146 | verify(mBundle).putDouble("one", 1); 147 | 148 | IntentTag.parseAndSetExtra(mBundle, "neg_one", "double(-1)"); 149 | verify(mBundle).putDouble("neg_one", -1); 150 | } 151 | 152 | @Test 153 | public void parseAndSetExtra_char() { 154 | IntentTag.parseAndSetExtra(mBundle, "a", "char(a)"); 155 | verify(mBundle).putChar("a", 'a'); 156 | 157 | IntentTag.parseAndSetExtra(mBundle, "sixtyfour", "char(64)"); 158 | verify(mBundle).putChar("sixtyfour", (char) 64); 159 | 160 | try { 161 | IntentTag.parseAndSetExtra(mBundle, "overflow", "char(256)"); 162 | } catch (BadParameterException e) { 163 | assertTrue(e.getCause() instanceof ClassCastException); 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /library/src/test/java/co/jasonwyatt/srml/tags/ParameterizedTagTest.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml.tags; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.Collections; 6 | import java.util.Comparator; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.regex.Pattern; 11 | 12 | import co.jasonwyatt.srml.utils.Utils; 13 | 14 | import static junit.framework.Assert.assertEquals; 15 | 16 | public class ParameterizedTagTest { 17 | @Test 18 | public void parseParams_single() { 19 | Map out = new HashMap<>(); 20 | ParameterizedTag.parseParams("tag test=value", out); 21 | 22 | assertEquals("value", out.get("test")); 23 | } 24 | 25 | @Test 26 | public void parseParams_quoted() { 27 | Map out = new HashMap<>(); 28 | ParameterizedTag.parseParams("tag test=`value is a string`", out); 29 | 30 | assertEquals("value is a string", out.get("test")); 31 | } 32 | 33 | @Test 34 | public void parseParams_quotedMultiple() { 35 | Map out = new HashMap<>(); 36 | ParameterizedTag.parseParams("tag test=`value is a string` test2=`this is another string`", out); 37 | 38 | assertEquals("value is a string", out.get("test")); 39 | assertEquals("this is another string", out.get("test2")); 40 | } 41 | 42 | @Test 43 | public void parseParams_multiple() { 44 | Map out = new HashMap<>(); 45 | ParameterizedTag.parseParams("tag test=value test2=3243 test_3=1124", out); 46 | 47 | assertEquals("value", out.get("test")); 48 | assertEquals("3243", out.get("test2")); 49 | assertEquals("1124", out.get("test_3")); 50 | } 51 | 52 | @Test 53 | public void getParamsMatching() { 54 | Map params = new HashMap<>(); 55 | params.put("x_first", "test"); 56 | params.put("x_second", "test_2"); 57 | params.put("another", "value"); 58 | params.put("something_else", "blah"); 59 | 60 | List> matchedParams = ParameterizedTag.getParamsMatching(Pattern.compile("x_[^=]+"), params); 61 | assertEquals(2, matchedParams.size()); 62 | 63 | Collections.sort(matchedParams, new Comparator>() { 64 | @Override 65 | public int compare(Utils.Pair o1, Utils.Pair o2) { 66 | return o1.first.compareTo(o2.first); 67 | } 68 | }); 69 | 70 | assertEquals("x_first", matchedParams.get(0).first); 71 | assertEquals("x_second", matchedParams.get(1).first); 72 | assertEquals("test", matchedParams.get(0).second); 73 | assertEquals("test_2", matchedParams.get(1).second); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /library/src/test/java/co/jasonwyatt/srml/utils/SafeStringTest.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml.utils; 2 | 3 | import org.junit.Test; 4 | 5 | import static junit.framework.Assert.assertEquals; 6 | import static junit.framework.Assert.assertFalse; 7 | import static junit.framework.Assert.assertTrue; 8 | 9 | public class SafeStringTest { 10 | @Test 11 | public void equals_returns_true() throws Exception { 12 | SafeString a = new SafeString("this is a test"); 13 | SafeString b = new SafeString("this is a test"); 14 | 15 | assertTrue(a.equals(b)); 16 | assertTrue(b.equals(a)); 17 | } 18 | 19 | @Test 20 | public void equals_returns_false() throws Exception { 21 | SafeString a = new SafeString("this is a test"); 22 | SafeString b = new SafeString("this is another test"); 23 | 24 | assertFalse(a.equals(b)); 25 | assertFalse(b.equals(a)); 26 | assertFalse(a.equals(null)); 27 | assertFalse(a.equals(5)); 28 | assertFalse(a.equals("this is a test")); 29 | } 30 | 31 | @Test 32 | public void hashCode_works() throws Exception { 33 | SafeString a = new SafeString("this is a test"); 34 | SafeString b = new SafeString("this is a test"); 35 | SafeString c = new SafeString("this is a third test"); 36 | 37 | assertEquals(a.hashCode(), b.hashCode()); 38 | assertEquals(a.hashCode(), "this is a test".hashCode()); 39 | assertFalse(a.hashCode() == c.hashCode()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /library/src/test/java/co/jasonwyatt/srml/utils/UtilsTest.java: -------------------------------------------------------------------------------- 1 | package co.jasonwyatt.srml.utils; 2 | 3 | import org.junit.Test; 4 | 5 | import co.jasonwyatt.srml.tags.BadParameterException; 6 | import co.jasonwyatt.srml.utils.Utils; 7 | 8 | import static junit.framework.Assert.assertEquals; 9 | import static junit.framework.Assert.assertTrue; 10 | 11 | public class UtilsTest { 12 | @Test 13 | public void join_simple() { 14 | assertEquals("foo", Utils.join("|", new String[]{"foo"})); 15 | assertEquals("foo|bar", Utils.join("|", new String[]{"foo", "bar"})); 16 | assertEquals("foo|bar|baz", Utils.join("|", new String[]{"foo", "bar", "baz"})); 17 | } 18 | 19 | @Test 20 | public void join_edge() { 21 | assertEquals("", Utils.join("|", new String[]{})); 22 | } 23 | 24 | @Test 25 | public void getColorInt_with_black_3() throws Exception { 26 | assertEquals(0xFF000000, Utils.getColorInt("#000")); 27 | } 28 | 29 | @Test 30 | public void getColorInt_with_length_3() throws Exception { 31 | assertEquals(0xFFFFFFFF, Utils.getColorInt("#FFF")); 32 | assertEquals(0xFFFF0000, Utils.getColorInt("#F00")); 33 | assertEquals(0xFF00FF00, Utils.getColorInt("#0F0")); 34 | assertEquals(0xFF0000FF, Utils.getColorInt("#00F")); 35 | } 36 | 37 | @Test 38 | public void getColorInt_with_length_6() throws Exception { 39 | assertEquals(0xFFFFFFFF, Utils.getColorInt("#FFFFFF")); 40 | assertEquals(0xFFFF0000, Utils.getColorInt("#FF0000")); 41 | assertEquals(0xFF00FF00, Utils.getColorInt("#00FF00")); 42 | assertEquals(0xFF0000FF, Utils.getColorInt("#0000FF")); 43 | } 44 | 45 | @Test 46 | public void getColorInt_with_length_8() throws Exception { 47 | assertEquals(0xABFFFFFF, Utils.getColorInt("#ABFFFFFF")); 48 | assertEquals(0xFFFF0000, Utils.getColorInt("#FFFF0000")); 49 | assertEquals(0x00000000, Utils.getColorInt("#00000000")); 50 | assertEquals(0x000000FF, Utils.getColorInt("#000000FF")); 51 | } 52 | 53 | @Test 54 | public void getColorInt_throws_when_non_hex() throws Exception { 55 | boolean thrown = false; 56 | 57 | try { 58 | Utils.getColorInt("R.color.test"); 59 | } catch (NumberFormatException e) { 60 | thrown = true; 61 | } 62 | assertTrue(thrown); 63 | thrown = false; 64 | 65 | try { 66 | Utils.getColorInt("0f0f0f"); 67 | } catch (NumberFormatException e) { 68 | thrown = true; 69 | } 70 | assertTrue(thrown); 71 | thrown = false; 72 | 73 | try { 74 | Utils.getColorInt("blah blah"); 75 | } catch (NumberFormatException e) { 76 | thrown = true; 77 | } 78 | assertTrue(thrown); 79 | } 80 | 81 | @Test 82 | public void getColorInt_throws_when_invalid_length() throws Exception { 83 | boolean thrown = false; 84 | try { 85 | Utils.getColorInt("#00"); 86 | } catch (BadParameterException e) { 87 | thrown = true; 88 | } 89 | assertTrue(thrown); 90 | thrown = false; 91 | 92 | try { 93 | Utils.getColorInt("#0000"); 94 | } catch (BadParameterException e) { 95 | thrown = true; 96 | } 97 | assertTrue(thrown); 98 | thrown = false; 99 | 100 | try { 101 | Utils.getColorInt("#00000"); 102 | } catch (BadParameterException e) { 103 | thrown = true; 104 | } 105 | assertTrue(thrown); 106 | thrown = false; 107 | 108 | try { 109 | Utils.getColorInt("#0000000"); 110 | } catch (BadParameterException e) { 111 | thrown = true; 112 | } 113 | assertTrue(thrown); 114 | thrown = false; 115 | 116 | try { 117 | Utils.getColorInt("#000000000"); 118 | } catch (BadParameterException e) { 119 | thrown = true; 120 | } 121 | assertTrue(thrown); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':library' 2 | --------------------------------------------------------------------------------