diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bd3867c..eaad5fec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * `core` - `MovementMethodPlugin.none()`, `MovementMethodPlugin.link()` factory methods * `core` - `CorePlugin` `hasExplicitMovementMethod` configuration method to **not** add implicit `LinkMovementMethod` in `afterSetText` * `ext-latex` - `JLatexMathTheme.Padding.of(int,int,int,int)` factory method +* `app-sample` - example application #### Changed * `html` - `SimpleTagHandler` visits children tags if supplied tag is block one ([#235]) @@ -22,6 +23,7 @@ #### Removed * `image` - `AsyncDrawable#hasKnownDimentions` (deprecated in `4.2.1`) +* `app` and `sample` applications (merged together in a `app-sample` single app) [#235]: https://github.com/noties/Markwon/issues/235 [#241]: https://github.com/noties/Markwon/issues/241 diff --git a/app-sample/build.gradle b/app-sample/build.gradle index 54909a37..a340eeaf 100644 --- a/app-sample/build.gradle +++ b/app-sample/build.gradle @@ -16,6 +16,8 @@ android { versionName version resConfig 'en' + + setProperty("archivesBaseName", "markwon-$versionName") } dexOptions { diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index aba0e620..00000000 --- a/app/build.gradle +++ /dev/null @@ -1,45 +0,0 @@ -apply plugin: 'com.android.application' - -android { - - compileSdkVersion config['compile-sdk'] - buildToolsVersion config['build-tools'] - - defaultConfig { - applicationId "io.noties.markwon" - minSdkVersion config['min-sdk'] - targetSdkVersion config['target-sdk'] - versionCode 1 - versionName version - setProperty("archivesBaseName", "markwon-$versionName") - } - - lintOptions { - abortOnError false - } -} - -dependencies { - - implementation project(':markwon-core') - implementation project(':markwon-ext-strikethrough') - implementation project(':markwon-ext-tables') - implementation project(':markwon-ext-tasklist') - implementation project(':markwon-html') - implementation project(':markwon-image') - implementation project(':markwon-syntax-highlight') - - deps.with { - implementation it['okhttp'] - implementation it['prism4j'] - implementation it['debug'] - implementation it['dagger'] - implementation it['android-svg'] - implementation it['android-gif'] - } - - deps['annotationProcessor'].with { - annotationProcessor it['prism4j-bundler'] - annotationProcessor it['dagger-compiler'] - } -} \ No newline at end of file diff --git a/app/proguard.pro b/app/proguard.pro deleted file mode 100644 index 9eee5753..00000000 --- a/app/proguard.pro +++ /dev/null @@ -1,5 +0,0 @@ --dontwarn okhttp3.** --dontwarn okio.** - --keep class com.caverock.androidsvg.** { *; } --dontwarn com.caverock.androidsvg.** \ No newline at end of file diff --git a/app/src/debug/java/io/noties/markwon/debug/ColorBlendView.java b/app/src/debug/java/io/noties/markwon/debug/ColorBlendView.java deleted file mode 100644 index baf5e92b..00000000 --- a/app/src/debug/java/io/noties/markwon/debug/ColorBlendView.java +++ /dev/null @@ -1,59 +0,0 @@ -package io.noties.markwon.debug; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Rect; -import android.util.AttributeSet; -import android.view.View; - -import androidx.annotation.Nullable; - -import io.noties.markwon.app.R; -import io.noties.markwon.utils.ColorUtils; - -public class ColorBlendView extends View { - - private final Rect rect = new Rect(); - private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); - - private int background; - private int foreground; - - public ColorBlendView(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - - if (attrs != null) { - final TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ColorBlendView); - try { - background = array.getColor(R.styleable.ColorBlendView_cbv_background, 0); - foreground = array.getColor(R.styleable.ColorBlendView_cbv_foreground, 0); - } finally { - array.recycle(); - } - } - - paint.setStyle(Paint.Style.FILL); - - setWillNotDraw(false); - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - - final int side = getWidth() / 11; - - rect.set(0, 0, side, getHeight()); - - canvas.translate(getPaddingLeft(), 0F); - - for (int i = 0; i < 11; i++) { - final float alpha = i / 10F; - paint.setColor(ColorUtils.blend(foreground, background, alpha)); - canvas.drawRect(rect, paint); - canvas.translate(side, 0F); - } - } -} diff --git a/app/src/debug/java/io/noties/markwon/debug/DebugCheckboxDrawableView.java b/app/src/debug/java/io/noties/markwon/debug/DebugCheckboxDrawableView.java deleted file mode 100644 index 7a506fc8..00000000 --- a/app/src/debug/java/io/noties/markwon/debug/DebugCheckboxDrawableView.java +++ /dev/null @@ -1,74 +0,0 @@ -package io.noties.markwon.debug; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.drawable.Drawable; -import androidx.annotation.Nullable; -import android.util.AttributeSet; -import android.view.View; - -import io.noties.markwon.app.R; -import io.noties.markwon.ext.tasklist.TaskListDrawable; - -public class DebugCheckboxDrawableView extends View { - - private Drawable drawable; - - public DebugCheckboxDrawableView(Context context) { - super(context); - init(context, null); - } - - public DebugCheckboxDrawableView(Context context, AttributeSet attrs) { - super(context, attrs); - init(context, attrs); - } - - private void init(Context context, @Nullable AttributeSet attrs) { - - int checkedColor = 0; - int normalColor = 0; - int checkMarkColor = 0; - - boolean checked = false; - - if (attrs != null) { - final TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.DebugCheckboxDrawableView); - try { - - checkedColor = array.getColor(R.styleable.DebugCheckboxDrawableView_fcdv_checkedFillColor, checkedColor); - normalColor = array.getColor(R.styleable.DebugCheckboxDrawableView_fcdv_normalOutlineColor, normalColor); - checkMarkColor = array.getColor(R.styleable.DebugCheckboxDrawableView_fcdv_checkMarkColor, checkMarkColor); - - //noinspection ConstantConditions - checked = array.getBoolean(R.styleable.DebugCheckboxDrawableView_fcdv_checked, checked); - } finally { - array.recycle(); - } - } - - final TaskListDrawable drawable = new TaskListDrawable(checkedColor, normalColor, checkMarkColor); - final int[] state; - if (checked) { - state = new int[]{android.R.attr.state_checked}; - } else { - state = new int[0]; - } - drawable.setState(state); - - this.drawable = drawable; - - setLayerType(LAYER_TYPE_SOFTWARE, null); - - setWillNotDraw(false); - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - - drawable.setBounds(0, 0, getWidth(), getHeight()); - drawable.draw(canvas); - } -} diff --git a/app/src/debug/res/layout/debug_checkbox.xml b/app/src/debug/res/layout/debug_checkbox.xml deleted file mode 100644 index 3e5d19eb..00000000 --- a/app/src/debug/res/layout/debug_checkbox.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/debug/res/layout/debug_color_blend.xml b/app/src/debug/res/layout/debug_color_blend.xml deleted file mode 100644 index cacc73fa..00000000 --- a/app/src/debug/res/layout/debug_color_blend.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/debug/res/values/attrs.xml b/app/src/debug/res/values/attrs.xml deleted file mode 100644 index 0636e107..00000000 --- a/app/src/debug/res/values/attrs.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml deleted file mode 100644 index 311a39e2..00000000 --- a/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/assets/README.md b/app/src/main/assets/README.md deleted file mode 120000 index ff5c7960..00000000 --- a/app/src/main/assets/README.md +++ /dev/null @@ -1 +0,0 @@ -../../../../README.md \ No newline at end of file diff --git a/app/src/main/java/io/noties/markwon/app/ActivityScope.java b/app/src/main/java/io/noties/markwon/app/ActivityScope.java deleted file mode 100644 index 38f20d61..00000000 --- a/app/src/main/java/io/noties/markwon/app/ActivityScope.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.noties.markwon.app; - -import javax.inject.Scope; - -@Scope -@interface ActivityScope { -} diff --git a/app/src/main/java/io/noties/markwon/app/App.java b/app/src/main/java/io/noties/markwon/app/App.java deleted file mode 100644 index 46e15e4b..00000000 --- a/app/src/main/java/io/noties/markwon/app/App.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.noties.markwon.app; - -import android.app.Application; -import android.content.Context; - -import androidx.annotation.NonNull; - -import io.noties.debug.AndroidLogDebugOutput; -import io.noties.debug.Debug; - -public class App extends Application { - - private AppComponent component; - - @Override - public void onCreate() { - super.onCreate(); - - Debug.init(new AndroidLogDebugOutput(BuildConfig.DEBUG)); - - component = DaggerAppComponent.builder() - .appModule(new AppModule(this)) - .build(); - } - - public static AppComponent component(@NonNull Context context) { - return ((App) context.getApplicationContext()).component; - } -} diff --git a/app/src/main/java/io/noties/markwon/app/AppBarItem.java b/app/src/main/java/io/noties/markwon/app/AppBarItem.java deleted file mode 100644 index a8b8a322..00000000 --- a/app/src/main/java/io/noties/markwon/app/AppBarItem.java +++ /dev/null @@ -1,42 +0,0 @@ -package io.noties.markwon.app; - -import android.text.TextUtils; -import android.view.View; -import android.widget.TextView; - -import androidx.annotation.NonNull; - -abstract class AppBarItem { - - static class State { - final String title; - final String subtitle; - - State(String title, String subtitle) { - this.title = title; - this.subtitle = subtitle; - } - } - - static class Renderer { - - final TextView title; - final TextView subtitle; - - Renderer(@NonNull View view, @NonNull View.OnClickListener themeChangeClicked) { - this.title = view.findViewById(R.id.app_bar_title); - this.subtitle = view.findViewById(R.id.app_bar_subtitle); - view.findViewById(R.id.app_bar_theme_changer) - .setOnClickListener(themeChangeClicked); - } - - void render(@NonNull State state) { - title.setText(state.title); - subtitle.setText(state.subtitle); - Views.setVisible(subtitle, !TextUtils.isEmpty(state.subtitle)); - } - } - - private AppBarItem() { - } -} diff --git a/app/src/main/java/io/noties/markwon/app/AppComponent.java b/app/src/main/java/io/noties/markwon/app/AppComponent.java deleted file mode 100644 index 6f657b1c..00000000 --- a/app/src/main/java/io/noties/markwon/app/AppComponent.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.noties.markwon.app; - -import javax.inject.Singleton; - -import dagger.Component; - -@Component(modules = AppModule.class) -@Singleton -interface AppComponent { - MainActivitySubcomponent mainActivitySubcomponent(); -} diff --git a/app/src/main/java/io/noties/markwon/app/AppModule.java b/app/src/main/java/io/noties/markwon/app/AppModule.java deleted file mode 100644 index 45673165..00000000 --- a/app/src/main/java/io/noties/markwon/app/AppModule.java +++ /dev/null @@ -1,93 +0,0 @@ -package io.noties.markwon.app; - -import android.content.Context; -import android.content.res.Resources; -import android.os.Handler; -import android.os.Looper; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import javax.inject.Singleton; - -import dagger.Module; -import dagger.Provides; -import io.noties.markwon.syntax.Prism4jThemeDarkula; -import io.noties.markwon.syntax.Prism4jThemeDefault; -import okhttp3.Cache; -import okhttp3.OkHttpClient; -import io.noties.prism4j.Prism4j; -import io.noties.prism4j.annotations.PrismBundle; - -@Module -@PrismBundle(includeAll = true) -class AppModule { - - private final App app; - - AppModule(App app) { - this.app = app; - } - - @Provides - Context context() { - return app.getApplicationContext(); - } - - @Provides - Resources resources() { - return app.getResources(); - } - - @Provides - @Singleton - OkHttpClient client() { - return new OkHttpClient.Builder() - .cache(new Cache(app.getCacheDir(), 1024L * 1024 * 20)) // 20 mb - .followRedirects(true) - .retryOnConnectionFailure(true) - .build(); - } - - @Singleton - @Provides - ExecutorService executorService() { - return Executors.newCachedThreadPool(); - } - - @Singleton - @Provides - Handler mainThread() { - return new Handler(Looper.getMainLooper()); - } - - @Singleton - @Provides - UriProcessor uriProcessor() { - return new UriProcessorImpl(); - } - - @Provides - @Singleton - Prism4j prism4j() { - return new Prism4j(new GrammarLocatorDef()); - } - - @Singleton - @Provides - Prism4jThemeDefault prism4jThemeDefault() { - return Prism4jThemeDefault.create(); - } - - @Singleton - @Provides - Prism4jThemeDarkula prism4jThemeDarkula() { - return Prism4jThemeDarkula.create(0x0Fffffff); - } -// -// @Singleton -// @Provides -// GifProcessor gifProcessor() { -// return GifProcessor.create(); -// } -} diff --git a/app/src/main/java/io/noties/markwon/app/ImageDestinationProcessorInitialReadme.java b/app/src/main/java/io/noties/markwon/app/ImageDestinationProcessorInitialReadme.java deleted file mode 100644 index 26653af9..00000000 --- a/app/src/main/java/io/noties/markwon/app/ImageDestinationProcessorInitialReadme.java +++ /dev/null @@ -1,30 +0,0 @@ -package io.noties.markwon.app; - -import android.net.Uri; -import android.text.TextUtils; - -import androidx.annotation.NonNull; - -import io.noties.markwon.image.destination.ImageDestinationProcessor; -import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute; - -class ImageDestinationProcessorInitialReadme extends ImageDestinationProcessor { - - private static final String GITHUB_BASE = "https://github.com/noties/Markwon/raw/master/"; - - private final ImageDestinationProcessorRelativeToAbsolute processor - = new ImageDestinationProcessorRelativeToAbsolute(GITHUB_BASE); - - @NonNull - @Override - public String process(@NonNull String destination) { - String out; - final Uri uri = Uri.parse(destination); - if (TextUtils.isEmpty(uri.getScheme())) { - out = processor.process(destination); - } else { - out = destination; - } - return out; - } -} diff --git a/app/src/main/java/io/noties/markwon/app/MainActivity.java b/app/src/main/java/io/noties/markwon/app/MainActivity.java deleted file mode 100644 index c0cb1390..00000000 --- a/app/src/main/java/io/noties/markwon/app/MainActivity.java +++ /dev/null @@ -1,138 +0,0 @@ -package io.noties.markwon.app; - -import android.app.Activity; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.text.Spanned; -import android.text.method.LinkMovementMethod; -import android.view.View; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import javax.inject.Inject; - -import io.noties.debug.Debug; -import io.noties.markwon.Markwon; -import io.noties.markwon.utils.NoCopySpannableFactory; - -public class MainActivity extends Activity { - - @Inject - MarkdownLoader markdownLoader; - - @Inject - MarkdownRenderer markdownRenderer; - - @Inject - Themes themes; - - @Inject - UriProcessor uriProcessor; - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - App.component(this) - .mainActivitySubcomponent() - .inject(this); - - themes.apply(this); - - setContentView(R.layout.activity_main); - - // we process additionally github urls, as if url has in path `blob`, we won't receive - // desired file, but instead rendered html - checkUri(); - - final AppBarItem.Renderer appBarRenderer - = new AppBarItem.Renderer(findViewById(R.id.app_bar), new View.OnClickListener() { - @Override - public void onClick(View v) { - themes.toggle(); - recreate(); - } - }); - - final TextView textView = findViewById(R.id.text); - final View progress = findViewById(R.id.progress); - - appBarRenderer.render(appBarState()); - - textView.setMovementMethod(LinkMovementMethod.getInstance()); - textView.setSpannableFactory(NoCopySpannableFactory.getInstance()); - - markdownLoader.load(uri(), new MarkdownLoader.OnMarkdownTextLoaded() { - @Override - public void apply(final String text) { - markdownRenderer.render(MainActivity.this, themes.isLight(), uri(), text, new MarkdownRenderer.MarkdownReadyListener() { - @Override - public void onMarkdownReady(@NonNull Markwon markwon, Spanned markdown) { - - markwon.setParsedMarkdown(textView, markdown); - - Views.setVisible(progress, false); - } - }); - } - }); - } - - @NonNull - private AppBarItem.State appBarState() { - - final String title; - final String subtitle; - - // two possible states: just opened from launcher (no subtitle) - // opened to display external resource (subtitle as a path/url/whatever) - - final Uri uri = uri(); - - if (uri != null) { - title = uri.getLastPathSegment(); - subtitle = uri.toString(); - } else { - title = getString(R.string.app_name); - subtitle = null; - } - - return new AppBarItem.State(title, subtitle); - } - - private void checkUri() { - final Uri uri = uri(); - if (uri != null) { - getIntent().setData(uriProcessor.process(uri)); - } - } - - @Nullable - private Uri uri() { - final Intent intent = getIntent(); - return intent != null - ? intent.getData() - : null; - } - - @Override - protected void onRestoreInstanceState(Bundle savedInstanceState) { - try { - super.onRestoreInstanceState(savedInstanceState); - } catch (Throwable t) { - // amazing stuff, we need this because on JB it will crash otherwise with NPE - Debug.e(t); - } - } - - @Override - protected void onDestroy() { - super.onDestroy(); - - markdownLoader.cancel(); - markdownRenderer.cancel(); - } -} diff --git a/app/src/main/java/io/noties/markwon/app/MainActivitySubcomponent.java b/app/src/main/java/io/noties/markwon/app/MainActivitySubcomponent.java deleted file mode 100644 index 24d67488..00000000 --- a/app/src/main/java/io/noties/markwon/app/MainActivitySubcomponent.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.noties.markwon.app; - -import dagger.Subcomponent; - -@Subcomponent -@ActivityScope -interface MainActivitySubcomponent { - void inject(MainActivity activity); -} diff --git a/app/src/main/java/io/noties/markwon/app/MarkdownLoader.java b/app/src/main/java/io/noties/markwon/app/MarkdownLoader.java deleted file mode 100644 index db135dc4..00000000 --- a/app/src/main/java/io/noties/markwon/app/MarkdownLoader.java +++ /dev/null @@ -1,196 +0,0 @@ -package io.noties.markwon.app; - -import android.content.ContentResolver; -import android.content.Context; -import android.net.Uri; -import android.os.Handler; -import android.text.TextUtils; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; - -import javax.inject.Inject; - -import io.noties.debug.Debug; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; - -@ActivityScope -public class MarkdownLoader { - - public interface OnMarkdownTextLoaded { - void apply(String text); - } - - @Inject - Context context; - - @Inject - ExecutorService service; - - @Inject - Handler handler; - - @Inject - OkHttpClient client; - - private Future task; - - @Inject - MarkdownLoader() { - } - - public void load(@Nullable final Uri uri, @NonNull final OnMarkdownTextLoaded loaded) { - cancel(); - task = service.submit(new Runnable() { - @Override - public void run() { - try { - deliver(loaded, text(uri)); - } catch (Throwable t) { - Debug.e(t); - } - } - }); - } - - public void cancel() { - if (task != null) { - task.cancel(true); - task = null; - } - } - - private boolean isCancelled() { - return task == null || task.isCancelled(); - } - - private void deliver(@NonNull final OnMarkdownTextLoaded loaded, final String text) { - if (!isCancelled()) { - handler.post(new Runnable() { - @Override - public void run() { - // as this call is async, we need to check again if we are cancelled - if (!isCancelled()) { - loaded.apply(text); - task = null; - } - } - }); - } - } - - private String text(@Nullable Uri uri) { - final String out; - if (uri == null) { - out = loadReadMe(); - } else { - out = loadExternalResource(uri); - } - return out; - } - - private String loadReadMe() { - InputStream stream = null; - try { - stream = context.getAssets().open("README.md"); - } catch (IOException e) { - Debug.e(e); - } - return readStream(stream); - } - - private String loadExternalResource(@NonNull Uri uri) { - final String out; - final String scheme = uri.getScheme(); - if (!TextUtils.isEmpty(scheme) && ContentResolver.SCHEME_FILE.equals(scheme)) { - out = loadExternalFile(uri); - } else { - out = loadExternalUrl(uri); - } - return out; - } - - private String loadExternalFile(@NonNull Uri uri) { - InputStream stream = null; - try { - stream = new FileInputStream(new File(uri.getPath())); - } catch (FileNotFoundException e) { - Debug.e(e); - } - return readStream(stream); - } - - private String loadExternalUrl(@NonNull Uri uri) { - - final Request request = new Request.Builder() - .url(uri.toString()) - .build(); - - Response response = null; - try { - response = client.newCall(request).execute(); - } catch (IOException e) { - Debug.e(e); - } - - final ResponseBody body = response != null - ? response.body() - : null; - - String out = null; - - if (body != null) { - try { - out = body.string(); - } catch (IOException e) { - Debug.e(e); - } - } - - return out; - } - - private static String readStream(@Nullable InputStream inputStream) { - - String out = null; - - if (inputStream != null) { - BufferedReader reader = null; - try { - reader = new BufferedReader(new InputStreamReader(inputStream)); - final StringBuilder builder = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - builder.append(line) - .append('\n'); - } - out = builder.toString(); - } catch (IOException e) { - Debug.e(e); - } finally { - if (reader != null) { - try { - reader.close(); - } catch (IOException e) { - // no op - } - } - } - } - - return out; - } -} diff --git a/app/src/main/java/io/noties/markwon/app/MarkdownRenderer.java b/app/src/main/java/io/noties/markwon/app/MarkdownRenderer.java deleted file mode 100644 index d820733e..00000000 --- a/app/src/main/java/io/noties/markwon/app/MarkdownRenderer.java +++ /dev/null @@ -1,160 +0,0 @@ -package io.noties.markwon.app; - -import android.content.Context; -import android.net.Uri; -import android.os.Handler; -import android.os.SystemClock; -import android.text.Spanned; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; - -import javax.inject.Inject; - -import io.noties.debug.Debug; -import io.noties.markwon.AbstractMarkwonPlugin; -import io.noties.markwon.Markwon; -import io.noties.markwon.MarkwonConfiguration; -import io.noties.markwon.app.gif.GifAwarePlugin; -import io.noties.markwon.ext.strikethrough.StrikethroughPlugin; -import io.noties.markwon.ext.tables.TablePlugin; -import io.noties.markwon.ext.tasklist.TaskListPlugin; -import io.noties.markwon.html.HtmlPlugin; -import io.noties.markwon.image.ImagesPlugin; -import io.noties.markwon.image.destination.ImageDestinationProcessor; -import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute; -import io.noties.markwon.image.file.FileSchemeHandler; -import io.noties.markwon.image.gif.GifMediaDecoder; -import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler; -import io.noties.markwon.syntax.Prism4jTheme; -import io.noties.markwon.syntax.Prism4jThemeDarkula; -import io.noties.markwon.syntax.Prism4jThemeDefault; -import io.noties.markwon.syntax.SyntaxHighlightPlugin; -import io.noties.prism4j.Prism4j; - -@ActivityScope -public class MarkdownRenderer { - - interface MarkdownReadyListener { - void onMarkdownReady(@NonNull Markwon markwon, Spanned markdown); - } - - @Inject - ExecutorService service; - - @Inject - Handler handler; - - @Inject - Prism4j prism4j; - - @Inject - Prism4jThemeDefault prism4jThemeDefault; - - @Inject - Prism4jThemeDarkula prism4JThemeDarkula; - - private Future task; - - @Inject - MarkdownRenderer() { - } - - public void render( - @NonNull final Context context, - final boolean isLightTheme, - @Nullable final Uri uri, - @NonNull final String markdown, - @NonNull final MarkdownReadyListener listener) { - - // todo: create prism4j theme factory (accepting light/dark argument) - - cancel(); - - task = service.submit(new Runnable() { - - @Override - public void run() { - try { - execute(); - } catch (Throwable t) { - Debug.e(t); - } - } - - private void execute() { - final ImageDestinationProcessor imageDestinationProcessor; - if (uri == null) { - imageDestinationProcessor = new ImageDestinationProcessorInitialReadme(); - } else { - imageDestinationProcessor = new ImageDestinationProcessorRelativeToAbsolute(uri.toString()); - } - - final Prism4jTheme prism4jTheme = isLightTheme - ? prism4jThemeDefault - : prism4JThemeDarkula; - - final Markwon markwon = Markwon.builder(context) - .usePlugin(ImagesPlugin.create(new ImagesPlugin.ImagesConfigure() { - @Override - public void configureImages(@NonNull ImagesPlugin plugin) { - // data uri scheme handler is added automatically - // SVG & GIF will be added if required dependencies are present in the classpath - // default-media-decoder is also added automatically - plugin - .addSchemeHandler(OkHttpNetworkSchemeHandler.create()) - .addSchemeHandler(FileSchemeHandler.createWithAssets(context.getAssets())) - .addMediaDecoder(GifMediaDecoder.create(false)); - } - })) - .usePlugin(SyntaxHighlightPlugin.create(prism4j, prism4jTheme)) - .usePlugin(GifAwarePlugin.create(context)) - .usePlugin(TablePlugin.create(context)) - .usePlugin(TaskListPlugin.create(context)) - .usePlugin(StrikethroughPlugin.create()) - .usePlugin(HtmlPlugin.create()) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { - builder.imageDestinationProcessor(imageDestinationProcessor); - } - }) - .build(); - - final long start = SystemClock.uptimeMillis(); - - final Spanned text = markwon.toMarkdown(markdown); - - final long end = SystemClock.uptimeMillis(); - - Debug.i("markdown rendered: %d ms", end - start); - - if (!isCancelled()) { - handler.post(new Runnable() { - @Override - public void run() { - if (!isCancelled()) { - listener.onMarkdownReady(markwon, text); - task = null; - } - } - }); - } - } - }); - } - - public void cancel() { - if (task != null) { - task.cancel(true); - task = null; - } - } - - private boolean isCancelled() { - return task == null || task.isCancelled(); - } -} diff --git a/app/src/main/java/io/noties/markwon/app/Themes.java b/app/src/main/java/io/noties/markwon/app/Themes.java deleted file mode 100644 index 14dc1f61..00000000 --- a/app/src/main/java/io/noties/markwon/app/Themes.java +++ /dev/null @@ -1,51 +0,0 @@ -package io.noties.markwon.app; - -import android.content.Context; -import android.content.SharedPreferences; - -import androidx.annotation.NonNull; - -import javax.inject.Inject; -import javax.inject.Singleton; - -@Singleton -public class Themes { - - private static final String PREF_NAME = "theme"; - private static final String KEY_THEME_DARK = "key.tD"; - - private SharedPreferences preferences; - - @Inject - Themes(Context context) { - this.preferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - } - - public void apply(@NonNull Context context) { - final boolean dark = preferences.getBoolean(KEY_THEME_DARK, false); - // we have only 2 themes and Light one is default - final int theme; - if (dark) { - theme = R.style.AppThemeDark; - } else { - theme = R.style.AppThemeLight; - } - - final Context appContext = context.getApplicationContext(); - if (appContext != context) { - appContext.setTheme(theme); - } - context.setTheme(theme); - } - - public void toggle() { - final boolean newValue = !preferences.getBoolean(KEY_THEME_DARK, false); - preferences.edit() - .putBoolean(KEY_THEME_DARK, newValue) - .apply(); - } - - public boolean isLight() { - return !preferences.getBoolean(KEY_THEME_DARK, false); - } -} diff --git a/app/src/main/java/io/noties/markwon/app/UriProcessor.java b/app/src/main/java/io/noties/markwon/app/UriProcessor.java deleted file mode 100644 index 45364c67..00000000 --- a/app/src/main/java/io/noties/markwon/app/UriProcessor.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.noties.markwon.app; - -import android.net.Uri; - -import androidx.annotation.NonNull; - -@SuppressWarnings("WeakerAccess") -public interface UriProcessor { - Uri process(@NonNull Uri uri); -} diff --git a/app/src/main/java/io/noties/markwon/app/UriProcessorImpl.java b/app/src/main/java/io/noties/markwon/app/UriProcessorImpl.java deleted file mode 100644 index a4bb3137..00000000 --- a/app/src/main/java/io/noties/markwon/app/UriProcessorImpl.java +++ /dev/null @@ -1,55 +0,0 @@ -package io.noties.markwon.app; - -import android.net.Uri; - -import androidx.annotation.NonNull; - -import java.util.List; - -class UriProcessorImpl implements UriProcessor { - - private static final String GITHUB = "github.com"; - - @Override - public Uri process(@NonNull final Uri uri) { - - // hm... github, even having a README.md in path will return rendered HTML - - final Uri out; - - if (GITHUB.equals(uri.getAuthority())) { - - final List segments = uri.getPathSegments(); - final int size = segments != null - ? segments.size() - : 0; - - if (size > 0) { - - // we need to modify the final uri - final Uri.Builder builder = new Uri.Builder() - .scheme(uri.getScheme()) - .authority(uri.getAuthority()) - .fragment(uri.getFragment()) - .query(uri.getQuery()); - - for (String segment : segments) { - final String part; - if ("blob".equals(segment)) { - part = "raw"; - } else { - part = segment; - } - builder.appendPath(part); - } - out = builder.build(); - } else { - out = uri; - } - } else { - out = uri; - } - - return out; - } -} diff --git a/app/src/main/java/io/noties/markwon/app/Views.java b/app/src/main/java/io/noties/markwon/app/Views.java deleted file mode 100644 index 587b2c9f..00000000 --- a/app/src/main/java/io/noties/markwon/app/Views.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.noties.markwon.app; - -import android.view.View; - -import androidx.annotation.IntDef; -import androidx.annotation.NonNull; - -@SuppressWarnings("WeakerAccess") -public abstract class Views { - - @IntDef({View.INVISIBLE, View.GONE}) - @interface NotVisible { - } - - public static void setVisible(@NonNull View view, boolean visible) { - setVisible(view, visible, View.GONE); - } - - public static void setVisible(@NonNull View view, boolean visible, @NotVisible int notVisible) { - final int visibility = visible - ? View.VISIBLE - : notVisible; - view.setVisibility(visibility); - } - - private Views() { - } -} diff --git a/app/src/main/java/io/noties/markwon/app/gif/GifAwareAsyncDrawable.java b/app/src/main/java/io/noties/markwon/app/gif/GifAwareAsyncDrawable.java deleted file mode 100644 index 58c0bad3..00000000 --- a/app/src/main/java/io/noties/markwon/app/gif/GifAwareAsyncDrawable.java +++ /dev/null @@ -1,60 +0,0 @@ -package io.noties.markwon.app.gif; - -import android.graphics.Canvas; -import android.graphics.drawable.Drawable; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import io.noties.markwon.image.AsyncDrawable; -import io.noties.markwon.image.AsyncDrawableLoader; -import io.noties.markwon.image.ImageSize; -import io.noties.markwon.image.ImageSizeResolver; -import pl.droidsonroids.gif.GifDrawable; - -public class GifAwareAsyncDrawable extends AsyncDrawable { - - public interface OnGifResultListener { - void onGifResult(@NonNull GifAwareAsyncDrawable drawable); - } - - private final Drawable gifPlaceholder; - private OnGifResultListener onGifResultListener; - private boolean isGif; - - public GifAwareAsyncDrawable( - @NonNull Drawable gifPlaceholder, - @NonNull String destination, - @NonNull AsyncDrawableLoader loader, - @Nullable ImageSizeResolver imageSizeResolver, - @Nullable ImageSize imageSize) { - super(destination, loader, imageSizeResolver, imageSize); - this.gifPlaceholder = gifPlaceholder; - } - - public void onGifResultListener(@Nullable OnGifResultListener onGifResultListener) { - this.onGifResultListener = onGifResultListener; - } - - @Override - public void setResult(@NonNull Drawable result) { - super.setResult(result); - isGif = result instanceof GifDrawable; - if (isGif && onGifResultListener != null) { - onGifResultListener.onGifResult(this); - } - } - - @Override - public void draw(@NonNull Canvas canvas) { - super.draw(canvas); - - if (isGif) { - final GifDrawable drawable = (GifDrawable) getResult(); - if (!drawable.isPlaying()) { - gifPlaceholder.setBounds(drawable.getBounds()); - gifPlaceholder.draw(canvas); - } - } - } -} diff --git a/app/src/main/java/io/noties/markwon/app/gif/GifAwarePlugin.java b/app/src/main/java/io/noties/markwon/app/gif/GifAwarePlugin.java deleted file mode 100644 index 30b33f5b..00000000 --- a/app/src/main/java/io/noties/markwon/app/gif/GifAwarePlugin.java +++ /dev/null @@ -1,65 +0,0 @@ -package io.noties.markwon.app.gif; - -import android.content.Context; -import android.widget.TextView; - -import androidx.annotation.NonNull; - -import org.commonmark.node.Image; - -import io.noties.markwon.AbstractMarkwonPlugin; -import io.noties.markwon.MarkwonConfiguration; -import io.noties.markwon.MarkwonSpansFactory; -import io.noties.markwon.RenderProps; -import io.noties.markwon.SpanFactory; -import io.noties.markwon.app.R; -import io.noties.markwon.image.AsyncDrawableSpan; -import io.noties.markwon.image.ImageProps; - -public class GifAwarePlugin extends AbstractMarkwonPlugin { - - @NonNull - public static GifAwarePlugin create(@NonNull Context context) { - return new GifAwarePlugin(context); - } - - private final Context context; - private final GifProcessor processor; - - GifAwarePlugin(@NonNull Context context) { - this.context = context; - this.processor = GifProcessor.create(); - } - - @Override - public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { - - final GifPlaceholder gifPlaceholder = new GifPlaceholder( - context.getResources().getDrawable(R.drawable.ic_play_circle_filled_18dp_white), - 0x20000000 - ); - - builder.setFactory(Image.class, new SpanFactory() { - @Override - public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { - return new AsyncDrawableSpan( - configuration.theme(), - new GifAwareAsyncDrawable( - gifPlaceholder, - ImageProps.DESTINATION.require(props), - configuration.asyncDrawableLoader(), - configuration.imageSizeResolver(), - ImageProps.IMAGE_SIZE.get(props) - ), - AsyncDrawableSpan.ALIGN_BOTTOM, - ImageProps.REPLACEMENT_TEXT_IS_LINK.get(props, false) - ); - } - }); - } - - @Override - public void afterSetText(@NonNull TextView textView) { - processor.process(textView); - } -} diff --git a/app/src/main/java/io/noties/markwon/app/gif/GifPlaceholder.java b/app/src/main/java/io/noties/markwon/app/gif/GifPlaceholder.java deleted file mode 100644 index 8417545b..00000000 --- a/app/src/main/java/io/noties/markwon/app/gif/GifPlaceholder.java +++ /dev/null @@ -1,78 +0,0 @@ -package io.noties.markwon.app.gif; - -import android.graphics.Canvas; -import android.graphics.ColorFilter; -import android.graphics.Paint; -import android.graphics.PixelFormat; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; - -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -public class GifPlaceholder extends Drawable { - - private final Drawable icon; - private final Paint paint; - - private float left; - private float top; - - public GifPlaceholder(@NonNull Drawable icon, @ColorInt int background) { - this.icon = icon; - if (icon.getBounds().isEmpty()) { - icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); - } - - if (background != 0) { - paint = new Paint(Paint.ANTI_ALIAS_FLAG); - paint.setStyle(Paint.Style.FILL); - paint.setColor(background); - } else { - paint = null; - } - } - - @Override - protected void onBoundsChange(Rect bounds) { - super.onBoundsChange(bounds); - - final int w = bounds.width(); - final int h = bounds.height(); - - this.left = (w - icon.getBounds().width()) / 2; - this.top = (h - icon.getBounds().height()) / 2; - } - - @Override - public void draw(@NonNull Canvas canvas) { - - if (paint != null) { - canvas.drawRect(getBounds(), paint); - } - - final int save = canvas.save(); - try { - canvas.translate(left, top); - icon.draw(canvas); - } finally { - canvas.restoreToCount(save); - } - } - - @Override - public void setAlpha(int alpha) { - // no op - } - - @Override - public void setColorFilter(@Nullable ColorFilter colorFilter) { - // no op - } - - @Override - public int getOpacity() { - return PixelFormat.OPAQUE; - } -} diff --git a/app/src/main/java/io/noties/markwon/app/gif/GifProcessor.java b/app/src/main/java/io/noties/markwon/app/gif/GifProcessor.java deleted file mode 100644 index 59d4dfef..00000000 --- a/app/src/main/java/io/noties/markwon/app/gif/GifProcessor.java +++ /dev/null @@ -1,129 +0,0 @@ -package io.noties.markwon.app.gif; - -import android.text.Spannable; -import android.text.Spanned; -import android.text.style.ClickableSpan; -import android.view.View; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import io.noties.markwon.image.AsyncDrawableSpan; -import pl.droidsonroids.gif.GifDrawable; - -public abstract class GifProcessor { - - public abstract void process(@NonNull TextView textView); - - @NonNull - public static GifProcessor create() { - return new Impl(); - } - - static class Impl extends GifProcessor { - - @Override - public void process(@NonNull final TextView textView) { - - // here is what we will do additionally: - // we query for all asyncDrawableSpans - // we check if they are inside clickableSpan - // if not we apply onGifListener - - final Spannable spannable = spannable(textView); - - if (spannable == null) { - return; - } - - final AsyncDrawableSpan[] asyncDrawableSpans = - spannable.getSpans(0, spannable.length(), AsyncDrawableSpan.class); - if (asyncDrawableSpans == null - || asyncDrawableSpans.length == 0) { - return; - } - - int start; - int end; - ClickableSpan[] clickableSpans; - - for (final AsyncDrawableSpan asyncDrawableSpan : asyncDrawableSpans) { - - start = spannable.getSpanStart(asyncDrawableSpan); - end = spannable.getSpanEnd(asyncDrawableSpan); - - if (start < 0 - || end < 0) { - continue; - } - - clickableSpans = spannable.getSpans(start, end, ClickableSpan.class); - if (clickableSpans != null - && clickableSpans.length > 0) { - continue; - } - - ((GifAwareAsyncDrawable) asyncDrawableSpan.getDrawable()).onGifResultListener(new GifAwareAsyncDrawable.OnGifResultListener() { - @Override - public void onGifResult(@NonNull GifAwareAsyncDrawable drawable) { - addGifClickSpan(textView, asyncDrawableSpan, drawable); - } - }); - } - } - - @Nullable - private static Spannable spannable(@NonNull TextView textView) { - final CharSequence charSequence = textView.getText(); - if (charSequence instanceof Spannable) { - return (Spannable) charSequence; - } - return null; - } - - private static void addGifClickSpan( - @NonNull TextView textView, - @NonNull AsyncDrawableSpan span, - @NonNull GifAwareAsyncDrawable drawable) { - - // important thing here is to obtain new spannable from textView - // as with each `setText()` new spannable is created and keeping reference - // to an older one won't affect textView - final Spannable spannable = spannable(textView); - - if (spannable == null) { - return; - } - - final int start = spannable.getSpanStart(span); - final int end = spannable.getSpanEnd(span); - if (start < 0 - || end < 0) { - return; - } - - final GifDrawable gifDrawable = (GifDrawable) drawable.getResult(); - spannable.setSpan(new GifToggleClickableSpan(gifDrawable), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - - private static class GifToggleClickableSpan extends ClickableSpan { - - private final GifDrawable gifDrawable; - - GifToggleClickableSpan(@NonNull GifDrawable gifDrawable) { - this.gifDrawable = gifDrawable; - } - - @Override - public void onClick(@NonNull View widget) { - if (gifDrawable.isPlaying()) { - gifDrawable.pause(); - } else { - gifDrawable.start(); - } - widget.invalidate(); - } - } - } -} diff --git a/app/src/main/res/drawable-hdpi/ic_play_circle_filled_18dp_white.png b/app/src/main/res/drawable-hdpi/ic_play_circle_filled_18dp_white.png deleted file mode 100644 index 7354e795..00000000 Binary files a/app/src/main/res/drawable-hdpi/ic_play_circle_filled_18dp_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-v26/ic_launcher_background.xml b/app/src/main/res/drawable-v26/ic_launcher_background.xml deleted file mode 100644 index 49c86ecb..00000000 --- a/app/src/main/res/drawable-v26/ic_launcher_background.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable-xhdpi/ic_play_circle_filled_18dp_white.png b/app/src/main/res/drawable-xhdpi/ic_play_circle_filled_18dp_white.png deleted file mode 100644 index c0358ebe..00000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_play_circle_filled_18dp_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_play_circle_filled_18dp_white.png b/app/src/main/res/drawable-xxhdpi/ic_play_circle_filled_18dp_white.png deleted file mode 100644 index 53dadfdf..00000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_play_circle_filled_18dp_white.png and /dev/null differ diff --git a/app/src/main/res/drawable/bg_app_bar_shadow.xml b/app/src/main/res/drawable/bg_app_bar_shadow.xml deleted file mode 100644 index 70124dc0..00000000 --- a/app/src/main/res/drawable/bg_app_bar_shadow.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_app_bar_theme_dark.xml b/app/src/main/res/drawable/ic_app_bar_theme_dark.xml deleted file mode 100644 index c3c5060d..00000000 --- a/app/src/main/res/drawable/ic_app_bar_theme_dark.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_app_bar_theme_light.xml b/app/src/main/res/drawable/ic_app_bar_theme_light.xml deleted file mode 100644 index b09b244b..00000000 --- a/app/src/main/res/drawable/ic_app_bar_theme_light.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 49c86ecb..00000000 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index 46a28b85..00000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,102 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index c4a603d4..00000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi-v26/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi-v26/ic_launcher_foreground.png deleted file mode 100644 index 89979b2e..00000000 Binary files a/app/src/main/res/mipmap-hdpi-v26/ic_launcher_foreground.png and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 61bb0e66..00000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi-v26/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi-v26/ic_launcher_foreground.png deleted file mode 100644 index c61b1584..00000000 Binary files a/app/src/main/res/mipmap-xhdpi-v26/ic_launcher_foreground.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 80952f3d..00000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi-v26/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi-v26/ic_launcher_foreground.png deleted file mode 100644 index f5213a49..00000000 Binary files a/app/src/main/res/mipmap-xxhdpi-v26/ic_launcher_foreground.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d0b015ba..00000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi-v26/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi-v26/ic_launcher_foreground.png deleted file mode 100644 index 37333547..00000000 Binary files a/app/src/main/res/mipmap-xxxhdpi-v26/ic_launcher_foreground.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index df7e0fc6..00000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/values-v21/styles.xml b/app/src/main/res/values-v21/styles.xml deleted file mode 100644 index 1b3d1da6..00000000 --- a/app/src/main/res/values-v21/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml deleted file mode 100644 index 9dd0654f..00000000 --- a/app/src/main/res/values/colors.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - #424242 - #212121 - #4caf50 - - #FFF - #dd000000 - - #FFF - #dd000000 - - #303030 - #ddffffff - - diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml deleted file mode 100644 index 47c82246..00000000 --- a/app/src/main/res/values/dimens.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 16dp - 16dp - diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml deleted file mode 100644 index edca2c81..00000000 --- a/app/src/main/res/values/ids.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml deleted file mode 100644 index 35f36bb5..00000000 --- a/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - Markwon - diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml deleted file mode 100644 index 1da91be9..00000000 --- a/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - diff --git a/sample/build.gradle b/sample/build.gradle deleted file mode 100644 index 81d14d9e..00000000 --- a/sample/build.gradle +++ /dev/null @@ -1,75 +0,0 @@ -apply plugin: 'com.android.application' - -android { - - compileSdkVersion config['compile-sdk'] - buildToolsVersion config['build-tools'] - - defaultConfig { - applicationId "io.noties.markwon.sample" - minSdkVersion config['min-sdk'] - targetSdkVersion config['target-sdk'] - versionCode 1 - versionName version - setProperty("archivesBaseName", "markwon-sample-$versionName") - - resConfig 'en' - } - - lintOptions { - abortOnError false - } - - dexOptions { - preDexLibraries true - javaMaxHeapSize '5g' - } - - compileOptions { - targetCompatibility JavaVersion.VERSION_1_8 - sourceCompatibility JavaVersion.VERSION_1_8 - } -} - -dependencies { - - implementation project(':markwon-core') - implementation project(':markwon-editor') - implementation project(':markwon-ext-latex') - implementation project(':markwon-ext-strikethrough') - implementation project(':markwon-ext-tables') - implementation project(':markwon-ext-tasklist') - implementation project(':markwon-html') - implementation project(':markwon-image') - implementation project(':markwon-inline-parser') - implementation project(':markwon-linkify') - implementation project(':markwon-recycler') - implementation project(':markwon-recycler-table') - implementation project(':markwon-simple-ext') - implementation project(':markwon-syntax-highlight') - - implementation project(':markwon-image-picasso') - implementation project(':markwon-image-glide') - - deps.with { - implementation it['x-recycler-view'] - implementation it['x-core'] // for precomputedTextCompat - implementation it['x-appcompat'] // for setTextFuture - implementation it['okhttp'] - implementation it['prism4j'] - implementation it['debug'] - implementation it['adapt'] - implementation it['android-svg'] - implementation it['android-gif'] - } - - deps['annotationProcessor'].with { - annotationProcessor it['prism4j-bundler'] - } - - deps['test'].with { - testImplementation it['junit'] - testImplementation it['robolectric'] - testImplementation it['mockito'] - } -} diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml deleted file mode 100644 index 190d2c05..00000000 --- a/sample/src/main/AndroidManifest.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/sample/src/main/assets/README.md b/sample/src/main/assets/README.md deleted file mode 120000 index ff5c7960..00000000 --- a/sample/src/main/assets/README.md +++ /dev/null @@ -1 +0,0 @@ -../../../../README.md \ No newline at end of file diff --git a/sample/src/main/java/io/noties/markwon/sample/ActivityWithMenuOptions.java b/sample/src/main/java/io/noties/markwon/sample/ActivityWithMenuOptions.java deleted file mode 100644 index 54f5342f..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/ActivityWithMenuOptions.java +++ /dev/null @@ -1,49 +0,0 @@ -package io.noties.markwon.sample; - -import android.app.Activity; -import android.os.Bundle; -import android.view.Menu; -import android.view.MenuItem; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -public abstract class ActivityWithMenuOptions extends Activity { - - @NonNull - public abstract MenuOptions menuOptions(); - - protected void beforeOptionSelected(@NonNull String option) { - // no op, override to customize - } - - protected void afterOptionSelected(@NonNull String option) { - // no op, override to customize - } - - private MenuOptions menuOptions; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - menuOptions = menuOptions(); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - return menuOptions.onCreateOptionsMenu(menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - final MenuOptions.Option option = menuOptions.onOptionsItemSelected(item); - if (option != null) { - beforeOptionSelected(option.title); - option.action.run(); - afterOptionSelected(option.title); - return true; - } - return false; - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/MainActivity.java b/sample/src/main/java/io/noties/markwon/sample/MainActivity.java deleted file mode 100644 index beeda582..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/MainActivity.java +++ /dev/null @@ -1,166 +0,0 @@ -package io.noties.markwon.sample; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import java.util.ArrayList; -import java.util.List; - -import io.noties.adapt.Adapt; -import io.noties.adapt.Item; -import io.noties.debug.AndroidLogDebugOutput; -import io.noties.debug.Debug; -import io.noties.markwon.Markwon; -import io.noties.markwon.sample.basicplugins.BasicPluginsActivity; -import io.noties.markwon.sample.core.CoreActivity; -import io.noties.markwon.sample.customextension.CustomExtensionActivity; -import io.noties.markwon.sample.customextension2.CustomExtensionActivity2; -import io.noties.markwon.sample.editor.EditorActivity; -import io.noties.markwon.sample.html.HtmlActivity; -import io.noties.markwon.sample.htmldetails.HtmlDetailsActivity; -import io.noties.markwon.sample.images.ImagesActivity; -import io.noties.markwon.sample.inlineparser.InlineParserActivity; -import io.noties.markwon.sample.latex.LatexActivity; -import io.noties.markwon.sample.notification.NotificationActivity; -import io.noties.markwon.sample.precomputed.PrecomputedActivity; -import io.noties.markwon.sample.precomputed.PrecomputedFutureActivity; -import io.noties.markwon.sample.recycler.RecyclerActivity; -import io.noties.markwon.sample.simpleext.SimpleExtActivity; -import io.noties.markwon.sample.table.TableActivity; -import io.noties.markwon.sample.tasklist.TaskListActivity; - -public class MainActivity extends Activity { - - static { - Debug.init(new AndroidLogDebugOutput(true)); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - - // obtain an instance of Markwon - // here we are creating as core markwon (no additional plugins are registered) - final Markwon markwon = Markwon.create(this); - - final Adapt adapt = Adapt.create(); - - final List items = new ArrayList<>(); - final SampleItem.OnClickListener onClickListener = this::showSample; - for (Sample sample : Sample.values()) { - items.add(new SampleItem(sample, markwon, onClickListener)); - } - adapt.setItems(items); - - final RecyclerView recyclerView = findViewById(R.id.recycler_view); - recyclerView.setLayoutManager(new LinearLayoutManager(this)); - recyclerView.setHasFixedSize(true); - recyclerView.addItemDecoration(createSampleItemDecoration()); - recyclerView.setAdapter(adapt); - } - - @NonNull - private SampleItemDecoration createSampleItemDecoration() { - final float density = getResources().getDisplayMetrics().density; - return new SampleItemDecoration( - 0xffeeeeee, - (int) (24 * density + .5F), - (int) (1 * density + .5F), - 0xFFBDBDBD - ); - } - - private void showSample(@NonNull Sample item) { - startActivity(sampleItemIntent(this, item)); - } - - @VisibleForTesting - static Intent sampleItemIntent(@NonNull Context context, @NonNull Sample item) { - - final Class activity; - - switch (item) { - - case CORE: - activity = CoreActivity.class; - break; - - case BASIC_PLUGINS: - activity = BasicPluginsActivity.class; - break; - - case LATEX: - activity = LatexActivity.class; - break; - - case CUSTOM_EXTENSION: - activity = CustomExtensionActivity.class; - break; - - case RECYCLER: - activity = RecyclerActivity.class; - break; - - case HTML: - activity = HtmlActivity.class; - break; - - case SIMPLE_EXT: - activity = SimpleExtActivity.class; - break; - - case CUSTOM_EXTENSION_2: - activity = CustomExtensionActivity2.class; - break; - - case PRECOMPUTED_TEXT: - activity = PrecomputedActivity.class; - break; - - case PRECOMPUTED_FUTURE_TEXT: - activity = PrecomputedFutureActivity.class; - break; - - case EDITOR: - activity = EditorActivity.class; - break; - - case INLINE_PARSER: - activity = InlineParserActivity.class; - break; - - case HTML_DETAILS: - activity = HtmlDetailsActivity.class; - break; - - case TASK_LIST: - activity = TaskListActivity.class; - break; - - case IMAGES: - activity = ImagesActivity.class; - break; - - case REMOTE_VIEWS: - activity = NotificationActivity.class; - break; - - case TABLE: - activity = TableActivity.class; - break; - - default: - throw new IllegalStateException("No Activity is associated with sample-item: " + item); - } - - return new Intent(context, activity); - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/MenuOptions.java b/sample/src/main/java/io/noties/markwon/sample/MenuOptions.java deleted file mode 100644 index 6fb5b310..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/MenuOptions.java +++ /dev/null @@ -1,57 +0,0 @@ -package io.noties.markwon.sample; - -import android.view.Menu; -import android.view.MenuItem; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.LinkedHashMap; -import java.util.Map; - -public class MenuOptions { - - @NonNull - public static MenuOptions create() { - return new MenuOptions(); - } - - static class Option { - final String title; - final Runnable action; - - Option(@NonNull String title, @NonNull Runnable action) { - this.title = title; - this.action = action; - } - } - - // to preserve order use LinkedHashMap - private final Map actions = new LinkedHashMap<>(); - - @NonNull - public MenuOptions add(@NonNull String title, @NonNull Runnable action) { - actions.put(title, action); - return this; - } - - boolean onCreateOptionsMenu(Menu menu) { - if (!actions.isEmpty()) { - for (String key : actions.keySet()) { - menu.add(key); - } - return true; - } - return false; - } - - @Nullable - Option onOptionsItemSelected(MenuItem item) { - final String title = String.valueOf(item.getTitle()); - final Runnable action = actions.get(title); - if (action != null) { - return new Option(title, action); - } - return null; - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/Sample.java b/sample/src/main/java/io/noties/markwon/sample/Sample.java deleted file mode 100644 index 2dfcc0be..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/Sample.java +++ /dev/null @@ -1,52 +0,0 @@ -package io.noties.markwon.sample; - -import androidx.annotation.StringRes; - -public enum Sample { - - // all usages of markwon without plugins (parse, render, setMarkwon, etc) - CORE(R.string.sample_core), - - BASIC_PLUGINS(R.string.sample_basic_plugins), - - LATEX(R.string.sample_latex), - - CUSTOM_EXTENSION(R.string.sample_custom_extension), - - RECYCLER(R.string.sample_recycler), - - HTML(R.string.sample_html), - - SIMPLE_EXT(R.string.sample_simple_ext), - - CUSTOM_EXTENSION_2(R.string.sample_custom_extension_2), - - PRECOMPUTED_TEXT(R.string.sample_precomputed_text), - - PRECOMPUTED_FUTURE_TEXT(R.string.sample_precomputed_future_text), - - EDITOR(R.string.sample_editor), - - INLINE_PARSER(R.string.sample_inline_parser), - - HTML_DETAILS(R.string.sample_html_details), - - TASK_LIST(R.string.sample_task_list), - - IMAGES(R.string.sample_images), - - REMOTE_VIEWS(R.string.sample_remote_views), - - TABLE(R.string.sample_table); - - private final int textResId; - - Sample(@StringRes int textResId) { - this.textResId = textResId; - } - - @StringRes - public int textResId() { - return textResId; - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/SampleItem.java b/sample/src/main/java/io/noties/markwon/sample/SampleItem.java deleted file mode 100644 index aa71551b..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/SampleItem.java +++ /dev/null @@ -1,80 +0,0 @@ -package io.noties.markwon.sample; - -import android.text.Spanned; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import androidx.annotation.NonNull; - -import io.noties.adapt.Item; -import io.noties.markwon.Markwon; -import io.noties.markwon.utils.NoCopySpannableFactory; - -class SampleItem extends Item { - - interface OnClickListener { - void onClick(@NonNull Sample sample); - } - - private final Sample sample; - - private final Markwon markwon; - - private final OnClickListener onClickListener; - - // instance specific cache - private Spanned cache; - - SampleItem(@NonNull Sample sample, @NonNull Markwon markwon, @NonNull OnClickListener onClickListener) { - super(sample.ordinal()); - this.sample = sample; - this.markwon = markwon; - this.onClickListener = onClickListener; - } - - @NonNull - @Override - public SampleHolder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { - - final SampleHolder holder = new SampleHolder(inflater.inflate( - R.layout.adapt_sample_item, - parent, - false)); - - // set Spannable.Factory so when TextView will receive a new content - // it won't create new Spannable and copy all the spans but instead - // re-use existing Spannable thus improving performance - holder.textView.setSpannableFactory(NoCopySpannableFactory.getInstance()); - - return holder; - } - - @Override - public void render(@NonNull SampleHolder holder) { - - // retrieve an item from cache or create new one - // simple lazy loading pattern (cache on first call then re-use) - Spanned spanned = this.cache; - if (spanned == null) { - spanned = cache = markwon.toMarkdown( - holder.textView.getResources().getString(sample.textResId())); - } - - holder.textView.setText(spanned); - - holder.itemView.setOnClickListener(v -> onClickListener.onClick(sample)); - } - - static class SampleHolder extends Item.Holder { - - final TextView textView; - - SampleHolder(@NonNull View view) { - super(view); - - this.textView = requireView(R.id.text); - } - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/SampleItemDecoration.java b/sample/src/main/java/io/noties/markwon/sample/SampleItemDecoration.java deleted file mode 100644 index 3a3ad1d5..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/SampleItemDecoration.java +++ /dev/null @@ -1,98 +0,0 @@ -package io.noties.markwon.sample; - -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Rect; -import android.view.View; - -import androidx.annotation.ColorInt; -import androidx.annotation.Px; -import androidx.recyclerview.widget.RecyclerView; - -class SampleItemDecoration extends RecyclerView.ItemDecoration { - - private final Rect rect = new Rect(); - private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); - - private final int oddItemBackgroundColor; - - private final int bottomPadding; - - private final int dividerHeight; - private final int dividerColor; - - SampleItemDecoration( - @ColorInt int oddItemBackgroundColor, - @Px int bottomPadding, - @Px int dividerHeight, - @ColorInt int dividerColor) { - this.oddItemBackgroundColor = oddItemBackgroundColor; - this.bottomPadding = bottomPadding; - this.dividerHeight = dividerHeight; - this.dividerColor = dividerColor; - - paint.setStyle(Paint.Style.FILL); - } - - @Override - public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { - - // if bottom < parent.getBottom() -> draw bottom background - - paint.setColor(dividerColor); - - View view; - - // we will use this flag afterwards (if we will have to draw bottom background) - // so, if last item is even (no background) -> draw odd - // if last item is odd -> draw no background - // - // let's start with true, so if we have no items no background will be drawn - boolean isOdd = true; - - for (int i = 0, count = parent.getChildCount(); i < count; i++) { - - view = parent.getChildAt(i); - isOdd = parent.getChildAdapterPosition(view) % 2 != 0; - - // odd - if (isOdd) { - rect.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()); - paint.setColor(oddItemBackgroundColor); - c.drawRect(rect, paint); - - // set divider color back - paint.setColor(dividerColor); - } - - rect.set(0, view.getBottom(), c.getWidth(), view.getBottom() + dividerHeight); - c.drawRect(rect, paint); - } - - if (!isOdd && rect.bottom < parent.getBottom()) { - - paint.setColor(oddItemBackgroundColor); - - rect.set(0, rect.bottom, c.getWidth(), parent.getBottom()); - c.drawRect(rect, paint); - } - } - - @Override - public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { - - // divider to bottom - // + {if last} -> bottomPadding - - final int position = parent.getChildAdapterPosition(view); - - final RecyclerView.Adapter adapter = parent.getAdapter(); - final boolean isLast = adapter != null && position == adapter.getItemCount() - 1; - - final int bottom = isLast - ? bottomPadding + dividerHeight - : dividerHeight; - - outRect.set(0, 0, 0, bottom); - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/basicplugins/AnchorHeadingPlugin.java b/sample/src/main/java/io/noties/markwon/sample/basicplugins/AnchorHeadingPlugin.java deleted file mode 100644 index 2471cd4f..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/basicplugins/AnchorHeadingPlugin.java +++ /dev/null @@ -1,97 +0,0 @@ -package io.noties.markwon.sample.basicplugins; - -import android.text.Spannable; -import android.text.Spanned; -import android.view.View; -import android.widget.TextView; - -import androidx.annotation.NonNull; - -import io.noties.markwon.AbstractMarkwonPlugin; -import io.noties.markwon.LinkResolverDef; -import io.noties.markwon.MarkwonConfiguration; -import io.noties.markwon.core.spans.HeadingSpan; - -public class AnchorHeadingPlugin extends AbstractMarkwonPlugin { - - public interface ScrollTo { - void scrollTo(@NonNull TextView view, int top); - } - - private final ScrollTo scrollTo; - - AnchorHeadingPlugin(@NonNull ScrollTo scrollTo) { - this.scrollTo = scrollTo; - } - - @Override - public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { - builder.linkResolver(new AnchorLinkResolver(scrollTo)); - } - - @Override - public void afterSetText(@NonNull TextView textView) { - final Spannable spannable = (Spannable) textView.getText(); - // obtain heading spans - final HeadingSpan[] spans = spannable.getSpans(0, spannable.length(), HeadingSpan.class); - if (spans != null) { - for (HeadingSpan span : spans) { - final int start = spannable.getSpanStart(span); - final int end = spannable.getSpanEnd(span); - final int flags = spannable.getSpanFlags(span); - spannable.setSpan( - new AnchorSpan(createAnchor(spannable.subSequence(start, end))), - start, - end, - flags - ); - } - } - } - - private static class AnchorLinkResolver extends LinkResolverDef { - - private final ScrollTo scrollTo; - - AnchorLinkResolver(@NonNull ScrollTo scrollTo) { - this.scrollTo = scrollTo; - } - - @Override - public void resolve(@NonNull View view, @NonNull String link) { - if (link.startsWith("#")) { - final TextView textView = (TextView) view; - final Spanned spanned = (Spannable) textView.getText(); - final AnchorSpan[] spans = spanned.getSpans(0, spanned.length(), AnchorSpan.class); - if (spans != null) { - final String anchor = link.substring(1); - for (AnchorSpan span : spans) { - if (anchor.equals(span.anchor)) { - final int start = spanned.getSpanStart(span); - final int line = textView.getLayout().getLineForOffset(start); - final int top = textView.getLayout().getLineTop(line); - scrollTo.scrollTo(textView, top); - return; - } - } - } - } - super.resolve(view, link); - } - } - - private static class AnchorSpan { - final String anchor; - - AnchorSpan(@NonNull String anchor) { - this.anchor = anchor; - } - } - - @NonNull - public static String createAnchor(@NonNull CharSequence content) { - return String.valueOf(content) - .replaceAll("[^\\w]", "") - .toLowerCase(); - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/basicplugins/BasicPluginsActivity.java b/sample/src/main/java/io/noties/markwon/sample/basicplugins/BasicPluginsActivity.java deleted file mode 100644 index a7b2fd7d..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/basicplugins/BasicPluginsActivity.java +++ /dev/null @@ -1,480 +0,0 @@ -package io.noties.markwon.sample.basicplugins; - -import android.graphics.Color; -import android.net.Uri; -import android.os.Bundle; -import android.text.TextUtils; -import android.text.style.ForegroundColorSpan; -import android.view.View; -import android.widget.ScrollView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.commonmark.node.Heading; -import org.commonmark.node.Node; -import org.commonmark.node.Paragraph; - -import java.util.Collection; -import java.util.Collections; - -import io.noties.markwon.AbstractMarkwonPlugin; -import io.noties.markwon.BlockHandlerDef; -import io.noties.markwon.LinkResolverDef; -import io.noties.markwon.Markwon; -import io.noties.markwon.MarkwonConfiguration; -import io.noties.markwon.MarkwonSpansFactory; -import io.noties.markwon.MarkwonVisitor; -import io.noties.markwon.SoftBreakAddsNewLinePlugin; -import io.noties.markwon.core.CoreProps; -import io.noties.markwon.core.MarkwonTheme; -import io.noties.markwon.core.spans.LastLineSpacingSpan; -import io.noties.markwon.ext.tables.TablePlugin; -import io.noties.markwon.image.ImageItem; -import io.noties.markwon.image.ImagesPlugin; -import io.noties.markwon.image.SchemeHandler; -import io.noties.markwon.image.network.NetworkSchemeHandler; -import io.noties.markwon.movement.MovementMethodPlugin; -import io.noties.markwon.sample.ActivityWithMenuOptions; -import io.noties.markwon.sample.MenuOptions; -import io.noties.markwon.sample.R; - -public class BasicPluginsActivity extends ActivityWithMenuOptions { - - private TextView textView; - private ScrollView scrollView; - - @NonNull - @Override - public MenuOptions menuOptions() { - return MenuOptions.create() - .add("paragraphSpan", this::paragraphSpan) - .add("disableNode", this::disableNode) - .add("customizeTheme", this::customizeTheme) - .add("linkWithMovementMethod", this::linkWithMovementMethod) - .add("imagesPlugin", this::imagesPlugin) - .add("softBreakAddsSpace", this::softBreakAddsSpace) - .add("softBreakAddsNewLine", this::softBreakAddsNewLine) - .add("additionalSpacing", this::additionalSpacing) - .add("headingNoSpace", this::headingNoSpace) - .add("headingNoSpaceBlockHandler", this::headingNoSpaceBlockHandler) - .add("allBlocksNoForcedLine", this::allBlocksNoForcedLine) - .add("anchor", this::anchor) - .add("letterOrderedList", this::letterOrderedList) - .add("tableOfContents", this::tableOfContents) - .add("readMore", this::readMore); - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_text_view); - - textView = findViewById(R.id.text_view); - scrollView = findViewById(R.id.scroll_view); - - paragraphSpan(); -// -// disableNode(); -// -// customizeTheme(); -// -// linkWithMovementMethod(); -// -// imagesPlugin(); - } - - /** - * In order to apply paragraph spans a custom plugin should be created (CorePlugin will take care - * of everything else). - */ - private void paragraphSpan() { - - final String markdown = "# Hello!\n\nA paragraph?\n\nIt should be!"; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { - builder.setFactory(Paragraph.class, (configuration, props) -> - new ForegroundColorSpan(Color.GREEN)); - } - }) - .build(); - - markwon.setMarkdown(textView, markdown); - } - - /** - * To disable some nodes from rendering another custom plugin can be used - */ - private void disableNode() { - - final String markdown = "# Heading 1\n\n## Heading 2\n\n**other** content [here](#)"; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { - - // for example to disable rendering of heading: - // try commenting this out to see that otherwise headings will be rendered - builder.on(Heading.class, null); - - // same method can be used to override existing visitor by specifying - // a new NodeVisitor instance - } - }) - .build(); - - markwon.setMarkdown(textView, markdown); - } - - /** - * To customize core theme plugin can be used again - */ - private void customizeTheme() { - - final String markdown = "`A code` that is rendered differently\n\n```\nHello!\n```"; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureTheme(@NonNull MarkwonTheme.Builder builder) { - builder - .codeBackgroundColor(Color.BLACK) - .codeTextColor(Color.RED); - } - }) - .build(); - - markwon.setMarkdown(textView, markdown); - } - - /** - * MarkwonConfiguration contains these utilities: - *
    - *
  • SyntaxHighlight
  • - *
  • LinkSpan.Resolver
  • - *
  • ImageDestinationProcessor
  • - *
  • ImageSizeResolver
  • - *
- *

- * In order to customize them a custom plugin should be used - */ - private void linkWithMovementMethod() { - - final String markdown = "[a link without scheme](github.com)"; - - final Markwon markwon = Markwon.builder(this) - // please note that Markwon does not handle MovementMethod, - // so if your markdown has links your should apply MovementMethod manually - // or use MovementMethodPlugin (which uses system LinkMovementMethod by default) - .usePlugin(MovementMethodPlugin.create()) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { - // for example if specified destination has no scheme info, we will - // _assume_ that it's network request and append HTTPS scheme - builder.linkResolver(new LinkResolverDef() { - @Override - public void resolve(@NonNull View view, @NonNull String link) { - final String destination; - final Uri uri = Uri.parse(link); - if (TextUtils.isEmpty(uri.getScheme())) { - destination = "https://" + link; - } else { - destination = link; - } - super.resolve(view, destination); - } - }); - } - }) - .build(); - - markwon.setMarkdown(textView, markdown); - } - - /** - * Images configuration. Can be used with (or without) ImagesPlugin, which does some basic - * images handling (parsing markdown containing images, obtain an image from network - * file system or assets). Please note that - */ - private void imagesPlugin() { - - final String markdown = "![image](myownscheme://en.wikipedia.org/static/images/project-logos/enwiki-2x.png)"; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(ImagesPlugin.create()) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configure(@NonNull Registry registry) { - - // use registry.require to obtain a plugin, does also - // a runtime validation if this plugin is registered - registry.require(ImagesPlugin.class, plugin -> plugin.addSchemeHandler(new SchemeHandler() { - - // it's a sample only, most likely you won't need to - // use existing scheme-handler, this for demonstration purposes only - final NetworkSchemeHandler handler = NetworkSchemeHandler.create(); - - @NonNull - @Override - public ImageItem handle(@NonNull String raw, @NonNull Uri uri) { - final String url = raw.replace("myownscheme", "https"); - return handler.handle(url, Uri.parse(url)); - } - - @NonNull - @Override - public Collection supportedSchemes() { - return Collections.singleton("myownscheme"); - } - })); - } - }) - // or we can init plugin with this factory method -// .usePlugin(ImagesPlugin.create(plugin -> { -// plugin.addSchemeHandler(/**/) -// })) - .build(); - - markwon.setMarkdown(textView, markdown); - } - - private void softBreakAddsSpace() { - // default behavior - - final String md = "" + - "Hello there ->(line)\n(break)<- going on and on"; - - Markwon.create(this).setMarkdown(textView, md); - } - - private void softBreakAddsNewLine() { - // insert a new line when markdown has a soft break - - final Markwon markwon = Markwon.builder(this) - .usePlugin(SoftBreakAddsNewLinePlugin.create()) - .build(); - - final String md = "" + - "Hello there ->(line)\n(break)<- going on and on"; - - markwon.setMarkdown(textView, md); - } - - private void additionalSpacing() { - - // please note that bottom line (after 1 & 2 levels) will be drawn _AFTER_ padding - final int spacing = (int) (128 * getResources().getDisplayMetrics().density + .5F); - - final Markwon markwon = Markwon.builder(this) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureTheme(@NonNull MarkwonTheme.Builder builder) { - builder.headingBreakHeight(0); - } - - @Override - public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { - builder.appendFactory( - Heading.class, - (configuration, props) -> new LastLineSpacingSpan(spacing)); - } - }) - .build(); - - final String md = "" + - "# Title title title title title title title title title title \n\ntext text text text"; - - markwon.setMarkdown(textView, md); - } - - private void headingNoSpace() { - final Markwon markwon = Markwon.builder(this) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureTheme(@NonNull MarkwonTheme.Builder builder) { - builder.headingBreakHeight(0); - } - - @Override - public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { - builder.on(Heading.class, (visitor, heading) -> { - - visitor.ensureNewLine(); - - final int length = visitor.length(); - visitor.visitChildren(heading); - - CoreProps.HEADING_LEVEL.set(visitor.renderProps(), heading.getLevel()); - - visitor.setSpansForNodeOptional(heading, length); - - if (visitor.hasNext(heading)) { - visitor.ensureNewLine(); -// visitor.forceNewLine(); - } - }); - } - }) - .build(); - - final String md = "" + - "# Title title title title title title title title title title \n\ntext text text text"; - - markwon.setMarkdown(textView, md); - } - - private void headingNoSpaceBlockHandler() { - final Markwon markwon = Markwon.builder(this) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { - builder.blockHandler(new BlockHandlerDef() { - @Override - public void blockEnd(@NonNull MarkwonVisitor visitor, @NonNull Node node) { - if (node instanceof Heading) { - if (visitor.hasNext(node)) { - visitor.ensureNewLine(); - // ensure new line but do not force insert one - } - } else { - super.blockEnd(visitor, node); - } - } - }); - } - }) - .build(); - - final String md = "" + - "# Title title title title title title title title title title \n\ntext text text text"; - - markwon.setMarkdown(textView, md); - } - - private void allBlocksNoForcedLine() { - final MarkwonVisitor.BlockHandler blockHandler = new BlockHandlerDef() { - @Override - public void blockEnd(@NonNull MarkwonVisitor visitor, @NonNull Node node) { - if (visitor.hasNext(node)) { - visitor.ensureNewLine(); - } - } - }; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { - builder.blockHandler(blockHandler); - } - }) - .build(); - - final String md = "" + - "# Hello there!\n\n" + - "* a first\n" + - "* second\n" + - "- third\n" + - "* * nested one\n\n" + - "> block quote\n\n" + - "> > and nested one\n\n" + - "```java\n" + - "final int i = 0;\n" + - "```\n\n"; - - markwon.setMarkdown(textView, md); - } - - private void anchor() { - final String lorem = getString(R.string.lorem); - final String md = "" + - "Hello [there](#there)!\n\n\n" + - lorem + "\n\n" + - "# There!\n\n" + - lorem; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(new AnchorHeadingPlugin((view, top) -> scrollView.smoothScrollTo(0, top))) - .build(); - - markwon.setMarkdown(textView, md); - } - - private void letterOrderedList() { - // bullet list nested in ordered list renders letters instead of bullets - final String md = "" + - "1. Hello there!\n" + - "1. And here is how:\n" + - " - First\n" + - " - Second\n" + - " - Third\n" + - " 1. And first here\n\n"; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(new BulletListIsOrderedWithLettersWhenNestedPlugin()) - .build(); - - markwon.setMarkdown(textView, md); - } - - private void tableOfContents() { - final String lorem = getString(R.string.lorem); - final String md = "" + - "# First\n" + - "" + lorem + "\n\n" + - "# Second\n" + - "" + lorem + "\n\n" + - "## Second level\n\n" + - "" + lorem + "\n\n" + - "### Level 3\n\n" + - "" + lorem + "\n\n" + - "# First again\n" + - "" + lorem + "\n\n"; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(new TableOfContentsPlugin()) - .usePlugin(new AnchorHeadingPlugin((view, top) -> scrollView.smoothScrollTo(0, top))) - .build(); - - markwon.setMarkdown(textView, md); - } - -// private void code() { -// final String md = "" + -// "hello `there`!\n\n" + -// "so this, `is super duper long very very very long line that should be going further and further and further down` yep.\n\n" + -// "`okay`"; -// final Markwon markwon = Markwon.builder(this) -// .usePlugin(new AbstractMarkwonPlugin() { -// @Override -// public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { -// builder.setFactory(Code.class, new SpanFactory() { -// @Override -// public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { -// return new CodeTextView.CodeSpan(); -// } -// }); -// } -// }) -// .build(); -// markwon.setMarkdown(textView, md); -// } - - private void readMore() { - final String md = "" + - "Lorem **ipsum** ![dolor](https://avatars2.githubusercontent.com/u/30618885?s=460&v=4) sit amet, consectetur adipiscing elit. Morbi vitae enim ut sem aliquet ultrices. Nunc a accumsan orci. Suspendisse tortor ante, lacinia ac scelerisque sed, dictum eget metus. Morbi ante augue, tristique eget quam in, vestibulum rutrum lacus. Nulla aliquam auctor cursus. Nulla at lacus condimentum, viverra lacus eget, sollicitudin ex. Cras efficitur leo dui, sit amet rutrum tellus venenatis et. Sed in facilisis libero. Etiam ultricies, nulla ut venenatis tincidunt, tortor erat tristique ante, non aliquet massa arcu eget nisl. Etiam gravida erat ante, sit amet lobortis mauris commodo nec. Praesent vitae sodales quam. Vivamus condimentum porta suscipit. Donec posuere id felis ac scelerisque. Vestibulum lacinia et leo id lobortis. Sed vitae dolor nec ligula dapibus finibus vel eu libero. Nam tincidunt maximus elit, sit amet tincidunt lacus laoreet malesuada.\n\n" + - "here we ![are](https://avatars2.githubusercontent.com/u/30618885?s=460&v=4)"; - final Markwon markwon = Markwon.builder(this) - .usePlugin(ImagesPlugin.create()) - .usePlugin(TablePlugin.create(this)) - .usePlugin(new ReadMorePlugin()) - .build(); - markwon.setMarkdown(textView, md); - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/basicplugins/BulletListIsOrderedWithLettersWhenNestedPlugin.java b/sample/src/main/java/io/noties/markwon/sample/basicplugins/BulletListIsOrderedWithLettersWhenNestedPlugin.java deleted file mode 100644 index 26712de0..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/basicplugins/BulletListIsOrderedWithLettersWhenNestedPlugin.java +++ /dev/null @@ -1,156 +0,0 @@ -package io.noties.markwon.sample.basicplugins; - -import android.text.TextUtils; -import android.util.SparseIntArray; - -import androidx.annotation.NonNull; - -import org.commonmark.node.BulletList; -import org.commonmark.node.ListItem; -import org.commonmark.node.Node; -import org.commonmark.node.OrderedList; - -import io.noties.markwon.AbstractMarkwonPlugin; -import io.noties.markwon.MarkwonSpansFactory; -import io.noties.markwon.MarkwonVisitor; -import io.noties.markwon.Prop; -import io.noties.markwon.core.CoreProps; -import io.noties.markwon.core.spans.BulletListItemSpan; -import io.noties.markwon.core.spans.OrderedListItemSpan; - -public class BulletListIsOrderedWithLettersWhenNestedPlugin extends AbstractMarkwonPlugin { - - private static final Prop BULLET_LETTER = Prop.of("my-bullet-letter"); - - // or introduce some kind of synchronization if planning to use from multiple threads, - // for example via ThreadLocal - private final SparseIntArray bulletCounter = new SparseIntArray(); - - @Override - public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) { - // clear counter after render - bulletCounter.clear(); - } - - @Override - public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { - // NB that both ordered and bullet lists are represented - // by ListItem (must inspect parent to detect the type) - builder.on(ListItem.class, (visitor, listItem) -> { - // mimic original behaviour (copy-pasta from CorePlugin) - - final int length = visitor.length(); - - visitor.visitChildren(listItem); - - final Node parent = listItem.getParent(); - if (parent instanceof OrderedList) { - - final int start = ((OrderedList) parent).getStartNumber(); - - CoreProps.LIST_ITEM_TYPE.set(visitor.renderProps(), CoreProps.ListItemType.ORDERED); - CoreProps.ORDERED_LIST_ITEM_NUMBER.set(visitor.renderProps(), start); - - // after we have visited the children increment start number - final OrderedList orderedList = (OrderedList) parent; - orderedList.setStartNumber(orderedList.getStartNumber() + 1); - - } else { - CoreProps.LIST_ITEM_TYPE.set(visitor.renderProps(), CoreProps.ListItemType.BULLET); - - if (isBulletOrdered(parent)) { - // obtain current count value - final int count = currentBulletCountIn(parent); - BULLET_LETTER.set(visitor.renderProps(), createBulletLetter(count)); - // update current count value - setCurrentBulletCountIn(parent, count + 1); - } else { - CoreProps.BULLET_LIST_ITEM_LEVEL.set(visitor.renderProps(), listLevel(listItem)); - // clear letter info when regular bullet list is used - BULLET_LETTER.clear(visitor.renderProps()); - } - } - - visitor.setSpansForNodeOptional(listItem, length); - - if (visitor.hasNext(listItem)) { - visitor.ensureNewLine(); - } - }); - } - - @Override - public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { - builder.setFactory(ListItem.class, (configuration, props) -> { - final Object spans; - - if (CoreProps.ListItemType.BULLET == CoreProps.LIST_ITEM_TYPE.require(props)) { - final String letter = BULLET_LETTER.get(props); - if (!TextUtils.isEmpty(letter)) { - // NB, we are using OrderedListItemSpan here! - spans = new OrderedListItemSpan( - configuration.theme(), - letter - ); - } else { - spans = new BulletListItemSpan( - configuration.theme(), - CoreProps.BULLET_LIST_ITEM_LEVEL.require(props) - ); - } - } else { - - final String number = String.valueOf(CoreProps.ORDERED_LIST_ITEM_NUMBER.require(props)) - + "." + '\u00a0'; - - spans = new OrderedListItemSpan( - configuration.theme(), - number - ); - } - - return spans; - }); - } - - private int currentBulletCountIn(@NonNull Node parent) { - return bulletCounter.get(parent.hashCode(), 0); - } - - private void setCurrentBulletCountIn(@NonNull Node parent, int count) { - bulletCounter.put(parent.hashCode(), count); - } - - @NonNull - private static String createBulletLetter(int count) { - // or lower `a` - // `'u00a0` is non-breakable space char - return ((char) ('A' + count)) + ".\u00a0"; - } - - private static int listLevel(@NonNull Node node) { - int level = 0; - Node parent = node.getParent(); - while (parent != null) { - if (parent instanceof ListItem) { - level += 1; - } - parent = parent.getParent(); - } - return level; - } - - private static boolean isBulletOrdered(@NonNull Node node) { - node = node.getParent(); - while (node != null) { - if (node instanceof OrderedList) { - return true; - } - if (node instanceof BulletList) { - return false; - } - node = node.getParent(); - } - return false; - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/basicplugins/CodeTextView.java b/sample/src/main/java/io/noties/markwon/sample/basicplugins/CodeTextView.java deleted file mode 100644 index fe795b63..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/basicplugins/CodeTextView.java +++ /dev/null @@ -1,192 +0,0 @@ -package io.noties.markwon.sample.basicplugins; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.RectF; -import android.os.Build; -import android.text.Layout; -import android.text.Spanned; -import android.util.AttributeSet; -import android.view.View; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import io.noties.debug.Debug; - -@SuppressLint("AppCompatCustomView") -public class CodeTextView extends TextView { - - static class CodeSpan { - } - - private int paddingHorizontal; - private int paddingVertical; - - private float cornerRadius; - private float strokeWidth; - private int strokeColor; - private int backgroundColor; - - public CodeTextView(Context context) { - super(context); - init(context, null); - } - - public CodeTextView(Context context, AttributeSet attrs) { - super(context, attrs); - init(context, attrs); - } - - private void init(Context context, @Nullable AttributeSet attrs) { - paint.setColor(0xFFff0000); - paint.setStyle(Paint.Style.FILL); - } - - private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); - - @Override - protected void onDraw(Canvas canvas) { - final Layout layout = getLayout(); - if (layout != null) { - draw(this, canvas, layout); - } - super.onDraw(canvas); - } - - private void draw( - @NonNull View view, - @NonNull Canvas canvas, - @NonNull Layout layout - ) { - - final CharSequence cs = layout.getText(); - if (!(cs instanceof Spanned)) { - return; - } - final Spanned spanned = (Spanned) cs; - - final int save = canvas.save(); - try { - canvas.translate(view.getPaddingLeft(), view.getPaddingTop()); - - // TODO: block? - // TODO: we must remove _original_ spans - // TODO: cache (attach a listener?) - // TODO: editor? - - final CodeSpan[] spans = spanned.getSpans(0, spanned.length(), CodeSpan.class); - if (spans != null && spans.length > 0) { - for (CodeSpan span : spans) { - - final int startOffset = spanned.getSpanStart(span); - final int endOffset = spanned.getSpanEnd(span); - - final int startLine = layout.getLineForOffset(startOffset); - final int endLine = layout.getLineForOffset(endOffset); - - // do we need to round them? - final float left = layout.getPrimaryHorizontal(startOffset) - + (-1 * layout.getParagraphDirection(startLine) * paddingHorizontal); - - final float right = layout.getPrimaryHorizontal(endOffset) - + (layout.getParagraphDirection(endLine) * paddingHorizontal); - - final float top = getLineTop(layout, startLine, paddingVertical); - final float bottom = getLineBottom(layout, endLine, paddingVertical); - - Debug.i(new RectF(left, top, right, bottom).toShortString()); - - if (startLine == endLine) { - canvas.drawRect(left, top, right, bottom, paint); - } else { - // draw first line (start until the lineEnd) - // draw everything in-between (startLine - endLine) - // draw last line (lineStart until the end - - canvas.drawRect( - left, - top, - layout.getLineRight(startLine), - getLineBottom(layout, startLine, paddingVertical), - paint - ); - - for (int line = startLine + 1; line < endLine; line++) { - canvas.drawRect( - layout.getLineLeft(line), - getLineTop(layout, line, paddingVertical), - layout.getLineRight(line), - getLineBottom(layout, line, paddingVertical), - paint - ); - } - - canvas.drawRect( - layout.getLineLeft(endLine), - getLineTop(layout, endLine, paddingVertical), - right, - getLineBottom(layout, endLine, paddingVertical), - paint - ); - } - } - } - } finally { - canvas.restoreToCount(save); - } - } - - private static float getLineTop(@NonNull Layout layout, int line, float padding) { - float value = layout.getLineTop(line) - padding; - if (line == 0) { - value -= layout.getTopPadding(); - } - return value; - } - - private static float getLineBottom(@NonNull Layout layout, int line, float padding) { - float value = getLineBottomWithoutSpacing(layout, line) - padding; - if (line == (layout.getLineCount() - 1)) { - value -= layout.getBottomPadding(); - } - return value; - } - - private static float getLineBottomWithoutSpacing(@NonNull Layout layout, int line) { - final float value = layout.getLineBottom(line); - - final boolean isLastLineSpacingNotAdded = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; - final boolean isLastLine = line == (layout.getLineCount() - 1); - - final float lineBottomWithoutSpacing; - - final float lineSpacingExtra = layout.getSpacingAdd(); - final float lineSpacingMultiplier = layout.getSpacingMultiplier(); - - final boolean hasLineSpacing = Float.compare(lineSpacingExtra, .0F) != 0 - || Float.compare(lineSpacingMultiplier, 1F) != 0; - - if (!hasLineSpacing || isLastLine && isLastLineSpacingNotAdded) { - lineBottomWithoutSpacing = value; - } else { - final float extra; - if (Float.compare(lineSpacingMultiplier, 1F) != 0) { - final float lineHeight = getLineHeight(layout, line); - extra = lineHeight - (lineHeight - lineSpacingExtra) / lineSpacingMultiplier; - } else { - extra = lineSpacingExtra; - } - lineBottomWithoutSpacing = value - extra; - } - - return lineBottomWithoutSpacing; - } - - private static float getLineHeight(@NonNull Layout layout, int line) { - return layout.getLineTop(line + 1) - layout.getLineTop(line); - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/basicplugins/ReadMorePlugin.java b/sample/src/main/java/io/noties/markwon/sample/basicplugins/ReadMorePlugin.java deleted file mode 100644 index 793daf06..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/basicplugins/ReadMorePlugin.java +++ /dev/null @@ -1,180 +0,0 @@ -package io.noties.markwon.sample.basicplugins; - -import android.text.SpannableStringBuilder; -import android.text.Spanned; -import android.text.style.ClickableSpan; -import android.text.style.ReplacementSpan; -import android.view.View; -import android.widget.TextView; - -import androidx.annotation.NonNull; - -import io.noties.markwon.AbstractMarkwonPlugin; -import io.noties.markwon.ext.tables.TablePlugin; -import io.noties.markwon.image.ImagesPlugin; - -/** - * Read more plugin based on text length. It is easier to implement than lines (need to adjust - * last line to include expand/collapse text). - */ -public class ReadMorePlugin extends AbstractMarkwonPlugin { - - @SuppressWarnings("FieldCanBeLocal") - private final int maxLength = 150; - - @SuppressWarnings("FieldCanBeLocal") - private final String labelMore = "Show more..."; - - @SuppressWarnings("FieldCanBeLocal") - private final String labelLess = "...Show less"; - - @Override - public void configure(@NonNull Registry registry) { - // establish connections with all _dynamic_ content that your markdown supports, - // like images, tables, latex, etc - registry.require(ImagesPlugin.class); - registry.require(TablePlugin.class); - } - - @Override - public void afterSetText(@NonNull TextView textView) { - final CharSequence text = textView.getText(); - if (text.length() < maxLength) { - // everything is OK, no need to ellipsize) - return; - } - - final int breakAt = breakTextAt(text, 0, maxLength); - final CharSequence cs = createCollapsedString(text, 0, breakAt); - textView.setText(cs); - } - - @SuppressWarnings("SameParameterValue") - @NonNull - private CharSequence createCollapsedString(@NonNull CharSequence text, int start, int end) { - final SpannableStringBuilder builder = new SpannableStringBuilder(text, start, end); - - // NB! each table row is represented as a space character and new-line (so length=2) no - // matter how many characters are inside table cells - - // we can _clean_ this builder, for example remove all dynamic content (like images and tables, - // but keep them in full/expanded version) - //noinspection ConstantConditions - if (true) { - // it is an implementation detail but _mostly_ dynamic content is implemented as - // ReplacementSpans - final ReplacementSpan[] spans = builder.getSpans(0, builder.length(), ReplacementSpan.class); - if (spans != null) { - for (ReplacementSpan span : spans) { - builder.removeSpan(span); - } - } - - // NB! if there will be a table in _preview_ (collapsed) then each row will be represented as a - // space and new-line - trim(builder); - } - - final CharSequence fullText = createFullText(text, builder); - - builder.append(' '); - - final int length = builder.length(); - builder.append(labelMore); - builder.setSpan(new ClickableSpan() { - @Override - public void onClick(@NonNull View widget) { - ((TextView) widget).setText(fullText); - } - }, length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - - return builder; - } - - @NonNull - private CharSequence createFullText(@NonNull CharSequence text, @NonNull CharSequence collapsedText) { - // full/expanded text can also be different, - // for example it can be kept as-is and have no `collapse` functionality (once expanded cannot collapse) - // or can contain collapse feature - final CharSequence fullText; - //noinspection ConstantConditions - if (true) { - // for example let's allow collapsing - final SpannableStringBuilder builder = new SpannableStringBuilder(text); - builder.append(' '); - - final int length = builder.length(); - builder.append(labelLess); - builder.setSpan(new ClickableSpan() { - @Override - public void onClick(@NonNull View widget) { - ((TextView) widget).setText(collapsedText); - } - }, length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - - fullText = builder; - } else { - fullText = text; - } - - return fullText; - } - - private static void trim(@NonNull SpannableStringBuilder builder) { - - // NB! tables use `\u00a0` (non breaking space) which is not reported as white-space - - char c; - - for (int i = 0, length = builder.length(); i < length; i++) { - c = builder.charAt(i); - if (!Character.isWhitespace(c) && c != '\u00a0') { - if (i > 0) { - builder.replace(0, i, ""); - } - break; - } - } - - for (int i = builder.length() - 1; i >= 0; i--) { - c = builder.charAt(i); - if (!Character.isWhitespace(c) && c != '\u00a0') { - if (i < builder.length() - 1) { - builder.replace(i, builder.length(), ""); - } - break; - } - } - } - - // depending on your locale these can be different - // There is a BreakIterator in Android, but it is not reliable, still theoretically - // it should work better than hand-written and hardcoded rules - @SuppressWarnings("SameParameterValue") - private static int breakTextAt(@NonNull CharSequence text, int start, int max) { - - int last = start; - - // no need to check for _start_ (anyway will be ignored) - for (int i = start + max - 1; i > start; i--) { - final char c = text.charAt(i); - if (Character.isWhitespace(c) - || c == '.' - || c == ',' - || c == '!' - || c == '?') { - // include this special character - last = i - 1; - break; - } - } - - if (last <= start) { - // when used in subSequence last index is exclusive, - // so given max=150 would result in 0-149 subSequence - return start + max; - } - - return last; - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/basicplugins/TableOfContentsPlugin.java b/sample/src/main/java/io/noties/markwon/sample/basicplugins/TableOfContentsPlugin.java deleted file mode 100644 index c172ec7e..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/basicplugins/TableOfContentsPlugin.java +++ /dev/null @@ -1,115 +0,0 @@ -package io.noties.markwon.sample.basicplugins; - -import androidx.annotation.NonNull; - -import org.commonmark.node.AbstractVisitor; -import org.commonmark.node.BulletList; -import org.commonmark.node.CustomBlock; -import org.commonmark.node.Heading; -import org.commonmark.node.Link; -import org.commonmark.node.ListItem; -import org.commonmark.node.Node; -import org.commonmark.node.Text; - -import io.noties.markwon.AbstractMarkwonPlugin; -import io.noties.markwon.MarkwonVisitor; -import io.noties.markwon.core.SimpleBlockNodeVisitor; - -public class TableOfContentsPlugin extends AbstractMarkwonPlugin { - @Override - public void configure(@NonNull Registry registry) { - // just to make it explicit - registry.require(AnchorHeadingPlugin.class); - } - - @Override - public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { - builder.on(TableOfContentsBlock.class, new SimpleBlockNodeVisitor()); - } - - @Override - public void beforeRender(@NonNull Node node) { - - // custom block to hold TOC - final TableOfContentsBlock block = new TableOfContentsBlock(); - - // create TOC title - { - final Text text = new Text("Table of contents"); - final Heading heading = new Heading(); - // important one - set TOC heading level - heading.setLevel(1); - heading.appendChild(text); - block.appendChild(heading); - } - - final HeadingVisitor visitor = new HeadingVisitor(block); - node.accept(visitor); - - // make it the very first node in rendered markdown - node.prependChild(block); - } - - private static class HeadingVisitor extends AbstractVisitor { - - private final BulletList bulletList = new BulletList(); - private final StringBuilder builder = new StringBuilder(); - private boolean isInsideHeading; - - HeadingVisitor(@NonNull Node node) { - node.appendChild(bulletList); - } - - @Override - public void visit(Heading heading) { - this.isInsideHeading = true; - try { - // reset build from previous content - builder.setLength(0); - - // obtain level (can additionally filter by level, to skip lower ones) - final int level = heading.getLevel(); - - // build heading title - visitChildren(heading); - - // initial list item - final ListItem listItem = new ListItem(); - - Node parent = listItem; - Node node = listItem; - - for (int i = 1; i < level; i++) { - final ListItem li = new ListItem(); - final BulletList bulletList = new BulletList(); - bulletList.appendChild(li); - parent.appendChild(bulletList); - parent = li; - node = li; - } - - final String content = builder.toString(); - final Link link = new Link("#" + AnchorHeadingPlugin.createAnchor(content), null); - final Text text = new Text(content); - link.appendChild(text); - node.appendChild(link); - bulletList.appendChild(listItem); - - - } finally { - isInsideHeading = false; - } - } - - @Override - public void visit(Text text) { - // can additionally check if we are building heading (to skip all other texts) - if (isInsideHeading) { - builder.append(text.getLiteral()); - } - } - } - - private static class TableOfContentsBlock extends CustomBlock { - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/core/CoreActivity.java b/sample/src/main/java/io/noties/markwon/sample/core/CoreActivity.java deleted file mode 100644 index 6251181e..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/core/CoreActivity.java +++ /dev/null @@ -1,279 +0,0 @@ -package io.noties.markwon.sample.core; - -import android.os.Bundle; -import android.text.Spanned; -import android.util.Log; -import android.view.View; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.commonmark.node.Block; -import org.commonmark.node.BlockQuote; -import org.commonmark.node.Link; -import org.commonmark.node.Node; -import org.commonmark.parser.Parser; - -import java.util.Set; - -import io.noties.markwon.AbstractMarkwonPlugin; -import io.noties.markwon.LinkResolver; -import io.noties.markwon.Markwon; -import io.noties.markwon.MarkwonSpansFactory; -import io.noties.markwon.core.CorePlugin; -import io.noties.markwon.core.CoreProps; -import io.noties.markwon.core.MarkwonTheme; -import io.noties.markwon.core.spans.LinkSpan; -import io.noties.markwon.movement.MovementMethodPlugin; -import io.noties.markwon.sample.ActivityWithMenuOptions; -import io.noties.markwon.sample.MenuOptions; -import io.noties.markwon.sample.R; - -public class CoreActivity extends ActivityWithMenuOptions { - - private TextView textView; - - @NonNull - @Override - public MenuOptions menuOptions() { - return MenuOptions.create() - .add("simple", this::simple) - .add("toast", this::toast) - .add("alreadyParsed", this::alreadyParsed) - .add("enabledBlockTypes", this::enabledBlockTypes) - .add("implicitMovementMethod", this::implicitMovementMethod) - .add("explicitMovementMethod", this::explicitMovementMethod) - .add("explicitMovementMethodPlugin", this::explicitMovementMethodPlugin) - .add("linkTitle", this::linkTitle); - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_text_view); - - textView = findViewById(R.id.text_view); - -// step_1(); - - simple(); - -// toast(); -// -// alreadyParsed(); - } - - /** - * Create a simple instance of Markwon with only Core plugin registered - * this will handle all _natively_ supported by commonmark-java nodes: - *

    - *
  • StrongEmphasis
  • - *
  • Emphasis
  • - *
  • BlockQuote
  • - *
  • Code
  • - *
  • FencedCodeBlock
  • - *
  • IndentedCodeBlock
  • - *
  • ListItem (bullet-list and ordered list
  • - *
  • Heading
  • - *
  • Link
  • - *
  • ThematicBreak
  • - *
  • Paragraph (please note that there is no default span for a paragraph registered)
  • - *
- *

- * and basic core functionality: - *

    - *
  • Append text
  • - *
  • Insert new lines (soft and hard breaks)
  • - *
- */ - private void step_1() { - - // short call - final Markwon markwon = Markwon.create(this); - - // this is the same as calling - final Markwon markwon2 = Markwon.builder(this) - .usePlugin(CorePlugin.create()) - .build(); - } - - /** - * To simply apply raw (non-parsed) markdown call {@link Markwon#setMarkdown(TextView, String)} - */ - private void simple() { - - // this is raw markdown - final String markdown = "Hello **markdown**!"; - - final Markwon markwon = Markwon.create(this); - - // this will parse raw markdown and set parsed content to specified TextView - markwon.setMarkdown(textView, markdown); - } - - /** - * To apply markdown in a different context (other than textView) use {@link Markwon#toMarkdown(String)} - *

- * Please note that some features won't work unless they are used in a TextView context. For example - * there might be misplaced ordered lists (ordered list must have TextPaint in order to properly measure - * its number). But also images and tables (they belong to independent modules now). Images and tables - * are using some work-arounds in order to be displayed in relatively limited context without proper way - * of invalidation. But if a Toast for example is created with a custom view - * ({@code new Toast(this).setView(...) }) and has access to a TextView everything should work. - */ - private void toast() { - - final String markdown = "*Toast* __here__!\n\n> And a quote!"; - - final Markwon markwon = Markwon.create(this); - - final Spanned spanned = markwon.toMarkdown(markdown); - - Toast.makeText(this, spanned, Toast.LENGTH_LONG).show(); - } - - /** - * To apply already parsed markdown use {@link Markwon#setParsedMarkdown(TextView, Spanned)} - */ - private void alreadyParsed() { - - final String markdown = "This **is** pre-parsed [markdown](#)"; - - final Markwon markwon = Markwon.create(this); - - // parse markdown to obtain a Node - final Node node = markwon.parse(markdown); - - // create a spanned content from parsed node - final Spanned spanned = markwon.render(node); - - // apply parsed markdown - markwon.setParsedMarkdown(textView, spanned); - } - - private void enabledBlockTypes() { - - final String md = "" + - "# Head\n\n" + - "> and disabled quote\n\n" + - "```\n" + - "yep\n" + - "```"; - - final Set> blocks = CorePlugin.enabledBlockTypes(); - blocks.remove(BlockQuote.class); - - final Markwon markwon = Markwon.builder(this) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureParser(@NonNull Parser.Builder builder) { - builder.enabledBlockTypes(blocks); - } - }) - .build(); - - markwon.setMarkdown(textView, md); - } - - private void implicitMovementMethod() { - // by default a LinkMovementMethod is applied automatically, so links are clickable - - final String md = "[0 link](#) here"; - - final Markwon markwon = Markwon.create(this); - - markwon.setMarkdown(textView, md); - } - - private void explicitMovementMethod() { - // NB! as movement method is set from other methods we _explicitly_ clear it - textView.setMovementMethod(null); - - // by default Markwon will set a LinkMovementMethod on a TextView if it is missing - // to control that `hasExplicitMovementMethod` can be used - final String md = "[1 link](#) here"; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configure(@NonNull Registry registry) { - // Markwon **won't** set implicit movement method - // thus making the link in markdown input not clickable - registry.require(CorePlugin.class) - .hasExplicitMovementMethod(true); - } - }) - .build(); - - markwon.setMarkdown(textView, md); - } - - private void explicitMovementMethodPlugin() { - // additionally special MovementMethodPlugin.none() can be used to control `hasExplicitMovementMethod` - - final String md = "[2 link](#) here"; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(MovementMethodPlugin.none()) - .build(); - - markwon.setMarkdown(textView, md); - } - - private void linkTitle() { - final String md = "" + - "# Links\n\n" + - "[link title](#)"; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { - builder.setFactory(Link.class, (configuration, props) -> - new ClickSelfSpan( - configuration.theme(), - CoreProps.LINK_DESTINATION.require(props), - configuration.linkResolver() - ) - ); - } - }) - .build(); - - markwon.setMarkdown(textView, md); - } - - private static class ClickSelfSpan extends LinkSpan { - - ClickSelfSpan( - @NonNull MarkwonTheme theme, - @NonNull String link, - @NonNull LinkResolver resolver) { - super(theme, link, resolver); - } - - @Override - public void onClick(View widget) { - Log.e("CLICK", "title: '" + linkTitle(widget) + "'"); - super.onClick(widget); - } - - @Nullable - private CharSequence linkTitle(@NonNull View widget) { - if (!(widget instanceof TextView)) { - return null; - } - final Spanned spanned = (Spanned) ((TextView) widget).getText(); - final int start = spanned.getSpanStart(this); - final int end = spanned.getSpanEnd(this); - - if (start < 0 || end < 0) { - return null; - } - - return spanned.subSequence(start, end); - } - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/customextension/CustomExtensionActivity.java b/sample/src/main/java/io/noties/markwon/sample/customextension/CustomExtensionActivity.java deleted file mode 100644 index 99bd6d4c..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/customextension/CustomExtensionActivity.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.noties.markwon.sample.customextension; - -import android.app.Activity; -import android.os.Bundle; -import android.widget.TextView; - -import androidx.annotation.Nullable; - -import io.noties.markwon.Markwon; -import io.noties.markwon.sample.R; - -public class CustomExtensionActivity extends Activity { - - // please note that this sample won't work on a device with SDK level < 21 - // as we are using vector drawables for the sake of brevity. Other than resources - // used, this is fully functional sample on all SDK levels - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_text_view); - - final TextView textView = findViewById(R.id.text_view); - - // note that we haven't registered CorePlugin, as it's the only one that can be - // implicitly deducted and added automatically. All other plugins require explicit - // `usePlugin` call - final Markwon markwon = Markwon.builder(this) - // try commenting out this line to see runtime dependency resolution -// .usePlugin(ImagesPlugin.create(this)) - .usePlugin(IconPlugin.create(IconSpanProvider.create(this, 0))) - .build(); - - markwon.setMarkdown(textView, getString(R.string.input)); - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/customextension/IconGroupNode.java b/sample/src/main/java/io/noties/markwon/sample/customextension/IconGroupNode.java deleted file mode 100644 index 40868396..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/customextension/IconGroupNode.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.noties.markwon.sample.customextension; - -import org.commonmark.node.CustomNode; - -@SuppressWarnings("WeakerAccess") -public class IconGroupNode extends CustomNode { - -} diff --git a/sample/src/main/java/io/noties/markwon/sample/customextension/IconNode.java b/sample/src/main/java/io/noties/markwon/sample/customextension/IconNode.java deleted file mode 100644 index 92447920..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/customextension/IconNode.java +++ /dev/null @@ -1,61 +0,0 @@ -package io.noties.markwon.sample.customextension; - -import androidx.annotation.NonNull; - -import org.commonmark.node.CustomNode; -import org.commonmark.node.Delimited; - -@SuppressWarnings("WeakerAccess") -public class IconNode extends CustomNode implements Delimited { - - public static final char DELIMITER = '@'; - - public static final String DELIMITER_STRING = "" + DELIMITER; - - - private final String name; - - private final String color; - - private final String size; - - public IconNode(@NonNull String name, @NonNull String color, @NonNull String size) { - this.name = name; - this.color = color; - this.size = size; - } - - @NonNull - public String name() { - return name; - } - - @NonNull - public String color() { - return color; - } - - @NonNull - public String size() { - return size; - } - - @Override - public String getOpeningDelimiter() { - return DELIMITER_STRING; - } - - @Override - public String getClosingDelimiter() { - return DELIMITER_STRING; - } - - @Override - public String toString() { - return "IconNode{" + - "name='" + name + '\'' + - ", color='" + color + '\'' + - ", size='" + size + '\'' + - '}'; - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/customextension/IconPlugin.java b/sample/src/main/java/io/noties/markwon/sample/customextension/IconPlugin.java deleted file mode 100644 index 53c94571..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/customextension/IconPlugin.java +++ /dev/null @@ -1,59 +0,0 @@ -package io.noties.markwon.sample.customextension; - -import android.text.TextUtils; - -import androidx.annotation.NonNull; - -import org.commonmark.parser.Parser; - -import io.noties.markwon.AbstractMarkwonPlugin; -import io.noties.markwon.MarkwonVisitor; - -public class IconPlugin extends AbstractMarkwonPlugin { - - @NonNull - public static IconPlugin create(@NonNull IconSpanProvider iconSpanProvider) { - return new IconPlugin(iconSpanProvider); - } - - private final IconSpanProvider iconSpanProvider; - - IconPlugin(@NonNull IconSpanProvider iconSpanProvider) { - this.iconSpanProvider = iconSpanProvider; - } - - @Override - public void configureParser(@NonNull Parser.Builder builder) { - builder.customDelimiterProcessor(IconProcessor.create()); - } - - @Override - public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { - builder.on(IconNode.class, new MarkwonVisitor.NodeVisitor() { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull IconNode iconNode) { - - final String name = iconNode.name(); - final String color = iconNode.color(); - final String size = iconNode.size(); - - if (!TextUtils.isEmpty(name) - && !TextUtils.isEmpty(color) - && !TextUtils.isEmpty(size)) { - - final int length = visitor.length(); - - visitor.builder().append(name); - visitor.setSpans(length, iconSpanProvider.provide(name, color, size)); - visitor.builder().append(' '); - } - } - }); - } - - @NonNull - @Override - public String processMarkdown(@NonNull String markdown) { - return IconProcessor.prepare(markdown); - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/customextension/IconProcessor.java b/sample/src/main/java/io/noties/markwon/sample/customextension/IconProcessor.java deleted file mode 100644 index 484575fb..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/customextension/IconProcessor.java +++ /dev/null @@ -1,161 +0,0 @@ -package io.noties.markwon.sample.customextension; - -import android.text.TextUtils; - -import androidx.annotation.NonNull; - -import org.commonmark.node.Node; -import org.commonmark.node.Text; -import org.commonmark.parser.delimiter.DelimiterProcessor; -import org.commonmark.parser.delimiter.DelimiterRun; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -@SuppressWarnings("WeakerAccess") -public class IconProcessor implements DelimiterProcessor { - - @NonNull - public static IconProcessor create() { - return new IconProcessor(); - } - - // ic-home-black-24 - private static final Pattern PATTERN = Pattern.compile("ic-(\\w+)-(\\w+)-(\\d+)"); - - private static final String TO_FIND = IconNode.DELIMITER_STRING + "ic-"; - - /** - * Should be used when input string does not wrap icon definition with `@` from both ends. - * So, `@ic-home-white-24` would become `@ic-home-white-24@`. This way parsing is easier - * and more predictable (cannot specify multiple ending delimiters, as we would require them: - * space, newline, end of a document, and a lot of more) - * - * @param input to process - * @return processed string - * @see #prepare(StringBuilder) - */ - @NonNull - public static String prepare(@NonNull String input) { - final StringBuilder builder = new StringBuilder(input); - prepare(builder); - return builder.toString(); - } - - public static void prepare(@NonNull StringBuilder builder) { - - int start = builder.indexOf(TO_FIND); - int end; - - while (start > -1) { - - end = iconDefinitionEnd(start + TO_FIND.length(), builder); - - // if we match our pattern, append `@` else ignore - if (iconDefinitionValid(builder.subSequence(start + 1, end))) { - builder.insert(end, '@'); - } - - // move to next - start = builder.indexOf(TO_FIND, end); - } - } - - @Override - public char getOpeningCharacter() { - return IconNode.DELIMITER; - } - - @Override - public char getClosingCharacter() { - return IconNode.DELIMITER; - } - - @Override - public int getMinLength() { - return 1; - } - - @Override - public int getDelimiterUse(DelimiterRun opener, DelimiterRun closer) { - return opener.length() >= 1 && closer.length() >= 1 ? 1 : 0; - } - - @Override - public void process(Text opener, Text closer, int delimiterUse) { - - final IconGroupNode iconGroupNode = new IconGroupNode(); - - final Node next = opener.getNext(); - - boolean handled = false; - - // process only if we have exactly one Text node - if (next instanceof Text && next.getNext() == closer) { - - final String text = ((Text) next).getLiteral(); - - if (!TextUtils.isEmpty(text)) { - - // attempt to match - final Matcher matcher = PATTERN.matcher(text); - if (matcher.matches()) { - final IconNode iconNode = new IconNode( - matcher.group(1), - matcher.group(2), - matcher.group(3) - ); - iconGroupNode.appendChild(iconNode); - next.unlink(); - handled = true; - } - } - } - - if (!handled) { - - // restore delimiters if we didn't match - - iconGroupNode.appendChild(new Text(IconNode.DELIMITER_STRING)); - - Node node; - for (Node tmp = opener.getNext(); tmp != null && tmp != closer; tmp = node) { - node = tmp.getNext(); - // append a child anyway - iconGroupNode.appendChild(tmp); - } - - iconGroupNode.appendChild(new Text(IconNode.DELIMITER_STRING)); - } - - opener.insertBefore(iconGroupNode); - } - - private static int iconDefinitionEnd(int index, @NonNull StringBuilder builder) { - - // all spaces, new lines, non-words or digits, - - char c; - - int end = -1; - for (int i = index; i < builder.length(); i++) { - c = builder.charAt(i); - if (Character.isWhitespace(c) - || !(Character.isLetterOrDigit(c) || c == '-' || c == '_')) { - end = i; - break; - } - } - - if (end == -1) { - end = builder.length(); - } - - return end; - } - - private static boolean iconDefinitionValid(@NonNull CharSequence cs) { - final Matcher matcher = PATTERN.matcher(cs); - return matcher.matches(); - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/customextension/IconSpan.java b/sample/src/main/java/io/noties/markwon/sample/customextension/IconSpan.java deleted file mode 100644 index acc2096f..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/customextension/IconSpan.java +++ /dev/null @@ -1,78 +0,0 @@ -package io.noties.markwon.sample.customextension; - -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.text.style.ReplacementSpan; - -import androidx.annotation.IntDef; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -@SuppressWarnings("WeakerAccess") -public class IconSpan extends ReplacementSpan { - - @IntDef({ALIGN_BOTTOM, ALIGN_BASELINE, ALIGN_CENTER}) - @Retention(RetentionPolicy.CLASS) - @interface Alignment { - } - - public static final int ALIGN_BOTTOM = 0; - public static final int ALIGN_BASELINE = 1; - public static final int ALIGN_CENTER = 2; // will only center if drawable height is less than text line height - - - private final Drawable drawable; - - private final int alignment; - - public IconSpan(@NonNull Drawable drawable, @Alignment int alignment) { - this.drawable = drawable; - this.alignment = alignment; - if (drawable.getBounds().isEmpty()) { - drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); - } - } - - @Override - public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm) { - - final Rect rect = drawable.getBounds(); - - if (fm != null) { - fm.ascent = -rect.bottom; - fm.descent = 0; - - fm.top = fm.ascent; - fm.bottom = 0; - } - - return rect.right; - } - - @Override - public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) { - - final int b = bottom - drawable.getBounds().bottom; - - final int save = canvas.save(); - try { - final int translationY; - if (ALIGN_CENTER == alignment) { - translationY = b - ((bottom - top - drawable.getBounds().height()) / 2); - } else if (ALIGN_BASELINE == alignment) { - translationY = b - paint.getFontMetricsInt().descent; - } else { - translationY = b; - } - canvas.translate(x, translationY); - drawable.draw(canvas); - } finally { - canvas.restoreToCount(save); - } - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/customextension/IconSpanProvider.java b/sample/src/main/java/io/noties/markwon/sample/customextension/IconSpanProvider.java deleted file mode 100644 index 24f542e8..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/customextension/IconSpanProvider.java +++ /dev/null @@ -1,68 +0,0 @@ -package io.noties.markwon.sample.customextension; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.drawable.Drawable; -import android.os.Build; - -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; - -@SuppressWarnings("WeakerAccess") -public abstract class IconSpanProvider { - - @SuppressWarnings("SameParameterValue") - @NonNull - public static IconSpanProvider create(@NonNull Context context, @DrawableRes int fallBack) { - return new Impl(context, fallBack); - } - - - @NonNull - public abstract IconSpan provide(@NonNull String name, @NonNull String color, @NonNull String size); - - - private static class Impl extends IconSpanProvider { - - private static final boolean IS_L = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; - - private final Context context; - private final Resources resources; - private final int fallBack; - - Impl(@NonNull Context context, @DrawableRes int fallBack) { - this.context = context; - this.resources = context.getResources(); - this.fallBack = fallBack; - } - - @NonNull - @Override - public IconSpan provide(@NonNull String name, @NonNull String color, @NonNull String size) { - final String resName = iconName(name, color, size); - int resId = resources.getIdentifier(resName, "drawable", context.getPackageName()); - if (resId == 0) { - resId = fallBack; - } - return new IconSpan(getDrawable(resId), IconSpan.ALIGN_CENTER); - } - - - @NonNull - private static String iconName(@NonNull String name, @NonNull String color, @NonNull String size) { - return "ic_" + name + "_" + color + "_" + size + "dp"; - } - - @NonNull - private Drawable getDrawable(int resId) { - final Drawable drawable; - if (IS_L) { - drawable = context.getDrawable(resId); - } else { - drawable = resources.getDrawable(resId); - } - //noinspection ConstantConditions - return drawable; - } - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/customextension2/CustomExtensionActivity2.java b/sample/src/main/java/io/noties/markwon/sample/customextension2/CustomExtensionActivity2.java deleted file mode 100644 index cd286198..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/customextension2/CustomExtensionActivity2.java +++ /dev/null @@ -1,221 +0,0 @@ -package io.noties.markwon.sample.customextension2; - -import android.os.Bundle; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.commonmark.node.Link; -import org.commonmark.node.Node; -import org.commonmark.parser.InlineParserFactory; -import org.commonmark.parser.Parser; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import io.noties.markwon.AbstractMarkwonPlugin; -import io.noties.markwon.Markwon; -import io.noties.markwon.MarkwonConfiguration; -import io.noties.markwon.MarkwonVisitor; -import io.noties.markwon.RenderProps; -import io.noties.markwon.SpannableBuilder; -import io.noties.markwon.core.CorePlugin; -import io.noties.markwon.core.CoreProps; -import io.noties.markwon.inlineparser.InlineProcessor; -import io.noties.markwon.inlineparser.MarkwonInlineParser; -import io.noties.markwon.sample.ActivityWithMenuOptions; -import io.noties.markwon.sample.MenuOptions; -import io.noties.markwon.sample.R; - -public class CustomExtensionActivity2 extends ActivityWithMenuOptions { - - private static final String MD = "" + - "# Custom Extension 2\n" + - "\n" + - "This is an issue #1\n" + - "Done by @noties"; - - private TextView textView; - - @NonNull - @Override - public MenuOptions menuOptions() { - return MenuOptions.create() - .add("text_added", this::text_added) - .add("inline_parsing", this::inline_parsing); - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_text_view); - - textView = findViewById(R.id.text_view); - - // let's look for github special links: - // * `#1` - an issue or a pull request - // * `@user` link to a user - -// inline_parsing(textView, md); - - text_added(); - } - - private void text_added() { - - final Markwon markwon = Markwon.builder(this) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configure(@NonNull Registry registry) { - registry.require(CorePlugin.class, corePlugin -> - corePlugin.addOnTextAddedListener(new GithubLinkifyRegexTextAddedListener())); - } - }) - .build(); - - markwon.setMarkdown(textView, MD); - } - - private void inline_parsing() { - - final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder() - // include all current defaults (otherwise will be empty - contain only our inline-processors) - // included by default, to create factory-builder without defaults call `factoryBuilderNoDefaults` -// .includeDefaults() - .addInlineProcessor(new IssueInlineProcessor()) - .addInlineProcessor(new UserInlineProcessor()) - .build(); - - final Markwon markwon = Markwon.builder(this) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureParser(@NonNull Parser.Builder builder) { - builder.inlineParserFactory(inlineParserFactory); - } - }) - .build(); - - markwon.setMarkdown(textView, MD); - } - - private static class IssueInlineProcessor extends InlineProcessor { - - private static final Pattern RE = Pattern.compile("\\d+"); - - @Override - public char specialCharacter() { - return '#'; - } - - @Override - protected Node parse() { - final String id = match(RE); - if (id != null) { - final Link link = new Link(createIssueOrPullRequestLinkDestination(id), null); - link.appendChild(text("#" + id)); - return link; - } - return null; - } - - @NonNull - private static String createIssueOrPullRequestLinkDestination(@NonNull String id) { - return "https://github.com/noties/Markwon/issues/" + id; - } - } - - private static class UserInlineProcessor extends InlineProcessor { - - private static final Pattern RE = Pattern.compile("\\w+"); - - @Override - public char specialCharacter() { - return '@'; - } - - @Override - protected Node parse() { - final String user = match(RE); - if (user != null) { - final Link link = new Link(createUserLinkDestination(user), null); - link.appendChild(text("@" + user)); - return link; - } - return null; - } - - @NonNull - private static String createUserLinkDestination(@NonNull String user) { - return "https://github.com/" + user; - } - } - - private static class GithubLinkifyRegexTextAddedListener implements CorePlugin.OnTextAddedListener { - - private static final Pattern PATTERN = Pattern.compile("((#\\d+)|(@\\w+))", Pattern.MULTILINE); - - @Override - public void onTextAdded(@NonNull MarkwonVisitor visitor, @NonNull String text, int start) { - - final Matcher matcher = PATTERN.matcher(text); - - String value; - String url; - int index; - - while (matcher.find()) { - - value = matcher.group(1); - - // detect which one it is - if ('#' == value.charAt(0)) { - url = createIssueOrPullRequestLink(value.substring(1)); - } else { - url = createUserLink(value.substring(1)); - } - - // it's important to use `start` value (represents start-index of `text` in the visitor) - index = start + matcher.start(); - - setLink(visitor, url, index, index + value.length()); - } - } - - @NonNull - private String createIssueOrPullRequestLink(@NonNull String number) { - // issues and pull-requests on github follow the same pattern and we - // cannot know for sure which one it is, but if we use issues for all types, - // github will automatically redirect to pull-request if it's the one which is opened - return "https://github.com/noties/Markwon/issues/" + number; - } - - @NonNull - private String createUserLink(@NonNull String user) { - return "https://github.com/" + user; - } - - private void setLink(@NonNull MarkwonVisitor visitor, @NonNull String destination, int start, int end) { - - // might a simpler one, but it doesn't respect possible changes to links -// visitor.builder().setSpan( -// new LinkSpan(visitor.configuration().theme(), destination, visitor.configuration().linkResolver()), -// start, -// end -// ); - - // use default handlers for links - final MarkwonConfiguration configuration = visitor.configuration(); - final RenderProps renderProps = visitor.renderProps(); - - CoreProps.LINK_DESTINATION.set(renderProps, destination); - - SpannableBuilder.setSpans( - visitor.builder(), - configuration.spansFactory().require(Link.class).getSpans(configuration, renderProps), - start, - end - ); - } - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/editor/BlockQuoteEditHandler.java b/sample/src/main/java/io/noties/markwon/sample/editor/BlockQuoteEditHandler.java deleted file mode 100644 index 704d40e3..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/editor/BlockQuoteEditHandler.java +++ /dev/null @@ -1,50 +0,0 @@ -package io.noties.markwon.sample.editor; - -import android.text.Editable; -import android.text.Spanned; - -import androidx.annotation.NonNull; - -import io.noties.markwon.Markwon; -import io.noties.markwon.core.MarkwonTheme; -import io.noties.markwon.core.spans.BlockQuoteSpan; -import io.noties.markwon.editor.EditHandler; -import io.noties.markwon.editor.PersistedSpans; - -class BlockQuoteEditHandler implements EditHandler { - - private MarkwonTheme theme; - - @Override - public void init(@NonNull Markwon markwon) { - this.theme = markwon.configuration().theme(); - } - - @Override - public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) { - builder.persistSpan(BlockQuoteSpan.class, () -> new BlockQuoteSpan(theme)); - } - - @Override - public void handleMarkdownSpan( - @NonNull PersistedSpans persistedSpans, - @NonNull Editable editable, - @NonNull String input, - @NonNull BlockQuoteSpan span, - int spanStart, - int spanTextLength) { - // todo: here we should actually find a proper ending of a block quote... - editable.setSpan( - persistedSpans.get(BlockQuoteSpan.class), - spanStart, - spanStart + spanTextLength, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE - ); - } - - @NonNull - @Override - public Class markdownSpanType() { - return BlockQuoteSpan.class; - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/editor/CodeEditHandler.java b/sample/src/main/java/io/noties/markwon/sample/editor/CodeEditHandler.java deleted file mode 100644 index c54e1a77..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/editor/CodeEditHandler.java +++ /dev/null @@ -1,54 +0,0 @@ -package io.noties.markwon.sample.editor; - -import android.text.Editable; -import android.text.Spanned; - -import androidx.annotation.NonNull; - -import io.noties.markwon.Markwon; -import io.noties.markwon.core.MarkwonTheme; -import io.noties.markwon.core.spans.CodeSpan; -import io.noties.markwon.editor.EditHandler; -import io.noties.markwon.editor.MarkwonEditorUtils; -import io.noties.markwon.editor.PersistedSpans; - -class CodeEditHandler implements EditHandler { - - private MarkwonTheme theme; - - @Override - public void init(@NonNull Markwon markwon) { - this.theme = markwon.configuration().theme(); - } - - @Override - public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) { - builder.persistSpan(CodeSpan.class, () -> new CodeSpan(theme)); - } - - @Override - public void handleMarkdownSpan( - @NonNull PersistedSpans persistedSpans, - @NonNull Editable editable, - @NonNull String input, - @NonNull CodeSpan span, - int spanStart, - int spanTextLength) { - final MarkwonEditorUtils.Match match = - MarkwonEditorUtils.findDelimited(input, spanStart, "`"); - if (match != null) { - editable.setSpan( - persistedSpans.get(CodeSpan.class), - match.start(), - match.end(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE - ); - } - } - - @NonNull - @Override - public Class markdownSpanType() { - return CodeSpan.class; - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/editor/EditorActivity.java b/sample/src/main/java/io/noties/markwon/sample/editor/EditorActivity.java deleted file mode 100644 index 84a4fdb7..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/editor/EditorActivity.java +++ /dev/null @@ -1,460 +0,0 @@ -package io.noties.markwon.sample.editor; - -import android.os.Bundle; -import android.text.Editable; -import android.text.SpannableStringBuilder; -import android.text.Spanned; -import android.text.TextPaint; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.text.method.LinkMovementMethod; -import android.text.style.ForegroundColorSpan; -import android.text.style.MetricAffectingSpan; -import android.text.style.StrikethroughSpan; -import android.view.View; -import android.widget.Button; -import android.widget.EditText; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.commonmark.parser.InlineParserFactory; -import org.commonmark.parser.Parser; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Executors; - -import io.noties.debug.AndroidLogDebugOutput; -import io.noties.debug.Debug; -import io.noties.markwon.AbstractMarkwonPlugin; -import io.noties.markwon.Markwon; -import io.noties.markwon.SoftBreakAddsNewLinePlugin; -import io.noties.markwon.core.spans.EmphasisSpan; -import io.noties.markwon.core.spans.StrongEmphasisSpan; -import io.noties.markwon.editor.AbstractEditHandler; -import io.noties.markwon.editor.MarkwonEditor; -import io.noties.markwon.editor.MarkwonEditorTextWatcher; -import io.noties.markwon.editor.MarkwonEditorUtils; -import io.noties.markwon.editor.PersistedSpans; -import io.noties.markwon.editor.handler.EmphasisEditHandler; -import io.noties.markwon.editor.handler.StrongEmphasisEditHandler; -import io.noties.markwon.ext.strikethrough.StrikethroughPlugin; -import io.noties.markwon.inlineparser.BangInlineProcessor; -import io.noties.markwon.inlineparser.EntityInlineProcessor; -import io.noties.markwon.inlineparser.HtmlInlineProcessor; -import io.noties.markwon.inlineparser.MarkwonInlineParser; -import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin; -import io.noties.markwon.linkify.LinkifyPlugin; -import io.noties.markwon.sample.ActivityWithMenuOptions; -import io.noties.markwon.sample.MenuOptions; -import io.noties.markwon.sample.R; - -public class EditorActivity extends ActivityWithMenuOptions { - - private EditText editText; - private String pendingInput; - - @NonNull - @Override - public MenuOptions menuOptions() { - return MenuOptions.create() - .add("simpleProcess", this::simple_process) - .add("simplePreRender", this::simple_pre_render) - .add("customPunctuationSpan", this::custom_punctuation_span) - .add("additionalEditSpan", this::additional_edit_span) - .add("additionalPlugins", this::additional_plugins) - .add("multipleEditSpans", this::multiple_edit_spans) - .add("multipleEditSpansPlugin", this::multiple_edit_spans_plugin) - .add("pluginRequire", this::plugin_require) - .add("pluginNoDefaults", this::plugin_no_defaults) - .add("heading", this::heading) - .add("newLine", this::newLine); - } - - @Override - protected void beforeOptionSelected(@NonNull String option) { - // we cannot _clear_ editText of text-watchers without keeping a reference to them... - pendingInput = editText != null - ? editText.getText().toString() - : null; - - createView(); - } - - @Override - protected void afterOptionSelected(@NonNull String option) { - if (!TextUtils.isEmpty(pendingInput)) { - editText.setText(pendingInput); - } - } - - private void createView() { - setContentView(R.layout.activity_editor); - - this.editText = findViewById(R.id.edit_text); - - initBottomBar(); - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - createView(); - - Debug.init(new AndroidLogDebugOutput(true)); - - multiple_edit_spans(); -// newLine(); - } - - private void simple_process() { - // Process highlight in-place (right after text has changed) - - // obtain Markwon instance - final Markwon markwon = Markwon.create(this); - - // create editor - final MarkwonEditor editor = MarkwonEditor.create(markwon); - - // set edit listener - editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor)); - } - - private void simple_pre_render() { - // Process highlight in background thread - - final Markwon markwon = Markwon.create(this); - final MarkwonEditor editor = MarkwonEditor.create(markwon); - - editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender( - editor, - Executors.newCachedThreadPool(), - editText)); - } - - private void custom_punctuation_span() { - // Use own punctuation span - - final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(this)) - .punctuationSpan(CustomPunctuationSpan.class, CustomPunctuationSpan::new) - .build(); - - editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor)); - } - - private void additional_edit_span() { - // An additional span is used to highlight strong-emphasis - - final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(this)) - .useEditHandler(new AbstractEditHandler() { - @Override - public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) { - // Here we define which span is _persisted_ in EditText, it is not removed - // from EditText between text changes, but instead - reused (by changing - // position). Consider it as a cache for spans. We could use `StrongEmphasisSpan` - // here also, but I chose Bold to indicate that this span is not the same - // as in off-screen rendered markdown - builder.persistSpan(Bold.class, Bold::new); - } - - @Override - public void handleMarkdownSpan( - @NonNull PersistedSpans persistedSpans, - @NonNull Editable editable, - @NonNull String input, - @NonNull StrongEmphasisSpan span, - int spanStart, - int spanTextLength) { - // Unfortunately we cannot hardcode delimiters length here (aka spanTextLength + 4) - // because multiple inline markdown nodes can refer to the same text. - // For example, `**_~~hey~~_**` - we will receive `**_~~` in this method, - // and thus will have to manually find actual position in raw user input - final MarkwonEditorUtils.Match match = - MarkwonEditorUtils.findDelimited(input, spanStart, "**", "__"); - if (match != null) { - editable.setSpan( - // we handle StrongEmphasisSpan and represent it with Bold in EditText - // we still could use StrongEmphasisSpan, but it must be accessed - // via persistedSpans - persistedSpans.get(Bold.class), - match.start(), - match.end(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE - ); - } - } - - @NonNull - @Override - public Class markdownSpanType() { - return StrongEmphasisSpan.class; - } - }) - .build(); - - editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor)); - } - - private void additional_plugins() { - // As highlight works based on text-diff, everything that is present in input, - // but missing in resulting markdown is considered to be punctuation, this is why - // additional plugins do not need special handling - - final Markwon markwon = Markwon.builder(this) - .usePlugin(StrikethroughPlugin.create()) - .build(); - - final MarkwonEditor editor = MarkwonEditor.create(markwon); - - editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor)); - } - - private void multiple_edit_spans() { - - // for links to be clickable - editText.setMovementMethod(LinkMovementMethod.getInstance()); - - final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder() - // no inline images will be parsed - .excludeInlineProcessor(BangInlineProcessor.class) - // no html tags will be parsed - .excludeInlineProcessor(HtmlInlineProcessor.class) - // no entities will be parsed (aka `&` etc) - .excludeInlineProcessor(EntityInlineProcessor.class) - .build(); - - final Markwon markwon = Markwon.builder(this) - .usePlugin(StrikethroughPlugin.create()) - .usePlugin(LinkifyPlugin.create()) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureParser(@NonNull Parser.Builder builder) { - - // disable all commonmark-java blocks, only inlines will be parsed -// builder.enabledBlockTypes(Collections.emptySet()); - - builder.inlineParserFactory(inlineParserFactory); - } - }) - .usePlugin(SoftBreakAddsNewLinePlugin.create()) - .build(); - - final LinkEditHandler.OnClick onClick = (widget, link) -> markwon.configuration().linkResolver().resolve(widget, link); - - final MarkwonEditor editor = MarkwonEditor.builder(markwon) - .useEditHandler(new EmphasisEditHandler()) - .useEditHandler(new StrongEmphasisEditHandler()) - .useEditHandler(new StrikethroughEditHandler()) - .useEditHandler(new CodeEditHandler()) - .useEditHandler(new BlockQuoteEditHandler()) - .useEditHandler(new LinkEditHandler(onClick)) - .build(); - -// editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor)); - editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender( - editor, Executors.newSingleThreadExecutor(), editText)); - } - - private void multiple_edit_spans_plugin() { - // inline parsing is configured via MarkwonInlineParserPlugin - - // for links to be clickable - editText.setMovementMethod(LinkMovementMethod.getInstance()); - - final Markwon markwon = Markwon.builder(this) - .usePlugin(StrikethroughPlugin.create()) - .usePlugin(LinkifyPlugin.create()) - .usePlugin(MarkwonInlineParserPlugin.create(builder -> { - builder - .excludeInlineProcessor(BangInlineProcessor.class) - .excludeInlineProcessor(HtmlInlineProcessor.class) - .excludeInlineProcessor(EntityInlineProcessor.class); - })) - .build(); - - final LinkEditHandler.OnClick onClick = (widget, link) -> markwon.configuration().linkResolver().resolve(widget, link); - - final MarkwonEditor editor = MarkwonEditor.builder(markwon) - .useEditHandler(new EmphasisEditHandler()) - .useEditHandler(new StrongEmphasisEditHandler()) - .useEditHandler(new StrikethroughEditHandler()) - .useEditHandler(new CodeEditHandler()) - .useEditHandler(new BlockQuoteEditHandler()) - .useEditHandler(new LinkEditHandler(onClick)) - .build(); - - editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender( - editor, Executors.newSingleThreadExecutor(), editText)); - } - - private void newLine() { - final Markwon markwon = Markwon.create(this); - final MarkwonEditor editor = MarkwonEditor.create(markwon); - final TextWatcher textWatcher = MarkdownNewLine.wrap(MarkwonEditorTextWatcher.withProcess(editor)); - editText.addTextChangedListener(textWatcher); - } - - private void plugin_require() { - // usage of plugin from other plugins - - final Markwon markwon = Markwon.builder(this) - .usePlugin(MarkwonInlineParserPlugin.create()) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configure(@NonNull Registry registry) { - registry.require(MarkwonInlineParserPlugin.class) - .factoryBuilder() - .excludeInlineProcessor(HtmlInlineProcessor.class); - } - }) - .build(); - - editText.setMovementMethod(LinkMovementMethod.getInstance()); - - final MarkwonEditor editor = MarkwonEditor.create(markwon); - - editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender( - editor, Executors.newSingleThreadExecutor(), editText)); - } - - private void plugin_no_defaults() { - // a plugin with no defaults registered - - final Markwon markwon = Markwon.builder(this) - .usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilderNoDefaults())) -// .usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilderNoDefaults(), factoryBuilder -> { -// // if anything, they can be included here -//// factoryBuilder.includeDefaults() -// })) - .build(); - - final MarkwonEditor editor = MarkwonEditor.create(markwon); - - editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender( - editor, Executors.newSingleThreadExecutor(), editText)); - } - - private void heading() { - final Markwon markwon = Markwon.create(this); - final MarkwonEditor editor = MarkwonEditor.builder(markwon) - .useEditHandler(new HeadingEditHandler()) - .build(); - - editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender( - editor, Executors.newSingleThreadExecutor(), editText)); - } - - private void initBottomBar() { - // all except block-quote wraps if have selection, or inserts at current cursor position - - final Button bold = findViewById(R.id.bold); - final Button italic = findViewById(R.id.italic); - final Button strike = findViewById(R.id.strike); - final Button quote = findViewById(R.id.quote); - final Button code = findViewById(R.id.code); - - addSpan(bold, new StrongEmphasisSpan()); - addSpan(italic, new EmphasisSpan()); - addSpan(strike, new StrikethroughSpan()); - - bold.setOnClickListener(new InsertOrWrapClickListener(editText, "**")); - italic.setOnClickListener(new InsertOrWrapClickListener(editText, "_")); - strike.setOnClickListener(new InsertOrWrapClickListener(editText, "~~")); - code.setOnClickListener(new InsertOrWrapClickListener(editText, "`")); - - quote.setOnClickListener(v -> { - - final int start = editText.getSelectionStart(); - final int end = editText.getSelectionEnd(); - - if (start < 0) { - return; - } - - if (start == end) { - editText.getText().insert(start, "> "); - } else { - // wrap the whole selected area in a quote - final List newLines = new ArrayList<>(3); - newLines.add(start); - - final String text = editText.getText().subSequence(start, end).toString(); - int index = text.indexOf('\n'); - while (index != -1) { - newLines.add(start + index + 1); - index = text.indexOf('\n', index + 1); - } - - for (int i = newLines.size() - 1; i >= 0; i--) { - editText.getText().insert(newLines.get(i), "> "); - } - } - }); - } - - private static void addSpan(@NonNull TextView textView, Object... spans) { - final SpannableStringBuilder builder = new SpannableStringBuilder(textView.getText()); - final int end = builder.length(); - for (Object span : spans) { - builder.setSpan(span, 0, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - textView.setText(builder); - } - - private static class InsertOrWrapClickListener implements View.OnClickListener { - - private final EditText editText; - private final String text; - - InsertOrWrapClickListener(@NonNull EditText editText, @NonNull String text) { - this.editText = editText; - this.text = text; - } - - @Override - public void onClick(View v) { - final int start = editText.getSelectionStart(); - final int end = editText.getSelectionEnd(); - - if (start < 0) { - return; - } - - if (start == end) { - // insert at current position - editText.getText().insert(start, text); - } else { - editText.getText().insert(end, text); - editText.getText().insert(start, text); - } - } - } - - private static class CustomPunctuationSpan extends ForegroundColorSpan { - CustomPunctuationSpan() { - super(0xFFFF0000); // RED - } - } - - private static class Bold extends MetricAffectingSpan { - public Bold() { - super(); - } - - @Override - public void updateDrawState(TextPaint tp) { - update(tp); - } - - @Override - public void updateMeasureState(@NonNull TextPaint textPaint) { - update(textPaint); - } - - private void update(@NonNull TextPaint paint) { - paint.setFakeBoldText(true); - } - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/editor/HeadingEditHandler.java b/sample/src/main/java/io/noties/markwon/sample/editor/HeadingEditHandler.java deleted file mode 100644 index f76499db..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/editor/HeadingEditHandler.java +++ /dev/null @@ -1,78 +0,0 @@ -package io.noties.markwon.sample.editor; - -import android.text.Editable; -import android.text.Spanned; - -import androidx.annotation.NonNull; - -import io.noties.markwon.Markwon; -import io.noties.markwon.core.MarkwonTheme; -import io.noties.markwon.core.spans.HeadingSpan; -import io.noties.markwon.editor.EditHandler; -import io.noties.markwon.editor.PersistedSpans; - -public class HeadingEditHandler implements EditHandler { - - private MarkwonTheme theme; - - @Override - public void init(@NonNull Markwon markwon) { - this.theme = markwon.configuration().theme(); - } - - @Override - public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) { - builder - .persistSpan(Head1.class, () -> new Head1(theme)) - .persistSpan(Head2.class, () -> new Head2(theme)); - } - - @Override - public void handleMarkdownSpan( - @NonNull PersistedSpans persistedSpans, - @NonNull Editable editable, - @NonNull String input, - @NonNull HeadingSpan span, - int spanStart, - int spanTextLength - ) { - final Class type; - switch (span.getLevel()) { - case 1: type = Head1.class; break; - case 2: type = Head2.class; break; - default: - type = null; - } - - if (type != null) { - final int index = input.indexOf('\n', spanStart + spanTextLength); - final int end = index < 0 - ? input.length() - : index; - editable.setSpan( - persistedSpans.get(type), - spanStart, - end, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE - ); - } - } - - @NonNull - @Override - public Class markdownSpanType() { - return HeadingSpan.class; - } - - private static class Head1 extends HeadingSpan { - Head1(@NonNull MarkwonTheme theme) { - super(theme, 1); - } - } - - private static class Head2 extends HeadingSpan { - Head2(@NonNull MarkwonTheme theme) { - super(theme, 2); - } - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/editor/LinkEditHandler.java b/sample/src/main/java/io/noties/markwon/sample/editor/LinkEditHandler.java deleted file mode 100644 index 3a6d60fd..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/editor/LinkEditHandler.java +++ /dev/null @@ -1,90 +0,0 @@ -package io.noties.markwon.sample.editor; - -import android.text.Editable; -import android.text.Spanned; -import android.text.style.ClickableSpan; -import android.view.View; - -import androidx.annotation.NonNull; - -import io.noties.markwon.core.spans.LinkSpan; -import io.noties.markwon.editor.AbstractEditHandler; -import io.noties.markwon.editor.PersistedSpans; - -class LinkEditHandler extends AbstractEditHandler { - - interface OnClick { - void onClick(@NonNull View widget, @NonNull String link); - } - - private final OnClick onClick; - - LinkEditHandler(@NonNull OnClick onClick) { - this.onClick = onClick; - } - - @Override - public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) { - builder.persistSpan(EditLinkSpan.class, () -> new EditLinkSpan(onClick)); - } - - @Override - public void handleMarkdownSpan( - @NonNull PersistedSpans persistedSpans, - @NonNull Editable editable, - @NonNull String input, - @NonNull LinkSpan span, - int spanStart, - int spanTextLength) { - - final EditLinkSpan editLinkSpan = persistedSpans.get(EditLinkSpan.class); - editLinkSpan.link = span.getLink(); - - // First first __letter__ to find link content (scheme start in URL, receiver in email address) - // NB! do not use phone number auto-link (via LinkifyPlugin) as we cannot guarantee proper link - // display. For example, we _could_ also look for a digit, but: - // * if phone number start with special symbol, we won't have it (`+`, `(`) - // * it might interfere with an ordered-list - int start = -1; - - for (int i = spanStart, length = input.length(); i < length; i++) { - if (Character.isLetter(input.charAt(i))) { - start = i; - break; - } - } - - if (start > -1) { - editable.setSpan( - editLinkSpan, - start, - start + spanTextLength, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE - ); - } - } - - @NonNull - @Override - public Class markdownSpanType() { - return LinkSpan.class; - } - - static class EditLinkSpan extends ClickableSpan { - - private final OnClick onClick; - - String link; - - EditLinkSpan(@NonNull OnClick onClick) { - this.onClick = onClick; - } - - @Override - public void onClick(@NonNull View widget) { - if (link != null) { - onClick.onClick(widget, link); - } - } - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/editor/MarkdownNewLine.java b/sample/src/main/java/io/noties/markwon/sample/editor/MarkdownNewLine.java deleted file mode 100644 index 9552f2ba..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/editor/MarkdownNewLine.java +++ /dev/null @@ -1,129 +0,0 @@ -package io.noties.markwon.sample.editor; - -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; - -import androidx.annotation.NonNull; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import io.noties.debug.Debug; - -abstract class MarkdownNewLine { - - @NonNull - static TextWatcher wrap(@NonNull TextWatcher textWatcher) { - return new NewLineTextWatcher(textWatcher); - } - - private MarkdownNewLine() { - } - - private static class NewLineTextWatcher implements TextWatcher { - - // NB! matches only bullet lists - private final Pattern RE = Pattern.compile("^( {0,3}[\\-+* ]+)(.+)*$"); - - private final TextWatcher wrapped; - - private boolean selfChange; - - // this content is pending to be inserted at the beginning - private String pendingNewLineContent; - private int pendingNewLineIndex; - - // mark current edited line for removal (range start/end) - private int clearLineStart; - private int clearLineEnd; - - NewLineTextWatcher(@NonNull TextWatcher wrapped) { - this.wrapped = wrapped; - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - // no op - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - if (selfChange) { - return; - } - - // just one new character added - if (before == 0 - && count == 1 - && '\n' == s.charAt(start)) { - int end = -1; - for (int i = start - 1; i >= 0; i--) { - if ('\n' == s.charAt(i)) { - end = i + 1; - break; - } - } - - // start at the very beginning - if (end < 0) { - end = 0; - } - - final String pendingNewLineContent; - - final int clearLineStart; - final int clearLineEnd; - - final Matcher matcher = RE.matcher(s.subSequence(end, start)); - if (matcher.matches()) { - // if second group is empty -> remove new line - final String content = matcher.group(2); - Debug.e("new line, content: '%s'", content); - if (TextUtils.isEmpty(content)) { - // another empty new line, remove this start - clearLineStart = end; - clearLineEnd = start; - pendingNewLineContent = null; - } else { - pendingNewLineContent = matcher.group(1); - clearLineStart = clearLineEnd = 0; - } - } else { - pendingNewLineContent = null; - clearLineStart = clearLineEnd = 0; - } - this.pendingNewLineContent = pendingNewLineContent; - this.pendingNewLineIndex = start + 1; - this.clearLineStart = clearLineStart; - this.clearLineEnd = clearLineEnd; - } - } - - @Override - public void afterTextChanged(Editable s) { - if (selfChange) { - return; - } - - if (pendingNewLineContent != null || clearLineStart < clearLineEnd) { - selfChange = true; - try { - if (pendingNewLineContent != null) { - s.insert(pendingNewLineIndex, pendingNewLineContent); - pendingNewLineContent = null; - } else { - s.replace(clearLineStart, clearLineEnd, ""); - clearLineStart = clearLineEnd = 0; - } - } finally { - selfChange = false; - } - } - - // NB, we assume MarkdownEditor text watcher that only listens for this event, - // other text-watchers must be interested in other events also - wrapped.afterTextChanged(s); - } - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/editor/StrikethroughEditHandler.java b/sample/src/main/java/io/noties/markwon/sample/editor/StrikethroughEditHandler.java deleted file mode 100644 index bffda27b..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/editor/StrikethroughEditHandler.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.noties.markwon.sample.editor; - -import android.text.Editable; -import android.text.Spanned; -import android.text.style.StrikethroughSpan; - -import androidx.annotation.NonNull; - -import io.noties.markwon.editor.AbstractEditHandler; -import io.noties.markwon.editor.MarkwonEditorUtils; -import io.noties.markwon.editor.PersistedSpans; - -class StrikethroughEditHandler extends AbstractEditHandler { - - @Override - public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) { - builder.persistSpan(StrikethroughSpan.class, StrikethroughSpan::new); - } - - @Override - public void handleMarkdownSpan( - @NonNull PersistedSpans persistedSpans, - @NonNull Editable editable, - @NonNull String input, - @NonNull StrikethroughSpan span, - int spanStart, - int spanTextLength) { - final MarkwonEditorUtils.Match match = - MarkwonEditorUtils.findDelimited(input, spanStart, "~~"); - if (match != null) { - editable.setSpan( - persistedSpans.get(StrikethroughSpan.class), - match.start(), - match.end(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE - ); - } - } - - @NonNull - @Override - public Class markdownSpanType() { - return StrikethroughSpan.class; - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/html/CenterTagHandler.java b/sample/src/main/java/io/noties/markwon/sample/html/CenterTagHandler.java deleted file mode 100644 index fa1d7e61..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/html/CenterTagHandler.java +++ /dev/null @@ -1,39 +0,0 @@ -package io.noties.markwon.sample.html; - -import android.text.Layout; -import android.text.style.AlignmentSpan; -import android.util.Log; - -import androidx.annotation.NonNull; - -import java.util.Collection; -import java.util.Collections; - -import io.noties.markwon.MarkwonVisitor; -import io.noties.markwon.SpannableBuilder; -import io.noties.markwon.html.HtmlTag; -import io.noties.markwon.html.MarkwonHtmlRenderer; -import io.noties.markwon.html.TagHandler; - -class CenterTagHandler extends TagHandler { - - @Override - public void handle(@NonNull MarkwonVisitor visitor, @NonNull MarkwonHtmlRenderer renderer, @NonNull HtmlTag tag) { - Log.e("HTML", String.format("center, isBlock: %s", tag.isBlock())); - if (tag.isBlock()) { - visitChildren(visitor, renderer, tag.getAsBlock()); - } - SpannableBuilder.setSpans( - visitor.builder(), - new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), - tag.start(), - tag.end() - ); - } - - @NonNull - @Override - public Collection supportedTags() { - return Collections.singleton("center"); - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/html/ElegantUnderlineSpan.java b/sample/src/main/java/io/noties/markwon/sample/html/ElegantUnderlineSpan.java deleted file mode 100644 index 307ea834..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/html/ElegantUnderlineSpan.java +++ /dev/null @@ -1,242 +0,0 @@ -package io.noties.markwon.sample.html; - -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Path; -import android.os.Build; -import android.text.Layout; -import android.text.Spanned; -import android.text.TextPaint; -import android.text.TextUtils; -import android.text.style.LineBackgroundSpan; -import android.text.style.MetricAffectingSpan; -import android.util.Log; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Px; -import androidx.annotation.RequiresApi; - -import io.noties.markwon.core.spans.TextLayoutSpan; -import io.noties.markwon.core.spans.TextViewSpan; - -import static java.lang.Math.max; -import static java.lang.Math.min; - -/** - * Credit goes to [Romain Guy](https://github.com/romainguy/elegant-underline) - *

- * Failed attempt to create elegant underline as a span - *

    - *
  • in a `TextView` span is rendered, but `draw` method is invoked constantly which put pressure on CPU and memory - *
  • in an `EditText` only the first line draws this underline span (seems to be a weird - * issue between LineBackgroundSpan and EditText). Also, in `EditText` `draw` method is invoked - * constantly (for each drawing of the blinking cursor) - *
  • cannot reliably receive proper text, for example if underline is applied to a text range which has - * different typefaces applied to different words (underline cannot know that, which applied to which) - *
- */ -// will apply other spans that 100% contain this one, so for example if -// an underline that inside some other spans (different typeface), they won't be applied and thus -// underline would be incorrect -// do not use in editor, due to some obscure thing, LineBackgroundSpan would be applied to the first line only -// also, in editor this span would be redrawn with each blink of the cursor -@RequiresApi(Build.VERSION_CODES.KITKAT) -class ElegantUnderlineSpan implements LineBackgroundSpan { - - private static final float DEFAULT_UNDERLINE_HEIGHT_DIP = 0.8F; - private static final float DEFAULT_UNDERLINE_CLEAR_GAP_DIP = 5.5F; - - @NonNull - public static ElegantUnderlineSpan create() { - return new ElegantUnderlineSpan(0, 0); - } - - @NonNull - public static ElegantUnderlineSpan create(@Px int underlineHeight) { - return new ElegantUnderlineSpan(underlineHeight, 0); - } - - @NonNull - public static ElegantUnderlineSpan create(@Px int underlineHeight, @Px int underlineClearGap) { - return new ElegantUnderlineSpan(underlineHeight, underlineClearGap); - } - - // TODO: underline color? - private final int underlineHeight; - private final int underlineClearGap; - - private final Path underline = new Path(); - private final Path outline = new Path(); - private final Paint stroke = new Paint(); - private final Path strokedOutline = new Path(); - - private final CharCache charCache = new CharCache(); - - private final TextPaint tempTextPaint = new TextPaint(); - - protected ElegantUnderlineSpan(@Px int underlineHeight, @Px int underlineClearGap) { - this.underlineHeight = underlineHeight; - this.underlineClearGap = underlineClearGap; - stroke.setStyle(Paint.Style.FILL_AND_STROKE); - stroke.setStrokeCap(Paint.Cap.BUTT); - } - - // is it possible that LineBackgroundSpan is not receiving proper spans? like typeface? - // it complicates things (like the need to have own copy of paint) - - // is it possible that LineBackgroundSpan is called constantly even in a TextView? - - @Override - public void drawBackground( - Canvas c, - Paint p, - int left, - int right, - int top, - int baseline, - int bottom, - CharSequence text, - int start, - int end, - int lnum - ) { - -// Debug.trace(); - - final Spanned spanned = (Spanned) text; - final TextView textView = TextViewSpan.textViewOf(spanned); - - if (textView == null) { - // TextView is required - Log.e("EU", "no text view"); - return; - } - - final Layout layout; - { - // check if there is dedicated layout, if not, use from textView - // (think tableRowSpan that uses own Layout) - final Layout layoutFromSpan = TextLayoutSpan.layoutOf(spanned); - if (layoutFromSpan != null) { - layout = layoutFromSpan; - } else { - layout = textView.getLayout(); - } - } - - if (layout == null) { - // we could call `p.setUnderlineText(true)` here a fallback, - // but this would make __all__ text in a TextView underlined, which is not - // what we want - Log.e("EU", "no layout"); - return; - } - - tempTextPaint.set((TextPaint) p); - - // we must use _selfStart_ because underline can start **not** at the beginning of a line. - // as we are using LineBackground `start` would indicate the start position of the line - // and not start of the span (self). The same goes for selfEnd (ended before line) - final int selfStart = spanned.getSpanStart(this); - final int selfEnd = spanned.getSpanEnd(this); - - final int s = max(selfStart, start); - - // all lines should use (end - 1) to receive proper line end coordinate X, - // unless it is last line in _layout_ - final boolean isLastLine = lnum == (layout.getLineCount() - 1); - final int e = min(selfEnd, end - (isLastLine ? 0 : 1)); - - if (true) { - Log.e("EU", String.format("lnum: %s, hash: %s, text: '%s'", - lnum, text.subSequence(s, e).hashCode(), text.subSequence(s, e))); - } - - final int leading; - final int trailing; - { - final int l = (int) (layout.getPrimaryHorizontal(s) + .5F); - final int r = (int) (layout.getPrimaryHorizontal(e) + .5F); - leading = min(l, r); - trailing = max(l, r); - } - - underline.rewind(); - - // middle between baseline and descent - final int diff = (int) (p.descent() / 2F + .5F); - - underline.addRect( - leading, baseline + diff, - trailing, baseline + diff + underlineHeight(textView), - Path.Direction.CW - ); - - outline.rewind(); - - final int charsLength = e - s; - final char[] chars = charCache.chars(charsLength); - TextUtils.getChars(spanned, s, e, chars, 0); - - if (true) { - final MetricAffectingSpan[] metricAffectingSpans = spanned.getSpans(s, e, MetricAffectingSpan.class); -// Log.e("EU", Arrays.toString(metricAffectingSpans)); - for (MetricAffectingSpan span : metricAffectingSpans) { - span.updateMeasureState(tempTextPaint); - } - } - - // todo: styleSpan - // todo all other spans (maybe UpdateMeasureSpans?) - tempTextPaint.getTextPath( - chars, - 0, charsLength, - leading, baseline, - outline - ); - - outline.op(underline, Path.Op.INTERSECT); - - strokedOutline.rewind(); - stroke.setStrokeWidth(underlineClearGap(textView)); - stroke.getFillPath(outline, strokedOutline); - - underline.op(strokedOutline, Path.Op.DIFFERENCE); - - c.drawPath(underline, p); - } - - private int underlineHeight(@NonNull TextView textView) { - if (underlineHeight > 0) { - return underlineHeight; - } - return (int) (DEFAULT_UNDERLINE_HEIGHT_DIP * textView.getResources().getDisplayMetrics().density + 0.5F); - } - - private int underlineClearGap(@NonNull TextView textView) { - if (underlineClearGap > 0) { - return underlineClearGap; - } - return (int) (DEFAULT_UNDERLINE_CLEAR_GAP_DIP * textView.getResources().getDisplayMetrics().density + 0.5F); - } - - // primitive cache that grows internal array (never shrinks, nor clear buffer) - // TODO: but... each span has own instance, so not much of the memory saving - private static class CharCache { - - @NonNull - char[] chars(int ofLength) { - final char[] out; - if (chars == null || chars.length < ofLength) { - out = chars = new char[ofLength]; - } else { - out = chars; - } - return out; - } - - private char[] chars; - } -} - diff --git a/sample/src/main/java/io/noties/markwon/sample/html/HtmlActivity.java b/sample/src/main/java/io/noties/markwon/sample/html/HtmlActivity.java deleted file mode 100644 index 02c65621..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/html/HtmlActivity.java +++ /dev/null @@ -1,419 +0,0 @@ -package io.noties.markwon.sample.html; - -import android.os.Build; -import android.os.Bundle; -import android.text.Layout; -import android.text.TextUtils; -import android.text.style.AbsoluteSizeSpan; -import android.text.style.AlignmentSpan; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.Px; - -import java.util.Collection; -import java.util.Collections; -import java.util.Random; - -import io.noties.markwon.AbstractMarkwonPlugin; -import io.noties.markwon.Markwon; -import io.noties.markwon.MarkwonConfiguration; -import io.noties.markwon.MarkwonVisitor; -import io.noties.markwon.RenderProps; -import io.noties.markwon.SpannableBuilder; -import io.noties.markwon.html.HtmlEmptyTagReplacement; -import io.noties.markwon.html.HtmlPlugin; -import io.noties.markwon.html.HtmlTag; -import io.noties.markwon.html.MarkwonHtmlRenderer; -import io.noties.markwon.html.TagHandler; -import io.noties.markwon.html.tag.SimpleTagHandler; -import io.noties.markwon.image.ImagesPlugin; -import io.noties.markwon.sample.ActivityWithMenuOptions; -import io.noties.markwon.sample.MenuOptions; -import io.noties.markwon.sample.R; - -public class HtmlActivity extends ActivityWithMenuOptions { - - @NonNull - @Override - public MenuOptions menuOptions() { - return MenuOptions.create() - .add("align", this::align) - .add("randomCharSize", this::randomCharSize) - .add("enhance", this::enhance) - .add("image", this::image) -// .add("elegantUnderline", this::elegantUnderline) - .add("iframe", this::iframe) - .add("emptyTagReplacement", this::emptyTagReplacement) - .add("centerTag", this::centerTag); - } - - private TextView textView; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_text_view); - - // let's define some custom tag-handlers - - textView = findViewById(R.id.text_view); - - emptyTagReplacement(); - } - - // we can use `SimpleTagHandler` for _simple_ cases (when the whole tag content - // will have spans from start to end) - // - // we can use any tag name, even not defined in HTML spec - private static class AlignTagHandler extends SimpleTagHandler { - - @Nullable - @Override - public Object getSpans( - @NonNull MarkwonConfiguration configuration, - @NonNull RenderProps renderProps, - @NonNull HtmlTag tag) { - - final Layout.Alignment alignment; - - // html attribute without value, - if (tag.attributes().containsKey("center")) { - alignment = Layout.Alignment.ALIGN_CENTER; - } else if (tag.attributes().containsKey("end")) { - alignment = Layout.Alignment.ALIGN_OPPOSITE; - } else { - // empty value or any other will make regular alignment - alignment = Layout.Alignment.ALIGN_NORMAL; - } - - return new AlignmentSpan.Standard(alignment); - } - - @NonNull - @Override - public Collection supportedTags() { - return Collections.singleton("align"); - } - } - - private void align() { - - final String md = "" + - "We are centered\n" + - "\n" + - "We are at the end\n" + - "\n" + - "We should be at the start\n" + - "\n"; - - - final Markwon markwon = Markwon.builder(this) - .usePlugin(HtmlPlugin.create()) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configure(@NonNull Registry registry) { - registry.require(HtmlPlugin.class, htmlPlugin -> htmlPlugin - .addHandler(new AlignTagHandler())); - } - }) - .build(); - - markwon.setMarkdown(textView, md); - } - - // each character will have random size - private static class RandomCharSize extends TagHandler { - - private final Random random; - private final float base; - - RandomCharSize(@NonNull Random random, float base) { - this.random = random; - this.base = base; - } - - @Override - public void handle( - @NonNull MarkwonVisitor visitor, - @NonNull MarkwonHtmlRenderer renderer, - @NonNull HtmlTag tag) { - - final SpannableBuilder builder = visitor.builder(); - - // text content is already added, we should only apply spans - - for (int i = tag.start(), end = tag.end(); i < end; i++) { - final int size = (int) (base * (random.nextFloat() + 0.5F) + 0.5F); - builder.setSpan(new AbsoluteSizeSpan(size, false), i, i + 1); - } - } - - @NonNull - @Override - public Collection supportedTags() { - return Collections.singleton("random-char-size"); - } - } - - private void randomCharSize() { - - final String md = "" + - "\n" + - "This message should have a jumpy feeling because of different sizes of characters\n" + - "\n\n"; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(HtmlPlugin.create()) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configure(@NonNull Registry registry) { - registry.require(HtmlPlugin.class, htmlPlugin -> htmlPlugin - .addHandler(new RandomCharSize(new Random(42L), textView.getTextSize()))); - } - }) - .build(); - - markwon.setMarkdown(textView, md); - } - - private static class EnhanceTagHandler extends TagHandler { - - private final int enhanceTextSize; - - EnhanceTagHandler(@Px int enhanceTextSize) { - this.enhanceTextSize = enhanceTextSize; - } - - @Override - public void handle( - @NonNull MarkwonVisitor visitor, - @NonNull MarkwonHtmlRenderer renderer, - @NonNull HtmlTag tag) { - - // we require start and end to be present - final int start = parsePosition(tag.attributes().get("start")); - final int end = parsePosition(tag.attributes().get("end")); - - if (start > -1 && end > -1) { - visitor.builder().setSpan( - new AbsoluteSizeSpan(enhanceTextSize), - tag.start() + start, - tag.start() + end - ); - } - } - - @NonNull - @Override - public Collection supportedTags() { - return Collections.singleton("enhance"); - } - - private static int parsePosition(@Nullable String value) { - int position; - if (!TextUtils.isEmpty(value)) { - try { - position = Integer.parseInt(value); - } catch (NumberFormatException e) { - e.printStackTrace(); - position = -1; - } - } else { - position = -1; - } - return position; - } - } - - private void enhance() { - - final String md = "" + - "This is text that must be enhanced, at least a part of it"; - - - final Markwon markwon = Markwon.builder(this) - .usePlugin(HtmlPlugin.create()) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configure(@NonNull Registry registry) { - registry.require(HtmlPlugin.class, htmlPlugin -> htmlPlugin - .addHandler(new EnhanceTagHandler((int) (textView.getTextSize() * 2 + .05F)))); - } - }) - .build(); - - markwon.setMarkdown(textView, md); - } - - private void image() { - // treat unclosed/void `img` tag as HTML inline - final String md = "" + - "## Try CommonMark\n" + - "\n" + - "Markwon IMG:\n" + - "\n" + - "![](https://upload.wikimedia.org/wikipedia/it/thumb/c/c5/GTA_2.JPG/220px-GTA_2.JPG)\n" + - "\n" + - "New lines...\n" + - "\n" + - "HTML IMG:\n" + - "\n" + - "\n" + - "\n" + - "New lines\n\n"; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(ImagesPlugin.create()) - .usePlugin(HtmlPlugin.create()) - .build(); - - markwon.setMarkdown(textView, md); - } - - private void elegantUnderline() { - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - Toast.makeText( - this, - "Elegant underline is supported on KitKat and up", - Toast.LENGTH_LONG - ).show(); - return; - } - - final String underline = "Well well wel, and now Gogogo, quite **perfect** yeah and nice and elegant"; - - final String md = "" + - underline + "\n\n" + - "" + underline + "\n\n" + - "" + underline + "\n\n" + - "" + underline + underline + underline + "\n\n" + - "" + underline + "\n\n" + - ""; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(HtmlPlugin.create(plugin -> plugin - .addHandler(new HtmlFontTagHandler()) - .addHandler(new HtmlElegantUnderlineTagHandler()))) - .build(); - - markwon.setMarkdown(textView, md); - } - - private void iframe() { - final String md = "" + - "# Hello iframe\n\n" + - "

\"JUMP

\n" + - "

 

\n" + - "

Switch owners will soon get to take part in the ultimate Shonen Jump rumble. Bandai Namco announced plans to bring Jump Force to Switch as Jump Force Deluxe Edition, with a release set for sometime this year. This version will include all of the original playable characters and those from Character Pass 1, and Character Pass 2 is also in the works for all versions, starting with Shoto Todoroki from My Hero Academia.

\n" + - "

 

\n" + - "

Other than Todoroki, Bandai Namco hinted that the four other Character Pass 2 characters will hail from Hunter x Hunter, Yu Yu Hakusho, Bleach, and JoJo's Bizarre Adventure. Character Pass 2 will be priced at $17.99, and Todoroki launches this spring. 

\n" + - "

 

\n" + - "

\n" + - "

 

\n" + - "

Character Pass 2 promo:

\n" + - "

 

\n" + - "

\n" + - "

 

\n" + - "

\"\"

\n" + - "

 

\n" + - "

-------

\n" + - "

Joseph Luster is the Games and Web editor at Otaku USA Magazine. You can read his webcomic, BIG DUMB FIGHTING IDIOTS at subhumanzoids. Follow him on Twitter @Moldilox. 

"; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(ImagesPlugin.create()) - .usePlugin(HtmlPlugin.create()) - .usePlugin(new IFrameHtmlPlugin()) - .build(); - - markwon.setMarkdown(textView, md); - } - - private void emptyTagReplacement() { - - final String md = "" + - " the `` is replaced?"; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(HtmlPlugin.create(plugin -> { - plugin.emptyTagReplacement(new HtmlEmptyTagReplacement() { - @Nullable - @Override - public String replace(@NonNull HtmlTag tag) { - if ("empty".equals(tag.name())) { - return "REPLACED_EMPTY_WITH_IT"; - } - return super.replace(tag); - } - }); - })) - .build(); - - markwon.setMarkdown(textView, md); - } - - private void centerTag() { - final String html = "\n" + - "\n" + - "\n" + - "\n" + - "\n" + - "

\n" + - "

LiSA's Sword Art Online: Alicization OP Song \"ADAMAS\" Certified Platinum with 250,000 Downloads

\n" + - "

\n" + - "
The upper tune was already certified Gold one month after its digital release
\n" + - "

According to The Recording Industry Association of Japan (RIAJ)'s monthly report for April 2020, one of the LiSA's 14th single songs,\n" + - " \"ADAMAS\" (the first OP theme for the TV anime Sword Art Online:\n" + - " Alicization) has been certified Platinum for\n" + - " surpassing 250,000 downloads.

\n" + - "

 

\n" + - "

As a double A-side single with \"Akai Wana (who loves it?),\" \"ADAMAS\" was\n" + - " released from SACRA Music in Japan on December 12, 2018. Its CD single ranked second in Oricon's weekly single\n" + - " chart by selling 35,000 copies in its first week. Meanwhile, the song was released digitally two months prior to\n" + - " its CD release, October 8, then reached Gold (100,000 downloads) in the following month.

\n" + - "

 

\n" + - "

 

\n" + - "
\n" + - "

\"ADAMAS\" MV YouTube EDIT ver.:

\n" + - "

\n" + - "

\n" + - "

 

\n" + - "

Standard edition CD jacket:

\n" + - "

\"\"

\n" + - "
\n" + - "

  

\n" + - "
\n" + - "

 

\n" + - "

Source: RIAJ press release

\n" + - "

 

\n" + - "

©SACRA MUSIC

\n" + - "

 

\n" + - "

\n" + - "\n" + - "\n" + - ""; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(HtmlPlugin.create(new HtmlPlugin.HtmlConfigure() { - @Override - public void configureHtml(@NonNull HtmlPlugin plugin) { - plugin.addHandler(new CenterTagHandler()); - } - })) - .usePlugin(new IFrameHtmlPlugin()) - .usePlugin(ImagesPlugin.create()) - .build(); - - markwon.setMarkdown(textView, html); - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/html/HtmlElegantUnderlineTagHandler.java b/sample/src/main/java/io/noties/markwon/sample/html/HtmlElegantUnderlineTagHandler.java deleted file mode 100644 index 76a2e5fb..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/html/HtmlElegantUnderlineTagHandler.java +++ /dev/null @@ -1,38 +0,0 @@ -package io.noties.markwon.sample.html; - -import android.os.Build; - -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; - -import java.util.Collection; -import java.util.Collections; - -import io.noties.markwon.MarkwonVisitor; -import io.noties.markwon.SpannableBuilder; -import io.noties.markwon.html.HtmlTag; -import io.noties.markwon.html.MarkwonHtmlRenderer; -import io.noties.markwon.html.TagHandler; - -@RequiresApi(Build.VERSION_CODES.KITKAT) -public class HtmlElegantUnderlineTagHandler extends TagHandler { - - @Override - public void handle(@NonNull MarkwonVisitor visitor, @NonNull MarkwonHtmlRenderer renderer, @NonNull HtmlTag tag) { - if (tag.isBlock()) { - visitChildren(visitor, renderer, tag.getAsBlock()); - } - SpannableBuilder.setSpans( - visitor.builder(), - ElegantUnderlineSpan.create(), - tag.start(), - tag.end() - ); - } - - @NonNull - @Override - public Collection supportedTags() { - return Collections.singleton("u"); - } -} \ No newline at end of file diff --git a/sample/src/main/java/io/noties/markwon/sample/html/HtmlFontTagHandler.java b/sample/src/main/java/io/noties/markwon/sample/html/HtmlFontTagHandler.java deleted file mode 100644 index 477cbd59..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/html/HtmlFontTagHandler.java +++ /dev/null @@ -1,42 +0,0 @@ -package io.noties.markwon.sample.html; - -import android.text.TextUtils; -import android.text.style.TypefaceSpan; - -import androidx.annotation.NonNull; - -import java.util.Collection; -import java.util.Collections; - -import io.noties.markwon.MarkwonVisitor; -import io.noties.markwon.SpannableBuilder; -import io.noties.markwon.html.HtmlTag; -import io.noties.markwon.html.MarkwonHtmlRenderer; -import io.noties.markwon.html.TagHandler; - -public class HtmlFontTagHandler extends TagHandler { - - @Override - public void handle(@NonNull MarkwonVisitor visitor, @NonNull MarkwonHtmlRenderer renderer, @NonNull HtmlTag tag) { - - if (tag.isBlock()) { - visitChildren(visitor, renderer, tag.getAsBlock()); - } - - final String font = tag.attributes().get("name"); - if (!TextUtils.isEmpty(font)) { - SpannableBuilder.setSpans( - visitor.builder(), - new TypefaceSpan(font), - tag.start(), - tag.end() - ); - } - } - - @NonNull - @Override - public Collection supportedTags() { - return Collections.singleton("font"); - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/html/IFrameHtmlPlugin.java b/sample/src/main/java/io/noties/markwon/sample/html/IFrameHtmlPlugin.java deleted file mode 100644 index effc6fdf..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/html/IFrameHtmlPlugin.java +++ /dev/null @@ -1,48 +0,0 @@ -package io.noties.markwon.sample.html; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.commonmark.node.Image; - -import java.util.Collection; -import java.util.Collections; - -import io.noties.debug.Debug; -import io.noties.markwon.AbstractMarkwonPlugin; -import io.noties.markwon.MarkwonConfiguration; -import io.noties.markwon.RenderProps; -import io.noties.markwon.html.HtmlPlugin; -import io.noties.markwon.html.HtmlTag; -import io.noties.markwon.html.tag.SimpleTagHandler; -import io.noties.markwon.image.ImageProps; -import io.noties.markwon.image.ImageSize; - -public class IFrameHtmlPlugin extends AbstractMarkwonPlugin { - @Override - public void configure(@NonNull Registry registry) { - registry.require(HtmlPlugin.class, htmlPlugin -> { - // TODO: empty tag replacement - htmlPlugin.addHandler(new EmbedTagHandler()); - }); - } - - private static class EmbedTagHandler extends SimpleTagHandler { - - @Nullable - @Override - public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps, @NonNull HtmlTag tag) { - final ImageSize imageSize = new ImageSize(new ImageSize.Dimension(640, "px"), new ImageSize.Dimension(480, "px")); - ImageProps.IMAGE_SIZE.set(renderProps, imageSize); - ImageProps.DESTINATION.set(renderProps, "https://img1.ak.crunchyroll.com/i/spire2/d7b1d6bc7563224388ef5ffc04a967581589950464_full.jpg"); - return configuration.spansFactory().require(Image.class) - .getSpans(configuration, renderProps); - } - - @NonNull - @Override - public Collection supportedTags() { - return Collections.singleton("iframe"); - } - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/htmldetails/HtmlDetailsActivity.java b/sample/src/main/java/io/noties/markwon/sample/htmldetails/HtmlDetailsActivity.java deleted file mode 100644 index 8c668cf4..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/htmldetails/HtmlDetailsActivity.java +++ /dev/null @@ -1,410 +0,0 @@ -package io.noties.markwon.sample.htmldetails; - -import android.app.Activity; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Rect; -import android.os.Bundle; -import android.text.Layout; -import android.text.Spanned; -import android.text.style.ClickableSpan; -import android.text.style.LeadingMarginSpan; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; - -import io.noties.markwon.Markwon; -import io.noties.markwon.MarkwonVisitor; -import io.noties.markwon.SpannableBuilder; -import io.noties.markwon.core.MarkwonTheme; -import io.noties.markwon.html.HtmlPlugin; -import io.noties.markwon.html.HtmlTag; -import io.noties.markwon.html.MarkwonHtmlRenderer; -import io.noties.markwon.html.TagHandler; -import io.noties.markwon.image.ImagesPlugin; -import io.noties.markwon.sample.R; -import io.noties.markwon.utils.LeadingMarginUtils; -import io.noties.markwon.utils.NoCopySpannableFactory; - -public class HtmlDetailsActivity extends Activity { - - private ViewGroup content; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_html_details); - - content = findViewById(R.id.content); - - sample_details(); - } - - private void sample_details() { - - final String md = "# Hello\n\n
\n" + - " stuff with \n\n*mark* **down**\n\n\n" + - "

\n\n" + - "\n" + - "## *formatted* **heading** with [a](link)\n" + - "```java\n" + - "code block\n" + - "```\n" + - "\n" + - "

\n" + - " nested stuff

\n" + - "\n" + - "\n" + - "* list\n" + - "* with\n" + - "\n\n" + - "![img](https://raw.githubusercontent.com/noties/Markwon/master/art/markwon_logo.png)\n\n" + - " 1. nested\n" + - " 1. items\n" + - "\n" + - " ```java\n" + - " // including code\n" + - " ```\n" + - " 1. blocks\n" + - "\n" + - "

The 3rd!\n\n" + - "**bold** _em_\n
" + - "

\n" + - "

\n\n" + - "and **this** *is* how..."; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(HtmlPlugin.create(plugin -> - plugin.addHandler(new DetailsTagHandler()))) - .usePlugin(ImagesPlugin.create()) - .build(); - - final Spanned spanned = markwon.toMarkdown(md); - final DetailsParsingSpan[] spans = spanned.getSpans(0, spanned.length(), DetailsParsingSpan.class); - - // if we have no details, proceed as usual (single text-view) - if (spans == null || spans.length == 0) { - // no details - final TextView textView = appendTextView(); - markwon.setParsedMarkdown(textView, spanned); - return; - } - - final List list = new ArrayList<>(); - - for (DetailsParsingSpan span : spans) { - final DetailsElement e = settle(new DetailsElement(spanned.getSpanStart(span), spanned.getSpanEnd(span), span.summary), list); - if (e != null) { - list.add(e); - } - } - - for (DetailsElement element : list) { - initDetails(element, spanned); - } - - sort(list); - - - TextView textView; - int start = 0; - - for (DetailsElement element : list) { - - if (element.start != start) { - // subSequence and add new TextView - textView = appendTextView(); - textView.setText(subSequenceTrimmed(spanned, start, element.start)); - } - - // now add details TextView - textView = appendTextView(); - initDetailsTextView(markwon, textView, element); - - start = element.end; - } - - if (start != spanned.length()) { - // another textView with rest content - textView = appendTextView(); - textView.setText(subSequenceTrimmed(spanned, start, spanned.length())); - } - } - - @NonNull - private TextView appendTextView() { - final View view = getLayoutInflater().inflate(R.layout.view_html_details_text_view, content, false); - final TextView textView = view.findViewById(R.id.text); - content.addView(view); - return textView; - } - - private void initDetailsTextView( - @NonNull Markwon markwon, - @NonNull TextView textView, - @NonNull DetailsElement element) { - - // minor optimization - textView.setSpannableFactory(NoCopySpannableFactory.getInstance()); - - // so, each element with children is a details tag - // there is a reason why we needed the SpannableBuilder in the first place -> we must revert spans -// final SpannableStringBuilder builder = new SpannableStringBuilder(); - final SpannableBuilder builder = new SpannableBuilder(); - append(builder, markwon, textView, element, element); - markwon.setParsedMarkdown(textView, builder.spannableStringBuilder()); - } - - private void append( - @NonNull SpannableBuilder builder, - @NonNull Markwon markwon, - @NonNull TextView textView, - @NonNull DetailsElement root, - @NonNull DetailsElement element) { - if (!element.children.isEmpty()) { - - final int start = builder.length(); - -// builder.append(element.content); - builder.append(subSequenceTrimmed(element.content, 0, element.content.length())); - - builder.setSpan(new ClickableSpan() { - @Override - public void onClick(@NonNull View widget) { - element.expanded = !element.expanded; - - initDetailsTextView(markwon, textView, root); - } - }, start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - - if (element.expanded) { - for (DetailsElement child : element.children) { - append(builder, markwon, textView, root, child); - } - } - - builder.setSpan(new DetailsSpan(markwon.configuration().theme(), element), start); - - } else { - builder.append(element.content); - } - } - - // if null -> remove from where it was processed, - // else replace from where it was processed with a new one (can become expandable) - @Nullable - private static DetailsElement settle( - @NonNull DetailsElement element, - @NonNull List elements) { - for (DetailsElement e : elements) { - if (element.start > e.start && element.end <= e.end) { - final DetailsElement settled = settle(element, e.children); - if (settled != null) { - - // the thing is we must balance children if done like this - // let's just create a tree actually, so we are easier to modify - final Iterator iterator = e.children.iterator(); - while (iterator.hasNext()) { - final DetailsElement balanced = settle(iterator.next(), Collections.singletonList(element)); - if (balanced == null) { - iterator.remove(); - } - } - - // add to our children - e.children.add(element); - } - return null; - } - } - return element; - } - - private static void initDetails(@NonNull DetailsElement element, @NonNull Spanned spanned) { - int end = element.end; - for (int i = element.children.size() - 1; i >= 0; i--) { - final DetailsElement child = element.children.get(i); - if (child.end < end) { - element.children.add(new DetailsElement(child.end, end, spanned.subSequence(child.end, end))); - } - initDetails(child, spanned); - end = child.start; - } - - final int start = (element.start + element.content.length()); - if (end != start) { - element.children.add(new DetailsElement(start, end, spanned.subSequence(start, end))); - } - } - - private static void sort(@NonNull List elements) { - Collections.sort(elements, (o1, o2) -> Integer.compare(o1.start, o2.start)); - for (DetailsElement element : elements) { - sort(element.children); - } - } - - @NonNull - private static CharSequence subSequenceTrimmed(@NonNull CharSequence cs, int start, int end) { - - while (start < end) { - - final boolean isStartEmpty = Character.isWhitespace(cs.charAt(start)); - final boolean isEndEmpty = Character.isWhitespace(cs.charAt(end - 1)); - - if (!isStartEmpty && !isEndEmpty) { - break; - } - - if (isStartEmpty) { - start += 1; - } - if (isEndEmpty) { - end -= 1; - } - } - - return cs.subSequence(start, end); - } - - private static class DetailsElement { - - final int start; - final int end; - final CharSequence content; - final List children = new ArrayList<>(0); - - boolean expanded; - - DetailsElement(int start, int end, @NonNull CharSequence content) { - this.start = start; - this.end = end; - this.content = content; - } - - @Override - public String toString() { - return "DetailsElement{" + - "start=" + start + - ", end=" + end + - ", content=" + toStringContent(content) + - ", children=" + children + - ", expanded=" + expanded + - '}'; - } - - @NonNull - private static String toStringContent(@NonNull CharSequence cs) { - return cs.toString().replaceAll("\n", "\\n"); - } - } - - private static class DetailsTagHandler extends TagHandler { - - @Override - public void handle( - @NonNull MarkwonVisitor visitor, - @NonNull MarkwonHtmlRenderer renderer, - @NonNull HtmlTag tag) { - - int summaryEnd = -1; - - for (HtmlTag child : tag.getAsBlock().children()) { - - if (!child.isClosed()) { - continue; - } - - if ("summary".equals(child.name())) { - summaryEnd = child.end(); - } - - final TagHandler tagHandler = renderer.tagHandler(child.name()); - if (tagHandler != null) { - tagHandler.handle(visitor, renderer, child); - } else if (child.isBlock()) { - visitChildren(visitor, renderer, child.getAsBlock()); - } - } - - if (summaryEnd > -1) { - visitor.builder().setSpan(new DetailsParsingSpan( - subSequenceTrimmed(visitor.builder(), tag.start(), summaryEnd) - ), tag.start(), tag.end()); - } - } - - @NonNull - @Override - public Collection supportedTags() { - return Collections.singleton("details"); - } - } - - private static class DetailsParsingSpan { - - final CharSequence summary; - - DetailsParsingSpan(@NonNull CharSequence summary) { - this.summary = summary; - } - } - - private static class DetailsSpan implements LeadingMarginSpan { - - private final DetailsElement element; - private final int blockMargin; - private final int blockQuoteWidth; - - private final Rect rect = new Rect(); - private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); - - DetailsSpan(@NonNull MarkwonTheme theme, @NonNull DetailsElement element) { - this.element = element; - this.blockMargin = theme.getBlockMargin(); - this.blockQuoteWidth = theme.getBlockQuoteWidth(); - this.paint.setStyle(Paint.Style.FILL); - } - - @Override - public int getLeadingMargin(boolean first) { - return blockMargin; - } - - @Override - public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) { - - if (LeadingMarginUtils.selfStart(start, text, this)) { - rect.set(x, top, x + blockMargin, bottom); - if (element.expanded) { - paint.setColor(Color.GREEN); - } else { - paint.setColor(Color.RED); - } - paint.setStyle(Paint.Style.FILL); - c.drawRect(rect, paint); - - } else { - - if (element.expanded) { - final int l = (blockMargin - blockQuoteWidth) / 2; - rect.set(x + l, top, x + l + blockQuoteWidth, bottom); - paint.setStyle(Paint.Style.FILL); - paint.setColor(Color.GRAY); - c.drawRect(rect, paint); - } - } - } - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/images/ImagesActivity.java b/sample/src/main/java/io/noties/markwon/sample/images/ImagesActivity.java deleted file mode 100644 index 6206a2c4..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/images/ImagesActivity.java +++ /dev/null @@ -1,99 +0,0 @@ -package io.noties.markwon.sample.images; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.text.method.LinkMovementMethod; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.RequestBuilder; -import com.bumptech.glide.request.target.Target; - -import io.noties.markwon.Markwon; -import io.noties.markwon.image.AsyncDrawable; -import io.noties.markwon.image.ImagesPlugin; -import io.noties.markwon.image.glide.GlideImagesPlugin; -import io.noties.markwon.sample.ActivityWithMenuOptions; -import io.noties.markwon.sample.MenuOptions; -import io.noties.markwon.sample.R; - -public class ImagesActivity extends ActivityWithMenuOptions { - - private TextView textView; - - @NonNull - @Override - public MenuOptions menuOptions() { - // todo: same for other plugins - return MenuOptions.create() - .add("glide-singleImage", this::glideSingleImage) - .add("glide-singleImageWithPlaceholder", this::glideSingleImageWithPlaceholder) - .add("click", this::click); - } - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_text_view); - textView = findViewById(R.id.text_view); - - glideSingleImageWithPlaceholder(); - } - - private void glideSingleImage() { - final String md = "[![undefined](https://img.youtube.com/vi/gs1I8_m4AOM/0.jpg)](https://www.youtube.com/watch?v=gs1I8_m4AOM)"; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(GlideImagesPlugin.create(this)) - .build(); - - markwon.setMarkdown(textView, md); - } - - // can be checked when used first, otherwise works as expected... - private void glideSingleImageWithPlaceholder() { - final String md = "[![undefined](https://img.youtube.com/vi/gs1I8_m4AOM/0.jpg)](https://www.youtube.com/watch?v=gs1I8_m4AOM)"; - - final Context context = this; - - final Markwon markwon = Markwon.builder(context) - .usePlugin(GlideImagesPlugin.create(new GlideImagesPlugin.GlideStore() { - @NonNull - @Override - public RequestBuilder load(@NonNull AsyncDrawable drawable) { -// final Drawable placeholder = ContextCompat.getDrawable(context, R.drawable.ic_home_black_36dp); -// placeholder.setBounds(0, 0, 100, 100); - return Glide.with(context) - .load(drawable.getDestination()) -// .placeholder(ContextCompat.getDrawable(context, R.drawable.ic_home_black_36dp)); -// .placeholder(placeholder); - .placeholder(R.drawable.ic_home_black_36dp); - } - - @Override - public void cancel(@NonNull Target target) { - Glide.with(context) - .clear(target); - } - })) - .build(); - - markwon.setMarkdown(textView, md); - } - - private void click() { - - textView.setMovementMethod(LinkMovementMethod.getInstance()); - - final String md = "[![markdown](https://www.mdeditor.com/images/logos/markdown.png \"markdown\")](https://www.mdeditor.com/images/logos/markdown.png)"; - final Markwon markwon = Markwon.builder(this) - .usePlugin(ImagesPlugin.create()) - .build(); - markwon.setMarkdown(textView, md); - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/inlineparser/InlineParserActivity.java b/sample/src/main/java/io/noties/markwon/sample/inlineparser/InlineParserActivity.java deleted file mode 100644 index 33ac4158..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/inlineparser/InlineParserActivity.java +++ /dev/null @@ -1,425 +0,0 @@ -package io.noties.markwon.sample.inlineparser; - -import android.app.Activity; -import android.graphics.Point; -import android.os.Bundle; -import android.text.Layout; -import android.text.Spannable; -import android.text.TextPaint; -import android.text.style.ClickableSpan; -import android.view.Gravity; -import android.view.View; -import android.view.Window; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.commonmark.internal.inline.AsteriskDelimiterProcessor; -import org.commonmark.internal.inline.UnderscoreDelimiterProcessor; -import org.commonmark.node.Block; -import org.commonmark.node.BlockQuote; -import org.commonmark.node.CustomNode; -import org.commonmark.node.FencedCodeBlock; -import org.commonmark.node.Heading; -import org.commonmark.node.HtmlBlock; -import org.commonmark.node.IndentedCodeBlock; -import org.commonmark.node.ListBlock; -import org.commonmark.node.Node; -import org.commonmark.node.ThematicBreak; -import org.commonmark.parser.InlineParserFactory; -import org.commonmark.parser.Parser; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import io.noties.markwon.AbstractMarkwonPlugin; -import io.noties.markwon.Markwon; -import io.noties.markwon.MarkwonVisitor; -import io.noties.markwon.inlineparser.BackticksInlineProcessor; -import io.noties.markwon.inlineparser.BangInlineProcessor; -import io.noties.markwon.inlineparser.CloseBracketInlineProcessor; -import io.noties.markwon.inlineparser.HtmlInlineProcessor; -import io.noties.markwon.inlineparser.InlineProcessor; -import io.noties.markwon.inlineparser.MarkwonInlineParser; -import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin; -import io.noties.markwon.inlineparser.OpenBracketInlineProcessor; -import io.noties.markwon.sample.ActivityWithMenuOptions; -import io.noties.markwon.sample.MenuOptions; -import io.noties.markwon.sample.R; - -public class InlineParserActivity extends ActivityWithMenuOptions { - - private TextView textView; - - @NonNull - @Override - public MenuOptions menuOptions() { - return MenuOptions.create() - .add("links_only", this::links_only) - .add("disable_code", this::disable_code) - .add("pluginWithDefaults", this::pluginWithDefaults) - .add("pluginNoDefaults", this::pluginNoDefaults) - .add("disableHtmlInlineParser", this::disableHtmlInlineParser) - .add("disableHtmlSanitize", this::disableHtmlSanitize) - .add("tooltip", this::tooltip); - } - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_text_view); - - textView = findViewById(R.id.text_view); - -// links_only(); - - disable_code(); - } - - private void links_only() { - - // create an inline-parser-factory that will _ONLY_ parse links - // this would mean: - // * no emphasises (strong and regular aka bold and italics), - // * no images, - // * no code, - // * no HTML entities (&) - // * no HTML tags - // markdown blocks are still parsed - final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilderNoDefaults() - .referencesEnabled(true) - .addInlineProcessor(new OpenBracketInlineProcessor()) - .addInlineProcessor(new CloseBracketInlineProcessor()) - .build(); - - final Markwon markwon = Markwon.builder(this) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureParser(@NonNull Parser.Builder builder) { - builder.inlineParserFactory(inlineParserFactory); - } - }) - .build(); - - // note that image is considered a link now - final String md = "**bold_bold-italic_** html-u, [link](#) ![alt](#image) `code`"; - markwon.setMarkdown(textView, md); - } - - private void disable_code() { - // parses all as usual, but ignores code (inline and block) - - final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder() - .excludeInlineProcessor(BackticksInlineProcessor.class) - .build(); - - // unfortunately there is no _exclude_ method for parser-builder - final Set> enabledBlocks = new HashSet>() {{ - // IndentedCodeBlock.class and FencedCodeBlock.class are missing - // this is full list (including above) that can be passed to `enabledBlockTypes` method - addAll(Arrays.asList( - BlockQuote.class, - Heading.class, - HtmlBlock.class, - ThematicBreak.class, - ListBlock.class)); - }}; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureParser(@NonNull Parser.Builder builder) { - builder - .inlineParserFactory(inlineParserFactory) - .enabledBlockTypes(enabledBlocks); - } - }) - .build(); - - final String md = "# Head!\n\n" + - "* one\n" + - "+ two\n\n" + - "and **bold** to `you`!\n\n" + - "> a quote _em_\n\n" + - "```java\n" + - "final int i = 0;\n" + - "```\n\n" + - "**Good day!**"; - markwon.setMarkdown(textView, md); - } - - private void pluginWithDefaults() { - // a plugin with defaults registered - - final String md = "no [links](#) for **you** `code`!"; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(MarkwonInlineParserPlugin.create()) - // the same as: -// .usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilder())) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configure(@NonNull Registry registry) { - registry.require(MarkwonInlineParserPlugin.class, plugin -> { - plugin.factoryBuilder() - .excludeInlineProcessor(OpenBracketInlineProcessor.class); - }); - } - }) - .build(); - - markwon.setMarkdown(textView, md); - } - - private void pluginNoDefaults() { - // a plugin with NO defaults registered - - final String md = "no [links](#) for **you** `code`!"; - - final Markwon markwon = Markwon.builder(this) - // pass `MarkwonInlineParser.factoryBuilderNoDefaults()` no disable all - .usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilderNoDefaults())) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configure(@NonNull Registry registry) { - registry.require(MarkwonInlineParserPlugin.class, plugin -> { - plugin.factoryBuilder() - .addInlineProcessor(new BackticksInlineProcessor()); - }); - } - }) - .build(); - - markwon.setMarkdown(textView, md); - } - - private void disableHtmlInlineParser() { - final String md = "# Html disabled\n\n" + - "emphasis strong\n\n" + - "

paragraph

\n\n" + - "\n\n" + - ""; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(MarkwonInlineParserPlugin.create()) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configure(@NonNull Registry registry) { - // NB! `AsteriskDelimiterProcessor` and `UnderscoreDelimiterProcessor` - // handles both emphasis and strong-emphasis nodes - registry.require(MarkwonInlineParserPlugin.class, plugin -> { - plugin.factoryBuilder() - .excludeInlineProcessor(HtmlInlineProcessor.class) - .excludeInlineProcessor(BangInlineProcessor.class) - .excludeInlineProcessor(OpenBracketInlineProcessor.class) - .excludeDelimiterProcessor(AsteriskDelimiterProcessor.class) - .excludeDelimiterProcessor(UnderscoreDelimiterProcessor.class); - }); - } - - @Override - public void configureParser(@NonNull Parser.Builder builder) { - builder.enabledBlockTypes(new HashSet<>(Arrays.asList( - Heading.class, -// HtmlBlock.class, - ThematicBreak.class, - FencedCodeBlock.class, - IndentedCodeBlock.class, - BlockQuote.class, - ListBlock.class - ))); - } - }) - .build(); - - markwon.setMarkdown(textView, md); - } - - private void disableHtmlSanitize() { - final String md = "# Html disabled\n\n" + - "emphasis strong\n\n" + - "

paragraph

\n\n" + - "\n\n" + - ""; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(new AbstractMarkwonPlugin() { - @NonNull - @Override - public String processMarkdown(@NonNull String markdown) { - return markdown - .replaceAll("<", "<") - .replaceAll(">", ">"); - } - }) - .build(); - - markwon.setMarkdown(textView, md); - } - - private void tooltip() { - // NB! tooltip contents cannot have new lines - final String md = "" + - "\n" + - "\n" + - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi vitae enim ut sem aliquet ultrices. Nunc a accumsan orci. Suspendisse tortor ante, lacinia ac scelerisque sed, dictum eget metus. Morbi ante augue, tristique eget quam in, vestibulum rutrum lacus. Nulla aliquam auctor cursus. Nulla at lacus condimentum, viverra lacus eget, sollicitudin ex. Cras efficitur leo dui, sit amet rutrum tellus venenatis et. Sed in facilisis libero. Etiam ultricies, nulla ut venenatis tincidunt, tortor erat tristique ante, non aliquet massa arcu eget nisl. Etiam gravida erat ante, sit amet lobortis mauris commodo nec. Praesent vitae sodales quam. Vivamus condimentum porta suscipit. Donec posuere id felis ac scelerisque. Vestibulum lacinia et leo id lobortis. Sed vitae dolor nec ligula dapibus finibus vel eu libero. Nam tincidunt maximus elit, sit amet tincidunt lacus laoreet malesuada.\n" + - "\n" + - "Aenean at urna leo. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla facilisi. Integer lectus elit, congue a orci sed, dignissim sagittis sem. Aenean et pretium magna, nec ornare justo. Sed quis nunc blandit, luctus justo eget, pellentesque arcu. Pellentesque porta semper tristique. Donec et odio arcu. Nullam ultrices gravida congue. Praesent vel leo sed orci tempor luctus. Vivamus eget tortor arcu. Nullam sapien nulla, iaculis sit amet semper in, mattis nec metus. In porttitor augue id elit euismod mattis. Ut est justo, dapibus suscipit erat eu, pellentesque porttitor magna.\n" + - "\n" + - "Nunc porta orci eget dictum malesuada. Donec vehicula felis sit amet leo tincidunt placerat. Cras quis elit faucibus, porta elit at, sodales tortor. Donec elit mi, eleifend et maximus vitae, pretium varius velit. Integer maximus egestas urna, at semper augue egestas vitae. Phasellus arcu tellus, tincidunt eget tellus nec, hendrerit mollis mauris. Pellentesque commodo urna quis nisi ultrices, quis vehicula felis ultricies. Vivamus eu feugiat leo.\n" + - "\n" + - "Etiam sit amet lorem et eros suscipit rhoncus a a tellus. Sed pharetra dui purus, quis molestie leo congue nec. Suspendisse sed scelerisque quam. Vestibulum non laoreet felis. Fusce interdum euismod purus at scelerisque. Vivamus tempus varius nibh, sed accumsan nisl interdum non. Pellentesque rutrum egestas eros sit amet sollicitudin. Vivamus ultrices est erat. Curabitur gravida justo non felis euismod mollis. Ut porta finibus nulla, sed pellentesque purus euismod ac.\n" + - "\n" + - "Aliquam erat volutpat. Nullam suscipit sit amet tortor vel fringilla. Nulla facilisi. Nullam lacinia ex lacus, sit amet scelerisque justo semper a. Nullam ullamcorper, erat ac malesuada porta, augue erat sagittis mi, in auctor turpis mauris nec orci. Nunc sit amet felis placerat, pharetra diam nec, dapibus metus. Proin nulla orci, iaculis vitae vulputate vel, placerat ac erat. Morbi sit amet blandit velit. Cras consectetur vehicula lacus vel sagittis. Nunc tincidunt lacus in blandit faucibus. Curabitur vestibulum auctor vehicula. Sed quis ligula sit amet quam venenatis venenatis eget id felis. Maecenas feugiat nisl elit, facilisis tempus risus malesuada quis. " + - "# Hello tooltip!\n\n" + - "This is the !{tooltip label}(and actual content comes here)\n\n" + - "what if it is !{here}(The contents can be blocks, limited though) instead?\n\n" + - "![image](#) anyway"; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(MarkwonInlineParserPlugin.create(factoryBuilder -> - factoryBuilder.addInlineProcessor(new TooltipInlineProcessor()))) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { - builder.on(TooltipNode.class, (visitor, tooltipNode) -> { - final int start = visitor.length(); - visitor.builder().append(tooltipNode.label); - visitor.setSpans(start, new TooltipSpan(tooltipNode.contents)); - }); - } - }) - .build(); - - markwon.setMarkdown(textView, md); - } - - private static class TooltipInlineProcessor extends InlineProcessor { - - // NB! without bang - // `\\{` is required (although marked as redundant), without it - runtime crash - @SuppressWarnings("RegExpRedundantEscape") - private static final Pattern RE = Pattern.compile("\\{(.+?)\\}\\((.+?)\\)"); - - @Override - public char specialCharacter() { - return '!'; - } - - @Nullable - @Override - protected Node parse() { - final String match = match(RE); - if (match == null) { - return null; - } - - final Matcher matcher = RE.matcher(match); - if (matcher.matches()) { - final String label = matcher.group(1); - final String contents = matcher.group(2); - return new TooltipNode(label, contents); - } - - return null; - } - } - - private static class TooltipNode extends CustomNode { - final String label; - final String contents; - - TooltipNode(@NonNull String label, @NonNull String contents) { - this.label = label; - this.contents = contents; - } - } - - private static class TooltipSpan extends ClickableSpan { - final String contents; - - TooltipSpan(@NonNull String contents) { - this.contents = contents; - } - - @Override - public void onClick(@NonNull View widget) { - // just to be safe - if (!(widget instanceof TextView)) { - return; - } - - final TextView textView = (TextView) widget; - final Spannable spannable = (Spannable) textView.getText(); - - // find self ending position (can also obtain start) -// final int start = spannable.getSpanStart(this); - final int end = spannable.getSpanEnd(this); - - // weird, didn't find self - if (/*start < 0 ||*/ end < 0) { - return; - } - - final Layout layout = textView.getLayout(); - if (layout == null) { - // also weird - return; - } - - final int line = layout.getLineForOffset(end); - - // position inside TextView, these values must also be adjusted to parent widget - // also note that container can - final int y = layout.getLineBottom(line); - final int x = (int) (layout.getPrimaryHorizontal(end) + 0.5F); - - final Window window = ((Activity) widget.getContext()).getWindow(); - final View decor = window.getDecorView(); - final Point point = relativeTo(decor, widget); - -// new Tooltip.Builder(widget.getContext()) -// .anchor(x + point.x, y + point.y) -// .text(contents) -// .create() -// .show(widget, Tooltip.Gravity.TOP, false); - - // Toast is not reliable when tried to position on the screen - // but anyway, this is to showcase only - final Toast toast = Toast.makeText(widget.getContext(), contents, Toast.LENGTH_LONG); - toast.setGravity(Gravity.TOP | Gravity.START, x + point.x, y + point.y); - toast.show(); - } - - @Override - public void updateDrawState(@NonNull TextPaint ds) { - // can customize appearance here as spans will be rendered as links - super.updateDrawState(ds); - } - - @NonNull - private static Point relativeTo(@NonNull View parent, @NonNull View who) { - return relativeTo(parent, who, new Point()); - } - - @NonNull - private static Point relativeTo(@NonNull View parent, @NonNull View who, @NonNull Point point) { - // NB! the scroll adjustments (we are interested in screen position, - // not real position inside parent) - point.x += who.getLeft(); - point.y += who.getTop(); - point.x -= who.getScrollX(); - point.y -= who.getScrollY(); - if (who != parent - && who.getParent() instanceof View) { - relativeTo(parent, (View) who.getParent(), point); - } - return point; - } - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/latex/LatexActivity.java b/sample/src/main/java/io/noties/markwon/sample/latex/LatexActivity.java deleted file mode 100644 index d2cd69ac..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/latex/LatexActivity.java +++ /dev/null @@ -1,271 +0,0 @@ -package io.noties.markwon.sample.latex; - -import android.content.res.Resources; -import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.view.View; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; - -import io.noties.debug.Debug; -import io.noties.markwon.Markwon; -import io.noties.markwon.ext.latex.JLatexMathPlugin; -import io.noties.markwon.ext.latex.JLatexMathTheme; -import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin; -import io.noties.markwon.sample.ActivityWithMenuOptions; -import io.noties.markwon.sample.MenuOptions; -import io.noties.markwon.sample.R; - -public class LatexActivity extends ActivityWithMenuOptions { - - private static final String LATEX_ARRAY; - - static { - String latex = "\\begin{array}{l}"; - latex += "\\forall\\varepsilon\\in\\mathbb{R}_+^*\\ \\exists\\eta>0\\ |x-x_0|\\leq\\eta\\Longrightarrow|f(x)-f(x_0)|\\leq\\varepsilon\\\\"; - latex += "\\det\\begin{bmatrix}a_{11}&a_{12}&\\cdots&a_{1n}\\\\a_{21}&\\ddots&&\\vdots\\\\\\vdots&&\\ddots&\\vdots\\\\a_{n1}&\\cdots&\\cdots&a_{nn}\\end{bmatrix}\\overset{\\mathrm{def}}{=}\\sum_{\\sigma\\in\\mathfrak{S}_n}\\varepsilon(\\sigma)\\prod_{k=1}^n a_{k\\sigma(k)}\\\\"; - latex += "\\sideset{_\\alpha^\\beta}{_\\gamma^\\delta}{\\begin{pmatrix}a&b\\\\c&d\\end{pmatrix}}\\\\"; - latex += "\\int_0^\\infty{x^{2n} e^{-a x^2}\\,dx} = \\frac{2n-1}{2a} \\int_0^\\infty{x^{2(n-1)} e^{-a x^2}\\,dx} = \\frac{(2n-1)!!}{2^{n+1}} \\sqrt{\\frac{\\pi}{a^{2n+1}}}\\\\"; - latex += "\\int_a^b{f(x)\\,dx} = (b - a) \\sum\\limits_{n = 1}^\\infty {\\sum\\limits_{m = 1}^{2^n - 1} {\\left( { - 1} \\right)^{m + 1} } } 2^{ - n} f(a + m\\left( {b - a} \\right)2^{-n} )\\\\"; - latex += "\\int_{-\\pi}^{\\pi} \\sin(\\alpha x) \\sin^n(\\beta x) dx = \\textstyle{\\left \\{ \\begin{array}{cc} (-1)^{(n+1)/2} (-1)^m \\frac{2 \\pi}{2^n} \\binom{n}{m} & n \\mbox{ odd},\\ \\alpha = \\beta (2m-n) \\\\ 0 & \\mbox{otherwise} \\\\ \\end{array} \\right .}\\\\"; - latex += "L = \\int_a^b \\sqrt{ \\left|\\sum_{i,j=1}^ng_{ij}(\\gamma(t))\\left(\\frac{d}{dt}x^i\\circ\\gamma(t)\\right)\\left(\\frac{d}{dt}x^j\\circ\\gamma(t)\\right)\\right|}\\,dt\\\\"; - latex += "\\begin{array}{rl} s &= \\int_a^b\\left\\|\\frac{d}{dt}\\vec{r}\\,(u(t),v(t))\\right\\|\\,dt \\\\ &= \\int_a^b \\sqrt{u'(t)^2\\,\\vec{r}_u\\cdot\\vec{r}_u + 2u'(t)v'(t)\\, \\vec{r}_u\\cdot\\vec{r}_v+ v'(t)^2\\,\\vec{r}_v\\cdot\\vec{r}_v}\\,\\,\\, dt. \\end{array}\\\\"; - latex += "\\end{array}"; - LATEX_ARRAY = latex; - } - - private static final String LATEX_LONG_DIVISION = "\\text{A long division \\longdiv{12345}{13}"; - private static final String LATEX_BANGLE = "{a \\bangle b} {c \\brace d} {e \\brack f} {g \\choose h}"; - private static final String LATEX_BOXES; - - static { - String latex = "\\begin{array}{cc}"; - latex += "\\fbox{\\text{A framed box with \\textdbend}}&\\shadowbox{\\text{A shadowed box}}\\cr"; - latex += "\\doublebox{\\text{A double framed box}}&\\ovalbox{\\text{An oval framed box}}\\cr"; - latex += "\\end{array}"; - LATEX_BOXES = latex; - } - - private TextView textView; - private View parent; - - @NonNull - @Override - public MenuOptions menuOptions() { - return MenuOptions.create() - .add("array", this::array) - .add("longDivision", this::longDivision) - .add("bangle", this::bangle) - .add("boxes", this::boxes) - .add("insideBlockQuote", this::insideBlockQuote) - .add("error", this::error) - .add("legacy", this::legacy) - .add("textColor", this::textColor) - .add("defaultTextColor", this::defaultTextColor) - .add("inlineAndBlock", this::inlineAndBlock) - .add("dark", this::dark) - .add("omega", this::omega); - } - - @Override - protected void beforeOptionSelected(@NonNull String option) { - super.beforeOptionSelected(option); - - // reset text color - textView.setTextColor(0xFF000000); - - // reset background - parent.setBackgroundColor(0xFFffffff); - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_text_view); - - textView = findViewById(R.id.text_view); - parent = findViewById(R.id.scroll_view); - -// array(); - longDivision(); - } - - private void array() { - renderWithBlocksAndInlines(wrapLatexInSampleMarkdown(LATEX_ARRAY)); - } - - private void longDivision() { - renderWithBlocksAndInlines(wrapLatexInSampleMarkdown(LATEX_LONG_DIVISION)); - } - - private void bangle() { - renderWithBlocksAndInlines(wrapLatexInSampleMarkdown(LATEX_BANGLE)); - } - - private void boxes() { - renderWithBlocksAndInlines(wrapLatexInSampleMarkdown(LATEX_BOXES)); - } - - private void insideBlockQuote() { - String latex = "W=W_1+W_2=F_1X_1-F_2X_2"; - final String md = "" + - "# LaTeX inside a blockquote\n" + - "> $$" + latex + "$$\n"; - renderWithBlocksAndInlines(md); - } - - private void error() { - final String md = wrapLatexInSampleMarkdown("\\sum_{i=0}^\\infty x \\cdot 0 \\rightarrow \\iMightNotExist{0}"); - - final Markwon markwon = Markwon.builder(this) - .usePlugin(MarkwonInlineParserPlugin.create()) - .usePlugin(JLatexMathPlugin.create(textView.getTextSize(), builder -> { - builder.inlinesEnabled(true); - //noinspection Convert2Lambda - builder.errorHandler(new JLatexMathPlugin.ErrorHandler() { - @Nullable - @Override - public Drawable handleError(@Nullable String latex, @NonNull Throwable error) { - Debug.e(error, latex); - return ContextCompat.getDrawable(LatexActivity.this, R.drawable.ic_android_black_24dp); - } - }); - })) - .build(); - - markwon.setMarkdown(textView, md); - } - - private void legacy() { - final String md = wrapLatexInSampleMarkdown(LATEX_BANGLE); - - final Markwon markwon = Markwon.builder(this) - // LEGACY does not require inline parser - .usePlugin(JLatexMathPlugin.create(textView.getTextSize(), builder -> { - builder.blocksLegacy(true); - builder.theme() - .backgroundProvider(() -> new ColorDrawable(0x100000ff)) - .padding(JLatexMathTheme.Padding.all(48)); - })) - .build(); - - markwon.setMarkdown(textView, md); - } - - private void textColor() { - final String md = wrapLatexInSampleMarkdown(LATEX_LONG_DIVISION); - final Markwon markwon = Markwon.builder(this) - .usePlugin(MarkwonInlineParserPlugin.create()) - .usePlugin(JLatexMathPlugin.create(textView.getTextSize(), builder -> { - builder.inlinesEnabled(true); - builder.theme() - .inlineTextColor(Color.RED) - .blockTextColor(Color.GREEN) - .inlineBackgroundProvider(() -> new ColorDrawable(Color.YELLOW)) - .blockBackgroundProvider(() -> new ColorDrawable(Color.GRAY)); - })) - .build(); - markwon.setMarkdown(textView, md); - } - - private void defaultTextColor() { - // @since 4.3.0 text color is automatically taken from textView - // (if it's not specified explicitly via configuration) - textView.setTextColor(0xFFff0000); - - final String md = wrapLatexInSampleMarkdown(LATEX_LONG_DIVISION); - final Markwon markwon = Markwon.builder(this) - .usePlugin(MarkwonInlineParserPlugin.create()) - .usePlugin(JLatexMathPlugin.create(textView.getTextSize(), new JLatexMathPlugin.BuilderConfigure() { - @Override - public void configureBuilder(@NonNull JLatexMathPlugin.Builder builder) { - builder.inlinesEnabled(true); - // override default text color - builder.theme() - .inlineTextColor(0xFF00ffff); - } - })) - .build(); - - markwon.setMarkdown(textView, md); - } - - private void inlineAndBlock() { - final String md = "" + - "# Inline and block\n\n" + - "$$\\int_{a}^{b} f(x)dx = F(b) - F(a)$$\n\n" + - "this was **inline** _LaTeX_ $$\\int_{a}^{b} f(x)dx = F(b) - F(a)$$ and once again it was\n\n" + - "Now a block:\n\n" + - "$$\n" + - "\\int_{a}^{b} f(x)dx = F(b) - F(a)\n" + - "$$\n\n" + - "Not a block (content on delimited line), but inline instead:\n\n" + - "$$\\int_{a}^{b} f(x)dx = F(b) - F(a)$$" + - "\n\n" + - "that's it"; - renderWithBlocksAndInlines(md); - } - - private void dark() { - parent.setBackgroundColor(0xFF000000); - textView.setTextColor(0xFFffffff); - - String latex = "W=W_1+W_2=F_1X_1-F_2X_2"; - final String md = "" + - "# LaTeX inside a blockquote\n" + - "> $$" + latex + "$$\n"; - renderWithBlocksAndInlines(md); - } - - private void omega() { - final String md = "" + - "# Block\n\n" + - "$$\n" + - "\\Omega\n" + - "$$\n\n" + - "# Inline\n\n" + - "$$\\Omega$$"; - - renderWithBlocksAndInlines(md); - } - - @NonNull - private static String wrapLatexInSampleMarkdown(@NonNull String latex) { - return "" + - "# Example of LaTeX\n\n" + - "(inline): $$" + latex + "$$ so nice, really-really really-really really-really? Now, (block):\n\n" + - "$$\n" + - "" + latex + "\n" + - "$$\n\n" + - "the end"; - } - - private void renderWithBlocksAndInlines(@NonNull String markdown) { - - final float textSize = textView.getTextSize(); - final Resources r = getResources(); - - final Markwon markwon = Markwon.builder(this) - // NB! `MarkwonInlineParserPlugin` is required in order to parse inlines - .usePlugin(MarkwonInlineParserPlugin.create()) - .usePlugin(JLatexMathPlugin.create(textSize, textSize * 1.25F, builder -> { - // Important thing to do is to enable inlines (by default disabled) - builder.inlinesEnabled(true); - builder.theme() - .inlineBackgroundProvider(() -> new ColorDrawable(0x1000ff00)) - .blockBackgroundProvider(() -> new ColorDrawable(0x10ff0000)) - .blockPadding(JLatexMathTheme.Padding.symmetric( - r.getDimensionPixelSize(R.dimen.latex_block_padding_vertical), - r.getDimensionPixelSize(R.dimen.latex_block_padding_horizontal) - )); - })) - .build(); - - markwon.setMarkdown(textView, markdown); - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/notification/NotificationActivity.java b/sample/src/main/java/io/noties/markwon/sample/notification/NotificationActivity.java deleted file mode 100644 index 0012f41f..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/notification/NotificationActivity.java +++ /dev/null @@ -1,254 +0,0 @@ -package io.noties.markwon.sample.notification; - -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Typeface; -import android.os.Build; -import android.text.SpannableStringBuilder; -import android.text.Spanned; -import android.text.style.AbsoluteSizeSpan; -import android.text.style.BulletSpan; -import android.text.style.DynamicDrawableSpan; -import android.text.style.ImageSpan; -import android.text.style.QuoteSpan; -import android.text.style.StrikethroughSpan; -import android.text.style.StyleSpan; - -import androidx.annotation.NonNull; - -import org.commonmark.ext.gfm.strikethrough.Strikethrough; -import org.commonmark.node.BlockQuote; -import org.commonmark.node.Emphasis; -import org.commonmark.node.Heading; -import org.commonmark.node.ListItem; -import org.commonmark.node.StrongEmphasis; - -import io.noties.debug.Debug; -import io.noties.markwon.AbstractMarkwonPlugin; -import io.noties.markwon.Markwon; -import io.noties.markwon.MarkwonSpansFactory; -import io.noties.markwon.core.CoreProps; -import io.noties.markwon.ext.strikethrough.StrikethroughPlugin; -import io.noties.markwon.sample.ActivityWithMenuOptions; -import io.noties.markwon.sample.MenuOptions; -import io.noties.markwon.sample.R; - -public class NotificationActivity extends ActivityWithMenuOptions { - - private static final String CHANNEL_ID = "whatever"; - - @NonNull - @Override - public MenuOptions menuOptions() { - return MenuOptions.create() - .add("bold-italic", this::bold_italic) - .add("heading", this::heading) - .add("lists", this::lists) - .add("image", this::image) - .add("link", this::link) - .add("blockquote", this::blockquote) - .add("strikethrough", this::strikethrough); - } - - private void bold_italic() { - // Unfortunately we cannot just use Markwon created CharSequence in a RemoteViews context - // because it requires for spans to be platform ones - - final String md = "Just a **bold** here and _italic_, but what if **it is bold _and italic_**?"; - final Markwon markwon = Markwon.builder(this) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { - builder - .setFactory(StrongEmphasis.class, (configuration, props) -> new StyleSpan(Typeface.BOLD)) - .setFactory(Emphasis.class, (configuration, props) -> new StyleSpan(Typeface.ITALIC)); - } - }) - .build(); - display(markwon.toMarkdown(md)); - } - - private void heading() { - - // please note that heading doesn't seem to be working in remote views, - // tried both `RelativeSizeSpan` and `AbsoluteSizeSpan` with no effect - - final float base = 12; - - final float[] sizes = { - 2.F, 1.5F, 1.17F, 1.F, .83F, .67F, - }; - - final String md = "" + - "# H1\n" + - "## H2\n" + - "### H3\n" + - "#### H4\n" + - "##### H5\n" + - "###### H6\n\n"; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { - builder.setFactory(Heading.class, (configuration, props) -> { - final Integer level = CoreProps.HEADING_LEVEL.get(props); - Debug.i(level); - if (level != null && level > 0 && level <= sizes.length) { -// return new RelativeSizeSpan(sizes[level - 1]); - final Object span = new AbsoluteSizeSpan((int) (base * sizes[level - 1] + .5F), true); - return new Object[]{ - span, - new StyleSpan(Typeface.BOLD) - }; - } - return null; - }); - } - }) - .build(); - display(markwon.toMarkdown(md)); - } - - private void lists() { - final String md = "" + - "* bullet 1\n" + - "* bullet 2\n" + - "* * bullet 2 1\n" + - " * bullet 2 0 1\n" + - "1) order 1\n" + - "1) order 2\n" + - "1) order 3\n"; - - // ordered lists _could_ be translated to raw text representation (`1.`, `1)` etc) in resulting markdown - // or they could be _disabled_ all together... (can ordered lists be disabled in parser?) - - final Markwon markwon = Markwon.builder(this) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { - builder.setFactory(ListItem.class, (configuration, props) -> { - final CoreProps.ListItemType type = CoreProps.LIST_ITEM_TYPE.get(props); - if (type != null) { - // bullet and ordered list share the same markdown node - return new BulletSpan(); - } - return null; - }); - } - }) - .build(); - - display(markwon.toMarkdown(md)); - } - - private void image() { - // please note that image _could_ be supported only if it would be available immediately - // debugging possibility - // - // doesn't seem to be working - - final Bitmap bitmap = Bitmap.createBitmap(128, 256, Bitmap.Config.ARGB_4444); - final Canvas canvas = new Canvas(bitmap); - canvas.drawColor(0xFFAD1457); - - final SpannableStringBuilder builder = new SpannableStringBuilder(); - builder.append("An image: "); - - final int length = builder.length(); - builder.append("[bitmap]"); - builder.setSpan( - new ImageSpan(this, bitmap, DynamicDrawableSpan.ALIGN_BOTTOM), - length, - builder.length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE - ); - - builder.append(" okay, and "); - - final int start = builder.length(); - builder.append("[resource]"); - builder.setSpan( - new ImageSpan(this, R.drawable.ic_memory_black_48dp), - start, - builder.length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE - ); - - display(builder); - } - - private void link() { - final String md = "" + - "[a link](https://isa.link/) is here, styling yes, clicking - no"; - display(Markwon.create(this).toMarkdown(md)); - } - - private void blockquote() { - final String md = "" + - "> This was once said by me\n" + - "> > And this one also\n\n" + - "Me"; - final Markwon markwon = Markwon.builder(this) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { - builder.setFactory(BlockQuote.class, (configuration, props) -> new QuoteSpan()); - } - }) - .build(); - display(markwon.toMarkdown(md)); - } - - private void strikethrough() { - final String md = "~~strike that!~~"; - final Markwon markwon = Markwon.builder(this) - .usePlugin(new StrikethroughPlugin()) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { - builder.setFactory(Strikethrough.class, (configuration, props) -> new StrikethroughSpan()); - } - }) - .build(); - display(markwon.toMarkdown(md)); - } - - private void display(@NonNull CharSequence cs) { - final NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - if (manager == null) { - throw new IllegalStateException("No NotificationManager is available"); - } - - ensureChannel(manager); - - final Notification.Builder builder = new Notification.Builder(this) - .setSmallIcon(R.drawable.ic_stat_name) - .setContentTitle("Markwon") - .setContentText(cs) - .setStyle(new Notification.BigTextStyle().bigText(cs)); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - builder.setChannelId(CHANNEL_ID); - } - - manager.notify(1, builder.build()); - } - - private void ensureChannel(@NonNull NotificationManager manager) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - return; - } - - final NotificationChannel channel = manager.getNotificationChannel(CHANNEL_ID); - if (channel == null) { - manager.createNotificationChannel(new NotificationChannel( - CHANNEL_ID, - CHANNEL_ID, - NotificationManager.IMPORTANCE_DEFAULT)); - } - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/precomputed/PrecomputedActivity.java b/sample/src/main/java/io/noties/markwon/sample/precomputed/PrecomputedActivity.java deleted file mode 100644 index 2628bffd..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/precomputed/PrecomputedActivity.java +++ /dev/null @@ -1,38 +0,0 @@ -package io.noties.markwon.sample.precomputed; - -import android.app.Activity; -import android.os.Bundle; -import android.widget.TextView; - -import androidx.annotation.Nullable; - -import java.util.concurrent.Executors; - -import io.noties.markwon.Markwon; -import io.noties.markwon.PrecomputedTextSetterCompat; -import io.noties.markwon.sample.R; - -public class PrecomputedActivity extends Activity { - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_text_view); - - final Markwon markwon = Markwon.builder(this) - // please note that precomputedTextCompat is no-op on devices lower than L (21) - .textSetter(PrecomputedTextSetterCompat.create(Executors.newCachedThreadPool())) - .build(); - - final TextView textView = findViewById(R.id.text_view); - final String markdown = "# Hello!\n\n" + - "This _displays_ how to implement and use `PrecomputedTextCompat` with the **Markwon**\n\n" + - "> consider using PrecomputedText only if your markdown content is large enough\n> \n" + - "> **please note** that it works badly with `markwon-recycler` due to asynchronous nature"; - - // please note that _sometimes_ (if done without `post` here) further `textView.post` - // (that is used in PrecomputedTextSetterCompat to deliver result to main-thread) won't be called - // making the result of pre-computation absent and text-view clear (no text) - textView.post(() -> markwon.setMarkdown(textView, markdown)); - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/precomputed/PrecomputedFutureActivity.java b/sample/src/main/java/io/noties/markwon/sample/precomputed/PrecomputedFutureActivity.java deleted file mode 100644 index 577f2406..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/precomputed/PrecomputedFutureActivity.java +++ /dev/null @@ -1,95 +0,0 @@ -package io.noties.markwon.sample.precomputed; - -import android.app.Activity; -import android.content.Context; -import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; - -import io.noties.markwon.Markwon; -import io.noties.markwon.PrecomputedFutureTextSetterCompat; -import io.noties.markwon.recycler.MarkwonAdapter; -import io.noties.markwon.sample.R; - -public class PrecomputedFutureActivity extends Activity { - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_recycler); - - final Markwon markwon = Markwon.builder(this) - .textSetter(PrecomputedFutureTextSetterCompat.create()) - .build(); - - // create MarkwonAdapter and register two blocks that will be rendered differently - final MarkwonAdapter adapter = MarkwonAdapter.builder(R.layout.adapter_appcompat_default_entry, R.id.text) - .build(); - - final RecyclerView recyclerView = findViewById(R.id.recycler_view); - recyclerView.setLayoutManager(new LinearLayoutManager(this)); - recyclerView.setHasFixedSize(true); - recyclerView.setAdapter(adapter); - - adapter.setMarkdown(markwon, loadReadMe(this)); - - // please note that we should notify updates (adapter doesn't do it implicitly) - adapter.notifyDataSetChanged(); - } - - @NonNull - private static String loadReadMe(@NonNull Context context) { - InputStream stream = null; - try { - stream = context.getAssets().open("README.md"); - } catch (IOException e) { - e.printStackTrace(); - } - return readStream(stream); - } - - @NonNull - private static String readStream(@Nullable InputStream inputStream) { - - String out = null; - - if (inputStream != null) { - BufferedReader reader = null; - //noinspection TryFinallyCanBeTryWithResources - try { - reader = new BufferedReader(new InputStreamReader(inputStream)); - final StringBuilder builder = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - builder.append(line) - .append('\n'); - } - out = builder.toString(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - if (reader != null) { - try { - reader.close(); - } catch (IOException e) { - // no op - } - } - } - } - - if (out == null) { - throw new RuntimeException("Cannot read stream"); - } - - return out; - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/recycler/RecyclerActivity.java b/sample/src/main/java/io/noties/markwon/sample/recycler/RecyclerActivity.java deleted file mode 100644 index 3d73de6d..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/recycler/RecyclerActivity.java +++ /dev/null @@ -1,212 +0,0 @@ -package io.noties.markwon.sample.recycler; - -import android.app.Activity; -import android.content.Context; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Bundle; -import android.text.TextPaint; -import android.text.TextUtils; -import android.text.style.CharacterStyle; -import android.text.style.UpdateAppearance; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import org.commonmark.ext.gfm.tables.TableBlock; -import org.commonmark.node.FencedCodeBlock; -import org.commonmark.node.Link; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; - -import io.noties.debug.AndroidLogDebugOutput; -import io.noties.debug.Debug; -import io.noties.markwon.AbstractMarkwonPlugin; -import io.noties.markwon.Markwon; -import io.noties.markwon.MarkwonConfiguration; -import io.noties.markwon.MarkwonSpansFactory; -import io.noties.markwon.MarkwonVisitor; -import io.noties.markwon.core.CorePlugin; -import io.noties.markwon.html.HtmlPlugin; -import io.noties.markwon.image.ImagesPlugin; -import io.noties.markwon.image.destination.ImageDestinationProcessor; -import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute; -import io.noties.markwon.image.file.FileSchemeHandler; -import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler; -import io.noties.markwon.image.svg.SvgMediaDecoder; -import io.noties.markwon.movement.MovementMethodPlugin; -import io.noties.markwon.recycler.MarkwonAdapter; -import io.noties.markwon.recycler.SimpleEntry; -import io.noties.markwon.recycler.table.TableEntry; -import io.noties.markwon.recycler.table.TableEntryPlugin; -import io.noties.markwon.sample.R; - -public class RecyclerActivity extends Activity { - - static { - Debug.init(new AndroidLogDebugOutput(true)); - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_recycler); - - // create MarkwonAdapter and register two blocks that will be rendered differently - // * fenced code block (can also specify the same Entry for indended code block) - // * table block - final MarkwonAdapter adapter = MarkwonAdapter.builder(R.layout.adapter_default_entry, R.id.text) - // we can simply use bundled SimpleEntry - .include(FencedCodeBlock.class, SimpleEntry.create(R.layout.adapter_fenced_code_block, R.id.text)) - .include(TableBlock.class, TableEntry.create(builder -> builder - .tableLayout(R.layout.adapter_table_block, R.id.table_layout) - .textLayoutIsRoot(R.layout.view_table_entry_cell))) - .build(); - - final RecyclerView recyclerView = findViewById(R.id.recycler_view); - recyclerView.setLayoutManager(new LinearLayoutManager(this)); - recyclerView.setHasFixedSize(true); - recyclerView.setAdapter(adapter); - - final Markwon markwon = markwon(this); - adapter.setMarkdown(markwon, loadReadMe(this)); - - // please note that we should notify updates (adapter doesn't do it implicitly) - adapter.notifyDataSetChanged(); - } - - @NonNull - private static Markwon markwon(@NonNull Context context) { - return Markwon.builder(context) - .usePlugin(ImagesPlugin.create(plugin -> { - plugin - .addSchemeHandler(FileSchemeHandler.createWithAssets(context)) - .addSchemeHandler(OkHttpNetworkSchemeHandler.create()) - .placeholderProvider(drawable -> { - final Drawable placeholder = new ColorDrawable(0xFFff0000); - placeholder.setBounds(0, 0, 100, 100); - return placeholder; - }); - })) -// .usePlugin(PicassoImagesPlugin.create(context)) -// .usePlugin(GlideImagesPlugin.create(context)) -// .usePlugin(CoilImagesPlugin.create(context)) - // important to use TableEntryPlugin instead of TablePlugin - .usePlugin(TableEntryPlugin.create(context)) - .usePlugin(HtmlPlugin.create()) -// .usePlugin(SyntaxHighlightPlugin.create()) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { - builder.imageDestinationProcessor(new ImageDestinationProcessorInitialReadme()); - } - - @Override - public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { - builder.on(FencedCodeBlock.class, (visitor, fencedCodeBlock) -> { - // we actually won't be applying code spans here, as our custom view will - // draw background and apply mono typeface - // - // NB the `trim` operation on literal (as code will have a new line at the end) - final CharSequence code = visitor.configuration() - .syntaxHighlight() - .highlight(fencedCodeBlock.getInfo(), fencedCodeBlock.getLiteral().trim()); - visitor.builder().append(code); - }); - } - - @Override - public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { - // `RemoveUnderlineSpan` will be added AFTER original, thus it will remove underline applied by original - builder.appendFactory(Link.class, (configuration, props) -> new RemoveUnderlineSpan()); - } - - - }) - .build(); - } - - private static class RemoveUnderlineSpan extends CharacterStyle implements UpdateAppearance { - - @Override - public void updateDrawState(TextPaint tp) { - tp.setUnderlineText(false); - } - } - - @NonNull - private static String loadReadMe(@NonNull Context context) { - InputStream stream = null; - try { - stream = context.getAssets().open("README.md"); - } catch (IOException e) { - e.printStackTrace(); - } - return readStream(stream); - } - - @NonNull - private static String readStream(@Nullable InputStream inputStream) { - - String out = null; - - if (inputStream != null) { - BufferedReader reader = null; - //noinspection TryFinallyCanBeTryWithResources - try { - reader = new BufferedReader(new InputStreamReader(inputStream)); - final StringBuilder builder = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - builder.append(line) - .append('\n'); - } - out = builder.toString(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - if (reader != null) { - try { - reader.close(); - } catch (IOException e) { - // no op - } - } - } - } - - if (out == null) { - throw new RuntimeException("Cannot read stream"); - } - - return out; - } - - private static class ImageDestinationProcessorInitialReadme extends ImageDestinationProcessor { - - private static final String GITHUB_BASE = "https://github.com/noties/Markwon/raw/master/"; - - private final ImageDestinationProcessorRelativeToAbsolute processor - = new ImageDestinationProcessorRelativeToAbsolute(GITHUB_BASE); - - @NonNull - @Override - public String process(@NonNull String destination) { - String out; - final Uri uri = Uri.parse(destination); - if (TextUtils.isEmpty(uri.getScheme())) { - out = processor.process(destination); - } else { - out = destination; - } - return out; - } - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/recycler/TriggerParentHotspot.java b/sample/src/main/java/io/noties/markwon/sample/recycler/TriggerParentHotspot.java deleted file mode 100644 index 08e583a0..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/recycler/TriggerParentHotspot.java +++ /dev/null @@ -1,149 +0,0 @@ -package io.noties.markwon.sample.recycler; - -import android.graphics.Canvas; -import android.graphics.ColorFilter; -import android.graphics.PixelFormat; -import android.graphics.Point; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.os.Build; -import android.util.StateSet; -import android.view.View; -import android.view.ViewTreeObserver; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; - -public class TriggerParentHotspot { - - public static void init(@NonNull final View parent, @NonNull final View view) { - final Drawable background = parent.getBackground(); - if (background == null) { - view.setBackground(null); - return; - } - - final Wrapper wrapper = ensureWrapper(view); - wrapper.wrapped = background; - - view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { - @Override - public boolean onPreDraw() { - final Point point = relativeTo(parent, view, POINT); - wrapper.offsetHorizontal = point.x; - wrapper.offsetVertical = point.y; - view.getViewTreeObserver().removeOnPreDrawListener(this); - return true; - } - }); - } - - private static final Point POINT = new Point(); - - @NonNull - private static Wrapper ensureWrapper(@NonNull View view) { - final Drawable drawable = view.getBackground(); - if (!(drawable instanceof Wrapper)) { - final Wrapper wrapper = new Wrapper(); - view.setBackground(wrapper); - return wrapper; - } - return (Wrapper) drawable; - } - - @NonNull - private static Point relativeTo(@NonNull View parent, @NonNull View who, @NonNull Point point) { - point.x += who.getLeft(); - point.y += who.getTop(); - if (who != parent - && who.getParent() instanceof View) { - relativeTo(parent, (View) who.getParent(), point); - } - return point; - } - - private static class Wrapper extends Drawable { - - private Drawable wrapped; - - private int offsetHorizontal; - private int offsetVertical; - - @Override - public void draw(@NonNull Canvas canvas) { - // no op - } - - @Override - public void setAlpha(int alpha) { - // no op - } - - @Override - public void setColorFilter(@Nullable ColorFilter colorFilter) { - // no op - } - - @Override - public int getOpacity() { - if (wrapped == null) { - return PixelFormat.TRANSPARENT; - } - return wrapped.getOpacity(); - } - - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - @Override - public void setHotspot(float x, float y) { - if (wrapped != null) { - wrapped.setHotspot(x + offsetHorizontal, y + offsetVertical); - } - } - - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - @Override - public void setHotspotBounds(int left, int top, int right, int bottom) { - if (wrapped != null) { - wrapped.setHotspotBounds( - left + offsetHorizontal, - top + offsetVertical, - right + offsetHorizontal, - bottom + offsetVertical - ); - } - } - - @RequiresApi(Build.VERSION_CODES.M) - @Override - public void getHotspotBounds(@NonNull Rect outRect) { - if (wrapped != null) { - wrapped.getHotspotBounds(outRect); - if (!outRect.isEmpty()) { - outRect.set( - outRect.left - offsetHorizontal, - outRect.top - offsetVertical, - outRect.right - offsetHorizontal, - outRect.bottom - offsetVertical - ); - } - } - } - - @Override - public boolean isStateful() { - return wrapped != null && wrapped.isStateful(); - } - - @Override - public boolean setState(@NonNull int[] stateSet) { - return wrapped != null && wrapped.setState(stateSet); - } - - @NonNull - @Override - public int[] getState() { - return wrapped != null ? wrapped.getState() : StateSet.WILD_CARD; - } - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/simpleext/SimpleExtActivity.java b/sample/src/main/java/io/noties/markwon/sample/simpleext/SimpleExtActivity.java deleted file mode 100644 index 77902e16..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/simpleext/SimpleExtActivity.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.noties.markwon.sample.simpleext; - -import android.app.Activity; -import android.graphics.Color; -import android.os.Bundle; -import android.text.style.ForegroundColorSpan; -import android.widget.TextView; - -import androidx.annotation.Nullable; - -import io.noties.markwon.Markwon; -import io.noties.markwon.core.spans.EmphasisSpan; -import io.noties.markwon.core.spans.StrongEmphasisSpan; -import io.noties.markwon.sample.R; -import io.noties.markwon.simple.ext.SimpleExtPlugin; - -public class SimpleExtActivity extends Activity { - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_text_view); - - final TextView textView = findViewById(R.id.text_view); - - final Markwon markwon = Markwon.builder(this) - .usePlugin(SimpleExtPlugin.create(plugin -> plugin - // +sometext+ - .addExtension(1, '+', (_1, _2) -> new EmphasisSpan()) - // ??sometext?? - .addExtension(2, '?', (_1, _2) -> new StrongEmphasisSpan()) - // @@sometext$$ - .addExtension(2, '@', '$', (_1, _2) -> new ForegroundColorSpan(Color.RED)))) - .build(); - - final String markdown = "# SimpleExt\n" + - "\n" + - "+let's start with `+`, ??then we can use this, and finally @@this$$??+"; - - markwon.setMarkdown(textView, markdown); - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/table/TableActivity.java b/sample/src/main/java/io/noties/markwon/sample/table/TableActivity.java deleted file mode 100644 index 3dade55c..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/table/TableActivity.java +++ /dev/null @@ -1,135 +0,0 @@ -package io.noties.markwon.sample.table; - -import android.graphics.Color; -import android.os.Bundle; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import io.noties.markwon.Markwon; -import io.noties.markwon.ext.latex.JLatexMathPlugin; -import io.noties.markwon.ext.tables.TablePlugin; -import io.noties.markwon.image.ImagesPlugin; -import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin; -import io.noties.markwon.linkify.LinkifyPlugin; -import io.noties.markwon.sample.ActivityWithMenuOptions; -import io.noties.markwon.sample.MenuOptions; -import io.noties.markwon.sample.R; -import io.noties.markwon.utils.ColorUtils; -import io.noties.markwon.utils.Dip; - -public class TableActivity extends ActivityWithMenuOptions { - - @NonNull - @Override - public MenuOptions menuOptions() { - return MenuOptions.create() - .add("customize", this::customize) - .add("tableAndLinkify", this::tableAndLinkify) - .add("withImages", this::withImages) - .add("withLatex", this::withLatex); - } - - private TextView textView; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_text_view); - textView = findViewById(R.id.text_view); - - tableAndLinkify(); - } - - private void customize() { - final String md = "" + - "| HEADER | HEADER | HEADER |\n" + - "|:----:|:----:|:----:|\n" + - "| 测试 | 测试 | 测试 |\n" + - "| 测试 | 测试 | 测测测12345试测试测试 |\n" + - "| 测试 | 测试 | 123445 |\n" + - "| 测试 | 测试 | (650) 555-1212 |\n" + - "| 测试 | 测试 | [link](#) |\n"; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(TablePlugin.create(builder -> { - final Dip dip = Dip.create(this); - builder - .tableBorderWidth(dip.toPx(2)) - .tableBorderColor(Color.YELLOW) - .tableCellPadding(dip.toPx(4)) - .tableHeaderRowBackgroundColor(ColorUtils.applyAlpha(Color.RED, 80)) - .tableEvenRowBackgroundColor(ColorUtils.applyAlpha(Color.GREEN, 80)) - .tableOddRowBackgroundColor(ColorUtils.applyAlpha(Color.BLUE, 80)); - })) - .build(); - - markwon.setMarkdown(textView, md); - } - - private void tableAndLinkify() { - final String md = "" + - "| HEADER | HEADER | HEADER |\n" + - "|:----:|:----:|:----:|\n" + - "| 测试 | 测试 | 测试 |\n" + - "| 测试 | 测试 | 测测测12345试测试测试 |\n" + - "| 测试 | 测试 | 123445 |\n" + - "| 测试 | 测试 | (650) 555-1212 |\n" + - "| 测试 | 测试 | [link](#) |\n" + - "\n" + - "测试\n" + - "\n" + - "[link link](https://link.link)"; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(LinkifyPlugin.create()) - .usePlugin(TablePlugin.create(this)) - .build(); - - markwon.setMarkdown(textView, md); - } - - private void withImages() { - - final String md = "" + - "| HEADER | HEADER |\n" + - "|:----:|:----:|\n" + - "| ![Build](https://github.com/noties/Markwon/workflows/Build/badge.svg) | Build |\n" + - "| Stable | ![stable](https://img.shields.io/maven-central/v/io.noties.markwon/core.svg?label=stable) |\n" + - "| BIG | ![image](https://images.pexels.com/photos/41171/brussels-sprouts-sprouts-cabbage-grocery-41171.jpeg) |\n" + - "\n"; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(ImagesPlugin.create()) - .usePlugin(TablePlugin.create(this)) - .build(); - - markwon.setMarkdown(textView, md); - } - - private void withLatex() { - - String latex = "\\begin{array}{cc}"; - latex += "\\fbox{\\text{A framed box with \\textdbend}}&\\shadowbox{\\text{A shadowed box}}\\cr"; - latex += "\\doublebox{\\text{A double framed box}}&\\ovalbox{\\text{An oval framed box}}\\cr"; - latex += "\\end{array}"; - - final String md = "" + - "| HEADER | HEADER |\n" + - "|:----:|:----:|\n" + - "| ![Build](https://github.com/noties/Markwon/workflows/Build/badge.svg) | Build |\n" + - "| Stable | ![stable](https://img.shields.io/maven-central/v/io.noties.markwon/core.svg?label=stable) |\n" + - "| BIG | $$" + latex + "$$ |\n" + - "\n"; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(MarkwonInlineParserPlugin.create()) - .usePlugin(ImagesPlugin.create()) - .usePlugin(JLatexMathPlugin.create(textView.getTextSize(), builder -> builder.inlinesEnabled(true))) - .usePlugin(TablePlugin.create(this)) - .build(); - - markwon.setMarkdown(textView, md); - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/tasklist/TaskListActivity.java b/sample/src/main/java/io/noties/markwon/sample/tasklist/TaskListActivity.java deleted file mode 100644 index b5c421d6..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/tasklist/TaskListActivity.java +++ /dev/null @@ -1,169 +0,0 @@ -package io.noties.markwon.sample.tasklist; - -import android.graphics.Color; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.text.Spanned; -import android.text.TextPaint; -import android.text.style.ClickableSpan; -import android.view.View; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; - -import java.util.Objects; - -import io.noties.debug.Debug; -import io.noties.markwon.AbstractMarkwonPlugin; -import io.noties.markwon.Markwon; -import io.noties.markwon.MarkwonSpansFactory; -import io.noties.markwon.SpanFactory; -import io.noties.markwon.ext.tasklist.TaskListItem; -import io.noties.markwon.ext.tasklist.TaskListPlugin; -import io.noties.markwon.ext.tasklist.TaskListSpan; -import io.noties.markwon.sample.ActivityWithMenuOptions; -import io.noties.markwon.sample.MenuOptions; -import io.noties.markwon.sample.R; - -public class TaskListActivity extends ActivityWithMenuOptions { - - private static final String MD = "" + - "- [ ] Not done here!\n" + - "- [x] and done\n" + - "- [X] and again!\n" + - "* [ ] **and** syntax _included_ `code`\n" + - "- [ ] [link](#)\n" + - "- [ ] [a check box](https://goog.le)\n" + - "- [x] [test]()\n" + - "- [List](https://goog.le) 3"; - - private TextView textView; - - @NonNull - @Override - public MenuOptions menuOptions() { - return MenuOptions.create() - .add("regular", this::regular) - .add("customColors", this::customColors) - .add("customDrawableResources", this::customDrawableResources) - .add("mutate", this::mutate); - } - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_text_view); - - textView = findViewById(R.id.text_view); - -// mutate(); - regular(); - } - - private void regular() { - // default theme - - final Markwon markwon = Markwon.builder(this) - .usePlugin(TaskListPlugin.create(this)) - .build(); - - markwon.setMarkdown(textView, MD); - } - - private void customColors() { - - final int checkedFillColor = Color.RED; - final int normalOutlineColor = Color.GREEN; - final int checkMarkColor = Color.BLUE; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(TaskListPlugin.create(checkedFillColor, normalOutlineColor, checkMarkColor)) - .build(); - - markwon.setMarkdown(textView, MD); - } - - private void customDrawableResources() { - // drawable **must** be stateful - - final Drawable drawable = Objects.requireNonNull( - ContextCompat.getDrawable(this, R.drawable.custom_task_list)); - - final Markwon markwon = Markwon.builder(this) - .usePlugin(TaskListPlugin.create(drawable)) - .build(); - - markwon.setMarkdown(textView, MD); - } - - private void mutate() { - - final Markwon markwon = Markwon.builder(this) - .usePlugin(TaskListPlugin.create(this)) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { - // obtain origin task-list-factory - final SpanFactory origin = builder.getFactory(TaskListItem.class); - if (origin == null) { - return; - } - - builder.setFactory(TaskListItem.class, (configuration, props) -> { - // maybe it's better to validate the actual type here also - // and not force cast to task-list-span - final TaskListSpan span = (TaskListSpan) origin.getSpans(configuration, props); - if (span == null) { - return null; - } - - // NB, toggle click will intercept possible links inside task-list-item - return new Object[]{ - span, - new TaskListToggleSpan(span) - }; - }); - } - }) - .build(); - - markwon.setMarkdown(textView, MD); - } - - private static class TaskListToggleSpan extends ClickableSpan { - - private final TaskListSpan span; - - TaskListToggleSpan(@NonNull TaskListSpan span) { - this.span = span; - } - - @Override - public void onClick(@NonNull View widget) { - // toggle span (this is a mere visual change) - span.setDone(!span.isDone()); - // request visual update - widget.invalidate(); - - // it must be a TextView - final TextView textView = (TextView) widget; - // it must be spanned - final Spanned spanned = (Spanned) textView.getText(); - - // actual text of the span (this can be used along with the `span`) - final CharSequence task = spanned.subSequence( - spanned.getSpanStart(this), - spanned.getSpanEnd(this) - ); - - Debug.i("task done: %s, '%s'", span.isDone(), task); - } - - @Override - public void updateDrawState(@NonNull TextPaint ds) { - // no op, so text is not rendered as a link - } - } -} diff --git a/sample/src/main/java/io/noties/markwon/sample/theme/ThemeActivity.java b/sample/src/main/java/io/noties/markwon/sample/theme/ThemeActivity.java deleted file mode 100644 index bddaef52..00000000 --- a/sample/src/main/java/io/noties/markwon/sample/theme/ThemeActivity.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.noties.markwon.sample.theme; - -import android.app.Activity; -import android.os.Bundle; - -import androidx.annotation.Nullable; - -import io.noties.markwon.sample.R; - -public class ThemeActivity extends Activity { - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_text_view); - } -} diff --git a/sample/src/main/res/drawable-anydpi-v24/ic_stat_name.xml b/sample/src/main/res/drawable-anydpi-v24/ic_stat_name.xml deleted file mode 100644 index fd7cefc2..00000000 --- a/sample/src/main/res/drawable-anydpi-v24/ic_stat_name.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/sample/src/main/res/drawable-hdpi/ic_stat_name.png b/sample/src/main/res/drawable-hdpi/ic_stat_name.png deleted file mode 100644 index 19e7a26b..00000000 Binary files a/sample/src/main/res/drawable-hdpi/ic_stat_name.png and /dev/null differ diff --git a/sample/src/main/res/drawable-mdpi/ic_stat_name.png b/sample/src/main/res/drawable-mdpi/ic_stat_name.png deleted file mode 100644 index 0525d874..00000000 Binary files a/sample/src/main/res/drawable-mdpi/ic_stat_name.png and /dev/null differ diff --git a/sample/src/main/res/drawable-xhdpi/ic_stat_name.png b/sample/src/main/res/drawable-xhdpi/ic_stat_name.png deleted file mode 100644 index c5f2f076..00000000 Binary files a/sample/src/main/res/drawable-xhdpi/ic_stat_name.png and /dev/null differ diff --git a/sample/src/main/res/drawable-xxhdpi/ic_stat_name.png b/sample/src/main/res/drawable-xxhdpi/ic_stat_name.png deleted file mode 100644 index 993df1f0..00000000 Binary files a/sample/src/main/res/drawable-xxhdpi/ic_stat_name.png and /dev/null differ diff --git a/sample/src/main/res/drawable/bg_table_cell.xml b/sample/src/main/res/drawable/bg_table_cell.xml deleted file mode 100644 index 9a2b40b8..00000000 --- a/sample/src/main/res/drawable/bg_table_cell.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/sample/src/main/res/drawable/custom_task_list.xml b/sample/src/main/res/drawable/custom_task_list.xml deleted file mode 100644 index 43c2e2a8..00000000 --- a/sample/src/main/res/drawable/custom_task_list.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/sample/src/main/res/drawable/ic_android_black_24dp.xml b/sample/src/main/res/drawable/ic_android_black_24dp.xml deleted file mode 100644 index 401cbf63..00000000 --- a/sample/src/main/res/drawable/ic_android_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/sample/src/main/res/drawable/ic_home_black_36dp.xml b/sample/src/main/res/drawable/ic_home_black_36dp.xml deleted file mode 100644 index c3b7e150..00000000 --- a/sample/src/main/res/drawable/ic_home_black_36dp.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - diff --git a/sample/src/main/res/drawable/ic_launcher_background.xml b/sample/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 02e8ba59..00000000 --- a/sample/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - diff --git a/sample/src/main/res/drawable/ic_memory_black_48dp.xml b/sample/src/main/res/drawable/ic_memory_black_48dp.xml deleted file mode 100644 index 88ac2954..00000000 --- a/sample/src/main/res/drawable/ic_memory_black_48dp.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - diff --git a/sample/src/main/res/drawable/ic_sentiment_satisfied_red_64dp.xml b/sample/src/main/res/drawable/ic_sentiment_satisfied_red_64dp.xml deleted file mode 100644 index f9c60705..00000000 --- a/sample/src/main/res/drawable/ic_sentiment_satisfied_red_64dp.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/sample/src/main/res/layout/activity_editor.xml b/sample/src/main/res/layout/activity_editor.xml deleted file mode 100644 index c401a8cb..00000000 --- a/sample/src/main/res/layout/activity_editor.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - -