├── .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 [](https://travis-ci.org/jasonwyatt/SRML) [](http://android-arsenal.com/details/1/4672) [](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 | 
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 |
--------------------------------------------------------------------------------
/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 extends Tag> 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 extends Tag> 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 extends Tag> 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 char
s in the sequence.
28 | *
29 | * @return the number of char
s 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 char
s) 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 |
--------------------------------------------------------------------------------