mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1012530 - Part 2. Reorder child nodes when swapping document state. r=florian
This commit is contained in:
parent
984087f192
commit
acf73e4b2e
@ -358,7 +358,7 @@ const TranslationItem_NodePlaceholder = {
|
||||
*/
|
||||
function generateTranslationHtmlForItem(item, content) {
|
||||
let localName = item.isRoot ? "div" : "b";
|
||||
return '<' + localName + ' id="n' + item.id + '">' +
|
||||
return '<' + localName + ' id=n' + item.id + '>' +
|
||||
content +
|
||||
"</" + localName + ">";
|
||||
}
|
||||
@ -426,6 +426,63 @@ function parseResultNode(item, node) {
|
||||
/**
|
||||
* Helper function to swap the text of a TranslationItem
|
||||
* between its original and translated states.
|
||||
* How it works:
|
||||
*
|
||||
* The function iterates through the target array (either the `original` or
|
||||
* `translation` array from the TranslationItem), while also keeping a pointer
|
||||
* to a current position in the child nodes from the actual DOM node that we
|
||||
* are modifying. This pointer is moved forward after each item of the array
|
||||
* is translated. If, at any given time, the pointer doesn't match the expected
|
||||
* node that was supposed to be seen, it means that the original and translated
|
||||
* contents have a different ordering, and thus we need to adjust that.
|
||||
*
|
||||
* A full example of the reordering process, swapping from Original to
|
||||
* Translation:
|
||||
*
|
||||
* Original (en): <div>I <em>miss</em> <b>you</b></div>
|
||||
*
|
||||
* Translation (fr): <div><b>Tu</b> me <em>manques</em></div>
|
||||
*
|
||||
* Step 1:
|
||||
* pointer points to firstChild of the DOM node, textnode "I "
|
||||
* first item in item.translation is [object TranslationItem <b>]
|
||||
*
|
||||
* pointer does not match the expected element, <b>. So let's move <b> to the
|
||||
* pointer position.
|
||||
*
|
||||
* Current state of the DOM:
|
||||
* <div><b>you</b>I <em>miss</em> </div>
|
||||
*
|
||||
* Step 2:
|
||||
* pointer moves forward to nextSibling, textnode "I " again.
|
||||
* second item in item.translation is the string " me "
|
||||
*
|
||||
* pointer points to a text node, and we were expecting a text node. Match!
|
||||
* just replace the text content.
|
||||
*
|
||||
* Current state of the DOM:
|
||||
* <div><b>you</b> me <em>miss</em> </div>
|
||||
*
|
||||
* Step 3:
|
||||
* pointer moves forward to nextSibling, <em>miss</em>
|
||||
* third item in item.translation is [object TranslationItem <em>]
|
||||
*
|
||||
* pointer points to the expected node. Match! Nothing to do.
|
||||
*
|
||||
* Step 4:
|
||||
* all items in this item.translation were transformed. The remaining
|
||||
* text nodes are cleared to "", and domNode.normalize() removes them.
|
||||
*
|
||||
* Current state of the DOM:
|
||||
* <div><b>you</b> me <em>miss</em></div>
|
||||
*
|
||||
* Further steps:
|
||||
* After that, the function will visit the child items (from the visitStack),
|
||||
* and the text inside the <b> and <em> nodes will be swapped as well,
|
||||
* yielding the final result:
|
||||
*
|
||||
* <div><b>Tu</b> me <em>manques</em></div>
|
||||
*
|
||||
*
|
||||
* @param item A TranslationItem object
|
||||
* @param target A string that is either "translation"
|
||||
@ -446,8 +503,6 @@ function swapTextForItem(item, target) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let sourceNodeCount = 0;
|
||||
|
||||
if (!curItem[target]) {
|
||||
// Translation not found for this item. This could be due to
|
||||
// an error in the server response. For example, if a translation
|
||||
@ -457,60 +512,162 @@ function swapTextForItem(item, target) {
|
||||
continue;
|
||||
}
|
||||
|
||||
domNode.normalize();
|
||||
|
||||
// curNode points to the child nodes of the DOM node that we are
|
||||
// modifying. During most of the process, while the target array is
|
||||
// being iterated (in the for loop below), it should walk together with
|
||||
// the array and be pointing to the correct node that needs to modified.
|
||||
// If it's not pointing to it, that means some sort of node reordering
|
||||
// will be necessary to produce the correct translation.
|
||||
// Note that text nodes don't need to be reordered, as we can just replace
|
||||
// the content of one text node with another.
|
||||
//
|
||||
// curNode starts in the firstChild...
|
||||
let curNode = domNode.firstChild;
|
||||
|
||||
// ... actually, let's make curNode start at the first useful node (either
|
||||
// a non-blank text node or something else). This is not strictly necessary,
|
||||
// as the reordering algorithm would correctly handle this case. However,
|
||||
// this better aligns the resulting translation with the DOM content of the
|
||||
// page, avoiding cases that would need to be unecessarily reordered.
|
||||
//
|
||||
// An example of how this helps:
|
||||
//
|
||||
// ---- Original: <div> <b>Hello </b> world.</div>
|
||||
// ^textnode 1 ^item 1 ^textnode 2
|
||||
//
|
||||
// - Translation: <div><b>Hallo </b> Welt.</div>
|
||||
//
|
||||
// Transformation process without this optimization:
|
||||
// 1 - start pointer at textnode 1
|
||||
// 2 - move item 1 to first position inside the <div>
|
||||
//
|
||||
// Node now looks like: <div><b>Hello </b>[ ][ world.]</div>
|
||||
// textnode 1^ ^textnode 2
|
||||
//
|
||||
// 3 - replace textnode 1 with " Welt."
|
||||
// 4 - clear remaining text nodes (in this case, textnode 2)
|
||||
//
|
||||
// Transformation process with this optimization:
|
||||
// 1 - start pointer at item 1
|
||||
// 2 - item 1 is already in position
|
||||
// 3 - replace textnode 2 with " Welt."
|
||||
//
|
||||
// which completely avoids any node reordering, and requires only one
|
||||
// text change instead of two (while also leaving the page closer to
|
||||
// its original state).
|
||||
while (curNode &&
|
||||
curNode.nodeType == TEXT_NODE &&
|
||||
curNode.nodeValue.trim() == "") {
|
||||
curNode = curNode.nextSibling;
|
||||
}
|
||||
|
||||
// Now let's walk through all items in the `target` array of the
|
||||
// TranslationItem. This means either the TranslationItem.original or
|
||||
// TranslationItem.translation array.
|
||||
for (let child of curItem[target]) {
|
||||
// If the array element is another TranslationItem object, let's
|
||||
// add it to the stack to be visited
|
||||
if (child instanceof TranslationItem) {
|
||||
// Adding this child to the stack.
|
||||
visitStack.push(child);
|
||||
continue;
|
||||
}
|
||||
for (let targetItem of curItem[target]) {
|
||||
|
||||
// If it's a string, say, the Nth string of the `target` array, let's
|
||||
// replace the Nth child TextNode of this element with this string.
|
||||
// During our translation process we skipped all empty text nodes, so we
|
||||
// must also skip them here. If there are not enough text nodes to be used,
|
||||
// a new text node will be created and appended to the end of the element.
|
||||
let targetTextNode = getNthNonEmptyTextNodeFromElement(sourceNodeCount++, domNode);
|
||||
if (targetItem instanceof TranslationItem) {
|
||||
// If the array element is another TranslationItem object, let's
|
||||
// add it to the stack to be visited.
|
||||
visitStack.push(targetItem);
|
||||
|
||||
// A trailing and a leading space must be preserved because they are meaningful in HTML.
|
||||
let preSpace = targetTextNode.nodeValue.startsWith(" ") ? " " : "";
|
||||
let endSpace = targetTextNode.nodeValue.endsWith(" ") ? " " : "";
|
||||
targetTextNode.nodeValue = preSpace + child + endSpace;
|
||||
}
|
||||
let targetNode = targetItem.nodeRef;
|
||||
|
||||
// The translated version of a node might have less text nodes than its original
|
||||
// version. If that's the case, let's clear the remaining nodes.
|
||||
if (sourceNodeCount > 0) {
|
||||
clearRemainingNonEmptyTextNodesFromElement(sourceNodeCount, domNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
// If the node is not in the expected position, let's reorder
|
||||
// it into position...
|
||||
if (curNode != targetNode &&
|
||||
// ...unless the page has reparented this node under a totally
|
||||
// different node (or removed it). In this case, all bets are off
|
||||
// on being able to do anything correctly, so it's better not to
|
||||
// bring back the node to this parent.
|
||||
targetNode.parentNode == domNode) {
|
||||
|
||||
function getNthNonEmptyTextNodeFromElement(n, element) {
|
||||
for (let childNode of element.childNodes) {
|
||||
if (childNode.nodeType == Ci.nsIDOMNode.TEXT_NODE &&
|
||||
childNode.nodeValue.trim() != "") {
|
||||
if (n-- == 0)
|
||||
return childNode;
|
||||
}
|
||||
}
|
||||
// We don't need to null-check curNode because insertBefore(..., null)
|
||||
// does what we need in that case: reorder this node to the end
|
||||
// of child nodes.
|
||||
domNode.insertBefore(targetNode, curNode);
|
||||
curNode = targetNode;
|
||||
}
|
||||
|
||||
// If there are not enough DOM nodes, let's create a new one.
|
||||
return element.appendChild(element.ownerDocument.createTextNode(""));
|
||||
}
|
||||
// Move pointer forward. Since we do not add empty text nodes to the
|
||||
// list of translation items, we must skip them here too while
|
||||
// traversing the DOM in order to get better alignment between the
|
||||
// text nodes and the translation items.
|
||||
if (curNode) {
|
||||
curNode = getNextSiblingSkippingEmptyTextNodes(curNode);
|
||||
}
|
||||
|
||||
function clearRemainingNonEmptyTextNodesFromElement(start, element) {
|
||||
let count = 0;
|
||||
for (let childNode of element.childNodes) {
|
||||
if (childNode.nodeType == Ci.nsIDOMNode.TEXT_NODE &&
|
||||
childNode.nodeValue.trim() != "") {
|
||||
if (count++ >= start) {
|
||||
childNode.nodeValue = "";
|
||||
} else if (targetItem === TranslationItem_NodePlaceholder) {
|
||||
// If the current item is a placeholder node, we need to move
|
||||
// our pointer "past" it, jumping from one side of a block of
|
||||
// elements + empty text nodes to the other side. Even if
|
||||
// non-placeholder elements exists inside the jumped block,
|
||||
// they will be pulled correctly later in the process when the
|
||||
// targetItem for those nodes are handled.
|
||||
|
||||
while (curNode &&
|
||||
(curNode.nodeType != TEXT_NODE ||
|
||||
curNode.nodeValue.trim() == "")) {
|
||||
curNode = curNode.nextSibling;
|
||||
}
|
||||
|
||||
} else {
|
||||
// Finally, if it's a text item, we just need to find the next
|
||||
// text node to use. Text nodes don't need to be reordered, so
|
||||
// the first one found can be used.
|
||||
while (curNode && curNode.nodeType != TEXT_NODE) {
|
||||
curNode = curNode.nextSibling;
|
||||
}
|
||||
|
||||
// If none was found and we reached the end of the child nodes,
|
||||
// let's create a new one.
|
||||
if (!curNode) {
|
||||
// We don't know if the original content had a space or not,
|
||||
// so the best bet is to create the text node with " " which
|
||||
// will add one space at the beginning and one at the end.
|
||||
curNode = domNode.appendChild(domNode.ownerDocument.createTextNode(" "));
|
||||
}
|
||||
|
||||
// A trailing and a leading space must be preserved because
|
||||
// they are meaningful in HTML.
|
||||
let preSpace = /^\s/.test(curNode.nodeValue) ? " " : "";
|
||||
let endSpace = /\s$/.test(curNode.nodeValue) ? " " : "";
|
||||
|
||||
curNode.nodeValue = preSpace + targetItem + endSpace;
|
||||
curNode = getNextSiblingSkippingEmptyTextNodes(curNode);
|
||||
}
|
||||
}
|
||||
|
||||
// The translated version of a node might have less text nodes than its
|
||||
// original version. If that's the case, let's clear the remaining nodes.
|
||||
if (curNode) {
|
||||
clearRemainingNonEmptyTextNodesFromElement(curNode);
|
||||
}
|
||||
|
||||
// And remove any garbage "" nodes left after clearing.
|
||||
domNode.normalize();
|
||||
}
|
||||
}
|
||||
|
||||
function getNextSiblingSkippingEmptyTextNodes(startSibling) {
|
||||
let item = startSibling.nextSibling;
|
||||
while (item &&
|
||||
item.nodeType == TEXT_NODE &&
|
||||
item.nodeValue.trim() == "") {
|
||||
item = item.nextSibling;
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
function clearRemainingNonEmptyTextNodesFromElement(startSibling) {
|
||||
let item = startSibling;
|
||||
while (item) {
|
||||
if (item.nodeType == TEXT_NODE &&
|
||||
item.nodeValue != "") {
|
||||
item.nodeValue = "";
|
||||
}
|
||||
item = item.nextSibling;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user