В ожидании ответов я начал писать собственный парсер. Это немного грубо, так как есть без поддержки кросс-браузера, и я не вношу никаких изменений в текст - это означает, что любые строки и другие пробелы из HTML будут сохранены.
Существует также много избыточности, которую я еще не очистил, например, пересечение детей узлов, которые я уже знаю, чтобы быть скрытыми.
Во всяком случае, код:
function ParsedRange(range){
this.text = "";
this.nodeIndices = [];
this.highlight = function(startIndex, endIndex){
var selection = window.getSelection();
var startNode = this.nodeIndices[startIndex].node;
var endNode = this.nodeIndices[endIndex].node;
var startOffset = startIndex - this.nodeIndices[startIndex].startIndex;
var endOffset = endIndex - this.nodeIndices[endIndex].startIndex + 1;
// Scroll into view
startNode.parentNode.scrollIntoViewIfNeeded();
// Highlight
range.setStart(startNode, startOffset);
range.setEnd(endNode, endOffset);
selection.removeAllRanges();
selection.addRange(range);
};
// Parsing starts here
var startIndex;
var rootNode = range.commonAncestorContainer;
var startNode = range.startContainer;
var endNode = range.endContainer;
var treeWalker = document.createTreeWalker(rootNode, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, null, false); // Only walk text and element nodes
var currentNode = treeWalker.currentNode;
// Move to start node
while (currentNode && currentNode != startNode) currentNode = treeWalker.nextNode();
// Extract text
var nodeText;
while (currentNode && currentNode != endNode){ // Handle end node separately
// Continue to next node if current node is hidden
if (isHidden(currentNode)){
currentNode = treeWalker.nextNode();
continue;
}
// Extract text if text node
if (currentNode.nodeType == 3){
if (currentNode == startNode) nodeText = currentNode.nodeValue.substring(range.startOffset); // Extract from start of selection if first node
else nodeText = currentNode.nodeValue; // Else extra entire node
this.text += nodeText;
if (currentNode == startNode) startIndex = range.startOffset * -1;
else startIndex = this.nodeIndices.length;
for (var i=0; i<nodeText.length; i++){
this.nodeIndices.push({
startIndex: startIndex,
node: currentNode
});
}
}
// Continue to next node
currentNode = treeWalker.nextNode();
}
// Extract text from end node if it's a text node
if (currentNode == endNode && currentNode.nodeType == 3 && !isHidden(currentNode)){
if (endNode == startNode) nodeText = currentNode.nodeValue.substring(range.startOffset, range.endOffset); // Extract only selected part if end and start nodes are the same
else nodeText = currentNode.nodeValue.substring(0, range.endOffset); // Else extract up to where the selection ends in the end node
this.text += nodeText;
if (currentNode == startNode) startIndex = range.startOffset*-1;
else startIndex = this.nodeIndices.length;
for (var i=0; i<nodeText.length; i++){
this.nodeIndices.push({
startIndex: startIndex,
node: currentNode
});
}
}
return this;
}
ParsedRange.removeHighlight = function(){
window.getSelection().removeAllRanges();
};
function isHidden(element){
// Get parent node if element is a text node
if (element.nodeType == 3) element = element.parentNode;
// Only check visibility of the element itself
if (window.getComputedStyle(element, null).getPropertyValue("visibility") == "hidden") return true;
// Check display and dimensions for element and its parents
while (element){
if (element.nodeType == 9) return false; // Document
if (element.tagName == "NOSCRIPT") return true;
if (window.getComputedStyle(element, null).getPropertyValue("display") == "none") return true;
if (element.offsetWidth == 0 || element.offsetHeight == 0){ // If element does not have overflow:visible it is hidden
if (window.getComputedStyle(element, null).getPropertyValue("overflow") != "visible"){
return true;
}
}
element = element.parentNode;
}
return false;
}
Я сделал это как класс (кроме функции isHidden()
хелперов) в связи с тем, как она интегрирована в моем проекте.
Это в сторону класса работает, передавая ему допустимый диапазон, который затем извлекает текст внутри диапазона и сохраняет ссылки на все узлы.Эти ссылки используются в функции highlight()
, которая использует выбор браузера для выделения на основе начальных и конечных символов.
Дополнительное примечание о собственности nodeIndices
(вид как это может не иметь смысла). nodeIndices
представляет собой массив, содержащий объекты с формой:
{
startIndex: // Int
node: // Reference to text node
}
Для каждого персонажа я экстрагировать в мой получившийся текст я толкнуть один из этих объектов на nodeIndices
, то node
свойство просто ссылка на текстовый узел, из которого текст пришел. startIndex
определяет, на каком знаке узел начинается во всем тексте.
Используя этот массив, я могу перевести его из символьного индекса в ParsedParagraph.text
на узел HTML и индекс соответствующего символа внутри этого узла.
Пример использования:
// Get start/end nodes and offsets for range
var startNode = // Code to get start node here, can be a text node or an element node
var startOffset = // Offset into the start node
var endNode = // Code to get end node here, can be a text node or an element node
var endOffset = // Offset into the end node
// Create the range
var range = document.createRange();
range.setStart(startNode, startOffset);
range.setEnd(endNode, endOffset);
// Parse the range using the ParsedRange class
var parsedRange = new ParsedRange(range);
parsedRange.text; // Contains visible text with whitespaces preserved.
parsedRange.highlight(startIndex, endIndex); // Will highlight the corresponding text inside parsedRange.text using browser selection
Код используется только в расширении Chrome, поэтому отсутствие перекрестной поддержки браузера не такая уж большая проблема. Во всяком случае, есть ли не измененная версия модуля Rangy/TextRange? Теперь у меня есть рабочее решение, но мне интересно узнать, как вы это сделали. – Woodgnome
@Woodgnome: Отсутствие спецификации все еще является проблемой на долгий срок: реализация Chrome может легко измениться в будущем. Источник для модуля TextRange находится здесь: http://rangy.googlecode.com/svn/trunk/src/js/modules/rangy-textrange.js, а также сборки Rangy 1.3 имеют уменьшенные и незавершенные версии всего. –
Хороший вопрос в отношении реализации - это было просто временное решение, поэтому должно быть хорошо в любом случае. – Woodgnome