2020-03-11 16:55:19 +00:00
|
|
|
/*\
|
|
|
|
title: $:/plugins/tiddlywiki/dynannotate/textmap.js
|
|
|
|
type: application/javascript
|
|
|
|
module-type: library
|
|
|
|
|
|
|
|
Structure for modelling mapping between a string and its representation in the DOM
|
|
|
|
|
|
|
|
\*/
|
|
|
|
(function(){
|
|
|
|
|
|
|
|
/*jslint node: true, browser: true */
|
|
|
|
/*global $tw: false */
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
var PREFIX_SUFFIX_LENGTH = 50;
|
|
|
|
|
|
|
|
/*
|
2023-05-06 10:30:21 +00:00
|
|
|
Build a map of the text content of a DOM node and its descendants:
|
2020-03-11 16:55:19 +00:00
|
|
|
|
|
|
|
string: concatenation of the text content of child nodes
|
|
|
|
metadata: array of {start,end,domNode} where start and end identify position in the string
|
|
|
|
*/
|
|
|
|
exports.TextMap = function(domNode) {
|
|
|
|
var self = this,
|
|
|
|
stringChunks = [],
|
|
|
|
p = 0;
|
|
|
|
this.metadata = [];
|
|
|
|
var processNode = function(domNode) {
|
|
|
|
// Check for text nodes
|
|
|
|
if(domNode.nodeType === 3) {
|
|
|
|
var text = domNode.textContent;
|
|
|
|
stringChunks.push(text);
|
|
|
|
self.metadata.push({
|
|
|
|
start: p,
|
|
|
|
end: p + text.length,
|
|
|
|
domNode: domNode
|
|
|
|
});
|
|
|
|
p += text.length;
|
|
|
|
} else {
|
|
|
|
// Otherwise look within the child nodes
|
|
|
|
if(domNode.childNodes) {
|
|
|
|
for(var t=0; t<domNode.childNodes.length; t++ ) {
|
|
|
|
processNode(domNode.childNodes[t]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
// Process our text nodes
|
|
|
|
processNode(domNode);
|
|
|
|
this.string = stringChunks.join("");
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
Locate the metadata record corresponding to a given position in the string
|
|
|
|
*/
|
|
|
|
exports.TextMap.prototype.locateMetadata = function(position) {
|
|
|
|
return this.metadata.find(function(metadata) {
|
|
|
|
return position >= metadata.start && position < metadata.end;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
2023-05-06 10:30:21 +00:00
|
|
|
Search for the first occurrence of a target string within the textmap of a DOM node
|
2020-03-11 16:55:19 +00:00
|
|
|
|
|
|
|
Returns an object with the following properties:
|
|
|
|
startNode: node containing the start of the text
|
|
|
|
startOffset: offset of the start of the text within the node
|
|
|
|
endNode: node containing the end of the text
|
|
|
|
endOffset: offset of the end of the text within the node
|
|
|
|
*/
|
|
|
|
exports.TextMap.prototype.findText = function(targetString,targetPrefix,targetSuffix) {
|
|
|
|
if(!targetString) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
targetPrefix = targetPrefix || "";
|
|
|
|
targetSuffix = targetSuffix || "";
|
|
|
|
var startPos = this.string.indexOf(targetPrefix + targetString + targetSuffix);
|
|
|
|
if(startPos !== -1) {
|
|
|
|
startPos += targetPrefix.length;
|
|
|
|
var startMetadata = this.locateMetadata(startPos),
|
2020-12-09 19:14:43 +00:00
|
|
|
endMetadata = this.locateMetadata(startPos + targetString.length - 1);
|
2020-03-11 16:55:19 +00:00
|
|
|
if(startMetadata && endMetadata) {
|
|
|
|
return {
|
|
|
|
startNode: startMetadata.domNode,
|
|
|
|
startOffset: startPos - startMetadata.start,
|
|
|
|
endNode: endMetadata.domNode,
|
|
|
|
endOffset: (startPos + targetString.length) - endMetadata.start
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
2023-05-06 10:30:21 +00:00
|
|
|
Search for all occurrences of a string within the textmap of a DOM node
|
2020-03-11 16:55:19 +00:00
|
|
|
|
|
|
|
Options include:
|
2023-05-06 10:30:21 +00:00
|
|
|
mode: "normal", "literal", "regexp", "whitespace", "some" or "words"
|
2020-03-11 16:55:19 +00:00
|
|
|
caseSensitive: true if the search should be case sensitive
|
|
|
|
|
|
|
|
Returns an array of objects with the following properties:
|
|
|
|
startPos: start position of the match within the string contained by this TextMap
|
|
|
|
startNode: node containing the start of the text
|
|
|
|
startOffset: offset of the start of the text within the node
|
|
|
|
endPos: end position of the match within the string contained by this TextMap
|
|
|
|
endNode: node containing the end of the text
|
|
|
|
endOffset: offset of the end of the text within the node
|
|
|
|
*/
|
|
|
|
exports.TextMap.prototype.search = function(searchString,options) {
|
|
|
|
if(!searchString) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
options = options || {};
|
|
|
|
// Compose the regexp
|
|
|
|
var regExpString,
|
|
|
|
flags = options.caseSensitive ? "g" : "gi";
|
|
|
|
if(options.mode === "regexp") {
|
|
|
|
regExpString = "(" + searchString + ")";
|
|
|
|
} else if(options.mode === "whitespace") {
|
|
|
|
// Normalise whitespace
|
|
|
|
regExpString = "(" + searchString.split(/\s+/g).filter(function(word) {
|
|
|
|
return !!word
|
|
|
|
}).map($tw.utils.escapeRegExp).join("\\s+") + ")";
|
2023-05-06 10:30:21 +00:00
|
|
|
} else if(options.mode === "words" || options.mode === "some") {
|
|
|
|
// Match any word separated by whitespace
|
|
|
|
regExpString = "(" + searchString.split(/\s+/g).filter(function(word) {
|
|
|
|
return !!word
|
|
|
|
}).map($tw.utils.escapeRegExp).join("|") + ")";
|
2020-03-11 16:55:19 +00:00
|
|
|
} else {
|
|
|
|
// Normal search
|
|
|
|
regExpString = "(" + $tw.utils.escapeRegExp(searchString) + ")";
|
|
|
|
}
|
|
|
|
// Compile the regular expression
|
|
|
|
var regExp;
|
|
|
|
try {
|
|
|
|
regExp = RegExp(regExpString,flags);
|
|
|
|
} catch(e) {
|
|
|
|
}
|
|
|
|
if(!regExp) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
// Find each match
|
|
|
|
var results = [],
|
|
|
|
match;
|
|
|
|
do {
|
|
|
|
match = regExp.exec(this.string);
|
|
|
|
if(match) {
|
|
|
|
var metadataStart = this.locateMetadata(match.index),
|
|
|
|
metadataEnd = this.locateMetadata(match.index + match[0].length);
|
|
|
|
if(metadataStart && metadataEnd) {
|
|
|
|
results.push({
|
|
|
|
startPos: match.index,
|
|
|
|
startNode: metadataStart.domNode,
|
|
|
|
startOffset: match.index - metadataStart.start,
|
|
|
|
endPos: match.index + match[0].length,
|
|
|
|
endNode: metadataEnd.domNode,
|
|
|
|
endOffset: match.index + match[0].length - metadataEnd.start
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} while(match);
|
|
|
|
return results;
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
Given a start container and offset and a search string, return a prefix and suffix to disambiguate the text
|
|
|
|
*/
|
|
|
|
exports.TextMap.prototype.extractContext = function(startContainer,startOffset,text) {
|
|
|
|
var startMetadata = this.metadata.find(function(metadata) {
|
|
|
|
return metadata.domNode === startContainer
|
|
|
|
});
|
|
|
|
if(!startMetadata) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
var startPos = startMetadata.start + startOffset;
|
|
|
|
return {
|
|
|
|
prefix: this.string.slice(Math.max(startPos - PREFIX_SUFFIX_LENGTH, 0), startPos),
|
|
|
|
suffix: this.string.slice(startPos + text.length, Math.min(startPos + text.length + PREFIX_SUFFIX_LENGTH, this.string.length))
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
})();
|