I repeat here this answer on Stack Overflow.
I first posted an answer with not finalized code, as a simple description of the solution I could think, without any test. But later I remained interested, so I worked to make it (hopefully) perfectly functional.
To precisely define what it is meant to do, let me cite my previous answer:
This is a classic dilemma for any CMS or blog, where the teaser should present the begin of an article: often the solution is either stripping text from its tags and cut at a precise count OR keep tags but cut approximately because the tags are counted too...
So here the intent is to take an HTML element with any number of children, and any nesting level and return:
- the "same" element (i.e. keeping its tag and attributes)
- where resulting text content (i.e. visible as characters in the resulting page) is limited to a given count
- where resulting text is built from successive text nodes in their natural order
- where encountered tags are keeped intact and at their natural place
Here is my actual solution:
function cutKeepingTags(elem, reqCount) {
var grabText = '',
missCount = reqCount;
$(elem).contents().each(function() {
switch (this.nodeType) {
case Node.TEXT_NODE:
// Get node text, limited to missCount.
grabText += this.data.substr(0,missCount);
missCount -= Math.min(this.data.length, missCount);
break;
case Node.ELEMENT_NODE:
// Explore current child:
var childPart = cutKeepingTags(this, missCount);
grabText += childPart.text;
missCount -= childPart.count;
break;
}
if (missCount == 0) {
// We got text enough, stop looping.
return false;
}
});
return {
text:
// Wrap text using current elem tag.
elem.outerHTML.match(/^<[^>]+>/m)[0]
+ grabText
+ '</' + elem.localName + '>',
count: reqCount - missCount
};
}
And here is a working example. (I kept the HTML example posted by the previous question OP)
<hr>
or<img>
. Thanks for pointing this missed case. \$\endgroup\$