Limiting the length of an inline element
How would you limit the length of a variable length text element which can contain text attributes (<b>, <i>, <sup>, ...)
and links. The tags would need to be preserved both opening and closing, though an entire tag (both opening 开发者_StackOverflow社区and close) could be removed if at appropriate position (cannot remove all tags to simplify the problem). I have c#, xslt, and css available to me. I would prefer not to do this with javascript.
For example:
On the <b>approximate realization</b> of continuous mappings by <i>neural networks</i> <a href='http://...very long link'>some text</a>...
Keep in mind that the tags themselves (and their attributes) should not count against the length.
Also, the text should wrap, so using width and overflow is out of the question.
Both Mrchief and Dimitre Novatchev have great solutions. I'm more fond of putting this logic in my xslt so I choose Dimitre Novatchev's as the answer, though both should be.
This XSLT 1.0 transformation:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:key name="kTextById" match="text()" use="generate-id()"/>
<xsl:param name="pMaxLength" select="60"/>
<xsl:variable name="vTextToSplit">
<xsl:apply-templates select="(//text())[1]" mode="calc"/>
</xsl:variable>
<xsl:variable name="vsplitNode" select=
"key('kTextById', substring-before(substring-after($vTextToSplit,'|'), '|'))"/>
<xsl:variable name="vsplitLength" select=
"substring-before($vTextToSplit,'|')"/>
<xsl:variable name="vsplitPos" select=
"substring-after(substring-after($vTextToSplit,'|'),'|')"/>
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="/">
<xsl:choose>
<xsl:when test="not($vTextToSplit)">
<xsl:copy-of select="."/>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="/node()"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="text()" mode="calc">
<xsl:param name="paccumLength" select="0"/>
<xsl:variable name="vPos" select="count(preceding::text())+1"/>
<xsl:variable name="vnewAccumLength" select=
"$paccumLength+string-length()"/>
<xsl:choose>
<xsl:when test="$vnewAccumLength >= $pMaxLength">
<xsl:value-of select=
"concat(string-length() - ($vnewAccumLength -$pMaxLength),
'|', generate-id(),
'|', $vPos
)"/>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates mode="calc"
select="(//text())[position() = $vPos+1]">
<xsl:with-param name="paccumLength" select="$vnewAccumLength"/>
</xsl:apply-templates>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="text()">
<xsl:variable name="vPos" select="count(preceding::text())+1"/>
<xsl:choose>
<xsl:when test="$vPos > $vsplitPos"/>
<xsl:when test="$vPos = $vsplitPos">
<xsl:value-of select="substring(.,1,$vsplitLength)"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="."/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
when applied on the provided input (wrapped into a single top element to make it a well-formed XML document):
<t>On the <b>approximate realization</b> of continuous mappings by <i>neural networks</i> <a href='http://...very long link'>some text</a>...</t>
produces the wanted, correct result -- a well-formed XML document that contains the elements of the source XML document and the total length of whose text nodes is equal exactly to the specified length (60) in the global parameter $pMaxLength
:
<t>On the <b>approximate realization</b> of continuous mappings by <i>neu</i>
<a href="http://...very long link"></a>
</t>
Explanation:
The global variable
$vTextToSplit
is calculated. It is a string, containing three values that are pipe-separated: the length in the "split node" that must be dropped off, thegenerate-id()
od the "split node" and the ordinal position of the "split node" among all text nodes, in document order. The "split node" is this text node that contains the last character of the total string of text nodes to be generated.The "split node, its "
generate-id()
and its length-to-be-trimmed are extracted from `$vTextToSplit" into three coresponding global variables.The template matching the root (
/
) of the document checks for the edge case when the total length of the text nodes is less than the specified wanted length. If so, the complete XML document is copied to the output. If not so, the processing continues by applying templates to its child nodes.The identity rule copies all nodes "as-is".
The template matching any text node overrides the identity template. It processes the matched text node in one of three ways: if this text node has smaller position than the "split node" it is copied entirely. If the matched node has position greater that the "split node" then its string value isn't copied. Finally, if this is the split node itself, all characters of its string value with the exception of the trailing
$vsplitLength
characters are copied.
II. XSLT 2.0 solution:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes"/>
<xsl:param name="pMaxLength" select="60"/>
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
<xsl:template match=
"text()[not(sum((.|preceding::text())/string-length(.))
gt
$pMaxLength)
]">
<xsl:copy-of select="."/>
</xsl:template>
<xsl:template match=
"text()[sum(preceding::text()/string-length(.))
gt
$pMaxLength
]"/>
<xsl:template match=
"text()[sum((.|preceding::text())/string-length(.))
ge
$pMaxLength
and
not(sum(preceding::text()/string-length(.))
gt
$pMaxLength)
]">
<xsl:variable name="vprevLength" select=
"sum(preceding::text()/string-length(.))"/>
<xsl:variable name="vremainingLength" select=
"$pMaxLength - $vprevLength"/>
<xsl:copy-of select="substring(.,1,$vremainingLength)"/>
</xsl:template>
</xsl:stylesheet>
when applied to the same source XML document (given above), the same correct result is produced:
<t>On the <b>approximate realization</b> of continuous mappings by <i>neu</i><a href="http://...very long link"/></t>
Note on performance: Both solutions presented will be slow for big XML documents. One way to avoid this is to use the scanl()
function/template of FXSL. I will provide this third solution later, when I have more free time.
Here's an attempted solution:
public static string LimitText(string input, int width)
{
const string pattern = @"(</?[a-zA-Z0-9 '=://.]+>)";
var rgx = new Regex(pattern, RegexOptions.Compiled);
// remove tags and chop text to set width
var result = rgx.Replace(input, string.Empty).Substring(0, width);
// split till word boundary (so that "shittake" doesn't end up as "shit")
result = result.Substring(0, result.LastIndexOf(' '));
var matches = rgx.Matches(input);
// non LINQ version to keep things simple
foreach (Match match in matches)
{
var groups = match.Groups;
if (groups[0].Index > result.Length) break;
result = result.Insert(groups[0].Index, groups[0].Value);
}
// check for unbalanced tags
matches = rgx.Matches(result);
if (matches.Count % 2 != 0)
{
// chop off unbalanced tag
return result.Substring(0, matches[matches.Count-1].Groups[0].Index);
}
return result;
}
Caveats:
- The regex matches all tags specifed in your post. You can expand upon it to include more characters based on your real scenario. However, parsing HTML with Regex is always going to be tricky.
- If your input string doesn't contain balanced tags (i.e. for each opening tag, there is a closing tag), this may not work as expected.
- If you expect self closing tags (like
<br />
) or openinput
tags in your input string, then a pre-flight sanitization is needed. The idea is the same, get a group of matches, run throughLimitText
and re-insert these tags on the result string. - The final rendered text on browser may still not be satisfactory as font size or screen resolution may produce incorrect results. For this, you need to resort to JS based solution.
This should get you started and then you can expand upon it for any corner cases.
a sites design should never limit what information you can place within a container. you could set a max-height of the element and using css allow full height on hover - you'd have to use some positioning and possibly some negative margin to prevent other elements jumping about.
Alternatively you could make use of the text-overflow css property but this is not fully implemented yet (as far as I know)- strangely its supposedly supported in ie6> !
the regex solution is difficult - you need find the position of the end of text with tags stripped out, cut the remainder of the string with tags in and append any unclosed tags - a tricky one!
精彩评论