Handling multi-language paragraph font style in HTML - javascript

I have a webpage, which consists of mostly Persian content, and in some paragraphs there is a word or some words in English. The content is generated automatically and I can't change it from my HTML source.
I need to detect where these English words are, and give them font-size:xx; The reason of the former is that my English font - which I haven't chosen and changing it would be out of the question- looks bigger than my Persian font and it has to be some pixels less than the font-size I assigned to Persian font of every page.
here goes an example:
<span class="common">سلام دنیا (helo world)</span>
This whole span receives the following style:
.common{
font-size:26px;
font-family:'Arial';
}
and I can't assign a different font-size to the "hello world" part.
Since the page content is produces via a script code which gets data from DB, I can't manually give English words any embedded style, like surrounding them with <em lang="en"></em> tag.
Is there any way to automatically detect English words and give them style- by assigning a class maybe?-
Any attempt to help will be highly appreciated.

You can manipulate DOM by searching for English char sequence and wrap those sequence with your own span.
This is not the complete solution, but you can do something like this:
document.querySelectorAll("span").forEach(function(el){
el.innerHTML = el.innerText.replace(/[a-z]+/g, '<span class="uncommon">$&</span>');
});
.common{
font-size:26px;
font-family:'Arial';
}
.uncommon{
font-size:36px;
font-weight: 600;
}
<span class="common">سلام دنیا (helo world)</span>

The above answer works. Just for further usage, I provide an Angular version of this idea. Hope this would help someone in future.
.directive( 'showData', function ( $compile ) {
return {
scope: true,
link: function ( scope, element, attrs ) {
var el;
var farsi = 0;
attrs.$observe( 'template', function ( tpl )
{
//var tpl = attrs.template;
//if ( angular.isDefined( tpl ) )
{
// compile the provided template against the current scope
//now work on tpl:
function containASCII(str){
var flag_only_ascii = 1;
var flag_contain_ascii = 0;
for(var i=0;i<str.length;i++){
if(str.charCodeAt(i)<127){
flag_contain_ascii = 1;
}
else
{
flag_only_ascii = 0;
}
}
if(flag_only_ascii == 1 && flag_contain_ascii == 0)
return 1; //just ascii
if(flag_only_ascii == 0 && flag_contain_ascii ==1)
{
return 2;//combination
}
if(flag_only_ascii == 0 && flag_contain_ascii == 0){
return 0; //just english...
}
if(flag_only_ascii == 1 && flag_contain_ascii == 1){
return 3; //other
}
};
if(scope.TranslationValue == 1)
{
var split_span = tpl.split(" ");
for (i = 0 ; i < split_span.length ; i++)
{
var str_1 = split_span[i];
if(containASCII(str_1) == 3){
//if(str_1.search("<em2 ") == -1)
split_span[i] = "<em2 class='uncommon'>" + str_1 + "</em2>";
}
}
var final_str = split_span.join(" ");
}
else
{
final_str = tpl;
}
final_str = '<span rep-eng-text-font>' + final_str + '</span>';
element.html(final_str);
// add the template content
}
});
}
};
})

Related

Format color while typing in textarea or pre

I'm trying to create a comments section that lets users #someone. When the user types #random and then space, I want it to be highlighted. So I've created something that searches and replaces the string, but I then when the html is replaced, it places the cursor at the beginning. Any way to solve this? Any other way of doing something like this?
$('#textarea').keyup(function() {
txt = this.innerText.split(" ")
new_txt = this.innerText
for (var i = txt.length - 1; i >= 0; i--) {
if (txt[i].startsWith('#') == false) {
delete txt[i]
}
}
txt = txt.sort().join(" ").trim().split(" ")
console.log(txt)
if (txt.length > 0 && txt[0] != "") {
for (var i = 0; i < txt.length; i++) {
new_txt = new_txt.replace(txt[i], '<mark>' + txt[i] + '</mark>')
}
$('#my_console_log').text(new_txt)
this.innerHTML = new_txt
}
});
pre {
border: solid black 1px;
}
mark {
background: blue;
color: red;
}
<script src="https://code.jquery.com/jquery-1.10.2.js"></script>
<title>Test page</title>
<form>
<pre id='textarea' contentEditable='true'></pre>
<div id="my_console_log"></div>
</form>
Here is a simple plugin available which can be useful to you,
Download the plugin and edit the file jquery.hashtags.js and remove the condition for #. You can also change the style as per your requirement.
(function($) {
$.fn.hashtags = function() {
$(this).wrap('<div class="jqueryHashtags"><div class="highlighter"></div></div>').unwrap().before('<div class="highlighter"></div>').wrap('<div class="typehead"></div></div>');
$(this).addClass("theSelector");
autosize($(this));
$(this).on("keyup", function() {
var str = $(this).val();
$(this).parent().parent().find(".highlighter").css("width",$(this).css("width"));
str = str.replace(/\n/g, '<br>');
if(!str.match(/(http|ftp|https):\/\/[\w-]+(\.[\w-]+)+([\w.,#?^=%&:\/~+#-]*[\w#?^=%&\/~+#-])?#([a-zA-Z0-9]+)/g) && !str.match(/(http|ftp|https):\/\/[\w-]+(\.[\w-]+)+([\w.,#?^=%&:\/~+#-]*[\w#?^=%&\/~+#-])?#([a-zA-Z0-9]+)/g) && !str.match(/(http|ftp|https):\/\/[\w-]+(\.[\w-]+)+([\w.,#?^=%&:\/~+#-]*[\w#?^=%&\/~+#-])?#([\u0600-\u06FF]+)/g) && !str.match(/(http|ftp|https):\/\/[\w-]+(\.[\w-]+)+([\w.,#?^=%&:\/~+#-]*[\w#?^=%&\/~+#-])?#([\u0600-\u06FF]+)/g)) {
// Remove below condition for hashtag.
if(!str.match(/#(([_a-zA-Z0-9]+)|([\u0600-\u06FF]+)|([ㄱ-ㅎㅏ-ㅣ가-힣]+)|([ぁ-んァ-ン]+)|([一-龯]+))#/g)) { //arabic support, CJK support
str = str.replace(/#(([_a-zA-Z0-9]+)|([\u0600-\u06FF]+)|([ㄱ-ㅎㅏ-ㅣ가-힣]+)|([ぁ-んァ-ン]+)|([一-龯]+))/g,'<span class="hashtag">#$1</span>');
}else{
str = str.replace(/#(([_a-zA-Z0-9]+)|([\u0600-\u06FF]+)|([ㄱ-ㅎㅏ-ㅣ가-힣]+)|([ぁ-んァ-ン]+)|([一-龯]+))#(([_a-zA-Z0-9]+)|([\u0600-\u06FF]+)|([ㄱ-ㅎㅏ-ㅣ가-힣]+)|([ぁ-んァ-ン]+)|([一-龯]+))/g,'<span class="hashtag">#$1</span>');
}
// Keep this condition.
if(!str.match(/#(([a-zA-Z0-9]+)|([\u0600-\u06FF]+)|([ㄱ-ㅎㅏ-ㅣ가-힣]+)|([ぁ-んァ-ン]+)|([一-龯]+))#/g)) {
str = str.replace(/#(([a-zA-Z0-9]+)|([\u0600-\u06FF]+)|([ㄱ-ㅎㅏ-ㅣ가-힣]+)|([ぁ-んァ-ン]+)|([一-龯]+))/g,'<span class="hashtag">#$1</span>');
}else{
str = str.replace(/#(([a-zA-Z0-9]+)|([\u0600-\u06FF]+)|([ㄱ-ㅎㅏ-ㅣ가-힣]+)|([ぁ-んァ-ン]+)|([一-龯]+))#(([a-zA-Z0-9]+)|([\u0600-\u06FF]+)|([ㄱ-ㅎㅏ-ㅣ가-힣]+)|([ぁ-んァ-ン]+)|([一-龯]+))/g,'<span class="hashtag">#$1</span>');
}
}
$(this).parent().parent().find(".highlighter").html(str);
});
$(this).parent().prev().on('click', function() {
$(this).parent().find(".theSelector").focus();
});
};
})(jQuery);
Instead of replacing the html just append a class with the color that you want

Angularjs ng-bind-html with custom Filter

I am currently working with ng-bind-html. Basically, what I am trying to do is, when I post a blog, the blog contains links and other styling. So when I am trying to show the list of blogs, I am using ng-bing-html like this:
<p ng-bind-html="blog.blogContent"></p>
which works fine.
But in addition, I try to truncate the blog and show only few paragraphs with view more option by passing a custom filter. But when I pass the filter I get the following:
<p ng-bind-html="blog.blogContent | Truncate"></p>
Error: [$sanitize:badparse] The sanitizer was unable to parse the
following block of html: <a href="https:.......
My Filter looks like this:
return function (text, length, end) {
if (text !== undefined) {
if (isNaN(length)) {
length = 450;
}
if (end === undefined) {
end = ".......";
}
if (text.length <= length || text.length - end.length <= length) {
return text;
} else {
return String(text).substring(0, length - end.length) + end;
}
}
You can solve this using custom directives and filters. try this one: https://stackoverflow.com/a/45076560/6816707
I used the solution posted by Minouris in this post (Javascript truncate HTML text) and adapted it into an AngularJS filter. It seems to work pretty well. The filter is
angular.module('plunker').filter('Truncate', function() {
return function(text, length, end) {
if (text !== undefined) {
if (isNaN(length)) {
length = 20;
}
if (end === undefined) {
end = ".......";
}
if (text.length <= length || text.length - end.length <= length) {
return text;
}
var truncated = text.substring(0, length);
// Remove line breaks and surrounding whitespace
truncated = truncated.replace(/(\r\n|\n|\r)/gm,"").trim();
// If the text ends with an incomplete start tag, trim it off
truncated = truncated.replace(/<(\w*)(?:(?:\s\w+(?:={0,1}(["']{0,1})\w*\2{0,1})))*$/g, '');
// If the text ends with a truncated end tag, fix it.
var truncatedEndTagExpr = /<\/((?:\w*))$/g;
var truncatedEndTagMatch = truncatedEndTagExpr.exec(truncated);
if (truncatedEndTagMatch != null) {
var truncatedEndTag = truncatedEndTagMatch[1];
// Check to see if there's an identifiable tag in the end tag
if (truncatedEndTag.length > 0) {
// If so, find the start tag, and close it
var startTagExpr = new RegExp(
"<(" + truncatedEndTag + "\\w?)(?:(?:\\s\\w+(?:=([\"\'])\\w*\\2)))*>");
var testString = truncated;
var startTagMatch = startTagExpr.exec(testString);
var startTag = null;
while (startTagMatch != null) {
startTag = startTagMatch[1];
testString = testString.replace(startTagExpr, '');
startTagMatch = startTagExpr.exec(testString);
}
if (startTag != null) {
truncated = truncated.replace(truncatedEndTagExpr, '</' + startTag + '>');
}
} else {
// Otherwise, cull off the broken end tag
truncated = truncated.replace(truncatedEndTagExpr, '');
}
}
// Now the tricky part. Reverse the text, and look for opening tags. For each opening tag,
// check to see that he closing tag before it is for that tag. If not, append a closing tag.
var testString = reverseHtml(truncated);
var reverseTagOpenExpr = /<(?:(["'])\w*\1=\w+ )*(\w*)>/;
var tagMatch = reverseTagOpenExpr.exec(testString);
while (tagMatch != null) {
var tag = tagMatch[0];
var tagName = tagMatch[2];
var startPos = tagMatch.index;
var endPos = startPos + tag.length;
var fragment = testString.substring(0, endPos);
// Test to see if an end tag is found in the fragment. If not, append one to the end
// of the truncated HTML, thus closing the last unclosed tag
if (!new RegExp("<" + tagName + "\/>").test(fragment)) {
truncated += '</' + reverseHtml(tagName) + '>';
}
// Get rid of the already tested fragment
testString = testString.replace(fragment, '');
// Get another tag to test
tagMatch = reverseTagOpenExpr.exec(testString);
}
return truncated;
}
}
function reverseHtml(str) {
var ph = String.fromCharCode(206);
var result = str.split('').reverse().join('');
while (result.indexOf('<') > -1) {
result = result.replace('<',ph);
}
while (result.indexOf('>') > -1) {
result = result.replace('>', '<');
}
while (result.indexOf(ph) > -1) {
result = result.replace(ph, '>');
}
return result;
}
});
Working plunkr:
http://plnkr.co/edit/oCwmGyBXB26omocT2q9m?p=preview
I havent tested the above solution and you may run into issues with more complicated HTML strings. May I suggest using a Jquery library like https://github.com/pathable/truncate to be safe?

Checking a div for duplicates before appending to the list using jQuery

This should be trivial but I'm having issues...
Basically what I am trying to do is append a new "div" to "selected-courses" when a user clicks on a "course". This should happen if and only if the current course is not already in the "selected-courses" box.
The problem I'm running into is that nothing is appended to the "selected-courses" section when this is executed. I have used alert statements to make sure the code is in fact being run. Is there something wrong with my understanding of the way .on and .each work ? can I use them this way.
Here is a fiddle http://jsfiddle.net/jq9dth4j/
$(document).on("click", "div.course", function() {
var title = $( this ).find("span").text();
var match_found = 0;
//if length 0 nothing in list, no need to check for a match
if ($(".selected-course").length > 0) {
match_found = match(title);
}
if (matched == 0) {
var out = '<div class="selected-course">' + '' + title + ''+'</div>';
$("#selected-box").append(out);
}
});
//checks to see if clicked course is already in list before adding.
function match(str) {
$(".selected-course").each(function() {
var retval = 0;
if(str == this.text()) {
//course already in selected-course section
retval = 1;
return false;
}
});
return retval;
}
There was a couple of little issues in your fiddle.
See fixed fiddle: http://jsfiddle.net/jq9dth4j/1/
function match(str) {
var retval = 0;
$(".selected-course").each(function() {
if(str == $(this).text()) {
retval = 1;
return false;
}
});
return retval;
}
You hadn't wrapped your this in a jquery object. So it threw an exception saying this had no method text().
Second your retval was declared inside the each so it wasn't available to return outside the each, wrong scope.
Lastly the if in the block:
if (matched== 0) {
var out = '';
out += '<div class="selected-course">' + '' + title + ''+'</div>';
$("#selected-box").append(out);
}
was looking at the wrong variable it was looking at matched which didn't exist causing an exception.
Relying on checking what text elements contain is not the best approach to solve this kind of question. It is prone to errors (as you have found out), it can be slow, it gives you long code and it is sensitive to small changes in the HTML. I would recommend using custom data-* attributes instead.
So you would get HTML like this:
<div class="course" data-course="Kite Flying 101">
<a href="#">
<span>Kite Flying 101</span>
</a>
</div>
Then the JS would be simple like this:
$(document).on('click', 'div.course', function() {
// Get the name of the course that was clicked from the attribute.
var title = $(this).attr('data-course');
// Create a selector that selects everything with class selected-course and the right data-course attribute.
var selector = '.selected-course[data-course="' + title + '"]';
if($(selector).length == 0) {
// If the selector didn't return anything, append the div.
// Do note that we need to add the data-course attribute here.
var out = '<div class="selected-course" data-course="' + title + '">' + title + '</div>';
$('#selected-box').append(out);
}
});
Beware of case sensitivity in course names, though!
Here is a working fiddle.
Try this code, read comment for where the changes are :
$(document).on("click", "div.course", function () {
var title = $(this).find("span").text().trim(); // use trim to remove first and end whitespace
var match_found = 0;
if ($(".selected-course").length > 0) {
match_found = match(title);
}
if (match_found == 0) { // should change into match_found
var out = '';
out += '<div class="selected-course">' + '' + title + '' + '</div>';
$("#selected-box").append(out);
}
});
function match(str) {
var retval = 0; // this variable should place in here
$(".selected-course").each(function () {
if (str == $(this).find('a').text().trim()) { // find a tag to catch values, and use $(this) instead of this
retval = 1;
return false;
}
});
return retval; // now can return variable, before will return undefined
}
Updated DEMO
Your Issues are :
1.this.text() is not valid. you have to use $(this).text().
2.you defined var retval = 0; inside each statement and trying to return it outside each statement. so move this line out of the each statement.
3.matched is not defined . it should be match_found in line if (matched == 0) {.
4. use trim() to get and set text, because text may contain leading and trailing spaces.
Your updated JS is
$(document).on("click", "div.course", function () {
var title = $(this).find("span").text();
var match_found = 0;
if ($(".selected-course").length > 0) {
match_found = match(title);
}
if (match_found == 0) {
var out = '<div class="selected-course">' + '' + title + '' + '</div>';
$("#selected-box").append(out);
}
});
function match(str) {
var retval = 0;
$(".selected-course").each(function () {
if (str.trim() == $(this).text().trim()) {
retval = 1;
return false;
}
});
return retval;
}
Updated you Fiddle

Wrapping Sentences within <p> Tags with <span>'s, But Keep Other Tags

To give you an idea of what I need, I have been using the below code to parse content within tags and wrap each sentence within tags so I can then interact with sentences on a page.
$('p').each(function() {
var sentences = $(this)
.text()
.replace(/(((?![.!?]['"]?\s).)*[.!?]['"]?)(\s|$)/g,
'<span class="sentence">$1</span>$3');
$(this).html(sentences);
});
However, the following line demonstrates my problem:
<p>This is a link and it is removed with the above code! Here is another sentence.</p>
Nested tags such as <a>, <img> etc...within <p> tags that I'm searching through are removed with the code that I'm using. I need to keep these tags intact, so the content stays the same within the <p> tags.
I need:
<p><span class="sentence">This is a link and it is removed with the above code!</sentence><sentence>Here is another sentence.</sentence></p>
After reading this barn-burner about parsing HTML with regex, I've concluded that I need to use a combo of an HTML parser of some sort to traverse through sub-tags within a <p> tag, and then use a regex to find the sentences. I think the regex I have listed above should work for most of my uses, if that helps.
So: how should I do it?
It is really difficult to tokenise language, reliably, into sentences and that is without the added complexity of throwing html into the equation. There are some applications etc out there that attempt to deal with Natural Language Processing, an example would be the Stanford Tokenizer with runs on Java (not Javascript)
And as people keep mentioning, a regex is not the solution to this problem, language is not regular so don't expect a Regular Expression only solution.
There is a question here on SO, Basic NLP in CoffeeScript or JavaScript — Punkt tokenizaton, simple trained Bayes models — where to start? Which I think summarises things fairly simply for Javascript.
Anyway, to at least give you a little something that you could play with, I knocked up a little code for you. This works reasonable well until the markup/language begins to resemble anything slightly complex or different, but ultimately fails the mark by a long way. But, it may be enough for what you need, I don't know.
CSS
.emphasis {
font-style: italic;
}
.bold {
font-weight: bold;
}
.emphasis.bold {
font-style: italic;
font-weight: bold;
}
.unidentified {
background-color: pink;
}
.sentence0 {
background-color: yellow;
}
.sentence1 {
background-color: green;
}
.sentence2 {
background-color: red;
}
.whitespace {
white-space: pre;
background-color: blue;
}
Javascript
/*jslint maxerr: 50, indent: 4, browser: true */
(function () {
"use strict";
var rxOpen = new RegExp("<[^\\/].+?>"),
rxClose = new RegExp("<\\/.+?>"),
rxWhitespace = new RegExp("^\\s+?"),
rxSupStart = new RegExp("^<sup\\b[^>]*>"),
rxSupEnd = new RegExp("<\/sup>"),
sentenceEnd = [],
color = 0,
rxIndex;
sentenceEnd.push(new RegExp("[^\\d][\\.!\\?]+"));
sentenceEnd.push(new RegExp("(?=([^\\\"]*\\\"[^\\\"]*\\\")*[^\\\"]*?$)"));
sentenceEnd.push(new RegExp("(?![^\\(]*?\\))"));
sentenceEnd.push(new RegExp("(?![^\\[]*?\\])"));
sentenceEnd.push(new RegExp("(?![^\\{]*?\\})"));
sentenceEnd.push(new RegExp("(?![^\\|]*?\\|)"));
//sentenceEnd.push(new RegExp("(?![^\\\\]*?\\\\)"));
//sentenceEnd.push(new RegExp("(?![^\\/.]*\\/)")); // all could be a problem, but this one is problematic
rxIndex = new RegExp(sentenceEnd.reduce(function (previousValue, currentValue) {
return previousValue + currentValue.source;
}, ""));
function indexSentenceEnd(html) {
var index = html.search(rxIndex);
if (index !== -1) {
index += html.match(rxIndex)[0].length - 1;
}
return index;
}
function pushSpan(array, className, string, classNameOpt) {
if (className === "sentence") {
className += color % 2;
if (classNameOpt) {
className += " " + classNameOpt;
}
color += 1;
}
array.push('<span class="' + className + '">' + string + '</span>');
}
function addSupToPrevious(html, array) {
var sup = html.search(rxSupStart),
end = 0,
last;
if (sup !== -1) {
end = html.search(rxSupEnd);
if (end !== -1) {
last = array.pop();
end = end + 6;
array.push(last.slice(0, -7) + html.slice(0, end) + last.slice(-7));
}
}
return html.slice(end);
}
function leadingWhitespaces(html, array) {
var whitespace = html.search(rxWhitespace),
count = 0;
if (whitespace !== -1) {
count = html.match(rxWhitespace)[0].length;
pushSpan(array, "whitespace", html.slice(0, count));
}
return html.slice(count);
}
function paragraphIsSentence(html, array) {
var index = indexSentenceEnd(html);
if (index === -1 || index === html.length) {
pushSpan(array, "sentence", html, "paragraphIsSentence");
html = "";
}
return html;
}
function paragraphNoMarkup(html, array) {
var open = html.search(rxOpen),
index = 0;
if (open === -1) {
index = indexSentenceEnd(html);
if (index === -1) {
index = html.length;
}
pushSpan(array, "sentence", html.slice(0, index += 1), "paragraphNoMarkup");
}
return html.slice(index);
}
function sentenceUncontained(html, array) {
var open = html.search(rxOpen),
index = 0,
close;
if (open !== -1) {
index = indexSentenceEnd(html);
if (index === -1) {
index = html.length;
}
close = html.search(rxClose);
if (index < open || index > close) {
pushSpan(array, "sentence", html.slice(0, index += 1), "sentenceUncontained");
} else {
index = 0;
}
}
return html.slice(index);
}
function sentenceContained(html, array) {
var open = html.search(rxOpen),
index = 0,
close,
count;
if (open !== -1) {
index = indexSentenceEnd(html);
if (index === -1) {
index = html.length;
}
close = html.search(rxClose);
if (index > open && index < close) {
count = html.match(rxClose)[0].length;
pushSpan(array, "sentence", html.slice(0, close + count), "sentenceContained");
index = close + count;
} else {
index = 0;
}
}
return html.slice(index);
}
function anythingElse(html, array) {
pushSpan(array, "sentence2", html, "anythingElse");
return "";
}
function guessSenetences() {
var paragraphs = document.getElementsByTagName("p");
Array.prototype.forEach.call(paragraphs, function (paragraph) {
var html = paragraph.innerHTML,
length = html.length,
array = [],
safety = 100;
while (length && safety) {
html = addSupToPrevious(html, array);
if (html.length === length) {
html = leadingWhitespaces(html, array);
if (html.length === length) {
html = paragraphIsSentence(html, array);
if (html.length === length) {
html = paragraphNoMarkup(html, array);
if (html.length === length) {
html = sentenceUncontained(html, array);
if (html.length === length) {
html = sentenceContained(html, array);
if (html.length === length) {
html = anythingElse(html, array);
}
}
}
}
}
}
length = html.length;
safety -= 1;
}
paragraph.innerHTML = array.join("");
});
}
guessSenetences();
}());
On jsfiddle
you need to use .html() instead of .text() if you want to keep tags intact.
Check below code and let me know if it doesn't work out.
DEMO
$('p').each(function() {
var sentences = $(this)
.html()
.replace(/(((?![.!?]['"]?\s).)*[.!?]['"]?)(\s|$)/g,
'<span class="sentence">$1</span>$3');
$(this).html(sentences);
});

Make lines of text have equal length

In centered h1 elements, if the text falls on multiple lines, line breaks make the text look like this:
This is a header that takes up two
lines
This is a header that takes up three
lines because it is really, really
long
Is there a way to manipulate these elements so that the length of the lines of text is roughly equal? Like this:
This is a header that
takes up two lines
This is a header that takes
up three lines because it
is really, really long
The jQuery plugin Widow Fix prevents single-word lines, but I'm looking for something that evens out all the lines in a multi-line element. Are there any jQuery plugins for this, or can you recommend a strategy?
I would solve it using only strict JavaScript, going this way:
1. put a class named 'truncate' to h1 tags you want to break
2. configure the javascript code on your needs knowing that
MAXCOUNT: (integer) max chars counted per line
COUNT_SPACES: (boolean) white spaces must be counted?
COUNT_PUNCTUATION: (boolean) punctuation must be counted?
EXACT: (boolean) the last word must be cut?
BLOCKS_CLASS: (string) the className of the h1 to consider
I wrote the code very quickly so it must be better tested for bugs,
but it can be a starting point I think.
I'm not using jQuery in this code to keep the code light and to avoid dependencies.
I think I'm using all cross-browser commands (cannot test it I've got only linux now). However any correction for cross-browser compatibility task (included the use of jQUery if requested) might be easy.
Here is the code:
<html>
<head>
<style>
h1 {background-color: yellow;}
#hiddenDiv {background-color: yellow; display: table-cell; visibility:hidden;}
</style>
<script>
var MAXCOUNT = 20;
var COUNT_SPACES = false;
var EXACT = false;
var COUNT_PUNCTUATION = true;
var BLOCKS_CLASS = 'truncate';
window.onload = function ()
{
var hidden = document.getElementById('hiddenDiv');
if (hidden == null)
{
hidden = document.createElement('div');
hidden.id = 'hiddenDiv';
document.body.appendChild(hidden);
}
var blocks = document.getElementsByClassName(BLOCKS_CLASS);
for (var i=0; i<blocks.length; i++)
{
var block = blocks[i];
var text = block.innerHTML;
var truncate = '';
var html_tag = false;
var special_char = false;
maxcount = MAXCOUNT;
for (var x=0; x<maxcount; x++)
{
var previous_char = (x>0) ? text.charAt(x-1) : '';
var current_char = text.charAt(x);
// Closing HTML tags
if (current_char == '>' && html_tag)
{
html_tag = false;
maxcount++;
continue;
}
// Closing special chars
if (current_char == ';' && special_char)
{
special_char = false;
maxcount++;
continue;
}
// Jumping HTML tags
if (html_tag)
{
maxcount++;
continue;
}
// Jumping special chars
if (special_char)
{
maxcount++;
continue;
}
// Checking for HTML tags
if (current_char == '<')
{
var next = text.substring(x,text.indexOf('>')+1);
var regex = /(^<\w+[^>]*>$)/gi;
var matches = regex.exec(next);
if (matches[0])
{
html_tag = true;
maxcount++;
continue;
}
}
// Checking for special chars
if (current_char == '&')
{
var next = text.substring(x,text.indexOf(';')+1);
var regex = /(^&#{0,1}[0-9a-z]+;$)/gi;
var matches = regex.exec(next);
if (matches[0])
{
special_char = true;
maxcount++;
continue;
}
}
// Shrink multiple white spaces into a single white space
if (current_char == ' ' && previous_char == ' ')
{
maxcount++;
continue;
}
// Jump new lines
if (current_char.match(/\n/))
{
maxcount++;
continue;
}
if (current_char == ' ')
{
// End of the last word
if (x == maxcount-1 && !EXACT) { break; }
// Must I count white spaces?
if ( !COUNT_SPACES ) { maxcount++; }
}
// Must I count punctuation?
if (current_char.match(/\W/) && current_char != ' ' && !COUNT_PUNCTUATION)
{
maxcount++;
}
// Adding this char
truncate += current_char;
// Must I cut exactly?
if (!EXACT && x == maxcount-1) { maxcount++; }
}
hidden.innerHTML = '<h1><nobr>'+truncate+'</nobr></h1>';
block.style.width = hidden.offsetWidth+"px";
}
}
</script>
</head>
<body>
<center>
<h1 class="truncate">
This is a header that
takes up two lines
</h1>
<br>
<h1 class="truncate">
This is a header that takes
up three lines because it
is really, really long
</h1>
<br>
<h1>
This is a header pretty short
or pretty long ... still undecided
which in any case is not truncated!
</h1>
</center>
</body>
</html>
And here is a demo of that: http://jsfiddle.net/6rtdF/
Late to this party, but here's my approach. I get the initial element height (any elements with the class balance_lines, in the code below), then incrementally shrink the width of the element. Once the height of the element changes, I've gone too far. The step before that should have lovely roughly-equal line lengths.
$('.balance_lines').each(function(){
var currentHeight = $(this).height();
var thisHeight = currentHeight;
var currentWidth = $(this).width();
var newWidth = currentWidth;
// Try shrinking width until height changes
while (thisHeight == currentHeight) {
var testWidth = newWidth - 10;
$(this).width(testWidth);
thisHeight = $(this).height();
if (thisHeight == currentHeight) {
newWidth = testWidth;
} else {
break;
}
}
$(this).width(newWidth);
});
You can see this code in action on the homepage at apollopad.com.
The CSS Text 4 draft proposes text-wrap: balance, but I don't think any browser implements it yet.
In the meantime, you can use Adobe's jQuery plugin (demo): https://github.com/adobe-webplatform/balance-text

Categories