mirror of
				https://github.com/Jermolene/TiddlyWiki5
				synced 2025-10-31 15:42:59 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			178 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			178 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /*\
 | |
| 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;
 | |
| 
 | |
| /*
 | |
| Build a map of the text content of a dom node and its descendents:
 | |
| 
 | |
| 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;
 | |
| 	});
 | |
| };
 | |
| 
 | |
| /*
 | |
| Search for the first occurance of a target string within the textmap of a dom node
 | |
| 
 | |
| 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),
 | |
| 			endMetadata = this.locateMetadata(startPos + targetString.length);
 | |
| 		if(startMetadata && endMetadata) {
 | |
| 			return {
 | |
| 				startNode: startMetadata.domNode,
 | |
| 				startOffset: startPos - startMetadata.start,
 | |
| 				endNode: endMetadata.domNode,
 | |
| 				endOffset: (startPos + targetString.length) - endMetadata.start
 | |
| 			}			
 | |
| 		}
 | |
| 	}
 | |
| 	return null;
 | |
| };
 | |
| 
 | |
| /*
 | |
| Search for all occurances of a string within the textmap of a dom node
 | |
| 
 | |
| Options include:
 | |
| 	mode: "normal", "regexp" or "whitespace"
 | |
| 	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+") + ")";
 | |
| 	} 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))
 | |
| 	};
 | |
| };
 | |
| 
 | |
| })();
 | 
