如何截断 HTML 字符串,用作原始的 "preview" 版本?

How to truncate HTML string, to be used as a "preview" version of the original?

背景

我们允许用户使用富文本编辑器库(称为 Android-RTEditor)创建一些将转换为 HTML 的文本。

输出 HTML 文本按原样保存在服务器和设备上。

因为在某些最终情况下,需要显示很多此内容(多个实例),我们希望还保存此内容的 "preview" 版本,这意味着它会更短长度(比如 120 个普通字符,不包括 HTML 标签的额外字符,这些字符不计算在内)。

我们想要的是HTML的最小化版本。可以选择删除一些标签,但无论我们选择做什么,我们仍然希望看到列表 (numbered/bullets),因为列表确实向用户显示文本(项目符号是一个字符,带点的数字)。

转到下一行的标记也应该处理,因为转到下一行很重要。

问题

与普通字符串相反,我可以在其中调用 substring 所需的字符数,在 HTML 上它可能会破坏标签。

我试过的

我想到了 2 种可能的解决方案:

  1. 转换为纯文本(同时处理了一些标签),然后截断:解析HTML,并用 Unicode 替换一些标签替代方案,同时删除其他方案。例如,代替项目符号列表,放置项目符号字符(可能 this),对于编号列表也是如此(代替数字)。所有其他标签都将被删除。转到下一行的标签(“
    ”)也是如此,应该用“\n”替换。在那之后,我可以安全地截断正常文本,因为没有更多的标签可以被破坏。

  2. 在 HTML 中很好地截断:解析 HTML,同时识别其中的文本,并在那里截断它到达截断位置时关闭所有标签。这可能更难。

我不确定哪个更容易,但我能想到每个可能的缺点。不过这只是一个预览,所以我认为这并不重要。

我在网上搜索了这样的解决方案,看看其他人是否已经做到了。 我发现了一些讨论 "cleaning" 或 "optimizing" HTML 的链接,但我看不到它们可以处理替换它们或截断它们。不仅如此,由于是HTML,大部分与Android无关,使用PHP、C#、Angular等语言

以下是我找到的一些链接:

问题

  1. 我写的那些解决方案可行吗?如果是这样,是否有一种已知的方法来实现它们?甚至 Java/Kotlin/Android 图书馆?做出这样的解决方案有多难?

  2. 也许我没有想到其他解决方案?


编辑: 我也尝试过使用我过去编写的旧代码 (here),它解析 XML。也许它会起作用。我现在也尝试研究一些用于解析 HTML 的第三方库,例如 Jsoup。我认为它可以帮助截断,同时支持 "faulty" HTML 输入。

好的,我想我明白了,使用我的旧代码将 XML 字符串转换为对象。看到更强大的解决方案仍然很棒,但我认为我得到的已经足够好了,至少现在是这样。

下面的代码使用它(原始 XmlTag class 可用 here):

XmlTagTruncationHelper.kt

object XmlTagTruncationHelper {
    /**@param maxLines max lines to permit. If <0, means there is no restriction
     * @param maxTextCharacters max text characters to permit. If <0, means there is no restriction*/
    class Restriction(val maxTextCharacters: Int, val maxLines: Int) {
        var currentTextCharactersCount: Int = 0
        var currentLinesCount: Int = 0
    }

    @JvmStatic
    fun truncateXmlTag(xmlTag: XmlTag, restriction: Restriction): String {
        if (restriction.maxLines == 0 || (restriction.maxTextCharacters >= 0 && restriction.currentTextCharactersCount >= restriction.maxTextCharacters))
            return ""
        val sb = StringBuilder()
        sb.append("<").append(xmlTag.tagName)
        val numberOfAttributes = if (xmlTag.tagAttributes != null) xmlTag.tagAttributes!!.size else 0
        if (numberOfAttributes != 0)
            for ((key, value) in xmlTag.tagAttributes!!)
                sb.append(" ").append(key).append("=\"").append(value).append("\"")
        val numberOfInnerContent = if (xmlTag.innerTagsAndContent != null) xmlTag.innerTagsAndContent!!.size else 0
        if (numberOfInnerContent == 0)
            sb.append("/>")
        else {
            sb.append(">")
            for (innerItem in xmlTag.innerTagsAndContent!!) {
                if (restriction.maxTextCharacters >= 0 && restriction.currentTextCharactersCount >= restriction.maxTextCharacters)
                    break
                if (innerItem is XmlTag) {
                    if (restriction.maxLines < 0)
                        sb.append(truncateXmlTag(innerItem, restriction))
                    else {
//                    Log.d("AppLog", "xmlTag:" + innerItem.tagName + " " + innerItem.innerTagsAndContent?.size)
                        var needToBreak = false
                        when {
                            innerItem.tagName == "br" -> {
                                ++restriction.currentLinesCount
                                needToBreak = restriction.currentLinesCount >= restriction.maxLines
                            }
                            innerItem.tagName == "li" -> {
                                ++restriction.currentLinesCount
                                needToBreak = restriction.currentLinesCount >= restriction.maxLines
                            }
                        }
                        if (needToBreak)
                            break
                        sb.append(truncateXmlTag(innerItem, restriction))
                    }
                } else if (innerItem is String) {
                    if (restriction.maxTextCharacters < 0)
                        sb.append(innerItem)
                    else
                        if (restriction.currentTextCharactersCount < restriction.maxTextCharacters) {
                            val str = innerItem
                            val extraCharactersAllowedToAdd = restriction.maxTextCharacters - restriction.currentTextCharactersCount
                            val strToAdd = str.substring(0, Math.min(str.length, extraCharactersAllowedToAdd))
                            if (strToAdd.isNotEmpty()) {
                                sb.append(strToAdd)
                                restriction.currentTextCharactersCount += strToAdd.length
                            }
                        }
                }
            }
            sb.append("</").append(xmlTag.tagName).append(">")
        }
        return sb.toString()
    }
}

XmlTag.kt

//based on 
/**
 * an xml tag , includes its name, value and attributes
 * @param tagName the name of the xml tag . for example : <a>b</a> . the name of the tag is "a"
 */
class XmlTag(val tagName: String) {
    /** a hashmap of all of the tag attributes. example: <a c="d" e="f">b</a> . attributes: {{"c"="d"},{"e"="f"}}     */
    @JvmField
    var tagAttributes: HashMap<String, String>? = null
    /**list of inner text and xml tags*/
    @JvmField
    var innerTagsAndContent: ArrayList<Any>? = null

    companion object {
        @JvmStatic
        fun getXmlFromString(input: String): XmlTag? {
            val factory = XmlPullParserFactory.newInstance()
            factory.isNamespaceAware = true
            val xpp = factory.newPullParser()
            xpp.setInput(StringReader(input))
            return getXmlRootTagOfXmlPullParser(xpp)
        }

        @JvmStatic
        fun getXmlRootTagOfXmlPullParser(xmlParser: XmlPullParser): XmlTag? {
            var currentTag: XmlTag? = null
            var rootTag: XmlTag? = null
            val tagsStack = Stack<XmlTag>()
            xmlParser.next()
            var eventType = xmlParser.eventType
            var doneParsing = false
            while (eventType != XmlPullParser.END_DOCUMENT && !doneParsing) {
                when (eventType) {
                    XmlPullParser.START_DOCUMENT -> {
                    }
                    XmlPullParser.START_TAG -> {
                        val xmlTagName = xmlParser.name
                        currentTag = XmlTag(xmlTagName)
                        if (tagsStack.isEmpty())
                            rootTag = currentTag
                        tagsStack.push(currentTag)
                        val numberOfAttributes = xmlParser.attributeCount
                        if (numberOfAttributes > 0) {
                            val attributes = HashMap<String, String>(numberOfAttributes)
                            for (i in 0 until numberOfAttributes) {
                                val attrName = xmlParser.getAttributeName(i)
                                val attrValue = xmlParser.getAttributeValue(i)
                                attributes[attrName] = attrValue
                            }
                            currentTag.tagAttributes = attributes
                        }
                    }
                    XmlPullParser.END_TAG -> {
                        currentTag = tagsStack.pop()
                        if (!tagsStack.isEmpty()) {
                            val parentTag = tagsStack.peek()
                            parentTag.addInnerXmlTag(currentTag)
                            currentTag = parentTag
                        } else
                            doneParsing = true
                    }
                    XmlPullParser.TEXT -> {
                        val innerText = xmlParser.text
                        if (currentTag != null)
                            currentTag.addInnerText(innerText)
                    }
                }
                eventType = xmlParser.next()
            }
            return rootTag
        }

        /**returns the root xml tag of the given xml resourceId , or null if not succeeded . */
        fun getXmlRootTagOfXmlFileResourceId(context: Context, xmlFileResourceId: Int): XmlTag? {
            val res = context.resources
            val xmlParser = res.getXml(xmlFileResourceId)
            return getXmlRootTagOfXmlPullParser(xmlParser)
        }
    }

    private fun addInnerXmlTag(tag: XmlTag) {
        if (innerTagsAndContent == null)
            innerTagsAndContent = ArrayList()
        innerTagsAndContent!!.add(tag)
    }

    private fun addInnerText(str: String) {
        if (innerTagsAndContent == null)
            innerTagsAndContent = ArrayList()
        innerTagsAndContent!!.add(str)
    }

    /**formats the xmlTag back to its string format,including its inner tags     */
    override fun toString(): String {
        val sb = StringBuilder()
        sb.append("<").append(tagName)
        val numberOfAttributes = if (tagAttributes != null) tagAttributes!!.size else 0
        if (numberOfAttributes != 0)
            for ((key, value) in tagAttributes!!)
                sb.append(" ").append(key).append("=\"").append(value).append("\"")
        val numberOfInnerContent = if (innerTagsAndContent != null) innerTagsAndContent!!.size else 0
        if (numberOfInnerContent == 0)
            sb.append("/>")
        else {
            sb.append(">")
            for (innerItem in innerTagsAndContent!!)
                sb.append(innerItem.toString())
            sb.append("</").append(tagName).append(">")
        }
        return sb.toString()
    }

}

示例用法:

build.grade

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

...
dependencies{
implementation 'com.1gravity:android-rteditor:1.6.7'
...
}
...

MainActivity.kt

class MainActivity : AppCompatActivity() {


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
//        val inputXmlString = "<zz>Zhshs<br/>ABC</zz>"
        val inputXmlString = "Aaa<br/><b>Bbb<br/></b>Ccc<br/><ul><li>Ddd</li><li>eee</li></ul>fff<br/><ol><li>ggg</li><li>hhh</li></ol>"

        // XML must have a root tag
        val xmlString = if (!inputXmlString.startsWith("<"))
            "<html>$inputXmlString</html>" else inputXmlString

        val rtApi = RTApi(this, RTProxyImpl(this), RTMediaFactoryImpl(this, true))
        val mRTManager = RTManager(rtApi, savedInstanceState)
        mRTManager.registerEditor(beforeTruncationTextView, true)
        mRTManager.registerEditor(afterTruncationTextView, true)
        beforeTruncationTextView.setRichTextEditing(true, inputXmlString)
        val xmlTag = XmlTag.getXmlFromString(xmlString)

        Log.d("AppLog", "xml parsed: " + xmlTag.toString())
        val maxTextCharacters = 10
        val maxLines = 20

        val output = XmlTagTruncationHelper.truncateXmlTag(xmlTag!!, XmlTagTruncationHelper.Restriction(maxTextCharacters, maxLines))
        afterTruncationTextView.setRichTextEditing(true, output)
        Log.d("AppLog", "xml with truncation : maxTextCharacters: $maxTextCharacters , maxLines: $maxLines output: " + output)
    }
}

activity_main.xml

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:gravity="center" android:orientation="vertical"
    tools:context=".MainActivity">

    <com.onegravity.rteditor.RTEditText
        android:id="@+id/beforeTruncationTextView" android:layout_width="match_parent"
        android:layout_height="wrap_content" android:background="#11ff0000" tools:text="beforeTruncationTextView"/>


    <com.onegravity.rteditor.RTEditText
        android:id="@+id/afterTruncationTextView" android:layout_width="match_parent"
        android:layout_height="wrap_content" android:background="#1100ff00" tools:text="afterTruncationTextView"/>
</LinearLayout>

结果: