add emoji support for markwon

This commit is contained in:
k0shk0sh 2019-07-21 11:08:47 +02:00
parent ee2c95dec4
commit 5313b35fa2
46 changed files with 175 additions and 1301 deletions

View File

@ -1,19 +1,32 @@
package com.fastaccess.github.di.modules
import android.annotation.SuppressLint
import android.content.Context
import com.fastaccess.data.storage.FastHubSharedPreference
import com.fastaccess.github.R
import com.fastaccess.github.base.engine.ThemeEngine
import com.fastaccess.github.di.annotations.ForApplication
import com.fastaccess.github.di.scopes.PerFragment
import com.fastaccess.github.extensions.getDrawableCompat
import com.fastaccess.github.platform.mentions.MentionsPresenter
import com.fastaccess.github.ui.modules.issue.fragment.IssueFragment
import com.fastaccess.github.usecase.search.FilterSearchUsersUseCase
import com.fastaccess.markdown.R
import com.fastaccess.markdown.spans.*
import com.fastaccess.github.utils.extensions.theme
import com.fastaccess.markdown.GrammarLocatorDef
import com.fastaccess.markdown.extension.markwon.emoji.EmojiPlugin
import dagger.Module
import dagger.Provides
import net.nightwhistler.htmlspanner.HtmlSpanner
import net.nightwhistler.htmlspanner.handlers.StyledTextHandler
import net.nightwhistler.htmlspanner.style.Style
import io.noties.markwon.Markwon
import io.noties.markwon.ext.latex.JLatexMathPlugin
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.glide.GlideImagesPlugin
import io.noties.markwon.linkify.LinkifyPlugin
import io.noties.markwon.syntax.Prism4jThemeDarkula
import io.noties.markwon.syntax.Prism4jThemeDefault
import io.noties.markwon.syntax.SyntaxHighlightPlugin
import io.noties.prism4j.Prism4j
/**
* Created by Kosh on 02.02.19.
@ -23,36 +36,29 @@ class FragmentModule {
@PerFragment @Provides fun provideContext(fragment: IssueFragment) = fragment.requireContext()
@PerFragment @Provides fun provideHtmlSpanner(@ForApplication context: Context): HtmlSpanner {
val mySpanner = HtmlSpanner()
mySpanner.isStripExtraWhiteSpace = true
val checked = context.getDrawableCompat(R.drawable.ic_checkbox_small)
val unchecked = context.getDrawableCompat(R.drawable.ic_checkbox_empty_small)
mySpanner.registerHandler("li", ListsHandler(checked, unchecked))
mySpanner.registerHandler("g-emoji", EmojiHandler())
mySpanner.registerHandler("b", StyledTextHandler(Style().setFontWeight(Style.FontWeight.BOLD)))
mySpanner.registerHandler("strong", StyledTextHandler(Style().setFontWeight(Style.FontWeight.BOLD)))
mySpanner.registerHandler("i", ItalicHandler())
mySpanner.registerHandler("em", ItalicHandler())
mySpanner.registerHandler("ul", MarginHandler())
mySpanner.registerHandler("ol", MarginHandler())
mySpanner.registerHandler("u", UnderlineHandler())
mySpanner.registerHandler("strike", StrikethroughHandler())
mySpanner.registerHandler("ins", UnderlineHandler())
mySpanner.registerHandler("del", StrikethroughHandler())
mySpanner.registerHandler("sub", SubScriptHandler())
mySpanner.registerHandler("sup", SuperScriptHandler())
mySpanner.registerHandler("a", LinkHandler())
mySpanner.registerHandler("emoji", EmojiHandler())
mySpanner.registerHandler("mention", LinkHandler())
mySpanner.registerHandler("h1", HeaderHandler(1.5f))
mySpanner.registerHandler("h2", HeaderHandler(1.4f))
mySpanner.registerHandler("h3", HeaderHandler(1.3f))
mySpanner.registerHandler("h4", HeaderHandler(1.2f))
mySpanner.registerHandler("h5", HeaderHandler(1.1f))
mySpanner.registerHandler("h6", HeaderHandler(1.0f))
return mySpanner
}
@SuppressLint("PrivateResource")
@PerFragment @Provides fun provideMarkwon(
@ForApplication context: Context,
preference: FastHubSharedPreference
): Markwon = Markwon.builder(context)
.usePlugin(JLatexMathPlugin.create(context.resources.getDimension(R.dimen.abc_text_size_subhead_material)))
.usePlugin(TaskListPlugin.create(context))
.usePlugin(HtmlPlugin.create())
.usePlugin(GlideImagesPlugin.create(context))
.usePlugin(TablePlugin.create(context))
.usePlugin(StrikethroughPlugin.create())
.usePlugin(LinkifyPlugin.create())
.usePlugin(
SyntaxHighlightPlugin.create(
Prism4j(GrammarLocatorDef()), if (ThemeEngine.isLightTheme(preference.theme)) {
Prism4jThemeDefault.create()
} else {
Prism4jThemeDarkula.create()
}
)
)
.usePlugin(EmojiPlugin.create())
.build()
@PerFragment @Provides fun provideMentionsPresenter(
context: Context,

View File

@ -44,20 +44,34 @@ class NetworkModule {
@Singleton @Provides fun provideInterceptor() = AuthenticationInterceptor()
@Singleton @Provides fun provideHttpClient(auth: AuthenticationInterceptor): OkHttpClient = OkHttpClient
@Singleton @Provides fun provideHttpLogging() = HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
}
@Singleton @Provides fun provideHttpClient(
auth: AuthenticationInterceptor,
httpLoggingInterceptor: HttpLoggingInterceptor
): OkHttpClient = OkHttpClient
.Builder()
.addInterceptor(ContentTypeInterceptor())
.addInterceptor(auth)
.addInterceptor(PaginationInterceptor())
.addInterceptor(Pandora.get().interceptor)
.addInterceptor(HttpLoggingInterceptor())
.addInterceptor(httpLoggingInterceptor)
.build()
@Named("apolloClient") @Singleton @Provides fun provideHttpClientForApollo(auth: AuthenticationInterceptor): OkHttpClient = OkHttpClient
@Named("apolloClient") @Singleton @Provides fun provideHttpClientForApollo(
auth: AuthenticationInterceptor,
httpLoggingInterceptor: HttpLoggingInterceptor
): OkHttpClient = OkHttpClient
.Builder()
.addInterceptor(auth)
.addInterceptor(Pandora.get().interceptor)
.addInterceptor(HttpLoggingInterceptor())
.addInterceptor(httpLoggingInterceptor)
.build()

View File

@ -1,27 +0,0 @@
package com.fastaccess.github.platform.markwon.hashtag
import org.commonmark.node.CustomNode
import org.commonmark.node.Delimited
/**
* Created by kosh on 20/08/2017.
*/
class HashTag : CustomNode(), Delimited {
var url: String? = null
get() = DELIMITER + field!!
override fun getOpeningDelimiter(): String {
return DELIMITER
}
override fun getClosingDelimiter(): String {
return " "
}
companion object {
private const val DELIMITER = "#"
}
}

View File

@ -1,29 +0,0 @@
package com.fastaccess.github.platform.markwon.hashtag
import com.fastaccess.github.platform.markwon.hashtag.internal.HashTagDelimiterProcessor
import com.fastaccess.github.platform.markwon.hashtag.internal.HashTagNodeRenderer
import org.commonmark.Extension
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
/**
* Created by kosh on 20/08/2017.
*/
class HashTagExtension private constructor() : Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension {
override fun extend(parserBuilder: Parser.Builder) {
parserBuilder.customDelimiterProcessor(HashTagDelimiterProcessor())
}
override fun extend(rendererBuilder: HtmlRenderer.Builder) {
rendererBuilder.nodeRendererFactory { HashTagNodeRenderer(it) }
}
companion object {
fun create(): Extension {
return HashTagExtension()
}
}
}

View File

@ -1,42 +0,0 @@
package com.fastaccess.github.platform.markwon.hashtag.internal
import com.fastaccess.github.platform.markwon.hashtag.HashTag
import org.commonmark.node.Node
import org.commonmark.node.Text
import org.commonmark.parser.delimiter.DelimiterProcessor
import org.commonmark.parser.delimiter.DelimiterRun
class HashTagDelimiterProcessor : DelimiterProcessor {
override fun getOpeningCharacter(): Char = '#'
override fun getClosingCharacter(): Char = ' '
override fun getMinLength(): Int = 1
override fun getDelimiterUse(
opener: DelimiterRun,
closer: DelimiterRun
): Int = if (opener.length() >= 1 && closer.length() >= 1) {
1
} else {
0
}
override fun process(
opener: Text,
closer: Text,
delimiterCount: Int
) {
val hashTag = HashTag()
var tmp: Node? = opener.next
if (tmp != null) {
hashTag.url = (tmp as Text).literal
}
while (tmp != null && tmp !== closer) {
val next = tmp.next
hashTag.appendChild(tmp)
tmp = next
}
opener.insertAfter(hashTag)
}
}

View File

@ -1,31 +0,0 @@
package com.fastaccess.github.platform.markwon.hashtag.internal
import com.fastaccess.github.platform.markwon.hashtag.HashTag
import org.commonmark.node.Node
import org.commonmark.renderer.NodeRenderer
import org.commonmark.renderer.html.HtmlNodeRendererContext
import org.commonmark.renderer.html.HtmlWriter
class HashTagNodeRenderer(private val context: HtmlNodeRendererContext) : NodeRenderer {
private val html: HtmlWriter = context.writer
override fun getNodeTypes(): Set<Class<out Node>> {
return setOf<Class<out Node>>(HashTag::class.java)
}
override fun render(node: Node) {
val attributes = context.extendAttributes(node, "hashtag", emptyMap())
html.tag("hashtag", attributes)
renderChildren(node)
html.tag("/hashtag")
}
private fun renderChildren(parent: Node) {
var node: Node? = parent.firstChild
while (node != null) {
val next = node.next
context.render(node)
node = next
}
}
}

View File

@ -1,24 +0,0 @@
package com.fastaccess.github.platform.markwon.mention
import com.fastaccess.github.utils.GITHUB_LINK
import org.commonmark.node.CustomNode
import org.commonmark.node.Delimited
/**
* Created by kosh on 20/08/2017.
*/
class Mention : CustomNode(), Delimited {
var url: String? = null
get() = BASE_URL + field!!
override fun getOpeningDelimiter(): String = DELIMITER
override fun getClosingDelimiter(): String = " "
companion object {
private const val BASE_URL = GITHUB_LINK
private const val DELIMITER = "@"
}
}

View File

@ -1,31 +0,0 @@
package com.fastaccess.github.platform.markwon.mention
import com.fastaccess.github.platform.markwon.mention.internal.MentionDelimiterProcessor
import com.fastaccess.github.platform.markwon.mention.internal.MentionNodeRenderer
import org.commonmark.Extension
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlNodeRendererFactory
import org.commonmark.renderer.html.HtmlRenderer
/**
* Created by kosh on 20/08/2017.
*/
class MentionExtension private constructor() : Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension {
override fun extend(parserBuilder: Parser.Builder) {
parserBuilder.customDelimiterProcessor(MentionDelimiterProcessor())
}
override fun extend(rendererBuilder: HtmlRenderer.Builder) {
rendererBuilder.nodeRendererFactory(HtmlNodeRendererFactory { MentionNodeRenderer(it) })
}
companion object {
fun create(): Extension {
return MentionExtension()
}
}
}

View File

@ -1,44 +0,0 @@
package com.fastaccess.github.platform.markwon.mention.internal
import com.fastaccess.github.platform.markwon.mention.Mention
import org.commonmark.node.Node
import org.commonmark.node.Text
import org.commonmark.parser.delimiter.DelimiterProcessor
import org.commonmark.parser.delimiter.DelimiterRun
class MentionDelimiterProcessor : DelimiterProcessor {
override fun getOpeningCharacter(): Char = '@'
override fun getClosingCharacter(): Char = ' '
override fun getMinLength(): Int = 1
override fun getDelimiterUse(
opener: DelimiterRun,
closer: DelimiterRun
): Int = if (opener.length() >= 1 && closer.length() >= 1) {
1
} else {
0
}
override fun process(
opener: Text,
closer: Text,
delimiterCount: Int
) {
val mention = Mention()
var tmp: Node? = opener.next
if (tmp != null) {
mention.url = (tmp as Text).literal
}
while (tmp != null && tmp !== closer) {
val next = tmp.next
mention.appendChild(tmp)
tmp = next
}
opener.insertAfter(mention)
}
}

View File

@ -1,31 +0,0 @@
package com.fastaccess.github.platform.markwon.mention.internal
import com.fastaccess.github.platform.markwon.mention.Mention
import org.commonmark.node.Node
import org.commonmark.renderer.NodeRenderer
import org.commonmark.renderer.html.HtmlNodeRendererContext
import org.commonmark.renderer.html.HtmlWriter
class MentionNodeRenderer(private val context: HtmlNodeRendererContext) : NodeRenderer {
private val html: HtmlWriter = context.writer
override fun getNodeTypes(): Set<Class<out Node>> {
return setOf<Class<out Node>>(Mention::class.java)
}
override fun render(node: Node) {
val attributes = context.extendAttributes(node, "mention", emptyMap())
html.tag("mention", attributes)
renderChildren(node)
html.tag("/mention")
}
private fun renderChildren(parent: Node) {
var node: Node? = parent.firstChild
while (node != null) {
val next = node.next
context.render(node)
node = next
}
}
}

View File

@ -10,13 +10,13 @@ import com.fastaccess.github.ui.adapter.base.BaseViewHolder
import com.fastaccess.github.ui.adapter.viewholder.CommentViewHolder
import com.fastaccess.github.ui.adapter.viewholder.IssueContentViewHolder
import com.fastaccess.github.ui.adapter.viewholder.LoadingViewHolder
import net.nightwhistler.htmlspanner.HtmlSpanner
import io.noties.markwon.Markwon
/**
* Created by Kosh on 20.01.19.
*/
class IssueTimelineAdapter(
private val htmlSpanner: HtmlSpanner,
private val markwon: Markwon,
private val theme: Int
) : ListAdapter<TimelineModel, RecyclerView.ViewHolder>(DIFF_CALLBACK) {
@ -38,7 +38,7 @@ class IssueTimelineAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
COMMENT -> CommentViewHolder(parent, htmlSpanner, theme, notifyCallback)
COMMENT -> CommentViewHolder(parent, markwon, theme, notifyCallback)
CONTENT -> IssueContentViewHolder(parent)
else -> LoadingViewHolder<Any>(parent).apply { itemView.isVisible = false }
}

View File

@ -1,6 +1,8 @@
package com.fastaccess.github.ui.adapter.viewholder
import android.annotation.SuppressLint
import android.text.util.Linkify
import android.util.Patterns
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
@ -12,9 +14,9 @@ import com.fastaccess.github.extensions.timeAgo
import com.fastaccess.github.ui.adapter.base.BaseViewHolder
import com.fastaccess.github.utils.extensions.popupEmoji
import com.fastaccess.markdown.MarkdownProvider
import com.fastaccess.markdown.spans.drawable.DrawableGetter
import io.noties.markwon.Markwon
import kotlinx.android.synthetic.main.comment_row_item.view.*
import net.nightwhistler.htmlspanner.HtmlSpanner
import java.util.regex.Pattern
/**
@ -23,7 +25,7 @@ import net.nightwhistler.htmlspanner.HtmlSpanner
class CommentViewHolder(
parent: ViewGroup,
private val htmlSpanner: HtmlSpanner,
private val markwon: Markwon,
private val theme: Int,
private val callback: (position: Int) -> Unit
) : BaseViewHolder<CommentModel?>(
@ -33,7 +35,7 @@ class CommentViewHolder(
@SuppressLint("SetTextI18n")
override fun bind(item: CommentModel?) {
val model = item ?: kotlin.run {
val model = item ?: run {
itemView.isVisible = false
return
}
@ -46,8 +48,23 @@ class CommentViewHolder(
"${model.authorAssociation?.value?.toLowerCase()?.replace("_", "")} ${model.updatedAt?.timeAgo()}"
}
MarkdownProvider.loadIntoTextView(htmlSpanner, description, model.body ?: "", ThemeEngine.getCodeBackground(theme),
ThemeEngine.isLightTheme(theme))
MarkdownProvider.loadIntoTextView(
markwon, description, model.body ?: "", ThemeEngine.getCodeBackground(theme),
ThemeEngine.isLightTheme(theme)
)
val filter = Linkify.TransformFilter { match, _ -> match.group() }
val mentionPattern = Pattern.compile("@([A-Za-z0-9_-]+)")
val mentionScheme = "https://www.github.com/"
Linkify.addLinks(description, mentionPattern, mentionScheme, null, filter)
val hashtagPattern = Pattern.compile("#([A-Za-z0-9_-]+)")
val hashtagScheme = "https://www.github.com/"
Linkify.addLinks(description, hashtagPattern, hashtagScheme, null, filter)
val urlPattern = Patterns.WEB_URL
Linkify.addLinks(description, urlPattern, null, null, filter)
addEmoji.setOnClickListener {
it.popupEmoji(requireNotNull(model.id), model.reactionGroups) {
@ -72,14 +89,4 @@ class CommentViewHolder(
}
}
}
override fun onDetached() {
super.onDetached()
itemView.description?.let {
if (it.tag is DrawableGetter) {
val target = it.tag as DrawableGetter
target.clear(target)
}
}
}
}

View File

@ -48,10 +48,10 @@ import com.otaliastudios.autocomplete.CharPolicy
import github.type.CommentAuthorAssociation
import github.type.IssueState
import github.type.LockReason
import io.noties.markwon.Markwon
import kotlinx.android.synthetic.main.empty_state_layout.*
import kotlinx.android.synthetic.main.issue_header_row_item.*
import kotlinx.android.synthetic.main.issue_pr_fragment_layout.*
import net.nightwhistler.htmlspanner.HtmlSpanner
import timber.log.Timber
import javax.inject.Inject
@ -63,7 +63,7 @@ class IssueFragment : BaseFragment(), LockUnlockFragment.OnLockReasonSelected,
MilestoneFragment.OnMilestoneChanged {
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
@Inject lateinit var htmlSpanner: HtmlSpanner
@Inject lateinit var markwon: Markwon
@Inject lateinit var preference: FastHubSharedPreference
@Inject lateinit var mentionsPresenter: MentionsPresenter
@ -71,7 +71,7 @@ class IssueFragment : BaseFragment(), LockUnlockFragment.OnLockReasonSelected,
private val login by lazy { arguments?.getString(EXTRA) ?: "" }
private val repo by lazy { arguments?.getString(EXTRA_TWO) ?: "" }
private val number by lazy { arguments?.getInt(EXTRA_THREE) ?: 0 }
private val adapter by lazy { IssueTimelineAdapter(htmlSpanner, preference.theme) }
private val adapter by lazy { IssueTimelineAdapter(markwon, preference.theme) }
override fun layoutRes(): Int = R.layout.issue_pr_fragment_layout
override fun viewModel(): BaseViewModel? = viewModel
@ -240,7 +240,7 @@ class IssueFragment : BaseFragment(), LockUnlockFragment.OnLockReasonSelected,
"${model.authorAssociation?.toLowerCase()?.replace("_", "")} ${model.updatedAt?.timeAgo()}"
}
MarkdownProvider.loadIntoTextView(
htmlSpanner, description, model.body ?: "", ThemeEngine.getCodeBackground(theme),
markwon, description, model.body ?: "", ThemeEngine.getCodeBackground(theme),
ThemeEngine.isLightTheme(theme)
)
state.text = model.state?.toLowerCase()

View File

@ -33,6 +33,8 @@ class CloseOpenIssuePrUseCase @Inject constructor(
.observeOn(schedulerProvider.uiThread())
.flatMapObservable { issue ->
issuePrService.editIssue(login, repo, number, IssueRequestModel(state = if ("closed".equals(issue.state, true)) "open" else "closed"))
.subscribeOn(schedulerProvider.ioThread())
.observeOn(schedulerProvider.uiThread())
.map {
issue.state = it.issueState
issueRepositoryProvider.upsert(issue)

View File

@ -121,25 +121,21 @@ ext {
]
markdown = [
'com.github.NightWhistler:HtmlSpanner:-SNAPSHOT',
'net.sourceforge.htmlcleaner:htmlcleaner:2.22',
"com.atlassian.commonmark:commonmark:${commonmark}",
"com.atlassian.commonmark:commonmark-ext-autolink:${commonmark}",
"com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:${commonmark}",
"com.atlassian.commonmark:commonmark-ext-gfm-tables:${commonmark}",
"com.atlassian.commonmark:commonmark-ext-ins:${commonmark}",
"com.atlassian.commonmark:commonmark-ext-yaml-front-matter:${commonmark}",
"io.noties.markwon:core:$markwon_version",
"io.noties.markwon:ext-latex:$markwon_version",
"io.noties.markwon:ext-strikethrough:$markwon_version",
"io.noties.markwon:ext-tables:$markwon_version",
"io.noties.markwon:ext-tasklist:$markwon_version",
"io.noties.markwon:html:$markwon_version",
"io.noties.markwon:image:$markwon_version",
"io.noties.markwon:image-glide:$markwon_version",
"io.noties.markwon:recycler:$markwon_version",
"io.noties.markwon:recycler-table:$markwon_version",
"io.noties.markwon:simple-ext:$markwon_version",
"io.noties.markwon:syntax-highlight:$markwon_version",
"io.noties.markwon:linkify:$markwon_version",
"com.caverock:androidsvg:1.4",
"pl.droidsonroids.gif:android-gif-drawable:1.2.14",
"io.noties:prism4j:2.0.0"
]

View File

@ -1,26 +1,14 @@
package com.fastaccess.markdown
import android.text.method.ScrollingMovementMethod
import android.widget.TextView
import androidx.core.text.HtmlCompat
import androidx.core.view.doOnPreDraw
import io.noties.markwon.Markwon
import io.noties.markwon.ext.latex.JLatexMathPlugin
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.glide.GlideImagesPlugin
import io.noties.markwon.movement.MovementMethodPlugin
import io.noties.markwon.syntax.Prism4jThemeDarkula
import io.noties.markwon.syntax.Prism4jThemeDefault
import io.noties.markwon.syntax.SyntaxHighlightPlugin
import io.noties.prism4j.Prism4j
import io.noties.prism4j.annotations.PrismBundle
import net.nightwhistler.htmlspanner.HtmlSpanner
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
/**
* Created by Kosh on 02.02.19.
*/
@ -40,7 +28,7 @@ object MarkdownProvider {
private const val PARAGRAPH_END = "</p>"
fun loadIntoTextView(
htmlSpanner: HtmlSpanner,
markwon: Markwon,
textView: TextView,
html: String,
windowBackground: Int,
@ -49,10 +37,10 @@ object MarkdownProvider {
) {
val width = textView.measuredWidth
if (width > 0) {
initTextView(width, htmlSpanner, textView, html, windowBackground, isLightTheme, onLinkClicked)
initTextView(width, markwon, textView, html, windowBackground, isLightTheme, onLinkClicked)
} else {
textView.doOnPreDraw {
initTextView(textView.width, htmlSpanner, textView, html, windowBackground, isLightTheme, onLinkClicked)
initTextView(textView.width, markwon, textView, html, windowBackground, isLightTheme, onLinkClicked)
}
}
}
@ -67,94 +55,13 @@ object MarkdownProvider {
private fun initTextView(
width: Int,
htmlSpanner: HtmlSpanner,
markwon: Markwon,
textView: TextView,
html: String,
windowBackground: Int,
isLightTheme: Boolean,
onLinkClicked: ((link: String) -> Unit)? = null
) {
val context = textView.context
Markwon.builder(context)
.usePlugin(JLatexMathPlugin.create(textView.textSize - 20))
.usePlugin(TaskListPlugin.create(context))
.usePlugin(HtmlPlugin.create())
.usePlugin(GlideImagesPlugin.create(context))
.usePlugin(TablePlugin.create(context))
.usePlugin(StrikethroughPlugin.create())
.usePlugin(MovementMethodPlugin.create(ScrollingMovementMethod.getInstance()))
.usePlugin(
SyntaxHighlightPlugin.create(
Prism4j(GrammarLocatorDef()), if (isLightTheme) {
Prism4jThemeDefault.create()
} else {
Prism4jThemeDarkula.create()
}
)
)
.build()
.setMarkdown(textView, html)
}
//https://github.com/k0shk0sh/GitHubSdk/blob/master/library/src/main/java/com/meisolsson/githubsdk/core/HtmlUtils.java
private fun format(html: String): CharSequence {
if (html.isEmpty()) return ""
val formatted = StringBuilder(html)
strip(formatted, TOGGLE_START, TOGGLE_END)
strip(formatted, SIGNATURE_START, SIGNATURE_END)
strip(formatted, REPLY_START, REPLY_END)
strip(formatted, HIDDEN_REPLY_START, HIDDEN_REPLY_END)
if (replace(formatted, PARAGRAPH_START, BREAK)) replace(formatted, PARAGRAPH_END, BREAK)
trim(formatted)
return formatted
}
private fun strip(
input: StringBuilder,
prefix: String,
suffix: String
) {
var start = input.indexOf(prefix)
while (start != -1) {
var end = input.indexOf(suffix, start + prefix.length)
if (end == -1)
end = input.length
input.delete(start, end + suffix.length)
start = input.indexOf(prefix, start)
}
}
private fun replace(
input: StringBuilder,
from: String,
to: String
): Boolean {
var start = input.indexOf(from)
if (start == -1) return false
val fromLength = from.length
val toLength = to.length
while (start != -1) {
input.replace(start, start + fromLength, to)
start = input.indexOf(from, start + toLength)
}
return true
}
private fun trim(input: StringBuilder) {
var length = input.length
val breakLength = BREAK.length
while (length > 0) {
if (input.indexOf(BREAK) == 0)
input.delete(0, breakLength)
else if (length >= breakLength && input.lastIndexOf(BREAK) == length - breakLength)
input.delete(length - breakLength, length)
else if (Character.isWhitespace(input[0]))
input.deleteCharAt(0)
else if (Character.isWhitespace(input[length - 1]))
input.deleteCharAt(length - 1)
else
break
length = input.length
}
markwon.setMarkdown(textView, html)
}
}

View File

@ -1,14 +0,0 @@
package com.fastaccess.markdown.extension
import android.text.SpannableStringBuilder
import net.nightwhistler.htmlspanner.TagNodeHandler
import net.nightwhistler.htmlspanner.spans.FontFamilySpan
/**
* Created by Kosh on 02.02.19.
*/
fun TagNodeHandler.getFontFamilySpan(builder: SpannableStringBuilder, start: Int, end: Int): FontFamilySpan? {
val spans = builder.getSpans(start, end, FontFamilySpan::class.java) as Array<FontFamilySpan>
return if (spans.isNotEmpty()) spans[spans.size - 1] else null
}

View File

@ -1,4 +1,4 @@
package com.fastaccess.github.platform.markwon.emoji
package com.fastaccess.markdown.extension.markwon.emoji
import org.commonmark.node.CustomNode
import org.commonmark.node.Delimited
@ -14,6 +14,8 @@ class Emoji : CustomNode(), Delimited {
override fun getClosingDelimiter(): String = DELIMITER
override fun toString(): String = emoji ?: "no emoji"
companion object {
private const val DELIMITER = ":"
}

View File

@ -1,7 +1,7 @@
package com.fastaccess.github.platform.markwon.emoji
package com.fastaccess.markdown.extension.markwon.emoji
import com.fastaccess.github.platform.markwon.emoji.internal.EmojiDelimiterProcessor
import com.fastaccess.github.platform.markwon.emoji.internal.EmojiNodeRenderer
import com.fastaccess.markdown.extension.markwon.emoji.internal.EmojiDelimiterProcessor
import com.fastaccess.markdown.extension.markwon.emoji.internal.EmojiNodeRenderer
import org.commonmark.Extension
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer

View File

@ -0,0 +1,33 @@
package com.fastaccess.markdown.extension.markwon.emoji
import android.text.SpannedString
import com.fastaccess.markdown.emoji.EmojiManager
import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.MarkwonVisitor
import org.commonmark.parser.Parser
import timber.log.Timber
class EmojiPlugin : AbstractMarkwonPlugin() {
override fun configureParser(builder: Parser.Builder) {
builder.extensions(setOf(EmojiExtension.create()))
}
override fun configureVisitor(builder: MarkwonVisitor.Builder) {
builder.on(Emoji::class.java) { visitor, emoji ->
val length = visitor.length()
val emojiUnicode = emoji.emoji
val unicode = EmojiManager.getForAlias(emoji.emoji)?.unicode
if (!unicode.isNullOrEmpty()) {
visitor.setSpans(length, SpannedString(unicode))
} else {
Timber.e(emojiUnicode)
}
}
}
companion object {
fun create() = EmojiPlugin()
}
}

View File

@ -0,0 +1,30 @@
package com.fastaccess.markdown.extension.markwon.emoji
import android.text.SpannedString
import com.fastaccess.markdown.emoji.EmojiManager
import io.noties.markwon.MarkwonConfiguration
import io.noties.markwon.Prop
import io.noties.markwon.RenderProps
import io.noties.markwon.SpanFactory
import timber.log.Timber
/**
* Created by Kosh on 2019-07-20.
*/
class EmojiSpanFactory : SpanFactory {
override fun getSpans(
configuration: MarkwonConfiguration,
props: RenderProps
): Any? {
val emoji = props.get<Emoji>(Prop.of(":"))
Timber.e("$props $emoji")
if (emoji != null) {
val unicode = EmojiManager.getForAlias(emoji.emoji)
if (unicode?.unicode != null) {
return SpannedString(unicode.unicode)
}
}
return null
}
}

View File

@ -1,7 +1,7 @@
package com.fastaccess.github.platform.markwon.emoji.internal
package com.fastaccess.markdown.extension.markwon.emoji.internal
import com.fastaccess.github.platform.markwon.emoji.Emoji
import com.fastaccess.markdown.extension.markwon.emoji.Emoji
import org.commonmark.node.Text
import org.commonmark.parser.delimiter.DelimiterProcessor
import org.commonmark.parser.delimiter.DelimiterRun

View File

@ -1,6 +1,6 @@
package com.fastaccess.github.platform.markwon.emoji.internal
package com.fastaccess.markdown.extension.markwon.emoji.internal
import com.fastaccess.github.platform.markwon.emoji.Emoji
import com.fastaccess.markdown.extension.markwon.emoji.Emoji
import org.commonmark.node.Node
import org.commonmark.renderer.NodeRenderer
import org.commonmark.renderer.html.HtmlNodeRendererContext

View File

@ -1,51 +0,0 @@
package com.fastaccess.markdown.spans
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import android.graphics.Typeface
import android.text.Layout
import android.text.TextPaint
import android.text.style.LeadingMarginSpan
import android.text.style.LineBackgroundSpan
import android.text.style.MetricAffectingSpan
class CodeBackgroundRoundedSpan constructor(
private val color: Int
) : MetricAffectingSpan(), LeadingMarginSpan, LineBackgroundSpan {
private val rect = RectF()
override fun updateMeasureState(paint: TextPaint) {
apply(paint)
}
override fun updateDrawState(paint: TextPaint) {
apply(paint)
}
private fun apply(paint: TextPaint) {
paint.typeface = Typeface.MONOSPACE
}
override fun drawBackground(
c: Canvas, p: Paint, left: Int, right: Int, top: Int, baseline: Int, bottom: Int,
text: CharSequence, start: Int, end: Int, lnum: Int
) {
val style = p.style
val color = p.color
p.style = Paint.Style.FILL
p.color = this.color
rect.set(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())
c.drawRect(rect, p)
p.color = color
p.style = style
}
override fun getLeadingMargin(first: Boolean) = 30
override fun drawLeadingMargin(
c: Canvas, p: Paint, x: Int, dir: Int, top: Int, baseline: Int, bottom: Int,
text: CharSequence, start: Int, end: Int, first: Boolean, layout: Layout
) = Unit
}

View File

@ -1,41 +0,0 @@
package com.fastaccess.markdown.spans
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.ImageSpan
import android.widget.TextView
import com.fastaccess.markdown.spans.drawable.DrawableGetter
import net.nightwhistler.htmlspanner.SpanStack
import net.nightwhistler.htmlspanner.TagNodeHandler
import org.htmlcleaner.ContentNode
import org.htmlcleaner.TagNode
/**
* Created by Kosh on 22 Apr 2017, 1:09 PM
*/
class DrawableHandler(
private val textView: TextView,
private val width: Int = 0
) : TagNodeHandler() {
override fun beforeChildren(node: TagNode?, builder: SpannableStringBuilder?, spanStack: SpanStack?) {
super.beforeChildren(node, builder, spanStack)
node?.addChild(ContentNode("\n"))
}
override fun handleTagNode(node: TagNode?, builder: SpannableStringBuilder?, start: Int, end: Int, spanStack: SpanStack?) {
node?.let { n ->
builder?.let { b ->
val src = n.getAttributeByName("fallback-src") ?: n.getAttributeByName("data-canonical-src") ?: n.getAttributeByName("src")
if (!src.isNullOrEmpty()) {
b.append("")
b.append("\n")
val imageGetter = DrawableGetter(textView, width, n.getAttributeByName("src") ?: src)
b.setSpan(ImageSpan(imageGetter.getDrawable(src)), start, b.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
// b.setSpan(CenterSpan(), start, b.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) // no center for now
b.append("\n")
}
}
}
}
}

View File

@ -1,28 +0,0 @@
package com.fastaccess.markdown.spans
import android.text.SpannableStringBuilder
import com.fastaccess.markdown.emoji.EmojiManager
import net.nightwhistler.htmlspanner.SpanStack
import net.nightwhistler.htmlspanner.TagNodeHandler
import org.htmlcleaner.TagNode
/**
* Created by Kosh on 27 May 2017, 4:54 PM
*/
class EmojiHandler : TagNodeHandler() {
override fun handleTagNode(node: TagNode?, builder: SpannableStringBuilder?, start: Int, end: Int, spanStack: SpanStack?) {
val emoji = node?.getAttributeByName("alias")
if (emoji != null) {
val unicode = EmojiManager.getForAlias(emoji)
if (unicode?.unicode != null) {
builder?.replace(start, end, " " + unicode.unicode + " ")
}
} else if (node?.text != null) {
val unicode = EmojiManager.getForAlias(node.text.toString())
if (unicode?.unicode != null) {
builder?.replace(start, end, " " + unicode.unicode + " ")
}
}
}
}

View File

@ -1,21 +0,0 @@
package com.fastaccess.markdown.spans
import android.text.ParcelableSpan
import android.text.TextPaint
import android.text.style.StyleSpan
class FontSpan(
private val size: Float,
style: Int
) : StyleSpan(style), ParcelableSpan {
override fun updateMeasureState(p: TextPaint) {
super.updateMeasureState(p)
p.textSize = p.textSize * size
}
override fun updateDrawState(tp: TextPaint) {
super.updateDrawState(tp)
updateMeasureState(tp)
}
}

View File

@ -1,38 +0,0 @@
package com.fastaccess.markdown.spans
import android.text.SpannableStringBuilder
import android.text.style.RelativeSizeSpan
import com.fastaccess.markdown.extension.getFontFamilySpan
import net.nightwhistler.htmlspanner.SpanStack
import net.nightwhistler.htmlspanner.TagNodeHandler
import net.nightwhistler.htmlspanner.spans.FontFamilySpan
import org.htmlcleaner.TagNode
/**
* Created by Kosh on 29.09.17.
*/
class HeaderHandler(private val size: Float) : TagNodeHandler() {
override fun beforeChildren(node: TagNode?, builder: SpannableStringBuilder?, spanStack: SpanStack?) {
builder?.let { appendNewLine(it) }
}
override fun handleTagNode(node: TagNode?, builder: SpannableStringBuilder?, start: Int, end: Int, spanStack: SpanStack?) {
builder?.let { b ->
b.setSpan(RelativeSizeSpan(this.size), start, end, 33)
val originalSpan = getFontFamilySpan(b, start, end)
val boldSpan: FontFamilySpan
if (originalSpan == null) {
boldSpan = FontFamilySpan(spanner.fontResolver.defaultFont)
} else {
boldSpan = FontFamilySpan(originalSpan.fontFamily)
boldSpan.isItalic = originalSpan.isItalic
}
boldSpan.isBold = true
b.setSpan(boldSpan, start, end, 33)
appendNewLine(b)
}
}
}

View File

@ -1,30 +0,0 @@
package com.fastaccess.markdown.spans
import android.text.SpannableStringBuilder
import android.text.Spanned
import net.nightwhistler.htmlspanner.SpanStack
import net.nightwhistler.htmlspanner.TagNodeHandler
import net.nightwhistler.htmlspanner.spans.CenterSpan
import org.htmlcleaner.TagNode
/**
* Created by kosh on 30/07/2017.
*/
class HrHandler(
private val color: Int = 0,
private val width: Int = 0
) : TagNodeHandler() {
override fun handleTagNode(node: TagNode?, builder: SpannableStringBuilder?, start: Int, end: Int, spanStack: SpanStack?) {
builder?.let { spannableStringBuilder ->
appendNewLine(spannableStringBuilder)
val b = SpannableStringBuilder("$")
val hrSpan = HrSpan(color, width)
b.setSpan(hrSpan, 0, b.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
b.setSpan(CenterSpan(), 0, b.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
appendNewLine(b)
spannableStringBuilder.append(b)
}
}
}

View File

@ -1,34 +0,0 @@
package com.fastaccess.markdown.spans
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.text.style.LineHeightSpan
import android.text.style.ReplacementSpan
class HrSpan internal constructor(
private val color: Int,
private val width: Int
) : ReplacementSpan(), LineHeightSpan {
override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
return paint.measureText(text, start, end).toInt()
}
override fun draw(canvas: Canvas, text: CharSequence, start: Int, end: Int, x: Float, top: Int,
y: Int, bottom: Int, paint: Paint) {
val currentColor = paint.color
paint.color = color
paint.style = Paint.Style.FILL
val height = 10
canvas.drawRect(Rect(0, bottom - height, x.toInt() + width, bottom), paint)
paint.color = currentColor
}
override fun chooseHeight(text: CharSequence, start: Int, end: Int, spanstartv: Int, v: Int, fm: Paint.FontMetricsInt) {
fm.top /= 3
fm.ascent /= 3
fm.bottom /= 3
fm.descent /= 3
}
}

View File

@ -1,17 +0,0 @@
package com.fastaccess.markdown.spans
import android.graphics.Typeface
import android.text.SpannableStringBuilder
import net.nightwhistler.htmlspanner.SpanStack
import net.nightwhistler.htmlspanner.TagNodeHandler
import org.htmlcleaner.TagNode
/**
* Created by Kosh on 06 May 2017, 11:02 AM
*/
class ItalicHandler : TagNodeHandler() {
override fun handleTagNode(node: TagNode?, builder: SpannableStringBuilder?, start: Int, end: Int, spanStack: SpanStack?) {
builder?.setSpan(FontSpan(1f, Typeface.ITALIC), start, builder.length, 33)
}
}

View File

@ -1,37 +0,0 @@
package com.fastaccess.markdown.spans
import android.graphics.Color
import android.text.SpannableStringBuilder
import net.nightwhistler.htmlspanner.SpanStack
import net.nightwhistler.htmlspanner.TagNodeHandler
import org.htmlcleaner.TagNode
/**
* Created by Kosh on 10 May 2017, 8:46 PM
*/
class LinkHandler : TagNodeHandler() {
override fun handleTagNode(
node: TagNode,
builder: SpannableStringBuilder,
start: Int,
end: Int,
spanStack: SpanStack
) {
val href = node.getAttributeByName("href")
val url = if (!href.isNullOrEmpty()) {
href.toString()
} else if (!node.text.isNullOrEmpty()) {
"https://github.com/${node.text}"
} else {
null
}
url?.let { builder.setSpan(UrlSpan(href, linkColor), start, end, 33) }
}
companion object {
private val linkColor = Color.parseColor("#4078C0")
}
}

View File

@ -1,79 +0,0 @@
package com.fastaccess.markdown.spans
import android.graphics.drawable.Drawable
import android.text.SpannableStringBuilder
import com.fastaccess.markdown.widget.SpannableBuilder
import net.nightwhistler.htmlspanner.SpanStack
import net.nightwhistler.htmlspanner.TagNodeHandler
import org.htmlcleaner.TagNode
class ListsHandler(
private val checked: Drawable? = null,
private val unchecked: Drawable? = null
) : TagNodeHandler() {
private fun getMyIndex(node: TagNode): Int {
if (node.parent == null) {
return -1
} else {
var i = 1
for (child in node.parent.children) {
if (child === node) {
return i
}
if (child is TagNode) {
if ("li" == child.name) {
++i
}
}
}
return -1
}
}
private fun getParentName(node: TagNode): String? {
return if (node.parent == null) null else node.parent.name
}
override fun beforeChildren(node: TagNode?, builder: SpannableStringBuilder?, spanStack: SpanStack?) {
node?.let { n ->
var todoItem: TodoItems? = null
if (n.childTags?.isNotEmpty() == true) {
for (tagNode in n.childTags) {
if (tagNode.name != null && tagNode.name == "input") {
todoItem = TodoItems()
todoItem.isChecked = tagNode.getAttributeByName("checked") != null
break
}
}
}
when {
"ol" == getParentName(n) -> builder?.append(getMyIndex(n).toString())?.append(". ")
"ul" == getParentName(n) -> if (todoItem != null) {
if (checked == null || unchecked == null) {
builder?.append(if (todoItem.isChecked) "" else "")
} else {
builder?.append(SpannableBuilder.builder()
.append(if (todoItem.isChecked) checked else unchecked))
?.append(" ")
}
} else {
builder?.append("\u2022 ")
}
else -> null
}
}
}
override fun handleTagNode(node: TagNode?, builder: SpannableStringBuilder?, start: Int, end: Int, spanStack: SpanStack?) {
builder?.let(this::appendNewLine)
}
internal class TodoItems {
var isChecked: Boolean = false
}
}

View File

@ -1,30 +0,0 @@
package com.fastaccess.markdown.spans
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.LeadingMarginSpan
import net.nightwhistler.htmlspanner.SpanStack
import net.nightwhistler.htmlspanner.TagNodeHandler
import org.htmlcleaner.TagNode
/**
* Created by Kosh on 29 Apr 2017, 11:59 PM
*/
class MarginHandler : TagNodeHandler() {
override fun beforeChildren(node: TagNode?, builder: SpannableStringBuilder?, spanStack: SpanStack?) {
builder?.let {
if (builder.isNotEmpty() && builder[builder.length - 1].toInt() != 10) { //'10 = \n'
this.appendNewLine(builder)
}
}
}
override fun handleTagNode(node: TagNode?, builder: SpannableStringBuilder?, start: Int, end: Int, spanStack: SpanStack?) {
builder?.let {
builder.setSpan(LeadingMarginSpan.Standard(30), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
appendNewLine(builder)
}
}
}

View File

@ -1,55 +0,0 @@
package com.fastaccess.markdown.spans
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.text.Layout
import android.text.style.LeadingMarginSpan
import androidx.annotation.ColorInt
import com.fastaccess.github.extensions.toPx
import kotlin.math.max
import kotlin.math.min
/**
* Created by Kosh on 2019-06-30.
*/
class MdQouteSpan(
@ColorInt private val paintColor: Int
) : LeadingMarginSpan {
private val rect = Rect()
private val paint = Paint()
override fun getLeadingMargin(first: Boolean): Int = LEADING_MARGIN.toPx()
override fun drawLeadingMargin(
c: Canvas?,
p: Paint?,
x: Int,
dir: Int,
top: Int,
baseline: Int,
bottom: Int,
text: CharSequence?,
start: Int,
end: Int,
first: Boolean,
layout: Layout?
) {
val w = WIDTH.toPx()
paint.set(p)
paint.style = Paint.Style.FILL
paint.color = paintColor
val leftWithMargin = x + dir * w
val rightWithMargin = leftWithMargin + dir * w
val left = min(leftWithMargin, rightWithMargin)
val right = max(leftWithMargin, rightWithMargin)
rect.set(left, top, right, bottom)
c?.drawRect(rect, paint)
}
companion object {
private const val LEADING_MARGIN = 24
private const val WIDTH = 4
}
}

View File

@ -1,82 +0,0 @@
package com.fastaccess.markdown.spans
import android.graphics.Color
import android.text.SpannableStringBuilder
import android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
import android.text.style.BackgroundColorSpan
import android.text.style.ForegroundColorSpan
import android.text.style.TypefaceSpan
import com.fastaccess.github.extensions.isTrue
import net.nightwhistler.htmlspanner.SpanStack
import net.nightwhistler.htmlspanner.TextUtil
import net.nightwhistler.htmlspanner.handlers.PreHandler
import org.htmlcleaner.ContentNode
import org.htmlcleaner.TagNode
/**
* Created by Kosh on 22 Apr 2017, 1:07 PM
*/
class PreTagHandler(
private val color: Int = 0,
private val isPre: Boolean = false,
private val isLightTheme: Boolean = true
) : PreHandler() {
private fun getPlainText(buffer: StringBuffer, node: Any?) {
if (node is ContentNode) {
val text = TextUtil.replaceHtmlEntities(node.content.toString(), true)
buffer.append(text)
} else if (node is TagNode) {
for (child in node.allChildren) {
getPlainText(buffer, child)
}
}
}
private fun replace(text: String): String {
return text.replace("&nbsp;".toRegex(), "\u00A0")
.replace("&amp;", "&", true)
.replace("&quot;", "\"", true)
.replace("&cent;", "¢", true)
.replace("&lt;", "<", true)
.replace("&gt;", ">", true)
.replace("&sect;", "§", true)
.replace("&ldquo;", "", true)
.replace("&rdquo;", "", true)
.replace("&lsquo;", "", true)
.replace("&rsquo;", "", true)
.replace("&ndash;", "\u2013", true)
.replace("&mdash;", "\u2014", true)
.replace("&horbar;", "\u2015", true)
}
override fun beforeChildren(node: TagNode?, builder: SpannableStringBuilder?, spanStack: SpanStack?) {
super.beforeChildren(node, builder, spanStack)
isPre.isTrue { node?.addChild(ContentNode("\n")) } // append fake hr
}
override fun handleTagNode(node: TagNode?, builder: SpannableStringBuilder?, start: Int, end: Int, spanStack: SpanStack?) {
builder?.let {
if (isPre) {
val buffer = StringBuffer()
buffer.append("\n")//fake padding top + make sure, pre is always by itself
getPlainText(buffer, node)
builder.append(replace(buffer.toString()))
this.appendNewLine(builder)
builder.setSpan(CodeBackgroundRoundedSpan(color), start, builder.length, SPAN_EXCLUSIVE_EXCLUSIVE)
} else {
val text = node?.text ?: ""
builder.append(" ")
builder.append(replace(text.toString()))
builder.append(" ")
val stringStart = start + 1
val stringEnd = builder.length - 1
builder.setSpan(BackgroundColorSpan(color), stringStart, stringEnd, SPAN_EXCLUSIVE_EXCLUSIVE)
if (isLightTheme) {
builder.setSpan(ForegroundColorSpan(Color.RED), stringStart, stringEnd, SPAN_EXCLUSIVE_EXCLUSIVE)
}
builder.setSpan(TypefaceSpan("monospace"), stringStart, stringEnd, SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
}
}

View File

@ -1,24 +0,0 @@
package com.fastaccess.markdown.spans
import android.text.SpannableStringBuilder
import net.nightwhistler.htmlspanner.SpanStack
import net.nightwhistler.htmlspanner.TagNodeHandler
import org.htmlcleaner.TagNode
/**
* Created by Kosh on 23 Apr 2017, 11:30 AM
*/
class QuoteHandler(private val color: Int = 0) : TagNodeHandler() {
override fun handleTagNode(
node: TagNode?,
builder: SpannableStringBuilder?,
start: Int,
end: Int,
spanStack: SpanStack?
) {
builder?.setSpan(MdQouteSpan(color), if (start > builder.length - 1) start + 1 else start, builder.length - 1, 33)
}
}

View File

@ -1,14 +0,0 @@
package com.fastaccess.markdown.spans
import android.text.SpannableStringBuilder
import android.text.style.StrikethroughSpan
import net.nightwhistler.htmlspanner.SpanStack
import net.nightwhistler.htmlspanner.TagNodeHandler
import org.htmlcleaner.TagNode
class StrikethroughHandler : TagNodeHandler() {
override fun handleTagNode(node: TagNode?, builder: SpannableStringBuilder?, start: Int, end: Int, spanStack: SpanStack?) {
builder?.setSpan(StrikethroughSpan(), start, end, 33)
}
}

View File

@ -1,17 +0,0 @@
package com.fastaccess.markdown.spans
import android.text.SpannableStringBuilder
import android.text.style.RelativeSizeSpan
import android.text.style.SubscriptSpan
import net.nightwhistler.htmlspanner.SpanStack
import net.nightwhistler.htmlspanner.TagNodeHandler
import org.htmlcleaner.TagNode
class SubScriptHandler : TagNodeHandler() {
override fun handleTagNode(node: TagNode?, builder: SpannableStringBuilder?, start: Int, end: Int, spanStack: SpanStack) {
builder?.let {
builder.setSpan(SubscriptSpan(), start, end, 33)
builder.setSpan(RelativeSizeSpan(0.8f), start, end, 33)
}
}
}

View File

@ -1,18 +0,0 @@
package com.fastaccess.markdown.spans
import android.text.SpannableStringBuilder
import android.text.style.RelativeSizeSpan
import android.text.style.SuperscriptSpan
import net.nightwhistler.htmlspanner.SpanStack
import net.nightwhistler.htmlspanner.TagNodeHandler
import org.htmlcleaner.TagNode
class SuperScriptHandler : TagNodeHandler() {
override fun handleTagNode(node: TagNode?, builder: SpannableStringBuilder?, start: Int, end: Int, spanStack: SpanStack?) {
builder?.let {
builder.setSpan(SuperscriptSpan(), start, end, 33)
builder.setSpan(RelativeSizeSpan(0.8f), start, end, 33)
}
}
}

View File

@ -1,15 +0,0 @@
package com.fastaccess.markdown.spans
import android.text.SpannableStringBuilder
import android.text.style.UnderlineSpan
import net.nightwhistler.htmlspanner.SpanStack
import net.nightwhistler.htmlspanner.TagNodeHandler
import org.htmlcleaner.TagNode
class UnderlineHandler : TagNodeHandler() {
override fun handleTagNode(node: TagNode?, builder: SpannableStringBuilder?, start: Int, end: Int, spanStack: SpanStack?) {
builder?.setSpan(UnderlineSpan(), start, end, 33)
}
}

View File

@ -1,18 +0,0 @@
package com.fastaccess.markdown.spans
import android.text.TextPaint
import android.text.style.URLSpan
/**
* Created by Kosh on 2019-06-30.
*/
class UrlSpan (
url: String,
private val textColor: Int
) : URLSpan(url) {
override fun updateDrawState(textPaint: TextPaint) {
super.updateDrawState(textPaint)
textPaint.color = textColor
textPaint.isUnderlineText = false
}
}

View File

@ -1,68 +0,0 @@
package com.fastaccess.markdown.spans.drawable
import android.graphics.drawable.Drawable
import android.text.Html
import android.widget.TextView
import androidx.annotation.NonNull
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.gif.GifDrawable
import com.bumptech.glide.request.RequestOptions
import com.fastaccess.github.extensions.getDrawableCompat
import com.fastaccess.markdown.R
import timber.log.Timber
/**
* Created by Kosh on 22 Apr 2017, 7:44 PM
*/
class DrawableGetter(
private val tv: TextView?,
private val width: Int,
private val url: String
) : Html.ImageGetter, Drawable.Callback {
private val cachedTargets = hashSetOf<GlideDrawableTarget<out Drawable>>()
init {
tv?.setTag(R.id.drawable_callback, this)
}
override fun getDrawable(oriUrl: String): Drawable {
val urlDrawable = UrlDrawable()
tv?.let {
val context = it.context ?: return urlDrawable
val imageTarget = if (oriUrl.endsWith(".gif")) {
GlideDrawableTarget<GifDrawable>(urlDrawable, tv, width)
} else {
GlideDrawableTarget<Drawable>(urlDrawable, tv, width)
}
Glide.with(it).apply {
clear(it)
applyDefaultRequestOptions(RequestOptions().diskCacheStrategy(DiskCacheStrategy.ALL)
.placeholder(context.getDrawableCompat(R.drawable.ic_image)))
if (oriUrl.endsWith(".gif")) {
asGif().load(url).into(imageTarget as GlideDrawableTarget<GifDrawable>)
} else {
asDrawable().load(url).into(imageTarget as GlideDrawableTarget<Drawable>)
}
}
cachedTargets.add(imageTarget)
}
return urlDrawable
}
override fun invalidateDrawable(@NonNull drawable: Drawable) {
tv?.invalidate()
}
override fun scheduleDrawable(@NonNull drawable: Drawable, @NonNull runnable: Runnable, l: Long) {}
override fun unscheduleDrawable(@NonNull drawable: Drawable, @NonNull runnable: Runnable) {}
fun clear(@NonNull drawableGetter: DrawableGetter) {
Timber.e("clearing......")
for (target in drawableGetter.cachedTargets) {
tv?.let { Glide.with(it).clear(target) }
}
}
}

View File

@ -1,67 +0,0 @@
package com.fastaccess.markdown.spans.drawable
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.widget.TextView
import com.bumptech.glide.load.resource.gif.GifDrawable
import com.bumptech.glide.request.target.SimpleTarget
import com.bumptech.glide.request.transition.Transition
import com.fastaccess.markdown.R
internal class GlideDrawableTarget<T : Drawable>(
private val urlDrawable: UrlDrawable,
private val container: TextView?,
private val width: Int
) : SimpleTarget<T>() {
override fun onResourceReady(resource: T, transition: Transition<in T>?) {
val textView = container
textView?.post {
val width: Float
val height: Float
if (resource.intrinsicWidth >= this.width) {
val downScale = resource.intrinsicWidth.toFloat() / this.width
width = (resource.intrinsicWidth.toDouble() / downScale.toDouble() / 1.3).toFloat()
height = (resource.intrinsicHeight.toDouble() / downScale.toDouble() / 1.3).toFloat()
} else {
val multiplier = this.width.toFloat() / resource.intrinsicWidth
width = resource.intrinsicWidth.toFloat() * multiplier
height = resource.intrinsicHeight.toFloat() * multiplier
}
val rect = Rect(0, 0, Math.round(width), Math.round(height))
resource.bounds = rect
urlDrawable.bounds = rect
urlDrawable.setDrawable(resource)
urlDrawable.callback = textView.getTag(R.id.drawable_callback) as DrawableGetter
if (resource is GifDrawable) {
resource.setLoopCount(GifDrawable.LOOP_FOREVER)
resource.start()
}
textView.text = textView.text
textView.invalidate()
}
}
override fun onLoadStarted(placeholder: Drawable?) {
super.onLoadStarted(placeholder)
applyTempDrawable(placeholder)
}
override fun onLoadFailed(errorDrawable: Drawable?) {
super.onLoadFailed(errorDrawable)
applyTempDrawable(errorDrawable)
}
private fun applyTempDrawable(placeholder: Drawable?) {
placeholder?.let { drawable ->
val rect = Rect(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
drawable.bounds = rect
urlDrawable.bounds = rect
urlDrawable.setDrawable(drawable)
container?.let {
it.text = it.text
it.invalidate()
}
}
}
}

View File

@ -1,45 +0,0 @@
package com.fastaccess.markdown.spans.drawable
import android.graphics.Canvas
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import com.bumptech.glide.load.resource.gif.GifDrawable
class UrlDrawable : BitmapDrawable(), Drawable.Callback {
private var drawable: Drawable? = null
override fun draw(canvas: Canvas) {
drawable?.let {
it.draw(canvas)
if (it is GifDrawable) {
if (!it.isRunning) it.start()
}
}
}
override fun invalidateDrawable(who: Drawable) {
if (callback != null) {
callback!!.invalidateDrawable(who)
}
}
override fun scheduleDrawable(who: Drawable, what: Runnable, `when`: Long) {
if (callback != null) {
callback!!.scheduleDrawable(who, what, `when`)
}
}
override fun unscheduleDrawable(who: Drawable, what: Runnable) {
if (callback != null) {
callback!!.unscheduleDrawable(who, what)
}
}
fun setDrawable(drawable: Drawable) {
this.drawable?.callback = this
drawable.callback = this
this.drawable = drawable
}
}

View File

@ -1,21 +0,0 @@
package com.fastaccess.markdown.widget
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
/**
* Created by Kosh on 02.02.19.
*/
class HtmlTextView constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : AppCompatTextView(context, attrs, defStyle) {
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
fun setHtml(text: String? = null) {
//TODO
}
}