如何截断 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 种可能的解决方案:
转换为纯文本(同时处理了一些标签),然后截断:解析HTML,并用 Unicode 替换一些标签替代方案,同时删除其他方案。例如,代替项目符号列表,放置项目符号字符(可能 this),对于编号列表也是如此(代替数字)。所有其他标签都将被删除。转到下一行的标签(“
”)也是如此,应该用“\n”替换。在那之后,我可以安全地截断正常文本,因为没有更多的标签可以被破坏。
在 HTML 中很好地截断:解析 HTML,同时识别其中的文本,并在那里截断它到达截断位置时关闭所有标签。这可能更难。
我不确定哪个更容易,但我能想到每个可能的缺点。不过这只是一个预览,所以我认为这并不重要。
我在网上搜索了这样的解决方案,看看其他人是否已经做到了。
我发现了一些讨论 "cleaning" 或 "optimizing" HTML 的链接,但我看不到它们可以处理替换它们或截断它们。不仅如此,由于是HTML,大部分与Android无关,使用PHP、C#、Angular等语言
以下是我找到的一些链接:
Java Library to truncate html strings?
how to truncate HTML string without leaving it malformated?
问题
我写的那些解决方案可行吗?如果是这样,是否有一种已知的方法来实现它们?甚至 Java/Kotlin/Android 图书馆?做出这样的解决方案有多难?
也许我没有想到其他解决方案?
编辑:
我也尝试过使用我过去编写的旧代码 (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>
结果:
背景
我们允许用户使用富文本编辑器库(称为 Android-RTEditor)创建一些将转换为 HTML 的文本。
输出 HTML 文本按原样保存在服务器和设备上。
因为在某些最终情况下,需要显示很多此内容(多个实例),我们希望还保存此内容的 "preview" 版本,这意味着它会更短长度(比如 120 个普通字符,不包括 HTML 标签的额外字符,这些字符不计算在内)。
我们想要的是HTML的最小化版本。可以选择删除一些标签,但无论我们选择做什么,我们仍然希望看到列表 (numbered/bullets),因为列表确实向用户显示文本(项目符号是一个字符,带点的数字)。
转到下一行的标记也应该处理,因为转到下一行很重要。
问题
与普通字符串相反,我可以在其中调用 substring
所需的字符数,在 HTML 上它可能会破坏标签。
我试过的
我想到了 2 种可能的解决方案:
转换为纯文本(同时处理了一些标签),然后截断:解析HTML,并用 Unicode 替换一些标签替代方案,同时删除其他方案。例如,代替项目符号列表,放置项目符号字符(可能 this),对于编号列表也是如此(代替数字)。所有其他标签都将被删除。转到下一行的标签(“
”)也是如此,应该用“\n”替换。在那之后,我可以安全地截断正常文本,因为没有更多的标签可以被破坏。在 HTML 中很好地截断:解析 HTML,同时识别其中的文本,并在那里截断它到达截断位置时关闭所有标签。这可能更难。
我不确定哪个更容易,但我能想到每个可能的缺点。不过这只是一个预览,所以我认为这并不重要。
我在网上搜索了这样的解决方案,看看其他人是否已经做到了。 我发现了一些讨论 "cleaning" 或 "optimizing" HTML 的链接,但我看不到它们可以处理替换它们或截断它们。不仅如此,由于是HTML,大部分与Android无关,使用PHP、C#、Angular等语言
以下是我找到的一些链接:
Java Library to truncate html strings?
how to truncate HTML string without leaving it malformated?
问题
我写的那些解决方案可行吗?如果是这样,是否有一种已知的方法来实现它们?甚至 Java/Kotlin/Android 图书馆?做出这样的解决方案有多难?
也许我没有想到其他解决方案?
编辑: 我也尝试过使用我过去编写的旧代码 (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>
结果: