Merge autoland to mozilla-central. a=merge

This commit is contained in:
Tiberius Oros 2018-03-06 11:52:14 +02:00
Родитель 1611e7fc4f 9d71742b36
Коммит f3c2b2ecf8
20 изменённых файлов: 1893 добавлений и 564 удалений

Просмотреть файл

@ -33,6 +33,9 @@ const PREF_TEST_ROOT = "mochitest.testRoot";
const PREF_ENABLED = "browser.policies.enabled";
const PREF_LOGLEVEL = "browser.policies.loglevel";
// To force disallowing enterprise-only policies during tests
const PREF_DISALLOW_ENTERPRISE = "browser.policies.testing.disallowEnterprise";
XPCOMUtils.defineLazyGetter(this, "log", () => {
let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm", {});
return new ConsoleAPI({
@ -127,6 +130,11 @@ EnterprisePoliciesManager.prototype = {
continue;
}
if (policySchema.enterprise_only && !areEnterpriseOnlyPoliciesAllowed()) {
log.error(`Policy ${policyName} is only allowed on ESR`);
continue;
}
let [parametersAreValid, parsedParameters] =
PoliciesValidator.validateAndParseParameters(policyParameters,
policySchema);
@ -305,6 +313,33 @@ EnterprisePoliciesManager.prototype = {
let DisallowedFeatures = {};
/**
* areEnterpriseOnlyPoliciesAllowed
*
* Checks whether the policies marked as enterprise_only in the
* schema are allowed to run on this browser.
*
* This is meant to only allow policies to run on ESR, but in practice
* we allow it to run on channels different than release, to allow
* these policies to be tested on pre-release channels.
*
* @returns {Bool} Whether the policy can run.
*/
function areEnterpriseOnlyPoliciesAllowed() {
if (Services.prefs.getBoolPref(PREF_DISALLOW_ENTERPRISE, false)) {
// This is used as an override to test the "enterprise_only"
// functionality itself on tests, which would always return
// true due to the Cu.isInAutomation check below.
return false;
}
if (AppConstants.MOZ_UPDATE_CHANNEL != "release" ||
Cu.isInAutomation) {
return true;
}
return false;
}
/*
* JSON PROVIDER OF POLICIES

Просмотреть файл

@ -7,6 +7,7 @@ support-files =
config_broken_json.json
[browser_policies_broken_json.js]
[browser_policies_enterprise_only.js]
[browser_policies_notice_in_aboutpreferences.js]
[browser_policies_popups_cookies_addons_flash.js]
[browser_policies_runOnce_helper.js]

Просмотреть файл

@ -0,0 +1,59 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const PREF_DISALLOW_ENTERPRISE = "browser.policies.testing.disallowEnterprise";
add_task(async function test_enterprise_only_policies() {
let { Policies } = ChromeUtils.import("resource:///modules/policies/Policies.jsm", {});
let normalPolicyRan = false, enterprisePolicyRan = false;
Policies.NormalPolicy = {
onProfileAfterChange(manager, param) {
normalPolicyRan = true;
}
};
Policies.EnterpriseOnlyPolicy = {
onProfileAfterChange(manager, param) {
enterprisePolicyRan = true;
}
};
Services.prefs.setBoolPref(PREF_DISALLOW_ENTERPRISE, true);
await setupPolicyEngineWithJson(
// policies.json
{
"policies": {
"NormalPolicy": true,
"EnterpriseOnlyPolicy": true
}
},
// custom schema
{
properties: {
"NormalPolicy": {
"type": "boolean"
},
"EnterpriseOnlyPolicy": {
"type": "boolean",
"enterprise_only": true
},
}
}
);
is(Services.policies.status, Ci.nsIEnterprisePolicies.ACTIVE, "Engine is active");
is(normalPolicyRan, true, "Normal policy ran as expected");
is(enterprisePolicyRan, false, "Enterprise-only policy was prevented from running");
// Clean-up
delete Policies.NormalPolicy;
delete Policies.EnterpriseOnlyPolicy;
Services.prefs.clearUserPref(PREF_DISALLOW_ENTERPRISE);
});

Просмотреть файл

@ -16,6 +16,5 @@ support-files =
[chrome/test_cssanimation_missing_keyframes.html]
[chrome/test_generated_content_getAnimations.html]
[chrome/test_running_on_compositor.html]
skip-if = os == 'android' # bug 1442150
[chrome/test_simulate_compute_values_failure.html]
skip-if = !debug

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -96,16 +96,6 @@ function waitForWheelEvent(aTarget) {
});
}
// Returns true if |aAnimation| begins at the current timeline time. We
// sometimes need to detect this case because if we started an animation
// asynchronously (e.g. using play()) and then ended up running the next frame
// at precisely the time the animation started (due to aligning with vsync
// refresh rate) then we won't end up restyling in that frame.
function startsRightNow(aAnimation) {
return aAnimation.startTime === aAnimation.timeline.currentTime &&
aAnimation.currentTime === 0;
}
function tweakExpectedRestyleCount(aAnimation, aExpectedRestyleCount) {
// Normally we expect one restyling for each requestAnimationFrame (as
// called by observeRestyling) PLUS one for the last frame because of bug
@ -123,10 +113,10 @@ function tweakExpectedRestyleCount(aAnimation, aExpectedRestyleCount) {
// If we have the conformant Promise handling and |aAnimation| begins at
// the current timeline time, we will not process restyling in the initial
// frame.
if (startsRightNow(aAnimation)) {
if (animationStartsRightNow(aAnimation)) {
return aExpectedRestyleCount - 1;
}
} else if (!startsRightNow(aAnimation)) {
} else if (!animationStartsRightNow(aAnimation)) {
// If we don't have the conformant Promise handling and |aAnimation|
// doesn't begin at the current timeline time, we will see an additional
// restyling in the last frame.

Просмотреть файл

@ -419,3 +419,14 @@ function waitForPaints() {
// (bug 1341294).
return waitForAnimationFrames(2);
}
// Returns true if |aAnimation| begins at the current timeline time. We
// sometimes need to detect this case because if we started an animation
// asynchronously (e.g. using play()) and then ended up running the next frame
// at precisely the time the animation started (due to aligning with vsync
// refresh rate) then we won't end up restyling in that frame.
function animationStartsRightNow(aAnimation) {
return aAnimation.startTime === aAnimation.timeline.currentTime &&
aAnimation.currentTime === 0;
}

Просмотреть файл

@ -5089,6 +5089,13 @@ EditorBase::FindSelectionRoot(nsINode* aNode)
return rootContent.forget();
}
void
EditorBase::InitializeSelectionAncestorLimit(Selection& aSelection,
nsIContent& aAncestorLimit)
{
aSelection.SetAncestorLimiter(&aAncestorLimit);
}
nsresult
EditorBase::InitializeSelection(nsIDOMEventTarget* aFocusEventTarget)
{
@ -5099,10 +5106,6 @@ EditorBase::InitializeSelection(nsIDOMEventTarget* aFocusEventTarget)
return NS_OK;
}
bool isTargetDoc =
targetNode->NodeType() == nsINode::DOCUMENT_NODE &&
targetNode->HasFlag(NODE_IS_EDITABLE);
RefPtr<Selection> selection = GetSelection();
NS_ENSURE_STATE(selection);
@ -5130,24 +5133,18 @@ EditorBase::InitializeSelection(nsIDOMEventTarget* aFocusEventTarget)
nsISelectionDisplay::DISPLAY_ALL);
selectionController->RepaintSelection(
nsISelectionController::SELECTION_NORMAL);
// If the computed selection root isn't root content, we should set it
// as selection ancestor limit. However, if that is root element, it means
// there is not limitation of the selection, then, we must set nullptr.
// NOTE: If we set a root element to the ancestor limit, some selection
// methods don't work fine.
if (selectionRootContent->GetParent()) {
selection->SetAncestorLimiter(selectionRootContent);
InitializeSelectionAncestorLimit(*selection, *selectionRootContent);
} else {
selection->SetAncestorLimiter(nullptr);
}
// XXX What case needs this?
if (isTargetDoc) {
if (!selection->RangeCount()) {
BeginningOfDocument();
}
}
// If there is composition when this is called, we may need to restore IME
// selection because if the editor is reframed, this already forgot IME
// selection and the transaction.

Просмотреть файл

@ -733,6 +733,19 @@ protected:
void BeginPlaceholderTransaction(nsAtom* aTransactionName);
void EndPlaceholderTransaction();
/**
* InitializeSelectionAncestorLimit() is called by InitializeSelection().
* When this is called, each implementation has to call
* aSelection.SetAncestorLimiter() with aAnotherLimit.
*
* @param aSelection The selection.
* @param aAncestorLimit New ancestor limit of aSelection. This always
* has parent node. So, it's always safe to
* call SetAncestorLimit() with this node.
*/
virtual void InitializeSelectionAncestorLimit(Selection& aSelection,
nsIContent& aAncestorLimit);
public:
/**
* All editor operations which alter the doc should be prefaced

Просмотреть файл

@ -507,81 +507,173 @@ HTMLEditor::InitRules()
NS_IMETHODIMP
HTMLEditor::BeginningOfDocument()
{
return MaybeCollapseSelectionAtFirstEditableNode(false);
}
void
HTMLEditor::InitializeSelectionAncestorLimit(Selection& aSelection,
nsIContent& aAncestorLimit)
{
// Hack for initializing selection.
// HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode() will try to
// collapse selection at first editable text node or inline element which
// cannot have text nodes as its children. However, selection has already
// set into the new editing host by user, we should not change it. For
// solving this issue, we should do nothing if selection range is in active
// editing host except it's not collapsed at start of the editing host since
// aSelection.SetAncestorLimiter(aAncestorLimit) will collapse selection
// at start of the new limiter if focus node of aSelection is outside of the
// editing host. However, we need to check here if selection is already
// collapsed at start of the editing host because it's possible JS to do it.
// In such case, we should not modify selection with calling
// MaybeCollapseSelectionAtFirstEditableNode().
// Basically, we should try to collapse selection at first editable node
// in HTMLEditor.
bool tryToCollapseSelectionAtFirstEditableNode = true;
if (aSelection.RangeCount() == 1 && aSelection.IsCollapsed()) {
Element* editingHost = GetActiveEditingHost();
nsRange* range = aSelection.GetRangeAt(0);
if (range->GetStartContainer() == editingHost &&
!range->StartOffset()) {
// JS or user operation has already collapsed selection at start of
// the editing host. So, we don't need to try to change selection
// in this case.
tryToCollapseSelectionAtFirstEditableNode = false;
}
}
EditorBase::InitializeSelectionAncestorLimit(aSelection, aAncestorLimit);
// XXX Do we need to check if we still need to change selection? E.g.,
// we could have already lost focus while we're changing the ancestor
// limiter because it may causes "selectionchange" event.
if (tryToCollapseSelectionAtFirstEditableNode) {
MaybeCollapseSelectionAtFirstEditableNode(true);
}
}
nsresult
HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode(
bool aIgnoreIfSelectionInEditingHost)
{
// XXX Why doesn't this check if the document is alive?
if (!IsInitialized()) {
return NS_ERROR_NOT_INITIALIZED;
}
// Get the selection
RefPtr<Selection> selection = GetSelection();
NS_ENSURE_TRUE(selection, NS_ERROR_NOT_INITIALIZED);
if (NS_WARN_IF(!selection)) {
return NS_ERROR_NOT_INITIALIZED;
}
// Get the root element.
nsCOMPtr<Element> rootElement = GetRoot();
if (!rootElement) {
NS_WARNING("GetRoot() returned a null pointer (mRootElement is null)");
// Use editing host. If you use root element here, selection may be
// moved to <head> element, e.g., if there is a text node in <script>
// element. So, we should use active editing host.
RefPtr<Element> editingHost = GetActiveEditingHost();
if (NS_WARN_IF(!editingHost)) {
return NS_OK;
}
// Find first editable thingy
bool done = false;
nsCOMPtr<nsINode> curNode = rootElement.get(), selNode;
int32_t curOffset = 0, selOffset = 0;
while (!done) {
WSRunObject wsObj(this, curNode, curOffset);
// If selection range is already in the editing host and the range is not
// start of the editing host, we shouldn't reset selection. E.g., window
// is activated when the editor had focus before inactivated.
if (aIgnoreIfSelectionInEditingHost && selection->RangeCount() == 1) {
nsRange* range = selection->GetRangeAt(0);
if (!range->Collapsed() ||
range->GetStartContainer() != editingHost.get() ||
range->StartOffset()) {
return NS_OK;
}
}
// Find first editable and visible node.
EditorRawDOMPoint pointToPutCaret(editingHost, 0);
for (;;) {
WSRunObject wsObj(this, pointToPutCaret.GetContainer(),
pointToPutCaret.Offset());
int32_t visOffset = 0;
WSType visType;
nsCOMPtr<nsINode> visNode;
wsObj.NextVisibleNode(curNode, curOffset, address_of(visNode), &visOffset,
&visType);
if (visType == WSType::normalWS || visType == WSType::text) {
selNode = visNode;
selOffset = visOffset;
done = true;
} else if (visType == WSType::br || visType == WSType::special) {
selNode = visNode->GetParentNode();
selOffset = selNode ? selNode->ComputeIndexOf(visNode) : -1;
done = true;
} else if (visType == WSType::otherBlock) {
// By definition of WSRunObject, a block element terminates a
// whitespace run. That is, although we are calling a method that is
// named "NextVisibleNode", the node returned might not be
// visible/editable!
//
// If the given block does not contain any visible/editable items, we
// want to skip it and continue our search.
wsObj.NextVisibleNode(pointToPutCaret.GetContainer(),
pointToPutCaret.Offset(),
address_of(visNode), &visOffset, &visType);
if (!IsContainer(visNode)) {
// However, we were given a block that is not a container. Since the
// block can not contain anything that's visible, such a block only
// makes sense if it is visible by itself, like a <hr>. We want to
// place the caret in front of that block.
selNode = visNode->GetParentNode();
selOffset = selNode ? selNode->ComputeIndexOf(visNode) : -1;
done = true;
} else {
bool isEmptyBlock;
if (NS_SUCCEEDED(IsEmptyNode(visNode, &isEmptyBlock)) &&
isEmptyBlock) {
// Skip the empty block
curNode = visNode->GetParentNode();
curOffset = curNode ? curNode->ComputeIndexOf(visNode) : -1;
curOffset++;
} else {
curNode = visNode;
curOffset = 0;
}
// Keep looping
}
// If we meet a non-editable node first, we should move caret to start of
// the editing host (perhaps, user may want to insert something before
// the first non-editable node? Chromium behaves so).
if (visNode && !visNode->IsEditable()) {
pointToPutCaret.Set(editingHost, 0);
break;
}
// WSRunObject::NextVisibleNode() returns WSType::special and the "special"
// node when it meets empty inline element. In this case, we should go to
// next sibling. For example, if current editor is:
// <div contenteditable><span></span><b><br></b></div>
// then, we should put caret at the <br> element. So, let's check if
// found node is an empty inline container element.
if (visType == WSType::special && visNode &&
TagCanContainTag(*visNode->NodeInfo()->NameAtom(),
*nsGkAtoms::textTagName)) {
pointToPutCaret.Set(visNode);
DebugOnly<bool> advanced = pointToPutCaret.AdvanceOffset();
NS_WARNING_ASSERTION(advanced,
"Failed to advance offset from found empty inline container element");
continue;
}
// If there is editable and visible text node, move caret at start of it.
if (visType == WSType::normalWS || visType == WSType::text) {
pointToPutCaret.Set(visNode, visOffset);
break;
}
// If there is editable <br> or something inline special element like
// <img>, <input>, etc, move caret before it.
if (visType == WSType::br || visType == WSType::special) {
pointToPutCaret.Set(visNode);
break;
}
// If there is no visible/editable node except another block element in
// current editing host, we should move caret to very first of the editing
// host.
// XXX This may not make sense, but Chromium behaves so. Therefore, the
// reason why we do this is just compatibility with Chromium.
if (visType != WSType::otherBlock) {
pointToPutCaret.Set(editingHost, 0);
break;
}
// By definition of WSRunObject, a block element terminates a whitespace
// run. That is, although we are calling a method that is named
// "NextVisibleNode", the node returned might not be visible/editable!
// However, we were given a block that is not a container. Since the
// block can not contain anything that's visible, such a block only
// makes sense if it is visible by itself, like a <hr>. We want to
// place the caret in front of that block.
if (!IsContainer(visNode)) {
pointToPutCaret.Set(visNode);
break;
}
// If the given block does not contain any visible/editable items, we want
// to skip it and continue our search.
bool isEmptyBlock;
if (NS_SUCCEEDED(IsEmptyNode(visNode, &isEmptyBlock)) && isEmptyBlock) {
// Skip the empty block
pointToPutCaret.Set(visNode);
DebugOnly<bool> advanced = pointToPutCaret.AdvanceOffset();
NS_WARNING_ASSERTION(advanced,
"Failed to advance offset from the found empty block node");
} else {
// Else we found nothing useful
selNode = curNode;
selOffset = curOffset;
done = true;
pointToPutCaret.Set(visNode, 0);
}
}
return selection->Collapse(selNode, selOffset);
return selection->Collapse(pointToPutCaret);
}
nsresult

Просмотреть файл

@ -327,6 +327,10 @@ protected:
using EditorBase::IsBlockNode;
virtual bool IsBlockNode(nsINode *aNode) override;
virtual void
InitializeSelectionAncestorLimit(Selection& aSelection,
nsIContent& aAncestorLimit) override;
public:
// XXX Why don't we move following methods above for grouping by the origins?
NS_IMETHOD SetFlags(uint32_t aFlags) override;
@ -540,6 +544,32 @@ public:
*/
nsresult DoInlineTableEditingAction(const Element& aUIAnonymousElement);
/**
* MaybeCollapseSelectionAtFirstEditableNode() may collapse selection at
* proper position to staring to edit. If there is a non-editable node
* before any editable text nodes or inline elements which can have text
* nodes as their children, collapse selection at start of the editing
* host. If there is an editable text node which is not collapsed, collapses
* selection at the start of the text node. If there is an editable inline
* element which cannot have text nodes as its child, collapses selection at
* before the element node. Otherwise, collapses selection at start of the
* editing host.
*
* @param aIgnoreIfSelectionInEditingHost
* This method does nothing if selection is in the
* editing host except if it's collapsed at start of
* the editing host.
* Note that if selection ranges were outside of
* current selection limiter, selection was collapsed
* at the start of the editing host therefore, if
* you call this with setting this to true, you can
* keep selection ranges if user has already been
* changed.
*/
nsresult
MaybeCollapseSelectionAtFirstEditableNode(
bool aIgnoreIfSelectionInEditingHost);
protected:
class BlobReader final : public nsIEditorBlobListener
{

Просмотреть файл

@ -101,8 +101,8 @@ function runTestsInternal()
var range = selection.getRangeAt(0);
ok(range.collapsed, "the selection range isn't collapsed");
var startNode = range.startContainer;
is(startNode.nodeType, 1, "the caret isn't set to the div node");
is(startNode, editor, "the caret isn't set to the editor");
is(startNode.nodeType, Node.TEXT_NODE, "the caret isn't set to the first text node");
is(startNode, editor.firstChild, "the caret isn't set to the editor");
ok(selCon.caretVisible, "caret isn't visible in the editor");
// Move focus to other editor
otherEditor.focus();
@ -113,8 +113,8 @@ function runTestsInternal()
range = selection.getRangeAt(0);
ok(range.collapsed, "the selection range isn't collapsed");
var startNode = range.startContainer;
is(startNode.nodeType, 1, "the caret isn't set to the div node");
is(startNode, otherEditor, "the caret isn't set to the other editor");
is(startNode.nodeType, Node.TEXT_NODE, "the caret isn't set to the text node");
is(startNode, otherEditor.firstChild, "the caret isn't set to the other editor");
ok(selCon.caretVisible, "caret isn't visible in the other editor");
// Move focus to inputTextInEditor
inputTextInEditor.focus();
@ -124,9 +124,9 @@ function runTestsInternal()
range = selection.getRangeAt(0);
ok(range.collapsed, "the selection range isn't collapsed");
var startNode = range.startContainer;
is(startNode.nodeType, 1, "the caret isn't set to the div node");
is(startNode.nodeType, Node.TEXT_NODE, "the caret isn't set to the first text node");
// XXX maybe, the caret can stay on the other editor if it's better.
is(startNode, editor,
is(startNode, editor.firstChild,
"the caret should stay on the other editor");
ok(selCon.caretVisible,
"caret isn't visible in the inputTextInEditor");

Просмотреть файл

@ -330,7 +330,18 @@ interface nsIEditor : nsISupports
/** sets the document selection to the entire contents of the document */
void selectAll();
/** sets the document selection to the beginning of the document */
/**
* Collapses selection at start of the document. If it's an HTML editor,
* collapses selection at start of current editing host (<body> element if
* it's in designMode) instead. If there is a non-editable node before any
* editable text nodes or inline elements which can have text nodes as their
* children, collapses selection at start of the editing host. If there is
* an editable text node which is not collapsed, collapses selection at
* start of the text node. If there is an editable inline element which
* cannot have text nodes as its child, collapses selection at before the
* element node. Otherwise, collapses selection at start of the editing
* host.
*/
void beginningOfDocument();
/** sets the document selection to the end of the document */

Просмотреть файл

@ -0,0 +1,683 @@
.. role:: html(code)
:language: html
.. role:: js(code)
:language: javascript
=============================
Fluent for Firefox Developers
=============================
This tutorial is intended for Firefox engineers already familiar with the previous
localization systems offered by Gecko - `DTD`_ and `StringBundle`_ - and assumes
prior experience with those systems.
Using Fluent in Gecko
=====================
`Fluent`_ is a modern localization system currently being progressively introduced into
the Gecko platform with a focus on quality, performance, maintenance and completeness.
In order to ensure that Fluent is ready for engineers to work with, the initial
migrations are performed manually with a lot of oversight from the involved
stakeholders.
In this initial phase, `Firefox Preferences`_ is being migrated as the first target
and as a result, the first bindings to be stabilized are for chrome-privileged
XUL context.
From there we plan to focus on two areas:
- `Unprivileged Contexts`_
- `System Add-ons`_
The end goal is replacing all uses of DTD and StringBundle within Firefox's codebase.
If you want to use Fluent and your code involves one of the areas currently unsupported,
we'd like to work with you on getting Fluent ready for your code.
Getting a Review
----------------
If you end up working on any patch which touches FTL files, we have a temporary
hook in place that will reject your patch unless you get an r+ from one of the following
L10n Drivers:
- Francesco Lodolo (:flod)
- Zibi Braniecki (:gandalf)
- Axel Hecht (:pike)
- Stas Malolepszy (:stas)
Major Benefits
==============
Not only was the previous system designed over 20 years ago using file formats
never intended for localization, but also the Web stack which Fluent ties into has
completely changed over the same period, and the domain of internationalization
got a powerful foundation in the form of `Unicode`_, `CLDR`_ and `ICU`_ which Fluent tightly
`interoperates with`__.
__ https://github.com/projectfluent/fluent/wiki/Fluent-and-Standards
While it is beyond the scope of this document to cover all the benefits of Fluent in detail,
below is an attempt to select some most observable changes for each group of consumers.
Developers
----------
- Support for XUL, XHTML, HTML, Web Components, React, JS, Python and Rust
- Strings are available in a single, unified localization context available for both DOM and runtime code
- Full internationalization (i18n) support: date and time formatting, number formatting, plurals, genders etc.
- Strong focus on `declarative API via DOM attributes`__
- Extensible with custom formatters, Mozilla-specific APIs etc.
- `Separation of concerns`__: localization details, and the added complexity of some languages, don't leak onto the source code and are no concern for developers
- Compound messages link a single translation unit to a single UI element
- `DOM Overlays`__ allow for localization of DOM fragments
- Simplified build system model
- No need for pre-processing instructions
__ https://github.com/projectfluent/fluent/wiki/Get-Started
__ https://github.com/projectfluent/fluent/wiki/Design-Principles
__ https://github.com/projectfluent/fluent.js/wiki/DOM-Overlays
Product Quality
------------------
- A robust, multilevel, `error fallback system`__ prevents XML errors and runtime errors
- Simplified l10n API reduces the amount of l10n specific code and resulting bugs
- Runtime localization allows for dynamic language changes and updates over-the-air
- DOM Overlays increase localization security
Many other smaller improvements will be noticed by the users of the system over time
and, with the new foundation, the Fluent team is `currently working`__ on multiple highly
requested features which will further improve the experience of developing
localizable UIs.
__ https://github.com/projectfluent/fluent/wiki/Error-Handling
__ https://github.com/projectfluent/fluent/wiki/Roadmap
Fluent Translation List - FTL
=============================
Fluent introduces a new localization format designed specifically for easy readability
and localization features offered by the system.
At first glance the format resembles `.properties` file. It may look like this:
.. code-block:: properties
home-page-header = Home Page
# The label of a button opening a new tab
new-tab-open = Open New Tab
But the FTL file format is significantly more powerful and the additional features
quickly add up. In order to familiarize yourself with the basic features,
consider reading through the `Fluent Syntax Guide`_ to understand
a more complex example like:
.. code-block:: properties
### These messages correspond to security and privacy user interface.
###
### Please, choose simple and non-threatening language when localizing
### to help user feel in control when interacting with the UI.
## General Section
-brand-short-name = Firefox
.gender = masculine
pref-pane =
.title =
{ PLATFORM() ->
[windows] Options
*[other] Preferences
}
.accesskey = C
# Variables:
# $tabCount (Number) - number of container tabs to be closed
containers-disable-alert-ok-button =
{ $tabCount ->
[one] Close { $tabCount } Container Tab
*[other] Close { $tabCount } Container Tabs
}
update-application-info =
You are using { -brand-short-name } Version: { $version }.
<span>Please, read the <a>privacy policy</a>.</span>
The above, of course, is a particular selection of complex strings intended to exemplify
the new features and concepts introduced by Fluent.
In order to ensure the quality of the output, a lot of new checks and tooling
has been added to the build system.
`Pontoon`_, the main localization tool used to translate Firefox, has been rebuilding
its user experience to support localizers in their work.
Social Contract
===============
Fluent uses the concept of a `social contract` between developer and localizers.
This contract is established by the selection of a unique identifier, called :js:`l10n-id`,
which carries a promise of being used in a particular place to carry a particular meaning.
The use of unique identifiers is not new for Firefox engineers, but it is important
to recognize that Fluent formalizes this relationship.
.. important::
An important part of the contract is that the developer commits to treat the
localization output as `opaque`. That means that no concatenations, replacements
or splitting should happen after the translation is completed to generate the
desired output.
In return, localizers enter the social contract by promising to provide an accurate
and clean translation of the messages that match the request.
In previous localization systems, developers were responsible for differentiating
string variant based on a platform via pre-processing instructions, or
selecting which strings should be formatted using `PluralForms.jsm`.
In Fluent, the developer is not to be bothered with inner logic and complexity that the
localization will use to construct the response. Whether `declensions`__ or other
variant selection techniques are used is up to a localizer and their particular translation.
From the developer perspective, Fluent returns a final string to be presented to
the user, with no l10n logic required in the running code.
__ https://en.wikipedia.org/wiki/Declension
Markup Localization
===================
Fluent fully replaces the use of `DTD`_ in localization.
To localize an element in Fluent, the developer adds a new message to
an FTL file and then has to associate an :js:`l10n-id` with the element
by defining a :js:`data-l10n-id` attribute:
.. code-block:: html
<h1 data-l10n-id="home-page-header" />
<button data-l10n-id="pref-pane" />
Fluent will take care of the rest, populating the element with the message value
in its content and all localizable attributes if defined.
The difference compared to the use of DTD is that the developer provides only a single
message to localize the whole element, rather than a separate entity for
the value and each of the attributes.
The other change is that the developer can localize a whole fragment of DOM:
.. code-block:: html
<p data-l10n-id="update-application-info" data-l10n-args="{'version': '60.0'}">
<span class="bold">
<a href="http://www.mozilla.org/privacy" />
</span>
</p>
.. code-block:: properties
-brand-short-name = Firefox
update-application-info =
You are using { -brand-short-name } Version: { $version }.
<span>Please, read the <a>privacy policy</a>.</span>
Fluent will overlay the translation onto the source fragment preserving attributes like
:code:`class` and :code:`href` from the source and adding translations for the elements
inside. The end result will look like this:
.. code-block:: html
<p data-l10n-id="update-application-info" data-l10n-args="{'version': '60.0'}">
You are using Firefox Version: 60.0.
<span class="bold">
Please, read the <a href="http://www.mozilla.org/privacy">privacy policy</a>.
</span>
</p>
This operation is sanitized, and Fluent takes care of selecting which elements and
attributes can be safely provided by the localization.
The list of allowed elements and attributes is `maintained by the W3C`__, and if
the developer needs to allow for localization of additional attributes, they can
whitelist them using :code:`data-l10n-attrs` list:
.. code-block:: html
<label data-l10n-id="search-input" data-l10n-attrs="style" />
The above example adds an attribute :code:`style` to be allowed on this
particular :code:`label` element.
External Arguments
------------------
Notice in the previous example the attribute :code:`data-l10n-args`, which is
a JSON object storing variables exposed by the developer to the localizer.
This is the main channel for the developer to provide additional variables
to be used in the localization.
It is very rare that the arguments are needed for localizations which previously
used DTD, because such variables will usually have to be computed from the runtime code,
but it is worth understanding that when the :code:`l10n-args` are set in
the runtime code, they are in fact encoded via JSON and stored together with
:code:`l10n-id` as an attribute on the element.
__ https://www.w3.org/TR/2011/WD-html5-20110525/text-level-semantics.html
Runtime Localization
====================
Fluent fully replaces the use of `StringBundle`_ in localization.
In almost every case the JS runtime code will operate on a particular document, either
XUL, XHTML or HTML.
If the document has its markup already localized, then Fluent exposes a new
attribute on the :js:`document` element - :js:`document.l10n`.
This property is an object of type :js:`DOMLocalization` which maintains the main
localization context for this document and exposes it to runtime code as well.
With a focus on `declarative localization`__, the primary method of localization is
to alter the localization attributes in the DOM. Fluent provides a method to facilitate this:
.. code-block:: javascript
document.l10n.setAttributes(element, "new-panel-header");
This will set the :code:`data-l10n-id` on the element and translate it before the next
animation frame.
The reason to use this API over manually setting the attribute is that it also
facilitates encoding l10n arguments as JSON:
.. code-block:: javascript
document.l10n.setAttributes(element "containers-disable-alert-ok-button", {
tabCount: 5
}
__ https://github.com/projectfluent/fluent/wiki/Good-Practices-for-Developers
Non-Markup Localization
-----------------------
In rare cases, when the runtime code needs to retrieve the translation and not
apply it onto the DOM, Fluent provides an API to retrieve it:
.. code-block:: javascript
let [ msg ] = await document.l10n.formatValues([
["remove-containers-description"]
]);
alert(msg);
This model is heavily discouraged and should be used only in cases where the
DOM annotation is not possible.
.. note::
This API is currently only available as asynchronous. In case of Firefox,
the only non-DOM localizable calls are used where the output goes to
a third-party like Bluetooth, Notifications etc.
All those cases should already be asynchronous.
Internationalization
====================
The majority of internationalization issues are implicitly handled by Fluent without
any additional requirement. Full Unicode support, `bidirectionality`__, and
correct number formatting work without any action required from either
developer or localizer.
__ https://github.com/projectfluent/fluent/wiki/BiDi-in-Fluent
.. code-block:: javascript
document.l10n.setAttributes(element, "welcome-message", {
userName: "اليسع",
count: 5
});
A message like this localized to American English will correctly wrap the user
name in directionality marks allowing the layout engine to determine how to
display the bidirectional text.
On the other hand, the same message localized to Arabic will use the Eastern Arabic
numeral for number "5".
Plural Rules
------------
The most common localization feature is the ability to provide different variants
of the same string depending on plural categories.
Fluent replaces the use of the proprietary :code:`PluralForms.jsm` with a Unicode CLDR
standard called `Plural Rules`_.
In order to allow localizers to use it, all the developer has to do is to pass
an external argument number:
.. code-block:: javascript
document.l10n.setAttributes(element, "unread-warning", { unreadCount: 5 });
Localizers can use the argument to build a multi variant message if their
language requires that:
.. code-block:: properties
unread-warning =
{ $unreadCount ->
[one] You have { $unreadCount } unread message
*[other] You have { $unreadCount } unread messages
}
Fluent guesses that since the variant selection is performed based on a number,
its `plural category`__ should be retrieved.
If the given translation doesn't need pluralization for the string (for example
Japanese often will not), the localizer can replace it with:
.. code-block:: properties
unread-warning = You have { $unreadCount } unread messages
and the message will preserve the social contract.
One additional feature is that the localizer can further improve the message by
specifying variants for particular values:
.. code-block:: properties
unread-warning =
{ $unreadCount ->
[0] You have no unread messages
[1] You have one unread message
*[other] You have { $unreadCount } unread messages
}
The advantage here is that per-locale choices don't leak onto the source code
and the developer is not affected.
.. note::
There is an important distinction between a variant keyed on plural category
`one` and digit `1`. Although in English the two are synonymous, in other
languages category `one` may be used for other numbers.
For example in `Bosnian`__, category `one` is used for numbers like `1`, `21`, `31`
and so on, and also for fractional numbers like `0.1`.
__ https://unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html
__ https://unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#bs
Partial Arguments
-----------------
When it comes to formatting data, Fluent allows the developer to provide
a set of parameters for the formatter, and the localizer can fine tune some of them.
This technique is called `partial arguments`__.
For example, when formatting a date, the developer can just pass a JS :js:`Date` object,
but its default formatting will be pretty expressive. In most cases, the developer
may want to use some of the :js:`Intl.DateTimeFormat` options to select the default
representation of the date in string:
.. code-block:: javascript
document.l10n.setAttributes(element, "welcome-message", {
startDate: FluentDateTime(new Date(), {
year: "numeric",
month: "long",
day: "numeric"
})
});
.. code-block:: properties
welcome-message = Your session will start date: { $startDate }
In most cases, that will be enough and the date would get formatted in the current
Firefox as `February 28, 2018`.
But if in some other locale the string would get too long, the localizer can fine
tune the options as well:
.. code-block:: properties
welcome-message = Początek Twojej sesji: { DATETIME($startDate, month="short") }
This will adjust the length of the month token in the message to short and get formatted
in Polish as `28 lut 2018`.
At the moment Fluent supports two formatters that match JS Intl API counterparts:
* **NUMBER**: `Intl.NumberFormat`__
* **DATETIME**: `Intl.DateTimeFormat`__
With time more formatters will be added.
__ http://projectfluent.org/fluent/guide/functions.html#partial-arguments
__ https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat
__ https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
Registering New L10n Files
==========================
In the previous system, a new localization file had to be registered in order to
add it in the `jar.mn` file for packaging.
Fluent uses a wildcard statement packaging all localization resources into
their component's `/localization/` directory.
That means that, if a new file is added to a component of Firefox already
covered by Fluent like `browser`, it's enough to add the new file to the
repository in a path like `browser/locales/en-US/browser/component/file.ftl` and
the toolchain will package it into `browser/localization/browser/component/file.ftl`.
At runtime Firefox uses a special registry for all localization data. It will
register the browser's `/localization/` directory and make all files inside it
available to be references.
To make the document localized using Fluent, all the developer has to do is add
a single polyfill for the Fluent API to the source and list the resources
that will be used:
.. code-block:: html
<link rel="localization" href="branding/brand.ftl"/>
<link rel="localization" href="browser/preferences/preferences.ftl"/>
<script src="chrome://global/content/l10n.js"></script>
For performance reasons the :html:`<link/>` elements have to be specified above the
:html:`<script/>` and the :html:`<script/>` itself has to be synchronous in order to ensure
that the localization happens before first paint.
This allows Fluent to trigger asynchronous resource loading early enough to
perform the initial DOM translation before the initial layout.
The URI provided to the :html:`<link/>` element are relative paths within the localization
system.
Notice that only the registration of the script is synchronous. All the I/O and
translation happen asynchronously.
Custom Contexts
===============
The above method creates a single localization context per document.
In almost all scenarios that's sufficient.
In rare edge cases where the developer needs to fetch additional resources, or
the same resources in another language, it is possible to create additional
contexts manually using `Localization` class:
.. code-block:: javascript
const { Localization } =
ChromeUtils.import("resource://gre/modules/Localization.jsm", {});
const myL10n = new Localization([
"branding/brand.ftl",
"browser/preferences/preferences.ftl"
]);
let [isDefaultMsg, isNotDefaultMsg] =
myL10n.formatValues(["is-default", "is-not-default"]);
.. admonition:: Example
An example of a use case is the Preferences UI in Firefox which uses the
main context to localize the UI but also to build a search index.
It is common to build such search index both in a current langauge and additionally
in English since a lot of documentation and online help exists in that language.
A developer may create manually a new context with the same resources as the main one
uses, but hardcode it to `en-US` and then build the search index using both contexts.
Designing Localizable APIs
==========================
When designing localizable APIs, the most important rule is to resolve localization as
late as possible. That means that instead of resolving strings somewhere deep in the
codebase and then passing them on or even caching, it is highly recommended to pass
around :code:`l10n-id` or :code:`[l10n-id, l10n-args]` pairs until the top-most code
resolves them or applies them onto the DOM element.
Testing
=======
When writing tests that involve both I18n and L10n, the general rule is that
result strings are opaque. That means that the developer should not assume any particular
value and should never test against it.
In case of raw i18n the :js:`resolvedOptions` method on all :js:`Intl.*` formatters
makes it relatively easy. In case of localization, the recommended way is to test that
the code sets the right :code:`l10n-id`/:code:`l10n-args` attributes like this:
.. code-block:: javascript
testedFunction();
const l10nAttrs = document.l10n.getAttributes(element);
deepEquals(l10nAttrs, {
id: "my-expected-id",
args: {
unreadCount: 5
}
});
If the code really has to test for particular values in the localized UI, it is
always better to scan for a variable:
.. code-block:: javascript
testedFunction();
equals(element.textContent.contains("John"));
.. important::
Testing against whole values is brittle and will break when we insert Unicode
bidirectionality marks into the result string or adapt the output in other ways.
Inner Structure of Fluent
=========================
The inner structure of Fluent in Gecko is out of scope of this tutorial, but
since the class and file names may show up during debugging or profiling,
below is a list of major components, each with a corresponding file in `/intl/l10n`
modules in Gecko.
MessageContext
--------------
MessageContext is the lowest level API. It's fully synchronous, contains a parser for the
FTL file format and a resolver for the logic. It is not meant to be used by
consumers directly.
In the future we intend to offer this layer for standardization and it may become
part of the :js:`mozIntl.*` or even :js:`Intl.*` API sets.
That part of the codebase is also the first that we'll be looking to port to Rust.
Localization
------------
Localization is a higher level API which uses :js:`MessageContext` internally but
provides a full layer of compound message formatting and robust error fall-backing.
It is intended for use in runtime code and contains all fundamental localization
methods.
DOMLocalization
---------------
DOMLocalization extends :js:`Localization` with functionality to operate on HTML, XUL
and the DOM directly including DOM Overlays and Mutation Observers.
l10n.js
-------
l10n.js is a small runtime code which fetches the :html:`<link>` elements specified
in the document and initializes the main :js:`DOMLocalization` context
on :js:`document.l10n`.
L10nRegistry
------------
L10nRegistry is our resource management service. It replaces :js:`ChromeRegistry` and
maintains the state of resources packaged into the build and language packs,
providing an asynchronous iterator of :js:`MessageContext` objects for a given locale set
and resources that the :js:`Localization` class uses.
.. _Fluent: http://projectfluent.org/
.. _DTD: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/Tutorial/Localization
.. _StringBundle: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/Tutorial/Property_Files
.. _Firefox Preferences: https://bugzilla.mozilla.org/show_bug.cgi?id=1415730
.. _Unprivileged Contexts: https://bugzilla.mozilla.org/show_bug.cgi?id=1407418
.. _System Add-ons: https://bugzilla.mozilla.org/show_bug.cgi?id=1425104
.. _CLDR: http://cldr.unicode.org/
.. _ICU: http://site.icu-project.org/
.. _Unicode: https://www.unicode.org/
.. _Fluent Syntax Guide: http://projectfluent.org/fluent/guide/
.. _Pontoon: https://pontoon.mozilla.org/
.. _Plural Rules: http://cldr.unicode.org/index/cldr-spec/plural-rules

23
intl/l10n/docs/index.rst Normal file
Просмотреть файл

@ -0,0 +1,23 @@
======
Fluent
======
`Fluent`_ is a new localization system, developed by Mozilla, which aims to replace
all existing localization models currently used at Mozilla.
In case of Firefox it directly superseeds DTD and StringBundle systems providing
a large number of new features and improvements over them both, for developers
and localizers.
.. toctree::
:maxdepth: 2
fluent_tutorial
Other resources:
* `Fluent Syntax Guide <http://projectfluent.org/fluent/guide/>`_
* `Fluent Wiki <https://github.com/projectfluent/fluent/wiki>`_
* `Fluent.js Wiki <https://github.com/projectfluent/fluent.js/wiki>`_
.. _Fluent: http://projectfluent.org/

Просмотреть файл

@ -17,4 +17,6 @@ MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini']
JAR_MANIFESTS += ['jar.mn']
SPHINX_TREES['l10n'] = 'docs'
FINAL_LIBRARY = 'xul'

12
servo/Cargo.lock сгенерированный
Просмотреть файл

@ -215,10 +215,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "blurmac"
version = "0.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
version = "0.1.0"
source = "git+https://github.com/servo/devices#1069d67cbacb28b77a3d5dd7f211171c05f32c62"
dependencies = [
"log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"objc 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -693,10 +693,10 @@ dependencies = [
[[package]]
name = "device"
version = "0.0.1"
source = "git+https://github.com/servo/devices#c3b012b0ac4fbc47d1ebc9bd3fc308f599be4ee4"
source = "git+https://github.com/servo/devices#1069d67cbacb28b77a3d5dd7f211171c05f32c62"
dependencies = [
"blurdroid 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"blurmac 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"blurmac 0.1.0 (git+https://github.com/servo/devices)",
"blurmock 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"blurz 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -3660,7 +3660,7 @@ dependencies = [
"checksum bitreader 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "80b13e2ab064ff3aa0bdbf1eff533f9822dc37899821f5f98c67f263eab51707"
"checksum block 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
"checksum blurdroid 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d7daba519d29beebfc7d302795af88a16b43f431b9b268586926ac61cc655a68"
"checksum blurmac 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "72af3718b3f652fb2026bf9d9dd5f92332cd287884283c343f03fff16cbb0172"
"checksum blurmac 0.1.0 (git+https://github.com/servo/devices)" = "<none>"
"checksum blurmock 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "68dd72da3a3bb40f3d3bdd366c4cf8e2b1d208c366304f382c80cef8126ca8da"
"checksum blurz 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e73bda0f4c71c63a047351070097f3f507e6718e86b9ee525173371ef7b94b73"
"checksum brotli 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "fe87b40996b84fdc56e57c165d93079f4b50cb806598118e692ddfaa3d3c57c0"

Просмотреть файл

@ -63,6 +63,9 @@ with Files("mozilla/meta/**"):
with Files("mozilla/tests/dom/**"):
BUG_COMPONENT = ("Core", "DOM")
with Files("mozilla/tests/editor/**"):
BUG_COMPONENT = ("Core", "Editor")
with Files("mozilla/tests/fetch/**"):
BUG_COMPONENT = ("Core", "DOM")

Просмотреть файл

@ -469,6 +469,12 @@
{}
]
],
"editor/initial_selection_on_focus.html": [
[
"/_mozilla/editor/initial_selection_on_focus.html",
{}
]
],
"fetch/api/redirect/redirect-referrer.https.html": [
[
"/_mozilla/fetch/api/redirect/redirect-referrer.https.html",
@ -1022,6 +1028,10 @@
"67a981ba2a4d08b684947ed42aba6648dcd262b4",
"testharness"
],
"editor/initial_selection_on_focus.html": [
"da3d0ff5305658e18f51a4f19b34927fb2691e60",
"testharness"
],
"fetch/api/redirect/redirect-referrer-mixed-content.js": [
"f9d7ec9cf9fa8c847e45664b05482e3f8c191385",
"support"

Просмотреть файл

@ -0,0 +1,360 @@
<!doctype html>
<meta charset=utf-8>
<title>initial selection on focus of contenteditable</title>
<!-- if you move this file into cross-browser's directly, you should include
editing/include/tests.js for using addBrackets() and get rid of it from
this file. -->
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<body>
<p id="staticText">out of editor</p>
<div id="editor" contenteditable style="min-height: 1em;"></div>
<script>
"use strict";
(function() {
var tests = [
{ description: "empty editor should set focus to start of it",
content: "{}",
},
{ description: "editor should set selection to start of the text node",
content: "[]abc",
},
{ description: "editor should set selection to before the <br> node",
content: "{}<br>",
},
{ description: "editor should set selection to before the first <br> node",
content: "{}<br><br>",
},
{ description: "editor should set selection to start of the text node in the <p> node",
content: "<p>[]abc</p>",
},
{ description: "editor should set selection to before the <br> node in the <p> node",
content: "<p>{}<br></p>",
},
{ description: "editor should set selection to before the first <br> node in the <p> node",
content: "<p>{}<br><br></p>",
},
{ description: "editor should set selection to start of the text node in the <span> node",
content: "<span>[]abc</span>",
},
{ description: "editor should set selection to before the <br> node in the <span> node",
content: "<span>{}<br></span>",
},
{ description: "editor should set selection to before the first <br> node in the <span> node",
content: "<span>{}<br><br></span>",
},
{ description: "editor should set selection to before the empty <span> node",
content: "{}<span></span>",
},
{ description: "editor should set selection to before the empty <b> node",
content: "{}<b></b>",
},
{ description: "editor should set selection to before the empty <i> node",
content: "{}<i></i>",
},
{ description: "editor should set selection to before the empty <u> node",
content: "{}<u></u>",
},
{ description: "editor should set selection to before the empty <s> node",
content: "{}<s></s>",
},
{ description: "editor should set selection to before the empty <code> node",
content: "{}<code></code>",
},
{ description: "editor should set selection to before the empty <a> node",
content: "{}<a href=\"foo.html\"></a>",
},
{ description: "editor should set selection to before the empty <foobar> node",
content: "{}<foobar></foobar>",
},
{ description: "editor should set selection to before the <input> node",
content: "{}<input>",
},
{ description: "editor should set selection to before the <img> node",
content: "{}<img alt=\"foo\">",
},
{ description: "editor should set selection to start of the text node in the second <span> node",
content: "<span></span><span>[]abc</span>",
},
{ description: "editor should set selection to before the <br> node in the second <span> node",
content: "<span></span><span>{}<br></span>",
},
{ description: "editor should set selection to start of the text node in the first <span> node #1",
content: "<span>[]abc</span><span>abc</span>",
},
{ description: "editor should set selection to start of the text node in the first <span> node #2",
content: "<span>[]abc</span><span><br></span>",
},
{ description: "editor should set selection to before the <br> node in the first <span> node #1",
content: "<span>{}<br></span><span><br></span>",
},
{ description: "editor should set selection to before the <br> node in the first <span> node #2",
content: "<span>{}<br></span><span>abc</span>",
},
{ description: "editor should set selection to start of the text node in the second <span> node since the text node in the first <span> node is only whitespaces",
content: "<span> </span><span>[]abc</span>",
},
{ description: "editor should set selection to before the <br> node in the second <span> node since the text node in the first <span> node is only whitespaces",
content: "<span> </span><span>{}<br></span>",
},
{ description: "editor should set selection to start of the text node in the second <span> node even if there is a whitespace only text node before the first <span> node",
content: " <span></span><span>[]abc</span>",
},
{ description: "editor should set selection to before the <br> node in the second <span> node even if there is a whitespace only text node before the first <span> node",
content: " <span></span><span>{}<br></span>",
},
{ description: "editor should set selection to start of the text node in the second <p> node",
content: "<p></p><p>[]abc</p>",
},
{ description: "editor should set selection to before the <br> node in the second <p> node",
content: "<p></p><p>{}<br></p>",
},
{ description: "editor should set selection to start of the text node in the first <p> node #1",
content: "<p>[]abc</p><p>abc</p>",
},
{ description: "editor should set selection to start of the text node in the first <p> node #2",
content: "<p>[]abc</p><p><br></p>",
},
{ description: "editor should set selection to before the <br> node in the first <p> node #1",
content: "<p>{}<br></p><p><br></p>",
},
{ description: "editor should set selection to before the <br> node in the first <p> node #2",
content: "<p>{}<br></p><p>abc</p>",
},
{ description: "editor should set selection to start of the text node in the second <p> node since the text node in the first <p> node is only whitespaces",
content: "<p> </p><p>[]abc</p>",
},
{ description: "editor should set selection to before the <br> node in the second <p> node since the text node in the first <p> node is only whitespaces",
content: "<p> </p><p>{}<br></p>",
},
{ description: "editor should set selection to start of the text node in the second <p> node even if there is a whitespace only text node before the first <p> node",
content: " <p></p><p>[]abc</p>",
},
{ description: "editor should set selection to before the <br> node in the second <p> node even if there is a whitespace only text node before the first <p> node",
content: " <p></p><p>{}<br></p>",
},
{ description: "editor should set selection to start of the text node in the <span> node in the second <p> node",
content: "<p><span></span></p><p><span>[]abc</span></p>",
},
{ description: "editor should set selection to before the <br> node in the <span> node in the second <p> node",
content: "<p><span></span></p><p><span>{}<br></span></p>",
},
{ description: "editor should set selection to start of the text node in the <span> node in the first <p> node #1",
content: "<p><span>[]abc</span></p><p><span>abc</span></p>",
},
{ description: "editor should set selection to start of the text node in the <span> node in the first <p> node #2",
content: "<p><span>[]abc</span></p><p><span><br></span></p>",
},
{ description: "editor should set selection to before the <br> node in the <span> node in the first <p> node #1",
content: "<p><span>{}<br></span></p><p><span><br></span></p>",
},
{ description: "editor should set selection to before the <br> node in the <span> node in the first <p> node #2",
content: "<p><span>{}<br></span></p><p><span>abc</span></p>",
},
{ description: "editor should set focus to before the non-editable <span> node",
content: "{}<span contenteditable=\"false\"></span>",
},
{ description: "editor should set focus to before the non-editable <span> node even if it has a text node",
content: "{}<span contenteditable=\"false\">abc</span>",
},
{ description: "editor should set focus to before the non-editable <span> node even if it has a <br> node",
content: "{}<span contenteditable=\"false\"><br></span>",
},
{ description: "editor should set focus to before the non-editable empty <span> node followed by a text node",
content: "{}<span contenteditable=\"false\"></span><span>abc</span>",
},
{ description: "editor should set focus to before the non-editable <span> node having a text node and followed by another text node",
content: "{}<span contenteditable=\"false\">abc</span><span>def</span>",
},
{ description: "editor should set focus to before the non-editable <span> node having a <br> node and followed by a text node",
content: "{}<span contenteditable=\"false\"><br></span><span>abc</span>",
},
{ description: "editor should set focus to before the non-editable empty <span> node followed by a <br> node",
content: "{}<span contenteditable=\"false\"></span><span><br></span>",
},
{ description: "editor should set focus to before the non-editable <span> node having text node and followed by a <br> node",
content: "{}<span contenteditable=\"false\">abc</span><span><br></span>",
},
{ description: "editor should set focus to before the non-editable <span> node having a <br> node and followed by another <br> node",
content: "{}<span contenteditable=\"false\"><br></span><span><br></span>",
},
{ description: "editor should set focus to before the non-editable empty <p> node followed by a text node",
content: "{}<p contenteditable=\"false\"></p><p>abc</p>",
},
{ description: "editor should set focus to before the non-editable <p> node having a text node and followed by another text node",
content: "{}<p contenteditable=\"false\">abc</p><p>def</p>",
},
{ description: "editor should set focus to before the non-editable <p> node having a <br> node and followed by a text node",
content: "{}<p contenteditable=\"false\"><br></p><p>abc</p>",
},
{ description: "editor should set focus to before the non-editable empty <p> node followed by a <br> node",
content: "{}<p contenteditable=\"false\"></p><p><br></p>",
},
{ description: "editor should set focus to before the non-editable <p> node having text node and followed by a <br> node",
content: "{}<p contenteditable=\"false\">abc</p><p><br></p>",
},
{ description: "editor should set focus to before the non-editable <p> node having a <br> node and followed by another <br> node",
content: "{}<p contenteditable=\"false\"><br></p><p><br></p>",
},
{ description: "editor should set focus to start of it if there is non-editable node before first editable text node",
content: "{}<span></span><span contenteditable=\"false\"></span><span>abc</span>",
},
{ description: "editor should set focus to start of it if there is non-editable node having a text node before first editable text node",
content: "{}<span></span><span contenteditable=\"false\">abc</span><span>def</span>",
},
{ description: "editor should set focus to start of it if there is non-editable node having a <br> node before first editable text node",
content: "{}<span></span><span contenteditable=\"false\"><br></span><span>abc</span>",
},
{ description: "editor should set focus to start of it if there is non-editable node before first editable <br> node",
content: "{}<span></span><span contenteditable=\"false\"></span><span><br></span>",
},
{ description: "editor should set focus to start of it if there is non-editable node having a text node before first editable <br> node",
content: "{}<span></span><span contenteditable=\"false\">abc</span><span><br></span>",
},
{ description: "editor should set focus to start of it if there is non-editable node having a <br> node before first editable <br> node",
content: "{}<span></span><span contenteditable=\"false\"><br></span><span><br></span>",
},
{ description: "editor should set focus to the first editable text node in the first <span> node even if followed by a non-editable node",
content: "<span>[]abc</span><span contenteditable=\"false\"></span>",
},
{ description: "editor should set focus to the first editable text node in the first <span> node even if followed by a non-editable node having another text node",
content: "<span>[]abc</span><span contenteditable=\"false\">def</span>",
},
{ description: "editor should set focus to the first editable text node in the first <span> node even if followed by a non-editable node having a <br> node",
content: "<span>[]abc</span><span contenteditable=\"false\"><br></span>",
},
{ description: "editor should set focus to the first editable <br> node in the first <span> node even if followed by a non-editable node",
content: "<span>{}<br></span><span contenteditable=\"false\"></span>",
},
{ description: "editor should set focus to the first editable <br> node in the first <span> node even if followed by a non-editable node having a text node",
content: "<span>{}<br></span><span contenteditable=\"false\">abc</span>",
},
{ description: "editor should set focus to the first editable <br> node in the first <span> node even if followed by a non-editable node having a <br> node",
content: "<span>{}<br></span><span contenteditable=\"false\"><br></span>",
},
{ description: "editor should set focus to the first editable text node in the first <p> node even if followed by a non-editable node",
content: "<p>[]abc</p><p contenteditable=\"false\"></p>",
},
{ description: "editor should set focus to the first editable text node in the first <p> node even if followed by a non-editable node having another text node",
content: "<p>[]abc</p><p contenteditable=\"false\">def</p>",
},
{ description: "editor should set focus to the first editable text node in the first <p> node even if followed by a non-editable node having a <br> node",
content: "<p>[]abc</p><p contenteditable=\"false\"><br></p>",
},
{ description: "editor should set focus to the first editable <br> node in the first <p> node even if followed by a non-editable node",
content: "<p>{}<br></p><p contenteditable=\"false\"></p>",
},
{ description: "editor should set focus to the first editable <br> node in the first <p> node even if followed by a non-editable node having a text node",
content: "<p>{}<br></p><p contenteditable=\"false\">abc</p>",
},
{ description: "editor should set focus to the first editable <br> node in the first <p> node even if followed by a non-editable node having a <br> node",
content: "<p>{}<br></p><p contenteditable=\"false\"><br></p>",
},
];
// This function is copied from editing/include/tests.js
function addBrackets(range) {
// Handle the collapsed case specially, to avoid confusingly getting the
// markers backwards in some cases
if (range.startContainer.nodeType == Node.TEXT_NODE ||
range.startContainer.nodeType == Node.COMMENT_NODE) {
if (range.collapsed) {
range.startContainer.insertData(range.startOffset, "[]");
} else {
range.startContainer.insertData(range.startOffset, "[");
}
} else {
var marker = range.collapsed ? "{}" : "{";
if (range.startOffset != range.startContainer.childNodes.length &&
range.startContainer.childNodes[range.startOffset].nodeType == Node.TEXT_NODE) {
range.startContainer.childNodes[range.startOffset].insertData(0, marker);
} else if (range.startOffset != 0 &&
range.startContainer.childNodes[range.startOffset - 1].nodeType == Node.TEXT_NODE) {
range.startContainer.childNodes[range.startOffset - 1].appendData(marker);
} else {
// Seems to serialize as I'd want even for tables . . . IE doesn't
// allow undefined to be passed as the second argument (it throws
// an exception), so we have to explicitly check the number of
// children and pass null.
range.startContainer.insertBefore(document.createTextNode(marker),
range.startContainer.childNodes.length == range.startOffset ?
null : range.startContainer.childNodes[range.startOffset]);
}
}
if (range.collapsed) {
return;
}
if (range.endContainer.nodeType == Node.TEXT_NODE ||
range.endContainer.nodeType == Node.COMMENT_NODE) {
range.endContainer.insertData(range.endOffset, "]");
} else {
if (range.endOffset != range.endContainer.childNodes.length &&
range.endContainer.childNodes[range.endOffset].nodeType == Node.TEXT_NODE) {
range.endContainer.childNodes[range.endOffset].insertData(0, "}");
} else if (range.endOffset != 0 &&
range.endContainer.childNodes[range.endOffset - 1].nodeType == Node.TEXT_NODE) {
range.endContainer.childNodes[range.endOffset - 1].appendData("}");
} else {
range.endContainer.insertBefore(document.createTextNode("}"),
range.endContainer.childNodes.length == range.endOffset ?
null : range.endContainer.childNodes[range.endOffset]);
}
}
}
var editor = document.getElementById("editor");
var textInP = document.getElementById("staticText").firstChild;
var selection = document.getSelection();
for (var i = 0; i < tests.length; i++) {
test(function() {
// Select outside the editor.
editor.blur();
selection.collapse(textInP);
// Initialize the editor content.
editor.innerHTML = tests[i].content.replace(/[{}\[\]]/g, "");
// Make the editor focused.
editor.focus();
assert_equals(selection.rangeCount, 1);
if (selection.rangeCount) {
addBrackets(selection.getRangeAt(0));
assert_equals(editor.innerHTML, tests[i].content);
}
}, tests[i].description);
}
test(function() {
// Check if selection is initialized after temporarily blurred.
editor.innerHTML = "<p>abc</p><p>def</p>";
editor.focus();
// Move selection to the second paragraph.
selection.collapse(editor.firstChild.nextSibling.firstChild);
// Reset focus.
editor.blur();
editor.focus();
// Then, selection should still be in the second paragraph.
assert_equals(selection.rangeCount, 1);
if (selection.rangeCount) {
addBrackets(selection.getRangeAt(0));
assert_equals(editor.innerHTML, "<p>abc</p><p>[]def</p>");
}
}, "editor shouldn't reset selection when it gets focus again");
})();
</script>