diff --git a/app-sample/samples.json b/app-sample/samples.json index 2ac8a06a..4ba4ff18 100644 --- a/app-sample/samples.json +++ b/app-sample/samples.json @@ -1,4 +1,16 @@ [ + { + "javaClassName": "io.noties.markwon.app.samples.editor.WYSIWYGEditorSample", + "id": "20200908133515", + "title": "WYSIWG editor", + "description": "A possible direction to implement what-you-see-is-what-you-get editor", + "artifacts": [ + "EDITOR" + ], + "tags": [ + "rendering" + ] + }, { "javaClassName": "io.noties.markwon.app.samples.tasklist.ListTaskListSample", "id": "20200902174132", diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/editor/WYSIWYGEditorSample.java b/app-sample/src/main/java/io/noties/markwon/app/samples/editor/WYSIWYGEditorSample.java new file mode 100644 index 00000000..01f916d8 --- /dev/null +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/editor/WYSIWYGEditorSample.java @@ -0,0 +1,134 @@ +package io.noties.markwon.app.samples.editor; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.text.method.LinkMovementMethod; +import android.text.style.ReplacementSpan; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import io.noties.debug.Debug; +import io.noties.markwon.Markwon; +import io.noties.markwon.SoftBreakAddsNewLinePlugin; +import io.noties.markwon.app.sample.Tags; +import io.noties.markwon.app.samples.editor.shared.BlockQuoteEditHandler; +import io.noties.markwon.app.samples.editor.shared.CodeEditHandler; +import io.noties.markwon.app.samples.editor.shared.HeadingEditHandler; +import io.noties.markwon.app.samples.editor.shared.LinkEditHandler; +import io.noties.markwon.app.samples.editor.shared.MarkwonEditTextSample; +import io.noties.markwon.app.samples.editor.shared.StrikethroughEditHandler; +import io.noties.markwon.editor.MarkwonEditor; +import io.noties.markwon.editor.MarkwonEditorTextWatcher; +import io.noties.markwon.editor.PersistedSpans; +import io.noties.markwon.editor.handler.EmphasisEditHandler; +import io.noties.markwon.editor.handler.StrongEmphasisEditHandler; +import io.noties.markwon.sample.annotations.MarkwonArtifact; +import io.noties.markwon.sample.annotations.MarkwonSampleInfo; + +@MarkwonSampleInfo( + id = "20200908133515", + title = "WYSIWG editor", + description = "A possible direction to implement what-you-see-is-what-you-get editor", + artifacts = MarkwonArtifact.EDITOR, + tags = Tags.rendering +) +public class WYSIWYGEditorSample extends MarkwonEditTextSample { + @Override + public void render() { + + // when automatic line break is inserted and text is inside margin span (blockquote, list, etc) + // be prepared to encounter selection bugs (selection would be drawn at the place as is no margin + // span is present) + + final Markwon markwon = Markwon.builder(context) + .usePlugin(SoftBreakAddsNewLinePlugin.create()) + .build(); + + final MarkwonEditor editor = MarkwonEditor.builder(markwon) + .punctuationSpan(HidePunctuationSpan.class, new PersistedSpans.SpanFactory() { + @NonNull + @Override + public HidePunctuationSpan create() { + return new HidePunctuationSpan(); + } + }) + .useEditHandler(new EmphasisEditHandler()) + .useEditHandler(new StrongEmphasisEditHandler()) + .useEditHandler(new StrikethroughEditHandler()) + .useEditHandler(new CodeEditHandler()) + .useEditHandler(new BlockQuoteEditHandler()) + .useEditHandler(new LinkEditHandler(new LinkEditHandler.OnClick() { + @Override + public void onClick(@NonNull View widget, @NonNull String link) { + Debug.e("clicked: %s", link); + } + })) + .useEditHandler(new HeadingEditHandler()) + .build(); + + // for links to be clickable + // NB! markwon MovementMethodPlugin cannot be used here as editor do not execute `beforeSetText`) + editText.setMovementMethod(LinkMovementMethod.getInstance()); + + editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor)); + } + + private static class HidePunctuationSpan extends ReplacementSpan { + + @Override + public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm) { + // last space (which is swallowed until next non-space character appears) + // block quote + // code tick + +// Debug.i("text: '%s', %d-%d (%d)", text.subSequence(start, end), start, end, text.length()); + if (end == text.length()) { + // TODO: find first non-space character (not just first one because commonmark allows + // arbitrary (0-3) white spaces before content starts + + // TODO: if all white space - render? + final char c = text.charAt(start); + if ('#' == c + || '>' == c + || '-' == c // TODO: not thematic break + || '+' == c + // `*` is fine but only for a list + || isBulletList(text, c, start, end) + || Character.isDigit(c) // assuming ordered list (replacement should only happen for ordered lists) + || Character.isWhitespace(c)) { + return (int) (paint.measureText(text, start, end) + 0.5F); + } + } + return 0; + } + + @Override + public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) { + // will be called only when getSize is not 0 (and if it was once reported as 0...) + if (end == text.length()) { + + // if first non-space is `*` then check for is bullet + // else `**` would be still rendered at the end of the emphasis + if (text.charAt(start) == '*' + && !isBulletList(text, '*', start, end)) { + return; + } + + // TODO: inline code last tick received here, handle it (do not highlight) + // why can't we have reported width in this method for supplied text? + + // let's use color to make it distinct from the rest of the text for demonstration purposes + paint.setColor(0xFFff0000); + + canvas.drawText(text, start, end, x, y, paint); + } + } + + private static boolean isBulletList(@NonNull CharSequence text, char firstChar, int start, int end) { + return '*' == firstChar + && ((end - start == 1) || (Character.isWhitespace(text.charAt(start + 1)))); + } + } +}