nsITextInputProcessor

This article needs a technical review. How you can help.

This interface is a text input events synthesizer and manages its composition and modifier state
1.0
50
Introduced
Gecko 38
Inherits from: nsISupports Last changed in Gecko 38.0 (Firefox 38.0 / Thunderbird 38.0 / SeaMonkey 2.35)

The motivation of this interface is to provide better API than nsIDOMWindowUtils to dispatch key events and create, modify, and commit composition in higher level. nsIDOMWindowUtils has provided the methods which dispatched keyboard events and composition events almost directly. Therefore they sometimes caused impossible scenarios in automated tests (what's tested with such events?) and JS-IME and/or JS-Keyboard on Firefox OS or add-ons may dispatch events with wrong rules. For solving that issue, methods of this interface have been designed for performing a key operation or representing a change of composition state.

For example, the implementation of this interface manages modifier state and composition state, initializes DOM events from minimum information, and doesn't dispatch some events if they are not necessary. This means that even when Gecko changes the DOM event behavior, it may not be necessary that the users of this interface need to be updated, i.e., the implementation of this class can be a cushion from the impact of Gecko's change.

You can create an instance of this interface with the following code:

var TIP = Components.classes["@mozilla.org/text-input-processor;1"].
            createInstance(Components.interfaces.nsITextInputProcessor);

Next, you need to get the rights to create composition or dispatch keyboard events with beginInputTransaction() or beginInputTransactionForTests():

if (!TIP.beginInputTransaction(window, callback)) {
  return;
}

If beginInputTransaction() or beginInputTransactionForTests() returns false, it means that another instance of nsITextInputProcessor has composition on the window or is dispatching an event. In this case, other instances cannot create composition nor dispatch keyboard events on the window.

The second argument, callback, should be an object which implements nsITextInputProcessorCallback or just a function which is the same as nsITextInputProcessorCallback.onNotify(). See the document of nsITextInputProcessorCallback for the detail.

Next, you can set the composition string or dispatch keyboard events.

The following example sets "foo-bar-buzz", "bar" is selected clause to convert, and caret position is the end of the selected clause:

// First, sets composition string.
TIP.setPendingCompositionString("foo-bar-buzz");
// Next, append clauses.
TIP.appendClauseToPendingComposition("foo-".length,  TIP.ATTR_CONVERTED_CLAUSE);
TIP.appendClauseToPendingComposition("bar".length,   TIP.ATTR_SELECTED_CLAUSE);
TIP.appendClauseToPendingComposition("-buzz".length, TIP.ATTR_CONVERTED_CLAUSE);
// Then, sets the caret if you need
TIP.setCaretInPendingComposition("foo-bar".length);
// Finally, flush the pending composition on the focused editor
if (!TIP.flushPendingComposition()) {
  return; // Composition is not started
}

If there is no composition, flushPendingComposition() starts composition with dispatching compositionstart event automatically. However, if the event is consumed by web content, it returns false. In this case, composition isn't created. So, JS-IME shouldn't continue to compose the string.

Finally, when you commit the composition with the last composition string, just do this:

TIP.commitComposition();

When you commit composition with specific string, specify commit string with its argument:

TIP.commitCompositionWith("foo-BAR-buzz");

When you cancel composition, just do this:

TIP.cancelComposition();

When you dispatch keydown event (and one or more keypress events), just do this:

var keyEvent =
  new KeyboardEvent("",                        // type attribute value should be empty.
    { key: "Enter",                            // Required.
      code: "Enter",                           // Optional.
      keyCode: KeyboardEvent.DOM_VK_RETURN,    // Required if printable key, but optional if non-printable key.
      location: KeyboardEvent.DOM_VK_STANDARD, // Optional, may be computed from code attribute value.
      repeat: false,                           // Optional.
    });                                        // The other attributes are always ignored.
var doDefault = TIP.keydown(keyEvent);

When you dispatch keyup event, just do this:

var keyEvent =
  new KeyboardEvent("", { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A });
var doDefault = TIP.keyup(keyEvent);

startComposition(), flushPendingComposition(), commitComposition(), commitCompositionWith(), and cancelComposition() can take a KeyboardEvent which causes modifying the composition state.  Specifying a keyboard event is strongly recommended if the TextInputProcessor emulates input from keyboard. For example:

TIP.setPendingCompositionString("a");
TIP.appendClauseToPendingComposition("a".length, TIP.ATTR_RAW_CLAUSE);
TIP.setCaretPosition("a".length);

// This means that the first composition character is inputted by a keypress of "a" key.
var AKeyEvent =
  new KeyboardEvent("", { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A });
TIP.flushPendingComposition(AKeyEvent);

// Pressing shift key for next input.
var shiftKeyEvent =
  new KeyboardEvent("", { key: "Shift", code: "ShiftLeft", keyCode: KeyboardEvent.DOM_VK_SHIFT });
TIP.keydown(shiftKeyEvent);

TIP.setPendingCompositionString("aB");
TIP.appendClauseToPendingComposition("aB".length, TIP.ATTR_RAW_CLAUSE);
TIP.setCaretPosition("aB".length);

// This means that the second composition character is inputted by a keypress of "b" key during left shift key is down.
var BKeyEvent = new KeyboardEvent("", { key: "b", code: "KeyB", keyCode: KeyboardEvent.DOM_VK_B });
TIP.flushPendingComposition(BKeyEvent);

// Releasing shift key
TIP.keyup(shiftKeyEvent);

TIP.setPendingCompositionString("AB");
TIP.appendClauseToPendingComposition("AB".length, TIP.ATTR_SELECTED_CLAUSE);
TIP.setCaretPosition("AB".length);

// This means that the composition string is converted by a keypress of "Convert" key.
var convertKeyEvent =
  new KeyboardEvent("", { key: "Convert", code: "Convert" });
TIP.flushPendingComposition(convertKeyEvent);

// This means that the composition is committed by a keypress of "Enter" key.
var enterKeyEvent =
  new KeyboardEvent("", { key: "Enter", code: "Enter" });
TIP.commitComposition(enterKeyEvent);

Note that specifying keyboard event may not be dispatched during composition due to conforming to the specification of DOM Level 3 Events. However, it's the best for every TextInputProcessor user to specify KeyboardEvent every time because when Gecko will change the behavior of keyboard events during composition, TextInputProcessor must not need to change for the new behavior in most cases.

Method overview

void appendClauseToPendingComposition(in unsigned long aLength, in unsigned long aAttribute);
boolean beginInputTransaction(in nsIDOMWindow aWindow, in nsITextInputProcessorCallback aCallback);
boolean beginInputTransactionForTests(in nsIDOMWindow aWindow, [optional] in nsITextInputProcessorCallback aCallback);
void cancelComposition([optional] in nsIDOMKeyEvent aDOMKeyEvent, [optional] in unsigned long aKeyFlags);
boolean commitComposition([optional] in nsIDOMKeyEvent aDOMKeyEvent, [optional] in unsigned long aKeyFlags);
boolean commitCompositionWith(in DOMString aCommitString, [optional] in nsIDOMKeyEvent aDOMKeyEvent, [optional] in unsigned long aKeyFlags);
boolean flushPendingComposition([optional] in nsIDOMKeyEvent aDOMKeyEvent, [optional] in unsigned long aKeyFlags);
boolean getModifierState(in DOMString aModifierKeyName);
boolean keydown([optional] in nsIDOMKeyEvent aDOMKeyEvent, [optional] in unsigned long aKeyFlags);
boolean keyup([optional] in nsIDOMKeyEvent aDOMKeyEvent, [optional] in unsigned long aKeyFlags);
void setCaretInPendingComposition(in unsigned long aOffset);
void setPendingCompositionString(in DOMString aString);
void shareModifierStateOf(in nsITextInputProcessor aOther);
boolean startComposition([optional] in nsIDOMKeyEvent aDOMKeyEvent, [optional] in unsigned long aKeyFlags);

Attributes

Attribute Type Description
hasComposition boolean Whether the instance has composition or not. true if it has composition. Read only.

Constants

Constant Value Description
ATTR_RAW_CLAUSE 0x02 A clause attribute. This means that the clause is not converted, i.e., raw text of user input.
ATTR_RAW_SELECTED_CLAUSE 0x03 A clause attribute but this is typically not used. This means that the clause is not converted but selected to convert or modify.
ATTR_CONVERTED_CLAUSE 0x04 A clause attribute. This means that the clause is already converted.
ATTR_SELECTED_CLAUSE 0x05 A clause attribute. This means that the clause is already converted and is selected to convert.
KEY_DEFAULT_PREVENTED 0x00000001 One of aKeyFlags. When this is specified, defaultPrevented attribute of dispatching keyboard events true. This is not useful in usual cases. This may be useful for automated tests.
KEY_NON_PRINTABLE_KEY 0x00000002 One of aKeyFlags. When you attempt to dispatch a non-printable key's events, it's recommented this be specified. If the key attribute value is not a registered key name, this flag causes throwing an exception. Therefore, this can prevent non-printable key events to cause dispatching as printable keyboard events and you can detect the registered key name change from the thrown exception.
KEY_FORCE_PRINTABLE_KEY 0x00000004 One of aKeyFlags. When you attempt to dispatch a printable key's event whose key attribute value (i.e., inputting string) will match with a registered key name, this flag makes dispatched keyboard events printable keyboard events forcibly.  For example, if a key causes inputting "Enter" as text, this flag is necessary. If this flag is not specified in such case, non-printable keyboard events will be dispatched.
KEY_KEEP_KEY_LOCATION_STANDARD 0x00000008 One of aKeyFlags. The location attribute value of dispatching keyboard events is computed automatically if the attribute isn't initialized or initialized with 0. For the latter case, you may not want TextInputProcessor to compute location attribute value. In this case, you can suppress the computation with this flag. If this is specified with non-zero location attribute value, this causes throwing an exception since it doesn't make sense.
KEY_KEEP_KEYCODE_ZERO 0x00000010 One of aKeyFlags. The keyCode attribute value of dispatching printable keyboard events is computed from key value automatically if the attribute isn't initialized or initialized with 0. For the latter case, you may not want TextInputProcessor to compute keyCode attribute value. In this case, you can suppress the computation with this flag. If this is specified with non-zero keyCode attribute value, this causes throwing an excepction since it doesn't make sense.
KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT 0x00000020 One of aKeyFlags. TextInputProcessor manages modifier state with every modifier key events. In other words, when you need to specify modifier key state of dispatching keyboard events, you need to dispatch modifier key events before and after that. However, especially for keeping compatibility with legacy API, you may want to modify only the modifier state of a TextInputProcessor instance. In this case, you can suppress TextInputProcessor dispatches modifier key events with this flag. If this is specified for non-modifier key, it causes throwing an exception since it doesn't make sense.

Methods

appendClauseToPendingComposition()

Appends a clause to the pending composition string which is set by setPendingCompositionString(). Sum of aLength must be same as the length of aString of setPendingCompositionString().

void appendClauseToPendingComposition(in unsigned long aLength,
                                      in unsigned long aAttribute);
Parameters
aLength
The length of appending clause.
aAttribute
The attribute of appending clause. Must be one of ATTR_* constants.

beginInputTransaction()

This must be called before starting composition every time. This tries to get the rights to create composition on the window. If no other instance is composing on the window, this associates the instance with the window and registers the callback to the instance.

boolean beginInputTransaction(in nsIDOMWindow aWindow,
                              in nsITextInputProcessorCallback aCallback);
Parameters
aWindow
The DOM window which has focused editor. This must not be null.
aCallback
The callback object. This must not be null. See the document of nsITextInputProcessorCallback for the details.
Return value

Returns true if the instance could create composition. Otherwise, false.

beginInputTransactionForTests()

This must be called before starting composition for automated tests every time. This tries to get the rights to create composition on the window. If no other instance is composing on the window, this associates the instance with the window and registers the callback to the instance if it's specified.

boolean beginInputTransactionForTests(in nsIDOMWindow aWindow,
                                      in nsITextInputProcessorCallback aCallback Optional);
Parameters
aWindow
The DOM window which has focused editor. This must not be null.
aCallback
The callback object. If this is not specified, requests to IMEs are handled automatically. See the document of nsITextInputProcessorCallback for the details.
Return value

Returns true if the instance could create composition. Otherwise, false.

cancelComposition()

Cancels the composition which was created by the instance. If you call this when there is no composition, this throws an exception.

void cancelComposition(in nsIDOMKeyEvent aDOMKeyEvent Optional,
                       in unsigned long aKeyFlags Optional);
Parameters
aDOMKeyEvent Optional
When the canceling composition is caused by a key operation, this should be specified. If this is specified, keyboard events may be dispatched when Gecko thinks that they should be. See also How to create KeyboardEvent instance for nsITextInputProcessor.
aKeyFlags Optional
See KEY_* constants. If this is not specified, the value is assumed as 0.

commitComposition()

Commits the composition with the last composition string. If you call this when there is no composition, this throws an exception.

void commitComposition(in nsIDOMKeyEvent aDOMKeyEvent Optional,
                       in unsigned long aKeyFlags Optional);
Parameters
aDOMKeyEvent Optional
When the committing composition is caused by a key operation, this should be specified. If this is specified, keyboard events may be dispatched when Gecko thinks that they should be. See also How to create KeyboardEvent instance for nsITextInputProcessor.
aKeyFlags Optional
See KEY_* constants. If this is not specified, the value is assumed as 0.

commitCompositionWith()

Commits the composition with the specified string. If you call this when there is no composition, this starts composition automatically and commits the composition with specified string immediately.

boolean commitCompositionWith(in DOMString aCommitString,
                              in nsIDOMKeyEvent aDOMKeyEvent Optional,
                              in unsigned long aKeyFlags aOptional);
Parameters
aCommitString
The commit string.
aDOMKeyEvent Optional
When the committing composition is caused by a key operation, this should be specified. If this is specified, keyboard events may be dispatched when Gecko thinks that they should be. See also How to create KeyboardEvent instance for nsITextInputProcessor.
aKeyFlags Optional
See KEY_* constants. If this is not specified, the value is assumed as 0.
Return value

If this couldn't insert the string when there is no composition (e.g., compositionstart is consumed by the web contents), it returns false. The other cases return true.

flushPendingComposition()

Flushes the pending composition which are set by setPendingCompositionString(), appendClauseToPendingComposition(), and setCaretInPendingComposition() on the focused editor.

If appendClauseToPendingComposition() and/or setCaretInPendingComposition() are not called properly, e.g., sum of aLength of calls of appendClauseToPendingComposition() is not same as composition string set by setPendingCompositionString(), this throws an exception.

After a call of this, the pending composition string information is cleared. Therefore, you can set new composition string continuously.

boolean flushPendingComposition(in nsIDOMKeyEvent aDOMKeyEvent Optional,
                                in unsigned long aKeyFlags Optional);
Parameters
aDOMKeyEvent Optional
When the modifying composition is caused by a key operation, this should be specified. If this is specified, keyboard events may be dispatched when Gecko thinks that they should be. See also How to create KeyboardEvent instance for nsITextInputProcessor.
aKeyFlags Optional
See KEY_* constants. If this is not specified, the value is assumed as 0.
Return value

When there is no composition which was created by the instance and if compositionstart is consumed by web contents, i.e., failed to start new composition, this returns false. Otherwise, it returns true.

getModifierState()

Getting modifier state. This is similar to KeyboardEvent.getModifierState() [en-US]. The result is the modifier state of managed by the instance.

boolean getModifierState(in DOMString aModifierKeyName);
Parameters
aModifierKeyName
A modifier key name which is defined by DOM Level 3 KeyboardEvents key Values.
Return value

true if the modifier state is active. Otherwise, false.

keydown()

Dispatches a keydown event and some keypress event when all of or some of them are necessary.

When this dispatches a normal modifier key event, the TextInputProcessor instance activates the modifier state and stores its code attribute value.

When this dispatches a lockable modifier key event, the TextInputProcessor instance activates the modifier state or If the modifier state is activated with the same code attribute value, the lockable modifier state is inactivated.

boolean keydown(in nsIDOMKeyEvent aDOMKeyEvent,
                in unsigned long aKeyFlags Optional);
Parameters
aDOMKeyEvent
The KeyboardEvent which is initialized with its constructor. See also How to create KeyboardEvent instance for nsITextInputProcessor.
aKeyFlags Optional
See KEY_* constants. If this is not specified, the value is assumed as 0.
Return value

true if neither dispatched keydown event nor keypress events are not consumed by a call of preventDefault().

keyup()

Dispatches a keyup event when it's necessary.

When this dispatches a normal modifier key event, the TextInputProcessor instance inactivates the modifier state if it was activated by same code attribute.

boolean keyup(in nsIDOMKeyEvent aDOMKeyEvent,
              in unsigned long aKeyFlags Optional);
Parameters
aDOMKeyEvent
The KeyboardEvent which is initialized with its constructor. See also How to create KeyboardEvent instance for nsITextInputProcessor.
aKeyFlags Optional
See KEY_* constants. If this is not specified, the value is assumed as 0.
Return value

true if keyup event isn't consumed by a call of preventDefault().

setCaretInPendingComposition()

Sets caret position in the composition string set by setPendingCompositionString(). This is optional, i.e., you may not call this before flushPendingComposition().

void setCaretInPendingComposition(in unsigned long aOffset);
Parameters
aOffset
Caret offset in the composition string. This value must be between 0 and the length of aString of setPendingCompositionString().

setPendingCompositionString()

Sets pending composition string. This must be called before every call of flushPendingComposition().

void setPendingCompositionString(in AString aString);
Parameters
aString
New pending composition string.

shareModifierStateOf()

Shares modifier state of another instance. After sharing modifier state between two or more instances, modifying modifier state on of this affects other instances. Therefore, this is useful if you want to create TextInputProcessor instance in every DOM window or something. See also Sharing modifier state during multiple instances.

void shareModifierStateOf(in nsITextInputProcessor aOther);
Parameters
aOther
Another instance. If this is null, the instance stops sharing modifier state with other instances and clears all modifier state.

startComposition()

This starts composition explicitly. This is useful if your IME doesn't generate composing string but provides some UI to select commit string.

If the instance has a composition already, this throws an exception.

boolean startComposition(in nsIDOMKeyEvent aDOMKeyEvent Optional,
                         in unsigned long aKeyFlags Optional);
Parameters
aDOMKeyEvent Optional
When the starting composition is caused by a key operation, this should be specified. If this is specified, keyboard events may be dispatched when Gecko thinks that they should be. See also How to create KeyboardEvent instance for nsITextInputProcessor.
aKeyFlags Optional
See KEY_* constants. If this is not specified, the value is assumed as 0.
Return value

When there is no composition which was created by the instance and if compositionstart is consumed by web contents, i.e., failed to start new composition, this returns false. Otherwise, returns true.

How to create KeyboardEvent instance for nsITextInputProcessor

This section describes how to create KeyboardEvent for arguments of some methods of nsITextInputProcessor.

You should/can specify following attribute values at creating a KeyboardEvent instance. Other attributes are always ignored. Especially, please note that modifier key states such as shiftKey are also ignored because each instance of nsITextInputProcessor manages modifier state which is modified when keydown() or keyup() is called with modifier key event.

type
When you use the KeyboardEvent instance for a call of either keydown() or keyup(), this value should be empty string.
Otherwise, when you use the KeyboardEvent instance for a call of startComposition(), flushPendingComposition(), commitComposition(), commitCompositionWith(), or cancelComposition(), you can specify "keydown" or empty string. If "keydown" is specified, the methods don't dispatch the "keyup" event.
key
Required.
When you dispatch printable key events, this value should be the text to be inputted. Even if the key event shouldn't cause text input actually, e.g., Ctrl key is pressed, this value should be the text when such modifiers causing suppressing text input are not pressed. For example, Ctrl+C should be "c", Ctrl+Shift+Cshould be "C", AltGr+C may be "©" and then, Ctrl+AltGr+C should be "©".
When you dispatch non-printable key events, this value should be one of the key names defined by DOM Level 3 KeyboardEvent key Values or registered by Gecko with "Moz" prefix.
code
Optional, but should be specified when you emulate PC keyboard operation.
The value should be one of the physical key names defined by DOM Level 3 KeyboardEvent code Values.
keyCode
Optional, but should be specified when you dispatch printable key events.
When you emulate PC keyboard, deciding the value may be complicated. Although the rules to decide the keyCode value are different on each web browser, you should use same rules as Gecko because your events are dispatched on Gecko. Basically, Gecko decides a keyCode value for a printable key from ASCII character which is inputted with a far simpler modifier state. See "Printable keys in standard position" section of KeyboardEvent.keyCode for the strict rules of Gecko.
When you dispatch non-printable key events, preferred keyCode value is computed from key value automatically if you don't initialize this value. But the computation doesn't emulate the mapping of native key event handling completely because it has some special cases depending on platform or selected keyboard layout. Therefore, if you need to emulate PC keyboard on specific platform, you may need to specify this explicitly.
location
Optional. It's not recommended to specify this explicitly since this value should be linked with code attribute value. The automatic computation can compute the best value for this.
repeat
Optional. When you emulate auto-repeat caused by long keypress, you can specify this true.

Modifier state management

This section describes how to manage modifier state.

Each nsITextInputProcessor instance manages modifier state internally and uses the state when it initializes a KeyboardEvent instance before dispatching it.

Activating normal modifier state

When keydown() is called with normal modifier key (i.e., not lockable like CapsLock) event, the instance stores the modifier key and code values. Stored modifier key information means that the modifier is active.

Inactivating normal modifier state

When keyup() is called with normal modifier key event, the instance checks if there is a stored modifier key whose key attribute and code attribute are the same as the event. If the same modifier key is stored, the instance forgets the key. This means that the modifier is inactivated.

Toggling lockable modifier state

When keydown() is called with lockable modifier key event, the instance checks if there is a stored modifier key whose key attribute and code attribute are the same as the event. If it's not stored, i.e., the modifier state is inactive, the instance stores the modifier key, otherwise forgets the key.

Examples of modifier state management

var leftShiftKey =
  new KeyboardEvent("", { key: "Shift", code: "ShiftLeft" });
var rightShiftKey =
  new KeyboardEvent("", { key: "Shift", code: "ShiftRight" });
var shiftKeyOnVirtualKeyboard =
  new KeyboardEvent("", { key: "Shift", code: "" });

TIP.keydown(leftShiftKey); // This activates Shift state.
TIP.keydown(rightShiftKey); // This looks like not modifying Shift state.

TIP.keyup(leftShiftKey); // This does NOT inactivate Shift state becaue right shift key is still pressed.
TIP.keyup(rightShiftKey); // This inactivates Shift state.

TIP.keydown(shiftKeyOnVirtualKeyboard); // This activates Shift state.
TIP.keydown(leftShiftKey);

TIP.keyup(shiftKeyOnVirtualKeyboard); // This also doesn't inactivate Shift state.
TIP.keyup(leftShiftKey); // This inactivates Shift state.

Examples of lockable modifier state management

var capsLockKey =
  new KeyboardEvent("", { key: "CapsLock", code: "CapsLock" });
var capsLockKeyOnVirtualKeyboard =
  new KeyboardEvent("", { key: "CapsLock", code: "" });

TIP.keydown(capsLockKey); // This activates CapsLock state.

TIP.keyup(capsLockKey); // This doesn't inactivate CapsLock state.

TIP.keyup(capsLockKeyOnVirtualKeyboard); // This also doesn't inactivate CapsLock state.

TIP.keydown(capsLockKey); // This inactivates CapsLock state.

TIP.keyup(capsLockKey); // This doesn't modify CapsLock state.

Sharing modifier state during multiple instances

shareModifierStateOf() allows to share modifier state between multiple instances:

var shiftKey =
  new KeyboardEvent("", { key: "Shift", code: "ShiftLeft" });
var ctrlKey =
  new KeyboardEvent("", { key: "Control", code: "ControlLeft" });
var altKey =
  new KeyboardEvent("", { key: "Alt", code: "AltLeft" });

TIP1.keydown(shiftKey); // TIP1's Shift state becomes active.
TIP1.keyup(shiftKey);

TIP2.keydown(ctrlKey); // TIP2's Control state becomes active.
TIP2.keyup(ctrlKey);

TIP3.shareModifierStateOf(TIP1); // TIP3 shares modifier state of TIP1. Therefore, TIP3's Shift state is active.

TIP3.keydown(altKey); // TIP1 and TIP3's Alt state becomes active.
TIP3.keyup(altKey);

TIP3.shareModifierStateOf(TIP2); // TIP3 shares modifier state of TIP2. Therefore, only Control state is active.
                                 // Shift and Alt state are still active in TIP1.

TIP3.shareModifierStateOf(null); // All TIP3's modifier state becomes deactive.
                                 // But TIP2's Control state is still active.

Document Tags and Contributors

 Contributors to this page: JorisW, kscarfone, Masayuki
 Last updated by: JorisW,