Skip to content

Commit

Permalink
Implement forceContext option to force certain elements to have
Browse files Browse the repository at this point in the history
their own matching contexts.

E.g. useful with block-level elements like P and DIV.
CC #29
  • Loading branch information
padolsey committed Apr 22, 2015
1 parent 99f2038 commit d41ae22
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 21 deletions.
3 changes: 2 additions & 1 deletion demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ <h3>This text is all randomly sized so you can have a go at matching "across ele
el.style.backgroundColor = colors[match.index % colors.length];
el.innerHTML = portion.text;
return el;
}
},
forceContext: findAndReplaceDOMText.BLOCK_LEVEL_MATCH
});
} catch(e) {
warning.innerHTML = 'Error: ' + e;
Expand Down
91 changes: 74 additions & 17 deletions src/findAndReplaceDOMText.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ window.findAndReplaceDOMText = (function() {
return new Finder(node, options);
}

exposed.BLOCK_LEVEL_ELEMENTS = {
address:1, article:1, aside:1, audio:1, blockquote:1, canvas:1, dd:1, div:1,
dl:1, fieldset:1, figcaption:1, figure:1, footer:1, form:1, h1:1, h2:1, h3:1,
h4:1, h5:1, h6:1, header:1, hgroup:1, hr:1, main:1, nav:1, noscript:1, ol:1,
output:1, p:1, pre:1, section:1, table:1, tfoot:1, ul:1, video:1
};
exposed.BLOCK_LEVEL_MATCH = function(el) {
return !!exposed.BLOCK_LEVEL_ELEMENTS[ el.nodeName.toLowerCase() ];
};

exposed.Finder = Finder;

/**
Expand Down Expand Up @@ -129,19 +139,38 @@ window.findAndReplaceDOMText = (function() {

var match;
var matchIndex = 0;
var offset = 0;
var regex = this.options.find;
var text = this.getAggregateText();
var textAggregation = this.getAggregateText();
var matches = [];
var self = this;

regex = typeof regex === 'string' ? RegExp(escapeRegExp(regex), 'g') : regex;

if (regex.global) {
while (match = regex.exec(text)) {
matches.push(this.prepMatch(match, matchIndex++));
}
} else {
if (match = text.match(regex)) {
matches.push(this.prepMatch(match, 0));
matchAggregation(textAggregation);

function matchAggregation(textAggregation) {
for (var i = 0, l = textAggregation.length; i < l; ++i) {

var text = textAggregation[i];

if (typeof text !== 'string') {
// Deal with nested contexts: (recursive)
matchAggregation(text);
continue;
}

if (regex.global) {
while (match = regex.exec(text)) {
matches.push(self.prepMatch(match, matchIndex++, offset));
}
} else {
if (match = text.match(regex)) {
matches.push(self.prepMatch(match, 0, offset));
}
}

offset += text.length;
}
}

Expand All @@ -152,14 +181,14 @@ window.findAndReplaceDOMText = (function() {
/**
* Prepares a single match with useful meta info:
*/
prepMatch: function(match, matchIndex) {
prepMatch: function(match, matchIndex, characterOffset) {

if (!match[0]) {
throw new Error('findAndReplaceDOMText cannot handle zero-length matches');
}

match.endIndex = match.index + match[0].length;
match.startIndex = match.index;
match.endIndex = characterOffset + match.index + match[0].length;
match.startIndex = characterOffset + match.index;
match.index = matchIndex;

return match;
Expand All @@ -171,33 +200,61 @@ window.findAndReplaceDOMText = (function() {
getAggregateText: function() {

var elementFilter = this.options.filterElements;
var forceContext = this.options.forceContext;

return getText(this.node);

/**
* Gets aggregate text of a node without resorting
* to broken innerText/textContent
*/
function getText(node) {
function getText(node, txt) {

if (node.nodeType === 3) {
return node.data;
return [node.data];
}

if (elementFilter && !elementFilter(node)) {
return '';
return [];
}

var txt = '';
var txt = [''];
var i = 0;

if (node = node.firstChild) do {
txt += getText(node);

if (node.nodeType === 3) {
txt[i] += node.data;
continue;
}

var innerText = getText(node);

if (
forceContext &&
node.nodeType === 1 &&
(forceContext === true || forceContext(node))
) {
txt[++i] = innerText;
txt[++i] = '';
} else {
if (typeof innerText[0] === 'string') {
// Bridge nested text-node data so that they're
// not considered their own contexts:
// I.e. ['some', ['thing']] -> ['something']
txt[i] += innerText.shift();
}
if (innerText.length) {
txt[++i] = innerText;
txt[++i] = '';
}
}
} while (node = node.nextSibling);

return txt;

}

},

/**
Expand Down
63 changes: 60 additions & 3 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,16 +80,73 @@ test('Only output specified groups', function() {

test('Word boundaries', function() {

var text = 'a go matching at test wordat at';
var text = 'a go matching at test wordat at <p>AAA</p><p>BBB</p>';
var d = document.createElement('div');

d.innerHTML = text;
findAndReplaceDOMText(d, { find: /\bat\b/, wrap: 'x' });
htmlEqual(d.innerHTML, 'a go matching <x>at</x> test wordat at');
htmlEqual(d.innerHTML, 'a go matching <x>at</x> test wordat at <p>AAA</p><p>BBB</p>');

d.innerHTML = text;
findAndReplaceDOMText(d, { find: /\bat\b/g, wrap: 'x' });
htmlEqual(d.innerHTML, 'a go matching <x>at</x> test wordat <x>at</x>');
htmlEqual(d.innerHTML, 'a go matching <x>at</x> test wordat <x>at</x> <p>AAA</p><p>BBB</p>');

d.innerHTML = text;
findAndReplaceDOMText(d, {
find: /\bAAA\b/,
wrap: 'x',
forceContext: function(el) {
return el.nodeName.toLowerCase() === 'p';
}
});
htmlEqual(d.innerHTML, 'a go matching at test wordat at <p><x>AAA</x></p><p>BBB</p>')
});

test('Explicit context configuration', function() {

var d = document.createElement('div');

// By default all elements have fluid inline boundaries / no forced contexts
d.innerHTML = '<v>Foo<v>Bar</v></v>';
findAndReplaceDOMText(d, { find: /FooBar/, wrap: 'x' });
htmlEqual(d.innerHTML, '<v><x>Foo</x><v><x>Bar</x></v></v>');

// Explicit true context
d.innerHTML = '<v>Foo<v>Bar</v></v>';
findAndReplaceDOMText(d, { find: /FooBar/, wrap: 'x', forceContext: true });
htmlEqual(d.innerHTML, '<v>Foo<v>Bar</v></v>');

// Explicit false context
d.innerHTML = '<v>Foo<v>Bar</v></v>';
findAndReplaceDOMText(d, { find: /FooBar/, wrap: 'x', forceContext: false });
htmlEqual(d.innerHTML, '<v><x>Foo</x><v><x>Bar</x></v></v>');

// <a> is forced context
// <b> is not
var forcedAContext = function (el) {
return el.nodeName.toLowerCase() == 'a';
};

d.innerHTML = '<a>Foo<b>BarFoo</b>Bar</a>';
findAndReplaceDOMText(d, { find: /FooBar/, wrap: 'x', forceContext: forcedAContext });
htmlEqual(d.innerHTML, '<a><x>Foo</x><b><x>Bar</x>Foo</b>Bar</a>');

d.innerHTML = '<a>Foo</a><b>Bar</b> <b>Foo</b><a>Bar</a>';
findAndReplaceDOMText(d, { find: /FooBar/, wrap: 'x', forceContext: forcedAContext });
htmlEqual(d.innerHTML, '<a>Foo</a><b>Bar</b> <b>Foo</b><a>Bar</a>');

});

test('BLOCK_LEVEL_MATCH context fn', function() {

var d = document.createElement('div');

d.innerHTML = '<p>Some</p>Thing<em>Some<span>Thing</span></em><div>Some</div>Thing';
findAndReplaceDOMText(d, {
find: /something/i, wrap: 'x', forceContext: findAndReplaceDOMText.BLOCK_LEVEL_MATCH
});
htmlEqual(d.innerHTML, '<p>Some</p>Thing<em><x>Some</x><span><x>Thing</x></span></em><div>Some</div>Thing');

});

module('Replacement (With Nodes)');
Expand Down

0 comments on commit d41ae22

Please sign in to comment.