From a4d9a9b9d7f55cb4b073f327226fcc5caf3cf006 Mon Sep 17 00:00:00 2001 From: colugo Date: Fri, 2 Aug 2019 03:00:29 +0000 Subject: [PATCH] Add basic readability highlighting Personally, I'm a terrible writer and I've found simple aids really help keep my prose tight. These changes will highlight sentences that are hard to read, based on the number of syllables they contain. Here's what happens based on syllable count: - less than 25 syllables: its easy to read (heuristically speaking), and has no background colour - between 25 and 35 syllables, it's a bit hard to understand, and has a yellow background colour - over 35 syllables, its quite hard to read, and has a red background color This might be well outside the scope of what you had in mind, but I personally find it usefull. At the moment it's on by default, in a seperate observer. Maybe you could add add a setting for it --- .gitignore | 4 - .gitlab-ci.yml | 38 +++++++++ README.md | 6 +- app/.gitignore | 2 - app/build.gradle | 44 ++++++++++- app/google-services.json | 78 +++++++++++++++++++ .../simplemarkdown/model/Readability.java | 35 +++++++++ .../simplemarkdown/model/Sentence.java | 48 ++++++++++++ .../utility/ReadabilityObserver.java | 59 ++++++++++++++ .../view/fragment/EditFragment.kt | 14 +++- app/src/main/res/values/strings.xml | 4 + app/src/main/res/xml/pref_general.xml | 6 ++ .../simplemarkdown/ReadabilityTest.java | 52 +++++++++++++ .../wbrawner/simplemarkdown/UtilsTest.java | 42 ---------- 14 files changed, 378 insertions(+), 54 deletions(-) create mode 100644 .gitlab-ci.yml create mode 100644 app/google-services.json create mode 100644 app/src/main/java/com/wbrawner/simplemarkdown/model/Readability.java create mode 100644 app/src/main/java/com/wbrawner/simplemarkdown/model/Sentence.java create mode 100644 app/src/main/java/com/wbrawner/simplemarkdown/utility/ReadabilityObserver.java create mode 100644 app/src/test/java/com/wbrawner/simplemarkdown/ReadabilityTest.java diff --git a/.gitignore b/.gitignore index e2f5000..c838db8 100644 --- a/.gitignore +++ b/.gitignore @@ -5,13 +5,9 @@ /.idea/libraries .DS_Store /build -IAP5Helper/build /captures .externalNativeBuild .idea/ -app/standard -app/samsung *~ *.log -app/acra.properties keystore.properties diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..3c11563 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,38 @@ +image: openjdk:8-jdk + +variables: + ANDROID_COMPILE_SDK: "28" + ANDROID_BUILD_TOOLS: "28.0.3" + ANDROID_SDK_TOOLS: "4333796" + +before_script: + - export GRADLE_USER_HOME=cache/.gradle + - apt-get --quiet update --yes + - apt-get --quiet install --yes wget tar unzip lib32stdc++6 lib32z1 + - wget --quiet --output-document=android-sdk.zip https://dl.google.com/android/repository/sdk-tools-linux-${ANDROID_SDK_TOOLS}.zip + - unzip -d android-sdk-linux android-sdk.zip + - echo y | android-sdk-linux/tools/bin/sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" >/dev/null + - echo y | android-sdk-linux/tools/bin/sdkmanager "platform-tools" >/dev/null + - echo y | android-sdk-linux/tools/bin/sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}" >/dev/null + - export ANDROID_HOME=$PWD/android-sdk-linux + - export PATH=$PATH:$PWD/android-sdk-linux/platform-tools/ + - chmod +x ./gradlew + # temporarily disable checking for EPIPE error and use yes to accept all licenses + - set +o pipefail + - yes | android-sdk-linux/tools/bin/sdkmanager --licenses + - set -o pipefail + +stages: + - test + +test: + stage: test + script: + - ./gradlew --stacktrace --console=plain :app:lintDebug jacocoTestReport + - cat app/build/reports/jacoco/jacocoTestReport/html/index.html + artifacts: + reports: + junit: app/build/test-results/testDebugUnitTest/TEST-*.xml + paths: + - app/build/reports/jacoco/jacocoTestReport/ + - app/build/outputs/ diff --git a/README.md b/README.md index 28221cc..36b0f59 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # [Simple Markdown](https://wbrawner.com/portfolio/simple-markdown/) -[![Build Status](https://ci.wbrawner.com/job/Simple%20Markdown/badge/icon)](https://ci.wbrawner.com/job/Simple%20Markdown/) +[![pipeline status](https://gitlab.com/billybrawner/SimpleMarkdown/badges/master/pipeline.svg)](https://gitlab.com/billybrawner/SimpleMarkdown/commits/master) +[![coverage report](https://gitlab.com/billybrawner/SimpleMarkdown/badges/master/coverage.svg)](https://gitlab.com/billybrawner/SimpleMarkdown/commits/master) Simple Markdown is simply a Markdown editor :) I wrote it to offer up an open source alternative to the other Markdown editors available on the Play Store. I also wanted to get some practice in @@ -27,8 +28,7 @@ Using Android Studio is the preferred way to build the project. To build from th ### Crashlytics SimpleMarkdown makes use of Firebase Crashlytics for error reporting. You'll need to follow the -[Get started with Firebase Crashlytics](https://firebase.google -.com/docs/crashlytics/get-started?platform=android) guide in order to build the project. +[Get started with Firebase Crashlytics](https://firebase.google.com/docs/crashlytics/get-started?platform=android) guide in order to build the project. ## Contributing diff --git a/app/.gitignore b/app/.gitignore index 284a226..cfabd20 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1,4 +1,2 @@ /build -crashlytics.properties *.apk -google-services.json \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 5600a9e..4f46a15 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,10 +3,20 @@ apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' apply plugin: 'io.fabric' +apply plugin: 'jacoco' -def keystorePropertiesFile = rootProject.file("keystore.properties") def keystoreProperties = new Properties() -keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +try { + def keystorePropertiesFile = rootProject.file("keystore.properties") + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} catch (FileNotFoundException e) { + logger.warn("Unable to load keystore properties. Automatic signing won't be available") + keystoreProperties['keyAlias'] = "" + keystoreProperties['keyPassword'] = "" + keystoreProperties['storeFile'] = File.createTempFile("temp", ".tmp").absolutePath + keystoreProperties['storePassword'] = "" +} + android { configurations.all { @@ -44,6 +54,9 @@ android { } } buildTypes { + debug { + testCoverageEnabled true + } release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' @@ -96,9 +109,36 @@ dependencies { implementation "androidx.core:core-ktx:1.0.2" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' + implementation 'eu.crydee:syllable-counter:4.0.2' } apply plugin: 'com.google.gms.google-services' repositories { mavenCentral() } + +jacoco { + toolVersion = '0.8.0' +} + +tasks.withType(Test) { + jacoco.includeNoLocationClasses = true +} + +task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) { + reports { + xml.enabled = true + html.enabled = true + } + + def fileFilter = [ '**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*' ] + def javaDebugTree = fileTree(dir: "$project.buildDir/intermediates/javac/debug/compileDebugJavaWithJavac/classes", excludes: fileFilter) + def kotlinDebugTree = fileTree(dir: "$project.buildDir/tmp/kotlin-classes/debug", excludes: fileFilter) + def mainSrc = "$project.projectDir/src/main/java" + + sourceDirectories = files([mainSrc]) + classDirectories = files([javaDebugTree, kotlinDebugTree]) + executionData = fileTree(dir: project.buildDir, includes: [ + 'jacoco/testDebugUnitTest.exec', 'outputs/code-coverage/connected/*coverage.ec' + ]) +} diff --git a/app/google-services.json b/app/google-services.json new file mode 100644 index 0000000..169bd73 --- /dev/null +++ b/app/google-services.json @@ -0,0 +1,78 @@ +{ + "project_info": { + "project_number": "318641233555", + "firebase_url": "https://simplemarkdown.firebaseio.com", + "project_id": "simplemarkdown", + "storage_bucket": "simplemarkdown.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:318641233555:android:09d865cad87e3b5f", + "android_client_info": { + "package_name": "com.wbrawner.simplemarkdown" + } + }, + "oauth_client": [ + { + "client_id": "318641233555-83n2k1mqhokf0b7lhccqiva9pspgripq.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.wbrawner.simplemarkdown", + "certificate_hash": "710e37c230689bc259fc6e2e032e2f56956f8d33" + } + }, + { + "client_id": "318641233555-a5rm2cqqf66jc9j5nmg3ttepdt2iaeno.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyBDMcXg-10NsXLDKJRtj5WnXoHrwg3m9Os" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "318641233555-a5rm2cqqf66jc9j5nmg3ttepdt2iaeno.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + }, + "admob_app_id": "ca-app-pub-3319579963502409~4576405307" + }, + { + "client_info": { + "mobilesdk_app_id": "1:318641233555:android:5dfb62206717437e", + "android_client_info": { + "package_name": "com.wbrawner.simplemarkdown.samsung" + } + }, + "oauth_client": [ + { + "client_id": "318641233555-a5rm2cqqf66jc9j5nmg3ttepdt2iaeno.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyBDMcXg-10NsXLDKJRtj5WnXoHrwg3m9Os" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "318641233555-a5rm2cqqf66jc9j5nmg3ttepdt2iaeno.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/model/Readability.java b/app/src/main/java/com/wbrawner/simplemarkdown/model/Readability.java new file mode 100644 index 0000000..53d6ca3 --- /dev/null +++ b/app/src/main/java/com/wbrawner/simplemarkdown/model/Readability.java @@ -0,0 +1,35 @@ +package com.wbrawner.simplemarkdown.model; + +import java.util.ArrayList; +import java.util.List; + +public class Readability { + private String content; + private static final String DELIMS = ".!?\n"; + + public Readability(String content) { + this.content = content; + } + + public List sentences() { + + ArrayList list = new ArrayList<>(); + + int startOfSentance = 0; + StringBuilder lineBuilder = new StringBuilder(); + for (int i = 0; i < content.length(); i++) { + String c = content.charAt(i) + ""; + if (DELIMS.contains(c)) { + list.add(new Sentence(content, startOfSentance, i)); + startOfSentance = i + 1; + lineBuilder = new StringBuilder(); + } else { + lineBuilder.append(c); + } + } + String line = lineBuilder.toString(); + if (!line.isEmpty()) list.add(new Sentence(content, startOfSentance, content.length())); + + return list; + } +} diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/model/Sentence.java b/app/src/main/java/com/wbrawner/simplemarkdown/model/Sentence.java new file mode 100644 index 0000000..fe5a2f6 --- /dev/null +++ b/app/src/main/java/com/wbrawner/simplemarkdown/model/Sentence.java @@ -0,0 +1,48 @@ +package com.wbrawner.simplemarkdown.model; + +import eu.crydee.syllablecounter.SyllableCounter; + +public class Sentence { + + private String sentence = ""; + private int start = 0; + private int end = 0; + + private final static SyllableCounter sc = new SyllableCounter(); + + public Sentence(String content, int start, int end){ + this.start = start; + this.end = end; + this.sentence = content.substring(start, end); + + trimStart(); + } + + public Sentence(String sentence){ + this.sentence = sentence; + } + + private void trimStart() { + while(sentence.startsWith(" ")){ + this.start++; + sentence = sentence.substring(1); + } + } + + @Override + public String toString() { + return sentence; + } + + public int start(){ + return start; + } + + public int end(){ + return end; + } + + public int syllableCount(){ + return sc.count(sentence); + } +} diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/utility/ReadabilityObserver.java b/app/src/main/java/com/wbrawner/simplemarkdown/utility/ReadabilityObserver.java new file mode 100644 index 0000000..01629be --- /dev/null +++ b/app/src/main/java/com/wbrawner/simplemarkdown/utility/ReadabilityObserver.java @@ -0,0 +1,59 @@ +package com.wbrawner.simplemarkdown.utility; + +import android.graphics.Color; +import android.text.SpannableString; +import android.text.style.BackgroundColorSpan; +import android.util.Log; +import android.widget.EditText; +import android.widget.TextView; + +import com.wbrawner.simplemarkdown.model.Readability; +import com.wbrawner.simplemarkdown.model.Sentence; + +import io.reactivex.Observer; +import io.reactivex.disposables.Disposable; + +public class ReadabilityObserver implements Observer { + private EditText text; + private String previousValue = ""; + + public ReadabilityObserver(EditText text) { + this.text = text; + } + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onNext(String markdown) { + long start = System.currentTimeMillis(); + if (markdown.length() < 1) return; + if (previousValue.equals(markdown)) return; + Readability readability = new Readability(markdown); + SpannableString span = new SpannableString(markdown); + for (Sentence sentence : readability.sentences()) { + int color = Color.TRANSPARENT; + if (sentence.syllableCount() > 25) color = Color.argb(100, 229, 232, 42); + if (sentence.syllableCount() > 35) color = Color.argb(100, 193, 66, 66); + span.setSpan(new BackgroundColorSpan(color), sentence.start(), sentence.end(), 0); + } + text.setTextKeepState(span, TextView.BufferType.SPANNABLE); + previousValue = markdown; + long timeTakenMs = System.currentTimeMillis() - start; + Log.d("SimpleMarkdown", "Handled markdown in " + timeTakenMs + "ms"); + } + + @Override + public void onError(Throwable e) { + System.err.println("An error occurred while handling the markdown"); + e.printStackTrace(); + // TODO: report this? + } + + @Override + public void onComplete() { + + } +} diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/EditFragment.kt b/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/EditFragment.kt index 3cf90b5..c583d4b 100644 --- a/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/EditFragment.kt +++ b/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/EditFragment.kt @@ -3,6 +3,7 @@ package com.wbrawner.simplemarkdown.view.fragment import android.annotation.SuppressLint import android.content.Context import android.os.Bundle +import android.preference.PreferenceManager import android.view.LayoutInflater import android.view.MotionEvent import android.view.View @@ -17,6 +18,7 @@ import com.wbrawner.simplemarkdown.MarkdownApplication import com.wbrawner.simplemarkdown.R import com.wbrawner.simplemarkdown.presentation.MarkdownPresenter import com.wbrawner.simplemarkdown.utility.MarkdownObserver +import com.wbrawner.simplemarkdown.utility.ReadabilityObserver import com.wbrawner.simplemarkdown.view.MarkdownEditView import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers @@ -48,6 +50,16 @@ class EditFragment : Fragment(), MarkdownEditView { .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) obs.subscribe(MarkdownObserver(presenter, obs)) + val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(requireContext()) + val enableReadability = sharedPrefs.getBoolean(getString(R.string.readability_enabled), false) + if (enableReadability) { + val readabilityObserver = RxTextView.textChanges(markdownEditor!!) + .debounce(250, TimeUnit.MILLISECONDS) + .map { it.toString() } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + readabilityObserver.subscribe(ReadabilityObserver(markdownEditor)) + } return view } @@ -89,7 +101,7 @@ class EditFragment : Fragment(), MarkdownEditView { } override fun setMarkdown(markdown: String) { - markdownEditor!!.setText(markdown) + markdownEditor?.setText(markdown) } override fun setTitle(title: String) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2cdca66..9b40a96 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -40,6 +40,10 @@ Enable automated error reports Error reports will not be sent Error reports will be sent + readability.enable + Enable readability highlighting (experimental) + Readability highlighting is off + Readability highlighting is on Files will automatically save Files will not be automatically saved pref.custom_css diff --git a/app/src/main/res/xml/pref_general.xml b/app/src/main/res/xml/pref_general.xml index 0aff4b7..8f392cd 100644 --- a/app/src/main/res/xml/pref_general.xml +++ b/app/src/main/res/xml/pref_general.xml @@ -23,5 +23,11 @@ android:summaryOff="@string/pref_error_reports_off" android:summaryOn="@string/pref_error_reports_on" android:title="@string/pref_title_error_reports" /> + diff --git a/app/src/test/java/com/wbrawner/simplemarkdown/ReadabilityTest.java b/app/src/test/java/com/wbrawner/simplemarkdown/ReadabilityTest.java new file mode 100644 index 0000000..3f73e04 --- /dev/null +++ b/app/src/test/java/com/wbrawner/simplemarkdown/ReadabilityTest.java @@ -0,0 +1,52 @@ +package com.wbrawner.simplemarkdown; + +import com.wbrawner.simplemarkdown.model.Readability; +import com.wbrawner.simplemarkdown.model.Sentence; +import eu.crydee.syllablecounter.SyllableCounter; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class ReadabilityTest { + + @Test + public void break_content_into_sentances() { + SyllableCounter sc = new SyllableCounter(); + assertEquals(4, sc.count("facility")); + } + + @Test + public void can_break_text_into_sentences_with_indexes(){ + String content = "Hop on pop. I am a fish. This is a test."; + Readability readability = new Readability(content); + List sentenceList = readability.sentences(); + + assertEquals(3, sentenceList.size()); + + Sentence hopOnPop = sentenceList.get(0); + assertEquals(hopOnPop.toString(), "Hop on pop"); + assertEquals(0, hopOnPop.start()); + assertEquals(10, hopOnPop.end()); + + Sentence iAmAFish = sentenceList.get(1); + assertEquals(iAmAFish.toString(), "I am a fish"); + assertEquals(12, iAmAFish.start()); + assertEquals(23, iAmAFish.end()); + + Sentence thisIsATest = sentenceList.get(2); + assertEquals(thisIsATest.toString(), "This is a test"); + assertEquals(25, thisIsATest.start()); + assertEquals(39, thisIsATest.end()); + } + + + @Test + public void get_syllable_count_for_sentence(){ + assertEquals(8, new Sentence("This is the song that never ends").syllableCount()); + assertEquals(10, new Sentence("facility facility downing").syllableCount()); + } + + +} diff --git a/app/src/test/java/com/wbrawner/simplemarkdown/UtilsTest.java b/app/src/test/java/com/wbrawner/simplemarkdown/UtilsTest.java index e3236d5..1ddce0a 100644 --- a/app/src/test/java/com/wbrawner/simplemarkdown/UtilsTest.java +++ b/app/src/test/java/com/wbrawner/simplemarkdown/UtilsTest.java @@ -15,9 +15,7 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import java.io.File; -import java.io.IOException; -import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -42,46 +40,6 @@ public class UtilsTest { rmdir(new File(rootDir)); } - @Test - public void getDocsPath() throws Exception { - sharedPreferences.edit().putString(Constants.KEY_DOCS_PATH, rootDir).apply(); - assertEquals(rootDir, Utils.getDocsPath(context)); - } - - @Test - public void getDefaultFileName() throws Exception { - sharedPreferences.edit().putString(Constants.KEY_DOCS_PATH, rootDir).apply(); - - new File(rootDir, "dummy.md").createNewFile(); - new File(rootDir, "dummy1.md").createNewFile(); - new File(rootDir, "Untitled-a.md").createNewFile(); - - String firstDefaultName = Utils.getDefaultFileName(context); - assertEquals("Untitled.md", firstDefaultName); - File firstFile = new File(rootDir, firstDefaultName); - firstFile.createNewFile(); - - String secondDefaultName = Utils.getDefaultFileName(context); - assertEquals("Untitled-1.md", secondDefaultName); - File secondFile = new File(rootDir, secondDefaultName); - secondFile.createNewFile(); - - String thirdDefaultName = Utils.getDefaultFileName(context); - assertEquals("Untitled-2.md", thirdDefaultName); - } - - @Test - public void getDefaultFileNameDoubleDigitTest() throws IOException { - sharedPreferences.edit().putString(Constants.KEY_DOCS_PATH, rootDir).apply(); - - for (int i = 0; i < 11; i++) { - new File(rootDir, "Untitled-" + i + ".md").createNewFile(); - } - assertTrue(new File(rootDir, "Untitled-10.md").exists()); - String defaultName = Utils.getDefaultFileName(context); - assertEquals("Untitled-11.md", defaultName); - } - @Test public void isAutosaveEnabled() throws Exception { assertTrue(Utils.isAutosaveEnabled(context));