Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Question] Can't seem to get SimpleTagHandler to work with custom html tags #235

Closed
wax911 opened this issue Apr 16, 2020 · 9 comments
Closed
Milestone

Comments

@wax911
Copy link

wax911 commented Apr 16, 2020

  • Markwon version: 4.3.1

Behaviour

I've been having trouble getting markwon to register iframe tags, I've got bunch of html with some iframe that embed youtube content e.g. <iframe src="https://www.youtube.com/embed/luWcue3t2OU" width="640" height="360"></iframe>

I tried to debug the issue, by adding a breakpoint on getSpans but the method never ran, suggesting that no iframe tags were found. The html is fairly plain, consisting of a few basic tags.

Here's an example of the input taken from an rss feed:

<p class="p1"><img title="JUMP FORCE" src="https://img1.ak.crunchyroll.com/i/spire1/f0c009039dd9f8dff5907fff148adfca1587067000_full.jpg" alt="JUMP FORCE" width="640" height="362" /></p>
<p class="p2">&nbsp;</p>
<p class="p1">Switch owners will soon get to take part in the ultimate <em>Shonen Jump </em>rumble. Bandai Namco announced plans to bring <strong><em>Jump Force </em></strong>to <strong>Switch</strong> as <strong><em>Jump Force Deluxe Edition</em></strong>, 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 <strong>Character Pass 2 is also in the works </strong>for all versions, starting with <strong>Shoto Todoroki from </strong><span style="color: #ff9900;"><a href="/my-hero-academia?utm_source=editorial_cr&amp;utm_medium=news&amp;utm_campaign=article_driven&amp;referrer=editorial_cr_news_article_driven"><span style="color: #ff9900;"><strong><em>My Hero Academia</em></strong></span></a></span>.</p>
<p class="p2">&nbsp;</p>
<p class="p1">Other than Todoroki, Bandai Namco hinted that the four other Character Pass 2 characters will hail from <span style="color: #ff9900;"><a href="/hunter-x-hunter?utm_source=editorial_cr&amp;utm_medium=news&amp;utm_campaign=article_driven&amp;referrer=editorial_cr_news_article_driven"><span style="color: #ff9900;"><em>Hunter x Hunter</em></span></a></span>, <em>Yu Yu Hakusho</em>, <span style="color: #ff9900;"><a href="/bleach?utm_source=editorial_cr&amp;utm_medium=news&amp;utm_campaign=article_driven&amp;referrer=editorial_cr_news_article_driven"><span style="color: #ff9900;"><em>Bleach</em></span></a></span>, and <span style="color: #ff9900;"><a href="/jojos-bizarre-adventure?utm_source=editorial_cr&amp;utm_medium=news&amp;utm_campaign=article_driven&amp;referrer=editorial_cr_news_article_driven"><span style="color: #ff9900;"><em>JoJo's Bizarre Adventure</em></span></a></span>. Character Pass 2 will be priced at $17.99, and Todoroki launches this spring.<span class="Apple-converted-space">&nbsp;</span></p>
<p class="p2">&nbsp;</p>
<p class="p1"><iframe style="display: block; margin-left: auto; margin-right: auto;" src="https://www.youtube.com/embed/At1qTj-LWCc" frameborder="0" width="640" height="360"></iframe></p>
<p class="p2">&nbsp;</p>
<p class="p1">Character Pass 2 promo:</p>
<p class="p2">&nbsp;</p>
<p class="p1"><iframe style="display: block; margin-left: auto; margin-right: auto;" src="https://www.youtube.com/embed/CukwN6kV4R4" frameborder="0" width="640" height="360"></iframe></p>
<p class="p2">&nbsp;</p>
<p class="p1"><a href="https://got.cr/PremiumTrial-NewsBanner4"><img style="display: block; margin-left: auto; margin-right: auto;" src="https://img1.ak.crunchyroll.com/i/spire4/78f5441d927cf160a93e037b567c2b1f1587067041_full.png" alt="" width="640" height="43" /></a></p>
<p class="p2">&nbsp;</p>
<p class="p1">-------</p>
<p class="p1"><em>Joseph Luster is the Games and Web editor at </em><a href="http://www.otakuusamagazine.com/ME2/Default.asp"><em>Otaku USA Magazine</em></a><em>. You can read his webcomic, </em><a href="http://subhumanzoids.com/comics/big-dumb-fighting-idiots/">BIG DUMB FIGHTING IDIOTS</a><em> at </em><a href="http://subhumanzoids.com/"><em>subhumanzoids</em></a><em>. Follow him on Twitter </em><a href="https://twitter.com/Moldilox"><em>@Moldilox</em></a><em>.</em><span class="Apple-converted-space">&nbsp;</span></p>

Reproduction

I have setup markwon as follows:

private val coreModule = module {
    single {
        Markwon.builder(androidApplication())
            .usePlugin(HtmlPlugin.create())
            .usePlugin(LinkifyPlugin.create())
            .usePlugin(TagPlugin.create())
            .usePlugin(
                GlideImagesPlugin.create(
                    object : GlideImagesPlugin.GlideStore {
                        override fun cancel(target: Target<*>) {
                            Glide.with(androidApplication()).clear(target)
                        }

                        override fun load(drawable: AsyncDrawable): RequestBuilder<Drawable> {
                            return Glide.with(androidApplication()).load(drawable.destination)
                                .transform(
                                    CenterCrop(),
                                    RoundedCorners(
                                        androidApplication().resources
                                            .getDimensionPixelSize(
                                                R.dimen.md_margin
                                            )
                                    )
                                ).placeholder(R.drawable.ic_launcher_foreground)
                        }
                    }
                )
            )
            .build()
    }
}
class TagPlugin private constructor(): AbstractMarkwonPlugin() {

    override fun configure(registry: MarkwonPlugin.Registry) {
        registry.require(HtmlPlugin::class.java) {
            it.addHandler(EmbedTagHandler.create())
            it.addHandler(TagAlignmentHandler.create())
        }
    }

    companion object {
        fun create() = TagPlugin()
    }
}
/**
 * Allows us to handle iframes and extract images from it, we will only target youtube iframes
 * to return a clickable image which should trigger the youtube
 *
 * <iframe src="https://www.youtube.com/embed/luWcue3t2OU" width="640" height="360" />
 */
class EmbedTagHandler private constructor() : SimpleTagHandler() {

    private fun getVideoId(src: String) = src.split('/').last()

    private fun getVideoUrl(src: String): String {
        val videoId = getVideoId(src)
        return "https://youtube.com/watch?v=$videoId"
    }

    private fun createImageLink(src: String): String {
        val videoId = getVideoId(src)
        return "https://img.youtube.com/vi/$videoId/hqdefault.jpg"
    }

    override fun getSpans(
        configuration: MarkwonConfiguration,
        renderProps: RenderProps,
        tag: HtmlTag
    ): Any? {
        val attributes = tag.attributes()
        val source = attributes["src"]
        return if (source?.contains("youtube") == true) {
            val imageSpanFactory = configuration
                .spansFactory()
                .get(Image::class.java)

           // ignore this, I'm actually not sure if this will work :'D (trying to make image clickable)
            val linkSpanFactory = configuration
                .spansFactory()
                .get(Link::class.java)

            val width = attributes["width"]?.toFloat()
            val height = attributes["height"]?.toFloat()
            val imageSize = ImageSize(
                width?.let { ImageSize.Dimension(it, "px") },
                height?.let { ImageSize.Dimension(it, "px") }
            )

            ImageProps.DESTINATION.set(renderProps, createImageLink(source))
            ImageProps.IMAGE_SIZE.set(renderProps, imageSize);
            ImageProps.REPLACEMENT_TEXT_IS_LINK.set(renderProps, false);
            CoreProps.LINK_DESTINATION.set(renderProps, getVideoUrl(source))

            arrayOf(
                imageSpanFactory?.getSpans(configuration, renderProps),
                linkSpanFactory?.getSpans(configuration, renderProps)
            )
        } else {
            // return some sort of unsupported span
            val textSpan = configuration.spansFactory().get(Text::class.java)
            renderProps.set(
                Prop.of("text-literal"),
                "Unsupported embedded element"
            )
            textSpan?.getSpans(configuration, renderProps)
        }
    }

    override fun supportedTags() = listOf("iframe")

    companion object {
        fun create() = EmbedTagHandler()
    }
}

Besides this everything else works perfectly!
Thank you in advance 😺

@noties
Copy link
Owner

noties commented Apr 17, 2020

Hello @wax911 !

Well, your handler is most likely being called, but the spans are not displayed. Markwon uses spans heavily and those cannot have zero length. If you look at your iframe definitions, you will see that those tags doesn't have any content. If you add even a single character (any really with non-0 length), you will see you iframe being handled.

This case is actually handled by HtmlEmptyTagReplacement class in markwon-html module. Unfortunately right now there is no way to actually specify it for the HtmlPlugin. I'm going to add this customization in the next release

@wax911
Copy link
Author

wax911 commented Apr 17, 2020

That makes sense 😄 In that case would that also apply to self terminating tags <iframe {props} />? If so I could apply the changes inside a mapper

@noties
Copy link
Owner

noties commented Apr 17, 2020

In that case would that also apply to self terminating tags <iframe {props} />?

You mean the HtmlEmptyTagReplacement ? If so, then yes, both - tags without content and self-closing tags - are handled by the HtmlEmptyTagReplacement (currently it handles br and img tags only)

What kind of a mapper you are talking about?

@wax911
Copy link
Author

wax911 commented Apr 17, 2020

(currently it handles br and img tags only)

I see, thanks for the clarification!

What kind of a mapper you are talking about?

Just a simple data mapper in my data layer for converting network models to application entities, just thought I could use some regex to add some dummy characters between the empty iframe tags

@noties
Copy link
Owner

noties commented Apr 17, 2020

If you do not control the content, then yes, adding a dummy character (even space would work)

P.S. leaving it here as you'd mentioned regex and parsing html in one sentence 😄

@wax911
Copy link
Author

wax911 commented Apr 17, 2020

🤣 the rabbit hole goes deep, I'll do a little bit of research see if I can find something light weight 😉

Thanks for heads up and help!!

@noties noties added this to the 4.4.0 milestone May 2, 2020
@noties noties mentioned this issue May 14, 2020
@noties noties closed this as completed May 19, 2020
@wax911
Copy link
Author

wax911 commented May 22, 2020

Thank you @noties 😃 just tested and it works everything works like a charm! Found a minor issue, it seems like if I have a tag handler for center the iframe tag handler stops working. So given the following examples:

<html>

<head></head>

<body>
    <p></p>
    <h3>LiSA's Sword Art Online: Alicization OP Song "ADAMAS" Certified Platinum with 250,000 Downloads</h3>
    <p></p>
    <h5>The upper tune was already certified Gold one month after its digital release</h5>
    <p>According to The Recording Industry Association of Japan (RIAJ)'s monthly report for April 2020, one of the <span
            style="color: #ff9900;"><strong><a href="http://www.lxixsxa.com/" target="_blank"><span
                        style="color: #ff9900;">LiSA</span></a></strong></span>'s 14th single songs,
        <strong>"ADAMAS"</strong>&nbsp;(the first OP theme for the TV anime <a href="/sword-art-online"
            target="_blank"><span style="color: #ff9900;"><strong><em>Sword Art Online:
                        Alicization</em></strong></span></a>) has been certified <strong>Platinum</strong> for
        surpassing 250,000 downloads.</p>
    <p>&nbsp;</p>
    <p>As a double A-side single with <strong>"Akai Wana (who loves it?),"</strong> <strong>"ADAMAS"</strong> was
        released from SACRA Music in Japan on December 12, 2018. Its CD single ranked second in Oricon's weekly single
        chart by selling 35,000 copies in its first week. Meanwhile, the song was released digitally two months prior to
        its CD release, October 8, then reached Gold (100,000 downloads) in the following month.</p>
    <p>&nbsp;</p>
    <p>&nbsp;</p>
    <center>
        <p><strong>"ADAMAS"</strong> MV YouTube EDIT ver.:</p>
        <p><iframe src="https://www.youtube.com/embed/UeEIl4JlE-g" frameborder="0" width="640" height="360"></iframe>
        </p>
        <p>&nbsp;</p>
        <p>Standard edition CD jacket:</p>
        <p><img src="https://img1.ak.crunchyroll.com/i/spire2/d7b1d6bc7563224388ef5ffc04a967581589950464_full.jpg"
                alt="" width="640" height="635"></p>
    </center>
    <p>&nbsp;&nbsp;</p>
    <hr>
    <p>&nbsp;</p>
    <p>Source: RIAJ press release</p>
    <p>&nbsp;</p>
    <p><em>©SACRA MUSIC</em></p>
    <p>&nbsp;</p>
    <p style="text-align: center;"><a href="https://got.cr/PremiumTrial-NewsBanner4"><em><img
                    src="https://img1.ak.crunchyroll.com/i/spire4/78f5441d927cf160a93e037b567c2b1f1559091520_full.png"
                    alt="" width="640" height="43"></em></a></p>
</body>

</html>

Everything but the iframe gets rendered, might you have an idea why this happens? If I remove my AlignmentTagHandler the problem gets solved.

internal class TagAlignmentHandler private constructor(): SimpleTagHandler() {

    override fun getSpans(
        configuration: MarkwonConfiguration,
        renderProps: RenderProps,
        tag: HtmlTag
    ): Any? = AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER)

    override fun supportedTags(): List<String> = listOf("center")

    companion object {
        fun create() = TagAlignmentHandler()
    }
}

@noties
Copy link
Owner

noties commented May 25, 2020

center tag is considered to be a block one and iframe is its child. You will need to use TagHandler instead of SimpleTagHandler:

class CenterTagHandler 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(),
                new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER),
                tag.start(),
                tag.end()
        );
    }

    @NonNull
    @Override
    public Collection<String> supportedTags() {
        return Collections.singleton("center");
    }
}

it feels like SimpleTagHandler could have this functionality build-in

@wax911
Copy link
Author

wax911 commented May 25, 2020

That seems to work thanks again 😆

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants