summaryrefslogtreecommitdiffstats
path: root/editor
diff options
context:
space:
mode:
Diffstat (limited to 'editor')
-rw-r--r--editor/AsyncSpellCheckTestHelper.jsm86
-rw-r--r--editor/composer/crashtests/351236-1.html37
-rw-r--r--editor/composer/crashtests/407062-1.html20
-rw-r--r--editor/composer/crashtests/419563-1.xhtml20
-rw-r--r--editor/composer/crashtests/428844-1-inner.xhtml4
-rw-r--r--editor/composer/crashtests/428844-1.html17
-rw-r--r--editor/composer/crashtests/461049-1.html25
-rw-r--r--editor/composer/crashtests/crashtests.list6
-rw-r--r--editor/composer/crashtests/removing-editable-xslt-inner.xhtml4
-rw-r--r--editor/composer/crashtests/removing-editable-xslt.html17
-rw-r--r--editor/composer/moz.build53
-rw-r--r--editor/composer/nsComposeTxtSrvFilter.cpp62
-rw-r--r--editor/composer/nsComposeTxtSrvFilter.h55
-rw-r--r--editor/composer/nsComposerCommands.cpp1525
-rw-r--r--editor/composer/nsComposerCommands.h281
-rw-r--r--editor/composer/nsComposerCommandsUpdater.cpp381
-rw-r--r--editor/composer/nsComposerCommandsUpdater.h102
-rw-r--r--editor/composer/nsComposerController.cpp140
-rw-r--r--editor/composer/nsComposerController.h34
-rw-r--r--editor/composer/nsComposerDocumentCommands.cpp480
-rw-r--r--editor/composer/nsComposerRegistration.cpp226
-rw-r--r--editor/composer/nsEditingSession.cpp1392
-rw-r--r--editor/composer/nsEditingSession.h147
-rw-r--r--editor/composer/nsEditorSpellCheck.cpp1005
-rw-r--r--editor/composer/nsEditorSpellCheck.h85
-rw-r--r--editor/composer/nsIEditingSession.idl104
-rw-r--r--editor/composer/res/EditorOverride.css323
-rw-r--r--editor/composer/res/grabber.gifbin0 -> 858 bytes
-rw-r--r--editor/composer/res/table-add-column-after-active.gifbin0 -> 58 bytes
-rw-r--r--editor/composer/res/table-add-column-after-hover.gifbin0 -> 826 bytes
-rw-r--r--editor/composer/res/table-add-column-after.gifbin0 -> 826 bytes
-rw-r--r--editor/composer/res/table-add-column-before-active.gifbin0 -> 57 bytes
-rw-r--r--editor/composer/res/table-add-column-before-hover.gifbin0 -> 825 bytes
-rw-r--r--editor/composer/res/table-add-column-before.gifbin0 -> 825 bytes
-rw-r--r--editor/composer/res/table-add-row-after-active.gifbin0 -> 57 bytes
-rw-r--r--editor/composer/res/table-add-row-after-hover.gifbin0 -> 826 bytes
-rw-r--r--editor/composer/res/table-add-row-after.gifbin0 -> 826 bytes
-rw-r--r--editor/composer/res/table-add-row-before-active.gifbin0 -> 57 bytes
-rw-r--r--editor/composer/res/table-add-row-before-hover.gifbin0 -> 825 bytes
-rw-r--r--editor/composer/res/table-add-row-before.gifbin0 -> 825 bytes
-rw-r--r--editor/composer/res/table-remove-column-active.gifbin0 -> 835 bytes
-rw-r--r--editor/composer/res/table-remove-column-hover.gifbin0 -> 841 bytes
-rw-r--r--editor/composer/res/table-remove-column.gifbin0 -> 841 bytes
-rw-r--r--editor/composer/res/table-remove-row-active.gifbin0 -> 835 bytes
-rw-r--r--editor/composer/res/table-remove-row-hover.gifbin0 -> 841 bytes
-rw-r--r--editor/composer/res/table-remove-row.gifbin0 -> 841 bytes
-rw-r--r--editor/composer/test/bug1200533_subframe.html14
-rw-r--r--editor/composer/test/bug1204147_subframe.html11
-rw-r--r--editor/composer/test/bug1204147_subframe2.html9
-rw-r--r--editor/composer/test/bug678842_subframe.html8
-rw-r--r--editor/composer/test/bug717433_subframe.html8
-rw-r--r--editor/composer/test/chrome.ini5
-rw-r--r--editor/composer/test/de-DE/de_DE.aff2
-rw-r--r--editor/composer/test/de-DE/de_DE.dic6
-rw-r--r--editor/composer/test/en-AU/en_AU.aff2
-rw-r--r--editor/composer/test/en-AU/en_AU.dic4
-rw-r--r--editor/composer/test/en-GB/en_GB.aff2
-rw-r--r--editor/composer/test/en-GB/en_GB.dic4
-rw-r--r--editor/composer/test/mochitest.ini40
-rw-r--r--editor/composer/test/test_async_UpdateCurrentDictionary.html75
-rw-r--r--editor/composer/test/test_bug1200533.html139
-rw-r--r--editor/composer/test/test_bug1204147.html112
-rw-r--r--editor/composer/test/test_bug1205983.html137
-rw-r--r--editor/composer/test/test_bug1209414.html150
-rw-r--r--editor/composer/test/test_bug1219928.html76
-rw-r--r--editor/composer/test/test_bug1266815.html82
-rw-r--r--editor/composer/test/test_bug338427.html61
-rw-r--r--editor/composer/test/test_bug348497.html36
-rw-r--r--editor/composer/test/test_bug384147.html204
-rw-r--r--editor/composer/test/test_bug389350.html33
-rw-r--r--editor/composer/test/test_bug434998.xul109
-rw-r--r--editor/composer/test/test_bug519928.html123
-rw-r--r--editor/composer/test/test_bug678842.html105
-rw-r--r--editor/composer/test/test_bug697981.html133
-rw-r--r--editor/composer/test/test_bug717433.html107
-rw-r--r--editor/composer/test/test_bug738440.html37
-rw-r--r--editor/libeditor/CSSEditUtils.cpp1422
-rw-r--r--editor/libeditor/CSSEditUtils.h473
-rw-r--r--editor/libeditor/ChangeAttributeTransaction.cpp98
-rw-r--r--editor/libeditor/ChangeAttributeTransaction.h72
-rw-r--r--editor/libeditor/ChangeStyleTransaction.cpp287
-rw-r--r--editor/libeditor/ChangeStyleTransaction.h123
-rw-r--r--editor/libeditor/CompositionTransaction.cpp332
-rw-r--r--editor/libeditor/CompositionTransaction.h104
-rw-r--r--editor/libeditor/CreateElementTransaction.cpp147
-rw-r--r--editor/libeditor/CreateElementTransaction.h80
-rw-r--r--editor/libeditor/DeleteNodeTransaction.cpp123
-rw-r--r--editor/libeditor/DeleteNodeTransaction.h66
-rw-r--r--editor/libeditor/DeleteRangeTransaction.cpp239
-rw-r--r--editor/libeditor/DeleteRangeTransaction.h78
-rw-r--r--editor/libeditor/DeleteTextTransaction.cpp102
-rw-r--r--editor/libeditor/DeleteTextTransaction.h76
-rw-r--r--editor/libeditor/EditActionListener.h17
-rw-r--r--editor/libeditor/EditAggregateTransaction.cpp145
-rw-r--r--editor/libeditor/EditAggregateTransaction.h58
-rw-r--r--editor/libeditor/EditTransactionBase.cpp55
-rw-r--r--editor/libeditor/EditTransactionBase.h44
-rw-r--r--editor/libeditor/EditorBase.cpp5244
-rw-r--r--editor/libeditor/EditorBase.h1046
-rw-r--r--editor/libeditor/EditorCommands.cpp1169
-rw-r--r--editor/libeditor/EditorCommands.h106
-rw-r--r--editor/libeditor/EditorController.cpp146
-rw-r--r--editor/libeditor/EditorController.h37
-rw-r--r--editor/libeditor/EditorEventListener.cpp1215
-rw-r--r--editor/libeditor/EditorEventListener.h102
-rw-r--r--editor/libeditor/EditorUtils.cpp227
-rw-r--r--editor/libeditor/EditorUtils.h315
-rw-r--r--editor/libeditor/EditorUtils.js34
-rw-r--r--editor/libeditor/EditorUtils.manifest2
-rw-r--r--editor/libeditor/HTMLAbsPositionEditor.cpp682
-rw-r--r--editor/libeditor/HTMLAnonymousNodeEditor.cpp548
-rw-r--r--editor/libeditor/HTMLEditRules.cpp8785
-rw-r--r--editor/libeditor/HTMLEditRules.h377
-rw-r--r--editor/libeditor/HTMLEditUtils.cpp842
-rw-r--r--editor/libeditor/HTMLEditUtils.h69
-rw-r--r--editor/libeditor/HTMLEditor.cpp5289
-rw-r--r--editor/libeditor/HTMLEditor.h1109
-rw-r--r--editor/libeditor/HTMLEditorDataTransfer.cpp2409
-rw-r--r--editor/libeditor/HTMLEditorEventListener.cpp215
-rw-r--r--editor/libeditor/HTMLEditorEventListener.h43
-rw-r--r--editor/libeditor/HTMLEditorObjectResizer.cpp1059
-rw-r--r--editor/libeditor/HTMLEditorObjectResizerUtils.h82
-rw-r--r--editor/libeditor/HTMLInlineTableEditor.cpp283
-rw-r--r--editor/libeditor/HTMLStyleEditor.cpp1752
-rw-r--r--editor/libeditor/HTMLTableEditor.cpp3458
-rw-r--r--editor/libeditor/HTMLURIRefObject.cpp262
-rw-r--r--editor/libeditor/HTMLURIRefObject.h48
-rw-r--r--editor/libeditor/InsertNodeTransaction.cpp97
-rw-r--r--editor/libeditor/InsertNodeTransaction.h58
-rw-r--r--editor/libeditor/InsertTextTransaction.cpp125
-rw-r--r--editor/libeditor/InsertTextTransaction.h88
-rw-r--r--editor/libeditor/InternetCiter.cpp366
-rw-r--r--editor/libeditor/InternetCiter.h40
-rw-r--r--editor/libeditor/JoinNodeTransaction.cpp109
-rw-r--r--editor/libeditor/JoinNodeTransaction.h68
-rw-r--r--editor/libeditor/PlaceholderTransaction.cpp270
-rw-r--r--editor/libeditor/PlaceholderTransaction.h92
-rw-r--r--editor/libeditor/SelectionState.cpp695
-rw-r--r--editor/libeditor/SelectionState.h357
-rw-r--r--editor/libeditor/SetDocumentTitleTransaction.cpp229
-rw-r--r--editor/libeditor/SetDocumentTitleTransaction.h60
-rw-r--r--editor/libeditor/SplitNodeTransaction.cpp128
-rw-r--r--editor/libeditor/SplitNodeTransaction.h71
-rw-r--r--editor/libeditor/StyleSheetTransactions.cpp157
-rw-r--r--editor/libeditor/StyleSheetTransactions.h73
-rw-r--r--editor/libeditor/TextEditRules.cpp1494
-rw-r--r--editor/libeditor/TextEditRules.h357
-rw-r--r--editor/libeditor/TextEditRulesBidi.cpp100
-rw-r--r--editor/libeditor/TextEditUtils.cpp113
-rw-r--r--editor/libeditor/TextEditUtils.h47
-rw-r--r--editor/libeditor/TextEditor.cpp1638
-rw-r--r--editor/libeditor/TextEditor.h246
-rw-r--r--editor/libeditor/TextEditorDataTransfer.cpp472
-rw-r--r--editor/libeditor/TextEditorTest.cpp259
-rw-r--r--editor/libeditor/TextEditorTest.h42
-rw-r--r--editor/libeditor/TypeInState.cpp397
-rw-r--r--editor/libeditor/TypeInState.h111
-rw-r--r--editor/libeditor/WSRunObject.cpp1926
-rw-r--r--editor/libeditor/WSRunObject.h411
-rw-r--r--editor/libeditor/crashtests/1057677.html9
-rw-r--r--editor/libeditor/crashtests/1128787.html17
-rw-r--r--editor/libeditor/crashtests/1134545.html22
-rw-r--r--editor/libeditor/crashtests/1158452.html10
-rw-r--r--editor/libeditor/crashtests/1158651.html18
-rw-r--r--editor/libeditor/crashtests/1244894.xhtml21
-rw-r--r--editor/libeditor/crashtests/1272490.html20
-rw-r--r--editor/libeditor/crashtests/1317704.html36
-rw-r--r--editor/libeditor/crashtests/336081-1.xhtml52
-rw-r--r--editor/libeditor/crashtests/336104.html37
-rw-r--r--editor/libeditor/crashtests/382527-1.html58
-rw-r--r--editor/libeditor/crashtests/382778-1.html53
-rw-r--r--editor/libeditor/crashtests/402172-1.html23
-rw-r--r--editor/libeditor/crashtests/403965-1.xhtml7
-rw-r--r--editor/libeditor/crashtests/407074-1.html7
-rw-r--r--editor/libeditor/crashtests/407079-1.html15
-rw-r--r--editor/libeditor/crashtests/407256-1.html23
-rw-r--r--editor/libeditor/crashtests/407277-1.html7
-rw-r--r--editor/libeditor/crashtests/414178-1.html22
-rw-r--r--editor/libeditor/crashtests/418923-1.html19
-rw-r--r--editor/libeditor/crashtests/420439.html30
-rw-r--r--editor/libeditor/crashtests/428489-1.html8
-rw-r--r--editor/libeditor/crashtests/429586-1.html8
-rw-r--r--editor/libeditor/crashtests/430624-1.html14
-rw-r--r--editor/libeditor/crashtests/431086-1.xhtml22
-rw-r--r--editor/libeditor/crashtests/448329-1.html72
-rw-r--r--editor/libeditor/crashtests/448329-2.html21
-rw-r--r--editor/libeditor/crashtests/448329-3.html112
-rw-r--r--editor/libeditor/crashtests/456727-1.html8
-rw-r--r--editor/libeditor/crashtests/456727-2.html8
-rw-r--r--editor/libeditor/crashtests/459613-iframe.html1
-rw-r--r--editor/libeditor/crashtests/459613.html17
-rw-r--r--editor/libeditor/crashtests/467647-1.html19
-rw-r--r--editor/libeditor/crashtests/475132-1.xhtml20
-rw-r--r--editor/libeditor/crashtests/499844-1.html15
-rw-r--r--editor/libeditor/crashtests/503709-1.xhtml11
-rw-r--r--editor/libeditor/crashtests/513375-1.xhtml19
-rw-r--r--editor/libeditor/crashtests/535632-1.xhtml1
-rw-r--r--editor/libeditor/crashtests/574558-1.xhtml15
-rw-r--r--editor/libeditor/crashtests/580151-1.xhtml26
-rw-r--r--editor/libeditor/crashtests/582138-1.xhtml10
-rw-r--r--editor/libeditor/crashtests/612565-1.html17
-rw-r--r--editor/libeditor/crashtests/615015-1.html17
-rw-r--r--editor/libeditor/crashtests/615450-1.html17
-rw-r--r--editor/libeditor/crashtests/633709.xhtml36
-rw-r--r--editor/libeditor/crashtests/636074-1.html18
-rw-r--r--editor/libeditor/crashtests/639736-1.xhtml14
-rw-r--r--editor/libeditor/crashtests/643786-1.html18
-rw-r--r--editor/libeditor/crashtests/650572-1.html18
-rw-r--r--editor/libeditor/crashtests/667321-1.html15
-rw-r--r--editor/libeditor/crashtests/682650-1.html30
-rw-r--r--editor/libeditor/crashtests/713427-1.html9
-rw-r--r--editor/libeditor/crashtests/713427-2.xhtml28
-rw-r--r--editor/libeditor/crashtests/716456-1.html29
-rw-r--r--editor/libeditor/crashtests/759748.html58
-rw-r--r--editor/libeditor/crashtests/761861.html15
-rw-r--r--editor/libeditor/crashtests/762183.html6
-rw-r--r--editor/libeditor/crashtests/766305.html21
-rw-r--r--editor/libeditor/crashtests/766360.html18
-rw-r--r--editor/libeditor/crashtests/766387.html20
-rw-r--r--editor/libeditor/crashtests/766413.html42
-rw-r--r--editor/libeditor/crashtests/766795.html21
-rw-r--r--editor/libeditor/crashtests/766845.xhtml27
-rw-r--r--editor/libeditor/crashtests/767169.html23
-rw-r--r--editor/libeditor/crashtests/768748.html16
-rw-r--r--editor/libeditor/crashtests/768765.html36
-rw-r--r--editor/libeditor/crashtests/769008-1.html23
-rw-r--r--editor/libeditor/crashtests/769967.xhtml16
-rw-r--r--editor/libeditor/crashtests/771749.html21
-rw-r--r--editor/libeditor/crashtests/772282.html27
-rw-r--r--editor/libeditor/crashtests/776323.html18
-rw-r--r--editor/libeditor/crashtests/793866.html21
-rw-r--r--editor/libeditor/crashtests/crashtests.list71
-rw-r--r--editor/libeditor/moz.build100
-rw-r--r--editor/libeditor/nsIAbsorbingTransaction.h57
-rw-r--r--editor/libeditor/nsIEditRules.h68
-rw-r--r--editor/libeditor/tests/browser.ini6
-rw-r--r--editor/libeditor/tests/browser_bug527935.js63
-rw-r--r--editor/libeditor/tests/browser_bug629172.js106
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext/LICENSE202
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext/README58
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext/README.Mozilla17
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext/currentStatus.js46
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext/current_revision1
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext/richtext/editable.html11
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext/richtext/js/range.js1069
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext/richtext/richtext.html1081
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext/update_from_upstream16
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/LICENSE202
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/README58
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/README.Mozilla23
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/currentStatus.js1850
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/current_revision1
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/__init__.py0
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/common.py25
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/handlers.py107
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/common.css116
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-body.html11
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-dM.html17
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-div.html11
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable.css66
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/canonicalize.js436
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/compare.js489
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/output.js456
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/pad.js269
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/range-bootstrap.js5
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/range.js6184
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/run.js383
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/units.js416
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/variables.js227
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/templates/output.html138
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/templates/richtext2.html107
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/__init__.py17
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/apply.py364
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/applyCSS.py244
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/change.py273
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/changeCSS.py210
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/delete.py330
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/forwarddelete.py315
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/insert.py285
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryEnabled.py215
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryIndeterm.py214
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryState.py575
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/querySupported.py226
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryValue.py429
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/selection.py772
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/unapply.py462
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/unapplyCSS.py226
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/unittestexample.html103
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/update_from_upstream19
-rw-r--r--editor/libeditor/tests/browserscope/mochitest.ini59
-rw-r--r--editor/libeditor/tests/browserscope/test_richtext.html48
-rw-r--r--editor/libeditor/tests/browserscope/test_richtext2.html233
-rw-r--r--editor/libeditor/tests/bug527935.html11
-rw-r--r--editor/libeditor/tests/bug629172.html15
-rw-r--r--editor/libeditor/tests/chrome.ini14
-rw-r--r--editor/libeditor/tests/data/cfhtml-chromium.txtbin0 -> 856 bytes
-rw-r--r--editor/libeditor/tests/data/cfhtml-firefox.txtbin0 -> 266 bytes
-rw-r--r--editor/libeditor/tests/data/cfhtml-ie.txtbin0 -> 1080 bytes
-rw-r--r--editor/libeditor/tests/data/cfhtml-nocontext.txt18
-rw-r--r--editor/libeditor/tests/data/cfhtml-ooo.txtbin0 -> 649 bytes
-rw-r--r--editor/libeditor/tests/file_bug549262.html8
-rw-r--r--editor/libeditor/tests/file_bug586662.html7
-rw-r--r--editor/libeditor/tests/file_bug674770-1.html5
-rw-r--r--editor/libeditor/tests/file_bug915962.html13
-rw-r--r--editor/libeditor/tests/file_select_all_without_body.html41
-rw-r--r--editor/libeditor/tests/green.pngbin0 -> 334 bytes
-rw-r--r--editor/libeditor/tests/mochitest.ini245
-rw-r--r--editor/libeditor/tests/spellcheck.js20
-rw-r--r--editor/libeditor/tests/test_CF_HTML_clipboard.html159
-rw-r--r--editor/libeditor/tests/test_backspace_vs.html130
-rw-r--r--editor/libeditor/tests/test_bug1026397.html103
-rw-r--r--editor/libeditor/tests/test_bug1053048.html73
-rw-r--r--editor/libeditor/tests/test_bug1067255.html57
-rw-r--r--editor/libeditor/tests/test_bug1068979.html72
-rw-r--r--editor/libeditor/tests/test_bug1094000.html104
-rw-r--r--editor/libeditor/tests/test_bug1100966.html65
-rw-r--r--editor/libeditor/tests/test_bug1101392.html78
-rw-r--r--editor/libeditor/tests/test_bug1102906.html51
-rw-r--r--editor/libeditor/tests/test_bug1109465.html69
-rw-r--r--editor/libeditor/tests/test_bug1140105.html71
-rw-r--r--editor/libeditor/tests/test_bug1140617.html37
-rw-r--r--editor/libeditor/tests/test_bug1153237.html49
-rw-r--r--editor/libeditor/tests/test_bug1154791.html67
-rw-r--r--editor/libeditor/tests/test_bug1162952.html43
-rw-r--r--editor/libeditor/tests/test_bug1181130-1.html50
-rw-r--r--editor/libeditor/tests/test_bug1181130-2.html44
-rw-r--r--editor/libeditor/tests/test_bug1186799.html81
-rw-r--r--editor/libeditor/tests/test_bug1230473.html124
-rw-r--r--editor/libeditor/tests/test_bug1247483.html61
-rw-r--r--editor/libeditor/tests/test_bug1248128.html52
-rw-r--r--editor/libeditor/tests/test_bug1248185.html57
-rw-r--r--editor/libeditor/tests/test_bug1250010.html93
-rw-r--r--editor/libeditor/tests/test_bug1257363.html182
-rw-r--r--editor/libeditor/tests/test_bug1258085.html66
-rw-r--r--editor/libeditor/tests/test_bug1268736.html64
-rw-r--r--editor/libeditor/tests/test_bug1270235.html46
-rw-r--r--editor/libeditor/tests/test_bug1310912.html93
-rw-r--r--editor/libeditor/tests/test_bug1314790.html65
-rw-r--r--editor/libeditor/tests/test_bug1315065.html145
-rw-r--r--editor/libeditor/tests/test_bug1328023.html64
-rw-r--r--editor/libeditor/tests/test_bug1330796.html101
-rw-r--r--editor/libeditor/tests/test_bug1332876.html49
-rw-r--r--editor/libeditor/tests/test_bug200416.html15
-rw-r--r--editor/libeditor/tests/test_bug289384.html49
-rw-r--r--editor/libeditor/tests/test_bug290026.html52
-rw-r--r--editor/libeditor/tests/test_bug291780.html50
-rw-r--r--editor/libeditor/tests/test_bug309731.html58
-rw-r--r--editor/libeditor/tests/test_bug316447.html16
-rw-r--r--editor/libeditor/tests/test_bug318065.html82
-rw-r--r--editor/libeditor/tests/test_bug332636.html75
-rw-r--r--editor/libeditor/tests/test_bug332636.html^headers^1
-rw-r--r--editor/libeditor/tests/test_bug366682.html67
-rw-r--r--editor/libeditor/tests/test_bug372345.html59
-rw-r--r--editor/libeditor/tests/test_bug404320.html91
-rw-r--r--editor/libeditor/tests/test_bug408231.html250
-rw-r--r--editor/libeditor/tests/test_bug410986.html80
-rw-r--r--editor/libeditor/tests/test_bug414526.html247
-rw-r--r--editor/libeditor/tests/test_bug417418.html78
-rw-r--r--editor/libeditor/tests/test_bug432225.html71
-rw-r--r--editor/libeditor/tests/test_bug439808.html37
-rw-r--r--editor/libeditor/tests/test_bug442186.html103
-rw-r--r--editor/libeditor/tests/test_bug449243.html136
-rw-r--r--editor/libeditor/tests/test_bug455992.html97
-rw-r--r--editor/libeditor/tests/test_bug456244.html70
-rw-r--r--editor/libeditor/tests/test_bug460740.html124
-rw-r--r--editor/libeditor/tests/test_bug46555.html47
-rw-r--r--editor/libeditor/tests/test_bug468353.html117
-rw-r--r--editor/libeditor/tests/test_bug471319.html80
-rw-r--r--editor/libeditor/tests/test_bug471722.html81
-rw-r--r--editor/libeditor/tests/test_bug478725.html131
-rw-r--r--editor/libeditor/tests/test_bug480647.html110
-rw-r--r--editor/libeditor/tests/test_bug480972.html98
-rw-r--r--editor/libeditor/tests/test_bug483651.html53
-rw-r--r--editor/libeditor/tests/test_bug484181.html78
-rw-r--r--editor/libeditor/tests/test_bug487524.html65
-rw-r--r--editor/libeditor/tests/test_bug489202.xul81
-rw-r--r--editor/libeditor/tests/test_bug490879.html45
-rw-r--r--editor/libeditor/tests/test_bug502673.html108
-rw-r--r--editor/libeditor/tests/test_bug514156.html50
-rw-r--r--editor/libeditor/tests/test_bug520189.html621
-rw-r--r--editor/libeditor/tests/test_bug525389.html198
-rw-r--r--editor/libeditor/tests/test_bug537046.html51
-rw-r--r--editor/libeditor/tests/test_bug549262.html132
-rw-r--r--editor/libeditor/tests/test_bug550434.html42
-rw-r--r--editor/libeditor/tests/test_bug551704.html125
-rw-r--r--editor/libeditor/tests/test_bug552782.html47
-rw-r--r--editor/libeditor/tests/test_bug567213.html58
-rw-r--r--editor/libeditor/tests/test_bug569988.html99
-rw-r--r--editor/libeditor/tests/test_bug570144.html123
-rw-r--r--editor/libeditor/tests/test_bug578771.html63
-rw-r--r--editor/libeditor/tests/test_bug586662.html60
-rw-r--r--editor/libeditor/tests/test_bug587461.html16
-rw-r--r--editor/libeditor/tests/test_bug590554.html36
-rw-r--r--editor/libeditor/tests/test_bug592592.html72
-rw-r--r--editor/libeditor/tests/test_bug596001.html55
-rw-r--r--editor/libeditor/tests/test_bug596333.html124
-rw-r--r--editor/libeditor/tests/test_bug596506.html60
-rw-r--r--editor/libeditor/tests/test_bug597331.html72
-rw-r--r--editor/libeditor/tests/test_bug597784.html37
-rw-r--r--editor/libeditor/tests/test_bug599322.html58
-rw-r--r--editor/libeditor/tests/test_bug599983.html16
-rw-r--r--editor/libeditor/tests/test_bug599983.xul70
-rw-r--r--editor/libeditor/tests/test_bug600570.html80
-rw-r--r--editor/libeditor/tests/test_bug602130.html45
-rw-r--r--editor/libeditor/tests/test_bug603556.html48
-rw-r--r--editor/libeditor/tests/test_bug604532.html42
-rw-r--r--editor/libeditor/tests/test_bug607584.html41
-rw-r--r--editor/libeditor/tests/test_bug607584.xul115
-rw-r--r--editor/libeditor/tests/test_bug611182.html239
-rw-r--r--editor/libeditor/tests/test_bug612128.html42
-rw-r--r--editor/libeditor/tests/test_bug612447.html74
-rw-r--r--editor/libeditor/tests/test_bug616590.xul105
-rw-r--r--editor/libeditor/tests/test_bug620906.html50
-rw-r--r--editor/libeditor/tests/test_bug622371.html40
-rw-r--r--editor/libeditor/tests/test_bug625452.html66
-rw-r--r--editor/libeditor/tests/test_bug629845.html60
-rw-r--r--editor/libeditor/tests/test_bug635636.html65
-rw-r--r--editor/libeditor/tests/test_bug636465.html54
-rw-r--r--editor/libeditor/tests/test_bug638596.html37
-rw-r--r--editor/libeditor/tests/test_bug640321.html190
-rw-r--r--editor/libeditor/tests/test_bug641466.html46
-rw-r--r--editor/libeditor/tests/test_bug645914.html63
-rw-r--r--editor/libeditor/tests/test_bug646194.html38
-rw-r--r--editor/libeditor/tests/test_bug668599.html73
-rw-r--r--editor/libeditor/tests/test_bug674770-1.html86
-rw-r--r--editor/libeditor/tests/test_bug674770-2.html395
-rw-r--r--editor/libeditor/tests/test_bug674861.html185
-rw-r--r--editor/libeditor/tests/test_bug676401.html119
-rw-r--r--editor/libeditor/tests/test_bug677752.html107
-rw-r--r--editor/libeditor/tests/test_bug681229.html51
-rw-r--r--editor/libeditor/tests/test_bug686203.html50
-rw-r--r--editor/libeditor/tests/test_bug692520.html42
-rw-r--r--editor/libeditor/tests/test_bug697842.html117
-rw-r--r--editor/libeditor/tests/test_bug725069.html35
-rw-r--r--editor/libeditor/tests/test_bug735059.html22
-rw-r--r--editor/libeditor/tests/test_bug738366.html24
-rw-r--r--editor/libeditor/tests/test_bug740784.html48
-rw-r--r--editor/libeditor/tests/test_bug742261.html14
-rw-r--r--editor/libeditor/tests/test_bug757371.html26
-rw-r--r--editor/libeditor/tests/test_bug757771.html31
-rw-r--r--editor/libeditor/tests/test_bug767684.html15
-rw-r--r--editor/libeditor/tests/test_bug772796.html223
-rw-r--r--editor/libeditor/tests/test_bug773262.html63
-rw-r--r--editor/libeditor/tests/test_bug780035.html22
-rw-r--r--editor/libeditor/tests/test_bug780908.xul113
-rw-r--r--editor/libeditor/tests/test_bug787432.html17
-rw-r--r--editor/libeditor/tests/test_bug790475.html95
-rw-r--r--editor/libeditor/tests/test_bug795418-2.html88
-rw-r--r--editor/libeditor/tests/test_bug795418-3.html88
-rw-r--r--editor/libeditor/tests/test_bug795418-4.html67
-rw-r--r--editor/libeditor/tests/test_bug795418-5.html67
-rw-r--r--editor/libeditor/tests/test_bug795418-6.html67
-rw-r--r--editor/libeditor/tests/test_bug795418.html67
-rw-r--r--editor/libeditor/tests/test_bug795785.html168
-rw-r--r--editor/libeditor/tests/test_bug796839.html17
-rw-r--r--editor/libeditor/tests/test_bug830600.html100
-rw-r--r--editor/libeditor/tests/test_bug832025.html42
-rw-r--r--editor/libeditor/tests/test_bug850043.html65
-rw-r--r--editor/libeditor/tests/test_bug857487.html72
-rw-r--r--editor/libeditor/tests/test_bug858918.html16
-rw-r--r--editor/libeditor/tests/test_bug915962.html100
-rw-r--r--editor/libeditor/tests/test_bug966155.html57
-rw-r--r--editor/libeditor/tests/test_bug966552.html45
-rw-r--r--editor/libeditor/tests/test_bug974309.html78
-rw-r--r--editor/libeditor/tests/test_bug998188.html52
-rw-r--r--editor/libeditor/tests/test_composition_event_created_in_chrome.html82
-rw-r--r--editor/libeditor/tests/test_contenteditable_focus.html209
-rw-r--r--editor/libeditor/tests/test_contenteditable_text_input_handling.html329
-rw-r--r--editor/libeditor/tests/test_css_chrome_load_access.html67
-rw-r--r--editor/libeditor/tests/test_dom_input_event_on_htmleditor.html182
-rw-r--r--editor/libeditor/tests/test_dom_input_event_on_texteditor.html140
-rw-r--r--editor/libeditor/tests/test_dragdrop.html178
-rw-r--r--editor/libeditor/tests/test_htmleditor_keyevent_handling.html664
-rw-r--r--editor/libeditor/tests/test_keypress_untrusted_event.html99
-rw-r--r--editor/libeditor/tests/test_root_element_replacement.html148
-rw-r--r--editor/libeditor/tests/test_select_all_without_body.html27
-rw-r--r--editor/libeditor/tests/test_selection_move_commands.html219
-rw-r--r--editor/libeditor/tests/test_set_document_title_transaction.html79
-rw-r--r--editor/libeditor/tests/test_spellcheck_pref.html23
-rw-r--r--editor/libeditor/tests/test_texteditor_keyevent_handling.html386
-rw-r--r--editor/moz.build44
-rw-r--r--editor/nsEditorCID.h25
-rw-r--r--editor/nsIContentFilter.idl85
-rw-r--r--editor/nsIDocumentStateListener.idl16
-rw-r--r--editor/nsIEditActionListener.idl199
-rw-r--r--editor/nsIEditor.idl567
-rw-r--r--editor/nsIEditorIMESupport.idl40
-rw-r--r--editor/nsIEditorMailSupport.idl81
-rw-r--r--editor/nsIEditorObserver.idl36
-rw-r--r--editor/nsIEditorSpellCheck.idl166
-rw-r--r--editor/nsIEditorStyleSheets.idl71
-rw-r--r--editor/nsIEditorUtils.idl32
-rw-r--r--editor/nsIHTMLAbsPosEditor.idl132
-rw-r--r--editor/nsIHTMLEditor.idl559
-rw-r--r--editor/nsIHTMLInlineTableEditor.idl44
-rw-r--r--editor/nsIHTMLObjectResizeListener.idl33
-rw-r--r--editor/nsIHTMLObjectResizer.idl93
-rw-r--r--editor/nsIPlaintextEditor.idl112
-rw-r--r--editor/nsITableEditor.idl337
-rw-r--r--editor/nsIURIRefObject.idl43
-rw-r--r--editor/nsPIEditorTransaction.idl20
-rw-r--r--editor/reftests/1088158-ref.html2
-rw-r--r--editor/reftests/1088158.html8
-rw-r--r--editor/reftests/338427-1-ref.html7
-rw-r--r--editor/reftests/338427-1.html7
-rw-r--r--editor/reftests/338427-2-ref.html19
-rw-r--r--editor/reftests/338427-2.html18
-rw-r--r--editor/reftests/338427-3-ref.html19
-rw-r--r--editor/reftests/338427-3.html19
-rw-r--r--editor/reftests/388980-1-ref.html25
-rw-r--r--editor/reftests/388980-1.html43
-rw-r--r--editor/reftests/462758-grabbers-resizers-ref.html34
-rw-r--r--editor/reftests/462758-grabbers-resizers.html33
-rw-r--r--editor/reftests/642800-iframe.html29
-rw-r--r--editor/reftests/642800-ref.html7
-rw-r--r--editor/reftests/642800.html18
-rw-r--r--editor/reftests/672709-ref.html22
-rw-r--r--editor/reftests/672709.html12
-rw-r--r--editor/reftests/674212-spellcheck-ref.html20
-rw-r--r--editor/reftests/674212-spellcheck.html20
-rw-r--r--editor/reftests/694880-1.html10
-rw-r--r--editor/reftests/694880-2.html11
-rw-r--r--editor/reftests/694880-3.html10
-rw-r--r--editor/reftests/694880-ref.html9
-rw-r--r--editor/reftests/824080-1-ref.html17
-rw-r--r--editor/reftests/824080-1.html19
-rw-r--r--editor/reftests/824080-2-ref.html22
-rw-r--r--editor/reftests/824080-2.html22
-rw-r--r--editor/reftests/824080-3-ref.html21
-rw-r--r--editor/reftests/824080-3.html21
-rw-r--r--editor/reftests/824080-4-ref.html21
-rw-r--r--editor/reftests/824080-4.html26
-rw-r--r--editor/reftests/824080-5-ref.html22
-rw-r--r--editor/reftests/824080-5.html25
-rw-r--r--editor/reftests/824080-6-ref.html18
-rw-r--r--editor/reftests/824080-6.html20
-rw-r--r--editor/reftests/824080-7-ref.html19
-rw-r--r--editor/reftests/824080-7.html22
-rw-r--r--editor/reftests/911201-ref.html2
-rw-r--r--editor/reftests/911201.html2
-rw-r--r--editor/reftests/969773-ref.html24
-rw-r--r--editor/reftests/969773.html29
-rw-r--r--editor/reftests/997805-ref.html2
-rw-r--r--editor/reftests/997805.html16
-rw-r--r--editor/reftests/caret_after_reframe-ref.html6
-rw-r--r--editor/reftests/caret_after_reframe.html14
-rw-r--r--editor/reftests/caret_on_focus-ref.html11
-rw-r--r--editor/reftests/caret_on_focus.html11
-rw-r--r--editor/reftests/caret_on_positioned-ref.html8
-rw-r--r--editor/reftests/caret_on_positioned.html8
-rw-r--r--editor/reftests/caret_on_presshell_reinit-2.html27
-rw-r--r--editor/reftests/caret_on_presshell_reinit-ref.html19
-rw-r--r--editor/reftests/caret_on_presshell_reinit.html22
-rw-r--r--editor/reftests/caret_on_textarea_lastline-ref.html13
-rw-r--r--editor/reftests/caret_on_textarea_lastline.html14
-rw-r--r--editor/reftests/dynamic-1.html9
-rw-r--r--editor/reftests/dynamic-overflow-change-ref.html13
-rw-r--r--editor/reftests/dynamic-overflow-change.html13
-rw-r--r--editor/reftests/dynamic-ref.html6
-rw-r--r--editor/reftests/dynamic-type-1.html11
-rw-r--r--editor/reftests/dynamic-type-2.html11
-rw-r--r--editor/reftests/dynamic-type-3.html11
-rw-r--r--editor/reftests/dynamic-type-4.html11
-rw-r--r--editor/reftests/emptypasswd-1.html6
-rw-r--r--editor/reftests/emptypasswd-2.html9
-rw-r--r--editor/reftests/emptypasswd-ref.html6
-rw-r--r--editor/reftests/input-text-notheme-onfocus-reframe-ref.html28
-rw-r--r--editor/reftests/input-text-notheme-onfocus-reframe.html32
-rw-r--r--editor/reftests/input-text-onfocus-reframe-ref.html25
-rw-r--r--editor/reftests/input-text-onfocus-reframe.html29
-rw-r--r--editor/reftests/newline-1.html6
-rw-r--r--editor/reftests/newline-2.html6
-rw-r--r--editor/reftests/newline-3.html6
-rw-r--r--editor/reftests/newline-4.html6
-rw-r--r--editor/reftests/newline-ref.html6
-rw-r--r--editor/reftests/nobogusnode-1.html6
-rw-r--r--editor/reftests/nobogusnode-2.html5
-rw-r--r--editor/reftests/nobogusnode-ref.html6
-rw-r--r--editor/reftests/passwd-1.html6
-rw-r--r--editor/reftests/passwd-2.html6
-rw-r--r--editor/reftests/passwd-3.html9
-rw-r--r--editor/reftests/passwd-4.html19
-rw-r--r--editor/reftests/passwd-ref.html6
-rw-r--r--editor/reftests/readonly-editable-ref.html13
-rw-r--r--editor/reftests/readonly-editable.html24
-rw-r--r--editor/reftests/readonly-non-editable-ref.html21
-rw-r--r--editor/reftests/readonly-non-editable.html24
-rw-r--r--editor/reftests/readwrite-editable-ref.html13
-rw-r--r--editor/reftests/readwrite-editable.html24
-rw-r--r--editor/reftests/readwrite-non-editable-ref.html21
-rw-r--r--editor/reftests/readwrite-non-editable.html24
-rw-r--r--editor/reftests/reftest-stylo.list177
-rw-r--r--editor/reftests/reftest.list137
-rw-r--r--editor/reftests/selection_visibility_after_reframe-2.html11
-rw-r--r--editor/reftests/selection_visibility_after_reframe-3.html14
-rw-r--r--editor/reftests/selection_visibility_after_reframe-ref.html6
-rw-r--r--editor/reftests/selection_visibility_after_reframe.html15
-rw-r--r--editor/reftests/spellcheck-comma-valid-ref.html6
-rw-r--r--editor/reftests/spellcheck-comma-valid.html6
-rw-r--r--editor/reftests/spellcheck-contenteditable-attr-dynamic-inherit.html11
-rw-r--r--editor/reftests/spellcheck-contenteditable-attr-dynamic-override-inherit.html11
-rw-r--r--editor/reftests/spellcheck-contenteditable-attr-dynamic-override.html11
-rw-r--r--editor/reftests/spellcheck-contenteditable-attr-dynamic.html11
-rw-r--r--editor/reftests/spellcheck-contenteditable-attr-inherit.html6
-rw-r--r--editor/reftests/spellcheck-contenteditable-attr.html6
-rw-r--r--editor/reftests/spellcheck-contenteditable-disabled-partial-ref.html12
-rw-r--r--editor/reftests/spellcheck-contenteditable-disabled-partial.html11
-rw-r--r--editor/reftests/spellcheck-contenteditable-disabled-ref.html6
-rw-r--r--editor/reftests/spellcheck-contenteditable-disabled.html11
-rw-r--r--editor/reftests/spellcheck-contenteditable-focused-reframe.html18
-rw-r--r--editor/reftests/spellcheck-contenteditable-focused.html13
-rw-r--r--editor/reftests/spellcheck-contenteditable-nofocus-ref.html6
-rw-r--r--editor/reftests/spellcheck-contenteditable-nofocus.html6
-rw-r--r--editor/reftests/spellcheck-contenteditable-property-dynamic-inherit.html11
-rw-r--r--editor/reftests/spellcheck-contenteditable-property-dynamic-override-inherit.html11
-rw-r--r--editor/reftests/spellcheck-contenteditable-property-dynamic-override.html11
-rw-r--r--editor/reftests/spellcheck-contenteditable-property-dynamic.html11
-rw-r--r--editor/reftests/spellcheck-contenteditable-ref.html11
-rw-r--r--editor/reftests/spellcheck-dotafterquote-valid-ref.html6
-rw-r--r--editor/reftests/spellcheck-dotafterquote-valid.html6
-rw-r--r--editor/reftests/spellcheck-hyphen-invalid-ref.html6
-rw-r--r--editor/reftests/spellcheck-hyphen-invalid.html6
-rw-r--r--editor/reftests/spellcheck-hyphen-multiple-invalid-ref.html6
-rw-r--r--editor/reftests/spellcheck-hyphen-multiple-invalid.html6
-rw-r--r--editor/reftests/spellcheck-hyphen-multiple-valid-ref.html6
-rw-r--r--editor/reftests/spellcheck-hyphen-multiple-valid.html6
-rw-r--r--editor/reftests/spellcheck-hyphen-valid-ref.html6
-rw-r--r--editor/reftests/spellcheck-hyphen-valid.html6
-rw-r--r--editor/reftests/spellcheck-input-attr-after.html6
-rw-r--r--editor/reftests/spellcheck-input-attr-before.html6
-rw-r--r--editor/reftests/spellcheck-input-attr-dynamic-inherit.html11
-rw-r--r--editor/reftests/spellcheck-input-attr-dynamic-override-inherit.html11
-rw-r--r--editor/reftests/spellcheck-input-attr-dynamic-override.html11
-rw-r--r--editor/reftests/spellcheck-input-attr-dynamic.html11
-rw-r--r--editor/reftests/spellcheck-input-attr-inherit.html6
-rw-r--r--editor/reftests/spellcheck-input-disabled.html6
-rw-r--r--editor/reftests/spellcheck-input-nofocus-ref.html6
-rw-r--r--editor/reftests/spellcheck-input-property-dynamic-inherit.html11
-rw-r--r--editor/reftests/spellcheck-input-property-dynamic-override-inherit.html11
-rw-r--r--editor/reftests/spellcheck-input-property-dynamic-override.html11
-rw-r--r--editor/reftests/spellcheck-input-property-dynamic.html11
-rw-r--r--editor/reftests/spellcheck-input-ref.html11
-rw-r--r--editor/reftests/spellcheck-non-latin-arabic-ref.html9
-rw-r--r--editor/reftests/spellcheck-non-latin-arabic.html9
-rw-r--r--editor/reftests/spellcheck-non-latin-chinese-simplified-ref.html9
-rw-r--r--editor/reftests/spellcheck-non-latin-chinese-simplified.html9
-rw-r--r--editor/reftests/spellcheck-non-latin-chinese-traditional-ref.html9
-rw-r--r--editor/reftests/spellcheck-non-latin-chinese-traditional.html9
-rw-r--r--editor/reftests/spellcheck-non-latin-hebrew-ref.html9
-rw-r--r--editor/reftests/spellcheck-non-latin-hebrew.html9
-rw-r--r--editor/reftests/spellcheck-non-latin-japanese-ref.html9
-rw-r--r--editor/reftests/spellcheck-non-latin-japanese.html9
-rw-r--r--editor/reftests/spellcheck-non-latin-korean-ref.html9
-rw-r--r--editor/reftests/spellcheck-non-latin-korean.html9
-rw-r--r--editor/reftests/spellcheck-period-valid-ref.html6
-rw-r--r--editor/reftests/spellcheck-period-valid.html6
-rw-r--r--editor/reftests/spellcheck-slash-valid-ref.html6
-rw-r--r--editor/reftests/spellcheck-slash-valid.html6
-rw-r--r--editor/reftests/spellcheck-space-valid-ref.html6
-rw-r--r--editor/reftests/spellcheck-space-valid.html6
-rw-r--r--editor/reftests/spellcheck-superscript-1-ref.html3
-rw-r--r--editor/reftests/spellcheck-superscript-1.html3
-rw-r--r--editor/reftests/spellcheck-superscript-2-ref.html3
-rw-r--r--editor/reftests/spellcheck-superscript-2.html3
-rw-r--r--editor/reftests/spellcheck-textarea-attr-dynamic-inherit.html11
-rw-r--r--editor/reftests/spellcheck-textarea-attr-dynamic-override-inherit.html11
-rw-r--r--editor/reftests/spellcheck-textarea-attr-dynamic-override.html11
-rw-r--r--editor/reftests/spellcheck-textarea-attr-dynamic.html11
-rw-r--r--editor/reftests/spellcheck-textarea-attr-inherit.html6
-rw-r--r--editor/reftests/spellcheck-textarea-attr.html6
-rw-r--r--editor/reftests/spellcheck-textarea-disabled.html6
-rw-r--r--editor/reftests/spellcheck-textarea-focused-notreadonly.html19
-rw-r--r--editor/reftests/spellcheck-textarea-focused-reframe.html18
-rw-r--r--editor/reftests/spellcheck-textarea-focused.html13
-rw-r--r--editor/reftests/spellcheck-textarea-nofocus-ref.html6
-rw-r--r--editor/reftests/spellcheck-textarea-nofocus.html6
-rw-r--r--editor/reftests/spellcheck-textarea-property-dynamic-inherit.html11
-rw-r--r--editor/reftests/spellcheck-textarea-property-dynamic-override-inherit.html11
-rw-r--r--editor/reftests/spellcheck-textarea-property-dynamic-override.html11
-rw-r--r--editor/reftests/spellcheck-textarea-property-dynamic.html11
-rw-r--r--editor/reftests/spellcheck-textarea-ref.html11
-rw-r--r--editor/reftests/spellcheck-textarea-ref2.html11
-rw-r--r--editor/reftests/spellcheck-url-valid-ref.html14
-rw-r--r--editor/reftests/spellcheck-url-valid.html14
-rw-r--r--editor/reftests/unneeded_scroll-ref.html16
-rw-r--r--editor/reftests/unneeded_scroll.html24
-rw-r--r--editor/reftests/xul/autocomplete-1.xul14
-rw-r--r--editor/reftests/xul/autocomplete-ref.xul13
-rw-r--r--editor/reftests/xul/empty-1.xul13
-rw-r--r--editor/reftests/xul/empty-2.xul12
-rw-r--r--editor/reftests/xul/empty-ref.xul13
-rw-r--r--editor/reftests/xul/emptyautocomplete-1.xul12
-rw-r--r--editor/reftests/xul/emptyautocomplete-ref.xul13
-rw-r--r--editor/reftests/xul/emptymultiline-1.xul13
-rw-r--r--editor/reftests/xul/emptymultiline-2.xul13
-rw-r--r--editor/reftests/xul/emptymultiline-ref.xul13
-rw-r--r--editor/reftests/xul/emptytextbox-1.xul12
-rw-r--r--editor/reftests/xul/emptytextbox-2.xul12
-rw-r--r--editor/reftests/xul/emptytextbox-3.xul12
-rw-r--r--editor/reftests/xul/emptytextbox-4.xul12
-rw-r--r--editor/reftests/xul/emptytextbox-5.xul12
-rw-r--r--editor/reftests/xul/emptytextbox-ref.xul13
-rw-r--r--editor/reftests/xul/input.css70
-rw-r--r--editor/reftests/xul/number-1.xul12
-rw-r--r--editor/reftests/xul/number-2.xul12
-rw-r--r--editor/reftests/xul/number-3.xul12
-rw-r--r--editor/reftests/xul/number-4.xul12
-rw-r--r--editor/reftests/xul/number-5.xul12
-rw-r--r--editor/reftests/xul/number-ref.xul13
-rw-r--r--editor/reftests/xul/numberwithvalue-1.xul12
-rw-r--r--editor/reftests/xul/numberwithvalue-ref.xul13
-rw-r--r--editor/reftests/xul/passwd-1.xul12
-rw-r--r--editor/reftests/xul/passwd-2.xul12
-rw-r--r--editor/reftests/xul/passwd-3.xul12
-rw-r--r--editor/reftests/xul/passwd-ref.xul13
-rw-r--r--editor/reftests/xul/placeholder-reset.css8
-rw-r--r--editor/reftests/xul/plain-1.xul12
-rw-r--r--editor/reftests/xul/plain-ref.xul13
-rw-r--r--editor/reftests/xul/platform.js28
-rw-r--r--editor/reftests/xul/reftest-stylo.list67
-rw-r--r--editor/reftests/xul/reftest.list29
-rw-r--r--editor/reftests/xul/textbox-1.xul12
-rw-r--r--editor/reftests/xul/textbox-disabled.xul12
-rw-r--r--editor/reftests/xul/textbox-readonly.xul12
-rw-r--r--editor/reftests/xul/textbox-ref.xul13
-rw-r--r--editor/txmgr/moz.build30
-rw-r--r--editor/txmgr/nsITransaction.idl63
-rw-r--r--editor/txmgr/nsITransactionList.idl70
-rw-r--r--editor/txmgr/nsITransactionListener.idl170
-rw-r--r--editor/txmgr/nsITransactionManager.idl168
-rw-r--r--editor/txmgr/nsTransactionItem.cpp349
-rw-r--r--editor/txmgr/nsTransactionItem.h68
-rw-r--r--editor/txmgr/nsTransactionList.cpp174
-rw-r--r--editor/txmgr/nsTransactionList.h46
-rw-r--r--editor/txmgr/nsTransactionManager.cpp752
-rw-r--r--editor/txmgr/nsTransactionManager.h83
-rw-r--r--editor/txmgr/nsTransactionManagerCID.h14
-rw-r--r--editor/txmgr/nsTransactionManagerFactory.cpp38
-rw-r--r--editor/txmgr/nsTransactionStack.cpp108
-rw-r--r--editor/txmgr/nsTransactionStack.h39
-rw-r--r--editor/txmgr/tests/TestTXMgr.cpp4478
-rw-r--r--editor/txmgr/tests/crashtests/407072-1.html22
-rw-r--r--editor/txmgr/tests/crashtests/449006-1.html28
-rw-r--r--editor/txmgr/tests/crashtests/crashtests.list2
-rw-r--r--editor/txmgr/tests/moz.build9
-rw-r--r--editor/txtsvc/moz.build26
-rw-r--r--editor/txtsvc/nsFilteredContentIterator.cpp422
-rw-r--r--editor/txtsvc/nsFilteredContentIterator.h75
-rw-r--r--editor/txtsvc/nsIInlineSpellChecker.idl50
-rw-r--r--editor/txtsvc/nsISpellChecker.h122
-rw-r--r--editor/txtsvc/nsITextService.h42
-rw-r--r--editor/txtsvc/nsITextServicesDocument.h185
-rw-r--r--editor/txtsvc/nsITextServicesFilter.idl21
-rw-r--r--editor/txtsvc/nsTSAtomList.h51
-rw-r--r--editor/txtsvc/nsTextServicesCID.h19
-rw-r--r--editor/txtsvc/nsTextServicesDocument.cpp3499
-rw-r--r--editor/txtsvc/nsTextServicesDocument.h236
-rw-r--r--editor/txtsvc/nsTextServicesFactory.cpp31
758 files changed, 115444 insertions, 0 deletions
diff --git a/editor/AsyncSpellCheckTestHelper.jsm b/editor/AsyncSpellCheckTestHelper.jsm
new file mode 100644
index 000000000..99aa14bd3
--- /dev/null
+++ b/editor/AsyncSpellCheckTestHelper.jsm
@@ -0,0 +1,86 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+this.EXPORTED_SYMBOLS = [
+ "onSpellCheck",
+];
+
+const SPELL_CHECK_ENDED_TOPIC = "inlineSpellChecker-spellCheck-ended";
+const SPELL_CHECK_STARTED_TOPIC = "inlineSpellChecker-spellCheck-started";
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+/**
+ * Waits until spell checking has stopped on the given element.
+ *
+ * When a spell check is pending, this waits indefinitely until the spell check
+ * ends. When a spell check is not pending, it waits a small number of turns of
+ * the event loop: if a spell check begins, it resumes waiting indefinitely for
+ * the end, and otherwise it stops waiting and calls the callback.
+ *
+ * This this can therefore trap spell checks that have not started at the time
+ * of calling, spell checks that have already started, multiple consecutive
+ * spell checks, and the absence of spell checks altogether.
+ *
+ * @param editableElement The element being spell checked.
+ * @param callback Called when spell check has completed or enough turns
+ * of the event loop have passed to determine it has not
+ * started.
+ */
+function onSpellCheck(editableElement, callback) {
+ let editor = editableElement.editor;
+ if (!editor) {
+ let win = editableElement.ownerDocument.defaultView;
+ editor = win.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIWebNavigation).
+ QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIEditingSession).
+ getEditorForWindow(win);
+ }
+ if (!editor)
+ throw new Error("Unable to find editor for element " + editableElement);
+
+ try {
+ // False is important here. Pass false so that the inline spell checker
+ // isn't created if it doesn't already exist.
+ var isc = editor.getInlineSpellChecker(false);
+ }
+ catch (err) {
+ // getInlineSpellChecker throws if spell checking is not enabled instead of
+ // just returning null, which seems kind of lame. (Spell checking is not
+ // enabled on Android.) The point here is only to determine whether spell
+ // check is pending, and if getInlineSpellChecker throws, then it's not
+ // pending.
+ }
+ let waitingForEnded = isc && isc.spellCheckPending;
+ let count = 0;
+
+ function observe(subj, topic, data) {
+ if (subj != editor)
+ return;
+ count = 0;
+ let expectedTopic = waitingForEnded ? SPELL_CHECK_ENDED_TOPIC :
+ SPELL_CHECK_STARTED_TOPIC;
+ if (topic != expectedTopic)
+ Cu.reportError("Expected " + expectedTopic + " but got " + topic + "!");
+ waitingForEnded = !waitingForEnded;
+ }
+
+ let os = Cc["@mozilla.org/observer-service;1"].
+ getService(Ci.nsIObserverService);
+ os.addObserver(observe, SPELL_CHECK_STARTED_TOPIC, false);
+ os.addObserver(observe, SPELL_CHECK_ENDED_TOPIC, false);
+
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.init(function tick() {
+ // Wait an arbitrarily large number -- 50 -- turns of the event loop before
+ // declaring that no spell checks will start.
+ if (waitingForEnded || ++count < 50)
+ return;
+ timer.cancel();
+ os.removeObserver(observe, SPELL_CHECK_STARTED_TOPIC);
+ os.removeObserver(observe, SPELL_CHECK_ENDED_TOPIC);
+ callback();
+ }, 0, Ci.nsITimer.TYPE_REPEATING_SLACK);
+};
diff --git a/editor/composer/crashtests/351236-1.html b/editor/composer/crashtests/351236-1.html
new file mode 100644
index 000000000..22133a215
--- /dev/null
+++ b/editor/composer/crashtests/351236-1.html
@@ -0,0 +1,37 @@
+<html><head>
+<title>Testcase bug 351236 - Crash [@ nsGetInterface::operator()] with designMode iframes, removing styles, removing iframes, reloading, etc</title>
+<script>
+function designmodes(i){
+try {
+window.frames[0].document.designMode='on';
+window.frames[0].focus();
+window.frames[0].getSelection().collapse(window.frames[0].document.body.childNodes[0],window.frames[0].document.body.childNodes[0].length-2)
+window.frames[0].document.execCommand('inserthtml', false, 'tesxt ');
+} catch(e) {}
+
+setTimeout(designmodes,50);
+}
+
+function removestyles(){
+document.getElementsByTagName('iframe')[0].removeAttribute('style');
+document.getElementsByTagName('q')[0].removeAttribute('style');
+}
+
+function doe() {
+setTimeout(designmodes,200);
+setTimeout(removestyles,500);
+setTimeout(function() {document.removeChild(document.documentElement);}, 1000);
+setTimeout(function() {window.location.reload();}, 1500);
+}
+window.onload=doe;
+</script>
+
+</head>
+<body>
+This page should not crash Mozilla within 2 seconds<br>
+<q style="display: table-row;">
+<iframe style="display: table-row;"></iframe>
+<iframe></iframe>
+</q>
+</body>
+</html> \ No newline at end of file
diff --git a/editor/composer/crashtests/407062-1.html b/editor/composer/crashtests/407062-1.html
new file mode 100644
index 000000000..81083c235
--- /dev/null
+++ b/editor/composer/crashtests/407062-1.html
@@ -0,0 +1,20 @@
+<html contentEditable="true">
+<head>
+
+<script type="text/javascript">
+
+function boom()
+{
+ var r = document.documentElement;
+ while(r.firstChild)
+ r.removeChild(r.firstChild);
+
+ document.execCommand("contentReadOnly", false, "");
+ document.documentElement.focus();
+}
+
+</script>
+</head>
+
+<body onload="boom();"></body>
+</html>
diff --git a/editor/composer/crashtests/419563-1.xhtml b/editor/composer/crashtests/419563-1.xhtml
new file mode 100644
index 000000000..417530c13
--- /dev/null
+++ b/editor/composer/crashtests/419563-1.xhtml
@@ -0,0 +1,20 @@
+<html xmlns="http://www.w3.org/1999/xhtml" class="reftest-wait">
+<head>
+<script type="text/javascript">
+
+function boom()
+{
+ document.documentElement.appendChild(document.body);
+ document.getElementById("s").contentEditable = "true";
+ document.getElementById("v").focus();
+ document.body.focus();
+ document.execCommand("delete", false, null);
+ document.documentElement.removeAttribute("class");
+}
+
+</script>
+</head>
+
+<span id="s">thesewords arenot realwords</span><body contenteditable="true" onload="setTimeout(boom, 0);"><span contenteditable="false"><div id="v" contenteditable="true"></div>Five</span></body>
+
+</html>
diff --git a/editor/composer/crashtests/428844-1-inner.xhtml b/editor/composer/crashtests/428844-1-inner.xhtml
new file mode 100644
index 000000000..1cc72d085
--- /dev/null
+++ b/editor/composer/crashtests/428844-1-inner.xhtml
@@ -0,0 +1,4 @@
+<?xml-stylesheet type="text/xsl" href="#a"?>
+<html xmlns="http://www.w3.org/1999/xhtml" onload="dump('Inner onload\n'); window.location.reload()" contenteditable="true">
+<xsl:stylesheet version="1.0" id="a" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"/>
+</html>
diff --git a/editor/composer/crashtests/428844-1.html b/editor/composer/crashtests/428844-1.html
new file mode 100644
index 000000000..8bebdbf4b
--- /dev/null
+++ b/editor/composer/crashtests/428844-1.html
@@ -0,0 +1,17 @@
+<html class="reftest-wait">
+<head>
+<script>
+function boom() {
+ var iframe = document.getElementById('inner');
+ iframe.addEventListener("load", function() {
+ document.documentElement.removeAttribute("class");
+ }, false);
+ iframe.src = "data:text/html,";
+ dump("Outer onload\n");
+}
+</script>
+</head>
+<body onload="boom()">
+<iframe src="428844-1-inner.xhtml" id="inner"></iframe>
+</body>
+</html>
diff --git a/editor/composer/crashtests/461049-1.html b/editor/composer/crashtests/461049-1.html
new file mode 100644
index 000000000..c25ed991f
--- /dev/null
+++ b/editor/composer/crashtests/461049-1.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script type="text/javascript">
+
+function uu()
+{
+ document.removeEventListener("DOMSubtreeModified", uu, false);
+ document.execCommand("undo", false, null);
+}
+
+function boom()
+{
+ document.execCommand("selectAll", false, null);
+ document.execCommand("strikethrough", false, null);
+ document.addEventListener("DOMSubtreeModified", uu, false);
+ document.execCommand("undo", false, null);
+}
+
+</script>
+</head>
+
+<body contenteditable="true" onload="boom();"><div></div></body>
+
+</html>
diff --git a/editor/composer/crashtests/crashtests.list b/editor/composer/crashtests/crashtests.list
new file mode 100644
index 000000000..db84e0e5b
--- /dev/null
+++ b/editor/composer/crashtests/crashtests.list
@@ -0,0 +1,6 @@
+load 351236-1.html
+load 407062-1.html
+load 419563-1.xhtml
+load 428844-1.html
+load 461049-1.html
+load removing-editable-xslt.html
diff --git a/editor/composer/crashtests/removing-editable-xslt-inner.xhtml b/editor/composer/crashtests/removing-editable-xslt-inner.xhtml
new file mode 100644
index 000000000..cbf206d7e
--- /dev/null
+++ b/editor/composer/crashtests/removing-editable-xslt-inner.xhtml
@@ -0,0 +1,4 @@
+<?xml-stylesheet type="text/xsl" href="#a"?>
+<html xmlns="http://www.w3.org/1999/xhtml" contenteditable="true">
+<xsl:stylesheet version="1.0" id="a" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"/>
+</html>
diff --git a/editor/composer/crashtests/removing-editable-xslt.html b/editor/composer/crashtests/removing-editable-xslt.html
new file mode 100644
index 000000000..cbf104ac9
--- /dev/null
+++ b/editor/composer/crashtests/removing-editable-xslt.html
@@ -0,0 +1,17 @@
+<html class="reftest-wait">
+<head>
+<script>
+function boom()
+{
+ document.getElementById("i").src = "removing-editable-xslt-inner.xhtml";
+ setTimeout(function() {
+ document.body.removeChild(document.getElementById("i"));
+ document.documentElement.removeAttribute("class");
+ }, 0);
+}
+</script>
+</head>
+<body onload="boom();">
+<iframe id="i"></iframe>
+</body>
+</html>
diff --git a/editor/composer/moz.build b/editor/composer/moz.build
new file mode 100644
index 000000000..4db8c9130
--- /dev/null
+++ b/editor/composer/moz.build
@@ -0,0 +1,53 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+MOCHITEST_MANIFESTS += ['test/mochitest.ini']
+
+MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini']
+
+XPIDL_SOURCES += [
+ 'nsIEditingSession.idl',
+]
+
+XPIDL_MODULE = 'composer'
+
+UNIFIED_SOURCES += [
+ 'nsComposerCommands.cpp',
+ 'nsComposerCommandsUpdater.cpp',
+ 'nsComposerController.cpp',
+ 'nsComposerDocumentCommands.cpp',
+ 'nsComposerRegistration.cpp',
+ 'nsComposeTxtSrvFilter.cpp',
+ 'nsEditingSession.cpp',
+ 'nsEditorSpellCheck.cpp',
+]
+
+FINAL_LIBRARY = 'xul'
+RESOURCE_FILES += [
+ 'res/EditorOverride.css',
+ 'res/grabber.gif',
+ 'res/table-add-column-after-active.gif',
+ 'res/table-add-column-after-hover.gif',
+ 'res/table-add-column-after.gif',
+ 'res/table-add-column-before-active.gif',
+ 'res/table-add-column-before-hover.gif',
+ 'res/table-add-column-before.gif',
+ 'res/table-add-row-after-active.gif',
+ 'res/table-add-row-after-hover.gif',
+ 'res/table-add-row-after.gif',
+ 'res/table-add-row-before-active.gif',
+ 'res/table-add-row-before-hover.gif',
+ 'res/table-add-row-before.gif',
+ 'res/table-remove-column-active.gif',
+ 'res/table-remove-column-hover.gif',
+ 'res/table-remove-column.gif',
+ 'res/table-remove-row-active.gif',
+ 'res/table-remove-row-hover.gif',
+ 'res/table-remove-row.gif',
+]
+
+if CONFIG['GNU_CXX']:
+ CXXFLAGS += ['-Wno-error=shadow']
diff --git a/editor/composer/nsComposeTxtSrvFilter.cpp b/editor/composer/nsComposeTxtSrvFilter.cpp
new file mode 100644
index 000000000..ba66bca95
--- /dev/null
+++ b/editor/composer/nsComposeTxtSrvFilter.cpp
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsComposeTxtSrvFilter.h"
+#include "nsError.h" // for NS_OK
+#include "nsIContent.h" // for nsIContent
+#include "nsIDOMNode.h" // for nsIDOMNode
+#include "nsNameSpaceManager.h" // for kNameSpaceID_None
+#include "nsLiteralString.h" // for NS_LITERAL_STRING
+#include "nscore.h" // for NS_IMETHODIMP
+
+nsComposeTxtSrvFilter::nsComposeTxtSrvFilter() :
+ mIsForMail(false)
+{
+}
+
+NS_IMPL_ISUPPORTS(nsComposeTxtSrvFilter, nsITextServicesFilter)
+
+NS_IMETHODIMP
+nsComposeTxtSrvFilter::Skip(nsIDOMNode* aNode, bool *_retval)
+{
+ *_retval = false;
+
+ // Check to see if we can skip this node
+ // For nodes that are blockquotes, we must make sure
+ // their type is "cite"
+ nsCOMPtr<nsIContent> content(do_QueryInterface(aNode));
+ if (content) {
+ if (content->IsHTMLElement(nsGkAtoms::blockquote)) {
+ if (mIsForMail) {
+ *_retval = content->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+ nsGkAtoms::cite, eIgnoreCase);
+ }
+ } else if (content->IsHTMLElement(nsGkAtoms::span)) {
+ if (mIsForMail) {
+ *_retval = content->AttrValueIs(kNameSpaceID_None, nsGkAtoms::mozquote,
+ nsGkAtoms::_true, eIgnoreCase);
+ if (!*_retval) {
+ *_retval = content->AttrValueIs(kNameSpaceID_None, nsGkAtoms::_class,
+ nsGkAtoms::mozsignature, eCaseMatters);
+ }
+ }
+ } else if (content->IsAnyOfHTMLElements(nsGkAtoms::script,
+ nsGkAtoms::textarea,
+ nsGkAtoms::select,
+ nsGkAtoms::style,
+ nsGkAtoms::map)) {
+ *_retval = true;
+ } else if (content->IsHTMLElement(nsGkAtoms::table)) {
+ if (mIsForMail) {
+ *_retval =
+ content->AttrValueIs(kNameSpaceID_None, nsGkAtoms::_class,
+ NS_LITERAL_STRING("moz-email-headers-table"),
+ eCaseMatters);
+ }
+ }
+ }
+
+ return NS_OK;
+}
diff --git a/editor/composer/nsComposeTxtSrvFilter.h b/editor/composer/nsComposeTxtSrvFilter.h
new file mode 100644
index 000000000..0e5bba433
--- /dev/null
+++ b/editor/composer/nsComposeTxtSrvFilter.h
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsComposeTxtSrvFilter_h__
+#define nsComposeTxtSrvFilter_h__
+
+#include "nsISupportsImpl.h" // for NS_DECL_ISUPPORTS
+#include "nsITextServicesFilter.h"
+
+/**
+ * This class implements a filter interface, that enables
+ * those using it to skip over certain nodes when traversing content
+ *
+ * This filter is used to skip over various form control nodes and
+ * mail's cite nodes
+ */
+class nsComposeTxtSrvFilter final : public nsITextServicesFilter
+{
+public:
+ nsComposeTxtSrvFilter();
+
+ // nsISupports interface...
+ NS_DECL_ISUPPORTS
+
+ // nsITextServicesFilter
+ NS_DECL_NSITEXTSERVICESFILTER
+
+ // Helper - Intializer
+ void Init(bool aIsForMail) { mIsForMail = aIsForMail; }
+
+private:
+ ~nsComposeTxtSrvFilter() {}
+
+ bool mIsForMail;
+};
+
+#define NS_COMPOSERTXTSRVFILTER_CID \
+{/* {171E72DB-0F8A-412a-8461-E4C927A3A2AC}*/ \
+0x171e72db, 0xf8a, 0x412a, \
+{ 0x84, 0x61, 0xe4, 0xc9, 0x27, 0xa3, 0xa2, 0xac} }
+
+#define NS_COMPOSERTXTSRVFILTERMAIL_CID \
+{/* {7FBD2146-5FF4-4674-B069-A7BBCE66E773}*/ \
+0x7fbd2146, 0x5ff4, 0x4674, \
+{ 0xb0, 0x69, 0xa7, 0xbb, 0xce, 0x66, 0xe7, 0x73} }
+
+// Generic for the editor
+#define COMPOSER_TXTSRVFILTER_CONTRACTID "@mozilla.org/editor/txtsrvfilter;1"
+
+// This is the same but includes "cite" typed blocked quotes
+#define COMPOSER_TXTSRVFILTERMAIL_CONTRACTID "@mozilla.org/editor/txtsrvfiltermail;1"
+
+#endif
diff --git a/editor/composer/nsComposerCommands.cpp b/editor/composer/nsComposerCommands.cpp
new file mode 100644
index 000000000..3853604e4
--- /dev/null
+++ b/editor/composer/nsComposerCommands.cpp
@@ -0,0 +1,1525 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+#include <stdio.h> // for printf
+
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
+#include "nsAString.h"
+#include "nsCOMPtr.h" // for nsCOMPtr, do_QueryInterface, etc
+#include "nsComponentManagerUtils.h" // for do_CreateInstance
+#include "nsComposerCommands.h"
+#include "nsDebug.h" // for NS_ENSURE_TRUE, etc
+#include "nsError.h" // for NS_OK, NS_ERROR_FAILURE, etc
+#include "nsGkAtoms.h" // for nsGkAtoms, nsGkAtoms::font, etc
+#include "nsIAtom.h" // for nsIAtom, etc
+#include "nsIClipboard.h" // for nsIClipboard, etc
+#include "nsICommandParams.h" // for nsICommandParams, etc
+#include "nsID.h"
+#include "nsIDOMElement.h" // for nsIDOMElement
+#include "nsIEditor.h" // for nsIEditor
+#include "nsIHTMLAbsPosEditor.h" // for nsIHTMLAbsPosEditor
+#include "nsIHTMLEditor.h" // for nsIHTMLEditor, etc
+#include "nsLiteralString.h" // for NS_LITERAL_STRING
+#include "nsReadableUtils.h" // for EmptyString
+#include "nsString.h" // for nsAutoString, nsString, etc
+#include "nsStringFwd.h" // for nsAFlatString
+
+class nsISupports;
+
+//prototype
+nsresult GetListState(nsIHTMLEditor* aEditor, bool* aMixed,
+ nsAString& aLocalName);
+nsresult RemoveOneProperty(nsIHTMLEditor* aEditor, const nsAString& aProp);
+nsresult RemoveTextProperty(nsIHTMLEditor* aEditor, const nsAString& aProp);
+nsresult SetTextProperty(nsIHTMLEditor *aEditor, const nsAString& aProp);
+
+
+//defines
+#define STATE_ENABLED "state_enabled"
+#define STATE_ALL "state_all"
+#define STATE_ANY "state_any"
+#define STATE_MIXED "state_mixed"
+#define STATE_BEGIN "state_begin"
+#define STATE_END "state_end"
+#define STATE_ATTRIBUTE "state_attribute"
+#define STATE_DATA "state_data"
+
+
+nsBaseComposerCommand::nsBaseComposerCommand()
+{
+}
+
+NS_IMPL_ISUPPORTS(nsBaseComposerCommand, nsIControllerCommand)
+
+
+nsBaseStateUpdatingCommand::nsBaseStateUpdatingCommand(nsIAtom* aTagName)
+: nsBaseComposerCommand()
+, mTagName(aTagName)
+{
+ MOZ_ASSERT(mTagName);
+}
+
+nsBaseStateUpdatingCommand::~nsBaseStateUpdatingCommand()
+{
+}
+
+NS_IMPL_ISUPPORTS_INHERITED0(nsBaseStateUpdatingCommand, nsBaseComposerCommand)
+
+NS_IMETHODIMP
+nsBaseStateUpdatingCommand::IsCommandEnabled(const char *aCommandName,
+ nsISupports *refCon,
+ bool *outCmdEnabled)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(refCon);
+ if (editor)
+ return editor->GetIsSelectionEditable(outCmdEnabled);
+
+ *outCmdEnabled = false;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsBaseStateUpdatingCommand::DoCommand(const char *aCommandName,
+ nsISupports *refCon)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(refCon);
+ NS_ENSURE_TRUE(editor, NS_ERROR_NOT_INITIALIZED);
+
+ return ToggleState(editor);
+}
+
+NS_IMETHODIMP
+nsBaseStateUpdatingCommand::DoCommandParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ return DoCommand(aCommandName, refCon);
+}
+
+NS_IMETHODIMP
+nsBaseStateUpdatingCommand::GetCommandStateParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(refCon);
+ if (editor)
+ return GetCurrentState(editor, aParams);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPasteNoFormattingCommand::IsCommandEnabled(const char * aCommandName,
+ nsISupports *refCon,
+ bool *outCmdEnabled)
+{
+ NS_ENSURE_ARG_POINTER(outCmdEnabled);
+ *outCmdEnabled = false;
+
+ // This command is only implemented by nsIHTMLEditor, since
+ // pasting in a plaintext editor automatically only supplies
+ // "unformatted" text
+ nsCOMPtr<nsIHTMLEditor> htmlEditor = do_QueryInterface(refCon);
+ NS_ENSURE_TRUE(htmlEditor, NS_ERROR_NOT_IMPLEMENTED);
+
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(htmlEditor);
+ NS_ENSURE_TRUE(editor, NS_ERROR_INVALID_ARG);
+
+ return editor->CanPaste(nsIClipboard::kGlobalClipboard, outCmdEnabled);
+}
+
+
+NS_IMETHODIMP
+nsPasteNoFormattingCommand::DoCommand(const char *aCommandName,
+ nsISupports *refCon)
+{
+ nsCOMPtr<nsIHTMLEditor> htmlEditor = do_QueryInterface(refCon);
+ NS_ENSURE_TRUE(htmlEditor, NS_ERROR_NOT_IMPLEMENTED);
+
+ return htmlEditor->PasteNoFormatting(nsIClipboard::kGlobalClipboard);
+}
+
+NS_IMETHODIMP
+nsPasteNoFormattingCommand::DoCommandParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ return DoCommand(aCommandName, refCon);
+}
+
+NS_IMETHODIMP
+nsPasteNoFormattingCommand::GetCommandStateParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ NS_ENSURE_ARG_POINTER(aParams);
+
+ bool enabled = false;
+ nsresult rv = IsCommandEnabled(aCommandName, refCon, &enabled);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return aParams->SetBooleanValue(STATE_ENABLED, enabled);
+}
+
+nsStyleUpdatingCommand::nsStyleUpdatingCommand(nsIAtom* aTagName)
+: nsBaseStateUpdatingCommand(aTagName)
+{
+}
+
+nsresult
+nsStyleUpdatingCommand::GetCurrentState(nsIEditor *aEditor,
+ nsICommandParams *aParams)
+{
+ NS_ASSERTION(aEditor, "Need editor here");
+ nsCOMPtr<nsIHTMLEditor> htmlEditor = do_QueryInterface(aEditor);
+ NS_ENSURE_TRUE(htmlEditor, NS_ERROR_NOT_INITIALIZED);
+
+ bool firstOfSelectionHasProp = false;
+ bool anyOfSelectionHasProp = false;
+ bool allOfSelectionHasProp = false;
+
+ nsresult rv = htmlEditor->GetInlineProperty(mTagName, EmptyString(),
+ EmptyString(),
+ &firstOfSelectionHasProp,
+ &anyOfSelectionHasProp,
+ &allOfSelectionHasProp);
+
+ aParams->SetBooleanValue(STATE_ENABLED, NS_SUCCEEDED(rv));
+ aParams->SetBooleanValue(STATE_ALL, allOfSelectionHasProp);
+ aParams->SetBooleanValue(STATE_ANY, anyOfSelectionHasProp);
+ aParams->SetBooleanValue(STATE_MIXED, anyOfSelectionHasProp
+ && !allOfSelectionHasProp);
+ aParams->SetBooleanValue(STATE_BEGIN, firstOfSelectionHasProp);
+ aParams->SetBooleanValue(STATE_END, allOfSelectionHasProp);//not completely accurate
+ return NS_OK;
+}
+
+nsresult
+nsStyleUpdatingCommand::ToggleState(nsIEditor *aEditor)
+{
+ nsCOMPtr<nsIHTMLEditor> htmlEditor = do_QueryInterface(aEditor);
+ NS_ENSURE_TRUE(htmlEditor, NS_ERROR_NO_INTERFACE);
+
+ //create some params now...
+ nsresult rv;
+ nsCOMPtr<nsICommandParams> params =
+ do_CreateInstance(NS_COMMAND_PARAMS_CONTRACTID,&rv);
+ if (NS_FAILED(rv) || !params)
+ return rv;
+
+ // tags "href" and "name" are special cases in the core editor
+ // they are used to remove named anchor/link and shouldn't be used for insertion
+ bool doTagRemoval;
+ if (mTagName == nsGkAtoms::href || mTagName == nsGkAtoms::name) {
+ doTagRemoval = true;
+ } else {
+ // check current selection; set doTagRemoval if formatting should be removed
+ rv = GetCurrentState(aEditor, params);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = params->GetBooleanValue(STATE_ALL, &doTagRemoval);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (doTagRemoval) {
+ // Also remove equivalent properties (bug 317093)
+ if (mTagName == nsGkAtoms::b) {
+ rv = RemoveTextProperty(htmlEditor, NS_LITERAL_STRING("strong"));
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else if (mTagName == nsGkAtoms::i) {
+ rv = RemoveTextProperty(htmlEditor, NS_LITERAL_STRING("em"));
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else if (mTagName == nsGkAtoms::strike) {
+ rv = RemoveTextProperty(htmlEditor, NS_LITERAL_STRING("s"));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = RemoveTextProperty(htmlEditor, nsDependentAtomString(mTagName));
+ } else {
+ // Superscript and Subscript styles are mutually exclusive
+ aEditor->BeginTransaction();
+
+ nsDependentAtomString tagName(mTagName);
+ if (mTagName == nsGkAtoms::sub || mTagName == nsGkAtoms::sup) {
+ rv = RemoveTextProperty(htmlEditor, tagName);
+ }
+ if (NS_SUCCEEDED(rv))
+ rv = SetTextProperty(htmlEditor, tagName);
+
+ aEditor->EndTransaction();
+ }
+
+ return rv;
+}
+
+nsListCommand::nsListCommand(nsIAtom* aTagName)
+: nsBaseStateUpdatingCommand(aTagName)
+{
+}
+
+nsresult
+nsListCommand::GetCurrentState(nsIEditor* aEditor, nsICommandParams* aParams)
+{
+ NS_ASSERTION(aEditor, "Need editor here");
+ nsCOMPtr<nsIHTMLEditor> htmlEditor = do_QueryInterface(aEditor);
+ NS_ENSURE_TRUE(htmlEditor, NS_ERROR_NO_INTERFACE);
+
+ bool bMixed;
+ nsAutoString localName;
+ nsresult rv = GetListState(htmlEditor, &bMixed, localName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool inList = mTagName->Equals(localName);
+ aParams->SetBooleanValue(STATE_ALL, !bMixed && inList);
+ aParams->SetBooleanValue(STATE_MIXED, bMixed);
+ aParams->SetBooleanValue(STATE_ENABLED, true);
+ return NS_OK;
+}
+
+nsresult
+nsListCommand::ToggleState(nsIEditor *aEditor)
+{
+ nsCOMPtr<nsIHTMLEditor> editor = do_QueryInterface(aEditor);
+ NS_ENSURE_TRUE(editor, NS_NOINTERFACE);
+
+ nsresult rv;
+ nsCOMPtr<nsICommandParams> params =
+ do_CreateInstance(NS_COMMAND_PARAMS_CONTRACTID,&rv);
+ if (NS_FAILED(rv) || !params)
+ return rv;
+
+ rv = GetCurrentState(aEditor, params);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool inList;
+ rv = params->GetBooleanValue(STATE_ALL,&inList);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsDependentAtomString listType(mTagName);
+ if (inList) {
+ rv = editor->RemoveList(listType);
+ } else {
+ rv = editor->MakeOrChangeList(listType, false, EmptyString());
+ }
+
+ return rv;
+}
+
+nsListItemCommand::nsListItemCommand(nsIAtom* aTagName)
+: nsBaseStateUpdatingCommand(aTagName)
+{
+}
+
+nsresult
+nsListItemCommand::GetCurrentState(nsIEditor* aEditor,
+ nsICommandParams *aParams)
+{
+ NS_ASSERTION(aEditor, "Need editor here");
+ // 39584
+ nsCOMPtr<nsIHTMLEditor> htmlEditor = do_QueryInterface(aEditor);
+ NS_ENSURE_TRUE(htmlEditor, NS_NOINTERFACE);
+
+ bool bMixed, bLI, bDT, bDD;
+ nsresult rv = htmlEditor->GetListItemState(&bMixed, &bLI, &bDT, &bDD);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool inList = false;
+ if (!bMixed) {
+ if (bLI) {
+ inList = mTagName == nsGkAtoms::li;
+ } else if (bDT) {
+ inList = mTagName == nsGkAtoms::dt;
+ } else if (bDD) {
+ inList = mTagName == nsGkAtoms::dd;
+ }
+ }
+
+ aParams->SetBooleanValue(STATE_ALL, !bMixed && inList);
+ aParams->SetBooleanValue(STATE_MIXED, bMixed);
+
+ return NS_OK;
+}
+
+nsresult
+nsListItemCommand::ToggleState(nsIEditor *aEditor)
+{
+ NS_ASSERTION(aEditor, "Need editor here");
+ nsCOMPtr<nsIHTMLEditor> htmlEditor = do_QueryInterface(aEditor);
+ NS_ENSURE_TRUE(htmlEditor, NS_ERROR_NOT_INITIALIZED);
+
+ bool inList;
+ // Need to use mTagName????
+ nsresult rv;
+ nsCOMPtr<nsICommandParams> params =
+ do_CreateInstance(NS_COMMAND_PARAMS_CONTRACTID,&rv);
+ if (NS_FAILED(rv) || !params)
+ return rv;
+ rv = GetCurrentState(aEditor, params);
+ rv = params->GetBooleanValue(STATE_ALL,&inList);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (inList) {
+ // To remove a list, first get what kind of list we're in
+ bool bMixed;
+ nsAutoString localName;
+ rv = GetListState(htmlEditor, &bMixed, localName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (localName.IsEmpty() || bMixed) {
+ return rv;
+ }
+ return htmlEditor->RemoveList(localName);
+ }
+
+ // Set to the requested paragraph type
+ //XXX Note: This actually doesn't work for "LI",
+ // but we currently don't use this for non DL lists anyway.
+ // Problem: won't this replace any current block paragraph style?
+ return htmlEditor->SetParagraphFormat(nsDependentAtomString(mTagName));
+}
+
+NS_IMETHODIMP
+nsRemoveListCommand::IsCommandEnabled(const char * aCommandName,
+ nsISupports *refCon,
+ bool *outCmdEnabled)
+{
+ *outCmdEnabled = false;
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(refCon);
+ NS_ENSURE_TRUE(editor, NS_OK);
+
+ bool isEditable = false;
+ nsresult rv = editor->GetIsSelectionEditable(&isEditable);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!isEditable) {
+ return NS_OK;
+ }
+
+ // It is enabled if we are in any list type
+ nsCOMPtr<nsIHTMLEditor> htmlEditor = do_QueryInterface(refCon);
+ NS_ENSURE_TRUE(htmlEditor, NS_ERROR_NO_INTERFACE);
+
+ bool bMixed;
+ nsAutoString localName;
+ rv = GetListState(htmlEditor, &bMixed, localName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *outCmdEnabled = bMixed || !localName.IsEmpty();
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsRemoveListCommand::DoCommand(const char *aCommandName, nsISupports *refCon)
+{
+ nsCOMPtr<nsIHTMLEditor> editor = do_QueryInterface(refCon);
+
+ nsresult rv = NS_OK;
+ if (editor) {
+ // This removes any list type
+ rv = editor->RemoveList(EmptyString());
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsRemoveListCommand::DoCommandParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ return DoCommand(aCommandName, refCon);
+}
+
+NS_IMETHODIMP
+nsRemoveListCommand::GetCommandStateParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ bool outCmdEnabled = false;
+ IsCommandEnabled(aCommandName, refCon, &outCmdEnabled);
+ return aParams->SetBooleanValue(STATE_ENABLED,outCmdEnabled);
+}
+
+NS_IMETHODIMP
+nsIndentCommand::IsCommandEnabled(const char * aCommandName,
+ nsISupports *refCon, bool *outCmdEnabled)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(refCon);
+ if (editor)
+ return editor->GetIsSelectionEditable(outCmdEnabled);
+
+ *outCmdEnabled = false;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsIndentCommand::DoCommand(const char *aCommandName, nsISupports *refCon)
+{
+ nsCOMPtr<nsIHTMLEditor> editor = do_QueryInterface(refCon);
+
+ nsresult rv = NS_OK;
+ if (editor) {
+ rv = editor->Indent(NS_LITERAL_STRING("indent"));
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsIndentCommand::DoCommandParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ return DoCommand(aCommandName, refCon);
+}
+
+NS_IMETHODIMP
+nsIndentCommand::GetCommandStateParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ bool outCmdEnabled = false;
+ IsCommandEnabled(aCommandName, refCon, &outCmdEnabled);
+ return aParams->SetBooleanValue(STATE_ENABLED,outCmdEnabled);
+}
+
+
+//OUTDENT
+
+NS_IMETHODIMP
+nsOutdentCommand::IsCommandEnabled(const char * aCommandName,
+ nsISupports *refCon,
+ bool *outCmdEnabled)
+{
+ *outCmdEnabled = false;
+
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(refCon);
+ if (editor) {
+ nsresult rv = editor->GetIsSelectionEditable(outCmdEnabled);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsOutdentCommand::DoCommand(const char *aCommandName, nsISupports *refCon)
+{
+ nsCOMPtr<nsIHTMLEditor> htmlEditor = do_QueryInterface(refCon);
+ if (htmlEditor)
+ return htmlEditor->Indent(NS_LITERAL_STRING("outdent"));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOutdentCommand::DoCommandParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ return DoCommand(aCommandName, refCon);
+}
+
+NS_IMETHODIMP
+nsOutdentCommand::GetCommandStateParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ bool outCmdEnabled = false;
+ IsCommandEnabled(aCommandName, refCon, &outCmdEnabled);
+ return aParams->SetBooleanValue(STATE_ENABLED,outCmdEnabled);
+}
+
+nsMultiStateCommand::nsMultiStateCommand()
+: nsBaseComposerCommand()
+{
+}
+
+nsMultiStateCommand::~nsMultiStateCommand()
+{
+}
+
+NS_IMPL_ISUPPORTS_INHERITED0(nsMultiStateCommand, nsBaseComposerCommand)
+
+NS_IMETHODIMP
+nsMultiStateCommand::IsCommandEnabled(const char * aCommandName,
+ nsISupports *refCon,
+ bool *outCmdEnabled)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(refCon);
+ // should be disabled sometimes, like if the current selection is an image
+ if (editor)
+ return editor->GetIsSelectionEditable(outCmdEnabled);
+
+ *outCmdEnabled = false;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsMultiStateCommand::DoCommand(const char *aCommandName, nsISupports *refCon)
+{
+#ifdef DEBUG
+ printf("who is calling nsMultiStateCommand::DoCommand \
+ (no implementation)? %s\n", aCommandName);
+#endif
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMultiStateCommand::DoCommandParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(refCon);
+
+ nsresult rv = NS_OK;
+ if (editor) {
+ nsAutoString tString;
+
+ if (aParams) {
+ nsXPIDLCString s;
+ rv = aParams->GetCStringValue(STATE_ATTRIBUTE, getter_Copies(s));
+ if (NS_SUCCEEDED(rv))
+ tString.AssignWithConversion(s);
+ else
+ rv = aParams->GetStringValue(STATE_ATTRIBUTE, tString);
+ }
+
+ rv = SetState(editor, tString);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMultiStateCommand::GetCommandStateParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(refCon);
+ nsresult rv = NS_OK;
+ if (editor) {
+ rv = GetCurrentState(editor, aParams);
+ }
+ return rv;
+}
+
+nsParagraphStateCommand::nsParagraphStateCommand()
+: nsMultiStateCommand()
+{
+}
+
+nsresult
+nsParagraphStateCommand::GetCurrentState(nsIEditor *aEditor,
+ nsICommandParams *aParams)
+{
+ NS_ASSERTION(aEditor, "Need an editor here");
+
+ nsCOMPtr<nsIHTMLEditor> htmlEditor = do_QueryInterface(aEditor);
+ NS_ENSURE_TRUE(htmlEditor, NS_ERROR_FAILURE);
+
+ bool outMixed;
+ nsAutoString outStateString;
+ nsresult rv = htmlEditor->GetParagraphState(&outMixed, outStateString);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString tOutStateString;
+ tOutStateString.AssignWithConversion(outStateString);
+ aParams->SetBooleanValue(STATE_MIXED,outMixed);
+ aParams->SetCStringValue(STATE_ATTRIBUTE, tOutStateString.get());
+ }
+ return rv;
+}
+
+
+nsresult
+nsParagraphStateCommand::SetState(nsIEditor *aEditor, nsString& newState)
+{
+ NS_ASSERTION(aEditor, "Need an editor here");
+ nsCOMPtr<nsIHTMLEditor> htmlEditor = do_QueryInterface(aEditor);
+ NS_ENSURE_TRUE(htmlEditor, NS_ERROR_FAILURE);
+
+ return htmlEditor->SetParagraphFormat(newState);
+}
+
+nsFontFaceStateCommand::nsFontFaceStateCommand()
+: nsMultiStateCommand()
+{
+}
+
+nsresult
+nsFontFaceStateCommand::GetCurrentState(nsIEditor *aEditor,
+ nsICommandParams *aParams)
+{
+ NS_ASSERTION(aEditor, "Need an editor here");
+ nsCOMPtr<nsIHTMLEditor> htmlEditor = do_QueryInterface(aEditor);
+ NS_ENSURE_TRUE(htmlEditor, NS_ERROR_FAILURE);
+
+ nsAutoString outStateString;
+ bool outMixed;
+ nsresult rv = htmlEditor->GetFontFaceState(&outMixed, outStateString);
+ if (NS_SUCCEEDED(rv)) {
+ aParams->SetBooleanValue(STATE_MIXED,outMixed);
+ aParams->SetCStringValue(STATE_ATTRIBUTE, NS_ConvertUTF16toUTF8(outStateString).get());
+ }
+ return rv;
+}
+
+
+nsresult
+nsFontFaceStateCommand::SetState(nsIEditor *aEditor, nsString& newState)
+{
+ NS_ASSERTION(aEditor, "Need an editor here");
+ nsCOMPtr<nsIHTMLEditor> htmlEditor = do_QueryInterface(aEditor);
+ NS_ENSURE_TRUE(htmlEditor, NS_ERROR_FAILURE);
+
+ if (newState.EqualsLiteral("tt")) {
+ // The old "teletype" attribute
+ nsresult rv = htmlEditor->SetInlineProperty(nsGkAtoms::tt, EmptyString(),
+ EmptyString());
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Clear existing font face
+ return htmlEditor->RemoveInlineProperty(nsGkAtoms::font,
+ NS_LITERAL_STRING("face"));
+ }
+
+ // Remove any existing TT nodes
+ nsresult rv = htmlEditor->RemoveInlineProperty(nsGkAtoms::tt, EmptyString());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (newState.IsEmpty() || newState.EqualsLiteral("normal")) {
+ return htmlEditor->RemoveInlineProperty(nsGkAtoms::font,
+ NS_LITERAL_STRING("face"));
+ }
+
+ return htmlEditor->SetInlineProperty(nsGkAtoms::font,
+ NS_LITERAL_STRING("face"), newState);
+}
+
+nsFontSizeStateCommand::nsFontSizeStateCommand()
+ : nsMultiStateCommand()
+{
+}
+
+// nsAutoCString tOutStateString;
+// tOutStateString.AssignWithConversion(outStateString);
+nsresult
+nsFontSizeStateCommand::GetCurrentState(nsIEditor *aEditor,
+ nsICommandParams *aParams)
+{
+ NS_ASSERTION(aEditor, "Need an editor here");
+ nsCOMPtr<nsIHTMLEditor> htmlEditor = do_QueryInterface(aEditor);
+ NS_ENSURE_TRUE(htmlEditor, NS_ERROR_INVALID_ARG);
+
+ nsAutoString outStateString;
+ nsCOMPtr<nsIAtom> fontAtom = NS_Atomize("font");
+ bool firstHas, anyHas, allHas;
+ nsresult rv = htmlEditor->GetInlinePropertyWithAttrValue(fontAtom,
+ NS_LITERAL_STRING("size"),
+ EmptyString(),
+ &firstHas, &anyHas, &allHas,
+ outStateString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString tOutStateString;
+ tOutStateString.AssignWithConversion(outStateString);
+ aParams->SetBooleanValue(STATE_MIXED, anyHas && !allHas);
+ aParams->SetCStringValue(STATE_ATTRIBUTE, tOutStateString.get());
+ aParams->SetBooleanValue(STATE_ENABLED, true);
+
+ return rv;
+}
+
+
+// acceptable values for "newState" are:
+// -2
+// -1
+// 0
+// +1
+// +2
+// +3
+// medium
+// normal
+nsresult
+nsFontSizeStateCommand::SetState(nsIEditor *aEditor, nsString& newState)
+{
+ NS_ASSERTION(aEditor, "Need an editor here");
+ nsCOMPtr<nsIHTMLEditor> htmlEditor = do_QueryInterface(aEditor);
+ NS_ENSURE_TRUE(htmlEditor, NS_ERROR_INVALID_ARG);
+
+ if (!newState.IsEmpty() &&
+ !newState.EqualsLiteral("normal") &&
+ !newState.EqualsLiteral("medium")) {
+ return htmlEditor->SetInlineProperty(nsGkAtoms::font,
+ NS_LITERAL_STRING("size"), newState);
+ }
+
+ // remove any existing font size, big or small
+ nsresult rv = htmlEditor->RemoveInlineProperty(nsGkAtoms::font,
+ NS_LITERAL_STRING("size"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = htmlEditor->RemoveInlineProperty(nsGkAtoms::big, EmptyString());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return htmlEditor->RemoveInlineProperty(nsGkAtoms::small, EmptyString());
+}
+
+nsFontColorStateCommand::nsFontColorStateCommand()
+: nsMultiStateCommand()
+{
+}
+
+nsresult
+nsFontColorStateCommand::GetCurrentState(nsIEditor *aEditor,
+ nsICommandParams *aParams)
+{
+ NS_ASSERTION(aEditor, "Need an editor here");
+
+ nsCOMPtr<nsIHTMLEditor> htmlEditor = do_QueryInterface(aEditor);
+ NS_ENSURE_TRUE(htmlEditor, NS_ERROR_FAILURE);
+
+ bool outMixed;
+ nsAutoString outStateString;
+ nsresult rv = htmlEditor->GetFontColorState(&outMixed, outStateString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString tOutStateString;
+ tOutStateString.AssignWithConversion(outStateString);
+ aParams->SetBooleanValue(STATE_MIXED, outMixed);
+ aParams->SetCStringValue(STATE_ATTRIBUTE, tOutStateString.get());
+ return NS_OK;
+}
+
+nsresult
+nsFontColorStateCommand::SetState(nsIEditor *aEditor, nsString& newState)
+{
+ NS_ASSERTION(aEditor, "Need an editor here");
+ nsCOMPtr<nsIHTMLEditor> htmlEditor = do_QueryInterface(aEditor);
+ NS_ENSURE_TRUE(htmlEditor, NS_ERROR_FAILURE);
+
+ if (newState.IsEmpty() || newState.EqualsLiteral("normal")) {
+ return htmlEditor->RemoveInlineProperty(nsGkAtoms::font,
+ NS_LITERAL_STRING("color"));
+ }
+
+ return htmlEditor->SetInlineProperty(nsGkAtoms::font,
+ NS_LITERAL_STRING("color"), newState);
+}
+
+nsHighlightColorStateCommand::nsHighlightColorStateCommand()
+: nsMultiStateCommand()
+{
+}
+
+nsresult
+nsHighlightColorStateCommand::GetCurrentState(nsIEditor *aEditor,
+ nsICommandParams *aParams)
+{
+ NS_ASSERTION(aEditor, "Need an editor here");
+ nsCOMPtr<nsIHTMLEditor> htmlEditor = do_QueryInterface(aEditor);
+ NS_ENSURE_TRUE(htmlEditor, NS_ERROR_FAILURE);
+
+ bool outMixed;
+ nsAutoString outStateString;
+ nsresult rv = htmlEditor->GetHighlightColorState(&outMixed, outStateString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString tOutStateString;
+ tOutStateString.AssignWithConversion(outStateString);
+ aParams->SetBooleanValue(STATE_MIXED, outMixed);
+ aParams->SetCStringValue(STATE_ATTRIBUTE, tOutStateString.get());
+ return NS_OK;
+}
+
+nsresult
+nsHighlightColorStateCommand::SetState(nsIEditor *aEditor, nsString& newState)
+{
+ NS_ASSERTION(aEditor, "Need an editor here");
+ nsCOMPtr<nsIHTMLEditor> htmlEditor = do_QueryInterface(aEditor);
+ NS_ENSURE_TRUE(htmlEditor, NS_ERROR_FAILURE);
+
+ if (newState.IsEmpty() || newState.EqualsLiteral("normal")) {
+ return htmlEditor->RemoveInlineProperty(nsGkAtoms::font,
+ NS_LITERAL_STRING("bgcolor"));
+ }
+
+ return htmlEditor->SetInlineProperty(nsGkAtoms::font,
+ NS_LITERAL_STRING("bgcolor"),
+ newState);
+}
+
+NS_IMETHODIMP
+nsHighlightColorStateCommand::IsCommandEnabled(const char * aCommandName,
+ nsISupports *refCon,
+ bool *outCmdEnabled)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(refCon);
+ if (editor)
+ return editor->GetIsSelectionEditable(outCmdEnabled);
+
+ *outCmdEnabled = false;
+ return NS_OK;
+}
+
+
+nsBackgroundColorStateCommand::nsBackgroundColorStateCommand()
+: nsMultiStateCommand()
+{
+}
+
+nsresult
+nsBackgroundColorStateCommand::GetCurrentState(nsIEditor *aEditor,
+ nsICommandParams *aParams)
+{
+ NS_ASSERTION(aEditor, "Need an editor here");
+
+ nsCOMPtr<nsIHTMLEditor> htmlEditor = do_QueryInterface(aEditor);
+ NS_ENSURE_TRUE(htmlEditor, NS_ERROR_FAILURE);
+
+ bool outMixed;
+ nsAutoString outStateString;
+ nsresult rv = htmlEditor->GetBackgroundColorState(&outMixed, outStateString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString tOutStateString;
+ tOutStateString.AssignWithConversion(outStateString);
+ aParams->SetBooleanValue(STATE_MIXED, outMixed);
+ aParams->SetCStringValue(STATE_ATTRIBUTE, tOutStateString.get());
+ return NS_OK;
+}
+
+nsresult
+nsBackgroundColorStateCommand::SetState(nsIEditor *aEditor, nsString& newState)
+{
+ NS_ASSERTION(aEditor, "Need an editor here");
+
+ nsCOMPtr<nsIHTMLEditor> htmlEditor = do_QueryInterface(aEditor);
+ NS_ENSURE_TRUE(htmlEditor, NS_ERROR_FAILURE);
+
+ return htmlEditor->SetBackgroundColor(newState);
+}
+
+nsAlignCommand::nsAlignCommand()
+: nsMultiStateCommand()
+{
+}
+
+nsresult
+nsAlignCommand::GetCurrentState(nsIEditor *aEditor, nsICommandParams *aParams)
+{
+ NS_ASSERTION(aEditor, "Need an editor here");
+
+ nsCOMPtr<nsIHTMLEditor> htmlEditor = do_QueryInterface(aEditor);
+ NS_ENSURE_TRUE(htmlEditor, NS_ERROR_FAILURE);
+
+ nsIHTMLEditor::EAlignment firstAlign;
+ bool outMixed;
+ nsresult rv = htmlEditor->GetAlignment(&outMixed, &firstAlign);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString outStateString;
+ switch (firstAlign) {
+ default:
+ case nsIHTMLEditor::eLeft:
+ outStateString.AssignLiteral("left");
+ break;
+
+ case nsIHTMLEditor::eCenter:
+ outStateString.AssignLiteral("center");
+ break;
+
+ case nsIHTMLEditor::eRight:
+ outStateString.AssignLiteral("right");
+ break;
+
+ case nsIHTMLEditor::eJustify:
+ outStateString.AssignLiteral("justify");
+ break;
+ }
+ nsAutoCString tOutStateString;
+ tOutStateString.AssignWithConversion(outStateString);
+ aParams->SetBooleanValue(STATE_MIXED,outMixed);
+ aParams->SetCStringValue(STATE_ATTRIBUTE, tOutStateString.get());
+ return NS_OK;
+}
+
+nsresult
+nsAlignCommand::SetState(nsIEditor *aEditor, nsString& newState)
+{
+ NS_ASSERTION(aEditor, "Need an editor here");
+
+ nsCOMPtr<nsIHTMLEditor> htmlEditor = do_QueryInterface(aEditor);
+ NS_ENSURE_TRUE(htmlEditor, NS_ERROR_FAILURE);
+
+ return htmlEditor->Align(newState);
+}
+
+nsAbsolutePositioningCommand::nsAbsolutePositioningCommand()
+: nsBaseStateUpdatingCommand(nsGkAtoms::_empty)
+{
+}
+
+NS_IMETHODIMP
+nsAbsolutePositioningCommand::IsCommandEnabled(const char * aCommandName,
+ nsISupports *aCommandRefCon,
+ bool *outCmdEnabled)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ nsCOMPtr<nsIHTMLAbsPosEditor> htmlEditor = do_QueryInterface(aCommandRefCon);
+ if (htmlEditor) {
+ bool isEditable = false;
+ nsresult rv = editor->GetIsSelectionEditable(&isEditable);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (isEditable)
+ return htmlEditor->GetAbsolutePositioningEnabled(outCmdEnabled);
+ }
+
+ *outCmdEnabled = false;
+ return NS_OK;
+}
+
+nsresult
+nsAbsolutePositioningCommand::GetCurrentState(nsIEditor *aEditor, nsICommandParams *aParams)
+{
+ NS_ASSERTION(aEditor, "Need an editor here");
+
+ nsCOMPtr<nsIHTMLAbsPosEditor> htmlEditor = do_QueryInterface(aEditor);
+ NS_ENSURE_TRUE(htmlEditor, NS_ERROR_FAILURE);
+
+ bool isEnabled;
+ htmlEditor->GetAbsolutePositioningEnabled(&isEnabled);
+ if (!isEnabled) {
+ aParams->SetBooleanValue(STATE_MIXED,false);
+ aParams->SetCStringValue(STATE_ATTRIBUTE, "");
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIDOMElement> elt;
+ nsresult rv = htmlEditor->GetAbsolutelyPositionedSelectionContainer(getter_AddRefs(elt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString outStateString;
+ if (elt)
+ outStateString.AssignLiteral("absolute");
+
+ aParams->SetBooleanValue(STATE_MIXED,false);
+ aParams->SetCStringValue(STATE_ATTRIBUTE, NS_ConvertUTF16toUTF8(outStateString).get());
+ return NS_OK;
+}
+
+nsresult
+nsAbsolutePositioningCommand::ToggleState(nsIEditor *aEditor)
+{
+ NS_ASSERTION(aEditor, "Need an editor here");
+
+ nsCOMPtr<nsIHTMLAbsPosEditor> htmlEditor = do_QueryInterface(aEditor);
+ NS_ENSURE_TRUE(htmlEditor, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIDOMElement> elt;
+ nsresult rv = htmlEditor->GetAbsolutelyPositionedSelectionContainer(getter_AddRefs(elt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return htmlEditor->AbsolutePositionSelection(!elt);
+}
+
+
+NS_IMETHODIMP
+nsDecreaseZIndexCommand::IsCommandEnabled(const char * aCommandName,
+ nsISupports *refCon,
+ bool *outCmdEnabled)
+{
+ nsCOMPtr<nsIHTMLAbsPosEditor> htmlEditor = do_QueryInterface(refCon);
+ NS_ENSURE_TRUE(htmlEditor, NS_ERROR_FAILURE);
+
+ htmlEditor->GetAbsolutePositioningEnabled(outCmdEnabled);
+ if (!(*outCmdEnabled))
+ return NS_OK;
+
+ nsCOMPtr<nsIDOMElement> positionedElement;
+ htmlEditor->GetPositionedElement(getter_AddRefs(positionedElement));
+ *outCmdEnabled = false;
+ if (positionedElement) {
+ int32_t z;
+ nsresult rv = htmlEditor->GetElementZIndex(positionedElement, &z);
+ NS_ENSURE_SUCCESS(rv, rv);
+ *outCmdEnabled = (z > 0);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDecreaseZIndexCommand::DoCommand(const char *aCommandName,
+ nsISupports *refCon)
+{
+ nsCOMPtr<nsIHTMLAbsPosEditor> htmlEditor = do_QueryInterface(refCon);
+ NS_ENSURE_TRUE(htmlEditor, NS_ERROR_NOT_IMPLEMENTED);
+
+ return htmlEditor->RelativeChangeZIndex(-1);
+}
+
+NS_IMETHODIMP
+nsDecreaseZIndexCommand::DoCommandParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ return DoCommand(aCommandName, refCon);
+}
+
+NS_IMETHODIMP
+nsDecreaseZIndexCommand::GetCommandStateParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ NS_ENSURE_ARG_POINTER(aParams);
+
+ bool enabled = false;
+ nsresult rv = IsCommandEnabled(aCommandName, refCon, &enabled);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return aParams->SetBooleanValue(STATE_ENABLED, enabled);
+}
+
+NS_IMETHODIMP
+nsIncreaseZIndexCommand::IsCommandEnabled(const char * aCommandName,
+ nsISupports *refCon,
+ bool *outCmdEnabled)
+{
+ nsCOMPtr<nsIHTMLAbsPosEditor> htmlEditor = do_QueryInterface(refCon);
+ NS_ENSURE_TRUE(htmlEditor, NS_ERROR_FAILURE);
+
+ htmlEditor->GetAbsolutePositioningEnabled(outCmdEnabled);
+ if (!(*outCmdEnabled))
+ return NS_OK;
+
+ nsCOMPtr<nsIDOMElement> positionedElement;
+ htmlEditor->GetPositionedElement(getter_AddRefs(positionedElement));
+ *outCmdEnabled = (nullptr != positionedElement);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsIncreaseZIndexCommand::DoCommand(const char *aCommandName,
+ nsISupports *refCon)
+{
+ nsCOMPtr<nsIHTMLAbsPosEditor> htmlEditor = do_QueryInterface(refCon);
+ NS_ENSURE_TRUE(htmlEditor, NS_ERROR_NOT_IMPLEMENTED);
+
+ return htmlEditor->RelativeChangeZIndex(1);
+}
+
+NS_IMETHODIMP
+nsIncreaseZIndexCommand::DoCommandParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ return DoCommand(aCommandName, refCon);
+}
+
+NS_IMETHODIMP
+nsIncreaseZIndexCommand::GetCommandStateParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ NS_ENSURE_ARG_POINTER(aParams);
+
+ bool enabled = false;
+ nsresult rv = IsCommandEnabled(aCommandName, refCon, &enabled);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return aParams->SetBooleanValue(STATE_ENABLED, enabled);
+}
+
+
+NS_IMETHODIMP
+nsRemoveStylesCommand::IsCommandEnabled(const char * aCommandName,
+ nsISupports *refCon,
+ bool *outCmdEnabled)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(refCon);
+ // test if we have any styles?
+ if (editor)
+ return editor->GetIsSelectionEditable(outCmdEnabled);
+
+ *outCmdEnabled = false;
+ return NS_OK;
+}
+
+
+
+NS_IMETHODIMP
+nsRemoveStylesCommand::DoCommand(const char *aCommandName,
+ nsISupports *refCon)
+{
+ nsCOMPtr<nsIHTMLEditor> editor = do_QueryInterface(refCon);
+
+ nsresult rv = NS_OK;
+ if (editor) {
+ rv = editor->RemoveAllInlineProperties();
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsRemoveStylesCommand::DoCommandParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ return DoCommand(aCommandName, refCon);
+}
+
+NS_IMETHODIMP
+nsRemoveStylesCommand::GetCommandStateParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ bool outCmdEnabled = false;
+ IsCommandEnabled(aCommandName, refCon, &outCmdEnabled);
+ return aParams->SetBooleanValue(STATE_ENABLED,outCmdEnabled);
+}
+
+NS_IMETHODIMP
+nsIncreaseFontSizeCommand::IsCommandEnabled(const char * aCommandName,
+ nsISupports *refCon,
+ bool *outCmdEnabled)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(refCon);
+ // test if we are at max size?
+ if (editor)
+ return editor->GetIsSelectionEditable(outCmdEnabled);
+
+ *outCmdEnabled = false;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsIncreaseFontSizeCommand::DoCommand(const char *aCommandName,
+ nsISupports *refCon)
+{
+ nsCOMPtr<nsIHTMLEditor> editor = do_QueryInterface(refCon);
+
+ nsresult rv = NS_OK;
+ if (editor) {
+ rv = editor->IncreaseFontSize();
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsIncreaseFontSizeCommand::DoCommandParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ return DoCommand(aCommandName, refCon);
+}
+
+NS_IMETHODIMP
+nsIncreaseFontSizeCommand::GetCommandStateParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ bool outCmdEnabled = false;
+ IsCommandEnabled(aCommandName, refCon, &outCmdEnabled);
+ return aParams->SetBooleanValue(STATE_ENABLED,outCmdEnabled);
+}
+
+NS_IMETHODIMP
+nsDecreaseFontSizeCommand::IsCommandEnabled(const char * aCommandName,
+ nsISupports *refCon,
+ bool *outCmdEnabled)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(refCon);
+ // test if we are at min size?
+ if (editor)
+ return editor->GetIsSelectionEditable(outCmdEnabled);
+
+ *outCmdEnabled = false;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsDecreaseFontSizeCommand::DoCommand(const char *aCommandName,
+ nsISupports *refCon)
+{
+ nsCOMPtr<nsIHTMLEditor> editor = do_QueryInterface(refCon);
+
+ nsresult rv = NS_OK;
+ if (editor) {
+ rv = editor->DecreaseFontSize();
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsDecreaseFontSizeCommand::DoCommandParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ return DoCommand(aCommandName, refCon);
+}
+
+NS_IMETHODIMP
+nsDecreaseFontSizeCommand::GetCommandStateParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ bool outCmdEnabled = false;
+ IsCommandEnabled(aCommandName, refCon, &outCmdEnabled);
+ return aParams->SetBooleanValue(STATE_ENABLED,outCmdEnabled);
+}
+
+NS_IMETHODIMP
+nsInsertHTMLCommand::IsCommandEnabled(const char * aCommandName,
+ nsISupports *refCon,
+ bool *outCmdEnabled)
+{
+ NS_ENSURE_ARG_POINTER(outCmdEnabled);
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(refCon);
+ if (editor)
+ return editor->GetIsSelectionEditable(outCmdEnabled);
+
+ *outCmdEnabled = false;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsInsertHTMLCommand::DoCommand(const char *aCommandName, nsISupports *refCon)
+{
+ // If nsInsertHTMLCommand is called with no parameters, it was probably called with
+ // an empty string parameter ''. In this case, it should act the same as the delete command
+ NS_ENSURE_ARG_POINTER(refCon);
+
+ nsCOMPtr<nsIHTMLEditor> editor = do_QueryInterface(refCon);
+ NS_ENSURE_TRUE(editor, NS_ERROR_NOT_IMPLEMENTED);
+
+ nsString html = EmptyString();
+ return editor->InsertHTML(html);
+}
+
+NS_IMETHODIMP
+nsInsertHTMLCommand::DoCommandParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ NS_ENSURE_ARG_POINTER(aParams);
+ NS_ENSURE_ARG_POINTER(refCon);
+
+ nsCOMPtr<nsIHTMLEditor> editor = do_QueryInterface(refCon);
+ NS_ENSURE_TRUE(editor, NS_ERROR_NOT_IMPLEMENTED);
+
+ // Get HTML source string to insert from command params
+ nsAutoString html;
+ nsresult rv = aParams->GetStringValue(STATE_DATA, html);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return editor->InsertHTML(html);
+}
+
+NS_IMETHODIMP
+nsInsertHTMLCommand::GetCommandStateParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ NS_ENSURE_ARG_POINTER(aParams);
+ NS_ENSURE_ARG_POINTER(refCon);
+
+ bool outCmdEnabled = false;
+ IsCommandEnabled(aCommandName, refCon, &outCmdEnabled);
+ return aParams->SetBooleanValue(STATE_ENABLED, outCmdEnabled);
+}
+
+NS_IMPL_ISUPPORTS_INHERITED0(nsInsertTagCommand, nsBaseComposerCommand)
+
+nsInsertTagCommand::nsInsertTagCommand(nsIAtom* aTagName)
+: nsBaseComposerCommand()
+, mTagName(aTagName)
+{
+ MOZ_ASSERT(mTagName);
+}
+
+nsInsertTagCommand::~nsInsertTagCommand()
+{
+}
+
+NS_IMETHODIMP
+nsInsertTagCommand::IsCommandEnabled(const char * aCommandName,
+ nsISupports *refCon,
+ bool *outCmdEnabled)
+{
+ NS_ENSURE_ARG_POINTER(outCmdEnabled);
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(refCon);
+ if (editor)
+ return editor->GetIsSelectionEditable(outCmdEnabled);
+
+ *outCmdEnabled = false;
+ return NS_OK;
+}
+
+
+// corresponding STATE_ATTRIBUTE is: src (img) and href (a)
+NS_IMETHODIMP
+nsInsertTagCommand::DoCommand(const char *aCmdName, nsISupports *refCon)
+{
+ NS_ENSURE_TRUE(mTagName == nsGkAtoms::hr, NS_ERROR_NOT_IMPLEMENTED);
+
+ nsCOMPtr<nsIHTMLEditor> editor = do_QueryInterface(refCon);
+ NS_ENSURE_TRUE(editor, NS_ERROR_NOT_IMPLEMENTED);
+
+ nsCOMPtr<nsIDOMElement> domElem;
+ nsresult rv = editor->CreateElementWithDefaults(
+ nsDependentAtomString(mTagName), getter_AddRefs(domElem));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return editor->InsertElementAtSelection(domElem, true);
+}
+
+NS_IMETHODIMP
+nsInsertTagCommand::DoCommandParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ NS_ENSURE_ARG_POINTER(refCon);
+
+ // inserting an hr shouldn't have an parameters, just call DoCommand for that
+ if (mTagName == nsGkAtoms::hr) {
+ return DoCommand(aCommandName, refCon);
+ }
+
+ NS_ENSURE_ARG_POINTER(aParams);
+
+ nsCOMPtr<nsIHTMLEditor> editor = do_QueryInterface(refCon);
+ NS_ENSURE_TRUE(editor, NS_ERROR_NOT_IMPLEMENTED);
+
+ // do we have an href to use for creating link?
+ nsXPIDLCString s;
+ nsresult rv = aParams->GetCStringValue(STATE_ATTRIBUTE, getter_Copies(s));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoString attrib; attrib.AssignWithConversion(s);
+
+ if (attrib.IsEmpty())
+ return NS_ERROR_INVALID_ARG;
+
+ // filter out tags we don't know how to insert
+ nsAutoString attributeType;
+ if (mTagName == nsGkAtoms::a) {
+ attributeType.AssignLiteral("href");
+ } else if (mTagName == nsGkAtoms::img) {
+ attributeType.AssignLiteral("src");
+ } else {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ nsCOMPtr<nsIDOMElement> domElem;
+ rv = editor->CreateElementWithDefaults(nsDependentAtomString(mTagName),
+ getter_AddRefs(domElem));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = domElem->SetAttribute(attributeType, attrib);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // do actual insertion
+ if (mTagName == nsGkAtoms::a)
+ return editor->InsertLinkAroundSelection(domElem);
+
+ return editor->InsertElementAtSelection(domElem, true);
+}
+
+NS_IMETHODIMP
+nsInsertTagCommand::GetCommandStateParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ NS_ENSURE_ARG_POINTER(aParams);
+ NS_ENSURE_ARG_POINTER(refCon);
+
+ bool outCmdEnabled = false;
+ IsCommandEnabled(aCommandName, refCon, &outCmdEnabled);
+ return aParams->SetBooleanValue(STATE_ENABLED, outCmdEnabled);
+}
+
+
+/****************************/
+//HELPER METHODS
+/****************************/
+
+nsresult
+GetListState(nsIHTMLEditor* aEditor, bool* aMixed, nsAString& aLocalName)
+{
+ MOZ_ASSERT(aEditor);
+ MOZ_ASSERT(aMixed);
+
+ *aMixed = false;
+ aLocalName.Truncate();
+
+ bool bOL, bUL, bDL;
+ nsresult rv = aEditor->GetListState(aMixed, &bOL, &bUL, &bDL);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (*aMixed) {
+ return NS_OK;
+ }
+
+ if (bOL) {
+ aLocalName.AssignLiteral("ol");
+ } else if (bUL) {
+ aLocalName.AssignLiteral("ul");
+ } else if (bDL) {
+ aLocalName.AssignLiteral("dl");
+ }
+ return NS_OK;
+}
+
+nsresult
+RemoveOneProperty(nsIHTMLEditor* aEditor, const nsAString& aProp)
+{
+ MOZ_ASSERT(aEditor);
+
+ /// XXX Hack alert! Look in nsIEditProperty.h for this
+ nsCOMPtr<nsIAtom> styleAtom = NS_Atomize(aProp);
+ NS_ENSURE_TRUE(styleAtom, NS_ERROR_OUT_OF_MEMORY);
+
+ return aEditor->RemoveInlineProperty(styleAtom, EmptyString());
+}
+
+
+// the name of the attribute here should be the contents of the appropriate
+// tag, e.g. 'b' for bold, 'i' for italics.
+nsresult
+RemoveTextProperty(nsIHTMLEditor* aEditor, const nsAString& aProp)
+{
+ MOZ_ASSERT(aEditor);
+
+ if (aProp.LowerCaseEqualsLiteral("all")) {
+ return aEditor->RemoveAllInlineProperties();
+ }
+
+ return RemoveOneProperty(aEditor, aProp);
+}
+
+// the name of the attribute here should be the contents of the appropriate
+// tag, e.g. 'b' for bold, 'i' for italics.
+nsresult
+SetTextProperty(nsIHTMLEditor* aEditor, const nsAString& aProp)
+{
+ MOZ_ASSERT(aEditor);
+
+ /// XXX Hack alert! Look in nsIEditProperty.h for this
+ nsCOMPtr<nsIAtom> styleAtom = NS_Atomize(aProp);
+ NS_ENSURE_TRUE(styleAtom, NS_ERROR_OUT_OF_MEMORY);
+
+ return aEditor->SetInlineProperty(styleAtom, EmptyString(), EmptyString());
+}
diff --git a/editor/composer/nsComposerCommands.h b/editor/composer/nsComposerCommands.h
new file mode 100644
index 000000000..3d3855d40
--- /dev/null
+++ b/editor/composer/nsComposerCommands.h
@@ -0,0 +1,281 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsComposerCommands_h_
+#define nsComposerCommands_h_
+
+#include "nsIControllerCommand.h"
+#include "nsISupportsImpl.h" // for NS_DECL_ISUPPORTS_INHERITED, etc
+#include "nscore.h" // for nsresult, NS_IMETHOD
+
+class nsIAtom;
+class nsICommandParams;
+class nsIEditor;
+class nsISupports;
+class nsString;
+
+// This is a virtual base class for commands registered with the composer controller.
+// Note that such commands are instantiated once per composer, so can store state.
+// Also note that IsCommandEnabled can be called with an editor that may not
+// have an editor yet (because the document is loading). Most commands will want
+// to return false in this case.
+// Don't hold on to any references to the editor or document from
+// your command. This will cause leaks. Also, be aware that the document the
+// editor is editing can change under you (if the user Reverts the file, for
+// instance).
+class nsBaseComposerCommand : public nsIControllerCommand
+{
+protected:
+ virtual ~nsBaseComposerCommand() {}
+
+public:
+
+ nsBaseComposerCommand();
+
+ // nsISupports
+ NS_DECL_ISUPPORTS
+
+ // nsIControllerCommand. Declared longhand so we can make them pure virtual
+ NS_IMETHOD IsCommandEnabled(const char * aCommandName, nsISupports *aCommandRefCon, bool *_retval) override = 0;
+ NS_IMETHOD DoCommand(const char * aCommandName, nsISupports *aCommandRefCon) override = 0;
+
+};
+
+
+#define NS_DECL_COMPOSER_COMMAND(_cmd) \
+class _cmd : public nsBaseComposerCommand \
+{ \
+public: \
+ NS_DECL_NSICONTROLLERCOMMAND \
+};
+
+// virtual base class for commands that need to save and update Boolean state (like styles etc)
+class nsBaseStateUpdatingCommand : public nsBaseComposerCommand
+{
+public:
+ explicit nsBaseStateUpdatingCommand(nsIAtom* aTagName);
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ NS_DECL_NSICONTROLLERCOMMAND
+
+protected:
+ virtual ~nsBaseStateUpdatingCommand();
+
+ // get the current state (on or off) for this style or block format
+ virtual nsresult GetCurrentState(nsIEditor* aEditor, nsICommandParams* aParams) = 0;
+
+ // add/remove the style
+ virtual nsresult ToggleState(nsIEditor* aEditor) = 0;
+
+protected:
+ nsIAtom* mTagName;
+};
+
+
+// Shared class for the various style updating commands like bold, italics etc.
+// Suitable for commands whose state is either 'on' or 'off'.
+class nsStyleUpdatingCommand : public nsBaseStateUpdatingCommand
+{
+public:
+ explicit nsStyleUpdatingCommand(nsIAtom* aTagName);
+
+protected:
+
+ // get the current state (on or off) for this style or block format
+ virtual nsresult GetCurrentState(nsIEditor* aEditor, nsICommandParams* aParams);
+
+ // add/remove the style
+ virtual nsresult ToggleState(nsIEditor* aEditor);
+};
+
+
+class nsInsertTagCommand : public nsBaseComposerCommand
+{
+public:
+ explicit nsInsertTagCommand(nsIAtom* aTagName);
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ NS_DECL_NSICONTROLLERCOMMAND
+
+protected:
+ virtual ~nsInsertTagCommand();
+
+ nsIAtom* mTagName;
+};
+
+
+class nsListCommand : public nsBaseStateUpdatingCommand
+{
+public:
+ explicit nsListCommand(nsIAtom* aTagName);
+
+protected:
+
+ // get the current state (on or off) for this style or block format
+ virtual nsresult GetCurrentState(nsIEditor* aEditor, nsICommandParams* aParams);
+
+ // add/remove the style
+ virtual nsresult ToggleState(nsIEditor* aEditor);
+};
+
+class nsListItemCommand : public nsBaseStateUpdatingCommand
+{
+public:
+ explicit nsListItemCommand(nsIAtom* aTagName);
+
+protected:
+
+ // get the current state (on or off) for this style or block format
+ virtual nsresult GetCurrentState(nsIEditor* aEditor, nsICommandParams* aParams);
+
+ // add/remove the style
+ virtual nsresult ToggleState(nsIEditor* aEditor);
+};
+
+// Base class for commands whose state consists of a string (e.g. para format)
+class nsMultiStateCommand : public nsBaseComposerCommand
+{
+public:
+
+ nsMultiStateCommand();
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSICONTROLLERCOMMAND
+
+protected:
+ virtual ~nsMultiStateCommand();
+
+ virtual nsresult GetCurrentState(nsIEditor *aEditor, nsICommandParams* aParams) =0;
+ virtual nsresult SetState(nsIEditor *aEditor, nsString& newState) = 0;
+
+};
+
+
+class nsParagraphStateCommand : public nsMultiStateCommand
+{
+public:
+ nsParagraphStateCommand();
+
+protected:
+
+ virtual nsresult GetCurrentState(nsIEditor *aEditor, nsICommandParams* aParams);
+ virtual nsresult SetState(nsIEditor *aEditor, nsString& newState);
+};
+
+class nsFontFaceStateCommand : public nsMultiStateCommand
+{
+public:
+ nsFontFaceStateCommand();
+
+protected:
+
+ virtual nsresult GetCurrentState(nsIEditor *aEditor, nsICommandParams* aParams);
+ virtual nsresult SetState(nsIEditor *aEditor, nsString& newState);
+};
+
+class nsFontSizeStateCommand : public nsMultiStateCommand
+{
+public:
+ nsFontSizeStateCommand();
+
+protected:
+
+ virtual nsresult GetCurrentState(nsIEditor *aEditor,
+ nsICommandParams* aParams);
+ virtual nsresult SetState(nsIEditor *aEditor, nsString& newState);
+};
+
+class nsHighlightColorStateCommand : public nsMultiStateCommand
+{
+public:
+ nsHighlightColorStateCommand();
+
+protected:
+
+ NS_IMETHOD IsCommandEnabled(const char *aCommandName, nsISupports *aCommandRefCon, bool *_retval);
+ virtual nsresult GetCurrentState(nsIEditor *aEditor, nsICommandParams* aParams);
+ virtual nsresult SetState(nsIEditor *aEditor, nsString& newState);
+
+};
+
+class nsFontColorStateCommand : public nsMultiStateCommand
+{
+public:
+ nsFontColorStateCommand();
+
+protected:
+
+ virtual nsresult GetCurrentState(nsIEditor *aEditor, nsICommandParams* aParams);
+ virtual nsresult SetState(nsIEditor *aEditor, nsString& newState);
+};
+
+class nsAlignCommand : public nsMultiStateCommand
+{
+public:
+ nsAlignCommand();
+
+protected:
+
+ virtual nsresult GetCurrentState(nsIEditor *aEditor, nsICommandParams* aParams);
+ virtual nsresult SetState(nsIEditor *aEditor, nsString& newState);
+};
+
+class nsBackgroundColorStateCommand : public nsMultiStateCommand
+{
+public:
+ nsBackgroundColorStateCommand();
+
+protected:
+
+ virtual nsresult GetCurrentState(nsIEditor *aEditor, nsICommandParams* aParams);
+ virtual nsresult SetState(nsIEditor *aEditor, nsString& newState);
+};
+
+class nsAbsolutePositioningCommand : public nsBaseStateUpdatingCommand
+{
+public:
+ nsAbsolutePositioningCommand();
+
+protected:
+
+ NS_IMETHOD IsCommandEnabled(const char *aCommandName, nsISupports *aCommandRefCon, bool *_retval);
+ virtual nsresult GetCurrentState(nsIEditor* aEditor, nsICommandParams* aParams);
+ virtual nsresult ToggleState(nsIEditor* aEditor);
+};
+
+// composer commands
+
+NS_DECL_COMPOSER_COMMAND(nsCloseCommand)
+NS_DECL_COMPOSER_COMMAND(nsDocumentStateCommand)
+NS_DECL_COMPOSER_COMMAND(nsSetDocumentStateCommand)
+NS_DECL_COMPOSER_COMMAND(nsSetDocumentOptionsCommand)
+//NS_DECL_COMPOSER_COMMAND(nsPrintingCommands)
+
+NS_DECL_COMPOSER_COMMAND(nsDecreaseZIndexCommand)
+NS_DECL_COMPOSER_COMMAND(nsIncreaseZIndexCommand)
+
+// Generic commands
+
+// File menu
+NS_DECL_COMPOSER_COMMAND(nsNewCommands) // handles 'new' anything
+
+// Edit menu
+NS_DECL_COMPOSER_COMMAND(nsPasteNoFormattingCommand)
+
+// Block transformations
+NS_DECL_COMPOSER_COMMAND(nsIndentCommand)
+NS_DECL_COMPOSER_COMMAND(nsOutdentCommand)
+
+NS_DECL_COMPOSER_COMMAND(nsRemoveListCommand)
+NS_DECL_COMPOSER_COMMAND(nsRemoveStylesCommand)
+NS_DECL_COMPOSER_COMMAND(nsIncreaseFontSizeCommand)
+NS_DECL_COMPOSER_COMMAND(nsDecreaseFontSizeCommand)
+
+// Insert content commands
+NS_DECL_COMPOSER_COMMAND(nsInsertHTMLCommand)
+
+#endif // nsComposerCommands_h_
diff --git a/editor/composer/nsComposerCommandsUpdater.cpp b/editor/composer/nsComposerCommandsUpdater.cpp
new file mode 100644
index 000000000..4999f7b92
--- /dev/null
+++ b/editor/composer/nsComposerCommandsUpdater.cpp
@@ -0,0 +1,381 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/mozalloc.h" // for operator new
+#include "nsAString.h"
+#include "nsComponentManagerUtils.h" // for do_CreateInstance
+#include "nsComposerCommandsUpdater.h"
+#include "nsDebug.h" // for NS_ENSURE_TRUE, etc
+#include "nsError.h" // for NS_OK, NS_ERROR_FAILURE, etc
+#include "nsICommandManager.h" // for nsICommandManager
+#include "nsID.h" // for NS_GET_IID, etc
+#include "nsIDOMWindow.h" // for nsIDOMWindow
+#include "nsIDocShell.h" // for nsIDocShell
+#include "nsIInterfaceRequestorUtils.h" // for do_GetInterface
+#include "nsISelection.h" // for nsISelection
+#include "nsITransactionManager.h" // for nsITransactionManager
+#include "nsLiteralString.h" // for NS_LITERAL_STRING
+#include "nsPICommandUpdater.h" // for nsPICommandUpdater
+#include "nsPIDOMWindow.h" // for nsPIDOMWindow
+
+class nsIDOMDocument;
+class nsITransaction;
+
+nsComposerCommandsUpdater::nsComposerCommandsUpdater()
+: mDirtyState(eStateUninitialized)
+, mSelectionCollapsed(eStateUninitialized)
+, mFirstDoOfFirstUndo(true)
+{
+}
+
+nsComposerCommandsUpdater::~nsComposerCommandsUpdater()
+{
+ // cancel any outstanding update timer
+ if (mUpdateTimer) {
+ mUpdateTimer->Cancel();
+ }
+}
+
+NS_IMPL_ISUPPORTS(nsComposerCommandsUpdater, nsISelectionListener,
+ nsIDocumentStateListener, nsITransactionListener, nsITimerCallback)
+
+#if 0
+#pragma mark -
+#endif
+
+NS_IMETHODIMP
+nsComposerCommandsUpdater::NotifyDocumentCreated()
+{
+ // Trigger an nsIObserve notification that the document has been created
+ UpdateOneCommand("obs_documentCreated");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsComposerCommandsUpdater::NotifyDocumentWillBeDestroyed()
+{
+ // cancel any outstanding update timer
+ if (mUpdateTimer) {
+ mUpdateTimer->Cancel();
+ mUpdateTimer = nullptr;
+ }
+
+ // We can't call this right now; it is too late in some cases and the window
+ // is already partially destructed (e.g. JS objects may be gone).
+#if 0
+ // Trigger an nsIObserve notification that the document will be destroyed
+ UpdateOneCommand("obs_documentWillBeDestroyed");
+#endif
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsComposerCommandsUpdater::NotifyDocumentStateChanged(bool aNowDirty)
+{
+ // update document modified. We should have some other notifications for this too.
+ return UpdateDirtyState(aNowDirty);
+}
+
+NS_IMETHODIMP
+nsComposerCommandsUpdater::NotifySelectionChanged(nsIDOMDocument *,
+ nsISelection *, int16_t)
+{
+ return PrimeUpdateTimer();
+}
+
+#if 0
+#pragma mark -
+#endif
+
+NS_IMETHODIMP
+nsComposerCommandsUpdater::WillDo(nsITransactionManager *aManager,
+ nsITransaction *aTransaction, bool *aInterrupt)
+{
+ *aInterrupt = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsComposerCommandsUpdater::DidDo(nsITransactionManager *aManager,
+ nsITransaction *aTransaction, nsresult aDoResult)
+{
+ // only need to update if the status of the Undo menu item changes.
+ int32_t undoCount;
+ aManager->GetNumberOfUndoItems(&undoCount);
+ if (undoCount == 1) {
+ if (mFirstDoOfFirstUndo) {
+ UpdateCommandGroup(NS_LITERAL_STRING("undo"));
+ }
+ mFirstDoOfFirstUndo = false;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsComposerCommandsUpdater::WillUndo(nsITransactionManager *aManager,
+ nsITransaction *aTransaction,
+ bool *aInterrupt)
+{
+ *aInterrupt = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsComposerCommandsUpdater::DidUndo(nsITransactionManager *aManager,
+ nsITransaction *aTransaction,
+ nsresult aUndoResult)
+{
+ int32_t undoCount;
+ aManager->GetNumberOfUndoItems(&undoCount);
+ if (undoCount == 0)
+ mFirstDoOfFirstUndo = true; // reset the state for the next do
+
+ UpdateCommandGroup(NS_LITERAL_STRING("undo"));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsComposerCommandsUpdater::WillRedo(nsITransactionManager *aManager,
+ nsITransaction *aTransaction,
+ bool *aInterrupt)
+{
+ *aInterrupt = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsComposerCommandsUpdater::DidRedo(nsITransactionManager *aManager,
+ nsITransaction *aTransaction,
+ nsresult aRedoResult)
+{
+ UpdateCommandGroup(NS_LITERAL_STRING("undo"));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsComposerCommandsUpdater::WillBeginBatch(nsITransactionManager *aManager,
+ bool *aInterrupt)
+{
+ *aInterrupt = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsComposerCommandsUpdater::DidBeginBatch(nsITransactionManager *aManager,
+ nsresult aResult)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsComposerCommandsUpdater::WillEndBatch(nsITransactionManager *aManager,
+ bool *aInterrupt)
+{
+ *aInterrupt = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsComposerCommandsUpdater::DidEndBatch(nsITransactionManager *aManager,
+ nsresult aResult)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsComposerCommandsUpdater::WillMerge(nsITransactionManager *aManager,
+ nsITransaction *aTopTransaction,
+ nsITransaction *aTransactionToMerge,
+ bool *aInterrupt)
+{
+ *aInterrupt = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsComposerCommandsUpdater::DidMerge(nsITransactionManager *aManager,
+ nsITransaction *aTopTransaction,
+ nsITransaction *aTransactionToMerge,
+ bool aDidMerge, nsresult aMergeResult)
+{
+ return NS_OK;
+}
+
+#if 0
+#pragma mark -
+#endif
+
+nsresult
+nsComposerCommandsUpdater::Init(nsPIDOMWindowOuter* aDOMWindow)
+{
+ NS_ENSURE_ARG(aDOMWindow);
+ mDOMWindow = do_GetWeakReference(aDOMWindow);
+ mDocShell = do_GetWeakReference(aDOMWindow->GetDocShell());
+ return NS_OK;
+}
+
+nsresult
+nsComposerCommandsUpdater::PrimeUpdateTimer()
+{
+ if (!mUpdateTimer) {
+ nsresult rv = NS_OK;
+ mUpdateTimer = do_CreateInstance("@mozilla.org/timer;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ const uint32_t kUpdateTimerDelay = 150;
+ return mUpdateTimer->InitWithCallback(static_cast<nsITimerCallback*>(this),
+ kUpdateTimerDelay,
+ nsITimer::TYPE_ONE_SHOT);
+}
+
+
+void nsComposerCommandsUpdater::TimerCallback()
+{
+ // if the selection state has changed, update stuff
+ bool isCollapsed = SelectionIsCollapsed();
+ if (static_cast<int8_t>(isCollapsed) != mSelectionCollapsed) {
+ UpdateCommandGroup(NS_LITERAL_STRING("select"));
+ mSelectionCollapsed = isCollapsed;
+ }
+
+ // isn't this redundant with the UpdateCommandGroup above?
+ // can we just nuke the above call? or create a meta command group?
+ UpdateCommandGroup(NS_LITERAL_STRING("style"));
+}
+
+nsresult
+nsComposerCommandsUpdater::UpdateDirtyState(bool aNowDirty)
+{
+ if (mDirtyState != static_cast<int8_t>(aNowDirty)) {
+ UpdateCommandGroup(NS_LITERAL_STRING("save"));
+ UpdateCommandGroup(NS_LITERAL_STRING("undo"));
+ mDirtyState = aNowDirty;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsComposerCommandsUpdater::UpdateCommandGroup(const nsAString& aCommandGroup)
+{
+ nsCOMPtr<nsPICommandUpdater> commandUpdater = GetCommandUpdater();
+ NS_ENSURE_TRUE(commandUpdater, NS_ERROR_FAILURE);
+
+
+ // This hardcoded list of commands is temporary.
+ // This code should use nsIControllerCommandGroup.
+ if (aCommandGroup.EqualsLiteral("undo")) {
+ commandUpdater->CommandStatusChanged("cmd_undo");
+ commandUpdater->CommandStatusChanged("cmd_redo");
+ return NS_OK;
+ }
+
+ if (aCommandGroup.EqualsLiteral("select") ||
+ aCommandGroup.EqualsLiteral("style")) {
+ commandUpdater->CommandStatusChanged("cmd_bold");
+ commandUpdater->CommandStatusChanged("cmd_italic");
+ commandUpdater->CommandStatusChanged("cmd_underline");
+ commandUpdater->CommandStatusChanged("cmd_tt");
+
+ commandUpdater->CommandStatusChanged("cmd_strikethrough");
+ commandUpdater->CommandStatusChanged("cmd_superscript");
+ commandUpdater->CommandStatusChanged("cmd_subscript");
+ commandUpdater->CommandStatusChanged("cmd_nobreak");
+
+ commandUpdater->CommandStatusChanged("cmd_em");
+ commandUpdater->CommandStatusChanged("cmd_strong");
+ commandUpdater->CommandStatusChanged("cmd_cite");
+ commandUpdater->CommandStatusChanged("cmd_abbr");
+ commandUpdater->CommandStatusChanged("cmd_acronym");
+ commandUpdater->CommandStatusChanged("cmd_code");
+ commandUpdater->CommandStatusChanged("cmd_samp");
+ commandUpdater->CommandStatusChanged("cmd_var");
+
+ commandUpdater->CommandStatusChanged("cmd_increaseFont");
+ commandUpdater->CommandStatusChanged("cmd_decreaseFont");
+
+ commandUpdater->CommandStatusChanged("cmd_paragraphState");
+ commandUpdater->CommandStatusChanged("cmd_fontFace");
+ commandUpdater->CommandStatusChanged("cmd_fontColor");
+ commandUpdater->CommandStatusChanged("cmd_backgroundColor");
+ commandUpdater->CommandStatusChanged("cmd_highlight");
+ return NS_OK;
+ }
+
+ if (aCommandGroup.EqualsLiteral("save")) {
+ // save commands (most are not in C++)
+ commandUpdater->CommandStatusChanged("cmd_setDocumentModified");
+ commandUpdater->CommandStatusChanged("cmd_save");
+ return NS_OK;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsComposerCommandsUpdater::UpdateOneCommand(const char *aCommand)
+{
+ nsCOMPtr<nsPICommandUpdater> commandUpdater = GetCommandUpdater();
+ NS_ENSURE_TRUE(commandUpdater, NS_ERROR_FAILURE);
+
+ commandUpdater->CommandStatusChanged(aCommand);
+
+ return NS_OK;
+}
+
+bool
+nsComposerCommandsUpdater::SelectionIsCollapsed()
+{
+ nsCOMPtr<nsPIDOMWindowOuter> domWindow = do_QueryReferent(mDOMWindow);
+ NS_ENSURE_TRUE(domWindow, true);
+
+ nsCOMPtr<nsISelection> domSelection = domWindow->GetSelection();
+ if (NS_WARN_IF(!domSelection)) {
+ return false;
+ }
+
+ bool selectionCollapsed = false;
+ domSelection->GetIsCollapsed(&selectionCollapsed);
+ return selectionCollapsed;
+}
+
+already_AddRefed<nsPICommandUpdater>
+nsComposerCommandsUpdater::GetCommandUpdater()
+{
+ nsCOMPtr<nsIDocShell> docShell = do_QueryReferent(mDocShell);
+ NS_ENSURE_TRUE(docShell, nullptr);
+ nsCOMPtr<nsICommandManager> manager = docShell->GetCommandManager();
+ nsCOMPtr<nsPICommandUpdater> updater = do_QueryInterface(manager);
+ return updater.forget();
+}
+
+#if 0
+#pragma mark -
+#endif
+
+nsresult
+nsComposerCommandsUpdater::Notify(nsITimer *timer)
+{
+ NS_ASSERTION(timer == mUpdateTimer.get(), "Hey, this ain't my timer!");
+ TimerCallback();
+ return NS_OK;
+}
+
+#if 0
+#pragma mark -
+#endif
+
+
+nsresult
+NS_NewComposerCommandsUpdater(nsISelectionListener** aInstancePtrResult)
+{
+ RefPtr<nsComposerCommandsUpdater> newThang = new nsComposerCommandsUpdater;
+ newThang.forget(aInstancePtrResult);
+ return NS_OK;
+}
diff --git a/editor/composer/nsComposerCommandsUpdater.h b/editor/composer/nsComposerCommandsUpdater.h
new file mode 100644
index 000000000..3b853edeb
--- /dev/null
+++ b/editor/composer/nsComposerCommandsUpdater.h
@@ -0,0 +1,102 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+#ifndef nsComposerCommandsUpdater_h__
+#define nsComposerCommandsUpdater_h__
+
+#include "nsCOMPtr.h" // for already_AddRefed, nsCOMPtr
+#include "nsIDocumentStateListener.h"
+#include "nsISelectionListener.h"
+#include "nsISupportsImpl.h" // for NS_DECL_ISUPPORTS
+#include "nsITimer.h" // for NS_DECL_NSITIMERCALLBACK, etc
+#include "nsITransactionListener.h" // for nsITransactionListener
+#include "nsIWeakReferenceUtils.h" // for nsWeakPtr
+#include "nscore.h" // for NS_IMETHOD, nsresult, etc
+
+class nsPIDOMWindowOuter;
+class nsITransaction;
+class nsITransactionManager;
+class nsPICommandUpdater;
+
+class nsComposerCommandsUpdater : public nsISelectionListener,
+ public nsIDocumentStateListener,
+ public nsITransactionListener,
+ public nsITimerCallback
+{
+public:
+
+ nsComposerCommandsUpdater();
+
+ // nsISupports
+ NS_DECL_ISUPPORTS
+
+ // nsISelectionListener
+ NS_DECL_NSISELECTIONLISTENER
+
+ // nsIDocumentStateListener
+ NS_DECL_NSIDOCUMENTSTATELISTENER
+
+ // nsITimerCallback interfaces
+ NS_DECL_NSITIMERCALLBACK
+
+ /** nsITransactionListener interfaces
+ */
+ NS_IMETHOD WillDo(nsITransactionManager *aManager, nsITransaction *aTransaction, bool *aInterrupt) override;
+ NS_IMETHOD DidDo(nsITransactionManager *aManager, nsITransaction *aTransaction, nsresult aDoResult) override;
+ NS_IMETHOD WillUndo(nsITransactionManager *aManager, nsITransaction *aTransaction, bool *aInterrupt) override;
+ NS_IMETHOD DidUndo(nsITransactionManager *aManager, nsITransaction *aTransaction, nsresult aUndoResult) override;
+ NS_IMETHOD WillRedo(nsITransactionManager *aManager, nsITransaction *aTransaction, bool *aInterrupt) override;
+ NS_IMETHOD DidRedo(nsITransactionManager *aManager, nsITransaction *aTransaction, nsresult aRedoResult) override;
+ NS_IMETHOD WillBeginBatch(nsITransactionManager *aManager, bool *aInterrupt) override;
+ NS_IMETHOD DidBeginBatch(nsITransactionManager *aManager, nsresult aResult) override;
+ NS_IMETHOD WillEndBatch(nsITransactionManager *aManager, bool *aInterrupt) override;
+ NS_IMETHOD DidEndBatch(nsITransactionManager *aManager, nsresult aResult) override;
+ NS_IMETHOD WillMerge(nsITransactionManager *aManager, nsITransaction *aTopTransaction,
+ nsITransaction *aTransactionToMerge, bool *aInterrupt) override;
+ NS_IMETHOD DidMerge(nsITransactionManager *aManager, nsITransaction *aTopTransaction,
+ nsITransaction *aTransactionToMerge,
+ bool aDidMerge, nsresult aMergeResult) override;
+
+
+ nsresult Init(nsPIDOMWindowOuter* aDOMWindow);
+
+protected:
+
+ virtual ~nsComposerCommandsUpdater();
+
+ enum {
+ eStateUninitialized = -1,
+ eStateOff = false,
+ eStateOn = true
+ };
+
+ bool SelectionIsCollapsed();
+ nsresult UpdateDirtyState(bool aNowDirty);
+ nsresult UpdateOneCommand(const char* aCommand);
+ nsresult UpdateCommandGroup(const nsAString& aCommandGroup);
+
+ already_AddRefed<nsPICommandUpdater> GetCommandUpdater();
+
+ nsresult PrimeUpdateTimer();
+ void TimerCallback();
+ nsCOMPtr<nsITimer> mUpdateTimer;
+
+ nsWeakPtr mDOMWindow;
+ nsWeakPtr mDocShell;
+ int8_t mDirtyState;
+ int8_t mSelectionCollapsed;
+ bool mFirstDoOfFirstUndo;
+
+
+};
+
+extern "C" nsresult NS_NewComposerCommandsUpdater(nsISelectionListener** aInstancePtrResult);
+
+
+#endif // nsComposerCommandsUpdater_h__
diff --git a/editor/composer/nsComposerController.cpp b/editor/composer/nsComposerController.cpp
new file mode 100644
index 000000000..5d90fbff0
--- /dev/null
+++ b/editor/composer/nsComposerController.cpp
@@ -0,0 +1,140 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/mozalloc.h" // for operator new
+#include "nsComposerCommands.h" // for nsStyleUpdatingCommand, etc
+#include "nsComposerController.h"
+#include "nsError.h" // for NS_OK
+#include "nsGkAtoms.h" // for nsGkAtoms, nsGkAtoms::a, etc
+#include "nsIControllerCommandTable.h" // for nsIControllerCommandTable
+
+class nsIControllerCommand;
+
+#define NS_REGISTER_ONE_COMMAND(_cmdClass, _cmdName) \
+ { \
+ _cmdClass* theCmd = new _cmdClass(); \
+ inCommandTable->RegisterCommand(_cmdName, \
+ static_cast<nsIControllerCommand *>(theCmd)); \
+ }
+
+#define NS_REGISTER_FIRST_COMMAND(_cmdClass, _cmdName) \
+ { \
+ _cmdClass* theCmd = new _cmdClass(); \
+ inCommandTable->RegisterCommand(_cmdName, \
+ static_cast<nsIControllerCommand *>(theCmd));
+
+#define NS_REGISTER_NEXT_COMMAND(_cmdClass, _cmdName) \
+ inCommandTable->RegisterCommand(_cmdName, \
+ static_cast<nsIControllerCommand *>(theCmd));
+
+#define NS_REGISTER_LAST_COMMAND(_cmdClass, _cmdName) \
+ inCommandTable->RegisterCommand(_cmdName, \
+ static_cast<nsIControllerCommand *>(theCmd)); \
+ }
+
+#define NS_REGISTER_STYLE_COMMAND(_cmdClass, _cmdName, _styleTag) \
+ { \
+ _cmdClass* theCmd = new _cmdClass(_styleTag); \
+ inCommandTable->RegisterCommand(_cmdName, \
+ static_cast<nsIControllerCommand *>(theCmd)); \
+ }
+
+#define NS_REGISTER_TAG_COMMAND(_cmdClass, _cmdName, _tagName) \
+ { \
+ _cmdClass* theCmd = new _cmdClass(_tagName); \
+ inCommandTable->RegisterCommand(_cmdName, \
+ static_cast<nsIControllerCommand *>(theCmd)); \
+ }
+
+
+// static
+nsresult
+nsComposerController::RegisterEditorDocStateCommands(
+ nsIControllerCommandTable *inCommandTable)
+{
+ // observer commands for document state
+ NS_REGISTER_FIRST_COMMAND(nsDocumentStateCommand, "obs_documentCreated")
+ NS_REGISTER_NEXT_COMMAND(nsDocumentStateCommand, "obs_documentWillBeDestroyed")
+ NS_REGISTER_LAST_COMMAND(nsDocumentStateCommand, "obs_documentLocationChanged")
+
+ // commands that may get or change state
+ NS_REGISTER_FIRST_COMMAND(nsSetDocumentStateCommand, "cmd_setDocumentModified")
+ NS_REGISTER_NEXT_COMMAND(nsSetDocumentStateCommand, "cmd_setDocumentUseCSS")
+ NS_REGISTER_NEXT_COMMAND(nsSetDocumentStateCommand, "cmd_setDocumentReadOnly")
+ NS_REGISTER_NEXT_COMMAND(nsSetDocumentStateCommand, "cmd_insertBrOnReturn")
+ NS_REGISTER_NEXT_COMMAND(nsSetDocumentStateCommand, "cmd_enableObjectResizing")
+ NS_REGISTER_LAST_COMMAND(nsSetDocumentStateCommand, "cmd_enableInlineTableEditing")
+
+ NS_REGISTER_ONE_COMMAND(nsSetDocumentOptionsCommand, "cmd_setDocumentOptions")
+
+ return NS_OK;
+}
+
+// static
+nsresult
+nsComposerController::RegisterHTMLEditorCommands(
+ nsIControllerCommandTable *inCommandTable)
+{
+ // Edit menu
+ NS_REGISTER_ONE_COMMAND(nsPasteNoFormattingCommand, "cmd_pasteNoFormatting");
+
+ // indent/outdent
+ NS_REGISTER_ONE_COMMAND(nsIndentCommand, "cmd_indent");
+ NS_REGISTER_ONE_COMMAND(nsOutdentCommand, "cmd_outdent");
+
+ // Styles
+ NS_REGISTER_STYLE_COMMAND(nsStyleUpdatingCommand, "cmd_bold", nsGkAtoms::b);
+ NS_REGISTER_STYLE_COMMAND(nsStyleUpdatingCommand, "cmd_italic", nsGkAtoms::i);
+ NS_REGISTER_STYLE_COMMAND(nsStyleUpdatingCommand, "cmd_underline", nsGkAtoms::u);
+ NS_REGISTER_STYLE_COMMAND(nsStyleUpdatingCommand, "cmd_tt", nsGkAtoms::tt);
+
+ NS_REGISTER_STYLE_COMMAND(nsStyleUpdatingCommand, "cmd_strikethrough", nsGkAtoms::strike);
+ NS_REGISTER_STYLE_COMMAND(nsStyleUpdatingCommand, "cmd_superscript", nsGkAtoms::sup);
+ NS_REGISTER_STYLE_COMMAND(nsStyleUpdatingCommand, "cmd_subscript", nsGkAtoms::sub);
+ NS_REGISTER_STYLE_COMMAND(nsStyleUpdatingCommand, "cmd_nobreak", nsGkAtoms::nobr);
+
+ NS_REGISTER_STYLE_COMMAND(nsStyleUpdatingCommand, "cmd_em", nsGkAtoms::em);
+ NS_REGISTER_STYLE_COMMAND(nsStyleUpdatingCommand, "cmd_strong", nsGkAtoms::strong);
+ NS_REGISTER_STYLE_COMMAND(nsStyleUpdatingCommand, "cmd_cite", nsGkAtoms::cite);
+ NS_REGISTER_STYLE_COMMAND(nsStyleUpdatingCommand, "cmd_abbr", nsGkAtoms::abbr);
+ NS_REGISTER_STYLE_COMMAND(nsStyleUpdatingCommand, "cmd_acronym", nsGkAtoms::acronym);
+ NS_REGISTER_STYLE_COMMAND(nsStyleUpdatingCommand, "cmd_code", nsGkAtoms::code);
+ NS_REGISTER_STYLE_COMMAND(nsStyleUpdatingCommand, "cmd_samp", nsGkAtoms::samp);
+ NS_REGISTER_STYLE_COMMAND(nsStyleUpdatingCommand, "cmd_var", nsGkAtoms::var);
+ NS_REGISTER_STYLE_COMMAND(nsStyleUpdatingCommand, "cmd_removeLinks", nsGkAtoms::href);
+
+ // lists
+ NS_REGISTER_STYLE_COMMAND(nsListCommand, "cmd_ol", nsGkAtoms::ol);
+ NS_REGISTER_STYLE_COMMAND(nsListCommand, "cmd_ul", nsGkAtoms::ul);
+ NS_REGISTER_STYLE_COMMAND(nsListItemCommand, "cmd_dt", nsGkAtoms::dt);
+ NS_REGISTER_STYLE_COMMAND(nsListItemCommand, "cmd_dd", nsGkAtoms::dd);
+ NS_REGISTER_ONE_COMMAND(nsRemoveListCommand, "cmd_removeList");
+
+ // format stuff
+ NS_REGISTER_ONE_COMMAND(nsParagraphStateCommand, "cmd_paragraphState");
+ NS_REGISTER_ONE_COMMAND(nsFontFaceStateCommand, "cmd_fontFace");
+ NS_REGISTER_ONE_COMMAND(nsFontSizeStateCommand, "cmd_fontSize");
+ NS_REGISTER_ONE_COMMAND(nsFontColorStateCommand, "cmd_fontColor");
+ NS_REGISTER_ONE_COMMAND(nsBackgroundColorStateCommand, "cmd_backgroundColor");
+ NS_REGISTER_ONE_COMMAND(nsHighlightColorStateCommand, "cmd_highlight");
+
+ NS_REGISTER_ONE_COMMAND(nsAlignCommand, "cmd_align");
+ NS_REGISTER_ONE_COMMAND(nsRemoveStylesCommand, "cmd_removeStyles");
+
+ NS_REGISTER_ONE_COMMAND(nsIncreaseFontSizeCommand, "cmd_increaseFont");
+ NS_REGISTER_ONE_COMMAND(nsDecreaseFontSizeCommand, "cmd_decreaseFont");
+
+ // Insert content
+ NS_REGISTER_ONE_COMMAND(nsInsertHTMLCommand, "cmd_insertHTML");
+ NS_REGISTER_TAG_COMMAND(nsInsertTagCommand, "cmd_insertLinkNoUI", nsGkAtoms::a);
+ NS_REGISTER_TAG_COMMAND(nsInsertTagCommand, "cmd_insertImageNoUI", nsGkAtoms::img);
+ NS_REGISTER_TAG_COMMAND(nsInsertTagCommand, "cmd_insertHR", nsGkAtoms::hr);
+
+ NS_REGISTER_ONE_COMMAND(nsAbsolutePositioningCommand, "cmd_absPos");
+ NS_REGISTER_ONE_COMMAND(nsDecreaseZIndexCommand, "cmd_decreaseZIndex");
+ NS_REGISTER_ONE_COMMAND(nsIncreaseZIndexCommand, "cmd_increaseZIndex");
+
+ return NS_OK;
+}
diff --git a/editor/composer/nsComposerController.h b/editor/composer/nsComposerController.h
new file mode 100644
index 000000000..768c67b80
--- /dev/null
+++ b/editor/composer/nsComposerController.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsComposerController_h__
+#define nsComposerController_h__
+
+
+#include "nscore.h" // for nsresult
+
+class nsIControllerCommandTable;
+
+
+// The plaintext editor controller is used for basic text editing and html editing
+// commands in composer
+// The refCon that gets passed to its commands is initially nsIEditingSession,
+// and after successfule editor creation it is changed to nsIEditor.
+#define NS_EDITORDOCSTATECONTROLLER_CID \
+ { 0x50e95301, 0x17a8, 0x11d4, { 0x9f, 0x7e, 0xdd, 0x53, 0x0d, 0x5f, 0x05, 0x7c } }
+
+// The HTMLEditor controller is used only for HTML editors and takes nsIEditor as refCon
+#define NS_HTMLEDITORCONTROLLER_CID \
+ { 0x62db0002, 0xdbb6, 0x43f4, { 0x8f, 0xb7, 0x9d, 0x25, 0x38, 0xbc, 0x57, 0x47 } }
+
+
+class nsComposerController
+{
+public:
+ static nsresult RegisterEditorDocStateCommands(nsIControllerCommandTable* inCommandTable);
+ static nsresult RegisterHTMLEditorCommands(nsIControllerCommandTable* inCommandTable);
+};
+
+#endif /* nsComposerController_h__ */
diff --git a/editor/composer/nsComposerDocumentCommands.cpp b/editor/composer/nsComposerDocumentCommands.cpp
new file mode 100644
index 000000000..d44e940f6
--- /dev/null
+++ b/editor/composer/nsComposerDocumentCommands.cpp
@@ -0,0 +1,480 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+#include "nsCOMPtr.h" // for nsCOMPtr, do_QueryInterface, etc
+#include "nsCRT.h" // for nsCRT
+#include "nsComposerCommands.h" // for nsSetDocumentOptionsCommand, etc
+#include "nsDebug.h" // for NS_ENSURE_ARG_POINTER, etc
+#include "nsError.h" // for NS_ERROR_INVALID_ARG, etc
+#include "nsICommandParams.h" // for nsICommandParams
+#include "nsIDOMDocument.h" // for nsIDOMDocument
+#include "nsIDocShell.h" // for nsIDocShell
+#include "nsIDocument.h" // for nsIDocument
+#include "nsIEditingSession.h" // for nsIEditingSession, etc
+#include "nsIEditor.h" // for nsIEditor
+#include "nsIHTMLEditor.h" // for nsIHTMLEditor
+#include "nsIHTMLInlineTableEditor.h" // for nsIHTMLInlineTableEditor
+#include "nsIHTMLObjectResizer.h" // for nsIHTMLObjectResizer
+#include "nsIPlaintextEditor.h" // for nsIPlaintextEditor, etc
+#include "nsIPresShell.h" // for nsIPresShell
+#include "nsISelectionController.h" // for nsISelectionController
+#include "nsISupportsImpl.h" // for nsPresContext::Release
+#include "nsISupportsUtils.h" // for NS_IF_ADDREF
+#include "nsIURI.h" // for nsIURI
+#include "nsPresContext.h" // for nsPresContext
+#include "nscore.h" // for NS_IMETHODIMP, nsresult, etc
+
+class nsISupports;
+
+//defines
+#define STATE_ENABLED "state_enabled"
+#define STATE_ALL "state_all"
+#define STATE_ATTRIBUTE "state_attribute"
+#define STATE_DATA "state_data"
+
+static
+nsresult
+GetPresContextFromEditor(nsIEditor *aEditor, nsPresContext **aResult)
+{
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = nullptr;
+ NS_ENSURE_ARG_POINTER(aEditor);
+
+ nsCOMPtr<nsISelectionController> selCon;
+ nsresult rv = aEditor->GetSelectionController(getter_AddRefs(selCon));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(selCon, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIPresShell> presShell = do_QueryInterface(selCon);
+ NS_ENSURE_TRUE(presShell, NS_ERROR_FAILURE);
+
+ NS_IF_ADDREF(*aResult = presShell->GetPresContext());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSetDocumentOptionsCommand::IsCommandEnabled(const char * aCommandName,
+ nsISupports *refCon,
+ bool *outCmdEnabled)
+{
+ NS_ENSURE_ARG_POINTER(outCmdEnabled);
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(refCon);
+ if (editor) {
+ return editor->GetIsSelectionEditable(outCmdEnabled);
+ }
+
+ *outCmdEnabled = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSetDocumentOptionsCommand::DoCommand(const char *aCommandName,
+ nsISupports *refCon)
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsSetDocumentOptionsCommand::DoCommandParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ NS_ENSURE_ARG_POINTER(aParams);
+
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(refCon);
+ NS_ENSURE_TRUE(editor, NS_ERROR_INVALID_ARG);
+
+ RefPtr<nsPresContext> presContext;
+ nsresult rv = GetPresContextFromEditor(editor, getter_AddRefs(presContext));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(presContext, NS_ERROR_FAILURE);
+
+ int32_t animationMode;
+ rv = aParams->GetLongValue("imageAnimation", &animationMode);
+ if (NS_SUCCEEDED(rv)) {
+ // for possible values of animation mode, see:
+ // http://lxr.mozilla.org/seamonkey/source/image/public/imgIContainer.idl
+ presContext->SetImageAnimationMode(animationMode);
+ }
+
+ bool allowPlugins;
+ rv = aParams->GetBooleanValue("plugins", &allowPlugins);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIDocShell> docShell(presContext->GetDocShell());
+ NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE);
+
+ rv = docShell->SetAllowPlugins(allowPlugins);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSetDocumentOptionsCommand::GetCommandStateParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ NS_ENSURE_ARG_POINTER(aParams);
+ NS_ENSURE_ARG_POINTER(refCon);
+
+ // The base editor owns most state info
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(refCon);
+ NS_ENSURE_TRUE(editor, NS_ERROR_INVALID_ARG);
+
+ // Always get the enabled state
+ bool outCmdEnabled = false;
+ IsCommandEnabled(aCommandName, refCon, &outCmdEnabled);
+ nsresult rv = aParams->SetBooleanValue(STATE_ENABLED, outCmdEnabled);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // get pres context
+ RefPtr<nsPresContext> presContext;
+ rv = GetPresContextFromEditor(editor, getter_AddRefs(presContext));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(presContext, NS_ERROR_FAILURE);
+
+ int32_t animationMode;
+ rv = aParams->GetLongValue("imageAnimation", &animationMode);
+ if (NS_SUCCEEDED(rv)) {
+ // for possible values of animation mode, see
+ // http://lxr.mozilla.org/seamonkey/source/image/public/imgIContainer.idl
+ rv = aParams->SetLongValue("imageAnimation",
+ presContext->ImageAnimationMode());
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ bool allowPlugins = false;
+ rv = aParams->GetBooleanValue("plugins", &allowPlugins);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIDocShell> docShell(presContext->GetDocShell());
+ NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE);
+
+ allowPlugins = docShell->PluginsAllowedInCurrentDoc();
+
+ rv = aParams->SetBooleanValue("plugins", allowPlugins);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+
+/**
+ * Commands for document state that may be changed via doCommandParams
+ * As of 11/11/02, this is just "cmd_setDocumentModified"
+ * Note that you can use the same command class, nsSetDocumentStateCommand,
+ * for more than one of this type of command
+ * We check the input command param for different behavior
+ */
+
+NS_IMETHODIMP
+nsSetDocumentStateCommand::IsCommandEnabled(const char * aCommandName,
+ nsISupports *refCon,
+ bool *outCmdEnabled)
+{
+ // These commands are always enabled
+ NS_ENSURE_ARG_POINTER(outCmdEnabled);
+ *outCmdEnabled = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSetDocumentStateCommand::DoCommand(const char *aCommandName,
+ nsISupports *refCon)
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsSetDocumentStateCommand::DoCommandParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(refCon);
+ NS_ENSURE_TRUE(editor, NS_ERROR_INVALID_ARG);
+
+ if (!nsCRT::strcmp(aCommandName, "cmd_setDocumentModified")) {
+ NS_ENSURE_ARG_POINTER(aParams);
+
+ bool modified;
+ nsresult rv = aParams->GetBooleanValue(STATE_ATTRIBUTE, &modified);
+
+ // Should we fail if this param wasn't set?
+ // I'm not sure we should be that strict
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (modified) {
+ return editor->IncrementModificationCount(1);
+ }
+
+ return editor->ResetModificationCount();
+ }
+
+ if (!nsCRT::strcmp(aCommandName, "cmd_setDocumentReadOnly")) {
+ NS_ENSURE_ARG_POINTER(aParams);
+ bool isReadOnly;
+ nsresult rvRO = aParams->GetBooleanValue(STATE_ATTRIBUTE, &isReadOnly);
+ NS_ENSURE_SUCCESS(rvRO, rvRO);
+
+ uint32_t flags;
+ editor->GetFlags(&flags);
+ if (isReadOnly) {
+ flags |= nsIPlaintextEditor::eEditorReadonlyMask;
+ } else {
+ flags &= ~(nsIPlaintextEditor::eEditorReadonlyMask);
+ }
+
+ return editor->SetFlags(flags);
+ }
+
+ if (!nsCRT::strcmp(aCommandName, "cmd_setDocumentUseCSS")) {
+ NS_ENSURE_ARG_POINTER(aParams);
+ nsCOMPtr<nsIHTMLEditor> htmleditor = do_QueryInterface(refCon);
+ NS_ENSURE_TRUE(htmleditor, NS_ERROR_INVALID_ARG);
+
+ bool desireCSS;
+ nsresult rvCSS = aParams->GetBooleanValue(STATE_ATTRIBUTE, &desireCSS);
+ NS_ENSURE_SUCCESS(rvCSS, rvCSS);
+
+ return htmleditor->SetIsCSSEnabled(desireCSS);
+ }
+
+ if (!nsCRT::strcmp(aCommandName, "cmd_insertBrOnReturn")) {
+ NS_ENSURE_ARG_POINTER(aParams);
+ nsCOMPtr<nsIHTMLEditor> htmleditor = do_QueryInterface(refCon);
+ NS_ENSURE_TRUE(htmleditor, NS_ERROR_INVALID_ARG);
+
+ bool insertBrOnReturn;
+ nsresult rvBR = aParams->GetBooleanValue(STATE_ATTRIBUTE,
+ &insertBrOnReturn);
+ NS_ENSURE_SUCCESS(rvBR, rvBR);
+
+ return htmleditor->SetReturnInParagraphCreatesNewParagraph(!insertBrOnReturn);
+ }
+
+ if (!nsCRT::strcmp(aCommandName, "cmd_enableObjectResizing")) {
+ NS_ENSURE_ARG_POINTER(aParams);
+ nsCOMPtr<nsIHTMLObjectResizer> resizer = do_QueryInterface(refCon);
+ NS_ENSURE_TRUE(resizer, NS_ERROR_INVALID_ARG);
+
+ bool enabled;
+ nsresult rvOR = aParams->GetBooleanValue(STATE_ATTRIBUTE, &enabled);
+ NS_ENSURE_SUCCESS(rvOR, rvOR);
+
+ return resizer->SetObjectResizingEnabled(enabled);
+ }
+
+ if (!nsCRT::strcmp(aCommandName, "cmd_enableInlineTableEditing")) {
+ NS_ENSURE_ARG_POINTER(aParams);
+ nsCOMPtr<nsIHTMLInlineTableEditor> editor = do_QueryInterface(refCon);
+ NS_ENSURE_TRUE(editor, NS_ERROR_INVALID_ARG);
+
+ bool enabled;
+ nsresult rvOR = aParams->GetBooleanValue(STATE_ATTRIBUTE, &enabled);
+ NS_ENSURE_SUCCESS(rvOR, rvOR);
+
+ return editor->SetInlineTableEditingEnabled(enabled);
+ }
+
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsSetDocumentStateCommand::GetCommandStateParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ NS_ENSURE_ARG_POINTER(aParams);
+ NS_ENSURE_ARG_POINTER(refCon);
+
+ // The base editor owns most state info
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(refCon);
+ NS_ENSURE_TRUE(editor, NS_ERROR_INVALID_ARG);
+
+ // Always get the enabled state
+ bool outCmdEnabled = false;
+ IsCommandEnabled(aCommandName, refCon, &outCmdEnabled);
+ nsresult rv = aParams->SetBooleanValue(STATE_ENABLED, outCmdEnabled);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!nsCRT::strcmp(aCommandName, "cmd_setDocumentModified")) {
+ bool modified;
+ rv = editor->GetDocumentModified(&modified);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return aParams->SetBooleanValue(STATE_ATTRIBUTE, modified);
+ }
+
+ if (!nsCRT::strcmp(aCommandName, "cmd_setDocumentReadOnly")) {
+ NS_ENSURE_ARG_POINTER(aParams);
+
+ uint32_t flags;
+ editor->GetFlags(&flags);
+ bool isReadOnly = flags & nsIPlaintextEditor::eEditorReadonlyMask;
+ return aParams->SetBooleanValue(STATE_ATTRIBUTE, isReadOnly);
+ }
+
+ if (!nsCRT::strcmp(aCommandName, "cmd_setDocumentUseCSS")) {
+ NS_ENSURE_ARG_POINTER(aParams);
+ nsCOMPtr<nsIHTMLEditor> htmleditor = do_QueryInterface(refCon);
+ NS_ENSURE_TRUE(htmleditor, NS_ERROR_INVALID_ARG);
+
+ bool isCSS;
+ htmleditor->GetIsCSSEnabled(&isCSS);
+ return aParams->SetBooleanValue(STATE_ALL, isCSS);
+ }
+
+ if (!nsCRT::strcmp(aCommandName, "cmd_insertBrOnReturn")) {
+ NS_ENSURE_ARG_POINTER(aParams);
+ nsCOMPtr<nsIHTMLEditor> htmleditor = do_QueryInterface(refCon);
+ NS_ENSURE_TRUE(htmleditor, NS_ERROR_INVALID_ARG);
+
+ bool createPOnReturn;
+ htmleditor->GetReturnInParagraphCreatesNewParagraph(&createPOnReturn);
+ return aParams->SetBooleanValue(STATE_ATTRIBUTE, !createPOnReturn);
+ }
+
+ if (!nsCRT::strcmp(aCommandName, "cmd_enableObjectResizing")) {
+ NS_ENSURE_ARG_POINTER(aParams);
+ nsCOMPtr<nsIHTMLObjectResizer> resizer = do_QueryInterface(refCon);
+ NS_ENSURE_TRUE(resizer, NS_ERROR_INVALID_ARG);
+
+ bool enabled;
+ resizer->GetObjectResizingEnabled(&enabled);
+ return aParams->SetBooleanValue(STATE_ATTRIBUTE, enabled);
+ }
+
+ if (!nsCRT::strcmp(aCommandName, "cmd_enableInlineTableEditing")) {
+ NS_ENSURE_ARG_POINTER(aParams);
+ nsCOMPtr<nsIHTMLInlineTableEditor> editor = do_QueryInterface(refCon);
+ NS_ENSURE_TRUE(editor, NS_ERROR_INVALID_ARG);
+
+ bool enabled;
+ editor->GetInlineTableEditingEnabled(&enabled);
+ return aParams->SetBooleanValue(STATE_ATTRIBUTE, enabled);
+ }
+
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/**
+ * Commands just for state notification
+ * As of 11/21/02, possible commands are:
+ * "obs_documentCreated"
+ * "obs_documentWillBeDestroyed"
+ * "obs_documentLocationChanged"
+ * Note that you can use the same command class, nsDocumentStateCommand
+ * for these or future observer commands.
+ * We check the input command param for different behavior
+ *
+ * How to use:
+ * 1. Get the nsICommandManager for the current editor
+ * 2. Implement an nsIObserve object, e.g:
+ *
+ * void Observe(
+ * in nsISupports aSubject, // The nsICommandManager calling this Observer
+ * in string aTopic, // command name, e.g.:"obs_documentCreated"
+ * // or "obs_documentWillBeDestroyed"
+ in wstring aData ); // ignored (set to "command_status_changed")
+ *
+ * 3. Add the observer by:
+ * commandManager.addObserver(observeobject, obs_documentCreated);
+ * 4. In the appropriate location in editorSession, editor, or commands code,
+ * trigger the notification of this observer by something like:
+ *
+ * nsCOMPtr<nsICommandManager> commandManager = mDocShell->GetCommandManager();
+ * nsCOMPtr<nsPICommandUpdater> commandUpdater = do_QueryInterface(commandManager);
+ * NS_ENSURE_TRUE(commandUpdater, NS_ERROR_FAILURE);
+ * commandUpdater->CommandStatusChanged(obs_documentCreated);
+ *
+ * 5. Use GetCommandStateParams() to obtain state information
+ * e.g., any creation state codes when creating an editor are
+ * supplied for "obs_documentCreated" command in the
+ * "state_data" param's value
+ *
+ */
+
+NS_IMETHODIMP
+nsDocumentStateCommand::IsCommandEnabled(const char* aCommandName,
+ nsISupports *refCon,
+ bool *outCmdEnabled)
+{
+ NS_ENSURE_ARG_POINTER(outCmdEnabled);
+ // Always return false to discourage callers from using DoCommand()
+ *outCmdEnabled = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocumentStateCommand::DoCommand(const char *aCommandName,
+ nsISupports *refCon)
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsDocumentStateCommand::DoCommandParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsDocumentStateCommand::GetCommandStateParams(const char *aCommandName,
+ nsICommandParams *aParams,
+ nsISupports *refCon)
+{
+ NS_ENSURE_ARG_POINTER(aParams);
+ NS_ENSURE_ARG_POINTER(aCommandName);
+ nsresult rv;
+
+ if (!nsCRT::strcmp(aCommandName, "obs_documentCreated")) {
+ uint32_t editorStatus = nsIEditingSession::eEditorErrorUnknown;
+
+ nsCOMPtr<nsIEditingSession> editingSession = do_QueryInterface(refCon);
+ if (editingSession) {
+ // refCon is initially set to nsIEditingSession until editor
+ // is successfully created and source doc is loaded
+ // Embedder gets error status if this fails
+ // If called before startup is finished,
+ // status = eEditorCreationInProgress
+ rv = editingSession->GetEditorStatus(&editorStatus);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // If refCon is an editor, then everything started up OK!
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(refCon);
+ if (editor) {
+ editorStatus = nsIEditingSession::eEditorOK;
+ }
+ }
+
+ // Note that if refCon is not-null, but is neither
+ // an nsIEditingSession or nsIEditor, we return "eEditorErrorUnknown"
+ aParams->SetLongValue(STATE_DATA, editorStatus);
+ return NS_OK;
+ }
+
+ if (!nsCRT::strcmp(aCommandName, "obs_documentLocationChanged")) {
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(refCon);
+ if (!editor) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIDOMDocument> domDoc;
+ editor->GetDocument(getter_AddRefs(domDoc));
+ nsCOMPtr<nsIDocument> doc = do_QueryInterface(domDoc);
+ NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE);
+
+ nsIURI *uri = doc->GetDocumentURI();
+ NS_ENSURE_TRUE(uri, NS_ERROR_FAILURE);
+
+ return aParams->SetISupportsValue(STATE_DATA, (nsISupports*)uri);
+ }
+
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
diff --git a/editor/composer/nsComposerRegistration.cpp b/editor/composer/nsComposerRegistration.cpp
new file mode 100644
index 000000000..7a0b3a440
--- /dev/null
+++ b/editor/composer/nsComposerRegistration.cpp
@@ -0,0 +1,226 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <stddef.h> // for nullptr
+
+#include "mozilla/Module.h" // for Module, Module::CIDEntry, etc
+#include "mozilla/ModuleUtils.h"
+#include "mozilla/mozalloc.h" // for operator new
+#include "nsCOMPtr.h" // for nsCOMPtr, getter_AddRefs, etc
+#include "nsComponentManagerUtils.h" // for do_CreateInstance
+#include "nsComposeTxtSrvFilter.h" // for nsComposeTxtSrvFilter, etc
+#include "nsComposerController.h" // for nsComposerController, etc
+#include "nsDebug.h" // for NS_ENSURE_SUCCESS
+#include "nsEditingSession.h" // for NS_EDITINGSESSION_CID, etc
+#include "nsEditorSpellCheck.h" // for NS_EDITORSPELLCHECK_CID, etc
+#include "nsError.h" // for NS_ERROR_NO_AGGREGATION, etc
+#include "nsIController.h" // for nsIController
+#include "nsIControllerCommandTable.h" // for nsIControllerCommandTable, etc
+#include "nsIControllerContext.h" // for nsIControllerContext
+#include "nsID.h" // for NS_DEFINE_NAMED_CID, etc
+#include "nsISupportsImpl.h"
+#include "nsISupportsUtils.h" // for NS_ADDREF, NS_RELEASE
+#include "nsServiceManagerUtils.h" // for do_GetService
+#include "nscore.h" // for nsresult
+
+class nsISupports;
+
+#define NS_HTMLEDITOR_COMMANDTABLE_CID \
+{ 0x13e50d8d, 0x9cee, 0x4ad1, { 0xa3, 0xa2, 0x4a, 0x44, 0x2f, 0xdf, 0x7d, 0xfa } }
+
+#define NS_HTMLEDITOR_DOCSTATE_COMMANDTABLE_CID \
+{ 0xa33982d3, 0x1adf, 0x4162, { 0x99, 0x41, 0xf7, 0x34, 0xbc, 0x45, 0xe4, 0xed } }
+
+
+static NS_DEFINE_CID(kHTMLEditorCommandTableCID, NS_HTMLEDITOR_COMMANDTABLE_CID);
+static NS_DEFINE_CID(kHTMLEditorDocStateCommandTableCID, NS_HTMLEDITOR_DOCSTATE_COMMANDTABLE_CID);
+
+
+////////////////////////////////////////////////////////////////////////
+// Define the contructor function for the objects
+//
+// NOTE: This creates an instance of objects by using the default constructor
+//
+
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsEditingSession)
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsEditorSpellCheck)
+
+// There are no macros that enable us to have 2 constructors
+// for the same object
+//
+// Here we are creating the same object with two different contract IDs
+// and then initializing it different.
+// Basically, we need to tell the filter whether it is doing mail or not
+static nsresult
+nsComposeTxtSrvFilterConstructor(nsISupports *aOuter, REFNSIID aIID,
+ void **aResult, bool aIsForMail)
+{
+ *aResult = nullptr;
+ if (aOuter) {
+ return NS_ERROR_NO_AGGREGATION;
+ }
+ nsComposeTxtSrvFilter * inst = new nsComposeTxtSrvFilter();
+ if (!inst) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ NS_ADDREF(inst);
+ inst->Init(aIsForMail);
+ nsresult rv = inst->QueryInterface(aIID, aResult);
+ NS_RELEASE(inst);
+ return rv;
+}
+
+static nsresult
+nsComposeTxtSrvFilterConstructorForComposer(nsISupports *aOuter,
+ REFNSIID aIID,
+ void **aResult)
+{
+ return nsComposeTxtSrvFilterConstructor(aOuter, aIID, aResult, false);
+}
+
+static nsresult
+nsComposeTxtSrvFilterConstructorForMail(nsISupports *aOuter,
+ REFNSIID aIID,
+ void **aResult)
+{
+ return nsComposeTxtSrvFilterConstructor(aOuter, aIID, aResult, true);
+}
+
+
+// Constructor for a controller set up with a command table specified
+// by the CID passed in. This function uses do_GetService to get the
+// command table, so that every controller shares a single command
+// table, for space-efficiency.
+//
+// The only reason to go via the service manager for the command table
+// is that it holds onto the singleton for us, avoiding static variables here.
+static nsresult
+CreateControllerWithSingletonCommandTable(const nsCID& inCommandTableCID, nsIController **aResult)
+{
+ nsresult rv;
+ nsCOMPtr<nsIController> controller = do_CreateInstance("@mozilla.org/embedcomp/base-command-controller;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIControllerCommandTable> composerCommandTable = do_GetService(inCommandTableCID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // this guy is a singleton, so make it immutable
+ composerCommandTable->MakeImmutable();
+
+ nsCOMPtr<nsIControllerContext> controllerContext = do_QueryInterface(controller, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = controllerContext->Init(composerCommandTable);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aResult = controller;
+ NS_ADDREF(*aResult);
+ return NS_OK;
+}
+
+
+// Here we make an instance of the controller that holds doc state commands.
+// We set it up with a singleton command table.
+static nsresult
+nsHTMLEditorDocStateControllerConstructor(nsISupports *aOuter, REFNSIID aIID,
+ void **aResult)
+{
+ nsCOMPtr<nsIController> controller;
+ nsresult rv = CreateControllerWithSingletonCommandTable(kHTMLEditorDocStateCommandTableCID, getter_AddRefs(controller));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return controller->QueryInterface(aIID, aResult);
+}
+
+// Tere we make an instance of the controller that holds composer commands.
+// We set it up with a singleton command table.
+static nsresult
+nsHTMLEditorControllerConstructor(nsISupports *aOuter, REFNSIID aIID, void **aResult)
+{
+ nsCOMPtr<nsIController> controller;
+ nsresult rv = CreateControllerWithSingletonCommandTable(kHTMLEditorCommandTableCID, getter_AddRefs(controller));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return controller->QueryInterface(aIID, aResult);
+}
+
+// Constructor for a command table that is pref-filled with HTML editor commands
+static nsresult
+nsHTMLEditorCommandTableConstructor(nsISupports *aOuter, REFNSIID aIID,
+ void **aResult)
+{
+ nsresult rv;
+ nsCOMPtr<nsIControllerCommandTable> commandTable =
+ do_CreateInstance(NS_CONTROLLERCOMMANDTABLE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = nsComposerController::RegisterHTMLEditorCommands(commandTable);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // we don't know here whether we're being created as an instance,
+ // or a service, so we can't become immutable
+
+ return commandTable->QueryInterface(aIID, aResult);
+}
+
+
+// Constructor for a command table that is pref-filled with HTML editor doc state commands
+static nsresult
+nsHTMLEditorDocStateCommandTableConstructor(nsISupports *aOuter, REFNSIID aIID,
+ void **aResult)
+{
+ nsresult rv;
+ nsCOMPtr<nsIControllerCommandTable> commandTable =
+ do_CreateInstance(NS_CONTROLLERCOMMANDTABLE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = nsComposerController::RegisterEditorDocStateCommands(commandTable);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // we don't know here whether we're being created as an instance,
+ // or a service, so we can't become immutable
+
+ return commandTable->QueryInterface(aIID, aResult);
+}
+
+NS_DEFINE_NAMED_CID(NS_HTMLEDITORCONTROLLER_CID);
+NS_DEFINE_NAMED_CID(NS_EDITORDOCSTATECONTROLLER_CID);
+NS_DEFINE_NAMED_CID(NS_HTMLEDITOR_COMMANDTABLE_CID);
+NS_DEFINE_NAMED_CID(NS_HTMLEDITOR_DOCSTATE_COMMANDTABLE_CID);
+NS_DEFINE_NAMED_CID(NS_EDITINGSESSION_CID);
+NS_DEFINE_NAMED_CID(NS_EDITORSPELLCHECK_CID);
+NS_DEFINE_NAMED_CID(NS_COMPOSERTXTSRVFILTER_CID);
+NS_DEFINE_NAMED_CID(NS_COMPOSERTXTSRVFILTERMAIL_CID);
+
+
+static const mozilla::Module::CIDEntry kComposerCIDs[] = {
+ { &kNS_HTMLEDITORCONTROLLER_CID, false, nullptr, nsHTMLEditorControllerConstructor },
+ { &kNS_EDITORDOCSTATECONTROLLER_CID, false, nullptr, nsHTMLEditorDocStateControllerConstructor },
+ { &kNS_HTMLEDITOR_COMMANDTABLE_CID, false, nullptr, nsHTMLEditorCommandTableConstructor },
+ { &kNS_HTMLEDITOR_DOCSTATE_COMMANDTABLE_CID, false, nullptr, nsHTMLEditorDocStateCommandTableConstructor },
+ { &kNS_EDITINGSESSION_CID, false, nullptr, nsEditingSessionConstructor },
+ { &kNS_EDITORSPELLCHECK_CID, false, nullptr, nsEditorSpellCheckConstructor },
+ { &kNS_COMPOSERTXTSRVFILTER_CID, false, nullptr, nsComposeTxtSrvFilterConstructorForComposer },
+ { &kNS_COMPOSERTXTSRVFILTERMAIL_CID, false, nullptr, nsComposeTxtSrvFilterConstructorForMail },
+ { nullptr }
+};
+
+static const mozilla::Module::ContractIDEntry kComposerContracts[] = {
+ { "@mozilla.org/editor/htmleditorcontroller;1", &kNS_HTMLEDITORCONTROLLER_CID },
+ { "@mozilla.org/editor/editordocstatecontroller;1", &kNS_EDITORDOCSTATECONTROLLER_CID },
+ { "@mozilla.org/editor/editingsession;1", &kNS_EDITINGSESSION_CID },
+ { "@mozilla.org/editor/editorspellchecker;1", &kNS_EDITORSPELLCHECK_CID },
+ { COMPOSER_TXTSRVFILTER_CONTRACTID, &kNS_COMPOSERTXTSRVFILTER_CID },
+ { COMPOSER_TXTSRVFILTERMAIL_CONTRACTID, &kNS_COMPOSERTXTSRVFILTERMAIL_CID },
+ { nullptr }
+};
+
+static const mozilla::Module kComposerModule = {
+ mozilla::Module::kVersion,
+ kComposerCIDs,
+ kComposerContracts
+};
+
+NSMODULE_DEFN(nsComposerModule) = &kComposerModule;
diff --git a/editor/composer/nsEditingSession.cpp b/editor/composer/nsEditingSession.cpp
new file mode 100644
index 000000000..677b3b50f
--- /dev/null
+++ b/editor/composer/nsEditingSession.cpp
@@ -0,0 +1,1392 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sw=2 et tw=78: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <string.h> // for nullptr, strcmp
+
+#include "imgIContainer.h" // for imgIContainer, etc
+#include "mozFlushType.h" // for mozFlushType::Flush_Frames
+#include "mozilla/mozalloc.h" // for operator new
+#include "nsAString.h"
+#include "nsComponentManagerUtils.h" // for do_CreateInstance
+#include "nsComposerCommandsUpdater.h" // for nsComposerCommandsUpdater
+#include "nsDebug.h" // for NS_ENSURE_SUCCESS, etc
+#include "nsEditingSession.h"
+#include "nsError.h" // for NS_ERROR_FAILURE, NS_OK, etc
+#include "nsIChannel.h" // for nsIChannel
+#include "nsICommandManager.h" // for nsICommandManager
+#include "nsIContentViewer.h" // for nsIContentViewer
+#include "nsIController.h" // for nsIController
+#include "nsIControllerContext.h" // for nsIControllerContext
+#include "nsIControllers.h" // for nsIControllers
+#include "nsID.h" // for NS_GET_IID, etc
+#include "nsIDOMDocument.h" // for nsIDOMDocument
+#include "nsIDOMHTMLDocument.h" // for nsIDOMHTMLDocument
+#include "nsIDOMWindow.h" // for nsIDOMWindow
+#include "nsIDocShell.h" // for nsIDocShell
+#include "nsIDocument.h" // for nsIDocument
+#include "nsIDocumentStateListener.h"
+#include "nsIEditor.h" // for nsIEditor
+#include "nsIHTMLDocument.h" // for nsIHTMLDocument, etc
+#include "nsIInterfaceRequestorUtils.h" // for do_GetInterface
+#include "nsIPlaintextEditor.h" // for nsIPlaintextEditor, etc
+#include "nsIPresShell.h" // for nsIPresShell
+#include "nsIRefreshURI.h" // for nsIRefreshURI
+#include "nsIRequest.h" // for nsIRequest
+#include "nsISelection.h" // for nsISelection
+#include "nsISelectionPrivate.h" // for nsISelectionPrivate
+#include "nsITimer.h" // for nsITimer, etc
+#include "nsITransactionManager.h" // for nsITransactionManager
+#include "nsIWeakReference.h" // for nsISupportsWeakReference, etc
+#include "nsIWebNavigation.h" // for nsIWebNavigation
+#include "nsIWebProgress.h" // for nsIWebProgress, etc
+#include "nsLiteralString.h" // for NS_LITERAL_STRING
+#include "nsPICommandUpdater.h" // for nsPICommandUpdater
+#include "nsPIDOMWindow.h" // for nsPIDOMWindow
+#include "nsPresContext.h" // for nsPresContext
+#include "nsReadableUtils.h" // for AppendUTF16toUTF8
+#include "nsStringFwd.h" // for nsAFlatString
+#include "mozilla/dom/Selection.h" // for AutoHideSelectionChanges
+#include "nsFrameSelection.h" // for nsFrameSelection
+
+class nsISupports;
+class nsIURI;
+
+/*---------------------------------------------------------------------------
+
+ nsEditingSession
+
+----------------------------------------------------------------------------*/
+nsEditingSession::nsEditingSession()
+: mDoneSetup(false)
+, mCanCreateEditor(false)
+, mInteractive(false)
+, mMakeWholeDocumentEditable(true)
+, mDisabledJSAndPlugins(false)
+, mScriptsEnabled(true)
+, mPluginsEnabled(true)
+, mProgressListenerRegistered(false)
+, mImageAnimationMode(0)
+, mEditorFlags(0)
+, mEditorStatus(eEditorOK)
+, mBaseCommandControllerId(0)
+, mDocStateControllerId(0)
+, mHTMLCommandControllerId(0)
+{
+}
+
+/*---------------------------------------------------------------------------
+
+ ~nsEditingSession
+
+----------------------------------------------------------------------------*/
+nsEditingSession::~nsEditingSession()
+{
+ // Must cancel previous timer?
+ if (mLoadBlankDocTimer)
+ mLoadBlankDocTimer->Cancel();
+}
+
+NS_IMPL_ISUPPORTS(nsEditingSession, nsIEditingSession, nsIWebProgressListener,
+ nsISupportsWeakReference)
+
+/*---------------------------------------------------------------------------
+
+ MakeWindowEditable
+
+ aEditorType string, "html" "htmlsimple" "text" "textsimple"
+ void makeWindowEditable(in nsIDOMWindow aWindow, in string aEditorType,
+ in boolean aDoAfterUriLoad,
+ in boolean aMakeWholeDocumentEditable,
+ in boolean aInteractive);
+----------------------------------------------------------------------------*/
+#define DEFAULT_EDITOR_TYPE "html"
+
+NS_IMETHODIMP
+nsEditingSession::MakeWindowEditable(mozIDOMWindowProxy* aWindow,
+ const char *aEditorType,
+ bool aDoAfterUriLoad,
+ bool aMakeWholeDocumentEditable,
+ bool aInteractive)
+{
+ mEditorType.Truncate();
+ mEditorFlags = 0;
+
+ NS_ENSURE_TRUE(aWindow, NS_ERROR_FAILURE);
+ auto* window = nsPIDOMWindowOuter::From(aWindow);
+
+ // disable plugins
+ nsCOMPtr<nsIDocShell> docShell = window->GetDocShell();
+ NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE);
+
+ mDocShell = do_GetWeakReference(docShell);
+ mInteractive = aInteractive;
+ mMakeWholeDocumentEditable = aMakeWholeDocumentEditable;
+
+ nsresult rv;
+ if (!mInteractive) {
+ rv = DisableJSAndPlugins(aWindow);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Always remove existing editor
+ TearDownEditorOnWindow(aWindow);
+
+ // Tells embedder that startup is in progress
+ mEditorStatus = eEditorCreationInProgress;
+
+ //temporary to set editor type here. we will need different classes soon.
+ if (!aEditorType)
+ aEditorType = DEFAULT_EDITOR_TYPE;
+ mEditorType = aEditorType;
+
+ // if all this does is setup listeners and I don't need listeners,
+ // can't this step be ignored?? (based on aDoAfterURILoad)
+ rv = PrepareForEditing(window);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // set the flag on the docShell to say that it's editable
+ rv = docShell->MakeEditable(aDoAfterUriLoad);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Setup commands common to plaintext and html editors,
+ // including the document creation observers
+ // the first is an editing controller
+ rv = SetupEditorCommandController("@mozilla.org/editor/editingcontroller;1",
+ aWindow,
+ static_cast<nsIEditingSession*>(this),
+ &mBaseCommandControllerId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // The second is a controller to monitor doc state,
+ // such as creation and "dirty flag"
+ rv = SetupEditorCommandController("@mozilla.org/editor/editordocstatecontroller;1",
+ aWindow,
+ static_cast<nsIEditingSession*>(this),
+ &mDocStateControllerId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // aDoAfterUriLoad can be false only when making an existing window editable
+ if (!aDoAfterUriLoad) {
+ rv = SetupEditorOnWindow(aWindow);
+
+ // mEditorStatus is set to the error reason
+ // Since this is used only when editing an existing page,
+ // it IS ok to destroy current editor
+ if (NS_FAILED(rv)) {
+ TearDownEditorOnWindow(aWindow);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsEditingSession::DisableJSAndPlugins(mozIDOMWindowProxy* aWindow)
+{
+ NS_ENSURE_TRUE(aWindow, NS_ERROR_FAILURE);
+ nsIDocShell *docShell = nsPIDOMWindowOuter::From(aWindow)->GetDocShell();
+ NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE);
+
+ bool tmp;
+ nsresult rv = docShell->GetAllowJavascript(&tmp);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mScriptsEnabled = tmp;
+
+ rv = docShell->SetAllowJavascript(false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Disable plugins in this document:
+ mPluginsEnabled = docShell->PluginsAllowedInCurrentDoc();
+
+ rv = docShell->SetAllowPlugins(false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mDisabledJSAndPlugins = true;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsEditingSession::RestoreJSAndPlugins(mozIDOMWindowProxy* aWindow)
+{
+ if (!mDisabledJSAndPlugins) {
+ return NS_OK;
+ }
+
+ mDisabledJSAndPlugins = false;
+
+ NS_ENSURE_TRUE(aWindow, NS_ERROR_FAILURE);
+ nsIDocShell *docShell = nsPIDOMWindowOuter::From(aWindow)->GetDocShell();
+ NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE);
+
+ nsresult rv = docShell->SetAllowJavascript(mScriptsEnabled);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Disable plugins in this document:
+ return docShell->SetAllowPlugins(mPluginsEnabled);
+}
+
+NS_IMETHODIMP
+nsEditingSession::GetJsAndPluginsDisabled(bool *aResult)
+{
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = mDisabledJSAndPlugins;
+ return NS_OK;
+}
+
+/*---------------------------------------------------------------------------
+
+ WindowIsEditable
+
+ boolean windowIsEditable (in nsIDOMWindow aWindow);
+----------------------------------------------------------------------------*/
+NS_IMETHODIMP
+nsEditingSession::WindowIsEditable(mozIDOMWindowProxy* aWindow,
+ bool *outIsEditable)
+{
+ NS_ENSURE_STATE(aWindow);
+ nsCOMPtr<nsIDocShell> docShell = nsPIDOMWindowOuter::From(aWindow)->GetDocShell();
+ NS_ENSURE_STATE(docShell);
+
+ return docShell->GetEditable(outIsEditable);
+}
+
+
+// These are MIME types that are automatically parsed as "text/plain"
+// and thus we can edit them as plaintext
+// Note: in older versions, we attempted to convert the mimetype of
+// the network channel for these and "text/xml" to "text/plain",
+// but further investigation reveals that strategy doesn't work
+const char* const gSupportedTextTypes[] = {
+ "text/plain",
+ "text/css",
+ "text/rdf",
+ "text/xsl",
+ "text/javascript", // obsolete type
+ "text/ecmascript", // obsolete type
+ "application/javascript",
+ "application/ecmascript",
+ "application/x-javascript", // obsolete type
+ "text/xul", // obsolete type
+ "application/vnd.mozilla.xul+xml",
+ nullptr // IMPORTANT! Null must be at end
+};
+
+bool
+IsSupportedTextType(const char* aMIMEType)
+{
+ NS_ENSURE_TRUE(aMIMEType, false);
+
+ for (size_t i = 0; gSupportedTextTypes[i]; ++i) {
+ if (!strcmp(gSupportedTextTypes[i], aMIMEType)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/*---------------------------------------------------------------------------
+
+ SetupEditorOnWindow
+
+ nsIEditor setupEditorOnWindow (in nsIDOMWindow aWindow);
+----------------------------------------------------------------------------*/
+NS_IMETHODIMP
+nsEditingSession::SetupEditorOnWindow(mozIDOMWindowProxy* aWindow)
+{
+ mDoneSetup = true;
+
+ NS_ENSURE_TRUE(aWindow, NS_ERROR_FAILURE);
+ auto* window = nsPIDOMWindowOuter::From(aWindow);
+
+ nsresult rv;
+
+ //MIME CHECKING
+ //must get the content type
+ // Note: the doc gets this from the network channel during StartPageLoad,
+ // so we don't have to get it from there ourselves
+ nsAutoCString mimeCType;
+
+ //then lets check the mime type
+ if (nsCOMPtr<nsIDocument> doc = window->GetDoc()) {
+ nsAutoString mimeType;
+ if (NS_SUCCEEDED(doc->GetContentType(mimeType)))
+ AppendUTF16toUTF8(mimeType, mimeCType);
+
+ if (IsSupportedTextType(mimeCType.get())) {
+ mEditorType.AssignLiteral("text");
+ mimeCType = "text/plain";
+ } else if (!mimeCType.EqualsLiteral("text/html") &&
+ !mimeCType.EqualsLiteral("application/xhtml+xml")) {
+ // Neither an acceptable text or html type.
+ mEditorStatus = eEditorErrorCantEditMimeType;
+
+ // Turn editor into HTML -- we will load blank page later
+ mEditorType.AssignLiteral("html");
+ mimeCType.AssignLiteral("text/html");
+ }
+
+ // Flush out frame construction to make sure that the subframe's
+ // presshell is set up if it needs to be.
+ nsCOMPtr<nsIDocument> document = do_QueryInterface(doc);
+ if (document) {
+ document->FlushPendingNotifications(Flush_Frames);
+ if (mMakeWholeDocumentEditable) {
+ document->SetEditableFlag(true);
+ nsCOMPtr<nsIHTMLDocument> htmlDocument = do_QueryInterface(document);
+ if (htmlDocument) {
+ // Enable usage of the execCommand API
+ htmlDocument->SetEditingState(nsIHTMLDocument::eDesignMode);
+ }
+ }
+ }
+ }
+ bool needHTMLController = false;
+
+ const char *classString = "@mozilla.org/editor/htmleditor;1";
+ if (mEditorType.EqualsLiteral("textmail")) {
+ mEditorFlags = nsIPlaintextEditor::eEditorPlaintextMask |
+ nsIPlaintextEditor::eEditorEnableWrapHackMask |
+ nsIPlaintextEditor::eEditorMailMask;
+ } else if (mEditorType.EqualsLiteral("text")) {
+ mEditorFlags = nsIPlaintextEditor::eEditorPlaintextMask |
+ nsIPlaintextEditor::eEditorEnableWrapHackMask;
+ } else if (mEditorType.EqualsLiteral("htmlmail")) {
+ if (mimeCType.EqualsLiteral("text/html")) {
+ needHTMLController = true;
+ mEditorFlags = nsIPlaintextEditor::eEditorMailMask;
+ } else {
+ // Set the flags back to textplain.
+ mEditorFlags = nsIPlaintextEditor::eEditorPlaintextMask |
+ nsIPlaintextEditor::eEditorEnableWrapHackMask;
+ }
+ } else {
+ // Defaulted to html
+ needHTMLController = true;
+ }
+
+ if (mInteractive) {
+ mEditorFlags |= nsIPlaintextEditor::eEditorAllowInteraction;
+ }
+
+ // make the UI state maintainer
+ mStateMaintainer = new nsComposerCommandsUpdater();
+
+ // now init the state maintainer
+ // This allows notification of error state
+ // even if we don't create an editor
+ rv = mStateMaintainer->Init(window);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mEditorStatus != eEditorCreationInProgress) {
+ mStateMaintainer->NotifyDocumentCreated();
+ return NS_ERROR_FAILURE;
+ }
+
+ // Create editor and do other things
+ // only if we haven't found some error above,
+ nsCOMPtr<nsIDocShell> docShell = window->GetDocShell();
+ NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE);
+ nsCOMPtr<nsIPresShell> presShell = docShell->GetPresShell();
+ NS_ENSURE_TRUE(presShell, NS_ERROR_FAILURE);
+
+ if (!mInteractive) {
+ // Disable animation of images in this document:
+ nsPresContext* presContext = presShell->GetPresContext();
+ NS_ENSURE_TRUE(presContext, NS_ERROR_FAILURE);
+
+ mImageAnimationMode = presContext->ImageAnimationMode();
+ presContext->SetImageAnimationMode(imgIContainer::kDontAnimMode);
+ }
+
+ // Hide selection changes during initialization, in order to hide this
+ // from web pages.
+ RefPtr<nsFrameSelection> fs = presShell->FrameSelection();
+ NS_ENSURE_TRUE(fs, NS_ERROR_FAILURE);
+ mozilla::dom::AutoHideSelectionChanges hideSelectionChanges(fs);
+
+ // create and set editor
+ // Try to reuse an existing editor
+ nsCOMPtr<nsIEditor> editor = do_QueryReferent(mExistingEditor);
+ if (editor) {
+ editor->PreDestroy(false);
+ } else {
+ editor = do_CreateInstance(classString, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mExistingEditor = do_GetWeakReference(editor);
+ }
+ // set the editor on the docShell. The docShell now owns it.
+ rv = docShell->SetEditor(editor);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // setup the HTML editor command controller
+ if (needHTMLController) {
+ // The third controller takes an nsIEditor as the context
+ rv = SetupEditorCommandController("@mozilla.org/editor/htmleditorcontroller;1",
+ aWindow, editor,
+ &mHTMLCommandControllerId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Set mimetype on editor
+ rv = editor->SetContentsMIMEType(mimeCType.get());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIContentViewer> contentViewer;
+ rv = docShell->GetContentViewer(getter_AddRefs(contentViewer));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(contentViewer, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIDOMDocument> domDoc;
+ rv = contentViewer->GetDOMDocument(getter_AddRefs(domDoc));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(domDoc, NS_ERROR_FAILURE);
+
+ // Set up as a doc state listener
+ // Important! We must have this to broadcast the "obs_documentCreated" message
+ rv = editor->AddDocumentStateListener(mStateMaintainer);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = editor->Init(domDoc, nullptr /* root content */,
+ nullptr, mEditorFlags, EmptyString());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISelection> selection;
+ editor->GetSelection(getter_AddRefs(selection));
+ nsCOMPtr<nsISelectionPrivate> selPriv = do_QueryInterface(selection);
+ NS_ENSURE_TRUE(selPriv, NS_ERROR_FAILURE);
+
+ rv = selPriv->AddSelectionListener(mStateMaintainer);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // and as a transaction listener
+ nsCOMPtr<nsITransactionManager> txnMgr;
+ editor->GetTransactionManager(getter_AddRefs(txnMgr));
+ if (txnMgr) {
+ txnMgr->AddListener(mStateMaintainer);
+ }
+
+ // Set context on all controllers to be the editor
+ rv = SetEditorOnControllers(aWindow, editor);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Everything went fine!
+ mEditorStatus = eEditorOK;
+
+ // This will trigger documentCreation notification
+ return editor->PostCreate();
+}
+
+// Removes all listeners and controllers from aWindow and aEditor.
+void
+nsEditingSession::RemoveListenersAndControllers(nsPIDOMWindowOuter* aWindow,
+ nsIEditor *aEditor)
+{
+ if (!mStateMaintainer || !aEditor) {
+ return;
+ }
+
+ // Remove all the listeners
+ nsCOMPtr<nsISelection> selection;
+ aEditor->GetSelection(getter_AddRefs(selection));
+ nsCOMPtr<nsISelectionPrivate> selPriv = do_QueryInterface(selection);
+ if (selPriv)
+ selPriv->RemoveSelectionListener(mStateMaintainer);
+
+ aEditor->RemoveDocumentStateListener(mStateMaintainer);
+
+ nsCOMPtr<nsITransactionManager> txnMgr;
+ aEditor->GetTransactionManager(getter_AddRefs(txnMgr));
+ if (txnMgr) {
+ txnMgr->RemoveListener(mStateMaintainer);
+ }
+
+ // Remove editor controllers from the window now that we're not
+ // editing in that window any more.
+ RemoveEditorControllers(aWindow);
+}
+
+/*---------------------------------------------------------------------------
+
+ TearDownEditorOnWindow
+
+ void tearDownEditorOnWindow (in nsIDOMWindow aWindow);
+----------------------------------------------------------------------------*/
+NS_IMETHODIMP
+nsEditingSession::TearDownEditorOnWindow(mozIDOMWindowProxy *aWindow)
+{
+ if (!mDoneSetup) {
+ return NS_OK;
+ }
+
+ NS_ENSURE_TRUE(aWindow, NS_ERROR_NULL_POINTER);
+
+ nsresult rv;
+
+ // Kill any existing reload timer
+ if (mLoadBlankDocTimer) {
+ mLoadBlankDocTimer->Cancel();
+ mLoadBlankDocTimer = nullptr;
+ }
+
+ mDoneSetup = false;
+
+ // Check if we're turning off editing (from contentEditable or designMode).
+ auto* window = nsPIDOMWindowOuter::From(aWindow);
+
+ nsCOMPtr<nsIDocument> doc = window->GetDoc();
+ nsCOMPtr<nsIHTMLDocument> htmlDoc = do_QueryInterface(doc);
+ bool stopEditing = htmlDoc && htmlDoc->IsEditingOn();
+ if (stopEditing) {
+ RemoveWebProgressListener(window);
+ }
+
+ nsCOMPtr<nsIDocShell> docShell = window->GetDocShell();
+ NS_ENSURE_STATE(docShell);
+
+ nsCOMPtr<nsIEditor> editor;
+ rv = docShell->GetEditor(getter_AddRefs(editor));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (stopEditing) {
+ htmlDoc->TearingDownEditor(editor);
+ }
+
+ if (mStateMaintainer && editor) {
+ // Null out the editor on the controllers first to prevent their weak
+ // references from pointing to a destroyed editor.
+ SetEditorOnControllers(aWindow, nullptr);
+ }
+
+ // Null out the editor on the docShell to trigger PreDestroy which
+ // needs to happen before document state listeners are removed below.
+ docShell->SetEditor(nullptr);
+
+ RemoveListenersAndControllers(window, editor);
+
+ if (stopEditing) {
+ // Make things the way they were before we started editing.
+ RestoreJSAndPlugins(aWindow);
+ RestoreAnimationMode(window);
+
+ if (mMakeWholeDocumentEditable) {
+ doc->SetEditableFlag(false);
+ nsCOMPtr<nsIHTMLDocument> htmlDocument = do_QueryInterface(doc);
+ if (htmlDocument) {
+ htmlDocument->SetEditingState(nsIHTMLDocument::eOff);
+ }
+ }
+ }
+
+ return rv;
+}
+
+/*---------------------------------------------------------------------------
+
+ GetEditorForFrame
+
+ nsIEditor getEditorForFrame (in nsIDOMWindow aWindow);
+----------------------------------------------------------------------------*/
+NS_IMETHODIMP
+nsEditingSession::GetEditorForWindow(mozIDOMWindowProxy* aWindow,
+ nsIEditor **outEditor)
+{
+ NS_ENSURE_STATE(aWindow);
+ nsCOMPtr<nsIDocShell> docShell = nsPIDOMWindowOuter::From(aWindow)->GetDocShell();
+ NS_ENSURE_STATE(docShell);
+
+ return docShell->GetEditor(outEditor);
+}
+
+/*---------------------------------------------------------------------------
+
+ OnStateChange
+
+----------------------------------------------------------------------------*/
+NS_IMETHODIMP
+nsEditingSession::OnStateChange(nsIWebProgress *aWebProgress,
+ nsIRequest *aRequest,
+ uint32_t aStateFlags, nsresult aStatus)
+{
+
+#ifdef NOISY_DOC_LOADING
+ nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest));
+ if (channel) {
+ nsAutoCString contentType;
+ channel->GetContentType(contentType);
+ if (!contentType.IsEmpty()) {
+ printf(" ++++++ MIMETYPE = %s\n", contentType.get());
+ }
+ }
+#endif
+
+ //
+ // A Request has started...
+ //
+ if (aStateFlags & nsIWebProgressListener::STATE_START) {
+#ifdef NOISY_DOC_LOADING
+ {
+ nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest));
+ if (channel) {
+ nsCOMPtr<nsIURI> uri;
+ channel->GetURI(getter_AddRefs(uri));
+ if (uri) {
+ nsXPIDLCString spec;
+ uri->GetSpec(spec);
+ printf(" **** STATE_START: CHANNEL URI=%s, flags=%x\n",
+ spec.get(), aStateFlags);
+ }
+ } else {
+ printf(" STATE_START: NO CHANNEL flags=%x\n", aStateFlags);
+ }
+ }
+#endif
+ // Page level notification...
+ if (aStateFlags & nsIWebProgressListener::STATE_IS_NETWORK) {
+ nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest));
+ StartPageLoad(channel);
+#ifdef NOISY_DOC_LOADING
+ printf("STATE_START & STATE_IS_NETWORK flags=%x\n", aStateFlags);
+#endif
+ }
+
+ // Document level notification...
+ if (aStateFlags & nsIWebProgressListener::STATE_IS_DOCUMENT &&
+ !(aStateFlags & nsIWebProgressListener::STATE_RESTORING)) {
+#ifdef NOISY_DOC_LOADING
+ printf("STATE_START & STATE_IS_DOCUMENT flags=%x\n", aStateFlags);
+#endif
+
+ bool progressIsForTargetDocument =
+ IsProgressForTargetDocument(aWebProgress);
+
+ if (progressIsForTargetDocument) {
+ nsCOMPtr<mozIDOMWindowProxy> window;
+ aWebProgress->GetDOMWindow(getter_AddRefs(window));
+
+ auto* piWindow = nsPIDOMWindowOuter::From(window);
+ nsCOMPtr<nsIDocument> doc = piWindow->GetDoc();
+
+ nsCOMPtr<nsIHTMLDocument> htmlDoc(do_QueryInterface(doc));
+
+ if (htmlDoc && htmlDoc->IsWriting()) {
+ nsCOMPtr<nsIDOMHTMLDocument> htmlDomDoc = do_QueryInterface(doc);
+ nsAutoString designMode;
+ htmlDomDoc->GetDesignMode(designMode);
+
+ if (designMode.EqualsLiteral("on")) {
+ // This notification is for data coming in through
+ // document.open/write/close(), ignore it.
+
+ return NS_OK;
+ }
+ }
+
+ mCanCreateEditor = true;
+ StartDocumentLoad(aWebProgress, progressIsForTargetDocument);
+ }
+ }
+ }
+ //
+ // A Request is being processed
+ //
+ else if (aStateFlags & nsIWebProgressListener::STATE_TRANSFERRING) {
+ if (aStateFlags & nsIWebProgressListener::STATE_IS_DOCUMENT) {
+ // document transfer started
+ }
+ }
+ //
+ // Got a redirection
+ //
+ else if (aStateFlags & nsIWebProgressListener::STATE_REDIRECTING) {
+ if (aStateFlags & nsIWebProgressListener::STATE_IS_DOCUMENT) {
+ // got a redirect
+ }
+ }
+ //
+ // A network or document Request has finished...
+ //
+ else if (aStateFlags & nsIWebProgressListener::STATE_STOP) {
+#ifdef NOISY_DOC_LOADING
+ {
+ nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest));
+ if (channel) {
+ nsCOMPtr<nsIURI> uri;
+ channel->GetURI(getter_AddRefs(uri));
+ if (uri) {
+ nsXPIDLCString spec;
+ uri->GetSpec(spec);
+ printf(" **** STATE_STOP: CHANNEL URI=%s, flags=%x\n",
+ spec.get(), aStateFlags);
+ }
+ } else {
+ printf(" STATE_STOP: NO CHANNEL flags=%x\n", aStateFlags);
+ }
+ }
+#endif
+
+ // Document level notification...
+ if (aStateFlags & nsIWebProgressListener::STATE_IS_DOCUMENT) {
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
+ EndDocumentLoad(aWebProgress, channel, aStatus,
+ IsProgressForTargetDocument(aWebProgress));
+#ifdef NOISY_DOC_LOADING
+ printf("STATE_STOP & STATE_IS_DOCUMENT flags=%x\n", aStateFlags);
+#endif
+ }
+
+ // Page level notification...
+ if (aStateFlags & nsIWebProgressListener::STATE_IS_NETWORK) {
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
+ (void)EndPageLoad(aWebProgress, channel, aStatus);
+#ifdef NOISY_DOC_LOADING
+ printf("STATE_STOP & STATE_IS_NETWORK flags=%x\n", aStateFlags);
+#endif
+ }
+ }
+
+ return NS_OK;
+}
+
+/*---------------------------------------------------------------------------
+
+ OnProgressChange
+
+----------------------------------------------------------------------------*/
+NS_IMETHODIMP
+nsEditingSession::OnProgressChange(nsIWebProgress *aWebProgress,
+ nsIRequest *aRequest,
+ int32_t aCurSelfProgress,
+ int32_t aMaxSelfProgress,
+ int32_t aCurTotalProgress,
+ int32_t aMaxTotalProgress)
+{
+ NS_NOTREACHED("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+/*---------------------------------------------------------------------------
+
+ OnLocationChange
+
+----------------------------------------------------------------------------*/
+NS_IMETHODIMP
+nsEditingSession::OnLocationChange(nsIWebProgress *aWebProgress,
+ nsIRequest *aRequest, nsIURI *aURI,
+ uint32_t aFlags)
+{
+ nsCOMPtr<mozIDOMWindowProxy> domWindow;
+ nsresult rv = aWebProgress->GetDOMWindow(getter_AddRefs(domWindow));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ auto* piWindow = nsPIDOMWindowOuter::From(domWindow);
+
+ nsCOMPtr<nsIDocument> doc = piWindow->GetDoc();
+ NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE);
+
+ doc->SetDocumentURI(aURI);
+
+ // Notify the location-changed observer that
+ // the document URL has changed
+ nsIDocShell *docShell = piWindow->GetDocShell();
+ NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsICommandManager> commandManager = docShell->GetCommandManager();
+ nsCOMPtr<nsPICommandUpdater> commandUpdater =
+ do_QueryInterface(commandManager);
+ NS_ENSURE_TRUE(commandUpdater, NS_ERROR_FAILURE);
+
+ return commandUpdater->CommandStatusChanged("obs_documentLocationChanged");
+}
+
+/*---------------------------------------------------------------------------
+
+ OnStatusChange
+
+----------------------------------------------------------------------------*/
+NS_IMETHODIMP
+nsEditingSession::OnStatusChange(nsIWebProgress *aWebProgress,
+ nsIRequest *aRequest,
+ nsresult aStatus,
+ const char16_t *aMessage)
+{
+ NS_NOTREACHED("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+/*---------------------------------------------------------------------------
+
+ OnSecurityChange
+
+----------------------------------------------------------------------------*/
+NS_IMETHODIMP
+nsEditingSession::OnSecurityChange(nsIWebProgress *aWebProgress,
+ nsIRequest *aRequest, uint32_t state)
+{
+ NS_NOTREACHED("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+
+/*---------------------------------------------------------------------------
+
+ IsProgressForTargetDocument
+
+ Check that this notification is for our document.
+----------------------------------------------------------------------------*/
+
+bool
+nsEditingSession::IsProgressForTargetDocument(nsIWebProgress *aWebProgress)
+{
+ nsCOMPtr<nsIWebProgress> editedWebProgress = do_QueryReferent(mDocShell);
+ return editedWebProgress == aWebProgress;
+}
+
+
+/*---------------------------------------------------------------------------
+
+ GetEditorStatus
+
+ Called during GetCommandStateParams("obs_documentCreated"...)
+ to determine if editor was created and document
+ was loaded successfully
+----------------------------------------------------------------------------*/
+NS_IMETHODIMP
+nsEditingSession::GetEditorStatus(uint32_t *aStatus)
+{
+ NS_ENSURE_ARG_POINTER(aStatus);
+ *aStatus = mEditorStatus;
+ return NS_OK;
+}
+
+/*---------------------------------------------------------------------------
+
+ StartDocumentLoad
+
+ Called on start of load in a single frame
+----------------------------------------------------------------------------*/
+nsresult
+nsEditingSession::StartDocumentLoad(nsIWebProgress *aWebProgress,
+ bool aIsToBeMadeEditable)
+{
+#ifdef NOISY_DOC_LOADING
+ printf("======= StartDocumentLoad ========\n");
+#endif
+
+ NS_ENSURE_ARG_POINTER(aWebProgress);
+
+ if (aIsToBeMadeEditable) {
+ mEditorStatus = eEditorCreationInProgress;
+ }
+
+ return NS_OK;
+}
+
+/*---------------------------------------------------------------------------
+
+ EndDocumentLoad
+
+ Called on end of load in a single frame
+----------------------------------------------------------------------------*/
+nsresult
+nsEditingSession::EndDocumentLoad(nsIWebProgress *aWebProgress,
+ nsIChannel* aChannel, nsresult aStatus,
+ bool aIsToBeMadeEditable)
+{
+ NS_ENSURE_ARG_POINTER(aWebProgress);
+
+#ifdef NOISY_DOC_LOADING
+ printf("======= EndDocumentLoad ========\n");
+ printf("with status %d, ", aStatus);
+ nsCOMPtr<nsIURI> uri;
+ nsXPIDLCString spec;
+ if (NS_SUCCEEDED(aChannel->GetURI(getter_AddRefs(uri)))) {
+ uri->GetSpec(spec);
+ printf(" uri %s\n", spec.get());
+ }
+#endif
+
+ // We want to call the base class EndDocumentLoad,
+ // but avoid some of the stuff
+ // that nsDocShell does (need to refactor).
+
+ // OK, time to make an editor on this document
+ nsCOMPtr<mozIDOMWindowProxy> domWindow;
+ aWebProgress->GetDOMWindow(getter_AddRefs(domWindow));
+ NS_ENSURE_TRUE(domWindow, NS_ERROR_FAILURE);
+
+ // Set the error state -- we will create an editor
+ // anyway and load empty doc later
+ if (aIsToBeMadeEditable && aStatus == NS_ERROR_FILE_NOT_FOUND) {
+ mEditorStatus = eEditorErrorFileNotFound;
+ }
+
+ nsIDocShell *docShell = nsPIDOMWindowOuter::From(domWindow)->GetDocShell();
+ NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE); // better error handling?
+
+ // cancel refresh from meta tags
+ // we need to make sure that all pages in editor (whether editable or not)
+ // can't refresh contents being edited
+ nsCOMPtr<nsIRefreshURI> refreshURI = do_QueryInterface(docShell);
+ if (refreshURI) {
+ refreshURI->CancelRefreshURITimers();
+ }
+
+ nsresult rv = NS_OK;
+
+ // did someone set the flag to make this shell editable?
+ if (aIsToBeMadeEditable && mCanCreateEditor) {
+ bool makeEditable;
+ docShell->GetEditable(&makeEditable);
+
+ if (makeEditable) {
+ // To keep pre Gecko 1.9 behavior, setup editor always when
+ // mMakeWholeDocumentEditable.
+ bool needsSetup = false;
+ if (mMakeWholeDocumentEditable) {
+ needsSetup = true;
+ } else {
+ // do we already have an editor here?
+ nsCOMPtr<nsIEditor> editor;
+ rv = docShell->GetEditor(getter_AddRefs(editor));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ needsSetup = !editor;
+ }
+
+ if (needsSetup) {
+ mCanCreateEditor = false;
+ rv = SetupEditorOnWindow(domWindow);
+ if (NS_FAILED(rv)) {
+ // If we had an error, setup timer to load a blank page later
+ if (mLoadBlankDocTimer) {
+ // Must cancel previous timer?
+ mLoadBlankDocTimer->Cancel();
+ mLoadBlankDocTimer = nullptr;
+ }
+
+ mLoadBlankDocTimer = do_CreateInstance("@mozilla.org/timer;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mEditorStatus = eEditorCreationInProgress;
+ mLoadBlankDocTimer->InitWithFuncCallback(
+ nsEditingSession::TimerCallback,
+ static_cast<void*> (mDocShell.get()),
+ 10, nsITimer::TYPE_ONE_SHOT);
+ }
+ }
+ }
+ }
+ return rv;
+}
+
+
+void
+nsEditingSession::TimerCallback(nsITimer* aTimer, void* aClosure)
+{
+ nsCOMPtr<nsIDocShell> docShell = do_QueryReferent(static_cast<nsIWeakReference*> (aClosure));
+ if (docShell) {
+ nsCOMPtr<nsIWebNavigation> webNav(do_QueryInterface(docShell));
+ if (webNav) {
+ webNav->LoadURI(u"about:blank", 0, nullptr, nullptr, nullptr);
+ }
+ }
+}
+
+/*---------------------------------------------------------------------------
+
+ StartPageLoad
+
+ Called on start load of the entire page (incl. subframes)
+----------------------------------------------------------------------------*/
+nsresult
+nsEditingSession::StartPageLoad(nsIChannel *aChannel)
+{
+#ifdef NOISY_DOC_LOADING
+ printf("======= StartPageLoad ========\n");
+#endif
+ return NS_OK;
+}
+
+/*---------------------------------------------------------------------------
+
+ EndPageLoad
+
+ Called on end load of the entire page (incl. subframes)
+----------------------------------------------------------------------------*/
+nsresult
+nsEditingSession::EndPageLoad(nsIWebProgress *aWebProgress,
+ nsIChannel* aChannel, nsresult aStatus)
+{
+#ifdef NOISY_DOC_LOADING
+ printf("======= EndPageLoad ========\n");
+ printf(" with status %d, ", aStatus);
+ nsCOMPtr<nsIURI> uri;
+ nsXPIDLCString spec;
+ if (NS_SUCCEEDED(aChannel->GetURI(getter_AddRefs(uri)))) {
+ uri->GetSpec(spec);
+ printf("uri %s\n", spec.get());
+ }
+
+ nsAutoCString contentType;
+ aChannel->GetContentType(contentType);
+ if (!contentType.IsEmpty()) {
+ printf(" flags = %d, status = %d, MIMETYPE = %s\n",
+ mEditorFlags, mEditorStatus, contentType.get());
+ }
+#endif
+
+ // Set the error state -- we will create an editor anyway
+ // and load empty doc later
+ if (aStatus == NS_ERROR_FILE_NOT_FOUND) {
+ mEditorStatus = eEditorErrorFileNotFound;
+ }
+
+ nsCOMPtr<mozIDOMWindowProxy> domWindow;
+ aWebProgress->GetDOMWindow(getter_AddRefs(domWindow));
+
+ nsIDocShell *docShell =
+ domWindow ? nsPIDOMWindowOuter::From(domWindow)->GetDocShell() : nullptr;
+ NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE);
+
+ // cancel refresh from meta tags
+ // we need to make sure that all pages in editor (whether editable or not)
+ // can't refresh contents being edited
+ nsCOMPtr<nsIRefreshURI> refreshURI = do_QueryInterface(docShell);
+ if (refreshURI) {
+ refreshURI->CancelRefreshURITimers();
+ }
+
+#if 0
+ // Shouldn't we do this when we want to edit sub-frames?
+ return MakeWindowEditable(domWindow, "html", false, mInteractive);
+#else
+ return NS_OK;
+#endif
+}
+
+/*---------------------------------------------------------------------------
+
+ PrepareForEditing
+
+ Set up this editing session for one or more editors
+----------------------------------------------------------------------------*/
+nsresult
+nsEditingSession::PrepareForEditing(nsPIDOMWindowOuter* aWindow)
+{
+ if (mProgressListenerRegistered) {
+ return NS_OK;
+ }
+
+ nsIDocShell *docShell = aWindow ? aWindow->GetDocShell() : nullptr;
+
+ // register callback
+ nsCOMPtr<nsIWebProgress> webProgress = do_GetInterface(docShell);
+ NS_ENSURE_TRUE(webProgress, NS_ERROR_FAILURE);
+
+ nsresult rv =
+ webProgress->AddProgressListener(this,
+ (nsIWebProgress::NOTIFY_STATE_NETWORK |
+ nsIWebProgress::NOTIFY_STATE_DOCUMENT |
+ nsIWebProgress::NOTIFY_LOCATION));
+
+ mProgressListenerRegistered = NS_SUCCEEDED(rv);
+
+ return rv;
+}
+
+/*---------------------------------------------------------------------------
+
+ SetupEditorCommandController
+
+ Create a command controller, append to controllers,
+ get and return the controller ID, and set the context
+----------------------------------------------------------------------------*/
+nsresult
+nsEditingSession::SetupEditorCommandController(
+ const char *aControllerClassName,
+ mozIDOMWindowProxy *aWindow,
+ nsISupports *aContext,
+ uint32_t *aControllerId)
+{
+ NS_ENSURE_ARG_POINTER(aControllerClassName);
+ NS_ENSURE_ARG_POINTER(aWindow);
+ NS_ENSURE_ARG_POINTER(aContext);
+ NS_ENSURE_ARG_POINTER(aControllerId);
+
+ auto* piWindow = nsPIDOMWindowOuter::From(aWindow);
+ MOZ_ASSERT(piWindow);
+
+ nsCOMPtr<nsIControllers> controllers;
+ nsresult rv = piWindow->GetControllers(getter_AddRefs(controllers));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We only have to create each singleton controller once
+ // We know this has happened once we have a controllerId value
+ if (!*aControllerId) {
+ nsCOMPtr<nsIController> controller;
+ controller = do_CreateInstance(aControllerClassName, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We must insert at head of the list to be sure our
+ // controller is found before other implementations
+ // (e.g., not-implemented versions by browser)
+ rv = controllers->InsertControllerAt(0, controller);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Remember the ID for the controller
+ rv = controllers->GetControllerId(controller, aControllerId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Set the context
+ return SetContextOnControllerById(controllers, aContext, *aControllerId);
+}
+
+/*---------------------------------------------------------------------------
+
+ SetEditorOnControllers
+
+ Set the editor on the controller(s) for this window
+----------------------------------------------------------------------------*/
+NS_IMETHODIMP
+nsEditingSession::SetEditorOnControllers(mozIDOMWindowProxy* aWindow,
+ nsIEditor* aEditor)
+{
+ NS_ENSURE_TRUE(aWindow, NS_ERROR_NULL_POINTER);
+
+ auto* piWindow = nsPIDOMWindowOuter::From(aWindow);
+
+ nsCOMPtr<nsIControllers> controllers;
+ nsresult rv = piWindow->GetControllers(getter_AddRefs(controllers));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISupports> editorAsISupports = do_QueryInterface(aEditor);
+ if (mBaseCommandControllerId) {
+ rv = SetContextOnControllerById(controllers, editorAsISupports,
+ mBaseCommandControllerId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (mDocStateControllerId) {
+ rv = SetContextOnControllerById(controllers, editorAsISupports,
+ mDocStateControllerId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (mHTMLCommandControllerId) {
+ rv = SetContextOnControllerById(controllers, editorAsISupports,
+ mHTMLCommandControllerId);
+ }
+
+ return rv;
+}
+
+nsresult
+nsEditingSession::SetContextOnControllerById(nsIControllers* aControllers,
+ nsISupports* aContext,
+ uint32_t aID)
+{
+ NS_ENSURE_ARG_POINTER(aControllers);
+
+ // aContext can be null (when destroying editor)
+ nsCOMPtr<nsIController> controller;
+ aControllers->GetControllerById(aID, getter_AddRefs(controller));
+
+ // ok with nil controller
+ nsCOMPtr<nsIControllerContext> editorController =
+ do_QueryInterface(controller);
+ NS_ENSURE_TRUE(editorController, NS_ERROR_FAILURE);
+
+ return editorController->SetCommandContext(aContext);
+}
+
+void
+nsEditingSession::RemoveEditorControllers(nsPIDOMWindowOuter* aWindow)
+{
+ // Remove editor controllers from the aWindow, call when we're
+ // tearing down/detaching editor.
+
+ nsCOMPtr<nsIControllers> controllers;
+ if (aWindow) {
+ aWindow->GetControllers(getter_AddRefs(controllers));
+ }
+
+ if (controllers) {
+ nsCOMPtr<nsIController> controller;
+ if (mBaseCommandControllerId) {
+ controllers->GetControllerById(mBaseCommandControllerId,
+ getter_AddRefs(controller));
+ if (controller) {
+ controllers->RemoveController(controller);
+ }
+ }
+
+ if (mDocStateControllerId) {
+ controllers->GetControllerById(mDocStateControllerId,
+ getter_AddRefs(controller));
+ if (controller) {
+ controllers->RemoveController(controller);
+ }
+ }
+
+ if (mHTMLCommandControllerId) {
+ controllers->GetControllerById(mHTMLCommandControllerId,
+ getter_AddRefs(controller));
+ if (controller) {
+ controllers->RemoveController(controller);
+ }
+ }
+ }
+
+ // Clear IDs to trigger creation of new controllers.
+ mBaseCommandControllerId = 0;
+ mDocStateControllerId = 0;
+ mHTMLCommandControllerId = 0;
+}
+
+void
+nsEditingSession::RemoveWebProgressListener(nsPIDOMWindowOuter* aWindow)
+{
+ nsIDocShell *docShell = aWindow ? aWindow->GetDocShell() : nullptr;
+ nsCOMPtr<nsIWebProgress> webProgress = do_GetInterface(docShell);
+ if (webProgress) {
+ webProgress->RemoveProgressListener(this);
+ mProgressListenerRegistered = false;
+ }
+}
+
+void
+nsEditingSession::RestoreAnimationMode(nsPIDOMWindowOuter* aWindow)
+{
+ if (mInteractive) {
+ return;
+ }
+
+ nsCOMPtr<nsIDocShell> docShell = aWindow ? aWindow->GetDocShell() : nullptr;
+ NS_ENSURE_TRUE_VOID(docShell);
+ nsCOMPtr<nsIPresShell> presShell = docShell->GetPresShell();
+ NS_ENSURE_TRUE_VOID(presShell);
+ nsPresContext* presContext = presShell->GetPresContext();
+ NS_ENSURE_TRUE_VOID(presContext);
+
+ presContext->SetImageAnimationMode(mImageAnimationMode);
+}
+
+nsresult
+nsEditingSession::DetachFromWindow(mozIDOMWindowProxy* aWindow)
+{
+ NS_ENSURE_TRUE(mDoneSetup, NS_OK);
+
+ NS_ASSERTION(mStateMaintainer, "mStateMaintainer should exist.");
+
+ // Kill any existing reload timer
+ if (mLoadBlankDocTimer) {
+ mLoadBlankDocTimer->Cancel();
+ mLoadBlankDocTimer = nullptr;
+ }
+
+ auto* window = nsPIDOMWindowOuter::From(aWindow);
+
+ // Remove controllers, webprogress listener, and otherwise
+ // make things the way they were before we started editing.
+ RemoveEditorControllers(window);
+ RemoveWebProgressListener(window);
+ RestoreJSAndPlugins(aWindow);
+ RestoreAnimationMode(window);
+
+ // Kill our weak reference to our original window, in case
+ // it changes on restore, or otherwise dies.
+ mDocShell = nullptr;
+
+ return NS_OK;
+}
+
+nsresult
+nsEditingSession::ReattachToWindow(mozIDOMWindowProxy* aWindow)
+{
+ NS_ENSURE_TRUE(mDoneSetup, NS_OK);
+ NS_ENSURE_TRUE(aWindow, NS_ERROR_FAILURE);
+
+ NS_ASSERTION(mStateMaintainer, "mStateMaintainer should exist.");
+
+ // Imitate nsEditorDocShell::MakeEditable() to reattach the
+ // old editor ot the window.
+ nsresult rv;
+
+ auto* window = nsPIDOMWindowOuter::From(aWindow);
+ nsIDocShell *docShell = window->GetDocShell();
+ NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE);
+ mDocShell = do_GetWeakReference(docShell);
+
+ // Disable plugins.
+ if (!mInteractive) {
+ rv = DisableJSAndPlugins(aWindow);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Tells embedder that startup is in progress.
+ mEditorStatus = eEditorCreationInProgress;
+
+ // Adds back web progress listener.
+ rv = PrepareForEditing(window);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Setup the command controllers again.
+ rv = SetupEditorCommandController("@mozilla.org/editor/editingcontroller;1",
+ aWindow,
+ static_cast<nsIEditingSession*>(this),
+ &mBaseCommandControllerId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = SetupEditorCommandController("@mozilla.org/editor/editordocstatecontroller;1",
+ aWindow,
+ static_cast<nsIEditingSession*>(this),
+ &mDocStateControllerId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mStateMaintainer) {
+ mStateMaintainer->Init(window);
+ }
+
+ // Get editor
+ nsCOMPtr<nsIEditor> editor;
+ rv = GetEditorForWindow(aWindow, getter_AddRefs(editor));
+ NS_ENSURE_TRUE(editor, NS_ERROR_FAILURE);
+
+ if (!mInteractive) {
+ // Disable animation of images in this document:
+ nsCOMPtr<nsIPresShell> presShell = docShell->GetPresShell();
+ NS_ENSURE_TRUE(presShell, NS_ERROR_FAILURE);
+ nsPresContext* presContext = presShell->GetPresContext();
+ NS_ENSURE_TRUE(presContext, NS_ERROR_FAILURE);
+
+ mImageAnimationMode = presContext->ImageAnimationMode();
+ presContext->SetImageAnimationMode(imgIContainer::kDontAnimMode);
+ }
+
+ // The third controller takes an nsIEditor as the context
+ rv = SetupEditorCommandController("@mozilla.org/editor/htmleditorcontroller;1",
+ aWindow, editor,
+ &mHTMLCommandControllerId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Set context on all controllers to be the editor
+ rv = SetEditorOnControllers(aWindow, editor);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+#ifdef DEBUG
+ {
+ bool isEditable;
+ rv = WindowIsEditable(aWindow, &isEditable);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ASSERTION(isEditable, "Window is not editable after reattaching editor.");
+ }
+#endif // DEBUG
+
+ return NS_OK;
+}
diff --git a/editor/composer/nsEditingSession.h b/editor/composer/nsEditingSession.h
new file mode 100644
index 000000000..6772d3a96
--- /dev/null
+++ b/editor/composer/nsEditingSession.h
@@ -0,0 +1,147 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsEditingSession_h__
+#define nsEditingSession_h__
+
+
+#ifndef nsWeakReference_h__
+#include "nsWeakReference.h" // for nsSupportsWeakReference, etc
+#endif
+
+#include "nsCOMPtr.h" // for nsCOMPtr
+#include "nsISupportsImpl.h" // for NS_DECL_ISUPPORTS
+#include "nsIWeakReferenceUtils.h" // for nsWeakPtr
+#include "nsWeakReference.h" // for nsSupportsWeakReference, etc
+#include "nscore.h" // for nsresult
+
+#ifndef __gen_nsIWebProgressListener_h__
+#include "nsIWebProgressListener.h"
+#endif
+
+#ifndef __gen_nsIEditingSession_h__
+#include "nsIEditingSession.h" // for NS_DECL_NSIEDITINGSESSION, etc
+#endif
+
+#include "nsString.h" // for nsCString
+
+class mozIDOMWindowProxy;
+class nsIDOMWindow;
+class nsISupports;
+class nsITimer;
+
+#define NS_EDITINGSESSION_CID \
+{ 0xbc26ff01, 0xf2bd, 0x11d4, { 0xa7, 0x3c, 0xe5, 0xa4, 0xb5, 0xa8, 0xbd, 0xfc } }
+
+
+class nsComposerCommandsUpdater;
+class nsIChannel;
+class nsIControllers;
+class nsIDocShell;
+class nsIEditor;
+class nsIWebProgress;
+
+class nsEditingSession final : public nsIEditingSession,
+ public nsIWebProgressListener,
+ public nsSupportsWeakReference
+{
+public:
+
+ nsEditingSession();
+
+ // nsISupports
+ NS_DECL_ISUPPORTS
+
+ // nsIWebProgressListener
+ NS_DECL_NSIWEBPROGRESSLISTENER
+
+ // nsIEditingSession
+ NS_DECL_NSIEDITINGSESSION
+
+protected:
+ virtual ~nsEditingSession();
+
+ nsresult SetupEditorCommandController(const char *aControllerClassName,
+ mozIDOMWindowProxy* aWindow,
+ nsISupports *aContext,
+ uint32_t *aControllerId);
+
+ nsresult SetContextOnControllerById(nsIControllers* aControllers,
+ nsISupports* aContext,
+ uint32_t aID);
+
+ nsresult PrepareForEditing(nsPIDOMWindowOuter* aWindow);
+
+ static void TimerCallback(nsITimer *aTimer, void *aClosure);
+ nsCOMPtr<nsITimer> mLoadBlankDocTimer;
+
+ // progress load stuff
+ nsresult StartDocumentLoad(nsIWebProgress *aWebProgress,
+ bool isToBeMadeEditable);
+ nsresult EndDocumentLoad(nsIWebProgress *aWebProgress,
+ nsIChannel* aChannel, nsresult aStatus,
+ bool isToBeMadeEditable);
+ nsresult StartPageLoad(nsIChannel *aChannel);
+ nsresult EndPageLoad(nsIWebProgress *aWebProgress,
+ nsIChannel* aChannel, nsresult aStatus);
+
+ bool IsProgressForTargetDocument(nsIWebProgress *aWebProgress);
+
+ void RemoveEditorControllers(nsPIDOMWindowOuter* aWindow);
+ void RemoveWebProgressListener(nsPIDOMWindowOuter* aWindow);
+ void RestoreAnimationMode(nsPIDOMWindowOuter* aWindow);
+ void RemoveListenersAndControllers(nsPIDOMWindowOuter* aWindow,
+ nsIEditor *aEditor);
+
+protected:
+
+ bool mDoneSetup; // have we prepared for editing yet?
+
+ // Used to prevent double creation of editor because nsIWebProgressListener
+ // receives a STATE_STOP notification before the STATE_START
+ // for our document, so we wait for the STATE_START, then STATE_STOP
+ // before creating an editor
+ bool mCanCreateEditor;
+
+ bool mInteractive;
+ bool mMakeWholeDocumentEditable;
+
+ bool mDisabledJSAndPlugins;
+
+ // True if scripts were enabled before the editor turned scripts
+ // off, otherwise false.
+ bool mScriptsEnabled;
+
+ // True if plugins were enabled before the editor turned plugins
+ // off, otherwise false.
+ bool mPluginsEnabled;
+
+ bool mProgressListenerRegistered;
+
+ // The image animation mode before it was turned off.
+ uint16_t mImageAnimationMode;
+
+ // THE REMAINING MEMBER VARIABLES WILL BECOME A SET WHEN WE EDIT
+ // MORE THAN ONE EDITOR PER EDITING SESSION
+ RefPtr<nsComposerCommandsUpdater> mStateMaintainer;
+
+ // Save the editor type so we can create the editor after loading uri
+ nsCString mEditorType;
+ uint32_t mEditorFlags;
+ uint32_t mEditorStatus;
+ uint32_t mBaseCommandControllerId;
+ uint32_t mDocStateControllerId;
+ uint32_t mHTMLCommandControllerId;
+
+ // Make sure the docshell we use is safe
+ nsWeakPtr mDocShell;
+
+ // See if we can reuse an existing editor
+ nsWeakPtr mExistingEditor;
+};
+
+
+
+#endif // nsEditingSession_h__
diff --git a/editor/composer/nsEditorSpellCheck.cpp b/editor/composer/nsEditorSpellCheck.cpp
new file mode 100644
index 000000000..7cae48c1f
--- /dev/null
+++ b/editor/composer/nsEditorSpellCheck.cpp
@@ -0,0 +1,1005 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sts=2 sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <stdlib.h> // for getenv
+
+#include "mozilla/Attributes.h" // for final
+#include "mozilla/Preferences.h" // for Preferences
+#include "mozilla/Services.h" // for GetXULChromeRegistryService
+#include "mozilla/dom/Element.h" // for Element
+#include "mozilla/dom/Selection.h"
+#include "mozilla/mozalloc.h" // for operator delete, etc
+#include "nsAString.h" // for nsAString_internal::IsEmpty, etc
+#include "nsComponentManagerUtils.h" // for do_CreateInstance
+#include "nsDebug.h" // for NS_ENSURE_TRUE, etc
+#include "nsDependentSubstring.h" // for Substring
+#include "nsEditorSpellCheck.h"
+#include "nsError.h" // for NS_ERROR_NOT_INITIALIZED, etc
+#include "nsIChromeRegistry.h" // for nsIXULChromeRegistry
+#include "nsIContent.h" // for nsIContent
+#include "nsIContentPrefService.h" // for nsIContentPrefService, etc
+#include "nsIContentPrefService2.h" // for nsIContentPrefService2, etc
+#include "nsIDOMDocument.h" // for nsIDOMDocument
+#include "nsIDOMElement.h" // for nsIDOMElement
+#include "nsIDocument.h" // for nsIDocument
+#include "nsIEditor.h" // for nsIEditor
+#include "nsIHTMLEditor.h" // for nsIHTMLEditor
+#include "nsILoadContext.h"
+#include "nsISelection.h" // for nsISelection
+#include "nsISpellChecker.h" // for nsISpellChecker, etc
+#include "nsISupportsBase.h" // for nsISupports
+#include "nsISupportsUtils.h" // for NS_ADDREF
+#include "nsITextServicesDocument.h" // for nsITextServicesDocument
+#include "nsITextServicesFilter.h" // for nsITextServicesFilter
+#include "nsIURI.h" // for nsIURI
+#include "nsVariant.h" // for nsIWritableVariant, etc
+#include "nsLiteralString.h" // for NS_LITERAL_STRING, etc
+#include "nsMemory.h" // for nsMemory
+#include "nsRange.h"
+#include "nsReadableUtils.h" // for ToNewUnicode, EmptyString, etc
+#include "nsServiceManagerUtils.h" // for do_GetService
+#include "nsString.h" // for nsAutoString, nsString, etc
+#include "nsStringFwd.h" // for nsAFlatString
+#include "nsStyleUtil.h" // for nsStyleUtil
+#include "nsXULAppAPI.h" // for XRE_GetProcessType
+#include "nsIPlaintextEditor.h" // for editor flags
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+class UpdateDictionaryHolder {
+ private:
+ nsEditorSpellCheck* mSpellCheck;
+ public:
+ explicit UpdateDictionaryHolder(nsEditorSpellCheck* esc): mSpellCheck(esc) {
+ if (mSpellCheck) {
+ mSpellCheck->BeginUpdateDictionary();
+ }
+ }
+ ~UpdateDictionaryHolder() {
+ if (mSpellCheck) {
+ mSpellCheck->EndUpdateDictionary();
+ }
+ }
+};
+
+#define CPS_PREF_NAME NS_LITERAL_STRING("spellcheck.lang")
+
+/**
+ * Gets the URI of aEditor's document.
+ */
+static nsresult
+GetDocumentURI(nsIEditor* aEditor, nsIURI * *aURI)
+{
+ NS_ENSURE_ARG_POINTER(aEditor);
+ NS_ENSURE_ARG_POINTER(aURI);
+
+ nsCOMPtr<nsIDOMDocument> domDoc;
+ aEditor->GetDocument(getter_AddRefs(domDoc));
+ NS_ENSURE_TRUE(domDoc, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIDocument> doc = do_QueryInterface(domDoc);
+ NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIURI> docUri = doc->GetDocumentURI();
+ NS_ENSURE_TRUE(docUri, NS_ERROR_FAILURE);
+
+ *aURI = docUri;
+ NS_ADDREF(*aURI);
+ return NS_OK;
+}
+
+static already_AddRefed<nsILoadContext>
+GetLoadContext(nsIEditor* aEditor)
+{
+ nsCOMPtr<nsIDOMDocument> domDoc;
+ aEditor->GetDocument(getter_AddRefs(domDoc));
+ NS_ENSURE_TRUE(domDoc, nullptr);
+
+ nsCOMPtr<nsIDocument> doc = do_QueryInterface(domDoc);
+ NS_ENSURE_TRUE(doc, nullptr);
+
+ nsCOMPtr<nsILoadContext> loadContext = doc->GetLoadContext();
+ return loadContext.forget();
+}
+
+/**
+ * Fetches the dictionary stored in content prefs and maintains state during the
+ * fetch, which is asynchronous.
+ */
+class DictionaryFetcher final : public nsIContentPrefCallback2
+{
+public:
+ NS_DECL_ISUPPORTS
+
+ DictionaryFetcher(nsEditorSpellCheck* aSpellCheck,
+ nsIEditorSpellCheckCallback* aCallback,
+ uint32_t aGroup)
+ : mCallback(aCallback), mGroup(aGroup), mSpellCheck(aSpellCheck) {}
+
+ NS_IMETHOD Fetch(nsIEditor* aEditor);
+
+ NS_IMETHOD HandleResult(nsIContentPref* aPref) override
+ {
+ nsCOMPtr<nsIVariant> value;
+ nsresult rv = aPref->GetValue(getter_AddRefs(value));
+ NS_ENSURE_SUCCESS(rv, rv);
+ value->GetAsAString(mDictionary);
+ return NS_OK;
+ }
+
+ NS_IMETHOD HandleCompletion(uint16_t reason) override
+ {
+ mSpellCheck->DictionaryFetched(this);
+ return NS_OK;
+ }
+
+ NS_IMETHOD HandleError(nsresult error) override
+ {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIEditorSpellCheckCallback> mCallback;
+ uint32_t mGroup;
+ nsString mRootContentLang;
+ nsString mRootDocContentLang;
+ nsString mDictionary;
+
+private:
+ ~DictionaryFetcher() {}
+
+ RefPtr<nsEditorSpellCheck> mSpellCheck;
+};
+NS_IMPL_ISUPPORTS(DictionaryFetcher, nsIContentPrefCallback2)
+
+NS_IMETHODIMP
+DictionaryFetcher::Fetch(nsIEditor* aEditor)
+{
+ NS_ENSURE_ARG_POINTER(aEditor);
+
+ nsresult rv;
+
+ nsCOMPtr<nsIURI> docUri;
+ rv = GetDocumentURI(aEditor, getter_AddRefs(docUri));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString docUriSpec;
+ rv = docUri->GetSpec(docUriSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIContentPrefService2> contentPrefService =
+ do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
+ NS_ENSURE_TRUE(contentPrefService, NS_ERROR_NOT_AVAILABLE);
+
+ nsCOMPtr<nsILoadContext> loadContext = GetLoadContext(aEditor);
+ rv = contentPrefService->GetByDomainAndName(NS_ConvertUTF8toUTF16(docUriSpec),
+ CPS_PREF_NAME, loadContext,
+ this);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+/**
+ * Stores the current dictionary for aEditor's document URL.
+ */
+static nsresult
+StoreCurrentDictionary(nsIEditor* aEditor, const nsAString& aDictionary)
+{
+ NS_ENSURE_ARG_POINTER(aEditor);
+
+ nsresult rv;
+
+ nsCOMPtr<nsIURI> docUri;
+ rv = GetDocumentURI(aEditor, getter_AddRefs(docUri));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString docUriSpec;
+ rv = docUri->GetSpec(docUriSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<nsVariant> prefValue = new nsVariant();
+ prefValue->SetAsAString(aDictionary);
+
+ nsCOMPtr<nsIContentPrefService2> contentPrefService =
+ do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
+ NS_ENSURE_TRUE(contentPrefService, NS_ERROR_NOT_INITIALIZED);
+
+ nsCOMPtr<nsILoadContext> loadContext = GetLoadContext(aEditor);
+ return contentPrefService->Set(NS_ConvertUTF8toUTF16(docUriSpec),
+ CPS_PREF_NAME, prefValue, loadContext,
+ nullptr);
+}
+
+/**
+ * Forgets the current dictionary stored for aEditor's document URL.
+ */
+static nsresult
+ClearCurrentDictionary(nsIEditor* aEditor)
+{
+ NS_ENSURE_ARG_POINTER(aEditor);
+
+ nsresult rv;
+
+ nsCOMPtr<nsIURI> docUri;
+ rv = GetDocumentURI(aEditor, getter_AddRefs(docUri));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString docUriSpec;
+ rv = docUri->GetSpec(docUriSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIContentPrefService2> contentPrefService =
+ do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
+ NS_ENSURE_TRUE(contentPrefService, NS_ERROR_NOT_INITIALIZED);
+
+ nsCOMPtr<nsILoadContext> loadContext = GetLoadContext(aEditor);
+ return contentPrefService->RemoveByDomainAndName(
+ NS_ConvertUTF8toUTF16(docUriSpec), CPS_PREF_NAME, loadContext, nullptr);
+}
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(nsEditorSpellCheck)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(nsEditorSpellCheck)
+
+NS_INTERFACE_MAP_BEGIN(nsEditorSpellCheck)
+ NS_INTERFACE_MAP_ENTRY(nsIEditorSpellCheck)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIEditorSpellCheck)
+ NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(nsEditorSpellCheck)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION(nsEditorSpellCheck,
+ mEditor,
+ mSpellChecker,
+ mTxtSrvFilter)
+
+nsEditorSpellCheck::nsEditorSpellCheck()
+ : mSuggestedWordIndex(0)
+ , mDictionaryIndex(0)
+ , mEditor(nullptr)
+ , mDictionaryFetcherGroup(0)
+ , mUpdateDictionaryRunning(false)
+{
+}
+
+nsEditorSpellCheck::~nsEditorSpellCheck()
+{
+ // Make sure we blow the spellchecker away, just in
+ // case it hasn't been destroyed already.
+ mSpellChecker = nullptr;
+}
+
+// The problem is that if the spell checker does not exist, we can not tell
+// which dictionaries are installed. This function works around the problem,
+// allowing callers to ask if we can spell check without actually doing so (and
+// enabling or disabling UI as necessary). This just creates a spellcheck
+// object if needed and asks it for the dictionary list.
+NS_IMETHODIMP
+nsEditorSpellCheck::CanSpellCheck(bool* _retval)
+{
+ nsresult rv;
+ nsCOMPtr<nsISpellChecker> spellChecker;
+ if (! mSpellChecker) {
+ spellChecker = do_CreateInstance(NS_SPELLCHECKER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ spellChecker = mSpellChecker;
+ }
+ nsTArray<nsString> dictList;
+ rv = spellChecker->GetDictionaryList(&dictList);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *_retval = (dictList.Length() > 0);
+ return NS_OK;
+}
+
+// Instances of this class can be used as either runnables or RAII helpers.
+class CallbackCaller final : public Runnable
+{
+public:
+ explicit CallbackCaller(nsIEditorSpellCheckCallback* aCallback)
+ : mCallback(aCallback) {}
+
+ ~CallbackCaller()
+ {
+ Run();
+ }
+
+ NS_IMETHOD Run() override
+ {
+ if (mCallback) {
+ mCallback->EditorSpellCheckDone();
+ mCallback = nullptr;
+ }
+ return NS_OK;
+ }
+
+private:
+ nsCOMPtr<nsIEditorSpellCheckCallback> mCallback;
+};
+
+NS_IMETHODIMP
+nsEditorSpellCheck::InitSpellChecker(nsIEditor* aEditor, bool aEnableSelectionChecking, nsIEditorSpellCheckCallback* aCallback)
+{
+ NS_ENSURE_TRUE(aEditor, NS_ERROR_NULL_POINTER);
+ mEditor = aEditor;
+
+ nsresult rv;
+
+ // We can spell check with any editor type
+ nsCOMPtr<nsITextServicesDocument>tsDoc =
+ do_CreateInstance("@mozilla.org/textservices/textservicesdocument;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ENSURE_TRUE(tsDoc, NS_ERROR_NULL_POINTER);
+
+ tsDoc->SetFilter(mTxtSrvFilter);
+
+ // Pass the editor to the text services document
+ rv = tsDoc->InitWithEditor(aEditor);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aEnableSelectionChecking) {
+ // Find out if the section is collapsed or not.
+ // If it isn't, we want to spellcheck just the selection.
+
+ nsCOMPtr<nsISelection> domSelection;
+ aEditor->GetSelection(getter_AddRefs(domSelection));
+ if (NS_WARN_IF(!domSelection)) {
+ return NS_ERROR_FAILURE;
+ }
+ RefPtr<Selection> selection = domSelection->AsSelection();
+
+ int32_t count = 0;
+
+ rv = selection->GetRangeCount(&count);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (count > 0) {
+ RefPtr<nsRange> range = selection->GetRangeAt(0);
+ NS_ENSURE_STATE(range);
+
+ bool collapsed = false;
+ rv = range->GetCollapsed(&collapsed);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!collapsed) {
+ // We don't want to touch the range in the selection,
+ // so create a new copy of it.
+
+ RefPtr<nsRange> rangeBounds = range->CloneRange();
+
+ // Make sure the new range spans complete words.
+
+ rv = tsDoc->ExpandRangeToWordBoundaries(rangeBounds);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Now tell the text services that you only want
+ // to iterate over the text in this range.
+
+ rv = tsDoc->SetExtent(rangeBounds);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ }
+
+ mSpellChecker = do_CreateInstance(NS_SPELLCHECKER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NULL_POINTER);
+
+ rv = mSpellChecker->SetDocument(tsDoc, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // do not fail if UpdateCurrentDictionary fails because this method may
+ // succeed later.
+ rv = UpdateCurrentDictionary(aCallback);
+ if (NS_FAILED(rv) && aCallback) {
+ // However, if it does fail, we still need to call the callback since we
+ // discard the failure. Do it asynchronously so that the caller is always
+ // guaranteed async behavior.
+ RefPtr<CallbackCaller> caller = new CallbackCaller(aCallback);
+ NS_ENSURE_STATE(caller);
+ rv = NS_DispatchToMainThread(caller);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsEditorSpellCheck::GetNextMisspelledWord(char16_t **aNextMisspelledWord)
+{
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ nsAutoString nextMisspelledWord;
+
+ DeleteSuggestedWordList();
+ // Beware! This may flush notifications via synchronous
+ // ScrollSelectionIntoView.
+ nsresult rv = mSpellChecker->NextMisspelledWord(nextMisspelledWord,
+ &mSuggestedWordList);
+
+ *aNextMisspelledWord = ToNewUnicode(nextMisspelledWord);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsEditorSpellCheck::GetSuggestedWord(char16_t **aSuggestedWord)
+{
+ nsAutoString word;
+ // XXX This is buggy if mSuggestedWordList.Length() is over INT32_MAX.
+ if (mSuggestedWordIndex < static_cast<int32_t>(mSuggestedWordList.Length())) {
+ *aSuggestedWord = ToNewUnicode(mSuggestedWordList[mSuggestedWordIndex]);
+ mSuggestedWordIndex++;
+ } else {
+ // A blank string signals that there are no more strings
+ *aSuggestedWord = ToNewUnicode(EmptyString());
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsEditorSpellCheck::CheckCurrentWord(const char16_t *aSuggestedWord,
+ bool *aIsMisspelled)
+{
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ DeleteSuggestedWordList();
+ return mSpellChecker->CheckWord(nsDependentString(aSuggestedWord),
+ aIsMisspelled, &mSuggestedWordList);
+}
+
+NS_IMETHODIMP
+nsEditorSpellCheck::CheckCurrentWordNoSuggest(const char16_t *aSuggestedWord,
+ bool *aIsMisspelled)
+{
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ return mSpellChecker->CheckWord(nsDependentString(aSuggestedWord),
+ aIsMisspelled, nullptr);
+}
+
+NS_IMETHODIMP
+nsEditorSpellCheck::ReplaceWord(const char16_t *aMisspelledWord,
+ const char16_t *aReplaceWord,
+ bool allOccurrences)
+{
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ return mSpellChecker->Replace(nsDependentString(aMisspelledWord),
+ nsDependentString(aReplaceWord), allOccurrences);
+}
+
+NS_IMETHODIMP
+nsEditorSpellCheck::IgnoreWordAllOccurrences(const char16_t *aWord)
+{
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ return mSpellChecker->IgnoreAll(nsDependentString(aWord));
+}
+
+NS_IMETHODIMP
+nsEditorSpellCheck::GetPersonalDictionary()
+{
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ // We can spell check with any editor type
+ mDictionaryList.Clear();
+ mDictionaryIndex = 0;
+ return mSpellChecker->GetPersonalDictionary(&mDictionaryList);
+}
+
+NS_IMETHODIMP
+nsEditorSpellCheck::GetPersonalDictionaryWord(char16_t **aDictionaryWord)
+{
+ // XXX This is buggy if mDictionaryList.Length() is over INT32_MAX.
+ if (mDictionaryIndex < static_cast<int32_t>(mDictionaryList.Length())) {
+ *aDictionaryWord = ToNewUnicode(mDictionaryList[mDictionaryIndex]);
+ mDictionaryIndex++;
+ } else {
+ // A blank string signals that there are no more strings
+ *aDictionaryWord = ToNewUnicode(EmptyString());
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsEditorSpellCheck::AddWordToDictionary(const char16_t *aWord)
+{
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ return mSpellChecker->AddWordToPersonalDictionary(nsDependentString(aWord));
+}
+
+NS_IMETHODIMP
+nsEditorSpellCheck::RemoveWordFromDictionary(const char16_t *aWord)
+{
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ return mSpellChecker->RemoveWordFromPersonalDictionary(nsDependentString(aWord));
+}
+
+NS_IMETHODIMP
+nsEditorSpellCheck::GetDictionaryList(char16_t ***aDictionaryList, uint32_t *aCount)
+{
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ NS_ENSURE_TRUE(aDictionaryList && aCount, NS_ERROR_NULL_POINTER);
+
+ *aDictionaryList = 0;
+ *aCount = 0;
+
+ nsTArray<nsString> dictList;
+
+ nsresult rv = mSpellChecker->GetDictionaryList(&dictList);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ char16_t **tmpPtr = 0;
+
+ if (dictList.IsEmpty()) {
+ // If there are no dictionaries, return an array containing
+ // one element and a count of one.
+
+ tmpPtr = (char16_t **)moz_xmalloc(sizeof(char16_t *));
+
+ NS_ENSURE_TRUE(tmpPtr, NS_ERROR_OUT_OF_MEMORY);
+
+ *tmpPtr = 0;
+ *aDictionaryList = tmpPtr;
+ *aCount = 0;
+
+ return NS_OK;
+ }
+
+ tmpPtr = (char16_t **)moz_xmalloc(sizeof(char16_t *) * dictList.Length());
+
+ NS_ENSURE_TRUE(tmpPtr, NS_ERROR_OUT_OF_MEMORY);
+
+ *aDictionaryList = tmpPtr;
+ *aCount = dictList.Length();
+
+ for (uint32_t i = 0; i < *aCount; i++) {
+ tmpPtr[i] = ToNewUnicode(dictList[i]);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsEditorSpellCheck::GetCurrentDictionary(nsAString& aDictionary)
+{
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ return mSpellChecker->GetCurrentDictionary(aDictionary);
+}
+
+NS_IMETHODIMP
+nsEditorSpellCheck::SetCurrentDictionary(const nsAString& aDictionary)
+{
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ RefPtr<nsEditorSpellCheck> kungFuDeathGrip = this;
+
+ // The purpose of mUpdateDictionaryRunning is to avoid doing all of this if
+ // UpdateCurrentDictionary's helper method DictionaryFetched, which calls us,
+ // is on the stack. In other words: Only do this, if the user manually selected a
+ // dictionary to use.
+ if (!mUpdateDictionaryRunning) {
+
+ // Ignore pending dictionary fetchers by increasing this number.
+ mDictionaryFetcherGroup++;
+
+ uint32_t flags = 0;
+ mEditor->GetFlags(&flags);
+ if (!(flags & nsIPlaintextEditor::eEditorMailMask)) {
+ if (!aDictionary.IsEmpty() && (mPreferredLang.IsEmpty() ||
+ !mPreferredLang.Equals(aDictionary,
+ nsCaseInsensitiveStringComparator()))) {
+ // When user sets dictionary manually, we store this value associated
+ // with editor url, if it doesn't match the document language exactly.
+ // For example on "en" sites, we need to store "en-GB", otherwise
+ // the language might jump back to en-US although the user explicitly
+ // chose otherwise.
+ StoreCurrentDictionary(mEditor, aDictionary);
+#ifdef DEBUG_DICT
+ printf("***** Writing content preferences for |%s|\n",
+ NS_ConvertUTF16toUTF8(aDictionary).get());
+#endif
+ } else {
+ // If user sets a dictionary matching the language defined by
+ // document, we consider content pref has been canceled, and we clear it.
+ ClearCurrentDictionary(mEditor);
+#ifdef DEBUG_DICT
+ printf("***** Clearing content preferences for |%s|\n",
+ NS_ConvertUTF16toUTF8(aDictionary).get());
+#endif
+ }
+
+ // Also store it in as a preference, so we can use it as a fallback.
+ // We don't want this for mail composer because it uses
+ // "spellchecker.dictionary" as a preference.
+ Preferences::SetString("spellchecker.dictionary", aDictionary);
+#ifdef DEBUG_DICT
+ printf("***** Storing spellchecker.dictionary |%s|\n",
+ NS_ConvertUTF16toUTF8(aDictionary).get());
+#endif
+ }
+ }
+ return mSpellChecker->SetCurrentDictionary(aDictionary);
+}
+
+NS_IMETHODIMP
+nsEditorSpellCheck::UninitSpellChecker()
+{
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ // Cleanup - kill the spell checker
+ DeleteSuggestedWordList();
+ mDictionaryList.Clear();
+ mDictionaryIndex = 0;
+ mSpellChecker = nullptr;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsEditorSpellCheck::SetFilter(nsITextServicesFilter *filter)
+{
+ mTxtSrvFilter = filter;
+ return NS_OK;
+}
+
+nsresult
+nsEditorSpellCheck::DeleteSuggestedWordList()
+{
+ mSuggestedWordList.Clear();
+ mSuggestedWordIndex = 0;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsEditorSpellCheck::UpdateCurrentDictionary(nsIEditorSpellCheckCallback* aCallback)
+{
+ if (NS_WARN_IF(!mSpellChecker)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ nsresult rv;
+
+ RefPtr<nsEditorSpellCheck> kungFuDeathGrip = this;
+
+ // Get language with html5 algorithm
+ nsCOMPtr<nsIContent> rootContent;
+ nsCOMPtr<nsIHTMLEditor> htmlEditor = do_QueryInterface(mEditor);
+ if (htmlEditor) {
+ rootContent = htmlEditor->GetActiveEditingHost();
+ } else {
+ nsCOMPtr<nsIDOMElement> rootElement;
+ rv = mEditor->GetRootElement(getter_AddRefs(rootElement));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rootContent = do_QueryInterface(rootElement);
+ }
+
+ // Try to get topmost document's document element for embedded mail editor.
+ uint32_t flags = 0;
+ mEditor->GetFlags(&flags);
+ if (flags & nsIPlaintextEditor::eEditorMailMask) {
+ nsCOMPtr<nsIDocument> ownerDoc = rootContent->OwnerDoc();
+ NS_ENSURE_TRUE(ownerDoc, NS_ERROR_FAILURE);
+ nsIDocument* parentDoc = ownerDoc->GetParentDocument();
+ if (parentDoc) {
+ rootContent = do_QueryInterface(parentDoc->GetDocumentElement());
+ }
+ }
+
+ if (!rootContent) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<DictionaryFetcher> fetcher =
+ new DictionaryFetcher(this, aCallback, mDictionaryFetcherGroup);
+ rootContent->GetLang(fetcher->mRootContentLang);
+ nsCOMPtr<nsIDocument> doc = rootContent->GetUncomposedDoc();
+ NS_ENSURE_STATE(doc);
+ doc->GetContentLanguage(fetcher->mRootDocContentLang);
+
+ rv = fetcher->Fetch(mEditor);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+// Helper function that iterates over the list of dictionaries and sets the one
+// that matches based on a given comparison type.
+nsresult
+nsEditorSpellCheck::TryDictionary(const nsAString& aDictName,
+ nsTArray<nsString>& aDictList,
+ enum dictCompare aCompareType)
+{
+ nsresult rv = NS_ERROR_NOT_AVAILABLE;
+
+ for (uint32_t i = 0; i < aDictList.Length(); i++) {
+ nsAutoString dictStr(aDictList.ElementAt(i));
+ bool equals = false;
+ switch (aCompareType) {
+ case DICT_NORMAL_COMPARE:
+ equals = aDictName.Equals(dictStr);
+ break;
+ case DICT_COMPARE_CASE_INSENSITIVE:
+ equals = aDictName.Equals(dictStr, nsCaseInsensitiveStringComparator());
+ break;
+ case DICT_COMPARE_DASHMATCH:
+ equals = nsStyleUtil::DashMatchCompare(dictStr, aDictName, nsCaseInsensitiveStringComparator());
+ break;
+ }
+ if (equals) {
+ rv = mSpellChecker->SetCurrentDictionary(dictStr);
+#ifdef DEBUG_DICT
+ if (NS_SUCCEEDED(rv))
+ printf("***** Set |%s|.\n", NS_ConvertUTF16toUTF8(dictStr).get());
+#endif
+ // We always break here. We tried to set the dictionary to an existing
+ // dictionary from the list. This must work, if it doesn't, there is
+ // no point trying another one.
+ break;
+ }
+ }
+ return rv;
+}
+
+nsresult
+nsEditorSpellCheck::DictionaryFetched(DictionaryFetcher* aFetcher)
+{
+ MOZ_ASSERT(aFetcher);
+ RefPtr<nsEditorSpellCheck> kungFuDeathGrip = this;
+
+ // Important: declare the holder after the callback caller so that the former
+ // is destructed first so that it's not active when the callback is called.
+ CallbackCaller callbackCaller(aFetcher->mCallback);
+ UpdateDictionaryHolder holder(this);
+
+ if (aFetcher->mGroup < mDictionaryFetcherGroup) {
+ // SetCurrentDictionary was called after the fetch started. Don't overwrite
+ // that dictionary with the fetched one.
+ return NS_OK;
+ }
+
+ /*
+ * We try to derive the dictionary to use based on the following priorities:
+ * 1) Content preference, so the language the user set for the site before.
+ * (Introduced in bug 678842 and corrected in bug 717433.)
+ * 2) Language set by the website, or any other dictionary that partly
+ * matches that. (Introduced in bug 338427.)
+ * Eg. if the website is "en-GB", a user who only has "en-US" will get
+ * that. If the website is generic "en", the user will get one of the
+ * "en-*" installed, (almost) at random.
+ * However, we prefer what is stored in "spellchecker.dictionary",
+ * so if the user chose "en-AU" before, they will get "en-AU" on a plain
+ * "en" site. (Introduced in bug 682564.)
+ * 3) The value of "spellchecker.dictionary" which reflects a previous
+ * language choice of the user (on another site).
+ * (This was the original behaviour before the aforementioned bugs
+ * landed).
+ * 4) The user's locale.
+ * 5) Use the current dictionary that is currently set.
+ * 6) The content of the "LANG" environment variable (if set).
+ * 7) The first spell check dictionary installed.
+ */
+
+ // Get the language from the element or its closest parent according to:
+ // https://html.spec.whatwg.org/#attr-lang
+ // This is used in SetCurrentDictionary.
+ mPreferredLang.Assign(aFetcher->mRootContentLang);
+#ifdef DEBUG_DICT
+ printf("***** mPreferredLang (element) |%s|\n",
+ NS_ConvertUTF16toUTF8(mPreferredLang).get());
+#endif
+
+ // If no luck, try the "Content-Language" header.
+ if (mPreferredLang.IsEmpty()) {
+ mPreferredLang.Assign(aFetcher->mRootDocContentLang);
+#ifdef DEBUG_DICT
+ printf("***** mPreferredLang (content-language) |%s|\n",
+ NS_ConvertUTF16toUTF8(mPreferredLang).get());
+#endif
+ }
+
+ // Auxiliary status.
+ nsresult rv2;
+
+ // We obtain a list of available dictionaries.
+ nsTArray<nsString> dictList;
+ rv2 = mSpellChecker->GetDictionaryList(&dictList);
+ NS_ENSURE_SUCCESS(rv2, rv2);
+
+ // Priority 1:
+ // If we successfully fetched a dictionary from content prefs, do not go
+ // further. Use this exact dictionary.
+ // Don't use content preferences for editor with eEditorMailMask flag.
+ nsAutoString dictName;
+ uint32_t flags;
+ mEditor->GetFlags(&flags);
+ if (!(flags & nsIPlaintextEditor::eEditorMailMask)) {
+ dictName.Assign(aFetcher->mDictionary);
+ if (!dictName.IsEmpty()) {
+ if (NS_SUCCEEDED(TryDictionary(dictName, dictList, DICT_NORMAL_COMPARE))) {
+#ifdef DEBUG_DICT
+ printf("***** Assigned from content preferences |%s|\n",
+ NS_ConvertUTF16toUTF8(dictName).get());
+#endif
+
+ // We take an early exit here, so let's not forget to clear the word
+ // list.
+ DeleteSuggestedWordList();
+ return NS_OK;
+ }
+ // May be dictionary was uninstalled ?
+ // Clear the content preference and continue.
+ ClearCurrentDictionary(mEditor);
+ }
+ }
+
+ // Priority 2:
+ // After checking the content preferences, we use the language of the element
+ // or document.
+ dictName.Assign(mPreferredLang);
+#ifdef DEBUG_DICT
+ printf("***** Assigned from element/doc |%s|\n",
+ NS_ConvertUTF16toUTF8(dictName).get());
+#endif
+
+ // Get the preference value.
+ nsAutoString preferredDict;
+ preferredDict = Preferences::GetLocalizedString("spellchecker.dictionary");
+
+ // The following will be driven by this status. Once we were able to set a
+ // dictionary successfully, we're done. So we start with a "failed" status.
+ nsresult rv = NS_ERROR_NOT_AVAILABLE;
+
+ if (!dictName.IsEmpty()) {
+ // RFC 5646 explicitly states that matches should be case-insensitive.
+ rv = TryDictionary (dictName, dictList, DICT_COMPARE_CASE_INSENSITIVE);
+
+ if (NS_FAILED(rv)) {
+#ifdef DEBUG_DICT
+ printf("***** Setting of |%s| failed (or it wasn't available)\n",
+ NS_ConvertUTF16toUTF8(dictName).get());
+#endif
+
+ // Required dictionary was not available. Try to get a dictionary
+ // matching at least language part of dictName.
+ nsAutoString langCode;
+ int32_t dashIdx = dictName.FindChar('-');
+ if (dashIdx != -1) {
+ langCode.Assign(Substring(dictName, 0, dashIdx));
+ } else {
+ langCode.Assign(dictName);
+ }
+
+ // Try dictionary.spellchecker preference, if it starts with langCode,
+ // so we don't just get any random dictionary matching the language.
+ if (!preferredDict.IsEmpty() &&
+ nsStyleUtil::DashMatchCompare(preferredDict, langCode, nsDefaultStringComparator())) {
+#ifdef DEBUG_DICT
+ printf("***** Trying preference value |%s| since it matches language code\n",
+ NS_ConvertUTF16toUTF8(preferredDict).get());
+#endif
+ rv = TryDictionary (preferredDict, dictList,
+ DICT_COMPARE_CASE_INSENSITIVE);
+ }
+
+ if (NS_FAILED(rv)) {
+ // Use any dictionary with the required language.
+#ifdef DEBUG_DICT
+ printf("***** Trying to find match for language code |%s|\n",
+ NS_ConvertUTF16toUTF8(langCode).get());
+#endif
+ rv = TryDictionary (langCode, dictList, DICT_COMPARE_DASHMATCH);
+ }
+ }
+ }
+
+ // Priority 3:
+ // If the document didn't supply a dictionary or the setting failed,
+ // try the user preference next.
+ if (NS_FAILED(rv)) {
+ if (!preferredDict.IsEmpty()) {
+#ifdef DEBUG_DICT
+ printf("***** Trying preference value |%s|\n",
+ NS_ConvertUTF16toUTF8(preferredDict).get());
+#endif
+ rv = TryDictionary (preferredDict, dictList, DICT_NORMAL_COMPARE);
+ }
+ }
+
+ // Priority 4:
+ // As next fallback, try the current locale.
+ if (NS_FAILED(rv)) {
+ nsCOMPtr<nsIXULChromeRegistry> packageRegistry =
+ mozilla::services::GetXULChromeRegistryService();
+
+ if (packageRegistry) {
+ nsAutoCString utf8DictName;
+ rv2 = packageRegistry->GetSelectedLocale(NS_LITERAL_CSTRING("global"),
+ false, utf8DictName);
+ dictName.Assign(EmptyString());
+ AppendUTF8toUTF16(utf8DictName, dictName);
+#ifdef DEBUG_DICT
+ printf("***** Trying locale |%s|\n",
+ NS_ConvertUTF16toUTF8(dictName).get());
+#endif
+ rv = TryDictionary (dictName, dictList, DICT_COMPARE_CASE_INSENSITIVE);
+ }
+ }
+
+ if (NS_FAILED(rv)) {
+ // Still no success.
+
+ // Priority 5:
+ // If we have a current dictionary, don't try anything else.
+ nsAutoString currentDictionary;
+ rv2 = GetCurrentDictionary(currentDictionary);
+#ifdef DEBUG_DICT
+ if (NS_SUCCEEDED(rv2)) {
+ printf("***** Retrieved current dict |%s|\n",
+ NS_ConvertUTF16toUTF8(currentDictionary).get());
+ }
+#endif
+
+ if (NS_FAILED(rv2) || currentDictionary.IsEmpty()) {
+ // Priority 6:
+ // Try to get current dictionary from environment variable LANG.
+ // LANG = language[_territory][.charset]
+ char* env_lang = getenv("LANG");
+ if (env_lang) {
+ nsString lang = NS_ConvertUTF8toUTF16(env_lang);
+ // Strip trailing charset, if there is any.
+ int32_t dot_pos = lang.FindChar('.');
+ if (dot_pos != -1) {
+ lang = Substring(lang, 0, dot_pos);
+ }
+
+ int32_t underScore = lang.FindChar('_');
+ if (underScore != -1) {
+ lang.Replace(underScore, 1, '-');
+#ifdef DEBUG_DICT
+ printf("***** Trying LANG from environment |%s|\n",
+ NS_ConvertUTF16toUTF8(lang).get());
+#endif
+ nsAutoString lang2;
+ lang2.Assign(lang);
+ rv = TryDictionary(lang2, dictList, DICT_COMPARE_CASE_INSENSITIVE);
+ }
+ }
+
+ // Priority 7:
+ // If it does not work, pick the first one.
+ if (NS_FAILED(rv) && !dictList.IsEmpty()) {
+ nsAutoString firstInList;
+ firstInList.Assign(dictList[0]);
+ rv = TryDictionary(firstInList, dictList, DICT_NORMAL_COMPARE);
+#ifdef DEBUG_DICT
+ printf("***** Trying first of list |%s|\n",
+ NS_ConvertUTF16toUTF8(dictList[0]).get());
+ if (NS_SUCCEEDED(rv)) {
+ printf ("***** Setting worked.\n");
+ }
+#endif
+ }
+ }
+ }
+
+ // If an error was thrown while setting the dictionary, just
+ // fail silently so that the spellchecker dialog is allowed to come
+ // up. The user can manually reset the language to their choice on
+ // the dialog if it is wrong.
+
+ DeleteSuggestedWordList();
+
+ return NS_OK;
+}
diff --git a/editor/composer/nsEditorSpellCheck.h b/editor/composer/nsEditorSpellCheck.h
new file mode 100644
index 000000000..e39a2c891
--- /dev/null
+++ b/editor/composer/nsEditorSpellCheck.h
@@ -0,0 +1,85 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsEditorSpellCheck_h___
+#define nsEditorSpellCheck_h___
+
+
+#include "nsCOMPtr.h" // for nsCOMPtr
+#include "nsCycleCollectionParticipant.h"
+#include "nsIEditorSpellCheck.h" // for NS_DECL_NSIEDITORSPELLCHECK, etc
+#include "nsISupportsImpl.h"
+#include "nsString.h" // for nsString
+#include "nsTArray.h" // for nsTArray
+#include "nscore.h" // for nsresult
+
+class nsIEditor;
+class nsISpellChecker;
+class nsITextServicesFilter;
+
+#define NS_EDITORSPELLCHECK_CID \
+{ /* {75656ad9-bd13-4c5d-939a-ec6351eea0cc} */ \
+ 0x75656ad9, 0xbd13, 0x4c5d, \
+ { 0x93, 0x9a, 0xec, 0x63, 0x51, 0xee, 0xa0, 0xcc }\
+}
+
+class DictionaryFetcher;
+
+enum dictCompare {
+ DICT_NORMAL_COMPARE,
+ DICT_COMPARE_CASE_INSENSITIVE,
+ DICT_COMPARE_DASHMATCH
+};
+
+class nsEditorSpellCheck final : public nsIEditorSpellCheck
+{
+ friend class DictionaryFetcher;
+
+public:
+ nsEditorSpellCheck();
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(nsEditorSpellCheck)
+
+ /* Declare all methods in the nsIEditorSpellCheck interface */
+ NS_DECL_NSIEDITORSPELLCHECK
+
+protected:
+ virtual ~nsEditorSpellCheck();
+
+ nsCOMPtr<nsISpellChecker> mSpellChecker;
+
+ nsTArray<nsString> mSuggestedWordList;
+ int32_t mSuggestedWordIndex;
+
+ // these are the words in the current personal dictionary,
+ // GetPersonalDictionary must be called to load them.
+ nsTArray<nsString> mDictionaryList;
+ int32_t mDictionaryIndex;
+
+ nsresult DeleteSuggestedWordList();
+
+ nsCOMPtr<nsITextServicesFilter> mTxtSrvFilter;
+ nsCOMPtr<nsIEditor> mEditor;
+
+ nsString mPreferredLang;
+
+ uint32_t mDictionaryFetcherGroup;
+
+ bool mUpdateDictionaryRunning;
+
+ nsresult TryDictionary(const nsAString& aDictName, nsTArray<nsString>& aDictList,
+ enum dictCompare aCompareType);
+
+ nsresult DictionaryFetched(DictionaryFetcher* aFetchState);
+
+public:
+ void BeginUpdateDictionary() { mUpdateDictionaryRunning = true ;}
+ void EndUpdateDictionary() { mUpdateDictionaryRunning = false ;}
+};
+
+#endif // nsEditorSpellCheck_h___
+
+
diff --git a/editor/composer/nsIEditingSession.idl b/editor/composer/nsIEditingSession.idl
new file mode 100644
index 000000000..6cbf00481
--- /dev/null
+++ b/editor/composer/nsIEditingSession.idl
@@ -0,0 +1,104 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "domstubs.idl"
+
+interface mozIDOMWindowProxy;
+interface nsIEditor;
+
+[scriptable, uuid(24f963d1-e6fc-43ea-a206-99ac5fcc5265)]
+
+interface nsIEditingSession : nsISupports
+{
+ /**
+ * Error codes when we fail to create an editor
+ * is placed in attribute editorStatus
+ */
+ const long eEditorOK = 0;
+ const long eEditorCreationInProgress = 1;
+ const long eEditorErrorCantEditMimeType = 2;
+ const long eEditorErrorFileNotFound = 3;
+ const long eEditorErrorCantEditFramesets = 8;
+ const long eEditorErrorUnknown = 9;
+
+ /**
+ * Status after editor creation and document loading
+ * Value is one of the above error codes
+ */
+ readonly attribute unsigned long editorStatus;
+
+ /**
+ * Make this window editable
+ * @param aWindow nsIDOMWindow, the window the embedder needs to make editable
+ * @param aEditorType string, "html" "htmlsimple" "text" "textsimple"
+ * @param aMakeWholeDocumentEditable if PR_TRUE make the whole document in
+ * aWindow editable, otherwise it's the
+ * embedder who should make the document
+ * (or part of it) editable.
+ * @param aInteractive if PR_FALSE turn off scripting and plugins
+ */
+ void makeWindowEditable(in mozIDOMWindowProxy window,
+ in string aEditorType,
+ in boolean doAfterUriLoad,
+ in boolean aMakeWholeDocumentEditable,
+ in boolean aInteractive);
+
+ /**
+ * Test whether a specific window has had its editable flag set; it may have an editor
+ * now, or will get one after the uri load.
+ *
+ * Use this, passing the content root window, to test if we've set up editing
+ * for this content.
+ */
+ boolean windowIsEditable(in mozIDOMWindowProxy window);
+
+ /**
+ * Get the editor for this window. May return null
+ */
+ nsIEditor getEditorForWindow(in mozIDOMWindowProxy window);
+
+ /**
+ * Setup editor and related support objects
+ */
+ void setupEditorOnWindow(in mozIDOMWindowProxy window);
+
+ /**
+ * Destroy editor and related support objects
+ */
+ void tearDownEditorOnWindow(in mozIDOMWindowProxy window);
+
+ void setEditorOnControllers(in mozIDOMWindowProxy aWindow,
+ in nsIEditor aEditor);
+
+ /**
+ * Disable scripts and plugins in aWindow.
+ */
+ void disableJSAndPlugins(in mozIDOMWindowProxy aWindow);
+
+ /**
+ * Restore JS and plugins (enable/disable them) according to the state they
+ * were before the last call to disableJSAndPlugins.
+ */
+ void restoreJSAndPlugins(in mozIDOMWindowProxy aWindow);
+
+ /**
+ * Removes all the editor's controllers/listeners etc and makes the window
+ * uneditable.
+ */
+ void detachFromWindow(in mozIDOMWindowProxy aWindow);
+
+ /**
+ * Undos detachFromWindow(), reattaches this editing session/editor
+ * to the window.
+ */
+ void reattachToWindow(in mozIDOMWindowProxy aWindow);
+
+ /**
+ * Whether this session has disabled JS and plugins.
+ */
+ readonly attribute boolean jsAndPluginsDisabled;
+};
+
diff --git a/editor/composer/res/EditorOverride.css b/editor/composer/res/EditorOverride.css
new file mode 100644
index 000000000..4940fbb30
--- /dev/null
+++ b/editor/composer/res/EditorOverride.css
@@ -0,0 +1,323 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+*|* {
+ -moz-user-modify: read-write;
+}
+
+/* Styles to alter look of things in the Editor content window
+ * that should NOT be removed when we display in completely WYSIWYG
+ * "Browser Preview" mode.
+ * Anything that should change, like appearance of table borders
+ * and Named Anchors, should be placed in EditorContent.css instead of here.
+*/
+
+/* Primary cursor is text I-beam */
+
+::-moz-canvas, a:link {
+ cursor: text;
+}
+
+/* Use default arrow over objects with size that
+ are selected when clicked on.
+ Override the browser's pointer cursor over links
+*/
+
+img, img[usemap], area,
+object, object[usemap],
+applet, hr, button, input, textarea, select,
+a:link img, a:visited img, a:active img,
+a[name]:-moz-only-whitespace {
+ cursor: default;
+}
+
+a:visited, a:active {
+ cursor: text;
+}
+
+/* Prevent clicking on links from going to link */
+a:link img, a:visited img {
+ -moz-user-input: none;
+}
+
+/* We suppress user/author's prefs for link underline,
+ so we must set explicitly. This isn't good!
+*/
+a:link {
+ text-decoration: underline -moz-anchor-decoration;
+ color: -moz-hyperlinktext;
+}
+
+/* Allow double-clicks on these widgets to open properties dialogs
+ XXX except when the widget has disabled attribute */
+input, button, textarea {
+ -moz-user-select: all !important;
+ -moz-user-input: auto !important;
+ -moz-user-focus: none !important;
+}
+
+/* XXX Still need a better way of blocking other events to these widgets */
+select, input[disabled], input[type="checkbox"], input[type="radio"], input[type="file"] {
+ -moz-user-select: all !important;
+ -moz-user-input: none !important;
+ -moz-user-focus: none !important;
+}
+
+input[type="hidden"] {
+ border: 1px solid black !important;
+ visibility: visible !important;
+}
+
+label {
+ -moz-user-select: all !important;
+}
+
+::-moz-display-comboboxcontrol-frame {
+ -moz-user-select: text !important;
+}
+
+option {
+ -moz-user-select: text !important;
+}
+
+#mozToc.readonly {
+ -moz-user-select: all !important;
+ -moz-user-input: none !important;
+}
+
+/* the following rules are for Image Resizing */
+
+span[\_moz_anonclass="mozResizer"] {
+ width: 5px;
+ height: 5px;
+ position: absolute;
+ border: 1px black solid;
+ background-color: white;
+ -moz-user-select: none;
+ z-index: 2147483646; /* max value -1 for this property */
+}
+
+/* we can't use :active below */
+span[\_moz_anonclass="mozResizer"][\_moz_activated],
+span[\_moz_anonclass="mozResizer"]:hover {
+ background-color: black;
+}
+
+span[\_moz_anonclass="mozResizer"].hidden,
+span[\_moz_anonclass="mozResizingShadow"].hidden,
+img[\_moz_anonclass="mozResizingShadow"].hidden,
+span[\_moz_anonclass="mozGrabber"].hidden,
+span[\_moz_anonclass="mozResizingInfo"].hidden,
+a[\_moz_anonclass="mozTableRemoveRow"].hidden,
+a[\_moz_anonclass="mozTableRemoveColumn"].hidden {
+ display: none !important;
+}
+
+span[\_moz_anonclass="mozResizer"][anonlocation="nw"] {
+ cursor: nw-resize;
+}
+span[\_moz_anonclass="mozResizer"][anonlocation="n"] {
+ cursor: n-resize;
+}
+span[\_moz_anonclass="mozResizer"][anonlocation="ne"] {
+ cursor: ne-resize;
+}
+span[\_moz_anonclass="mozResizer"][anonlocation="w"] {
+ cursor: w-resize;
+}
+span[\_moz_anonclass="mozResizer"][anonlocation="e"] {
+ cursor: e-resize;
+}
+span[\_moz_anonclass="mozResizer"][anonlocation="sw"] {
+ cursor: sw-resize;
+}
+span[\_moz_anonclass="mozResizer"][anonlocation="s"] {
+ cursor: s-resize;
+}
+span[\_moz_anonclass="mozResizer"][anonlocation="se"] {
+ cursor: se-resize;
+}
+
+span[\_moz_anonclass="mozResizingShadow"],
+img[\_moz_anonclass="mozResizingShadow"] {
+ outline: thin dashed black;
+ -moz-user-select: none;
+ opacity: 0.5;
+ position: absolute;
+ z-index: 2147483647; /* max value for this property */
+}
+
+span[\_moz_anonclass="mozResizingInfo"] {
+ font-family: sans-serif;
+ font-size: x-small;
+ color: black;
+ background-color: #d0d0d0;
+ border: ridge 2px #d0d0d0;
+ padding: 2px;
+ position: absolute;
+ z-index: 2147483647; /* max value for this property */
+}
+
+img[\_moz_resizing] {
+ outline: thin solid black;
+}
+
+*[\_moz_abspos] {
+ outline: silver ridge 2px;
+ z-index: 2147483645 !important; /* max value -2 for this property */
+}
+*[\_moz_abspos="white"] {
+ background-color: white !important;
+}
+*[\_moz_abspos="black"] {
+ background-color: black !important;
+}
+
+span[\_moz_anonclass="mozGrabber"] {
+ outline: ridge 2px silver;
+ padding: 2px;
+ position: absolute;
+ width: 12px;
+ height: 12px;
+ background-image: url("resource://gre/res/grabber.gif");
+ background-repeat: no-repeat;
+ background-position: center center;
+ -moz-user-select: none;
+ cursor: move;
+ z-index: 2147483647; /* max value for this property */
+}
+
+/* INLINE TABLE EDITING */
+
+a[\_moz_anonclass="mozTableAddColumnBefore"] {
+ position: absolute;
+ z-index: 2147483647; /* max value for this property */
+ text-decoration: none !important;
+ border: none 0px !important;
+ width: 4px;
+ height: 8px;
+ background-image: url("resource://gre/res/table-add-column-before.gif");
+ background-repeat: no-repeat;
+ background-position: center center;
+ -moz-user-select: none !important;
+ -moz-user-focus: none !important;
+}
+
+a[\_moz_anonclass="mozTableAddColumnBefore"]:hover {
+ background-image: url("resource://gre/res/table-add-column-before-hover.gif");
+}
+
+a[\_moz_anonclass="mozTableAddColumnBefore"]:active {
+ background-image: url("resource://gre/res/table-add-column-before-active.gif");
+}
+
+a[\_moz_anonclass="mozTableAddColumnAfter"] {
+ position: absolute;
+ z-index: 2147483647; /* max value for this property */
+ text-decoration: none !important;
+ border: none 0px !important;
+ width: 4px;
+ height: 8px;
+ background-image: url("resource://gre/res/table-add-column-after.gif");
+ background-repeat: no-repeat;
+ background-position: center center;
+ -moz-user-select: none !important;
+ -moz-user-focus: none !important;
+}
+
+a[\_moz_anonclass="mozTableAddColumnAfter"]:hover {
+ background-image: url("resource://gre/res/table-add-column-after-hover.gif");
+}
+
+a[\_moz_anonclass="mozTableAddColumnAfter"]:active {
+ background-image: url("resource://gre/res/table-add-column-after-active.gif");
+}
+
+a[\_moz_anonclass="mozTableRemoveColumn"] {
+ position: absolute;
+ z-index: 2147483647; /* max value for this property */
+ text-decoration: none !important;
+ border: none 0px !important;
+ width: 8px;
+ height: 8px;
+ background-image: url("resource://gre/res/table-remove-column.gif");
+ background-repeat: no-repeat;
+ background-position: center center;
+ -moz-user-select: none !important;
+ -moz-user-focus: none !important;
+}
+
+a[\_moz_anonclass="mozTableRemoveColumn"]:hover {
+ background-image: url("resource://gre/res/table-remove-column-hover.gif");
+}
+
+a[\_moz_anonclass="mozTableRemoveColumn"]:active {
+ background-image: url("resource://gre/res/table-remove-column-active.gif");
+}
+
+a[\_moz_anonclass="mozTableAddRowBefore"] {
+ position: absolute;
+ z-index: 2147483647; /* max value for this property */
+ text-decoration: none !important;
+ border: none 0px !important;
+ width: 8px;
+ height: 4px;
+ background-image: url("resource://gre/res/table-add-row-before.gif");
+ background-repeat: no-repeat;
+ background-position: center center;
+ -moz-user-select: none !important;
+ -moz-user-focus: none !important;
+}
+
+a[\_moz_anonclass="mozTableAddRowBefore"]:hover {
+ background-image: url("resource://gre/res/table-add-row-before-hover.gif");
+}
+
+a[\_moz_anonclass="mozTableAddRowBefore"]:active {
+ background-image: url("resource://gre/res/table-add-row-before-active.gif");
+}
+
+a[\_moz_anonclass="mozTableAddRowAfter"] {
+ position: absolute;
+ z-index: 2147483647; /* max value for this property */
+ text-decoration: none !important;
+ border: none 0px !important;
+ width: 8px;
+ height: 4px;
+ background-image: url("resource://gre/res/table-add-row-after.gif");
+ background-repeat: no-repeat;
+ background-position: center center;
+ -moz-user-select: none !important;
+ -moz-user-focus: none !important;
+}
+
+a[\_moz_anonclass="mozTableAddRowAfter"]:hover {
+ background-image: url("resource://gre/res/table-add-row-after-hover.gif");
+}
+
+a[\_moz_anonclass="mozTableAddRowAfter"]:active {
+ background-image: url("resource://gre/res/table-add-row-after-active.gif");
+}
+
+a[\_moz_anonclass="mozTableRemoveRow"] {
+ position: absolute;
+ z-index: 2147483647; /* max value for this property */
+ text-decoration: none !important;
+ border: none 0px !important;
+ width: 8px;
+ height: 8px;
+ background-image: url("resource://gre/res/table-remove-row.gif");
+ background-repeat: no-repeat;
+ background-position: center center;
+ -moz-user-select: none !important;
+ -moz-user-focus: none !important;
+}
+
+a[\_moz_anonclass="mozTableRemoveRow"]:hover {
+ background-image: url("resource://gre/res/table-remove-row-hover.gif");
+}
+
+a[\_moz_anonclass="mozTableRemoveRow"]:active {
+ background-image: url("resource://gre/res/table-remove-row-active.gif");
+}
diff --git a/editor/composer/res/grabber.gif b/editor/composer/res/grabber.gif
new file mode 100644
index 000000000..06749a64f
--- /dev/null
+++ b/editor/composer/res/grabber.gif
Binary files differ
diff --git a/editor/composer/res/table-add-column-after-active.gif b/editor/composer/res/table-add-column-after-active.gif
new file mode 100644
index 000000000..3ec50b82e
--- /dev/null
+++ b/editor/composer/res/table-add-column-after-active.gif
Binary files differ
diff --git a/editor/composer/res/table-add-column-after-hover.gif b/editor/composer/res/table-add-column-after-hover.gif
new file mode 100644
index 000000000..29679f981
--- /dev/null
+++ b/editor/composer/res/table-add-column-after-hover.gif
Binary files differ
diff --git a/editor/composer/res/table-add-column-after.gif b/editor/composer/res/table-add-column-after.gif
new file mode 100644
index 000000000..8891be969
--- /dev/null
+++ b/editor/composer/res/table-add-column-after.gif
Binary files differ
diff --git a/editor/composer/res/table-add-column-before-active.gif b/editor/composer/res/table-add-column-before-active.gif
new file mode 100644
index 000000000..1e205291e
--- /dev/null
+++ b/editor/composer/res/table-add-column-before-active.gif
Binary files differ
diff --git a/editor/composer/res/table-add-column-before-hover.gif b/editor/composer/res/table-add-column-before-hover.gif
new file mode 100644
index 000000000..7b54537e4
--- /dev/null
+++ b/editor/composer/res/table-add-column-before-hover.gif
Binary files differ
diff --git a/editor/composer/res/table-add-column-before.gif b/editor/composer/res/table-add-column-before.gif
new file mode 100644
index 000000000..d4a3ffe5e
--- /dev/null
+++ b/editor/composer/res/table-add-column-before.gif
Binary files differ
diff --git a/editor/composer/res/table-add-row-after-active.gif b/editor/composer/res/table-add-row-after-active.gif
new file mode 100644
index 000000000..cc01da2c9
--- /dev/null
+++ b/editor/composer/res/table-add-row-after-active.gif
Binary files differ
diff --git a/editor/composer/res/table-add-row-after-hover.gif b/editor/composer/res/table-add-row-after-hover.gif
new file mode 100644
index 000000000..a829351b6
--- /dev/null
+++ b/editor/composer/res/table-add-row-after-hover.gif
Binary files differ
diff --git a/editor/composer/res/table-add-row-after.gif b/editor/composer/res/table-add-row-after.gif
new file mode 100644
index 000000000..3f1a39d98
--- /dev/null
+++ b/editor/composer/res/table-add-row-after.gif
Binary files differ
diff --git a/editor/composer/res/table-add-row-before-active.gif b/editor/composer/res/table-add-row-before-active.gif
new file mode 100644
index 000000000..34f1e0ade
--- /dev/null
+++ b/editor/composer/res/table-add-row-before-active.gif
Binary files differ
diff --git a/editor/composer/res/table-add-row-before-hover.gif b/editor/composer/res/table-add-row-before-hover.gif
new file mode 100644
index 000000000..e8f1d10b0
--- /dev/null
+++ b/editor/composer/res/table-add-row-before-hover.gif
Binary files differ
diff --git a/editor/composer/res/table-add-row-before.gif b/editor/composer/res/table-add-row-before.gif
new file mode 100644
index 000000000..1682170cb
--- /dev/null
+++ b/editor/composer/res/table-add-row-before.gif
Binary files differ
diff --git a/editor/composer/res/table-remove-column-active.gif b/editor/composer/res/table-remove-column-active.gif
new file mode 100644
index 000000000..4dfbde4ce
--- /dev/null
+++ b/editor/composer/res/table-remove-column-active.gif
Binary files differ
diff --git a/editor/composer/res/table-remove-column-hover.gif b/editor/composer/res/table-remove-column-hover.gif
new file mode 100644
index 000000000..fd11bb52c
--- /dev/null
+++ b/editor/composer/res/table-remove-column-hover.gif
Binary files differ
diff --git a/editor/composer/res/table-remove-column.gif b/editor/composer/res/table-remove-column.gif
new file mode 100644
index 000000000..d8071da0a
--- /dev/null
+++ b/editor/composer/res/table-remove-column.gif
Binary files differ
diff --git a/editor/composer/res/table-remove-row-active.gif b/editor/composer/res/table-remove-row-active.gif
new file mode 100644
index 000000000..4dfbde4ce
--- /dev/null
+++ b/editor/composer/res/table-remove-row-active.gif
Binary files differ
diff --git a/editor/composer/res/table-remove-row-hover.gif b/editor/composer/res/table-remove-row-hover.gif
new file mode 100644
index 000000000..fd11bb52c
--- /dev/null
+++ b/editor/composer/res/table-remove-row-hover.gif
Binary files differ
diff --git a/editor/composer/res/table-remove-row.gif b/editor/composer/res/table-remove-row.gif
new file mode 100644
index 000000000..d8071da0a
--- /dev/null
+++ b/editor/composer/res/table-remove-row.gif
Binary files differ
diff --git a/editor/composer/test/bug1200533_subframe.html b/editor/composer/test/bug1200533_subframe.html
new file mode 100644
index 000000000..6eaefc266
--- /dev/null
+++ b/editor/composer/test/bug1200533_subframe.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta http-equiv="Content-Language" content="en-US">
+</head>
+<body>
+<textarea id="none">root en-US</textarea>
+<textarea id="en-GB" lang="en-GB">root en-US, but element en-GB</textarea>
+<textarea id="en-gb" lang="en-gb">root en-US, but element en-gb (lower case)</textarea>
+<textarea id="en-ZA-not-avail" lang="en-ZA">root en-US, but element en-ZA (which is not installed)</textarea>
+<textarea id="en-generic" lang="en">root en-US, but element en</textarea>
+<textarea id="ko-not-avail" lang="ko">root en-US, but element ko (which is not installed)</textarea>
+</body>
+</html>
diff --git a/editor/composer/test/bug1204147_subframe.html b/editor/composer/test/bug1204147_subframe.html
new file mode 100644
index 000000000..a9b1225cd
--- /dev/null
+++ b/editor/composer/test/bug1204147_subframe.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+<textarea id="en-GB" lang="en-GB">element en-GB</textarea>
+<textarea id="en-US" lang="testing-XX">element should default to en-US</textarea>
+
+<div id="trouble-maker" contenteditable>the presence of this div triggers the faulty code path</div>
+</body>
+</html>
diff --git a/editor/composer/test/bug1204147_subframe2.html b/editor/composer/test/bug1204147_subframe2.html
new file mode 100644
index 000000000..935777bd9
--- /dev/null
+++ b/editor/composer/test/bug1204147_subframe2.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+<textarea id="en-GB" lang="en-GB">element en-GB</textarea>
+<textarea id="en-US" lang="testing-XX">element should default to en-US</textarea>
+</body>
+</html>
diff --git a/editor/composer/test/bug678842_subframe.html b/editor/composer/test/bug678842_subframe.html
new file mode 100644
index 000000000..39d578ee4
--- /dev/null
+++ b/editor/composer/test/bug678842_subframe.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+<textarea id="textarea" lang="testing-XXX"></textarea>
+</body>
+</html>
diff --git a/editor/composer/test/bug717433_subframe.html b/editor/composer/test/bug717433_subframe.html
new file mode 100644
index 000000000..3c2927e88
--- /dev/null
+++ b/editor/composer/test/bug717433_subframe.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+<textarea id="textarea" lang="en"></textarea>
+</body>
+</html>
diff --git a/editor/composer/test/chrome.ini b/editor/composer/test/chrome.ini
new file mode 100644
index 000000000..015cad3d5
--- /dev/null
+++ b/editor/composer/test/chrome.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+skip-if = os == 'android'
+
+[test_bug434998.xul]
+[test_bug1266815.html]
diff --git a/editor/composer/test/de-DE/de_DE.aff b/editor/composer/test/de-DE/de_DE.aff
new file mode 100644
index 000000000..5dc6896b6
--- /dev/null
+++ b/editor/composer/test/de-DE/de_DE.aff
@@ -0,0 +1,2 @@
+# Affix file for German English dictionary
+# Fake file, nothing here.
diff --git a/editor/composer/test/de-DE/de_DE.dic b/editor/composer/test/de-DE/de_DE.dic
new file mode 100644
index 000000000..415c21686
--- /dev/null
+++ b/editor/composer/test/de-DE/de_DE.dic
@@ -0,0 +1,6 @@
+5
+ein
+guter
+heute
+ist
+Tag
diff --git a/editor/composer/test/en-AU/en_AU.aff b/editor/composer/test/en-AU/en_AU.aff
new file mode 100644
index 000000000..e0c467248
--- /dev/null
+++ b/editor/composer/test/en-AU/en_AU.aff
@@ -0,0 +1,2 @@
+# Affix file for British English dictionary
+# Fake file, nothing here.
diff --git a/editor/composer/test/en-AU/en_AU.dic b/editor/composer/test/en-AU/en_AU.dic
new file mode 100644
index 000000000..0a1be725d
--- /dev/null
+++ b/editor/composer/test/en-AU/en_AU.dic
@@ -0,0 +1,4 @@
+3
+Mary
+Paul
+Peter
diff --git a/editor/composer/test/en-GB/en_GB.aff b/editor/composer/test/en-GB/en_GB.aff
new file mode 100644
index 000000000..e0c467248
--- /dev/null
+++ b/editor/composer/test/en-GB/en_GB.aff
@@ -0,0 +1,2 @@
+# Affix file for British English dictionary
+# Fake file, nothing here.
diff --git a/editor/composer/test/en-GB/en_GB.dic b/editor/composer/test/en-GB/en_GB.dic
new file mode 100644
index 000000000..0a1be725d
--- /dev/null
+++ b/editor/composer/test/en-GB/en_GB.dic
@@ -0,0 +1,4 @@
+3
+Mary
+Paul
+Peter
diff --git a/editor/composer/test/mochitest.ini b/editor/composer/test/mochitest.ini
new file mode 100644
index 000000000..832137d60
--- /dev/null
+++ b/editor/composer/test/mochitest.ini
@@ -0,0 +1,40 @@
+[DEFAULT]
+support-files =
+ bug678842_subframe.html
+ bug717433_subframe.html
+ bug1200533_subframe.html
+ bug1204147_subframe.html
+ bug1204147_subframe2.html
+ en-GB/en_GB.dic
+ en-GB/en_GB.aff
+ en-AU/en_AU.dic
+ en-AU/en_AU.aff
+ de-DE/de_DE.dic
+ de-DE/de_DE.aff
+
+[test_async_UpdateCurrentDictionary.html]
+skip-if = os == 'android'
+[test_bug338427.html]
+skip-if = os == 'android'
+[test_bug348497.html]
+[test_bug384147.html]
+[test_bug389350.html]
+skip-if = toolkit == 'android'
+[test_bug519928.html]
+[test_bug678842.html]
+skip-if = os == 'android'
+[test_bug697981.html]
+skip-if = os == 'android'
+[test_bug717433.html]
+skip-if = os == 'android'
+[test_bug738440.html]
+[test_bug1200533.html]
+skip-if = os == 'android'
+[test_bug1204147.html]
+skip-if = os == 'android'
+[test_bug1205983.html]
+skip-if = os == 'android'
+[test_bug1209414.html]
+skip-if = os == 'android'
+[test_bug1219928.html]
+skip-if = e10s || os == 'android'
diff --git a/editor/composer/test/test_async_UpdateCurrentDictionary.html b/editor/composer/test/test_async_UpdateCurrentDictionary.html
new file mode 100644
index 000000000..53d9ff307
--- /dev/null
+++ b/editor/composer/test/test_async_UpdateCurrentDictionary.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=856270
+-->
+<head>
+ <title>Test for Bug 856270 - Async UpdateCurrentDictionary</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=856270">Mozilla Bug 856270</a>
+<p id="display"></p>
+<div id="content">
+<textarea id="editor" spellcheck="true"></textarea>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript;version=1.8">
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(start);
+
+function start() {
+ var textarea = document.getElementById("editor");
+ textarea.focus();
+
+ SpecialPowers.Cu.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm")
+ .onSpellCheck(textarea, function () {
+ var isc = SpecialPowers.wrap(textarea).editor.getInlineSpellChecker(false);
+ ok(isc, "Inline spell checker should exist after focus and spell check");
+ var sc = isc.spellChecker;
+ isnot(sc.GetCurrentDictionary(), lang,
+ "Current dictionary should not be set yet.");
+
+ // First, set the lang attribute on the textarea, call Update, and make
+ // sure the spell checker's language was updated appropriately.
+ var lang = "en-US";
+ textarea.setAttribute("lang", lang);
+ sc.UpdateCurrentDictionary(function () {
+ is(sc.GetCurrentDictionary(), lang,
+ "UpdateCurrentDictionary should set the current dictionary.");
+
+ // Second, make some Update calls, but then do a Set. The Set should
+ // effectively cancel the Updates, but the Updates' callbacks should be
+ // called nonetheless.
+ var numCalls = 3;
+ for (var i = 0; i < numCalls; i++) {
+ sc.UpdateCurrentDictionary(function () {
+ is(sc.GetCurrentDictionary(), "",
+ "No dictionary should be active after Update.");
+ if (--numCalls == 0) {
+ // This will clear the content preferences and reset "spellchecker.dictionary".
+ sc.SetCurrentDictionary("");
+ SimpleTest.finish();
+ }
+ });
+ }
+ try {
+ sc.SetCurrentDictionary("testing-XX");
+ }
+ catch (err) {
+ // Set throws NS_ERROR_NOT_AVAILABLE because "testing-XX" isn't really
+ // an available dictionary.
+ }
+ is(sc.GetCurrentDictionary(), "",
+ "No dictionary should be active after Set.");
+ });
+ });
+}
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/editor/composer/test/test_bug1200533.html b/editor/composer/test/test_bug1200533.html
new file mode 100644
index 000000000..310b8d7cf
--- /dev/null
+++ b/editor/composer/test/test_bug1200533.html
@@ -0,0 +1,139 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1200533
+-->
+<head>
+ <title>Test for Bug 1200533</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1200533">Mozilla Bug 1200533</a>
+<p id="display"></p>
+<iframe id="content"></iframe>
+
+</div>
+<pre id="test">
+<script class="testbody" ttype="application/javascript">
+
+/** Test for Bug 1200533 **/
+/** Visit the elements defined above and check the dictionary we got **/
+SimpleTest.waitForExplicitFinish();
+var content = document.getElementById('content');
+
+var tests = [
+ // text area, value of spellchecker.dictionary, result.
+ // Result: Document language.
+ [ "none", "", "en-US" ],
+ // Result: Element language.
+ [ "en-GB", "", "en-GB" ],
+ [ "en-gb", "", "en-GB" ],
+ // Result: Random en-*.
+ [ "en-ZA-not-avail", "", "*" ],
+ [ "en-generic", "", "*" ],
+ // Result: Locale.
+ [ "ko-not-avail", "", "en-US" ],
+
+ // Result: Preference value in all cases.
+ [ "en-ZA-not-avail", "en-AU", "en-AU" ],
+ [ "en-generic", "en-AU", "en-AU" ],
+ [ "ko-not-avail", "en-AU", "en-AU" ],
+
+ // Result: Random en-*.
+ [ "en-ZA-not-avail", "de-DE", "*" ],
+ [ "en-generic", "de-DE", "*" ],
+ // Result: Preference value.
+ [ "ko-not-avail", "de-DE", "de-DE" ],
+ ];
+
+var loadCount = 0;
+var script;
+
+var loadListener = function(evt) {
+ if (loadCount == 0) {
+ script = SpecialPowers.loadChromeScript(function() {
+ var dir = Components.classes["@mozilla.org/file/directory_service;1"]
+ .getService(Components.interfaces.nsIProperties)
+ .get("CurWorkD", Components.interfaces.nsIFile);
+ dir.append("tests");
+ dir.append("editor");
+ dir.append("composer");
+ dir.append("test");
+
+ var hunspell = Components.classes["@mozilla.org/spellchecker/engine;1"]
+ .getService(Components.interfaces.mozISpellCheckingEngine);
+
+ // Install en-GB, en-AU and de-DE dictionaries.
+ var en_GB = dir.clone();
+ var en_AU = dir.clone();
+ var de_DE = dir.clone();
+ en_GB.append("en-GB");
+ en_AU.append("en-AU");
+ de_DE.append("de-DE");
+ hunspell.addDirectory(en_GB);
+ hunspell.addDirectory(en_AU);
+ hunspell.addDirectory(de_DE);
+
+ addMessageListener("check-existence",
+ () => [en_GB.exists(), en_AU.exists(),
+ de_DE.exists()]);
+ addMessageListener("destroy", () => {
+ hunspell.removeDirectory(en_GB);
+ hunspell.removeDirectory(en_AU);
+ hunspell.removeDirectory(de_DE);
+ });
+ });
+ var existenceChecks = script.sendSyncMessage("check-existence")[0][0];
+ is(existenceChecks[0], true, "true expected (en-GB directory should exist)");
+ is(existenceChecks[1], true, "true expected (en-AU directory should exist)");
+ is(existenceChecks[2], true, "true expected (de-DE directory should exist)");
+ }
+
+ SpecialPowers.pushPrefEnv({set: [["spellchecker.dictionary", tests[loadCount][1]]]},
+ function() { continueTest(evt) });
+}
+
+function continueTest(evt) {
+ var doc = evt.target.contentDocument;
+ var elem = doc.getElementById(tests[loadCount][0]);
+ var editor = SpecialPowers.wrap(elem).QueryInterface(SpecialPowers.Ci.nsIDOMNSEditableElement).editor;
+ editor.setSpellcheckUserOverride(true);
+ var inlineSpellChecker = editor.getInlineSpellChecker(true);
+
+ SpecialPowers.Cu.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm")
+ .onSpellCheck(elem, function () {
+ var spellchecker = inlineSpellChecker.spellChecker;
+ try {
+ var dict = spellchecker.GetCurrentDictionary();
+ } catch(e) {}
+
+ if (tests[loadCount][2] != "*") {
+ is (dict, tests[loadCount][2], "expected " + tests[loadCount][2]);
+ } else {
+ var gotEn = (dict == "en-GB" || dict == "en-AU" || dict == "en-US");
+ is (gotEn, true, "expected en-AU or en-GB or en-US");
+ }
+
+ loadCount++;
+ if (loadCount < tests.length) {
+ // Load the iframe again.
+ content.src = 'http://mochi.test:8888/tests/editor/composer/test/bug1200533_subframe.html?firstload=false';
+ } else {
+ // Remove the fake dictionaries again, since it's otherwise picked up by later tests.
+ script.sendSyncMessage("destroy");
+
+ SimpleTest.finish();
+ }
+ });
+
+}
+
+content.addEventListener('load', loadListener, false);
+
+content.src = 'http://mochi.test:8888/tests/editor/composer/test/bug1200533_subframe.html?firstload=true';
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/composer/test/test_bug1204147.html b/editor/composer/test/test_bug1204147.html
new file mode 100644
index 000000000..d7dd90474
--- /dev/null
+++ b/editor/composer/test/test_bug1204147.html
@@ -0,0 +1,112 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1204147
+-->
+<head>
+ <title>Test for Bug 1204147</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1204147">Mozilla Bug 1204147</a>
+<p id="display"></p>
+<iframe id="content"></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 1204147 **/
+SimpleTest.waitForExplicitFinish();
+var content = document.getElementById('content');
+// Load a subframe containing an editor with using "en-GB". At first
+// load, it will set the dictionary to "en-GB". The bug was that a content preference
+// was also created. At second load, we check the dictionary for another element,
+// one that should use "en-US". With the bug corrected, we get "en-US", before
+// we got "en-GB" from the content preference.
+
+var firstLoad = true;
+var script;
+
+var loadListener = function(evt) {
+ if (firstLoad) {
+ script = SpecialPowers.loadChromeScript(function() {
+ var dir = Components.classes["@mozilla.org/file/directory_service;1"]
+ .getService(Components.interfaces.nsIProperties)
+ .get("CurWorkD", Components.interfaces.nsIFile);
+ dir.append("tests");
+ dir.append("editor");
+ dir.append("composer");
+ dir.append("test");
+
+ var hunspell = Components.classes["@mozilla.org/spellchecker/engine;1"]
+ .getService(Components.interfaces.mozISpellCheckingEngine);
+
+ // Install en-GB dictionary.
+ en_GB = dir.clone();
+ en_GB.append("en-GB");
+ hunspell.addDirectory(en_GB);
+
+ addMessageListener("en_GB-exists", () => en_GB.exists());
+ addMessageListener("destroy", () => hunspell.removeDirectory(en_GB));
+ });
+ is(script.sendSyncMessage("en_GB-exists")[0][0], true,
+ "true expected (en-GB directory should exist)");
+ }
+
+ var doc = evt.target.contentDocument;
+ var elem;
+ if (firstLoad) {
+ elem = doc.getElementById('en-GB');
+ } else {
+ elem = doc.getElementById('en-US');
+ }
+
+ var editor = SpecialPowers.wrap(elem).QueryInterface(SpecialPowers.Ci.nsIDOMNSEditableElement).editor;
+ editor.setSpellcheckUserOverride(true);
+ var inlineSpellChecker = editor.getInlineSpellChecker(true);
+
+ SpecialPowers.Cu.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm")
+ .onSpellCheck(elem, function () {
+ var spellchecker = inlineSpellChecker.spellChecker;
+ try {
+ var currentDictonary = spellchecker.GetCurrentDictionary();
+ } catch(e) {}
+
+ if (firstLoad) {
+ firstLoad = false;
+
+ // First time around, the element's language should be used.
+ is (currentDictonary, "en-GB", "unexpected lang " + currentDictonary + " instead of en-GB");
+
+ // Note that on second load, we load a different page, which does NOT have the trouble-causing
+ // contenteditable in it. Sadly, loading the same page with the trouble-maker in it
+ // doesn't allow the retrieval of the spell check dictionary used for the element,
+ // because the trouble-maker causes the 'global' spell check dictionary to be set to "en-GB"
+ // (since it picks the first one from the list) before we have the chance to retrieve
+ // the dictionary for the element (which happens asynchonously after the spell check has completed).
+ content.src = 'http://mochi.test:8888/tests/editor/composer/test/bug1204147_subframe2.html?firstload=false';
+ } else {
+ // Second time around, the element should default to en-US.
+ // Without the fix, the first run sets the content preference to en-GB for the whole site.
+ is (currentDictonary, "en-US", "unexpected lang " + currentDictonary + " instead of en-US");
+ content.removeEventListener('load', loadListener, false);
+
+ // Remove the fake en-GB dictionary again, since it's otherwise picked up by later tests.
+ script.sendSyncMessage("destroy");
+
+ // Reset the preference, so the last value we set doesn't collide with the next test.
+ SimpleTest.finish();
+ }
+ });
+}
+
+content.addEventListener('load', loadListener, false);
+
+content.src = 'http://mochi.test:8888/tests/editor/composer/test/bug1204147_subframe.html?firstload=true';
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/composer/test/test_bug1205983.html b/editor/composer/test/test_bug1205983.html
new file mode 100644
index 000000000..139f6fc3c
--- /dev/null
+++ b/editor/composer/test/test_bug1205983.html
@@ -0,0 +1,137 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1205983
+-->
+<head>
+ <title>Test for Bug 1205983</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1205983">Mozilla Bug 1205983</a>
+<p id="display"></p>
+</div>
+
+<div contenteditable id="de-DE" lang="de-DE" onfocus="deFocus()">German heute ist ein guter Tag</div>
+<textarea id="en-US" lang="en-US" onfocus="enFocus()">Nogoodword today is a nice day</textarea>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+function getMisspelledWords(editor) {
+ return editor.selectionController.getSelection(SpecialPowers.Ci.nsISelectionController.SELECTION_SPELLCHECK).toString();
+}
+
+var elem_de;
+var editor_de;
+var selcon_de;
+var script;
+
+var onSpellCheck =
+ SpecialPowers.Cu.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm")
+ .onSpellCheck;
+
+/** Test for Bug 1205983 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ script = SpecialPowers.loadChromeScript(function() {
+ var dir = Components.classes["@mozilla.org/file/directory_service;1"]
+ .getService(Components.interfaces.nsIProperties)
+ .get("CurWorkD", Components.interfaces.nsIFile);
+ dir.append("tests");
+ dir.append("editor");
+ dir.append("composer");
+ dir.append("test");
+
+ var hunspell = Components.classes["@mozilla.org/spellchecker/engine;1"]
+ .getService(Components.interfaces.mozISpellCheckingEngine);
+
+ // Install de-DE dictionary.
+ var de_DE = dir.clone();
+ de_DE.append("de-DE");
+ hunspell.addDirectory(de_DE);
+
+ addMessageListener("de_DE-exists", () => de_DE.exists());
+ addMessageListener("destroy", () => hunspell.removeDirectory(de_DE));
+ });
+ is(script.sendSyncMessage("de_DE-exists")[0][0], true,
+ "true expected (de_DE directory should exist)");
+
+ document.getElementById('de-DE').focus();
+});
+
+function deFocus() {
+ elem_de = document.getElementById('de-DE');
+
+ onSpellCheck(elem_de, function () {
+ var Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIEditingSession);
+ editor_de = editingSession.getEditorForWindow(window);
+ selcon_de = editor_de.selectionController;
+ var sel = selcon_de.getSelection(selcon_de.SELECTION_SPELLCHECK);
+
+ // Check that we spelled in German, so there is only one misspelled word.
+ is(sel.toString(), "German", "one misspelled word expected: German");
+
+ // Now focus the textarea, which requires English spelling.
+ document.getElementById('en-US').focus();
+ });
+}
+
+function enFocus() {
+ var elem_en = document.getElementById('en-US');
+ var editor_en =
+ SpecialPowers.wrap(elem_en)
+ .QueryInterface(SpecialPowers.Ci.nsIDOMNSEditableElement)
+ .editor;
+ editor_en.setSpellcheckUserOverride(true);
+ var inlineSpellChecker = editor_en.getInlineSpellChecker(true);
+
+ onSpellCheck(elem_en, function () {
+ var spellchecker = inlineSpellChecker.spellChecker;
+ try {
+ currentDictonary = spellchecker.GetCurrentDictionary();
+ } catch(e) {}
+
+ // Check that the English dictionary is loaded and that the spell check has worked.
+ is(currentDictonary, "en-US", "expected en-US");
+ is(getMisspelledWords(editor_en), "Nogoodword", "one misspelled word expected: Nogoodword");
+
+ // So far all was boring. The important thing is whether the spell check result
+ // in the de-DE editor is still the same. After losing focus, no spell check
+ // updates should take place there.
+ var sel = selcon_de.getSelection(selcon_de.SELECTION_SPELLCHECK);
+ is(sel.toString(), "German", "one misspelled word expected: German");
+
+ // Remove the fake de_DE dictionary again.
+ script.sendSyncMessage("destroy");
+
+ // Focus again, so the spelling gets updated, but before we need to kill the focus handler.
+ elem_de.onfocus = null;
+ elem_de.blur();
+ elem_de.focus();
+
+ // After removal, the de_DE editor should refresh the spelling with en-US.
+ onSpellCheck(elem_de, function () {
+ var sel = selcon_de.getSelection(selcon_de.SELECTION_SPELLCHECK);
+ is(sel.toString(), "heute" + "ist" + "ein" + "guter",
+ "some misspelled words expected: heute ist ein guter");
+
+ // If we don't reset this, we cause massive leaks.
+ selcon_de = null;
+ editor_de = null;
+
+ SimpleTest.finish();
+ });
+ });
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/composer/test/test_bug1209414.html b/editor/composer/test/test_bug1209414.html
new file mode 100644
index 000000000..bce1bb313
--- /dev/null
+++ b/editor/composer/test/test_bug1209414.html
@@ -0,0 +1,150 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1209414
+-->
+<head>
+ <title>Test for Bug 1209414</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1209414">Mozilla Bug 1209414</a>
+<p id="display"></p>
+</div>
+
+<textarea id="de-DE" lang="de-DE">heute ist ein guter Tag - today is a good day</textarea>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+const Ci = SpecialPowers.Ci;
+
+function getMisspelledWords(editor) {
+ return editor.selectionController.getSelection(Ci.nsISelectionController.SELECTION_SPELLCHECK).toString();
+}
+
+var elem_de;
+var editor_de;
+var script;
+
+/** Test for Bug 1209414 **/
+/*
+ * All we want to do in this test is change the spelling using a right-click and selection from the menu.
+ * This is necessary since all the other tests use SetCurrentDictionary() which doesn't reflect
+ * user behaviour.
+ */
+
+var onSpellCheck =
+ SpecialPowers.Cu.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm")
+ .onSpellCheck;
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ script = SpecialPowers.loadChromeScript(function() {
+ const Ci = Components.interfaces;
+ var chromeWin = browserElement.ownerDocument.defaultView
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow)
+ .QueryInterface(Ci.nsIDOMChromeWindow);
+ var contextMenu = chromeWin.document.getElementById("contentAreaContextMenu");
+ contextMenu.addEventListener("popupshown",
+ () => sendAsyncMessage("popupshown"), false);
+
+ var dir = Components.classes["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("CurWorkD", Ci.nsIFile);
+ dir.append("tests");
+ dir.append("editor");
+ dir.append("composer");
+ dir.append("test");
+
+ var hunspell = Components.classes["@mozilla.org/spellchecker/engine;1"]
+ .getService(Ci.mozISpellCheckingEngine);
+
+ // Install de-DE dictionary.
+ de_DE = dir.clone();
+ de_DE.append("de-DE");
+ hunspell.addDirectory(de_DE);
+
+ addMessageListener("hidepopup", function() {
+ var state = contextMenu.state;
+
+ // Select Language from the menu. Take a look at
+ // toolkit/modules/InlineSpellChecker.jsm to see how the menu works.
+
+ contextMenu.ownerDocument.getElementById("spell-check-dictionary-en-US")
+ .doCommand();
+ contextMenu.hidePopup();
+
+ return state;
+ });
+ addMessageListener("destroy", () => hunspell.removeDirectory(de_DE));
+ addMessageListener("contextMenu-not-null", () => contextMenu != null);
+ addMessageListener("de_DE-exists", () => de_DE.exists());
+ });
+ is(script.sendSyncMessage("contextMenu-not-null")[0][0], true,
+ "Got context menu XUL");
+ is(script.sendSyncMessage("de_DE-exists")[0][0], true,
+ "true expected (de_DE directory should exist)");
+ script.addMessageListener("popupshown", handlePopup);
+
+ elem_de = document.getElementById('de-DE');
+ editor_de = SpecialPowers.wrap(elem_de)
+ .QueryInterface(Ci.nsIDOMNSEditableElement).editor;
+ editor_de.setSpellcheckUserOverride(true);
+
+ onSpellCheck(elem_de, function () {
+ var inlineSpellChecker = editor_de.getInlineSpellChecker(true);
+ var spellchecker = inlineSpellChecker.spellChecker;
+ try {
+ var currentDictonary = spellchecker.GetCurrentDictionary();
+ } catch(e) {}
+
+ // Check that the German dictionary is loaded and that the spell check has worked.
+ is(currentDictonary, "de-DE", "expected de-DE");
+ is(getMisspelledWords(editor_de), "today" + "is" + "a" + "good" + "day", "some misspelled words expected: today is a good day");
+
+ // Focus again, just to be sure that the context-click won't trigger another spell check.
+ elem_de.focus();
+
+ // Make sure all spell checking action is done before right-click to select the en-US dictionary.
+ onSpellCheck(elem_de, function () {
+ synthesizeMouse(elem_de, 2, 2, { type : "contextmenu", button : 2 }, window);
+ });
+ });
+});
+
+function handlePopup() {
+ var state = script.sendSyncMessage("hidepopup")[0][0];
+ is(state, "open", "checking if popup is open");
+
+ onSpellCheck(elem_de, function () {
+ var inlineSpellChecker = editor_de.getInlineSpellChecker(true);
+ var spellchecker = inlineSpellChecker.spellChecker;
+ try {
+ currentDictonary = spellchecker.GetCurrentDictionary();
+ } catch(e) {}
+
+ // Check that the English dictionary is loaded and that the spell check has worked.
+ is(currentDictonary, "en-US", "expected en-US");
+ is(getMisspelledWords(editor_de), "heute" + "ist" + "ein" + "guter", "some misspelled words expected: heute ist ein guter");
+
+ // Remove the fake de_DE dictionary again.
+ script.sendSyncMessage("destroy");
+
+ // This will clear the content preferences and reset "spellchecker.dictionary".
+ spellchecker.SetCurrentDictionary("");
+ SimpleTest.finish();
+ });
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/composer/test/test_bug1219928.html b/editor/composer/test/test_bug1219928.html
new file mode 100644
index 000000000..2f646f00f
--- /dev/null
+++ b/editor/composer/test/test_bug1219928.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1219928
+-->
+<head>
+ <title>Test for Bug 1219928</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1219928">Mozilla Bug 1219928</a>
+<p id="display"></p>
+
+<div contenteditable id="en-US" lang="en-US">
+<p>And here a missspelled word</p>
+<style>
+<!-- and here another onnee in a style comment -->
+</style>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 1219928 **/
+/* Very simple test to check that <style> blocks are skipped in the spell check */
+
+var spellchecker;
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ SpecialPowers.Cu.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm",
+ window);
+
+ var elem = document.getElementById('en-US');
+ elem.focus();
+
+ onSpellCheck(elem, function () {
+ var Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIEditingSession);
+ var editor = editingSession.getEditorForWindow(window);
+ var selcon = editor.selectionController;
+ var sel = selcon.getSelection(selcon.SELECTION_SPELLCHECK);
+
+ is(sel.toString(), "missspelled", "one misspelled word expected: missspelled");
+
+ spellchecker = SpecialPowers.Cc['@mozilla.org/editor/editorspellchecker;1']
+ .createInstance(Ci.nsIEditorSpellCheck);
+ var filterContractId = "@mozilla.org/editor/txtsrvfilter;1";
+ spellchecker.setFilter(SpecialPowers.Cc[filterContractId]
+ .createInstance(Ci.nsITextServicesFilter));
+ spellchecker.InitSpellChecker(editor, false, spellCheckStarted);
+ });
+});
+
+function spellCheckStarted() {
+ var misspelledWord = spellchecker.GetNextMisspelledWord();
+ is(misspelledWord, "missspelled", "first misspelled word expected: missspelled");
+
+ // Without the fix, the next misspelled word was 'onnee', so we check that we don't get it.
+ misspelledWord = spellchecker.GetNextMisspelledWord();
+ isnot(misspelledWord, "onnee", "second misspelled word should not be: onnee");
+
+ spellchecker = "";
+
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/composer/test/test_bug1266815.html b/editor/composer/test/test_bug1266815.html
new file mode 100644
index 000000000..a2ab0b048
--- /dev/null
+++ b/editor/composer/test/test_bug1266815.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<script type="text/javascript">
+const Cc = SpecialPowers.Cc;
+const Ci = SpecialPowers.Ci;
+const Cu = SpecialPowers.Cu;
+
+const {XPCOMUtils} = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+
+const HELPERAPP_DIALOG_CID =
+ SpecialPowers.wrap(SpecialPowers.Components)
+ .ID(Cc["@mozilla.org/helperapplauncherdialog;1"].number);
+const HELPERAPP_DIALOG_CONTRACT_ID = "@mozilla.org/helperapplauncherdialog;1";
+const MOCK_HELPERAPP_DIALOG_CID =
+ SpecialPowers.wrap(SpecialPowers.Components)
+ .ID("{391832c8-5232-4676-b838-cc8ad373f3d8}");
+
+var registrar = SpecialPowers.wrap(Components).manager
+ .QueryInterface(Ci.nsIComponentRegistrar);
+
+var helperAppDlgPromise = new Promise(function(resolve) {
+ var mockHelperAppService;
+
+ function HelperAppLauncherDialog() {
+ }
+
+ HelperAppLauncherDialog.prototype = {
+ show: function(aLauncher, aWindowContext, aReason) {
+ ok(true, "Whether showing Dialog");
+ resolve();
+ registrar.unregisterFactory(MOCK_HELPERAPP_DIALOG_CID,
+ mockHelperAppService);
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIHelperAppLauncherDialog])
+ };
+
+ mockHelperAppService = XPCOMUtils._getFactory(HelperAppLauncherDialog);
+ registrar.registerFactory(MOCK_HELPERAPP_DIALOG_CID, "",
+ HELPERAPP_DIALOG_CONTRACT_ID,
+ mockHelperAppService);
+});
+
+add_task(function*() {
+ let promise = new Promise(function(resolve) {
+ let iframe = document.createElement("iframe");
+ iframe.onload = function() {
+ is(iframe.contentDocument.getElementById("edit").innerText, "abc",
+ "load iframe source");
+ resolve();
+ };
+ iframe.id = "testframe";
+ iframe.src = "data:text/html,<div id=edit contenteditable=true>abc</div>";
+ document.body.appendChild(iframe);
+ });
+
+ yield promise;
+
+ let iframe = document.getElementById("testframe");
+ let docShell = SpecialPowers.wrap(iframe.contentWindow)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell);
+
+ ok(docShell.hasEditingSession, "Should have editing session");
+
+ document.getElementById("testframe").src =
+ "data:application/octet-stream,TESTCONTENT";
+
+ yield helperAppDlgPromise;
+
+ ok(docShell.hasEditingSession, "Should have editing session");
+});
+</script>
+</body>
+</html>
diff --git a/editor/composer/test/test_bug338427.html b/editor/composer/test/test_bug338427.html
new file mode 100644
index 000000000..f16194b3d
--- /dev/null
+++ b/editor/composer/test/test_bug338427.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=338427
+-->
+<head>
+ <title>Test for Bug 338427</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=338427">Mozilla Bug 338427</a>
+<p id="display"></p>
+<div id="content">
+<textarea id="editor" lang="testing-XX" spellcheck="true"></textarea>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 338427 **/
+function init() {
+ var onSpellCheck =
+ SpecialPowers.Cu.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm")
+ .onSpellCheck;
+ var textarea = document.getElementById("editor");
+ var editor = SpecialPowers.wrap(textarea).editor;
+ var spellchecker = editor.getInlineSpellChecker(true);
+ spellchecker.enableRealTimeSpell = true;
+ textarea.focus();
+
+ onSpellCheck(textarea, function () {
+ var list = {}, count = {};
+ spellchecker.spellChecker.GetDictionaryList(list, count);
+ ok(count.value > 0, "At least one dictionary should be present");
+
+ var lang = list.value[0];
+ spellchecker.spellChecker.SetCurrentDictionary(lang);
+
+ onSpellCheck(textarea, function () {
+ try {
+ var dictionary =
+ spellchecker.spellChecker.GetCurrentDictionary();
+ } catch(e) {}
+ is(dictionary, lang, "Unexpected spell check dictionary");
+
+ // This will clear the content preferences and reset "spellchecker.dictionary".
+ spellchecker.spellChecker.SetCurrentDictionary("");
+ SimpleTest.finish();
+ });
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(init);
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/editor/composer/test/test_bug348497.html b/editor/composer/test/test_bug348497.html
new file mode 100644
index 000000000..9483b727a
--- /dev/null
+++ b/editor/composer/test/test_bug348497.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=348497
+-->
+<head>
+ <title>Test for Bug 348497</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=348497">Mozilla Bug 348497</a>
+<p id="display"></p>
+<div id="content">
+ This page should not crash Mozilla<br>
+ <iframe id="testIframe"></iframe>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 348497 **/
+function doe() {
+ document.getElementById('testIframe').style.display = 'block';
+ document.getElementById('testIframe').contentDocument.designMode = 'on';
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(doe);
+addLoadEvent(function() { ok(true, "enabling designmode on an iframe onload does not crash Mozilla")});
+addLoadEvent(SimpleTest.finish);
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/editor/composer/test/test_bug384147.html b/editor/composer/test/test_bug384147.html
new file mode 100644
index 000000000..35f0e533e
--- /dev/null
+++ b/editor/composer/test/test_bug384147.html
@@ -0,0 +1,204 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=384147
+-->
+<head>
+ <title>Test for Bug 384147</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=384147">Mozilla Bug 384147</a>
+<p id="display"></p>
+<div id="content" style="display: block">
+<div contentEditable id="editor"></div>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript;version=1.7">
+
+/** Test for Bug 384147 **/
+
+SimpleTest.waitForExplicitFinish();
+
+var editor = document.getElementById("editor");
+
+editor.innerHTML = "<ol><li>Item 1</li><li>Item 2</li><ol><li>Item 3</li></ol></ol><ul><li>Item 4</li><li>Item 5</li></ul>";
+editor.focus();
+
+// If executed directly, a race condition exists that will cause execCommand
+// to fail occasionally (but often). Defer test execution to page load.
+addLoadEvent(function() {
+
+ var sel = window.getSelection();
+
+ // Test the effect that the tab key has on list items. Each test is
+ // documented with the initial state of the list on the left, and the
+ // expected state of the list on the right. {\t} indicates the list item
+ // that will be indented. {\st} indicates that a shift-tab will be simulated
+ // on that list item, outdenting it.
+ //
+ // Note: any test failing will likely result in all following tests failing
+ // as well, since each test depends on the document being in a given state.
+ // Unfortunately, due to the problems getting document focus and key events
+ // to fire consistently, it's difficult to reset state between tests.
+ // If there are test failures here, only debug the first test failure.
+
+ // *** test 1 ***
+ // 1. Item 1 1. Item 1
+ // 2. {\t}Item 2 1. Item 2
+ // 1. Item 3 2. Item 3
+ // * Item 4 * Item 4
+ // * Item 5 * Item 5
+ sel.removeAllRanges();
+ sel.selectAllChildren(editor.getElementsByTagName("li")[1]);
+ document.execCommand("indent", false, null);
+ ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li><li>Item 3</li></ol></ol><ul><li>Item 4</li><li>Item 5</li></ul>",
+ "html output doesn't match expected value in test 1");
+
+ // *** test 2 ***
+ // 1. Item 1 1. Item 1
+ // 1. Item 2 1. Item 2
+ // 2. {\t}Item 3 1. Item 3
+ // * Item 4 * Item 4
+ // * Item 5 * Item 5
+ sel.removeAllRanges();
+ sel.selectAllChildren(editor.getElementsByTagName("li")[2]);
+ document.execCommand("indent", false, null);
+ ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li><ol><li>Item 3</li></ol></ol></ol><ul><li>Item 4</li><li>Item 5</li></ul>",
+ "html output doesn't match expected value in test 2");
+
+ // *** test 3 ***
+ // 1. Item 1 1. Item 1
+ // 1. Item 2 1. Item 2
+ // 1. {\st}Item 3 2. Item 3
+ // * Item 4 * Item 4
+ // * Item 5 * Item 5
+ document.execCommand("outdent", false, null);
+ ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li><li>Item 3</li></ol></ol><ul><li>Item 4</li><li>Item 5</li></ul>",
+ "html output doesn't match expected value in test 3");
+
+ // *** test 4 ***
+ // 1. Item 1 1. Item 1
+ // 1. Item 2 1. Item 2
+ // 2. {\st}Item 3 2. Item 3
+ // * Item 4 * Item 4
+ // * Item 5 * Item 5
+ document.execCommand("outdent", false, null);
+ ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li></ol><li>Item 3</li></ol><ul><li>Item 4</li><li>Item 5</li></ul>",
+ "html output doesn't match expected value in test 4");
+
+ // *** test 5 ***
+ // 1. Item 1 1. Item 1
+ // 1. {\st}Item 2 2. Item 2
+ // 2. Item 3 3. Item 3
+ // * Item 4 * Item 4
+ // * Item 5 * Item 5
+ sel.removeAllRanges();
+ sel.selectAllChildren(editor.getElementsByTagName("li")[1]);
+ document.execCommand("outdent", false, null);
+ ok(editor.innerHTML == "<ol><li>Item 1</li><li>Item 2</li><li>Item 3</li></ol><ul><li>Item 4</li><li>Item 5</li></ul>",
+ "html output doesn't match expected value in test 5");
+
+ // *** test 6 ***
+ // 1. Item 1 1. Item 1
+ // 2. {\t}Item 2 1. Item 2
+ // 3. Item 3 2. Item 3
+ // * Item 4 * Item 4
+ // * Item 5 * Item 5
+ document.execCommand("indent", false, null);
+ ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li></ol><li>Item 3</li></ol><ul><li>Item 4</li><li>Item 5</li></ul>",
+ "html output doesn't match expected value in test 6");
+
+ // *** test 7 ***
+ // 1. Item 1 1. Item 1
+ // 1. Item 2 1. Item 2
+ // 2. {\t}Item 3 2. Item 3
+ // * Item 4 * Item 4
+ // * Item 5 * Item 5
+ sel.removeAllRanges();
+ sel.selectAllChildren(editor.getElementsByTagName("li")[2]);
+ document.execCommand("indent", false, null);
+ ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li><li>Item 3</li></ol></ol><ul><li>Item 4</li><li>Item 5</li></ul>",
+ "html output doesn't match expected value in test 7");
+
+ // That covers the basics of merging lists on indent and outdent.
+ // We also want to check that ul / ol lists won't be merged together,
+ // since they're different types of lists.
+ // *** test 8 ***
+ // 1. Item 1 1. Item 1
+ // 1. Item 2 1. Item 2
+ // 2. Item 3 2. Item 3
+ // * {\t}Item 4 * Item 4
+ // * Item 5 * Item 5
+ sel.removeAllRanges();
+ sel.selectAllChildren(editor.getElementsByTagName("li")[3]);
+ document.execCommand("indent", false, null);
+ ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li><li>Item 3</li></ol></ol><ul><ul><li>Item 4</li></ul><li>Item 5</li></ul>",
+ "html output doesn't match expected value in test 8");
+
+ // Better test merging with <ul> rather than <ol> too.
+ // *** test 9 ***
+ // 1. Item 1 1. Item 1
+ // 1. Item 2 1. Item 2
+ // 2. Item 3 2. Item 3
+ // * Item 4 * Item 4
+ // * {\t}Item 5 * Item 5
+ sel.removeAllRanges();
+ sel.selectAllChildren(editor.getElementsByTagName("li")[4]);
+ document.execCommand("indent", false, null);
+ ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li><li>Item 3</li></ol></ol><ul><ul><li>Item 4</li><li>Item 5</li></ul></ul>",
+ "html output doesn't match expected value in test 9");
+
+ // Same test as test 8, but with outdent rather than indent.
+ // *** test 10 ***
+ // 1. Item 1 1. Item 1
+ // 1. Item 2 1. Item 2
+ // 2. Item 3 2. Item 3
+ // * {\st}Item 4 * Item 4
+ // * Item 5 * Item 5
+ sel.removeAllRanges();
+ sel.selectAllChildren(editor.getElementsByTagName("li")[3]);
+ document.execCommand("outdent", false, null);
+ ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li><li>Item 3</li></ol></ol><ul><li>Item 4</li><ul><li>Item 5</li></ul></ul>",
+ "html output doesn't match expected value in test 10");
+
+ // Test indenting multiple items at once. Hold down "shift" and select
+ // upwards to get all the <ol> items and the first <ul> item.
+ // *** test 11 ***
+ // 1. Item 1 1. Item 1
+ // 1. {\t}Item 2 1. Item 2
+ // 2. {\t}Item 3 2. Item 3
+ // * {\t}Item 4 * Item 4
+ // * Item 5 * Item 5
+ sel.removeAllRanges();
+ var range = document.createRange();
+ range.setStart(editor.getElementsByTagName("li")[1], 0);
+ range.setEnd(editor.getElementsByTagName("li")[3], editor.getElementsByTagName("li")[3].childNodes.length);
+ sel.addRange(range);
+ document.execCommand("indent", false, null);
+ ok(editor.innerHTML == "<ol><li>Item 1</li><ol><ol><li>Item 2</li><li>Item 3</li></ol></ol></ol><ul><ul><li>Item 4</li><li>Item 5</li></ul></ul>",
+ "html output doesn't match expected value in test 11");
+
+ // Test outdenting multiple items at once. Selection is already ready...
+ // *** test 12 ***
+ // 1. Item 1 1. Item 1
+ // 1. {\st}Item 2 1. Item 2
+ // 2. {\st}Item 3 2. Item 3
+ // * {\st}Item 4 * Item 4
+ // * Item 5 * Item 5
+ document.execCommand("outdent", false, null);
+ ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li><li>Item 3</li></ol></ol><ul><li>Item 4</li><ul><li>Item 5</li></ul></ul>",
+ "html output doesn't match expected value in test 12");
+
+ SimpleTest.finish();
+});
+
+
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/editor/composer/test/test_bug389350.html b/editor/composer/test/test_bug389350.html
new file mode 100644
index 000000000..bf1d514ad
--- /dev/null
+++ b/editor/composer/test/test_bug389350.html
@@ -0,0 +1,33 @@
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=389350
+-->
+<head>
+<title>Test for Bug 389350</title>
+<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+<script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+
+<script type="text/javascript">
+
+function runTest() {
+ var e = document.getElementById("edit");
+ e.contentDocument.designMode='on';
+ e.style.display='block';
+ e.focus();
+ sendString('abc');
+ var expected = "<head></head><body>abc</body>";
+ var result = e.contentDocument.documentElement.innerHTML;
+ is(result, expected, "iframe with designmode on had incorrect content");
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+</script>
+
+</head>
+<body id="body">
+<iframe id="edit" width="200" height="100" style="display: none;" src="">
+</body>
+</html>
diff --git a/editor/composer/test/test_bug434998.xul b/editor/composer/test/test_bug434998.xul
new file mode 100644
index 000000000..4a384ac1c
--- /dev/null
+++ b/editor/composer/test/test_bug434998.xul
@@ -0,0 +1,109 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin"
+ type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=434998
+-->
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Mozilla Bug 434998" onload="runTest();">
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=434998"
+ target="_blank">Mozilla Bug 434998</a>
+ <p/>
+ <editor xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="editor"
+ type="content-primary"
+ editortype="html"
+ style="width: 400px; height: 100px; border: thin solid black"/>
+ <p/>
+ <pre id="test">
+ </pre>
+ </body>
+ <script class="testbody" type="application/javascript">
+ <![CDATA[
+
+ SimpleTest.waitForExplicitFinish();
+
+ function EditorContentListener(aEditor)
+ {
+ this.init(aEditor);
+ }
+
+ EditorContentListener.prototype = {
+ init : function(aEditor)
+ {
+ this.mEditor = aEditor;
+ },
+
+ QueryInterface : function(aIID)
+ {
+ if (aIID.equals(Components.interfaces.nsIWebProgressListener) ||
+ aIID.equals(Components.interfaces.nsISupportsWeakReference) ||
+ aIID.equals(Components.interfaces.nsISupports))
+ return this;
+ throw Components.results.NS_NOINTERFACE;
+ },
+
+ onStateChange : function(aWebProgress, aRequest, aStateFlags, aStatus)
+ {
+ if (aStateFlags & Components.interfaces.nsIWebProgressListener.STATE_STOP)
+ {
+ var editor = this.mEditor.getEditor(this.mEditor.contentWindow);
+ if (editor) {
+ // Should not throw
+ var threw = false;
+ try {
+ this.mEditor.contentDocument.execCommand("bold", false, null);
+ } catch (e) {
+ threw = true;
+ }
+ ok(!threw, "The execCommand API should work on <xul:editor>");
+ progress.removeProgressListener(progressListener, Components.interfaces.nsIWebProgress.NOTIFY_ALL);
+ SimpleTest.finish();
+ }
+ }
+ },
+
+
+ onProgressChange : function(aWebProgress, aRequest,
+ aCurSelfProgress, aMaxSelfProgress,
+ aCurTotalProgress, aMaxTotalProgress)
+ {
+ },
+
+ onLocationChange : function(aWebProgress, aRequest, aLocation, aFlags)
+ {
+ },
+
+ onStatusChange : function(aWebProgress, aRequest, aStatus, aMessage)
+ {
+ },
+
+ onSecurityChange : function(aWebProgress, aRequest, aState)
+ {
+ },
+
+ mEditor: null
+ };
+
+ var progress, progressListener;
+
+ function runTest() {
+ var newEditorElement = document.getElementById("editor");
+ newEditorElement.makeEditable("html", true);
+ var docShell = newEditorElement.boxObject.docShell;
+ progress = docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIWebProgress);
+ progressListener = new EditorContentListener(newEditorElement);
+ progress.addProgressListener(progressListener, Components.interfaces.nsIWebProgress.NOTIFY_ALL);
+ newEditorElement.setAttribute("src", "data:text/html,");
+ }
+]]>
+</script>
+</window>
diff --git a/editor/composer/test/test_bug519928.html b/editor/composer/test/test_bug519928.html
new file mode 100644
index 000000000..3b1a9ba17
--- /dev/null
+++ b/editor/composer/test/test_bug519928.html
@@ -0,0 +1,123 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=519928
+-->
+<head>
+ <title>Test for Bug 519928</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=519928">Mozilla Bug 519928</a>
+<p id="display"></p>
+<div id="content">
+<iframe id="load-frame"></iframe>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var iframe = document.getElementById("load-frame");
+
+function enableJS() { allowJS(true, iframe); }
+function disableJS() { allowJS(false, iframe); }
+function allowJS(allow, frame) {
+ SpecialPowers.wrap(frame.contentWindow)
+ .QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
+ .getInterface(SpecialPowers.Ci.nsIWebNavigation)
+ .QueryInterface(SpecialPowers.Ci.nsIDocShell)
+ .allowJavascript = allow;
+}
+
+function expectJSAllowed(allowed, testCondition, callback) {
+ window.ICanRunMyJS = false;
+ var self_ = window;
+ testCondition();
+
+ var doc = iframe.contentDocument;
+ doc.body.innerHTML = "<iframe></iframe>";
+ var innerFrame = doc.querySelector("iframe");
+ innerFrame.addEventListener("load", function() {
+ innerFrame.removeEventListener("load", arguments.callee, false);
+
+ var msg = "The inner iframe should" + (allowed ? "" : " not") + " be able to run Javascript";
+ is(self_.ICanRunMyJS, allowed, msg);
+ callback();
+ }, false);
+ var iframeSrc = "data:text/html,<script>parent.parent.ICanRunMyJS = true;</scr" + "ipt>";
+ innerFrame.src = iframeSrc;
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var enterDesignMode = function() { document.designMode = "on"; };
+ var leaveDesignMode = function() { document.designMode = "off"; };
+ expectJSAllowed(false, disableJS, function() {
+ expectJSAllowed(true, enableJS, function() {
+ expectJSAllowed(true, enterDesignMode, function() {
+ expectJSAllowed(true, leaveDesignMode, function() {
+ expectJSAllowed(false, disableJS, function() {
+ expectJSAllowed(false, enterDesignMode, function() {
+ expectJSAllowed(false, leaveDesignMode, function() {
+ expectJSAllowed(true, enableJS, function() {
+ enterDesignMode = function() { iframe.contentDocument.designMode = "on"; };
+ leaveDesignMode = function() { iframe.contentDocument.designMode = "off"; };
+ expectJSAllowed(false, disableJS, function() {
+ expectJSAllowed(true, enableJS, function() {
+ expectJSAllowed(true, enterDesignMode, function() {
+ expectJSAllowed(true, leaveDesignMode, function() {
+ expectJSAllowed(false, disableJS, function() {
+ expectJSAllowed(false, enterDesignMode, function() {
+ expectJSAllowed(false, leaveDesignMode, function() {
+ expectJSAllowed(true, enableJS, function() {
+ testDocumentDisabledJS();
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+});
+
+function testDocumentDisabledJS() {
+ window.ICanRunMyJS = false;
+ var self_ = window;
+ // Ensure design modes are disabled
+ document.designMode = "off";
+ iframe.contentDocument.designMode = "off";
+
+ // Javascript enabled on the main iframe
+ enableJS();
+
+ var doc = iframe.contentDocument;
+ doc.body.innerHTML = "<iframe></iframe>";
+ var innerFrame = doc.querySelector("iframe");
+
+ // Javascript disabled on the innerFrame.
+ allowJS(false, innerFrame);
+
+ innerFrame.addEventListener("load", function() {
+ innerFrame.removeEventListener("load", arguments.callee, false);
+
+ var msg = "The inner iframe should not be able to run Javascript";
+ is(self_.ICanRunMyJS, false, msg);
+ SimpleTest.finish();
+ }, false);
+ var iframeSrc = "data:text/html,<script>parent.parent.ICanRunMyJS = true;</scr" + "ipt>";
+ innerFrame.src = iframeSrc;
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/composer/test/test_bug678842.html b/editor/composer/test/test_bug678842.html
new file mode 100644
index 000000000..226b25ee2
--- /dev/null
+++ b/editor/composer/test/test_bug678842.html
@@ -0,0 +1,105 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=678842
+-->
+<head>
+ <title>Test for Bug 678842</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=678842">Mozilla Bug 678842</a>
+<p id="display"></p>
+<iframe id="content"></iframe>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 678842 **/
+SimpleTest.waitForExplicitFinish();
+var content = document.getElementById('content');
+// load a subframe containing an editor with a defined unknown lang. At first
+// load, it will set dictionary to en-US. At second load, it will return current
+// dictionary. So, we can check, dictionary is correctly remembered between
+// loads.
+
+var firstLoad = true;
+var script;
+
+var loadListener = function(evt) {
+ if (firstLoad) {
+ script = SpecialPowers.loadChromeScript(function() {
+ var dir = Components.classes["@mozilla.org/file/directory_service;1"]
+ .getService(Components.interfaces.nsIProperties)
+ .get("CurWorkD", Components.interfaces.nsIFile);
+ dir.append("tests");
+ dir.append("editor");
+ dir.append("composer");
+ dir.append("test");
+
+ var hunspell = Components.classes["@mozilla.org/spellchecker/engine;1"]
+ .getService(Components.interfaces.mozISpellCheckingEngine);
+
+ // Install en-GB dictionary.
+ en_GB = dir.clone();
+ en_GB.append("en-GB");
+ hunspell.addDirectory(en_GB);
+
+ addMessageListener("en_GB-exists", () => en_GB.exists());
+ addMessageListener("destroy", () => hunspell.removeDirectory(en_GB));
+ });
+ is(script.sendSyncMessage("en_GB-exists")[0][0], true,
+ "true expected (en-GB directory should exist)");
+ }
+
+ var doc = evt.target.contentDocument;
+ var elem = doc.getElementById('textarea');
+ var editor = SpecialPowers.wrap(elem).QueryInterface(SpecialPowers.Ci.nsIDOMNSEditableElement).editor;
+ editor.setSpellcheckUserOverride(true);
+ var inlineSpellChecker = editor.getInlineSpellChecker(true);
+
+ SpecialPowers.Cu.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm")
+ .onSpellCheck(elem, function () {
+ var spellchecker = inlineSpellChecker.spellChecker;
+ try {
+ var currentDictonary = spellchecker.GetCurrentDictionary();
+ } catch(e) {}
+
+ if (!currentDictonary) {
+ spellchecker.SetCurrentDictionary('en-US');
+ }
+
+ if (firstLoad) {
+ firstLoad = false;
+
+ // First time around, the dictionary defaults to the locale.
+ is (currentDictonary, "en-US", "unexpected lang " + currentDictonary + " instead of en-US");
+
+ // Select en-GB.
+ spellchecker.SetCurrentDictionary("en-GB");
+
+ content.src = 'http://mochi.test:8888/tests/editor/composer/test/bug678842_subframe.html?firstload=false';
+ } else {
+ is (currentDictonary, "en-GB", "unexpected lang " + currentDictonary + " instead of en-GB");
+ content.removeEventListener('load', loadListener, false);
+
+ // Remove the fake en-GB dictionary again, since it's otherwise picked up by later tests.
+ script.sendSyncMessage("destroy");
+
+ // This will clear the content preferences and reset "spellchecker.dictionary".
+ spellchecker.SetCurrentDictionary("");
+ SimpleTest.finish();
+ }
+ });
+}
+
+content.addEventListener('load', loadListener, false);
+
+content.src = 'http://mochi.test:8888/tests/editor/composer/test/bug678842_subframe.html?firstload=true';
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/composer/test/test_bug697981.html b/editor/composer/test/test_bug697981.html
new file mode 100644
index 000000000..f9417acb0
--- /dev/null
+++ b/editor/composer/test/test_bug697981.html
@@ -0,0 +1,133 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=697981
+-->
+<head>
+ <title>Test for Bug 697981</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=697981">Mozilla Bug 697981</a>
+<p id="display"></p>
+</div>
+
+<textarea id="de-DE" lang="de-DE" onfocus="deFocus()">German heute ist ein guter Tag</textarea>
+<textarea id="en-US" lang="en-US" onfocus="enFocus()">Nogoodword today is a nice day</textarea>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+function getMisspelledWords(editor) {
+ return editor.selectionController.getSelection(SpecialPowers.Ci.nsISelectionController.SELECTION_SPELLCHECK).toString();
+}
+
+var elem_de;
+var editor_de;
+var script;
+
+var onSpellCheck =
+ SpecialPowers.Cu.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm")
+ .onSpellCheck;
+
+/** Test for Bug 697981 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ script = SpecialPowers.loadChromeScript(function() {
+ var dir = Components.classes["@mozilla.org/file/directory_service;1"]
+ .getService(Components.interfaces.nsIProperties)
+ .get("CurWorkD", Components.interfaces.nsIFile);
+ dir.append("tests");
+ dir.append("editor");
+ dir.append("composer");
+ dir.append("test");
+
+ var hunspell = Components.classes["@mozilla.org/spellchecker/engine;1"]
+ .getService(Components.interfaces.mozISpellCheckingEngine);
+
+ // Install de-DE dictionary.
+ var de_DE = dir.clone();
+ de_DE.append("de-DE");
+ hunspell.addDirectory(de_DE);
+
+ addMessageListener("de_DE-exists", () => de_DE.exists());
+ addMessageListener("destroy", () => hunspell.removeDirectory(de_DE));
+ });
+ is(script.sendSyncMessage("de_DE-exists")[0][0], true,
+ "true expected (de_DE directory should exist)");
+
+ document.getElementById('de-DE').focus();
+});
+
+function deFocus() {
+ elem_de = document.getElementById('de-DE');
+ editor_de = SpecialPowers.wrap(elem_de).QueryInterface(SpecialPowers.Ci.nsIDOMNSEditableElement).editor;
+ editor_de.setSpellcheckUserOverride(true);
+ var inlineSpellChecker = editor_de.getInlineSpellChecker(true);
+
+ onSpellCheck(elem_de, function () {
+ var spellchecker = inlineSpellChecker.spellChecker;
+ try {
+ var currentDictonary = spellchecker.GetCurrentDictionary();
+ } catch(e) {}
+
+ // Check that the German dictionary is loaded and that the spell check has worked.
+ is(currentDictonary, "de-DE", "expected de-DE");
+ is(getMisspelledWords(editor_de), "German", "one misspelled word expected: German");
+
+ // Now focus the other textarea, which requires English spelling.
+ document.getElementById('en-US').focus();
+ });
+}
+
+function enFocus() {
+ var elem_en = document.getElementById('en-US');
+ var editor_en = SpecialPowers.wrap(elem_en).QueryInterface(SpecialPowers.Ci.nsIDOMNSEditableElement).editor;
+ editor_en.setSpellcheckUserOverride(true);
+ var inlineSpellChecker = editor_en.getInlineSpellChecker(true);
+
+ onSpellCheck(elem_en, function () {
+ var spellchecker = inlineSpellChecker.spellChecker;
+ try {
+ currentDictonary = spellchecker.GetCurrentDictionary();
+ } catch(e) {}
+
+ // Check that the English dictionary is loaded and that the spell check has worked.
+ is(currentDictonary, "en-US", "expected en-US");
+ is(getMisspelledWords(editor_en), "Nogoodword", "one misspelled word expected: Nogoodword");
+
+ // So far all was boring. The important thing is whether the spell check result
+ // in the de-DE editor is still the same. After losing focus, no spell check
+ // updates should take place there.
+ is(getMisspelledWords(editor_de), "German", "one misspelled word expected: German");
+
+ // Remove the fake de_DE dictionary again.
+ script.sendSyncMessage("destroy");
+
+ // Focus again, so the spelling gets updated, but before we need to kill the focus handler.
+ elem_de.onfocus = null;
+ elem_de.blur();
+ elem_de.focus();
+
+ // After removal, the de_DE editor should refresh the spelling with en-US.
+ onSpellCheck(elem_de, function () {
+ spellchecker = inlineSpellChecker.spellChecker;
+ try {
+ currentDictonary = spellchecker.GetCurrentDictionary();
+ } catch(e) {}
+
+ // Check that the default English dictionary is loaded and that the spell check has worked.
+ is(currentDictonary, "en-US", "expected en-US");
+ is(getMisspelledWords(editor_de), "heute" + "ist" + "ein" + "guter",
+ "some misspelled words expected: heute ist ein guter");
+
+ SimpleTest.finish();
+ });
+ });
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/composer/test/test_bug717433.html b/editor/composer/test/test_bug717433.html
new file mode 100644
index 000000000..59ad9b247
--- /dev/null
+++ b/editor/composer/test/test_bug717433.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=717433
+-->
+<head>
+ <title>Test for Bug 717433</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=717433">Mozilla Bug 717433</a>
+<p id="display"></p>
+<iframe id="content"></iframe>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 717433 **/
+SimpleTest.waitForExplicitFinish();
+var content = document.getElementById('content');
+// Load a subframe containing an editor with language "en". At first
+// load, it will set the dictionary to en-GB or en-US. We set the other one.
+// At second load, it will return the current dictionary. We can check that the
+// dictionary is correctly remembered between loads.
+
+var firstLoad = true;
+var expected = "";
+var script;
+
+var loadListener = function(evt) {
+
+ if (firstLoad) {
+ script = SpecialPowers.loadChromeScript(function() {
+ var dir = Components.classes["@mozilla.org/file/directory_service;1"]
+ .getService(Components.interfaces.nsIProperties)
+ .get("CurWorkD", Components.interfaces.nsIFile);
+ dir.append("tests");
+ dir.append("editor");
+ dir.append("composer");
+ dir.append("test");
+
+ var hunspell = Components.classes["@mozilla.org/spellchecker/engine;1"]
+ .getService(Components.interfaces.mozISpellCheckingEngine);
+
+ // Install en-GB dictionary.
+ var en_GB = dir.clone();
+ en_GB.append("en-GB");
+ hunspell.addDirectory(en_GB);
+
+ addMessageListener("en_GB-exists", () => en_GB.exists());
+ addMessageListener("destroy", () => hunspell.removeDirectory(en_GB));
+ });
+ is(script.sendSyncMessage("en_GB-exists")[0][0], true,
+ "true expected (en-GB directory should exist)");
+ }
+
+ var doc = evt.target.contentDocument;
+ var elem = doc.getElementById('textarea');
+ var editor = SpecialPowers.wrap(elem).QueryInterface(SpecialPowers.Ci.nsIDOMNSEditableElement).editor;
+ editor.setSpellcheckUserOverride(true);
+ var inlineSpellChecker = editor.getInlineSpellChecker(true);
+
+ SpecialPowers.Cu.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm")
+ .onSpellCheck(elem, function () {
+ var spellchecker = inlineSpellChecker.spellChecker;
+ try {
+ var currentDictonary = spellchecker.GetCurrentDictionary();
+ } catch(e) {}
+
+ if (firstLoad) {
+ firstLoad = false;
+
+ // First time around, we get a random dictionary based on the language "en".
+ if (currentDictonary == "en-GB") {
+ spellchecker.SetCurrentDictionary("en-US");
+ expected = "en-US";
+ } else if (currentDictonary == "en-US") {
+ spellchecker.SetCurrentDictionary("en-GB");
+ expected = "en-GB";
+ } else {
+ is(true, false, "Neither en-US nor en-GB are current");
+ }
+ content.src = 'http://mochi.test:8888/tests/editor/composer/test/bug717433_subframe.html?firstload=false';
+ } else {
+ is(currentDictonary, expected, expected + " expected");
+ content.removeEventListener('load', loadListener, false);
+
+ // Remove the fake en-GB dictionary again, since it's otherwise picked up by later tests.
+ script.sendSyncMessage("destroy");
+
+ // This will clear the content preferences and reset "spellchecker.dictionary".
+ spellchecker.SetCurrentDictionary("");
+ SimpleTest.finish();
+ }
+ });
+}
+
+content.addEventListener('load', loadListener, false);
+
+content.src = 'http://mochi.test:8888/tests/editor/composer/test/bug717433_subframe.html?firstload=true';
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/composer/test/test_bug738440.html b/editor/composer/test/test_bug738440.html
new file mode 100644
index 000000000..a021906cf
--- /dev/null
+++ b/editor/composer/test/test_bug738440.html
@@ -0,0 +1,37 @@
+<!doctype html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=738440
+-->
+<title>Test for Bug 738440</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=738440">Mozilla Bug 738440</a>
+<div contenteditable></div>
+<script>
+
+/** Test for Bug 738440 **/
+document.execCommand("stylewithcss", false, "true");
+is(document.queryCommandState("stylewithcss"), true,
+ "setting stylewithcss to true should cause its state to be true");
+is(document.queryCommandState("usecss"), false,
+ "usecss state should always be false");
+
+document.execCommand("stylewithcss", false, "false");
+is(document.queryCommandState("stylewithcss"), false,
+ "setting stylewithcss to false should cause its state to be false");
+is(document.queryCommandState("usecss"), false,
+ "usecss state should always be false");
+
+document.execCommand("usecss", false, "true");
+is(document.queryCommandState("stylewithcss"), false,
+ "setting usecss to true should cause stylewithcss state to be false");
+is(document.queryCommandState("usecss"), false,
+ "usecss state should always be false");
+
+document.execCommand("usecss", false, "false");
+is(document.queryCommandState("stylewithcss"), true,
+ "setting usecss to false should cause stylewithcss state to be true");
+is(document.queryCommandState("usecss"), false,
+ "usecss state should always be false");
+
+</script>
diff --git a/editor/libeditor/CSSEditUtils.cpp b/editor/libeditor/CSSEditUtils.cpp
new file mode 100644
index 000000000..5199838c0
--- /dev/null
+++ b/editor/libeditor/CSSEditUtils.cpp
@@ -0,0 +1,1422 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/CSSEditUtils.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/ChangeStyleTransaction.h"
+#include "mozilla/HTMLEditor.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/DeclarationBlockInlines.h"
+#include "mozilla/css/StyleRule.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/mozalloc.h"
+#include "nsAString.h"
+#include "nsCOMPtr.h"
+#include "nsColor.h"
+#include "nsComputedDOMStyle.h"
+#include "nsDebug.h"
+#include "nsDependentSubstring.h"
+#include "nsError.h"
+#include "nsGkAtoms.h"
+#include "nsIAtom.h"
+#include "nsIContent.h"
+#include "nsIDOMCSSStyleDeclaration.h"
+#include "nsIDOMElement.h"
+#include "nsIDOMNode.h"
+#include "nsIDOMWindow.h"
+#include "nsIDocument.h"
+#include "nsIEditor.h"
+#include "nsINode.h"
+#include "nsISupportsImpl.h"
+#include "nsISupportsUtils.h"
+#include "nsLiteralString.h"
+#include "nsPIDOMWindow.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsStringIterator.h"
+#include "nsStyledElement.h"
+#include "nsSubstringTuple.h"
+#include "nsUnicharUtils.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+static
+void ProcessBValue(const nsAString* aInputString,
+ nsAString& aOutputString,
+ const char* aDefaultValueString,
+ const char* aPrependString,
+ const char* aAppendString)
+{
+ if (aInputString && aInputString->EqualsLiteral("-moz-editor-invert-value")) {
+ aOutputString.AssignLiteral("normal");
+ }
+ else {
+ aOutputString.AssignLiteral("bold");
+ }
+}
+
+static
+void ProcessDefaultValue(const nsAString* aInputString,
+ nsAString& aOutputString,
+ const char* aDefaultValueString,
+ const char* aPrependString,
+ const char* aAppendString)
+{
+ CopyASCIItoUTF16(aDefaultValueString, aOutputString);
+}
+
+static
+void ProcessSameValue(const nsAString* aInputString,
+ nsAString & aOutputString,
+ const char* aDefaultValueString,
+ const char* aPrependString,
+ const char* aAppendString)
+{
+ if (aInputString) {
+ aOutputString.Assign(*aInputString);
+ }
+ else
+ aOutputString.Truncate();
+}
+
+static
+void ProcessExtendedValue(const nsAString* aInputString,
+ nsAString& aOutputString,
+ const char* aDefaultValueString,
+ const char* aPrependString,
+ const char* aAppendString)
+{
+ aOutputString.Truncate();
+ if (aInputString) {
+ if (aPrependString) {
+ AppendASCIItoUTF16(aPrependString, aOutputString);
+ }
+ aOutputString.Append(*aInputString);
+ if (aAppendString) {
+ AppendASCIItoUTF16(aAppendString, aOutputString);
+ }
+ }
+}
+
+static
+void ProcessLengthValue(const nsAString* aInputString,
+ nsAString& aOutputString,
+ const char* aDefaultValueString,
+ const char* aPrependString,
+ const char* aAppendString)
+{
+ aOutputString.Truncate();
+ if (aInputString) {
+ aOutputString.Append(*aInputString);
+ if (-1 == aOutputString.FindChar(char16_t('%'))) {
+ aOutputString.AppendLiteral("px");
+ }
+ }
+}
+
+static
+void ProcessListStyleTypeValue(const nsAString* aInputString,
+ nsAString& aOutputString,
+ const char* aDefaultValueString,
+ const char* aPrependString,
+ const char* aAppendString)
+{
+ aOutputString.Truncate();
+ if (aInputString) {
+ if (aInputString->EqualsLiteral("1")) {
+ aOutputString.AppendLiteral("decimal");
+ }
+ else if (aInputString->EqualsLiteral("a")) {
+ aOutputString.AppendLiteral("lower-alpha");
+ }
+ else if (aInputString->EqualsLiteral("A")) {
+ aOutputString.AppendLiteral("upper-alpha");
+ }
+ else if (aInputString->EqualsLiteral("i")) {
+ aOutputString.AppendLiteral("lower-roman");
+ }
+ else if (aInputString->EqualsLiteral("I")) {
+ aOutputString.AppendLiteral("upper-roman");
+ }
+ else if (aInputString->EqualsLiteral("square")
+ || aInputString->EqualsLiteral("circle")
+ || aInputString->EqualsLiteral("disc")) {
+ aOutputString.Append(*aInputString);
+ }
+ }
+}
+
+static
+void ProcessMarginLeftValue(const nsAString* aInputString,
+ nsAString& aOutputString,
+ const char* aDefaultValueString,
+ const char* aPrependString,
+ const char* aAppendString)
+{
+ aOutputString.Truncate();
+ if (aInputString) {
+ if (aInputString->EqualsLiteral("center") ||
+ aInputString->EqualsLiteral("-moz-center")) {
+ aOutputString.AppendLiteral("auto");
+ }
+ else if (aInputString->EqualsLiteral("right") ||
+ aInputString->EqualsLiteral("-moz-right")) {
+ aOutputString.AppendLiteral("auto");
+ }
+ else {
+ aOutputString.AppendLiteral("0px");
+ }
+ }
+}
+
+static
+void ProcessMarginRightValue(const nsAString* aInputString,
+ nsAString& aOutputString,
+ const char* aDefaultValueString,
+ const char* aPrependString,
+ const char* aAppendString)
+{
+ aOutputString.Truncate();
+ if (aInputString) {
+ if (aInputString->EqualsLiteral("center") ||
+ aInputString->EqualsLiteral("-moz-center")) {
+ aOutputString.AppendLiteral("auto");
+ }
+ else if (aInputString->EqualsLiteral("left") ||
+ aInputString->EqualsLiteral("-moz-left")) {
+ aOutputString.AppendLiteral("auto");
+ }
+ else {
+ aOutputString.AppendLiteral("0px");
+ }
+ }
+}
+
+const CSSEditUtils::CSSEquivTable boldEquivTable[] = {
+ { CSSEditUtils::eCSSEditableProperty_font_weight, ProcessBValue, nullptr, nullptr, nullptr, true, false },
+ { CSSEditUtils::eCSSEditableProperty_NONE, 0 }
+};
+
+const CSSEditUtils::CSSEquivTable italicEquivTable[] = {
+ { CSSEditUtils::eCSSEditableProperty_font_style, ProcessDefaultValue, "italic", nullptr, nullptr, true, false },
+ { CSSEditUtils::eCSSEditableProperty_NONE, 0 }
+};
+
+const CSSEditUtils::CSSEquivTable underlineEquivTable[] = {
+ { CSSEditUtils::eCSSEditableProperty_text_decoration, ProcessDefaultValue, "underline", nullptr, nullptr, true, false },
+ { CSSEditUtils::eCSSEditableProperty_NONE, 0 }
+};
+
+const CSSEditUtils::CSSEquivTable strikeEquivTable[] = {
+ { CSSEditUtils::eCSSEditableProperty_text_decoration, ProcessDefaultValue, "line-through", nullptr, nullptr, true, false },
+ { CSSEditUtils::eCSSEditableProperty_NONE, 0 }
+};
+
+const CSSEditUtils::CSSEquivTable ttEquivTable[] = {
+ { CSSEditUtils::eCSSEditableProperty_font_family, ProcessDefaultValue, "monospace", nullptr, nullptr, true, false },
+ { CSSEditUtils::eCSSEditableProperty_NONE, 0 }
+};
+
+const CSSEditUtils::CSSEquivTable fontColorEquivTable[] = {
+ { CSSEditUtils::eCSSEditableProperty_color, ProcessSameValue, nullptr, nullptr, nullptr, true, false },
+ { CSSEditUtils::eCSSEditableProperty_NONE, 0 }
+};
+
+const CSSEditUtils::CSSEquivTable fontFaceEquivTable[] = {
+ { CSSEditUtils::eCSSEditableProperty_font_family, ProcessSameValue, nullptr, nullptr, nullptr, true, false },
+ { CSSEditUtils::eCSSEditableProperty_NONE, 0 }
+};
+
+const CSSEditUtils::CSSEquivTable bgcolorEquivTable[] = {
+ { CSSEditUtils::eCSSEditableProperty_background_color, ProcessSameValue, nullptr, nullptr, nullptr, true, false },
+ { CSSEditUtils::eCSSEditableProperty_NONE, 0 }
+};
+
+const CSSEditUtils::CSSEquivTable backgroundImageEquivTable[] = {
+ { CSSEditUtils::eCSSEditableProperty_background_image, ProcessExtendedValue, nullptr, "url(", ")", true, true },
+ { CSSEditUtils::eCSSEditableProperty_NONE, 0 }
+};
+
+const CSSEditUtils::CSSEquivTable textColorEquivTable[] = {
+ { CSSEditUtils::eCSSEditableProperty_color, ProcessSameValue, nullptr, nullptr, nullptr, true, false },
+ { CSSEditUtils::eCSSEditableProperty_NONE, 0 }
+};
+
+const CSSEditUtils::CSSEquivTable borderEquivTable[] = {
+ { CSSEditUtils::eCSSEditableProperty_border, ProcessExtendedValue, nullptr, nullptr, "px solid", true, false },
+ { CSSEditUtils::eCSSEditableProperty_NONE, 0 }
+};
+
+const CSSEditUtils::CSSEquivTable textAlignEquivTable[] = {
+ { CSSEditUtils::eCSSEditableProperty_text_align, ProcessSameValue, nullptr, nullptr, nullptr, true, false },
+ { CSSEditUtils::eCSSEditableProperty_NONE, 0 }
+};
+
+const CSSEditUtils::CSSEquivTable captionAlignEquivTable[] = {
+ { CSSEditUtils::eCSSEditableProperty_caption_side, ProcessSameValue, nullptr, nullptr, nullptr, true, false },
+ { CSSEditUtils::eCSSEditableProperty_NONE, 0 }
+};
+
+const CSSEditUtils::CSSEquivTable verticalAlignEquivTable[] = {
+ { CSSEditUtils::eCSSEditableProperty_vertical_align, ProcessSameValue, nullptr, nullptr, nullptr, true, false },
+ { CSSEditUtils::eCSSEditableProperty_NONE, 0 }
+};
+
+const CSSEditUtils::CSSEquivTable nowrapEquivTable[] = {
+ { CSSEditUtils::eCSSEditableProperty_whitespace, ProcessDefaultValue, "nowrap", nullptr, nullptr, true, false },
+ { CSSEditUtils::eCSSEditableProperty_NONE, 0 }
+};
+
+const CSSEditUtils::CSSEquivTable widthEquivTable[] = {
+ { CSSEditUtils::eCSSEditableProperty_width, ProcessLengthValue, nullptr, nullptr, nullptr, true, false },
+ { CSSEditUtils::eCSSEditableProperty_NONE, 0 }
+};
+
+const CSSEditUtils::CSSEquivTable heightEquivTable[] = {
+ { CSSEditUtils::eCSSEditableProperty_height, ProcessLengthValue, nullptr, nullptr, nullptr, true, false },
+ { CSSEditUtils::eCSSEditableProperty_NONE, 0 }
+};
+
+const CSSEditUtils::CSSEquivTable listStyleTypeEquivTable[] = {
+ { CSSEditUtils::eCSSEditableProperty_list_style_type, ProcessListStyleTypeValue, nullptr, nullptr, nullptr, true, true },
+ { CSSEditUtils::eCSSEditableProperty_NONE, 0 }
+};
+
+const CSSEditUtils::CSSEquivTable tableAlignEquivTable[] = {
+ { CSSEditUtils::eCSSEditableProperty_text_align, ProcessDefaultValue, "left", nullptr, nullptr, false, false },
+ { CSSEditUtils::eCSSEditableProperty_margin_left, ProcessMarginLeftValue, nullptr, nullptr, nullptr, true, false },
+ { CSSEditUtils::eCSSEditableProperty_margin_right, ProcessMarginRightValue, nullptr, nullptr, nullptr, true, false },
+ { CSSEditUtils::eCSSEditableProperty_NONE, 0 }
+};
+
+const CSSEditUtils::CSSEquivTable hrAlignEquivTable[] = {
+ { CSSEditUtils::eCSSEditableProperty_margin_left, ProcessMarginLeftValue, nullptr, nullptr, nullptr, true, false },
+ { CSSEditUtils::eCSSEditableProperty_margin_right, ProcessMarginRightValue, nullptr, nullptr, nullptr, true, false },
+ { CSSEditUtils::eCSSEditableProperty_NONE, 0 }
+};
+
+CSSEditUtils::CSSEditUtils(HTMLEditor* aHTMLEditor)
+ : mHTMLEditor(aHTMLEditor)
+ , mIsCSSPrefChecked(true)
+{
+ // let's retrieve the value of the "CSS editing" pref
+ mIsCSSPrefChecked = Preferences::GetBool("editor.use_css", mIsCSSPrefChecked);
+}
+
+CSSEditUtils::~CSSEditUtils()
+{
+}
+
+// Answers true if we have some CSS equivalence for the HTML style defined
+// by aProperty and/or aAttribute for the node aNode
+bool
+CSSEditUtils::IsCSSEditableProperty(nsINode* aNode,
+ nsIAtom* aProperty,
+ const nsAString* aAttribute)
+{
+ MOZ_ASSERT(aNode);
+
+ nsINode* node = aNode;
+ // we need an element node here
+ if (node->NodeType() == nsIDOMNode::TEXT_NODE) {
+ node = node->GetParentNode();
+ NS_ENSURE_TRUE(node, false);
+ }
+
+ // html inline styles B I TT U STRIKE and COLOR/FACE on FONT
+ if (nsGkAtoms::b == aProperty ||
+ nsGkAtoms::i == aProperty ||
+ nsGkAtoms::tt == aProperty ||
+ nsGkAtoms::u == aProperty ||
+ nsGkAtoms::strike == aProperty ||
+ (nsGkAtoms::font == aProperty && aAttribute &&
+ (aAttribute->EqualsLiteral("color") ||
+ aAttribute->EqualsLiteral("face")))) {
+ return true;
+ }
+
+ // ALIGN attribute on elements supporting it
+ if (aAttribute && (aAttribute->EqualsLiteral("align")) &&
+ node->IsAnyOfHTMLElements(nsGkAtoms::div,
+ nsGkAtoms::p,
+ nsGkAtoms::h1,
+ nsGkAtoms::h2,
+ nsGkAtoms::h3,
+ nsGkAtoms::h4,
+ nsGkAtoms::h5,
+ nsGkAtoms::h6,
+ nsGkAtoms::td,
+ nsGkAtoms::th,
+ nsGkAtoms::table,
+ nsGkAtoms::hr,
+ // For the above, why not use
+ // HTMLEditUtils::SupportsAlignAttr?
+ // It also checks for tbody, tfoot, thead.
+ // Let's add the following elements here even
+ // if "align" has a different meaning for them
+ nsGkAtoms::legend,
+ nsGkAtoms::caption)) {
+ return true;
+ }
+
+ if (aAttribute && (aAttribute->EqualsLiteral("valign")) &&
+ node->IsAnyOfHTMLElements(nsGkAtoms::col,
+ nsGkAtoms::colgroup,
+ nsGkAtoms::tbody,
+ nsGkAtoms::td,
+ nsGkAtoms::th,
+ nsGkAtoms::tfoot,
+ nsGkAtoms::thead,
+ nsGkAtoms::tr)) {
+ return true;
+ }
+
+ // attributes TEXT, BACKGROUND and BGCOLOR on BODY
+ if (aAttribute && node->IsHTMLElement(nsGkAtoms::body) &&
+ (aAttribute->EqualsLiteral("text")
+ || aAttribute->EqualsLiteral("background")
+ || aAttribute->EqualsLiteral("bgcolor"))) {
+ return true;
+ }
+
+ // attribute BGCOLOR on other elements
+ if (aAttribute && aAttribute->EqualsLiteral("bgcolor")) {
+ return true;
+ }
+
+ // attributes HEIGHT, WIDTH and NOWRAP on TD and TH
+ if (aAttribute &&
+ node->IsAnyOfHTMLElements(nsGkAtoms::td, nsGkAtoms::th) &&
+ (aAttribute->EqualsLiteral("height")
+ || aAttribute->EqualsLiteral("width")
+ || aAttribute->EqualsLiteral("nowrap"))) {
+ return true;
+ }
+
+ // attributes HEIGHT and WIDTH on TABLE
+ if (aAttribute && node->IsHTMLElement(nsGkAtoms::table) &&
+ (aAttribute->EqualsLiteral("height")
+ || aAttribute->EqualsLiteral("width"))) {
+ return true;
+ }
+
+ // attributes SIZE and WIDTH on HR
+ if (aAttribute && node->IsHTMLElement(nsGkAtoms::hr) &&
+ (aAttribute->EqualsLiteral("size")
+ || aAttribute->EqualsLiteral("width"))) {
+ return true;
+ }
+
+ // attribute TYPE on OL UL LI
+ if (aAttribute &&
+ node->IsAnyOfHTMLElements(nsGkAtoms::ol, nsGkAtoms::ul,
+ nsGkAtoms::li) &&
+ aAttribute->EqualsLiteral("type")) {
+ return true;
+ }
+
+ if (aAttribute && node->IsHTMLElement(nsGkAtoms::img) &&
+ (aAttribute->EqualsLiteral("border")
+ || aAttribute->EqualsLiteral("width")
+ || aAttribute->EqualsLiteral("height"))) {
+ return true;
+ }
+
+ // other elements that we can align using CSS even if they
+ // can't carry the html ALIGN attribute
+ if (aAttribute && aAttribute->EqualsLiteral("align") &&
+ node->IsAnyOfHTMLElements(nsGkAtoms::ul,
+ nsGkAtoms::ol,
+ nsGkAtoms::dl,
+ nsGkAtoms::li,
+ nsGkAtoms::dd,
+ nsGkAtoms::dt,
+ nsGkAtoms::address,
+ nsGkAtoms::pre)) {
+ return true;
+ }
+
+ return false;
+}
+
+// The lowest level above the transaction; adds the CSS declaration
+// "aProperty : aValue" to the inline styles carried by aElement
+nsresult
+CSSEditUtils::SetCSSProperty(Element& aElement,
+ nsIAtom& aProperty,
+ const nsAString& aValue,
+ bool aSuppressTxn)
+{
+ RefPtr<ChangeStyleTransaction> transaction =
+ CreateCSSPropertyTxn(aElement, aProperty, aValue,
+ ChangeStyleTransaction::eSet);
+ if (aSuppressTxn) {
+ return transaction->DoTransaction();
+ }
+ return mHTMLEditor->DoTransaction(transaction);
+}
+
+nsresult
+CSSEditUtils::SetCSSPropertyPixels(Element& aElement,
+ nsIAtom& aProperty,
+ int32_t aIntValue)
+{
+ nsAutoString s;
+ s.AppendInt(aIntValue);
+ return SetCSSProperty(aElement, aProperty, s + NS_LITERAL_STRING("px"),
+ /* suppress txn */ false);
+}
+
+// The lowest level above the transaction; removes the value aValue from the
+// list of values specified for the CSS property aProperty, or totally remove
+// the declaration if this property accepts only one value
+nsresult
+CSSEditUtils::RemoveCSSProperty(Element& aElement,
+ nsIAtom& aProperty,
+ const nsAString& aValue,
+ bool aSuppressTxn)
+{
+ RefPtr<ChangeStyleTransaction> transaction =
+ CreateCSSPropertyTxn(aElement, aProperty, aValue,
+ ChangeStyleTransaction::eRemove);
+ if (aSuppressTxn) {
+ return transaction->DoTransaction();
+ }
+ return mHTMLEditor->DoTransaction(transaction);
+}
+
+already_AddRefed<ChangeStyleTransaction>
+CSSEditUtils::CreateCSSPropertyTxn(
+ Element& aElement,
+ nsIAtom& aAttribute,
+ const nsAString& aValue,
+ ChangeStyleTransaction::EChangeType aChangeType)
+{
+ RefPtr<ChangeStyleTransaction> transaction =
+ new ChangeStyleTransaction(aElement, aAttribute, aValue, aChangeType);
+ return transaction.forget();
+}
+
+nsresult
+CSSEditUtils::GetSpecifiedProperty(nsINode& aNode,
+ nsIAtom& aProperty,
+ nsAString& aValue)
+{
+ return GetCSSInlinePropertyBase(&aNode, &aProperty, aValue, eSpecified);
+}
+
+nsresult
+CSSEditUtils::GetComputedProperty(nsINode& aNode,
+ nsIAtom& aProperty,
+ nsAString& aValue)
+{
+ return GetCSSInlinePropertyBase(&aNode, &aProperty, aValue, eComputed);
+}
+
+nsresult
+CSSEditUtils::GetCSSInlinePropertyBase(nsINode* aNode,
+ nsIAtom* aProperty,
+ nsAString& aValue,
+ StyleType aStyleType)
+{
+ MOZ_ASSERT(aNode && aProperty);
+ aValue.Truncate();
+
+ nsCOMPtr<Element> element = GetElementContainerOrSelf(aNode);
+ NS_ENSURE_TRUE(element, NS_ERROR_NULL_POINTER);
+
+ if (aStyleType == eComputed) {
+ // Get the all the computed css styles attached to the element node
+ RefPtr<nsComputedDOMStyle> cssDecl = GetComputedStyle(element);
+ NS_ENSURE_STATE(cssDecl);
+
+ // from these declarations, get the one we want and that one only
+ MOZ_ALWAYS_SUCCEEDS(
+ cssDecl->GetPropertyValue(nsDependentAtomString(aProperty), aValue));
+
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(aStyleType == eSpecified);
+ RefPtr<DeclarationBlock> decl = element->GetInlineStyleDeclaration();
+ if (!decl) {
+ return NS_OK;
+ }
+ if (decl->IsServo()) {
+ MOZ_CRASH("stylo: not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+ nsCSSPropertyID prop =
+ nsCSSProps::LookupProperty(nsDependentAtomString(aProperty),
+ CSSEnabledState::eForAllContent);
+ MOZ_ASSERT(prop != eCSSProperty_UNKNOWN);
+ decl->AsGecko()->GetPropertyValueByID(prop, aValue);
+
+ return NS_OK;
+}
+
+already_AddRefed<nsComputedDOMStyle>
+CSSEditUtils::GetComputedStyle(Element* aElement)
+{
+ MOZ_ASSERT(aElement);
+
+ nsIDocument* doc = aElement->GetUncomposedDoc();
+ NS_ENSURE_TRUE(doc, nullptr);
+
+ nsIPresShell* presShell = doc->GetShell();
+ NS_ENSURE_TRUE(presShell, nullptr);
+
+ RefPtr<nsComputedDOMStyle> style =
+ NS_NewComputedDOMStyle(aElement, EmptyString(), presShell);
+
+ return style.forget();
+}
+
+// remove the CSS style "aProperty : aPropertyValue" and possibly remove the whole node
+// if it is a span and if its only attribute is _moz_dirty
+nsresult
+CSSEditUtils::RemoveCSSInlineStyle(nsIDOMNode* aNode,
+ nsIAtom* aProperty,
+ const nsAString& aPropertyValue)
+{
+ nsCOMPtr<Element> element = do_QueryInterface(aNode);
+ NS_ENSURE_STATE(element);
+
+ // remove the property from the style attribute
+ nsresult rv = RemoveCSSProperty(*element, *aProperty, aPropertyValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!element->IsHTMLElement(nsGkAtoms::span) ||
+ HTMLEditor::HasAttributes(element)) {
+ return NS_OK;
+ }
+
+ return mHTMLEditor->RemoveContainer(element);
+}
+
+// Answers true if the property can be removed by setting a "none" CSS value
+// on a node
+bool
+CSSEditUtils::IsCSSInvertible(nsIAtom& aProperty,
+ const nsAString* aAttribute)
+{
+ return nsGkAtoms::b == &aProperty;
+}
+
+// Get the default browser background color if we need it for GetCSSBackgroundColorState
+void
+CSSEditUtils::GetDefaultBackgroundColor(nsAString& aColor)
+{
+ if (Preferences::GetBool("editor.use_custom_colors", false)) {
+ nsresult rv = Preferences::GetString("editor.background_color", &aColor);
+ // XXX Why don't you validate the pref value?
+ if (NS_FAILED(rv)) {
+ NS_WARNING("failed to get editor.background_color");
+ aColor.AssignLiteral("#ffffff"); // Default to white
+ }
+ return;
+ }
+
+ if (Preferences::GetBool("browser.display.use_system_colors", false)) {
+ return;
+ }
+
+ nsresult rv =
+ Preferences::GetString("browser.display.background_color", &aColor);
+ // XXX Why don't you validate the pref value?
+ if (NS_FAILED(rv)) {
+ NS_WARNING("failed to get browser.display.background_color");
+ aColor.AssignLiteral("#ffffff"); // Default to white
+ }
+}
+
+// Get the default length unit used for CSS Indent/Outdent
+void
+CSSEditUtils::GetDefaultLengthUnit(nsAString& aLengthUnit)
+{
+ nsresult rv =
+ Preferences::GetString("editor.css.default_length_unit", &aLengthUnit);
+ // XXX Why don't you validate the pref value?
+ if (NS_FAILED(rv)) {
+ aLengthUnit.AssignLiteral("px");
+ }
+}
+
+// Unfortunately, CSSStyleDeclaration::GetPropertyCSSValue is not yet
+// implemented... We need then a way to determine the number part and the unit
+// from aString, aString being the result of a GetPropertyValue query...
+void
+CSSEditUtils::ParseLength(const nsAString& aString,
+ float* aValue,
+ nsIAtom** aUnit)
+{
+ if (aString.IsEmpty()) {
+ *aValue = 0;
+ *aUnit = NS_Atomize(aString).take();
+ return;
+ }
+
+ nsAString::const_iterator iter;
+ aString.BeginReading(iter);
+
+ float a = 10.0f , b = 1.0f, value = 0;
+ int8_t sign = 1;
+ int32_t i = 0, j = aString.Length();
+ char16_t c;
+ bool floatingPointFound = false;
+ c = *iter;
+ if (char16_t('-') == c) { sign = -1; iter++; i++; }
+ else if (char16_t('+') == c) { iter++; i++; }
+ while (i < j) {
+ c = *iter;
+ if ((char16_t('0') == c) ||
+ (char16_t('1') == c) ||
+ (char16_t('2') == c) ||
+ (char16_t('3') == c) ||
+ (char16_t('4') == c) ||
+ (char16_t('5') == c) ||
+ (char16_t('6') == c) ||
+ (char16_t('7') == c) ||
+ (char16_t('8') == c) ||
+ (char16_t('9') == c)) {
+ value = (value * a) + (b * (c - char16_t('0')));
+ b = b / 10 * a;
+ }
+ else if (!floatingPointFound && (char16_t('.') == c)) {
+ floatingPointFound = true;
+ a = 1.0f; b = 0.1f;
+ }
+ else break;
+ iter++;
+ i++;
+ }
+ *aValue = value * sign;
+ *aUnit = NS_Atomize(StringTail(aString, j-i)).take();
+}
+
+void
+CSSEditUtils::GetCSSPropertyAtom(nsCSSEditableProperty aProperty,
+ nsIAtom** aAtom)
+{
+ *aAtom = nullptr;
+ switch (aProperty) {
+ case eCSSEditableProperty_background_color:
+ *aAtom = nsGkAtoms::backgroundColor;
+ break;
+ case eCSSEditableProperty_background_image:
+ *aAtom = nsGkAtoms::background_image;
+ break;
+ case eCSSEditableProperty_border:
+ *aAtom = nsGkAtoms::border;
+ break;
+ case eCSSEditableProperty_caption_side:
+ *aAtom = nsGkAtoms::caption_side;
+ break;
+ case eCSSEditableProperty_color:
+ *aAtom = nsGkAtoms::color;
+ break;
+ case eCSSEditableProperty_float:
+ *aAtom = nsGkAtoms::_float;
+ break;
+ case eCSSEditableProperty_font_family:
+ *aAtom = nsGkAtoms::font_family;
+ break;
+ case eCSSEditableProperty_font_size:
+ *aAtom = nsGkAtoms::font_size;
+ break;
+ case eCSSEditableProperty_font_style:
+ *aAtom = nsGkAtoms::font_style;
+ break;
+ case eCSSEditableProperty_font_weight:
+ *aAtom = nsGkAtoms::fontWeight;
+ break;
+ case eCSSEditableProperty_height:
+ *aAtom = nsGkAtoms::height;
+ break;
+ case eCSSEditableProperty_list_style_type:
+ *aAtom = nsGkAtoms::list_style_type;
+ break;
+ case eCSSEditableProperty_margin_left:
+ *aAtom = nsGkAtoms::marginLeft;
+ break;
+ case eCSSEditableProperty_margin_right:
+ *aAtom = nsGkAtoms::marginRight;
+ break;
+ case eCSSEditableProperty_text_align:
+ *aAtom = nsGkAtoms::textAlign;
+ break;
+ case eCSSEditableProperty_text_decoration:
+ *aAtom = nsGkAtoms::text_decoration;
+ break;
+ case eCSSEditableProperty_vertical_align:
+ *aAtom = nsGkAtoms::vertical_align;
+ break;
+ case eCSSEditableProperty_whitespace:
+ *aAtom = nsGkAtoms::white_space;
+ break;
+ case eCSSEditableProperty_width:
+ *aAtom = nsGkAtoms::width;
+ break;
+ case eCSSEditableProperty_NONE:
+ // intentionally empty
+ break;
+ }
+}
+
+// Populate aProperty and aValueArray with the CSS declarations equivalent to the
+// value aValue according to the equivalence table aEquivTable
+void
+CSSEditUtils::BuildCSSDeclarations(nsTArray<nsIAtom*>& aPropertyArray,
+ nsTArray<nsString>& aValueArray,
+ const CSSEquivTable* aEquivTable,
+ const nsAString* aValue,
+ bool aGetOrRemoveRequest)
+{
+ // clear arrays
+ aPropertyArray.Clear();
+ aValueArray.Clear();
+
+ // if we have an input value, let's use it
+ nsAutoString value, lowerCasedValue;
+ if (aValue) {
+ value.Assign(*aValue);
+ lowerCasedValue.Assign(*aValue);
+ ToLowerCase(lowerCasedValue);
+ }
+
+ int8_t index = 0;
+ nsCSSEditableProperty cssProperty = aEquivTable[index].cssProperty;
+ while (cssProperty) {
+ if (!aGetOrRemoveRequest|| aEquivTable[index].gettable) {
+ nsAutoString cssValue, cssPropertyString;
+ nsIAtom * cssPropertyAtom;
+ // find the equivalent css value for the index-th property in
+ // the equivalence table
+ (*aEquivTable[index].processValueFunctor) ((!aGetOrRemoveRequest || aEquivTable[index].caseSensitiveValue) ? &value : &lowerCasedValue,
+ cssValue,
+ aEquivTable[index].defaultValue,
+ aEquivTable[index].prependValue,
+ aEquivTable[index].appendValue);
+ GetCSSPropertyAtom(cssProperty, &cssPropertyAtom);
+ aPropertyArray.AppendElement(cssPropertyAtom);
+ aValueArray.AppendElement(cssValue);
+ }
+ index++;
+ cssProperty = aEquivTable[index].cssProperty;
+ }
+}
+
+// Populate cssPropertyArray and cssValueArray with the declarations equivalent
+// to aHTMLProperty/aAttribute/aValue for the node aNode
+void
+CSSEditUtils::GenerateCSSDeclarationsFromHTMLStyle(
+ Element* aElement,
+ nsIAtom* aHTMLProperty,
+ const nsAString* aAttribute,
+ const nsAString* aValue,
+ nsTArray<nsIAtom*>& cssPropertyArray,
+ nsTArray<nsString>& cssValueArray,
+ bool aGetOrRemoveRequest)
+{
+ MOZ_ASSERT(aElement);
+ const CSSEditUtils::CSSEquivTable* equivTable = nullptr;
+
+ if (nsGkAtoms::b == aHTMLProperty) {
+ equivTable = boldEquivTable;
+ } else if (nsGkAtoms::i == aHTMLProperty) {
+ equivTable = italicEquivTable;
+ } else if (nsGkAtoms::u == aHTMLProperty) {
+ equivTable = underlineEquivTable;
+ } else if (nsGkAtoms::strike == aHTMLProperty) {
+ equivTable = strikeEquivTable;
+ } else if (nsGkAtoms::tt == aHTMLProperty) {
+ equivTable = ttEquivTable;
+ } else if (aAttribute) {
+ if (nsGkAtoms::font == aHTMLProperty &&
+ aAttribute->EqualsLiteral("color")) {
+ equivTable = fontColorEquivTable;
+ } else if (nsGkAtoms::font == aHTMLProperty &&
+ aAttribute->EqualsLiteral("face")) {
+ equivTable = fontFaceEquivTable;
+ } else if (aAttribute->EqualsLiteral("bgcolor")) {
+ equivTable = bgcolorEquivTable;
+ } else if (aAttribute->EqualsLiteral("background")) {
+ equivTable = backgroundImageEquivTable;
+ } else if (aAttribute->EqualsLiteral("text")) {
+ equivTable = textColorEquivTable;
+ } else if (aAttribute->EqualsLiteral("border")) {
+ equivTable = borderEquivTable;
+ } else if (aAttribute->EqualsLiteral("align")) {
+ if (aElement->IsHTMLElement(nsGkAtoms::table)) {
+ equivTable = tableAlignEquivTable;
+ } else if (aElement->IsHTMLElement(nsGkAtoms::hr)) {
+ equivTable = hrAlignEquivTable;
+ } else if (aElement->IsAnyOfHTMLElements(nsGkAtoms::legend,
+ nsGkAtoms::caption)) {
+ equivTable = captionAlignEquivTable;
+ } else {
+ equivTable = textAlignEquivTable;
+ }
+ } else if (aAttribute->EqualsLiteral("valign")) {
+ equivTable = verticalAlignEquivTable;
+ } else if (aAttribute->EqualsLiteral("nowrap")) {
+ equivTable = nowrapEquivTable;
+ } else if (aAttribute->EqualsLiteral("width")) {
+ equivTable = widthEquivTable;
+ } else if (aAttribute->EqualsLiteral("height") ||
+ (aElement->IsHTMLElement(nsGkAtoms::hr) &&
+ aAttribute->EqualsLiteral("size"))) {
+ equivTable = heightEquivTable;
+ } else if (aAttribute->EqualsLiteral("type") &&
+ aElement->IsAnyOfHTMLElements(nsGkAtoms::ol,
+ nsGkAtoms::ul,
+ nsGkAtoms::li)) {
+ equivTable = listStyleTypeEquivTable;
+ }
+ }
+ if (equivTable) {
+ BuildCSSDeclarations(cssPropertyArray, cssValueArray, equivTable,
+ aValue, aGetOrRemoveRequest);
+ }
+}
+
+// Add to aNode the CSS inline style equivalent to HTMLProperty/aAttribute/
+// aValue for the node, and return in aCount the number of CSS properties set
+// by the call. The Element version returns aCount instead.
+int32_t
+CSSEditUtils::SetCSSEquivalentToHTMLStyle(Element* aElement,
+ nsIAtom* aProperty,
+ const nsAString* aAttribute,
+ const nsAString* aValue,
+ bool aSuppressTransaction)
+{
+ MOZ_ASSERT(aElement && aProperty);
+ MOZ_ASSERT_IF(aAttribute, aValue);
+ int32_t count;
+ // This can only fail if SetCSSProperty fails, which should only happen if
+ // something is pretty badly wrong. In this case we assert so that hopefully
+ // someone will notice, but there's nothing more sensible to do than just
+ // return the count and carry on.
+ nsresult rv = SetCSSEquivalentToHTMLStyle(aElement->AsDOMNode(),
+ aProperty, aAttribute,
+ aValue, &count,
+ aSuppressTransaction);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "SetCSSEquivalentToHTMLStyle failed");
+ NS_ENSURE_SUCCESS(rv, count);
+ return count;
+}
+
+nsresult
+CSSEditUtils::SetCSSEquivalentToHTMLStyle(nsIDOMNode* aNode,
+ nsIAtom* aHTMLProperty,
+ const nsAString* aAttribute,
+ const nsAString* aValue,
+ int32_t* aCount,
+ bool aSuppressTransaction)
+{
+ nsCOMPtr<Element> element = do_QueryInterface(aNode);
+ *aCount = 0;
+ if (!element || !IsCSSEditableProperty(element, aHTMLProperty, aAttribute)) {
+ return NS_OK;
+ }
+
+ // we can apply the styles only if the node is an element and if we have
+ // an equivalence for the requested HTML style in this implementation
+
+ // Find the CSS equivalence to the HTML style
+ nsTArray<nsIAtom*> cssPropertyArray;
+ nsTArray<nsString> cssValueArray;
+ GenerateCSSDeclarationsFromHTMLStyle(element, aHTMLProperty, aAttribute,
+ aValue, cssPropertyArray, cssValueArray,
+ false);
+
+ // set the individual CSS inline styles
+ *aCount = cssPropertyArray.Length();
+ for (int32_t index = 0; index < *aCount; index++) {
+ nsresult rv = SetCSSProperty(*element, *cssPropertyArray[index],
+ cssValueArray[index], aSuppressTransaction);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+// Remove from aNode the CSS inline style equivalent to HTMLProperty/aAttribute/aValue for the node
+nsresult
+CSSEditUtils::RemoveCSSEquivalentToHTMLStyle(nsIDOMNode* aNode,
+ nsIAtom* aHTMLProperty,
+ const nsAString* aAttribute,
+ const nsAString* aValue,
+ bool aSuppressTransaction)
+{
+ nsCOMPtr<Element> element = do_QueryInterface(aNode);
+ NS_ENSURE_TRUE(element, NS_OK);
+
+ return RemoveCSSEquivalentToHTMLStyle(element, aHTMLProperty, aAttribute,
+ aValue, aSuppressTransaction);
+}
+
+nsresult
+CSSEditUtils::RemoveCSSEquivalentToHTMLStyle(Element* aElement,
+ nsIAtom* aHTMLProperty,
+ const nsAString* aAttribute,
+ const nsAString* aValue,
+ bool aSuppressTransaction)
+{
+ MOZ_ASSERT(aElement);
+
+ if (!IsCSSEditableProperty(aElement, aHTMLProperty, aAttribute)) {
+ return NS_OK;
+ }
+
+ // we can apply the styles only if the node is an element and if we have
+ // an equivalence for the requested HTML style in this implementation
+
+ // Find the CSS equivalence to the HTML style
+ nsTArray<nsIAtom*> cssPropertyArray;
+ nsTArray<nsString> cssValueArray;
+ GenerateCSSDeclarationsFromHTMLStyle(aElement, aHTMLProperty, aAttribute,
+ aValue, cssPropertyArray, cssValueArray,
+ true);
+
+ // remove the individual CSS inline styles
+ int32_t count = cssPropertyArray.Length();
+ for (int32_t index = 0; index < count; index++) {
+ nsresult rv = RemoveCSSProperty(*aElement,
+ *cssPropertyArray[index],
+ cssValueArray[index],
+ aSuppressTransaction);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+// returns in aValueString the list of values for the CSS equivalences to
+// the HTML style aHTMLProperty/aAttribute/aValueString for the node aNode;
+// the value of aStyleType controls the styles we retrieve : specified or
+// computed.
+nsresult
+CSSEditUtils::GetCSSEquivalentToHTMLInlineStyleSet(nsINode* aNode,
+ nsIAtom* aHTMLProperty,
+ const nsAString* aAttribute,
+ nsAString& aValueString,
+ StyleType aStyleType)
+{
+ aValueString.Truncate();
+ nsCOMPtr<Element> theElement = GetElementContainerOrSelf(aNode);
+ NS_ENSURE_TRUE(theElement, NS_ERROR_NULL_POINTER);
+
+ if (!theElement || !IsCSSEditableProperty(theElement, aHTMLProperty, aAttribute)) {
+ return NS_OK;
+ }
+
+ // Yes, the requested HTML style has a CSS equivalence in this implementation
+ nsTArray<nsIAtom*> cssPropertyArray;
+ nsTArray<nsString> cssValueArray;
+ // get the CSS equivalence with last param true indicating we want only the
+ // "gettable" properties
+ GenerateCSSDeclarationsFromHTMLStyle(theElement, aHTMLProperty, aAttribute, nullptr,
+ cssPropertyArray, cssValueArray, true);
+ int32_t count = cssPropertyArray.Length();
+ for (int32_t index = 0; index < count; index++) {
+ nsAutoString valueString;
+ // retrieve the specified/computed value of the property
+ nsresult rv = GetCSSInlinePropertyBase(theElement, cssPropertyArray[index],
+ valueString, aStyleType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // append the value to aValueString (possibly with a leading whitespace)
+ if (index) {
+ aValueString.Append(char16_t(' '));
+ }
+ aValueString.Append(valueString);
+ }
+ return NS_OK;
+}
+
+// Does the node aNode (or its parent, if it's not an element node) have a CSS
+// style equivalent to the HTML style aHTMLProperty/aHTMLAttribute/valueString?
+// The value of aStyleType controls the styles we retrieve: specified or
+// computed. The return value aIsSet is true if the CSS styles are set.
+//
+// The nsIContent variant returns aIsSet instead of using an out parameter, and
+// does not modify aValue.
+bool
+CSSEditUtils::IsCSSEquivalentToHTMLInlineStyleSet(nsINode* aNode,
+ nsIAtom* aProperty,
+ const nsAString* aAttribute,
+ const nsAString& aValue,
+ StyleType aStyleType)
+{
+ // Use aValue as only an in param, not in-out
+ nsAutoString value(aValue);
+ return IsCSSEquivalentToHTMLInlineStyleSet(aNode, aProperty, aAttribute,
+ value, aStyleType);
+}
+
+bool
+CSSEditUtils::IsCSSEquivalentToHTMLInlineStyleSet(nsINode* aNode,
+ nsIAtom* aProperty,
+ const nsAString* aAttribute,
+ nsAString& aValue,
+ StyleType aStyleType)
+{
+ MOZ_ASSERT(aNode && aProperty);
+ bool isSet;
+ nsresult rv = IsCSSEquivalentToHTMLInlineStyleSet(aNode->AsDOMNode(),
+ aProperty, aAttribute,
+ isSet, aValue, aStyleType);
+ NS_ENSURE_SUCCESS(rv, false);
+ return isSet;
+}
+
+nsresult
+CSSEditUtils::IsCSSEquivalentToHTMLInlineStyleSet(
+ nsIDOMNode* aNode,
+ nsIAtom* aHTMLProperty,
+ const nsAString* aHTMLAttribute,
+ bool& aIsSet,
+ nsAString& valueString,
+ StyleType aStyleType)
+{
+ NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER);
+
+ nsAutoString htmlValueString(valueString);
+ aIsSet = false;
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ do {
+ valueString.Assign(htmlValueString);
+ // get the value of the CSS equivalent styles
+ nsresult rv =
+ GetCSSEquivalentToHTMLInlineStyleSet(node, aHTMLProperty, aHTMLAttribute,
+ valueString, aStyleType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // early way out if we can
+ if (valueString.IsEmpty()) {
+ return NS_OK;
+ }
+
+ if (nsGkAtoms::b == aHTMLProperty) {
+ if (valueString.EqualsLiteral("bold")) {
+ aIsSet = true;
+ } else if (valueString.EqualsLiteral("normal")) {
+ aIsSet = false;
+ } else if (valueString.EqualsLiteral("bolder")) {
+ aIsSet = true;
+ valueString.AssignLiteral("bold");
+ } else {
+ int32_t weight = 0;
+ nsresult errorCode;
+ nsAutoString value(valueString);
+ weight = value.ToInteger(&errorCode);
+ if (400 < weight) {
+ aIsSet = true;
+ valueString.AssignLiteral("bold");
+ } else {
+ aIsSet = false;
+ valueString.AssignLiteral("normal");
+ }
+ }
+ } else if (nsGkAtoms::i == aHTMLProperty) {
+ if (valueString.EqualsLiteral("italic") ||
+ valueString.EqualsLiteral("oblique")) {
+ aIsSet = true;
+ }
+ } else if (nsGkAtoms::u == aHTMLProperty) {
+ nsAutoString val;
+ val.AssignLiteral("underline");
+ aIsSet = ChangeStyleTransaction::ValueIncludes(valueString, val);
+ } else if (nsGkAtoms::strike == aHTMLProperty) {
+ nsAutoString val;
+ val.AssignLiteral("line-through");
+ aIsSet = ChangeStyleTransaction::ValueIncludes(valueString, val);
+ } else if (aHTMLAttribute &&
+ ((nsGkAtoms::font == aHTMLProperty &&
+ aHTMLAttribute->EqualsLiteral("color")) ||
+ aHTMLAttribute->EqualsLiteral("bgcolor"))) {
+ if (htmlValueString.IsEmpty()) {
+ aIsSet = true;
+ } else {
+ nscolor rgba;
+ nsAutoString subStr;
+ htmlValueString.Right(subStr, htmlValueString.Length() - 1);
+ if (NS_ColorNameToRGB(htmlValueString, &rgba) ||
+ NS_HexToRGBA(subStr, nsHexColorType::NoAlpha, &rgba)) {
+ nsAutoString htmlColor, tmpStr;
+
+ if (NS_GET_A(rgba) != 255) {
+ // This should only be hit by the "transparent" keyword, which
+ // currently serializes to "transparent" (not "rgba(0, 0, 0, 0)").
+ MOZ_ASSERT(NS_GET_R(rgba) == 0 && NS_GET_G(rgba) == 0 &&
+ NS_GET_B(rgba) == 0 && NS_GET_A(rgba) == 0);
+ htmlColor.AppendLiteral("transparent");
+ } else {
+ htmlColor.AppendLiteral("rgb(");
+
+ NS_NAMED_LITERAL_STRING(comma, ", ");
+
+ tmpStr.AppendInt(NS_GET_R(rgba), 10);
+ htmlColor.Append(tmpStr + comma);
+
+ tmpStr.Truncate();
+ tmpStr.AppendInt(NS_GET_G(rgba), 10);
+ htmlColor.Append(tmpStr + comma);
+
+ tmpStr.Truncate();
+ tmpStr.AppendInt(NS_GET_B(rgba), 10);
+ htmlColor.Append(tmpStr);
+
+ htmlColor.Append(char16_t(')'));
+ }
+
+ aIsSet = htmlColor.Equals(valueString,
+ nsCaseInsensitiveStringComparator());
+ } else {
+ aIsSet = htmlValueString.Equals(valueString,
+ nsCaseInsensitiveStringComparator());
+ }
+ }
+ } else if (nsGkAtoms::tt == aHTMLProperty) {
+ aIsSet = StringBeginsWith(valueString, NS_LITERAL_STRING("monospace"));
+ } else if (nsGkAtoms::font == aHTMLProperty && aHTMLAttribute &&
+ aHTMLAttribute->EqualsLiteral("face")) {
+ if (!htmlValueString.IsEmpty()) {
+ const char16_t commaSpace[] = { char16_t(','), char16_t(' '), 0 };
+ const char16_t comma[] = { char16_t(','), 0 };
+ htmlValueString.ReplaceSubstring(commaSpace, comma);
+ nsAutoString valueStringNorm(valueString);
+ valueStringNorm.ReplaceSubstring(commaSpace, comma);
+ aIsSet = htmlValueString.Equals(valueStringNorm,
+ nsCaseInsensitiveStringComparator());
+ } else {
+ aIsSet = true;
+ }
+ return NS_OK;
+ } else if (aHTMLAttribute && aHTMLAttribute->EqualsLiteral("align")) {
+ aIsSet = true;
+ } else {
+ aIsSet = false;
+ return NS_OK;
+ }
+
+ if (!htmlValueString.IsEmpty() &&
+ htmlValueString.Equals(valueString,
+ nsCaseInsensitiveStringComparator())) {
+ aIsSet = true;
+ }
+
+ if (htmlValueString.EqualsLiteral("-moz-editor-invert-value")) {
+ aIsSet = !aIsSet;
+ }
+
+ if (nsGkAtoms::u == aHTMLProperty || nsGkAtoms::strike == aHTMLProperty) {
+ // unfortunately, the value of the text-decoration property is not inherited.
+ // that means that we have to look at ancestors of node to see if they are underlined
+ node = node->GetParentElement(); // set to null if it's not a dom element
+ }
+ } while ((nsGkAtoms::u == aHTMLProperty ||
+ nsGkAtoms::strike == aHTMLProperty) && !aIsSet && node);
+ return NS_OK;
+}
+
+void
+CSSEditUtils::SetCSSEnabled(bool aIsCSSPrefChecked)
+{
+ mIsCSSPrefChecked = aIsCSSPrefChecked;
+}
+
+bool
+CSSEditUtils::IsCSSPrefChecked()
+{
+ return mIsCSSPrefChecked ;
+}
+
+// ElementsSameStyle compares two elements and checks if they have the same
+// specified CSS declarations in the STYLE attribute
+// The answer is always negative if at least one of them carries an ID or a class
+bool
+CSSEditUtils::ElementsSameStyle(nsIDOMNode* aFirstNode,
+ nsIDOMNode* aSecondNode)
+{
+ nsCOMPtr<Element> firstElement = do_QueryInterface(aFirstNode);
+ nsCOMPtr<Element> secondElement = do_QueryInterface(aSecondNode);
+
+ NS_ASSERTION((firstElement && secondElement), "Non element nodes passed to ElementsSameStyle.");
+ NS_ENSURE_TRUE(firstElement, false);
+ NS_ENSURE_TRUE(secondElement, false);
+
+ return ElementsSameStyle(firstElement, secondElement);
+}
+
+bool
+CSSEditUtils::ElementsSameStyle(Element* aFirstElement,
+ Element* aSecondElement)
+{
+ MOZ_ASSERT(aFirstElement);
+ MOZ_ASSERT(aSecondElement);
+
+ if (aFirstElement->HasAttr(kNameSpaceID_None, nsGkAtoms::id) ||
+ aSecondElement->HasAttr(kNameSpaceID_None, nsGkAtoms::id)) {
+ // at least one of the spans carries an ID ; suspect a CSS rule applies to it and
+ // refuse to merge the nodes
+ return false;
+ }
+
+ nsAutoString firstClass, secondClass;
+ bool isFirstClassSet = aFirstElement->GetAttr(kNameSpaceID_None, nsGkAtoms::_class, firstClass);
+ bool isSecondClassSet = aSecondElement->GetAttr(kNameSpaceID_None, nsGkAtoms::_class, secondClass);
+ if (isFirstClassSet && isSecondClassSet) {
+ // both spans carry a class, let's compare them
+ if (!firstClass.Equals(secondClass)) {
+ // WARNING : technically, the comparison just above is questionable :
+ // from a pure HTML/CSS point of view class="a b" is NOT the same than
+ // class="b a" because a CSS rule could test the exact value of the class
+ // attribute to be "a b" for instance ; from a user's point of view, a
+ // wysiwyg editor should probably NOT make any difference. CSS people
+ // need to discuss this issue before any modification.
+ return false;
+ }
+ } else if (isFirstClassSet || isSecondClassSet) {
+ // one span only carries a class, early way out
+ return false;
+ }
+
+ nsCOMPtr<nsIDOMCSSStyleDeclaration> firstCSSDecl, secondCSSDecl;
+ uint32_t firstLength, secondLength;
+ nsresult rv = GetInlineStyles(aFirstElement, getter_AddRefs(firstCSSDecl), &firstLength);
+ if (NS_FAILED(rv) || !firstCSSDecl) {
+ return false;
+ }
+ rv = GetInlineStyles(aSecondElement, getter_AddRefs(secondCSSDecl), &secondLength);
+ if (NS_FAILED(rv) || !secondCSSDecl) {
+ return false;
+ }
+
+ if (firstLength != secondLength) {
+ // early way out if we can
+ return false;
+ }
+
+ if (!firstLength) {
+ // no inline style !
+ return true;
+ }
+
+ nsAutoString propertyNameString;
+ nsAutoString firstValue, secondValue;
+ for (uint32_t i = 0; i < firstLength; i++) {
+ firstCSSDecl->Item(i, propertyNameString);
+ firstCSSDecl->GetPropertyValue(propertyNameString, firstValue);
+ secondCSSDecl->GetPropertyValue(propertyNameString, secondValue);
+ if (!firstValue.Equals(secondValue)) {
+ return false;
+ }
+ }
+ for (uint32_t i = 0; i < secondLength; i++) {
+ secondCSSDecl->Item(i, propertyNameString);
+ secondCSSDecl->GetPropertyValue(propertyNameString, secondValue);
+ firstCSSDecl->GetPropertyValue(propertyNameString, firstValue);
+ if (!firstValue.Equals(secondValue)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+nsresult
+CSSEditUtils::GetInlineStyles(Element* aElement,
+ nsIDOMCSSStyleDeclaration** aCssDecl,
+ uint32_t* aLength)
+{
+ return GetInlineStyles(static_cast<nsISupports*>(aElement), aCssDecl, aLength);
+}
+
+nsresult
+CSSEditUtils::GetInlineStyles(nsIDOMElement* aElement,
+ nsIDOMCSSStyleDeclaration** aCssDecl,
+ uint32_t* aLength)
+{
+ return GetInlineStyles(static_cast<nsISupports*>(aElement), aCssDecl, aLength);
+}
+
+nsresult
+CSSEditUtils::GetInlineStyles(nsISupports* aElement,
+ nsIDOMCSSStyleDeclaration** aCssDecl,
+ uint32_t* aLength)
+{
+ NS_ENSURE_TRUE(aElement && aLength, NS_ERROR_NULL_POINTER);
+ *aLength = 0;
+ nsCOMPtr<nsStyledElement> inlineStyles = do_QueryInterface(aElement);
+ NS_ENSURE_TRUE(inlineStyles, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsIDOMCSSStyleDeclaration> cssDecl =
+ do_QueryInterface(inlineStyles->Style());
+ MOZ_ASSERT(cssDecl);
+
+ cssDecl.forget(aCssDecl);
+ (*aCssDecl)->GetLength(aLength);
+ return NS_OK;
+}
+
+already_AddRefed<nsIDOMElement>
+CSSEditUtils::GetElementContainerOrSelf(nsIDOMNode* aNode)
+{
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ NS_ENSURE_TRUE(node, nullptr);
+ nsCOMPtr<nsIDOMElement> element =
+ do_QueryInterface(GetElementContainerOrSelf(node));
+ return element.forget();
+}
+
+Element*
+CSSEditUtils::GetElementContainerOrSelf(nsINode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ if (nsIDOMNode::DOCUMENT_NODE == aNode->NodeType()) {
+ return nullptr;
+ }
+
+ nsINode* node = aNode;
+ // Loop until we find an element.
+ while (node && !node->IsElement()) {
+ node = node->GetParentNode();
+ }
+
+ NS_ENSURE_TRUE(node, nullptr);
+ return node->AsElement();
+}
+
+nsresult
+CSSEditUtils::SetCSSProperty(nsIDOMElement* aElement,
+ const nsAString& aProperty,
+ const nsAString& aValue)
+{
+ nsCOMPtr<nsIDOMCSSStyleDeclaration> cssDecl;
+ uint32_t length;
+ nsresult rv = GetInlineStyles(aElement, getter_AddRefs(cssDecl), &length);
+ if (NS_FAILED(rv) || !cssDecl) {
+ return rv;
+ }
+
+ return cssDecl->SetProperty(aProperty,
+ aValue,
+ EmptyString());
+}
+
+nsresult
+CSSEditUtils::SetCSSPropertyPixels(nsIDOMElement* aElement,
+ const nsAString& aProperty,
+ int32_t aIntValue)
+{
+ nsAutoString s;
+ s.AppendInt(aIntValue);
+ return SetCSSProperty(aElement, aProperty, s + NS_LITERAL_STRING("px"));
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/CSSEditUtils.h b/editor/libeditor/CSSEditUtils.h
new file mode 100644
index 000000000..0b9a12952
--- /dev/null
+++ b/editor/libeditor/CSSEditUtils.h
@@ -0,0 +1,473 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_CSSEditUtils_h
+#define mozilla_CSSEditUtils_h
+
+#include "mozilla/ChangeStyleTransaction.h" // for ChangeStyleTransaction
+#include "nsCOMPtr.h" // for already_AddRefed
+#include "nsTArray.h" // for nsTArray
+#include "nscore.h" // for nsAString, nsresult, nullptr
+
+class nsComputedDOMStyle;
+class nsIAtom;
+class nsIContent;
+class nsIDOMCSSStyleDeclaration;
+class nsIDOMElement;
+class nsIDOMNode;
+class nsINode;
+class nsString;
+
+namespace mozilla {
+
+class HTMLEditor;
+namespace dom {
+class Element;
+} // namespace dom
+
+typedef void (*nsProcessValueFunc)(const nsAString* aInputString,
+ nsAString& aOutputString,
+ const char* aDefaultValueString,
+ const char* aPrependString,
+ const char* aAppendString);
+
+class CSSEditUtils final
+{
+public:
+ explicit CSSEditUtils(HTMLEditor* aEditor);
+ ~CSSEditUtils();
+
+ enum nsCSSEditableProperty
+ {
+ eCSSEditableProperty_NONE=0,
+ eCSSEditableProperty_background_color,
+ eCSSEditableProperty_background_image,
+ eCSSEditableProperty_border,
+ eCSSEditableProperty_caption_side,
+ eCSSEditableProperty_color,
+ eCSSEditableProperty_float,
+ eCSSEditableProperty_font_family,
+ eCSSEditableProperty_font_size,
+ eCSSEditableProperty_font_style,
+ eCSSEditableProperty_font_weight,
+ eCSSEditableProperty_height,
+ eCSSEditableProperty_list_style_type,
+ eCSSEditableProperty_margin_left,
+ eCSSEditableProperty_margin_right,
+ eCSSEditableProperty_text_align,
+ eCSSEditableProperty_text_decoration,
+ eCSSEditableProperty_vertical_align,
+ eCSSEditableProperty_whitespace,
+ eCSSEditableProperty_width
+ };
+
+ enum StyleType { eSpecified, eComputed };
+
+
+ struct CSSEquivTable
+ {
+ nsCSSEditableProperty cssProperty;
+ nsProcessValueFunc processValueFunctor;
+ const char* defaultValue;
+ const char* prependValue;
+ const char* appendValue;
+ bool gettable;
+ bool caseSensitiveValue;
+ };
+
+ /**
+ * Answers true if the given combination element_name/attribute_name
+ * has a CSS equivalence in this implementation.
+ *
+ * @param aNode [IN] A DOM node.
+ * @param aProperty [IN] An atom containing a HTML tag name.
+ * @param aAttribute [IN] A string containing the name of a HTML
+ * attribute carried by the element above.
+ * @return A boolean saying if the tag/attribute has a CSS
+ * equiv.
+ */
+ bool IsCSSEditableProperty(nsINode* aNode, nsIAtom* aProperty,
+ const nsAString* aAttribute);
+
+ /**
+ * Adds/remove a CSS declaration to the STYLE atrribute carried by a given
+ * element.
+ *
+ * @param aElement [IN] A DOM element.
+ * @param aProperty [IN] An atom containing the CSS property to set.
+ * @param aValue [IN] A string containing the value of the CSS
+ * property.
+ * @param aSuppressTransaction [IN] A boolean indicating, when true,
+ * that no transaction should be recorded.
+ */
+ nsresult SetCSSProperty(dom::Element& aElement, nsIAtom& aProperty,
+ const nsAString& aValue, bool aSuppressTxn = false);
+ nsresult SetCSSPropertyPixels(dom::Element& aElement,
+ nsIAtom& aProperty, int32_t aIntValue);
+ nsresult RemoveCSSProperty(dom::Element& aElement,
+ nsIAtom& aProperty,
+ const nsAString& aPropertyValue,
+ bool aSuppressTxn = false);
+
+ /**
+ * Directly adds/remove a CSS declaration to the STYLE atrribute carried by
+ * a given element without going through the transaction manager.
+ *
+ * @param aElement [IN] A DOM element.
+ * @param aProperty [IN] A string containing the CSS property to
+ * set/remove.
+ * @param aValue [IN] A string containing the new value of the CSS
+ * property.
+ */
+ nsresult SetCSSProperty(nsIDOMElement* aElement,
+ const nsAString& aProperty,
+ const nsAString& aValue);
+ nsresult SetCSSPropertyPixels(nsIDOMElement* aElement,
+ const nsAString& aProperty,
+ int32_t aIntValue);
+
+ /**
+ * Gets the specified/computed style value of a CSS property for a given
+ * node (or its element ancestor if it is not an element).
+ *
+ * @param aNode [IN] A DOM node.
+ * @param aProperty [IN] An atom containing the CSS property to get.
+ * @param aPropertyValue [OUT] The retrieved value of the property.
+ */
+ nsresult GetSpecifiedProperty(nsINode& aNode, nsIAtom& aProperty,
+ nsAString& aValue);
+ nsresult GetComputedProperty(nsINode& aNode, nsIAtom& aProperty,
+ nsAString& aValue);
+
+ /**
+ * Removes a CSS property from the specified declarations in STYLE attribute
+ * and removes the node if it is an useless span.
+ *
+ * @param aNode [IN] The specific node we want to remove a style
+ * from.
+ * @param aProperty [IN] The CSS property atom to remove.
+ * @param aPropertyValue [IN] The value of the property we have to remove
+ * if the property accepts more than one value.
+ */
+ nsresult RemoveCSSInlineStyle(nsIDOMNode* aNode, nsIAtom* aProperty,
+ const nsAString& aPropertyValue);
+
+ /**
+ * Answers true is the property can be removed by setting a "none" CSS value
+ * on a node.
+ *
+ * @param aProperty [IN] An atom containing a CSS property.
+ * @param aAttribute [IN] Pointer to an attribute name or null if this
+ * information is irrelevant.
+ * @return A boolean saying if the property can be remove by
+ * setting a "none" value.
+ */
+ bool IsCSSInvertible(nsIAtom& aProperty, const nsAString* aAttribute);
+
+ /**
+ * Get the default browser background color if we need it for
+ * GetCSSBackgroundColorState().
+ *
+ * @param aColor [OUT] The default color as it is defined in prefs.
+ */
+ void GetDefaultBackgroundColor(nsAString& aColor);
+
+ /**
+ * Get the default length unit used for CSS Indent/Outdent.
+ *
+ * @param aLengthUnit [OUT] The default length unit as it is defined in
+ * prefs.
+ */
+ void GetDefaultLengthUnit(nsAString & aLengthUnit);
+
+ /**
+ * Returns the list of values for the CSS equivalences to
+ * the passed HTML style for the passed node.
+ *
+ * @param aNode [IN] A DOM node.
+ * @param aHTMLProperty [IN] An atom containing an HTML property.
+ * @param aAttribute [IN] A pointer to an attribute name or nullptr if
+ * irrelevant.
+ * @param aValueString [OUT] The list of CSS values.
+ * @param aStyleType [IN] eSpecified or eComputed.
+ */
+ nsresult GetCSSEquivalentToHTMLInlineStyleSet(nsINode* aNode,
+ nsIAtom* aHTMLProperty,
+ const nsAString* aAttribute,
+ nsAString& aValueString,
+ StyleType aStyleType);
+
+ /**
+ * Does the node aNode (or his parent if it is not an element node) carries
+ * the CSS equivalent styles to the HTML style for this node ?
+ *
+ * @param aNode [IN] A DOM node.
+ * @param aHTMLProperty [IN] An atom containing an HTML property.
+ * @param aAttribute [IN] A pointer to an attribute name or nullptr if
+ * irrelevant.
+ * @param aIsSet [OUT] A boolean being true if the css properties are
+ * set.
+ * @param aValueString [IN/OUT] The attribute value (in) the list of CSS
+ * values (out).
+ * @param aStyleType [IN] eSpecified or eComputed.
+ *
+ * The nsIContent variant returns aIsSet instead of using an out parameter.
+ */
+ bool IsCSSEquivalentToHTMLInlineStyleSet(nsINode* aContent,
+ nsIAtom* aProperty,
+ const nsAString* aAttribute,
+ const nsAString& aValue,
+ StyleType aStyleType);
+
+ bool IsCSSEquivalentToHTMLInlineStyleSet(nsINode* aContent,
+ nsIAtom* aProperty,
+ const nsAString* aAttribute,
+ nsAString& aValue,
+ StyleType aStyleType);
+
+ nsresult IsCSSEquivalentToHTMLInlineStyleSet(nsIDOMNode* aNode,
+ nsIAtom* aHTMLProperty,
+ const nsAString* aAttribute,
+ bool& aIsSet,
+ nsAString& aValueString,
+ StyleType aStyleType);
+
+ /**
+ * Adds to the node the CSS inline styles equivalent to the HTML style
+ * and return the number of CSS properties set by the call.
+ *
+ * @param aNode [IN] A DOM node.
+ * @param aHTMLProperty [IN] An atom containing an HTML property.
+ * @param aAttribute [IN] A pointer to an attribute name or nullptr if
+ * irrelevant.
+ * @param aValue [IN] The attribute value.
+ * @param aCount [OUT] The number of CSS properties set by the call.
+ * @param aSuppressTransaction [IN] A boolean indicating, when true,
+ * that no transaction should be recorded.
+ *
+ * aCount is returned by the dom::Element variant instead of being an out
+ * parameter.
+ */
+ int32_t SetCSSEquivalentToHTMLStyle(dom::Element* aElement,
+ nsIAtom* aProperty,
+ const nsAString* aAttribute,
+ const nsAString* aValue,
+ bool aSuppressTransaction);
+ nsresult SetCSSEquivalentToHTMLStyle(nsIDOMNode* aNode,
+ nsIAtom* aHTMLProperty,
+ const nsAString* aAttribute,
+ const nsAString* aValue,
+ int32_t* aCount,
+ bool aSuppressTransaction);
+
+ /**
+ * Removes from the node the CSS inline styles equivalent to the HTML style.
+ *
+ * @param aNode [IN] A DOM node.
+ * @param aHTMLProperty [IN] An atom containing an HTML property.
+ * @param aAttribute [IN] A pointer to an attribute name or nullptr if
+ * irrelevant.
+ * @param aValue [IN] The attribute value.
+ * @param aSuppressTransaction [IN] A boolean indicating, when true,
+ * that no transaction should be recorded.
+ */
+ nsresult RemoveCSSEquivalentToHTMLStyle(nsIDOMNode* aNode,
+ nsIAtom* aHTMLProperty,
+ const nsAString* aAttribute,
+ const nsAString* aValue,
+ bool aSuppressTransaction);
+
+ /**
+ * Removes from the node the CSS inline styles equivalent to the HTML style.
+ *
+ * @param aElement [IN] A DOM Element (must not be null).
+ * @param aHTMLProperty [IN] An atom containing an HTML property.
+ * @param aAttribute [IN] A pointer to an attribute name or nullptr if
+ * irrelevant.
+ * @param aValue [IN] The attribute value.
+ * @param aSuppressTransaction [IN] A boolean indicating, when true,
+ * that no transaction should be recorded.
+ */
+ nsresult RemoveCSSEquivalentToHTMLStyle(dom::Element* aElement,
+ nsIAtom* aHTMLProperty,
+ const nsAString* aAttribute,
+ const nsAString* aValue,
+ bool aSuppressTransaction);
+
+ /**
+ * Parses a "xxxx.xxxxxuuu" string where x is a digit and u an alpha char
+ * we need such a parser because
+ * nsIDOMCSSStyleDeclaration::GetPropertyCSSValue() is not implemented.
+ *
+ * @param aString [IN] Input string to parse.
+ * @param aValue [OUT] Numeric part.
+ * @param aUnit [OUT] Unit part.
+ */
+ void ParseLength(const nsAString& aString, float* aValue, nsIAtom** aUnit);
+
+ /**
+ * Sets the mIsCSSPrefChecked private member; used as callback from observer
+ * when the CSS pref state is changed.
+ *
+ * @param aIsCSSPrefChecked [IN] The new boolean state for the pref.
+ */
+ void SetCSSEnabled(bool aIsCSSPrefChecked);
+
+ /**
+ * Retrieves the mIsCSSPrefChecked private member, true if the CSS pref is
+ * checked, false if it is not.
+ *
+ * @return the boolean value of the CSS pref.
+ */
+ bool IsCSSPrefChecked();
+
+ /**
+ * ElementsSameStyle compares two elements and checks if they have the same
+ * specified CSS declarations in the STYLE attribute.
+ * The answer is always false if at least one of them carries an ID or a
+ * class.
+ *
+ * @param aFirstNode [IN] A DOM node.
+ * @param aSecondNode [IN] A DOM node.
+ * @return true if the two elements are considered to
+ * have same styles.
+ */
+ bool ElementsSameStyle(dom::Element* aFirstNode,
+ dom::Element* aSecondNode);
+ bool ElementsSameStyle(nsIDOMNode* aFirstNode, nsIDOMNode* aSecondNode);
+
+ /**
+ * Get the specified inline styles (style attribute) for an element.
+ *
+ * @param aElement [IN] The element node.
+ * @param aCssDecl [OUT] The CSS declaration corresponding to the
+ * style attribute.
+ * @param aLength [OUT] The number of declarations in aCssDecl.
+ */
+ nsresult GetInlineStyles(dom::Element* aElement,
+ nsIDOMCSSStyleDeclaration** aCssDecl,
+ uint32_t* aLength);
+ nsresult GetInlineStyles(nsIDOMElement* aElement,
+ nsIDOMCSSStyleDeclaration** aCssDecl,
+ uint32_t* aLength);
+private:
+ nsresult GetInlineStyles(nsISupports* aElement,
+ nsIDOMCSSStyleDeclaration** aCssDecl,
+ uint32_t* aLength);
+
+public:
+ /**
+ * Returns aNode itself if it is an element node, or the first ancestors
+ * being an element node if aNode is not one itself.
+ *
+ * @param aNode [IN] A node
+ * @param aElement [OUT] The deepest element node containing aNode
+ * (possibly aNode itself)
+ */
+ dom::Element* GetElementContainerOrSelf(nsINode* aNode);
+ already_AddRefed<nsIDOMElement> GetElementContainerOrSelf(nsIDOMNode* aNode);
+
+ /**
+ * Gets the computed style for a given element. Can return null.
+ */
+ already_AddRefed<nsComputedDOMStyle> GetComputedStyle(dom::Element* aElement);
+
+private:
+ /**
+ * Retrieves the CSS property atom from an enum.
+ *
+ * @param aProperty [IN] The enum value for the property.
+ * @param aAtom [OUT] The corresponding atom.
+ */
+ void GetCSSPropertyAtom(nsCSSEditableProperty aProperty, nsIAtom** aAtom);
+
+ /**
+ * Retrieves the CSS declarations equivalent to a HTML style value for
+ * a given equivalence table.
+ *
+ * @param aPropertyArray [OUT] The array of css properties.
+ * @param aValueArray [OUT] The array of values for the CSS properties
+ * above.
+ * @param aEquivTable [IN] The equivalence table.
+ * @param aValue [IN] The HTML style value.
+ * @param aGetOrRemoveRequest [IN] A boolean value being true if the call to
+ * the current method is made for
+ * GetCSSEquivalentToHTMLInlineStyleSet() or
+ * RemoveCSSEquivalentToHTMLInlineStyleSet().
+ */
+ void BuildCSSDeclarations(nsTArray<nsIAtom*>& aPropertyArray,
+ nsTArray<nsString>& cssValueArray,
+ const CSSEquivTable* aEquivTable,
+ const nsAString* aValue,
+ bool aGetOrRemoveRequest);
+
+ /**
+ * Retrieves the CSS declarations equivalent to the given HTML
+ * property/attribute/value for a given node.
+ *
+ * @param aNode [IN] The DOM node.
+ * @param aHTMLProperty [IN] An atom containing an HTML property.
+ * @param aAttribute [IN] A pointer to an attribute name or nullptr
+ * if irrelevant
+ * @param aValue [IN] The attribute value.
+ * @param aPropertyArray [OUT] The array of CSS properties.
+ * @param aValueArray [OUT] The array of values for the CSS properties
+ * above.
+ * @param aGetOrRemoveRequest [IN] A boolean value being true if the call to
+ * the current method is made for
+ * GetCSSEquivalentToHTMLInlineStyleSet() or
+ * RemoveCSSEquivalentToHTMLInlineStyleSet().
+ */
+ void GenerateCSSDeclarationsFromHTMLStyle(dom::Element* aNode,
+ nsIAtom* aHTMLProperty,
+ const nsAString* aAttribute,
+ const nsAString* aValue,
+ nsTArray<nsIAtom*>& aPropertyArray,
+ nsTArray<nsString>& aValueArray,
+ bool aGetOrRemoveRequest);
+
+ /**
+ * Creates a Transaction for setting or removing a CSS property. Never
+ * returns null.
+ *
+ * @param aElement [IN] A DOM element.
+ * @param aProperty [IN] A CSS property.
+ * @param aValue [IN] The value to set for this CSS property.
+ * @param aChangeType [IN] eSet to set, eRemove to remove.
+ */
+ already_AddRefed<ChangeStyleTransaction>
+ CreateCSSPropertyTxn(dom::Element& aElement,
+ nsIAtom& aProperty, const nsAString& aValue,
+ ChangeStyleTransaction::EChangeType aChangeType);
+
+ /**
+ * Back-end for GetSpecifiedProperty and GetComputedProperty.
+ *
+ * @param aNode [IN] A DOM node.
+ * @param aProperty [IN] A CSS property.
+ * @param aValue [OUT] The retrieved value for this property.
+ * @param aStyleType [IN] eSpecified or eComputed.
+ */
+ nsresult GetCSSInlinePropertyBase(nsINode* aNode, nsIAtom* aProperty,
+ nsAString& aValue, StyleType aStyleType);
+
+private:
+ HTMLEditor* mHTMLEditor;
+ bool mIsCSSPrefChecked;
+};
+
+#define NS_EDITOR_INDENT_INCREMENT_IN 0.4134f
+#define NS_EDITOR_INDENT_INCREMENT_CM 1.05f
+#define NS_EDITOR_INDENT_INCREMENT_MM 10.5f
+#define NS_EDITOR_INDENT_INCREMENT_PT 29.76f
+#define NS_EDITOR_INDENT_INCREMENT_PC 2.48f
+#define NS_EDITOR_INDENT_INCREMENT_EM 3
+#define NS_EDITOR_INDENT_INCREMENT_EX 6
+#define NS_EDITOR_INDENT_INCREMENT_PX 40
+#define NS_EDITOR_INDENT_INCREMENT_PERCENT 4
+
+} // namespace mozilla
+
+#endif // #ifndef mozilla_CSSEditUtils_h
diff --git a/editor/libeditor/ChangeAttributeTransaction.cpp b/editor/libeditor/ChangeAttributeTransaction.cpp
new file mode 100644
index 000000000..04f539856
--- /dev/null
+++ b/editor/libeditor/ChangeAttributeTransaction.cpp
@@ -0,0 +1,98 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ChangeAttributeTransaction.h"
+
+#include "mozilla/dom/Element.h" // for Element
+
+#include "nsAString.h"
+#include "nsError.h" // for NS_ERROR_NOT_INITIALIZED, etc.
+
+namespace mozilla {
+
+using namespace dom;
+
+ChangeAttributeTransaction::ChangeAttributeTransaction(Element& aElement,
+ nsIAtom& aAttribute,
+ const nsAString* aValue)
+ : EditTransactionBase()
+ , mElement(&aElement)
+ , mAttribute(&aAttribute)
+ , mValue(aValue ? *aValue : EmptyString())
+ , mRemoveAttribute(!aValue)
+ , mAttributeWasSet(false)
+ , mUndoValue()
+{
+}
+
+ChangeAttributeTransaction::~ChangeAttributeTransaction()
+{
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(ChangeAttributeTransaction,
+ EditTransactionBase,
+ mElement)
+
+NS_IMPL_ADDREF_INHERITED(ChangeAttributeTransaction, EditTransactionBase)
+NS_IMPL_RELEASE_INHERITED(ChangeAttributeTransaction, EditTransactionBase)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ChangeAttributeTransaction)
+NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
+
+NS_IMETHODIMP
+ChangeAttributeTransaction::DoTransaction()
+{
+ // Need to get the current value of the attribute and save it, and set
+ // mAttributeWasSet
+ mAttributeWasSet = mElement->GetAttr(kNameSpaceID_None, mAttribute,
+ mUndoValue);
+
+ // XXX: hack until attribute-was-set code is implemented
+ if (!mUndoValue.IsEmpty()) {
+ mAttributeWasSet = true;
+ }
+ // XXX: end hack
+
+ // Now set the attribute to the new value
+ if (mRemoveAttribute) {
+ return mElement->UnsetAttr(kNameSpaceID_None, mAttribute, true);
+ }
+
+ return mElement->SetAttr(kNameSpaceID_None, mAttribute, mValue, true);
+}
+
+NS_IMETHODIMP
+ChangeAttributeTransaction::UndoTransaction()
+{
+ if (mAttributeWasSet) {
+ return mElement->SetAttr(kNameSpaceID_None, mAttribute, mUndoValue, true);
+ }
+ return mElement->UnsetAttr(kNameSpaceID_None, mAttribute, true);
+}
+
+NS_IMETHODIMP
+ChangeAttributeTransaction::RedoTransaction()
+{
+ if (mRemoveAttribute) {
+ return mElement->UnsetAttr(kNameSpaceID_None, mAttribute, true);
+ }
+
+ return mElement->SetAttr(kNameSpaceID_None, mAttribute, mValue, true);
+}
+
+NS_IMETHODIMP
+ChangeAttributeTransaction::GetTxnDescription(nsAString& aString)
+{
+ aString.AssignLiteral("ChangeAttributeTransaction: [mRemoveAttribute == ");
+
+ if (mRemoveAttribute) {
+ aString.AppendLiteral("true] ");
+ } else {
+ aString.AppendLiteral("false] ");
+ }
+ aString += nsDependentAtomString(mAttribute);
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/ChangeAttributeTransaction.h b/editor/libeditor/ChangeAttributeTransaction.h
new file mode 100644
index 000000000..bb0c26c38
--- /dev/null
+++ b/editor/libeditor/ChangeAttributeTransaction.h
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ChangeAttributeTransaction_h
+#define ChangeAttributeTransaction_h
+
+#include "mozilla/Attributes.h" // override
+#include "mozilla/EditTransactionBase.h" // base class
+#include "nsCOMPtr.h" // nsCOMPtr members
+#include "nsCycleCollectionParticipant.h" // NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED
+#include "nsISupportsImpl.h" // NS_DECL_ISUPPORTS_INHERITED
+#include "nsString.h" // nsString members
+
+class nsIAtom;
+
+namespace mozilla {
+
+namespace dom {
+class Element;
+} // namespace dom
+
+/**
+ * A transaction that changes an attribute of a content node. This transaction
+ * covers add, remove, and change attribute.
+ */
+class ChangeAttributeTransaction final : public EditTransactionBase
+{
+public:
+ /**
+ * @param aElement the element whose attribute will be changed
+ * @param aAttribute the name of the attribute to change
+ * @param aValue the new value for aAttribute, or null to remove
+ */
+ ChangeAttributeTransaction(dom::Element& aElement,
+ nsIAtom& aAttribute,
+ const nsAString* aValue);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ChangeAttributeTransaction,
+ EditTransactionBase)
+
+ NS_DECL_EDITTRANSACTIONBASE
+
+ NS_IMETHOD RedoTransaction() override;
+
+private:
+ virtual ~ChangeAttributeTransaction();
+
+ // The element to operate upon
+ nsCOMPtr<dom::Element> mElement;
+
+ // The attribute to change
+ nsCOMPtr<nsIAtom> mAttribute;
+
+ // The value to set the attribute to (ignored if mRemoveAttribute==true)
+ nsString mValue;
+
+ // True if the operation is to remove mAttribute from mElement
+ bool mRemoveAttribute;
+
+ // True if the mAttribute was set on mElement at the time of execution
+ bool mAttributeWasSet;
+
+ // The value to set the attribute to for undo
+ nsString mUndoValue;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef ChangeAttributeTransaction_h
diff --git a/editor/libeditor/ChangeStyleTransaction.cpp b/editor/libeditor/ChangeStyleTransaction.cpp
new file mode 100644
index 000000000..103d9c455
--- /dev/null
+++ b/editor/libeditor/ChangeStyleTransaction.cpp
@@ -0,0 +1,287 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/ChangeStyleTransaction.h"
+
+#include "mozilla/dom/Element.h" // for Element
+#include "nsAString.h" // for nsAString_internal::Append, etc.
+#include "nsCRT.h" // for nsCRT::IsAsciiSpace
+#include "nsDebug.h" // for NS_ENSURE_SUCCESS, etc.
+#include "nsError.h" // for NS_ERROR_NULL_POINTER, etc.
+#include "nsGkAtoms.h" // for nsGkAtoms, etc.
+#include "nsICSSDeclaration.h" // for nsICSSDeclaration.
+#include "nsLiteralString.h" // for NS_LITERAL_STRING, etc.
+#include "nsReadableUtils.h" // for ToNewUnicode
+#include "nsString.h" // for nsAutoString, nsString, etc.
+#include "nsStyledElement.h" // for nsStyledElement.
+#include "nsUnicharUtils.h" // for nsCaseInsensitiveStringComparator
+
+namespace mozilla {
+
+using namespace dom;
+
+#define kNullCh (char16_t('\0'))
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(ChangeStyleTransaction, EditTransactionBase,
+ mElement)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ChangeStyleTransaction)
+NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
+
+NS_IMPL_ADDREF_INHERITED(ChangeStyleTransaction, EditTransactionBase)
+NS_IMPL_RELEASE_INHERITED(ChangeStyleTransaction, EditTransactionBase)
+
+ChangeStyleTransaction::~ChangeStyleTransaction()
+{
+}
+
+// Answers true if aValue is in the string list of white-space separated values
+// aValueList.
+bool
+ChangeStyleTransaction::ValueIncludes(const nsAString& aValueList,
+ const nsAString& aValue)
+{
+ nsAutoString valueList(aValueList);
+ bool result = false;
+
+ // put an extra null at the end
+ valueList.Append(kNullCh);
+
+ char16_t* value = ToNewUnicode(aValue);
+ char16_t* start = valueList.BeginWriting();
+ char16_t* end = start;
+
+ while (kNullCh != *start) {
+ while (kNullCh != *start && nsCRT::IsAsciiSpace(*start)) {
+ // skip leading space
+ start++;
+ }
+ end = start;
+
+ while (kNullCh != *end && !nsCRT::IsAsciiSpace(*end)) {
+ // look for space or end
+ end++;
+ }
+ // end string here
+ *end = kNullCh;
+
+ if (start < end) {
+ if (nsDependentString(value).Equals(nsDependentString(start),
+ nsCaseInsensitiveStringComparator())) {
+ result = true;
+ break;
+ }
+ }
+ start = ++end;
+ }
+ free(value);
+ return result;
+}
+
+// Removes the value aRemoveValue from the string list of white-space separated
+// values aValueList
+void
+ChangeStyleTransaction::RemoveValueFromListOfValues(
+ nsAString& aValues,
+ const nsAString& aRemoveValue)
+{
+ nsAutoString classStr(aValues);
+ nsAutoString outString;
+ // put an extra null at the end
+ classStr.Append(kNullCh);
+
+ char16_t* start = classStr.BeginWriting();
+ char16_t* end = start;
+
+ while (kNullCh != *start) {
+ while (kNullCh != *start && nsCRT::IsAsciiSpace(*start)) {
+ // skip leading space
+ start++;
+ }
+ end = start;
+
+ while (kNullCh != *end && !nsCRT::IsAsciiSpace(*end)) {
+ // look for space or end
+ end++;
+ }
+ // end string here
+ *end = kNullCh;
+
+ if (start < end && !aRemoveValue.Equals(start)) {
+ outString.Append(start);
+ outString.Append(char16_t(' '));
+ }
+
+ start = ++end;
+ }
+ aValues.Assign(outString);
+}
+
+ChangeStyleTransaction::ChangeStyleTransaction(Element& aElement,
+ nsIAtom& aProperty,
+ const nsAString& aValue,
+ EChangeType aChangeType)
+ : EditTransactionBase()
+ , mElement(&aElement)
+ , mProperty(&aProperty)
+ , mValue(aValue)
+ , mRemoveProperty(aChangeType == eRemove)
+ , mUndoValue()
+ , mRedoValue()
+ , mUndoAttributeWasSet(false)
+ , mRedoAttributeWasSet(false)
+{
+}
+
+NS_IMETHODIMP
+ChangeStyleTransaction::DoTransaction()
+{
+ nsCOMPtr<nsStyledElement> inlineStyles = do_QueryInterface(mElement);
+ NS_ENSURE_TRUE(inlineStyles, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsICSSDeclaration> cssDecl = inlineStyles->Style();
+
+ nsAutoString propertyNameString;
+ mProperty->ToString(propertyNameString);
+
+ mUndoAttributeWasSet = mElement->HasAttr(kNameSpaceID_None,
+ nsGkAtoms::style);
+
+ nsAutoString values;
+ nsresult rv = cssDecl->GetPropertyValue(propertyNameString, values);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mUndoValue.Assign(values);
+
+ // Does this property accept more than one value? (bug 62682)
+ bool multiple = AcceptsMoreThanOneValue(*mProperty);
+
+ if (mRemoveProperty) {
+ nsAutoString returnString;
+ if (multiple) {
+ // Let's remove only the value we have to remove and not the others
+
+ // The two lines below are a workaround because
+ // nsDOMCSSDeclaration::GetPropertyCSSValue is not yet implemented (bug
+ // 62682)
+ RemoveValueFromListOfValues(values, NS_LITERAL_STRING("none"));
+ RemoveValueFromListOfValues(values, mValue);
+ if (values.IsEmpty()) {
+ rv = cssDecl->RemoveProperty(propertyNameString, returnString);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ nsAutoString priority;
+ cssDecl->GetPropertyPriority(propertyNameString, priority);
+ rv = cssDecl->SetProperty(propertyNameString, values, priority);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ } else {
+ rv = cssDecl->RemoveProperty(propertyNameString, returnString);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ } else {
+ nsAutoString priority;
+ cssDecl->GetPropertyPriority(propertyNameString, priority);
+ if (multiple) {
+ // Let's add the value we have to add to the others
+
+ // The line below is a workaround because
+ // nsDOMCSSDeclaration::GetPropertyCSSValue is not yet implemented (bug
+ // 62682)
+ AddValueToMultivalueProperty(values, mValue);
+ } else {
+ values.Assign(mValue);
+ }
+ rv = cssDecl->SetProperty(propertyNameString, values, priority);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Let's be sure we don't keep an empty style attribute
+ uint32_t length;
+ rv = cssDecl->GetLength(&length);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!length) {
+ rv = mElement->UnsetAttr(kNameSpaceID_None, nsGkAtoms::style, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ mRedoAttributeWasSet = true;
+ }
+
+ return cssDecl->GetPropertyValue(propertyNameString, mRedoValue);
+}
+
+nsresult
+ChangeStyleTransaction::SetStyle(bool aAttributeWasSet,
+ nsAString& aValue)
+{
+ if (aAttributeWasSet) {
+ // The style attribute was not empty, let's recreate the declaration
+ nsAutoString propertyNameString;
+ mProperty->ToString(propertyNameString);
+
+ nsCOMPtr<nsStyledElement> inlineStyles = do_QueryInterface(mElement);
+ NS_ENSURE_TRUE(inlineStyles, NS_ERROR_NULL_POINTER);
+ nsCOMPtr<nsICSSDeclaration> cssDecl = inlineStyles->Style();
+
+ if (aValue.IsEmpty()) {
+ // An empty value means we have to remove the property
+ nsAutoString returnString;
+ return cssDecl->RemoveProperty(propertyNameString, returnString);
+ }
+ // Let's recreate the declaration as it was
+ nsAutoString priority;
+ cssDecl->GetPropertyPriority(propertyNameString, priority);
+ return cssDecl->SetProperty(propertyNameString, aValue, priority);
+ }
+ return mElement->UnsetAttr(kNameSpaceID_None, nsGkAtoms::style, true);
+}
+
+NS_IMETHODIMP
+ChangeStyleTransaction::UndoTransaction()
+{
+ return SetStyle(mUndoAttributeWasSet, mUndoValue);
+}
+
+NS_IMETHODIMP
+ChangeStyleTransaction::RedoTransaction()
+{
+ return SetStyle(mRedoAttributeWasSet, mRedoValue);
+}
+
+NS_IMETHODIMP
+ChangeStyleTransaction::GetTxnDescription(nsAString& aString)
+{
+ aString.AssignLiteral("ChangeStyleTransaction: [mRemoveProperty == ");
+
+ if (mRemoveProperty) {
+ aString.AppendLiteral("true] ");
+ } else {
+ aString.AppendLiteral("false] ");
+ }
+ aString += nsDependentAtomString(mProperty);
+ return NS_OK;
+}
+
+// True if the CSS property accepts more than one value
+bool
+ChangeStyleTransaction::AcceptsMoreThanOneValue(nsIAtom& aCSSProperty)
+{
+ return &aCSSProperty == nsGkAtoms::text_decoration;
+}
+
+// Adds the value aNewValue to the list of white-space separated values aValues
+void
+ChangeStyleTransaction::AddValueToMultivalueProperty(nsAString& aValues,
+ const nsAString& aNewValue)
+{
+ if (aValues.IsEmpty() || aValues.LowerCaseEqualsLiteral("none")) {
+ aValues.Assign(aNewValue);
+ } else if (!ValueIncludes(aValues, aNewValue)) {
+ // We already have another value but not this one; add it
+ aValues.Append(char16_t(' '));
+ aValues.Append(aNewValue);
+ }
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/ChangeStyleTransaction.h b/editor/libeditor/ChangeStyleTransaction.h
new file mode 100644
index 000000000..14c2cdcb5
--- /dev/null
+++ b/editor/libeditor/ChangeStyleTransaction.h
@@ -0,0 +1,123 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_ChangeStyleTransaction_h
+#define mozilla_ChangeStyleTransaction_h
+
+#include "mozilla/EditTransactionBase.h" // base class
+#include "nsCOMPtr.h" // nsCOMPtr members
+#include "nsCycleCollectionParticipant.h" // various macros
+#include "nsString.h" // nsString members
+
+class nsAString;
+class nsIAtom;
+
+namespace mozilla {
+
+namespace dom {
+class Element;
+} // namespace dom
+
+/**
+ * A transaction that changes the value of a CSS inline style of a content
+ * node. This transaction covers add, remove, and change a property's value.
+ */
+class ChangeStyleTransaction final : public EditTransactionBase
+{
+public:
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ChangeStyleTransaction,
+ EditTransactionBase)
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ NS_DECL_EDITTRANSACTIONBASE
+
+ NS_IMETHOD RedoTransaction() override;
+
+ enum EChangeType { eSet, eRemove };
+
+ /**
+ * @param aNode [IN] the node whose style attribute will be changed
+ * @param aProperty [IN] the name of the property to change
+ * @param aValue [IN] new value for aProperty, or value to remove
+ * @param aChangeType [IN] whether to set or remove
+ */
+ ChangeStyleTransaction(dom::Element& aElement,
+ nsIAtom& aProperty,
+ const nsAString& aValue,
+ EChangeType aChangeType);
+
+ /**
+ * Returns true if the list of white-space separated values contains aValue
+ *
+ * @param aValueList [IN] a list of white-space separated values
+ * @param aValue [IN] the value to look for in the list
+ * @return true if the value is in the list of values
+ */
+ static bool ValueIncludes(const nsAString& aValueList,
+ const nsAString& aValue);
+
+private:
+ virtual ~ChangeStyleTransaction();
+
+ /*
+ * Adds the value aNewValue to list of white-space separated values aValues.
+ *
+ * @param aValues [IN/OUT] a list of wite-space separated values
+ * @param aNewValue [IN] a value this code adds to aValues if it is not
+ * already in
+ */
+ void AddValueToMultivalueProperty(nsAString& aValues,
+ const nsAString& aNewValue);
+
+ /**
+ * Returns true if the property accepts more than one value.
+ *
+ * @param aCSSProperty [IN] the CSS property
+ * @return true if the property accepts more than one value
+ */
+ bool AcceptsMoreThanOneValue(nsIAtom& aCSSProperty);
+
+ /**
+ * Remove a value from a list of white-space separated values.
+ * @param aValues [IN] a list of white-space separated values
+ * @param aRemoveValue [IN] the value to remove from the list
+ */
+ void RemoveValueFromListOfValues(nsAString& aValues,
+ const nsAString& aRemoveValue);
+
+ /**
+ * If the boolean is true and if the value is not the empty string,
+ * set the property in the transaction to that value; if the value
+ * is empty, remove the property from element's styles. If the boolean
+ * is false, just remove the style attribute.
+ */
+ nsresult SetStyle(bool aAttributeWasSet, nsAString& aValue);
+
+ // The element to operate upon.
+ nsCOMPtr<dom::Element> mElement;
+
+ // The CSS property to change.
+ nsCOMPtr<nsIAtom> mProperty;
+
+ // The value to set the property to (ignored if mRemoveProperty==true).
+ nsString mValue;
+
+ // true if the operation is to remove mProperty from mElement.
+ bool mRemoveProperty;
+
+ // The value to set the property to for undo.
+ nsString mUndoValue;
+ // The value to set the property to for redo.
+ nsString mRedoValue;
+ // True if the style attribute was present and not empty before DoTransaction.
+ bool mUndoAttributeWasSet;
+ // True if the style attribute is present and not empty after DoTransaction.
+ bool mRedoAttributeWasSet;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef mozilla_ChangeStyleTransaction_h
diff --git a/editor/libeditor/CompositionTransaction.cpp b/editor/libeditor/CompositionTransaction.cpp
new file mode 100644
index 000000000..25938fa60
--- /dev/null
+++ b/editor/libeditor/CompositionTransaction.cpp
@@ -0,0 +1,332 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CompositionTransaction.h"
+
+#include "mozilla/EditorBase.h" // mEditorBase
+#include "mozilla/SelectionState.h" // RangeUpdater
+#include "mozilla/dom/Selection.h" // local var
+#include "mozilla/dom/Text.h" // mTextNode
+#include "nsAString.h" // params
+#include "nsDebug.h" // for NS_ASSERTION, etc
+#include "nsError.h" // for NS_SUCCEEDED, NS_FAILED, etc
+#include "nsIPresShell.h" // nsISelectionController constants
+#include "nsRange.h" // local var
+#include "nsQueryObject.h" // for do_QueryObject
+
+namespace mozilla {
+
+using namespace dom;
+
+CompositionTransaction::CompositionTransaction(
+ Text& aTextNode,
+ uint32_t aOffset,
+ uint32_t aReplaceLength,
+ TextRangeArray* aTextRangeArray,
+ const nsAString& aStringToInsert,
+ EditorBase& aEditorBase,
+ RangeUpdater* aRangeUpdater)
+ : mTextNode(&aTextNode)
+ , mOffset(aOffset)
+ , mReplaceLength(aReplaceLength)
+ , mRanges(aTextRangeArray)
+ , mStringToInsert(aStringToInsert)
+ , mEditorBase(aEditorBase)
+ , mRangeUpdater(aRangeUpdater)
+ , mFixed(false)
+{
+ MOZ_ASSERT(mTextNode->TextLength() >= mOffset);
+}
+
+CompositionTransaction::~CompositionTransaction()
+{
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(CompositionTransaction, EditTransactionBase,
+ mTextNode)
+// mRangeList can't lead to cycles
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CompositionTransaction)
+ if (aIID.Equals(NS_GET_IID(CompositionTransaction))) {
+ foundInterface = static_cast<nsITransaction*>(this);
+ } else
+NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
+
+NS_IMPL_ADDREF_INHERITED(CompositionTransaction, EditTransactionBase)
+NS_IMPL_RELEASE_INHERITED(CompositionTransaction, EditTransactionBase)
+
+NS_IMETHODIMP
+CompositionTransaction::DoTransaction()
+{
+ // Fail before making any changes if there's no selection controller
+ nsCOMPtr<nsISelectionController> selCon;
+ mEditorBase.GetSelectionController(getter_AddRefs(selCon));
+ NS_ENSURE_TRUE(selCon, NS_ERROR_NOT_INITIALIZED);
+
+ // Advance caret: This requires the presentation shell to get the selection.
+ if (mReplaceLength == 0) {
+ nsresult rv = mTextNode->InsertData(mOffset, mStringToInsert);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ mRangeUpdater->SelAdjInsertText(*mTextNode, mOffset, mStringToInsert);
+ } else {
+ uint32_t replaceableLength = mTextNode->TextLength() - mOffset;
+ nsresult rv =
+ mTextNode->ReplaceData(mOffset, mReplaceLength, mStringToInsert);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ mRangeUpdater->SelAdjDeleteText(mTextNode, mOffset, mReplaceLength);
+ mRangeUpdater->SelAdjInsertText(*mTextNode, mOffset, mStringToInsert);
+
+ // If IME text node is multiple node, ReplaceData doesn't remove all IME
+ // text. So we need remove remained text into other text node.
+ if (replaceableLength < mReplaceLength) {
+ int32_t remainLength = mReplaceLength - replaceableLength;
+ nsCOMPtr<nsINode> node = mTextNode->GetNextSibling();
+ while (node && node->IsNodeOfType(nsINode::eTEXT) &&
+ remainLength > 0) {
+ Text* text = static_cast<Text*>(node.get());
+ uint32_t textLength = text->TextLength();
+ text->DeleteData(0, remainLength);
+ mRangeUpdater->SelAdjDeleteText(text, 0, remainLength);
+ remainLength -= textLength;
+ node = node->GetNextSibling();
+ }
+ }
+ }
+
+ nsresult rv = SetSelectionForRanges();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CompositionTransaction::UndoTransaction()
+{
+ // Get the selection first so we'll fail before making any changes if we
+ // can't get it
+ RefPtr<Selection> selection = mEditorBase.GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NOT_INITIALIZED);
+
+ nsresult rv = mTextNode->DeleteData(mOffset, mStringToInsert.Length());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // set the selection to the insertion point where the string was removed
+ rv = selection->Collapse(mTextNode, mOffset);
+ NS_ASSERTION(NS_SUCCEEDED(rv),
+ "Selection could not be collapsed after undo of IME insert.");
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CompositionTransaction::Merge(nsITransaction* aTransaction,
+ bool* aDidMerge)
+{
+ NS_ENSURE_ARG_POINTER(aTransaction && aDidMerge);
+
+ // Check to make sure we aren't fixed, if we are then nothing gets absorbed
+ if (mFixed) {
+ *aDidMerge = false;
+ return NS_OK;
+ }
+
+ // If aTransaction is another CompositionTransaction then absorb it
+ RefPtr<CompositionTransaction> otherTransaction =
+ do_QueryObject(aTransaction);
+ if (otherTransaction) {
+ // We absorb the next IME transaction by adopting its insert string
+ mStringToInsert = otherTransaction->mStringToInsert;
+ mRanges = otherTransaction->mRanges;
+ *aDidMerge = true;
+ return NS_OK;
+ }
+
+ *aDidMerge = false;
+ return NS_OK;
+}
+
+void
+CompositionTransaction::MarkFixed()
+{
+ mFixed = true;
+}
+
+NS_IMETHODIMP
+CompositionTransaction::GetTxnDescription(nsAString& aString)
+{
+ aString.AssignLiteral("CompositionTransaction: ");
+ aString += mStringToInsert;
+ return NS_OK;
+}
+
+/* ============ private methods ================== */
+
+nsresult
+CompositionTransaction::SetSelectionForRanges()
+{
+ return SetIMESelection(mEditorBase, mTextNode, mOffset,
+ mStringToInsert.Length(), mRanges);
+}
+
+// static
+nsresult
+CompositionTransaction::SetIMESelection(EditorBase& aEditorBase,
+ Text* aTextNode,
+ uint32_t aOffsetInNode,
+ uint32_t aLengthOfCompositionString,
+ const TextRangeArray* aRanges)
+{
+ RefPtr<Selection> selection = aEditorBase.GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NOT_INITIALIZED);
+
+ nsresult rv = selection->StartBatchChanges();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // First, remove all selections of IME composition.
+ static const RawSelectionType kIMESelections[] = {
+ nsISelectionController::SELECTION_IME_RAWINPUT,
+ nsISelectionController::SELECTION_IME_SELECTEDRAWTEXT,
+ nsISelectionController::SELECTION_IME_CONVERTEDTEXT,
+ nsISelectionController::SELECTION_IME_SELECTEDCONVERTEDTEXT
+ };
+
+ nsCOMPtr<nsISelectionController> selCon;
+ aEditorBase.GetSelectionController(getter_AddRefs(selCon));
+ NS_ENSURE_TRUE(selCon, NS_ERROR_NOT_INITIALIZED);
+
+ for (uint32_t i = 0; i < ArrayLength(kIMESelections); ++i) {
+ nsCOMPtr<nsISelection> selectionOfIME;
+ if (NS_FAILED(selCon->GetSelection(kIMESelections[i],
+ getter_AddRefs(selectionOfIME)))) {
+ continue;
+ }
+ rv = selectionOfIME->RemoveAllRanges();
+ NS_ASSERTION(NS_SUCCEEDED(rv),
+ "Failed to remove all ranges of IME selection");
+ }
+
+ // Set caret position and selection of IME composition with TextRangeArray.
+ bool setCaret = false;
+ uint32_t countOfRanges = aRanges ? aRanges->Length() : 0;
+
+#ifdef DEBUG
+ // Bounds-checking on debug builds
+ uint32_t maxOffset = aTextNode->Length();
+#endif
+
+ // NOTE: composition string may be truncated when it's committed and
+ // maxlength attribute value doesn't allow input of all text of this
+ // composition.
+ for (uint32_t i = 0; i < countOfRanges; ++i) {
+ const TextRange& textRange = aRanges->ElementAt(i);
+
+ // Caret needs special handling since its length may be 0 and if it's not
+ // specified explicitly, we need to handle it ourselves later.
+ if (textRange.mRangeType == TextRangeType::eCaret) {
+ NS_ASSERTION(!setCaret, "The ranges already has caret position");
+ NS_ASSERTION(!textRange.Length(),
+ "EditorBase doesn't support wide caret");
+ int32_t caretOffset = static_cast<int32_t>(
+ aOffsetInNode +
+ std::min(textRange.mStartOffset, aLengthOfCompositionString));
+ MOZ_ASSERT(caretOffset >= 0 &&
+ static_cast<uint32_t>(caretOffset) <= maxOffset);
+ rv = selection->Collapse(aTextNode, caretOffset);
+ setCaret = setCaret || NS_SUCCEEDED(rv);
+ if (NS_WARN_IF(!setCaret)) {
+ continue;
+ }
+ // If caret range is specified explicitly, we should show the caret if
+ // it should be so.
+ aEditorBase.HideCaret(false);
+ continue;
+ }
+
+ // If the clause length is 0, it should be a bug.
+ if (!textRange.Length()) {
+ NS_WARNING("Any clauses must not be empty");
+ continue;
+ }
+
+ RefPtr<nsRange> clauseRange;
+ int32_t startOffset = static_cast<int32_t>(
+ aOffsetInNode +
+ std::min(textRange.mStartOffset, aLengthOfCompositionString));
+ MOZ_ASSERT(startOffset >= 0 &&
+ static_cast<uint32_t>(startOffset) <= maxOffset);
+ int32_t endOffset = static_cast<int32_t>(
+ aOffsetInNode +
+ std::min(textRange.mEndOffset, aLengthOfCompositionString));
+ MOZ_ASSERT(endOffset >= startOffset &&
+ static_cast<uint32_t>(endOffset) <= maxOffset);
+ rv = nsRange::CreateRange(aTextNode, startOffset,
+ aTextNode, endOffset,
+ getter_AddRefs(clauseRange));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to create a DOM range for a clause of composition");
+ break;
+ }
+
+ // Set the range of the clause to selection.
+ nsCOMPtr<nsISelection> selectionOfIME;
+ rv = selCon->GetSelection(ToRawSelectionType(textRange.mRangeType),
+ getter_AddRefs(selectionOfIME));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to get IME selection");
+ break;
+ }
+
+ rv = selectionOfIME->AddRange(clauseRange);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to add selection range for a clause of composition");
+ break;
+ }
+
+ // Set the style of the clause.
+ nsCOMPtr<nsISelectionPrivate> selectionOfIMEPriv =
+ do_QueryInterface(selectionOfIME);
+ if (!selectionOfIMEPriv) {
+ NS_WARNING("Failed to get nsISelectionPrivate interface from selection");
+ continue; // Since this is additional feature, we can continue this job.
+ }
+ rv = selectionOfIMEPriv->SetTextRangeStyle(clauseRange,
+ textRange.mRangeStyle);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to set selection style");
+ break; // but this is unexpected...
+ }
+ }
+
+ // If the ranges doesn't include explicit caret position, let's set the
+ // caret to the end of composition string.
+ if (!setCaret) {
+ int32_t caretOffset =
+ static_cast<int32_t>(aOffsetInNode + aLengthOfCompositionString);
+ MOZ_ASSERT(caretOffset >= 0 &&
+ static_cast<uint32_t>(caretOffset) <= maxOffset);
+ rv = selection->Collapse(aTextNode, caretOffset);
+ NS_ASSERTION(NS_SUCCEEDED(rv),
+ "Failed to set caret at the end of composition string");
+
+ // If caret range isn't specified explicitly, we should hide the caret.
+ // Hiding the caret benefits a Windows build (see bug 555642 comment #6).
+ // However, when there is no range, we should keep showing caret.
+ if (countOfRanges) {
+ aEditorBase.HideCaret(true);
+ }
+ }
+
+ rv = selection->EndBatchChangesInternal();
+ NS_ASSERTION(NS_SUCCEEDED(rv), "Failed to end batch changes");
+
+ return rv;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/CompositionTransaction.h b/editor/libeditor/CompositionTransaction.h
new file mode 100644
index 000000000..acb3d8beb
--- /dev/null
+++ b/editor/libeditor/CompositionTransaction.h
@@ -0,0 +1,104 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef CompositionTransaction_h
+#define CompositionTransaction_h
+
+#include "mozilla/EditTransactionBase.h" // base class
+#include "nsCycleCollectionParticipant.h" // various macros
+#include "nsString.h" // mStringToInsert
+
+#define NS_IMETEXTTXN_IID \
+ { 0xb391355d, 0x346c, 0x43d1, \
+ { 0x85, 0xed, 0x9e, 0x65, 0xbe, 0xe7, 0x7e, 0x48 } }
+
+namespace mozilla {
+
+class EditorBase;
+class RangeUpdater;
+class TextRangeArray;
+
+namespace dom {
+class Text;
+} // namespace dom
+
+/**
+ * CompositionTransaction stores all edit for a composition, i.e.,
+ * from compositionstart event to compositionend event. E.g., inserting a
+ * composition string, modifying the composition string or its IME selection
+ * ranges and commit or cancel the composition.
+ */
+class CompositionTransaction final : public EditTransactionBase
+{
+public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IMETEXTTXN_IID)
+
+ /**
+ * @param aTextNode The start node of text content.
+ * @param aOffset The location in aTextNode to do the insertion.
+ * @param aReplaceLength The length of text to replace. 0 means not
+ * replacing existing text.
+ * @param aTextRangeArray Clauses and/or caret information. This may be
+ * null.
+ * @param aString The new text to insert.
+ * @param aEditorBase Used to get and set the selection.
+ * @param aRangeUpdater The range updater
+ */
+ CompositionTransaction(dom::Text& aTextNode,
+ uint32_t aOffset, uint32_t aReplaceLength,
+ TextRangeArray* aTextRangeArray,
+ const nsAString& aString,
+ EditorBase& aEditorBase,
+ RangeUpdater* aRangeUpdater);
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(CompositionTransaction,
+ EditTransactionBase)
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ NS_DECL_EDITTRANSACTIONBASE
+
+ NS_IMETHOD Merge(nsITransaction* aTransaction, bool* aDidMerge) override;
+
+ void MarkFixed();
+
+ static nsresult SetIMESelection(EditorBase& aEditorBase,
+ dom::Text* aTextNode,
+ uint32_t aOffsetInNode,
+ uint32_t aLengthOfCompositionString,
+ const TextRangeArray* aRanges);
+
+private:
+ ~CompositionTransaction();
+
+ nsresult SetSelectionForRanges();
+
+ // The text element to operate upon.
+ RefPtr<dom::Text> mTextNode;
+
+ // The offsets into mTextNode where the insertion should be placed.
+ uint32_t mOffset;
+
+ uint32_t mReplaceLength;
+
+ // The range list.
+ RefPtr<TextRangeArray> mRanges;
+
+ // The text to insert into mTextNode at mOffset.
+ nsString mStringToInsert;
+
+ // The editor, which is used to get the selection controller.
+ EditorBase& mEditorBase;
+
+ RangeUpdater* mRangeUpdater;
+
+ bool mFixed;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(CompositionTransaction, NS_IMETEXTTXN_IID)
+
+} // namespace mozilla
+
+#endif // #ifndef CompositionTransaction_h
diff --git a/editor/libeditor/CreateElementTransaction.cpp b/editor/libeditor/CreateElementTransaction.cpp
new file mode 100644
index 000000000..5e4bd961c
--- /dev/null
+++ b/editor/libeditor/CreateElementTransaction.cpp
@@ -0,0 +1,147 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CreateElementTransaction.h"
+
+#include <algorithm>
+#include <stdio.h>
+
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Selection.h"
+
+#include "mozilla/Casting.h"
+#include "mozilla/EditorBase.h"
+
+#include "nsAlgorithm.h"
+#include "nsAString.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsIContent.h"
+#include "nsIDOMCharacterData.h"
+#include "nsIEditor.h"
+#include "nsINode.h"
+#include "nsISupportsUtils.h"
+#include "nsMemory.h"
+#include "nsReadableUtils.h"
+#include "nsStringFwd.h"
+#include "nsString.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+CreateElementTransaction::CreateElementTransaction(EditorBase& aEditorBase,
+ nsIAtom& aTag,
+ nsINode& aParent,
+ int32_t aOffsetInParent)
+ : EditTransactionBase()
+ , mEditorBase(&aEditorBase)
+ , mTag(&aTag)
+ , mParent(&aParent)
+ , mOffsetInParent(aOffsetInParent)
+{
+}
+
+CreateElementTransaction::~CreateElementTransaction()
+{
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(CreateElementTransaction,
+ EditTransactionBase,
+ mParent,
+ mNewNode,
+ mRefNode)
+
+NS_IMPL_ADDREF_INHERITED(CreateElementTransaction, EditTransactionBase)
+NS_IMPL_RELEASE_INHERITED(CreateElementTransaction, EditTransactionBase)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CreateElementTransaction)
+NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
+
+
+NS_IMETHODIMP
+CreateElementTransaction::DoTransaction()
+{
+ MOZ_ASSERT(mEditorBase && mTag && mParent);
+
+ mNewNode = mEditorBase->CreateHTMLContent(mTag);
+ NS_ENSURE_STATE(mNewNode);
+
+ // Try to insert formatting whitespace for the new node:
+ mEditorBase->MarkNodeDirty(GetAsDOMNode(mNewNode));
+
+ // Insert the new node
+ ErrorResult rv;
+ if (mOffsetInParent == -1) {
+ mParent->AppendChild(*mNewNode, rv);
+ return rv.StealNSResult();
+ }
+
+ mOffsetInParent = std::min(mOffsetInParent,
+ static_cast<int32_t>(mParent->GetChildCount()));
+
+ // Note, it's ok for mRefNode to be null. That means append
+ mRefNode = mParent->GetChildAt(mOffsetInParent);
+
+ nsCOMPtr<nsIContent> refNode = mRefNode;
+ mParent->InsertBefore(*mNewNode, refNode, rv);
+ NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult());
+
+ // Only set selection to insertion point if editor gives permission
+ if (!mEditorBase->GetShouldTxnSetSelection()) {
+ // Do nothing - DOM range gravity will adjust selection
+ return NS_OK;
+ }
+
+ RefPtr<Selection> selection = mEditorBase->GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+
+ rv = selection->CollapseNative(mParent, mParent->IndexOf(mNewNode) + 1);
+ NS_ASSERTION(!rv.Failed(),
+ "selection could not be collapsed after insert");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CreateElementTransaction::UndoTransaction()
+{
+ MOZ_ASSERT(mEditorBase && mParent);
+
+ ErrorResult rv;
+ mParent->RemoveChild(*mNewNode, rv);
+
+ return rv.StealNSResult();
+}
+
+NS_IMETHODIMP
+CreateElementTransaction::RedoTransaction()
+{
+ MOZ_ASSERT(mEditorBase && mParent);
+
+ // First, reset mNewNode so it has no attributes or content
+ // XXX We never actually did this, we only cleared mNewNode's contents if it
+ // was a CharacterData node (which it's not, it's an Element)
+
+ // Now, reinsert mNewNode
+ ErrorResult rv;
+ nsCOMPtr<nsIContent> refNode = mRefNode;
+ mParent->InsertBefore(*mNewNode, refNode, rv);
+ return rv.StealNSResult();
+}
+
+NS_IMETHODIMP
+CreateElementTransaction::GetTxnDescription(nsAString& aString)
+{
+ aString.AssignLiteral("CreateElementTransaction: ");
+ aString += nsDependentAtomString(mTag);
+ return NS_OK;
+}
+
+already_AddRefed<Element>
+CreateElementTransaction::GetNewNode()
+{
+ return nsCOMPtr<Element>(mNewNode).forget();
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/CreateElementTransaction.h b/editor/libeditor/CreateElementTransaction.h
new file mode 100644
index 000000000..70fecceae
--- /dev/null
+++ b/editor/libeditor/CreateElementTransaction.h
@@ -0,0 +1,80 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef CreateElementTransaction_h
+#define CreateElementTransaction_h
+
+#include "mozilla/EditTransactionBase.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupportsImpl.h"
+
+class nsIAtom;
+class nsIContent;
+class nsINode;
+
+/**
+ * A transaction that creates a new node in the content tree.
+ */
+namespace mozilla {
+
+class EditorBase;
+namespace dom {
+class Element;
+} // namespace dom
+
+class CreateElementTransaction final : public EditTransactionBase
+{
+public:
+ /**
+ * Initialize the transaction.
+ * @param aEditorBase The provider of basic editing functionality.
+ * @param aTag The tag (P, HR, TABLE, etc.) for the new element.
+ * @param aParent The node into which the new element will be
+ * inserted.
+ * @param aOffsetInParent The location in aParent to insert the new element.
+ * If eAppend, the new element is appended as the last
+ * child.
+ */
+ CreateElementTransaction(EditorBase& aEditorBase,
+ nsIAtom& aTag,
+ nsINode& aParent,
+ int32_t aOffsetInParent);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(CreateElementTransaction,
+ EditTransactionBase)
+
+ NS_DECL_EDITTRANSACTIONBASE
+
+ NS_IMETHOD RedoTransaction() override;
+
+ already_AddRefed<dom::Element> GetNewNode();
+
+protected:
+ virtual ~CreateElementTransaction();
+
+ // The document into which the new node will be inserted.
+ EditorBase* mEditorBase;
+
+ // The tag (mapping to object type) for the new element.
+ nsCOMPtr<nsIAtom> mTag;
+
+ // The node into which the new node will be inserted.
+ nsCOMPtr<nsINode> mParent;
+
+ // The index in mParent for the new node.
+ int32_t mOffsetInParent;
+
+ // The new node to insert.
+ nsCOMPtr<dom::Element> mNewNode;
+
+ // The node we will insert mNewNode before. We compute this ourselves.
+ nsCOMPtr<nsIContent> mRefNode;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef CreateElementTransaction_h
diff --git a/editor/libeditor/DeleteNodeTransaction.cpp b/editor/libeditor/DeleteNodeTransaction.cpp
new file mode 100644
index 000000000..7f485b066
--- /dev/null
+++ b/editor/libeditor/DeleteNodeTransaction.cpp
@@ -0,0 +1,123 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DeleteNodeTransaction.h"
+#include "mozilla/EditorBase.h"
+#include "mozilla/SelectionState.h" // RangeUpdater
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsAString.h"
+
+namespace mozilla {
+
+DeleteNodeTransaction::DeleteNodeTransaction()
+ : mEditorBase(nullptr)
+ , mRangeUpdater(nullptr)
+{
+}
+
+DeleteNodeTransaction::~DeleteNodeTransaction()
+{
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(DeleteNodeTransaction, EditTransactionBase,
+ mNode,
+ mParent,
+ mRefNode)
+
+NS_IMPL_ADDREF_INHERITED(DeleteNodeTransaction, EditTransactionBase)
+NS_IMPL_RELEASE_INHERITED(DeleteNodeTransaction, EditTransactionBase)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DeleteNodeTransaction)
+NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
+
+nsresult
+DeleteNodeTransaction::Init(EditorBase* aEditorBase,
+ nsINode* aNode,
+ RangeUpdater* aRangeUpdater)
+{
+ NS_ENSURE_TRUE(aEditorBase && aNode, NS_ERROR_NULL_POINTER);
+ mEditorBase = aEditorBase;
+ mNode = aNode;
+ mParent = aNode->GetParentNode();
+
+ // do nothing if the node has a parent and it's read-only
+ NS_ENSURE_TRUE(!mParent || mEditorBase->IsModifiableNode(mParent),
+ NS_ERROR_FAILURE);
+
+ mRangeUpdater = aRangeUpdater;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DeleteNodeTransaction::DoTransaction()
+{
+ NS_ENSURE_TRUE(mNode, NS_ERROR_NOT_INITIALIZED);
+
+ if (!mParent) {
+ // this is a no-op, there's no parent to delete mNode from
+ return NS_OK;
+ }
+
+ // remember which child mNode was (by remembering which child was next);
+ // mRefNode can be null
+ mRefNode = mNode->GetNextSibling();
+
+ // give range updater a chance. SelAdjDeleteNode() needs to be called
+ // *before* we do the action, unlike some of the other RangeItem update
+ // methods.
+ if (mRangeUpdater) {
+ mRangeUpdater->SelAdjDeleteNode(mNode->AsDOMNode());
+ }
+
+ ErrorResult error;
+ mParent->RemoveChild(*mNode, error);
+ return error.StealNSResult();
+}
+
+NS_IMETHODIMP
+DeleteNodeTransaction::UndoTransaction()
+{
+ if (!mParent) {
+ // this is a legal state, the txn is a no-op
+ return NS_OK;
+ }
+ if (!mNode) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ ErrorResult error;
+ nsCOMPtr<nsIContent> refNode = mRefNode;
+ mParent->InsertBefore(*mNode, refNode, error);
+ return error.StealNSResult();
+}
+
+NS_IMETHODIMP
+DeleteNodeTransaction::RedoTransaction()
+{
+ if (!mParent) {
+ // this is a legal state, the txn is a no-op
+ return NS_OK;
+ }
+ if (!mNode) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ if (mRangeUpdater) {
+ mRangeUpdater->SelAdjDeleteNode(mNode->AsDOMNode());
+ }
+
+ ErrorResult error;
+ mParent->RemoveChild(*mNode, error);
+ return error.StealNSResult();
+}
+
+NS_IMETHODIMP
+DeleteNodeTransaction::GetTxnDescription(nsAString& aString)
+{
+ aString.AssignLiteral("DeleteNodeTransaction");
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/DeleteNodeTransaction.h b/editor/libeditor/DeleteNodeTransaction.h
new file mode 100644
index 000000000..d0bc0dd46
--- /dev/null
+++ b/editor/libeditor/DeleteNodeTransaction.h
@@ -0,0 +1,66 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DeleteNodeTransaction_h
+#define DeleteNodeTransaction_h
+
+#include "mozilla/EditTransactionBase.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIContent.h"
+#include "nsINode.h"
+#include "nsISupportsImpl.h"
+#include "nscore.h"
+
+namespace mozilla {
+
+class EditorBase;
+class RangeUpdater;
+
+/**
+ * A transaction that deletes a single element
+ */
+class DeleteNodeTransaction final : public EditTransactionBase
+{
+public:
+ /**
+ * Initialize the transaction.
+ * @param aElement The node to delete.
+ */
+ nsresult Init(EditorBase* aEditorBase, nsINode* aNode,
+ RangeUpdater* aRangeUpdater);
+
+ DeleteNodeTransaction();
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DeleteNodeTransaction,
+ EditTransactionBase)
+
+ NS_DECL_EDITTRANSACTIONBASE
+
+ NS_IMETHOD RedoTransaction() override;
+
+protected:
+ virtual ~DeleteNodeTransaction();
+
+ // The element to delete.
+ nsCOMPtr<nsINode> mNode;
+
+ // Parent of node to delete.
+ nsCOMPtr<nsINode> mParent;
+
+ // Next sibling to remember for undo/redo purposes.
+ nsCOMPtr<nsIContent> mRefNode;
+
+ // The editor for this transaction.
+ EditorBase* mEditorBase;
+
+ // Range updater object.
+ RangeUpdater* mRangeUpdater;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef DeleteNodeTransaction_h
diff --git a/editor/libeditor/DeleteRangeTransaction.cpp b/editor/libeditor/DeleteRangeTransaction.cpp
new file mode 100644
index 000000000..977de4873
--- /dev/null
+++ b/editor/libeditor/DeleteRangeTransaction.cpp
@@ -0,0 +1,239 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DeleteRangeTransaction.h"
+
+#include "DeleteNodeTransaction.h"
+#include "DeleteTextTransaction.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/EditorBase.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/mozalloc.h"
+#include "nsCOMPtr.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsIContent.h"
+#include "nsIContentIterator.h"
+#include "nsIDOMCharacterData.h"
+#include "nsINode.h"
+#include "nsAString.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+// note that aEditorBase is not refcounted
+DeleteRangeTransaction::DeleteRangeTransaction()
+ : mEditorBase(nullptr)
+ , mRangeUpdater(nullptr)
+{
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(DeleteRangeTransaction,
+ EditAggregateTransaction,
+ mRange)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DeleteRangeTransaction)
+NS_INTERFACE_MAP_END_INHERITING(EditAggregateTransaction)
+
+nsresult
+DeleteRangeTransaction::Init(EditorBase* aEditorBase,
+ nsRange* aRange,
+ RangeUpdater* aRangeUpdater)
+{
+ MOZ_ASSERT(aEditorBase && aRange);
+
+ mEditorBase = aEditorBase;
+ mRange = aRange->CloneRange();
+ mRangeUpdater = aRangeUpdater;
+
+ NS_ENSURE_TRUE(mEditorBase->IsModifiableNode(mRange->GetStartParent()),
+ NS_ERROR_FAILURE);
+ NS_ENSURE_TRUE(mEditorBase->IsModifiableNode(mRange->GetEndParent()),
+ NS_ERROR_FAILURE);
+ NS_ENSURE_TRUE(mEditorBase->IsModifiableNode(mRange->GetCommonAncestor()),
+ NS_ERROR_FAILURE);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DeleteRangeTransaction::DoTransaction()
+{
+ MOZ_ASSERT(mRange && mEditorBase);
+
+ // build the child transactions
+ nsCOMPtr<nsINode> startParent = mRange->GetStartParent();
+ int32_t startOffset = mRange->StartOffset();
+ nsCOMPtr<nsINode> endParent = mRange->GetEndParent();
+ int32_t endOffset = mRange->EndOffset();
+ MOZ_ASSERT(startParent && endParent);
+
+ if (startParent == endParent) {
+ // the selection begins and ends in the same node
+ nsresult rv =
+ CreateTxnsToDeleteBetween(startParent, startOffset, endOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // the selection ends in a different node from where it started. delete
+ // the relevant content in the start node
+ nsresult rv =
+ CreateTxnsToDeleteContent(startParent, startOffset, nsIEditor::eNext);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // delete the intervening nodes
+ rv = CreateTxnsToDeleteNodesBetween();
+ NS_ENSURE_SUCCESS(rv, rv);
+ // delete the relevant content in the end node
+ rv = CreateTxnsToDeleteContent(endParent, endOffset, nsIEditor::ePrevious);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // if we've successfully built this aggregate transaction, then do it.
+ nsresult rv = EditAggregateTransaction::DoTransaction();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // only set selection to deletion point if editor gives permission
+ bool bAdjustSelection;
+ mEditorBase->ShouldTxnSetSelection(&bAdjustSelection);
+ if (bAdjustSelection) {
+ RefPtr<Selection> selection = mEditorBase->GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+ rv = selection->Collapse(startParent, startOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // else do nothing - dom range gravity will adjust selection
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DeleteRangeTransaction::UndoTransaction()
+{
+ MOZ_ASSERT(mRange && mEditorBase);
+
+ return EditAggregateTransaction::UndoTransaction();
+}
+
+NS_IMETHODIMP
+DeleteRangeTransaction::RedoTransaction()
+{
+ MOZ_ASSERT(mRange && mEditorBase);
+
+ return EditAggregateTransaction::RedoTransaction();
+}
+
+NS_IMETHODIMP
+DeleteRangeTransaction::GetTxnDescription(nsAString& aString)
+{
+ aString.AssignLiteral("DeleteRangeTransaction");
+ return NS_OK;
+}
+
+nsresult
+DeleteRangeTransaction::CreateTxnsToDeleteBetween(nsINode* aNode,
+ int32_t aStartOffset,
+ int32_t aEndOffset)
+{
+ // see what kind of node we have
+ if (aNode->IsNodeOfType(nsINode::eDATA_NODE)) {
+ // if the node is a chardata node, then delete chardata content
+ int32_t numToDel;
+ if (aStartOffset == aEndOffset) {
+ numToDel = 1;
+ } else {
+ numToDel = aEndOffset - aStartOffset;
+ }
+
+ RefPtr<nsGenericDOMDataNode> charDataNode =
+ static_cast<nsGenericDOMDataNode*>(aNode);
+
+ RefPtr<DeleteTextTransaction> transaction =
+ new DeleteTextTransaction(*mEditorBase, *charDataNode, aStartOffset,
+ numToDel, mRangeUpdater);
+
+ nsresult rv = transaction->Init();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ AppendChild(transaction);
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIContent> child = aNode->GetChildAt(aStartOffset);
+ NS_ENSURE_STATE(child);
+
+ // XXX This looks odd. Only when the last transaction causes error at
+ // calling Init(), the result becomes error. Otherwise, always NS_OK.
+ nsresult rv = NS_OK;
+ for (int32_t i = aStartOffset; i < aEndOffset; ++i) {
+ RefPtr<DeleteNodeTransaction> transaction = new DeleteNodeTransaction();
+ rv = transaction->Init(mEditorBase, child, mRangeUpdater);
+ if (NS_SUCCEEDED(rv)) {
+ AppendChild(transaction);
+ }
+
+ child = child->GetNextSibling();
+ }
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+nsresult
+DeleteRangeTransaction::CreateTxnsToDeleteContent(nsINode* aNode,
+ int32_t aOffset,
+ nsIEditor::EDirection aAction)
+{
+ // see what kind of node we have
+ if (aNode->IsNodeOfType(nsINode::eDATA_NODE)) {
+ // if the node is a chardata node, then delete chardata content
+ uint32_t start, numToDelete;
+ if (nsIEditor::eNext == aAction) {
+ start = aOffset;
+ numToDelete = aNode->Length() - aOffset;
+ } else {
+ start = 0;
+ numToDelete = aOffset;
+ }
+
+ if (numToDelete) {
+ RefPtr<nsGenericDOMDataNode> dataNode =
+ static_cast<nsGenericDOMDataNode*>(aNode);
+ RefPtr<DeleteTextTransaction> transaction =
+ new DeleteTextTransaction(*mEditorBase, *dataNode, start, numToDelete,
+ mRangeUpdater);
+
+ nsresult rv = transaction->Init();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ AppendChild(transaction);
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult
+DeleteRangeTransaction::CreateTxnsToDeleteNodesBetween()
+{
+ nsCOMPtr<nsIContentIterator> iter = NS_NewContentSubtreeIterator();
+
+ nsresult rv = iter->Init(mRange);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ while (!iter->IsDone()) {
+ nsCOMPtr<nsINode> node = iter->GetCurrentNode();
+ NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER);
+
+ RefPtr<DeleteNodeTransaction> transaction = new DeleteNodeTransaction();
+ rv = transaction->Init(mEditorBase, node, mRangeUpdater);
+ NS_ENSURE_SUCCESS(rv, rv);
+ AppendChild(transaction);
+
+ iter->Next();
+ }
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/DeleteRangeTransaction.h b/editor/libeditor/DeleteRangeTransaction.h
new file mode 100644
index 000000000..9b60a5ba2
--- /dev/null
+++ b/editor/libeditor/DeleteRangeTransaction.h
@@ -0,0 +1,78 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DeleteRangeTransaction_h
+#define DeleteRangeTransaction_h
+
+#include "EditAggregateTransaction.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsID.h"
+#include "nsIEditor.h"
+#include "nsISupportsImpl.h"
+#include "nsRange.h"
+#include "nscore.h"
+
+class nsINode;
+
+namespace mozilla {
+
+class EditorBase;
+class RangeUpdater;
+
+/**
+ * A transaction that deletes an entire range in the content tree
+ */
+class DeleteRangeTransaction final : public EditAggregateTransaction
+{
+public:
+ /**
+ * Initialize the transaction.
+ * @param aEditorBase The object providing basic editing operations.
+ * @param aRange The range to delete.
+ */
+ nsresult Init(EditorBase* aEditorBase,
+ nsRange* aRange,
+ RangeUpdater* aRangeUpdater);
+
+ DeleteRangeTransaction();
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DeleteRangeTransaction,
+ EditAggregateTransaction)
+ NS_IMETHOD QueryInterface(REFNSIID aIID, void** aInstancePtr) override;
+
+ NS_DECL_EDITTRANSACTIONBASE
+
+ NS_IMETHOD RedoTransaction() override;
+
+ virtual void LastRelease() override
+ {
+ mRange = nullptr;
+ EditAggregateTransaction::LastRelease();
+ }
+
+protected:
+ nsresult CreateTxnsToDeleteBetween(nsINode* aNode,
+ int32_t aStartOffset,
+ int32_t aEndOffset);
+
+ nsresult CreateTxnsToDeleteNodesBetween();
+
+ nsresult CreateTxnsToDeleteContent(nsINode* aParent,
+ int32_t aOffset,
+ nsIEditor::EDirection aAction);
+
+ // P1 in the range.
+ RefPtr<nsRange> mRange;
+
+ // The editor for this transaction.
+ EditorBase* mEditorBase;
+
+ // Range updater object.
+ RangeUpdater* mRangeUpdater;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef DeleteRangeTransaction_h
diff --git a/editor/libeditor/DeleteTextTransaction.cpp b/editor/libeditor/DeleteTextTransaction.cpp
new file mode 100644
index 000000000..6de3181da
--- /dev/null
+++ b/editor/libeditor/DeleteTextTransaction.cpp
@@ -0,0 +1,102 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DeleteTextTransaction.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/EditorBase.h"
+#include "mozilla/SelectionState.h"
+#include "mozilla/dom/Selection.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsIEditor.h"
+#include "nsISupportsImpl.h"
+#include "nsAString.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+DeleteTextTransaction::DeleteTextTransaction(
+ EditorBase& aEditorBase,
+ nsGenericDOMDataNode& aCharData,
+ uint32_t aOffset,
+ uint32_t aNumCharsToDelete,
+ RangeUpdater* aRangeUpdater)
+ : mEditorBase(aEditorBase)
+ , mCharData(&aCharData)
+ , mOffset(aOffset)
+ , mNumCharsToDelete(aNumCharsToDelete)
+ , mRangeUpdater(aRangeUpdater)
+{
+ NS_ASSERTION(mCharData->Length() >= aOffset + aNumCharsToDelete,
+ "Trying to delete more characters than in node");
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(DeleteTextTransaction, EditTransactionBase,
+ mCharData)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DeleteTextTransaction)
+NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
+
+nsresult
+DeleteTextTransaction::Init()
+{
+ // Do nothing if the node is read-only
+ if (!mEditorBase.IsModifiableNode(mCharData)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DeleteTextTransaction::DoTransaction()
+{
+ MOZ_ASSERT(mCharData);
+
+ // Get the text that we're about to delete
+ nsresult rv = mCharData->SubstringData(mOffset, mNumCharsToDelete,
+ mDeletedText);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ rv = mCharData->DeleteData(mOffset, mNumCharsToDelete);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mRangeUpdater) {
+ mRangeUpdater->SelAdjDeleteText(mCharData, mOffset, mNumCharsToDelete);
+ }
+
+ // Only set selection to deletion point if editor gives permission
+ if (mEditorBase.GetShouldTxnSetSelection()) {
+ RefPtr<Selection> selection = mEditorBase.GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+ rv = selection->Collapse(mCharData, mOffset);
+ NS_ASSERTION(NS_SUCCEEDED(rv),
+ "Selection could not be collapsed after undo of deletetext");
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // Else do nothing - DOM Range gravity will adjust selection
+ return NS_OK;
+}
+
+//XXX: We may want to store the selection state and restore it properly. Was
+// it an insertion point or an extended selection?
+NS_IMETHODIMP
+DeleteTextTransaction::UndoTransaction()
+{
+ MOZ_ASSERT(mCharData);
+
+ return mCharData->InsertData(mOffset, mDeletedText);
+}
+
+NS_IMETHODIMP
+DeleteTextTransaction::GetTxnDescription(nsAString& aString)
+{
+ aString.AssignLiteral("DeleteTextTransaction: ");
+ aString += mDeletedText;
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/DeleteTextTransaction.h b/editor/libeditor/DeleteTextTransaction.h
new file mode 100644
index 000000000..855d14349
--- /dev/null
+++ b/editor/libeditor/DeleteTextTransaction.h
@@ -0,0 +1,76 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DeleteTextTransaction_h
+#define DeleteTextTransaction_h
+
+#include "mozilla/EditTransactionBase.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsGenericDOMDataNode.h"
+#include "nsID.h"
+#include "nsString.h"
+#include "nscore.h"
+
+namespace mozilla {
+
+class EditorBase;
+class RangeUpdater;
+
+/**
+ * A transaction that removes text from a content node.
+ */
+class DeleteTextTransaction final : public EditTransactionBase
+{
+public:
+ /**
+ * Initialize the transaction.
+ * @param aEditorBase The provider of basic editing operations.
+ * @param aElement The content node to remove text from.
+ * @param aOffset The location in aElement to begin the deletion.
+ * @param aNumCharsToDelete The number of characters to delete. Not the
+ * number of bytes!
+ */
+ DeleteTextTransaction(EditorBase& aEditorBase,
+ nsGenericDOMDataNode& aCharData,
+ uint32_t aOffset,
+ uint32_t aNumCharsToDelete,
+ RangeUpdater* aRangeUpdater);
+
+ nsresult Init();
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DeleteTextTransaction,
+ EditTransactionBase)
+ NS_IMETHOD QueryInterface(REFNSIID aIID, void** aInstancePtr) override;
+
+ NS_DECL_EDITTRANSACTIONBASE
+
+ uint32_t GetOffset() { return mOffset; }
+
+ uint32_t GetNumCharsToDelete() { return mNumCharsToDelete; }
+
+protected:
+ // The provider of basic editing operations.
+ EditorBase& mEditorBase;
+
+ // The CharacterData node to operate upon.
+ RefPtr<nsGenericDOMDataNode> mCharData;
+
+ // The offset into mCharData where the deletion is to take place.
+ uint32_t mOffset;
+
+ // The number of characters to delete.
+ uint32_t mNumCharsToDelete;
+
+ // The text that was deleted.
+ nsString mDeletedText;
+
+ // Range updater object.
+ RangeUpdater* mRangeUpdater;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef DeleteTextTransaction_h
diff --git a/editor/libeditor/EditActionListener.h b/editor/libeditor/EditActionListener.h
new file mode 100644
index 000000000..834ec76b9
--- /dev/null
+++ b/editor/libeditor/EditActionListener.h
@@ -0,0 +1,17 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __editActionListener_h__
+#define __editActionListener_h__
+
+class EditActionListener
+{
+public:
+
+ virtual void EditAction() = 0;
+
+};
+
+#endif /* __editActionListener_h__ */
diff --git a/editor/libeditor/EditAggregateTransaction.cpp b/editor/libeditor/EditAggregateTransaction.cpp
new file mode 100644
index 000000000..f50e8a67c
--- /dev/null
+++ b/editor/libeditor/EditAggregateTransaction.cpp
@@ -0,0 +1,145 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "EditAggregateTransaction.h"
+#include "nsAString.h"
+#include "nsCOMPtr.h" // for nsCOMPtr
+#include "nsError.h" // for NS_OK, etc.
+#include "nsISupportsUtils.h" // for NS_ADDREF
+#include "nsITransaction.h" // for nsITransaction
+#include "nsString.h" // for nsAutoString
+
+namespace mozilla {
+
+EditAggregateTransaction::EditAggregateTransaction()
+{
+}
+
+EditAggregateTransaction::~EditAggregateTransaction()
+{
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(EditAggregateTransaction,
+ EditTransactionBase,
+ mChildren)
+
+NS_IMPL_ADDREF_INHERITED(EditAggregateTransaction, EditTransactionBase)
+NS_IMPL_RELEASE_INHERITED(EditAggregateTransaction, EditTransactionBase)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(EditAggregateTransaction)
+NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
+
+NS_IMETHODIMP
+EditAggregateTransaction::DoTransaction()
+{
+ // FYI: It's legal (but not very useful) to have an empty child list.
+ for (uint32_t i = 0, length = mChildren.Length(); i < length; ++i) {
+ nsITransaction *txn = mChildren[i];
+ if (!txn) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ nsresult rv = txn->DoTransaction();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditAggregateTransaction::UndoTransaction()
+{
+ // FYI: It's legal (but not very useful) to have an empty child list.
+ // Undo goes through children backwards.
+ for (uint32_t i = mChildren.Length(); i--; ) {
+ nsITransaction *txn = mChildren[i];
+ if (!txn) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ nsresult rv = txn->UndoTransaction();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditAggregateTransaction::RedoTransaction()
+{
+ // It's legal (but not very useful) to have an empty child list.
+ for (uint32_t i = 0, length = mChildren.Length(); i < length; ++i) {
+ nsITransaction *txn = mChildren[i];
+ if (!txn) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ nsresult rv = txn->RedoTransaction();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditAggregateTransaction::Merge(nsITransaction* aTransaction,
+ bool* aDidMerge)
+{
+ if (aDidMerge) {
+ *aDidMerge = false;
+ }
+ if (mChildren.IsEmpty()) {
+ return NS_OK;
+ }
+ // FIXME: Is this really intended not to loop? It looks like the code
+ // that used to be here sort of intended to loop, but didn't.
+ nsITransaction *txn = mChildren[0];
+ if (!txn) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ return txn->Merge(aTransaction, aDidMerge);
+}
+
+NS_IMETHODIMP
+EditAggregateTransaction::GetTxnDescription(nsAString& aString)
+{
+ aString.AssignLiteral("EditAggregateTransaction: ");
+
+ if (mName) {
+ nsAutoString name;
+ mName->ToString(name);
+ aString += name;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditAggregateTransaction::AppendChild(EditTransactionBase* aTransaction)
+{
+ if (!aTransaction) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ RefPtr<EditTransactionBase>* slot = mChildren.AppendElement();
+ if (!slot) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ *slot = aTransaction;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditAggregateTransaction::GetName(nsIAtom** aName)
+{
+ if (aName && mName) {
+ *aName = mName;
+ NS_ADDREF(*aName);
+ return NS_OK;
+ }
+ return NS_ERROR_NULL_POINTER;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/EditAggregateTransaction.h b/editor/libeditor/EditAggregateTransaction.h
new file mode 100644
index 000000000..6ba27a01f
--- /dev/null
+++ b/editor/libeditor/EditAggregateTransaction.h
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef EditAggregateTransaction_h
+#define EditAggregateTransaction_h
+
+#include "mozilla/EditTransactionBase.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIAtom.h"
+#include "nsISupportsImpl.h"
+#include "nsTArray.h"
+#include "nscore.h"
+
+class nsITransaction;
+
+namespace mozilla {
+
+/**
+ * base class for all document editing transactions that require aggregation.
+ * provides a list of child transactions.
+ */
+class EditAggregateTransaction : public EditTransactionBase
+{
+public:
+ EditAggregateTransaction();
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(EditAggregateTransaction,
+ EditTransactionBase)
+
+ NS_DECL_EDITTRANSACTIONBASE
+
+ NS_IMETHOD RedoTransaction() override;
+ NS_IMETHOD Merge(nsITransaction* aTransaction, bool* aDidMerge) override;
+
+ /**
+ * Append a transaction to this aggregate.
+ */
+ NS_IMETHOD AppendChild(EditTransactionBase* aTransaction);
+
+ /**
+ * Get the name assigned to this transaction.
+ */
+ NS_IMETHOD GetName(nsIAtom** aName);
+
+protected:
+ virtual ~EditAggregateTransaction();
+
+ nsTArray<RefPtr<EditTransactionBase>> mChildren;
+ nsCOMPtr<nsIAtom> mName;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef EditAggregateTransaction_h
diff --git a/editor/libeditor/EditTransactionBase.cpp b/editor/libeditor/EditTransactionBase.cpp
new file mode 100644
index 000000000..2905baa8c
--- /dev/null
+++ b/editor/libeditor/EditTransactionBase.cpp
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/EditTransactionBase.h"
+#include "nsError.h"
+#include "nsISupportsBase.h"
+
+namespace mozilla {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(EditTransactionBase)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_0(EditTransactionBase)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(EditTransactionBase)
+ // We don't have anything to traverse, but some of our subclasses do.
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(EditTransactionBase)
+ NS_INTERFACE_MAP_ENTRY(nsITransaction)
+ NS_INTERFACE_MAP_ENTRY(nsPIEditorTransaction)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsITransaction)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(EditTransactionBase)
+NS_IMPL_CYCLE_COLLECTING_RELEASE_WITH_LAST_RELEASE(EditTransactionBase,
+ LastRelease())
+
+EditTransactionBase::~EditTransactionBase()
+{
+}
+
+NS_IMETHODIMP
+EditTransactionBase::RedoTransaction()
+{
+ return DoTransaction();
+}
+
+NS_IMETHODIMP
+EditTransactionBase::GetIsTransient(bool* aIsTransient)
+{
+ *aIsTransient = false;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditTransactionBase::Merge(nsITransaction* aTransaction, bool* aDidMerge)
+{
+ *aDidMerge = false;
+
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/EditTransactionBase.h b/editor/libeditor/EditTransactionBase.h
new file mode 100644
index 000000000..f09449f07
--- /dev/null
+++ b/editor/libeditor/EditTransactionBase.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_EditTransactionBase_h
+#define mozilla_EditTransactionBase_h
+
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupportsImpl.h"
+#include "nsITransaction.h"
+#include "nsPIEditorTransaction.h"
+#include "nscore.h"
+
+namespace mozilla {
+
+/**
+ * Base class for all document editing transactions.
+ */
+class EditTransactionBase : public nsITransaction
+ , public nsPIEditorTransaction
+{
+public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(EditTransactionBase, nsITransaction)
+
+ virtual void LastRelease() {}
+
+ NS_IMETHOD RedoTransaction(void) override;
+ NS_IMETHOD GetIsTransient(bool* aIsTransient) override;
+ NS_IMETHOD Merge(nsITransaction* aTransaction, bool* aDidMerge) override;
+
+protected:
+ virtual ~EditTransactionBase();
+};
+
+} // namespace mozilla
+
+#define NS_DECL_EDITTRANSACTIONBASE \
+ NS_IMETHOD DoTransaction() override; \
+ NS_IMETHOD UndoTransaction() override; \
+ NS_IMETHOD GetTxnDescription(nsAString& aTransactionDescription) override;
+
+#endif // #ifndef mozilla_EditTransactionBase_h
diff --git a/editor/libeditor/EditorBase.cpp b/editor/libeditor/EditorBase.cpp
new file mode 100644
index 000000000..13505b2d3
--- /dev/null
+++ b/editor/libeditor/EditorBase.cpp
@@ -0,0 +1,5244 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/EditorBase.h"
+
+#include "mozilla/DebugOnly.h" // for DebugOnly
+
+#include <stdio.h> // for nullptr, stdout
+#include <string.h> // for strcmp
+
+#include "ChangeAttributeTransaction.h" // for ChangeAttributeTransaction
+#include "CompositionTransaction.h" // for CompositionTransaction
+#include "CreateElementTransaction.h" // for CreateElementTransaction
+#include "DeleteNodeTransaction.h" // for DeleteNodeTransaction
+#include "DeleteRangeTransaction.h" // for DeleteRangeTransaction
+#include "DeleteTextTransaction.h" // for DeleteTextTransaction
+#include "EditAggregateTransaction.h" // for EditAggregateTransaction
+#include "EditorEventListener.h" // for EditorEventListener
+#include "InsertNodeTransaction.h" // for InsertNodeTransaction
+#include "InsertTextTransaction.h" // for InsertTextTransaction
+#include "JoinNodeTransaction.h" // for JoinNodeTransaction
+#include "PlaceholderTransaction.h" // for PlaceholderTransaction
+#include "SplitNodeTransaction.h" // for SplitNodeTransaction
+#include "StyleSheetTransactions.h" // for AddStyleSheetTransaction, etc.
+#include "TextEditUtils.h" // for TextEditUtils
+#include "mozFlushType.h" // for mozFlushType::Flush_Frames
+#include "mozInlineSpellChecker.h" // for mozInlineSpellChecker
+#include "mozilla/CheckedInt.h" // for CheckedInt
+#include "mozilla/EditorUtils.h" // for AutoRules, etc.
+#include "mozilla/EditTransactionBase.h" // for EditTransactionBase
+#include "mozilla/IMEStateManager.h" // for IMEStateManager
+#include "mozilla/Preferences.h" // for Preferences
+#include "mozilla/dom/Selection.h" // for Selection, etc.
+#include "mozilla/Services.h" // for GetObserverService
+#include "mozilla/TextComposition.h" // for TextComposition
+#include "mozilla/TextEvents.h"
+#include "mozilla/dom/Element.h" // for Element, nsINode::AsElement
+#include "mozilla/dom/Text.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/mozalloc.h" // for operator new, etc.
+#include "nsAString.h" // for nsAString_internal::Length, etc.
+#include "nsCCUncollectableMarker.h" // for nsCCUncollectableMarker
+#include "nsCaret.h" // for nsCaret
+#include "nsCaseTreatment.h"
+#include "nsCharTraits.h" // for NS_IS_HIGH_SURROGATE, etc.
+#include "nsComponentManagerUtils.h" // for do_CreateInstance
+#include "nsComputedDOMStyle.h" // for nsComputedDOMStyle
+#include "nsContentUtils.h" // for nsContentUtils
+#include "nsDOMString.h" // for DOMStringIsNull
+#include "nsDebug.h" // for NS_ENSURE_TRUE, etc.
+#include "nsError.h" // for NS_OK, etc.
+#include "nsFocusManager.h" // for nsFocusManager
+#include "nsFrameSelection.h" // for nsFrameSelection
+#include "nsGkAtoms.h" // for nsGkAtoms, nsGkAtoms::dir
+#include "nsIAbsorbingTransaction.h" // for nsIAbsorbingTransaction
+#include "nsIAtom.h" // for nsIAtom
+#include "nsIContent.h" // for nsIContent
+#include "nsIDOMAttr.h" // for nsIDOMAttr
+#include "nsIDOMCharacterData.h" // for nsIDOMCharacterData
+#include "nsIDOMDocument.h" // for nsIDOMDocument
+#include "nsIDOMElement.h" // for nsIDOMElement
+#include "nsIDOMEvent.h" // for nsIDOMEvent
+#include "nsIDOMEventListener.h" // for nsIDOMEventListener
+#include "nsIDOMEventTarget.h" // for nsIDOMEventTarget
+#include "nsIDOMHTMLElement.h" // for nsIDOMHTMLElement
+#include "nsIDOMKeyEvent.h" // for nsIDOMKeyEvent, etc.
+#include "nsIDOMMozNamedAttrMap.h" // for nsIDOMMozNamedAttrMap
+#include "nsIDOMMouseEvent.h" // for nsIDOMMouseEvent
+#include "nsIDOMNode.h" // for nsIDOMNode, etc.
+#include "nsIDOMNodeList.h" // for nsIDOMNodeList
+#include "nsIDOMText.h" // for nsIDOMText
+#include "nsIDocument.h" // for nsIDocument
+#include "nsIDocumentStateListener.h" // for nsIDocumentStateListener
+#include "nsIEditActionListener.h" // for nsIEditActionListener
+#include "nsIEditorObserver.h" // for nsIEditorObserver
+#include "nsIEditorSpellCheck.h" // for nsIEditorSpellCheck
+#include "nsIFrame.h" // for nsIFrame
+#include "nsIHTMLDocument.h" // for nsIHTMLDocument
+#include "nsIInlineSpellChecker.h" // for nsIInlineSpellChecker, etc.
+#include "nsNameSpaceManager.h" // for kNameSpaceID_None, etc.
+#include "nsINode.h" // for nsINode, etc.
+#include "nsIPlaintextEditor.h" // for nsIPlaintextEditor, etc.
+#include "nsIPresShell.h" // for nsIPresShell
+#include "nsISelectionController.h" // for nsISelectionController, etc.
+#include "nsISelectionDisplay.h" // for nsISelectionDisplay, etc.
+#include "nsISupportsBase.h" // for nsISupports
+#include "nsISupportsUtils.h" // for NS_ADDREF, NS_IF_ADDREF
+#include "nsITransaction.h" // for nsITransaction
+#include "nsITransactionManager.h"
+#include "nsIWeakReference.h" // for nsISupportsWeakReference
+#include "nsIWidget.h" // for nsIWidget, IMEState, etc.
+#include "nsPIDOMWindow.h" // for nsPIDOMWindow
+#include "nsPresContext.h" // for nsPresContext
+#include "nsRange.h" // for nsRange
+#include "nsReadableUtils.h" // for EmptyString, ToNewCString
+#include "nsString.h" // for nsAutoString, nsString, etc.
+#include "nsStringFwd.h" // for nsAFlatString
+#include "nsStyleConsts.h" // for NS_STYLE_DIRECTION_RTL, etc.
+#include "nsStyleContext.h" // for nsStyleContext
+#include "nsStyleStruct.h" // for nsStyleDisplay, nsStyleText, etc.
+#include "nsStyleStructFwd.h" // for nsIFrame::StyleUIReset, etc.
+#include "nsTextNode.h" // for nsTextNode
+#include "nsThreadUtils.h" // for nsRunnable
+#include "nsTransactionManager.h" // for nsTransactionManager
+#include "prtime.h" // for PR_Now
+
+class nsIOutputStream;
+class nsIParserService;
+class nsITransferable;
+
+#ifdef DEBUG
+#include "nsIDOMHTMLDocument.h" // for nsIDOMHTMLDocument
+#endif
+
+// Defined in nsEditorRegistration.cpp
+extern nsIParserService *sParserService;
+
+namespace mozilla {
+
+using namespace dom;
+using namespace widget;
+
+/*****************************************************************************
+ * mozilla::EditorBase
+ *****************************************************************************/
+
+EditorBase::EditorBase()
+ : mPlaceHolderName(nullptr)
+ , mSelState(nullptr)
+ , mPhonetic(nullptr)
+ , mModCount(0)
+ , mFlags(0)
+ , mUpdateCount(0)
+ , mPlaceHolderBatch(0)
+ , mAction(EditAction::none)
+ , mIMETextOffset(0)
+ , mIMETextLength(0)
+ , mDirection(eNone)
+ , mDocDirtyState(-1)
+ , mSpellcheckCheckboxState(eTriUnset)
+ , mShouldTxnSetSelection(true)
+ , mDidPreDestroy(false)
+ , mDidPostCreate(false)
+ , mDispatchInputEvent(true)
+ , mIsInEditAction(false)
+ , mHidingCaret(false)
+{
+}
+
+EditorBase::~EditorBase()
+{
+ NS_ASSERTION(!mDocWeak || mDidPreDestroy, "Why PreDestroy hasn't been called?");
+
+ if (mComposition) {
+ mComposition->OnEditorDestroyed();
+ mComposition = nullptr;
+ }
+ // If this editor is still hiding the caret, we need to restore it.
+ HideCaret(false);
+ mTxnMgr = nullptr;
+
+ delete mPhonetic;
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(EditorBase)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(EditorBase)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mRootElement)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mInlineSpellChecker)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mTxnMgr)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mIMETextNode)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mActionListeners)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mEditorObservers)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocStateListeners)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mEventTarget)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mEventListener)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mSavedSel);
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mRangeUpdater);
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(EditorBase)
+ nsIDocument* currentDoc =
+ tmp->mRootElement ? tmp->mRootElement->GetUncomposedDoc() : nullptr;
+ if (currentDoc &&
+ nsCCUncollectableMarker::InGeneration(cb, currentDoc->GetMarkedCCGeneration())) {
+ return NS_SUCCESS_INTERRUPTED_TRAVERSE;
+ }
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRootElement)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInlineSpellChecker)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTxnMgr)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIMETextNode)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mActionListeners)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEditorObservers)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocStateListeners)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEventTarget)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEventListener)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSavedSel);
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRangeUpdater);
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(EditorBase)
+ NS_INTERFACE_MAP_ENTRY(nsIPhonetic)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+ NS_INTERFACE_MAP_ENTRY(nsIEditorIMESupport)
+ NS_INTERFACE_MAP_ENTRY(nsIEditor)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIEditor)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(EditorBase)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(EditorBase)
+
+
+NS_IMETHODIMP
+EditorBase::Init(nsIDOMDocument* aDoc,
+ nsIContent* aRoot,
+ nsISelectionController* aSelCon,
+ uint32_t aFlags,
+ const nsAString& aValue)
+{
+ MOZ_ASSERT(mAction == EditAction::none,
+ "Initializing during an edit action is an error");
+ MOZ_ASSERT(aDoc);
+ if (!aDoc)
+ return NS_ERROR_NULL_POINTER;
+
+ // First only set flags, but other stuff shouldn't be initialized now.
+ // Don't move this call after initializing mDocWeak.
+ // SetFlags() can check whether it's called during initialization or not by
+ // them. Note that SetFlags() will be called by PostCreate().
+#ifdef DEBUG
+ nsresult rv =
+#endif
+ SetFlags(aFlags);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "SetFlags() failed");
+
+ mDocWeak = do_GetWeakReference(aDoc); // weak reference to doc
+ // HTML editors currently don't have their own selection controller,
+ // so they'll pass null as aSelCon, and we'll get the selection controller
+ // off of the presshell.
+ nsCOMPtr<nsISelectionController> selCon;
+ if (aSelCon) {
+ mSelConWeak = do_GetWeakReference(aSelCon); // weak reference to selectioncontroller
+ selCon = aSelCon;
+ } else {
+ nsCOMPtr<nsIPresShell> presShell = GetPresShell();
+ selCon = do_QueryInterface(presShell);
+ }
+ NS_ASSERTION(selCon, "Selection controller should be available at this point");
+
+ //set up root element if we are passed one.
+ if (aRoot)
+ mRootElement = do_QueryInterface(aRoot);
+
+ mUpdateCount=0;
+
+ // If this is an editor for <input> or <textarea>, mIMETextNode is always
+ // recreated with same content. Therefore, we need to forget mIMETextNode,
+ // but we need to keep storing mIMETextOffset and mIMETextLength becuase
+ // they are necessary to restore IME selection and replacing composing string
+ // when this receives eCompositionChange event next time.
+ if (mIMETextNode && !mIMETextNode->IsInComposedDoc()) {
+ mIMETextNode = nullptr;
+ }
+
+ /* Show the caret */
+ selCon->SetCaretReadOnly(false);
+ selCon->SetDisplaySelection(nsISelectionController::SELECTION_ON);
+
+ selCon->SetSelectionFlags(nsISelectionDisplay::DISPLAY_ALL);//we want to see all the selection reflected to user
+
+ NS_POSTCONDITION(mDocWeak, "bad state");
+
+ // Make sure that the editor will be destroyed properly
+ mDidPreDestroy = false;
+ // Make sure that the ediotr will be created properly
+ mDidPostCreate = false;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::PostCreate()
+{
+ // Synchronize some stuff for the flags. SetFlags() will initialize
+ // something by the flag difference. This is first time of that, so, all
+ // initializations must be run. For such reason, we need to invert mFlags
+ // value first.
+ mFlags = ~mFlags;
+ nsresult rv = SetFlags(~mFlags);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // These operations only need to happen on the first PostCreate call
+ if (!mDidPostCreate) {
+ mDidPostCreate = true;
+
+ // Set up listeners
+ CreateEventListeners();
+ rv = InstallEventListeners();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // nuke the modification count, so the doc appears unmodified
+ // do this before we notify listeners
+ ResetModificationCount();
+
+ // update the UI with our state
+ NotifyDocumentListeners(eDocumentCreated);
+ NotifyDocumentListeners(eDocumentStateChanged);
+ }
+
+ // update nsTextStateManager and caret if we have focus
+ nsCOMPtr<nsIContent> focusedContent = GetFocusedContent();
+ if (focusedContent) {
+ nsCOMPtr<nsIDOMEventTarget> target = do_QueryInterface(focusedContent);
+ if (target) {
+ InitializeSelection(target);
+ }
+
+ // If the text control gets reframed during focus, Focus() would not be
+ // called, so take a chance here to see if we need to spell check the text
+ // control.
+ EditorEventListener* listener =
+ reinterpret_cast<EditorEventListener*>(mEventListener.get());
+ listener->SpellCheckIfNeeded();
+
+ IMEState newState;
+ rv = GetPreferredIMEState(&newState);
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+ nsCOMPtr<nsIContent> content = GetFocusedContentForIME();
+ IMEStateManager::UpdateIMEState(newState, content, *this);
+ }
+
+ // FYI: This call might cause destroying this editor.
+ IMEStateManager::OnEditorInitialized(this);
+
+ return NS_OK;
+}
+
+void
+EditorBase::CreateEventListeners()
+{
+ // Don't create the handler twice
+ if (!mEventListener) {
+ mEventListener = new EditorEventListener();
+ }
+}
+
+nsresult
+EditorBase::InstallEventListeners()
+{
+ NS_ENSURE_TRUE(mDocWeak && mEventListener,
+ NS_ERROR_NOT_INITIALIZED);
+
+ // Initialize the event target.
+ nsCOMPtr<nsIContent> rootContent = GetRoot();
+ NS_ENSURE_TRUE(rootContent, NS_ERROR_NOT_AVAILABLE);
+ mEventTarget = do_QueryInterface(rootContent->GetParent());
+ NS_ENSURE_TRUE(mEventTarget, NS_ERROR_NOT_AVAILABLE);
+
+ EditorEventListener* listener =
+ reinterpret_cast<EditorEventListener*>(mEventListener.get());
+ nsresult rv = listener->Connect(this);
+ if (mComposition) {
+ // Restart to handle composition with new editor contents.
+ mComposition->StartHandlingComposition(this);
+ }
+ return rv;
+}
+
+void
+EditorBase::RemoveEventListeners()
+{
+ if (!mDocWeak || !mEventListener) {
+ return;
+ }
+ reinterpret_cast<EditorEventListener*>(mEventListener.get())->Disconnect();
+ if (mComposition) {
+ // Even if this is called, don't release mComposition because this is
+ // may be reused after reframing.
+ mComposition->EndHandlingComposition(this);
+ }
+ mEventTarget = nullptr;
+}
+
+bool
+EditorBase::GetDesiredSpellCheckState()
+{
+ // Check user override on this element
+ if (mSpellcheckCheckboxState != eTriUnset) {
+ return (mSpellcheckCheckboxState == eTriTrue);
+ }
+
+ // Check user preferences
+ int32_t spellcheckLevel = Preferences::GetInt("layout.spellcheckDefault", 1);
+
+ if (!spellcheckLevel) {
+ return false; // Spellchecking forced off globally
+ }
+
+ if (!CanEnableSpellCheck()) {
+ return false;
+ }
+
+ nsCOMPtr<nsIPresShell> presShell = GetPresShell();
+ if (presShell) {
+ nsPresContext* context = presShell->GetPresContext();
+ if (context && !context->IsDynamic()) {
+ return false;
+ }
+ }
+
+ // Check DOM state
+ nsCOMPtr<nsIContent> content = GetExposedRoot();
+ if (!content) {
+ return false;
+ }
+
+ nsCOMPtr<nsIDOMHTMLElement> element = do_QueryInterface(content);
+ if (!element) {
+ return false;
+ }
+
+ if (!IsPlaintextEditor()) {
+ // Some of the page content might be editable and some not, if spellcheck=
+ // is explicitly set anywhere, so if there's anything editable on the page,
+ // return true and let the spellchecker figure it out.
+ nsCOMPtr<nsIHTMLDocument> doc = do_QueryInterface(content->GetUncomposedDoc());
+ return doc && doc->IsEditingOn();
+ }
+
+ bool enable;
+ element->GetSpellcheck(&enable);
+
+ return enable;
+}
+
+NS_IMETHODIMP
+EditorBase::PreDestroy(bool aDestroyingFrames)
+{
+ if (mDidPreDestroy)
+ return NS_OK;
+
+ IMEStateManager::OnEditorDestroying(this);
+
+ // Let spellchecker clean up its observers etc. It is important not to
+ // actually free the spellchecker here, since the spellchecker could have
+ // caused flush notifications, which could have gotten here if a textbox
+ // is being removed. Setting the spellchecker to nullptr could free the
+ // object that is still in use! It will be freed when the editor is
+ // destroyed.
+ if (mInlineSpellChecker)
+ mInlineSpellChecker->Cleanup(aDestroyingFrames);
+
+ // tell our listeners that the doc is going away
+ NotifyDocumentListeners(eDocumentToBeDestroyed);
+
+ // Unregister event listeners
+ RemoveEventListeners();
+ // If this editor is still hiding the caret, we need to restore it.
+ HideCaret(false);
+ mActionListeners.Clear();
+ mEditorObservers.Clear();
+ mDocStateListeners.Clear();
+ mInlineSpellChecker = nullptr;
+ mSpellcheckCheckboxState = eTriUnset;
+ mRootElement = nullptr;
+
+ mDidPreDestroy = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::GetFlags(uint32_t* aFlags)
+{
+ *aFlags = mFlags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::SetFlags(uint32_t aFlags)
+{
+ if (mFlags == aFlags) {
+ return NS_OK;
+ }
+
+ bool spellcheckerWasEnabled = CanEnableSpellCheck();
+ mFlags = aFlags;
+
+ if (!mDocWeak) {
+ // If we're initializing, we shouldn't do anything now.
+ // SetFlags() will be called by PostCreate(),
+ // we should synchronize some stuff for the flags at that time.
+ return NS_OK;
+ }
+
+ // The flag change may cause the spellchecker state change
+ if (CanEnableSpellCheck() != spellcheckerWasEnabled) {
+ nsresult rv = SyncRealTimeSpell();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // If this is called from PostCreate(), it will update the IME state if it's
+ // necessary.
+ if (!mDidPostCreate) {
+ return NS_OK;
+ }
+
+ // Might be changing editable state, so, we need to reset current IME state
+ // if we're focused and the flag change causes IME state change.
+ nsCOMPtr<nsIContent> focusedContent = GetFocusedContent();
+ if (focusedContent) {
+ IMEState newState;
+ nsresult rv = GetPreferredIMEState(&newState);
+ if (NS_SUCCEEDED(rv)) {
+ // NOTE: When the enabled state isn't going to be modified, this method
+ // is going to do nothing.
+ nsCOMPtr<nsIContent> content = GetFocusedContentForIME();
+ IMEStateManager::UpdateIMEState(newState, content, *this);
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::GetIsSelectionEditable(bool* aIsSelectionEditable)
+{
+ NS_ENSURE_ARG_POINTER(aIsSelectionEditable);
+
+ // get current selection
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+
+ // XXX we just check that the anchor node is editable at the moment
+ // we should check that all nodes in the selection are editable
+ nsCOMPtr<nsINode> anchorNode = selection->GetAnchorNode();
+ *aIsSelectionEditable = anchorNode && IsEditable(anchorNode);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::GetIsDocumentEditable(bool* aIsDocumentEditable)
+{
+ NS_ENSURE_ARG_POINTER(aIsDocumentEditable);
+ nsCOMPtr<nsIDocument> doc = GetDocument();
+ *aIsDocumentEditable = !!doc;
+
+ return NS_OK;
+}
+
+already_AddRefed<nsIDocument>
+EditorBase::GetDocument()
+{
+ NS_PRECONDITION(mDocWeak, "bad state, mDocWeak weak pointer not initialized");
+ nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak);
+ return doc.forget();
+}
+
+already_AddRefed<nsIDOMDocument>
+EditorBase::GetDOMDocument()
+{
+ NS_PRECONDITION(mDocWeak, "bad state, mDocWeak weak pointer not initialized");
+ nsCOMPtr<nsIDOMDocument> doc = do_QueryReferent(mDocWeak);
+ return doc.forget();
+}
+
+NS_IMETHODIMP
+EditorBase::GetDocument(nsIDOMDocument** aDoc)
+{
+ *aDoc = GetDOMDocument().take();
+ return *aDoc ? NS_OK : NS_ERROR_NOT_INITIALIZED;
+}
+
+already_AddRefed<nsIPresShell>
+EditorBase::GetPresShell()
+{
+ NS_PRECONDITION(mDocWeak, "bad state, null mDocWeak");
+ nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak);
+ NS_ENSURE_TRUE(doc, nullptr);
+ nsCOMPtr<nsIPresShell> ps = doc->GetShell();
+ return ps.forget();
+}
+
+already_AddRefed<nsIWidget>
+EditorBase::GetWidget()
+{
+ nsCOMPtr<nsIPresShell> ps = GetPresShell();
+ NS_ENSURE_TRUE(ps, nullptr);
+ nsPresContext* pc = ps->GetPresContext();
+ NS_ENSURE_TRUE(pc, nullptr);
+ nsCOMPtr<nsIWidget> widget = pc->GetRootWidget();
+ NS_ENSURE_TRUE(widget.get(), nullptr);
+ return widget.forget();
+}
+
+NS_IMETHODIMP
+EditorBase::GetContentsMIMEType(char** aContentsMIMEType)
+{
+ NS_ENSURE_ARG_POINTER(aContentsMIMEType);
+ *aContentsMIMEType = ToNewCString(mContentMIMEType);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::SetContentsMIMEType(const char* aContentsMIMEType)
+{
+ mContentMIMEType.Assign(aContentsMIMEType ? aContentsMIMEType : "");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::GetSelectionController(nsISelectionController** aSel)
+{
+ NS_ENSURE_TRUE(aSel, NS_ERROR_NULL_POINTER);
+ *aSel = nullptr; // init out param
+ nsCOMPtr<nsISelectionController> selCon;
+ if (mSelConWeak) {
+ selCon = do_QueryReferent(mSelConWeak);
+ } else {
+ nsCOMPtr<nsIPresShell> presShell = GetPresShell();
+ selCon = do_QueryInterface(presShell);
+ }
+ if (!selCon) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ NS_ADDREF(*aSel = selCon);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::DeleteSelection(EDirection aAction,
+ EStripWrappers aStripWrappers)
+{
+ MOZ_ASSERT(aStripWrappers == eStrip || aStripWrappers == eNoStrip);
+ return DeleteSelectionImpl(aAction, aStripWrappers);
+}
+
+NS_IMETHODIMP
+EditorBase::GetSelection(nsISelection** aSelection)
+{
+ return GetSelection(SelectionType::eNormal, aSelection);
+}
+
+nsresult
+EditorBase::GetSelection(SelectionType aSelectionType,
+ nsISelection** aSelection)
+{
+ NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER);
+ *aSelection = nullptr;
+ nsCOMPtr<nsISelectionController> selcon;
+ GetSelectionController(getter_AddRefs(selcon));
+ if (!selcon) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ return selcon->GetSelection(ToRawSelectionType(aSelectionType), aSelection);
+}
+
+Selection*
+EditorBase::GetSelection(SelectionType aSelectionType)
+{
+ nsCOMPtr<nsISelection> sel;
+ nsresult rv = GetSelection(aSelectionType, getter_AddRefs(sel));
+ if (NS_WARN_IF(NS_FAILED(rv)) || NS_WARN_IF(!sel)) {
+ return nullptr;
+ }
+
+ return sel->AsSelection();
+}
+
+NS_IMETHODIMP
+EditorBase::DoTransaction(nsITransaction* aTxn)
+{
+ if (mPlaceHolderBatch && !mPlaceHolderTxn) {
+ nsCOMPtr<nsIAbsorbingTransaction> placeholderTransaction =
+ new PlaceholderTransaction();
+
+ // Save off weak reference to placeholder transaction
+ mPlaceHolderTxn = do_GetWeakReference(placeholderTransaction);
+ placeholderTransaction->Init(mPlaceHolderName, mSelState, this);
+ // placeholder txn took ownership of this pointer
+ mSelState = nullptr;
+
+ // QI to an nsITransaction since that's what DoTransaction() expects
+ nsCOMPtr<nsITransaction> transaction =
+ do_QueryInterface(placeholderTransaction);
+ // We will recurse, but will not hit this case in the nested call
+ DoTransaction(transaction);
+
+ if (mTxnMgr) {
+ nsCOMPtr<nsITransaction> topTxn = mTxnMgr->PeekUndoStack();
+ if (topTxn) {
+ placeholderTransaction = do_QueryInterface(topTxn);
+ if (placeholderTransaction) {
+ // there is a placeholder transaction on top of the undo stack. It
+ // is either the one we just created, or an earlier one that we are
+ // now merging into. From here on out remember this placeholder
+ // instead of the one we just created.
+ mPlaceHolderTxn = do_GetWeakReference(placeholderTransaction);
+ }
+ }
+ }
+ }
+
+ if (aTxn) {
+ // XXX: Why are we doing selection specific batching stuff here?
+ // XXX: Most entry points into the editor have auto variables that
+ // XXX: should trigger Begin/EndUpdateViewBatch() calls that will make
+ // XXX: these selection batch calls no-ops.
+ // XXX:
+ // XXX: I suspect that this was placed here to avoid multiple
+ // XXX: selection changed notifications from happening until after
+ // XXX: the transaction was done. I suppose that can still happen
+ // XXX: if an embedding application called DoTransaction() directly
+ // XXX: to pump its own transactions through the system, but in that
+ // XXX: case, wouldn't we want to use Begin/EndUpdateViewBatch() or
+ // XXX: its auto equivalent AutoUpdateViewBatch to ensure that
+ // XXX: selection listeners have access to accurate frame data?
+ // XXX:
+ // XXX: Note that if we did add Begin/EndUpdateViewBatch() calls
+ // XXX: we will need to make sure that they are disabled during
+ // XXX: the init of the editor for text widgets to avoid layout
+ // XXX: re-entry during initial reflow. - kin
+
+ // get the selection and start a batch change
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+
+ selection->StartBatchChanges();
+
+ nsresult rv;
+ if (mTxnMgr) {
+ RefPtr<nsTransactionManager> txnMgr = mTxnMgr;
+ rv = txnMgr->DoTransaction(aTxn);
+ } else {
+ rv = aTxn->DoTransaction();
+ }
+ if (NS_SUCCEEDED(rv)) {
+ DoAfterDoTransaction(aTxn);
+ }
+
+ // no need to check rv here, don't lose result of operation
+ selection->EndBatchChanges();
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::EnableUndo(bool aEnable)
+{
+ if (aEnable) {
+ if (!mTxnMgr) {
+ mTxnMgr = new nsTransactionManager();
+ }
+ mTxnMgr->SetMaxTransactionCount(-1);
+ } else if (mTxnMgr) {
+ // disable the transaction manager if it is enabled
+ mTxnMgr->Clear();
+ mTxnMgr->SetMaxTransactionCount(0);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::GetNumberOfUndoItems(int32_t* aNumItems)
+{
+ *aNumItems = 0;
+ return mTxnMgr ? mTxnMgr->GetNumberOfUndoItems(aNumItems) : NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::GetNumberOfRedoItems(int32_t* aNumItems)
+{
+ *aNumItems = 0;
+ return mTxnMgr ? mTxnMgr->GetNumberOfRedoItems(aNumItems) : NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::GetTransactionManager(nsITransactionManager** aTxnManager)
+{
+ NS_ENSURE_ARG_POINTER(aTxnManager);
+
+ *aTxnManager = nullptr;
+ NS_ENSURE_TRUE(mTxnMgr, NS_ERROR_FAILURE);
+
+ NS_ADDREF(*aTxnManager = mTxnMgr);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::SetTransactionManager(nsITransactionManager* aTxnManager)
+{
+ NS_ENSURE_TRUE(aTxnManager, NS_ERROR_FAILURE);
+
+ // nsITransactionManager is builtinclass, so this is safe
+ mTxnMgr = static_cast<nsTransactionManager*>(aTxnManager);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::Undo(uint32_t aCount)
+{
+ ForceCompositionEnd();
+
+ bool hasTxnMgr, hasTransaction = false;
+ CanUndo(&hasTxnMgr, &hasTransaction);
+ NS_ENSURE_TRUE(hasTransaction, NS_OK);
+
+ AutoRules beginRulesSniffing(this, EditAction::undo, nsIEditor::eNone);
+
+ if (!mTxnMgr) {
+ return NS_OK;
+ }
+
+ RefPtr<nsTransactionManager> txnMgr = mTxnMgr;
+ for (uint32_t i = 0; i < aCount; ++i) {
+ nsresult rv = txnMgr->UndoTransaction();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ DoAfterUndoTransaction();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::CanUndo(bool* aIsEnabled,
+ bool* aCanUndo)
+{
+ NS_ENSURE_TRUE(aIsEnabled && aCanUndo, NS_ERROR_NULL_POINTER);
+ *aIsEnabled = !!mTxnMgr;
+ if (*aIsEnabled) {
+ int32_t numTxns = 0;
+ mTxnMgr->GetNumberOfUndoItems(&numTxns);
+ *aCanUndo = !!numTxns;
+ } else {
+ *aCanUndo = false;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::Redo(uint32_t aCount)
+{
+ bool hasTxnMgr, hasTransaction = false;
+ CanRedo(&hasTxnMgr, &hasTransaction);
+ NS_ENSURE_TRUE(hasTransaction, NS_OK);
+
+ AutoRules beginRulesSniffing(this, EditAction::redo, nsIEditor::eNone);
+
+ if (!mTxnMgr) {
+ return NS_OK;
+ }
+
+ RefPtr<nsTransactionManager> txnMgr = mTxnMgr;
+ for (uint32_t i = 0; i < aCount; ++i) {
+ nsresult rv = txnMgr->RedoTransaction();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ DoAfterRedoTransaction();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::CanRedo(bool* aIsEnabled, bool* aCanRedo)
+{
+ NS_ENSURE_TRUE(aIsEnabled && aCanRedo, NS_ERROR_NULL_POINTER);
+
+ *aIsEnabled = !!mTxnMgr;
+ if (*aIsEnabled) {
+ int32_t numTxns = 0;
+ mTxnMgr->GetNumberOfRedoItems(&numTxns);
+ *aCanRedo = !!numTxns;
+ } else {
+ *aCanRedo = false;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::BeginTransaction()
+{
+ BeginUpdateViewBatch();
+
+ if (mTxnMgr) {
+ RefPtr<nsTransactionManager> txnMgr = mTxnMgr;
+ txnMgr->BeginBatch(nullptr);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::EndTransaction()
+{
+ if (mTxnMgr) {
+ RefPtr<nsTransactionManager> txnMgr = mTxnMgr;
+ txnMgr->EndBatch(false);
+ }
+
+ EndUpdateViewBatch();
+
+ return NS_OK;
+}
+
+
+// These two routines are similar to the above, but do not use
+// the transaction managers batching feature. Instead we use
+// a placeholder transaction to wrap up any further transaction
+// while the batch is open. The advantage of this is that
+// placeholder transactions can later merge, if needed. Merging
+// is unavailable between transaction manager batches.
+
+NS_IMETHODIMP
+EditorBase::BeginPlaceHolderTransaction(nsIAtom* aName)
+{
+ NS_PRECONDITION(mPlaceHolderBatch >= 0, "negative placeholder batch count!");
+ if (!mPlaceHolderBatch) {
+ NotifyEditorObservers(eNotifyEditorObserversOfBefore);
+ // time to turn on the batch
+ BeginUpdateViewBatch();
+ mPlaceHolderTxn = nullptr;
+ mPlaceHolderName = aName;
+ RefPtr<Selection> selection = GetSelection();
+ if (selection) {
+ mSelState = new SelectionState();
+ mSelState->SaveSelection(selection);
+ // Composition transaction can modify multiple nodes and it merges text
+ // node for ime into single text node.
+ // So if current selection is into IME text node, it might be failed
+ // to restore selection by UndoTransaction.
+ // So we need update selection by range updater.
+ if (mPlaceHolderName == nsGkAtoms::IMETxnName) {
+ mRangeUpdater.RegisterSelectionState(*mSelState);
+ }
+ }
+ }
+ mPlaceHolderBatch++;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::EndPlaceHolderTransaction()
+{
+ NS_PRECONDITION(mPlaceHolderBatch > 0, "zero or negative placeholder batch count when ending batch!");
+ if (mPlaceHolderBatch == 1) {
+ RefPtr<Selection> selection = GetSelection();
+
+ // By making the assumption that no reflow happens during the calls
+ // to EndUpdateViewBatch and ScrollSelectionIntoView, we are able to
+ // allow the selection to cache a frame offset which is used by the
+ // caret drawing code. We only enable this cache here; at other times,
+ // we have no way to know whether reflow invalidates it
+ // See bugs 35296 and 199412.
+ if (selection) {
+ selection->SetCanCacheFrameOffset(true);
+ }
+
+ {
+ // Hide the caret here to avoid hiding it twice, once in EndUpdateViewBatch
+ // and once in ScrollSelectionIntoView.
+ RefPtr<nsCaret> caret;
+ nsCOMPtr<nsIPresShell> presShell = GetPresShell();
+
+ if (presShell) {
+ caret = presShell->GetCaret();
+ }
+
+ // time to turn off the batch
+ EndUpdateViewBatch();
+ // make sure selection is in view
+
+ // After ScrollSelectionIntoView(), the pending notifications might be
+ // flushed and PresShell/PresContext/Frames may be dead. See bug 418470.
+ ScrollSelectionIntoView(false);
+ }
+
+ // cached for frame offset are Not available now
+ if (selection) {
+ selection->SetCanCacheFrameOffset(false);
+ }
+
+ if (mSelState) {
+ // we saved the selection state, but never got to hand it to placeholder
+ // (else we ould have nulled out this pointer), so destroy it to prevent leaks.
+ if (mPlaceHolderName == nsGkAtoms::IMETxnName) {
+ mRangeUpdater.DropSelectionState(*mSelState);
+ }
+ delete mSelState;
+ mSelState = nullptr;
+ }
+ // We might have never made a placeholder if no action took place.
+ if (mPlaceHolderTxn) {
+ nsCOMPtr<nsIAbsorbingTransaction> plcTxn = do_QueryReferent(mPlaceHolderTxn);
+ if (plcTxn) {
+ plcTxn->EndPlaceHolderBatch();
+ } else {
+ // in the future we will check to make sure undo is off here,
+ // since that is the only known case where the placeholdertxn would disappear on us.
+ // For now just removing the assert.
+ }
+ // notify editor observers of action but if composing, it's done by
+ // compositionchange event handler.
+ if (!mComposition) {
+ NotifyEditorObservers(eNotifyEditorObserversOfEnd);
+ }
+ } else {
+ NotifyEditorObservers(eNotifyEditorObserversOfCancel);
+ }
+ }
+ mPlaceHolderBatch--;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::ShouldTxnSetSelection(bool* aResult)
+{
+ NS_ENSURE_TRUE(aResult, NS_ERROR_NULL_POINTER);
+ *aResult = mShouldTxnSetSelection;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::SetShouldTxnSetSelection(bool aShould)
+{
+ mShouldTxnSetSelection = aShould;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::GetDocumentIsEmpty(bool* aDocumentIsEmpty)
+{
+ *aDocumentIsEmpty = true;
+
+ dom::Element* root = GetRoot();
+ NS_ENSURE_TRUE(root, NS_ERROR_NULL_POINTER);
+
+ *aDocumentIsEmpty = !root->HasChildren();
+ return NS_OK;
+}
+
+// XXX: The rule system should tell us which node to select all on (ie, the
+// root, or the body)
+NS_IMETHODIMP
+EditorBase::SelectAll()
+{
+ if (!mDocWeak) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ ForceCompositionEnd();
+
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NOT_INITIALIZED);
+ return SelectEntireDocument(selection);
+}
+
+NS_IMETHODIMP
+EditorBase::BeginningOfDocument()
+{
+ if (!mDocWeak) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // get the selection
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NOT_INITIALIZED);
+
+ // get the root element
+ dom::Element* rootElement = GetRoot();
+ NS_ENSURE_TRUE(rootElement, NS_ERROR_NULL_POINTER);
+
+ // find first editable thingy
+ nsCOMPtr<nsINode> firstNode = GetFirstEditableNode(rootElement);
+ if (!firstNode) {
+ // just the root node, set selection to inside the root
+ return selection->CollapseNative(rootElement, 0);
+ }
+
+ if (firstNode->NodeType() == nsIDOMNode::TEXT_NODE) {
+ // If firstNode is text, set selection to beginning of the text node.
+ return selection->CollapseNative(firstNode, 0);
+ }
+
+ // Otherwise, it's a leaf node and we set the selection just in front of it.
+ nsCOMPtr<nsIContent> parent = firstNode->GetParent();
+ if (!parent) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ int32_t offsetInParent = parent->IndexOf(firstNode);
+ return selection->CollapseNative(parent, offsetInParent);
+}
+
+NS_IMETHODIMP
+EditorBase::EndOfDocument()
+{
+ NS_ENSURE_TRUE(mDocWeak, NS_ERROR_NOT_INITIALIZED);
+
+ // get selection
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+
+ // get the root element
+ nsINode* node = GetRoot();
+ NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER);
+ nsINode* child = node->GetLastChild();
+
+ while (child && IsContainer(child->AsDOMNode())) {
+ node = child;
+ child = node->GetLastChild();
+ }
+
+ uint32_t length = node->Length();
+ return selection->CollapseNative(node, int32_t(length));
+}
+
+NS_IMETHODIMP
+EditorBase::GetDocumentModified(bool* outDocModified)
+{
+ NS_ENSURE_TRUE(outDocModified, NS_ERROR_NULL_POINTER);
+
+ int32_t modCount = 0;
+ GetModificationCount(&modCount);
+
+ *outDocModified = (modCount != 0);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::GetDocumentCharacterSet(nsACString& characterSet)
+{
+ nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak);
+ NS_ENSURE_TRUE(doc, NS_ERROR_UNEXPECTED);
+
+ characterSet = doc->GetDocumentCharacterSet();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::SetDocumentCharacterSet(const nsACString& characterSet)
+{
+ nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak);
+ NS_ENSURE_TRUE(doc, NS_ERROR_UNEXPECTED);
+
+ doc->SetDocumentCharacterSet(characterSet);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::Cut()
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+EditorBase::CanCut(bool* aCanCut)
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+EditorBase::Copy()
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+EditorBase::CanCopy(bool* aCanCut)
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+EditorBase::CanDelete(bool* aCanDelete)
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+EditorBase::Paste(int32_t aSelectionType)
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+EditorBase::PasteTransferable(nsITransferable* aTransferable)
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+EditorBase::CanPaste(int32_t aSelectionType, bool* aCanPaste)
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+EditorBase::CanPasteTransferable(nsITransferable* aTransferable,
+ bool* aCanPaste)
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+EditorBase::SetAttribute(nsIDOMElement* aElement,
+ const nsAString& aAttribute,
+ const nsAString& aValue)
+{
+ nsCOMPtr<Element> element = do_QueryInterface(aElement);
+ NS_ENSURE_TRUE(element, NS_ERROR_NULL_POINTER);
+ nsCOMPtr<nsIAtom> attribute = NS_Atomize(aAttribute);
+
+ RefPtr<ChangeAttributeTransaction> transaction =
+ CreateTxnForSetAttribute(*element, *attribute, aValue);
+ return DoTransaction(transaction);
+}
+
+NS_IMETHODIMP
+EditorBase::GetAttributeValue(nsIDOMElement* aElement,
+ const nsAString& aAttribute,
+ nsAString& aResultValue,
+ bool* aResultIsSet)
+{
+ NS_ENSURE_TRUE(aResultIsSet, NS_ERROR_NULL_POINTER);
+ *aResultIsSet = false;
+ if (!aElement) {
+ return NS_OK;
+ }
+ nsAutoString value;
+ nsresult rv = aElement->GetAttribute(aAttribute, value);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!DOMStringIsNull(value)) {
+ *aResultIsSet = true;
+ aResultValue = value;
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+EditorBase::RemoveAttribute(nsIDOMElement* aElement,
+ const nsAString& aAttribute)
+{
+ nsCOMPtr<Element> element = do_QueryInterface(aElement);
+ NS_ENSURE_TRUE(element, NS_ERROR_NULL_POINTER);
+ nsCOMPtr<nsIAtom> attribute = NS_Atomize(aAttribute);
+
+ RefPtr<ChangeAttributeTransaction> transaction =
+ CreateTxnForRemoveAttribute(*element, *attribute);
+ return DoTransaction(transaction);
+}
+
+bool
+EditorBase::OutputsMozDirty()
+{
+ // Return true for Composer (!eEditorAllowInteraction) or mail
+ // (eEditorMailMask), but false for webpages.
+ return !(mFlags & nsIPlaintextEditor::eEditorAllowInteraction) ||
+ (mFlags & nsIPlaintextEditor::eEditorMailMask);
+}
+
+NS_IMETHODIMP
+EditorBase::MarkNodeDirty(nsIDOMNode* aNode)
+{
+ // Mark the node dirty, but not for webpages (bug 599983)
+ if (!OutputsMozDirty()) {
+ return NS_OK;
+ }
+ nsCOMPtr<dom::Element> element = do_QueryInterface(aNode);
+ if (element) {
+ element->SetAttr(kNameSpaceID_None, nsGkAtoms::mozdirty,
+ EmptyString(), false);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::GetInlineSpellChecker(bool autoCreate,
+ nsIInlineSpellChecker** aInlineSpellChecker)
+{
+ NS_ENSURE_ARG_POINTER(aInlineSpellChecker);
+
+ if (mDidPreDestroy) {
+ // Don't allow people to get or create the spell checker once the editor
+ // is going away.
+ *aInlineSpellChecker = nullptr;
+ return autoCreate ? NS_ERROR_NOT_AVAILABLE : NS_OK;
+ }
+
+ // We don't want to show the spell checking UI if there are no spell check dictionaries available.
+ bool canSpell = mozInlineSpellChecker::CanEnableInlineSpellChecking();
+ if (!canSpell) {
+ *aInlineSpellChecker = nullptr;
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv;
+ if (!mInlineSpellChecker && autoCreate) {
+ mInlineSpellChecker = do_CreateInstance(MOZ_INLINESPELLCHECKER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (mInlineSpellChecker) {
+ rv = mInlineSpellChecker->Init(this);
+ if (NS_FAILED(rv)) {
+ mInlineSpellChecker = nullptr;
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ NS_IF_ADDREF(*aInlineSpellChecker = mInlineSpellChecker);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::SyncRealTimeSpell()
+{
+ bool enable = GetDesiredSpellCheckState();
+
+ // Initializes mInlineSpellChecker
+ nsCOMPtr<nsIInlineSpellChecker> spellChecker;
+ GetInlineSpellChecker(enable, getter_AddRefs(spellChecker));
+
+ if (mInlineSpellChecker) {
+ // We might have a mInlineSpellChecker even if there are no dictionaries
+ // available since we don't destroy the mInlineSpellChecker when the last
+ // dictionariy is removed, but in that case spellChecker is null
+ mInlineSpellChecker->SetEnableRealTimeSpell(enable && spellChecker);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::SetSpellcheckUserOverride(bool enable)
+{
+ mSpellcheckCheckboxState = enable ? eTriTrue : eTriFalse;
+
+ return SyncRealTimeSpell();
+}
+
+NS_IMETHODIMP
+EditorBase::CreateNode(const nsAString& aTag,
+ nsIDOMNode* aParent,
+ int32_t aPosition,
+ nsIDOMNode** aNewNode)
+{
+ nsCOMPtr<nsIAtom> tag = NS_Atomize(aTag);
+ nsCOMPtr<nsINode> parent = do_QueryInterface(aParent);
+ NS_ENSURE_STATE(parent);
+ *aNewNode = GetAsDOMNode(CreateNode(tag, parent, aPosition).take());
+ NS_ENSURE_STATE(*aNewNode);
+ return NS_OK;
+}
+
+already_AddRefed<Element>
+EditorBase::CreateNode(nsIAtom* aTag,
+ nsINode* aParent,
+ int32_t aPosition)
+{
+ MOZ_ASSERT(aTag && aParent);
+
+ AutoRules beginRulesSniffing(this, EditAction::createNode, nsIEditor::eNext);
+
+ for (auto& listener : mActionListeners) {
+ listener->WillCreateNode(nsDependentAtomString(aTag),
+ GetAsDOMNode(aParent), aPosition);
+ }
+
+ nsCOMPtr<Element> ret;
+
+ RefPtr<CreateElementTransaction> transaction =
+ CreateTxnForCreateElement(*aTag, *aParent, aPosition);
+ nsresult rv = DoTransaction(transaction);
+ if (NS_SUCCEEDED(rv)) {
+ ret = transaction->GetNewNode();
+ MOZ_ASSERT(ret);
+ }
+
+ mRangeUpdater.SelAdjCreateNode(aParent, aPosition);
+
+ for (auto& listener : mActionListeners) {
+ listener->DidCreateNode(nsDependentAtomString(aTag), GetAsDOMNode(ret),
+ GetAsDOMNode(aParent), aPosition, rv);
+ }
+
+ return ret.forget();
+}
+
+NS_IMETHODIMP
+EditorBase::InsertNode(nsIDOMNode* aNode,
+ nsIDOMNode* aParent,
+ int32_t aPosition)
+{
+ nsCOMPtr<nsIContent> node = do_QueryInterface(aNode);
+ nsCOMPtr<nsINode> parent = do_QueryInterface(aParent);
+ NS_ENSURE_TRUE(node && parent, NS_ERROR_NULL_POINTER);
+
+ return InsertNode(*node, *parent, aPosition);
+}
+
+nsresult
+EditorBase::InsertNode(nsIContent& aNode,
+ nsINode& aParent,
+ int32_t aPosition)
+{
+ AutoRules beginRulesSniffing(this, EditAction::insertNode, nsIEditor::eNext);
+
+ for (auto& listener : mActionListeners) {
+ listener->WillInsertNode(aNode.AsDOMNode(), aParent.AsDOMNode(),
+ aPosition);
+ }
+
+ RefPtr<InsertNodeTransaction> transaction =
+ CreateTxnForInsertNode(aNode, aParent, aPosition);
+ nsresult rv = DoTransaction(transaction);
+
+ mRangeUpdater.SelAdjInsertNode(aParent.AsDOMNode(), aPosition);
+
+ for (auto& listener : mActionListeners) {
+ listener->DidInsertNode(aNode.AsDOMNode(), aParent.AsDOMNode(), aPosition,
+ rv);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+EditorBase::SplitNode(nsIDOMNode* aNode,
+ int32_t aOffset,
+ nsIDOMNode** aNewLeftNode)
+{
+ nsCOMPtr<nsIContent> node = do_QueryInterface(aNode);
+ NS_ENSURE_STATE(node);
+ ErrorResult rv;
+ nsCOMPtr<nsIContent> newNode = SplitNode(*node, aOffset, rv);
+ *aNewLeftNode = GetAsDOMNode(newNode.forget().take());
+ return rv.StealNSResult();
+}
+
+nsIContent*
+EditorBase::SplitNode(nsIContent& aNode,
+ int32_t aOffset,
+ ErrorResult& aResult)
+{
+ AutoRules beginRulesSniffing(this, EditAction::splitNode, nsIEditor::eNext);
+
+ for (auto& listener : mActionListeners) {
+ listener->WillSplitNode(aNode.AsDOMNode(), aOffset);
+ }
+
+ RefPtr<SplitNodeTransaction> transaction =
+ CreateTxnForSplitNode(aNode, aOffset);
+ aResult = DoTransaction(transaction);
+
+ nsCOMPtr<nsIContent> newNode = aResult.Failed() ? nullptr
+ : transaction->GetNewNode();
+
+ mRangeUpdater.SelAdjSplitNode(aNode, aOffset, newNode);
+
+ nsresult rv = aResult.StealNSResult();
+ for (auto& listener : mActionListeners) {
+ listener->DidSplitNode(aNode.AsDOMNode(), aOffset, GetAsDOMNode(newNode),
+ rv);
+ }
+ // Note: result might be a success code, so we can't use Throw() to
+ // set it on aResult.
+ aResult = rv;
+
+ return newNode;
+}
+
+NS_IMETHODIMP
+EditorBase::JoinNodes(nsIDOMNode* aLeftNode,
+ nsIDOMNode* aRightNode,
+ nsIDOMNode*)
+{
+ nsCOMPtr<nsINode> leftNode = do_QueryInterface(aLeftNode);
+ nsCOMPtr<nsINode> rightNode = do_QueryInterface(aRightNode);
+ NS_ENSURE_STATE(leftNode && rightNode && leftNode->GetParentNode());
+ return JoinNodes(*leftNode, *rightNode);
+}
+
+nsresult
+EditorBase::JoinNodes(nsINode& aLeftNode,
+ nsINode& aRightNode)
+{
+ nsCOMPtr<nsINode> parent = aLeftNode.GetParentNode();
+ MOZ_ASSERT(parent);
+
+ AutoRules beginRulesSniffing(this, EditAction::joinNode,
+ nsIEditor::ePrevious);
+
+ // Remember some values; later used for saved selection updating.
+ // Find the offset between the nodes to be joined.
+ int32_t offset = parent->IndexOf(&aRightNode);
+ // Find the number of children of the lefthand node
+ uint32_t oldLeftNodeLen = aLeftNode.Length();
+
+ for (auto& listener : mActionListeners) {
+ listener->WillJoinNodes(aLeftNode.AsDOMNode(), aRightNode.AsDOMNode(),
+ parent->AsDOMNode());
+ }
+
+ nsresult rv = NS_OK;
+ RefPtr<JoinNodeTransaction> transaction =
+ CreateTxnForJoinNode(aLeftNode, aRightNode);
+ if (transaction) {
+ rv = DoTransaction(transaction);
+ }
+
+ mRangeUpdater.SelAdjJoinNodes(aLeftNode, aRightNode, *parent, offset,
+ (int32_t)oldLeftNodeLen);
+
+ for (auto& listener : mActionListeners) {
+ listener->DidJoinNodes(aLeftNode.AsDOMNode(), aRightNode.AsDOMNode(),
+ parent->AsDOMNode(), rv);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+EditorBase::DeleteNode(nsIDOMNode* aNode)
+{
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ NS_ENSURE_STATE(node);
+ return DeleteNode(node);
+}
+
+nsresult
+EditorBase::DeleteNode(nsINode* aNode)
+{
+ AutoRules beginRulesSniffing(this, EditAction::createNode,
+ nsIEditor::ePrevious);
+
+ // save node location for selection updating code.
+ for (auto& listener : mActionListeners) {
+ listener->WillDeleteNode(aNode->AsDOMNode());
+ }
+
+ RefPtr<DeleteNodeTransaction> transaction;
+ nsresult rv = CreateTxnForDeleteNode(aNode, getter_AddRefs(transaction));
+ if (NS_SUCCEEDED(rv)) {
+ rv = DoTransaction(transaction);
+ }
+
+ for (auto& listener : mActionListeners) {
+ listener->DidDeleteNode(aNode->AsDOMNode(), rv);
+ }
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+/**
+ * ReplaceContainer() replaces inNode with a new node (outNode) which is
+ * constructed to be of type aNodeType. Put inNodes children into outNode.
+ * Callers responsibility to make sure inNode's children can go in outNode.
+ */
+already_AddRefed<Element>
+EditorBase::ReplaceContainer(Element* aOldContainer,
+ nsIAtom* aNodeType,
+ nsIAtom* aAttribute,
+ const nsAString* aValue,
+ ECloneAttributes aCloneAttributes)
+{
+ MOZ_ASSERT(aOldContainer && aNodeType);
+
+ nsCOMPtr<nsIContent> parent = aOldContainer->GetParent();
+ NS_ENSURE_TRUE(parent, nullptr);
+
+ int32_t offset = parent->IndexOf(aOldContainer);
+
+ // create new container
+ nsCOMPtr<Element> ret = CreateHTMLContent(aNodeType);
+ NS_ENSURE_TRUE(ret, nullptr);
+
+ // set attribute if needed
+ if (aAttribute && aValue && aAttribute != nsGkAtoms::_empty) {
+ nsresult rv = ret->SetAttr(kNameSpaceID_None, aAttribute, *aValue, true);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ }
+ if (aCloneAttributes == eCloneAttributes) {
+ CloneAttributes(ret, aOldContainer);
+ }
+
+ // notify our internal selection state listener
+ // (Note: An AutoSelectionRestorer object must be created
+ // before calling this to initialize mRangeUpdater)
+ AutoReplaceContainerSelNotify selStateNotify(mRangeUpdater, aOldContainer,
+ ret);
+ {
+ AutoTransactionsConserveSelection conserveSelection(this);
+ while (aOldContainer->HasChildren()) {
+ nsCOMPtr<nsIContent> child = aOldContainer->GetFirstChild();
+
+ nsresult rv = DeleteNode(child);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ rv = InsertNode(*child, *ret, -1);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ }
+ }
+
+ // insert new container into tree
+ nsresult rv = InsertNode(*ret, *parent, offset);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ // delete old container
+ rv = DeleteNode(aOldContainer);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ return ret.forget();
+}
+
+/**
+ * RemoveContainer() removes inNode, reparenting its children (if any) into the
+ * parent of inNode.
+ */
+nsresult
+EditorBase::RemoveContainer(nsIContent* aNode)
+{
+ MOZ_ASSERT(aNode);
+
+ nsCOMPtr<nsINode> parent = aNode->GetParentNode();
+ NS_ENSURE_STATE(parent);
+
+ int32_t offset = parent->IndexOf(aNode);
+
+ // Loop through the children of inNode and promote them into inNode's parent
+ uint32_t nodeOrigLen = aNode->GetChildCount();
+
+ // notify our internal selection state listener
+ AutoRemoveContainerSelNotify selNotify(mRangeUpdater, aNode, parent,
+ offset, nodeOrigLen);
+
+ while (aNode->HasChildren()) {
+ nsCOMPtr<nsIContent> child = aNode->GetLastChild();
+ nsresult rv = DeleteNode(child);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = InsertNode(*child, *parent, offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return DeleteNode(aNode);
+}
+
+/**
+ * InsertContainerAbove() inserts a new parent for inNode, which is contructed
+ * to be of type aNodeType. outNode becomes a child of inNode's earlier
+ * parent. Caller's responsibility to make sure inNode's can be child of
+ * outNode, and outNode can be child of old parent.
+ */
+already_AddRefed<Element>
+EditorBase::InsertContainerAbove(nsIContent* aNode,
+ nsIAtom* aNodeType,
+ nsIAtom* aAttribute,
+ const nsAString* aValue)
+{
+ MOZ_ASSERT(aNode && aNodeType);
+
+ nsCOMPtr<nsIContent> parent = aNode->GetParent();
+ NS_ENSURE_TRUE(parent, nullptr);
+ int32_t offset = parent->IndexOf(aNode);
+
+ // Create new container
+ nsCOMPtr<Element> newContent = CreateHTMLContent(aNodeType);
+ NS_ENSURE_TRUE(newContent, nullptr);
+
+ // Set attribute if needed
+ if (aAttribute && aValue && aAttribute != nsGkAtoms::_empty) {
+ nsresult rv =
+ newContent->SetAttr(kNameSpaceID_None, aAttribute, *aValue, true);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ }
+
+ // Notify our internal selection state listener
+ AutoInsertContainerSelNotify selNotify(mRangeUpdater);
+
+ // Put inNode in new parent, outNode
+ nsresult rv = DeleteNode(aNode);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ {
+ AutoTransactionsConserveSelection conserveSelection(this);
+ rv = InsertNode(*aNode, *newContent, 0);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ }
+
+ // Put new parent in doc
+ rv = InsertNode(*newContent, *parent, offset);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ return newContent.forget();
+}
+
+/**
+ * MoveNode() moves aNode to {aParent,aOffset}.
+ */
+nsresult
+EditorBase::MoveNode(nsIContent* aNode,
+ nsINode* aParent,
+ int32_t aOffset)
+{
+ MOZ_ASSERT(aNode);
+ MOZ_ASSERT(aParent);
+ MOZ_ASSERT(aOffset == -1 ||
+ (0 <= aOffset &&
+ AssertedCast<uint32_t>(aOffset) <= aParent->Length()));
+
+ nsCOMPtr<nsINode> oldParent = aNode->GetParentNode();
+ int32_t oldOffset = oldParent ? oldParent->IndexOf(aNode) : -1;
+
+ if (aOffset == -1) {
+ // Magic value meaning "move to end of aParent"
+ aOffset = AssertedCast<int32_t>(aParent->Length());
+ }
+
+ // Don't do anything if it's already in right place
+ if (aParent == oldParent && aOffset == oldOffset) {
+ return NS_OK;
+ }
+
+ // Notify our internal selection state listener
+ AutoMoveNodeSelNotify selNotify(mRangeUpdater, oldParent, oldOffset,
+ aParent, aOffset);
+
+ // Need to adjust aOffset if we're moving aNode later in its current parent
+ if (aParent == oldParent && oldOffset < aOffset) {
+ // When we delete aNode, it will make the offsets after it off by one
+ aOffset--;
+ }
+
+ // Hold a reference so aNode doesn't go away when we remove it (bug 772282)
+ nsCOMPtr<nsINode> kungFuDeathGrip = aNode;
+
+ nsresult rv = DeleteNode(aNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return InsertNode(*aNode, *aParent, aOffset);
+}
+
+NS_IMETHODIMP
+EditorBase::AddEditorObserver(nsIEditorObserver* aObserver)
+{
+ // we don't keep ownership of the observers. They must
+ // remove themselves as observers before they are destroyed.
+
+ NS_ENSURE_TRUE(aObserver, NS_ERROR_NULL_POINTER);
+
+ // Make sure the listener isn't already on the list
+ if (!mEditorObservers.Contains(aObserver)) {
+ mEditorObservers.AppendElement(*aObserver);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::RemoveEditorObserver(nsIEditorObserver* aObserver)
+{
+ NS_ENSURE_TRUE(aObserver, NS_ERROR_FAILURE);
+
+ mEditorObservers.RemoveElement(aObserver);
+
+ return NS_OK;
+}
+
+class EditorInputEventDispatcher final : public Runnable
+{
+public:
+ EditorInputEventDispatcher(EditorBase* aEditorBase,
+ nsIContent* aTarget,
+ bool aIsComposing)
+ : mEditorBase(aEditorBase)
+ , mTarget(aTarget)
+ , mIsComposing(aIsComposing)
+ {
+ }
+
+ NS_IMETHOD Run() override
+ {
+ // Note that we don't need to check mDispatchInputEvent here. We need
+ // to check it only when the editor requests to dispatch the input event.
+
+ if (!mTarget->IsInComposedDoc()) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIPresShell> ps = mEditorBase->GetPresShell();
+ if (!ps) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIWidget> widget = mEditorBase->GetWidget();
+ if (!widget) {
+ return NS_OK;
+ }
+
+ // Even if the change is caused by untrusted event, we need to dispatch
+ // trusted input event since it's a fact.
+ InternalEditorInputEvent inputEvent(true, eEditorInput, widget);
+ inputEvent.mTime = static_cast<uint64_t>(PR_Now() / 1000);
+ inputEvent.mIsComposing = mIsComposing;
+ nsEventStatus status = nsEventStatus_eIgnore;
+ nsresult rv =
+ ps->HandleEventWithTarget(&inputEvent, nullptr, mTarget, &status);
+ NS_ENSURE_SUCCESS(rv, NS_OK); // print the warning if error
+ return NS_OK;
+ }
+
+private:
+ RefPtr<EditorBase> mEditorBase;
+ nsCOMPtr<nsIContent> mTarget;
+ bool mIsComposing;
+};
+
+void
+EditorBase::NotifyEditorObservers(NotificationForEditorObservers aNotification)
+{
+ // Copy the observers since EditAction()s can modify mEditorObservers.
+ nsTArray<mozilla::OwningNonNull<nsIEditorObserver>> observers(mEditorObservers);
+ switch (aNotification) {
+ case eNotifyEditorObserversOfEnd:
+ mIsInEditAction = false;
+ for (auto& observer : observers) {
+ observer->EditAction();
+ }
+
+ if (!mDispatchInputEvent) {
+ return;
+ }
+
+ FireInputEvent();
+ break;
+ case eNotifyEditorObserversOfBefore:
+ if (NS_WARN_IF(mIsInEditAction)) {
+ break;
+ }
+ mIsInEditAction = true;
+ for (auto& observer : observers) {
+ observer->BeforeEditAction();
+ }
+ break;
+ case eNotifyEditorObserversOfCancel:
+ mIsInEditAction = false;
+ for (auto& observer : observers) {
+ observer->CancelEditAction();
+ }
+ break;
+ default:
+ MOZ_CRASH("Handle all notifications here");
+ break;
+ }
+}
+
+void
+EditorBase::FireInputEvent()
+{
+ // We don't need to dispatch multiple input events if there is a pending
+ // input event. However, it may have different event target. If we resolved
+ // this issue, we need to manage the pending events in an array. But it's
+ // overwork. We don't need to do it for the very rare case.
+
+ nsCOMPtr<nsIContent> target = GetInputEventTargetContent();
+ NS_ENSURE_TRUE_VOID(target);
+
+ // NOTE: Don't refer IsIMEComposing() because it returns false even before
+ // compositionend. However, DOM Level 3 Events defines it should be
+ // true after compositionstart and before compositionend.
+ nsContentUtils::AddScriptRunner(
+ new EditorInputEventDispatcher(this, target, !!GetComposition()));
+}
+
+NS_IMETHODIMP
+EditorBase::AddEditActionListener(nsIEditActionListener* aListener)
+{
+ NS_ENSURE_TRUE(aListener, NS_ERROR_NULL_POINTER);
+
+ // Make sure the listener isn't already on the list
+ if (!mActionListeners.Contains(aListener)) {
+ mActionListeners.AppendElement(*aListener);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::RemoveEditActionListener(nsIEditActionListener* aListener)
+{
+ NS_ENSURE_TRUE(aListener, NS_ERROR_FAILURE);
+
+ mActionListeners.RemoveElement(aListener);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::AddDocumentStateListener(nsIDocumentStateListener* aListener)
+{
+ NS_ENSURE_TRUE(aListener, NS_ERROR_NULL_POINTER);
+
+ if (!mDocStateListeners.Contains(aListener)) {
+ mDocStateListeners.AppendElement(*aListener);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::RemoveDocumentStateListener(nsIDocumentStateListener* aListener)
+{
+ NS_ENSURE_TRUE(aListener, NS_ERROR_NULL_POINTER);
+
+ mDocStateListeners.RemoveElement(aListener);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::OutputToString(const nsAString& aFormatType,
+ uint32_t aFlags,
+ nsAString& aOutputString)
+{
+ // these should be implemented by derived classes.
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+EditorBase::OutputToStream(nsIOutputStream* aOutputStream,
+ const nsAString& aFormatType,
+ const nsACString& aCharsetOverride,
+ uint32_t aFlags)
+{
+ // these should be implemented by derived classes.
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+EditorBase::DumpContentTree()
+{
+#ifdef DEBUG
+ if (mRootElement) {
+ mRootElement->List(stdout);
+ }
+#endif
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::DebugDumpContent()
+{
+#ifdef DEBUG
+ nsCOMPtr<nsIDOMHTMLDocument> doc = do_QueryReferent(mDocWeak);
+ NS_ENSURE_TRUE(doc, NS_ERROR_NOT_INITIALIZED);
+
+ nsCOMPtr<nsIDOMHTMLElement>bodyElem;
+ doc->GetBody(getter_AddRefs(bodyElem));
+ nsCOMPtr<nsIContent> content = do_QueryInterface(bodyElem);
+ if (content) {
+ content->List();
+ }
+#endif
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::DebugUnitTests(int32_t* outNumTests,
+ int32_t* outNumTestsFailed)
+{
+#ifdef DEBUG
+ NS_NOTREACHED("This should never get called. Overridden by subclasses");
+#endif
+ return NS_OK;
+}
+
+bool
+EditorBase::ArePreservingSelection()
+{
+ return !(mSavedSel.IsEmpty());
+}
+
+void
+EditorBase::PreserveSelectionAcrossActions(Selection* aSel)
+{
+ mSavedSel.SaveSelection(aSel);
+ mRangeUpdater.RegisterSelectionState(mSavedSel);
+}
+
+nsresult
+EditorBase::RestorePreservedSelection(Selection* aSel)
+{
+ if (mSavedSel.IsEmpty()) {
+ return NS_ERROR_FAILURE;
+ }
+ mSavedSel.RestoreSelection(aSel);
+ StopPreservingSelection();
+ return NS_OK;
+}
+
+void
+EditorBase::StopPreservingSelection()
+{
+ mRangeUpdater.DropSelectionState(mSavedSel);
+ mSavedSel.MakeEmpty();
+}
+
+bool
+EditorBase::EnsureComposition(WidgetCompositionEvent* aCompositionEvent)
+{
+ if (mComposition) {
+ return true;
+ }
+ // The compositionstart event must cause creating new TextComposition
+ // instance at being dispatched by IMEStateManager.
+ mComposition = IMEStateManager::GetTextCompositionFor(aCompositionEvent);
+ if (!mComposition) {
+ // However, TextComposition may be committed before the composition
+ // event comes here.
+ return false;
+ }
+ mComposition->StartHandlingComposition(this);
+ return true;
+}
+
+nsresult
+EditorBase::BeginIMEComposition(WidgetCompositionEvent* aCompositionEvent)
+{
+ MOZ_ASSERT(!mComposition, "There is composition already");
+ if (!EnsureComposition(aCompositionEvent)) {
+ return NS_OK;
+ }
+ if (mPhonetic) {
+ mPhonetic->Truncate(0);
+ }
+ return NS_OK;
+}
+
+void
+EditorBase::EndIMEComposition()
+{
+ NS_ENSURE_TRUE_VOID(mComposition); // nothing to do
+
+ // commit the IME transaction..we can get at it via the transaction mgr.
+ // Note that this means IME won't work without an undo stack!
+ if (mTxnMgr) {
+ nsCOMPtr<nsITransaction> txn = mTxnMgr->PeekUndoStack();
+ nsCOMPtr<nsIAbsorbingTransaction> plcTxn = do_QueryInterface(txn);
+ if (plcTxn) {
+ DebugOnly<nsresult> rv = plcTxn->Commit();
+ NS_ASSERTION(NS_SUCCEEDED(rv),
+ "nsIAbsorbingTransaction::Commit() failed");
+ }
+ }
+
+ // Composition string may have hidden the caret. Therefore, we need to
+ // cancel it here.
+ HideCaret(false);
+
+ /* reset the data we need to construct a transaction */
+ mIMETextNode = nullptr;
+ mIMETextOffset = 0;
+ mIMETextLength = 0;
+ mComposition->EndHandlingComposition(this);
+ mComposition = nullptr;
+
+ // notify editor observers of action
+ NotifyEditorObservers(eNotifyEditorObserversOfEnd);
+}
+
+NS_IMETHODIMP
+EditorBase::GetPhonetic(nsAString& aPhonetic)
+{
+ if (mPhonetic) {
+ aPhonetic = *mPhonetic;
+ } else {
+ aPhonetic.Truncate(0);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::ForceCompositionEnd()
+{
+ nsCOMPtr<nsIPresShell> ps = GetPresShell();
+ if (!ps) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ nsPresContext* pc = ps->GetPresContext();
+ if (!pc) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return mComposition ?
+ IMEStateManager::NotifyIME(REQUEST_TO_COMMIT_COMPOSITION, pc) : NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::GetPreferredIMEState(IMEState* aState)
+{
+ NS_ENSURE_ARG_POINTER(aState);
+ aState->mEnabled = IMEState::ENABLED;
+ aState->mOpen = IMEState::DONT_CHANGE_OPEN_STATE;
+
+ if (IsReadonly() || IsDisabled()) {
+ aState->mEnabled = IMEState::DISABLED;
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIContent> content = GetRoot();
+ NS_ENSURE_TRUE(content, NS_ERROR_FAILURE);
+
+ nsIFrame* frame = content->GetPrimaryFrame();
+ NS_ENSURE_TRUE(frame, NS_ERROR_FAILURE);
+
+ switch (frame->StyleUIReset()->mIMEMode) {
+ case NS_STYLE_IME_MODE_AUTO:
+ if (IsPasswordEditor())
+ aState->mEnabled = IMEState::PASSWORD;
+ break;
+ case NS_STYLE_IME_MODE_DISABLED:
+ // we should use password state for |ime-mode: disabled;|.
+ aState->mEnabled = IMEState::PASSWORD;
+ break;
+ case NS_STYLE_IME_MODE_ACTIVE:
+ aState->mOpen = IMEState::OPEN;
+ break;
+ case NS_STYLE_IME_MODE_INACTIVE:
+ aState->mOpen = IMEState::CLOSED;
+ break;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::GetComposing(bool* aResult)
+{
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = IsIMEComposing();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::GetRootElement(nsIDOMElement** aRootElement)
+{
+ NS_ENSURE_ARG_POINTER(aRootElement);
+ NS_ENSURE_TRUE(mRootElement, NS_ERROR_NOT_AVAILABLE);
+ nsCOMPtr<nsIDOMElement> rootElement = do_QueryInterface(mRootElement);
+ rootElement.forget(aRootElement);
+ return NS_OK;
+}
+
+/**
+ * All editor operations which alter the doc should be prefaced
+ * with a call to StartOperation, naming the action and direction.
+ */
+NS_IMETHODIMP
+EditorBase::StartOperation(EditAction opID,
+ nsIEditor::EDirection aDirection)
+{
+ mAction = opID;
+ mDirection = aDirection;
+ return NS_OK;
+}
+
+/**
+ * All editor operations which alter the doc should be followed
+ * with a call to EndOperation.
+ */
+NS_IMETHODIMP
+EditorBase::EndOperation()
+{
+ mAction = EditAction::none;
+ mDirection = eNone;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::CloneAttribute(const nsAString& aAttribute,
+ nsIDOMNode* aDestNode,
+ nsIDOMNode* aSourceNode)
+{
+ NS_ENSURE_TRUE(aDestNode && aSourceNode, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsIDOMElement> destElement = do_QueryInterface(aDestNode);
+ nsCOMPtr<nsIDOMElement> sourceElement = do_QueryInterface(aSourceNode);
+ NS_ENSURE_TRUE(destElement && sourceElement, NS_ERROR_NO_INTERFACE);
+
+ nsAutoString attrValue;
+ bool isAttrSet;
+ nsresult rv = GetAttributeValue(sourceElement,
+ aAttribute,
+ attrValue,
+ &isAttrSet);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (isAttrSet) {
+ rv = SetAttribute(destElement, aAttribute, attrValue);
+ } else {
+ rv = RemoveAttribute(destElement, aAttribute);
+ }
+
+ return rv;
+}
+
+/**
+ * @param aDest Must be a DOM element.
+ * @param aSource Must be a DOM element.
+ */
+NS_IMETHODIMP
+EditorBase::CloneAttributes(nsIDOMNode* aDest,
+ nsIDOMNode* aSource)
+{
+ NS_ENSURE_TRUE(aDest && aSource, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<Element> dest = do_QueryInterface(aDest);
+ nsCOMPtr<Element> source = do_QueryInterface(aSource);
+ NS_ENSURE_TRUE(dest && source, NS_ERROR_NO_INTERFACE);
+
+ CloneAttributes(dest, source);
+
+ return NS_OK;
+}
+
+void
+EditorBase::CloneAttributes(Element* aDest,
+ Element* aSource)
+{
+ MOZ_ASSERT(aDest && aSource);
+
+ AutoEditBatch beginBatching(this);
+
+ // Use transaction system for undo only if destination is already in the
+ // document
+ NS_ENSURE_TRUE(GetRoot(), );
+ bool destInBody = GetRoot()->Contains(aDest);
+
+ // Clear existing attributes
+ RefPtr<nsDOMAttributeMap> destAttributes = aDest->Attributes();
+ while (RefPtr<Attr> attr = destAttributes->Item(0)) {
+ if (destInBody) {
+ RemoveAttribute(static_cast<nsIDOMElement*>(GetAsDOMNode(aDest)),
+ attr->NodeName());
+ } else {
+ ErrorResult ignored;
+ aDest->RemoveAttribute(attr->NodeName(), ignored);
+ }
+ }
+
+ // Set just the attributes that the source element has
+ RefPtr<nsDOMAttributeMap> sourceAttributes = aSource->Attributes();
+ uint32_t sourceCount = sourceAttributes->Length();
+ for (uint32_t i = 0; i < sourceCount; i++) {
+ RefPtr<Attr> attr = sourceAttributes->Item(i);
+ nsAutoString value;
+ attr->GetValue(value);
+ if (destInBody) {
+ SetAttributeOrEquivalent(static_cast<nsIDOMElement*>(GetAsDOMNode(aDest)),
+ attr->NodeName(), value, false);
+ } else {
+ // The element is not inserted in the document yet, we don't want to put
+ // a transaction on the UndoStack
+ SetAttributeOrEquivalent(static_cast<nsIDOMElement*>(GetAsDOMNode(aDest)),
+ attr->NodeName(), value, true);
+ }
+ }
+}
+
+NS_IMETHODIMP
+EditorBase::ScrollSelectionIntoView(bool aScrollToAnchor)
+{
+ nsCOMPtr<nsISelectionController> selCon;
+ if (NS_SUCCEEDED(GetSelectionController(getter_AddRefs(selCon))) && selCon) {
+ int16_t region = nsISelectionController::SELECTION_FOCUS_REGION;
+
+ if (aScrollToAnchor) {
+ region = nsISelectionController::SELECTION_ANCHOR_REGION;
+ }
+
+ selCon->ScrollSelectionIntoView(nsISelectionController::SELECTION_NORMAL,
+ region, nsISelectionController::SCROLL_OVERFLOW_HIDDEN);
+ }
+
+ return NS_OK;
+}
+
+void
+EditorBase::FindBetterInsertionPoint(nsCOMPtr<nsIDOMNode>& aNode,
+ int32_t& aOffset)
+{
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ FindBetterInsertionPoint(node, aOffset);
+ aNode = do_QueryInterface(node);
+}
+
+void
+EditorBase::FindBetterInsertionPoint(nsCOMPtr<nsINode>& aNode,
+ int32_t& aOffset)
+{
+ if (aNode->IsNodeOfType(nsINode::eTEXT)) {
+ // There is no "better" insertion point.
+ return;
+ }
+
+ if (!IsPlaintextEditor()) {
+ // We cannot find "better" insertion point in HTML editor.
+ // WARNING: When you add some code to find better node in HTML editor,
+ // you need to call this before calling InsertTextImpl() in
+ // HTMLEditRules.
+ return;
+ }
+
+ nsCOMPtr<nsINode> node = aNode;
+ int32_t offset = aOffset;
+
+ nsCOMPtr<nsINode> root = GetRoot();
+ if (aNode == root) {
+ // In some cases, aNode is the anonymous DIV, and offset is 0. To avoid
+ // injecting unneeded text nodes, we first look to see if we have one
+ // available. In that case, we'll just adjust node and offset accordingly.
+ if (!offset && node->HasChildren() &&
+ node->GetFirstChild()->IsNodeOfType(nsINode::eTEXT)) {
+ aNode = node->GetFirstChild();
+ aOffset = 0;
+ return;
+ }
+
+ // In some other cases, aNode is the anonymous DIV, and offset points to the
+ // terminating mozBR. In that case, we'll adjust aInOutNode and
+ // aInOutOffset to the preceding text node, if any.
+ if (offset > 0 && node->GetChildAt(offset - 1) &&
+ node->GetChildAt(offset - 1)->IsNodeOfType(nsINode::eTEXT)) {
+ NS_ENSURE_TRUE_VOID(node->Length() <= INT32_MAX);
+ aNode = node->GetChildAt(offset - 1);
+ aOffset = static_cast<int32_t>(aNode->Length());
+ return;
+ }
+ }
+
+ // Sometimes, aNode is the mozBR element itself. In that case, we'll adjust
+ // the insertion point to the previous text node, if one exists, or to the
+ // parent anonymous DIV.
+ if (TextEditUtils::IsMozBR(node) && !offset) {
+ if (node->GetPreviousSibling() &&
+ node->GetPreviousSibling()->IsNodeOfType(nsINode::eTEXT)) {
+ NS_ENSURE_TRUE_VOID(node->Length() <= INT32_MAX);
+ aNode = node->GetPreviousSibling();
+ aOffset = static_cast<int32_t>(aNode->Length());
+ return;
+ }
+
+ if (node->GetParentNode() && node->GetParentNode() == root) {
+ aNode = node->GetParentNode();
+ aOffset = 0;
+ return;
+ }
+ }
+}
+
+nsresult
+EditorBase::InsertTextImpl(const nsAString& aStringToInsert,
+ nsCOMPtr<nsINode>* aInOutNode,
+ int32_t* aInOutOffset,
+ nsIDocument* aDoc)
+{
+ // NOTE: caller *must* have already used AutoTransactionsConserveSelection
+ // stack-based class to turn off txn selection updating. Caller also turned
+ // on rules sniffing if desired.
+
+ NS_ENSURE_TRUE(aInOutNode && *aInOutNode && aInOutOffset && aDoc,
+ NS_ERROR_NULL_POINTER);
+
+ if (!ShouldHandleIMEComposition() && aStringToInsert.IsEmpty()) {
+ return NS_OK;
+ }
+
+ // This method doesn't support over INT32_MAX length text since aInOutOffset
+ // is int32_t*.
+ CheckedInt<int32_t> lengthToInsert(aStringToInsert.Length());
+ NS_ENSURE_TRUE(lengthToInsert.isValid(), NS_ERROR_INVALID_ARG);
+
+ nsCOMPtr<nsINode> node = *aInOutNode;
+ int32_t offset = *aInOutOffset;
+
+ // In some cases, the node may be the anonymous div elemnt or a mozBR
+ // element. Let's try to look for better insertion point in the nearest
+ // text node if there is.
+ FindBetterInsertionPoint(node, offset);
+
+ if (ShouldHandleIMEComposition()) {
+ CheckedInt<int32_t> newOffset;
+ if (!node->IsNodeOfType(nsINode::eTEXT)) {
+ // create a text node
+ RefPtr<nsTextNode> newNode = aDoc->CreateTextNode(EmptyString());
+ // then we insert it into the dom tree
+ nsresult rv = InsertNode(*newNode, *node, offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ node = newNode;
+ offset = 0;
+ newOffset = lengthToInsert;
+ } else {
+ newOffset = lengthToInsert + offset;
+ NS_ENSURE_TRUE(newOffset.isValid(), NS_ERROR_FAILURE);
+ }
+ nsresult rv =
+ InsertTextIntoTextNodeImpl(aStringToInsert, *node->GetAsText(), offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ offset = newOffset.value();
+ } else {
+ if (node->IsNodeOfType(nsINode::eTEXT)) {
+ CheckedInt<int32_t> newOffset = lengthToInsert + offset;
+ NS_ENSURE_TRUE(newOffset.isValid(), NS_ERROR_FAILURE);
+ // we are inserting text into an existing text node.
+ nsresult rv =
+ InsertTextIntoTextNodeImpl(aStringToInsert, *node->GetAsText(), offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ offset = newOffset.value();
+ } else {
+ // we are inserting text into a non-text node. first we have to create a
+ // textnode (this also populates it with the text)
+ RefPtr<nsTextNode> newNode = aDoc->CreateTextNode(aStringToInsert);
+ // then we insert it into the dom tree
+ nsresult rv = InsertNode(*newNode, *node, offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ node = newNode;
+ offset = lengthToInsert.value();
+ }
+ }
+
+ *aInOutNode = node;
+ *aInOutOffset = offset;
+ return NS_OK;
+}
+
+nsresult
+EditorBase::InsertTextIntoTextNodeImpl(const nsAString& aStringToInsert,
+ Text& aTextNode,
+ int32_t aOffset,
+ bool aSuppressIME)
+{
+ RefPtr<EditTransactionBase> transaction;
+ bool isIMETransaction = false;
+ RefPtr<Text> insertedTextNode = &aTextNode;
+ int32_t insertedOffset = aOffset;
+ // aSuppressIME is used when editor must insert text, yet this text is not
+ // part of the current IME operation. Example: adjusting whitespace around an
+ // IME insertion.
+ if (ShouldHandleIMEComposition() && !aSuppressIME) {
+ if (!mIMETextNode) {
+ mIMETextNode = &aTextNode;
+ mIMETextOffset = aOffset;
+ }
+ // Modify mPhonetic with raw text input clauses.
+ const TextRangeArray* ranges = mComposition->GetRanges();
+ for (uint32_t i = 0; i < (ranges ? ranges->Length() : 0); ++i) {
+ const TextRange& textRange = ranges->ElementAt(i);
+ if (!textRange.Length() ||
+ textRange.mRangeType != TextRangeType::eRawClause) {
+ continue;
+ }
+ if (!mPhonetic) {
+ mPhonetic = new nsString();
+ }
+ nsAutoString stringToInsert(aStringToInsert);
+ stringToInsert.Mid(*mPhonetic,
+ textRange.mStartOffset, textRange.Length());
+ }
+
+ transaction = CreateTxnForComposition(aStringToInsert);
+ isIMETransaction = true;
+ // All characters of the composition string will be replaced with
+ // aStringToInsert. So, we need to emulate to remove the composition
+ // string.
+ insertedTextNode = mIMETextNode;
+ insertedOffset = mIMETextOffset;
+ mIMETextLength = aStringToInsert.Length();
+ } else {
+ transaction = CreateTxnForInsertText(aStringToInsert, aTextNode, aOffset);
+ }
+
+ // Let listeners know what's up
+ for (auto& listener : mActionListeners) {
+ listener->WillInsertText(
+ static_cast<nsIDOMCharacterData*>(insertedTextNode->AsDOMNode()),
+ insertedOffset, aStringToInsert);
+ }
+
+ // XXX We may not need these view batches anymore. This is handled at a
+ // higher level now I believe.
+ BeginUpdateViewBatch();
+ nsresult rv = DoTransaction(transaction);
+ EndUpdateViewBatch();
+
+ // let listeners know what happened
+ for (auto& listener : mActionListeners) {
+ listener->DidInsertText(
+ static_cast<nsIDOMCharacterData*>(insertedTextNode->AsDOMNode()),
+ insertedOffset, aStringToInsert, rv);
+ }
+
+ // Added some cruft here for bug 43366. Layout was crashing because we left
+ // an empty text node lying around in the document. So I delete empty text
+ // nodes caused by IME. I have to mark the IME transaction as "fixed", which
+ // means that furure IME txns won't merge with it. This is because we don't
+ // want future IME txns trying to put their text into a node that is no
+ // longer in the document. This does not break undo/redo, because all these
+ // txns are wrapped in a parent PlaceHolder txn, and placeholder txns are
+ // already savvy to having multiple ime txns inside them.
+
+ // Delete empty IME text node if there is one
+ if (isIMETransaction && mIMETextNode) {
+ uint32_t len = mIMETextNode->Length();
+ if (!len) {
+ DeleteNode(mIMETextNode);
+ mIMETextNode = nullptr;
+ static_cast<CompositionTransaction*>(transaction.get())->MarkFixed();
+ }
+ }
+
+ return rv;
+}
+
+nsresult
+EditorBase::SelectEntireDocument(Selection* aSelection)
+{
+ if (!aSelection) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ nsCOMPtr<nsIDOMElement> rootElement = do_QueryInterface(GetRoot());
+ if (!rootElement) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ return aSelection->SelectAllChildren(rootElement);
+}
+
+nsINode*
+EditorBase::GetFirstEditableNode(nsINode* aRoot)
+{
+ MOZ_ASSERT(aRoot);
+
+ nsIContent* node = GetLeftmostChild(aRoot);
+ if (node && !IsEditable(node)) {
+ node = GetNextNode(node, /* aEditableNode = */ true);
+ }
+
+ return (node != aRoot) ? node : nullptr;
+}
+
+NS_IMETHODIMP
+EditorBase::NotifyDocumentListeners(
+ TDocumentListenerNotification aNotificationType)
+{
+ if (!mDocStateListeners.Length()) {
+ // Maybe there just aren't any.
+ return NS_OK;
+ }
+
+ nsTArray<OwningNonNull<nsIDocumentStateListener>>
+ listeners(mDocStateListeners);
+ nsresult rv = NS_OK;
+
+ switch (aNotificationType) {
+ case eDocumentCreated:
+ for (auto& listener : listeners) {
+ rv = listener->NotifyDocumentCreated();
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ }
+ break;
+
+ case eDocumentToBeDestroyed:
+ for (auto& listener : listeners) {
+ rv = listener->NotifyDocumentWillBeDestroyed();
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ }
+ break;
+
+ case eDocumentStateChanged: {
+ bool docIsDirty;
+ rv = GetDocumentModified(&docIsDirty);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (static_cast<int8_t>(docIsDirty) == mDocDirtyState) {
+ return NS_OK;
+ }
+
+ mDocDirtyState = docIsDirty;
+
+ for (auto& listener : listeners) {
+ rv = listener->NotifyDocumentStateChanged(mDocDirtyState);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ }
+ break;
+ }
+ default:
+ NS_NOTREACHED("Unknown notification");
+ }
+
+ return rv;
+}
+
+already_AddRefed<InsertTextTransaction>
+EditorBase::CreateTxnForInsertText(const nsAString& aStringToInsert,
+ Text& aTextNode,
+ int32_t aOffset)
+{
+ RefPtr<InsertTextTransaction> transaction =
+ new InsertTextTransaction(aTextNode, aOffset, aStringToInsert, *this,
+ &mRangeUpdater);
+ return transaction.forget();
+}
+
+nsresult
+EditorBase::DeleteText(nsGenericDOMDataNode& aCharData,
+ uint32_t aOffset,
+ uint32_t aLength)
+{
+ RefPtr<DeleteTextTransaction> transaction =
+ CreateTxnForDeleteText(aCharData, aOffset, aLength);
+ NS_ENSURE_STATE(transaction);
+
+ AutoRules beginRulesSniffing(this, EditAction::deleteText,
+ nsIEditor::ePrevious);
+
+ // Let listeners know what's up
+ for (auto& listener : mActionListeners) {
+ listener->WillDeleteText(
+ static_cast<nsIDOMCharacterData*>(GetAsDOMNode(&aCharData)), aOffset,
+ aLength);
+ }
+
+ nsresult rv = DoTransaction(transaction);
+
+ // Let listeners know what happened
+ for (auto& listener : mActionListeners) {
+ listener->DidDeleteText(
+ static_cast<nsIDOMCharacterData*>(GetAsDOMNode(&aCharData)), aOffset,
+ aLength, rv);
+ }
+
+ return rv;
+}
+
+already_AddRefed<DeleteTextTransaction>
+EditorBase::CreateTxnForDeleteText(nsGenericDOMDataNode& aCharData,
+ uint32_t aOffset,
+ uint32_t aLength)
+{
+ RefPtr<DeleteTextTransaction> transaction =
+ new DeleteTextTransaction(*this, aCharData, aOffset, aLength,
+ &mRangeUpdater);
+ nsresult rv = transaction->Init();
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ return transaction.forget();
+}
+
+already_AddRefed<SplitNodeTransaction>
+EditorBase::CreateTxnForSplitNode(nsIContent& aNode,
+ uint32_t aOffset)
+{
+ RefPtr<SplitNodeTransaction> transaction =
+ new SplitNodeTransaction(*this, aNode, aOffset);
+ return transaction.forget();
+}
+
+already_AddRefed<JoinNodeTransaction>
+EditorBase::CreateTxnForJoinNode(nsINode& aLeftNode,
+ nsINode& aRightNode)
+{
+ RefPtr<JoinNodeTransaction> transaction =
+ new JoinNodeTransaction(*this, aLeftNode, aRightNode);
+
+ NS_ENSURE_SUCCESS(transaction->CheckValidity(), nullptr);
+
+ return transaction.forget();
+}
+
+struct SavedRange final
+{
+ RefPtr<Selection> mSelection;
+ nsCOMPtr<nsINode> mStartNode;
+ nsCOMPtr<nsINode> mEndNode;
+ int32_t mStartOffset;
+ int32_t mEndOffset;
+};
+
+nsresult
+EditorBase::SplitNodeImpl(nsIContent& aExistingRightNode,
+ int32_t aOffset,
+ nsIContent& aNewLeftNode)
+{
+ // Remember all selection points.
+ AutoTArray<SavedRange, 10> savedRanges;
+ for (size_t i = 0; i < kPresentSelectionTypeCount; ++i) {
+ SelectionType selectionType(ToSelectionType(1 << i));
+ SavedRange range;
+ range.mSelection = GetSelection(selectionType);
+ if (selectionType == SelectionType::eNormal) {
+ NS_ENSURE_TRUE(range.mSelection, NS_ERROR_NULL_POINTER);
+ } else if (!range.mSelection) {
+ // For non-normal selections, skip over the non-existing ones.
+ continue;
+ }
+
+ for (uint32_t j = 0; j < range.mSelection->RangeCount(); ++j) {
+ RefPtr<nsRange> r = range.mSelection->GetRangeAt(j);
+ MOZ_ASSERT(r->IsPositioned());
+ range.mStartNode = r->GetStartParent();
+ range.mStartOffset = r->StartOffset();
+ range.mEndNode = r->GetEndParent();
+ range.mEndOffset = r->EndOffset();
+
+ savedRanges.AppendElement(range);
+ }
+ }
+
+ nsCOMPtr<nsINode> parent = aExistingRightNode.GetParentNode();
+ NS_ENSURE_TRUE(parent, NS_ERROR_NULL_POINTER);
+
+ ErrorResult rv;
+ nsCOMPtr<nsINode> refNode = &aExistingRightNode;
+ parent->InsertBefore(aNewLeftNode, refNode, rv);
+ NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult());
+
+ // Split the children between the two nodes. At this point,
+ // aExistingRightNode has all the children. Move all the children whose
+ // index is < aOffset to aNewLeftNode.
+ if (aOffset < 0) {
+ // This means move no children
+ return NS_OK;
+ }
+
+ // If it's a text node, just shuffle around some text
+ if (aExistingRightNode.GetAsText() && aNewLeftNode.GetAsText()) {
+ // Fix right node
+ nsAutoString leftText;
+ aExistingRightNode.GetAsText()->SubstringData(0, aOffset, leftText);
+ aExistingRightNode.GetAsText()->DeleteData(0, aOffset);
+ // Fix left node
+ aNewLeftNode.GetAsText()->SetData(leftText);
+ } else {
+ // Otherwise it's an interior node, so shuffle around the children. Go
+ // through list backwards so deletes don't interfere with the iteration.
+ nsCOMPtr<nsINodeList> childNodes = aExistingRightNode.ChildNodes();
+ for (int32_t i = aOffset - 1; i >= 0; i--) {
+ nsCOMPtr<nsIContent> childNode = childNodes->Item(i);
+ if (childNode) {
+ aExistingRightNode.RemoveChild(*childNode, rv);
+ if (!rv.Failed()) {
+ nsCOMPtr<nsIContent> firstChild = aNewLeftNode.GetFirstChild();
+ aNewLeftNode.InsertBefore(*childNode, firstChild, rv);
+ }
+ }
+ if (rv.Failed()) {
+ break;
+ }
+ }
+ }
+
+ // Handle selection
+ nsCOMPtr<nsIPresShell> ps = GetPresShell();
+ if (ps) {
+ ps->FlushPendingNotifications(Flush_Frames);
+ }
+
+ bool shouldSetSelection = GetShouldTxnSetSelection();
+
+ RefPtr<Selection> previousSelection;
+ for (size_t i = 0; i < savedRanges.Length(); ++i) {
+ // Adjust the selection if needed.
+ SavedRange& range = savedRanges[i];
+
+ // If we have not seen the selection yet, clear all of its ranges.
+ if (range.mSelection != previousSelection) {
+ nsresult rv = range.mSelection->RemoveAllRanges();
+ NS_ENSURE_SUCCESS(rv, rv);
+ previousSelection = range.mSelection;
+ }
+
+ if (shouldSetSelection &&
+ range.mSelection->Type() == SelectionType::eNormal) {
+ // If the editor should adjust the selection, don't bother restoring
+ // the ranges for the normal selection here.
+ continue;
+ }
+
+ // Split the selection into existing node and new node.
+ if (range.mStartNode == &aExistingRightNode) {
+ if (range.mStartOffset < aOffset) {
+ range.mStartNode = &aNewLeftNode;
+ } else {
+ range.mStartOffset -= aOffset;
+ }
+ }
+
+ if (range.mEndNode == &aExistingRightNode) {
+ if (range.mEndOffset < aOffset) {
+ range.mEndNode = &aNewLeftNode;
+ } else {
+ range.mEndOffset -= aOffset;
+ }
+ }
+
+ RefPtr<nsRange> newRange;
+ nsresult rv = nsRange::CreateRange(range.mStartNode, range.mStartOffset,
+ range.mEndNode, range.mEndOffset,
+ getter_AddRefs(newRange));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = range.mSelection->AddRange(newRange);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (shouldSetSelection) {
+ // Editor wants us to set selection at split point.
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+ selection->Collapse(&aNewLeftNode, aOffset);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+EditorBase::JoinNodesImpl(nsINode* aNodeToKeep,
+ nsINode* aNodeToJoin,
+ nsINode* aParent)
+{
+ MOZ_ASSERT(aNodeToKeep);
+ MOZ_ASSERT(aNodeToJoin);
+ MOZ_ASSERT(aParent);
+
+ uint32_t firstNodeLength = aNodeToJoin->Length();
+
+ int32_t joinOffset;
+ GetNodeLocation(aNodeToJoin, &joinOffset);
+ int32_t keepOffset;
+ nsINode* parent = GetNodeLocation(aNodeToKeep, &keepOffset);
+
+ // Remember all selection points.
+ AutoTArray<SavedRange, 10> savedRanges;
+ for (size_t i = 0; i < kPresentSelectionTypeCount; ++i) {
+ SelectionType selectionType(ToSelectionType(1 << i));
+ SavedRange range;
+ range.mSelection = GetSelection(selectionType);
+ if (selectionType == SelectionType::eNormal) {
+ NS_ENSURE_TRUE(range.mSelection, NS_ERROR_NULL_POINTER);
+ } else if (!range.mSelection) {
+ // For non-normal selections, skip over the non-existing ones.
+ continue;
+ }
+
+ for (uint32_t j = 0; j < range.mSelection->RangeCount(); ++j) {
+ RefPtr<nsRange> r = range.mSelection->GetRangeAt(j);
+ MOZ_ASSERT(r->IsPositioned());
+ range.mStartNode = r->GetStartParent();
+ range.mStartOffset = r->StartOffset();
+ range.mEndNode = r->GetEndParent();
+ range.mEndOffset = r->EndOffset();
+
+ // If selection endpoint is between the nodes, remember it as being
+ // in the one that is going away instead. This simplifies later selection
+ // adjustment logic at end of this method.
+ if (range.mStartNode) {
+ if (range.mStartNode == parent &&
+ joinOffset < range.mStartOffset &&
+ range.mStartOffset <= keepOffset) {
+ range.mStartNode = aNodeToJoin;
+ range.mStartOffset = firstNodeLength;
+ }
+ if (range.mEndNode == parent &&
+ joinOffset < range.mEndOffset &&
+ range.mEndOffset <= keepOffset) {
+ range.mEndNode = aNodeToJoin;
+ range.mEndOffset = firstNodeLength;
+ }
+ }
+
+ savedRanges.AppendElement(range);
+ }
+ }
+
+ // OK, ready to do join now.
+ // If it's a text node, just shuffle around some text.
+ nsCOMPtr<nsIDOMCharacterData> keepNodeAsText( do_QueryInterface(aNodeToKeep) );
+ nsCOMPtr<nsIDOMCharacterData> joinNodeAsText( do_QueryInterface(aNodeToJoin) );
+ if (keepNodeAsText && joinNodeAsText) {
+ nsAutoString rightText;
+ nsAutoString leftText;
+ keepNodeAsText->GetData(rightText);
+ joinNodeAsText->GetData(leftText);
+ leftText += rightText;
+ keepNodeAsText->SetData(leftText);
+ } else {
+ // Otherwise it's an interior node, so shuffle around the children.
+ nsCOMPtr<nsINodeList> childNodes = aNodeToJoin->ChildNodes();
+ MOZ_ASSERT(childNodes);
+
+ // Remember the first child in aNodeToKeep, we'll insert all the children of aNodeToJoin in front of it
+ // GetFirstChild returns nullptr firstNode if aNodeToKeep has no children, that's OK.
+ nsCOMPtr<nsIContent> firstNode = aNodeToKeep->GetFirstChild();
+
+ // Have to go through the list backwards to keep deletes from interfering with iteration.
+ for (uint32_t i = childNodes->Length(); i; --i) {
+ nsCOMPtr<nsIContent> childNode = childNodes->Item(i - 1);
+ if (childNode) {
+ // prepend children of aNodeToJoin
+ ErrorResult err;
+ aNodeToKeep->InsertBefore(*childNode, firstNode, err);
+ NS_ENSURE_TRUE(!err.Failed(), err.StealNSResult());
+ firstNode = childNode.forget();
+ }
+ }
+ }
+
+ // Delete the extra node.
+ ErrorResult err;
+ aParent->RemoveChild(*aNodeToJoin, err);
+
+ bool shouldSetSelection = GetShouldTxnSetSelection();
+
+ RefPtr<Selection> previousSelection;
+ for (size_t i = 0; i < savedRanges.Length(); ++i) {
+ // And adjust the selection if needed.
+ SavedRange& range = savedRanges[i];
+
+ // If we have not seen the selection yet, clear all of its ranges.
+ if (range.mSelection != previousSelection) {
+ nsresult rv = range.mSelection->RemoveAllRanges();
+ NS_ENSURE_SUCCESS(rv, rv);
+ previousSelection = range.mSelection;
+ }
+
+ if (shouldSetSelection &&
+ range.mSelection->Type() == SelectionType::eNormal) {
+ // If the editor should adjust the selection, don't bother restoring
+ // the ranges for the normal selection here.
+ continue;
+ }
+
+ // Check to see if we joined nodes where selection starts.
+ if (range.mStartNode == aNodeToJoin) {
+ range.mStartNode = aNodeToKeep;
+ } else if (range.mStartNode == aNodeToKeep) {
+ range.mStartOffset += firstNodeLength;
+ }
+
+ // Check to see if we joined nodes where selection ends.
+ if (range.mEndNode == aNodeToJoin) {
+ range.mEndNode = aNodeToKeep;
+ } else if (range.mEndNode == aNodeToKeep) {
+ range.mEndOffset += firstNodeLength;
+ }
+
+ RefPtr<nsRange> newRange;
+ nsresult rv = nsRange::CreateRange(range.mStartNode, range.mStartOffset,
+ range.mEndNode, range.mEndOffset,
+ getter_AddRefs(newRange));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = range.mSelection->AddRange(newRange);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (shouldSetSelection) {
+ // Editor wants us to set selection at join point.
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+ selection->Collapse(aNodeToKeep, AssertedCast<int32_t>(firstNodeLength));
+ }
+
+ return err.StealNSResult();
+}
+
+int32_t
+EditorBase::GetChildOffset(nsIDOMNode* aChild,
+ nsIDOMNode* aParent)
+{
+ MOZ_ASSERT(aChild && aParent);
+
+ nsCOMPtr<nsINode> parent = do_QueryInterface(aParent);
+ nsCOMPtr<nsINode> child = do_QueryInterface(aChild);
+ MOZ_ASSERT(parent && child);
+
+ int32_t idx = parent->IndexOf(child);
+ MOZ_ASSERT(idx != -1);
+ return idx;
+}
+
+// static
+already_AddRefed<nsIDOMNode>
+EditorBase::GetNodeLocation(nsIDOMNode* aChild,
+ int32_t* outOffset)
+{
+ MOZ_ASSERT(aChild && outOffset);
+ NS_ENSURE_TRUE(aChild && outOffset, nullptr);
+ *outOffset = -1;
+
+ nsCOMPtr<nsIDOMNode> parent;
+
+ MOZ_ALWAYS_SUCCEEDS(aChild->GetParentNode(getter_AddRefs(parent)));
+ if (parent) {
+ *outOffset = GetChildOffset(aChild, parent);
+ }
+
+ return parent.forget();
+}
+
+nsINode*
+EditorBase::GetNodeLocation(nsINode* aChild,
+ int32_t* aOffset)
+{
+ MOZ_ASSERT(aChild);
+ MOZ_ASSERT(aOffset);
+
+ nsINode* parent = aChild->GetParentNode();
+ if (parent) {
+ *aOffset = parent->IndexOf(aChild);
+ MOZ_ASSERT(*aOffset != -1);
+ } else {
+ *aOffset = -1;
+ }
+ return parent;
+}
+
+/**
+ * Returns the number of things inside aNode. If aNode is text, returns number
+ * of characters. If not, returns number of children nodes.
+ */
+nsresult
+EditorBase::GetLengthOfDOMNode(nsIDOMNode* aNode,
+ uint32_t& aCount)
+{
+ aCount = 0;
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER);
+ aCount = node->Length();
+ return NS_OK;
+}
+
+nsIContent*
+EditorBase::GetPriorNode(nsINode* aParentNode,
+ int32_t aOffset,
+ bool aEditableNode,
+ bool aNoBlockCrossing)
+{
+ MOZ_ASSERT(aParentNode);
+
+ // If we are at the beginning of the node, or it is a text node, then just
+ // look before it.
+ if (!aOffset || aParentNode->NodeType() == nsIDOMNode::TEXT_NODE) {
+ if (aNoBlockCrossing && IsBlockNode(aParentNode)) {
+ // If we aren't allowed to cross blocks, don't look before this block.
+ return nullptr;
+ }
+ return GetPriorNode(aParentNode, aEditableNode, aNoBlockCrossing);
+ }
+
+ // else look before the child at 'aOffset'
+ if (nsIContent* child = aParentNode->GetChildAt(aOffset)) {
+ return GetPriorNode(child, aEditableNode, aNoBlockCrossing);
+ }
+
+ // unless there isn't one, in which case we are at the end of the node
+ // and want the deep-right child.
+ nsIContent* resultNode = GetRightmostChild(aParentNode, aNoBlockCrossing);
+ if (!resultNode || !aEditableNode || IsEditable(resultNode)) {
+ return resultNode;
+ }
+
+ // restart the search from the non-editable node we just found
+ return GetPriorNode(resultNode, aEditableNode, aNoBlockCrossing);
+}
+
+nsIContent*
+EditorBase::GetNextNode(nsINode* aParentNode,
+ int32_t aOffset,
+ bool aEditableNode,
+ bool aNoBlockCrossing)
+{
+ MOZ_ASSERT(aParentNode);
+
+ // if aParentNode is a text node, use its location instead
+ if (aParentNode->NodeType() == nsIDOMNode::TEXT_NODE) {
+ nsINode* parent = aParentNode->GetParentNode();
+ NS_ENSURE_TRUE(parent, nullptr);
+ aOffset = parent->IndexOf(aParentNode) + 1; // _after_ the text node
+ aParentNode = parent;
+ }
+
+ // look at the child at 'aOffset'
+ nsIContent* child = aParentNode->GetChildAt(aOffset);
+ if (child) {
+ if (aNoBlockCrossing && IsBlockNode(child)) {
+ return child;
+ }
+
+ nsIContent* resultNode = GetLeftmostChild(child, aNoBlockCrossing);
+ if (!resultNode) {
+ return child;
+ }
+
+ if (!IsDescendantOfEditorRoot(resultNode)) {
+ return nullptr;
+ }
+
+ if (!aEditableNode || IsEditable(resultNode)) {
+ return resultNode;
+ }
+
+ // restart the search from the non-editable node we just found
+ return GetNextNode(resultNode, aEditableNode, aNoBlockCrossing);
+ }
+
+ // unless there isn't one, in which case we are at the end of the node
+ // and want the next one.
+ if (aNoBlockCrossing && IsBlockNode(aParentNode)) {
+ // don't cross out of parent block
+ return nullptr;
+ }
+
+ return GetNextNode(aParentNode, aEditableNode, aNoBlockCrossing);
+}
+
+nsIContent*
+EditorBase::GetPriorNode(nsINode* aCurrentNode,
+ bool aEditableNode,
+ bool aNoBlockCrossing /* = false */)
+{
+ MOZ_ASSERT(aCurrentNode);
+
+ if (!IsDescendantOfEditorRoot(aCurrentNode)) {
+ return nullptr;
+ }
+
+ return FindNode(aCurrentNode, false, aEditableNode, aNoBlockCrossing);
+}
+
+nsIContent*
+EditorBase::FindNextLeafNode(nsINode* aCurrentNode,
+ bool aGoForward,
+ bool bNoBlockCrossing)
+{
+ // called only by GetPriorNode so we don't need to check params.
+ NS_PRECONDITION(IsDescendantOfEditorRoot(aCurrentNode) &&
+ !IsEditorRoot(aCurrentNode),
+ "Bogus arguments");
+
+ nsINode* cur = aCurrentNode;
+ for (;;) {
+ // if aCurrentNode has a sibling in the right direction, return
+ // that sibling's closest child (or itself if it has no children)
+ nsIContent* sibling =
+ aGoForward ? cur->GetNextSibling() : cur->GetPreviousSibling();
+ if (sibling) {
+ if (bNoBlockCrossing && IsBlockNode(sibling)) {
+ // don't look inside prevsib, since it is a block
+ return sibling;
+ }
+ nsIContent *leaf =
+ aGoForward ? GetLeftmostChild(sibling, bNoBlockCrossing) :
+ GetRightmostChild(sibling, bNoBlockCrossing);
+ if (!leaf) {
+ return sibling;
+ }
+
+ return leaf;
+ }
+
+ nsINode *parent = cur->GetParentNode();
+ if (!parent) {
+ return nullptr;
+ }
+
+ NS_ASSERTION(IsDescendantOfEditorRoot(parent),
+ "We started with a proper descendant of root, and should stop "
+ "if we ever hit the root, so we better have a descendant of "
+ "root now!");
+ if (IsEditorRoot(parent) ||
+ (bNoBlockCrossing && IsBlockNode(parent))) {
+ return nullptr;
+ }
+
+ cur = parent;
+ }
+
+ NS_NOTREACHED("What part of for(;;) do you not understand?");
+ return nullptr;
+}
+
+nsIContent*
+EditorBase::GetNextNode(nsINode* aCurrentNode,
+ bool aEditableNode,
+ bool bNoBlockCrossing)
+{
+ MOZ_ASSERT(aCurrentNode);
+
+ if (!IsDescendantOfEditorRoot(aCurrentNode)) {
+ return nullptr;
+ }
+
+ return FindNode(aCurrentNode, true, aEditableNode, bNoBlockCrossing);
+}
+
+nsIContent*
+EditorBase::FindNode(nsINode* aCurrentNode,
+ bool aGoForward,
+ bool aEditableNode,
+ bool bNoBlockCrossing)
+{
+ if (IsEditorRoot(aCurrentNode)) {
+ // Don't allow traversal above the root node! This helps
+ // prevent us from accidentally editing browser content
+ // when the editor is in a text widget.
+
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIContent> candidate =
+ FindNextLeafNode(aCurrentNode, aGoForward, bNoBlockCrossing);
+
+ if (!candidate) {
+ return nullptr;
+ }
+
+ if (!aEditableNode || IsEditable(candidate)) {
+ return candidate;
+ }
+
+ return FindNode(candidate, aGoForward, aEditableNode, bNoBlockCrossing);
+}
+
+nsIContent*
+EditorBase::GetRightmostChild(nsINode* aCurrentNode,
+ bool bNoBlockCrossing)
+{
+ NS_ENSURE_TRUE(aCurrentNode, nullptr);
+ nsIContent *cur = aCurrentNode->GetLastChild();
+ if (!cur) {
+ return nullptr;
+ }
+ for (;;) {
+ if (bNoBlockCrossing && IsBlockNode(cur)) {
+ return cur;
+ }
+ nsIContent* next = cur->GetLastChild();
+ if (!next) {
+ return cur;
+ }
+ cur = next;
+ }
+
+ NS_NOTREACHED("What part of for(;;) do you not understand?");
+ return nullptr;
+}
+
+nsIContent*
+EditorBase::GetLeftmostChild(nsINode* aCurrentNode,
+ bool bNoBlockCrossing)
+{
+ NS_ENSURE_TRUE(aCurrentNode, nullptr);
+ nsIContent *cur = aCurrentNode->GetFirstChild();
+ if (!cur) {
+ return nullptr;
+ }
+ for (;;) {
+ if (bNoBlockCrossing && IsBlockNode(cur)) {
+ return cur;
+ }
+ nsIContent *next = cur->GetFirstChild();
+ if (!next) {
+ return cur;
+ }
+ cur = next;
+ }
+
+ NS_NOTREACHED("What part of for(;;) do you not understand?");
+ return nullptr;
+}
+
+bool
+EditorBase::IsBlockNode(nsINode* aNode)
+{
+ // stub to be overridden in HTMLEditor.
+ // screwing around with the class hierarchy here in order
+ // to not duplicate the code in GetNextNode/GetPrevNode
+ // across both EditorBase/HTMLEditor.
+ return false;
+}
+
+bool
+EditorBase::CanContain(nsINode& aParent,
+ nsIContent& aChild)
+{
+ switch (aParent.NodeType()) {
+ case nsIDOMNode::ELEMENT_NODE:
+ case nsIDOMNode::DOCUMENT_FRAGMENT_NODE:
+ return TagCanContain(*aParent.NodeInfo()->NameAtom(), aChild);
+ }
+ return false;
+}
+
+bool
+EditorBase::CanContainTag(nsINode& aParent,
+ nsIAtom& aChildTag)
+{
+ switch (aParent.NodeType()) {
+ case nsIDOMNode::ELEMENT_NODE:
+ case nsIDOMNode::DOCUMENT_FRAGMENT_NODE:
+ return TagCanContainTag(*aParent.NodeInfo()->NameAtom(), aChildTag);
+ }
+ return false;
+}
+
+bool
+EditorBase::TagCanContain(nsIAtom& aParentTag,
+ nsIContent& aChild)
+{
+ switch (aChild.NodeType()) {
+ case nsIDOMNode::TEXT_NODE:
+ case nsIDOMNode::ELEMENT_NODE:
+ case nsIDOMNode::DOCUMENT_FRAGMENT_NODE:
+ return TagCanContainTag(aParentTag, *aChild.NodeInfo()->NameAtom());
+ }
+ return false;
+}
+
+bool
+EditorBase::TagCanContainTag(nsIAtom& aParentTag,
+ nsIAtom& aChildTag)
+{
+ return true;
+}
+
+bool
+EditorBase::IsRoot(nsIDOMNode* inNode)
+{
+ NS_ENSURE_TRUE(inNode, false);
+
+ nsCOMPtr<nsIDOMNode> rootNode = do_QueryInterface(GetRoot());
+
+ return inNode == rootNode;
+}
+
+bool
+EditorBase::IsRoot(nsINode* inNode)
+{
+ NS_ENSURE_TRUE(inNode, false);
+
+ nsCOMPtr<nsINode> rootNode = GetRoot();
+
+ return inNode == rootNode;
+}
+
+bool
+EditorBase::IsEditorRoot(nsINode* aNode)
+{
+ NS_ENSURE_TRUE(aNode, false);
+ nsCOMPtr<nsINode> rootNode = GetEditorRoot();
+ return aNode == rootNode;
+}
+
+bool
+EditorBase::IsDescendantOfRoot(nsIDOMNode* inNode)
+{
+ nsCOMPtr<nsINode> node = do_QueryInterface(inNode);
+ return IsDescendantOfRoot(node);
+}
+
+bool
+EditorBase::IsDescendantOfRoot(nsINode* inNode)
+{
+ NS_ENSURE_TRUE(inNode, false);
+ nsCOMPtr<nsIContent> root = GetRoot();
+ NS_ENSURE_TRUE(root, false);
+
+ return nsContentUtils::ContentIsDescendantOf(inNode, root);
+}
+
+bool
+EditorBase::IsDescendantOfEditorRoot(nsINode* aNode)
+{
+ NS_ENSURE_TRUE(aNode, false);
+ nsCOMPtr<nsIContent> root = GetEditorRoot();
+ NS_ENSURE_TRUE(root, false);
+
+ return nsContentUtils::ContentIsDescendantOf(aNode, root);
+}
+
+bool
+EditorBase::IsContainer(nsINode* aNode)
+{
+ return aNode ? true : false;
+}
+
+bool
+EditorBase::IsContainer(nsIDOMNode* aNode)
+{
+ return aNode ? true : false;
+}
+
+static inline bool
+IsElementVisible(Element* aElement)
+{
+ if (aElement->GetPrimaryFrame()) {
+ // It's visible, for our purposes
+ return true;
+ }
+
+ nsIContent *cur = aElement;
+ for (;;) {
+ // Walk up the tree looking for the nearest ancestor with a frame.
+ // The state of the child right below it will determine whether
+ // we might possibly have a frame or not.
+ bool haveLazyBitOnChild = cur->HasFlag(NODE_NEEDS_FRAME);
+ cur = cur->GetFlattenedTreeParent();
+ if (!cur) {
+ if (!haveLazyBitOnChild) {
+ // None of our ancestors have lazy bits set, so we shouldn't
+ // have a frame
+ return false;
+ }
+
+ // The root has a lazy frame construction bit. We need to check
+ // our style.
+ break;
+ }
+
+ if (cur->GetPrimaryFrame()) {
+ if (!haveLazyBitOnChild) {
+ // Our ancestor directly under |cur| doesn't have lazy bits;
+ // that means we won't get a frame
+ return false;
+ }
+
+ if (cur->GetPrimaryFrame()->IsLeaf()) {
+ // Nothing under here will ever get frames
+ return false;
+ }
+
+ // Otherwise, we might end up with a frame when that lazy bit is
+ // processed. Figure out our actual style.
+ break;
+ }
+ }
+
+ // Now it might be that we have no frame because we're in a
+ // display:none subtree, or it might be that we're just dealing with
+ // lazy frame construction and it hasn't happened yet. Check which
+ // one it is.
+ RefPtr<nsStyleContext> styleContext =
+ nsComputedDOMStyle::GetStyleContextForElementNoFlush(aElement,
+ nullptr, nullptr);
+ if (styleContext) {
+ return styleContext->StyleDisplay()->mDisplay != StyleDisplay::None;
+ }
+ return false;
+}
+
+bool
+EditorBase::IsEditable(nsIDOMNode* aNode)
+{
+ nsCOMPtr<nsIContent> content = do_QueryInterface(aNode);
+ return IsEditable(content);
+}
+
+bool
+EditorBase::IsEditable(nsINode* aNode)
+{
+ NS_ENSURE_TRUE(aNode, false);
+
+ if (!aNode->IsNodeOfType(nsINode::eCONTENT) || IsMozEditorBogusNode(aNode) ||
+ !IsModifiableNode(aNode)) {
+ return false;
+ }
+
+ // see if it has a frame. If so, we'll edit it.
+ // special case for textnodes: frame must have width.
+ if (aNode->IsElement() && !IsElementVisible(aNode->AsElement())) {
+ // If the element has no frame, it's not editable. Note that we
+ // need to check IsElement() here, because some of our tests
+ // rely on frameless textnodes being visible.
+ return false;
+ }
+ switch (aNode->NodeType()) {
+ case nsIDOMNode::ELEMENT_NODE:
+ case nsIDOMNode::TEXT_NODE:
+ return true; // element or text node; not invisible
+ default:
+ return false;
+ }
+}
+
+bool
+EditorBase::IsMozEditorBogusNode(nsINode* element)
+{
+ return element && element->IsElement() &&
+ element->AsElement()->AttrValueIs(kNameSpaceID_None,
+ kMOZEditorBogusNodeAttrAtom, kMOZEditorBogusNodeValue,
+ eCaseMatters);
+}
+
+uint32_t
+EditorBase::CountEditableChildren(nsINode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ uint32_t count = 0;
+ for (nsIContent* child = aNode->GetFirstChild();
+ child;
+ child = child->GetNextSibling()) {
+ if (IsEditable(child)) {
+ ++count;
+ }
+ }
+ return count;
+}
+
+NS_IMETHODIMP
+EditorBase::IncrementModificationCount(int32_t inNumMods)
+{
+ uint32_t oldModCount = mModCount;
+
+ mModCount += inNumMods;
+
+ if ((!oldModCount && mModCount) ||
+ (oldModCount && !mModCount)) {
+ NotifyDocumentListeners(eDocumentStateChanged);
+ }
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+EditorBase::GetModificationCount(int32_t* outModCount)
+{
+ NS_ENSURE_ARG_POINTER(outModCount);
+ *outModCount = mModCount;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+EditorBase::ResetModificationCount()
+{
+ bool doNotify = (mModCount != 0);
+
+ mModCount = 0;
+
+ if (doNotify) {
+ NotifyDocumentListeners(eDocumentStateChanged);
+ }
+ return NS_OK;
+}
+
+nsIAtom*
+EditorBase::GetTag(nsIDOMNode* aNode)
+{
+ nsCOMPtr<nsIContent> content = do_QueryInterface(aNode);
+
+ if (!content) {
+ NS_ASSERTION(aNode, "null node passed to EditorBase::GetTag()");
+ return nullptr;
+ }
+
+ return content->NodeInfo()->NameAtom();
+}
+
+nsresult
+EditorBase::GetTagString(nsIDOMNode* aNode,
+ nsAString& outString)
+{
+ if (!aNode) {
+ NS_NOTREACHED("null node passed to EditorBase::GetTagString()");
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ nsIAtom *atom = GetTag(aNode);
+ if (!atom) {
+ return NS_ERROR_FAILURE;
+ }
+
+ atom->ToString(outString);
+ return NS_OK;
+}
+
+bool
+EditorBase::NodesSameType(nsIDOMNode* aNode1,
+ nsIDOMNode* aNode2)
+{
+ if (!aNode1 || !aNode2) {
+ NS_NOTREACHED("null node passed to EditorBase::NodesSameType()");
+ return false;
+ }
+
+ nsCOMPtr<nsIContent> content1 = do_QueryInterface(aNode1);
+ NS_ENSURE_TRUE(content1, false);
+
+ nsCOMPtr<nsIContent> content2 = do_QueryInterface(aNode2);
+ NS_ENSURE_TRUE(content2, false);
+
+ return AreNodesSameType(content1, content2);
+}
+
+bool
+EditorBase::AreNodesSameType(nsIContent* aNode1,
+ nsIContent* aNode2)
+{
+ MOZ_ASSERT(aNode1);
+ MOZ_ASSERT(aNode2);
+ return aNode1->NodeInfo()->NameAtom() == aNode2->NodeInfo()->NameAtom();
+}
+
+bool
+EditorBase::IsTextNode(nsIDOMNode* aNode)
+{
+ if (!aNode) {
+ NS_NOTREACHED("null node passed to IsTextNode()");
+ return false;
+ }
+
+ uint16_t nodeType;
+ aNode->GetNodeType(&nodeType);
+ return (nodeType == nsIDOMNode::TEXT_NODE);
+}
+
+bool
+EditorBase::IsTextNode(nsINode* aNode)
+{
+ return aNode->NodeType() == nsIDOMNode::TEXT_NODE;
+}
+
+nsCOMPtr<nsIDOMNode>
+EditorBase::GetChildAt(nsIDOMNode* aParent, int32_t aOffset)
+{
+ nsCOMPtr<nsIDOMNode> resultNode;
+
+ nsCOMPtr<nsIContent> parent = do_QueryInterface(aParent);
+
+ NS_ENSURE_TRUE(parent, resultNode);
+
+ resultNode = do_QueryInterface(parent->GetChildAt(aOffset));
+
+ return resultNode;
+}
+
+/**
+ * GetNodeAtRangeOffsetPoint() returns the node at this position in a range,
+ * assuming that aParentOrNode is the node itself if it's a text node, or
+ * the node's parent otherwise.
+ */
+nsIContent*
+EditorBase::GetNodeAtRangeOffsetPoint(nsIDOMNode* aParentOrNode,
+ int32_t aOffset)
+{
+ nsCOMPtr<nsINode> parentOrNode = do_QueryInterface(aParentOrNode);
+ NS_ENSURE_TRUE(parentOrNode || !aParentOrNode, nullptr);
+ if (parentOrNode->GetAsText()) {
+ return parentOrNode->AsContent();
+ }
+ return parentOrNode->GetChildAt(aOffset);
+}
+
+/**
+ * GetStartNodeAndOffset() returns whatever the start parent & offset is of
+ * the first range in the selection.
+ */
+nsresult
+EditorBase::GetStartNodeAndOffset(Selection* aSelection,
+ nsIDOMNode** outStartNode,
+ int32_t* outStartOffset)
+{
+ NS_ENSURE_TRUE(outStartNode && outStartOffset && aSelection, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsINode> startNode;
+ nsresult rv = GetStartNodeAndOffset(aSelection, getter_AddRefs(startNode),
+ outStartOffset);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (startNode) {
+ NS_ADDREF(*outStartNode = startNode->AsDOMNode());
+ } else {
+ *outStartNode = nullptr;
+ }
+ return NS_OK;
+}
+
+nsresult
+EditorBase::GetStartNodeAndOffset(Selection* aSelection,
+ nsINode** aStartNode,
+ int32_t* aStartOffset)
+{
+ MOZ_ASSERT(aSelection);
+ MOZ_ASSERT(aStartNode);
+ MOZ_ASSERT(aStartOffset);
+
+ *aStartNode = nullptr;
+ *aStartOffset = 0;
+
+ if (!aSelection->RangeCount()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ const nsRange* range = aSelection->GetRangeAt(0);
+ NS_ENSURE_TRUE(range, NS_ERROR_FAILURE);
+
+ NS_ENSURE_TRUE(range->IsPositioned(), NS_ERROR_FAILURE);
+
+ NS_IF_ADDREF(*aStartNode = range->GetStartParent());
+ *aStartOffset = range->StartOffset();
+ return NS_OK;
+}
+
+/**
+ * GetEndNodeAndOffset() returns whatever the end parent & offset is of
+ * the first range in the selection.
+ */
+nsresult
+EditorBase::GetEndNodeAndOffset(Selection* aSelection,
+ nsIDOMNode** outEndNode,
+ int32_t* outEndOffset)
+{
+ NS_ENSURE_TRUE(outEndNode && outEndOffset && aSelection, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsINode> endNode;
+ nsresult rv = GetEndNodeAndOffset(aSelection, getter_AddRefs(endNode),
+ outEndOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (endNode) {
+ NS_ADDREF(*outEndNode = endNode->AsDOMNode());
+ } else {
+ *outEndNode = nullptr;
+ }
+ return NS_OK;
+}
+
+nsresult
+EditorBase::GetEndNodeAndOffset(Selection* aSelection,
+ nsINode** aEndNode,
+ int32_t* aEndOffset)
+{
+ MOZ_ASSERT(aSelection);
+ MOZ_ASSERT(aEndNode);
+ MOZ_ASSERT(aEndOffset);
+
+ *aEndNode = nullptr;
+ *aEndOffset = 0;
+
+ NS_ENSURE_TRUE(aSelection->RangeCount(), NS_ERROR_FAILURE);
+
+ const nsRange* range = aSelection->GetRangeAt(0);
+ NS_ENSURE_TRUE(range, NS_ERROR_FAILURE);
+
+ NS_ENSURE_TRUE(range->IsPositioned(), NS_ERROR_FAILURE);
+
+ NS_IF_ADDREF(*aEndNode = range->GetEndParent());
+ *aEndOffset = range->EndOffset();
+ return NS_OK;
+}
+
+/**
+ * IsPreformatted() checks the style info for the node for the preformatted
+ * text style.
+ */
+nsresult
+EditorBase::IsPreformatted(nsIDOMNode* aNode,
+ bool* aResult)
+{
+ nsCOMPtr<nsIContent> content = do_QueryInterface(aNode);
+
+ NS_ENSURE_TRUE(aResult && content, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsIPresShell> ps = GetPresShell();
+ NS_ENSURE_TRUE(ps, NS_ERROR_NOT_INITIALIZED);
+
+ // Look at the node (and its parent if it's not an element), and grab its style context
+ RefPtr<nsStyleContext> elementStyle;
+ if (!content->IsElement()) {
+ content = content->GetParent();
+ }
+ if (content && content->IsElement()) {
+ elementStyle = nsComputedDOMStyle::GetStyleContextForElementNoFlush(content->AsElement(),
+ nullptr,
+ ps);
+ }
+
+ if (!elementStyle) {
+ // Consider nodes without a style context to be NOT preformatted:
+ // For instance, this is true of JS tags inside the body (which show
+ // up as #text nodes but have no style context).
+ *aResult = false;
+ return NS_OK;
+ }
+
+ const nsStyleText* styleText = elementStyle->StyleText();
+
+ *aResult = styleText->WhiteSpaceIsSignificant();
+ return NS_OK;
+}
+
+
+/**
+ * This splits a node "deeply", splitting children as appropriate. The place
+ * to split is represented by a DOM point at {splitPointParent,
+ * splitPointOffset}. That DOM point must be inside aNode, which is the node
+ * to split. We return the offset in the parent of aNode where the split
+ * terminates - where you would want to insert a new element, for instance, if
+ * that's why you were splitting the node.
+ *
+ * -1 is returned on failure, in unlikely cases like the selection being
+ * unavailable or cloning the node failing. Make sure not to use the returned
+ * offset for anything without checking that it's valid! If you're not using
+ * the offset, it's okay to ignore the return value.
+ */
+int32_t
+EditorBase::SplitNodeDeep(nsIContent& aNode,
+ nsIContent& aSplitPointParent,
+ int32_t aSplitPointOffset,
+ EmptyContainers aEmptyContainers,
+ nsIContent** aOutLeftNode,
+ nsIContent** aOutRightNode)
+{
+ MOZ_ASSERT(&aSplitPointParent == &aNode ||
+ EditorUtils::IsDescendantOf(&aSplitPointParent, &aNode));
+ int32_t offset = aSplitPointOffset;
+
+ nsCOMPtr<nsIContent> leftNode, rightNode;
+ OwningNonNull<nsIContent> nodeToSplit = aSplitPointParent;
+ while (true) {
+ // Need to insert rules code call here to do things like not split a list
+ // if you are after the last <li> or before the first, etc. For now we
+ // just have some smarts about unneccessarily splitting text nodes, which
+ // should be universal enough to put straight in this EditorBase routine.
+
+ bool didSplit = false;
+
+ if ((aEmptyContainers == EmptyContainers::yes &&
+ !nodeToSplit->GetAsText()) ||
+ (offset && offset != (int32_t)nodeToSplit->Length())) {
+ didSplit = true;
+ ErrorResult rv;
+ nsCOMPtr<nsIContent> newLeftNode = SplitNode(nodeToSplit, offset, rv);
+ NS_ENSURE_TRUE(!NS_FAILED(rv.StealNSResult()), -1);
+
+ rightNode = nodeToSplit;
+ leftNode = newLeftNode;
+ }
+
+ NS_ENSURE_TRUE(nodeToSplit->GetParent(), -1);
+ OwningNonNull<nsIContent> parentNode = *nodeToSplit->GetParent();
+
+ if (!didSplit && offset) {
+ // Must be "end of text node" case, we didn't split it, just move past it
+ offset = parentNode->IndexOf(nodeToSplit) + 1;
+ leftNode = nodeToSplit;
+ } else {
+ offset = parentNode->IndexOf(nodeToSplit);
+ rightNode = nodeToSplit;
+ }
+
+ if (nodeToSplit == &aNode) {
+ // we split all the way up to (and including) aNode; we're done
+ break;
+ }
+
+ nodeToSplit = parentNode;
+ }
+
+ if (aOutLeftNode) {
+ leftNode.forget(aOutLeftNode);
+ }
+ if (aOutRightNode) {
+ rightNode.forget(aOutRightNode);
+ }
+
+ return offset;
+}
+
+/**
+ * This joins two like nodes "deeply", joining children as appropriate.
+ * Returns the point of the join, or (nullptr, -1) in case of error.
+ */
+EditorDOMPoint
+EditorBase::JoinNodeDeep(nsIContent& aLeftNode,
+ nsIContent& aRightNode)
+{
+ // While the rightmost children and their descendants of the left node match
+ // the leftmost children and their descendants of the right node, join them
+ // up.
+
+ nsCOMPtr<nsIContent> leftNodeToJoin = &aLeftNode;
+ nsCOMPtr<nsIContent> rightNodeToJoin = &aRightNode;
+ nsCOMPtr<nsINode> parentNode = aRightNode.GetParentNode();
+
+ EditorDOMPoint ret;
+
+ while (leftNodeToJoin && rightNodeToJoin && parentNode &&
+ AreNodesSameType(leftNodeToJoin, rightNodeToJoin)) {
+ uint32_t length = leftNodeToJoin->Length();
+
+ ret.node = rightNodeToJoin;
+ ret.offset = length;
+
+ // Do the join
+ nsresult rv = JoinNodes(*leftNodeToJoin, *rightNodeToJoin);
+ NS_ENSURE_SUCCESS(rv, EditorDOMPoint());
+
+ if (parentNode->GetAsText()) {
+ // We've joined all the way down to text nodes, we're done!
+ return ret;
+ }
+
+ // Get new left and right nodes, and begin anew
+ parentNode = rightNodeToJoin;
+ leftNodeToJoin = parentNode->GetChildAt(length - 1);
+ rightNodeToJoin = parentNode->GetChildAt(length);
+
+ // Skip over non-editable nodes
+ while (leftNodeToJoin && !IsEditable(leftNodeToJoin)) {
+ leftNodeToJoin = leftNodeToJoin->GetPreviousSibling();
+ }
+ if (!leftNodeToJoin) {
+ return ret;
+ }
+
+ while (rightNodeToJoin && !IsEditable(rightNodeToJoin)) {
+ rightNodeToJoin = rightNodeToJoin->GetNextSibling();
+ }
+ if (!rightNodeToJoin) {
+ return ret;
+ }
+ }
+
+ return ret;
+}
+
+void
+EditorBase::BeginUpdateViewBatch()
+{
+ NS_PRECONDITION(mUpdateCount >= 0, "bad state");
+
+ if (!mUpdateCount) {
+ // Turn off selection updates and notifications.
+ RefPtr<Selection> selection = GetSelection();
+ if (selection) {
+ selection->StartBatchChanges();
+ }
+ }
+
+ mUpdateCount++;
+}
+
+nsresult
+EditorBase::EndUpdateViewBatch()
+{
+ NS_PRECONDITION(mUpdateCount > 0, "bad state");
+
+ if (mUpdateCount <= 0) {
+ mUpdateCount = 0;
+ return NS_ERROR_FAILURE;
+ }
+
+ mUpdateCount--;
+
+ if (!mUpdateCount) {
+ // Turn selection updating and notifications back on.
+ RefPtr<Selection> selection = GetSelection();
+ if (selection) {
+ selection->EndBatchChanges();
+ }
+ }
+
+ return NS_OK;
+}
+
+bool
+EditorBase::GetShouldTxnSetSelection()
+{
+ return mShouldTxnSetSelection;
+}
+
+NS_IMETHODIMP
+EditorBase::DeleteSelectionImpl(EDirection aAction,
+ EStripWrappers aStripWrappers)
+{
+ MOZ_ASSERT(aStripWrappers == eStrip || aStripWrappers == eNoStrip);
+
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_STATE(selection);
+ RefPtr<EditAggregateTransaction> transaction;
+ nsCOMPtr<nsINode> deleteNode;
+ int32_t deleteCharOffset = 0, deleteCharLength = 0;
+ nsresult rv = CreateTxnForDeleteSelection(aAction,
+ getter_AddRefs(transaction),
+ getter_AddRefs(deleteNode),
+ &deleteCharOffset,
+ &deleteCharLength);
+ nsCOMPtr<nsIDOMCharacterData> deleteCharData(do_QueryInterface(deleteNode));
+
+ if (NS_SUCCEEDED(rv)) {
+ AutoRules beginRulesSniffing(this, EditAction::deleteSelection, aAction);
+ // Notify nsIEditActionListener::WillDelete[Selection|Text|Node]
+ if (!deleteNode) {
+ for (auto& listener : mActionListeners) {
+ listener->WillDeleteSelection(selection);
+ }
+ } else if (deleteCharData) {
+ for (auto& listener : mActionListeners) {
+ listener->WillDeleteText(deleteCharData, deleteCharOffset, 1);
+ }
+ } else {
+ for (auto& listener : mActionListeners) {
+ listener->WillDeleteNode(deleteNode->AsDOMNode());
+ }
+ }
+
+ // Delete the specified amount
+ rv = DoTransaction(transaction);
+
+ // Notify nsIEditActionListener::DidDelete[Selection|Text|Node]
+ if (!deleteNode) {
+ for (auto& listener : mActionListeners) {
+ listener->DidDeleteSelection(selection);
+ }
+ } else if (deleteCharData) {
+ for (auto& listener : mActionListeners) {
+ listener->DidDeleteText(deleteCharData, deleteCharOffset, 1, rv);
+ }
+ } else {
+ for (auto& listener : mActionListeners) {
+ listener->DidDeleteNode(deleteNode->AsDOMNode(), rv);
+ }
+ }
+ }
+
+ return rv;
+}
+
+already_AddRefed<Element>
+EditorBase::DeleteSelectionAndCreateElement(nsIAtom& aTag)
+{
+ nsresult rv = DeleteSelectionAndPrepareToCreateNode();
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, nullptr);
+
+ nsCOMPtr<nsINode> node = selection->GetAnchorNode();
+ uint32_t offset = selection->AnchorOffset();
+
+ nsCOMPtr<Element> newElement = CreateNode(&aTag, node, offset);
+
+ // We want the selection to be just after the new node
+ rv = selection->Collapse(node, offset + 1);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ return newElement.forget();
+}
+
+TextComposition*
+EditorBase::GetComposition() const
+{
+ return mComposition;
+}
+
+bool
+EditorBase::IsIMEComposing() const
+{
+ return mComposition && mComposition->IsComposing();
+}
+
+bool
+EditorBase::ShouldHandleIMEComposition() const
+{
+ // When the editor is being reframed, the old value may be restored with
+ // InsertText(). In this time, the text should be inserted as not a part
+ // of the composition.
+ return mComposition && mDidPostCreate;
+}
+
+nsresult
+EditorBase::DeleteSelectionAndPrepareToCreateNode()
+{
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+ MOZ_ASSERT(selection->GetAnchorFocusRange());
+
+ if (!selection->GetAnchorFocusRange()->Collapsed()) {
+ nsresult rv = DeleteSelection(nsIEditor::eNone, nsIEditor::eStrip);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ MOZ_ASSERT(selection->GetAnchorFocusRange() &&
+ selection->GetAnchorFocusRange()->Collapsed(),
+ "Selection not collapsed after delete");
+ }
+
+ // If the selection is a chardata node, split it if necessary and compute
+ // where to put the new node
+ nsCOMPtr<nsINode> node = selection->GetAnchorNode();
+ MOZ_ASSERT(node, "Selection has no ranges in it");
+
+ if (node && node->IsNodeOfType(nsINode::eDATA_NODE)) {
+ NS_ASSERTION(node->GetParentNode(),
+ "It's impossible to insert into chardata with no parent -- "
+ "fix the caller");
+ NS_ENSURE_STATE(node->GetParentNode());
+
+ uint32_t offset = selection->AnchorOffset();
+
+ if (!offset) {
+ nsresult rv = selection->Collapse(node->GetParentNode(),
+ node->GetParentNode()->IndexOf(node));
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else if (offset == node->Length()) {
+ nsresult rv =
+ selection->Collapse(node->GetParentNode(),
+ node->GetParentNode()->IndexOf(node) + 1);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ nsCOMPtr<nsIDOMNode> tmp;
+ nsresult rv = SplitNode(node->AsDOMNode(), offset, getter_AddRefs(tmp));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = selection->Collapse(node->GetParentNode(),
+ node->GetParentNode()->IndexOf(node));
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ return NS_OK;
+}
+
+void
+EditorBase::DoAfterDoTransaction(nsITransaction* aTxn)
+{
+ bool isTransientTransaction;
+ MOZ_ALWAYS_SUCCEEDS(aTxn->GetIsTransient(&isTransientTransaction));
+
+ if (!isTransientTransaction) {
+ // we need to deal here with the case where the user saved after some
+ // edits, then undid one or more times. Then, the undo count is -ve,
+ // but we can't let a do take it back to zero. So we flip it up to
+ // a +ve number.
+ int32_t modCount;
+ GetModificationCount(&modCount);
+ if (modCount < 0) {
+ modCount = -modCount;
+ }
+
+ // don't count transient transactions
+ MOZ_ALWAYS_SUCCEEDS(IncrementModificationCount(1));
+ }
+}
+
+void
+EditorBase::DoAfterUndoTransaction()
+{
+ // all undoable transactions are non-transient
+ MOZ_ALWAYS_SUCCEEDS(IncrementModificationCount(-1));
+}
+
+void
+EditorBase::DoAfterRedoTransaction()
+{
+ // all redoable transactions are non-transient
+ MOZ_ALWAYS_SUCCEEDS(IncrementModificationCount(1));
+}
+
+already_AddRefed<ChangeAttributeTransaction>
+EditorBase::CreateTxnForSetAttribute(Element& aElement,
+ nsIAtom& aAttribute,
+ const nsAString& aValue)
+{
+ RefPtr<ChangeAttributeTransaction> transaction =
+ new ChangeAttributeTransaction(aElement, aAttribute, &aValue);
+
+ return transaction.forget();
+}
+
+already_AddRefed<ChangeAttributeTransaction>
+EditorBase::CreateTxnForRemoveAttribute(Element& aElement,
+ nsIAtom& aAttribute)
+{
+ RefPtr<ChangeAttributeTransaction> transaction =
+ new ChangeAttributeTransaction(aElement, aAttribute, nullptr);
+
+ return transaction.forget();
+}
+
+already_AddRefed<CreateElementTransaction>
+EditorBase::CreateTxnForCreateElement(nsIAtom& aTag,
+ nsINode& aParent,
+ int32_t aPosition)
+{
+ RefPtr<CreateElementTransaction> transaction =
+ new CreateElementTransaction(*this, aTag, aParent, aPosition);
+
+ return transaction.forget();
+}
+
+
+already_AddRefed<InsertNodeTransaction>
+EditorBase::CreateTxnForInsertNode(nsIContent& aNode,
+ nsINode& aParent,
+ int32_t aPosition)
+{
+ RefPtr<InsertNodeTransaction> transaction =
+ new InsertNodeTransaction(aNode, aParent, aPosition, *this);
+ return transaction.forget();
+}
+
+nsresult
+EditorBase::CreateTxnForDeleteNode(nsINode* aNode,
+ DeleteNodeTransaction** aTransaction)
+{
+ NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER);
+
+ RefPtr<DeleteNodeTransaction> transaction = new DeleteNodeTransaction();
+
+ nsresult rv = transaction->Init(this, aNode, &mRangeUpdater);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ transaction.forget(aTransaction);
+ return NS_OK;
+}
+
+already_AddRefed<CompositionTransaction>
+EditorBase::CreateTxnForComposition(const nsAString& aStringToInsert)
+{
+ MOZ_ASSERT(mIMETextNode);
+ // During handling IME composition, mComposition must have been initialized.
+ // TODO: We can simplify CompositionTransaction::Init() with TextComposition
+ // class.
+ RefPtr<CompositionTransaction> transaction =
+ new CompositionTransaction(*mIMETextNode, mIMETextOffset, mIMETextLength,
+ mComposition->GetRanges(), aStringToInsert,
+ *this, &mRangeUpdater);
+ return transaction.forget();
+}
+
+NS_IMETHODIMP
+EditorBase::CreateTxnForAddStyleSheet(StyleSheet* aSheet,
+ AddStyleSheetTransaction** aTransaction)
+{
+ RefPtr<AddStyleSheetTransaction> transaction = new AddStyleSheetTransaction();
+
+ nsresult rv = transaction->Init(this, aSheet);
+ if (NS_SUCCEEDED(rv)) {
+ transaction.forget(aTransaction);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+EditorBase::CreateTxnForRemoveStyleSheet(
+ StyleSheet* aSheet,
+ RemoveStyleSheetTransaction** aTransaction)
+{
+ RefPtr<RemoveStyleSheetTransaction> transaction =
+ new RemoveStyleSheetTransaction();
+
+ nsresult rv = transaction->Init(this, aSheet);
+ if (NS_SUCCEEDED(rv)) {
+ transaction.forget(aTransaction);
+ }
+
+ return rv;
+}
+
+nsresult
+EditorBase::CreateTxnForDeleteSelection(EDirection aAction,
+ EditAggregateTransaction** aTransaction,
+ nsINode** aNode,
+ int32_t* aOffset,
+ int32_t* aLength)
+{
+ MOZ_ASSERT(aTransaction);
+ *aTransaction = nullptr;
+
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_STATE(selection);
+
+ // Check whether the selection is collapsed and we should do nothing:
+ if (selection->Collapsed() && aAction == eNone) {
+ return NS_OK;
+ }
+
+ // allocate the out-param transaction
+ RefPtr<EditAggregateTransaction> aggregateTransaction =
+ new EditAggregateTransaction();
+
+ for (uint32_t rangeIdx = 0; rangeIdx < selection->RangeCount(); ++rangeIdx) {
+ RefPtr<nsRange> range = selection->GetRangeAt(rangeIdx);
+ NS_ENSURE_STATE(range);
+
+ // Same with range as with selection; if it is collapsed and action
+ // is eNone, do nothing.
+ if (!range->Collapsed()) {
+ RefPtr<DeleteRangeTransaction> transaction = new DeleteRangeTransaction();
+ transaction->Init(this, range, &mRangeUpdater);
+ aggregateTransaction->AppendChild(transaction);
+ } else if (aAction != eNone) {
+ // we have an insertion point. delete the thing in front of it or
+ // behind it, depending on aAction
+ nsresult rv = CreateTxnForDeleteInsertionPoint(range, aAction,
+ aggregateTransaction,
+ aNode, aOffset, aLength);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ aggregateTransaction.forget(aTransaction);
+
+ return NS_OK;
+}
+
+already_AddRefed<DeleteTextTransaction>
+EditorBase::CreateTxnForDeleteCharacter(nsGenericDOMDataNode& aData,
+ uint32_t aOffset,
+ EDirection aDirection)
+{
+ NS_ASSERTION(aDirection == eNext || aDirection == ePrevious,
+ "Invalid direction");
+ nsAutoString data;
+ aData.GetData(data);
+ NS_ASSERTION(data.Length(), "Trying to delete from a zero-length node");
+ NS_ENSURE_TRUE(data.Length(), nullptr);
+
+ uint32_t segOffset = aOffset, segLength = 1;
+ if (aDirection == eNext) {
+ if (segOffset + 1 < data.Length() &&
+ NS_IS_HIGH_SURROGATE(data[segOffset]) &&
+ NS_IS_LOW_SURROGATE(data[segOffset+1])) {
+ // Delete both halves of the surrogate pair
+ ++segLength;
+ }
+ } else if (aOffset > 0) {
+ --segOffset;
+ if (segOffset > 0 &&
+ NS_IS_LOW_SURROGATE(data[segOffset]) &&
+ NS_IS_HIGH_SURROGATE(data[segOffset-1])) {
+ ++segLength;
+ --segOffset;
+ }
+ } else {
+ return nullptr;
+ }
+ return CreateTxnForDeleteText(aData, segOffset, segLength);
+}
+
+//XXX: currently, this doesn't handle edge conditions because GetNext/GetPrior
+//are not implemented
+nsresult
+EditorBase::CreateTxnForDeleteInsertionPoint(
+ nsRange* aRange,
+ EDirection aAction,
+ EditAggregateTransaction* aTransaction,
+ nsINode** aNode,
+ int32_t* aOffset,
+ int32_t* aLength)
+{
+ MOZ_ASSERT(aAction != eNone);
+
+ // get the node and offset of the insertion point
+ nsCOMPtr<nsINode> node = aRange->GetStartParent();
+ NS_ENSURE_STATE(node);
+
+ int32_t offset = aRange->StartOffset();
+
+ // determine if the insertion point is at the beginning, middle, or end of
+ // the node
+
+ uint32_t count = node->Length();
+
+ bool isFirst = !offset;
+ bool isLast = (count == (uint32_t)offset);
+
+ // XXX: if isFirst && isLast, then we'll need to delete the node
+ // as well as the 1 child
+
+ // build a transaction for deleting the appropriate data
+ // XXX: this has to come from rule section
+ if (aAction == ePrevious && isFirst) {
+ // we're backspacing from the beginning of the node. Delete the first
+ // thing to our left
+ nsCOMPtr<nsIContent> priorNode = GetPriorNode(node, true);
+ NS_ENSURE_STATE(priorNode);
+
+ // there is a priorNode, so delete its last child (if chardata, delete the
+ // last char). if it has no children, delete it
+ if (priorNode->IsNodeOfType(nsINode::eDATA_NODE)) {
+ RefPtr<nsGenericDOMDataNode> priorNodeAsCharData =
+ static_cast<nsGenericDOMDataNode*>(priorNode.get());
+ uint32_t length = priorNode->Length();
+ // Bail out for empty chardata XXX: Do we want to do something else?
+ NS_ENSURE_STATE(length);
+ RefPtr<DeleteTextTransaction> transaction =
+ CreateTxnForDeleteCharacter(*priorNodeAsCharData, length, ePrevious);
+ NS_ENSURE_STATE(transaction);
+
+ *aOffset = transaction->GetOffset();
+ *aLength = transaction->GetNumCharsToDelete();
+ aTransaction->AppendChild(transaction);
+ } else {
+ // priorNode is not chardata, so tell its parent to delete it
+ RefPtr<DeleteNodeTransaction> transaction;
+ nsresult rv =
+ CreateTxnForDeleteNode(priorNode, getter_AddRefs(transaction));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aTransaction->AppendChild(transaction);
+ }
+
+ NS_ADDREF(*aNode = priorNode);
+
+ return NS_OK;
+ }
+
+ if (aAction == eNext && isLast) {
+ // we're deleting from the end of the node. Delete the first thing to our
+ // right
+ nsCOMPtr<nsIContent> nextNode = GetNextNode(node, true);
+ NS_ENSURE_STATE(nextNode);
+
+ // there is a nextNode, so delete its first child (if chardata, delete the
+ // first char). if it has no children, delete it
+ if (nextNode->IsNodeOfType(nsINode::eDATA_NODE)) {
+ RefPtr<nsGenericDOMDataNode> nextNodeAsCharData =
+ static_cast<nsGenericDOMDataNode*>(nextNode.get());
+ uint32_t length = nextNode->Length();
+ // Bail out for empty chardata XXX: Do we want to do something else?
+ NS_ENSURE_STATE(length);
+ RefPtr<DeleteTextTransaction> transaction =
+ CreateTxnForDeleteCharacter(*nextNodeAsCharData, 0, eNext);
+ NS_ENSURE_STATE(transaction);
+
+ *aOffset = transaction->GetOffset();
+ *aLength = transaction->GetNumCharsToDelete();
+ aTransaction->AppendChild(transaction);
+ } else {
+ // nextNode is not chardata, so tell its parent to delete it
+ RefPtr<DeleteNodeTransaction> transaction;
+ nsresult rv =
+ CreateTxnForDeleteNode(nextNode, getter_AddRefs(transaction));
+ NS_ENSURE_SUCCESS(rv, rv);
+ aTransaction->AppendChild(transaction);
+ }
+
+ NS_ADDREF(*aNode = nextNode);
+
+ return NS_OK;
+ }
+
+ if (node->IsNodeOfType(nsINode::eDATA_NODE)) {
+ RefPtr<nsGenericDOMDataNode> nodeAsCharData =
+ static_cast<nsGenericDOMDataNode*>(node.get());
+ // we have chardata, so delete a char at the proper offset
+ RefPtr<DeleteTextTransaction> transaction =
+ CreateTxnForDeleteCharacter(*nodeAsCharData, offset, aAction);
+ NS_ENSURE_STATE(transaction);
+
+ aTransaction->AppendChild(transaction);
+ NS_ADDREF(*aNode = node);
+ *aOffset = transaction->GetOffset();
+ *aLength = transaction->GetNumCharsToDelete();
+ } else {
+ // we're either deleting a node or chardata, need to dig into the next/prev
+ // node to find out
+ nsCOMPtr<nsINode> selectedNode;
+ if (aAction == ePrevious) {
+ selectedNode = GetPriorNode(node, offset, true);
+ } else if (aAction == eNext) {
+ selectedNode = GetNextNode(node, offset, true);
+ }
+
+ while (selectedNode &&
+ selectedNode->IsNodeOfType(nsINode::eDATA_NODE) &&
+ !selectedNode->Length()) {
+ // Can't delete an empty chardata node (bug 762183)
+ if (aAction == ePrevious) {
+ selectedNode = GetPriorNode(selectedNode, true);
+ } else if (aAction == eNext) {
+ selectedNode = GetNextNode(selectedNode, true);
+ }
+ }
+ NS_ENSURE_STATE(selectedNode);
+
+ if (selectedNode->IsNodeOfType(nsINode::eDATA_NODE)) {
+ RefPtr<nsGenericDOMDataNode> selectedNodeAsCharData =
+ static_cast<nsGenericDOMDataNode*>(selectedNode.get());
+ // we are deleting from a chardata node, so do a character deletion
+ uint32_t position = 0;
+ if (aAction == ePrevious) {
+ position = selectedNode->Length();
+ }
+ RefPtr<DeleteTextTransaction> deleteTextTransaction =
+ CreateTxnForDeleteCharacter(*selectedNodeAsCharData, position,
+ aAction);
+ NS_ENSURE_TRUE(deleteTextTransaction, NS_ERROR_NULL_POINTER);
+
+ aTransaction->AppendChild(deleteTextTransaction);
+ *aOffset = deleteTextTransaction->GetOffset();
+ *aLength = deleteTextTransaction->GetNumCharsToDelete();
+ } else {
+ RefPtr<DeleteNodeTransaction> deleteNodeTransaction;
+ nsresult rv =
+ CreateTxnForDeleteNode(selectedNode,
+ getter_AddRefs(deleteNodeTransaction));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(deleteNodeTransaction, NS_ERROR_NULL_POINTER);
+
+ aTransaction->AppendChild(deleteNodeTransaction);
+ }
+
+ NS_ADDREF(*aNode = selectedNode);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+EditorBase::CreateRange(nsIDOMNode* aStartParent,
+ int32_t aStartOffset,
+ nsIDOMNode* aEndParent,
+ int32_t aEndOffset,
+ nsRange** aRange)
+{
+ return nsRange::CreateRange(aStartParent, aStartOffset, aEndParent,
+ aEndOffset, aRange);
+}
+
+nsresult
+EditorBase::AppendNodeToSelectionAsRange(nsIDOMNode* aNode)
+{
+ NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER);
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIDOMNode> parentNode;
+ nsresult rv = aNode->GetParentNode(getter_AddRefs(parentNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(parentNode, NS_ERROR_NULL_POINTER);
+
+ int32_t offset = GetChildOffset(aNode, parentNode);
+
+ RefPtr<nsRange> range;
+ rv = CreateRange(parentNode, offset, parentNode, offset + 1,
+ getter_AddRefs(range));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(range, NS_ERROR_NULL_POINTER);
+
+ return selection->AddRange(range);
+}
+
+nsresult
+EditorBase::ClearSelection()
+{
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE);
+ return selection->RemoveAllRanges();
+}
+
+already_AddRefed<Element>
+EditorBase::CreateHTMLContent(nsIAtom* aTag)
+{
+ MOZ_ASSERT(aTag);
+
+ nsCOMPtr<nsIDocument> doc = GetDocument();
+ if (!doc) {
+ return nullptr;
+ }
+
+ // XXX Wallpaper over editor bug (editor tries to create elements with an
+ // empty nodename).
+ if (aTag == nsGkAtoms::_empty) {
+ NS_ERROR("Don't pass an empty tag to EditorBase::CreateHTMLContent, "
+ "check caller.");
+ return nullptr;
+ }
+
+ return doc->CreateElem(nsDependentAtomString(aTag), nullptr,
+ kNameSpaceID_XHTML);
+}
+
+nsresult
+EditorBase::SetAttributeOrEquivalent(nsIDOMElement* aElement,
+ const nsAString& aAttribute,
+ const nsAString& aValue,
+ bool aSuppressTransaction)
+{
+ return SetAttribute(aElement, aAttribute, aValue);
+}
+
+nsresult
+EditorBase::RemoveAttributeOrEquivalent(nsIDOMElement* aElement,
+ const nsAString& aAttribute,
+ bool aSuppressTransaction)
+{
+ return RemoveAttribute(aElement, aAttribute);
+}
+
+nsresult
+EditorBase::HandleKeyPressEvent(nsIDOMKeyEvent* aKeyEvent)
+{
+ // NOTE: When you change this method, you should also change:
+ // * editor/libeditor/tests/test_texteditor_keyevent_handling.html
+ // * editor/libeditor/tests/test_htmleditor_keyevent_handling.html
+ //
+ // And also when you add new key handling, you need to change the subclass's
+ // HandleKeyPressEvent()'s switch statement.
+
+ WidgetKeyboardEvent* nativeKeyEvent =
+ aKeyEvent->AsEvent()->WidgetEventPtr()->AsKeyboardEvent();
+ NS_ENSURE_TRUE(nativeKeyEvent, NS_ERROR_UNEXPECTED);
+ NS_ASSERTION(nativeKeyEvent->mMessage == eKeyPress,
+ "HandleKeyPressEvent gets non-keypress event");
+
+ // if we are readonly or disabled, then do nothing.
+ if (IsReadonly() || IsDisabled()) {
+ // consume backspace for disabled and readonly textfields, to prevent
+ // back in history, which could be confusing to users
+ if (nativeKeyEvent->mKeyCode == NS_VK_BACK) {
+ aKeyEvent->AsEvent()->PreventDefault();
+ }
+ return NS_OK;
+ }
+
+ switch (nativeKeyEvent->mKeyCode) {
+ case NS_VK_META:
+ case NS_VK_WIN:
+ case NS_VK_SHIFT:
+ case NS_VK_CONTROL:
+ case NS_VK_ALT:
+ aKeyEvent->AsEvent()->PreventDefault(); // consumed
+ return NS_OK;
+ case NS_VK_BACK:
+ if (nativeKeyEvent->IsControl() || nativeKeyEvent->IsAlt() ||
+ nativeKeyEvent->IsMeta() || nativeKeyEvent->IsOS()) {
+ return NS_OK;
+ }
+ DeleteSelection(nsIEditor::ePrevious, nsIEditor::eStrip);
+ aKeyEvent->AsEvent()->PreventDefault(); // consumed
+ return NS_OK;
+ case NS_VK_DELETE:
+ // on certain platforms (such as windows) the shift key
+ // modifies what delete does (cmd_cut in this case).
+ // bailing here to allow the keybindings to do the cut.
+ if (nativeKeyEvent->IsShift() || nativeKeyEvent->IsControl() ||
+ nativeKeyEvent->IsAlt() || nativeKeyEvent->IsMeta() ||
+ nativeKeyEvent->IsOS()) {
+ return NS_OK;
+ }
+ DeleteSelection(nsIEditor::eNext, nsIEditor::eStrip);
+ aKeyEvent->AsEvent()->PreventDefault(); // consumed
+ return NS_OK;
+ }
+ return NS_OK;
+}
+
+nsresult
+EditorBase::HandleInlineSpellCheck(EditAction action,
+ Selection* aSelection,
+ nsIDOMNode* previousSelectedNode,
+ int32_t previousSelectedOffset,
+ nsIDOMNode* aStartNode,
+ int32_t aStartOffset,
+ nsIDOMNode* aEndNode,
+ int32_t aEndOffset)
+{
+ // Have to cast action here because this method is from an IDL
+ return mInlineSpellChecker ? mInlineSpellChecker->SpellCheckAfterEditorChange(
+ (int32_t)action, aSelection,
+ previousSelectedNode, previousSelectedOffset,
+ aStartNode, aStartOffset, aEndNode,
+ aEndOffset)
+ : NS_OK;
+}
+
+already_AddRefed<nsIContent>
+EditorBase::FindSelectionRoot(nsINode* aNode)
+{
+ nsCOMPtr<nsIContent> rootContent = GetRoot();
+ return rootContent.forget();
+}
+
+nsresult
+EditorBase::InitializeSelection(nsIDOMEventTarget* aFocusEventTarget)
+{
+ nsCOMPtr<nsINode> targetNode = do_QueryInterface(aFocusEventTarget);
+ NS_ENSURE_TRUE(targetNode, NS_ERROR_INVALID_ARG);
+ nsCOMPtr<nsIContent> selectionRootContent = FindSelectionRoot(targetNode);
+ if (!selectionRootContent) {
+ return NS_OK;
+ }
+
+ bool isTargetDoc =
+ targetNode->NodeType() == nsIDOMNode::DOCUMENT_NODE &&
+ targetNode->HasFlag(NODE_IS_EDITABLE);
+
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_STATE(selection);
+
+ nsCOMPtr<nsIPresShell> presShell = GetPresShell();
+ NS_ENSURE_TRUE(presShell, NS_ERROR_NOT_INITIALIZED);
+
+ nsCOMPtr<nsISelectionController> selCon;
+ nsresult rv = GetSelectionController(getter_AddRefs(selCon));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Init the caret
+ RefPtr<nsCaret> caret = presShell->GetCaret();
+ NS_ENSURE_TRUE(caret, NS_ERROR_UNEXPECTED);
+ caret->SetIgnoreUserModify(false);
+ caret->SetSelection(selection);
+ selCon->SetCaretReadOnly(IsReadonly());
+ selCon->SetCaretEnabled(true);
+
+ // Init selection
+ selCon->SetDisplaySelection(nsISelectionController::SELECTION_ON);
+ selCon->SetSelectionFlags(nsISelectionDisplay::DISPLAY_ALL);
+ selCon->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);
+ } else {
+ selection->SetAncestorLimiter(nullptr);
+ }
+
+ // XXX What case needs this?
+ if (isTargetDoc) {
+ int32_t rangeCount;
+ selection->GetRangeCount(&rangeCount);
+ if (!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.
+ if (mComposition && !mIMETextNode && mIMETextLength) {
+ // We need to look for the new mIMETextNode from current selection.
+ // XXX If selection is changed during reframe, this doesn't work well!
+ nsRange* firstRange = selection->GetRangeAt(0);
+ NS_ENSURE_TRUE(firstRange, NS_ERROR_FAILURE);
+ nsCOMPtr<nsINode> startNode = firstRange->GetStartParent();
+ int32_t startOffset = firstRange->StartOffset();
+ FindBetterInsertionPoint(startNode, startOffset);
+ Text* textNode = startNode->GetAsText();
+ MOZ_ASSERT(textNode,
+ "There must be text node if mIMETextLength is larger than 0");
+ if (textNode) {
+ MOZ_ASSERT(textNode->Length() >= mIMETextOffset + mIMETextLength,
+ "The text node must be different from the old mIMETextNode");
+ CompositionTransaction::SetIMESelection(*this, textNode, mIMETextOffset,
+ mIMETextLength,
+ mComposition->GetRanges());
+ }
+ }
+
+ return NS_OK;
+}
+
+class RepaintSelectionRunner final : public Runnable {
+public:
+ explicit RepaintSelectionRunner(nsISelectionController* aSelectionController)
+ : mSelectionController(aSelectionController)
+ {
+ }
+
+ NS_IMETHOD Run() override
+ {
+ mSelectionController->RepaintSelection(
+ nsISelectionController::SELECTION_NORMAL);
+ return NS_OK;
+ }
+
+private:
+ nsCOMPtr<nsISelectionController> mSelectionController;
+};
+
+NS_IMETHODIMP
+EditorBase::FinalizeSelection()
+{
+ nsCOMPtr<nsISelectionController> selCon;
+ nsresult rv = GetSelectionController(getter_AddRefs(selCon));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_STATE(selection);
+
+ selection->SetAncestorLimiter(nullptr);
+
+ nsCOMPtr<nsIPresShell> presShell = GetPresShell();
+ NS_ENSURE_TRUE(presShell, NS_ERROR_NOT_INITIALIZED);
+
+ selCon->SetCaretEnabled(false);
+
+ nsFocusManager* fm = nsFocusManager::GetFocusManager();
+ NS_ENSURE_TRUE(fm, NS_ERROR_NOT_INITIALIZED);
+ fm->UpdateCaretForCaretBrowsingMode();
+
+ if (!HasIndependentSelection()) {
+ // If this editor doesn't have an independent selection, i.e., it must
+ // mean that it is an HTML editor, the selection controller is shared with
+ // presShell. So, even this editor loses focus, other part of the document
+ // may still have focus.
+ nsCOMPtr<nsIDocument> doc = GetDocument();
+ ErrorResult ret;
+ if (!doc || !doc->HasFocus(ret)) {
+ // If the document already lost focus, mark the selection as disabled.
+ selCon->SetDisplaySelection(nsISelectionController::SELECTION_DISABLED);
+ } else {
+ // Otherwise, mark selection as normal because outside of a
+ // contenteditable element should be selected with normal selection
+ // color after here.
+ selCon->SetDisplaySelection(nsISelectionController::SELECTION_ON);
+ }
+ } else if (IsFormWidget() || IsPasswordEditor() ||
+ IsReadonly() || IsDisabled() || IsInputFiltered()) {
+ // In <input> or <textarea>, the independent selection should be hidden
+ // while this editor doesn't have focus.
+ selCon->SetDisplaySelection(nsISelectionController::SELECTION_HIDDEN);
+ } else {
+ // Otherwise, although we're not sure how this case happens, the
+ // independent selection should be marked as disabled.
+ selCon->SetDisplaySelection(nsISelectionController::SELECTION_DISABLED);
+ }
+
+
+ // FinalizeSelection might be called from ContentRemoved even if selection
+ // isn't updated. So we need to call RepaintSelection after updated it.
+ nsContentUtils::AddScriptRunner(
+ new RepaintSelectionRunner(selCon));
+ return NS_OK;
+}
+
+Element*
+EditorBase::GetRoot()
+{
+ if (!mRootElement) {
+ // Let GetRootElement() do the work
+ nsCOMPtr<nsIDOMElement> root;
+ GetRootElement(getter_AddRefs(root));
+ }
+
+ return mRootElement;
+}
+
+Element*
+EditorBase::GetEditorRoot()
+{
+ return GetRoot();
+}
+
+Element*
+EditorBase::GetExposedRoot()
+{
+ Element* rootElement = GetRoot();
+
+ // For plaintext editors, we need to ask the input/textarea element directly.
+ if (rootElement && rootElement->IsRootOfNativeAnonymousSubtree()) {
+ rootElement = rootElement->GetParent()->AsElement();
+ }
+
+ return rootElement;
+}
+
+nsresult
+EditorBase::DetermineCurrentDirection()
+{
+ // Get the current root direction from its frame
+ nsIContent* rootElement = GetExposedRoot();
+ NS_ENSURE_TRUE(rootElement, NS_ERROR_FAILURE);
+
+ // If we don't have an explicit direction, determine our direction
+ // from the content's direction
+ if (!(mFlags & (nsIPlaintextEditor::eEditorLeftToRight |
+ nsIPlaintextEditor::eEditorRightToLeft))) {
+ nsIFrame* frame = rootElement->GetPrimaryFrame();
+ NS_ENSURE_TRUE(frame, NS_ERROR_FAILURE);
+
+ // Set the flag here, to enable us to use the same code path below.
+ // It will be flipped before returning from the function.
+ if (frame->StyleVisibility()->mDirection == NS_STYLE_DIRECTION_RTL) {
+ mFlags |= nsIPlaintextEditor::eEditorRightToLeft;
+ } else {
+ mFlags |= nsIPlaintextEditor::eEditorLeftToRight;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::SwitchTextDirection()
+{
+ // Get the current root direction from its frame
+ nsIContent* rootElement = GetExposedRoot();
+
+ nsresult rv = DetermineCurrentDirection();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Apply the opposite direction
+ if (mFlags & nsIPlaintextEditor::eEditorRightToLeft) {
+ NS_ASSERTION(!(mFlags & nsIPlaintextEditor::eEditorLeftToRight),
+ "Unexpected mutually exclusive flag");
+ mFlags &= ~nsIPlaintextEditor::eEditorRightToLeft;
+ mFlags |= nsIPlaintextEditor::eEditorLeftToRight;
+ rv = rootElement->SetAttr(kNameSpaceID_None, nsGkAtoms::dir, NS_LITERAL_STRING("ltr"), true);
+ } else if (mFlags & nsIPlaintextEditor::eEditorLeftToRight) {
+ NS_ASSERTION(!(mFlags & nsIPlaintextEditor::eEditorRightToLeft),
+ "Unexpected mutually exclusive flag");
+ mFlags |= nsIPlaintextEditor::eEditorRightToLeft;
+ mFlags &= ~nsIPlaintextEditor::eEditorLeftToRight;
+ rv = rootElement->SetAttr(kNameSpaceID_None, nsGkAtoms::dir, NS_LITERAL_STRING("rtl"), true);
+ }
+
+ if (NS_SUCCEEDED(rv)) {
+ FireInputEvent();
+ }
+
+ return rv;
+}
+
+void
+EditorBase::SwitchTextDirectionTo(uint32_t aDirection)
+{
+ // Get the current root direction from its frame
+ nsIContent* rootElement = GetExposedRoot();
+
+ nsresult rv = DetermineCurrentDirection();
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ // Apply the requested direction
+ if (aDirection == nsIPlaintextEditor::eEditorLeftToRight &&
+ (mFlags & nsIPlaintextEditor::eEditorRightToLeft)) {
+ NS_ASSERTION(!(mFlags & nsIPlaintextEditor::eEditorLeftToRight),
+ "Unexpected mutually exclusive flag");
+ mFlags &= ~nsIPlaintextEditor::eEditorRightToLeft;
+ mFlags |= nsIPlaintextEditor::eEditorLeftToRight;
+ rv = rootElement->SetAttr(kNameSpaceID_None, nsGkAtoms::dir, NS_LITERAL_STRING("ltr"), true);
+ } else if (aDirection == nsIPlaintextEditor::eEditorRightToLeft &&
+ (mFlags & nsIPlaintextEditor::eEditorLeftToRight)) {
+ NS_ASSERTION(!(mFlags & nsIPlaintextEditor::eEditorRightToLeft),
+ "Unexpected mutually exclusive flag");
+ mFlags |= nsIPlaintextEditor::eEditorRightToLeft;
+ mFlags &= ~nsIPlaintextEditor::eEditorLeftToRight;
+ rv = rootElement->SetAttr(kNameSpaceID_None, nsGkAtoms::dir, NS_LITERAL_STRING("rtl"), true);
+ }
+
+ if (NS_SUCCEEDED(rv)) {
+ FireInputEvent();
+ }
+}
+
+#if DEBUG_JOE
+void
+EditorBase::DumpNode(nsIDOMNode* aNode,
+ int32_t indent)
+{
+ for (int32_t i = 0; i < indent; i++) {
+ printf(" ");
+ }
+
+ nsCOMPtr<nsIDOMElement> element = do_QueryInterface(aNode);
+ nsCOMPtr<nsIDOMDocumentFragment> docfrag = do_QueryInterface(aNode);
+
+ if (element || docfrag) {
+ if (element) {
+ nsAutoString tag;
+ element->GetTagName(tag);
+ printf("<%s>\n", NS_LossyConvertUTF16toASCII(tag).get());
+ } else {
+ printf("<document fragment>\n");
+ }
+ nsCOMPtr<nsIDOMNodeList> childList;
+ aNode->GetChildNodes(getter_AddRefs(childList));
+ NS_ENSURE_TRUE(childList, NS_ERROR_NULL_POINTER);
+ uint32_t numChildren;
+ childList->GetLength(&numChildren);
+ nsCOMPtr<nsIDOMNode> child, tmp;
+ aNode->GetFirstChild(getter_AddRefs(child));
+ for (uint32_t i = 0; i < numChildren; i++) {
+ DumpNode(child, indent + 1);
+ child->GetNextSibling(getter_AddRefs(tmp));
+ child = tmp;
+ }
+ } else if (IsTextNode(aNode)) {
+ nsCOMPtr<nsIDOMCharacterData> textNode = do_QueryInterface(aNode);
+ nsAutoString str;
+ textNode->GetData(str);
+ nsAutoCString cstr;
+ LossyCopyUTF16toASCII(str, cstr);
+ cstr.ReplaceChar('\n', ' ');
+ printf("<textnode> %s\n", cstr.get());
+ }
+}
+#endif
+
+bool
+EditorBase::IsModifiableNode(nsIDOMNode* aNode)
+{
+ return true;
+}
+
+bool
+EditorBase::IsModifiableNode(nsINode* aNode)
+{
+ return true;
+}
+
+already_AddRefed<nsIContent>
+EditorBase::GetFocusedContent()
+{
+ nsCOMPtr<nsIDOMEventTarget> piTarget = GetDOMEventTarget();
+ if (!piTarget) {
+ return nullptr;
+ }
+
+ nsFocusManager* fm = nsFocusManager::GetFocusManager();
+ NS_ENSURE_TRUE(fm, nullptr);
+
+ nsCOMPtr<nsIContent> content = fm->GetFocusedContent();
+ return SameCOMIdentity(content, piTarget) ? content.forget() : nullptr;
+}
+
+already_AddRefed<nsIContent>
+EditorBase::GetFocusedContentForIME()
+{
+ return GetFocusedContent();
+}
+
+bool
+EditorBase::IsActiveInDOMWindow()
+{
+ nsCOMPtr<nsIDOMEventTarget> piTarget = GetDOMEventTarget();
+ if (!piTarget) {
+ return false;
+ }
+
+ nsFocusManager* fm = nsFocusManager::GetFocusManager();
+ NS_ENSURE_TRUE(fm, false);
+
+ nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak);
+ nsPIDOMWindowOuter* ourWindow = doc->GetWindow();
+ nsCOMPtr<nsPIDOMWindowOuter> win;
+ nsIContent* content =
+ nsFocusManager::GetFocusedDescendant(ourWindow, false,
+ getter_AddRefs(win));
+ return SameCOMIdentity(content, piTarget);
+}
+
+bool
+EditorBase::IsAcceptableInputEvent(nsIDOMEvent* aEvent)
+{
+ // If the event is trusted, the event should always cause input.
+ NS_ENSURE_TRUE(aEvent, false);
+
+ WidgetEvent* widgetEvent = aEvent->WidgetEventPtr();
+ if (NS_WARN_IF(!widgetEvent)) {
+ return false;
+ }
+
+ // If this is dispatched by using cordinates but this editor doesn't have
+ // focus, we shouldn't handle it.
+ if (widgetEvent->IsUsingCoordinates()) {
+ nsCOMPtr<nsIContent> focusedContent = GetFocusedContent();
+ if (!focusedContent) {
+ return false;
+ }
+ }
+
+ // If a composition event isn't dispatched via widget, we need to ignore them
+ // since they cannot be managed by TextComposition. E.g., the event was
+ // created by chrome JS.
+ // Note that if we allow to handle such events, editor may be confused by
+ // strange event order.
+ bool needsWidget = false;
+ WidgetGUIEvent* widgetGUIEvent = nullptr;
+ switch (widgetEvent->mMessage) {
+ case eUnidentifiedEvent:
+ // If events are not created with proper event interface, their message
+ // are initialized with eUnidentifiedEvent. Let's ignore such event.
+ return false;
+ case eCompositionStart:
+ case eCompositionEnd:
+ case eCompositionUpdate:
+ case eCompositionChange:
+ case eCompositionCommitAsIs:
+ // Don't allow composition events whose internal event are not
+ // WidgetCompositionEvent.
+ widgetGUIEvent = aEvent->WidgetEventPtr()->AsCompositionEvent();
+ needsWidget = true;
+ break;
+ default:
+ break;
+ }
+ if (needsWidget &&
+ (!widgetGUIEvent || !widgetGUIEvent->mWidget)) {
+ return false;
+ }
+
+ // Accept all trusted events.
+ if (widgetEvent->IsTrusted()) {
+ return true;
+ }
+
+ // Ignore untrusted mouse event.
+ // XXX Why are we handling other untrusted input events?
+ if (widgetEvent->AsMouseEventBase()) {
+ return false;
+ }
+
+ // Otherwise, we shouldn't handle any input events when we're not an active
+ // element of the DOM window.
+ return IsActiveInDOMWindow();
+}
+
+void
+EditorBase::OnFocus(nsIDOMEventTarget* aFocusEventTarget)
+{
+ InitializeSelection(aFocusEventTarget);
+ if (mInlineSpellChecker) {
+ mInlineSpellChecker->UpdateCurrentDictionary();
+ }
+}
+
+NS_IMETHODIMP
+EditorBase::GetSuppressDispatchingInputEvent(bool* aSuppressed)
+{
+ NS_ENSURE_ARG_POINTER(aSuppressed);
+ *aSuppressed = !mDispatchInputEvent;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::SetSuppressDispatchingInputEvent(bool aSuppress)
+{
+ mDispatchInputEvent = !aSuppress;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorBase::GetIsInEditAction(bool* aIsInEditAction)
+{
+ MOZ_ASSERT(aIsInEditAction, "aIsInEditAction must not be null");
+ *aIsInEditAction = mIsInEditAction;
+ return NS_OK;
+}
+
+int32_t
+EditorBase::GetIMESelectionStartOffsetIn(nsINode* aTextNode)
+{
+ MOZ_ASSERT(aTextNode, "aTextNode must not be nullptr");
+
+ nsCOMPtr<nsISelectionController> selectionController;
+ nsresult rv = GetSelectionController(getter_AddRefs(selectionController));
+ NS_ENSURE_SUCCESS(rv, -1);
+ NS_ENSURE_TRUE(selectionController, -1);
+
+ int32_t minOffset = INT32_MAX;
+ static const SelectionType kIMESelectionTypes[] = {
+ SelectionType::eIMERawClause,
+ SelectionType::eIMESelectedRawClause,
+ SelectionType::eIMEConvertedClause,
+ SelectionType::eIMESelectedClause
+ };
+ for (auto selectionType : kIMESelectionTypes) {
+ RefPtr<Selection> selection = GetSelection(selectionType);
+ if (!selection) {
+ continue;
+ }
+ for (uint32_t i = 0; i < selection->RangeCount(); i++) {
+ RefPtr<nsRange> range = selection->GetRangeAt(i);
+ if (NS_WARN_IF(!range)) {
+ continue;
+ }
+ if (NS_WARN_IF(range->GetStartParent() != aTextNode)) {
+ // ignore the start offset...
+ } else {
+ MOZ_ASSERT(range->StartOffset() >= 0,
+ "start offset shouldn't be negative");
+ minOffset = std::min(minOffset, range->StartOffset());
+ }
+ if (NS_WARN_IF(range->GetEndParent() != aTextNode)) {
+ // ignore the end offset...
+ } else {
+ MOZ_ASSERT(range->EndOffset() >= 0,
+ "start offset shouldn't be negative");
+ minOffset = std::min(minOffset, range->EndOffset());
+ }
+ }
+ }
+ return minOffset < INT32_MAX ? minOffset : -1;
+}
+
+void
+EditorBase::HideCaret(bool aHide)
+{
+ if (mHidingCaret == aHide) {
+ return;
+ }
+
+ nsCOMPtr<nsIPresShell> presShell = GetPresShell();
+ NS_ENSURE_TRUE_VOID(presShell);
+ RefPtr<nsCaret> caret = presShell->GetCaret();
+ NS_ENSURE_TRUE_VOID(caret);
+
+ mHidingCaret = aHide;
+ if (aHide) {
+ caret->AddForceHide();
+ } else {
+ caret->RemoveForceHide();
+ }
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/EditorBase.h b/editor/libeditor/EditorBase.h
new file mode 100644
index 000000000..dd4b9695e
--- /dev/null
+++ b/editor/libeditor/EditorBase.h
@@ -0,0 +1,1046 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_EditorBase_h
+#define mozilla_EditorBase_h
+
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc.
+#include "mozFlushType.h" // for mozFlushType enum
+#include "mozilla/OwningNonNull.h" // for OwningNonNull
+#include "mozilla/SelectionState.h" // for RangeUpdater, etc.
+#include "mozilla/StyleSheet.h" // for StyleSheet
+#include "mozilla/dom/Text.h"
+#include "nsCOMPtr.h" // for already_AddRefed, nsCOMPtr
+#include "nsCycleCollectionParticipant.h"
+#include "nsGkAtoms.h"
+#include "nsIEditor.h" // for nsIEditor::EDirection, etc.
+#include "nsIEditorIMESupport.h" // for NS_DECL_NSIEDITORIMESUPPORT, etc.
+#include "nsIObserver.h" // for NS_DECL_NSIOBSERVER, etc.
+#include "nsIPhonetic.h" // for NS_DECL_NSIPHONETIC, etc.
+#include "nsIPlaintextEditor.h" // for nsIPlaintextEditor, etc.
+#include "nsISelectionController.h" // for nsISelectionController constants
+#include "nsISupportsImpl.h" // for EditorBase::Release, etc.
+#include "nsIWeakReferenceUtils.h" // for nsWeakPtr
+#include "nsLiteralString.h" // for NS_LITERAL_STRING
+#include "nsString.h" // for nsCString
+#include "nsWeakReference.h" // for nsSupportsWeakReference
+#include "nscore.h" // for nsresult, nsAString, etc.
+
+class nsIAtom;
+class nsIContent;
+class nsIDOMDocument;
+class nsIDOMEvent;
+class nsIDOMEventListener;
+class nsIDOMEventTarget;
+class nsIDOMKeyEvent;
+class nsIDOMNode;
+class nsIDocument;
+class nsIDocumentStateListener;
+class nsIEditActionListener;
+class nsIEditorObserver;
+class nsIInlineSpellChecker;
+class nsINode;
+class nsIPresShell;
+class nsISupports;
+class nsITransaction;
+class nsIWidget;
+class nsRange;
+class nsString;
+class nsTransactionManager;
+
+// This is int32_t instead of int16_t because nsIInlineSpellChecker.idl's
+// spellCheckAfterEditorChange is defined to take it as a long.
+// XXX EditAction causes unnecessary include of EditorBase from some places.
+// Why don't you move this to nsIEditor.idl?
+enum class EditAction : int32_t
+{
+ ignore = -1,
+ none = 0,
+ undo,
+ redo,
+ insertNode,
+ createNode,
+ deleteNode,
+ splitNode,
+ joinNode,
+ deleteText = 1003,
+
+ // text commands
+ insertText = 2000,
+ insertIMEText = 2001,
+ deleteSelection = 2002,
+ setTextProperty = 2003,
+ removeTextProperty = 2004,
+ outputText = 2005,
+
+ // html only action
+ insertBreak = 3000,
+ makeList = 3001,
+ indent = 3002,
+ outdent = 3003,
+ align = 3004,
+ makeBasicBlock = 3005,
+ removeList = 3006,
+ makeDefListItem = 3007,
+ insertElement = 3008,
+ insertQuotation = 3009,
+ htmlPaste = 3012,
+ loadHTML = 3013,
+ resetTextProperties = 3014,
+ setAbsolutePosition = 3015,
+ removeAbsolutePosition = 3016,
+ decreaseZIndex = 3017,
+ increaseZIndex = 3018
+};
+
+inline bool operator!(const EditAction& aOp)
+{
+ return aOp == EditAction::none;
+}
+
+namespace mozilla {
+class AddStyleSheetTransaction;
+class AutoRules;
+class AutoSelectionRestorer;
+class AutoTransactionsConserveSelection;
+class ChangeAttributeTransaction;
+class CompositionTransaction;
+class CreateElementTransaction;
+class DeleteNodeTransaction;
+class DeleteTextTransaction;
+class EditAggregateTransaction;
+class ErrorResult;
+class InsertNodeTransaction;
+class InsertTextTransaction;
+class JoinNodeTransaction;
+class RemoveStyleSheetTransaction;
+class SplitNodeTransaction;
+class TextComposition;
+struct EditorDOMPoint;
+
+namespace dom {
+class DataTransfer;
+class Element;
+class EventTarget;
+class Selection;
+class Text;
+} // namespace dom
+
+namespace widget {
+struct IMEState;
+} // namespace widget
+
+#define kMOZEditorBogusNodeAttrAtom nsGkAtoms::mozeditorbogusnode
+#define kMOZEditorBogusNodeValue NS_LITERAL_STRING("TRUE")
+
+/**
+ * Implementation of an editor object. it will be the controller/focal point
+ * for the main editor services. i.e. the GUIManager, publishing, transaction
+ * manager, event interfaces. the idea for the event interfaces is to have them
+ * delegate the actual commands to the editor independent of the XPFE
+ * implementation.
+ */
+class EditorBase : public nsIEditor
+ , public nsIEditorIMESupport
+ , public nsSupportsWeakReference
+ , public nsIPhonetic
+{
+public:
+ typedef dom::Element Element;
+ typedef dom::Selection Selection;
+ typedef dom::Text Text;
+
+ enum IterDirection
+ {
+ kIterForward,
+ kIterBackward
+ };
+
+ /**
+ * The default constructor. This should suffice. the setting of the
+ * interfaces is done after the construction of the editor class.
+ */
+ EditorBase();
+
+protected:
+ /**
+ * The default destructor. This should suffice. Should this be pure virtual
+ * for someone to derive from the EditorBase later? I don't believe so.
+ */
+ virtual ~EditorBase();
+
+public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(EditorBase, nsIEditor)
+
+ already_AddRefed<nsIDOMDocument> GetDOMDocument();
+ already_AddRefed<nsIDocument> GetDocument();
+ already_AddRefed<nsIPresShell> GetPresShell();
+ already_AddRefed<nsIWidget> GetWidget();
+ enum NotificationForEditorObservers
+ {
+ eNotifyEditorObserversOfEnd,
+ eNotifyEditorObserversOfBefore,
+ eNotifyEditorObserversOfCancel
+ };
+ void NotifyEditorObservers(NotificationForEditorObservers aNotification);
+
+ // nsIEditor methods
+ NS_DECL_NSIEDITOR
+
+ // nsIEditorIMESupport methods
+ NS_DECL_NSIEDITORIMESUPPORT
+
+ // nsIPhonetic
+ NS_DECL_NSIPHONETIC
+
+public:
+ virtual bool IsModifiableNode(nsINode* aNode);
+
+ virtual nsresult InsertTextImpl(const nsAString& aStringToInsert,
+ nsCOMPtr<nsINode>* aInOutNode,
+ int32_t* aInOutOffset,
+ nsIDocument* aDoc);
+ nsresult InsertTextIntoTextNodeImpl(const nsAString& aStringToInsert,
+ Text& aTextNode, int32_t aOffset,
+ bool aSuppressIME = false);
+ NS_IMETHOD DeleteSelectionImpl(EDirection aAction,
+ EStripWrappers aStripWrappers);
+
+ already_AddRefed<Element> DeleteSelectionAndCreateElement(nsIAtom& aTag);
+
+ /**
+ * Helper routines for node/parent manipulations.
+ */
+ nsresult DeleteNode(nsINode* aNode);
+ nsresult InsertNode(nsIContent& aNode, nsINode& aParent, int32_t aPosition);
+ enum ECloneAttributes { eDontCloneAttributes, eCloneAttributes };
+ already_AddRefed<Element> ReplaceContainer(Element* aOldContainer,
+ nsIAtom* aNodeType,
+ nsIAtom* aAttribute = nullptr,
+ const nsAString* aValue = nullptr,
+ ECloneAttributes aCloneAttributes
+ = eDontCloneAttributes);
+ void CloneAttributes(Element* aDest, Element* aSource);
+
+ nsresult RemoveContainer(nsIContent* aNode);
+ already_AddRefed<Element> InsertContainerAbove(nsIContent* aNode,
+ nsIAtom* aNodeType,
+ nsIAtom* aAttribute = nullptr,
+ const nsAString* aValue =
+ nullptr);
+ nsIContent* SplitNode(nsIContent& aNode, int32_t aOffset,
+ ErrorResult& aResult);
+ nsresult JoinNodes(nsINode& aLeftNode, nsINode& aRightNode);
+ nsresult MoveNode(nsIContent* aNode, nsINode* aParent, int32_t aOffset);
+
+ /**
+ * Method to replace certain CreateElementNS() calls.
+ *
+ * @param aTag Tag you want.
+ */
+ already_AddRefed<Element> CreateHTMLContent(nsIAtom* aTag);
+
+ /**
+ * IME event handlers.
+ */
+ virtual nsresult BeginIMEComposition(WidgetCompositionEvent* aEvent);
+ virtual nsresult UpdateIMEComposition(nsIDOMEvent* aDOMTextEvent) = 0;
+ void EndIMEComposition();
+
+ void SwitchTextDirectionTo(uint32_t aDirection);
+
+protected:
+ nsresult DetermineCurrentDirection();
+ void FireInputEvent();
+
+ /**
+ * Create a transaction for setting aAttribute to aValue on aElement. Never
+ * returns null.
+ */
+ already_AddRefed<ChangeAttributeTransaction>
+ CreateTxnForSetAttribute(Element& aElement, nsIAtom& aAttribute,
+ const nsAString& aValue);
+
+ /**
+ * Create a transaction for removing aAttribute on aElement. Never returns
+ * null.
+ */
+ already_AddRefed<ChangeAttributeTransaction>
+ CreateTxnForRemoveAttribute(Element& aElement, nsIAtom& aAttribute);
+
+ /**
+ * Create a transaction for creating a new child node of aParent of type aTag.
+ */
+ already_AddRefed<CreateElementTransaction>
+ CreateTxnForCreateElement(nsIAtom& aTag,
+ nsINode& aParent,
+ int32_t aPosition);
+
+ already_AddRefed<Element> CreateNode(nsIAtom* aTag, nsINode* aParent,
+ int32_t aPosition);
+
+ /**
+ * Create a transaction for inserting aNode as a child of aParent.
+ */
+ already_AddRefed<InsertNodeTransaction>
+ CreateTxnForInsertNode(nsIContent& aNode, nsINode& aParent,
+ int32_t aOffset);
+
+ /**
+ * Create a transaction for removing aNode from its parent.
+ */
+ nsresult CreateTxnForDeleteNode(nsINode* aNode,
+ DeleteNodeTransaction** aTransaction);
+
+ nsresult CreateTxnForDeleteSelection(
+ EDirection aAction,
+ EditAggregateTransaction** aTransaction,
+ nsINode** aNode,
+ int32_t* aOffset,
+ int32_t* aLength);
+
+ nsresult CreateTxnForDeleteInsertionPoint(
+ nsRange* aRange,
+ EDirection aAction,
+ EditAggregateTransaction* aTransaction,
+ nsINode** aNode,
+ int32_t* aOffset,
+ int32_t* aLength);
+
+
+ /**
+ * Create a transaction for inserting aStringToInsert into aTextNode. Never
+ * returns null.
+ */
+ already_AddRefed<mozilla::InsertTextTransaction>
+ CreateTxnForInsertText(const nsAString& aStringToInsert, Text& aTextNode,
+ int32_t aOffset);
+
+ /**
+ * Never returns null.
+ */
+ already_AddRefed<mozilla::CompositionTransaction>
+ CreateTxnForComposition(const nsAString& aStringToInsert);
+
+ /**
+ * Create a transaction for adding a style sheet.
+ */
+ NS_IMETHOD CreateTxnForAddStyleSheet(
+ StyleSheet* aSheet,
+ AddStyleSheetTransaction** aTransaction);
+
+ /**
+ * Create a transaction for removing a style sheet.
+ */
+ NS_IMETHOD CreateTxnForRemoveStyleSheet(
+ StyleSheet* aSheet,
+ RemoveStyleSheetTransaction** aTransaction);
+
+ nsresult DeleteText(nsGenericDOMDataNode& aElement,
+ uint32_t aOffset, uint32_t aLength);
+
+ already_AddRefed<DeleteTextTransaction>
+ CreateTxnForDeleteText(nsGenericDOMDataNode& aElement,
+ uint32_t aOffset, uint32_t aLength);
+
+ already_AddRefed<DeleteTextTransaction>
+ CreateTxnForDeleteCharacter(nsGenericDOMDataNode& aData, uint32_t aOffset,
+ EDirection aDirection);
+
+ already_AddRefed<SplitNodeTransaction>
+ CreateTxnForSplitNode(nsIContent& aNode, uint32_t aOffset);
+
+ already_AddRefed<JoinNodeTransaction>
+ CreateTxnForJoinNode(nsINode& aLeftNode, nsINode& aRightNode);
+
+ /**
+ * This method first deletes the selection, if it's not collapsed. Then if
+ * the selection lies in a CharacterData node, it splits it. If the
+ * selection is at this point collapsed in a CharacterData node, it's
+ * adjusted to be collapsed right before or after the node instead (which is
+ * always possible, since the node was split).
+ */
+ nsresult DeleteSelectionAndPrepareToCreateNode();
+
+ /**
+ * Called after a transaction is done successfully.
+ */
+ void DoAfterDoTransaction(nsITransaction *aTxn);
+
+ /**
+ * Called after a transaction is undone successfully.
+ */
+
+ void DoAfterUndoTransaction();
+
+ /**
+ * Called after a transaction is redone successfully.
+ */
+ void DoAfterRedoTransaction();
+
+ enum TDocumentListenerNotification
+ {
+ eDocumentCreated,
+ eDocumentToBeDestroyed,
+ eDocumentStateChanged
+ };
+
+ /**
+ * Tell the doc state listeners that the doc state has changed.
+ */
+ NS_IMETHOD NotifyDocumentListeners(
+ TDocumentListenerNotification aNotificationType);
+
+ /**
+ * Make the given selection span the entire document.
+ */
+ virtual nsresult SelectEntireDocument(Selection* aSelection);
+
+ /**
+ * Helper method for scrolling the selection into view after
+ * an edit operation. aScrollToAnchor should be true if you
+ * want to scroll to the point where the selection was started.
+ * If false, it attempts to scroll the end of the selection into view.
+ *
+ * Editor methods *should* call this method instead of the versions
+ * in the various selection interfaces, since this version makes sure
+ * that the editor's sync/async settings for reflowing, painting, and
+ * scrolling match.
+ */
+ NS_IMETHOD ScrollSelectionIntoView(bool aScrollToAnchor);
+
+ virtual bool IsBlockNode(nsINode* aNode);
+
+ /**
+ * Helper for GetPriorNode() and GetNextNode().
+ */
+ nsIContent* FindNextLeafNode(nsINode* aCurrentNode,
+ bool aGoForward,
+ bool bNoBlockCrossing);
+
+ virtual nsresult InstallEventListeners();
+ virtual void CreateEventListeners();
+ virtual void RemoveEventListeners();
+
+ /**
+ * Return true if spellchecking should be enabled for this editor.
+ */
+ bool GetDesiredSpellCheckState();
+
+ bool CanEnableSpellCheck()
+ {
+ // Check for password/readonly/disabled, which are not spellchecked
+ // regardless of DOM. Also, check to see if spell check should be skipped
+ // or not.
+ return !IsPasswordEditor() && !IsReadonly() && !IsDisabled() &&
+ !ShouldSkipSpellCheck();
+ }
+
+ /**
+ * EnsureComposition() should be called by composition event handlers. This
+ * tries to get the composition for the event and set it to mComposition.
+ * However, this may fail because the composition may be committed before
+ * the event comes to the editor.
+ *
+ * @return true if there is a composition. Otherwise, for example,
+ * a composition event handler in web contents moved focus
+ * for committing the composition, returns false.
+ */
+ bool EnsureComposition(WidgetCompositionEvent* aCompositionEvent);
+
+ nsresult GetSelection(SelectionType aSelectionType,
+ nsISelection** aSelection);
+
+public:
+ /**
+ * All editor operations which alter the doc should be prefaced
+ * with a call to StartOperation, naming the action and direction.
+ */
+ NS_IMETHOD StartOperation(EditAction opID,
+ nsIEditor::EDirection aDirection);
+
+ /**
+ * All editor operations which alter the doc should be followed
+ * with a call to EndOperation.
+ */
+ NS_IMETHOD EndOperation();
+
+ /**
+ * Routines for managing the preservation of selection across
+ * various editor actions.
+ */
+ bool ArePreservingSelection();
+ void PreserveSelectionAcrossActions(Selection* aSel);
+ nsresult RestorePreservedSelection(Selection* aSel);
+ void StopPreservingSelection();
+
+ /**
+ * SplitNode() creates a new node identical to an existing node, and split
+ * the contents between the two nodes
+ * @param aExistingRightNode The node to split. It will become the new
+ * node's next sibling.
+ * @param aOffset The offset of aExistingRightNode's
+ * content|children to do the split at
+ * @param aNewLeftNode The new node resulting from the split, becomes
+ * aExistingRightNode's previous sibling.
+ */
+ nsresult SplitNodeImpl(nsIContent& aExistingRightNode,
+ int32_t aOffset,
+ nsIContent& aNewLeftNode);
+
+ /**
+ * JoinNodes() takes 2 nodes and merge their content|children.
+ * @param aNodeToKeep The node that will remain after the join.
+ * @param aNodeToJoin The node that will be joined with aNodeToKeep.
+ * There is no requirement that the two nodes be of the
+ * same type.
+ * @param aParent The parent of aNodeToKeep
+ */
+ nsresult JoinNodesImpl(nsINode* aNodeToKeep,
+ nsINode* aNodeToJoin,
+ nsINode* aParent);
+
+ /**
+ * Return the offset of aChild in aParent. Asserts fatally if parent or
+ * child is null, or parent is not child's parent.
+ */
+ static int32_t GetChildOffset(nsIDOMNode* aChild,
+ nsIDOMNode* aParent);
+
+ /**
+ * Set outOffset to the offset of aChild in the parent.
+ * Returns the parent of aChild.
+ */
+ static already_AddRefed<nsIDOMNode> GetNodeLocation(nsIDOMNode* aChild,
+ int32_t* outOffset);
+ static nsINode* GetNodeLocation(nsINode* aChild, int32_t* aOffset);
+
+ /**
+ * Returns the number of things inside aNode in the out-param aCount.
+ * @param aNode is the node to get the length of.
+ * If aNode is text, returns number of characters.
+ * If not, returns number of children nodes.
+ * @param aCount [OUT] the result of the above calculation.
+ */
+ static nsresult GetLengthOfDOMNode(nsIDOMNode *aNode, uint32_t &aCount);
+
+ /**
+ * Get the node immediately prior to aCurrentNode.
+ * @param aCurrentNode the node from which we start the search
+ * @param aEditableNode if true, only return an editable node
+ * @param aResultNode [OUT] the node that occurs before aCurrentNode in
+ * the tree, skipping non-editable nodes if
+ * aEditableNode is true. If there is no prior
+ * node, aResultNode will be nullptr.
+ * @param bNoBlockCrossing If true, don't move across "block" nodes,
+ * whatever that means.
+ */
+ nsIContent* GetPriorNode(nsINode* aCurrentNode, bool aEditableNode,
+ bool aNoBlockCrossing = false);
+
+ /**
+ * And another version that takes a {parent,offset} pair rather than a node.
+ */
+ nsIContent* GetPriorNode(nsINode* aParentNode,
+ int32_t aOffset,
+ bool aEditableNode,
+ bool aNoBlockCrossing = false);
+
+
+ /**
+ * Get the node immediately after to aCurrentNode.
+ * @param aCurrentNode the node from which we start the search
+ * @param aEditableNode if true, only return an editable node
+ * @param aResultNode [OUT] the node that occurs after aCurrentNode in the
+ * tree, skipping non-editable nodes if
+ * aEditableNode is true. If there is no prior
+ * node, aResultNode will be nullptr.
+ */
+ nsIContent* GetNextNode(nsINode* aCurrentNode,
+ bool aEditableNode,
+ bool bNoBlockCrossing = false);
+
+ /**
+ * And another version that takes a {parent,offset} pair rather than a node.
+ */
+ nsIContent* GetNextNode(nsINode* aParentNode,
+ int32_t aOffset,
+ bool aEditableNode,
+ bool aNoBlockCrossing = false);
+
+ /**
+ * Helper for GetNextNode() and GetPriorNode().
+ */
+ nsIContent* FindNode(nsINode* aCurrentNode,
+ bool aGoForward,
+ bool aEditableNode,
+ bool bNoBlockCrossing);
+ /**
+ * Get the rightmost child of aCurrentNode;
+ * return nullptr if aCurrentNode has no children.
+ */
+ nsIContent* GetRightmostChild(nsINode* aCurrentNode,
+ bool bNoBlockCrossing = false);
+
+ /**
+ * Get the leftmost child of aCurrentNode;
+ * return nullptr if aCurrentNode has no children.
+ */
+ nsIContent* GetLeftmostChild(nsINode *aCurrentNode,
+ bool bNoBlockCrossing = false);
+
+ /**
+ * Returns true if aNode is of the type implied by aTag.
+ */
+ static inline bool NodeIsType(nsIDOMNode* aNode, nsIAtom* aTag)
+ {
+ return GetTag(aNode) == aTag;
+ }
+
+ /**
+ * Returns true if aParent can contain a child of type aTag.
+ */
+ bool CanContain(nsINode& aParent, nsIContent& aChild);
+ bool CanContainTag(nsINode& aParent, nsIAtom& aTag);
+ bool TagCanContain(nsIAtom& aParentTag, nsIContent& aChild);
+ virtual bool TagCanContainTag(nsIAtom& aParentTag, nsIAtom& aChildTag);
+
+ /**
+ * Returns true if aNode is our root node.
+ */
+ bool IsRoot(nsIDOMNode* inNode);
+ bool IsRoot(nsINode* inNode);
+ bool IsEditorRoot(nsINode* aNode);
+
+ /**
+ * Returns true if aNode is a descendant of our root node.
+ */
+ bool IsDescendantOfRoot(nsIDOMNode* inNode);
+ bool IsDescendantOfRoot(nsINode* inNode);
+ bool IsDescendantOfEditorRoot(nsINode* aNode);
+
+ /**
+ * Returns true if aNode is a container.
+ */
+ virtual bool IsContainer(nsINode* aNode);
+ virtual bool IsContainer(nsIDOMNode* aNode);
+
+ /**
+ * returns true if aNode is an editable node.
+ */
+ bool IsEditable(nsIDOMNode* aNode);
+ virtual bool IsEditable(nsINode* aNode);
+
+ /**
+ * Returns true if aNode is a MozEditorBogus node.
+ */
+ bool IsMozEditorBogusNode(nsINode* aNode);
+
+ /**
+ * Counts number of editable child nodes.
+ */
+ uint32_t CountEditableChildren(nsINode* aNode);
+
+ /**
+ * Find the deep first and last children.
+ */
+ nsINode* GetFirstEditableNode(nsINode* aRoot);
+
+ /**
+ * Returns current composition.
+ */
+ TextComposition* GetComposition() const;
+
+ /**
+ * Returns true if there is composition string and not fixed.
+ */
+ bool IsIMEComposing() const;
+
+ /**
+ * Returns true when inserting text should be a part of current composition.
+ */
+ bool ShouldHandleIMEComposition() const;
+
+ /**
+ * From html rules code - migration in progress.
+ */
+ static nsresult GetTagString(nsIDOMNode* aNode, nsAString& outString);
+ static nsIAtom* GetTag(nsIDOMNode* aNode);
+
+ bool NodesSameType(nsIDOMNode* aNode1, nsIDOMNode* aNode2);
+ virtual bool AreNodesSameType(nsIContent* aNode1, nsIContent* aNode2);
+
+ static bool IsTextNode(nsIDOMNode* aNode);
+ static bool IsTextNode(nsINode* aNode);
+
+ static nsCOMPtr<nsIDOMNode> GetChildAt(nsIDOMNode* aParent, int32_t aOffset);
+ static nsIContent* GetNodeAtRangeOffsetPoint(nsIDOMNode* aParentOrNode,
+ int32_t aOffset);
+
+ static nsresult GetStartNodeAndOffset(Selection* aSelection,
+ nsIDOMNode** outStartNode,
+ int32_t* outStartOffset);
+ static nsresult GetStartNodeAndOffset(Selection* aSelection,
+ nsINode** aStartNode,
+ int32_t* aStartOffset);
+ static nsresult GetEndNodeAndOffset(Selection* aSelection,
+ nsIDOMNode** outEndNode,
+ int32_t* outEndOffset);
+ static nsresult GetEndNodeAndOffset(Selection* aSelection,
+ nsINode** aEndNode,
+ int32_t* aEndOffset);
+#if DEBUG_JOE
+ static void DumpNode(nsIDOMNode* aNode, int32_t indent = 0);
+#endif
+ Selection* GetSelection(SelectionType aSelectionType =
+ SelectionType::eNormal);
+
+ /**
+ * Helpers to add a node to the selection.
+ * Used by table cell selection methods.
+ */
+ nsresult CreateRange(nsIDOMNode* aStartParent, int32_t aStartOffset,
+ nsIDOMNode* aEndParent, int32_t aEndOffset,
+ nsRange** aRange);
+
+ /**
+ * Creates a range with just the supplied node and appends that to the
+ * selection.
+ */
+ nsresult AppendNodeToSelectionAsRange(nsIDOMNode *aNode);
+
+ /**
+ * When you are using AppendNodeToSelectionAsRange(), call this first to
+ * start a new selection.
+ */
+ nsresult ClearSelection();
+
+ nsresult IsPreformatted(nsIDOMNode* aNode, bool* aResult);
+
+ enum class EmptyContainers { no, yes };
+ int32_t SplitNodeDeep(nsIContent& aNode, nsIContent& aSplitPointParent,
+ int32_t aSplitPointOffset,
+ EmptyContainers aEmptyContainers =
+ EmptyContainers::yes,
+ nsIContent** outLeftNode = nullptr,
+ nsIContent** outRightNode = nullptr);
+ EditorDOMPoint JoinNodeDeep(nsIContent& aLeftNode,
+ nsIContent& aRightNode);
+
+ nsresult GetString(const nsAString& name, nsAString& value);
+
+ void BeginUpdateViewBatch();
+ virtual nsresult EndUpdateViewBatch();
+
+ bool GetShouldTxnSetSelection();
+
+ virtual nsresult HandleKeyPressEvent(nsIDOMKeyEvent* aKeyEvent);
+
+ nsresult HandleInlineSpellCheck(EditAction action,
+ Selection* aSelection,
+ nsIDOMNode* previousSelectedNode,
+ int32_t previousSelectedOffset,
+ nsIDOMNode* aStartNode,
+ int32_t aStartOffset,
+ nsIDOMNode* aEndNode,
+ int32_t aEndOffset);
+
+ virtual already_AddRefed<dom::EventTarget> GetDOMEventTarget() = 0;
+
+ /**
+ * Fast non-refcounting editor root element accessor
+ */
+ Element* GetRoot();
+
+ /**
+ * Likewise, but gets the editor's root instead, which is different for HTML
+ * editors.
+ */
+ virtual Element* GetEditorRoot();
+
+ /**
+ * Likewise, but gets the text control element instead of the root for
+ * plaintext editors.
+ */
+ Element* GetExposedRoot();
+
+ /**
+ * Accessor methods to flags.
+ */
+ bool IsPlaintextEditor() const
+ {
+ return (mFlags & nsIPlaintextEditor::eEditorPlaintextMask) != 0;
+ }
+
+ bool IsSingleLineEditor() const
+ {
+ return (mFlags & nsIPlaintextEditor::eEditorSingleLineMask) != 0;
+ }
+
+ bool IsPasswordEditor() const
+ {
+ return (mFlags & nsIPlaintextEditor::eEditorPasswordMask) != 0;
+ }
+
+ bool IsReadonly() const
+ {
+ return (mFlags & nsIPlaintextEditor::eEditorReadonlyMask) != 0;
+ }
+
+ bool IsDisabled() const
+ {
+ return (mFlags & nsIPlaintextEditor::eEditorDisabledMask) != 0;
+ }
+
+ bool IsInputFiltered() const
+ {
+ return (mFlags & nsIPlaintextEditor::eEditorFilterInputMask) != 0;
+ }
+
+ bool IsMailEditor() const
+ {
+ return (mFlags & nsIPlaintextEditor::eEditorMailMask) != 0;
+ }
+
+ bool IsWrapHackEnabled() const
+ {
+ return (mFlags & nsIPlaintextEditor::eEditorEnableWrapHackMask) != 0;
+ }
+
+ bool IsFormWidget() const
+ {
+ return (mFlags & nsIPlaintextEditor::eEditorWidgetMask) != 0;
+ }
+
+ bool NoCSS() const
+ {
+ return (mFlags & nsIPlaintextEditor::eEditorNoCSSMask) != 0;
+ }
+
+ bool IsInteractionAllowed() const
+ {
+ return (mFlags & nsIPlaintextEditor::eEditorAllowInteraction) != 0;
+ }
+
+ bool DontEchoPassword() const
+ {
+ return (mFlags & nsIPlaintextEditor::eEditorDontEchoPassword) != 0;
+ }
+
+ bool ShouldSkipSpellCheck() const
+ {
+ return (mFlags & nsIPlaintextEditor::eEditorSkipSpellCheck) != 0;
+ }
+
+ bool IsTabbable() const
+ {
+ return IsSingleLineEditor() || IsPasswordEditor() || IsFormWidget() ||
+ IsInteractionAllowed();
+ }
+
+ bool HasIndependentSelection() const
+ {
+ return !!mSelConWeak;
+ }
+
+ /**
+ * Get the input event target. This might return null.
+ */
+ virtual already_AddRefed<nsIContent> GetInputEventTargetContent() = 0;
+
+ /**
+ * Get the focused content, if we're focused. Returns null otherwise.
+ */
+ virtual already_AddRefed<nsIContent> GetFocusedContent();
+
+ /**
+ * Get the focused content for the argument of some IMEStateManager's
+ * methods.
+ */
+ virtual already_AddRefed<nsIContent> GetFocusedContentForIME();
+
+ /**
+ * Whether the editor is active on the DOM window. Note that when this
+ * returns true but GetFocusedContent() returns null, it means that this editor was
+ * focused when the DOM window was active.
+ */
+ virtual bool IsActiveInDOMWindow();
+
+ /**
+ * Whether the aEvent should be handled by this editor or not. When this
+ * returns FALSE, The aEvent shouldn't be handled on this editor,
+ * i.e., The aEvent should be handled by another inner editor or ancestor
+ * elements.
+ */
+ virtual bool IsAcceptableInputEvent(nsIDOMEvent* aEvent);
+
+ /**
+ * FindSelectionRoot() returns a selection root of this editor when aNode
+ * gets focus. aNode must be a content node or a document node. When the
+ * target isn't a part of this editor, returns nullptr. If this is for
+ * designMode, you should set the document node to aNode except that an
+ * element in the document has focus.
+ */
+ virtual already_AddRefed<nsIContent> FindSelectionRoot(nsINode* aNode);
+
+ /**
+ * Initializes selection and caret for the editor. If aEventTarget isn't
+ * a host of the editor, i.e., the editor doesn't get focus, this does
+ * nothing.
+ */
+ nsresult InitializeSelection(nsIDOMEventTarget* aFocusEventTarget);
+
+ /**
+ * This method has to be called by EditorEventListener::Focus.
+ * All actions that have to be done when the editor is focused needs to be
+ * added here.
+ */
+ void OnFocus(nsIDOMEventTarget* aFocusEventTarget);
+
+ /**
+ * Used to insert content from a data transfer into the editable area.
+ * This is called for each item in the data transfer, with the index of
+ * each item passed as aIndex.
+ */
+ virtual nsresult InsertFromDataTransfer(dom::DataTransfer* aDataTransfer,
+ int32_t aIndex,
+ nsIDOMDocument* aSourceDoc,
+ nsIDOMNode* aDestinationNode,
+ int32_t aDestOffset,
+ bool aDoDeleteSelection) = 0;
+
+ virtual nsresult InsertFromDrop(nsIDOMEvent* aDropEvent) = 0;
+
+ virtual already_AddRefed<nsIDOMNode> FindUserSelectAllNode(nsIDOMNode* aNode)
+ {
+ return nullptr;
+ }
+
+ /**
+ * GetIMESelectionStartOffsetIn() returns the start offset of IME selection in
+ * the aTextNode. If there is no IME selection, returns -1.
+ */
+ int32_t GetIMESelectionStartOffsetIn(nsINode* aTextNode);
+
+ /**
+ * FindBetterInsertionPoint() tries to look for better insertion point which
+ * is typically the nearest text node and offset in it.
+ */
+ void FindBetterInsertionPoint(nsCOMPtr<nsIDOMNode>& aNode,
+ int32_t& aOffset);
+ void FindBetterInsertionPoint(nsCOMPtr<nsINode>& aNode,
+ int32_t& aOffset);
+
+ /**
+ * HideCaret() hides caret with nsCaret::AddForceHide() or may show carent
+ * with nsCaret::RemoveForceHide(). This does NOT set visibility of
+ * nsCaret. Therefore, this is stateless.
+ */
+ void HideCaret(bool aHide);
+
+ void FlushFrames()
+ {
+ nsCOMPtr<nsIDocument> doc = GetDocument();
+ if (doc) {
+ doc->FlushPendingNotifications(Flush_Frames);
+ }
+ }
+
+protected:
+ enum Tristate
+ {
+ eTriUnset,
+ eTriFalse,
+ eTriTrue
+ };
+
+ // MIME type of the doc we are editing.
+ nsCString mContentMIMEType;
+
+ nsCOMPtr<nsIInlineSpellChecker> mInlineSpellChecker;
+
+ RefPtr<nsTransactionManager> mTxnMgr;
+ // Cached root node.
+ nsCOMPtr<Element> mRootElement;
+ // Current IME text node.
+ RefPtr<Text> mIMETextNode;
+ // The form field as an event receiver.
+ nsCOMPtr<dom::EventTarget> mEventTarget;
+ nsCOMPtr<nsIDOMEventListener> mEventListener;
+ // Weak reference to the nsISelectionController.
+ nsWeakPtr mSelConWeak;
+ // Weak reference to placeholder for begin/end batch purposes.
+ nsWeakPtr mPlaceHolderTxn;
+ // Weak reference to the nsIDOMDocument.
+ nsWeakPtr mDocWeak;
+ // Name of placeholder transaction.
+ nsIAtom* mPlaceHolderName;
+ // Saved selection state for placeholder transaction batching.
+ SelectionState* mSelState;
+ nsString* mPhonetic;
+ // IME composition this is not null between compositionstart and
+ // compositionend.
+ RefPtr<TextComposition> mComposition;
+
+ // Listens to all low level actions on the doc.
+ nsTArray<OwningNonNull<nsIEditActionListener>> mActionListeners;
+ // Just notify once per high level change.
+ nsTArray<OwningNonNull<nsIEditorObserver>> mEditorObservers;
+ // Listen to overall doc state (dirty or not, just created, etc.).
+ nsTArray<OwningNonNull<nsIDocumentStateListener>> mDocStateListeners;
+
+ // Cached selection for AutoSelectionRestorer.
+ SelectionState mSavedSel;
+ // Utility class object for maintaining preserved ranges.
+ RangeUpdater mRangeUpdater;
+
+ // Number of modifications (for undo/redo stack).
+ uint32_t mModCount;
+ // Behavior flags. See nsIPlaintextEditor.idl for the flags we use.
+ uint32_t mFlags;
+
+ int32_t mUpdateCount;
+
+ // Nesting count for batching.
+ int32_t mPlaceHolderBatch;
+ // The current editor action.
+ EditAction mAction;
+
+ // Offset in text node where IME comp string begins.
+ uint32_t mIMETextOffset;
+ // The Length of the composition string or commit string. If this is length
+ // of commit string, the length is truncated by maxlength attribute.
+ uint32_t mIMETextLength;
+
+ // The current direction of editor action.
+ EDirection mDirection;
+ // -1 = not initialized
+ int8_t mDocDirtyState;
+ // A Tristate value.
+ uint8_t mSpellcheckCheckboxState;
+
+ // Turn off for conservative selection adjustment by transactions.
+ bool mShouldTxnSetSelection;
+ // Whether PreDestroy has been called.
+ bool mDidPreDestroy;
+ // Whether PostCreate has been called.
+ bool mDidPostCreate;
+ bool mDispatchInputEvent;
+ // True while the instance is handling an edit action.
+ bool mIsInEditAction;
+ // Whether caret is hidden forcibly.
+ bool mHidingCaret;
+
+ friend bool NSCanUnload(nsISupports* serviceMgr);
+ friend class AutoRules;
+ friend class AutoSelectionRestorer;
+ friend class AutoTransactionsConserveSelection;
+ friend class RangeUpdater;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef mozilla_EditorBase_h
diff --git a/editor/libeditor/EditorCommands.cpp b/editor/libeditor/EditorCommands.cpp
new file mode 100644
index 000000000..2bb32e2aa
--- /dev/null
+++ b/editor/libeditor/EditorCommands.cpp
@@ -0,0 +1,1169 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "EditorCommands.h"
+
+#include "mozFlushType.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/TextEditor.h"
+#include "nsCOMPtr.h"
+#include "nsCRT.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsIClipboard.h"
+#include "nsICommandParams.h"
+#include "nsID.h"
+#include "nsIDOMDocument.h"
+#include "nsIDocument.h"
+#include "nsIEditor.h"
+#include "nsIEditorMailSupport.h"
+#include "nsIPlaintextEditor.h"
+#include "nsISelection.h"
+#include "nsISelectionController.h"
+#include "nsITransferable.h"
+#include "nsString.h"
+#include "nsAString.h"
+
+class nsISupports;
+
+#define STATE_ENABLED "state_enabled"
+#define STATE_DATA "state_data"
+
+namespace mozilla {
+
+/******************************************************************************
+ * mozilla::EditorCommandBase
+ ******************************************************************************/
+
+EditorCommandBase::EditorCommandBase()
+{
+}
+
+NS_IMPL_ISUPPORTS(EditorCommandBase, nsIControllerCommand)
+
+/******************************************************************************
+ * mozilla::UndoCommand
+ ******************************************************************************/
+
+NS_IMETHODIMP
+UndoCommand::IsCommandEnabled(const char* aCommandName,
+ nsISupports* aCommandRefCon,
+ bool* aIsEnabled)
+{
+ NS_ENSURE_ARG_POINTER(aIsEnabled);
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (editor) {
+ bool isEnabled, isEditable = false;
+ nsresult rv = editor->GetIsSelectionEditable(&isEditable);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (isEditable)
+ return editor->CanUndo(&isEnabled, aIsEnabled);
+ }
+
+ *aIsEnabled = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UndoCommand::DoCommand(const char* aCommandName,
+ nsISupports* aCommandRefCon)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (editor)
+ return editor->Undo(1);
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+UndoCommand::DoCommandParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ return DoCommand(aCommandName, aCommandRefCon);
+}
+
+NS_IMETHODIMP
+UndoCommand::GetCommandStateParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ bool canUndo;
+ IsCommandEnabled(aCommandName, aCommandRefCon, &canUndo);
+ return aParams->SetBooleanValue(STATE_ENABLED, canUndo);
+}
+
+/******************************************************************************
+ * mozilla::RedoCommand
+ ******************************************************************************/
+
+NS_IMETHODIMP
+RedoCommand::IsCommandEnabled(const char* aCommandName,
+ nsISupports* aCommandRefCon,
+ bool* aIsEnabled)
+{
+ NS_ENSURE_ARG_POINTER(aIsEnabled);
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (editor) {
+ bool isEnabled, isEditable = false;
+ nsresult rv = editor->GetIsSelectionEditable(&isEditable);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (isEditable)
+ return editor->CanRedo(&isEnabled, aIsEnabled);
+ }
+
+ *aIsEnabled = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+RedoCommand::DoCommand(const char* aCommandName,
+ nsISupports* aCommandRefCon)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (editor)
+ return editor->Redo(1);
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+RedoCommand::DoCommandParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ return DoCommand(aCommandName, aCommandRefCon);
+}
+
+NS_IMETHODIMP
+RedoCommand::GetCommandStateParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ bool canUndo;
+ IsCommandEnabled(aCommandName, aCommandRefCon, &canUndo);
+ return aParams->SetBooleanValue(STATE_ENABLED, canUndo);
+}
+
+/******************************************************************************
+ * mozilla::ClearUndoCommand
+ ******************************************************************************/
+
+NS_IMETHODIMP
+ClearUndoCommand::IsCommandEnabled(const char* aCommandName,
+ nsISupports* aCommandRefCon,
+ bool* aIsEnabled)
+{
+ NS_ENSURE_ARG_POINTER(aIsEnabled);
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (editor)
+ return editor->GetIsSelectionEditable(aIsEnabled);
+
+ *aIsEnabled = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ClearUndoCommand::DoCommand(const char* aCommandName,
+ nsISupports* aCommandRefCon)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ NS_ENSURE_TRUE(editor, NS_ERROR_NOT_IMPLEMENTED);
+
+ editor->EnableUndo(false); // Turning off undo clears undo/redo stacks.
+ editor->EnableUndo(true); // This re-enables undo/redo.
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ClearUndoCommand::DoCommandParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ return DoCommand(aCommandName, aCommandRefCon);
+}
+
+NS_IMETHODIMP
+ClearUndoCommand::GetCommandStateParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ NS_ENSURE_ARG_POINTER(aParams);
+
+ bool enabled;
+ nsresult rv = IsCommandEnabled(aCommandName, aCommandRefCon, &enabled);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return aParams->SetBooleanValue(STATE_ENABLED, enabled);
+}
+
+/******************************************************************************
+ * mozilla::CutCommand
+ ******************************************************************************/
+
+NS_IMETHODIMP
+CutCommand::IsCommandEnabled(const char* aCommandName,
+ nsISupports* aCommandRefCon,
+ bool* aIsEnabled)
+{
+ NS_ENSURE_ARG_POINTER(aIsEnabled);
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (editor) {
+ bool isEditable = false;
+ nsresult rv = editor->GetIsSelectionEditable(&isEditable);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (isEditable)
+ return editor->CanCut(aIsEnabled);
+ }
+
+ *aIsEnabled = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CutCommand::DoCommand(const char* aCommandName,
+ nsISupports* aCommandRefCon)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (editor)
+ return editor->Cut();
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+CutCommand::DoCommandParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ return DoCommand(aCommandName, aCommandRefCon);
+}
+
+NS_IMETHODIMP
+CutCommand::GetCommandStateParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ bool canUndo;
+ IsCommandEnabled(aCommandName, aCommandRefCon, &canUndo);
+ return aParams->SetBooleanValue(STATE_ENABLED, canUndo);
+}
+
+/******************************************************************************
+ * mozilla::CutOrDeleteCommand
+ ******************************************************************************/
+
+NS_IMETHODIMP
+CutOrDeleteCommand::IsCommandEnabled(const char* aCommandName,
+ nsISupports* aCommandRefCon,
+ bool* aIsEnabled)
+{
+ NS_ENSURE_ARG_POINTER(aIsEnabled);
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (editor)
+ return editor->GetIsSelectionEditable(aIsEnabled);
+
+ *aIsEnabled = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CutOrDeleteCommand::DoCommand(const char* aCommandName,
+ nsISupports* aCommandRefCon)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (editor) {
+ nsCOMPtr<nsISelection> selection;
+ nsresult rv = editor->GetSelection(getter_AddRefs(selection));
+ if (NS_SUCCEEDED(rv) && selection && selection->Collapsed()) {
+ return editor->DeleteSelection(nsIEditor::eNext, nsIEditor::eStrip);
+ }
+ return editor->Cut();
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+CutOrDeleteCommand::DoCommandParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ return DoCommand(aCommandName, aCommandRefCon);
+}
+
+NS_IMETHODIMP
+CutOrDeleteCommand::GetCommandStateParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ bool canUndo;
+ IsCommandEnabled(aCommandName, aCommandRefCon, &canUndo);
+ return aParams->SetBooleanValue(STATE_ENABLED, canUndo);
+}
+
+/******************************************************************************
+ * mozilla::CopyCommand
+ ******************************************************************************/
+
+NS_IMETHODIMP
+CopyCommand::IsCommandEnabled(const char* aCommandName,
+ nsISupports* aCommandRefCon,
+ bool* aIsEnabled)
+{
+ NS_ENSURE_ARG_POINTER(aIsEnabled);
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (editor)
+ return editor->CanCopy(aIsEnabled);
+
+ *aIsEnabled = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CopyCommand::DoCommand(const char* aCommandName,
+ nsISupports* aCommandRefCon)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (editor)
+ return editor->Copy();
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+CopyCommand::DoCommandParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ return DoCommand(aCommandName, aCommandRefCon);
+}
+
+NS_IMETHODIMP
+CopyCommand::GetCommandStateParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ bool canUndo;
+ IsCommandEnabled(aCommandName, aCommandRefCon, &canUndo);
+ return aParams->SetBooleanValue(STATE_ENABLED, canUndo);
+}
+
+/******************************************************************************
+ * mozilla::CopyOrDeleteCommand
+ ******************************************************************************/
+
+NS_IMETHODIMP
+CopyOrDeleteCommand::IsCommandEnabled(const char* aCommandName,
+ nsISupports* aCommandRefCon,
+ bool* aIsEnabled)
+{
+ NS_ENSURE_ARG_POINTER(aIsEnabled);
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (editor)
+ return editor->GetIsSelectionEditable(aIsEnabled);
+
+ *aIsEnabled = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CopyOrDeleteCommand::DoCommand(const char* aCommandName,
+ nsISupports* aCommandRefCon)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (editor) {
+ nsCOMPtr<nsISelection> selection;
+ nsresult rv = editor->GetSelection(getter_AddRefs(selection));
+ if (NS_SUCCEEDED(rv) && selection && selection->Collapsed()) {
+ return editor->DeleteSelection(nsIEditor::eNextWord, nsIEditor::eStrip);
+ }
+ return editor->Copy();
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+CopyOrDeleteCommand::DoCommandParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ return DoCommand(aCommandName, aCommandRefCon);
+}
+
+NS_IMETHODIMP
+CopyOrDeleteCommand::GetCommandStateParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ bool canUndo;
+ IsCommandEnabled(aCommandName, aCommandRefCon, &canUndo);
+ return aParams->SetBooleanValue(STATE_ENABLED, canUndo);
+}
+
+/******************************************************************************
+ * mozilla::CopyAndCollapseToEndCommand
+ ******************************************************************************/
+
+NS_IMETHODIMP
+CopyAndCollapseToEndCommand::IsCommandEnabled(const char* aCommandName,
+ nsISupports* aCommandRefCon,
+ bool* aIsEnabled)
+{
+ NS_ENSURE_ARG_POINTER(aIsEnabled);
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (editor)
+ return editor->CanCopy(aIsEnabled);
+
+ *aIsEnabled = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CopyAndCollapseToEndCommand::DoCommand(const char* aCommandName,
+ nsISupports* aCommandRefCon)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (editor) {
+ nsresult rv = editor->Copy();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ nsCOMPtr<nsISelection> selection;
+ rv = editor->GetSelection(getter_AddRefs(selection));
+ if (NS_SUCCEEDED(rv) && selection) {
+ selection->CollapseToEnd();
+ }
+ return rv;
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+CopyAndCollapseToEndCommand::DoCommandParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ return DoCommand(aCommandName, aCommandRefCon);
+}
+
+NS_IMETHODIMP
+CopyAndCollapseToEndCommand::GetCommandStateParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ bool canUndo;
+ IsCommandEnabled(aCommandName, aCommandRefCon, &canUndo);
+ return aParams->SetBooleanValue(STATE_ENABLED, canUndo);
+}
+
+/******************************************************************************
+ * mozilla::PasteCommand
+ ******************************************************************************/
+
+NS_IMETHODIMP
+PasteCommand::IsCommandEnabled(const char* aCommandName,
+ nsISupports* aCommandRefCon,
+ bool* aIsEnabled)
+{
+ NS_ENSURE_ARG_POINTER(aIsEnabled);
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (editor) {
+ bool isEditable = false;
+ nsresult rv = editor->GetIsSelectionEditable(&isEditable);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (isEditable)
+ return editor->CanPaste(nsIClipboard::kGlobalClipboard, aIsEnabled);
+ }
+
+ *aIsEnabled = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PasteCommand::DoCommand(const char* aCommandName,
+ nsISupports* aCommandRefCon)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ NS_ENSURE_TRUE(editor, NS_ERROR_FAILURE);
+
+ return editor->Paste(nsIClipboard::kGlobalClipboard);
+}
+
+NS_IMETHODIMP
+PasteCommand::DoCommandParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ return DoCommand(aCommandName, aCommandRefCon);
+}
+
+NS_IMETHODIMP
+PasteCommand::GetCommandStateParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ bool canUndo;
+ IsCommandEnabled(aCommandName, aCommandRefCon, &canUndo);
+ return aParams->SetBooleanValue(STATE_ENABLED, canUndo);
+}
+
+/******************************************************************************
+ * mozilla::PasteTransferableCommand
+ ******************************************************************************/
+
+NS_IMETHODIMP
+PasteTransferableCommand::IsCommandEnabled(const char* aCommandName,
+ nsISupports* aCommandRefCon,
+ bool* aIsEnabled)
+{
+ NS_ENSURE_ARG_POINTER(aIsEnabled);
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (editor) {
+ bool isEditable = false;
+ nsresult rv = editor->GetIsSelectionEditable(&isEditable);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (isEditable)
+ return editor->CanPasteTransferable(nullptr, aIsEnabled);
+ }
+
+ *aIsEnabled = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PasteTransferableCommand::DoCommand(const char* aCommandName,
+ nsISupports* aCommandRefCon)
+{
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+PasteTransferableCommand::DoCommandParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ NS_ENSURE_TRUE(editor, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsISupports> supports;
+ aParams->GetISupportsValue("transferable", getter_AddRefs(supports));
+ NS_ENSURE_TRUE(supports, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsITransferable> trans = do_QueryInterface(supports);
+ NS_ENSURE_TRUE(trans, NS_ERROR_FAILURE);
+
+ return editor->PasteTransferable(trans);
+}
+
+NS_IMETHODIMP
+PasteTransferableCommand::GetCommandStateParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ NS_ENSURE_TRUE(editor, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsITransferable> trans;
+
+ nsCOMPtr<nsISupports> supports;
+ aParams->GetISupportsValue("transferable", getter_AddRefs(supports));
+ if (supports) {
+ trans = do_QueryInterface(supports);
+ NS_ENSURE_TRUE(trans, NS_ERROR_FAILURE);
+ }
+
+ bool canPaste;
+ nsresult rv = editor->CanPasteTransferable(trans, &canPaste);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return aParams->SetBooleanValue(STATE_ENABLED, canPaste);
+}
+
+/******************************************************************************
+ * mozilla::SwitchTextDirectionCommand
+ ******************************************************************************/
+
+NS_IMETHODIMP
+SwitchTextDirectionCommand::IsCommandEnabled(const char* aCommandName,
+ nsISupports* aCommandRefCon,
+ bool* aIsEnabled)
+{
+ NS_ENSURE_ARG_POINTER(aIsEnabled);
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (editor)
+ return editor->GetIsSelectionEditable(aIsEnabled);
+
+ *aIsEnabled = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SwitchTextDirectionCommand::DoCommand(const char* aCommandName,
+ nsISupports* aCommandRefCon)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ NS_ENSURE_TRUE(editor, NS_ERROR_FAILURE);
+
+ return editor->SwitchTextDirection();
+}
+
+NS_IMETHODIMP
+SwitchTextDirectionCommand::DoCommandParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ return DoCommand(aCommandName, aCommandRefCon);
+}
+
+NS_IMETHODIMP
+SwitchTextDirectionCommand::GetCommandStateParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ bool canSwitchTextDirection = true;
+ IsCommandEnabled(aCommandName, aCommandRefCon, &canSwitchTextDirection);
+ return aParams->SetBooleanValue(STATE_ENABLED, canSwitchTextDirection);
+}
+
+/******************************************************************************
+ * mozilla::DeleteCommand
+ ******************************************************************************/
+
+NS_IMETHODIMP
+DeleteCommand::IsCommandEnabled(const char* aCommandName,
+ nsISupports* aCommandRefCon,
+ bool* aIsEnabled)
+{
+ NS_ENSURE_ARG_POINTER(aIsEnabled);
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ *aIsEnabled = false;
+
+ if (!editor) {
+ return NS_OK;
+ }
+
+ // We can generally delete whenever the selection is editable. However,
+ // cmd_delete doesn't make sense if the selection is collapsed because it's
+ // directionless, which is the same condition under which we can't cut.
+ nsresult rv = editor->GetIsSelectionEditable(aIsEnabled);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!nsCRT::strcmp("cmd_delete", aCommandName) && *aIsEnabled) {
+ rv = editor->CanDelete(aIsEnabled);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DeleteCommand::DoCommand(const char* aCommandName,
+ nsISupports* aCommandRefCon)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ NS_ENSURE_TRUE(editor, NS_ERROR_FAILURE);
+
+ nsIEditor::EDirection deleteDir = nsIEditor::eNone;
+
+ if (!nsCRT::strcmp("cmd_delete", aCommandName)) {
+ // Really this should probably be eNone, but it only makes a difference if
+ // the selection is collapsed, and then this command is disabled. So let's
+ // keep it as it always was to avoid breaking things.
+ deleteDir = nsIEditor::ePrevious;
+ } else if (!nsCRT::strcmp("cmd_deleteCharForward", aCommandName)) {
+ deleteDir = nsIEditor::eNext;
+ } else if (!nsCRT::strcmp("cmd_deleteCharBackward", aCommandName)) {
+ deleteDir = nsIEditor::ePrevious;
+ } else if (!nsCRT::strcmp("cmd_deleteWordBackward", aCommandName)) {
+ deleteDir = nsIEditor::ePreviousWord;
+ } else if (!nsCRT::strcmp("cmd_deleteWordForward", aCommandName)) {
+ deleteDir = nsIEditor::eNextWord;
+ } else if (!nsCRT::strcmp("cmd_deleteToBeginningOfLine", aCommandName)) {
+ deleteDir = nsIEditor::eToBeginningOfLine;
+ } else if (!nsCRT::strcmp("cmd_deleteToEndOfLine", aCommandName)) {
+ deleteDir = nsIEditor::eToEndOfLine;
+ } else {
+ MOZ_CRASH("Unrecognized nsDeleteCommand");
+ }
+
+ return editor->DeleteSelection(deleteDir, nsIEditor::eStrip);
+}
+
+NS_IMETHODIMP
+DeleteCommand::DoCommandParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ return DoCommand(aCommandName, aCommandRefCon);
+}
+
+NS_IMETHODIMP
+DeleteCommand::GetCommandStateParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ bool canUndo;
+ IsCommandEnabled(aCommandName, aCommandRefCon, &canUndo);
+ return aParams->SetBooleanValue(STATE_ENABLED, canUndo);
+}
+
+/******************************************************************************
+ * mozilla::SelectAllCommand
+ ******************************************************************************/
+
+NS_IMETHODIMP
+SelectAllCommand::IsCommandEnabled(const char* aCommandName,
+ nsISupports* aCommandRefCon,
+ bool* aIsEnabled)
+{
+ NS_ENSURE_ARG_POINTER(aIsEnabled);
+
+ nsresult rv = NS_OK;
+ // You can always select all, unless the selection is editable,
+ // and the editable region is empty!
+ *aIsEnabled = true;
+ bool docIsEmpty;
+
+ // you can select all if there is an editor which is non-empty
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (editor) {
+ rv = editor->GetDocumentIsEmpty(&docIsEmpty);
+ NS_ENSURE_SUCCESS(rv, rv);
+ *aIsEnabled = !docIsEmpty;
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+SelectAllCommand::DoCommand(const char* aCommandName,
+ nsISupports* aCommandRefCon)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (editor)
+ return editor->SelectAll();
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+SelectAllCommand::DoCommandParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ return DoCommand(aCommandName, aCommandRefCon);
+}
+
+NS_IMETHODIMP
+SelectAllCommand::GetCommandStateParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ bool canUndo;
+ IsCommandEnabled(aCommandName, aCommandRefCon, &canUndo);
+ return aParams->SetBooleanValue(STATE_ENABLED, canUndo);
+}
+
+/******************************************************************************
+ * mozilla::SelectionMoveCommands
+ ******************************************************************************/
+
+NS_IMETHODIMP
+SelectionMoveCommands::IsCommandEnabled(const char* aCommandName,
+ nsISupports* aCommandRefCon,
+ bool* aIsEnabled)
+{
+ NS_ENSURE_ARG_POINTER(aIsEnabled);
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (editor)
+ return editor->GetIsSelectionEditable(aIsEnabled);
+
+ *aIsEnabled = false;
+ return NS_OK;
+}
+
+static const struct ScrollCommand {
+ const char *reverseScroll;
+ const char *forwardScroll;
+ nsresult (NS_STDCALL nsISelectionController::*scroll)(bool);
+} scrollCommands[] = {
+ { "cmd_scrollTop", "cmd_scrollBottom",
+ &nsISelectionController::CompleteScroll },
+ { "cmd_scrollPageUp", "cmd_scrollPageDown",
+ &nsISelectionController::ScrollPage },
+ { "cmd_scrollLineUp", "cmd_scrollLineDown",
+ &nsISelectionController::ScrollLine }
+};
+
+static const struct MoveCommand {
+ const char *reverseMove;
+ const char *forwardMove;
+ const char *reverseSelect;
+ const char *forwardSelect;
+ nsresult (NS_STDCALL nsISelectionController::*move)(bool, bool);
+} moveCommands[] = {
+ { "cmd_charPrevious", "cmd_charNext",
+ "cmd_selectCharPrevious", "cmd_selectCharNext",
+ &nsISelectionController::CharacterMove },
+ { "cmd_linePrevious", "cmd_lineNext",
+ "cmd_selectLinePrevious", "cmd_selectLineNext",
+ &nsISelectionController::LineMove },
+ { "cmd_wordPrevious", "cmd_wordNext",
+ "cmd_selectWordPrevious", "cmd_selectWordNext",
+ &nsISelectionController::WordMove },
+ { "cmd_beginLine", "cmd_endLine",
+ "cmd_selectBeginLine", "cmd_selectEndLine",
+ &nsISelectionController::IntraLineMove },
+ { "cmd_movePageUp", "cmd_movePageDown",
+ "cmd_selectPageUp", "cmd_selectPageDown",
+ &nsISelectionController::PageMove },
+ { "cmd_moveTop", "cmd_moveBottom",
+ "cmd_selectTop", "cmd_selectBottom",
+ &nsISelectionController::CompleteMove }
+};
+
+static const struct PhysicalCommand {
+ const char *move;
+ const char *select;
+ int16_t direction;
+ int16_t amount;
+} physicalCommands[] = {
+ { "cmd_moveLeft", "cmd_selectLeft",
+ nsISelectionController::MOVE_LEFT, 0 },
+ { "cmd_moveRight", "cmd_selectRight",
+ nsISelectionController::MOVE_RIGHT, 0 },
+ { "cmd_moveUp", "cmd_selectUp",
+ nsISelectionController::MOVE_UP, 0 },
+ { "cmd_moveDown", "cmd_selectDown",
+ nsISelectionController::MOVE_DOWN, 0 },
+ { "cmd_moveLeft2", "cmd_selectLeft2",
+ nsISelectionController::MOVE_LEFT, 1 },
+ { "cmd_moveRight2", "cmd_selectRight2",
+ nsISelectionController::MOVE_RIGHT, 1 },
+ { "cmd_moveUp2", "cmd_selectUp2",
+ nsISelectionController::MOVE_UP, 1 },
+ { "cmd_moveDown2", "cmd_selectDown2",
+ nsISelectionController::MOVE_DOWN, 1 }
+};
+
+NS_IMETHODIMP
+SelectionMoveCommands::DoCommand(const char* aCommandName,
+ nsISupports* aCommandRefCon)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ NS_ENSURE_TRUE(editor, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIDOMDocument> domDoc;
+ editor->GetDocument(getter_AddRefs(domDoc));
+ nsCOMPtr<nsIDocument> doc = do_QueryInterface(domDoc);
+ if (doc) {
+ // Most of the commands below (possibly all of them) need layout to
+ // be up to date.
+ doc->FlushPendingNotifications(Flush_Layout);
+ }
+
+ nsCOMPtr<nsISelectionController> selCont;
+ nsresult rv = editor->GetSelectionController(getter_AddRefs(selCont));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(selCont, NS_ERROR_FAILURE);
+
+ // scroll commands
+ for (size_t i = 0; i < mozilla::ArrayLength(scrollCommands); i++) {
+ const ScrollCommand &cmd = scrollCommands[i];
+ if (!nsCRT::strcmp(aCommandName, cmd.reverseScroll)) {
+ return (selCont->*(cmd.scroll))(false);
+ } else if (!nsCRT::strcmp(aCommandName, cmd.forwardScroll)) {
+ return (selCont->*(cmd.scroll))(true);
+ }
+ }
+
+ // caret movement/selection commands
+ for (size_t i = 0; i < mozilla::ArrayLength(moveCommands); i++) {
+ const MoveCommand &cmd = moveCommands[i];
+ if (!nsCRT::strcmp(aCommandName, cmd.reverseMove)) {
+ return (selCont->*(cmd.move))(false, false);
+ } else if (!nsCRT::strcmp(aCommandName, cmd.forwardMove)) {
+ return (selCont->*(cmd.move))(true, false);
+ } else if (!nsCRT::strcmp(aCommandName, cmd.reverseSelect)) {
+ return (selCont->*(cmd.move))(false, true);
+ } else if (!nsCRT::strcmp(aCommandName, cmd.forwardSelect)) {
+ return (selCont->*(cmd.move))(true, true);
+ }
+ }
+
+ // physical-direction movement/selection
+ for (size_t i = 0; i < mozilla::ArrayLength(physicalCommands); i++) {
+ const PhysicalCommand &cmd = physicalCommands[i];
+ if (!nsCRT::strcmp(aCommandName, cmd.move)) {
+ return selCont->PhysicalMove(cmd.direction, cmd.amount, false);
+ } else if (!nsCRT::strcmp(aCommandName, cmd.select)) {
+ return selCont->PhysicalMove(cmd.direction, cmd.amount, true);
+ }
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+SelectionMoveCommands::DoCommandParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ return DoCommand(aCommandName, aCommandRefCon);
+}
+
+NS_IMETHODIMP
+SelectionMoveCommands::GetCommandStateParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ bool canUndo;
+ IsCommandEnabled(aCommandName, aCommandRefCon, &canUndo);
+ return aParams->SetBooleanValue(STATE_ENABLED, canUndo);
+}
+
+/******************************************************************************
+ * mozilla::InsertPlaintextCommand
+ ******************************************************************************/
+
+NS_IMETHODIMP
+InsertPlaintextCommand::IsCommandEnabled(const char* aCommandName,
+ nsISupports* aCommandRefCon,
+ bool* aIsEnabled)
+{
+ NS_ENSURE_ARG_POINTER(aIsEnabled);
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (editor)
+ return editor->GetIsSelectionEditable(aIsEnabled);
+
+ *aIsEnabled = false;
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+InsertPlaintextCommand::DoCommand(const char* aCommandName,
+ nsISupports* aCommandRefCon)
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+InsertPlaintextCommand::DoCommandParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ NS_ENSURE_ARG_POINTER(aParams);
+
+ nsCOMPtr<nsIPlaintextEditor> editor = do_QueryInterface(aCommandRefCon);
+ NS_ENSURE_TRUE(editor, NS_ERROR_NOT_IMPLEMENTED);
+
+ // Get text to insert from command params
+ nsAutoString text;
+ nsresult rv = aParams->GetStringValue(STATE_DATA, text);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!text.IsEmpty())
+ return editor->InsertText(text);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+InsertPlaintextCommand::GetCommandStateParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ if (NS_WARN_IF(!aParams)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ bool aIsEnabled = false;
+ IsCommandEnabled(aCommandName, aCommandRefCon, &aIsEnabled);
+ return aParams->SetBooleanValue(STATE_ENABLED, aIsEnabled);
+}
+
+/******************************************************************************
+ * mozilla::InsertParagraphCommand
+ ******************************************************************************/
+
+NS_IMETHODIMP
+InsertParagraphCommand::IsCommandEnabled(const char* aCommandName,
+ nsISupports* aCommandRefCon,
+ bool* aIsEnabled)
+{
+ if (NS_WARN_IF(!aIsEnabled)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (NS_WARN_IF(!editor)) {
+ *aIsEnabled = false;
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ return editor->GetIsSelectionEditable(aIsEnabled);
+}
+
+NS_IMETHODIMP
+InsertParagraphCommand::DoCommand(const char* aCommandName,
+ nsISupports* aCommandRefCon)
+{
+ nsCOMPtr<nsIPlaintextEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (NS_WARN_IF(!editor)) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ TextEditor* textEditor = static_cast<TextEditor*>(editor.get());
+
+ return textEditor->TypedText(EmptyString(), TextEditor::eTypedBreak);
+}
+
+NS_IMETHODIMP
+InsertParagraphCommand::DoCommandParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ return DoCommand(aCommandName, aCommandRefCon);
+}
+
+NS_IMETHODIMP
+InsertParagraphCommand::GetCommandStateParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ if (NS_WARN_IF(!aParams)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ bool aIsEnabled = false;
+ IsCommandEnabled(aCommandName, aCommandRefCon, &aIsEnabled);
+ return aParams->SetBooleanValue(STATE_ENABLED, aIsEnabled);
+}
+
+/******************************************************************************
+ * mozilla::InsertLineBreakCommand
+ ******************************************************************************/
+
+NS_IMETHODIMP
+InsertLineBreakCommand::IsCommandEnabled(const char* aCommandName,
+ nsISupports* aCommandRefCon,
+ bool* aIsEnabled)
+{
+ if (NS_WARN_IF(!aIsEnabled)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (NS_WARN_IF(!editor)) {
+ *aIsEnabled = false;
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ return editor->GetIsSelectionEditable(aIsEnabled);
+}
+
+NS_IMETHODIMP
+InsertLineBreakCommand::DoCommand(const char* aCommandName,
+ nsISupports* aCommandRefCon)
+{
+ nsCOMPtr<nsIPlaintextEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (NS_WARN_IF(!editor)) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ TextEditor* textEditor = static_cast<TextEditor*>(editor.get());
+
+ return textEditor->TypedText(EmptyString(), TextEditor::eTypedBR);
+}
+
+NS_IMETHODIMP
+InsertLineBreakCommand::DoCommandParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ return DoCommand(aCommandName, aCommandRefCon);
+}
+
+NS_IMETHODIMP
+InsertLineBreakCommand::GetCommandStateParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ if (NS_WARN_IF(!aParams)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ bool aIsEnabled = false;
+ IsCommandEnabled(aCommandName, aCommandRefCon, &aIsEnabled);
+ return aParams->SetBooleanValue(STATE_ENABLED, aIsEnabled);
+}
+
+/******************************************************************************
+ * mozilla::PasteQuotationCommand
+ ******************************************************************************/
+
+NS_IMETHODIMP
+PasteQuotationCommand::IsCommandEnabled(const char* aCommandName,
+ nsISupports* aCommandRefCon,
+ bool* aIsEnabled)
+{
+ NS_ENSURE_ARG_POINTER(aIsEnabled);
+
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ nsCOMPtr<nsIEditorMailSupport> mailEditor = do_QueryInterface(aCommandRefCon);
+ if (editor && mailEditor) {
+ uint32_t flags;
+ editor->GetFlags(&flags);
+ if (!(flags & nsIPlaintextEditor::eEditorSingleLineMask))
+ return editor->CanPaste(nsIClipboard::kGlobalClipboard, aIsEnabled);
+ }
+
+ *aIsEnabled = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PasteQuotationCommand::DoCommand(const char* aCommandName,
+ nsISupports* aCommandRefCon)
+{
+ nsCOMPtr<nsIEditorMailSupport> mailEditor = do_QueryInterface(aCommandRefCon);
+ if (mailEditor)
+ return mailEditor->PasteAsQuotation(nsIClipboard::kGlobalClipboard);
+
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+PasteQuotationCommand::DoCommandParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ nsCOMPtr<nsIEditorMailSupport> mailEditor = do_QueryInterface(aCommandRefCon);
+ if (mailEditor)
+ return mailEditor->PasteAsQuotation(nsIClipboard::kGlobalClipboard);
+
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+PasteQuotationCommand::GetCommandStateParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (editor) {
+ bool enabled = false;
+ editor->CanPaste(nsIClipboard::kGlobalClipboard, &enabled);
+ aParams->SetBooleanValue(STATE_ENABLED, enabled);
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/EditorCommands.h b/editor/libeditor/EditorCommands.h
new file mode 100644
index 000000000..d7dc27b94
--- /dev/null
+++ b/editor/libeditor/EditorCommands.h
@@ -0,0 +1,106 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef EditorCommands_h_
+#define EditorCommands_h_
+
+#include "nsIControllerCommand.h"
+#include "nsISupportsImpl.h"
+#include "nscore.h"
+
+class nsICommandParams;
+class nsISupports;
+
+namespace mozilla {
+
+/**
+ * This is a virtual base class for commands registered with the editor
+ * controller. Note that such commands can be shared by more than on editor
+ * instance, so MUST be stateless. Any state must be stored via the refCon
+ * (an nsIEditor).
+ */
+
+class EditorCommandBase : public nsIControllerCommand
+{
+public:
+ EditorCommandBase();
+
+ NS_DECL_ISUPPORTS
+
+ NS_IMETHOD IsCommandEnabled(const char* aCommandName,
+ nsISupports* aCommandRefCon,
+ bool* aIsEnabled) override = 0;
+ NS_IMETHOD DoCommand(const char* aCommandName,
+ nsISupports* aCommandRefCon) override = 0;
+
+protected:
+ virtual ~EditorCommandBase() {}
+};
+
+
+#define NS_DECL_EDITOR_COMMAND(_cmd) \
+class _cmd final : public EditorCommandBase \
+{ \
+public: \
+ NS_IMETHOD IsCommandEnabled(const char* aCommandName, \
+ nsISupports* aCommandRefCon, \
+ bool* aIsEnabled) override; \
+ NS_IMETHOD DoCommand(const char* aCommandName, \
+ nsISupports* aCommandRefCon) override; \
+ NS_IMETHOD DoCommandParams(const char* aCommandName, \
+ nsICommandParams* aParams, \
+ nsISupports* aCommandRefCon) override; \
+ NS_IMETHOD GetCommandStateParams(const char* aCommandName, \
+ nsICommandParams* aParams, \
+ nsISupports* aCommandRefCon) override; \
+};
+
+// basic editor commands
+NS_DECL_EDITOR_COMMAND(UndoCommand)
+NS_DECL_EDITOR_COMMAND(RedoCommand)
+NS_DECL_EDITOR_COMMAND(ClearUndoCommand)
+
+NS_DECL_EDITOR_COMMAND(CutCommand)
+NS_DECL_EDITOR_COMMAND(CutOrDeleteCommand)
+NS_DECL_EDITOR_COMMAND(CopyCommand)
+NS_DECL_EDITOR_COMMAND(CopyOrDeleteCommand)
+NS_DECL_EDITOR_COMMAND(CopyAndCollapseToEndCommand)
+NS_DECL_EDITOR_COMMAND(PasteCommand)
+NS_DECL_EDITOR_COMMAND(PasteTransferableCommand)
+NS_DECL_EDITOR_COMMAND(SwitchTextDirectionCommand)
+NS_DECL_EDITOR_COMMAND(DeleteCommand)
+NS_DECL_EDITOR_COMMAND(SelectAllCommand)
+
+NS_DECL_EDITOR_COMMAND(SelectionMoveCommands)
+
+// Insert content commands
+NS_DECL_EDITOR_COMMAND(InsertPlaintextCommand)
+NS_DECL_EDITOR_COMMAND(InsertParagraphCommand)
+NS_DECL_EDITOR_COMMAND(InsertLineBreakCommand)
+NS_DECL_EDITOR_COMMAND(PasteQuotationCommand)
+
+
+#if 0
+// template for new command
+NS_IMETHODIMP
+FooCommand::IsCommandEnabled(const char* aCommandName,
+ nsISupports* aCommandRefCon,
+ bool* aIsEnabled)
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FooCommand::DoCommand(const char* aCommandName,
+ const nsAString& aCommandParams,
+ nsISupports* aCommandRefCon)
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+#endif
+
+} // namespace mozilla
+
+#endif // #ifndef EditorCommands_h_
diff --git a/editor/libeditor/EditorController.cpp b/editor/libeditor/EditorController.cpp
new file mode 100644
index 000000000..b9e499978
--- /dev/null
+++ b/editor/libeditor/EditorController.cpp
@@ -0,0 +1,146 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/EditorController.h"
+
+#include "EditorCommands.h"
+#include "mozilla/mozalloc.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsIControllerCommandTable.h"
+
+class nsIControllerCommand;
+
+namespace mozilla {
+
+#define NS_REGISTER_ONE_COMMAND(_cmdClass, _cmdName) \
+ { \
+ _cmdClass* theCmd = new _cmdClass(); \
+ NS_ENSURE_TRUE(theCmd, NS_ERROR_OUT_OF_MEMORY); \
+ aCommandTable->RegisterCommand( \
+ _cmdName, \
+ static_cast<nsIControllerCommand *>(theCmd)); \
+ }
+
+#define NS_REGISTER_FIRST_COMMAND(_cmdClass, _cmdName) \
+ { \
+ _cmdClass* theCmd = new _cmdClass(); \
+ NS_ENSURE_TRUE(theCmd, NS_ERROR_OUT_OF_MEMORY); \
+ aCommandTable->RegisterCommand( \
+ _cmdName, \
+ static_cast<nsIControllerCommand *>(theCmd));
+
+#define NS_REGISTER_NEXT_COMMAND(_cmdClass, _cmdName) \
+ aCommandTable->RegisterCommand( \
+ _cmdName, \
+ static_cast<nsIControllerCommand *>(theCmd));
+
+#define NS_REGISTER_LAST_COMMAND(_cmdClass, _cmdName) \
+ aCommandTable->RegisterCommand( \
+ _cmdName, \
+ static_cast<nsIControllerCommand *>(theCmd)); \
+ }
+
+// static
+nsresult
+EditorController::RegisterEditingCommands(
+ nsIControllerCommandTable* aCommandTable)
+{
+ // now register all our commands
+ // These are commands that will be used in text widgets, and in composer
+
+ NS_REGISTER_ONE_COMMAND(UndoCommand, "cmd_undo");
+ NS_REGISTER_ONE_COMMAND(RedoCommand, "cmd_redo");
+ NS_REGISTER_ONE_COMMAND(ClearUndoCommand, "cmd_clearUndo");
+
+ NS_REGISTER_ONE_COMMAND(CutCommand, "cmd_cut");
+ NS_REGISTER_ONE_COMMAND(CutOrDeleteCommand, "cmd_cutOrDelete");
+ NS_REGISTER_ONE_COMMAND(CopyCommand, "cmd_copy");
+ NS_REGISTER_ONE_COMMAND(CopyOrDeleteCommand, "cmd_copyOrDelete");
+ NS_REGISTER_ONE_COMMAND(CopyAndCollapseToEndCommand,
+ "cmd_copyAndCollapseToEnd");
+ NS_REGISTER_ONE_COMMAND(SelectAllCommand, "cmd_selectAll");
+
+ NS_REGISTER_ONE_COMMAND(PasteCommand, "cmd_paste");
+ NS_REGISTER_ONE_COMMAND(PasteTransferableCommand, "cmd_pasteTransferable");
+
+ NS_REGISTER_ONE_COMMAND(SwitchTextDirectionCommand,
+ "cmd_switchTextDirection");
+
+ NS_REGISTER_FIRST_COMMAND(DeleteCommand, "cmd_delete");
+ NS_REGISTER_NEXT_COMMAND(DeleteCommand, "cmd_deleteCharBackward");
+ NS_REGISTER_NEXT_COMMAND(DeleteCommand, "cmd_deleteCharForward");
+ NS_REGISTER_NEXT_COMMAND(DeleteCommand, "cmd_deleteWordBackward");
+ NS_REGISTER_NEXT_COMMAND(DeleteCommand, "cmd_deleteWordForward");
+ NS_REGISTER_NEXT_COMMAND(DeleteCommand, "cmd_deleteToBeginningOfLine");
+ NS_REGISTER_LAST_COMMAND(DeleteCommand, "cmd_deleteToEndOfLine");
+
+ // Insert content
+ NS_REGISTER_ONE_COMMAND(InsertPlaintextCommand, "cmd_insertText");
+ NS_REGISTER_ONE_COMMAND(InsertParagraphCommand, "cmd_insertParagraph");
+ NS_REGISTER_ONE_COMMAND(InsertLineBreakCommand, "cmd_insertLineBreak");
+ NS_REGISTER_ONE_COMMAND(PasteQuotationCommand, "cmd_pasteQuote");
+
+ return NS_OK;
+}
+
+// static
+nsresult
+EditorController::RegisterEditorCommands(
+ nsIControllerCommandTable* aCommandTable)
+{
+ // These are commands that will be used in text widgets only.
+
+ NS_REGISTER_FIRST_COMMAND(SelectionMoveCommands, "cmd_scrollTop");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_scrollBottom");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_moveTop");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_moveBottom");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectTop");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectBottom");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_lineNext");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_linePrevious");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectLineNext");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectLinePrevious");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_charPrevious");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_charNext");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectCharPrevious");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectCharNext");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_beginLine");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_endLine");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectBeginLine");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectEndLine");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_wordPrevious");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_wordNext");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectWordPrevious");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectWordNext");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_scrollPageUp");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_scrollPageDown");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_scrollLineUp");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_scrollLineDown");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_movePageUp");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_movePageDown");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectPageUp");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectPageDown");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_moveLeft");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_moveRight");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_moveUp");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_moveDown");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_moveLeft2");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_moveRight2");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_moveUp2");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_moveDown2");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectLeft");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectRight");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectUp");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectDown");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectLeft2");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectRight2");
+ NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectUp2");
+ NS_REGISTER_LAST_COMMAND(SelectionMoveCommands, "cmd_selectDown2");
+
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/EditorController.h b/editor/libeditor/EditorController.h
new file mode 100644
index 000000000..e9fb654b8
--- /dev/null
+++ b/editor/libeditor/EditorController.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_EditorController_h
+#define mozilla_EditorController_h
+
+#include "nscore.h"
+
+#define NS_EDITORCONTROLLER_CID \
+{ 0x26fb965c, 0x9de6, 0x11d3, \
+ { 0xbc, 0xcc, 0x0, 0x60, 0xb0, 0xfc, 0x76, 0xbd } }
+
+#define NS_EDITINGCONTROLLER_CID \
+{ 0x2c5a5cdd, 0xe742, 0x4dfe, \
+ { 0x86, 0xb8, 0x06, 0x93, 0x09, 0xbf, 0x6c, 0x91 } }
+
+class nsIControllerCommandTable;
+
+namespace mozilla {
+
+// the editor controller is used for both text widgets, and basic text editing
+// commands in composer. The refCon that gets passed to its commands is an nsIEditor.
+
+class EditorController final
+{
+public:
+ static nsresult RegisterEditorCommands(
+ nsIControllerCommandTable* aCommandTable);
+ static nsresult RegisterEditingCommands(
+ nsIControllerCommandTable* aCommandTable);
+};
+
+} // namespace mozilla
+
+#endif // #ifndef mozilla_EditorController_h
diff --git a/editor/libeditor/EditorEventListener.cpp b/editor/libeditor/EditorEventListener.cpp
new file mode 100644
index 000000000..f90458d3e
--- /dev/null
+++ b/editor/libeditor/EditorEventListener.cpp
@@ -0,0 +1,1215 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=4 sw=2 et tw=78: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "EditorEventListener.h"
+
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc.
+#include "mozilla/EditorBase.h" // for EditorBase, etc.
+#include "mozilla/EventListenerManager.h" // for EventListenerManager
+#include "mozilla/IMEStateManager.h" // for IMEStateManager
+#include "mozilla/Preferences.h" // for Preferences
+#include "mozilla/TextEvents.h" // for WidgetCompositionEvent
+#include "mozilla/dom/Element.h" // for Element
+#include "mozilla/dom/Event.h" // for Event
+#include "mozilla/dom/EventTarget.h" // for EventTarget
+#include "mozilla/dom/Selection.h"
+#include "nsAString.h"
+#include "nsCaret.h" // for nsCaret
+#include "nsDebug.h" // for NS_ENSURE_TRUE, etc.
+#include "nsFocusManager.h" // for nsFocusManager
+#include "nsGkAtoms.h" // for nsGkAtoms, nsGkAtoms::input
+#include "nsIClipboard.h" // for nsIClipboard, etc.
+#include "nsIContent.h" // for nsIContent
+#include "nsIController.h" // for nsIController
+#include "nsID.h"
+#include "mozilla/dom/DOMStringList.h"
+#include "mozilla/dom/DataTransfer.h"
+#include "nsIDOMDocument.h" // for nsIDOMDocument
+#include "nsIDOMDragEvent.h" // for nsIDOMDragEvent
+#include "nsIDOMElement.h" // for nsIDOMElement
+#include "nsIDOMEvent.h" // for nsIDOMEvent
+#include "nsIDOMEventTarget.h" // for nsIDOMEventTarget
+#include "nsIDOMKeyEvent.h" // for nsIDOMKeyEvent
+#include "nsIDOMMouseEvent.h" // for nsIDOMMouseEvent
+#include "nsIDOMNode.h" // for nsIDOMNode
+#include "nsIDocument.h" // for nsIDocument
+#include "nsIEditor.h" // for EditorBase::GetSelection, etc.
+#include "nsIEditorIMESupport.h"
+#include "nsIEditorMailSupport.h" // for nsIEditorMailSupport
+#include "nsIFocusManager.h" // for nsIFocusManager
+#include "nsIFormControl.h" // for nsIFormControl, etc.
+#include "nsIHTMLEditor.h" // for nsIHTMLEditor
+#include "nsINode.h" // for nsINode, ::NODE_IS_EDITABLE, etc.
+#include "nsIPlaintextEditor.h" // for nsIPlaintextEditor, etc.
+#include "nsIPresShell.h" // for nsIPresShell
+#include "nsISelectionController.h" // for nsISelectionController, etc.
+#include "nsITransferable.h" // for kFileMime, kHTMLMime, etc.
+#include "nsIWidget.h" // for nsIWidget
+#include "nsLiteralString.h" // for NS_LITERAL_STRING
+#include "nsPIWindowRoot.h" // for nsPIWindowRoot
+#include "nsPrintfCString.h" // for nsPrintfCString
+#include "nsRange.h"
+#include "nsServiceManagerUtils.h" // for do_GetService
+#include "nsString.h" // for nsAutoString
+#include "nsQueryObject.h" // for do_QueryObject
+#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
+#include "nsContentUtils.h" // for nsContentUtils, etc.
+#include "nsIBidiKeyboard.h" // for nsIBidiKeyboard
+#endif
+
+#include "mozilla/dom/TabParent.h"
+
+class nsPresContext;
+
+namespace mozilla {
+
+using namespace dom;
+
+static void
+DoCommandCallback(Command aCommand, void* aData)
+{
+ nsIDocument* doc = static_cast<nsIDocument*>(aData);
+ nsPIDOMWindowOuter* win = doc->GetWindow();
+ if (!win) {
+ return;
+ }
+ nsCOMPtr<nsPIWindowRoot> root = win->GetTopWindowRoot();
+ if (!root) {
+ return;
+ }
+
+ const char* commandStr = WidgetKeyboardEvent::GetCommandStr(aCommand);
+
+ nsCOMPtr<nsIController> controller;
+ root->GetControllerForCommand(commandStr, getter_AddRefs(controller));
+ if (!controller) {
+ return;
+ }
+
+ bool commandEnabled;
+ nsresult rv = controller->IsCommandEnabled(commandStr, &commandEnabled);
+ NS_ENSURE_SUCCESS_VOID(rv);
+ if (commandEnabled) {
+ controller->DoCommand(commandStr);
+ }
+}
+
+EditorEventListener::EditorEventListener()
+ : mEditorBase(nullptr)
+ , mCommitText(false)
+ , mInTransaction(false)
+ , mMouseDownOrUpConsumedByIME(false)
+#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
+ , mHaveBidiKeyboards(false)
+ , mShouldSwitchTextDirection(false)
+ , mSwitchToRTL(false)
+#endif
+{
+}
+
+EditorEventListener::~EditorEventListener()
+{
+ if (mEditorBase) {
+ NS_WARNING("We're not uninstalled");
+ Disconnect();
+ }
+}
+
+nsresult
+EditorEventListener::Connect(EditorBase* aEditorBase)
+{
+ NS_ENSURE_ARG(aEditorBase);
+
+#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
+ nsIBidiKeyboard* bidiKeyboard = nsContentUtils::GetBidiKeyboard();
+ if (bidiKeyboard) {
+ bool haveBidiKeyboards = false;
+ bidiKeyboard->GetHaveBidiKeyboards(&haveBidiKeyboards);
+ mHaveBidiKeyboards = haveBidiKeyboards;
+ }
+#endif
+
+ mEditorBase = aEditorBase;
+
+ nsresult rv = InstallToEditor();
+ if (NS_FAILED(rv)) {
+ Disconnect();
+ }
+ return rv;
+}
+
+nsresult
+EditorEventListener::InstallToEditor()
+{
+ NS_PRECONDITION(mEditorBase, "The caller must set mEditorBase");
+
+ nsCOMPtr<EventTarget> piTarget = mEditorBase->GetDOMEventTarget();
+ NS_ENSURE_TRUE(piTarget, NS_ERROR_FAILURE);
+
+ // register the event listeners with the listener manager
+ EventListenerManager* elmP = piTarget->GetOrCreateListenerManager();
+ NS_ENSURE_STATE(elmP);
+
+#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
+ elmP->AddEventListenerByType(this,
+ NS_LITERAL_STRING("keydown"),
+ TrustedEventsAtSystemGroupBubble());
+ elmP->AddEventListenerByType(this,
+ NS_LITERAL_STRING("keyup"),
+ TrustedEventsAtSystemGroupBubble());
+#endif
+ elmP->AddEventListenerByType(this,
+ NS_LITERAL_STRING("keypress"),
+ TrustedEventsAtSystemGroupBubble());
+ elmP->AddEventListenerByType(this,
+ NS_LITERAL_STRING("dragenter"),
+ TrustedEventsAtSystemGroupBubble());
+ elmP->AddEventListenerByType(this,
+ NS_LITERAL_STRING("dragover"),
+ TrustedEventsAtSystemGroupBubble());
+ elmP->AddEventListenerByType(this,
+ NS_LITERAL_STRING("dragexit"),
+ TrustedEventsAtSystemGroupBubble());
+ elmP->AddEventListenerByType(this,
+ NS_LITERAL_STRING("drop"),
+ TrustedEventsAtSystemGroupBubble());
+ // XXX We should add the mouse event listeners as system event group.
+ // E.g., web applications cannot prevent middle mouse paste by
+ // preventDefault() of click event at bubble phase.
+ // However, if we do so, all click handlers in any frames and frontend
+ // code need to check if it's editable. It makes easier create new bugs.
+ elmP->AddEventListenerByType(this,
+ NS_LITERAL_STRING("mousedown"),
+ TrustedEventsAtCapture());
+ elmP->AddEventListenerByType(this,
+ NS_LITERAL_STRING("mouseup"),
+ TrustedEventsAtCapture());
+ elmP->AddEventListenerByType(this,
+ NS_LITERAL_STRING("click"),
+ TrustedEventsAtCapture());
+// Focus event doesn't bubble so adding the listener to capturing phase.
+// Make sure this works after bug 235441 gets fixed.
+ elmP->AddEventListenerByType(this,
+ NS_LITERAL_STRING("blur"),
+ TrustedEventsAtCapture());
+ elmP->AddEventListenerByType(this,
+ NS_LITERAL_STRING("focus"),
+ TrustedEventsAtCapture());
+ elmP->AddEventListenerByType(this,
+ NS_LITERAL_STRING("text"),
+ TrustedEventsAtSystemGroupBubble());
+ elmP->AddEventListenerByType(this,
+ NS_LITERAL_STRING("compositionstart"),
+ TrustedEventsAtSystemGroupBubble());
+ elmP->AddEventListenerByType(this,
+ NS_LITERAL_STRING("compositionend"),
+ TrustedEventsAtSystemGroupBubble());
+
+ return NS_OK;
+}
+
+void
+EditorEventListener::Disconnect()
+{
+ if (!mEditorBase) {
+ return;
+ }
+ UninstallFromEditor();
+
+ nsIFocusManager* fm = nsFocusManager::GetFocusManager();
+ if (fm) {
+ nsCOMPtr<nsIDOMElement> domFocus;
+ fm->GetFocusedElement(getter_AddRefs(domFocus));
+ nsCOMPtr<nsINode> focusedElement = do_QueryInterface(domFocus);
+ mozilla::dom::Element* root = mEditorBase->GetRoot();
+ if (focusedElement && root &&
+ nsContentUtils::ContentIsDescendantOf(focusedElement, root)) {
+ // Reset the Selection ancestor limiter and SelectionController state
+ // that EditorBase::InitializeSelection set up.
+ mEditorBase->FinalizeSelection();
+ }
+ }
+
+ mEditorBase = nullptr;
+}
+
+void
+EditorEventListener::UninstallFromEditor()
+{
+ nsCOMPtr<EventTarget> piTarget = mEditorBase->GetDOMEventTarget();
+ if (!piTarget) {
+ return;
+ }
+
+ EventListenerManager* elmP = piTarget->GetOrCreateListenerManager();
+ if (!elmP) {
+ return;
+ }
+
+#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
+ elmP->RemoveEventListenerByType(this,
+ NS_LITERAL_STRING("keydown"),
+ TrustedEventsAtSystemGroupBubble());
+ elmP->RemoveEventListenerByType(this,
+ NS_LITERAL_STRING("keyup"),
+ TrustedEventsAtSystemGroupBubble());
+#endif
+ elmP->RemoveEventListenerByType(this,
+ NS_LITERAL_STRING("keypress"),
+ TrustedEventsAtSystemGroupBubble());
+ elmP->RemoveEventListenerByType(this,
+ NS_LITERAL_STRING("dragenter"),
+ TrustedEventsAtSystemGroupBubble());
+ elmP->RemoveEventListenerByType(this,
+ NS_LITERAL_STRING("dragover"),
+ TrustedEventsAtSystemGroupBubble());
+ elmP->RemoveEventListenerByType(this,
+ NS_LITERAL_STRING("dragexit"),
+ TrustedEventsAtSystemGroupBubble());
+ elmP->RemoveEventListenerByType(this,
+ NS_LITERAL_STRING("drop"),
+ TrustedEventsAtSystemGroupBubble());
+ elmP->RemoveEventListenerByType(this,
+ NS_LITERAL_STRING("mousedown"),
+ TrustedEventsAtCapture());
+ elmP->RemoveEventListenerByType(this,
+ NS_LITERAL_STRING("mouseup"),
+ TrustedEventsAtCapture());
+ elmP->RemoveEventListenerByType(this,
+ NS_LITERAL_STRING("click"),
+ TrustedEventsAtCapture());
+ elmP->RemoveEventListenerByType(this,
+ NS_LITERAL_STRING("blur"),
+ TrustedEventsAtCapture());
+ elmP->RemoveEventListenerByType(this,
+ NS_LITERAL_STRING("focus"),
+ TrustedEventsAtCapture());
+ elmP->RemoveEventListenerByType(this,
+ NS_LITERAL_STRING("text"),
+ TrustedEventsAtSystemGroupBubble());
+ elmP->RemoveEventListenerByType(this,
+ NS_LITERAL_STRING("compositionstart"),
+ TrustedEventsAtSystemGroupBubble());
+ elmP->RemoveEventListenerByType(this,
+ NS_LITERAL_STRING("compositionend"),
+ TrustedEventsAtSystemGroupBubble());
+}
+
+already_AddRefed<nsIPresShell>
+EditorEventListener::GetPresShell()
+{
+ NS_PRECONDITION(mEditorBase,
+ "The caller must check whether this is connected to an editor");
+ return mEditorBase->GetPresShell();
+}
+
+nsPresContext*
+EditorEventListener::GetPresContext()
+{
+ nsCOMPtr<nsIPresShell> presShell = GetPresShell();
+ return presShell ? presShell->GetPresContext() : nullptr;
+}
+
+nsIContent*
+EditorEventListener::GetFocusedRootContent()
+{
+ NS_ENSURE_TRUE(mEditorBase, nullptr);
+
+ nsCOMPtr<nsIContent> focusedContent = mEditorBase->GetFocusedContent();
+ if (!focusedContent) {
+ return nullptr;
+ }
+
+ nsIDocument* composedDoc = focusedContent->GetComposedDoc();
+ NS_ENSURE_TRUE(composedDoc, nullptr);
+
+ if (composedDoc->HasFlag(NODE_IS_EDITABLE)) {
+ return nullptr;
+ }
+
+ return focusedContent;
+}
+
+bool
+EditorEventListener::EditorHasFocus()
+{
+ NS_PRECONDITION(mEditorBase,
+ "The caller must check whether this is connected to an editor");
+ nsCOMPtr<nsIContent> focusedContent = mEditorBase->GetFocusedContent();
+ if (!focusedContent) {
+ return false;
+ }
+ nsIDocument* composedDoc = focusedContent->GetComposedDoc();
+ return !!composedDoc;
+}
+
+NS_IMPL_ISUPPORTS(EditorEventListener, nsIDOMEventListener)
+
+NS_IMETHODIMP
+EditorEventListener::HandleEvent(nsIDOMEvent* aEvent)
+{
+ NS_ENSURE_TRUE(mEditorBase, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIEditor> kungFuDeathGrip = mEditorBase;
+ Unused << kungFuDeathGrip; // mEditorBase is not referred to in this function
+
+ WidgetEvent* internalEvent = aEvent->WidgetEventPtr();
+
+ // Let's handle each event with the message of the internal event of the
+ // coming event. If the DOM event was created with improper interface,
+ // e.g., keydown event is created with |new MouseEvent("keydown", {});|,
+ // its message is always 0. Therefore, we can ban such strange event easy.
+ // However, we need to handle strange "focus" and "blur" event. See the
+ // following code of this switch statement.
+ // NOTE: Each event handler may require specific event interface. Before
+ // calling it, this queries the specific interface. If it would fail,
+ // each event handler would just ignore the event. So, in this method,
+ // you don't need to check if the QI succeeded before each call.
+ switch (internalEvent->mMessage) {
+ // dragenter
+ case eDragEnter: {
+ nsCOMPtr<nsIDOMDragEvent> dragEvent = do_QueryInterface(aEvent);
+ return DragEnter(dragEvent);
+ }
+ // dragover
+ case eDragOver: {
+ nsCOMPtr<nsIDOMDragEvent> dragEvent = do_QueryInterface(aEvent);
+ return DragOver(dragEvent);
+ }
+ // dragexit
+ case eDragExit: {
+ nsCOMPtr<nsIDOMDragEvent> dragEvent = do_QueryInterface(aEvent);
+ return DragExit(dragEvent);
+ }
+ // drop
+ case eDrop: {
+ nsCOMPtr<nsIDOMDragEvent> dragEvent = do_QueryInterface(aEvent);
+ return Drop(dragEvent);
+ }
+#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
+ // keydown
+ case eKeyDown: {
+ nsCOMPtr<nsIDOMKeyEvent> keyEvent = do_QueryInterface(aEvent);
+ return KeyDown(keyEvent);
+ }
+ // keyup
+ case eKeyUp: {
+ nsCOMPtr<nsIDOMKeyEvent> keyEvent = do_QueryInterface(aEvent);
+ return KeyUp(keyEvent);
+ }
+#endif // #ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
+ // keypress
+ case eKeyPress: {
+ nsCOMPtr<nsIDOMKeyEvent> keyEvent = do_QueryInterface(aEvent);
+ return KeyPress(keyEvent);
+ }
+ // mousedown
+ case eMouseDown: {
+ nsCOMPtr<nsIDOMMouseEvent> mouseEvent = do_QueryInterface(aEvent);
+ NS_ENSURE_TRUE(mouseEvent, NS_OK);
+ // EditorEventListener may receive (1) all mousedown, mouseup and click
+ // events, (2) only mousedown event or (3) only mouseup event.
+ // mMouseDownOrUpConsumedByIME is used only for ignoring click event if
+ // preceding mousedown and/or mouseup event is consumed by IME.
+ // Therefore, even if case #2 or case #3 occurs,
+ // mMouseDownOrUpConsumedByIME is true here. Therefore, we should always
+ // overwrite it here.
+ mMouseDownOrUpConsumedByIME = NotifyIMEOfMouseButtonEvent(mouseEvent);
+ return mMouseDownOrUpConsumedByIME ? NS_OK : MouseDown(mouseEvent);
+ }
+ // mouseup
+ case eMouseUp: {
+ nsCOMPtr<nsIDOMMouseEvent> mouseEvent = do_QueryInterface(aEvent);
+ NS_ENSURE_TRUE(mouseEvent, NS_OK);
+ // See above comment in the eMouseDown case, first.
+ // This code assumes that case #1 is occuring. However, if case #3 may
+ // occurs after case #2 and the mousedown is consumed,
+ // mMouseDownOrUpConsumedByIME is true even though EditorEventListener
+ // has not received the preceding mousedown event of this mouseup event.
+ // So, mMouseDownOrUpConsumedByIME may be invalid here. However,
+ // this is not a matter because mMouseDownOrUpConsumedByIME is referred
+ // only by eMouseClick case but click event is fired only in case #1.
+ // So, before a click event is fired, mMouseDownOrUpConsumedByIME is
+ // always initialized in the eMouseDown case if it's referred.
+ if (NotifyIMEOfMouseButtonEvent(mouseEvent)) {
+ mMouseDownOrUpConsumedByIME = true;
+ }
+ return mMouseDownOrUpConsumedByIME ? NS_OK : MouseUp(mouseEvent);
+ }
+ // click
+ case eMouseClick: {
+ nsCOMPtr<nsIDOMMouseEvent> mouseEvent = do_QueryInterface(aEvent);
+ NS_ENSURE_TRUE(mouseEvent, NS_OK);
+ // If the preceding mousedown event or mouseup event was consumed,
+ // editor shouldn't handle this click event.
+ if (mMouseDownOrUpConsumedByIME) {
+ mMouseDownOrUpConsumedByIME = false;
+ mouseEvent->AsEvent()->PreventDefault();
+ return NS_OK;
+ }
+ return MouseClick(mouseEvent);
+ }
+ // focus
+ case eFocus:
+ return Focus(aEvent);
+ // blur
+ case eBlur:
+ return Blur(aEvent);
+ // text
+ case eCompositionChange:
+ return HandleText(aEvent);
+ // compositionstart
+ case eCompositionStart:
+ return HandleStartComposition(aEvent);
+ // compositionend
+ case eCompositionEnd:
+ HandleEndComposition(aEvent);
+ return NS_OK;
+ default:
+ break;
+ }
+
+ nsAutoString eventType;
+ aEvent->GetType(eventType);
+ // We should accept "focus" and "blur" event even if it's synthesized with
+ // wrong interface for compatibility with older Gecko.
+ if (eventType.EqualsLiteral("focus")) {
+ return Focus(aEvent);
+ }
+ if (eventType.EqualsLiteral("blur")) {
+ return Blur(aEvent);
+ }
+#ifdef DEBUG
+ nsPrintfCString assertMessage("Editor doesn't handle \"%s\" event "
+ "because its internal event doesn't have proper message",
+ NS_ConvertUTF16toUTF8(eventType).get());
+ NS_ASSERTION(false, assertMessage.get());
+#endif
+
+ return NS_OK;
+}
+
+#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
+namespace {
+
+// This function is borrowed from Chromium's ImeInput::IsCtrlShiftPressed
+bool IsCtrlShiftPressed(nsIDOMKeyEvent* aEvent, bool& isRTL)
+{
+ // To check if a user is pressing only a control key and a right-shift key
+ // (or a left-shift key), we use the steps below:
+ // 1. Check if a user is pressing a control key and a right-shift key (or
+ // a left-shift key).
+ // 2. If the condition 1 is true, we should check if there are any other
+ // keys pressed at the same time.
+ // To ignore the keys checked in 1, we set their status to 0 before
+ // checking the key status.
+ WidgetKeyboardEvent* keyboardEvent =
+ aEvent->AsEvent()->WidgetEventPtr()->AsKeyboardEvent();
+ MOZ_ASSERT(keyboardEvent,
+ "DOM key event's internal event must be WidgetKeyboardEvent");
+
+ if (!keyboardEvent->IsControl()) {
+ return false;
+ }
+
+ uint32_t location = keyboardEvent->mLocation;
+ if (location == nsIDOMKeyEvent::DOM_KEY_LOCATION_RIGHT) {
+ isRTL = true;
+ } else if (location == nsIDOMKeyEvent::DOM_KEY_LOCATION_LEFT) {
+ isRTL = false;
+ } else {
+ return false;
+ }
+
+ // Scan the key status to find pressed keys. We should abandon changing the
+ // text direction when there are other pressed keys.
+ if (keyboardEvent->IsAlt() || keyboardEvent->IsOS()) {
+ return false;
+ }
+
+ return true;
+}
+
+}
+
+// This logic is mostly borrowed from Chromium's
+// RenderWidgetHostViewWin::OnKeyEvent.
+
+nsresult
+EditorEventListener::KeyUp(nsIDOMKeyEvent* aKeyEvent)
+{
+ NS_ENSURE_TRUE(aKeyEvent, NS_OK);
+
+ if (!mHaveBidiKeyboards) {
+ return NS_OK;
+ }
+
+ uint32_t keyCode = 0;
+ aKeyEvent->GetKeyCode(&keyCode);
+ if ((keyCode == nsIDOMKeyEvent::DOM_VK_SHIFT ||
+ keyCode == nsIDOMKeyEvent::DOM_VK_CONTROL) &&
+ mShouldSwitchTextDirection && mEditorBase->IsPlaintextEditor()) {
+ mEditorBase->SwitchTextDirectionTo(mSwitchToRTL ?
+ nsIPlaintextEditor::eEditorRightToLeft :
+ nsIPlaintextEditor::eEditorLeftToRight);
+ mShouldSwitchTextDirection = false;
+ }
+ return NS_OK;
+}
+
+nsresult
+EditorEventListener::KeyDown(nsIDOMKeyEvent* aKeyEvent)
+{
+ NS_ENSURE_TRUE(aKeyEvent, NS_OK);
+
+ if (!mHaveBidiKeyboards) {
+ return NS_OK;
+ }
+
+ uint32_t keyCode = 0;
+ aKeyEvent->GetKeyCode(&keyCode);
+ if (keyCode == nsIDOMKeyEvent::DOM_VK_SHIFT) {
+ bool switchToRTL;
+ if (IsCtrlShiftPressed(aKeyEvent, switchToRTL)) {
+ mShouldSwitchTextDirection = true;
+ mSwitchToRTL = switchToRTL;
+ }
+ } else if (keyCode != nsIDOMKeyEvent::DOM_VK_CONTROL) {
+ // In case the user presses any other key besides Ctrl and Shift
+ mShouldSwitchTextDirection = false;
+ }
+ return NS_OK;
+}
+#endif
+
+nsresult
+EditorEventListener::KeyPress(nsIDOMKeyEvent* aKeyEvent)
+{
+ NS_ENSURE_TRUE(aKeyEvent, NS_OK);
+
+ if (!mEditorBase->IsAcceptableInputEvent(aKeyEvent->AsEvent())) {
+ return NS_OK;
+ }
+
+ // DOM event handling happens in two passes, the client pass and the system
+ // pass. We do all of our processing in the system pass, to allow client
+ // handlers the opportunity to cancel events and prevent typing in the editor.
+ // If the client pass cancelled the event, defaultPrevented will be true
+ // below.
+
+ bool defaultPrevented;
+ aKeyEvent->AsEvent()->GetDefaultPrevented(&defaultPrevented);
+ if (defaultPrevented) {
+ return NS_OK;
+ }
+
+ nsresult rv = mEditorBase->HandleKeyPressEvent(aKeyEvent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aKeyEvent->AsEvent()->GetDefaultPrevented(&defaultPrevented);
+ if (defaultPrevented) {
+ return NS_OK;
+ }
+
+ if (!ShouldHandleNativeKeyBindings(aKeyEvent)) {
+ return NS_OK;
+ }
+
+ // Now, ask the native key bindings to handle the event.
+ WidgetKeyboardEvent* keyEvent =
+ aKeyEvent->AsEvent()->WidgetEventPtr()->AsKeyboardEvent();
+ MOZ_ASSERT(keyEvent,
+ "DOM key event's internal event must be WidgetKeyboardEvent");
+ nsIWidget* widget = keyEvent->mWidget;
+ // If the event is created by chrome script, the widget is always nullptr.
+ if (!widget) {
+ nsCOMPtr<nsIPresShell> ps = GetPresShell();
+ nsPresContext* pc = ps ? ps->GetPresContext() : nullptr;
+ widget = pc ? pc->GetNearestWidget() : nullptr;
+ NS_ENSURE_TRUE(widget, NS_OK);
+ }
+
+ nsCOMPtr<nsIDocument> doc = mEditorBase->GetDocument();
+ bool handled = widget->ExecuteNativeKeyBinding(
+ nsIWidget::NativeKeyBindingsForRichTextEditor,
+ *keyEvent, DoCommandCallback, doc);
+ if (handled) {
+ aKeyEvent->AsEvent()->PreventDefault();
+ }
+ return NS_OK;
+}
+
+nsresult
+EditorEventListener::MouseClick(nsIDOMMouseEvent* aMouseEvent)
+{
+ // nothing to do if editor isn't editable or clicked on out of the editor.
+ if (mEditorBase->IsReadonly() || mEditorBase->IsDisabled() ||
+ !mEditorBase->IsAcceptableInputEvent(aMouseEvent->AsEvent())) {
+ return NS_OK;
+ }
+
+ // Notifies clicking on editor to IMEStateManager even when the event was
+ // consumed.
+ if (EditorHasFocus()) {
+ nsPresContext* presContext = GetPresContext();
+ if (presContext) {
+ IMEStateManager::OnClickInEditor(presContext, GetFocusedRootContent(),
+ aMouseEvent);
+ }
+ }
+
+ bool preventDefault;
+ nsresult rv = aMouseEvent->AsEvent()->GetDefaultPrevented(&preventDefault);
+ if (NS_FAILED(rv) || preventDefault) {
+ // We're done if 'preventdefault' is true (see for example bug 70698).
+ return rv;
+ }
+
+ // IMEStateManager::OnClickInEditor() may cause anything because it may
+ // set input context. For example, it may cause opening VKB, changing focus
+ // or reflow. So, mEditorBase here might have been gone.
+ if (!mEditorBase) {
+ return NS_OK;
+ }
+
+ // If we got a mouse down inside the editing area, we should force the
+ // IME to commit before we change the cursor position
+ mEditorBase->ForceCompositionEnd();
+
+ int16_t button = -1;
+ aMouseEvent->GetButton(&button);
+ if (button == 1) {
+ return HandleMiddleClickPaste(aMouseEvent);
+ }
+ return NS_OK;
+}
+
+nsresult
+EditorEventListener::HandleMiddleClickPaste(nsIDOMMouseEvent* aMouseEvent)
+{
+ if (!Preferences::GetBool("middlemouse.paste", false)) {
+ // Middle click paste isn't enabled.
+ return NS_OK;
+ }
+
+ // Set the selection to the point under the mouse cursor:
+ nsCOMPtr<nsIDOMNode> parent;
+ if (NS_FAILED(aMouseEvent->GetRangeParent(getter_AddRefs(parent)))) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ int32_t offset = 0;
+ if (NS_FAILED(aMouseEvent->GetRangeOffset(&offset))) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ RefPtr<Selection> selection = mEditorBase->GetSelection();
+ if (selection) {
+ selection->Collapse(parent, offset);
+ }
+
+ // If the ctrl key is pressed, we'll do paste as quotation.
+ // Would've used the alt key, but the kde wmgr treats alt-middle specially.
+ bool ctrlKey = false;
+ aMouseEvent->GetCtrlKey(&ctrlKey);
+
+ nsCOMPtr<nsIEditorMailSupport> mailEditor;
+ if (ctrlKey) {
+ mailEditor = do_QueryObject(mEditorBase);
+ }
+
+ nsresult rv;
+ int32_t clipboard = nsIClipboard::kGlobalClipboard;
+ nsCOMPtr<nsIClipboard> clipboardService =
+ do_GetService("@mozilla.org/widget/clipboard;1", &rv);
+ if (NS_SUCCEEDED(rv)) {
+ bool selectionSupported;
+ rv = clipboardService->SupportsSelectionClipboard(&selectionSupported);
+ if (NS_SUCCEEDED(rv) && selectionSupported) {
+ clipboard = nsIClipboard::kSelectionClipboard;
+ }
+ }
+
+ if (mailEditor) {
+ mailEditor->PasteAsQuotation(clipboard);
+ } else {
+ mEditorBase->Paste(clipboard);
+ }
+
+ // Prevent the event from propagating up to be possibly handled
+ // again by the containing window:
+ aMouseEvent->AsEvent()->StopPropagation();
+ aMouseEvent->AsEvent()->PreventDefault();
+
+ // We processed the event, whether drop/paste succeeded or not
+ return NS_OK;
+}
+
+bool
+EditorEventListener::NotifyIMEOfMouseButtonEvent(
+ nsIDOMMouseEvent* aMouseEvent)
+{
+ if (!EditorHasFocus()) {
+ return false;
+ }
+
+ bool defaultPrevented;
+ nsresult rv = aMouseEvent->AsEvent()->GetDefaultPrevented(&defaultPrevented);
+ NS_ENSURE_SUCCESS(rv, false);
+ if (defaultPrevented) {
+ return false;
+ }
+ nsPresContext* presContext = GetPresContext();
+ NS_ENSURE_TRUE(presContext, false);
+ return IMEStateManager::OnMouseButtonEventInEditor(presContext,
+ GetFocusedRootContent(),
+ aMouseEvent);
+}
+
+nsresult
+EditorEventListener::MouseDown(nsIDOMMouseEvent* aMouseEvent)
+{
+ // FYI: This may be called by HTMLEditorEventListener::MouseDown() even
+ // when the event is not acceptable for committing composition.
+ if (mEditorBase) {
+ mEditorBase->ForceCompositionEnd();
+ }
+ return NS_OK;
+}
+
+nsresult
+EditorEventListener::HandleText(nsIDOMEvent* aTextEvent)
+{
+ if (!mEditorBase->IsAcceptableInputEvent(aTextEvent)) {
+ return NS_OK;
+ }
+
+ // if we are readonly or disabled, then do nothing.
+ if (mEditorBase->IsReadonly() || mEditorBase->IsDisabled()) {
+ return NS_OK;
+ }
+
+ return mEditorBase->UpdateIMEComposition(aTextEvent);
+}
+
+/**
+ * Drag event implementation
+ */
+
+nsresult
+EditorEventListener::DragEnter(nsIDOMDragEvent* aDragEvent)
+{
+ NS_ENSURE_TRUE(aDragEvent, NS_OK);
+
+ nsCOMPtr<nsIPresShell> presShell = GetPresShell();
+ NS_ENSURE_TRUE(presShell, NS_OK);
+
+ if (!mCaret) {
+ mCaret = new nsCaret();
+ mCaret->Init(presShell);
+ mCaret->SetCaretReadOnly(true);
+ // This is to avoid the requirement that the Selection is Collapsed which
+ // it can't be when dragging a selection in the same shell.
+ // See nsCaret::IsVisible().
+ mCaret->SetVisibilityDuringSelection(true);
+ }
+
+ presShell->SetCaret(mCaret);
+
+ return DragOver(aDragEvent);
+}
+
+nsresult
+EditorEventListener::DragOver(nsIDOMDragEvent* aDragEvent)
+{
+ NS_ENSURE_TRUE(aDragEvent, NS_OK);
+
+ nsCOMPtr<nsIDOMNode> parent;
+ bool defaultPrevented;
+ aDragEvent->AsEvent()->GetDefaultPrevented(&defaultPrevented);
+ if (defaultPrevented) {
+ return NS_OK;
+ }
+
+ aDragEvent->GetRangeParent(getter_AddRefs(parent));
+ nsCOMPtr<nsIContent> dropParent = do_QueryInterface(parent);
+ NS_ENSURE_TRUE(dropParent, NS_ERROR_FAILURE);
+
+ if (dropParent->IsEditable() && CanDrop(aDragEvent)) {
+ aDragEvent->AsEvent()->PreventDefault(); // consumed
+
+ if (!mCaret) {
+ return NS_OK;
+ }
+
+ int32_t offset = 0;
+ nsresult rv = aDragEvent->GetRangeOffset(&offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mCaret->SetVisible(true);
+ mCaret->SetCaretPosition(parent, offset);
+
+ return NS_OK;
+ }
+
+ if (!IsFileControlTextBox()) {
+ // This is needed when dropping on an input, to prevent the editor for
+ // the editable parent from receiving the event.
+ aDragEvent->AsEvent()->StopPropagation();
+ }
+
+ if (mCaret) {
+ mCaret->SetVisible(false);
+ }
+ return NS_OK;
+}
+
+void
+EditorEventListener::CleanupDragDropCaret()
+{
+ if (!mCaret) {
+ return;
+ }
+
+ mCaret->SetVisible(false); // hide it, so that it turns off its timer
+
+ nsCOMPtr<nsIPresShell> presShell = GetPresShell();
+ if (presShell) {
+ presShell->RestoreCaret();
+ }
+
+ mCaret->Terminate();
+ mCaret = nullptr;
+}
+
+nsresult
+EditorEventListener::DragExit(nsIDOMDragEvent* aDragEvent)
+{
+ NS_ENSURE_TRUE(aDragEvent, NS_OK);
+
+ CleanupDragDropCaret();
+
+ return NS_OK;
+}
+
+nsresult
+EditorEventListener::Drop(nsIDOMDragEvent* aDragEvent)
+{
+ NS_ENSURE_TRUE(aDragEvent, NS_OK);
+
+ CleanupDragDropCaret();
+
+ bool defaultPrevented;
+ aDragEvent->AsEvent()->GetDefaultPrevented(&defaultPrevented);
+ if (defaultPrevented) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIDOMNode> parent;
+ aDragEvent->GetRangeParent(getter_AddRefs(parent));
+ nsCOMPtr<nsIContent> dropParent = do_QueryInterface(parent);
+ NS_ENSURE_TRUE(dropParent, NS_ERROR_FAILURE);
+
+ if (!dropParent->IsEditable() || !CanDrop(aDragEvent)) {
+ // was it because we're read-only?
+ if ((mEditorBase->IsReadonly() || mEditorBase->IsDisabled()) &&
+ !IsFileControlTextBox()) {
+ // it was decided to "eat" the event as this is the "least surprise"
+ // since someone else handling it might be unintentional and the
+ // user could probably re-drag to be not over the disabled/readonly
+ // editfields if that is what is desired.
+ return aDragEvent->AsEvent()->StopPropagation();
+ }
+ return NS_OK;
+ }
+
+ aDragEvent->AsEvent()->StopPropagation();
+ aDragEvent->AsEvent()->PreventDefault();
+ return mEditorBase->InsertFromDrop(aDragEvent->AsEvent());
+}
+
+bool
+EditorEventListener::CanDrop(nsIDOMDragEvent* aEvent)
+{
+ // if the target doc is read-only, we can't drop
+ if (mEditorBase->IsReadonly() || mEditorBase->IsDisabled()) {
+ return false;
+ }
+
+ nsCOMPtr<nsIDOMDataTransfer> domDataTransfer;
+ aEvent->GetDataTransfer(getter_AddRefs(domDataTransfer));
+ nsCOMPtr<DataTransfer> dataTransfer = do_QueryInterface(domDataTransfer);
+ NS_ENSURE_TRUE(dataTransfer, false);
+
+ nsTArray<nsString> types;
+ dataTransfer->GetTypes(types, *nsContentUtils::GetSystemPrincipal());
+
+ // Plaintext editors only support dropping text. Otherwise, HTML and files
+ // can be dropped as well.
+ if (!types.Contains(NS_LITERAL_STRING(kTextMime)) &&
+ !types.Contains(NS_LITERAL_STRING(kMozTextInternal)) &&
+ (mEditorBase->IsPlaintextEditor() ||
+ (!types.Contains(NS_LITERAL_STRING(kHTMLMime)) &&
+ !types.Contains(NS_LITERAL_STRING(kFileMime))))) {
+ return false;
+ }
+
+ // If there is no source node, this is probably an external drag and the
+ // drop is allowed. The later checks rely on checking if the drag target
+ // is the same as the drag source.
+ nsCOMPtr<nsIDOMNode> sourceNode;
+ dataTransfer->GetMozSourceNode(getter_AddRefs(sourceNode));
+ if (!sourceNode) {
+ return true;
+ }
+
+ // There is a source node, so compare the source documents and this document.
+ // Disallow drops on the same document.
+
+ nsCOMPtr<nsIDOMDocument> domdoc = mEditorBase->GetDOMDocument();
+ NS_ENSURE_TRUE(domdoc, false);
+
+ nsCOMPtr<nsIDOMDocument> sourceDoc;
+ nsresult rv = sourceNode->GetOwnerDocument(getter_AddRefs(sourceDoc));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ // If the source and the dest are not same document, allow to drop it always.
+ if (domdoc != sourceDoc) {
+ return true;
+ }
+
+ // If the source node is a remote browser, treat this as coming from a
+ // different document and allow the drop.
+ nsCOMPtr<nsIContent> sourceContent = do_QueryInterface(sourceNode);
+ TabParent* tp = TabParent::GetFrom(sourceContent);
+ if (tp) {
+ return true;
+ }
+
+ RefPtr<Selection> selection = mEditorBase->GetSelection();
+ if (!selection) {
+ return false;
+ }
+
+ // If selection is collapsed, allow to drop it always.
+ if (selection->Collapsed()) {
+ return true;
+ }
+
+ nsCOMPtr<nsIDOMNode> parent;
+ rv = aEvent->GetRangeParent(getter_AddRefs(parent));
+ if (NS_FAILED(rv) || !parent) {
+ return false;
+ }
+
+ int32_t offset = 0;
+ rv = aEvent->GetRangeOffset(&offset);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ int32_t rangeCount;
+ rv = selection->GetRangeCount(&rangeCount);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ for (int32_t i = 0; i < rangeCount; i++) {
+ RefPtr<nsRange> range = selection->GetRangeAt(i);
+ if (!range) {
+ // Don't bail yet, iterate through them all
+ continue;
+ }
+
+ bool inRange = true;
+ range->IsPointInRange(parent, offset, &inRange);
+ if (inRange) {
+ // Okay, now you can bail, we are over the orginal selection
+ return false;
+ }
+ }
+ return true;
+}
+
+nsresult
+EditorEventListener::HandleStartComposition(nsIDOMEvent* aCompositionEvent)
+{
+ if (!mEditorBase->IsAcceptableInputEvent(aCompositionEvent)) {
+ return NS_OK;
+ }
+ WidgetCompositionEvent* compositionStart =
+ aCompositionEvent->WidgetEventPtr()->AsCompositionEvent();
+ return mEditorBase->BeginIMEComposition(compositionStart);
+}
+
+void
+EditorEventListener::HandleEndComposition(nsIDOMEvent* aCompositionEvent)
+{
+ if (!mEditorBase->IsAcceptableInputEvent(aCompositionEvent)) {
+ return;
+ }
+
+ mEditorBase->EndIMEComposition();
+}
+
+nsresult
+EditorEventListener::Focus(nsIDOMEvent* aEvent)
+{
+ NS_ENSURE_TRUE(aEvent, NS_OK);
+
+ // Don't turn on selection and caret when the editor is disabled.
+ if (mEditorBase->IsDisabled()) {
+ return NS_OK;
+ }
+
+ // Spell check a textarea the first time that it is focused.
+ SpellCheckIfNeeded();
+ if (!mEditorBase) {
+ // In e10s, this can cause us to flush notifications, which can destroy
+ // the node we're about to focus.
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIDOMEventTarget> target;
+ aEvent->GetTarget(getter_AddRefs(target));
+ nsCOMPtr<nsINode> node = do_QueryInterface(target);
+ NS_ENSURE_TRUE(node, NS_ERROR_UNEXPECTED);
+
+ // If the target is a document node but it's not editable, we should ignore
+ // it because actual focused element's event is going to come.
+ if (node->IsNodeOfType(nsINode::eDOCUMENT) &&
+ !node->HasFlag(NODE_IS_EDITABLE)) {
+ return NS_OK;
+ }
+
+ if (node->IsNodeOfType(nsINode::eCONTENT)) {
+ // XXX If the focus event target is a form control in contenteditable
+ // element, perhaps, the parent HTML editor should do nothing by this
+ // handler. However, FindSelectionRoot() returns the root element of the
+ // contenteditable editor. So, the editableRoot value is invalid for
+ // the plain text editor, and it will be set to the wrong limiter of
+ // the selection. However, fortunately, actual bugs are not found yet.
+ nsCOMPtr<nsIContent> editableRoot = mEditorBase->FindSelectionRoot(node);
+
+ // make sure that the element is really focused in case an earlier
+ // listener in the chain changed the focus.
+ if (editableRoot) {
+ nsIFocusManager* fm = nsFocusManager::GetFocusManager();
+ NS_ENSURE_TRUE(fm, NS_OK);
+
+ nsCOMPtr<nsIDOMElement> element;
+ fm->GetFocusedElement(getter_AddRefs(element));
+ if (!element) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIDOMEventTarget> originalTarget;
+ aEvent->GetOriginalTarget(getter_AddRefs(originalTarget));
+
+ nsCOMPtr<nsIContent> originalTargetAsContent =
+ do_QueryInterface(originalTarget);
+ nsCOMPtr<nsIContent> focusedElementAsContent =
+ do_QueryInterface(element);
+
+ if (!SameCOMIdentity(
+ focusedElementAsContent->FindFirstNonChromeOnlyAccessContent(),
+ originalTargetAsContent->FindFirstNonChromeOnlyAccessContent())) {
+ return NS_OK;
+ }
+ }
+ }
+
+ mEditorBase->OnFocus(target);
+
+ nsCOMPtr<nsIPresShell> ps = GetPresShell();
+ NS_ENSURE_TRUE(ps, NS_OK);
+ nsCOMPtr<nsIContent> focusedContent = mEditorBase->GetFocusedContentForIME();
+ IMEStateManager::OnFocusInEditor(ps->GetPresContext(), focusedContent,
+ mEditorBase);
+
+ return NS_OK;
+}
+
+nsresult
+EditorEventListener::Blur(nsIDOMEvent* aEvent)
+{
+ NS_ENSURE_TRUE(aEvent, NS_OK);
+
+ // check if something else is focused. If another element is focused, then
+ // we should not change the selection.
+ nsIFocusManager* fm = nsFocusManager::GetFocusManager();
+ NS_ENSURE_TRUE(fm, NS_OK);
+
+ nsCOMPtr<nsIDOMElement> element;
+ fm->GetFocusedElement(getter_AddRefs(element));
+ if (!element) {
+ mEditorBase->FinalizeSelection();
+ }
+ return NS_OK;
+}
+
+void
+EditorEventListener::SpellCheckIfNeeded()
+{
+ // If the spell check skip flag is still enabled from creation time,
+ // disable it because focused editors are allowed to spell check.
+ uint32_t currentFlags = 0;
+ mEditorBase->GetFlags(&currentFlags);
+ if(currentFlags & nsIPlaintextEditor::eEditorSkipSpellCheck) {
+ currentFlags ^= nsIPlaintextEditor::eEditorSkipSpellCheck;
+ mEditorBase->SetFlags(currentFlags);
+ }
+}
+
+bool
+EditorEventListener::IsFileControlTextBox()
+{
+ Element* root = mEditorBase->GetRoot();
+ if (!root || !root->ChromeOnlyAccess()) {
+ return false;
+ }
+ nsIContent* parent = root->FindFirstNonChromeOnlyAccessContent();
+ if (!parent || !parent->IsHTMLElement(nsGkAtoms::input)) {
+ return false;
+ }
+ nsCOMPtr<nsIFormControl> formControl = do_QueryInterface(parent);
+ return formControl->GetType() == NS_FORM_INPUT_FILE;
+}
+
+bool
+EditorEventListener::ShouldHandleNativeKeyBindings(nsIDOMKeyEvent* aKeyEvent)
+{
+ // Only return true if the target of the event is a desendant of the active
+ // editing host in order to match the similar decision made in
+ // nsXBLWindowKeyHandler.
+ // Note that IsAcceptableInputEvent doesn't check for the active editing
+ // host for keyboard events, otherwise this check would have been
+ // unnecessary. IsAcceptableInputEvent currently makes a similar check for
+ // mouse events.
+
+ nsCOMPtr<nsIDOMEventTarget> target;
+ aKeyEvent->AsEvent()->GetTarget(getter_AddRefs(target));
+ nsCOMPtr<nsIContent> targetContent = do_QueryInterface(target);
+ if (!targetContent) {
+ return false;
+ }
+
+ nsCOMPtr<nsIHTMLEditor> htmlEditor =
+ do_QueryInterface(static_cast<nsIEditor*>(mEditorBase));
+ if (!htmlEditor) {
+ return false;
+ }
+
+ nsCOMPtr<nsIDocument> doc = mEditorBase->GetDocument();
+ if (doc->HasFlag(NODE_IS_EDITABLE)) {
+ // Don't need to perform any checks in designMode documents.
+ return true;
+ }
+
+ nsIContent* editingHost = htmlEditor->GetActiveEditingHost();
+ if (!editingHost) {
+ return false;
+ }
+
+ return nsContentUtils::ContentIsDescendantOf(targetContent, editingHost);
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/EditorEventListener.h b/editor/libeditor/EditorEventListener.h
new file mode 100644
index 000000000..505b711c7
--- /dev/null
+++ b/editor/libeditor/EditorEventListener.h
@@ -0,0 +1,102 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef EditorEventListener_h
+#define EditorEventListener_h
+
+#include "nsCOMPtr.h"
+#include "nsError.h"
+#include "nsIDOMEventListener.h"
+#include "nsISupportsImpl.h"
+#include "nscore.h"
+
+class nsCaret;
+class nsIContent;
+class nsIDOMDragEvent;
+class nsIDOMEvent;
+class nsIDOMKeyEvent;
+class nsIDOMMouseEvent;
+class nsIPresShell;
+class nsPresContext;
+
+// X.h defines KeyPress
+#ifdef KeyPress
+#undef KeyPress
+#endif
+
+#ifdef XP_WIN
+// On Windows, we support switching the text direction by pressing Ctrl+Shift
+#define HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
+#endif
+
+namespace mozilla {
+
+class EditorBase;
+
+class EditorEventListener : public nsIDOMEventListener
+{
+public:
+ EditorEventListener();
+
+ virtual nsresult Connect(EditorBase* aEditorBase);
+
+ void Disconnect();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIDOMEVENTLISTENER
+
+ void SpellCheckIfNeeded();
+
+protected:
+ virtual ~EditorEventListener();
+
+ nsresult InstallToEditor();
+ void UninstallFromEditor();
+
+#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
+ nsresult KeyDown(nsIDOMKeyEvent* aKeyEvent);
+ nsresult KeyUp(nsIDOMKeyEvent* aKeyEvent);
+#endif
+ nsresult KeyPress(nsIDOMKeyEvent* aKeyEvent);
+ nsresult HandleText(nsIDOMEvent* aTextEvent);
+ nsresult HandleStartComposition(nsIDOMEvent* aCompositionEvent);
+ void HandleEndComposition(nsIDOMEvent* aCompositionEvent);
+ virtual nsresult MouseDown(nsIDOMMouseEvent* aMouseEvent);
+ virtual nsresult MouseUp(nsIDOMMouseEvent* aMouseEvent) { return NS_OK; }
+ virtual nsresult MouseClick(nsIDOMMouseEvent* aMouseEvent);
+ nsresult Focus(nsIDOMEvent* aEvent);
+ nsresult Blur(nsIDOMEvent* aEvent);
+ nsresult DragEnter(nsIDOMDragEvent* aDragEvent);
+ nsresult DragOver(nsIDOMDragEvent* aDragEvent);
+ nsresult DragExit(nsIDOMDragEvent* aDragEvent);
+ nsresult Drop(nsIDOMDragEvent* aDragEvent);
+
+ bool CanDrop(nsIDOMDragEvent* aEvent);
+ void CleanupDragDropCaret();
+ already_AddRefed<nsIPresShell> GetPresShell();
+ nsPresContext* GetPresContext();
+ nsIContent* GetFocusedRootContent();
+ // Returns true if IME consumes the mouse event.
+ bool NotifyIMEOfMouseButtonEvent(nsIDOMMouseEvent* aMouseEvent);
+ bool EditorHasFocus();
+ bool IsFileControlTextBox();
+ bool ShouldHandleNativeKeyBindings(nsIDOMKeyEvent* aKeyEvent);
+ nsresult HandleMiddleClickPaste(nsIDOMMouseEvent* aMouseEvent);
+
+ EditorBase* mEditorBase; // weak
+ RefPtr<nsCaret> mCaret;
+ bool mCommitText;
+ bool mInTransaction;
+ bool mMouseDownOrUpConsumedByIME;
+#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
+ bool mHaveBidiKeyboards;
+ bool mShouldSwitchTextDirection;
+ bool mSwitchToRTL;
+#endif
+};
+
+} // namespace mozilla
+
+#endif // #ifndef EditorEventListener_h
diff --git a/editor/libeditor/EditorUtils.cpp b/editor/libeditor/EditorUtils.cpp
new file mode 100644
index 000000000..4ec60c830
--- /dev/null
+++ b/editor/libeditor/EditorUtils.cpp
@@ -0,0 +1,227 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/EditorUtils.h"
+
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/dom/Selection.h"
+#include "nsComponentManagerUtils.h"
+#include "nsError.h"
+#include "nsIClipboardDragDropHookList.h"
+// hooks
+#include "nsIClipboardDragDropHooks.h"
+#include "nsIContent.h"
+#include "nsIContentIterator.h"
+#include "nsIDOMDocument.h"
+#include "nsIDocShell.h"
+#include "nsIDocument.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsINode.h"
+#include "nsISimpleEnumerator.h"
+
+class nsISupports;
+class nsRange;
+
+namespace mozilla {
+
+using namespace dom;
+
+/******************************************************************************
+ * AutoSelectionRestorer
+ *****************************************************************************/
+
+AutoSelectionRestorer::AutoSelectionRestorer(
+ Selection* aSelection,
+ EditorBase* aEditorBase
+ MOZ_GUARD_OBJECT_NOTIFIER_PARAM_IN_IMPL)
+ : mEditorBase(nullptr)
+{
+ MOZ_GUARD_OBJECT_NOTIFIER_INIT;
+ if (NS_WARN_IF(!aSelection) || NS_WARN_IF(!aEditorBase)) {
+ return;
+ }
+ if (aEditorBase->ArePreservingSelection()) {
+ // We already have initialized mSavedSel, so this must be nested call.
+ return;
+ }
+ mSelection = aSelection;
+ mEditorBase = aEditorBase;
+ mEditorBase->PreserveSelectionAcrossActions(mSelection);
+}
+
+AutoSelectionRestorer::~AutoSelectionRestorer()
+{
+ NS_ASSERTION(!mSelection || mEditorBase,
+ "mEditorBase should be non-null when mSelection is");
+ // mSelection will be null if this was nested call.
+ if (mSelection && mEditorBase->ArePreservingSelection()) {
+ mEditorBase->RestorePreservedSelection(mSelection);
+ }
+}
+
+void
+AutoSelectionRestorer::Abort()
+{
+ NS_ASSERTION(!mSelection || mEditorBase,
+ "mEditorBase should be non-null when mSelection is");
+ if (mSelection) {
+ mEditorBase->StopPreservingSelection();
+ }
+}
+
+/******************************************************************************
+ * some helper classes for iterating the dom tree
+ *****************************************************************************/
+
+DOMIterator::DOMIterator(nsINode& aNode MOZ_GUARD_OBJECT_NOTIFIER_PARAM_IN_IMPL)
+{
+ MOZ_GUARD_OBJECT_NOTIFIER_INIT;
+ mIter = NS_NewContentIterator();
+ DebugOnly<nsresult> rv = mIter->Init(&aNode);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+}
+
+nsresult
+DOMIterator::Init(nsRange& aRange)
+{
+ mIter = NS_NewContentIterator();
+ return mIter->Init(&aRange);
+}
+
+DOMIterator::DOMIterator(MOZ_GUARD_OBJECT_NOTIFIER_ONLY_PARAM_IN_IMPL)
+{
+ MOZ_GUARD_OBJECT_NOTIFIER_INIT;
+}
+
+DOMIterator::~DOMIterator()
+{
+}
+
+void
+DOMIterator::AppendList(const BoolDomIterFunctor& functor,
+ nsTArray<OwningNonNull<nsINode>>& arrayOfNodes) const
+{
+ // Iterate through dom and build list
+ for (; !mIter->IsDone(); mIter->Next()) {
+ nsCOMPtr<nsINode> node = mIter->GetCurrentNode();
+
+ if (functor(node)) {
+ arrayOfNodes.AppendElement(*node);
+ }
+ }
+}
+
+DOMSubtreeIterator::DOMSubtreeIterator(
+ MOZ_GUARD_OBJECT_NOTIFIER_ONLY_PARAM_IN_IMPL)
+ : DOMIterator(MOZ_GUARD_OBJECT_NOTIFIER_ONLY_PARAM_TO_PARENT)
+{
+}
+
+nsresult
+DOMSubtreeIterator::Init(nsRange& aRange)
+{
+ mIter = NS_NewContentSubtreeIterator();
+ return mIter->Init(&aRange);
+}
+
+DOMSubtreeIterator::~DOMSubtreeIterator()
+{
+}
+
+/******************************************************************************
+ * some general purpose editor utils
+ *****************************************************************************/
+
+bool
+EditorUtils::IsDescendantOf(nsINode* aNode,
+ nsINode* aParent,
+ int32_t* aOffset)
+{
+ MOZ_ASSERT(aNode && aParent);
+ if (aNode == aParent) {
+ return false;
+ }
+
+ for (nsCOMPtr<nsINode> node = aNode; node; node = node->GetParentNode()) {
+ if (node->GetParentNode() == aParent) {
+ if (aOffset) {
+ *aOffset = aParent->IndexOf(node);
+ }
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool
+EditorUtils::IsDescendantOf(nsIDOMNode* aNode,
+ nsIDOMNode* aParent,
+ int32_t* aOffset)
+{
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ nsCOMPtr<nsINode> parent = do_QueryInterface(aParent);
+ NS_ENSURE_TRUE(node && parent, false);
+ return IsDescendantOf(node, parent, aOffset);
+}
+
+bool
+EditorUtils::IsLeafNode(nsIDOMNode* aNode)
+{
+ bool hasChildren = false;
+ if (aNode)
+ aNode->HasChildNodes(&hasChildren);
+ return !hasChildren;
+}
+
+/******************************************************************************
+ * utility methods for drag/drop/copy/paste hooks
+ *****************************************************************************/
+
+nsresult
+EditorHookUtils::GetHookEnumeratorFromDocument(nsIDOMDocument* aDoc,
+ nsISimpleEnumerator** aResult)
+{
+ nsCOMPtr<nsIDocument> doc = do_QueryInterface(aDoc);
+ NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIDocShell> docShell = doc->GetDocShell();
+ nsCOMPtr<nsIClipboardDragDropHookList> hookObj = do_GetInterface(docShell);
+ NS_ENSURE_TRUE(hookObj, NS_ERROR_FAILURE);
+
+ return hookObj->GetHookEnumerator(aResult);
+}
+
+bool
+EditorHookUtils::DoInsertionHook(nsIDOMDocument* aDoc,
+ nsIDOMEvent* aDropEvent,
+ nsITransferable *aTrans)
+{
+ nsCOMPtr<nsISimpleEnumerator> enumerator;
+ GetHookEnumeratorFromDocument(aDoc, getter_AddRefs(enumerator));
+ NS_ENSURE_TRUE(enumerator, true);
+
+ bool hasMoreHooks = false;
+ while (NS_SUCCEEDED(enumerator->HasMoreElements(&hasMoreHooks)) &&
+ hasMoreHooks) {
+ nsCOMPtr<nsISupports> isupp;
+ if (NS_FAILED(enumerator->GetNext(getter_AddRefs(isupp)))) {
+ break;
+ }
+
+ nsCOMPtr<nsIClipboardDragDropHooks> override = do_QueryInterface(isupp);
+ if (override) {
+ bool doInsert = true;
+ DebugOnly<nsresult> hookResult =
+ override->OnPasteOrDrop(aDropEvent, aTrans, &doInsert);
+ NS_ASSERTION(NS_SUCCEEDED(hookResult), "hook failure in OnPasteOrDrop");
+ NS_ENSURE_TRUE(doInsert, false);
+ }
+ }
+
+ return true;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/EditorUtils.h b/editor/libeditor/EditorUtils.h
new file mode 100644
index 000000000..34286da8a
--- /dev/null
+++ b/editor/libeditor/EditorUtils.h
@@ -0,0 +1,315 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+#ifndef mozilla_EditorUtils_h
+#define mozilla_EditorUtils_h
+
+#include "mozilla/EditorBase.h"
+#include "mozilla/GuardObjects.h"
+#include "nsCOMPtr.h"
+#include "nsDebug.h"
+#include "nsIDOMNode.h"
+#include "nsIEditor.h"
+#include "nscore.h"
+
+class nsIAtom;
+class nsIContentIterator;
+class nsIDOMDocument;
+class nsIDOMEvent;
+class nsISimpleEnumerator;
+class nsITransferable;
+class nsRange;
+
+namespace mozilla {
+template <class T> class OwningNonNull;
+
+namespace dom {
+class Selection;
+} // namespace dom
+
+/***************************************************************************
+ * stack based helper class for batching a collection of txns inside a
+ * placeholder txn.
+ */
+class MOZ_RAII AutoPlaceHolderBatch
+{
+private:
+ nsCOMPtr<nsIEditor> mEditor;
+ MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
+
+public:
+ AutoPlaceHolderBatch(nsIEditor* aEditor,
+ nsIAtom* aAtom
+ MOZ_GUARD_OBJECT_NOTIFIER_PARAM)
+ : mEditor(aEditor)
+ {
+ MOZ_GUARD_OBJECT_NOTIFIER_INIT;
+ if (mEditor) {
+ mEditor->BeginPlaceHolderTransaction(aAtom);
+ }
+ }
+ ~AutoPlaceHolderBatch()
+ {
+ if (mEditor) {
+ mEditor->EndPlaceHolderTransaction();
+ }
+ }
+};
+
+/***************************************************************************
+ * stack based helper class for batching a collection of txns.
+ * Note: I changed this to use placeholder batching so that we get
+ * proper selection save/restore across undo/redo.
+ */
+class MOZ_RAII AutoEditBatch final : public AutoPlaceHolderBatch
+{
+private:
+ MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
+
+public:
+ explicit AutoEditBatch(nsIEditor* aEditor
+ MOZ_GUARD_OBJECT_NOTIFIER_PARAM)
+ : AutoPlaceHolderBatch(aEditor, nullptr)
+ {
+ MOZ_GUARD_OBJECT_NOTIFIER_INIT;
+ }
+ ~AutoEditBatch() {}
+};
+
+/***************************************************************************
+ * stack based helper class for saving/restoring selection. Note that this
+ * assumes that the nodes involved are still around afterwards!
+ */
+class MOZ_RAII AutoSelectionRestorer final
+{
+private:
+ // Ref-counted reference to the selection that we are supposed to restore.
+ RefPtr<dom::Selection> mSelection;
+ EditorBase* mEditorBase; // Non-owning ref to EditorBase.
+ MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
+
+public:
+ /**
+ * Constructor responsible for remembering all state needed to restore
+ * aSelection.
+ */
+ AutoSelectionRestorer(dom::Selection* aSelection,
+ EditorBase* aEditorBase
+ MOZ_GUARD_OBJECT_NOTIFIER_PARAM);
+
+ /**
+ * Destructor restores mSelection to its former state
+ */
+ ~AutoSelectionRestorer();
+
+ /**
+ * Abort() cancels to restore the selection.
+ */
+ void Abort();
+};
+
+/***************************************************************************
+ * stack based helper class for StartOperation()/EndOperation() sandwich
+ */
+class MOZ_RAII AutoRules final
+{
+public:
+ AutoRules(EditorBase* aEditorBase, EditAction aAction,
+ nsIEditor::EDirection aDirection
+ MOZ_GUARD_OBJECT_NOTIFIER_PARAM)
+ : mEditorBase(aEditorBase)
+ , mDoNothing(false)
+ {
+ MOZ_GUARD_OBJECT_NOTIFIER_INIT;
+ // mAction will already be set if this is nested call
+ if (mEditorBase && !mEditorBase->mAction) {
+ mEditorBase->StartOperation(aAction, aDirection);
+ } else {
+ mDoNothing = true; // nested calls will end up here
+ }
+ }
+
+ ~AutoRules()
+ {
+ if (mEditorBase && !mDoNothing) {
+ mEditorBase->EndOperation();
+ }
+ }
+
+protected:
+ EditorBase* mEditorBase;
+ bool mDoNothing;
+ MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
+};
+
+/***************************************************************************
+ * stack based helper class for turning off active selection adjustment
+ * by low level transactions
+ */
+class MOZ_RAII AutoTransactionsConserveSelection final
+{
+public:
+ explicit AutoTransactionsConserveSelection(EditorBase* aEditorBase
+ MOZ_GUARD_OBJECT_NOTIFIER_PARAM)
+ : mEditorBase(aEditorBase)
+ , mOldState(true)
+ {
+ MOZ_GUARD_OBJECT_NOTIFIER_INIT;
+ if (mEditorBase) {
+ mOldState = mEditorBase->GetShouldTxnSetSelection();
+ mEditorBase->SetShouldTxnSetSelection(false);
+ }
+ }
+
+ ~AutoTransactionsConserveSelection()
+ {
+ if (mEditorBase) {
+ mEditorBase->SetShouldTxnSetSelection(mOldState);
+ }
+ }
+
+protected:
+ EditorBase* mEditorBase;
+ bool mOldState;
+ MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
+};
+
+/***************************************************************************
+ * stack based helper class for batching reflow and paint requests.
+ */
+class MOZ_RAII AutoUpdateViewBatch final
+{
+public:
+ explicit AutoUpdateViewBatch(EditorBase* aEditorBase
+ MOZ_GUARD_OBJECT_NOTIFIER_PARAM)
+ : mEditorBase(aEditorBase)
+ {
+ MOZ_GUARD_OBJECT_NOTIFIER_INIT;
+ NS_ASSERTION(mEditorBase, "null mEditorBase pointer!");
+
+ if (mEditorBase) {
+ mEditorBase->BeginUpdateViewBatch();
+ }
+ }
+
+ ~AutoUpdateViewBatch()
+ {
+ if (mEditorBase) {
+ mEditorBase->EndUpdateViewBatch();
+ }
+ }
+
+protected:
+ EditorBase* mEditorBase;
+ MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
+};
+
+/******************************************************************************
+ * some helper classes for iterating the dom tree
+ *****************************************************************************/
+
+class BoolDomIterFunctor
+{
+public:
+ virtual bool operator()(nsINode* aNode) const = 0;
+};
+
+class MOZ_RAII DOMIterator
+{
+public:
+ explicit DOMIterator(MOZ_GUARD_OBJECT_NOTIFIER_ONLY_PARAM);
+
+ explicit DOMIterator(nsINode& aNode MOZ_GUARD_OBJECT_NOTIFIER_PARAM);
+ virtual ~DOMIterator();
+
+ nsresult Init(nsRange& aRange);
+
+ void AppendList(
+ const BoolDomIterFunctor& functor,
+ nsTArray<mozilla::OwningNonNull<nsINode>>& arrayOfNodes) const;
+
+protected:
+ nsCOMPtr<nsIContentIterator> mIter;
+ MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
+};
+
+class MOZ_RAII DOMSubtreeIterator final : public DOMIterator
+{
+public:
+ explicit DOMSubtreeIterator(MOZ_GUARD_OBJECT_NOTIFIER_ONLY_PARAM);
+ virtual ~DOMSubtreeIterator();
+
+ nsresult Init(nsRange& aRange);
+};
+
+class TrivialFunctor final : public BoolDomIterFunctor
+{
+public:
+ // Used to build list of all nodes iterator covers
+ virtual bool operator()(nsINode* aNode) const
+ {
+ return true;
+ }
+};
+
+/******************************************************************************
+ * general dom point utility struct
+ *****************************************************************************/
+struct MOZ_STACK_CLASS EditorDOMPoint final
+{
+ nsCOMPtr<nsINode> node;
+ int32_t offset;
+
+ EditorDOMPoint()
+ : node(nullptr)
+ , offset(-1)
+ {}
+ EditorDOMPoint(nsINode* aNode, int32_t aOffset)
+ : node(aNode)
+ , offset(aOffset)
+ {}
+ EditorDOMPoint(nsIDOMNode* aNode, int32_t aOffset)
+ : node(do_QueryInterface(aNode))
+ , offset(aOffset)
+ {}
+
+ void SetPoint(nsINode* aNode, int32_t aOffset)
+ {
+ node = aNode;
+ offset = aOffset;
+ }
+ void SetPoint(nsIDOMNode* aNode, int32_t aOffset)
+ {
+ node = do_QueryInterface(aNode);
+ offset = aOffset;
+ }
+};
+
+class EditorUtils final
+{
+public:
+ static bool IsDescendantOf(nsINode* aNode, nsINode* aParent,
+ int32_t* aOffset = 0);
+ static bool IsDescendantOf(nsIDOMNode* aNode, nsIDOMNode* aParent,
+ int32_t* aOffset = 0);
+ static bool IsLeafNode(nsIDOMNode* aNode);
+};
+
+class EditorHookUtils final
+{
+public:
+ static bool DoInsertionHook(nsIDOMDocument* aDoc, nsIDOMEvent* aEvent,
+ nsITransferable* aTrans);
+
+private:
+ static nsresult GetHookEnumeratorFromDocument(
+ nsIDOMDocument*aDoc,
+ nsISimpleEnumerator** aEnumerator);
+};
+
+} // namespace mozilla
+
+#endif // #ifndef mozilla_EditorUtils_h
diff --git a/editor/libeditor/EditorUtils.js b/editor/libeditor/EditorUtils.js
new file mode 100644
index 000000000..2959f67ab
--- /dev/null
+++ b/editor/libeditor/EditorUtils.js
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+"use strict";
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const EDITORUTILS_CID = Components.ID('{12e63991-86ac-4dff-bb1a-703495d67d17}');
+
+function EditorUtils() {
+}
+
+EditorUtils.prototype = {
+ classID: EDITORUTILS_CID,
+ QueryInterface: XPCOMUtils.generateQI([ Ci.nsIEditorUtils ]),
+
+ slurpBlob(aBlob, aScope, aListener) {
+ let reader = new aScope.FileReader();
+ reader.addEventListener("load", (event) => {
+ aListener.onResult(event.target.result);
+ });
+ reader.addEventListener("error", (event) => {
+ aListener.onError(event.target.error.message);
+ });
+
+ reader.readAsBinaryString(aBlob);
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([EditorUtils]);
diff --git a/editor/libeditor/EditorUtils.manifest b/editor/libeditor/EditorUtils.manifest
new file mode 100644
index 000000000..5c479e0b2
--- /dev/null
+++ b/editor/libeditor/EditorUtils.manifest
@@ -0,0 +1,2 @@
+component {12e63991-86ac-4dff-bb1a-703495d67d17} EditorUtils.js
+contract @mozilla.org/editor-utils;1 {12e63991-86ac-4dff-bb1a-703495d67d17}
diff --git a/editor/libeditor/HTMLAbsPositionEditor.cpp b/editor/libeditor/HTMLAbsPositionEditor.cpp
new file mode 100644
index 000000000..670da78ae
--- /dev/null
+++ b/editor/libeditor/HTMLAbsPositionEditor.cpp
@@ -0,0 +1,682 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/HTMLEditor.h"
+
+#include <math.h>
+
+#include "HTMLEditorObjectResizerUtils.h"
+#include "HTMLEditRules.h"
+#include "HTMLEditUtils.h"
+#include "TextEditUtils.h"
+#include "mozilla/EditorUtils.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/mozalloc.h"
+#include "nsAString.h"
+#include "nsAlgorithm.h"
+#include "nsCOMPtr.h"
+#include "nsComputedDOMStyle.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsGkAtoms.h"
+#include "nsIContent.h"
+#include "nsROCSSPrimitiveValue.h"
+#include "nsIDOMCSSStyleDeclaration.h"
+#include "nsIDOMElement.h"
+#include "nsIDOMEventListener.h"
+#include "nsIDOMEventTarget.h"
+#include "nsIDOMNode.h"
+#include "nsDOMCSSRGBColor.h"
+#include "nsIDOMWindow.h"
+#include "nsIEditor.h"
+#include "nsIEditRules.h"
+#include "nsIHTMLEditor.h"
+#include "nsIHTMLObjectResizer.h"
+#include "nsINode.h"
+#include "nsIPresShell.h"
+#include "nsISupportsImpl.h"
+#include "nsISupportsUtils.h"
+#include "nsLiteralString.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nscore.h"
+#include <algorithm>
+
+namespace mozilla {
+
+using namespace dom;
+
+#define BLACK_BG_RGB_TRIGGER 0xd0
+
+NS_IMETHODIMP
+HTMLEditor::AbsolutePositionSelection(bool aEnabled)
+{
+ AutoEditBatch beginBatching(this);
+ AutoRules beginRulesSniffing(this,
+ aEnabled ? EditAction::setAbsolutePosition :
+ EditAction::removeAbsolutePosition,
+ nsIEditor::eNext);
+
+ // the line below does not match the code; should it be removed?
+ // Find out if the selection is collapsed:
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+
+ TextRulesInfo ruleInfo(aEnabled ? EditAction::setAbsolutePosition :
+ EditAction::removeAbsolutePosition);
+ bool cancel, handled;
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+ nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
+ if (NS_FAILED(rv) || cancel) {
+ return rv;
+ }
+
+ return rules->DidDoAction(selection, &ruleInfo, rv);
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetAbsolutelyPositionedSelectionContainer(nsIDOMElement** _retval)
+{
+ nsAutoString positionStr;
+ nsCOMPtr<nsINode> node = GetSelectionContainer();
+ nsCOMPtr<nsIDOMNode> resultNode;
+
+ while (!resultNode && node && !node->IsHTMLElement(nsGkAtoms::html)) {
+ nsresult rv =
+ mCSSEditUtils->GetComputedProperty(*node, *nsGkAtoms::position,
+ positionStr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (positionStr.EqualsLiteral("absolute"))
+ resultNode = GetAsDOMNode(node);
+ else {
+ node = node->GetParentNode();
+ }
+ }
+
+ nsCOMPtr<nsIDOMElement> element = do_QueryInterface(resultNode);
+ element.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetSelectionContainerAbsolutelyPositioned(
+ bool* aIsSelectionContainerAbsolutelyPositioned)
+{
+ *aIsSelectionContainerAbsolutelyPositioned = (mAbsolutelyPositionedObject != nullptr);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetAbsolutePositioningEnabled(bool* aIsEnabled)
+{
+ *aIsEnabled = mIsAbsolutelyPositioningEnabled;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::SetAbsolutePositioningEnabled(bool aIsEnabled)
+{
+ mIsAbsolutelyPositioningEnabled = aIsEnabled;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::RelativeChangeElementZIndex(nsIDOMElement* aElement,
+ int32_t aChange,
+ int32_t* aReturn)
+{
+ NS_ENSURE_ARG_POINTER(aElement);
+ NS_ENSURE_ARG_POINTER(aReturn);
+ if (!aChange) // early way out, no change
+ return NS_OK;
+
+ int32_t zIndex;
+ nsresult rv = GetElementZIndex(aElement, &zIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ zIndex = std::max(zIndex + aChange, 0);
+ SetElementZIndex(aElement, zIndex);
+ *aReturn = zIndex;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::SetElementZIndex(nsIDOMElement* aElement,
+ int32_t aZindex)
+{
+ nsCOMPtr<Element> element = do_QueryInterface(aElement);
+ NS_ENSURE_ARG_POINTER(element);
+
+ nsAutoString zIndexStr;
+ zIndexStr.AppendInt(aZindex);
+
+ mCSSEditUtils->SetCSSProperty(*element, *nsGkAtoms::z_index, zIndexStr);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::RelativeChangeZIndex(int32_t aChange)
+{
+ AutoEditBatch beginBatching(this);
+ AutoRules beginRulesSniffing(this,
+ (aChange < 0) ? EditAction::decreaseZIndex :
+ EditAction::increaseZIndex,
+ nsIEditor::eNext);
+
+ // brade: can we get rid of this comment?
+ // Find out if the selection is collapsed:
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+ TextRulesInfo ruleInfo(aChange < 0 ? EditAction::decreaseZIndex :
+ EditAction::increaseZIndex);
+ bool cancel, handled;
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+ nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
+ if (cancel || NS_FAILED(rv)) {
+ return rv;
+ }
+
+ return rules->DidDoAction(selection, &ruleInfo, rv);
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetElementZIndex(nsIDOMElement* aElement,
+ int32_t* aZindex)
+{
+ nsCOMPtr<Element> element = do_QueryInterface(aElement);
+ NS_ENSURE_STATE(element || !aElement);
+ nsAutoString zIndexStr;
+ *aZindex = 0;
+
+ nsresult rv =
+ mCSSEditUtils->GetSpecifiedProperty(*element, *nsGkAtoms::z_index,
+ zIndexStr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (zIndexStr.EqualsLiteral("auto")) {
+ // we have to look at the positioned ancestors
+ // cf. CSS 2 spec section 9.9.1
+ nsCOMPtr<nsIDOMNode> parentNode;
+ rv = aElement->GetParentNode(getter_AddRefs(parentNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsINode> node = do_QueryInterface(parentNode);
+ nsAutoString positionStr;
+ while (node && zIndexStr.EqualsLiteral("auto") &&
+ !node->IsHTMLElement(nsGkAtoms::body)) {
+ rv = mCSSEditUtils->GetComputedProperty(*node, *nsGkAtoms::position,
+ positionStr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (positionStr.EqualsLiteral("absolute")) {
+ // ah, we found one, what's its z-index ? If its z-index is auto,
+ // we have to continue climbing the document's tree
+ rv = mCSSEditUtils->GetComputedProperty(*node, *nsGkAtoms::z_index,
+ zIndexStr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ node = node->GetParentNode();
+ }
+ }
+
+ if (!zIndexStr.EqualsLiteral("auto")) {
+ nsresult errorCode;
+ *aZindex = zIndexStr.ToInteger(&errorCode);
+ }
+
+ return NS_OK;
+}
+
+already_AddRefed<Element>
+HTMLEditor::CreateGrabber(nsINode* aParentNode)
+{
+ // let's create a grabber through the element factory
+ nsCOMPtr<nsIDOMElement> retDOM;
+ CreateAnonymousElement(NS_LITERAL_STRING("span"), GetAsDOMNode(aParentNode),
+ NS_LITERAL_STRING("mozGrabber"), false,
+ getter_AddRefs(retDOM));
+
+ NS_ENSURE_TRUE(retDOM, nullptr);
+
+ // add the mouse listener so we can detect a click on a resizer
+ nsCOMPtr<nsIDOMEventTarget> evtTarget(do_QueryInterface(retDOM));
+ evtTarget->AddEventListener(NS_LITERAL_STRING("mousedown"),
+ mEventListener, false);
+
+ nsCOMPtr<Element> ret = do_QueryInterface(retDOM);
+ return ret.forget();
+}
+
+NS_IMETHODIMP
+HTMLEditor::RefreshGrabber()
+{
+ NS_ENSURE_TRUE(mAbsolutelyPositionedObject, NS_ERROR_NULL_POINTER);
+
+ nsresult rv =
+ GetPositionAndDimensions(
+ static_cast<nsIDOMElement*>(GetAsDOMNode(mAbsolutelyPositionedObject)),
+ mPositionedObjectX,
+ mPositionedObjectY,
+ mPositionedObjectWidth,
+ mPositionedObjectHeight,
+ mPositionedObjectBorderLeft,
+ mPositionedObjectBorderTop,
+ mPositionedObjectMarginLeft,
+ mPositionedObjectMarginTop);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ SetAnonymousElementPosition(mPositionedObjectX+12,
+ mPositionedObjectY-14,
+ static_cast<nsIDOMElement*>(GetAsDOMNode(mGrabber)));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::HideGrabber()
+{
+ nsresult rv = mAbsolutelyPositionedObject->UnsetAttr(kNameSpaceID_None,
+ nsGkAtoms::_moz_abspos,
+ true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mAbsolutelyPositionedObject = nullptr;
+ NS_ENSURE_TRUE(mGrabber, NS_ERROR_NULL_POINTER);
+
+ // get the presshell's document observer interface.
+ nsCOMPtr<nsIPresShell> ps = GetPresShell();
+ // We allow the pres shell to be null; when it is, we presume there
+ // are no document observers to notify, but we still want to
+ // UnbindFromTree.
+
+ nsCOMPtr<nsIContent> parentContent = mGrabber->GetParent();
+ NS_ENSURE_TRUE(parentContent, NS_ERROR_NULL_POINTER);
+
+ DeleteRefToAnonymousNode(static_cast<nsIDOMElement*>(GetAsDOMNode(mGrabber)), parentContent, ps);
+ mGrabber = nullptr;
+ DeleteRefToAnonymousNode(static_cast<nsIDOMElement*>(GetAsDOMNode(mPositioningShadow)), parentContent, ps);
+ mPositioningShadow = nullptr;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::ShowGrabberOnElement(nsIDOMElement* aElement)
+{
+ nsCOMPtr<Element> element = do_QueryInterface(aElement);
+ NS_ENSURE_ARG_POINTER(element);
+
+ if (NS_WARN_IF(!IsDescendantOfEditorRoot(element))) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (mGrabber) {
+ NS_ERROR("call HideGrabber first");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsAutoString classValue;
+ nsresult rv = CheckPositionedElementBGandFG(aElement, classValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = element->SetAttr(kNameSpaceID_None, nsGkAtoms::_moz_abspos,
+ classValue, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // first, let's keep track of that element...
+ mAbsolutelyPositionedObject = element;
+
+ mGrabber = CreateGrabber(element->GetParentNode());
+ NS_ENSURE_TRUE(mGrabber, NS_ERROR_FAILURE);
+
+ // and set its position
+ return RefreshGrabber();
+}
+
+nsresult
+HTMLEditor::StartMoving(nsIDOMElement* aHandle)
+{
+ nsCOMPtr<nsINode> parentNode = mGrabber->GetParentNode();
+
+ // now, let's create the resizing shadow
+ mPositioningShadow = CreateShadow(GetAsDOMNode(parentNode),
+ static_cast<nsIDOMElement*>(GetAsDOMNode(mAbsolutelyPositionedObject)));
+ NS_ENSURE_TRUE(mPositioningShadow, NS_ERROR_FAILURE);
+ nsresult rv = SetShadowPosition(mPositioningShadow,
+ mAbsolutelyPositionedObject,
+ mPositionedObjectX, mPositionedObjectY);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // make the shadow appear
+ mPositioningShadow->UnsetAttr(kNameSpaceID_None, nsGkAtoms::_class, true);
+
+ // position it
+ mCSSEditUtils->SetCSSPropertyPixels(*mPositioningShadow, *nsGkAtoms::width,
+ mPositionedObjectWidth);
+ mCSSEditUtils->SetCSSPropertyPixels(*mPositioningShadow, *nsGkAtoms::height,
+ mPositionedObjectHeight);
+
+ mIsMoving = true;
+ return NS_OK; // XXX Looks like nobody refers this result
+}
+
+void
+HTMLEditor::SnapToGrid(int32_t& newX, int32_t& newY)
+{
+ if (mSnapToGridEnabled && mGridSize) {
+ newX = (int32_t) floor( ((float)newX / (float)mGridSize) + 0.5f ) * mGridSize;
+ newY = (int32_t) floor( ((float)newY / (float)mGridSize) + 0.5f ) * mGridSize;
+ }
+}
+
+nsresult
+HTMLEditor::GrabberClicked()
+{
+ // add a mouse move listener to the editor
+ nsresult rv = NS_OK;
+ if (!mMouseMotionListenerP) {
+ mMouseMotionListenerP = new ResizerMouseMotionListener(this);
+ if (!mMouseMotionListenerP) {return NS_ERROR_NULL_POINTER;}
+
+ nsCOMPtr<nsIDOMEventTarget> piTarget = GetDOMEventTarget();
+ NS_ENSURE_TRUE(piTarget, NS_ERROR_FAILURE);
+
+ rv = piTarget->AddEventListener(NS_LITERAL_STRING("mousemove"),
+ mMouseMotionListenerP,
+ false, false);
+ NS_ASSERTION(NS_SUCCEEDED(rv),
+ "failed to register mouse motion listener");
+ }
+ mGrabberClicked = true;
+ return rv;
+}
+
+nsresult
+HTMLEditor::EndMoving()
+{
+ if (mPositioningShadow) {
+ nsCOMPtr<nsIPresShell> ps = GetPresShell();
+ NS_ENSURE_TRUE(ps, NS_ERROR_NOT_INITIALIZED);
+
+ nsCOMPtr<nsIContent> parentContent = mGrabber->GetParent();
+ NS_ENSURE_TRUE(parentContent, NS_ERROR_FAILURE);
+
+ DeleteRefToAnonymousNode(static_cast<nsIDOMElement*>(GetAsDOMNode(mPositioningShadow)),
+ parentContent, ps);
+
+ mPositioningShadow = nullptr;
+ }
+ nsCOMPtr<nsIDOMEventTarget> piTarget = GetDOMEventTarget();
+
+ if (piTarget && mMouseMotionListenerP) {
+ DebugOnly<nsresult> rv =
+ piTarget->RemoveEventListener(NS_LITERAL_STRING("mousemove"),
+ mMouseMotionListenerP,
+ false);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to remove mouse motion listener");
+ }
+ mMouseMotionListenerP = nullptr;
+
+ mGrabberClicked = false;
+ mIsMoving = false;
+ RefPtr<Selection> selection = GetSelection();
+ if (!selection) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ return CheckSelectionStateForAnonymousButtons(selection);
+}
+nsresult
+HTMLEditor::SetFinalPosition(int32_t aX,
+ int32_t aY)
+{
+ nsresult rv = EndMoving();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // we have now to set the new width and height of the resized object
+ // we don't set the x and y position because we don't control that in
+ // a normal HTML layout
+ int32_t newX = mPositionedObjectX + aX - mOriginalX - (mPositionedObjectBorderLeft+mPositionedObjectMarginLeft);
+ int32_t newY = mPositionedObjectY + aY - mOriginalY - (mPositionedObjectBorderTop+mPositionedObjectMarginTop);
+
+ SnapToGrid(newX, newY);
+
+ nsAutoString x, y;
+ x.AppendInt(newX);
+ y.AppendInt(newY);
+
+ // we want one transaction only from a user's point of view
+ AutoEditBatch batchIt(this);
+
+ nsCOMPtr<Element> absolutelyPositionedObject =
+ do_QueryInterface(mAbsolutelyPositionedObject);
+ NS_ENSURE_STATE(absolutelyPositionedObject);
+ mCSSEditUtils->SetCSSPropertyPixels(*absolutelyPositionedObject,
+ *nsGkAtoms::top, newY);
+ mCSSEditUtils->SetCSSPropertyPixels(*absolutelyPositionedObject,
+ *nsGkAtoms::left, newX);
+ // keep track of that size
+ mPositionedObjectX = newX;
+ mPositionedObjectY = newY;
+
+ return RefreshResizers();
+}
+
+void
+HTMLEditor::AddPositioningOffset(int32_t& aX,
+ int32_t& aY)
+{
+ // Get the positioning offset
+ int32_t positioningOffset =
+ Preferences::GetInt("editor.positioning.offset", 0);
+
+ aX += positioningOffset;
+ aY += positioningOffset;
+}
+
+NS_IMETHODIMP
+HTMLEditor::AbsolutelyPositionElement(nsIDOMElement* aElement,
+ bool aEnabled)
+{
+ nsCOMPtr<Element> element = do_QueryInterface(aElement);
+ NS_ENSURE_ARG_POINTER(element);
+
+ nsAutoString positionStr;
+ mCSSEditUtils->GetComputedProperty(*element, *nsGkAtoms::position,
+ positionStr);
+ bool isPositioned = (positionStr.EqualsLiteral("absolute"));
+
+ // nothing to do if the element is already in the state we want
+ if (isPositioned == aEnabled)
+ return NS_OK;
+
+ AutoEditBatch batchIt(this);
+
+ if (aEnabled) {
+ int32_t x, y;
+ GetElementOrigin(aElement, x, y);
+
+ mCSSEditUtils->SetCSSProperty(*element, *nsGkAtoms::position,
+ NS_LITERAL_STRING("absolute"));
+
+ AddPositioningOffset(x, y);
+ SnapToGrid(x, y);
+ SetElementPosition(*element, x, y);
+
+ // we may need to create a br if the positioned element is alone in its
+ // container
+ nsCOMPtr<nsINode> element = do_QueryInterface(aElement);
+ NS_ENSURE_STATE(element);
+
+ nsINode* parentNode = element->GetParentNode();
+ if (parentNode->GetChildCount() == 1) {
+ nsCOMPtr<nsIDOMNode> brNode;
+ nsresult rv = CreateBR(parentNode->AsDOMNode(), 0, address_of(brNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ else {
+ mCSSEditUtils->RemoveCSSProperty(*element, *nsGkAtoms::position,
+ EmptyString());
+ mCSSEditUtils->RemoveCSSProperty(*element, *nsGkAtoms::top,
+ EmptyString());
+ mCSSEditUtils->RemoveCSSProperty(*element, *nsGkAtoms::left,
+ EmptyString());
+ mCSSEditUtils->RemoveCSSProperty(*element, *nsGkAtoms::z_index,
+ EmptyString());
+
+ if (!HTMLEditUtils::IsImage(aElement)) {
+ mCSSEditUtils->RemoveCSSProperty(*element, *nsGkAtoms::width,
+ EmptyString());
+ mCSSEditUtils->RemoveCSSProperty(*element, *nsGkAtoms::height,
+ EmptyString());
+ }
+
+ nsCOMPtr<dom::Element> element = do_QueryInterface(aElement);
+ if (element && element->IsHTMLElement(nsGkAtoms::div) &&
+ !HasStyleOrIdOrClass(element)) {
+ RefPtr<HTMLEditRules> htmlRules =
+ static_cast<HTMLEditRules*>(mRules.get());
+ NS_ENSURE_TRUE(htmlRules, NS_ERROR_FAILURE);
+ nsresult rv = htmlRules->MakeSureElemStartsOrEndsOnCR(aElement);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = RemoveContainer(element);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::SetSnapToGridEnabled(bool aEnabled)
+{
+ mSnapToGridEnabled = aEnabled;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetSnapToGridEnabled(bool* aIsEnabled)
+{
+ *aIsEnabled = mSnapToGridEnabled;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::SetGridSize(uint32_t aSize)
+{
+ mGridSize = aSize;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetGridSize(uint32_t* aSize)
+{
+ *aSize = mGridSize;
+ return NS_OK;
+}
+
+// self-explanatory
+NS_IMETHODIMP
+HTMLEditor::SetElementPosition(nsIDOMElement* aElement,
+ int32_t aX,
+ int32_t aY)
+{
+ nsCOMPtr<Element> element = do_QueryInterface(aElement);
+ NS_ENSURE_STATE(element);
+
+ SetElementPosition(*element, aX, aY);
+ return NS_OK;
+}
+
+void
+HTMLEditor::SetElementPosition(Element& aElement,
+ int32_t aX,
+ int32_t aY)
+{
+ AutoEditBatch batchIt(this);
+ mCSSEditUtils->SetCSSPropertyPixels(aElement, *nsGkAtoms::left, aX);
+ mCSSEditUtils->SetCSSPropertyPixels(aElement, *nsGkAtoms::top, aY);
+}
+
+// self-explanatory
+NS_IMETHODIMP
+HTMLEditor::GetPositionedElement(nsIDOMElement** aReturn)
+{
+ nsCOMPtr<nsIDOMElement> ret =
+ static_cast<nsIDOMElement*>(GetAsDOMNode(mAbsolutelyPositionedObject));
+ ret.forget(aReturn);
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::CheckPositionedElementBGandFG(nsIDOMElement* aElement,
+ nsAString& aReturn)
+{
+ // we are going to outline the positioned element and bring it to the
+ // front to overlap any other element intersecting with it. But
+ // first, let's see what's the background and foreground colors of the
+ // positioned element.
+ // if background-image computed value is 'none,
+ // If the background color is 'auto' and R G B values of the foreground are
+ // each above #d0, use a black background
+ // If the background color is 'auto' and at least one of R G B values of
+ // the foreground is below #d0, use a white background
+ // Otherwise don't change background/foreground
+ nsCOMPtr<Element> element = do_QueryInterface(aElement);
+ NS_ENSURE_STATE(element || !aElement);
+
+ aReturn.Truncate();
+
+ nsAutoString bgImageStr;
+ nsresult rv =
+ mCSSEditUtils->GetComputedProperty(*element, *nsGkAtoms::background_image,
+ bgImageStr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (bgImageStr.EqualsLiteral("none")) {
+ nsAutoString bgColorStr;
+ rv =
+ mCSSEditUtils->GetComputedProperty(*element, *nsGkAtoms::backgroundColor,
+ bgColorStr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (bgColorStr.EqualsLiteral("transparent")) {
+ RefPtr<nsComputedDOMStyle> cssDecl =
+ mCSSEditUtils->GetComputedStyle(element);
+ NS_ENSURE_STATE(cssDecl);
+
+ // from these declarations, get the one we want and that one only
+ ErrorResult error;
+ RefPtr<dom::CSSValue> cssVal = cssDecl->GetPropertyCSSValue(NS_LITERAL_STRING("color"), error);
+ NS_ENSURE_TRUE(!error.Failed(), error.StealNSResult());
+
+ nsROCSSPrimitiveValue* val = cssVal->AsPrimitiveValue();
+ NS_ENSURE_TRUE(val, NS_ERROR_FAILURE);
+
+ if (nsIDOMCSSPrimitiveValue::CSS_RGBCOLOR == val->PrimitiveType()) {
+ nsDOMCSSRGBColor* rgbVal = val->GetRGBColorValue(error);
+ NS_ENSURE_TRUE(!error.Failed(), error.StealNSResult());
+ float r = rgbVal->Red()->
+ GetFloatValue(nsIDOMCSSPrimitiveValue::CSS_NUMBER, error);
+ NS_ENSURE_TRUE(!error.Failed(), error.StealNSResult());
+ float g = rgbVal->Green()->
+ GetFloatValue(nsIDOMCSSPrimitiveValue::CSS_NUMBER, error);
+ NS_ENSURE_TRUE(!error.Failed(), error.StealNSResult());
+ float b = rgbVal->Blue()->
+ GetFloatValue(nsIDOMCSSPrimitiveValue::CSS_NUMBER, error);
+ NS_ENSURE_TRUE(!error.Failed(), error.StealNSResult());
+ if (r >= BLACK_BG_RGB_TRIGGER &&
+ g >= BLACK_BG_RGB_TRIGGER &&
+ b >= BLACK_BG_RGB_TRIGGER)
+ aReturn.AssignLiteral("black");
+ else
+ aReturn.AssignLiteral("white");
+ return NS_OK;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/HTMLAnonymousNodeEditor.cpp b/editor/libeditor/HTMLAnonymousNodeEditor.cpp
new file mode 100644
index 000000000..48f20fd04
--- /dev/null
+++ b/editor/libeditor/HTMLAnonymousNodeEditor.cpp
@@ -0,0 +1,548 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/HTMLEditor.h"
+
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/mozalloc.h"
+#include "nsAString.h"
+#include "nsCOMPtr.h"
+#include "nsComputedDOMStyle.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsGkAtoms.h"
+#include "nsIAtom.h"
+#include "nsIContent.h"
+#include "nsID.h"
+#include "nsIDOMCSSPrimitiveValue.h"
+#include "nsIDOMCSSStyleDeclaration.h"
+#include "nsIDOMCSSValue.h"
+#include "nsIDOMElement.h"
+#include "nsIDOMEventTarget.h"
+#include "nsIDOMHTMLElement.h"
+#include "nsIDOMNode.h"
+#include "nsIDOMWindow.h"
+#include "nsIDocument.h"
+#include "nsIDocumentObserver.h"
+#include "nsIHTMLAbsPosEditor.h"
+#include "nsIHTMLInlineTableEditor.h"
+#include "nsIHTMLObjectResizer.h"
+#include "nsStubMutationObserver.h"
+#include "nsINode.h"
+#include "nsIPresShell.h"
+#include "nsISupportsImpl.h"
+#include "nsISupportsUtils.h"
+#include "nsLiteralString.h"
+#include "nsPresContext.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsUnicharUtils.h"
+#include "nscore.h"
+#include "nsContentUtils.h" // for nsAutoScriptBlocker
+
+class nsIDOMEventListener;
+class nsISelection;
+
+namespace mozilla {
+
+using namespace dom;
+
+// retrieve an integer stored into a CSS computed float value
+static int32_t GetCSSFloatValue(nsIDOMCSSStyleDeclaration * aDecl,
+ const nsAString & aProperty)
+{
+ MOZ_ASSERT(aDecl);
+
+ nsCOMPtr<nsIDOMCSSValue> value;
+ // get the computed CSSValue of the property
+ nsresult rv = aDecl->GetPropertyCSSValue(aProperty, getter_AddRefs(value));
+ if (NS_FAILED(rv) || !value) {
+ return 0;
+ }
+
+ // check the type of the returned CSSValue; we handle here only
+ // pixel and enum types
+ nsCOMPtr<nsIDOMCSSPrimitiveValue> val = do_QueryInterface(value);
+ uint16_t type;
+ val->GetPrimitiveType(&type);
+
+ float f = 0;
+ switch (type) {
+ case nsIDOMCSSPrimitiveValue::CSS_PX:
+ // the value is in pixels, just get it
+ rv = val->GetFloatValue(nsIDOMCSSPrimitiveValue::CSS_PX, &f);
+ NS_ENSURE_SUCCESS(rv, 0);
+ break;
+ case nsIDOMCSSPrimitiveValue::CSS_IDENT: {
+ // the value is keyword, we have to map these keywords into
+ // numeric values
+ nsAutoString str;
+ val->GetStringValue(str);
+ if (str.EqualsLiteral("thin")) {
+ f = 1;
+ } else if (str.EqualsLiteral("medium")) {
+ f = 3;
+ } else if (str.EqualsLiteral("thick")) {
+ f = 5;
+ }
+ break;
+ }
+ }
+
+ return (int32_t) f;
+}
+
+class ElementDeletionObserver final : public nsStubMutationObserver
+{
+public:
+ ElementDeletionObserver(nsIContent* aNativeAnonNode,
+ nsIContent* aObservedNode)
+ : mNativeAnonNode(aNativeAnonNode)
+ , mObservedNode(aObservedNode)
+ {}
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMUTATIONOBSERVER_PARENTCHAINCHANGED
+ NS_DECL_NSIMUTATIONOBSERVER_NODEWILLBEDESTROYED
+
+protected:
+ ~ElementDeletionObserver() {}
+ nsIContent* mNativeAnonNode;
+ nsIContent* mObservedNode;
+};
+
+NS_IMPL_ISUPPORTS(ElementDeletionObserver, nsIMutationObserver)
+
+void
+ElementDeletionObserver::ParentChainChanged(nsIContent* aContent)
+{
+ // If the native anonymous content has been unbound already in
+ // DeleteRefToAnonymousNode, mNativeAnonNode's parentNode is null.
+ if (aContent == mObservedNode && mNativeAnonNode &&
+ mNativeAnonNode->GetParentNode() == aContent) {
+ // If the observed node has been moved to another document, there isn't much
+ // we can do easily. But at least be safe and unbind the native anonymous
+ // content and stop observing changes.
+ if (mNativeAnonNode->OwnerDoc() != mObservedNode->OwnerDoc()) {
+ mObservedNode->RemoveMutationObserver(this);
+ mObservedNode = nullptr;
+ mNativeAnonNode->RemoveMutationObserver(this);
+ mNativeAnonNode->UnbindFromTree();
+ mNativeAnonNode = nullptr;
+ NS_RELEASE_THIS();
+ return;
+ }
+
+ // We're staying in the same document, just rebind the native anonymous
+ // node so that the subtree root points to the right object etc.
+ mNativeAnonNode->UnbindFromTree();
+ mNativeAnonNode->BindToTree(mObservedNode->GetUncomposedDoc(), mObservedNode,
+ mObservedNode, true);
+ }
+}
+
+void
+ElementDeletionObserver::NodeWillBeDestroyed(const nsINode* aNode)
+{
+ NS_ASSERTION(aNode == mNativeAnonNode || aNode == mObservedNode,
+ "Wrong aNode!");
+ if (aNode == mNativeAnonNode) {
+ mObservedNode->RemoveMutationObserver(this);
+ mObservedNode = nullptr;
+ } else {
+ mNativeAnonNode->RemoveMutationObserver(this);
+ mNativeAnonNode->UnbindFromTree();
+ mNativeAnonNode = nullptr;
+ }
+
+ NS_RELEASE_THIS();
+}
+
+// Returns in *aReturn an anonymous nsDOMElement of type aTag,
+// child of aParentNode. If aIsCreatedHidden is true, the class
+// "hidden" is added to the created element. If aAnonClass is not
+// the empty string, it becomes the value of the attribute "_moz_anonclass"
+nsresult
+HTMLEditor::CreateAnonymousElement(const nsAString& aTag,
+ nsIDOMNode* aParentNode,
+ const nsAString& aAnonClass,
+ bool aIsCreatedHidden,
+ nsIDOMElement** aReturn)
+{
+ NS_ENSURE_ARG_POINTER(aParentNode);
+ NS_ENSURE_ARG_POINTER(aReturn);
+ *aReturn = nullptr;
+
+ nsCOMPtr<nsIContent> parentContent( do_QueryInterface(aParentNode) );
+ NS_ENSURE_TRUE(parentContent, NS_OK);
+
+ nsCOMPtr<nsIDocument> doc = GetDocument();
+ NS_ENSURE_TRUE(doc, NS_ERROR_NULL_POINTER);
+
+ // Get the pres shell
+ nsCOMPtr<nsIPresShell> ps = GetPresShell();
+ NS_ENSURE_TRUE(ps, NS_ERROR_NOT_INITIALIZED);
+
+ // Create a new node through the element factory
+ nsCOMPtr<nsIAtom> tagAtom = NS_Atomize(aTag);
+ nsCOMPtr<Element> newContent = CreateHTMLContent(tagAtom);
+ NS_ENSURE_STATE(newContent);
+
+ nsCOMPtr<nsIDOMElement> newElement = do_QueryInterface(newContent);
+ NS_ENSURE_TRUE(newElement, NS_ERROR_FAILURE);
+
+ // add the "hidden" class if needed
+ if (aIsCreatedHidden) {
+ nsresult rv = newElement->SetAttribute(NS_LITERAL_STRING("class"),
+ NS_LITERAL_STRING("hidden"));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // add an _moz_anonclass attribute if needed
+ if (!aAnonClass.IsEmpty()) {
+ nsresult rv = newElement->SetAttribute(NS_LITERAL_STRING("_moz_anonclass"),
+ aAnonClass);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ {
+ nsAutoScriptBlocker scriptBlocker;
+
+ // establish parenthood of the element
+ newContent->SetIsNativeAnonymousRoot();
+ nsresult rv =
+ newContent->BindToTree(doc, parentContent, parentContent, true);
+ if (NS_FAILED(rv)) {
+ newContent->UnbindFromTree();
+ return rv;
+ }
+ }
+
+ ElementDeletionObserver* observer =
+ new ElementDeletionObserver(newContent, parentContent);
+ NS_ADDREF(observer); // NodeWillBeDestroyed releases.
+ parentContent->AddMutationObserver(observer);
+ newContent->AddMutationObserver(observer);
+
+#ifdef DEBUG
+ // Editor anonymous content gets passed to RecreateFramesFor... which can't
+ // _really_ deal with anonymous content (because it can't get the frame tree
+ // ordering right). But for us the ordering doesn't matter so this is sort of
+ // ok.
+ newContent->SetProperty(nsGkAtoms::restylableAnonymousNode,
+ reinterpret_cast<void*>(true));
+#endif // DEBUG
+
+ // display the element
+ ps->RecreateFramesFor(newContent);
+
+ newElement.forget(aReturn);
+ return NS_OK;
+}
+
+// Removes event listener and calls DeleteRefToAnonymousNode.
+void
+HTMLEditor::RemoveListenerAndDeleteRef(const nsAString& aEvent,
+ nsIDOMEventListener* aListener,
+ bool aUseCapture,
+ Element* aElement,
+ nsIContent* aParentContent,
+ nsIPresShell* aShell)
+{
+ nsCOMPtr<nsIDOMEventTarget> evtTarget(do_QueryInterface(aElement));
+ if (evtTarget) {
+ evtTarget->RemoveEventListener(aEvent, aListener, aUseCapture);
+ }
+ DeleteRefToAnonymousNode(static_cast<nsIDOMElement*>(GetAsDOMNode(aElement)), aParentContent, aShell);
+}
+
+// Deletes all references to an anonymous element
+void
+HTMLEditor::DeleteRefToAnonymousNode(nsIDOMElement* aElement,
+ nsIContent* aParentContent,
+ nsIPresShell* aShell)
+{
+ // call ContentRemoved() for the anonymous content
+ // node so its references get removed from the frame manager's
+ // undisplay map, and its layout frames get destroyed!
+
+ if (aElement) {
+ nsCOMPtr<nsIContent> content = do_QueryInterface(aElement);
+ if (content) {
+ nsAutoScriptBlocker scriptBlocker;
+ // Need to check whether aShell has been destroyed (but not yet deleted).
+ // In that case presContext->GetPresShell() returns nullptr.
+ // See bug 338129.
+ if (content->IsInComposedDoc() && aShell && aShell->GetPresContext() &&
+ aShell->GetPresContext()->GetPresShell() == aShell) {
+ nsCOMPtr<nsIDocumentObserver> docObserver = do_QueryInterface(aShell);
+ if (docObserver) {
+ // Call BeginUpdate() so that the nsCSSFrameConstructor/PresShell
+ // knows we're messing with the frame tree.
+ nsCOMPtr<nsIDocument> document = GetDocument();
+ if (document) {
+ docObserver->BeginUpdate(document, UPDATE_CONTENT_MODEL);
+ }
+
+ // XXX This is wrong (bug 439258). Once it's fixed, the NS_WARNING
+ // in RestyleManager::RestyleForRemove should be changed back
+ // to an assertion.
+ docObserver->ContentRemoved(content->GetComposedDoc(),
+ aParentContent, content, -1,
+ content->GetPreviousSibling());
+ if (document) {
+ docObserver->EndUpdate(document, UPDATE_CONTENT_MODEL);
+ }
+ }
+ }
+ content->UnbindFromTree();
+ }
+ }
+}
+
+// The following method is mostly called by a selection listener. When a
+// selection change is notified, the method is called to check if resizing
+// handles, a grabber and/or inline table editing UI need to be displayed
+// or refreshed
+NS_IMETHODIMP
+HTMLEditor::CheckSelectionStateForAnonymousButtons(nsISelection* aSelection)
+{
+ NS_ENSURE_ARG_POINTER(aSelection);
+
+ // early way out if all contextual UI extensions are disabled
+ NS_ENSURE_TRUE(mIsObjectResizingEnabled ||
+ mIsAbsolutelyPositioningEnabled ||
+ mIsInlineTableEditingEnabled, NS_OK);
+
+ // Don't change selection state if we're moving.
+ if (mIsMoving) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIDOMElement> focusElement;
+ // let's get the containing element of the selection
+ nsresult rv = GetSelectionContainer(getter_AddRefs(focusElement));
+ NS_ENSURE_TRUE(focusElement, NS_OK);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If we're not in a document, don't try to add resizers
+ nsCOMPtr<dom::Element> focusElementNode = do_QueryInterface(focusElement);
+ NS_ENSURE_STATE(focusElementNode);
+ if (!focusElementNode->IsInUncomposedDoc()) {
+ return NS_OK;
+ }
+
+ // what's its tag?
+ nsAutoString focusTagName;
+ rv = focusElement->GetTagName(focusTagName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ ToLowerCase(focusTagName);
+ nsCOMPtr<nsIAtom> focusTagAtom = NS_Atomize(focusTagName);
+
+ nsCOMPtr<nsIDOMElement> absPosElement;
+ if (mIsAbsolutelyPositioningEnabled) {
+ // Absolute Positioning support is enabled, is the selection contained
+ // in an absolutely positioned element ?
+ rv =
+ GetAbsolutelyPositionedSelectionContainer(getter_AddRefs(absPosElement));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsIDOMElement> cellElement;
+ if (mIsObjectResizingEnabled || mIsInlineTableEditingEnabled) {
+ // Resizing or Inline Table Editing is enabled, we need to check if the
+ // selection is contained in a table cell
+ rv = GetElementOrParentByTagName(NS_LITERAL_STRING("td"),
+ nullptr,
+ getter_AddRefs(cellElement));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (mIsObjectResizingEnabled && cellElement) {
+ // we are here because Resizing is enabled AND selection is contained in
+ // a cell
+
+ // get the enclosing table
+ if (nsGkAtoms::img != focusTagAtom) {
+ // the element container of the selection is not an image, so we'll show
+ // the resizers around the table
+ nsCOMPtr<nsIDOMNode> tableNode = GetEnclosingTable(cellElement);
+ focusElement = do_QueryInterface(tableNode);
+ focusTagAtom = nsGkAtoms::table;
+ }
+ }
+
+ // we allow resizers only around images, tables, and absolutely positioned
+ // elements. If we don't have image/table, let's look at the latter case.
+ if (nsGkAtoms::img != focusTagAtom && nsGkAtoms::table != focusTagAtom) {
+ focusElement = absPosElement;
+ }
+
+ // at this point, focusElement contains the element for Resizing,
+ // cellElement contains the element for InlineTableEditing
+ // absPosElement contains the element for Positioning
+
+ // Note: All the Hide/Show methods below may change attributes on real
+ // content which means a DOMAttrModified handler may cause arbitrary
+ // side effects while this code runs (bug 420439).
+
+ if (mIsAbsolutelyPositioningEnabled && mAbsolutelyPositionedObject &&
+ absPosElement != GetAsDOMNode(mAbsolutelyPositionedObject)) {
+ rv = HideGrabber();
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ASSERTION(!mAbsolutelyPositionedObject, "HideGrabber failed");
+ }
+
+ if (mIsObjectResizingEnabled && mResizedObject &&
+ GetAsDOMNode(mResizedObject) != focusElement) {
+ rv = HideResizers();
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ASSERTION(!mResizedObject, "HideResizers failed");
+ }
+
+ if (mIsInlineTableEditingEnabled && mInlineEditedCell &&
+ mInlineEditedCell != cellElement) {
+ rv = HideInlineTableEditingUI();
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ASSERTION(!mInlineEditedCell, "HideInlineTableEditingUI failed");
+ }
+
+ // now, let's display all contextual UI for good
+ nsIContent* hostContent = GetActiveEditingHost();
+ nsCOMPtr<nsIDOMNode> hostNode = do_QueryInterface(hostContent);
+
+ if (mIsObjectResizingEnabled && focusElement &&
+ IsModifiableNode(focusElement) && focusElement != hostNode) {
+ if (nsGkAtoms::img == focusTagAtom) {
+ mResizedObjectIsAnImage = true;
+ }
+ if (mResizedObject) {
+ nsresult rv = RefreshResizers();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ nsresult rv = ShowResizers(focusElement);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+ }
+
+ if (mIsAbsolutelyPositioningEnabled && absPosElement &&
+ IsModifiableNode(absPosElement) && absPosElement != hostNode) {
+ if (mAbsolutelyPositionedObject) {
+ nsresult rv = RefreshGrabber();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ nsresult rv = ShowGrabberOnElement(absPosElement);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+ }
+
+ if (mIsInlineTableEditingEnabled && cellElement &&
+ IsModifiableNode(cellElement) && cellElement != hostNode) {
+ if (mInlineEditedCell) {
+ nsresult rv = RefreshInlineTableEditingUI();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ nsresult rv = ShowInlineTableEditingUI(cellElement);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+// Resizing and Absolute Positioning need to know everything about the
+// containing box of the element: position, size, margins, borders
+nsresult
+HTMLEditor::GetPositionAndDimensions(nsIDOMElement* aElement,
+ int32_t& aX,
+ int32_t& aY,
+ int32_t& aW,
+ int32_t& aH,
+ int32_t& aBorderLeft,
+ int32_t& aBorderTop,
+ int32_t& aMarginLeft,
+ int32_t& aMarginTop)
+{
+ nsCOMPtr<Element> element = do_QueryInterface(aElement);
+ NS_ENSURE_ARG_POINTER(element);
+
+ // Is the element positioned ? let's check the cheap way first...
+ bool isPositioned = false;
+ nsresult rv =
+ aElement->HasAttribute(NS_LITERAL_STRING("_moz_abspos"), &isPositioned);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!isPositioned) {
+ // hmmm... the expensive way now...
+ nsAutoString positionStr;
+ mCSSEditUtils->GetComputedProperty(*element, *nsGkAtoms::position,
+ positionStr);
+ isPositioned = positionStr.EqualsLiteral("absolute");
+ }
+
+ if (isPositioned) {
+ // Yes, it is absolutely positioned
+ mResizedObjectIsAbsolutelyPositioned = true;
+
+ // Get the all the computed css styles attached to the element node
+ RefPtr<nsComputedDOMStyle> cssDecl =
+ mCSSEditUtils->GetComputedStyle(element);
+ NS_ENSURE_STATE(cssDecl);
+
+ aBorderLeft = GetCSSFloatValue(cssDecl, NS_LITERAL_STRING("border-left-width"));
+ aBorderTop = GetCSSFloatValue(cssDecl, NS_LITERAL_STRING("border-top-width"));
+ aMarginLeft = GetCSSFloatValue(cssDecl, NS_LITERAL_STRING("margin-left"));
+ aMarginTop = GetCSSFloatValue(cssDecl, NS_LITERAL_STRING("margin-top"));
+
+ aX = GetCSSFloatValue(cssDecl, NS_LITERAL_STRING("left")) +
+ aMarginLeft + aBorderLeft;
+ aY = GetCSSFloatValue(cssDecl, NS_LITERAL_STRING("top")) +
+ aMarginTop + aBorderTop;
+ aW = GetCSSFloatValue(cssDecl, NS_LITERAL_STRING("width"));
+ aH = GetCSSFloatValue(cssDecl, NS_LITERAL_STRING("height"));
+ } else {
+ mResizedObjectIsAbsolutelyPositioned = false;
+ nsCOMPtr<nsIDOMHTMLElement> htmlElement = do_QueryInterface(aElement);
+ if (!htmlElement) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ GetElementOrigin(aElement, aX, aY);
+
+ if (NS_WARN_IF(NS_FAILED(htmlElement->GetOffsetWidth(&aW))) ||
+ NS_WARN_IF(NS_FAILED(htmlElement->GetOffsetHeight(&aH)))) {
+ return rv;
+ }
+
+ aBorderLeft = 0;
+ aBorderTop = 0;
+ aMarginLeft = 0;
+ aMarginTop = 0;
+ }
+ return NS_OK;
+}
+
+// self-explanatory
+void
+HTMLEditor::SetAnonymousElementPosition(int32_t aX,
+ int32_t aY,
+ nsIDOMElement* aElement)
+{
+ mCSSEditUtils->SetCSSPropertyPixels(aElement, NS_LITERAL_STRING("left"), aX);
+ mCSSEditUtils->SetCSSPropertyPixels(aElement, NS_LITERAL_STRING("top"), aY);
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/HTMLEditRules.cpp b/editor/libeditor/HTMLEditRules.cpp
new file mode 100644
index 000000000..c2d61f767
--- /dev/null
+++ b/editor/libeditor/HTMLEditRules.cpp
@@ -0,0 +1,8785 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sw=2 et tw=79: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HTMLEditRules.h"
+
+#include <stdlib.h>
+
+#include "HTMLEditUtils.h"
+#include "TextEditUtils.h"
+#include "WSRunObject.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/CSSEditUtils.h"
+#include "mozilla/EditorUtils.h"
+#include "mozilla/HTMLEditor.h"
+#include "mozilla/MathAlgorithms.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/mozalloc.h"
+#include "nsAutoPtr.h"
+#include "nsAString.h"
+#include "nsAlgorithm.h"
+#include "nsCRT.h"
+#include "nsCRTGlue.h"
+#include "nsComponentManagerUtils.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsGkAtoms.h"
+#include "nsIAtom.h"
+#include "nsIContent.h"
+#include "nsIContentIterator.h"
+#include "nsID.h"
+#include "nsIDOMCharacterData.h"
+#include "nsIDOMDocument.h"
+#include "nsIDOMElement.h"
+#include "nsIDOMNode.h"
+#include "nsIDOMText.h"
+#include "nsIFrame.h"
+#include "nsIHTMLAbsPosEditor.h"
+#include "nsIHTMLDocument.h"
+#include "nsINode.h"
+#include "nsLiteralString.h"
+#include "nsRange.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+#include "nsTextNode.h"
+#include "nsThreadUtils.h"
+#include "nsUnicharUtils.h"
+#include <algorithm>
+
+// Workaround for windows headers
+#ifdef SetProp
+#undef SetProp
+#endif
+
+class nsISupports;
+
+namespace mozilla {
+
+class RulesInfo;
+
+using namespace dom;
+
+//const static char* kMOZEditorBogusNodeAttr="MOZ_EDITOR_BOGUS_NODE";
+//const static char* kMOZEditorBogusNodeValue="TRUE";
+
+enum
+{
+ kLonely = 0,
+ kPrevSib = 1,
+ kNextSib = 2,
+ kBothSibs = 3
+};
+
+/********************************************************
+ * first some helpful functors we will use
+ ********************************************************/
+
+static bool IsBlockNode(const nsINode& node)
+{
+ return HTMLEditor::NodeIsBlockStatic(&node);
+}
+
+static bool IsInlineNode(const nsINode& node)
+{
+ return !IsBlockNode(node);
+}
+
+static bool
+IsStyleCachePreservingAction(EditAction action)
+{
+ return action == EditAction::deleteSelection ||
+ action == EditAction::insertBreak ||
+ action == EditAction::makeList ||
+ action == EditAction::indent ||
+ action == EditAction::outdent ||
+ action == EditAction::align ||
+ action == EditAction::makeBasicBlock ||
+ action == EditAction::removeList ||
+ action == EditAction::makeDefListItem ||
+ action == EditAction::insertElement ||
+ action == EditAction::insertQuotation;
+}
+
+class TableCellAndListItemFunctor final : public BoolDomIterFunctor
+{
+public:
+ // Used to build list of all li's, td's & th's iterator covers
+ virtual bool operator()(nsINode* aNode) const
+ {
+ return HTMLEditUtils::IsTableCell(aNode) ||
+ HTMLEditUtils::IsListItem(aNode);
+ }
+};
+
+class BRNodeFunctor final : public BoolDomIterFunctor
+{
+public:
+ virtual bool operator()(nsINode* aNode) const
+ {
+ return aNode->IsHTMLElement(nsGkAtoms::br);
+ }
+};
+
+class EmptyEditableFunctor final : public BoolDomIterFunctor
+{
+public:
+ explicit EmptyEditableFunctor(HTMLEditor* aHTMLEditor)
+ : mHTMLEditor(aHTMLEditor)
+ {}
+
+ virtual bool operator()(nsINode* aNode) const
+ {
+ if (mHTMLEditor->IsEditable(aNode) &&
+ (HTMLEditUtils::IsListItem(aNode) ||
+ HTMLEditUtils::IsTableCellOrCaption(*aNode))) {
+ bool bIsEmptyNode;
+ nsresult rv =
+ mHTMLEditor->IsEmptyNode(aNode, &bIsEmptyNode, false, false);
+ NS_ENSURE_SUCCESS(rv, false);
+ if (bIsEmptyNode) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+protected:
+ HTMLEditor* mHTMLEditor;
+};
+
+/********************************************************
+ * mozilla::HTMLEditRules
+ ********************************************************/
+
+HTMLEditRules::HTMLEditRules()
+ : mHTMLEditor(nullptr)
+ , mListenerEnabled(false)
+ , mReturnInEmptyLIKillsList(false)
+ , mDidDeleteSelection(false)
+ , mDidRangedDelete(false)
+ , mRestoreContentEditableCount(false)
+ , mJoinOffset(0)
+{
+ InitFields();
+}
+
+void
+HTMLEditRules::InitFields()
+{
+ mHTMLEditor = nullptr;
+ mDocChangeRange = nullptr;
+ mListenerEnabled = true;
+ mReturnInEmptyLIKillsList = true;
+ mDidDeleteSelection = false;
+ mDidRangedDelete = false;
+ mRestoreContentEditableCount = false;
+ mUtilRange = nullptr;
+ mJoinOffset = 0;
+ mNewBlock = nullptr;
+ mRangeItem = new RangeItem();
+ // populate mCachedStyles
+ mCachedStyles[0] = StyleCache(nsGkAtoms::b, EmptyString(), EmptyString());
+ mCachedStyles[1] = StyleCache(nsGkAtoms::i, EmptyString(), EmptyString());
+ mCachedStyles[2] = StyleCache(nsGkAtoms::u, EmptyString(), EmptyString());
+ mCachedStyles[3] = StyleCache(nsGkAtoms::font, NS_LITERAL_STRING("face"), EmptyString());
+ mCachedStyles[4] = StyleCache(nsGkAtoms::font, NS_LITERAL_STRING("size"), EmptyString());
+ mCachedStyles[5] = StyleCache(nsGkAtoms::font, NS_LITERAL_STRING("color"), EmptyString());
+ mCachedStyles[6] = StyleCache(nsGkAtoms::tt, EmptyString(), EmptyString());
+ mCachedStyles[7] = StyleCache(nsGkAtoms::em, EmptyString(), EmptyString());
+ mCachedStyles[8] = StyleCache(nsGkAtoms::strong, EmptyString(), EmptyString());
+ mCachedStyles[9] = StyleCache(nsGkAtoms::dfn, EmptyString(), EmptyString());
+ mCachedStyles[10] = StyleCache(nsGkAtoms::code, EmptyString(), EmptyString());
+ mCachedStyles[11] = StyleCache(nsGkAtoms::samp, EmptyString(), EmptyString());
+ mCachedStyles[12] = StyleCache(nsGkAtoms::var, EmptyString(), EmptyString());
+ mCachedStyles[13] = StyleCache(nsGkAtoms::cite, EmptyString(), EmptyString());
+ mCachedStyles[14] = StyleCache(nsGkAtoms::abbr, EmptyString(), EmptyString());
+ mCachedStyles[15] = StyleCache(nsGkAtoms::acronym, EmptyString(), EmptyString());
+ mCachedStyles[16] = StyleCache(nsGkAtoms::backgroundColor, EmptyString(), EmptyString());
+ mCachedStyles[17] = StyleCache(nsGkAtoms::sub, EmptyString(), EmptyString());
+ mCachedStyles[18] = StyleCache(nsGkAtoms::sup, EmptyString(), EmptyString());
+}
+
+HTMLEditRules::~HTMLEditRules()
+{
+ // remove ourselves as a listener to edit actions
+ // In some cases, we have already been removed by
+ // ~HTMLEditor, in which case we will get a null pointer here
+ // which we ignore. But this allows us to add the ability to
+ // switch rule sets on the fly if we want.
+ if (mHTMLEditor) {
+ mHTMLEditor->RemoveEditActionListener(this);
+ }
+}
+
+NS_IMPL_ADDREF_INHERITED(HTMLEditRules, TextEditRules)
+NS_IMPL_RELEASE_INHERITED(HTMLEditRules, TextEditRules)
+NS_INTERFACE_TABLE_HEAD_CYCLE_COLLECTION_INHERITED(HTMLEditRules)
+ NS_INTERFACE_TABLE_INHERITED(HTMLEditRules, nsIEditActionListener)
+NS_INTERFACE_TABLE_TAIL_INHERITING(TextEditRules)
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLEditRules, TextEditRules,
+ mDocChangeRange, mUtilRange, mNewBlock,
+ mRangeItem)
+
+NS_IMETHODIMP
+HTMLEditRules::Init(TextEditor* aTextEditor)
+{
+ InitFields();
+
+ mHTMLEditor = static_cast<HTMLEditor*>(aTextEditor);
+
+ // call through to base class Init
+ nsresult rv = TextEditRules::Init(aTextEditor);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // cache any prefs we care about
+ static const char kPrefName[] =
+ "editor.html.typing.returnInEmptyListItemClosesList";
+ nsAdoptingCString returnInEmptyLIKillsList =
+ Preferences::GetCString(kPrefName);
+
+ // only when "false", becomes FALSE. Otherwise (including empty), TRUE.
+ // XXX Why was this pref designed as a string and not bool?
+ mReturnInEmptyLIKillsList = !returnInEmptyLIKillsList.EqualsLiteral("false");
+
+ // make a utility range for use by the listenter
+ nsCOMPtr<nsINode> node = mHTMLEditor->GetRoot();
+ if (!node) {
+ node = mHTMLEditor->GetDocument();
+ }
+
+ NS_ENSURE_STATE(node);
+
+ mUtilRange = new nsRange(node);
+
+ // set up mDocChangeRange to be whole doc
+ // temporarily turn off rules sniffing
+ AutoLockRulesSniffing lockIt(this);
+ if (!mDocChangeRange) {
+ mDocChangeRange = new nsRange(node);
+ }
+
+ if (node->IsElement()) {
+ ErrorResult rv;
+ mDocChangeRange->SelectNode(*node, rv);
+ NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult());
+ AdjustSpecialBreaks();
+ }
+
+ // add ourselves as a listener to edit actions
+ return mHTMLEditor->AddEditActionListener(this);
+}
+
+NS_IMETHODIMP
+HTMLEditRules::DetachEditor()
+{
+ if (mHTMLEditor) {
+ mHTMLEditor->RemoveEditActionListener(this);
+ }
+ mHTMLEditor = nullptr;
+ return TextEditRules::DetachEditor();
+}
+
+NS_IMETHODIMP
+HTMLEditRules::BeforeEdit(EditAction action,
+ nsIEditor::EDirection aDirection)
+{
+ if (mLockRulesSniffing) {
+ return NS_OK;
+ }
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+
+ AutoLockRulesSniffing lockIt(this);
+ mDidExplicitlySetInterline = false;
+
+ if (!mActionNesting) {
+ mActionNesting++;
+
+ // Clear our flag about if just deleted a range
+ mDidRangedDelete = false;
+
+ // Remember where our selection was before edit action took place:
+
+ // Get selection
+ RefPtr<Selection> selection = htmlEditor->GetSelection();
+
+ // Get the selection location
+ if (NS_WARN_IF(!selection) || !selection->RangeCount()) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ mRangeItem->startNode = selection->GetRangeAt(0)->GetStartParent();
+ mRangeItem->startOffset = selection->GetRangeAt(0)->StartOffset();
+ mRangeItem->endNode = selection->GetRangeAt(0)->GetEndParent();
+ mRangeItem->endOffset = selection->GetRangeAt(0)->EndOffset();
+ nsCOMPtr<nsINode> selStartNode = mRangeItem->startNode;
+ nsCOMPtr<nsINode> selEndNode = mRangeItem->endNode;
+
+ // Register with range updater to track this as we perturb the doc
+ htmlEditor->mRangeUpdater.RegisterRangeItem(mRangeItem);
+
+ // Clear deletion state bool
+ mDidDeleteSelection = false;
+
+ // Clear out mDocChangeRange and mUtilRange
+ if (mDocChangeRange) {
+ // Clear out our accounting of what changed
+ mDocChangeRange->Reset();
+ }
+ if (mUtilRange) {
+ // Ditto for mUtilRange.
+ mUtilRange->Reset();
+ }
+
+ // Remember current inline styles for deletion and normal insertion ops
+ if (action == EditAction::insertText ||
+ action == EditAction::insertIMEText ||
+ action == EditAction::deleteSelection ||
+ IsStyleCachePreservingAction(action)) {
+ nsCOMPtr<nsINode> selNode =
+ aDirection == nsIEditor::eNext ? selEndNode : selStartNode;
+ nsresult rv = CacheInlineStyles(GetAsDOMNode(selNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Stabilize the document against contenteditable count changes
+ nsCOMPtr<nsIDOMDocument> doc = htmlEditor->GetDOMDocument();
+ NS_ENSURE_TRUE(doc, NS_ERROR_NOT_INITIALIZED);
+ nsCOMPtr<nsIHTMLDocument> htmlDoc = do_QueryInterface(doc);
+ NS_ENSURE_TRUE(htmlDoc, NS_ERROR_FAILURE);
+ if (htmlDoc->GetEditingState() == nsIHTMLDocument::eContentEditable) {
+ htmlDoc->ChangeContentEditableCount(nullptr, +1);
+ mRestoreContentEditableCount = true;
+ }
+
+ // Check that selection is in subtree defined by body node
+ ConfirmSelectionInBody();
+ // Let rules remember the top level action
+ mTheAction = action;
+ }
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+HTMLEditRules::AfterEdit(EditAction action,
+ nsIEditor::EDirection aDirection)
+{
+ if (mLockRulesSniffing) {
+ return NS_OK;
+ }
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+
+ AutoLockRulesSniffing lockIt(this);
+
+ MOZ_ASSERT(mActionNesting > 0);
+ nsresult rv = NS_OK;
+ mActionNesting--;
+ if (!mActionNesting) {
+ // Do all the tricky stuff
+ rv = AfterEditInner(action, aDirection);
+
+ // Free up selectionState range item
+ htmlEditor->mRangeUpdater.DropRangeItem(mRangeItem);
+
+ // Reset the contenteditable count to its previous value
+ if (mRestoreContentEditableCount) {
+ nsCOMPtr<nsIDOMDocument> doc = htmlEditor->GetDOMDocument();
+ NS_ENSURE_TRUE(doc, NS_ERROR_NOT_INITIALIZED);
+ nsCOMPtr<nsIHTMLDocument> htmlDoc = do_QueryInterface(doc);
+ NS_ENSURE_TRUE(htmlDoc, NS_ERROR_FAILURE);
+ if (htmlDoc->GetEditingState() == nsIHTMLDocument::eContentEditable) {
+ htmlDoc->ChangeContentEditableCount(nullptr, -1);
+ }
+ mRestoreContentEditableCount = false;
+ }
+ }
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+HTMLEditRules::AfterEditInner(EditAction action,
+ nsIEditor::EDirection aDirection)
+{
+ ConfirmSelectionInBody();
+ if (action == EditAction::ignore) {
+ return NS_OK;
+ }
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<Selection> selection = mHTMLEditor->GetSelection();
+ NS_ENSURE_STATE(selection);
+
+ nsCOMPtr<nsIDOMNode> rangeStartParent, rangeEndParent;
+ int32_t rangeStartOffset = 0, rangeEndOffset = 0;
+ // do we have a real range to act on?
+ bool bDamagedRange = false;
+ if (mDocChangeRange) {
+ mDocChangeRange->GetStartContainer(getter_AddRefs(rangeStartParent));
+ mDocChangeRange->GetEndContainer(getter_AddRefs(rangeEndParent));
+ mDocChangeRange->GetStartOffset(&rangeStartOffset);
+ mDocChangeRange->GetEndOffset(&rangeEndOffset);
+ if (rangeStartParent && rangeEndParent)
+ bDamagedRange = true;
+ }
+
+ if (bDamagedRange && !((action == EditAction::undo) ||
+ (action == EditAction::redo))) {
+ // don't let any txns in here move the selection around behind our back.
+ // Note that this won't prevent explicit selection setting from working.
+ NS_ENSURE_STATE(mHTMLEditor);
+ AutoTransactionsConserveSelection dontSpazMySelection(mHTMLEditor);
+
+ // expand the "changed doc range" as needed
+ PromoteRange(*mDocChangeRange, action);
+
+ // if we did a ranged deletion or handling backspace key, make sure we have
+ // a place to put caret.
+ // Note we only want to do this if the overall operation was deletion,
+ // not if deletion was done along the way for EditAction::loadHTML, EditAction::insertText, etc.
+ // That's why this is here rather than DidDeleteSelection().
+ if (action == EditAction::deleteSelection && mDidRangedDelete) {
+ nsresult rv = InsertBRIfNeeded(selection);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // add in any needed <br>s, and remove any unneeded ones.
+ AdjustSpecialBreaks();
+
+ // merge any adjacent text nodes
+ if (action != EditAction::insertText &&
+ action != EditAction::insertIMEText) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv = mHTMLEditor->CollapseAdjacentTextNodes(mDocChangeRange);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // clean up any empty nodes in the selection
+ nsresult rv = RemoveEmptyNodes();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // attempt to transform any unneeded nbsp's into spaces after doing various operations
+ if (action == EditAction::insertText ||
+ action == EditAction::insertIMEText ||
+ action == EditAction::deleteSelection ||
+ action == EditAction::insertBreak ||
+ action == EditAction::htmlPaste ||
+ action == EditAction::loadHTML) {
+ rv = AdjustWhitespace(selection);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // also do this for original selection endpoints.
+ NS_ENSURE_STATE(mHTMLEditor);
+ NS_ENSURE_STATE(mRangeItem->startNode);
+ NS_ENSURE_STATE(mRangeItem->endNode);
+ WSRunObject(mHTMLEditor, mRangeItem->startNode,
+ mRangeItem->startOffset).AdjustWhitespace();
+ // we only need to handle old selection endpoint if it was different from start
+ if (mRangeItem->startNode != mRangeItem->endNode ||
+ mRangeItem->startOffset != mRangeItem->endOffset) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ WSRunObject(mHTMLEditor, mRangeItem->endNode,
+ mRangeItem->endOffset).AdjustWhitespace();
+ }
+ }
+
+ // if we created a new block, make sure selection lands in it
+ if (mNewBlock) {
+ rv = PinSelectionToNewBlock(selection);
+ mNewBlock = nullptr;
+ }
+
+ // adjust selection for insert text, html paste, and delete actions
+ if (action == EditAction::insertText ||
+ action == EditAction::insertIMEText ||
+ action == EditAction::deleteSelection ||
+ action == EditAction::insertBreak ||
+ action == EditAction::htmlPaste ||
+ action == EditAction::loadHTML) {
+ rv = AdjustSelection(selection, aDirection);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // check for any styles which were removed inappropriately
+ if (action == EditAction::insertText ||
+ action == EditAction::insertIMEText ||
+ action == EditAction::deleteSelection ||
+ IsStyleCachePreservingAction(action)) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ mHTMLEditor->mTypeInState->UpdateSelState(selection);
+ rv = ReapplyCachedStyles();
+ NS_ENSURE_SUCCESS(rv, rv);
+ ClearCachedStyles();
+ }
+ }
+
+ NS_ENSURE_STATE(mHTMLEditor);
+
+ nsresult rv =
+ mHTMLEditor->HandleInlineSpellCheck(action, selection,
+ GetAsDOMNode(mRangeItem->startNode),
+ mRangeItem->startOffset,
+ rangeStartParent, rangeStartOffset,
+ rangeEndParent, rangeEndOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // detect empty doc
+ rv = CreateBogusNodeIfNeeded(selection);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // adjust selection HINT if needed
+ if (!mDidExplicitlySetInterline) {
+ CheckInterlinePosition(*selection);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditRules::WillDoAction(Selection* aSelection,
+ RulesInfo* aInfo,
+ bool* aCancel,
+ bool* aHandled)
+{
+ MOZ_ASSERT(aInfo && aCancel && aHandled);
+
+ *aCancel = false;
+ *aHandled = false;
+
+ // my kingdom for dynamic cast
+ TextRulesInfo* info = static_cast<TextRulesInfo*>(aInfo);
+
+ // Deal with actions for which we don't need to check whether the selection is
+ // editable.
+ if (info->action == EditAction::outputText ||
+ info->action == EditAction::undo ||
+ info->action == EditAction::redo) {
+ return TextEditRules::WillDoAction(aSelection, aInfo, aCancel, aHandled);
+ }
+
+ // Nothing to do if there's no selection to act on
+ if (!aSelection) {
+ return NS_OK;
+ }
+ NS_ENSURE_TRUE(aSelection->RangeCount(), NS_OK);
+
+ RefPtr<nsRange> range = aSelection->GetRangeAt(0);
+ nsCOMPtr<nsINode> selStartNode = range->GetStartParent();
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ if (!mHTMLEditor->IsModifiableNode(selStartNode)) {
+ *aCancel = true;
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsINode> selEndNode = range->GetEndParent();
+
+ if (selStartNode != selEndNode) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ if (!mHTMLEditor->IsModifiableNode(selEndNode)) {
+ *aCancel = true;
+ return NS_OK;
+ }
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ if (!mHTMLEditor->IsModifiableNode(range->GetCommonAncestor())) {
+ *aCancel = true;
+ return NS_OK;
+ }
+ }
+
+ switch (info->action) {
+ case EditAction::insertText:
+ case EditAction::insertIMEText:
+ UndefineCaretBidiLevel(aSelection);
+ return WillInsertText(info->action, aSelection, aCancel, aHandled,
+ info->inString, info->outString, info->maxLength);
+ case EditAction::loadHTML:
+ return WillLoadHTML(aSelection, aCancel);
+ case EditAction::insertBreak:
+ UndefineCaretBidiLevel(aSelection);
+ return WillInsertBreak(*aSelection, aCancel, aHandled);
+ case EditAction::deleteSelection:
+ return WillDeleteSelection(aSelection, info->collapsedAction,
+ info->stripWrappers, aCancel, aHandled);
+ case EditAction::makeList:
+ return WillMakeList(aSelection, info->blockType, info->entireList,
+ info->bulletType, aCancel, aHandled);
+ case EditAction::indent:
+ return WillIndent(aSelection, aCancel, aHandled);
+ case EditAction::outdent:
+ return WillOutdent(*aSelection, aCancel, aHandled);
+ case EditAction::setAbsolutePosition:
+ return WillAbsolutePosition(*aSelection, aCancel, aHandled);
+ case EditAction::removeAbsolutePosition:
+ return WillRemoveAbsolutePosition(aSelection, aCancel, aHandled);
+ case EditAction::align:
+ return WillAlign(*aSelection, *info->alignType, aCancel, aHandled);
+ case EditAction::makeBasicBlock:
+ return WillMakeBasicBlock(*aSelection, *info->blockType, aCancel,
+ aHandled);
+ case EditAction::removeList:
+ return WillRemoveList(aSelection, info->bOrdered, aCancel, aHandled);
+ case EditAction::makeDefListItem:
+ return WillMakeDefListItem(aSelection, info->blockType, info->entireList,
+ aCancel, aHandled);
+ case EditAction::insertElement:
+ WillInsert(*aSelection, aCancel);
+ return NS_OK;
+ case EditAction::decreaseZIndex:
+ return WillRelativeChangeZIndex(aSelection, -1, aCancel, aHandled);
+ case EditAction::increaseZIndex:
+ return WillRelativeChangeZIndex(aSelection, 1, aCancel, aHandled);
+ default:
+ return TextEditRules::WillDoAction(aSelection, aInfo,
+ aCancel, aHandled);
+ }
+}
+
+NS_IMETHODIMP
+HTMLEditRules::DidDoAction(Selection* aSelection,
+ RulesInfo* aInfo,
+ nsresult aResult)
+{
+ TextRulesInfo* info = static_cast<TextRulesInfo*>(aInfo);
+ switch (info->action) {
+ case EditAction::insertBreak:
+ return DidInsertBreak(aSelection, aResult);
+ case EditAction::deleteSelection:
+ return DidDeleteSelection(aSelection, info->collapsedAction, aResult);
+ case EditAction::makeBasicBlock:
+ case EditAction::indent:
+ case EditAction::outdent:
+ case EditAction::align:
+ return DidMakeBasicBlock(aSelection, aInfo, aResult);
+ case EditAction::setAbsolutePosition: {
+ nsresult rv = DidMakeBasicBlock(aSelection, aInfo, aResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return DidAbsolutePosition();
+ }
+ default:
+ // pass through to TextEditRules
+ return TextEditRules::DidDoAction(aSelection, aInfo, aResult);
+ }
+}
+
+nsresult
+HTMLEditRules::GetListState(bool* aMixed,
+ bool* aOL,
+ bool* aUL,
+ bool* aDL)
+{
+ NS_ENSURE_TRUE(aMixed && aOL && aUL && aDL, NS_ERROR_NULL_POINTER);
+ *aMixed = false;
+ *aOL = false;
+ *aUL = false;
+ *aDL = false;
+ bool bNonList = false;
+
+ nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
+ nsresult rv = GetListActionNodes(arrayOfNodes, EntireList::no,
+ TouchContent::no);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Examine list type for nodes in selection.
+ for (const auto& curNode : arrayOfNodes) {
+ if (!curNode->IsElement()) {
+ bNonList = true;
+ } else if (curNode->IsHTMLElement(nsGkAtoms::ul)) {
+ *aUL = true;
+ } else if (curNode->IsHTMLElement(nsGkAtoms::ol)) {
+ *aOL = true;
+ } else if (curNode->IsHTMLElement(nsGkAtoms::li)) {
+ if (dom::Element* parent = curNode->GetParentElement()) {
+ if (parent->IsHTMLElement(nsGkAtoms::ul)) {
+ *aUL = true;
+ } else if (parent->IsHTMLElement(nsGkAtoms::ol)) {
+ *aOL = true;
+ }
+ }
+ } else if (curNode->IsAnyOfHTMLElements(nsGkAtoms::dl,
+ nsGkAtoms::dt,
+ nsGkAtoms::dd)) {
+ *aDL = true;
+ } else {
+ bNonList = true;
+ }
+ }
+
+ // hokey arithmetic with booleans
+ if ((*aUL + *aOL + *aDL + bNonList) > 1) {
+ *aMixed = true;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+HTMLEditRules::GetListItemState(bool* aMixed,
+ bool* aLI,
+ bool* aDT,
+ bool* aDD)
+{
+ NS_ENSURE_TRUE(aMixed && aLI && aDT && aDD, NS_ERROR_NULL_POINTER);
+ *aMixed = false;
+ *aLI = false;
+ *aDT = false;
+ *aDD = false;
+ bool bNonList = false;
+
+ nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
+ nsresult rv = GetListActionNodes(arrayOfNodes, EntireList::no,
+ TouchContent::no);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // examine list type for nodes in selection
+ for (const auto& node : arrayOfNodes) {
+ if (!node->IsElement()) {
+ bNonList = true;
+ } else if (node->IsAnyOfHTMLElements(nsGkAtoms::ul,
+ nsGkAtoms::ol,
+ nsGkAtoms::li)) {
+ *aLI = true;
+ } else if (node->IsHTMLElement(nsGkAtoms::dt)) {
+ *aDT = true;
+ } else if (node->IsHTMLElement(nsGkAtoms::dd)) {
+ *aDD = true;
+ } else if (node->IsHTMLElement(nsGkAtoms::dl)) {
+ // need to look inside dl and see which types of items it has
+ bool bDT, bDD;
+ GetDefinitionListItemTypes(node->AsElement(), &bDT, &bDD);
+ *aDT |= bDT;
+ *aDD |= bDD;
+ } else {
+ bNonList = true;
+ }
+ }
+
+ // hokey arithmetic with booleans
+ if (*aDT + *aDD + bNonList > 1) {
+ *aMixed = true;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+HTMLEditRules::GetAlignment(bool* aMixed,
+ nsIHTMLEditor::EAlignment* aAlign)
+{
+ MOZ_ASSERT(aMixed && aAlign);
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+
+ // For now, just return first alignment. We'll lie about if it's mixed.
+ // This is for efficiency given that our current ui doesn't care if it's
+ // mixed.
+ // cmanske: NOT TRUE! We would like to pay attention to mixed state in Format
+ // | Align submenu!
+
+ // This routine assumes that alignment is done ONLY via divs
+
+ // Default alignment is left
+ *aMixed = false;
+ *aAlign = nsIHTMLEditor::eLeft;
+
+ // Get selection
+ NS_ENSURE_STATE(htmlEditor->GetSelection());
+ OwningNonNull<Selection> selection = *htmlEditor->GetSelection();
+
+ // Get selection location
+ NS_ENSURE_TRUE(htmlEditor->GetRoot(), NS_ERROR_FAILURE);
+ OwningNonNull<Element> root = *htmlEditor->GetRoot();
+
+ int32_t rootOffset = root->GetParentNode() ?
+ root->GetParentNode()->IndexOf(root) : -1;
+
+ NS_ENSURE_STATE(selection->GetRangeAt(0) &&
+ selection->GetRangeAt(0)->GetStartParent());
+ OwningNonNull<nsINode> parent = *selection->GetRangeAt(0)->GetStartParent();
+ int32_t offset = selection->GetRangeAt(0)->StartOffset();
+
+ // Is the selection collapsed?
+ nsCOMPtr<nsINode> nodeToExamine;
+ if (selection->Collapsed() || parent->GetAsText()) {
+ // If selection is collapsed, we want to look at 'parent' and its ancestors
+ // for divs with alignment on them. If we are in a text node, then that is
+ // the node of interest.
+ nodeToExamine = parent;
+ } else if (parent->IsHTMLElement(nsGkAtoms::html) && offset == rootOffset) {
+ // If we have selected the body, let's look at the first editable node
+ nodeToExamine = htmlEditor->GetNextNode(parent, offset, true);
+ } else {
+ nsTArray<RefPtr<nsRange>> arrayOfRanges;
+ GetPromotedRanges(selection, arrayOfRanges, EditAction::align);
+
+ // Use these ranges to construct a list of nodes to act on.
+ nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
+ nsresult rv = GetNodesForOperation(arrayOfRanges, arrayOfNodes,
+ EditAction::align, TouchContent::no);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nodeToExamine = arrayOfNodes.SafeElementAt(0);
+ }
+
+ NS_ENSURE_TRUE(nodeToExamine, NS_ERROR_NULL_POINTER);
+
+ NS_NAMED_LITERAL_STRING(typeAttrName, "align");
+ nsCOMPtr<Element> blockParent = htmlEditor->GetBlock(*nodeToExamine);
+
+ NS_ENSURE_TRUE(blockParent, NS_ERROR_FAILURE);
+
+ if (htmlEditor->IsCSSEnabled() &&
+ htmlEditor->mCSSEditUtils->IsCSSEditableProperty(blockParent, nullptr,
+ &typeAttrName)) {
+ // We are in CSS mode and we know how to align this element with CSS
+ nsAutoString value;
+ // Let's get the value(s) of text-align or margin-left/margin-right
+ htmlEditor->mCSSEditUtils->GetCSSEquivalentToHTMLInlineStyleSet(
+ blockParent, nullptr, &typeAttrName, value, CSSEditUtils::eComputed);
+ if (value.EqualsLiteral("center") ||
+ value.EqualsLiteral("-moz-center") ||
+ value.EqualsLiteral("auto auto")) {
+ *aAlign = nsIHTMLEditor::eCenter;
+ return NS_OK;
+ }
+ if (value.EqualsLiteral("right") ||
+ value.EqualsLiteral("-moz-right") ||
+ value.EqualsLiteral("auto 0px")) {
+ *aAlign = nsIHTMLEditor::eRight;
+ return NS_OK;
+ }
+ if (value.EqualsLiteral("justify")) {
+ *aAlign = nsIHTMLEditor::eJustify;
+ return NS_OK;
+ }
+ *aAlign = nsIHTMLEditor::eLeft;
+ return NS_OK;
+ }
+
+ // Check up the ladder for divs with alignment
+ bool isFirstNodeToExamine = true;
+ for (; nodeToExamine; nodeToExamine = nodeToExamine->GetParentNode()) {
+ if (!isFirstNodeToExamine &&
+ nodeToExamine->IsHTMLElement(nsGkAtoms::table)) {
+ // The node to examine is a table and this is not the first node we
+ // examine; let's break here to materialize the 'inline-block' behaviour
+ // of html tables regarding to text alignment
+ return NS_OK;
+ }
+ if (HTMLEditUtils::SupportsAlignAttr(GetAsDOMNode(nodeToExamine))) {
+ // Check for alignment
+ nsAutoString typeAttrVal;
+ nodeToExamine->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::align,
+ typeAttrVal);
+ ToLowerCase(typeAttrVal);
+ if (!typeAttrVal.IsEmpty()) {
+ if (typeAttrVal.EqualsLiteral("center")) {
+ *aAlign = nsIHTMLEditor::eCenter;
+ } else if (typeAttrVal.EqualsLiteral("right")) {
+ *aAlign = nsIHTMLEditor::eRight;
+ } else if (typeAttrVal.EqualsLiteral("justify")) {
+ *aAlign = nsIHTMLEditor::eJustify;
+ } else {
+ *aAlign = nsIHTMLEditor::eLeft;
+ }
+ return NS_OK;
+ }
+ }
+ isFirstNodeToExamine = false;
+ }
+ return NS_OK;
+}
+
+static nsIAtom& MarginPropertyAtomForIndent(CSSEditUtils& aHTMLCSSUtils,
+ nsINode& aNode)
+{
+ nsAutoString direction;
+ aHTMLCSSUtils.GetComputedProperty(aNode, *nsGkAtoms::direction, direction);
+ return direction.EqualsLiteral("rtl") ?
+ *nsGkAtoms::marginRight : *nsGkAtoms::marginLeft;
+}
+
+nsresult
+HTMLEditRules::GetIndentState(bool* aCanIndent,
+ bool* aCanOutdent)
+{
+ // XXX Looks like that this is implementation of
+ // nsIHTMLEditor::getIndentState() however nobody calls this method
+ // even with the interface method.
+ NS_ENSURE_TRUE(aCanIndent && aCanOutdent, NS_ERROR_FAILURE);
+ *aCanIndent = true;
+ *aCanOutdent = false;
+
+ // get selection
+ NS_ENSURE_STATE(mHTMLEditor && mHTMLEditor->GetSelection());
+ OwningNonNull<Selection> selection = *mHTMLEditor->GetSelection();
+
+ // contruct a list of nodes to act on.
+ nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
+ nsresult rv = GetNodesFromSelection(*selection, EditAction::indent,
+ arrayOfNodes, TouchContent::no);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // examine nodes in selection for blockquotes or list elements;
+ // these we can outdent. Note that we return true for canOutdent
+ // if *any* of the selection is outdentable, rather than all of it.
+ NS_ENSURE_STATE(mHTMLEditor);
+ bool useCSS = mHTMLEditor->IsCSSEnabled();
+ for (auto& curNode : Reversed(arrayOfNodes)) {
+ if (HTMLEditUtils::IsNodeThatCanOutdent(GetAsDOMNode(curNode))) {
+ *aCanOutdent = true;
+ break;
+ } else if (useCSS) {
+ // we are in CSS mode, indentation is done using the margin-left (or margin-right) property
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsIAtom& marginProperty =
+ MarginPropertyAtomForIndent(*mHTMLEditor->mCSSEditUtils, curNode);
+ nsAutoString value;
+ // retrieve its specified value
+ NS_ENSURE_STATE(mHTMLEditor);
+ mHTMLEditor->mCSSEditUtils->GetSpecifiedProperty(*curNode,
+ marginProperty, value);
+ float f;
+ nsCOMPtr<nsIAtom> unit;
+ // get its number part and its unit
+ NS_ENSURE_STATE(mHTMLEditor);
+ mHTMLEditor->mCSSEditUtils->ParseLength(value, &f, getter_AddRefs(unit));
+ // if the number part is strictly positive, outdent is possible
+ if (0 < f) {
+ *aCanOutdent = true;
+ break;
+ }
+ }
+ }
+
+ if (!*aCanOutdent) {
+ // if we haven't found something to outdent yet, also check the parents
+ // of selection endpoints. We might have a blockquote or list item
+ // in the parent hierarchy.
+
+ // gather up info we need for test
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsCOMPtr<nsIDOMNode> parent, tmp, root = do_QueryInterface(mHTMLEditor->GetRoot());
+ NS_ENSURE_TRUE(root, NS_ERROR_NULL_POINTER);
+ int32_t selOffset;
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<Selection> selection = mHTMLEditor->GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+
+ // test start parent hierarchy
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->GetStartNodeAndOffset(selection, getter_AddRefs(parent),
+ &selOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ while (parent && parent != root) {
+ if (HTMLEditUtils::IsNodeThatCanOutdent(parent)) {
+ *aCanOutdent = true;
+ break;
+ }
+ tmp = parent;
+ tmp->GetParentNode(getter_AddRefs(parent));
+ }
+
+ // test end parent hierarchy
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->GetEndNodeAndOffset(selection, getter_AddRefs(parent),
+ &selOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ while (parent && parent != root) {
+ if (HTMLEditUtils::IsNodeThatCanOutdent(parent)) {
+ *aCanOutdent = true;
+ break;
+ }
+ tmp = parent;
+ tmp->GetParentNode(getter_AddRefs(parent));
+ }
+ }
+ return NS_OK;
+}
+
+
+nsresult
+HTMLEditRules::GetParagraphState(bool* aMixed,
+ nsAString& outFormat)
+{
+ // This routine is *heavily* tied to our ui choices in the paragraph
+ // style popup. I can't see a way around that.
+ NS_ENSURE_TRUE(aMixed, NS_ERROR_NULL_POINTER);
+ *aMixed = true;
+ outFormat.Truncate(0);
+
+ bool bMixed = false;
+ // using "x" as an uninitialized value, since "" is meaningful
+ nsAutoString formatStr(NS_LITERAL_STRING("x"));
+
+ nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
+ nsresult rv = GetParagraphFormatNodes(arrayOfNodes, TouchContent::no);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // post process list. We need to replace any block nodes that are not format
+ // nodes with their content. This is so we only have to look "up" the hierarchy
+ // to find format nodes, instead of both up and down.
+ for (int32_t i = arrayOfNodes.Length() - 1; i >= 0; i--) {
+ auto& curNode = arrayOfNodes[i];
+ nsAutoString format;
+ // if it is a known format node we have it easy
+ if (IsBlockNode(curNode) && !HTMLEditUtils::IsFormatNode(curNode)) {
+ // arrayOfNodes.RemoveObject(curNode);
+ rv = AppendInnerFormatNodes(arrayOfNodes, curNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ // we might have an empty node list. if so, find selection parent
+ // and put that on the list
+ if (arrayOfNodes.IsEmpty()) {
+ nsCOMPtr<nsINode> selNode;
+ int32_t selOffset;
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<Selection> selection = mHTMLEditor->GetSelection();
+ NS_ENSURE_STATE(selection);
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->GetStartNodeAndOffset(selection, getter_AddRefs(selNode),
+ &selOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(selNode, NS_ERROR_NULL_POINTER);
+ arrayOfNodes.AppendElement(*selNode);
+ }
+
+ // remember root node
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsCOMPtr<nsIDOMElement> rootElem = do_QueryInterface(mHTMLEditor->GetRoot());
+ NS_ENSURE_TRUE(rootElem, NS_ERROR_NULL_POINTER);
+
+ // loop through the nodes in selection and examine their paragraph format
+ for (auto& curNode : Reversed(arrayOfNodes)) {
+ nsAutoString format;
+ // if it is a known format node we have it easy
+ if (HTMLEditUtils::IsFormatNode(curNode)) {
+ GetFormatString(GetAsDOMNode(curNode), format);
+ } else if (IsBlockNode(curNode)) {
+ // this is a div or some other non-format block.
+ // we should ignore it. Its children were appended to this list
+ // by AppendInnerFormatNodes() call above. We will get needed
+ // info when we examine them instead.
+ continue;
+ } else {
+ nsCOMPtr<nsIDOMNode> node, tmp = GetAsDOMNode(curNode);
+ tmp->GetParentNode(getter_AddRefs(node));
+ while (node) {
+ if (node == rootElem) {
+ format.Truncate(0);
+ break;
+ } else if (HTMLEditUtils::IsFormatNode(node)) {
+ GetFormatString(node, format);
+ break;
+ }
+ // else keep looking up
+ tmp = node;
+ tmp->GetParentNode(getter_AddRefs(node));
+ }
+ }
+
+ // if this is the first node, we've found, remember it as the format
+ if (formatStr.EqualsLiteral("x")) {
+ formatStr = format;
+ }
+ // else make sure it matches previously found format
+ else if (format != formatStr) {
+ bMixed = true;
+ break;
+ }
+ }
+
+ *aMixed = bMixed;
+ outFormat = formatStr;
+ return NS_OK;
+}
+
+nsresult
+HTMLEditRules::AppendInnerFormatNodes(nsTArray<OwningNonNull<nsINode>>& aArray,
+ nsINode* aNode)
+{
+ MOZ_ASSERT(aNode);
+
+ // we only need to place any one inline inside this node onto
+ // the list. They are all the same for purposes of determining
+ // paragraph style. We use foundInline to track this as we are
+ // going through the children in the loop below.
+ bool foundInline = false;
+ for (nsIContent* child = aNode->GetFirstChild();
+ child;
+ child = child->GetNextSibling()) {
+ bool isBlock = IsBlockNode(*child);
+ bool isFormat = HTMLEditUtils::IsFormatNode(child);
+ if (isBlock && !isFormat) {
+ // if it's a div, etc., recurse
+ AppendInnerFormatNodes(aArray, child);
+ } else if (isFormat) {
+ aArray.AppendElement(*child);
+ } else if (!foundInline) {
+ // if this is the first inline we've found, use it
+ foundInline = true;
+ aArray.AppendElement(*child);
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+HTMLEditRules::GetFormatString(nsIDOMNode* aNode,
+ nsAString& outFormat)
+{
+ NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER);
+
+ if (HTMLEditUtils::IsFormatNode(aNode)) {
+ nsCOMPtr<nsIAtom> atom = EditorBase::GetTag(aNode);
+ atom->ToString(outFormat);
+ } else {
+ outFormat.Truncate();
+ }
+ return NS_OK;
+}
+
+void
+HTMLEditRules::WillInsert(Selection& aSelection,
+ bool* aCancel)
+{
+ MOZ_ASSERT(aCancel);
+
+ TextEditRules::WillInsert(aSelection, aCancel);
+
+ NS_ENSURE_TRUE_VOID(mHTMLEditor);
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+
+ // Adjust selection to prevent insertion after a moz-BR. This next only
+ // works for collapsed selections right now, because selection is a pain to
+ // work with when not collapsed. (no good way to extend start or end of
+ // selection), so we ignore those types of selections.
+ if (!aSelection.Collapsed()) {
+ return;
+ }
+
+ // If we are after a mozBR in the same block, then move selection to be
+ // before it
+ NS_ENSURE_TRUE_VOID(aSelection.GetRangeAt(0) &&
+ aSelection.GetRangeAt(0)->GetStartParent());
+ OwningNonNull<nsINode> selNode = *aSelection.GetRangeAt(0)->GetStartParent();
+ int32_t selOffset = aSelection.GetRangeAt(0)->StartOffset();
+
+ // Get prior node
+ nsCOMPtr<nsIContent> priorNode = htmlEditor->GetPriorHTMLNode(selNode,
+ selOffset);
+ if (priorNode && TextEditUtils::IsMozBR(priorNode)) {
+ nsCOMPtr<Element> block1 = htmlEditor->GetBlock(selNode);
+ nsCOMPtr<Element> block2 = htmlEditor->GetBlockNodeParent(priorNode);
+
+ if (block1 && block1 == block2) {
+ // If we are here then the selection is right after a mozBR that is in
+ // the same block as the selection. We need to move the selection start
+ // to be before the mozBR.
+ selNode = priorNode->GetParentNode();
+ selOffset = selNode->IndexOf(priorNode);
+ nsresult rv = aSelection.Collapse(selNode, selOffset);
+ NS_ENSURE_SUCCESS_VOID(rv);
+ }
+ }
+
+ if (mDidDeleteSelection &&
+ (mTheAction == EditAction::insertText ||
+ mTheAction == EditAction::insertIMEText ||
+ mTheAction == EditAction::deleteSelection)) {
+ nsresult rv = ReapplyCachedStyles();
+ NS_ENSURE_SUCCESS_VOID(rv);
+ }
+ // For most actions we want to clear the cached styles, but there are
+ // exceptions
+ if (!IsStyleCachePreservingAction(mTheAction)) {
+ ClearCachedStyles();
+ }
+}
+
+nsresult
+HTMLEditRules::WillInsertText(EditAction aAction,
+ Selection* aSelection,
+ bool* aCancel,
+ bool* aHandled,
+ const nsAString* inString,
+ nsAString* outString,
+ int32_t aMaxLength)
+{
+ if (!aSelection || !aCancel || !aHandled) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ if (inString->IsEmpty() && aAction != EditAction::insertIMEText) {
+ // HACK: this is a fix for bug 19395
+ // I can't outlaw all empty insertions
+ // because IME transaction depend on them
+ // There is more work to do to make the
+ // world safe for IME.
+ *aCancel = true;
+ *aHandled = false;
+ return NS_OK;
+ }
+
+ // initialize out param
+ *aCancel = false;
+ *aHandled = true;
+ // If the selection isn't collapsed, delete it. Don't delete existing inline
+ // tags, because we're hopefully going to insert text (bug 787432).
+ if (!aSelection->Collapsed()) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv =
+ mHTMLEditor->DeleteSelection(nsIEditor::eNone, nsIEditor::eNoStrip);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ WillInsert(*aSelection, aCancel);
+ // initialize out param
+ // we want to ignore result of WillInsert()
+ *aCancel = false;
+
+ // we need to get the doc
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsCOMPtr<nsIDocument> doc = mHTMLEditor->GetDocument();
+ NS_ENSURE_STATE(doc);
+
+ // for every property that is set, insert a new inline style node
+ nsresult rv = CreateStyleForInsertText(*aSelection, *doc);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // get the (collapsed) selection location
+ NS_ENSURE_STATE(mHTMLEditor);
+ NS_ENSURE_STATE(aSelection->GetRangeAt(0));
+ nsCOMPtr<nsINode> selNode = aSelection->GetRangeAt(0)->GetStartParent();
+ int32_t selOffset = aSelection->GetRangeAt(0)->StartOffset();
+ NS_ENSURE_STATE(selNode);
+
+ // dont put text in places that can't have it
+ NS_ENSURE_STATE(mHTMLEditor);
+ if (!mHTMLEditor->IsTextNode(selNode) &&
+ (!mHTMLEditor || !mHTMLEditor->CanContainTag(*selNode,
+ *nsGkAtoms::textTagName))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (aAction == EditAction::insertIMEText) {
+ // Right now the WSRunObject code bails on empty strings, but IME needs
+ // the InsertTextImpl() call to still happen since empty strings are meaningful there.
+ NS_ENSURE_STATE(mHTMLEditor);
+ // If there is one or more IME selections, its minimum offset should be
+ // the insertion point.
+ int32_t IMESelectionOffset =
+ mHTMLEditor->GetIMESelectionStartOffsetIn(selNode);
+ if (IMESelectionOffset >= 0) {
+ selOffset = IMESelectionOffset;
+ }
+ if (inString->IsEmpty()) {
+ rv = mHTMLEditor->InsertTextImpl(*inString, address_of(selNode),
+ &selOffset, doc);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ WSRunObject wsObj(mHTMLEditor, selNode, selOffset);
+ rv = wsObj.InsertText(*inString, address_of(selNode), &selOffset, doc);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+ }
+ // aAction == kInsertText
+ else {
+ // find where we are
+ nsCOMPtr<nsINode> curNode = selNode;
+ int32_t curOffset = selOffset;
+
+ // is our text going to be PREformatted?
+ // We remember this so that we know how to handle tabs.
+ bool isPRE;
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->IsPreformatted(GetAsDOMNode(selNode), &isPRE);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // turn off the edit listener: we know how to
+ // build the "doc changed range" ourselves, and it's
+ // must faster to do it once here than to track all
+ // the changes one at a time.
+ AutoLockListener lockit(&mListenerEnabled);
+
+ // don't spaz my selection in subtransactions
+ NS_ENSURE_STATE(mHTMLEditor);
+ AutoTransactionsConserveSelection dontSpazMySelection(mHTMLEditor);
+ nsAutoString tString(*inString);
+ const char16_t *unicodeBuf = tString.get();
+ int32_t pos = 0;
+ NS_NAMED_LITERAL_STRING(newlineStr, LFSTR);
+
+ // for efficiency, break out the pre case separately. This is because
+ // its a lot cheaper to search the input string for only newlines than
+ // it is to search for both tabs and newlines.
+ if (isPRE || IsPlaintextEditor()) {
+ while (unicodeBuf && pos != -1 &&
+ pos < static_cast<int32_t>(inString->Length())) {
+ int32_t oldPos = pos;
+ int32_t subStrLen;
+ pos = tString.FindChar(nsCRT::LF, oldPos);
+
+ if (pos != -1) {
+ subStrLen = pos - oldPos;
+ // if first char is newline, then use just it
+ if (!subStrLen) {
+ subStrLen = 1;
+ }
+ } else {
+ subStrLen = tString.Length() - oldPos;
+ pos = tString.Length();
+ }
+
+ nsDependentSubstring subStr(tString, oldPos, subStrLen);
+
+ // is it a return?
+ if (subStr.Equals(newlineStr)) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsCOMPtr<Element> br =
+ mHTMLEditor->CreateBRImpl(address_of(curNode), &curOffset,
+ nsIEditor::eNone);
+ NS_ENSURE_STATE(br);
+ pos++;
+ } else {
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->InsertTextImpl(subStr, address_of(curNode),
+ &curOffset, doc);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ } else {
+ NS_NAMED_LITERAL_STRING(tabStr, "\t");
+ NS_NAMED_LITERAL_STRING(spacesStr, " ");
+ char specialChars[] = {TAB, nsCRT::LF, 0};
+ while (unicodeBuf && pos != -1 &&
+ pos < static_cast<int32_t>(inString->Length())) {
+ int32_t oldPos = pos;
+ int32_t subStrLen;
+ pos = tString.FindCharInSet(specialChars, oldPos);
+
+ if (pos != -1) {
+ subStrLen = pos - oldPos;
+ // if first char is newline, then use just it
+ if (!subStrLen) {
+ subStrLen = 1;
+ }
+ } else {
+ subStrLen = tString.Length() - oldPos;
+ pos = tString.Length();
+ }
+
+ nsDependentSubstring subStr(tString, oldPos, subStrLen);
+ NS_ENSURE_STATE(mHTMLEditor);
+ WSRunObject wsObj(mHTMLEditor, curNode, curOffset);
+
+ // is it a tab?
+ if (subStr.Equals(tabStr)) {
+ rv =
+ wsObj.InsertText(spacesStr, address_of(curNode), &curOffset, doc);
+ NS_ENSURE_SUCCESS(rv, rv);
+ pos++;
+ }
+ // is it a return?
+ else if (subStr.Equals(newlineStr)) {
+ nsCOMPtr<Element> br = wsObj.InsertBreak(address_of(curNode),
+ &curOffset,
+ nsIEditor::eNone);
+ NS_ENSURE_TRUE(br, NS_ERROR_FAILURE);
+ pos++;
+ } else {
+ rv = wsObj.InsertText(subStr, address_of(curNode), &curOffset, doc);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ }
+ aSelection->SetInterlinePosition(false);
+ if (curNode) aSelection->Collapse(curNode, curOffset);
+ // manually update the doc changed range so that AfterEdit will clean up
+ // the correct portion of the document.
+ if (!mDocChangeRange) {
+ mDocChangeRange = new nsRange(selNode);
+ }
+ rv = mDocChangeRange->SetStart(selNode, selOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (curNode) {
+ rv = mDocChangeRange->SetEnd(curNode, curOffset);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ rv = mDocChangeRange->SetEnd(selNode, selOffset);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+HTMLEditRules::WillLoadHTML(Selection* aSelection,
+ bool* aCancel)
+{
+ NS_ENSURE_TRUE(aSelection && aCancel, NS_ERROR_NULL_POINTER);
+
+ *aCancel = false;
+
+ // Delete mBogusNode if it exists. If we really need one,
+ // it will be added during post-processing in AfterEditInner().
+
+ if (mBogusNode) {
+ mTextEditor->DeleteNode(mBogusNode);
+ mBogusNode = nullptr;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+HTMLEditRules::WillInsertBreak(Selection& aSelection,
+ bool* aCancel,
+ bool* aHandled)
+{
+ MOZ_ASSERT(aCancel && aHandled);
+ *aCancel = false;
+ *aHandled = false;
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+
+ // If the selection isn't collapsed, delete it.
+ if (!aSelection.Collapsed()) {
+ nsresult rv =
+ htmlEditor->DeleteSelection(nsIEditor::eNone, nsIEditor::eStrip);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ WillInsert(aSelection, aCancel);
+
+ // Initialize out param. We want to ignore result of WillInsert().
+ *aCancel = false;
+
+ // Split any mailcites in the way. Should we abort this if we encounter
+ // table cell boundaries?
+ if (IsMailEditor()) {
+ nsresult rv = SplitMailCites(&aSelection, aHandled);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (*aHandled) {
+ return NS_OK;
+ }
+ }
+
+ // Smart splitting rules
+ NS_ENSURE_TRUE(aSelection.GetRangeAt(0) &&
+ aSelection.GetRangeAt(0)->GetStartParent(),
+ NS_ERROR_FAILURE);
+ OwningNonNull<nsINode> node = *aSelection.GetRangeAt(0)->GetStartParent();
+ int32_t offset = aSelection.GetRangeAt(0)->StartOffset();
+
+ // Do nothing if the node is read-only
+ if (!htmlEditor->IsModifiableNode(node)) {
+ *aCancel = true;
+ return NS_OK;
+ }
+
+ // Identify the block
+ nsCOMPtr<Element> blockParent = htmlEditor->GetBlock(node);
+ NS_ENSURE_TRUE(blockParent, NS_ERROR_FAILURE);
+
+ // If the active editing host is an inline element, or if the active editing
+ // host is the block parent itself, just append a br.
+ nsCOMPtr<Element> host = htmlEditor->GetActiveEditingHost();
+ if (!EditorUtils::IsDescendantOf(blockParent, host)) {
+ nsresult rv = StandardBreakImpl(node, offset, aSelection);
+ NS_ENSURE_SUCCESS(rv, rv);
+ *aHandled = true;
+ return NS_OK;
+ }
+
+ // If block is empty, populate with br. (For example, imagine a div that
+ // contains the word "text". The user selects "text" and types return.
+ // "Text" is deleted leaving an empty block. We want to put in one br to
+ // make block have a line. Then code further below will put in a second br.)
+ bool isEmpty;
+ IsEmptyBlock(*blockParent, &isEmpty);
+ if (isEmpty) {
+ nsCOMPtr<Element> br = htmlEditor->CreateBR(blockParent,
+ blockParent->Length());
+ NS_ENSURE_STATE(br);
+ }
+
+ nsCOMPtr<Element> listItem = IsInListItem(blockParent);
+ if (listItem && listItem != host) {
+ ReturnInListItem(aSelection, *listItem, node, offset);
+ *aHandled = true;
+ return NS_OK;
+ } else if (HTMLEditUtils::IsHeader(*blockParent)) {
+ // Headers: close (or split) header
+ ReturnInHeader(aSelection, *blockParent, node, offset);
+ *aHandled = true;
+ return NS_OK;
+ } else if (blockParent->IsHTMLElement(nsGkAtoms::p)) {
+ // Paragraphs: special rules to look for <br>s
+ nsresult rv =
+ ReturnInParagraph(&aSelection, GetAsDOMNode(blockParent),
+ GetAsDOMNode(node), offset, aCancel, aHandled);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Fall through, we may not have handled it in ReturnInParagraph()
+ }
+
+ // If not already handled then do the standard thing
+ if (!(*aHandled)) {
+ *aHandled = true;
+ return StandardBreakImpl(node, offset, aSelection);
+ }
+ return NS_OK;
+}
+
+nsresult
+HTMLEditRules::StandardBreakImpl(nsINode& aNode,
+ int32_t aOffset,
+ Selection& aSelection)
+{
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+
+ nsCOMPtr<Element> brNode;
+ bool bAfterBlock = false;
+ bool bBeforeBlock = false;
+ nsCOMPtr<nsINode> node = &aNode;
+
+ if (IsPlaintextEditor()) {
+ brNode = htmlEditor->CreateBR(node, aOffset);
+ NS_ENSURE_STATE(brNode);
+ } else {
+ WSRunObject wsObj(htmlEditor, node, aOffset);
+ int32_t visOffset = 0;
+ WSType wsType;
+ nsCOMPtr<nsINode> visNode;
+ wsObj.PriorVisibleNode(node, aOffset, address_of(visNode),
+ &visOffset, &wsType);
+ if (wsType & WSType::block) {
+ bAfterBlock = true;
+ }
+ wsObj.NextVisibleNode(node, aOffset, address_of(visNode),
+ &visOffset, &wsType);
+ if (wsType & WSType::block) {
+ bBeforeBlock = true;
+ }
+ nsCOMPtr<nsIDOMNode> linkDOMNode;
+ if (htmlEditor->IsInLink(GetAsDOMNode(node), address_of(linkDOMNode))) {
+ // Split the link
+ nsCOMPtr<Element> linkNode = do_QueryInterface(linkDOMNode);
+ NS_ENSURE_STATE(linkNode || !linkDOMNode);
+ nsCOMPtr<nsINode> linkParent = linkNode->GetParentNode();
+ aOffset = htmlEditor->SplitNodeDeep(*linkNode, *node->AsContent(),
+ aOffset,
+ HTMLEditor::EmptyContainers::no);
+ NS_ENSURE_STATE(aOffset != -1);
+ node = linkParent;
+ }
+ brNode = wsObj.InsertBreak(address_of(node), &aOffset, nsIEditor::eNone);
+ NS_ENSURE_TRUE(brNode, NS_ERROR_FAILURE);
+ }
+ node = brNode->GetParentNode();
+ NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER);
+ int32_t offset = node->IndexOf(brNode);
+ if (bAfterBlock && bBeforeBlock) {
+ // We just placed a br between block boundaries. This is the one case
+ // where we want the selection to be before the br we just placed, as the
+ // br will be on a new line, rather than at end of prior line.
+ aSelection.SetInterlinePosition(true);
+ nsresult rv = aSelection.Collapse(node, offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ WSRunObject wsObj(htmlEditor, node, offset + 1);
+ nsCOMPtr<nsINode> secondBR;
+ int32_t visOffset = 0;
+ WSType wsType;
+ wsObj.NextVisibleNode(node, offset + 1, address_of(secondBR),
+ &visOffset, &wsType);
+ if (wsType == WSType::br) {
+ // The next thing after the break we inserted is another break. Move the
+ // second break to be the first break's sibling. This will prevent them
+ // from being in different inline nodes, which would break
+ // SetInterlinePosition(). It will also assure that if the user clicks
+ // away and then clicks back on their new blank line, they will still get
+ // the style from the line above.
+ nsCOMPtr<nsINode> brParent = secondBR->GetParentNode();
+ int32_t brOffset = brParent ? brParent->IndexOf(secondBR) : -1;
+ if (brParent != node || brOffset != offset + 1) {
+ nsresult rv =
+ htmlEditor->MoveNode(secondBR->AsContent(), node, offset + 1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ // SetInterlinePosition(true) means we want the caret to stick to the
+ // content on the "right". We want the caret to stick to whatever is past
+ // the break. This is because the break is on the same line we were on,
+ // but the next content will be on the following line.
+
+ // An exception to this is if the break has a next sibling that is a block
+ // node. Then we stick to the left to avoid an uber caret.
+ nsCOMPtr<nsIContent> siblingNode = brNode->GetNextSibling();
+ if (siblingNode && IsBlockNode(*siblingNode)) {
+ aSelection.SetInterlinePosition(false);
+ } else {
+ aSelection.SetInterlinePosition(true);
+ }
+ nsresult rv = aSelection.Collapse(node, offset + 1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+nsresult
+HTMLEditRules::DidInsertBreak(Selection* aSelection,
+ nsresult aResult)
+{
+ return NS_OK;
+}
+
+nsresult
+HTMLEditRules::SplitMailCites(Selection* aSelection,
+ bool* aHandled)
+{
+ NS_ENSURE_TRUE(aSelection && aHandled, NS_ERROR_NULL_POINTER);
+ nsCOMPtr<nsIContent> leftCite, rightCite;
+ nsCOMPtr<nsINode> selNode;
+ nsCOMPtr<Element> citeNode;
+ int32_t selOffset;
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv =
+ mHTMLEditor->GetStartNodeAndOffset(aSelection, getter_AddRefs(selNode),
+ &selOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ citeNode = GetTopEnclosingMailCite(*selNode);
+ if (citeNode) {
+ // If our selection is just before a break, nudge it to be
+ // just after it. This does two things for us. It saves us the trouble of having to add
+ // a break here ourselves to preserve the "blockness" of the inline span mailquote
+ // (in the inline case), and :
+ // it means the break won't end up making an empty line that happens to be inside a
+ // mailquote (in either inline or block case).
+ // The latter can confuse a user if they click there and start typing,
+ // because being in the mailquote may affect wrapping behavior, or font color, etc.
+ NS_ENSURE_STATE(mHTMLEditor);
+ WSRunObject wsObj(mHTMLEditor, selNode, selOffset);
+ nsCOMPtr<nsINode> visNode;
+ int32_t visOffset=0;
+ WSType wsType;
+ wsObj.NextVisibleNode(selNode, selOffset, address_of(visNode),
+ &visOffset, &wsType);
+ if (wsType == WSType::br) {
+ // ok, we are just before a break. is it inside the mailquote?
+ if (visNode != citeNode && citeNode->Contains(visNode)) {
+ // it is. so lets reset our selection to be just after it.
+ NS_ENSURE_STATE(mHTMLEditor);
+ selNode = mHTMLEditor->GetNodeLocation(visNode, &selOffset);
+ ++selOffset;
+ }
+ }
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ NS_ENSURE_STATE(selNode->IsContent());
+ int32_t newOffset = mHTMLEditor->SplitNodeDeep(*citeNode,
+ *selNode->AsContent(), selOffset, HTMLEditor::EmptyContainers::no,
+ getter_AddRefs(leftCite), getter_AddRefs(rightCite));
+ NS_ENSURE_STATE(newOffset != -1);
+
+ // Add an invisible <br> to the end of the left part if it was a <span> of
+ // style="display: block". This is important, since when serialising the
+ // cite to plain text, the span which caused the visual break is discarded.
+ // So the added <br> will guarantee that the serialiser will insert a
+ // break where the user saw one.
+ if (leftCite &&
+ leftCite->IsHTMLElement(nsGkAtoms::span) &&
+ leftCite->GetPrimaryFrame()->IsFrameOfType(nsIFrame::eBlockFrame)) {
+ nsCOMPtr<nsINode> lastChild = leftCite->GetLastChild();
+ if (lastChild && !lastChild->IsHTMLElement(nsGkAtoms::br)) {
+ // We ignore the result here.
+ nsCOMPtr<Element> invisBR =
+ mHTMLEditor->CreateBR(leftCite, leftCite->Length());
+ }
+ }
+
+ selNode = citeNode->GetParentNode();
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsCOMPtr<Element> brNode = mHTMLEditor->CreateBR(selNode, newOffset);
+ NS_ENSURE_STATE(brNode);
+
+ // want selection before the break, and on same line
+ aSelection->SetInterlinePosition(true);
+ rv = aSelection->Collapse(selNode, newOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // if citeNode wasn't a block, we might also want another break before it.
+ // We need to examine the content both before the br we just added and also
+ // just after it. If we don't have another br or block boundary adjacent,
+ // then we will need a 2nd br added to achieve blank line that user expects.
+ if (IsInlineNode(*citeNode)) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ WSRunObject wsObj(mHTMLEditor, selNode, newOffset);
+ nsCOMPtr<nsINode> visNode;
+ int32_t visOffset=0;
+ WSType wsType;
+ wsObj.PriorVisibleNode(selNode, newOffset, address_of(visNode),
+ &visOffset, &wsType);
+ if (wsType == WSType::normalWS || wsType == WSType::text ||
+ wsType == WSType::special) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ WSRunObject wsObjAfterBR(mHTMLEditor, selNode, newOffset+1);
+ wsObjAfterBR.NextVisibleNode(selNode, newOffset + 1,
+ address_of(visNode), &visOffset, &wsType);
+ if (wsType == WSType::normalWS || wsType == WSType::text ||
+ wsType == WSType::special ||
+ // In case we're at the very end.
+ wsType == WSType::thisBlock) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ brNode = mHTMLEditor->CreateBR(selNode, newOffset);
+ NS_ENSURE_STATE(brNode);
+ }
+ }
+ }
+
+ // delete any empty cites
+ bool bEmptyCite = false;
+ if (leftCite) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->IsEmptyNode(leftCite, &bEmptyCite, true, false);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ if (bEmptyCite) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->DeleteNode(leftCite);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+ }
+
+ if (rightCite) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->IsEmptyNode(rightCite, &bEmptyCite, true, false);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ if (bEmptyCite) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->DeleteNode(rightCite);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+ }
+ *aHandled = true;
+ }
+ return NS_OK;
+}
+
+
+nsresult
+HTMLEditRules::WillDeleteSelection(Selection* aSelection,
+ nsIEditor::EDirection aAction,
+ nsIEditor::EStripWrappers aStripWrappers,
+ bool* aCancel,
+ bool* aHandled)
+{
+ MOZ_ASSERT(aStripWrappers == nsIEditor::eStrip ||
+ aStripWrappers == nsIEditor::eNoStrip);
+
+ if (!aSelection || !aCancel || !aHandled) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ // Initialize out params
+ *aCancel = false;
+ *aHandled = false;
+
+ // Remember that we did a selection deletion. Used by CreateStyleForInsertText()
+ mDidDeleteSelection = true;
+
+ // If there is only bogus content, cancel the operation
+ if (mBogusNode) {
+ *aCancel = true;
+ return NS_OK;
+ }
+
+ // First check for table selection mode. If so, hand off to table editor.
+ nsCOMPtr<nsIDOMElement> cell;
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv =
+ mHTMLEditor->GetFirstSelectedCell(nullptr, getter_AddRefs(cell));
+ if (NS_SUCCEEDED(rv) && cell) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->DeleteTableCellContents();
+ *aHandled = true;
+ return rv;
+ }
+ cell = nullptr;
+
+ // origCollapsed is used later to determine whether we should join blocks. We
+ // don't really care about bCollapsed because it will be modified by
+ // ExtendSelectionForDelete later. JoinBlocks should happen if the original
+ // selection is collapsed and the cursor is at the end of a block element, in
+ // which case ExtendSelectionForDelete would always make the selection not
+ // collapsed.
+ bool bCollapsed = aSelection->Collapsed();
+ bool join = false;
+ bool origCollapsed = bCollapsed;
+
+ nsCOMPtr<nsINode> selNode;
+ int32_t selOffset;
+
+ NS_ENSURE_STATE(aSelection->GetRangeAt(0));
+ nsCOMPtr<nsINode> startNode = aSelection->GetRangeAt(0)->GetStartParent();
+ int32_t startOffset = aSelection->GetRangeAt(0)->StartOffset();
+ NS_ENSURE_TRUE(startNode, NS_ERROR_FAILURE);
+
+ if (bCollapsed) {
+ // If we are inside an empty block, delete it.
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsCOMPtr<Element> host = mHTMLEditor->GetActiveEditingHost();
+ NS_ENSURE_TRUE(host, NS_ERROR_FAILURE);
+ rv = CheckForEmptyBlock(startNode, host, aSelection, aAction, aHandled);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (*aHandled) {
+ return NS_OK;
+ }
+
+ // Test for distance between caret and text that will be deleted
+ rv = CheckBidiLevelForDeletion(aSelection, GetAsDOMNode(startNode),
+ startOffset, aAction, aCancel);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (*aCancel) {
+ return NS_OK;
+ }
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->ExtendSelectionForDelete(aSelection, &aAction);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We should delete nothing.
+ if (aAction == nsIEditor::eNone) {
+ return NS_OK;
+ }
+
+ // ExtendSelectionForDelete() may have changed the selection, update it
+ NS_ENSURE_STATE(aSelection->GetRangeAt(0));
+ startNode = aSelection->GetRangeAt(0)->GetStartParent();
+ startOffset = aSelection->GetRangeAt(0)->StartOffset();
+ NS_ENSURE_TRUE(startNode, NS_ERROR_FAILURE);
+
+ bCollapsed = aSelection->Collapsed();
+ }
+
+ if (bCollapsed) {
+ // What's in the direction we are deleting?
+ NS_ENSURE_STATE(mHTMLEditor);
+ WSRunObject wsObj(mHTMLEditor, startNode, startOffset);
+ nsCOMPtr<nsINode> visNode;
+ int32_t visOffset;
+ WSType wsType;
+
+ // Find next visible node
+ if (aAction == nsIEditor::eNext) {
+ wsObj.NextVisibleNode(startNode, startOffset, address_of(visNode),
+ &visOffset, &wsType);
+ } else {
+ wsObj.PriorVisibleNode(startNode, startOffset, address_of(visNode),
+ &visOffset, &wsType);
+ }
+
+ if (!visNode) {
+ // Can't find anything to delete!
+ *aCancel = true;
+ // XXX This is the result of mHTMLEditor->GetFirstSelectedCell().
+ // The value could be both an error and NS_OK.
+ return rv;
+ }
+
+ if (wsType == WSType::normalWS) {
+ // We found some visible ws to delete. Let ws code handle it.
+ *aHandled = true;
+ if (aAction == nsIEditor::eNext) {
+ rv = wsObj.DeleteWSForward();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ rv = wsObj.DeleteWSBackward();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+ return InsertBRIfNeeded(aSelection);
+ }
+
+ if (wsType == WSType::text) {
+ // Found normal text to delete.
+ OwningNonNull<Text> nodeAsText = *visNode->GetAsText();
+ int32_t so = visOffset;
+ int32_t eo = visOffset + 1;
+ if (aAction == nsIEditor::ePrevious) {
+ if (!so) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ so--;
+ eo--;
+ // Bug 1068979: delete both codepoints if surrogate pair
+ if (so > 0) {
+ const nsTextFragment *text = nodeAsText->GetText();
+ if (NS_IS_LOW_SURROGATE(text->CharAt(so)) &&
+ NS_IS_HIGH_SURROGATE(text->CharAt(so - 1))) {
+ so--;
+ }
+ }
+ } else {
+ RefPtr<nsRange> range = aSelection->GetRangeAt(0);
+ NS_ENSURE_STATE(range);
+
+ NS_ASSERTION(range->GetStartParent() == visNode,
+ "selection start not in visNode");
+ NS_ASSERTION(range->GetEndParent() == visNode,
+ "selection end not in visNode");
+
+ so = range->StartOffset();
+ eo = range->EndOffset();
+ }
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = WSRunObject::PrepareToDeleteRange(mHTMLEditor, address_of(visNode),
+ &so, address_of(visNode), &eo);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_STATE(mHTMLEditor);
+ *aHandled = true;
+ rv = mHTMLEditor->DeleteText(nodeAsText, std::min(so, eo),
+ DeprecatedAbs(eo - so));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // XXX When Backspace key is pressed, Chromium removes following empty
+ // text nodes when removing the last character of the non-empty text
+ // node. However, Edge never removes empty text nodes even if
+ // selection is in the following empty text node(s). For now, we
+ // should keep our traditional behavior same as Edge for backward
+ // compatibility.
+ // XXX When Delete key is pressed, Edge removes all preceding empty
+ // text nodes when removing the first character of the non-empty
+ // text node. Chromium removes only selected empty text node and
+ // following empty text nodes and the first character of the
+ // non-empty text node. For now, we should keep our traditional
+ // behavior same as Chromium for backward compatibility.
+
+ DeleteNodeIfCollapsedText(nodeAsText);
+
+ rv = InsertBRIfNeeded(aSelection);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Remember that we did a ranged delete for the benefit of
+ // AfterEditInner().
+ mDidRangedDelete = true;
+
+ return NS_OK;
+ }
+
+ if (wsType == WSType::special || wsType == WSType::br ||
+ visNode->IsHTMLElement(nsGkAtoms::hr)) {
+ // Short circuit for invisible breaks. delete them and recurse.
+ if (visNode->IsHTMLElement(nsGkAtoms::br) &&
+ (!mHTMLEditor || !mHTMLEditor->IsVisBreak(visNode))) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->DeleteNode(visNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return WillDeleteSelection(aSelection, aAction, aStripWrappers,
+ aCancel, aHandled);
+ }
+
+ // Special handling for backspace when positioned after <hr>
+ if (aAction == nsIEditor::ePrevious &&
+ visNode->IsHTMLElement(nsGkAtoms::hr)) {
+ // Only if the caret is positioned at the end-of-hr-line position, we
+ // want to delete the <hr>.
+ //
+ // In other words, we only want to delete, if our selection position
+ // (indicated by startNode and startOffset) is the position directly
+ // after the <hr>, on the same line as the <hr>.
+ //
+ // To detect this case we check:
+ // startNode == parentOfVisNode
+ // and
+ // startOffset -1 == visNodeOffsetToVisNodeParent
+ // and
+ // interline position is false (left)
+ //
+ // In any other case we set the position to startnode -1 and
+ // interlineposition to false, only moving the caret to the
+ // end-of-hr-line position.
+ bool moveOnly = true;
+
+ selNode = visNode->GetParentNode();
+ selOffset = selNode ? selNode->IndexOf(visNode) : -1;
+
+ bool interLineIsRight;
+ rv = aSelection->GetInterlinePosition(&interLineIsRight);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (startNode == selNode && startOffset - 1 == selOffset &&
+ !interLineIsRight) {
+ moveOnly = false;
+ }
+
+ if (moveOnly) {
+ // Go to the position after the <hr>, but to the end of the <hr> line
+ // by setting the interline position to left.
+ ++selOffset;
+ aSelection->Collapse(selNode, selOffset);
+ aSelection->SetInterlinePosition(false);
+ mDidExplicitlySetInterline = true;
+ *aHandled = true;
+
+ // There is one exception to the move only case. If the <hr> is
+ // followed by a <br> we want to delete the <br>.
+
+ WSType otherWSType;
+ nsCOMPtr<nsINode> otherNode;
+ int32_t otherOffset;
+
+ wsObj.NextVisibleNode(startNode, startOffset, address_of(otherNode),
+ &otherOffset, &otherWSType);
+
+ if (otherWSType == WSType::br) {
+ // Delete the <br>
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsCOMPtr<nsIContent> otherContent(do_QueryInterface(otherNode));
+ rv = WSRunObject::PrepareToDeleteNode(mHTMLEditor, otherContent);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->DeleteNode(otherNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+ }
+ // Else continue with normal delete code
+ }
+
+ // Found break or image, or hr.
+ NS_ENSURE_STATE(mHTMLEditor);
+ NS_ENSURE_STATE(visNode->IsContent());
+ rv = WSRunObject::PrepareToDeleteNode(mHTMLEditor, visNode->AsContent());
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Remember sibling to visnode, if any
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsCOMPtr<nsIContent> sibling = mHTMLEditor->GetPriorHTMLSibling(visNode);
+ // Delete the node, and join like nodes if appropriate
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->DeleteNode(visNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // We did something, so let's say so.
+ *aHandled = true;
+ // Is there a prior node and are they siblings?
+ nsCOMPtr<nsINode> stepbrother;
+ if (sibling) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ stepbrother = mHTMLEditor->GetNextHTMLSibling(sibling);
+ }
+ // Are they both text nodes? If so, join them!
+ if (startNode == stepbrother && startNode->GetAsText() &&
+ sibling->GetAsText()) {
+ EditorDOMPoint pt = JoinNodesSmart(*sibling, *startNode->AsContent());
+ NS_ENSURE_STATE(pt.node);
+ // Fix up selection
+ rv = aSelection->Collapse(pt.node, pt.offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ rv = InsertBRIfNeeded(aSelection);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+ }
+
+ if (wsType == WSType::otherBlock) {
+ // Make sure it's not a table element. If so, cancel the operation
+ // (translation: users cannot backspace or delete across table cells)
+ if (HTMLEditUtils::IsTableElement(visNode)) {
+ *aCancel = true;
+ return NS_OK;
+ }
+
+ // Next to a block. See if we are between a block and a br. If so, we
+ // really want to delete the br. Else join content at selection to the
+ // block.
+ bool bDeletedBR = false;
+ WSType otherWSType;
+ nsCOMPtr<nsINode> otherNode;
+ int32_t otherOffset;
+
+ // Find node in other direction
+ if (aAction == nsIEditor::eNext) {
+ wsObj.PriorVisibleNode(startNode, startOffset, address_of(otherNode),
+ &otherOffset, &otherWSType);
+ } else {
+ wsObj.NextVisibleNode(startNode, startOffset, address_of(otherNode),
+ &otherOffset, &otherWSType);
+ }
+
+ // First find the adjacent node in the block
+ nsCOMPtr<nsIContent> leafNode;
+ nsCOMPtr<nsINode> leftNode, rightNode;
+ if (aAction == nsIEditor::ePrevious) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ leafNode = mHTMLEditor->GetLastEditableLeaf(*visNode);
+ leftNode = leafNode;
+ rightNode = startNode;
+ } else {
+ NS_ENSURE_STATE(mHTMLEditor);
+ leafNode = mHTMLEditor->GetFirstEditableLeaf(*visNode);
+ leftNode = startNode;
+ rightNode = leafNode;
+ }
+
+ if (otherNode->IsHTMLElement(nsGkAtoms::br)) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->DeleteNode(otherNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // XXX Only in this case, setting "handled" to true only when it
+ // succeeds?
+ *aHandled = true;
+ bDeletedBR = true;
+ }
+
+ // Don't cross table boundaries
+ if (leftNode && rightNode &&
+ InDifferentTableElements(leftNode, rightNode)) {
+ return NS_OK;
+ }
+
+ if (bDeletedBR) {
+ // Put selection at edge of block and we are done.
+ NS_ENSURE_STATE(leafNode);
+ EditorDOMPoint newSel = GetGoodSelPointForNode(*leafNode, aAction);
+ NS_ENSURE_STATE(newSel.node);
+ aSelection->Collapse(newSel.node, newSel.offset);
+ return NS_OK;
+ }
+
+ // Else we are joining content to block
+
+ nsCOMPtr<nsINode> selPointNode = startNode;
+ int32_t selPointOffset = startOffset;
+ {
+ NS_ENSURE_STATE(mHTMLEditor);
+ AutoTrackDOMPoint tracker(mHTMLEditor->mRangeUpdater,
+ address_of(selPointNode), &selPointOffset);
+ NS_ENSURE_STATE(leftNode && leftNode->IsContent() &&
+ rightNode && rightNode->IsContent());
+ *aHandled = true;
+ rv = JoinBlocks(*leftNode->AsContent(), *rightNode->AsContent(),
+ aCancel);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ aSelection->Collapse(selPointNode, selPointOffset);
+ return NS_OK;
+ }
+
+ if (wsType == WSType::thisBlock) {
+ // At edge of our block. Look beside it and see if we can join to an
+ // adjacent block
+
+ // Make sure it's not a table element. If so, cancel the operation
+ // (translation: users cannot backspace or delete across table cells)
+ if (HTMLEditUtils::IsTableElement(visNode)) {
+ *aCancel = true;
+ return NS_OK;
+ }
+
+ // First find the relevant nodes
+ nsCOMPtr<nsINode> leftNode, rightNode;
+ if (aAction == nsIEditor::ePrevious) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ leftNode = mHTMLEditor->GetPriorHTMLNode(visNode);
+ rightNode = startNode;
+ } else {
+ NS_ENSURE_STATE(mHTMLEditor);
+ rightNode = mHTMLEditor->GetNextHTMLNode(visNode);
+ leftNode = startNode;
+ }
+
+ // Nothing to join
+ if (!leftNode || !rightNode) {
+ *aCancel = true;
+ return NS_OK;
+ }
+
+ // Don't cross table boundaries -- cancel it
+ if (InDifferentTableElements(leftNode, rightNode)) {
+ *aCancel = true;
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsINode> selPointNode = startNode;
+ int32_t selPointOffset = startOffset;
+ {
+ NS_ENSURE_STATE(mHTMLEditor);
+ AutoTrackDOMPoint tracker(mHTMLEditor->mRangeUpdater,
+ address_of(selPointNode), &selPointOffset);
+ NS_ENSURE_STATE(leftNode->IsContent() && rightNode->IsContent());
+ *aHandled = true;
+ rv = JoinBlocks(*leftNode->AsContent(), *rightNode->AsContent(),
+ aCancel);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ aSelection->Collapse(selPointNode, selPointOffset);
+ return NS_OK;
+ }
+ }
+
+
+ // Else we have a non-collapsed selection. First adjust the selection.
+ rv = ExpandSelectionForDeletion(*aSelection);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Remember that we did a ranged delete for the benefit of AfterEditInner().
+ mDidRangedDelete = true;
+
+ // Refresh start and end points
+ NS_ENSURE_STATE(aSelection->GetRangeAt(0));
+ startNode = aSelection->GetRangeAt(0)->GetStartParent();
+ startOffset = aSelection->GetRangeAt(0)->StartOffset();
+ NS_ENSURE_TRUE(startNode, NS_ERROR_FAILURE);
+ nsCOMPtr<nsINode> endNode = aSelection->GetRangeAt(0)->GetEndParent();
+ int32_t endOffset = aSelection->GetRangeAt(0)->EndOffset();
+ NS_ENSURE_TRUE(endNode, NS_ERROR_FAILURE);
+
+ // Figure out if the endpoints are in nodes that can be merged. Adjust
+ // surrounding whitespace in preparation to delete selection.
+ if (!IsPlaintextEditor()) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ AutoTransactionsConserveSelection dontSpazMySelection(mHTMLEditor);
+ rv = WSRunObject::PrepareToDeleteRange(mHTMLEditor,
+ address_of(startNode), &startOffset,
+ address_of(endNode), &endOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ {
+ // Track location of where we are deleting
+ NS_ENSURE_STATE(mHTMLEditor);
+ AutoTrackDOMPoint startTracker(mHTMLEditor->mRangeUpdater,
+ address_of(startNode), &startOffset);
+ AutoTrackDOMPoint endTracker(mHTMLEditor->mRangeUpdater,
+ address_of(endNode), &endOffset);
+ // We are handling all ranged deletions directly now.
+ *aHandled = true;
+
+ if (endNode == startNode) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->DeleteSelectionImpl(aAction, aStripWrappers);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // Figure out mailcite ancestors
+ nsCOMPtr<Element> startCiteNode = GetTopEnclosingMailCite(*startNode);
+ nsCOMPtr<Element> endCiteNode = GetTopEnclosingMailCite(*endNode);
+
+ // If we only have a mailcite at one of the two endpoints, set the
+ // directionality of the deletion so that the selection will end up
+ // outside the mailcite.
+ if (startCiteNode && !endCiteNode) {
+ aAction = nsIEditor::eNext;
+ } else if (!startCiteNode && endCiteNode) {
+ aAction = nsIEditor::ePrevious;
+ }
+
+ // Figure out block parents
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsCOMPtr<Element> leftParent = mHTMLEditor->GetBlock(*startNode);
+ nsCOMPtr<Element> rightParent = mHTMLEditor->GetBlock(*endNode);
+
+ // Are endpoint block parents the same? Use default deletion
+ if (leftParent && leftParent == rightParent) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ mHTMLEditor->DeleteSelectionImpl(aAction, aStripWrappers);
+ } else {
+ // Deleting across blocks. Are the blocks of same type?
+ NS_ENSURE_STATE(leftParent && rightParent);
+
+ // Are the blocks siblings?
+ nsCOMPtr<nsINode> leftBlockParent = leftParent->GetParentNode();
+ nsCOMPtr<nsINode> rightBlockParent = rightParent->GetParentNode();
+
+ // MOOSE: this could conceivably screw up a table.. fix me.
+ NS_ENSURE_STATE(mHTMLEditor);
+ if (leftBlockParent == rightBlockParent &&
+ mHTMLEditor->NodesSameType(GetAsDOMNode(leftParent),
+ GetAsDOMNode(rightParent)) &&
+ // XXX What's special about these three types of block?
+ (leftParent->IsHTMLElement(nsGkAtoms::p) ||
+ HTMLEditUtils::IsListItem(leftParent) ||
+ HTMLEditUtils::IsHeader(*leftParent))) {
+ // First delete the selection
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->DeleteSelectionImpl(aAction, aStripWrappers);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Join blocks
+ NS_ENSURE_STATE(mHTMLEditor);
+ EditorDOMPoint pt =
+ mHTMLEditor->JoinNodeDeep(*leftParent, *rightParent);
+ NS_ENSURE_STATE(pt.node);
+ // Fix up selection
+ rv = aSelection->Collapse(pt.node, pt.offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+ }
+
+ // Else blocks not same type, or not siblings. Delete everything
+ // except table elements.
+ join = true;
+
+ uint32_t rangeCount = aSelection->RangeCount();
+ for (uint32_t rangeIdx = 0; rangeIdx < rangeCount; ++rangeIdx) {
+ OwningNonNull<nsRange> range = *aSelection->GetRangeAt(rangeIdx);
+
+ // Build a list of nodes in the range
+ nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
+ TrivialFunctor functor;
+ DOMSubtreeIterator iter;
+ nsresult rv = iter.Init(*range);
+ NS_ENSURE_SUCCESS(rv, rv);
+ iter.AppendList(functor, arrayOfNodes);
+
+ // Now that we have the list, delete non-table elements
+ int32_t listCount = arrayOfNodes.Length();
+ for (int32_t j = 0; j < listCount; j++) {
+ nsCOMPtr<nsINode> somenode = do_QueryInterface(arrayOfNodes[0]);
+ NS_ENSURE_STATE(somenode);
+ DeleteNonTableElements(somenode);
+ arrayOfNodes.RemoveElementAt(0);
+ // If something visible is deleted, no need to join. Visible means
+ // all nodes except non-visible textnodes and breaks.
+ if (join && origCollapsed) {
+ if (!somenode->IsContent()) {
+ join = false;
+ continue;
+ }
+ nsCOMPtr<nsIContent> content = somenode->AsContent();
+ if (content->NodeType() == nsIDOMNode::TEXT_NODE) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ mHTMLEditor->IsVisTextNode(content, &join, true);
+ } else {
+ NS_ENSURE_STATE(mHTMLEditor);
+ join = content->IsHTMLElement(nsGkAtoms::br) &&
+ !mHTMLEditor->IsVisBreak(somenode);
+ }
+ }
+ }
+ }
+
+ // Check endpoints for possible text deletion. We can assume that if
+ // text node is found, we can delete to end or to begining as
+ // appropriate, since the case where both sel endpoints in same text
+ // node was already handled (we wouldn't be here)
+ if (startNode->GetAsText() &&
+ startNode->Length() > static_cast<uint32_t>(startOffset)) {
+ // Delete to last character
+ OwningNonNull<nsGenericDOMDataNode> dataNode =
+ *static_cast<nsGenericDOMDataNode*>(startNode.get());
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->DeleteText(dataNode, startOffset,
+ startNode->Length() - startOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ if (endNode->GetAsText() && endOffset) {
+ // Delete to first character
+ NS_ENSURE_STATE(mHTMLEditor);
+ OwningNonNull<nsGenericDOMDataNode> dataNode =
+ *static_cast<nsGenericDOMDataNode*>(endNode.get());
+ rv = mHTMLEditor->DeleteText(dataNode, 0, endOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (join) {
+ rv = JoinBlocks(*leftParent, *rightParent, aCancel);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ }
+ }
+
+ // We might have left only collapsed whitespace in the start/end nodes
+ {
+ AutoTrackDOMPoint startTracker(mHTMLEditor->mRangeUpdater,
+ address_of(startNode), &startOffset);
+ AutoTrackDOMPoint endTracker(mHTMLEditor->mRangeUpdater,
+ address_of(endNode), &endOffset);
+
+ DeleteNodeIfCollapsedText(*startNode);
+ DeleteNodeIfCollapsedText(*endNode);
+ }
+
+ // If we're joining blocks: if deleting forward the selection should be
+ // collapsed to the end of the selection, if deleting backward the selection
+ // should be collapsed to the beginning of the selection. But if we're not
+ // joining then the selection should collapse to the beginning of the
+ // selection if we'redeleting forward, because the end of the selection will
+ // still be in the next block. And same thing for deleting backwards
+ // (selection should collapse to the end, because the beginning will still be
+ // in the first block). See Bug 507936
+ if (aAction == (join ? nsIEditor::eNext : nsIEditor::ePrevious)) {
+ rv = aSelection->Collapse(endNode, endOffset);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ rv = aSelection->Collapse(startNode, startOffset);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+/**
+ * If aNode is a text node that contains only collapsed whitespace, delete it.
+ * It doesn't serve any useful purpose, and we don't want it to confuse code
+ * that doesn't correctly skip over it.
+ *
+ * If deleting the node fails (like if it's not editable), the caller should
+ * proceed as usual, so don't return any errors.
+ */
+void
+HTMLEditRules::DeleteNodeIfCollapsedText(nsINode& aNode)
+{
+ if (!aNode.GetAsText()) {
+ return;
+ }
+ bool empty;
+ nsresult rv = mHTMLEditor->IsVisTextNode(aNode.AsContent(), &empty, false);
+ NS_ENSURE_SUCCESS_VOID(rv);
+ if (empty) {
+ mHTMLEditor->DeleteNode(&aNode);
+ }
+}
+
+
+/**
+ * InsertBRIfNeeded() determines if a br is needed for current selection to not
+ * be spastic. If so, it inserts one. Callers responsibility to only call
+ * with collapsed selection.
+ *
+ * @param aSelection The collapsed selection.
+ */
+nsresult
+HTMLEditRules::InsertBRIfNeeded(Selection* aSelection)
+{
+ NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER);
+
+ // get selection
+ nsCOMPtr<nsINode> node;
+ int32_t offset;
+ nsresult rv =
+ mTextEditor->GetStartNodeAndOffset(aSelection,
+ getter_AddRefs(node), &offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(node, NS_ERROR_FAILURE);
+
+ // inline elements don't need any br
+ if (!IsBlockNode(*node)) {
+ return NS_OK;
+ }
+
+ // examine selection
+ NS_ENSURE_STATE(mHTMLEditor);
+ WSRunObject wsObj(mHTMLEditor, node, offset);
+ if (((wsObj.mStartReason & WSType::block) ||
+ (wsObj.mStartReason & WSType::br)) &&
+ (wsObj.mEndReason & WSType::block)) {
+ // if we are tucked between block boundaries then insert a br
+ // first check that we are allowed to
+ NS_ENSURE_STATE(mHTMLEditor);
+ if (mHTMLEditor->CanContainTag(*node, *nsGkAtoms::br)) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsCOMPtr<Element> br =
+ mHTMLEditor->CreateBR(node, offset, nsIEditor::ePrevious);
+ return br ? NS_OK : NS_ERROR_FAILURE;
+ }
+ }
+ return NS_OK;
+}
+
+/**
+ * GetGoodSelPointForNode() finds where at a node you would want to set the
+ * selection if you were trying to have a caret next to it. Always returns a
+ * valid value (unless mHTMLEditor has gone away).
+ *
+ * @param aNode The node
+ * @param aAction Which edge to find:
+ * eNext/eNextWord/eToEndOfLine indicates beginning,
+ * ePrevious/PreviousWord/eToBeginningOfLine ending.
+ */
+EditorDOMPoint
+HTMLEditRules::GetGoodSelPointForNode(nsINode& aNode,
+ nsIEditor::EDirection aAction)
+{
+ MOZ_ASSERT(aAction == nsIEditor::eNext ||
+ aAction == nsIEditor::eNextWord ||
+ aAction == nsIEditor::ePrevious ||
+ aAction == nsIEditor::ePreviousWord ||
+ aAction == nsIEditor::eToBeginningOfLine ||
+ aAction == nsIEditor::eToEndOfLine);
+
+ bool isPreviousAction = (aAction == nsIEditor::ePrevious ||
+ aAction == nsIEditor::ePreviousWord ||
+ aAction == nsIEditor::eToBeginningOfLine);
+
+ NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint());
+ if (aNode.GetAsText() || mHTMLEditor->IsContainer(&aNode) ||
+ NS_WARN_IF(!aNode.GetParentNode())) {
+ return EditorDOMPoint(&aNode, isPreviousAction ? aNode.Length() : 0);
+ }
+
+ EditorDOMPoint ret;
+ ret.node = aNode.GetParentNode();
+ ret.offset = ret.node ? ret.node->IndexOf(&aNode) : -1;
+ NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint());
+ if ((!aNode.IsHTMLElement(nsGkAtoms::br) ||
+ mHTMLEditor->IsVisBreak(&aNode)) && isPreviousAction) {
+ ret.offset++;
+ }
+ return ret;
+}
+
+
+/**
+ * This method is used to join two block elements. The right element is always
+ * joined to the left element. If the elements are the same type and not
+ * nested within each other, JoinNodesSmart is called (example, joining two
+ * list items together into one). If the elements are not the same type, or
+ * one is a descendant of the other, we instead destroy the right block placing
+ * its children into leftblock. DTD containment rules are followed throughout.
+ */
+nsresult
+HTMLEditRules::JoinBlocks(nsIContent& aLeftNode,
+ nsIContent& aRightNode,
+ bool* aCanceled)
+{
+ MOZ_ASSERT(aCanceled);
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+
+ nsCOMPtr<Element> leftBlock = htmlEditor->GetBlock(aLeftNode);
+ nsCOMPtr<Element> rightBlock = htmlEditor->GetBlock(aRightNode);
+
+ // Sanity checks
+ NS_ENSURE_TRUE(leftBlock && rightBlock, NS_ERROR_NULL_POINTER);
+ NS_ENSURE_STATE(leftBlock != rightBlock);
+
+ if (HTMLEditUtils::IsTableElement(leftBlock) ||
+ HTMLEditUtils::IsTableElement(rightBlock)) {
+ // Do not try to merge table elements
+ *aCanceled = true;
+ return NS_OK;
+ }
+
+ // Make sure we don't try to move things into HR's, which look like blocks
+ // but aren't containers
+ if (leftBlock->IsHTMLElement(nsGkAtoms::hr)) {
+ leftBlock = htmlEditor->GetBlockNodeParent(leftBlock);
+ }
+ if (rightBlock->IsHTMLElement(nsGkAtoms::hr)) {
+ rightBlock = htmlEditor->GetBlockNodeParent(rightBlock);
+ }
+ NS_ENSURE_STATE(leftBlock && rightBlock);
+
+ // Bail if both blocks the same
+ if (leftBlock == rightBlock) {
+ *aCanceled = true;
+ return NS_OK;
+ }
+
+ // Joining a list item to its parent is a NOP.
+ if (HTMLEditUtils::IsList(leftBlock) &&
+ HTMLEditUtils::IsListItem(rightBlock) &&
+ rightBlock->GetParentNode() == leftBlock) {
+ return NS_OK;
+ }
+
+ // Special rule here: if we are trying to join list items, and they are in
+ // different lists, join the lists instead.
+ bool mergeLists = false;
+ nsIAtom* existingList = nsGkAtoms::_empty;
+ int32_t offset;
+ nsCOMPtr<Element> leftList, rightList;
+ if (HTMLEditUtils::IsListItem(leftBlock) &&
+ HTMLEditUtils::IsListItem(rightBlock)) {
+ leftList = leftBlock->GetParentElement();
+ rightList = rightBlock->GetParentElement();
+ if (leftList && rightList && leftList != rightList &&
+ !EditorUtils::IsDescendantOf(leftList, rightBlock, &offset) &&
+ !EditorUtils::IsDescendantOf(rightList, leftBlock, &offset)) {
+ // There are some special complications if the lists are descendants of
+ // the other lists' items. Note that it is okay for them to be
+ // descendants of the other lists themselves, which is the usual case for
+ // sublists in our implementation.
+ leftBlock = leftList;
+ rightBlock = rightList;
+ mergeLists = true;
+ existingList = leftList->NodeInfo()->NameAtom();
+ }
+ }
+
+ AutoTransactionsConserveSelection dontSpazMySelection(htmlEditor);
+
+ int32_t rightOffset = 0;
+ int32_t leftOffset = -1;
+
+ // offset below is where you find yourself in rightBlock when you traverse
+ // upwards from leftBlock
+ if (EditorUtils::IsDescendantOf(leftBlock, rightBlock, &rightOffset)) {
+ // Tricky case. Left block is inside right block. Do ws adjustment. This
+ // just destroys non-visible ws at boundaries we will be joining.
+ rightOffset++;
+ nsresult rv = WSRunObject::ScrubBlockBoundary(htmlEditor,
+ WSRunObject::kBlockEnd,
+ leftBlock);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ {
+ // We can't just track rightBlock because it's an Element.
+ nsCOMPtr<nsINode> trackingRightBlock(rightBlock);
+ AutoTrackDOMPoint tracker(htmlEditor->mRangeUpdater,
+ address_of(trackingRightBlock), &rightOffset);
+ rv = WSRunObject::ScrubBlockBoundary(htmlEditor,
+ WSRunObject::kAfterBlock,
+ rightBlock, rightOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (trackingRightBlock->IsElement()) {
+ rightBlock = trackingRightBlock->AsElement();
+ } else {
+ NS_ENSURE_STATE(trackingRightBlock->GetParentElement());
+ rightBlock = trackingRightBlock->GetParentElement();
+ }
+ }
+ // Do br adjustment.
+ nsCOMPtr<Element> brNode =
+ CheckForInvisibleBR(*leftBlock, BRLocation::blockEnd);
+ if (mergeLists) {
+ // The idea here is to take all children in rightList that are past
+ // offset, and pull them into leftlist.
+ for (nsCOMPtr<nsIContent> child = rightList->GetChildAt(offset);
+ child; child = rightList->GetChildAt(rightOffset)) {
+ rv = htmlEditor->MoveNode(child, leftList, -1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ } else {
+ MoveBlock(*leftBlock, *rightBlock, leftOffset, rightOffset);
+ }
+ if (brNode) {
+ htmlEditor->DeleteNode(brNode);
+ }
+ // Offset below is where you find yourself in leftBlock when you traverse
+ // upwards from rightBlock
+ } else if (EditorUtils::IsDescendantOf(rightBlock, leftBlock, &leftOffset)) {
+ // Tricky case. Right block is inside left block. Do ws adjustment. This
+ // just destroys non-visible ws at boundaries we will be joining.
+ nsresult rv = WSRunObject::ScrubBlockBoundary(htmlEditor,
+ WSRunObject::kBlockStart,
+ rightBlock);
+ NS_ENSURE_SUCCESS(rv, rv);
+ {
+ // We can't just track leftBlock because it's an Element, so track
+ // something else.
+ nsCOMPtr<nsINode> trackingLeftBlock(leftBlock);
+ AutoTrackDOMPoint tracker(htmlEditor->mRangeUpdater,
+ address_of(trackingLeftBlock), &leftOffset);
+ rv = WSRunObject::ScrubBlockBoundary(htmlEditor,
+ WSRunObject::kBeforeBlock,
+ leftBlock, leftOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (trackingLeftBlock->IsElement()) {
+ leftBlock = trackingLeftBlock->AsElement();
+ } else {
+ NS_ENSURE_STATE(trackingLeftBlock->GetParentElement());
+ leftBlock = trackingLeftBlock->GetParentElement();
+ }
+ }
+ // Do br adjustment.
+ nsCOMPtr<Element> brNode =
+ CheckForInvisibleBR(*leftBlock, BRLocation::beforeBlock, leftOffset);
+ if (mergeLists) {
+ MoveContents(*rightList, *leftList, &leftOffset);
+ } else {
+ // Left block is a parent of right block, and the parent of the previous
+ // visible content. Right block is a child and contains the contents we
+ // want to move.
+
+ int32_t previousContentOffset;
+ nsCOMPtr<nsINode> previousContentParent;
+
+ if (&aLeftNode == leftBlock) {
+ // We are working with valid HTML, aLeftNode is a block node, and is
+ // therefore allowed to contain rightBlock. This is the simple case,
+ // we will simply move the content in rightBlock out of its block.
+ previousContentParent = leftBlock;
+ previousContentOffset = leftOffset;
+ } else {
+ // We try to work as well as possible with HTML that's already invalid.
+ // Although "right block" is a block, and a block must not be contained
+ // in inline elements, reality is that broken documents do exist. The
+ // DIRECT parent of "left NODE" might be an inline element. Previous
+ // versions of this code skipped inline parents until the first block
+ // parent was found (and used "left block" as the destination).
+ // However, in some situations this strategy moves the content to an
+ // unexpected position. (see bug 200416) The new idea is to make the
+ // moving content a sibling, next to the previous visible content.
+
+ previousContentParent = aLeftNode.GetParentNode();
+ previousContentOffset = previousContentParent ?
+ previousContentParent->IndexOf(&aLeftNode) : -1;
+
+ // We want to move our content just after the previous visible node.
+ previousContentOffset++;
+ }
+
+ // Because we don't want the moving content to receive the style of the
+ // previous content, we split the previous content's style.
+
+ nsCOMPtr<Element> editorRoot = htmlEditor->GetEditorRoot();
+ if (!editorRoot || &aLeftNode != editorRoot) {
+ nsCOMPtr<nsIContent> splittedPreviousContent;
+ rv = htmlEditor->SplitStyleAbovePoint(
+ address_of(previousContentParent),
+ &previousContentOffset,
+ nullptr, nullptr, nullptr,
+ getter_AddRefs(splittedPreviousContent));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (splittedPreviousContent) {
+ previousContentParent = splittedPreviousContent->GetParentNode();
+ previousContentOffset = previousContentParent ?
+ previousContentParent->IndexOf(splittedPreviousContent) : -1;
+ }
+ }
+
+ NS_ENSURE_TRUE(previousContentParent, NS_ERROR_NULL_POINTER);
+
+ rv = MoveBlock(*previousContentParent->AsElement(), *rightBlock,
+ previousContentOffset, rightOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ if (brNode) {
+ htmlEditor->DeleteNode(brNode);
+ }
+ } else {
+ // Normal case. Blocks are siblings, or at least close enough. An example
+ // of the latter is <p>paragraph</p><ul><li>one<li>two<li>three</ul>. The
+ // first li and the p are not true siblings, but we still want to join them
+ // if you backspace from li into p.
+
+ // Adjust whitespace at block boundaries
+ nsresult rv =
+ WSRunObject::PrepareToJoinBlocks(htmlEditor, leftBlock, rightBlock);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Do br adjustment.
+ nsCOMPtr<Element> brNode =
+ CheckForInvisibleBR(*leftBlock, BRLocation::blockEnd);
+ if (mergeLists || leftBlock->NodeInfo()->NameAtom() ==
+ rightBlock->NodeInfo()->NameAtom()) {
+ // Nodes are same type. merge them.
+ EditorDOMPoint pt = JoinNodesSmart(*leftBlock, *rightBlock);
+ if (pt.node && mergeLists) {
+ nsCOMPtr<Element> newBlock;
+ ConvertListType(rightBlock, getter_AddRefs(newBlock),
+ existingList, nsGkAtoms::li);
+ }
+ } else {
+ // Nodes are dissimilar types.
+ rv = MoveBlock(*leftBlock, *rightBlock, leftOffset, rightOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ if (brNode) {
+ rv = htmlEditor->DeleteNode(brNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ return NS_OK;
+}
+
+
+/**
+ * Moves the content from aRightBlock starting from aRightOffset into
+ * aLeftBlock at aLeftOffset. Note that the "block" might merely be inline
+ * nodes between <br>s, or between blocks, etc. DTD containment rules are
+ * followed throughout.
+ */
+nsresult
+HTMLEditRules::MoveBlock(Element& aLeftBlock,
+ Element& aRightBlock,
+ int32_t aLeftOffset,
+ int32_t aRightOffset)
+{
+ nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
+ // GetNodesFromPoint is the workhorse that figures out what we wnat to move.
+ nsresult rv = GetNodesFromPoint(EditorDOMPoint(&aRightBlock, aRightOffset),
+ EditAction::makeList, arrayOfNodes,
+ TouchContent::yes);
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (uint32_t i = 0; i < arrayOfNodes.Length(); i++) {
+ // get the node to act on
+ if (IsBlockNode(arrayOfNodes[i])) {
+ // For block nodes, move their contents only, then delete block.
+ rv = MoveContents(*arrayOfNodes[i]->AsElement(), aLeftBlock,
+ &aLeftOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->DeleteNode(arrayOfNodes[i]);
+ } else {
+ // Otherwise move the content as is, checking against the DTD.
+ rv = MoveNodeSmart(*arrayOfNodes[i]->AsContent(), aLeftBlock,
+ &aLeftOffset);
+ }
+ }
+
+ // XXX We're only checking return value of the last iteration
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+/**
+ * This method is used to move node aNode to (aDestElement, aInOutDestOffset).
+ * DTD containment rules are followed throughout. aInOutDestOffset is updated
+ * to point _after_ inserted content.
+ */
+nsresult
+HTMLEditRules::MoveNodeSmart(nsIContent& aNode,
+ Element& aDestElement,
+ int32_t* aInOutDestOffset)
+{
+ MOZ_ASSERT(aInOutDestOffset);
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+
+ // Check if this node can go into the destination node
+ if (htmlEditor->CanContain(aDestElement, aNode)) {
+ // If it can, move it there
+ nsresult rv =
+ htmlEditor->MoveNode(&aNode, &aDestElement, *aInOutDestOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (*aInOutDestOffset != -1) {
+ (*aInOutDestOffset)++;
+ }
+ } else {
+ // If it can't, move its children (if any), and then delete it.
+ if (aNode.IsElement()) {
+ nsresult rv =
+ MoveContents(*aNode.AsElement(), aDestElement, aInOutDestOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsresult rv = htmlEditor->DeleteNode(&aNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+/**
+ * Moves the _contents_ of aElement to (aDestElement, aInOutDestOffset). DTD
+ * containment rules are followed throughout. aInOutDestOffset is updated to
+ * point _after_ inserted content.
+ */
+nsresult
+HTMLEditRules::MoveContents(Element& aElement,
+ Element& aDestElement,
+ int32_t* aInOutDestOffset)
+{
+ MOZ_ASSERT(aInOutDestOffset);
+
+ NS_ENSURE_TRUE(&aElement != &aDestElement, NS_ERROR_ILLEGAL_VALUE);
+
+ while (aElement.GetFirstChild()) {
+ nsresult rv = MoveNodeSmart(*aElement.GetFirstChild(), aDestElement,
+ aInOutDestOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+
+nsresult
+HTMLEditRules::DeleteNonTableElements(nsINode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ if (!HTMLEditUtils::IsTableElementButNotTable(aNode)) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ return mHTMLEditor->DeleteNode(aNode->AsDOMNode());
+ }
+
+ for (int32_t i = aNode->GetChildCount() - 1; i >= 0; --i) {
+ nsresult rv = DeleteNonTableElements(aNode->GetChildAt(i));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+nsresult
+HTMLEditRules::DidDeleteSelection(Selection* aSelection,
+ nsIEditor::EDirection aDir,
+ nsresult aResult)
+{
+ if (!aSelection) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ // find where we are
+ nsCOMPtr<nsINode> startNode;
+ int32_t startOffset;
+ nsresult rv = mTextEditor->GetStartNodeAndOffset(aSelection,
+ getter_AddRefs(startNode),
+ &startOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(startNode, NS_ERROR_FAILURE);
+
+ // find any enclosing mailcite
+ nsCOMPtr<Element> citeNode = GetTopEnclosingMailCite(*startNode);
+ if (citeNode) {
+ bool isEmpty = true, seenBR = false;
+ NS_ENSURE_STATE(mHTMLEditor);
+ mHTMLEditor->IsEmptyNodeImpl(citeNode, &isEmpty, true, true, false,
+ &seenBR);
+ if (isEmpty) {
+ int32_t offset;
+ nsCOMPtr<nsINode> parent = EditorBase::GetNodeLocation(citeNode, &offset);
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->DeleteNode(citeNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (parent && seenBR) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsCOMPtr<Element> brNode = mHTMLEditor->CreateBR(parent, offset);
+ NS_ENSURE_STATE(brNode);
+ aSelection->Collapse(parent, offset);
+ }
+ }
+ }
+
+ // call through to base class
+ return TextEditRules::DidDeleteSelection(aSelection, aDir, aResult);
+}
+
+nsresult
+HTMLEditRules::WillMakeList(Selection* aSelection,
+ const nsAString* aListType,
+ bool aEntireList,
+ const nsAString* aBulletType,
+ bool* aCancel,
+ bool* aHandled,
+ const nsAString* aItemType)
+{
+ if (!aSelection || !aListType || !aCancel || !aHandled) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ OwningNonNull<nsIAtom> listType = NS_Atomize(*aListType);
+
+ WillInsert(*aSelection, aCancel);
+
+ // initialize out param
+ // we want to ignore result of WillInsert()
+ *aCancel = false;
+ *aHandled = false;
+
+ // deduce what tag to use for list items
+ nsCOMPtr<nsIAtom> itemType;
+ if (aItemType) {
+ itemType = NS_Atomize(*aItemType);
+ NS_ENSURE_TRUE(itemType, NS_ERROR_OUT_OF_MEMORY);
+ } else if (listType == nsGkAtoms::dl) {
+ itemType = nsGkAtoms::dd;
+ } else {
+ itemType = nsGkAtoms::li;
+ }
+
+ // convert the selection ranges into "promoted" selection ranges:
+ // this basically just expands the range to include the immediate
+ // block parent, and then further expands to include any ancestors
+ // whose children are all in the range
+
+ *aHandled = true;
+
+ nsresult rv = NormalizeSelection(aSelection);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_STATE(mHTMLEditor);
+ AutoSelectionRestorer selectionRestorer(aSelection, mHTMLEditor);
+
+ nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
+ rv = GetListActionNodes(arrayOfNodes,
+ aEntireList ? EntireList::yes : EntireList::no);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // check if all our nodes are <br>s, or empty inlines
+ bool bOnlyBreaks = true;
+ for (auto& curNode : arrayOfNodes) {
+ // if curNode is not a Break or empty inline, we're done
+ if (!TextEditUtils::IsBreak(curNode) &&
+ !IsEmptyInline(curNode)) {
+ bOnlyBreaks = false;
+ break;
+ }
+ }
+
+ // if no nodes, we make empty list. Ditto if the user tried to make a list
+ // of some # of breaks.
+ if (arrayOfNodes.IsEmpty() || bOnlyBreaks) {
+ // if only breaks, delete them
+ if (bOnlyBreaks) {
+ for (auto& node : arrayOfNodes) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->DeleteNode(node);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ // get selection location
+ NS_ENSURE_STATE(aSelection->RangeCount());
+ nsCOMPtr<nsINode> parent = aSelection->GetRangeAt(0)->GetStartParent();
+ int32_t offset = aSelection->GetRangeAt(0)->StartOffset();
+ NS_ENSURE_STATE(parent);
+
+ // make sure we can put a list here
+ NS_ENSURE_STATE(mHTMLEditor);
+ if (!mHTMLEditor->CanContainTag(*parent, listType)) {
+ *aCancel = true;
+ return NS_OK;
+ }
+ rv = SplitAsNeeded(listType, parent, offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsCOMPtr<Element> theList =
+ mHTMLEditor->CreateNode(listType, parent, offset);
+ NS_ENSURE_STATE(theList);
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsCOMPtr<Element> theListItem =
+ mHTMLEditor->CreateNode(itemType, theList, 0);
+ NS_ENSURE_STATE(theListItem);
+
+ // remember our new block for postprocessing
+ mNewBlock = theListItem;
+ // put selection in new list item
+ *aHandled = true;
+ rv = aSelection->Collapse(theListItem, 0);
+ // Don't restore the selection
+ selectionRestorer.Abort();
+ return rv;
+ }
+
+ // if there is only one node in the array, and it is a list, div, or
+ // blockquote, then look inside of it until we find inner list or content.
+
+ LookInsideDivBQandList(arrayOfNodes);
+
+ // Ok, now go through all the nodes and put then in the list,
+ // or whatever is approriate. Wohoo!
+
+ uint32_t listCount = arrayOfNodes.Length();
+ nsCOMPtr<nsINode> curParent;
+ nsCOMPtr<Element> curList, prevListItem;
+
+ for (uint32_t i = 0; i < listCount; i++) {
+ // here's where we actually figure out what to do
+ nsCOMPtr<Element> newBlock;
+ NS_ENSURE_STATE(arrayOfNodes[i]->IsContent());
+ OwningNonNull<nsIContent> curNode = *arrayOfNodes[i]->AsContent();
+ int32_t offset;
+ curParent = EditorBase::GetNodeLocation(curNode, &offset);
+
+ // make sure we don't assemble content that is in different table cells
+ // into the same list. respect table cell boundaries when listifying.
+ if (curList && InDifferentTableElements(curList, curNode)) {
+ curList = nullptr;
+ }
+
+ // if curNode is a Break, delete it, and quit remembering prev list item
+ if (TextEditUtils::IsBreak(curNode)) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->DeleteNode(curNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ prevListItem = nullptr;
+ continue;
+ } else if (IsEmptyInline(curNode)) {
+ // if curNode is an empty inline container, delete it
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->DeleteNode(curNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ continue;
+ }
+
+ if (HTMLEditUtils::IsList(curNode)) {
+ // do we have a curList already?
+ if (curList && !EditorUtils::IsDescendantOf(curNode, curList)) {
+ // move all of our children into curList. cheezy way to do it: move
+ // whole list and then RemoveContainer() on the list. ConvertListType
+ // first: that routine handles converting the list item types, if
+ // needed
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->MoveNode(curNode, curList, -1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = ConvertListType(curNode->AsElement(), getter_AddRefs(newBlock),
+ listType, itemType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->RemoveBlockContainer(*newBlock);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // replace list with new list type
+ rv = ConvertListType(curNode->AsElement(), getter_AddRefs(newBlock),
+ listType, itemType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ curList = newBlock;
+ }
+ prevListItem = nullptr;
+ continue;
+ }
+
+ if (HTMLEditUtils::IsListItem(curNode)) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ if (!curParent->IsHTMLElement(listType)) {
+ // list item is in wrong type of list. if we don't have a curList,
+ // split the old list and make a new list of correct type.
+ if (!curList || EditorUtils::IsDescendantOf(curNode, curList)) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ NS_ENSURE_STATE(curParent->IsContent());
+ ErrorResult rv;
+ nsCOMPtr<nsIContent> splitNode =
+ mHTMLEditor->SplitNode(*curParent->AsContent(), offset, rv);
+ NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult());
+ newBlock = splitNode ? splitNode->AsElement() : nullptr;
+ int32_t offset;
+ nsCOMPtr<nsINode> parent = EditorBase::GetNodeLocation(curParent,
+ &offset);
+ NS_ENSURE_STATE(mHTMLEditor);
+ curList = mHTMLEditor->CreateNode(listType, parent, offset);
+ NS_ENSURE_STATE(curList);
+ }
+ // move list item to new list
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->MoveNode(curNode, curList, -1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // convert list item type if needed
+ NS_ENSURE_STATE(mHTMLEditor);
+ if (!curNode->IsHTMLElement(itemType)) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ newBlock = mHTMLEditor->ReplaceContainer(curNode->AsElement(),
+ itemType);
+ NS_ENSURE_STATE(newBlock);
+ }
+ } else {
+ // item is in right type of list. But we might still have to move it.
+ // and we might need to convert list item types.
+ if (!curList) {
+ curList = curParent->AsElement();
+ } else if (curParent != curList) {
+ // move list item to new list
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->MoveNode(curNode, curList, -1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ NS_ENSURE_STATE(mHTMLEditor);
+ if (!curNode->IsHTMLElement(itemType)) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ newBlock = mHTMLEditor->ReplaceContainer(curNode->AsElement(),
+ itemType);
+ NS_ENSURE_STATE(newBlock);
+ }
+ }
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsCOMPtr<nsIDOMElement> curElement = do_QueryInterface(curNode);
+ NS_NAMED_LITERAL_STRING(typestr, "type");
+ if (aBulletType && !aBulletType->IsEmpty()) {
+ rv = mHTMLEditor->SetAttribute(curElement, typestr, *aBulletType);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ rv = mHTMLEditor->RemoveAttribute(curElement, typestr);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+ continue;
+ }
+
+ // if we hit a div clear our prevListItem, insert divs contents
+ // into our node array, and remove the div
+ if (curNode->IsHTMLElement(nsGkAtoms::div)) {
+ prevListItem = nullptr;
+ int32_t j = i + 1;
+ GetInnerContent(*curNode, arrayOfNodes, &j);
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->RemoveContainer(curNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ listCount = arrayOfNodes.Length();
+ continue;
+ }
+
+ // need to make a list to put things in if we haven't already,
+ if (!curList) {
+ rv = SplitAsNeeded(listType, curParent, offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_STATE(mHTMLEditor);
+ curList = mHTMLEditor->CreateNode(listType, curParent, offset);
+ // remember our new block for postprocessing
+ mNewBlock = curList;
+ // curList is now the correct thing to put curNode in
+ prevListItem = nullptr;
+ }
+
+ // if curNode isn't a list item, we must wrap it in one
+ nsCOMPtr<Element> listItem;
+ if (!HTMLEditUtils::IsListItem(curNode)) {
+ if (IsInlineNode(curNode) && prevListItem) {
+ // this is a continuation of some inline nodes that belong together in
+ // the same list item. use prevListItem
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->MoveNode(curNode, prevListItem, -1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // don't wrap li around a paragraph. instead replace paragraph with li
+ if (curNode->IsHTMLElement(nsGkAtoms::p)) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ listItem = mHTMLEditor->ReplaceContainer(curNode->AsElement(),
+ itemType);
+ NS_ENSURE_STATE(listItem);
+ } else {
+ NS_ENSURE_STATE(mHTMLEditor);
+ listItem = mHTMLEditor->InsertContainerAbove(curNode, itemType);
+ NS_ENSURE_STATE(listItem);
+ }
+ if (IsInlineNode(curNode)) {
+ prevListItem = listItem;
+ } else {
+ prevListItem = nullptr;
+ }
+ }
+ } else {
+ listItem = curNode->AsElement();
+ }
+
+ if (listItem) {
+ // if we made a new list item, deal with it: tuck the listItem into the
+ // end of the active list
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->MoveNode(listItem, curList, -1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ return NS_OK;
+}
+
+
+nsresult
+HTMLEditRules::WillRemoveList(Selection* aSelection,
+ bool aOrdered,
+ bool* aCancel,
+ bool* aHandled)
+{
+ if (!aSelection || !aCancel || !aHandled) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ // initialize out param
+ *aCancel = false;
+ *aHandled = true;
+
+ nsresult rv = NormalizeSelection(aSelection);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_STATE(mHTMLEditor);
+ AutoSelectionRestorer selectionRestorer(aSelection, mHTMLEditor);
+
+ nsTArray<RefPtr<nsRange>> arrayOfRanges;
+ GetPromotedRanges(*aSelection, arrayOfRanges, EditAction::makeList);
+
+ // use these ranges to contruct a list of nodes to act on.
+ nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
+ rv = GetListActionNodes(arrayOfNodes, EntireList::no);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Remove all non-editable nodes. Leave them be.
+ for (int32_t i = arrayOfNodes.Length() - 1; i >= 0; i--) {
+ OwningNonNull<nsINode> testNode = arrayOfNodes[i];
+ NS_ENSURE_STATE(mHTMLEditor);
+ if (!mHTMLEditor->IsEditable(testNode)) {
+ arrayOfNodes.RemoveElementAt(i);
+ }
+ }
+
+ // Only act on lists or list items in the array
+ for (auto& curNode : arrayOfNodes) {
+ // here's where we actually figure out what to do
+ if (HTMLEditUtils::IsListItem(curNode)) {
+ // unlist this listitem
+ bool bOutOfList;
+ do {
+ rv = PopListItem(GetAsDOMNode(curNode), &bOutOfList);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } while (!bOutOfList); // keep popping it out until it's not in a list anymore
+ } else if (HTMLEditUtils::IsList(curNode)) {
+ // node is a list, move list items out
+ rv = RemoveListStructure(*curNode->AsElement());
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+HTMLEditRules::WillMakeDefListItem(Selection* aSelection,
+ const nsAString *aItemType,
+ bool aEntireList,
+ bool* aCancel,
+ bool* aHandled)
+{
+ // for now we let WillMakeList handle this
+ NS_NAMED_LITERAL_STRING(listType, "dl");
+ return WillMakeList(aSelection, &listType, aEntireList, nullptr, aCancel, aHandled, aItemType);
+}
+
+nsresult
+HTMLEditRules::WillMakeBasicBlock(Selection& aSelection,
+ const nsAString& aBlockType,
+ bool* aCancel,
+ bool* aHandled)
+{
+ MOZ_ASSERT(aCancel && aHandled);
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+
+ OwningNonNull<nsIAtom> blockType = NS_Atomize(aBlockType);
+
+ WillInsert(aSelection, aCancel);
+ // We want to ignore result of WillInsert()
+ *aCancel = false;
+ *aHandled = false;
+
+ nsresult rv = NormalizeSelection(&aSelection);
+ NS_ENSURE_SUCCESS(rv, rv);
+ AutoSelectionRestorer selectionRestorer(&aSelection, htmlEditor);
+ AutoTransactionsConserveSelection dontSpazMySelection(htmlEditor);
+ *aHandled = true;
+
+ // Contruct a list of nodes to act on.
+ nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
+ rv = GetNodesFromSelection(aSelection, EditAction::makeBasicBlock,
+ arrayOfNodes);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Remove all non-editable nodes. Leave them be.
+ for (int32_t i = arrayOfNodes.Length() - 1; i >= 0; i--) {
+ if (!htmlEditor->IsEditable(arrayOfNodes[i])) {
+ arrayOfNodes.RemoveElementAt(i);
+ }
+ }
+
+ // If nothing visible in list, make an empty block
+ if (ListIsEmptyLine(arrayOfNodes)) {
+ // Get selection location
+ NS_ENSURE_STATE(aSelection.GetRangeAt(0) &&
+ aSelection.GetRangeAt(0)->GetStartParent());
+ OwningNonNull<nsINode> parent =
+ *aSelection.GetRangeAt(0)->GetStartParent();
+ int32_t offset = aSelection.GetRangeAt(0)->StartOffset();
+
+ if (blockType == nsGkAtoms::normal ||
+ blockType == nsGkAtoms::_empty) {
+ // We are removing blocks (going to "body text")
+ NS_ENSURE_TRUE(htmlEditor->GetBlock(parent), NS_ERROR_NULL_POINTER);
+ OwningNonNull<Element> curBlock = *htmlEditor->GetBlock(parent);
+ if (HTMLEditUtils::IsFormatNode(curBlock)) {
+ // If the first editable node after selection is a br, consume it.
+ // Otherwise it gets pushed into a following block after the split,
+ // which is visually bad.
+ nsCOMPtr<nsIContent> brNode =
+ htmlEditor->GetNextHTMLNode(parent, offset);
+ if (brNode && brNode->IsHTMLElement(nsGkAtoms::br)) {
+ rv = htmlEditor->DeleteNode(brNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // Do the splits!
+ offset = htmlEditor->SplitNodeDeep(curBlock, *parent->AsContent(),
+ offset,
+ HTMLEditor::EmptyContainers::no);
+ NS_ENSURE_STATE(offset != -1);
+ // Put a br at the split point
+ brNode = htmlEditor->CreateBR(curBlock->GetParentNode(), offset);
+ NS_ENSURE_STATE(brNode);
+ // Put selection at the split point
+ *aHandled = true;
+ rv = aSelection.Collapse(curBlock->GetParentNode(), offset);
+ // Don't restore the selection
+ selectionRestorer.Abort();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // Else nothing to do!
+ } else {
+ // We are making a block. Consume a br, if needed.
+ nsCOMPtr<nsIContent> brNode =
+ htmlEditor->GetNextHTMLNode(parent, offset, true);
+ if (brNode && brNode->IsHTMLElement(nsGkAtoms::br)) {
+ rv = htmlEditor->DeleteNode(brNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // We don't need to act on this node any more
+ arrayOfNodes.RemoveElement(brNode);
+ }
+ // Make sure we can put a block here
+ rv = SplitAsNeeded(blockType, parent, offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<Element> block =
+ htmlEditor->CreateNode(blockType, parent, offset);
+ NS_ENSURE_STATE(block);
+ // Remember our new block for postprocessing
+ mNewBlock = block;
+ // Delete anything that was in the list of nodes
+ while (!arrayOfNodes.IsEmpty()) {
+ OwningNonNull<nsINode> curNode = arrayOfNodes[0];
+ rv = htmlEditor->DeleteNode(curNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ arrayOfNodes.RemoveElementAt(0);
+ }
+ // Put selection in new block
+ *aHandled = true;
+ rv = aSelection.Collapse(block, 0);
+ // Don't restore the selection
+ selectionRestorer.Abort();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+ }
+ // Okay, now go through all the nodes and make the right kind of blocks, or
+ // whatever is approriate. Woohoo! Note: blockquote is handled a little
+ // differently.
+ if (blockType == nsGkAtoms::blockquote) {
+ rv = MakeBlockquote(arrayOfNodes);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else if (blockType == nsGkAtoms::normal ||
+ blockType == nsGkAtoms::_empty) {
+ rv = RemoveBlockStyle(arrayOfNodes);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ rv = ApplyBlockStyle(arrayOfNodes, blockType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+nsresult
+HTMLEditRules::DidMakeBasicBlock(Selection* aSelection,
+ RulesInfo* aInfo,
+ nsresult aResult)
+{
+ NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER);
+ // check for empty block. if so, put a moz br in it.
+ if (!aSelection->Collapsed()) {
+ return NS_OK;
+ }
+
+ NS_ENSURE_STATE(aSelection->GetRangeAt(0) &&
+ aSelection->GetRangeAt(0)->GetStartParent());
+ nsresult rv =
+ InsertMozBRIfNeeded(*aSelection->GetRangeAt(0)->GetStartParent());
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+nsresult
+HTMLEditRules::WillIndent(Selection* aSelection,
+ bool* aCancel,
+ bool* aHandled)
+{
+ NS_ENSURE_STATE(mHTMLEditor);
+ if (mHTMLEditor->IsCSSEnabled()) {
+ nsresult rv = WillCSSIndent(aSelection, aCancel, aHandled);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ nsresult rv = WillHTMLIndent(aSelection, aCancel, aHandled);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+HTMLEditRules::WillCSSIndent(Selection* aSelection,
+ bool* aCancel,
+ bool* aHandled)
+{
+ if (!aSelection || !aCancel || !aHandled) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ WillInsert(*aSelection, aCancel);
+
+ // initialize out param
+ // we want to ignore result of WillInsert()
+ *aCancel = false;
+ *aHandled = true;
+
+ nsresult rv = NormalizeSelection(aSelection);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_STATE(mHTMLEditor);
+ AutoSelectionRestorer selectionRestorer(aSelection, mHTMLEditor);
+ nsTArray<OwningNonNull<nsRange>> arrayOfRanges;
+ nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
+
+ // short circuit: detect case of collapsed selection inside an <li>.
+ // just sublist that <li>. This prevents bug 97797.
+
+ nsCOMPtr<Element> liNode;
+ if (aSelection->Collapsed()) {
+ nsCOMPtr<nsINode> node;
+ int32_t offset;
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv =
+ mHTMLEditor->GetStartNodeAndOffset(aSelection,
+ getter_AddRefs(node), &offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<Element> block = mHTMLEditor->GetBlock(*node);
+ if (block && HTMLEditUtils::IsListItem(block)) {
+ liNode = block;
+ }
+ }
+
+ if (liNode) {
+ arrayOfNodes.AppendElement(*liNode);
+ } else {
+ // convert the selection ranges into "promoted" selection ranges:
+ // this basically just expands the range to include the immediate
+ // block parent, and then further expands to include any ancestors
+ // whose children are all in the range
+ rv = GetNodesFromSelection(*aSelection, EditAction::indent, arrayOfNodes);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // if nothing visible in list, make an empty block
+ if (ListIsEmptyLine(arrayOfNodes)) {
+ // get selection location
+ NS_ENSURE_STATE(aSelection->RangeCount());
+ nsCOMPtr<nsINode> parent = aSelection->GetRangeAt(0)->GetStartParent();
+ int32_t offset = aSelection->GetRangeAt(0)->StartOffset();
+ NS_ENSURE_STATE(parent);
+
+ // make sure we can put a block here
+ rv = SplitAsNeeded(*nsGkAtoms::div, parent, offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsCOMPtr<Element> theBlock = mHTMLEditor->CreateNode(nsGkAtoms::div,
+ parent, offset);
+ NS_ENSURE_STATE(theBlock);
+ // remember our new block for postprocessing
+ mNewBlock = theBlock;
+ ChangeIndentation(*theBlock, Change::plus);
+ // delete anything that was in the list of nodes
+ while (!arrayOfNodes.IsEmpty()) {
+ OwningNonNull<nsINode> curNode = arrayOfNodes[0];
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->DeleteNode(curNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ arrayOfNodes.RemoveElementAt(0);
+ }
+ // put selection in new block
+ *aHandled = true;
+ rv = aSelection->Collapse(theBlock, 0);
+ // Don't restore the selection
+ selectionRestorer.Abort();
+ return rv;
+ }
+
+ // Ok, now go through all the nodes and put them in a blockquote,
+ // or whatever is appropriate. Wohoo!
+ nsCOMPtr<nsINode> curParent;
+ nsCOMPtr<Element> curList, curQuote;
+ nsCOMPtr<nsIContent> sibling;
+ int32_t listCount = arrayOfNodes.Length();
+ for (int32_t i = 0; i < listCount; i++) {
+ // here's where we actually figure out what to do
+ NS_ENSURE_STATE(arrayOfNodes[i]->IsContent());
+ nsCOMPtr<nsIContent> curNode = arrayOfNodes[i]->AsContent();
+
+ // Ignore all non-editable nodes. Leave them be.
+ NS_ENSURE_STATE(mHTMLEditor);
+ if (!mHTMLEditor->IsEditable(curNode)) {
+ continue;
+ }
+
+ curParent = curNode->GetParentNode();
+ int32_t offset = curParent ? curParent->IndexOf(curNode) : -1;
+
+ // some logic for putting list items into nested lists...
+ if (HTMLEditUtils::IsList(curParent)) {
+ sibling = nullptr;
+
+ // Check for whether we should join a list that follows curNode.
+ // We do this if the next element is a list, and the list is of the
+ // same type (li/ol) as curNode was a part it.
+ NS_ENSURE_STATE(mHTMLEditor);
+ sibling = mHTMLEditor->GetNextHTMLSibling(curNode);
+ if (sibling && HTMLEditUtils::IsList(sibling) &&
+ curParent->NodeInfo()->NameAtom() ==
+ sibling->NodeInfo()->NameAtom() &&
+ curParent->NodeInfo()->NamespaceID() ==
+ sibling->NodeInfo()->NamespaceID()) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->MoveNode(curNode, sibling, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ continue;
+ }
+ // Check for whether we should join a list that preceeds curNode.
+ // We do this if the previous element is a list, and the list is of
+ // the same type (li/ol) as curNode was a part of.
+ NS_ENSURE_STATE(mHTMLEditor);
+ sibling = mHTMLEditor->GetPriorHTMLSibling(curNode);
+ if (sibling && HTMLEditUtils::IsList(sibling) &&
+ curParent->NodeInfo()->NameAtom() ==
+ sibling->NodeInfo()->NameAtom() &&
+ curParent->NodeInfo()->NamespaceID() ==
+ sibling->NodeInfo()->NamespaceID()) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->MoveNode(curNode, sibling, -1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ continue;
+ }
+ sibling = nullptr;
+
+ // check to see if curList is still appropriate. Which it is if
+ // curNode is still right after it in the same list.
+ if (curList) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ sibling = mHTMLEditor->GetPriorHTMLSibling(curNode);
+ }
+
+ if (!curList || (sibling && sibling != curList)) {
+ // create a new nested list of correct type
+ rv =
+ SplitAsNeeded(*curParent->NodeInfo()->NameAtom(), curParent, offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_STATE(mHTMLEditor);
+ curList = mHTMLEditor->CreateNode(curParent->NodeInfo()->NameAtom(),
+ curParent, offset);
+ NS_ENSURE_STATE(curList);
+ // curList is now the correct thing to put curNode in
+ // remember our new block for postprocessing
+ mNewBlock = curList;
+ }
+ // tuck the node into the end of the active list
+ uint32_t listLen = curList->Length();
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->MoveNode(curNode, curList, listLen);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // Not a list item.
+ else {
+ if (curNode && IsBlockNode(*curNode)) {
+ ChangeIndentation(*curNode->AsElement(), Change::plus);
+ curQuote = nullptr;
+ } else {
+ if (!curQuote) {
+ // First, check that our element can contain a div.
+ if (!mTextEditor->CanContainTag(*curParent, *nsGkAtoms::div)) {
+ return NS_OK; // cancelled
+ }
+
+ rv = SplitAsNeeded(*nsGkAtoms::div, curParent, offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_STATE(mHTMLEditor);
+ curQuote = mHTMLEditor->CreateNode(nsGkAtoms::div, curParent,
+ offset);
+ NS_ENSURE_STATE(curQuote);
+ ChangeIndentation(*curQuote, Change::plus);
+ // remember our new block for postprocessing
+ mNewBlock = curQuote;
+ // curQuote is now the correct thing to put curNode in
+ }
+
+ // tuck the node into the end of the active blockquote
+ uint32_t quoteLen = curQuote->Length();
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->MoveNode(curNode, curQuote, quoteLen);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+HTMLEditRules::WillHTMLIndent(Selection* aSelection,
+ bool* aCancel,
+ bool* aHandled)
+{
+ if (!aSelection || !aCancel || !aHandled) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ WillInsert(*aSelection, aCancel);
+
+ // initialize out param
+ // we want to ignore result of WillInsert()
+ *aCancel = false;
+ *aHandled = true;
+
+ nsresult rv = NormalizeSelection(aSelection);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_STATE(mHTMLEditor);
+ AutoSelectionRestorer selectionRestorer(aSelection, mHTMLEditor);
+
+ // convert the selection ranges into "promoted" selection ranges:
+ // this basically just expands the range to include the immediate
+ // block parent, and then further expands to include any ancestors
+ // whose children are all in the range
+
+ nsTArray<RefPtr<nsRange>> arrayOfRanges;
+ GetPromotedRanges(*aSelection, arrayOfRanges, EditAction::indent);
+
+ // use these ranges to contruct a list of nodes to act on.
+ nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
+ rv = GetNodesForOperation(arrayOfRanges, arrayOfNodes, EditAction::indent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // if nothing visible in list, make an empty block
+ if (ListIsEmptyLine(arrayOfNodes)) {
+ // get selection location
+ NS_ENSURE_STATE(aSelection->RangeCount());
+ nsCOMPtr<nsINode> parent = aSelection->GetRangeAt(0)->GetStartParent();
+ int32_t offset = aSelection->GetRangeAt(0)->StartOffset();
+ NS_ENSURE_STATE(parent);
+
+ // make sure we can put a block here
+ rv = SplitAsNeeded(*nsGkAtoms::blockquote, parent, offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsCOMPtr<Element> theBlock = mHTMLEditor->CreateNode(nsGkAtoms::blockquote,
+ parent, offset);
+ NS_ENSURE_STATE(theBlock);
+ // remember our new block for postprocessing
+ mNewBlock = theBlock;
+ // delete anything that was in the list of nodes
+ while (!arrayOfNodes.IsEmpty()) {
+ OwningNonNull<nsINode> curNode = arrayOfNodes[0];
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->DeleteNode(curNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ arrayOfNodes.RemoveElementAt(0);
+ }
+ // put selection in new block
+ *aHandled = true;
+ rv = aSelection->Collapse(theBlock, 0);
+ // Don't restore the selection
+ selectionRestorer.Abort();
+ return rv;
+ }
+
+ // Ok, now go through all the nodes and put them in a blockquote,
+ // or whatever is appropriate. Wohoo!
+ nsCOMPtr<nsINode> curParent;
+ nsCOMPtr<nsIContent> sibling;
+ nsCOMPtr<Element> curList, curQuote, indentedLI;
+ int32_t listCount = arrayOfNodes.Length();
+ for (int32_t i = 0; i < listCount; i++) {
+ // here's where we actually figure out what to do
+ NS_ENSURE_STATE(arrayOfNodes[i]->IsContent());
+ nsCOMPtr<nsIContent> curNode = arrayOfNodes[i]->AsContent();
+
+ // Ignore all non-editable nodes. Leave them be.
+ NS_ENSURE_STATE(mHTMLEditor);
+ if (!mHTMLEditor->IsEditable(curNode)) {
+ continue;
+ }
+
+ curParent = curNode->GetParentNode();
+ int32_t offset = curParent ? curParent->IndexOf(curNode) : -1;
+
+ // some logic for putting list items into nested lists...
+ if (HTMLEditUtils::IsList(curParent)) {
+ sibling = nullptr;
+
+ // Check for whether we should join a list that follows curNode.
+ // We do this if the next element is a list, and the list is of the
+ // same type (li/ol) as curNode was a part it.
+ NS_ENSURE_STATE(mHTMLEditor);
+ sibling = mHTMLEditor->GetNextHTMLSibling(curNode);
+ if (sibling && HTMLEditUtils::IsList(sibling) &&
+ curParent->NodeInfo()->NameAtom() ==
+ sibling->NodeInfo()->NameAtom() &&
+ curParent->NodeInfo()->NamespaceID() ==
+ sibling->NodeInfo()->NamespaceID()) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->MoveNode(curNode, sibling, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ continue;
+ }
+
+ // Check for whether we should join a list that preceeds curNode.
+ // We do this if the previous element is a list, and the list is of
+ // the same type (li/ol) as curNode was a part of.
+ NS_ENSURE_STATE(mHTMLEditor);
+ sibling = mHTMLEditor->GetPriorHTMLSibling(curNode);
+ if (sibling && HTMLEditUtils::IsList(sibling) &&
+ curParent->NodeInfo()->NameAtom() ==
+ sibling->NodeInfo()->NameAtom() &&
+ curParent->NodeInfo()->NamespaceID() ==
+ sibling->NodeInfo()->NamespaceID()) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->MoveNode(curNode, sibling, -1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ continue;
+ }
+
+ sibling = nullptr;
+
+ // check to see if curList is still appropriate. Which it is if
+ // curNode is still right after it in the same list.
+ if (curList) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ sibling = mHTMLEditor->GetPriorHTMLSibling(curNode);
+ }
+
+ if (!curList || (sibling && sibling != curList)) {
+ // create a new nested list of correct type
+ rv =
+ SplitAsNeeded(*curParent->NodeInfo()->NameAtom(), curParent, offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_STATE(mHTMLEditor);
+ curList = mHTMLEditor->CreateNode(curParent->NodeInfo()->NameAtom(),
+ curParent, offset);
+ NS_ENSURE_STATE(curList);
+ // curList is now the correct thing to put curNode in
+ // remember our new block for postprocessing
+ mNewBlock = curList;
+ }
+ // tuck the node into the end of the active list
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->MoveNode(curNode, curList, -1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // forget curQuote, if any
+ curQuote = nullptr;
+ }
+ // Not a list item, use blockquote?
+ else {
+ // if we are inside a list item, we don't want to blockquote, we want
+ // to sublist the list item. We may have several nodes listed in the
+ // array of nodes to act on, that are in the same list item. Since
+ // we only want to indent that li once, we must keep track of the most
+ // recent indented list item, and not indent it if we find another node
+ // to act on that is still inside the same li.
+ nsCOMPtr<Element> listItem = IsInListItem(curNode);
+ if (listItem) {
+ if (indentedLI == listItem) {
+ // already indented this list item
+ continue;
+ }
+ curParent = listItem->GetParentNode();
+ offset = curParent ? curParent->IndexOf(listItem) : -1;
+ // check to see if curList is still appropriate. Which it is if
+ // curNode is still right after it in the same list.
+ if (curList) {
+ sibling = nullptr;
+ NS_ENSURE_STATE(mHTMLEditor);
+ sibling = mHTMLEditor->GetPriorHTMLSibling(curNode);
+ }
+
+ if (!curList || (sibling && sibling != curList)) {
+ // create a new nested list of correct type
+ rv = SplitAsNeeded(*curParent->NodeInfo()->NameAtom(), curParent,
+ offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_STATE(mHTMLEditor);
+ curList = mHTMLEditor->CreateNode(curParent->NodeInfo()->NameAtom(),
+ curParent, offset);
+ NS_ENSURE_STATE(curList);
+ }
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->MoveNode(listItem, curList, -1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // remember we indented this li
+ indentedLI = listItem;
+ } else {
+ // need to make a blockquote to put things in if we haven't already,
+ // or if this node doesn't go in blockquote we used earlier.
+ // One reason it might not go in prio blockquote is if we are now
+ // in a different table cell.
+ if (curQuote && InDifferentTableElements(curQuote, curNode)) {
+ curQuote = nullptr;
+ }
+
+ if (!curQuote) {
+ // First, check that our element can contain a blockquote.
+ if (!mTextEditor->CanContainTag(*curParent, *nsGkAtoms::blockquote)) {
+ return NS_OK; // cancelled
+ }
+
+ rv = SplitAsNeeded(*nsGkAtoms::blockquote, curParent, offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_STATE(mHTMLEditor);
+ curQuote = mHTMLEditor->CreateNode(nsGkAtoms::blockquote, curParent,
+ offset);
+ NS_ENSURE_STATE(curQuote);
+ // remember our new block for postprocessing
+ mNewBlock = curQuote;
+ // curQuote is now the correct thing to put curNode in
+ }
+
+ // tuck the node into the end of the active blockquote
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->MoveNode(curNode, curQuote, -1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // forget curList, if any
+ curList = nullptr;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+
+nsresult
+HTMLEditRules::WillOutdent(Selection& aSelection,
+ bool* aCancel,
+ bool* aHandled)
+{
+ MOZ_ASSERT(aCancel && aHandled);
+ *aCancel = false;
+ *aHandled = true;
+ nsCOMPtr<nsIContent> rememberedLeftBQ, rememberedRightBQ;
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+ bool useCSS = htmlEditor->IsCSSEnabled();
+
+ nsresult rv = NormalizeSelection(&aSelection);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Some scoping for selection resetting - we may need to tweak it
+ {
+ AutoSelectionRestorer selectionRestorer(&aSelection, htmlEditor);
+
+ // Convert the selection ranges into "promoted" selection ranges: this
+ // basically just expands the range to include the immediate block parent,
+ // and then further expands to include any ancestors whose children are all
+ // in the range
+ nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
+ rv = GetNodesFromSelection(aSelection, EditAction::outdent, arrayOfNodes);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Okay, now go through all the nodes and remove a level of blockquoting,
+ // or whatever is appropriate. Wohoo!
+
+ nsCOMPtr<Element> curBlockQuote;
+ nsCOMPtr<nsIContent> firstBQChild, lastBQChild;
+ bool curBlockQuoteIsIndentedWithCSS = false;
+ for (uint32_t i = 0; i < arrayOfNodes.Length(); i++) {
+ if (!arrayOfNodes[i]->IsContent()) {
+ continue;
+ }
+ OwningNonNull<nsIContent> curNode = *arrayOfNodes[i]->AsContent();
+
+ // Here's where we actually figure out what to do
+ nsCOMPtr<nsINode> curParent = curNode->GetParentNode();
+ int32_t offset = curParent ? curParent->IndexOf(curNode) : -1;
+
+ // Is it a blockquote?
+ if (curNode->IsHTMLElement(nsGkAtoms::blockquote)) {
+ // If it is a blockquote, remove it. So we need to finish up dealng
+ // with any curBlockQuote first.
+ if (curBlockQuote) {
+ rv = OutdentPartOfBlock(*curBlockQuote, *firstBQChild, *lastBQChild,
+ curBlockQuoteIsIndentedWithCSS,
+ getter_AddRefs(rememberedLeftBQ),
+ getter_AddRefs(rememberedRightBQ));
+ NS_ENSURE_SUCCESS(rv, rv);
+ curBlockQuote = nullptr;
+ firstBQChild = nullptr;
+ lastBQChild = nullptr;
+ curBlockQuoteIsIndentedWithCSS = false;
+ }
+ rv = htmlEditor->RemoveBlockContainer(curNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ continue;
+ }
+ // Is it a block with a 'margin' property?
+ if (useCSS && IsBlockNode(curNode)) {
+ nsIAtom& marginProperty =
+ MarginPropertyAtomForIndent(*htmlEditor->mCSSEditUtils, curNode);
+ nsAutoString value;
+ htmlEditor->mCSSEditUtils->GetSpecifiedProperty(curNode,
+ marginProperty,
+ value);
+ float f;
+ nsCOMPtr<nsIAtom> unit;
+ NS_ENSURE_STATE(htmlEditor);
+ htmlEditor->mCSSEditUtils->ParseLength(value, &f,
+ getter_AddRefs(unit));
+ if (f > 0) {
+ ChangeIndentation(*curNode->AsElement(), Change::minus);
+ continue;
+ }
+ }
+ // Is it a list item?
+ if (HTMLEditUtils::IsListItem(curNode)) {
+ // If it is a list item, that means we are not outdenting whole list.
+ // So we need to finish up dealing with any curBlockQuote, and then pop
+ // this list item.
+ if (curBlockQuote) {
+ rv = OutdentPartOfBlock(*curBlockQuote, *firstBQChild, *lastBQChild,
+ curBlockQuoteIsIndentedWithCSS,
+ getter_AddRefs(rememberedLeftBQ),
+ getter_AddRefs(rememberedRightBQ));
+ NS_ENSURE_SUCCESS(rv, rv);
+ curBlockQuote = nullptr;
+ firstBQChild = nullptr;
+ lastBQChild = nullptr;
+ curBlockQuoteIsIndentedWithCSS = false;
+ }
+ bool unused;
+ rv = PopListItem(GetAsDOMNode(curNode), &unused);
+ NS_ENSURE_SUCCESS(rv, rv);
+ continue;
+ }
+ // Do we have a blockquote that we are already committed to removing?
+ if (curBlockQuote) {
+ // If so, is this node a descendant?
+ if (EditorUtils::IsDescendantOf(curNode, curBlockQuote)) {
+ lastBQChild = curNode;
+ // Then we don't need to do anything different for this node
+ continue;
+ }
+ // Otherwise, we have progressed beyond end of curBlockQuote, so
+ // let's handle it now. We need to remove the portion of
+ // curBlockQuote that contains [firstBQChild - lastBQChild].
+ rv = OutdentPartOfBlock(*curBlockQuote, *firstBQChild, *lastBQChild,
+ curBlockQuoteIsIndentedWithCSS,
+ getter_AddRefs(rememberedLeftBQ),
+ getter_AddRefs(rememberedRightBQ));
+ NS_ENSURE_SUCCESS(rv, rv);
+ curBlockQuote = nullptr;
+ firstBQChild = nullptr;
+ lastBQChild = nullptr;
+ curBlockQuoteIsIndentedWithCSS = false;
+ // Fall out and handle curNode
+ }
+
+ // Are we inside a blockquote?
+ OwningNonNull<nsINode> n = curNode;
+ curBlockQuoteIsIndentedWithCSS = false;
+ // Keep looking up the hierarchy as long as we don't hit the body or the
+ // active editing host or a table element (other than an entire table)
+ while (!n->IsHTMLElement(nsGkAtoms::body) &&
+ htmlEditor->IsDescendantOfEditorRoot(n) &&
+ (n->IsHTMLElement(nsGkAtoms::table) ||
+ !HTMLEditUtils::IsTableElement(n))) {
+ if (!n->GetParentNode()) {
+ break;
+ }
+ n = *n->GetParentNode();
+ if (n->IsHTMLElement(nsGkAtoms::blockquote)) {
+ // If so, remember it and the first node we are taking out of it.
+ curBlockQuote = n->AsElement();
+ firstBQChild = curNode;
+ lastBQChild = curNode;
+ break;
+ } else if (useCSS) {
+ nsIAtom& marginProperty =
+ MarginPropertyAtomForIndent(*htmlEditor->mCSSEditUtils, curNode);
+ nsAutoString value;
+ htmlEditor->mCSSEditUtils->GetSpecifiedProperty(*n, marginProperty,
+ value);
+ float f;
+ nsCOMPtr<nsIAtom> unit;
+ htmlEditor->mCSSEditUtils->ParseLength(value, &f, getter_AddRefs(unit));
+ if (f > 0 && !(HTMLEditUtils::IsList(curParent) &&
+ HTMLEditUtils::IsList(curNode))) {
+ curBlockQuote = n->AsElement();
+ firstBQChild = curNode;
+ lastBQChild = curNode;
+ curBlockQuoteIsIndentedWithCSS = true;
+ break;
+ }
+ }
+ }
+
+ if (!curBlockQuote) {
+ // Couldn't find enclosing blockquote. Handle list cases.
+ if (HTMLEditUtils::IsList(curParent)) {
+ // Move node out of list
+ if (HTMLEditUtils::IsList(curNode)) {
+ // Just unwrap this sublist
+ rv = htmlEditor->RemoveBlockContainer(curNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // handled list item case above
+ } else if (HTMLEditUtils::IsList(curNode)) {
+ // node is a list, but parent is non-list: move list items out
+ nsCOMPtr<nsIContent> child = curNode->GetLastChild();
+ while (child) {
+ if (HTMLEditUtils::IsListItem(child)) {
+ bool unused;
+ rv = PopListItem(GetAsDOMNode(child), &unused);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else if (HTMLEditUtils::IsList(child)) {
+ // We have an embedded list, so move it out from under the parent
+ // list. Be sure to put it after the parent list because this
+ // loop iterates backwards through the parent's list of children.
+
+ rv = htmlEditor->MoveNode(child, curParent, offset + 1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // Delete any non-list items for now
+ rv = htmlEditor->DeleteNode(child);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ child = curNode->GetLastChild();
+ }
+ // Delete the now-empty list
+ rv = htmlEditor->RemoveBlockContainer(curNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else if (useCSS) {
+ nsCOMPtr<Element> element;
+ if (curNode->GetAsText()) {
+ // We want to outdent the parent of text nodes
+ element = curNode->GetParentElement();
+ } else if (curNode->IsElement()) {
+ element = curNode->AsElement();
+ }
+ if (element) {
+ ChangeIndentation(*element, Change::minus);
+ }
+ }
+ }
+ }
+ if (curBlockQuote) {
+ // We have a blockquote we haven't finished handling
+ rv = OutdentPartOfBlock(*curBlockQuote, *firstBQChild, *lastBQChild,
+ curBlockQuoteIsIndentedWithCSS,
+ getter_AddRefs(rememberedLeftBQ),
+ getter_AddRefs(rememberedRightBQ));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ // Make sure selection didn't stick to last piece of content in old bq (only
+ // a problem for collapsed selections)
+ if (rememberedLeftBQ || rememberedRightBQ) {
+ if (aSelection.Collapsed()) {
+ // Push selection past end of rememberedLeftBQ
+ NS_ENSURE_TRUE(aSelection.GetRangeAt(0), NS_OK);
+ nsCOMPtr<nsINode> startNode = aSelection.GetRangeAt(0)->GetStartParent();
+ int32_t startOffset = aSelection.GetRangeAt(0)->StartOffset();
+ if (rememberedLeftBQ &&
+ (startNode == rememberedLeftBQ ||
+ EditorUtils::IsDescendantOf(startNode, rememberedLeftBQ))) {
+ // Selection is inside rememberedLeftBQ - push it past it.
+ startNode = rememberedLeftBQ->GetParentNode();
+ startOffset = startNode ? 1 + startNode->IndexOf(rememberedLeftBQ) : 0;
+ aSelection.Collapse(startNode, startOffset);
+ }
+ // And pull selection before beginning of rememberedRightBQ
+ startNode = aSelection.GetRangeAt(0)->GetStartParent();
+ startOffset = aSelection.GetRangeAt(0)->StartOffset();
+ if (rememberedRightBQ &&
+ (startNode == rememberedRightBQ ||
+ EditorUtils::IsDescendantOf(startNode, rememberedRightBQ))) {
+ // Selection is inside rememberedRightBQ - push it before it.
+ startNode = rememberedRightBQ->GetParentNode();
+ startOffset = startNode ? startNode->IndexOf(rememberedRightBQ) : -1;
+ aSelection.Collapse(startNode, startOffset);
+ }
+ }
+ return NS_OK;
+ }
+ return NS_OK;
+}
+
+
+/**
+ * RemovePartOfBlock() splits aBlock and move aStartChild to aEndChild out of
+ * aBlock.
+ */
+nsresult
+HTMLEditRules::RemovePartOfBlock(Element& aBlock,
+ nsIContent& aStartChild,
+ nsIContent& aEndChild)
+{
+ SplitBlock(aBlock, aStartChild, aEndChild);
+ // Get rid of part of blockquote we are outdenting
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv = mHTMLEditor->RemoveBlockContainer(aBlock);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+void
+HTMLEditRules::SplitBlock(Element& aBlock,
+ nsIContent& aStartChild,
+ nsIContent& aEndChild,
+ nsIContent** aOutLeftNode,
+ nsIContent** aOutRightNode,
+ nsIContent** aOutMiddleNode)
+{
+ // aStartChild and aEndChild must be exclusive descendants of aBlock
+ MOZ_ASSERT(EditorUtils::IsDescendantOf(&aStartChild, &aBlock) &&
+ EditorUtils::IsDescendantOf(&aEndChild, &aBlock));
+ NS_ENSURE_TRUE_VOID(mHTMLEditor);
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+
+ // Get split point location
+ OwningNonNull<nsIContent> startParent = *aStartChild.GetParent();
+ int32_t startOffset = startParent->IndexOf(&aStartChild);
+
+ // Do the splits!
+ nsCOMPtr<nsIContent> newMiddleNode1;
+ htmlEditor->SplitNodeDeep(aBlock, startParent, startOffset,
+ HTMLEditor::EmptyContainers::no,
+ aOutLeftNode, getter_AddRefs(newMiddleNode1));
+
+ // Get split point location
+ OwningNonNull<nsIContent> endParent = *aEndChild.GetParent();
+ // +1 because we want to be after the child
+ int32_t endOffset = 1 + endParent->IndexOf(&aEndChild);
+
+ // Do the splits!
+ nsCOMPtr<nsIContent> newMiddleNode2;
+ htmlEditor->SplitNodeDeep(aBlock, endParent, endOffset,
+ HTMLEditor::EmptyContainers::no,
+ getter_AddRefs(newMiddleNode2), aOutRightNode);
+
+ if (aOutMiddleNode) {
+ if (newMiddleNode2) {
+ newMiddleNode2.forget(aOutMiddleNode);
+ } else {
+ newMiddleNode1.forget(aOutMiddleNode);
+ }
+ }
+}
+
+nsresult
+HTMLEditRules::OutdentPartOfBlock(Element& aBlock,
+ nsIContent& aStartChild,
+ nsIContent& aEndChild,
+ bool aIsBlockIndentedWithCSS,
+ nsIContent** aOutLeftNode,
+ nsIContent** aOutRightNode)
+{
+ MOZ_ASSERT(aOutLeftNode && aOutRightNode);
+
+ nsCOMPtr<nsIContent> middleNode;
+ SplitBlock(aBlock, aStartChild, aEndChild, aOutLeftNode, aOutRightNode,
+ getter_AddRefs(middleNode));
+
+ NS_ENSURE_STATE(middleNode);
+
+ if (!aIsBlockIndentedWithCSS) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv = mHTMLEditor->RemoveBlockContainer(*middleNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else if (middleNode->IsElement()) {
+ // We do nothing if middleNode isn't an element
+ nsresult rv = ChangeIndentation(*middleNode->AsElement(), Change::minus);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+/**
+ * ConvertListType() converts list type and list item type.
+ */
+nsresult
+HTMLEditRules::ConvertListType(Element* aList,
+ Element** aOutList,
+ nsIAtom* aListType,
+ nsIAtom* aItemType)
+{
+ MOZ_ASSERT(aList);
+ MOZ_ASSERT(aOutList);
+ MOZ_ASSERT(aListType);
+ MOZ_ASSERT(aItemType);
+
+ nsCOMPtr<nsINode> child = aList->GetFirstChild();
+ while (child) {
+ if (child->IsElement()) {
+ dom::Element* element = child->AsElement();
+ if (HTMLEditUtils::IsListItem(element) &&
+ !element->IsHTMLElement(aItemType)) {
+ child = mHTMLEditor->ReplaceContainer(element, aItemType);
+ NS_ENSURE_STATE(child);
+ } else if (HTMLEditUtils::IsList(element) &&
+ !element->IsHTMLElement(aListType)) {
+ nsCOMPtr<dom::Element> temp;
+ nsresult rv = ConvertListType(child->AsElement(), getter_AddRefs(temp),
+ aListType, aItemType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ child = temp.forget();
+ }
+ }
+ child = child->GetNextSibling();
+ }
+
+ if (aList->IsHTMLElement(aListType)) {
+ nsCOMPtr<dom::Element> list = aList->AsElement();
+ list.forget(aOutList);
+ return NS_OK;
+ }
+
+ *aOutList = mHTMLEditor->ReplaceContainer(aList, aListType).take();
+ NS_ENSURE_STATE(aOutList);
+
+ return NS_OK;
+}
+
+
+/**
+ * CreateStyleForInsertText() takes care of clearing and setting appropriate
+ * style nodes for text insertion.
+ */
+nsresult
+HTMLEditRules::CreateStyleForInsertText(Selection& aSelection,
+ nsIDocument& aDoc)
+{
+ MOZ_ASSERT(mHTMLEditor->mTypeInState);
+
+ bool weDidSomething = false;
+ NS_ENSURE_STATE(aSelection.GetRangeAt(0));
+ nsCOMPtr<nsINode> node = aSelection.GetRangeAt(0)->GetStartParent();
+ int32_t offset = aSelection.GetRangeAt(0)->StartOffset();
+
+ // next examine our present style and make sure default styles are either
+ // present or explicitly overridden. If neither, add the default style to
+ // the TypeInState
+ int32_t length = mHTMLEditor->mDefaultStyles.Length();
+ for (int32_t j = 0; j < length; j++) {
+ PropItem* propItem = mHTMLEditor->mDefaultStyles[j];
+ MOZ_ASSERT(propItem);
+ bool bFirst, bAny, bAll;
+
+ // GetInlineProperty also examine TypeInState. The only gotcha here is
+ // that a cleared property looks like an unset property. For now I'm
+ // assuming that's not a problem: that default styles will always be
+ // multivalue styles (like font face or size) where clearing the style
+ // means we want to go back to the default. If we ever wanted a "toggle"
+ // style like bold for a default, though, I'll have to add code to detect
+ // the difference between unset and explicitly cleared, else user would
+ // never be able to unbold, for instance.
+ nsAutoString curValue;
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv =
+ mHTMLEditor->GetInlinePropertyBase(*propItem->tag, &propItem->attr,
+ nullptr, &bFirst, &bAny, &bAll,
+ &curValue, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!bAny) {
+ // no style set for this prop/attr
+ mHTMLEditor->mTypeInState->SetProp(propItem->tag, propItem->attr,
+ propItem->value);
+ }
+ }
+
+ nsCOMPtr<Element> rootElement = aDoc.GetRootElement();
+ NS_ENSURE_STATE(rootElement);
+
+ // process clearing any styles first
+ nsAutoPtr<PropItem> item(mHTMLEditor->mTypeInState->TakeClearProperty());
+ while (item && node != rootElement) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv =
+ mHTMLEditor->ClearStyle(address_of(node), &offset,
+ item->tag, &item->attr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ item = mHTMLEditor->mTypeInState->TakeClearProperty();
+ weDidSomething = true;
+ }
+
+ // then process setting any styles
+ int32_t relFontSize = mHTMLEditor->mTypeInState->TakeRelativeFontSize();
+ item = mHTMLEditor->mTypeInState->TakeSetProperty();
+
+ if (item || relFontSize) {
+ // we have at least one style to add; make a new text node to insert style
+ // nodes above.
+ if (RefPtr<Text> text = node->GetAsText()) {
+ // if we are in a text node, split it
+ NS_ENSURE_STATE(mHTMLEditor);
+ offset = mHTMLEditor->SplitNodeDeep(*text, *text, offset);
+ NS_ENSURE_STATE(offset != -1);
+ node = node->GetParentNode();
+ }
+ if (!mHTMLEditor->IsContainer(node)) {
+ return NS_OK;
+ }
+ OwningNonNull<Text> newNode = aDoc.CreateTextNode(EmptyString());
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv = mHTMLEditor->InsertNode(*newNode, *node, offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ node = newNode;
+ offset = 0;
+ weDidSomething = true;
+
+ if (relFontSize) {
+ // dir indicated bigger versus smaller. 1 = bigger, -1 = smaller
+ HTMLEditor::FontSize dir = relFontSize > 0 ?
+ HTMLEditor::FontSize::incr : HTMLEditor::FontSize::decr;
+ for (int32_t j = 0; j < DeprecatedAbs(relFontSize); j++) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->RelativeFontChangeOnTextNode(dir, newNode, 0, -1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ while (item) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->SetInlinePropertyOnNode(*node->AsContent(),
+ *item->tag, &item->attr,
+ item->value);
+ NS_ENSURE_SUCCESS(rv, rv);
+ item = mHTMLEditor->mTypeInState->TakeSetProperty();
+ }
+ }
+ if (weDidSomething) {
+ return aSelection.Collapse(node, offset);
+ }
+
+ return NS_OK;
+}
+
+
+/**
+ * Figure out if aNode is (or is inside) an empty block. A block can have
+ * children and still be considered empty, if the children are empty or
+ * non-editable.
+ */
+nsresult
+HTMLEditRules::IsEmptyBlock(Element& aNode,
+ bool* aOutIsEmptyBlock,
+ MozBRCounts aMozBRCounts)
+{
+ MOZ_ASSERT(aOutIsEmptyBlock);
+ *aOutIsEmptyBlock = true;
+
+ NS_ENSURE_TRUE(IsBlockNode(aNode), NS_ERROR_NULL_POINTER);
+
+ return mHTMLEditor->IsEmptyNode(aNode.AsDOMNode(), aOutIsEmptyBlock,
+ aMozBRCounts == MozBRCounts::yes ? false
+ : true);
+}
+
+
+nsresult
+HTMLEditRules::WillAlign(Selection& aSelection,
+ const nsAString& aAlignType,
+ bool* aCancel,
+ bool* aHandled)
+{
+ MOZ_ASSERT(aCancel && aHandled);
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+
+ WillInsert(aSelection, aCancel);
+
+ // Initialize out param. We want to ignore result of WillInsert().
+ *aCancel = false;
+ *aHandled = false;
+
+ nsresult rv = NormalizeSelection(&aSelection);
+ NS_ENSURE_SUCCESS(rv, rv);
+ AutoSelectionRestorer selectionRestorer(&aSelection, htmlEditor);
+
+ // Convert the selection ranges into "promoted" selection ranges: This
+ // basically just expands the range to include the immediate block parent,
+ // and then further expands to include any ancestors whose children are all
+ // in the range
+ *aHandled = true;
+ nsTArray<OwningNonNull<nsINode>> nodeArray;
+ rv = GetNodesFromSelection(aSelection, EditAction::align, nodeArray);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If we don't have any nodes, or we have only a single br, then we are
+ // creating an empty alignment div. We have to do some different things for
+ // these.
+ bool emptyDiv = nodeArray.IsEmpty();
+ if (nodeArray.Length() == 1) {
+ OwningNonNull<nsINode> node = nodeArray[0];
+
+ if (HTMLEditUtils::SupportsAlignAttr(GetAsDOMNode(node))) {
+ // The node is a table element, an hr, a paragraph, a div or a section
+ // header; in HTML 4, it can directly carry the ALIGN attribute and we
+ // don't need to make a div! If we are in CSS mode, all the work is done
+ // in AlignBlock
+ rv = AlignBlock(*node->AsElement(), aAlignType, ContentsOnly::yes);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+ }
+
+ if (TextEditUtils::IsBreak(node)) {
+ // The special case emptyDiv code (below) that consumes BRs can cause
+ // tables to split if the start node of the selection is not in a table
+ // cell or caption, for example parent is a <tr>. Avoid this unnecessary
+ // splitting if possible by leaving emptyDiv FALSE so that we fall
+ // through to the normal case alignment code.
+ //
+ // XXX: It seems a little error prone for the emptyDiv special case code
+ // to assume that the start node of the selection is the parent of the
+ // single node in the nodeArray, as the paragraph above points out. Do we
+ // rely on the selection start node because of the fact that nodeArray
+ // can be empty? We should probably revisit this issue. - kin
+
+ NS_ENSURE_STATE(aSelection.GetRangeAt(0) &&
+ aSelection.GetRangeAt(0)->GetStartParent());
+ OwningNonNull<nsINode> parent =
+ *aSelection.GetRangeAt(0)->GetStartParent();
+
+ emptyDiv = !HTMLEditUtils::IsTableElement(parent) ||
+ HTMLEditUtils::IsTableCellOrCaption(parent);
+ }
+ }
+ if (emptyDiv) {
+ nsCOMPtr<nsINode> parent =
+ aSelection.GetRangeAt(0) ? aSelection.GetRangeAt(0)->GetStartParent()
+ : nullptr;
+ NS_ENSURE_STATE(parent);
+ int32_t offset = aSelection.GetRangeAt(0)->StartOffset();
+
+ rv = SplitAsNeeded(*nsGkAtoms::div, parent, offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Consume a trailing br, if any. This is to keep an alignment from
+ // creating extra lines, if possible.
+ nsCOMPtr<nsIContent> brContent =
+ htmlEditor->GetNextHTMLNode(parent, offset);
+ if (brContent && TextEditUtils::IsBreak(brContent)) {
+ // Making use of html structure... if next node after where we are
+ // putting our div is not a block, then the br we found is in same block
+ // we are, so it's safe to consume it.
+ nsCOMPtr<nsIContent> sibling = htmlEditor->GetNextHTMLSibling(parent,
+ offset);
+ if (sibling && !IsBlockNode(*sibling)) {
+ rv = htmlEditor->DeleteNode(brContent);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ nsCOMPtr<Element> div = htmlEditor->CreateNode(nsGkAtoms::div, parent,
+ offset);
+ NS_ENSURE_STATE(div);
+ // Remember our new block for postprocessing
+ mNewBlock = div;
+ // Set up the alignment on the div, using HTML or CSS
+ rv = AlignBlock(*div, aAlignType, ContentsOnly::yes);
+ NS_ENSURE_SUCCESS(rv, rv);
+ *aHandled = true;
+ // Put in a moz-br so that it won't get deleted
+ rv = CreateMozBR(div->AsDOMNode(), 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aSelection.Collapse(div, 0);
+ // Don't restore the selection
+ selectionRestorer.Abort();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+ }
+
+ // Next we detect all the transitions in the array, where a transition
+ // means that adjacent nodes in the array don't have the same parent.
+
+ nsTArray<bool> transitionList;
+ MakeTransitionList(nodeArray, transitionList);
+
+ // Okay, now go through all the nodes and give them an align attrib or put
+ // them in a div, or whatever is appropriate. Woohoo!
+
+ nsCOMPtr<Element> curDiv;
+ bool useCSS = htmlEditor->IsCSSEnabled();
+ for (size_t i = 0; i < nodeArray.Length(); i++) {
+ auto& curNode = nodeArray[i];
+ // Here's where we actually figure out what to do
+
+ // Ignore all non-editable nodes. Leave them be.
+ if (!htmlEditor->IsEditable(curNode)) {
+ continue;
+ }
+
+ // The node is a table element, an hr, a paragraph, a div or a section
+ // header; in HTML 4, it can directly carry the ALIGN attribute and we
+ // don't need to nest it, just set the alignment. In CSS, assign the
+ // corresponding CSS styles in AlignBlock
+ if (HTMLEditUtils::SupportsAlignAttr(GetAsDOMNode(curNode))) {
+ rv = AlignBlock(*curNode->AsElement(), aAlignType, ContentsOnly::no);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Clear out curDiv so that we don't put nodes after this one into it
+ curDiv = nullptr;
+ continue;
+ }
+
+ nsCOMPtr<nsINode> curParent = curNode->GetParentNode();
+ int32_t offset = curParent ? curParent->IndexOf(curNode) : -1;
+
+ // Skip insignificant formatting text nodes to prevent unnecessary
+ // structure splitting!
+ bool isEmptyTextNode = false;
+ if (curNode->GetAsText() &&
+ ((HTMLEditUtils::IsTableElement(curParent) &&
+ !HTMLEditUtils::IsTableCellOrCaption(*curParent)) ||
+ HTMLEditUtils::IsList(curParent) ||
+ (NS_SUCCEEDED(htmlEditor->IsEmptyNode(curNode, &isEmptyTextNode)) &&
+ isEmptyTextNode))) {
+ continue;
+ }
+
+ // If it's a list item, or a list inside a list, forget any "current" div,
+ // and instead put divs inside the appropriate block (td, li, etc.)
+ if (HTMLEditUtils::IsListItem(curNode) ||
+ HTMLEditUtils::IsList(curNode)) {
+ rv = RemoveAlignment(GetAsDOMNode(curNode), aAlignType, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (useCSS) {
+ htmlEditor->mCSSEditUtils->SetCSSEquivalentToHTMLStyle(
+ curNode->AsElement(), nullptr, &NS_LITERAL_STRING("align"),
+ &aAlignType, false);
+ curDiv = nullptr;
+ continue;
+ }
+ if (HTMLEditUtils::IsList(curParent)) {
+ // If we don't use CSS, add a contraint to list element: they have to
+ // be inside another list, i.e., >= second level of nesting
+ rv = AlignInnerBlocks(*curNode, &aAlignType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ curDiv = nullptr;
+ continue;
+ }
+ // Clear out curDiv so that we don't put nodes after this one into it
+ }
+
+ // Need to make a div to put things in if we haven't already, or if this
+ // node doesn't go in div we used earlier.
+ if (!curDiv || transitionList[i]) {
+ // First, check that our element can contain a div.
+ if (!mTextEditor->CanContainTag(*curParent, *nsGkAtoms::div)) {
+ // Cancelled
+ return NS_OK;
+ }
+
+ rv = SplitAsNeeded(*nsGkAtoms::div, curParent, offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ curDiv = htmlEditor->CreateNode(nsGkAtoms::div, curParent, offset);
+ NS_ENSURE_STATE(curDiv);
+ // Remember our new block for postprocessing
+ mNewBlock = curDiv;
+ // Set up the alignment on the div
+ rv = AlignBlock(*curDiv, aAlignType, ContentsOnly::yes);
+ }
+
+ NS_ENSURE_STATE(curNode->IsContent());
+
+ // Tuck the node into the end of the active div
+ rv = htmlEditor->MoveNode(curNode->AsContent(), curDiv, -1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+
+/**
+ * AlignInnerBlocks() aligns inside table cells or list items.
+ */
+nsresult
+HTMLEditRules::AlignInnerBlocks(nsINode& aNode,
+ const nsAString* alignType)
+{
+ NS_ENSURE_TRUE(alignType, NS_ERROR_NULL_POINTER);
+
+ // Gather list of table cells or list items
+ nsTArray<OwningNonNull<nsINode>> nodeArray;
+ TableCellAndListItemFunctor functor;
+ DOMIterator iter(aNode);
+ iter.AppendList(functor, nodeArray);
+
+ // Now that we have the list, align their contents as requested
+ for (auto& node : nodeArray) {
+ nsresult rv = AlignBlockContents(GetAsDOMNode(node), alignType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+
+/**
+ * AlignBlockContents() aligns contents of a block element.
+ */
+nsresult
+HTMLEditRules::AlignBlockContents(nsIDOMNode* aNode,
+ const nsAString* alignType)
+{
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ NS_ENSURE_TRUE(node && alignType, NS_ERROR_NULL_POINTER);
+ nsCOMPtr<nsIContent> firstChild, lastChild;
+ nsCOMPtr<Element> divNode;
+
+ bool useCSS = mHTMLEditor->IsCSSEnabled();
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ firstChild = mHTMLEditor->GetFirstEditableChild(*node);
+ NS_ENSURE_STATE(mHTMLEditor);
+ lastChild = mHTMLEditor->GetLastEditableChild(*node);
+ NS_NAMED_LITERAL_STRING(attr, "align");
+ if (!firstChild) {
+ // this cell has no content, nothing to align
+ } else if (firstChild == lastChild &&
+ firstChild->IsHTMLElement(nsGkAtoms::div)) {
+ // the cell already has a div containing all of its content: just
+ // act on this div.
+ nsCOMPtr<nsIDOMElement> divElem = do_QueryInterface(firstChild);
+ if (useCSS) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv = mHTMLEditor->SetAttributeOrEquivalent(divElem, attr,
+ *alignType, false);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv = mHTMLEditor->SetAttribute(divElem, attr, *alignType);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+ } else {
+ // else we need to put in a div, set the alignment, and toss in all the children
+ NS_ENSURE_STATE(mHTMLEditor);
+ divNode = mHTMLEditor->CreateNode(nsGkAtoms::div, node, 0);
+ NS_ENSURE_STATE(divNode);
+ // set up the alignment on the div
+ nsCOMPtr<nsIDOMElement> divElem = do_QueryInterface(divNode);
+ if (useCSS) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv =
+ mHTMLEditor->SetAttributeOrEquivalent(divElem, attr, *alignType, false);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv = mHTMLEditor->SetAttribute(divElem, attr, *alignType);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+ // tuck the children into the end of the active div
+ while (lastChild && (lastChild != divNode)) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv = mHTMLEditor->MoveNode(lastChild, divNode, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_STATE(mHTMLEditor);
+ lastChild = mHTMLEditor->GetLastEditableChild(*node);
+ }
+ }
+ return NS_OK;
+}
+
+/**
+ * CheckForEmptyBlock() is called by WillDeleteSelection() to detect and handle
+ * case of deleting from inside an empty block.
+ */
+nsresult
+HTMLEditRules::CheckForEmptyBlock(nsINode* aStartNode,
+ Element* aBodyNode,
+ Selection* aSelection,
+ nsIEditor::EDirection aAction,
+ bool* aHandled)
+{
+ // If the editing host is an inline element, bail out early.
+ if (aBodyNode && IsInlineNode(*aBodyNode)) {
+ return NS_OK;
+ }
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+
+ // If we are inside an empty block, delete it. Note: do NOT delete table
+ // elements this way.
+ nsCOMPtr<Element> block = htmlEditor->GetBlock(*aStartNode);
+ bool bIsEmptyNode;
+ nsCOMPtr<Element> emptyBlock;
+ if (block && block != aBodyNode) {
+ // Efficiency hack, avoiding IsEmptyNode() call when in body
+ nsresult rv = htmlEditor->IsEmptyNode(block, &bIsEmptyNode, true, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ while (block && bIsEmptyNode && !HTMLEditUtils::IsTableElement(block) &&
+ block != aBodyNode) {
+ emptyBlock = block;
+ block = htmlEditor->GetBlockNodeParent(emptyBlock);
+ if (block) {
+ rv = htmlEditor->IsEmptyNode(block, &bIsEmptyNode, true, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ }
+
+ if (emptyBlock && emptyBlock->IsEditable()) {
+ nsCOMPtr<nsINode> blockParent = emptyBlock->GetParentNode();
+ NS_ENSURE_TRUE(blockParent, NS_ERROR_FAILURE);
+ int32_t offset = blockParent->IndexOf(emptyBlock);
+
+ if (HTMLEditUtils::IsListItem(emptyBlock)) {
+ // Are we the first list item in the list?
+ bool bIsFirst;
+ NS_ENSURE_STATE(htmlEditor);
+ nsresult rv =
+ htmlEditor->IsFirstEditableChild(GetAsDOMNode(emptyBlock), &bIsFirst);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (bIsFirst) {
+ nsCOMPtr<nsINode> listParent = blockParent->GetParentNode();
+ NS_ENSURE_TRUE(listParent, NS_ERROR_FAILURE);
+ int32_t listOffset = listParent->IndexOf(blockParent);
+ // If we are a sublist, skip the br creation
+ if (!HTMLEditUtils::IsList(listParent)) {
+ // Create a br before list
+ NS_ENSURE_STATE(htmlEditor);
+ nsCOMPtr<Element> br =
+ htmlEditor->CreateBR(listParent, listOffset);
+ NS_ENSURE_STATE(br);
+ // Adjust selection to be right before it
+ rv = aSelection->Collapse(listParent, listOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // Else just let selection percolate up. We'll adjust it in
+ // AfterEdit()
+ }
+ } else {
+ if (aAction == nsIEditor::eNext || aAction == nsIEditor::eNextWord ||
+ aAction == nsIEditor::eToEndOfLine) {
+ // Move to the start of the next node, if any
+ nsCOMPtr<nsIContent> nextNode = htmlEditor->GetNextNode(blockParent,
+ offset + 1, true);
+ if (nextNode) {
+ EditorDOMPoint pt = GetGoodSelPointForNode(*nextNode, aAction);
+ nsresult rv = aSelection->Collapse(pt.node, pt.offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // Adjust selection to be right after it.
+ nsresult rv = aSelection->Collapse(blockParent, offset + 1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ } else if (aAction == nsIEditor::ePrevious ||
+ aAction == nsIEditor::ePreviousWord ||
+ aAction == nsIEditor::eToBeginningOfLine) {
+ // Move to the end of the previous node
+ nsCOMPtr<nsIContent> priorNode = htmlEditor->GetPriorNode(blockParent,
+ offset,
+ true);
+ if (priorNode) {
+ EditorDOMPoint pt = GetGoodSelPointForNode(*priorNode, aAction);
+ nsresult rv = aSelection->Collapse(pt.node, pt.offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ nsresult rv = aSelection->Collapse(blockParent, offset + 1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ } else if (aAction != nsIEditor::eNone) {
+ NS_RUNTIMEABORT("CheckForEmptyBlock doesn't support this action yet");
+ }
+ }
+ NS_ENSURE_STATE(htmlEditor);
+ *aHandled = true;
+ nsresult rv = htmlEditor->DeleteNode(emptyBlock);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+Element*
+HTMLEditRules::CheckForInvisibleBR(Element& aBlock,
+ BRLocation aWhere,
+ int32_t aOffset)
+{
+ nsCOMPtr<nsINode> testNode;
+ int32_t testOffset = 0;
+
+ if (aWhere == BRLocation::blockEnd) {
+ // No block crossing
+ nsCOMPtr<nsIContent> rightmostNode =
+ mHTMLEditor->GetRightmostChild(&aBlock, true);
+
+ if (!rightmostNode) {
+ return nullptr;
+ }
+
+ testNode = rightmostNode->GetParentNode();
+ // Use offset + 1, so last node is included in our evaluation
+ testOffset = testNode->IndexOf(rightmostNode) + 1;
+ } else if (aOffset) {
+ testNode = &aBlock;
+ // We'll check everything to the left of the input position
+ testOffset = aOffset;
+ } else {
+ return nullptr;
+ }
+
+ WSRunObject wsTester(mHTMLEditor, testNode, testOffset);
+ if (WSType::br == wsTester.mStartReason) {
+ return wsTester.mStartReasonNode->AsElement();
+ }
+
+ return nullptr;
+}
+
+/**
+ * aLists and aTables allow the caller to specify what kind of content to
+ * "look inside". If aTables is Tables::yes, look inside any table content,
+ * and insert the inner content into the supplied issupportsarray at offset
+ * aIndex. Similarly with aLists and list content. aIndex is updated to
+ * point past inserted elements.
+ */
+void
+HTMLEditRules::GetInnerContent(
+ nsINode& aNode,
+ nsTArray<OwningNonNull<nsINode>>& aOutArrayOfNodes,
+ int32_t* aIndex,
+ Lists aLists,
+ Tables aTables)
+{
+ MOZ_ASSERT(aIndex);
+
+ for (nsCOMPtr<nsIContent> node = mHTMLEditor->GetFirstEditableChild(aNode);
+ node; node = node->GetNextSibling()) {
+ if ((aLists == Lists::yes && (HTMLEditUtils::IsList(node) ||
+ HTMLEditUtils::IsListItem(node))) ||
+ (aTables == Tables::yes && HTMLEditUtils::IsTableElement(node))) {
+ GetInnerContent(*node, aOutArrayOfNodes, aIndex, aLists, aTables);
+ } else {
+ aOutArrayOfNodes.InsertElementAt(*aIndex, *node);
+ (*aIndex)++;
+ }
+ }
+}
+
+/**
+ * Promotes selection to include blocks that have all their children selected.
+ */
+nsresult
+HTMLEditRules::ExpandSelectionForDeletion(Selection& aSelection)
+{
+ // Don't need to touch collapsed selections
+ if (aSelection.Collapsed()) {
+ return NS_OK;
+ }
+
+ // We don't need to mess with cell selections, and we assume multirange
+ // selections are those.
+ if (aSelection.RangeCount() != 1) {
+ return NS_OK;
+ }
+
+ // Find current sel start and end
+ NS_ENSURE_TRUE(aSelection.GetRangeAt(0), NS_ERROR_NULL_POINTER);
+ OwningNonNull<nsRange> range = *aSelection.GetRangeAt(0);
+
+ nsCOMPtr<nsINode> selStartNode = range->GetStartParent();
+ int32_t selStartOffset = range->StartOffset();
+ nsCOMPtr<nsINode> selEndNode = range->GetEndParent();
+ int32_t selEndOffset = range->EndOffset();
+
+ // Find current selection common block parent
+ nsCOMPtr<Element> selCommon =
+ HTMLEditor::GetBlock(*range->GetCommonAncestor());
+ NS_ENSURE_STATE(selCommon);
+
+ // Set up for loops and cache our root element
+ nsCOMPtr<nsINode> firstBRParent;
+ nsCOMPtr<nsINode> unused;
+ int32_t visOffset = 0, firstBROffset = 0;
+ WSType wsType;
+ nsCOMPtr<Element> root = mHTMLEditor->GetActiveEditingHost();
+ NS_ENSURE_TRUE(root, NS_ERROR_FAILURE);
+
+ // Find previous visible thingy before start of selection
+ if (selStartNode != selCommon && selStartNode != root) {
+ while (true) {
+ WSRunObject wsObj(mHTMLEditor, selStartNode, selStartOffset);
+ wsObj.PriorVisibleNode(selStartNode, selStartOffset, address_of(unused),
+ &visOffset, &wsType);
+ if (wsType != WSType::thisBlock) {
+ break;
+ }
+ // We want to keep looking up. But stop if we are crossing table
+ // element boundaries, or if we hit the root.
+ if (HTMLEditUtils::IsTableElement(wsObj.mStartReasonNode) ||
+ selCommon == wsObj.mStartReasonNode ||
+ root == wsObj.mStartReasonNode) {
+ break;
+ }
+ selStartNode = wsObj.mStartReasonNode->GetParentNode();
+ selStartOffset = selStartNode ?
+ selStartNode->IndexOf(wsObj.mStartReasonNode) : -1;
+ }
+ }
+
+ // Find next visible thingy after end of selection
+ if (selEndNode != selCommon && selEndNode != root) {
+ for (;;) {
+ WSRunObject wsObj(mHTMLEditor, selEndNode, selEndOffset);
+ wsObj.NextVisibleNode(selEndNode, selEndOffset, address_of(unused),
+ &visOffset, &wsType);
+ if (wsType == WSType::br) {
+ if (mHTMLEditor->IsVisBreak(wsObj.mEndReasonNode)) {
+ break;
+ }
+ if (!firstBRParent) {
+ firstBRParent = selEndNode;
+ firstBROffset = selEndOffset;
+ }
+ selEndNode = wsObj.mEndReasonNode->GetParentNode();
+ selEndOffset = selEndNode
+ ? selEndNode->IndexOf(wsObj.mEndReasonNode) + 1 : 0;
+ } else if (wsType == WSType::thisBlock) {
+ // We want to keep looking up. But stop if we are crossing table
+ // element boundaries, or if we hit the root.
+ if (HTMLEditUtils::IsTableElement(wsObj.mEndReasonNode) ||
+ selCommon == wsObj.mEndReasonNode ||
+ root == wsObj.mEndReasonNode) {
+ break;
+ }
+ selEndNode = wsObj.mEndReasonNode->GetParentNode();
+ selEndOffset = 1 + selEndNode->IndexOf(wsObj.mEndReasonNode);
+ } else {
+ break;
+ }
+ }
+ }
+ // Now set the selection to the new range
+ aSelection.Collapse(selStartNode, selStartOffset);
+
+ // Expand selection endpoint only if we didn't pass a br, or if we really
+ // needed to pass that br (i.e., its block is now totally selected)
+ bool doEndExpansion = true;
+ if (firstBRParent) {
+ // Find block node containing br
+ nsCOMPtr<Element> brBlock = HTMLEditor::GetBlock(*firstBRParent);
+ bool nodeBefore = false, nodeAfter = false;
+
+ // Create a range that represents expanded selection
+ RefPtr<nsRange> range = new nsRange(selStartNode);
+ nsresult rv = range->SetStart(selStartNode, selStartOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = range->SetEnd(selEndNode, selEndOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Check if block is entirely inside range
+ if (brBlock) {
+ nsRange::CompareNodeToRange(brBlock, range, &nodeBefore, &nodeAfter);
+ }
+
+ // If block isn't contained, forgo grabbing the br in expanded selection
+ if (nodeBefore || nodeAfter) {
+ doEndExpansion = false;
+ }
+ }
+ if (doEndExpansion) {
+ nsresult rv = aSelection.Extend(selEndNode, selEndOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // Only expand to just before br
+ nsresult rv = aSelection.Extend(firstBRParent, firstBROffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+/**
+ * NormalizeSelection() tweaks non-collapsed selections to be more "natural".
+ * Idea here is to adjust selection endpoint so that they do not cross breaks
+ * or block boundaries unless something editable beyond that boundary is also
+ * selected. This adjustment makes it much easier for the various block
+ * operations to determine what nodes to act on.
+ */
+nsresult
+HTMLEditRules::NormalizeSelection(Selection* inSelection)
+{
+ NS_ENSURE_TRUE(inSelection, NS_ERROR_NULL_POINTER);
+
+ // don't need to touch collapsed selections
+ if (inSelection->Collapsed()) {
+ return NS_OK;
+ }
+
+ int32_t rangeCount;
+ nsresult rv = inSelection->GetRangeCount(&rangeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // we don't need to mess with cell selections, and we assume multirange selections are those.
+ if (rangeCount != 1) {
+ return NS_OK;
+ }
+
+ RefPtr<nsRange> range = inSelection->GetRangeAt(0);
+ NS_ENSURE_TRUE(range, NS_ERROR_NULL_POINTER);
+ nsCOMPtr<nsIDOMNode> startNode, endNode;
+ int32_t startOffset, endOffset;
+ nsCOMPtr<nsIDOMNode> newStartNode, newEndNode;
+ int32_t newStartOffset, newEndOffset;
+
+ rv = range->GetStartContainer(getter_AddRefs(startNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = range->GetStartOffset(&startOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = range->GetEndContainer(getter_AddRefs(endNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = range->GetEndOffset(&endOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // adjusted values default to original values
+ newStartNode = startNode;
+ newStartOffset = startOffset;
+ newEndNode = endNode;
+ newEndOffset = endOffset;
+
+ // some locals we need for whitespace code
+ nsCOMPtr<nsINode> unused;
+ int32_t offset;
+ WSType wsType;
+
+ // let the whitespace code do the heavy lifting
+ WSRunObject wsEndObj(mHTMLEditor, endNode, endOffset);
+ // is there any intervening visible whitespace? if so we can't push selection past that,
+ // it would visibly change maening of users selection
+ nsCOMPtr<nsINode> endNode_(do_QueryInterface(endNode));
+ wsEndObj.PriorVisibleNode(endNode_, endOffset, address_of(unused),
+ &offset, &wsType);
+ if (wsType != WSType::text && wsType != WSType::normalWS) {
+ // eThisBlock and eOtherBlock conveniently distinquish cases
+ // of going "down" into a block and "up" out of a block.
+ if (wsEndObj.mStartReason == WSType::otherBlock) {
+ // endpoint is just after the close of a block.
+ nsCOMPtr<nsIDOMNode> child =
+ GetAsDOMNode(mHTMLEditor->GetRightmostChild(wsEndObj.mStartReasonNode,
+ true));
+ if (child) {
+ newEndNode = EditorBase::GetNodeLocation(child, &newEndOffset);
+ ++newEndOffset; // offset *after* child
+ }
+ // else block is empty - we can leave selection alone here, i think.
+ } else if (wsEndObj.mStartReason == WSType::thisBlock) {
+ // endpoint is just after start of this block
+ nsCOMPtr<nsIDOMNode> child;
+ NS_ENSURE_STATE(mHTMLEditor);
+ mHTMLEditor->GetPriorHTMLNode(endNode, endOffset, address_of(child));
+ if (child) {
+ newEndNode = EditorBase::GetNodeLocation(child, &newEndOffset);
+ ++newEndOffset; // offset *after* child
+ }
+ // else block is empty - we can leave selection alone here, i think.
+ } else if (wsEndObj.mStartReason == WSType::br) {
+ // endpoint is just after break. lets adjust it to before it.
+ newEndNode =
+ EditorBase::GetNodeLocation(GetAsDOMNode(wsEndObj.mStartReasonNode),
+ &newEndOffset);
+ }
+ }
+
+
+ // similar dealio for start of range
+ WSRunObject wsStartObj(mHTMLEditor, startNode, startOffset);
+ // is there any intervening visible whitespace? if so we can't push selection past that,
+ // it would visibly change maening of users selection
+ nsCOMPtr<nsINode> startNode_(do_QueryInterface(startNode));
+ wsStartObj.NextVisibleNode(startNode_, startOffset, address_of(unused),
+ &offset, &wsType);
+ if (wsType != WSType::text && wsType != WSType::normalWS) {
+ // eThisBlock and eOtherBlock conveniently distinquish cases
+ // of going "down" into a block and "up" out of a block.
+ if (wsStartObj.mEndReason == WSType::otherBlock) {
+ // startpoint is just before the start of a block.
+ nsCOMPtr<nsIDOMNode> child =
+ GetAsDOMNode(mHTMLEditor->GetLeftmostChild(wsStartObj.mEndReasonNode,
+ true));
+ if (child) {
+ newStartNode = EditorBase::GetNodeLocation(child, &newStartOffset);
+ }
+ // else block is empty - we can leave selection alone here, i think.
+ } else if (wsStartObj.mEndReason == WSType::thisBlock) {
+ // startpoint is just before end of this block
+ nsCOMPtr<nsIDOMNode> child;
+ NS_ENSURE_STATE(mHTMLEditor);
+ mHTMLEditor->GetNextHTMLNode(startNode, startOffset, address_of(child));
+ if (child) {
+ newStartNode = EditorBase::GetNodeLocation(child, &newStartOffset);
+ }
+ // else block is empty - we can leave selection alone here, i think.
+ } else if (wsStartObj.mEndReason == WSType::br) {
+ // startpoint is just before a break. lets adjust it to after it.
+ newStartNode =
+ EditorBase::GetNodeLocation(GetAsDOMNode(wsStartObj.mEndReasonNode),
+ &newStartOffset);
+ ++newStartOffset; // offset *after* break
+ }
+ }
+
+ // there is a demented possiblity we have to check for. We might have a very strange selection
+ // that is not collapsed and yet does not contain any editable content, and satisfies some of the
+ // above conditions that cause tweaking. In this case we don't want to tweak the selection into
+ // a block it was never in, etc. There are a variety of strategies one might use to try to
+ // detect these cases, but I think the most straightforward is to see if the adjusted locations
+ // "cross" the old values: ie, new end before old start, or new start after old end. If so
+ // then just leave things alone.
+
+ int16_t comp;
+ comp = nsContentUtils::ComparePoints(startNode, startOffset,
+ newEndNode, newEndOffset);
+ if (comp == 1) {
+ return NS_OK; // New end before old start.
+ }
+ comp = nsContentUtils::ComparePoints(newStartNode, newStartOffset,
+ endNode, endOffset);
+ if (comp == 1) {
+ return NS_OK; // New start after old end.
+ }
+
+ // otherwise set selection to new values.
+ inSelection->Collapse(newStartNode, newStartOffset);
+ inSelection->Extend(newEndNode, newEndOffset);
+ return NS_OK;
+}
+
+/**
+ * GetPromotedPoint() figures out where a start or end point for a block
+ * operation really is.
+ */
+void
+HTMLEditRules::GetPromotedPoint(RulesEndpoint aWhere,
+ nsIDOMNode* aNode,
+ int32_t aOffset,
+ EditAction actionID,
+ nsCOMPtr<nsIDOMNode>* outNode,
+ int32_t* outOffset)
+{
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ MOZ_ASSERT(node && outNode && outOffset);
+
+ // default values
+ *outNode = node->AsDOMNode();
+ *outOffset = aOffset;
+
+ // we do one thing for text actions, something else entirely for other
+ // actions
+ if (actionID == EditAction::insertText ||
+ actionID == EditAction::insertIMEText ||
+ actionID == EditAction::insertBreak ||
+ actionID == EditAction::deleteText) {
+ bool isSpace, isNBSP;
+ nsCOMPtr<nsIContent> content = do_QueryInterface(node), temp;
+ // for text actions, we want to look backwards (or forwards, as
+ // appropriate) for additional whitespace or nbsp's. We may have to act on
+ // these later even though they are outside of the initial selection. Even
+ // if they are in another node!
+ while (content) {
+ int32_t offset;
+ if (aWhere == kStart) {
+ NS_ENSURE_TRUE_VOID(mHTMLEditor);
+ mHTMLEditor->IsPrevCharInNodeWhitespace(content, *outOffset,
+ &isSpace, &isNBSP,
+ getter_AddRefs(temp), &offset);
+ } else {
+ NS_ENSURE_TRUE_VOID(mHTMLEditor);
+ mHTMLEditor->IsNextCharInNodeWhitespace(content, *outOffset,
+ &isSpace, &isNBSP,
+ getter_AddRefs(temp), &offset);
+ }
+ if (isSpace || isNBSP) {
+ content = temp;
+ *outOffset = offset;
+ } else {
+ break;
+ }
+ }
+
+ *outNode = content->AsDOMNode();
+ return;
+ }
+
+ int32_t offset = aOffset;
+
+ // else not a text section. In this case we want to see if we should grab
+ // any adjacent inline nodes and/or parents and other ancestors
+ if (aWhere == kStart) {
+ // some special casing for text nodes
+ if (node->IsNodeOfType(nsINode::eTEXT)) {
+ if (!node->GetParentNode()) {
+ // Okay, can't promote any further
+ return;
+ }
+ offset = node->GetParentNode()->IndexOf(node);
+ node = node->GetParentNode();
+ }
+
+ // look back through any further inline nodes that aren't across a <br>
+ // from us, and that are enclosed in the same block.
+ NS_ENSURE_TRUE_VOID(mHTMLEditor);
+ nsCOMPtr<nsINode> priorNode =
+ mHTMLEditor->GetPriorHTMLNode(node, offset, true);
+
+ while (priorNode && priorNode->GetParentNode() &&
+ mHTMLEditor && !mHTMLEditor->IsVisBreak(priorNode) &&
+ !IsBlockNode(*priorNode)) {
+ offset = priorNode->GetParentNode()->IndexOf(priorNode);
+ node = priorNode->GetParentNode();
+ NS_ENSURE_TRUE_VOID(mHTMLEditor);
+ priorNode = mHTMLEditor->GetPriorHTMLNode(node, offset, true);
+ }
+
+ // finding the real start for this point. look up the tree for as long as
+ // we are the first node in the container, and as long as we haven't hit
+ // the body node.
+ NS_ENSURE_TRUE_VOID(mHTMLEditor);
+ nsCOMPtr<nsIContent> nearNode =
+ mHTMLEditor->GetPriorHTMLNode(node, offset, true);
+ while (!nearNode && !node->IsHTMLElement(nsGkAtoms::body) &&
+ node->GetParentNode()) {
+ // some cutoffs are here: we don't need to also include them in the
+ // aWhere == kEnd case. as long as they are in one or the other it will
+ // work. special case for outdent: don't keep looking up if we have
+ // found a blockquote element to act on
+ if (actionID == EditAction::outdent &&
+ node->IsHTMLElement(nsGkAtoms::blockquote)) {
+ break;
+ }
+
+ int32_t parentOffset = node->GetParentNode()->IndexOf(node);
+ nsCOMPtr<nsINode> parent = node->GetParentNode();
+
+ // Don't walk past the editable section. Note that we need to check
+ // before walking up to a parent because we need to return the parent
+ // object, so the parent itself might not be in the editable area, but
+ // it's OK if we're not performing a block-level action.
+ bool blockLevelAction = actionID == EditAction::indent ||
+ actionID == EditAction::outdent ||
+ actionID == EditAction::align ||
+ actionID == EditAction::makeBasicBlock;
+ NS_ENSURE_TRUE_VOID(mHTMLEditor);
+ if (!mHTMLEditor->IsDescendantOfEditorRoot(parent) &&
+ (blockLevelAction || !mHTMLEditor ||
+ !mHTMLEditor->IsDescendantOfEditorRoot(node))) {
+ NS_ENSURE_TRUE_VOID(mHTMLEditor);
+ break;
+ }
+
+ node = parent;
+ offset = parentOffset;
+ NS_ENSURE_TRUE_VOID(mHTMLEditor);
+ nearNode = mHTMLEditor->GetPriorHTMLNode(node, offset, true);
+ }
+ *outNode = node->AsDOMNode();
+ *outOffset = offset;
+ return;
+ }
+
+ // aWhere == kEnd
+ // some special casing for text nodes
+ if (node->IsNodeOfType(nsINode::eTEXT)) {
+ if (!node->GetParentNode()) {
+ // Okay, can't promote any further
+ return;
+ }
+ // want to be after the text node
+ offset = 1 + node->GetParentNode()->IndexOf(node);
+ node = node->GetParentNode();
+ }
+
+ // look ahead through any further inline nodes that aren't across a <br> from
+ // us, and that are enclosed in the same block.
+ NS_ENSURE_TRUE(mHTMLEditor, /* void */);
+ nsCOMPtr<nsIContent> nextNode =
+ mHTMLEditor->GetNextHTMLNode(node, offset, true);
+
+ while (nextNode && !IsBlockNode(*nextNode) && nextNode->GetParentNode()) {
+ offset = 1 + nextNode->GetParentNode()->IndexOf(nextNode);
+ node = nextNode->GetParentNode();
+ NS_ENSURE_TRUE_VOID(mHTMLEditor);
+ if (mHTMLEditor->IsVisBreak(nextNode)) {
+ break;
+ }
+
+ // Check for newlines in pre-formatted text nodes.
+ bool isPRE;
+ mHTMLEditor->IsPreformatted(nextNode->AsDOMNode(), &isPRE);
+ if (isPRE) {
+ nsCOMPtr<nsIDOMText> textNode = do_QueryInterface(nextNode);
+ if (textNode) {
+ nsAutoString tempString;
+ textNode->GetData(tempString);
+ int32_t newlinePos = tempString.FindChar(nsCRT::LF);
+ if (newlinePos >= 0) {
+ if (static_cast<uint32_t>(newlinePos) + 1 == tempString.Length()) {
+ // No need for special processing if the newline is at the end.
+ break;
+ }
+ *outNode = nextNode->AsDOMNode();
+ *outOffset = newlinePos + 1;
+ return;
+ }
+ }
+ }
+ NS_ENSURE_TRUE_VOID(mHTMLEditor);
+ nextNode = mHTMLEditor->GetNextHTMLNode(node, offset, true);
+ }
+
+ // finding the real end for this point. look up the tree for as long as we
+ // are the last node in the container, and as long as we haven't hit the body
+ // node.
+ NS_ENSURE_TRUE_VOID(mHTMLEditor);
+ nsCOMPtr<nsIContent> nearNode =
+ mHTMLEditor->GetNextHTMLNode(node, offset, true);
+ while (!nearNode && !node->IsHTMLElement(nsGkAtoms::body) &&
+ node->GetParentNode()) {
+ int32_t parentOffset = node->GetParentNode()->IndexOf(node);
+ nsCOMPtr<nsINode> parent = node->GetParentNode();
+
+ // Don't walk past the editable section. Note that we need to check before
+ // walking up to a parent because we need to return the parent object, so
+ // the parent itself might not be in the editable area, but it's OK.
+ if ((!mHTMLEditor || !mHTMLEditor->IsDescendantOfEditorRoot(node)) &&
+ (!mHTMLEditor || !mHTMLEditor->IsDescendantOfEditorRoot(parent))) {
+ NS_ENSURE_TRUE_VOID(mHTMLEditor);
+ break;
+ }
+
+ node = parent;
+ // we want to be AFTER nearNode
+ offset = parentOffset + 1;
+ NS_ENSURE_TRUE_VOID(mHTMLEditor);
+ nearNode = mHTMLEditor->GetNextHTMLNode(node, offset, true);
+ }
+ *outNode = node->AsDOMNode();
+ *outOffset = offset;
+}
+
+/**
+ * GetPromotedRanges() runs all the selection range endpoint through
+ * GetPromotedPoint().
+ */
+void
+HTMLEditRules::GetPromotedRanges(Selection& aSelection,
+ nsTArray<RefPtr<nsRange>>& outArrayOfRanges,
+ EditAction inOperationType)
+{
+ uint32_t rangeCount = aSelection.RangeCount();
+
+ for (uint32_t i = 0; i < rangeCount; i++) {
+ RefPtr<nsRange> selectionRange = aSelection.GetRangeAt(i);
+ MOZ_ASSERT(selectionRange);
+
+ // Clone range so we don't muck with actual selection ranges
+ RefPtr<nsRange> opRange = selectionRange->CloneRange();
+
+ // Make a new adjusted range to represent the appropriate block content.
+ // The basic idea is to push out the range endpoints to truly enclose the
+ // blocks that we will affect. This call alters opRange.
+ PromoteRange(*opRange, inOperationType);
+
+ // Stuff new opRange into array
+ outArrayOfRanges.AppendElement(opRange);
+ }
+}
+
+/**
+ * PromoteRange() expands a range to include any parents for which all editable
+ * children are already in range.
+ */
+void
+HTMLEditRules::PromoteRange(nsRange& aRange,
+ EditAction aOperationType)
+{
+ NS_ENSURE_TRUE(mHTMLEditor, );
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+
+ nsCOMPtr<nsINode> startNode = aRange.GetStartParent();
+ nsCOMPtr<nsINode> endNode = aRange.GetEndParent();
+ int32_t startOffset = aRange.StartOffset();
+ int32_t endOffset = aRange.EndOffset();
+
+ // MOOSE major hack:
+ // GetPromotedPoint doesn't really do the right thing for collapsed ranges
+ // inside block elements that contain nothing but a solo <br>. It's easier
+ // to put a workaround here than to revamp GetPromotedPoint. :-(
+ if (startNode == endNode && startOffset == endOffset) {
+ nsCOMPtr<Element> block = htmlEditor->GetBlock(*startNode);
+ if (block) {
+ bool bIsEmptyNode = false;
+ nsCOMPtr<nsIContent> root = htmlEditor->GetActiveEditingHost();
+ // Make sure we don't go higher than our root element in the content tree
+ NS_ENSURE_TRUE(root, );
+ if (!nsContentUtils::ContentIsDescendantOf(root, block)) {
+ htmlEditor->IsEmptyNode(block, &bIsEmptyNode, true, false);
+ }
+ if (bIsEmptyNode) {
+ startNode = block;
+ endNode = block;
+ startOffset = 0;
+ endOffset = block->Length();
+ }
+ }
+ }
+
+ // Make a new adjusted range to represent the appropriate block content.
+ // This is tricky. The basic idea is to push out the range endpoints to
+ // truly enclose the blocks that we will affect.
+
+ nsCOMPtr<nsIDOMNode> opStartNode;
+ nsCOMPtr<nsIDOMNode> opEndNode;
+ int32_t opStartOffset, opEndOffset;
+
+ GetPromotedPoint(kStart, GetAsDOMNode(startNode), startOffset,
+ aOperationType, address_of(opStartNode), &opStartOffset);
+ GetPromotedPoint(kEnd, GetAsDOMNode(endNode), endOffset, aOperationType,
+ address_of(opEndNode), &opEndOffset);
+
+ // Make sure that the new range ends up to be in the editable section.
+ if (!htmlEditor->IsDescendantOfEditorRoot(
+ EditorBase::GetNodeAtRangeOffsetPoint(opStartNode, opStartOffset)) ||
+ !htmlEditor->IsDescendantOfEditorRoot(
+ EditorBase::GetNodeAtRangeOffsetPoint(opEndNode, opEndOffset - 1))) {
+ return;
+ }
+
+ DebugOnly<nsresult> rv = aRange.SetStart(opStartNode, opStartOffset);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ rv = aRange.SetEnd(opEndNode, opEndOffset);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+}
+
+class UniqueFunctor final : public BoolDomIterFunctor
+{
+public:
+ explicit UniqueFunctor(nsTArray<OwningNonNull<nsINode>>& aArray)
+ : mArray(aArray)
+ {
+ }
+
+ // Used to build list of all nodes iterator covers.
+ virtual bool operator()(nsINode* aNode) const
+ {
+ return !mArray.Contains(aNode);
+ }
+
+private:
+ nsTArray<OwningNonNull<nsINode>>& mArray;
+};
+
+/**
+ * GetNodesForOperation() runs through the ranges in the array and construct a
+ * new array of nodes to be acted on.
+ */
+nsresult
+HTMLEditRules::GetNodesForOperation(
+ nsTArray<RefPtr<nsRange>>& aArrayOfRanges,
+ nsTArray<OwningNonNull<nsINode>>& aOutArrayOfNodes,
+ EditAction aOperationType,
+ TouchContent aTouchContent)
+{
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+
+ int32_t rangeCount = aArrayOfRanges.Length();
+ if (aTouchContent == TouchContent::yes) {
+ // Split text nodes. This is necessary, since GetPromotedPoint() may return a
+ // range ending in a text node in case where part of a pre-formatted
+ // elements needs to be moved.
+ for (int32_t i = 0; i < rangeCount; i++) {
+ RefPtr<nsRange> r = aArrayOfRanges[i];
+ nsCOMPtr<nsIContent> endParent = do_QueryInterface(r->GetEndParent());
+ if (!htmlEditor->IsTextNode(endParent)) {
+ continue;
+ }
+ nsCOMPtr<nsIDOMText> textNode = do_QueryInterface(endParent);
+ if (textNode) {
+ int32_t offset = r->EndOffset();
+ nsAutoString tempString;
+ textNode->GetData(tempString);
+
+ if (0 < offset && offset < static_cast<int32_t>(tempString.Length())) {
+ // Split the text node.
+ nsCOMPtr<nsIDOMNode> tempNode;
+ nsresult rv = htmlEditor->SplitNode(endParent->AsDOMNode(), offset,
+ getter_AddRefs(tempNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Correct the range.
+ // The new end parent becomes the parent node of the text.
+ nsCOMPtr<nsIContent> newParent = endParent->GetParent();
+ r->SetEnd(newParent, newParent->IndexOf(endParent));
+ }
+ }
+ }
+ }
+
+ // Bust up any inlines that cross our range endpoints, but only if we are
+ // allowed to touch content.
+
+ if (aTouchContent == TouchContent::yes) {
+ nsTArray<OwningNonNull<RangeItem>> rangeItemArray;
+ rangeItemArray.AppendElements(rangeCount);
+
+ // First register ranges for special editor gravity
+ for (int32_t i = 0; i < rangeCount; i++) {
+ rangeItemArray[i] = new RangeItem();
+ rangeItemArray[i]->StoreRange(aArrayOfRanges[0]);
+ htmlEditor->mRangeUpdater.RegisterRangeItem(rangeItemArray[i]);
+ aArrayOfRanges.RemoveElementAt(0);
+ }
+ // Now bust up inlines.
+ nsresult rv = NS_OK;
+ for (auto& item : Reversed(rangeItemArray)) {
+ rv = BustUpInlinesAtRangeEndpoints(*item);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ }
+ // Then unregister the ranges
+ for (auto& item : rangeItemArray) {
+ htmlEditor->mRangeUpdater.DropRangeItem(item);
+ aArrayOfRanges.AppendElement(item->GetRange());
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // Gather up a list of all the nodes
+ for (auto& range : aArrayOfRanges) {
+ DOMSubtreeIterator iter;
+ nsresult rv = iter.Init(*range);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (aOutArrayOfNodes.IsEmpty()) {
+ iter.AppendList(TrivialFunctor(), aOutArrayOfNodes);
+ } else {
+ // We don't want duplicates in aOutArrayOfNodes, so we use an
+ // iterator/functor that only return nodes that are not already in
+ // aOutArrayOfNodes.
+ nsTArray<OwningNonNull<nsINode>> nodes;
+ iter.AppendList(UniqueFunctor(aOutArrayOfNodes), nodes);
+ aOutArrayOfNodes.AppendElements(nodes);
+ }
+ }
+
+ // Certain operations should not act on li's and td's, but rather inside
+ // them. Alter the list as needed.
+ if (aOperationType == EditAction::makeBasicBlock) {
+ for (int32_t i = aOutArrayOfNodes.Length() - 1; i >= 0; i--) {
+ OwningNonNull<nsINode> node = aOutArrayOfNodes[i];
+ if (HTMLEditUtils::IsListItem(node)) {
+ int32_t j = i;
+ aOutArrayOfNodes.RemoveElementAt(i);
+ GetInnerContent(*node, aOutArrayOfNodes, &j);
+ }
+ }
+ }
+ // Indent/outdent already do something special for list items, but we still
+ // need to make sure we don't act on table elements
+ else if (aOperationType == EditAction::outdent ||
+ aOperationType == EditAction::indent ||
+ aOperationType == EditAction::setAbsolutePosition) {
+ for (int32_t i = aOutArrayOfNodes.Length() - 1; i >= 0; i--) {
+ OwningNonNull<nsINode> node = aOutArrayOfNodes[i];
+ if (HTMLEditUtils::IsTableElementButNotTable(node)) {
+ int32_t j = i;
+ aOutArrayOfNodes.RemoveElementAt(i);
+ GetInnerContent(*node, aOutArrayOfNodes, &j);
+ }
+ }
+ }
+ // Outdent should look inside of divs.
+ if (aOperationType == EditAction::outdent &&
+ !htmlEditor->IsCSSEnabled()) {
+ for (int32_t i = aOutArrayOfNodes.Length() - 1; i >= 0; i--) {
+ OwningNonNull<nsINode> node = aOutArrayOfNodes[i];
+ if (node->IsHTMLElement(nsGkAtoms::div)) {
+ int32_t j = i;
+ aOutArrayOfNodes.RemoveElementAt(i);
+ GetInnerContent(*node, aOutArrayOfNodes, &j, Lists::no, Tables::no);
+ }
+ }
+ }
+
+
+ // Post-process the list to break up inline containers that contain br's, but
+ // only for operations that might care, like making lists or paragraphs
+ if (aOperationType == EditAction::makeBasicBlock ||
+ aOperationType == EditAction::makeList ||
+ aOperationType == EditAction::align ||
+ aOperationType == EditAction::setAbsolutePosition ||
+ aOperationType == EditAction::indent ||
+ aOperationType == EditAction::outdent) {
+ for (int32_t i = aOutArrayOfNodes.Length() - 1; i >= 0; i--) {
+ OwningNonNull<nsINode> node = aOutArrayOfNodes[i];
+ if (aTouchContent == TouchContent::yes && IsInlineNode(node) &&
+ htmlEditor->IsContainer(node) && !htmlEditor->IsTextNode(node)) {
+ nsTArray<OwningNonNull<nsINode>> arrayOfInlines;
+ nsresult rv = BustUpInlinesAtBRs(*node->AsContent(), arrayOfInlines);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Put these nodes in aOutArrayOfNodes, replacing the current node
+ aOutArrayOfNodes.RemoveElementAt(i);
+ aOutArrayOfNodes.InsertElementsAt(i, arrayOfInlines);
+ }
+ }
+ }
+ return NS_OK;
+}
+
+void
+HTMLEditRules::GetChildNodesForOperation(
+ nsINode& aNode,
+ nsTArray<OwningNonNull<nsINode>>& outArrayOfNodes)
+{
+ for (nsCOMPtr<nsIContent> child = aNode.GetFirstChild();
+ child; child = child->GetNextSibling()) {
+ outArrayOfNodes.AppendElement(*child);
+ }
+}
+
+nsresult
+HTMLEditRules::GetListActionNodes(
+ nsTArray<OwningNonNull<nsINode>>& aOutArrayOfNodes,
+ EntireList aEntireList,
+ TouchContent aTouchContent)
+{
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+
+ RefPtr<Selection> selection = htmlEditor->GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE);
+
+ // Added this in so that ui code can ask to change an entire list, even if
+ // selection is only in part of it. used by list item dialog.
+ if (aEntireList == EntireList::yes) {
+ uint32_t rangeCount = selection->RangeCount();
+ for (uint32_t rangeIdx = 0; rangeIdx < rangeCount; ++rangeIdx) {
+ RefPtr<nsRange> range = selection->GetRangeAt(rangeIdx);
+ for (nsCOMPtr<nsINode> parent = range->GetCommonAncestor();
+ parent; parent = parent->GetParentNode()) {
+ if (HTMLEditUtils::IsList(parent)) {
+ aOutArrayOfNodes.AppendElement(*parent);
+ break;
+ }
+ }
+ }
+ // If we didn't find any nodes this way, then try the normal way. Perhaps
+ // the selection spans multiple lists but with no common list parent.
+ if (!aOutArrayOfNodes.IsEmpty()) {
+ return NS_OK;
+ }
+ }
+
+ {
+ // We don't like other people messing with our selection!
+ AutoTransactionsConserveSelection dontSpazMySelection(htmlEditor);
+
+ // contruct a list of nodes to act on.
+ nsresult rv = GetNodesFromSelection(*selection, EditAction::makeList,
+ aOutArrayOfNodes, aTouchContent);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Pre-process our list of nodes
+ for (int32_t i = aOutArrayOfNodes.Length() - 1; i >= 0; i--) {
+ OwningNonNull<nsINode> testNode = aOutArrayOfNodes[i];
+
+ // Remove all non-editable nodes. Leave them be.
+ if (!htmlEditor->IsEditable(testNode)) {
+ aOutArrayOfNodes.RemoveElementAt(i);
+ continue;
+ }
+
+ // Scan for table elements and divs. If we find table elements other than
+ // table, replace it with a list of any editable non-table content.
+ if (HTMLEditUtils::IsTableElementButNotTable(testNode)) {
+ int32_t j = i;
+ aOutArrayOfNodes.RemoveElementAt(i);
+ GetInnerContent(*testNode, aOutArrayOfNodes, &j, Lists::no);
+ }
+ }
+
+ // If there is only one node in the array, and it is a list, div, or
+ // blockquote, then look inside of it until we find inner list or content.
+ LookInsideDivBQandList(aOutArrayOfNodes);
+
+ return NS_OK;
+}
+
+void
+HTMLEditRules::LookInsideDivBQandList(
+ nsTArray<OwningNonNull<nsINode>>& aNodeArray)
+{
+ NS_ENSURE_TRUE(mHTMLEditor, );
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+
+ // If there is only one node in the array, and it is a list, div, or
+ // blockquote, then look inside of it until we find inner list or content.
+ if (aNodeArray.Length() != 1) {
+ return;
+ }
+
+ OwningNonNull<nsINode> curNode = aNodeArray[0];
+
+ while (curNode->IsHTMLElement(nsGkAtoms::div) ||
+ HTMLEditUtils::IsList(curNode) ||
+ curNode->IsHTMLElement(nsGkAtoms::blockquote)) {
+ // Dive as long as there's only one child, and it's a list, div, blockquote
+ uint32_t numChildren = htmlEditor->CountEditableChildren(curNode);
+ if (numChildren != 1) {
+ break;
+ }
+
+ // Keep diving! XXX One would expect to dive into the one editable node.
+ nsCOMPtr<nsIContent> child = curNode->GetFirstChild();
+ if (!child->IsHTMLElement(nsGkAtoms::div) &&
+ !HTMLEditUtils::IsList(child) &&
+ !child->IsHTMLElement(nsGkAtoms::blockquote)) {
+ break;
+ }
+
+ // check editability XXX floppy moose
+ curNode = child;
+ }
+
+ // We've found innermost list/blockquote/div: replace the one node in the
+ // array with these nodes
+ aNodeArray.RemoveElementAt(0);
+ if (curNode->IsAnyOfHTMLElements(nsGkAtoms::div,
+ nsGkAtoms::blockquote)) {
+ int32_t j = 0;
+ GetInnerContent(*curNode, aNodeArray, &j, Lists::no, Tables::no);
+ return;
+ }
+
+ aNodeArray.AppendElement(*curNode);
+}
+
+void
+HTMLEditRules::GetDefinitionListItemTypes(dom::Element* aElement,
+ bool* aDT,
+ bool* aDD)
+{
+ MOZ_ASSERT(aElement);
+ MOZ_ASSERT(aElement->IsHTMLElement(nsGkAtoms::dl));
+ MOZ_ASSERT(aDT);
+ MOZ_ASSERT(aDD);
+
+ *aDT = *aDD = false;
+ for (nsIContent* child = aElement->GetFirstChild();
+ child;
+ child = child->GetNextSibling()) {
+ if (child->IsHTMLElement(nsGkAtoms::dt)) {
+ *aDT = true;
+ } else if (child->IsHTMLElement(nsGkAtoms::dd)) {
+ *aDD = true;
+ }
+ }
+}
+
+nsresult
+HTMLEditRules::GetParagraphFormatNodes(
+ nsTArray<OwningNonNull<nsINode>>& outArrayOfNodes,
+ TouchContent aTouchContent)
+{
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+
+ RefPtr<Selection> selection = htmlEditor->GetSelection();
+ NS_ENSURE_STATE(selection);
+
+ // Contruct a list of nodes to act on.
+ nsresult rv = GetNodesFromSelection(*selection, EditAction::makeBasicBlock,
+ outArrayOfNodes, aTouchContent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Pre-process our list of nodes
+ for (int32_t i = outArrayOfNodes.Length() - 1; i >= 0; i--) {
+ OwningNonNull<nsINode> testNode = outArrayOfNodes[i];
+
+ // Remove all non-editable nodes. Leave them be.
+ if (!htmlEditor->IsEditable(testNode)) {
+ outArrayOfNodes.RemoveElementAt(i);
+ continue;
+ }
+
+ // Scan for table elements. If we find table elements other than table,
+ // replace it with a list of any editable non-table content. Ditto for
+ // list elements.
+ if (HTMLEditUtils::IsTableElement(testNode) ||
+ HTMLEditUtils::IsList(testNode) ||
+ HTMLEditUtils::IsListItem(testNode)) {
+ int32_t j = i;
+ outArrayOfNodes.RemoveElementAt(i);
+ GetInnerContent(testNode, outArrayOfNodes, &j);
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+HTMLEditRules::BustUpInlinesAtRangeEndpoints(RangeItem& item)
+{
+ bool isCollapsed = ((item.startNode == item.endNode) && (item.startOffset == item.endOffset));
+
+ nsCOMPtr<nsIContent> endInline = GetHighestInlineParent(*item.endNode);
+
+ // if we have inline parents above range endpoints, split them
+ if (endInline && !isCollapsed) {
+ nsCOMPtr<nsINode> resultEndNode = endInline->GetParentNode();
+ NS_ENSURE_STATE(mHTMLEditor);
+ // item.endNode must be content if endInline isn't null
+ int32_t resultEndOffset =
+ mHTMLEditor->SplitNodeDeep(*endInline, *item.endNode->AsContent(),
+ item.endOffset,
+ EditorBase::EmptyContainers::no);
+ NS_ENSURE_TRUE(resultEndOffset != -1, NS_ERROR_FAILURE);
+ // reset range
+ item.endNode = resultEndNode;
+ item.endOffset = resultEndOffset;
+ }
+
+ nsCOMPtr<nsIContent> startInline = GetHighestInlineParent(*item.startNode);
+
+ if (startInline) {
+ nsCOMPtr<nsINode> resultStartNode = startInline->GetParentNode();
+ NS_ENSURE_STATE(mHTMLEditor);
+ int32_t resultStartOffset =
+ mHTMLEditor->SplitNodeDeep(*startInline, *item.startNode->AsContent(),
+ item.startOffset,
+ EditorBase::EmptyContainers::no);
+ NS_ENSURE_TRUE(resultStartOffset != -1, NS_ERROR_FAILURE);
+ // reset range
+ item.startNode = resultStartNode;
+ item.startOffset = resultStartOffset;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+HTMLEditRules::BustUpInlinesAtBRs(
+ nsIContent& aNode,
+ nsTArray<OwningNonNull<nsINode>>& aOutArrayOfNodes)
+{
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+
+ // First build up a list of all the break nodes inside the inline container.
+ nsTArray<OwningNonNull<nsINode>> arrayOfBreaks;
+ BRNodeFunctor functor;
+ DOMIterator iter(aNode);
+ iter.AppendList(functor, arrayOfBreaks);
+
+ // If there aren't any breaks, just put inNode itself in the array
+ if (arrayOfBreaks.IsEmpty()) {
+ aOutArrayOfNodes.AppendElement(aNode);
+ return NS_OK;
+ }
+
+ // Else we need to bust up inNode along all the breaks
+ nsCOMPtr<nsINode> inlineParentNode = aNode.GetParentNode();
+ nsCOMPtr<nsIContent> splitDeepNode = &aNode;
+ nsCOMPtr<nsIContent> leftNode, rightNode;
+
+ for (uint32_t i = 0; i < arrayOfBreaks.Length(); i++) {
+ OwningNonNull<Element> breakNode = *arrayOfBreaks[i]->AsElement();
+ NS_ENSURE_TRUE(splitDeepNode, NS_ERROR_NULL_POINTER);
+ NS_ENSURE_TRUE(breakNode->GetParent(), NS_ERROR_NULL_POINTER);
+ OwningNonNull<nsIContent> splitParentNode = *breakNode->GetParent();
+ int32_t splitOffset = splitParentNode->IndexOf(breakNode);
+
+ int32_t resultOffset =
+ htmlEditor->SplitNodeDeep(*splitDeepNode, splitParentNode, splitOffset,
+ HTMLEditor::EmptyContainers::yes,
+ getter_AddRefs(leftNode),
+ getter_AddRefs(rightNode));
+ NS_ENSURE_STATE(resultOffset != -1);
+
+ // Put left node in node list
+ if (leftNode) {
+ // Might not be a left node. A break might have been at the very
+ // beginning of inline container, in which case SplitNodeDeep would not
+ // actually split anything
+ aOutArrayOfNodes.AppendElement(*leftNode);
+ }
+ // Move break outside of container and also put in node list
+ nsresult rv =
+ htmlEditor->MoveNode(breakNode, inlineParentNode, resultOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aOutArrayOfNodes.AppendElement(*breakNode);
+
+ // Now rightNode becomes the new node to split
+ splitDeepNode = rightNode;
+ }
+ // Now tack on remaining rightNode, if any, to the list
+ if (rightNode) {
+ aOutArrayOfNodes.AppendElement(*rightNode);
+ }
+ return NS_OK;
+}
+
+nsIContent*
+HTMLEditRules::GetHighestInlineParent(nsINode& aNode)
+{
+ if (!aNode.IsContent() || IsBlockNode(aNode)) {
+ return nullptr;
+ }
+ OwningNonNull<nsIContent> node = *aNode.AsContent();
+
+ while (node->GetParent() && IsInlineNode(*node->GetParent())) {
+ node = *node->GetParent();
+ }
+ return node;
+}
+
+/**
+ * GetNodesFromPoint() constructs a list of nodes from a point that will be
+ * operated on.
+ */
+nsresult
+HTMLEditRules::GetNodesFromPoint(
+ EditorDOMPoint aPoint,
+ EditAction aOperation,
+ nsTArray<OwningNonNull<nsINode>>& outArrayOfNodes,
+ TouchContent aTouchContent)
+{
+ NS_ENSURE_STATE(aPoint.node);
+ RefPtr<nsRange> range = new nsRange(aPoint.node);
+ nsresult rv = range->SetStart(aPoint.node, aPoint.offset);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ // Expand the range to include adjacent inlines
+ PromoteRange(*range, aOperation);
+
+ // Make array of ranges
+ nsTArray<RefPtr<nsRange>> arrayOfRanges;
+
+ // Stuff new opRange into array
+ arrayOfRanges.AppendElement(range);
+
+ // Use these ranges to contruct a list of nodes to act on
+ rv = GetNodesForOperation(arrayOfRanges, outArrayOfNodes, aOperation,
+ aTouchContent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+/**
+ * GetNodesFromSelection() constructs a list of nodes from the selection that
+ * will be operated on.
+ */
+nsresult
+HTMLEditRules::GetNodesFromSelection(
+ Selection& aSelection,
+ EditAction aOperation,
+ nsTArray<OwningNonNull<nsINode>>& outArrayOfNodes,
+ TouchContent aTouchContent)
+{
+ // Promote selection ranges
+ nsTArray<RefPtr<nsRange>> arrayOfRanges;
+ GetPromotedRanges(aSelection, arrayOfRanges, aOperation);
+
+ // Use these ranges to contruct a list of nodes to act on.
+ nsresult rv = GetNodesForOperation(arrayOfRanges, outArrayOfNodes,
+ aOperation, aTouchContent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+/**
+ * MakeTransitionList() detects all the transitions in the array, where a
+ * transition means that adjacent nodes in the array don't have the same parent.
+ */
+void
+HTMLEditRules::MakeTransitionList(nsTArray<OwningNonNull<nsINode>>& aNodeArray,
+ nsTArray<bool>& aTransitionArray)
+{
+ nsCOMPtr<nsINode> prevParent;
+
+ aTransitionArray.EnsureLengthAtLeast(aNodeArray.Length());
+ for (uint32_t i = 0; i < aNodeArray.Length(); i++) {
+ if (aNodeArray[i]->GetParentNode() != prevParent) {
+ // Different parents: transition point
+ aTransitionArray[i] = true;
+ } else {
+ // Same parents: these nodes grew up together
+ aTransitionArray[i] = false;
+ }
+ prevParent = aNodeArray[i]->GetParentNode();
+ }
+}
+
+/**
+ * If aNode is the descendant of a listitem, return that li. But table element
+ * boundaries are stoppers on the search. Also stops on the active editor host
+ * (contenteditable). Also test if aNode is an li itself.
+ */
+Element*
+HTMLEditRules::IsInListItem(nsINode* aNode)
+{
+ NS_ENSURE_TRUE(aNode, nullptr);
+ if (HTMLEditUtils::IsListItem(aNode)) {
+ return aNode->AsElement();
+ }
+
+ Element* parent = aNode->GetParentElement();
+ while (parent &&
+ mHTMLEditor && mHTMLEditor->IsDescendantOfEditorRoot(parent) &&
+ !HTMLEditUtils::IsTableElement(parent)) {
+ if (HTMLEditUtils::IsListItem(parent)) {
+ return parent;
+ }
+ parent = parent->GetParentElement();
+ }
+ return nullptr;
+}
+
+/**
+ * ReturnInHeader: do the right thing for returns pressed in headers
+ */
+nsresult
+HTMLEditRules::ReturnInHeader(Selection& aSelection,
+ Element& aHeader,
+ nsINode& aNode,
+ int32_t aOffset)
+{
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+
+ // Remember where the header is
+ nsCOMPtr<nsINode> headerParent = aHeader.GetParentNode();
+ int32_t offset = headerParent ? headerParent->IndexOf(&aHeader) : -1;
+
+ // Get ws code to adjust any ws
+ nsCOMPtr<nsINode> node = &aNode;
+ nsresult rv = WSRunObject::PrepareToSplitAcrossBlocks(htmlEditor,
+ address_of(node),
+ &aOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Split the header
+ NS_ENSURE_STATE(node->IsContent());
+ htmlEditor->SplitNodeDeep(aHeader, *node->AsContent(), aOffset);
+
+ // If the left-hand heading is empty, put a mozbr in it
+ nsCOMPtr<nsIContent> prevItem = htmlEditor->GetPriorHTMLSibling(&aHeader);
+ if (prevItem && HTMLEditUtils::IsHeader(*prevItem)) {
+ bool isEmptyNode;
+ rv = htmlEditor->IsEmptyNode(prevItem, &isEmptyNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (isEmptyNode) {
+ rv = CreateMozBR(prevItem->AsDOMNode(), 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ // If the new (righthand) header node is empty, delete it
+ bool isEmpty;
+ rv = IsEmptyBlock(aHeader, &isEmpty, MozBRCounts::no);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (isEmpty) {
+ rv = htmlEditor->DeleteNode(&aHeader);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Layout tells the caret to blink in a weird place if we don't place a
+ // break after the header.
+ nsCOMPtr<nsIContent> sibling =
+ htmlEditor->GetNextHTMLSibling(headerParent, offset + 1);
+ if (!sibling || !sibling->IsHTMLElement(nsGkAtoms::br)) {
+ ClearCachedStyles();
+ htmlEditor->mTypeInState->ClearAllProps();
+
+ // Create a paragraph
+ nsCOMPtr<Element> pNode =
+ htmlEditor->CreateNode(nsGkAtoms::p, headerParent, offset + 1);
+ NS_ENSURE_STATE(pNode);
+
+ // Append a <br> to it
+ nsCOMPtr<Element> brNode = htmlEditor->CreateBR(pNode, 0);
+ NS_ENSURE_STATE(brNode);
+
+ // Set selection to before the break
+ rv = aSelection.Collapse(pNode, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ headerParent = sibling->GetParentNode();
+ offset = headerParent ? headerParent->IndexOf(sibling) : -1;
+ // Put selection after break
+ rv = aSelection.Collapse(headerParent, offset + 1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ } else {
+ // Put selection at front of righthand heading
+ rv = aSelection.Collapse(&aHeader, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+/**
+ * ReturnInParagraph() does the right thing for returns pressed in paragraphs.
+ */
+nsresult
+HTMLEditRules::ReturnInParagraph(Selection* aSelection,
+ nsIDOMNode* aPara,
+ nsIDOMNode* aNode,
+ int32_t aOffset,
+ bool* aCancel,
+ bool* aHandled)
+{
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ if (!aSelection || !aPara || !node || !aCancel || !aHandled) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ *aCancel = false;
+ *aHandled = false;
+
+ int32_t offset;
+ nsCOMPtr<nsINode> parent = EditorBase::GetNodeLocation(node, &offset);
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ bool doesCRCreateNewP = mHTMLEditor->GetReturnInParagraphCreatesNewParagraph();
+
+ bool newBRneeded = false;
+ bool newSelNode = false;
+ nsCOMPtr<nsIContent> sibling;
+ nsCOMPtr<nsIDOMNode> selNode = aNode;
+ int32_t selOffset = aOffset;
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ if (aNode == aPara && doesCRCreateNewP) {
+ // we are at the edges of the block, newBRneeded not needed!
+ sibling = node->AsContent();
+ } else if (mHTMLEditor->IsTextNode(aNode)) {
+ nsCOMPtr<nsIDOMText> textNode = do_QueryInterface(aNode);
+ uint32_t strLength;
+ nsresult rv = textNode->GetLength(&strLength);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // at beginning of text node?
+ if (!aOffset) {
+ // is there a BR prior to it?
+ NS_ENSURE_STATE(mHTMLEditor);
+ sibling = mHTMLEditor->GetPriorHTMLSibling(node);
+ if (!sibling || !mHTMLEditor || !mHTMLEditor->IsVisBreak(sibling) ||
+ TextEditUtils::HasMozAttr(GetAsDOMNode(sibling))) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ newBRneeded = true;
+ }
+ } else if (aOffset == (int32_t)strLength) {
+ // we're at the end of text node...
+ // is there a BR after to it?
+ NS_ENSURE_STATE(mHTMLEditor);
+ sibling = mHTMLEditor->GetNextHTMLSibling(node);
+ if (!sibling || !mHTMLEditor || !mHTMLEditor->IsVisBreak(sibling) ||
+ TextEditUtils::HasMozAttr(GetAsDOMNode(sibling))) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ newBRneeded = true;
+ offset++;
+ }
+ } else {
+ if (doesCRCreateNewP) {
+ nsCOMPtr<nsIDOMNode> tmp;
+ rv = mTextEditor->SplitNode(aNode, aOffset, getter_AddRefs(tmp));
+ NS_ENSURE_SUCCESS(rv, rv);
+ selNode = tmp;
+ }
+
+ newBRneeded = true;
+ offset++;
+ }
+ } else {
+ // not in a text node.
+ // is there a BR prior to it?
+ nsCOMPtr<nsIContent> nearNode;
+ NS_ENSURE_STATE(mHTMLEditor);
+ nearNode = mHTMLEditor->GetPriorHTMLNode(node, aOffset);
+ NS_ENSURE_STATE(mHTMLEditor);
+ if (!nearNode || !mHTMLEditor->IsVisBreak(nearNode) ||
+ TextEditUtils::HasMozAttr(GetAsDOMNode(nearNode))) {
+ // is there a BR after it?
+ NS_ENSURE_STATE(mHTMLEditor);
+ nearNode = mHTMLEditor->GetNextHTMLNode(node, aOffset);
+ NS_ENSURE_STATE(mHTMLEditor);
+ if (!nearNode || !mHTMLEditor->IsVisBreak(nearNode) ||
+ TextEditUtils::HasMozAttr(GetAsDOMNode(nearNode))) {
+ newBRneeded = true;
+ parent = node;
+ offset = aOffset;
+ newSelNode = true;
+ }
+ }
+ if (!newBRneeded) {
+ sibling = nearNode;
+ }
+ }
+ if (newBRneeded) {
+ // if CR does not create a new P, default to BR creation
+ NS_ENSURE_TRUE(doesCRCreateNewP, NS_OK);
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ sibling = mHTMLEditor->CreateBR(parent, offset);
+ if (newSelNode) {
+ // We split the parent after the br we've just inserted.
+ selNode = GetAsDOMNode(parent);
+ selOffset = offset + 1;
+ }
+ }
+ *aHandled = true;
+ return SplitParagraph(aPara, sibling, aSelection, address_of(selNode), &selOffset);
+}
+
+/**
+ * SplitParagraph() splits a paragraph at selection point, possibly deleting a
+ * br.
+ */
+nsresult
+HTMLEditRules::SplitParagraph(nsIDOMNode *aPara,
+ nsIContent* aBRNode,
+ Selection* aSelection,
+ nsCOMPtr<nsIDOMNode>* aSelNode,
+ int32_t* aOffset)
+{
+ nsCOMPtr<Element> para = do_QueryInterface(aPara);
+ NS_ENSURE_TRUE(para && aBRNode && aSelNode && *aSelNode && aOffset &&
+ aSelection, NS_ERROR_NULL_POINTER);
+
+ // split para
+ // get ws code to adjust any ws
+ nsCOMPtr<nsIContent> leftPara, rightPara;
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsCOMPtr<nsINode> selNode(do_QueryInterface(*aSelNode));
+ nsresult rv =
+ WSRunObject::PrepareToSplitAcrossBlocks(mHTMLEditor,
+ address_of(selNode), aOffset);
+ // XXX When it fails, why do we need to return selection node? (Why can the
+ // caller trust the result even when it returns error?)
+ *aSelNode = GetAsDOMNode(selNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // split the paragraph
+ NS_ENSURE_STATE(mHTMLEditor);
+ NS_ENSURE_STATE(selNode->IsContent());
+ mHTMLEditor->SplitNodeDeep(*para, *selNode->AsContent(), *aOffset,
+ HTMLEditor::EmptyContainers::yes,
+ getter_AddRefs(leftPara),
+ getter_AddRefs(rightPara));
+ // get rid of the break, if it is visible (otherwise it may be needed to prevent an empty p)
+ NS_ENSURE_STATE(mHTMLEditor);
+ if (mHTMLEditor->IsVisBreak(aBRNode)) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->DeleteNode(aBRNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // remove ID attribute on the paragraph we just created
+ nsCOMPtr<nsIDOMElement> rightElt = do_QueryInterface(rightPara);
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->RemoveAttribute(rightElt, NS_LITERAL_STRING("id"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // check both halves of para to see if we need mozBR
+ rv = InsertMozBRIfNeeded(*leftPara);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = InsertMozBRIfNeeded(*rightPara);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // selection to beginning of right hand para;
+ // look inside any containers that are up front.
+ nsCOMPtr<nsINode> rightParaNode = do_QueryInterface(rightPara);
+ NS_ENSURE_STATE(mHTMLEditor && rightParaNode);
+ nsCOMPtr<nsIDOMNode> child =
+ GetAsDOMNode(mHTMLEditor->GetLeftmostChild(rightParaNode, true));
+ if (mHTMLEditor->IsTextNode(child) ||
+ mHTMLEditor->IsContainer(child)) {
+ aSelection->Collapse(child,0);
+ } else {
+ int32_t offset;
+ nsCOMPtr<nsIDOMNode> parent = EditorBase::GetNodeLocation(child, &offset);
+ aSelection->Collapse(parent,offset);
+ }
+ return NS_OK;
+}
+
+/**
+ * ReturnInListItem: do the right thing for returns pressed in list items
+ */
+nsresult
+HTMLEditRules::ReturnInListItem(Selection& aSelection,
+ Element& aListItem,
+ nsINode& aNode,
+ int32_t aOffset)
+{
+ MOZ_ASSERT(HTMLEditUtils::IsListItem(&aListItem));
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+
+ // Get the item parent and the active editing host.
+ nsCOMPtr<Element> root = htmlEditor->GetActiveEditingHost();
+
+ nsCOMPtr<Element> list = aListItem.GetParentElement();
+ int32_t itemOffset = list ? list->IndexOf(&aListItem) : -1;
+
+ // If we are in an empty item, then we want to pop up out of the list, but
+ // only if prefs say it's okay and if the parent isn't the active editing
+ // host.
+ bool isEmpty;
+ nsresult rv = IsEmptyBlock(aListItem, &isEmpty, MozBRCounts::no);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (isEmpty && root != list && mReturnInEmptyLIKillsList) {
+ // Get the list offset now -- before we might eventually split the list
+ nsCOMPtr<nsINode> listParent = list->GetParentNode();
+ int32_t offset = listParent ? listParent->IndexOf(list) : -1;
+
+ // Are we the last list item in the list?
+ bool isLast;
+ rv = htmlEditor->IsLastEditableChild(aListItem.AsDOMNode(), &isLast);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!isLast) {
+ // We need to split the list!
+ ErrorResult rv;
+ htmlEditor->SplitNode(*list, itemOffset, rv);
+ NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult());
+ }
+
+ // Are we in a sublist?
+ if (HTMLEditUtils::IsList(listParent)) {
+ // If so, move item out of this list and into the grandparent list
+ rv = htmlEditor->MoveNode(&aListItem, listParent, offset + 1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aSelection.Collapse(&aListItem, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // Otherwise kill this item
+ rv = htmlEditor->DeleteNode(&aListItem);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Time to insert a paragraph
+ nsCOMPtr<Element> pNode =
+ htmlEditor->CreateNode(nsGkAtoms::p, listParent, offset + 1);
+ NS_ENSURE_STATE(pNode);
+
+ // Append a <br> to it
+ nsCOMPtr<Element> brNode = htmlEditor->CreateBR(pNode, 0);
+ NS_ENSURE_STATE(brNode);
+
+ // Set selection to before the break
+ rv = aSelection.Collapse(pNode, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+ }
+
+ // Else we want a new list item at the same list level. Get ws code to
+ // adjust any ws.
+ nsCOMPtr<nsINode> selNode = &aNode;
+ rv = WSRunObject::PrepareToSplitAcrossBlocks(htmlEditor,
+ address_of(selNode), &aOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Now split list item
+ NS_ENSURE_STATE(selNode->IsContent());
+ htmlEditor->SplitNodeDeep(aListItem, *selNode->AsContent(), aOffset);
+
+ // Hack: until I can change the damaged doc range code back to being
+ // extra-inclusive, I have to manually detect certain list items that may be
+ // left empty.
+ nsCOMPtr<nsIContent> prevItem = htmlEditor->GetPriorHTMLSibling(&aListItem);
+ if (prevItem && HTMLEditUtils::IsListItem(prevItem)) {
+ bool isEmptyNode;
+ rv = htmlEditor->IsEmptyNode(prevItem, &isEmptyNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (isEmptyNode) {
+ rv = CreateMozBR(prevItem->AsDOMNode(), 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ rv = htmlEditor->IsEmptyNode(&aListItem, &isEmptyNode, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (isEmptyNode) {
+ nsCOMPtr<nsIAtom> nodeAtom = aListItem.NodeInfo()->NameAtom();
+ if (nodeAtom == nsGkAtoms::dd || nodeAtom == nsGkAtoms::dt) {
+ nsCOMPtr<nsINode> list = aListItem.GetParentNode();
+ int32_t itemOffset = list ? list->IndexOf(&aListItem) : -1;
+
+ nsIAtom* listAtom = nodeAtom == nsGkAtoms::dt ? nsGkAtoms::dd
+ : nsGkAtoms::dt;
+ nsCOMPtr<Element> newListItem =
+ htmlEditor->CreateNode(listAtom, list, itemOffset + 1);
+ NS_ENSURE_STATE(newListItem);
+ rv = mTextEditor->DeleteNode(&aListItem);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aSelection.Collapse(newListItem, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+ }
+
+ nsCOMPtr<Element> brNode;
+ rv = htmlEditor->CopyLastEditableChildStyles(GetAsDOMNode(prevItem),
+ GetAsDOMNode(&aListItem),
+ getter_AddRefs(brNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (brNode) {
+ nsCOMPtr<nsINode> brParent = brNode->GetParentNode();
+ int32_t offset = brParent ? brParent->IndexOf(brNode) : -1;
+ rv = aSelection.Collapse(brParent, offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+ }
+ } else {
+ WSRunObject wsObj(htmlEditor, &aListItem, 0);
+ nsCOMPtr<nsINode> visNode;
+ int32_t visOffset = 0;
+ WSType wsType;
+ wsObj.NextVisibleNode(&aListItem, 0, address_of(visNode),
+ &visOffset, &wsType);
+ if (wsType == WSType::special || wsType == WSType::br ||
+ visNode->IsHTMLElement(nsGkAtoms::hr)) {
+ nsCOMPtr<nsINode> parent = visNode->GetParentNode();
+ int32_t offset = parent ? parent->IndexOf(visNode) : -1;
+ rv = aSelection.Collapse(parent, offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+ } else {
+ rv = aSelection.Collapse(visNode, visOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+ }
+ }
+ }
+ }
+ rv = aSelection.Collapse(&aListItem, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+/**
+ * MakeBlockquote() puts the list of nodes into one or more blockquotes.
+ */
+nsresult
+HTMLEditRules::MakeBlockquote(nsTArray<OwningNonNull<nsINode>>& aNodeArray)
+{
+ // The idea here is to put the nodes into a minimal number of blockquotes.
+ // When the user blockquotes something, they expect one blockquote. That may
+ // not be possible (for instance, if they have two table cells selected, you
+ // need two blockquotes inside the cells).
+ nsCOMPtr<Element> curBlock;
+ nsCOMPtr<nsINode> prevParent;
+
+ for (auto& curNode : aNodeArray) {
+ // Get the node to act on, and its location
+ NS_ENSURE_STATE(curNode->IsContent());
+
+ // If the node is a table element or list item, dive inside
+ if (HTMLEditUtils::IsTableElementButNotTable(curNode) ||
+ HTMLEditUtils::IsListItem(curNode)) {
+ // Forget any previous block
+ curBlock = nullptr;
+ // Recursion time
+ nsTArray<OwningNonNull<nsINode>> childArray;
+ GetChildNodesForOperation(*curNode, childArray);
+ nsresult rv = MakeBlockquote(childArray);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // If the node has different parent than previous node, further nodes in a
+ // new parent
+ if (prevParent) {
+ if (prevParent != curNode->GetParentNode()) {
+ // Forget any previous blockquote node we were using
+ curBlock = nullptr;
+ prevParent = curNode->GetParentNode();
+ }
+ } else {
+ prevParent = curNode->GetParentNode();
+ }
+
+ // If no curBlock, make one
+ if (!curBlock) {
+ nsCOMPtr<nsINode> curParent = curNode->GetParentNode();
+ int32_t offset = curParent ? curParent->IndexOf(curNode) : -1;
+ nsresult rv = SplitAsNeeded(*nsGkAtoms::blockquote, curParent, offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_STATE(mHTMLEditor);
+ curBlock = mHTMLEditor->CreateNode(nsGkAtoms::blockquote, curParent,
+ offset);
+ NS_ENSURE_STATE(curBlock);
+ // remember our new block for postprocessing
+ mNewBlock = curBlock;
+ // note: doesn't matter if we set mNewBlock multiple times.
+ }
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv = mHTMLEditor->MoveNode(curNode->AsContent(), curBlock, -1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+/**
+ * RemoveBlockStyle() makes the nodes have no special block type.
+ */
+nsresult
+HTMLEditRules::RemoveBlockStyle(nsTArray<OwningNonNull<nsINode>>& aNodeArray)
+{
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+
+ // Intent of this routine is to be used for converting to/from headers,
+ // paragraphs, pre, and address. Those blocks that pretty much just contain
+ // inline things...
+ nsCOMPtr<Element> curBlock;
+ nsCOMPtr<nsIContent> firstNode, lastNode;
+ for (auto& curNode : aNodeArray) {
+ // If curNode is a address, p, header, address, or pre, remove it
+ if (HTMLEditUtils::IsFormatNode(curNode)) {
+ // Process any partial progress saved
+ if (curBlock) {
+ nsresult rv = RemovePartOfBlock(*curBlock, *firstNode, *lastNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ firstNode = lastNode = curBlock = nullptr;
+ }
+ // Remove current block
+ nsresult rv = htmlEditor->RemoveBlockContainer(*curNode->AsContent());
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else if (curNode->IsAnyOfHTMLElements(nsGkAtoms::table,
+ nsGkAtoms::tr,
+ nsGkAtoms::tbody,
+ nsGkAtoms::td,
+ nsGkAtoms::li,
+ nsGkAtoms::blockquote,
+ nsGkAtoms::div) ||
+ HTMLEditUtils::IsList(curNode)) {
+ // Process any partial progress saved
+ if (curBlock) {
+ nsresult rv = RemovePartOfBlock(*curBlock, *firstNode, *lastNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ firstNode = lastNode = curBlock = nullptr;
+ }
+ // Recursion time
+ nsTArray<OwningNonNull<nsINode>> childArray;
+ GetChildNodesForOperation(*curNode, childArray);
+ nsresult rv = RemoveBlockStyle(childArray);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else if (IsInlineNode(curNode)) {
+ if (curBlock) {
+ // If so, is this node a descendant?
+ if (EditorUtils::IsDescendantOf(curNode, curBlock)) {
+ // Then we don't need to do anything different for this node
+ lastNode = curNode->AsContent();
+ continue;
+ }
+ // Otherwise, we have progressed beyond end of curBlock, so let's
+ // handle it now. We need to remove the portion of curBlock that
+ // contains [firstNode - lastNode].
+ nsresult rv = RemovePartOfBlock(*curBlock, *firstNode, *lastNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ firstNode = lastNode = curBlock = nullptr;
+ // Fall out and handle curNode
+ }
+ curBlock = htmlEditor->GetBlockNodeParent(curNode);
+ if (curBlock && HTMLEditUtils::IsFormatNode(curBlock)) {
+ firstNode = lastNode = curNode->AsContent();
+ } else {
+ // Not a block kind that we care about.
+ curBlock = nullptr;
+ }
+ } else if (curBlock) {
+ // Some node that is already sans block style. Skip over it and process
+ // any partial progress saved.
+ nsresult rv = RemovePartOfBlock(*curBlock, *firstNode, *lastNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ firstNode = lastNode = curBlock = nullptr;
+ }
+ }
+ // Process any partial progress saved
+ if (curBlock) {
+ nsresult rv = RemovePartOfBlock(*curBlock, *firstNode, *lastNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ firstNode = lastNode = curBlock = nullptr;
+ }
+ return NS_OK;
+}
+
+/**
+ * ApplyBlockStyle() does whatever it takes to make the list of nodes into one
+ * or more blocks of type aBlockTag.
+ */
+nsresult
+HTMLEditRules::ApplyBlockStyle(nsTArray<OwningNonNull<nsINode>>& aNodeArray,
+ nsIAtom& aBlockTag)
+{
+ // Intent of this routine is to be used for converting to/from headers,
+ // paragraphs, pre, and address. Those blocks that pretty much just contain
+ // inline things...
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+
+ // Remove all non-editable nodes. Leave them be.
+ for (int32_t i = aNodeArray.Length() - 1; i >= 0; i--) {
+ if (!htmlEditor->IsEditable(aNodeArray[i])) {
+ aNodeArray.RemoveElementAt(i);
+ }
+ }
+
+ nsCOMPtr<Element> newBlock;
+
+ nsCOMPtr<Element> curBlock;
+ for (auto& curNode : aNodeArray) {
+ nsCOMPtr<nsINode> curParent = curNode->GetParentNode();
+ int32_t offset = curParent ? curParent->IndexOf(curNode) : -1;
+
+ // Is it already the right kind of block?
+ if (curNode->IsHTMLElement(&aBlockTag)) {
+ // Forget any previous block used for previous inline nodes
+ curBlock = nullptr;
+ // Do nothing to this block
+ continue;
+ }
+
+ // If curNode is a address, p, header, address, or pre, replace it with a
+ // new block of correct type.
+ // XXX: pre can't hold everything the others can
+ if (HTMLEditUtils::IsMozDiv(curNode) ||
+ HTMLEditUtils::IsFormatNode(curNode)) {
+ // Forget any previous block used for previous inline nodes
+ curBlock = nullptr;
+ newBlock = htmlEditor->ReplaceContainer(curNode->AsElement(),
+ &aBlockTag, nullptr, nullptr,
+ EditorBase::eCloneAttributes);
+ NS_ENSURE_STATE(newBlock);
+ } else if (HTMLEditUtils::IsTable(curNode) ||
+ HTMLEditUtils::IsList(curNode) ||
+ curNode->IsAnyOfHTMLElements(nsGkAtoms::tbody,
+ nsGkAtoms::tr,
+ nsGkAtoms::td,
+ nsGkAtoms::li,
+ nsGkAtoms::blockquote,
+ nsGkAtoms::div)) {
+ // Forget any previous block used for previous inline nodes
+ curBlock = nullptr;
+ // Recursion time
+ nsTArray<OwningNonNull<nsINode>> childArray;
+ GetChildNodesForOperation(*curNode, childArray);
+ if (!childArray.IsEmpty()) {
+ nsresult rv = ApplyBlockStyle(childArray, aBlockTag);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // Make sure we can put a block here
+ nsresult rv = SplitAsNeeded(aBlockTag, curParent, offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<Element> theBlock =
+ htmlEditor->CreateNode(&aBlockTag, curParent, offset);
+ NS_ENSURE_STATE(theBlock);
+ // Remember our new block for postprocessing
+ mNewBlock = theBlock;
+ }
+ } else if (curNode->IsHTMLElement(nsGkAtoms::br)) {
+ // If the node is a break, we honor it by putting further nodes in a new
+ // parent
+ if (curBlock) {
+ // Forget any previous block used for previous inline nodes
+ curBlock = nullptr;
+ nsresult rv = htmlEditor->DeleteNode(curNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // The break is the first (or even only) node we encountered. Create a
+ // block for it.
+ nsresult rv = SplitAsNeeded(aBlockTag, curParent, offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ curBlock = htmlEditor->CreateNode(&aBlockTag, curParent, offset);
+ NS_ENSURE_STATE(curBlock);
+ // Remember our new block for postprocessing
+ mNewBlock = curBlock;
+ // Note: doesn't matter if we set mNewBlock multiple times.
+ rv = htmlEditor->MoveNode(curNode->AsContent(), curBlock, -1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ } else if (IsInlineNode(curNode)) {
+ // If curNode is inline, pull it into curBlock. Note: it's assumed that
+ // consecutive inline nodes in aNodeArray are actually members of the
+ // same block parent. This happens to be true now as a side effect of
+ // how aNodeArray is contructed, but some additional logic should be
+ // added here if that should change
+ //
+ // If curNode is a non editable, drop it if we are going to <pre>.
+ if (&aBlockTag == nsGkAtoms::pre && !htmlEditor->IsEditable(curNode)) {
+ // Do nothing to this block
+ continue;
+ }
+
+ // If no curBlock, make one
+ if (!curBlock) {
+ nsresult rv = SplitAsNeeded(aBlockTag, curParent, offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ curBlock = htmlEditor->CreateNode(&aBlockTag, curParent, offset);
+ NS_ENSURE_STATE(curBlock);
+ // Remember our new block for postprocessing
+ mNewBlock = curBlock;
+ // Note: doesn't matter if we set mNewBlock multiple times.
+ }
+
+ // XXX If curNode is a br, replace it with a return if going to <pre>
+
+ // This is a continuation of some inline nodes that belong together in
+ // the same block item. Use curBlock.
+ nsresult rv = htmlEditor->MoveNode(curNode->AsContent(), curBlock, -1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ return NS_OK;
+}
+
+/**
+ * Given a tag name, split inOutParent up to the point where we can insert the
+ * tag. Adjust inOutParent and inOutOffset to point to new location for tag.
+ */
+nsresult
+HTMLEditRules::SplitAsNeeded(nsIAtom& aTag,
+ OwningNonNull<nsINode>& aInOutParent,
+ int32_t& aInOutOffset)
+{
+ // XXX Is there a better way to do this?
+ nsCOMPtr<nsINode> parent = aInOutParent.forget();
+ nsresult rv = SplitAsNeeded(aTag, parent, aInOutOffset);
+ aInOutParent = parent.forget();
+ return rv;
+}
+
+nsresult
+HTMLEditRules::SplitAsNeeded(nsIAtom& aTag,
+ nsCOMPtr<nsINode>& inOutParent,
+ int32_t& inOutOffset)
+{
+ NS_ENSURE_TRUE(inOutParent, NS_ERROR_NULL_POINTER);
+
+ // Check that we have a place that can legally contain the tag
+ nsCOMPtr<nsINode> tagParent, splitNode;
+ for (nsCOMPtr<nsINode> parent = inOutParent; parent;
+ parent = parent->GetParentNode()) {
+ // Sniffing up the parent tree until we find a legal place for the block
+
+ // Don't leave the active editing host
+ NS_ENSURE_STATE(mHTMLEditor);
+ if (!mHTMLEditor->IsDescendantOfEditorRoot(parent)) {
+ // XXX Why do we need to check mHTMLEditor again here?
+ NS_ENSURE_STATE(mHTMLEditor);
+ if (parent != mHTMLEditor->GetActiveEditingHost()) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ if (mHTMLEditor->CanContainTag(*parent, aTag)) {
+ // Success
+ tagParent = parent;
+ break;
+ }
+
+ splitNode = parent;
+ }
+ if (!tagParent) {
+ // Could not find a place to build tag!
+ return NS_ERROR_FAILURE;
+ }
+ if (splitNode && splitNode->IsContent() && inOutParent->IsContent()) {
+ // We found a place for block, but above inOutParent. We need to split.
+ NS_ENSURE_STATE(mHTMLEditor);
+ int32_t offset = mHTMLEditor->SplitNodeDeep(*splitNode->AsContent(),
+ *inOutParent->AsContent(),
+ inOutOffset);
+ NS_ENSURE_STATE(offset != -1);
+ inOutParent = tagParent;
+ inOutOffset = offset;
+ }
+ return NS_OK;
+}
+
+/**
+ * JoinNodesSmart: Join two nodes, doing whatever makes sense for their
+ * children (which often means joining them, too). aNodeLeft & aNodeRight must
+ * be same type of node.
+ *
+ * Returns the point where they're merged, or (nullptr, -1) on failure.
+ */
+EditorDOMPoint
+HTMLEditRules::JoinNodesSmart(nsIContent& aNodeLeft,
+ nsIContent& aNodeRight)
+{
+ // Caller responsible for left and right node being the same type
+ nsCOMPtr<nsINode> parent = aNodeLeft.GetParentNode();
+ NS_ENSURE_TRUE(parent, EditorDOMPoint());
+ int32_t parOffset = parent->IndexOf(&aNodeLeft);
+ nsCOMPtr<nsINode> rightParent = aNodeRight.GetParentNode();
+
+ // If they don't have the same parent, first move the right node to after the
+ // left one
+ if (parent != rightParent) {
+ NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint());
+ nsresult rv = mHTMLEditor->MoveNode(&aNodeRight, parent, parOffset);
+ NS_ENSURE_SUCCESS(rv, EditorDOMPoint());
+ }
+
+ EditorDOMPoint ret(&aNodeRight, aNodeLeft.Length());
+
+ // Separate join rules for differing blocks
+ if (HTMLEditUtils::IsList(&aNodeLeft) || aNodeLeft.GetAsText()) {
+ // For lists, merge shallow (wouldn't want to combine list items)
+ nsresult rv = mHTMLEditor->JoinNodes(aNodeLeft, aNodeRight);
+ NS_ENSURE_SUCCESS(rv, EditorDOMPoint());
+ return ret;
+ }
+
+ // Remember the last left child, and first right child
+ NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint());
+ nsCOMPtr<nsIContent> lastLeft = mHTMLEditor->GetLastEditableChild(aNodeLeft);
+ NS_ENSURE_TRUE(lastLeft, EditorDOMPoint());
+
+ NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint());
+ nsCOMPtr<nsIContent> firstRight = mHTMLEditor->GetFirstEditableChild(aNodeRight);
+ NS_ENSURE_TRUE(firstRight, EditorDOMPoint());
+
+ // For list items, divs, etc., merge smart
+ NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint());
+ nsresult rv = mHTMLEditor->JoinNodes(aNodeLeft, aNodeRight);
+ NS_ENSURE_SUCCESS(rv, EditorDOMPoint());
+
+ if (lastLeft && firstRight && mHTMLEditor &&
+ mHTMLEditor->AreNodesSameType(lastLeft, firstRight) &&
+ (lastLeft->GetAsText() || !mHTMLEditor ||
+ (lastLeft->IsElement() && firstRight->IsElement() &&
+ mHTMLEditor->mCSSEditUtils->ElementsSameStyle(lastLeft->AsElement(),
+ firstRight->AsElement())))) {
+ NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint());
+ return JoinNodesSmart(*lastLeft, *firstRight);
+ }
+ return ret;
+}
+
+Element*
+HTMLEditRules::GetTopEnclosingMailCite(nsINode& aNode)
+{
+ nsCOMPtr<Element> ret;
+
+ for (nsCOMPtr<nsINode> node = &aNode; node; node = node->GetParentNode()) {
+ if ((IsPlaintextEditor() && node->IsHTMLElement(nsGkAtoms::pre)) ||
+ HTMLEditUtils::IsMailCite(node)) {
+ ret = node->AsElement();
+ }
+ if (node->IsHTMLElement(nsGkAtoms::body)) {
+ break;
+ }
+ }
+
+ return ret;
+}
+
+nsresult
+HTMLEditRules::CacheInlineStyles(nsIDOMNode* aNode)
+{
+ NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER);
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ bool useCSS = mHTMLEditor->IsCSSEnabled();
+
+ for (int32_t j = 0; j < SIZE_STYLE_TABLE; ++j) {
+ // If type-in state is set, don't intervene
+ bool typeInSet, unused;
+ if (NS_WARN_IF(!mHTMLEditor)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ mHTMLEditor->mTypeInState->GetTypingState(typeInSet, unused,
+ mCachedStyles[j].tag, mCachedStyles[j].attr, nullptr);
+ if (typeInSet) {
+ continue;
+ }
+
+ bool isSet = false;
+ nsAutoString outValue;
+ // Don't use CSS for <font size>, we don't support it usefully (bug 780035)
+ if (!useCSS || (mCachedStyles[j].tag == nsGkAtoms::font &&
+ mCachedStyles[j].attr.EqualsLiteral("size"))) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ mHTMLEditor->IsTextPropertySetByContent(aNode, mCachedStyles[j].tag,
+ &(mCachedStyles[j].attr), nullptr,
+ isSet, &outValue);
+ } else {
+ NS_ENSURE_STATE(mHTMLEditor);
+ mHTMLEditor->mCSSEditUtils->IsCSSEquivalentToHTMLInlineStyleSet(aNode,
+ mCachedStyles[j].tag, &(mCachedStyles[j].attr), isSet, outValue,
+ CSSEditUtils::eComputed);
+ }
+ if (isSet) {
+ mCachedStyles[j].mPresent = true;
+ mCachedStyles[j].value.Assign(outValue);
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+HTMLEditRules::ReapplyCachedStyles()
+{
+ // The idea here is to examine our cached list of styles and see if any have
+ // been removed. If so, add typeinstate for them, so that they will be
+ // reinserted when new content is added.
+
+ // remember if we are in css mode
+ NS_ENSURE_STATE(mHTMLEditor);
+ bool useCSS = mHTMLEditor->IsCSSEnabled();
+
+ // get selection point; if it doesn't exist, we have nothing to do
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<Selection> selection = mHTMLEditor->GetSelection();
+ if (!selection) {
+ // If the document is removed from its parent document during executing an
+ // editor operation with DOMMutationEvent or something, there may be no
+ // selection.
+ return NS_OK;
+ }
+ if (!selection->RangeCount()) {
+ // Nothing to do
+ return NS_OK;
+ }
+ nsCOMPtr<nsIContent> selNode =
+ do_QueryInterface(selection->GetRangeAt(0)->GetStartParent());
+ if (!selNode) {
+ // Nothing to do
+ return NS_OK;
+ }
+
+ for (int32_t i = 0; i < SIZE_STYLE_TABLE; ++i) {
+ if (mCachedStyles[i].mPresent) {
+ bool bFirst, bAny, bAll;
+ bFirst = bAny = bAll = false;
+
+ nsAutoString curValue;
+ if (useCSS) {
+ // check computed style first in css case
+ NS_ENSURE_STATE(mHTMLEditor);
+ bAny = mHTMLEditor->mCSSEditUtils->IsCSSEquivalentToHTMLInlineStyleSet(
+ selNode, mCachedStyles[i].tag, &(mCachedStyles[i].attr), curValue,
+ CSSEditUtils::eComputed);
+ }
+ if (!bAny) {
+ // then check typeinstate and html style
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv =
+ mHTMLEditor->GetInlinePropertyBase(*mCachedStyles[i].tag,
+ &(mCachedStyles[i].attr),
+ &(mCachedStyles[i].value),
+ &bFirst, &bAny, &bAll,
+ &curValue, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // this style has disappeared through deletion. Add to our typeinstate:
+ if (!bAny || IsStyleCachePreservingAction(mTheAction)) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ mHTMLEditor->mTypeInState->SetProp(mCachedStyles[i].tag,
+ mCachedStyles[i].attr,
+ mCachedStyles[i].value);
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+void
+HTMLEditRules::ClearCachedStyles()
+{
+ // clear the mPresent bits in mCachedStyles array
+ for (uint32_t j = 0; j < SIZE_STYLE_TABLE; j++) {
+ mCachedStyles[j].mPresent = false;
+ mCachedStyles[j].value.Truncate();
+ }
+}
+
+void
+HTMLEditRules::AdjustSpecialBreaks()
+{
+ NS_ENSURE_TRUE_VOID(mHTMLEditor);
+
+ // Gather list of empty nodes
+ nsTArray<OwningNonNull<nsINode>> nodeArray;
+ EmptyEditableFunctor functor(mHTMLEditor);
+ DOMIterator iter;
+ if (NS_WARN_IF(NS_FAILED(iter.Init(*mDocChangeRange)))) {
+ return;
+ }
+ iter.AppendList(functor, nodeArray);
+
+ // Put moz-br's into these empty li's and td's
+ for (auto& node : nodeArray) {
+ // Need to put br at END of node. It may have empty containers in it and
+ // still pass the "IsEmptyNode" test, and we want the br's to be after
+ // them. Also, we want the br to be after the selection if the selection
+ // is in this node.
+ nsresult rv = CreateMozBR(node->AsDOMNode(), (int32_t)node->Length());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+ }
+}
+
+nsresult
+HTMLEditRules::AdjustWhitespace(Selection* aSelection)
+{
+ // get selection point
+ nsCOMPtr<nsIDOMNode> selNode;
+ int32_t selOffset;
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv =
+ mHTMLEditor->GetStartNodeAndOffset(aSelection,
+ getter_AddRefs(selNode), &selOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // ask whitespace object to tweak nbsp's
+ NS_ENSURE_STATE(mHTMLEditor);
+ return WSRunObject(mHTMLEditor, selNode, selOffset).AdjustWhitespace();
+}
+
+nsresult
+HTMLEditRules::PinSelectionToNewBlock(Selection* aSelection)
+{
+ NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER);
+ if (!aSelection->Collapsed()) {
+ return NS_OK;
+ }
+
+ // get the (collapsed) selection location
+ nsCOMPtr<nsIDOMNode> selNode, temp;
+ int32_t selOffset;
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv =
+ mHTMLEditor->GetStartNodeAndOffset(aSelection,
+ getter_AddRefs(selNode), &selOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ temp = selNode;
+
+ // use ranges and sRangeHelper to compare sel point to new block
+ nsCOMPtr<nsINode> node = do_QueryInterface(selNode);
+ NS_ENSURE_STATE(node);
+ RefPtr<nsRange> range = new nsRange(node);
+ rv = range->SetStart(selNode, selOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = range->SetEnd(selNode, selOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIContent> block = mNewBlock.get();
+ NS_ENSURE_TRUE(block, NS_ERROR_NO_INTERFACE);
+ bool nodeBefore, nodeAfter;
+ rv = nsRange::CompareNodeToRange(block, range, &nodeBefore, &nodeAfter);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (nodeBefore && nodeAfter) {
+ return NS_OK; // selection is inside block
+ } else if (nodeBefore) {
+ // selection is after block. put at end of block.
+ nsCOMPtr<nsIDOMNode> tmp = GetAsDOMNode(mNewBlock);
+ NS_ENSURE_STATE(mHTMLEditor);
+ tmp = GetAsDOMNode(mHTMLEditor->GetLastEditableChild(*block));
+ uint32_t endPoint;
+ if (mHTMLEditor->IsTextNode(tmp) ||
+ mHTMLEditor->IsContainer(tmp)) {
+ rv = EditorBase::GetLengthOfDOMNode(tmp, endPoint);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ tmp = EditorBase::GetNodeLocation(tmp, (int32_t*)&endPoint);
+ endPoint++; // want to be after this node
+ }
+ return aSelection->Collapse(tmp, (int32_t)endPoint);
+ } else {
+ // selection is before block. put at start of block.
+ nsCOMPtr<nsIDOMNode> tmp = GetAsDOMNode(mNewBlock);
+ NS_ENSURE_STATE(mHTMLEditor);
+ tmp = GetAsDOMNode(mHTMLEditor->GetFirstEditableChild(*block));
+ int32_t offset;
+ if (mHTMLEditor->IsTextNode(tmp) ||
+ mHTMLEditor->IsContainer(tmp)) {
+ tmp = EditorBase::GetNodeLocation(tmp, &offset);
+ }
+ return aSelection->Collapse(tmp, 0);
+ }
+}
+
+void
+HTMLEditRules::CheckInterlinePosition(Selection& aSelection)
+{
+ // If the selection isn't collapsed, do nothing.
+ if (!aSelection.Collapsed()) {
+ return;
+ }
+
+ NS_ENSURE_TRUE_VOID(mHTMLEditor);
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+
+ // Get the (collapsed) selection location
+ NS_ENSURE_TRUE_VOID(aSelection.GetRangeAt(0) &&
+ aSelection.GetRangeAt(0)->GetStartParent());
+ OwningNonNull<nsINode> selNode = *aSelection.GetRangeAt(0)->GetStartParent();
+ int32_t selOffset = aSelection.GetRangeAt(0)->StartOffset();
+
+ // First, let's check to see if we are after a <br>. We take care of this
+ // special-case first so that we don't accidentally fall through into one of
+ // the other conditionals.
+ nsCOMPtr<nsIContent> node =
+ htmlEditor->GetPriorHTMLNode(selNode, selOffset, true);
+ if (node && node->IsHTMLElement(nsGkAtoms::br)) {
+ aSelection.SetInterlinePosition(true);
+ return;
+ }
+
+ // Are we after a block? If so try set caret to following content
+ node = htmlEditor->GetPriorHTMLSibling(selNode, selOffset);
+ if (node && IsBlockNode(*node)) {
+ aSelection.SetInterlinePosition(true);
+ return;
+ }
+
+ // Are we before a block? If so try set caret to prior content
+ node = htmlEditor->GetNextHTMLSibling(selNode, selOffset);
+ if (node && IsBlockNode(*node)) {
+ aSelection.SetInterlinePosition(false);
+ }
+}
+
+nsresult
+HTMLEditRules::AdjustSelection(Selection* aSelection,
+ nsIEditor::EDirection aAction)
+{
+ NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER);
+
+ // if the selection isn't collapsed, do nothing.
+ // moose: one thing to do instead is check for the case of
+ // only a single break selected, and collapse it. Good thing? Beats me.
+ if (!aSelection->Collapsed()) {
+ return NS_OK;
+ }
+
+ // get the (collapsed) selection location
+ nsCOMPtr<nsINode> selNode, temp;
+ int32_t selOffset;
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv =
+ mHTMLEditor->GetStartNodeAndOffset(aSelection,
+ getter_AddRefs(selNode), &selOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ temp = selNode;
+
+ // are we in an editable node?
+ NS_ENSURE_STATE(mHTMLEditor);
+ while (!mHTMLEditor->IsEditable(selNode)) {
+ // scan up the tree until we find an editable place to be
+ selNode = EditorBase::GetNodeLocation(temp, &selOffset);
+ NS_ENSURE_TRUE(selNode, NS_ERROR_FAILURE);
+ temp = selNode;
+ NS_ENSURE_STATE(mHTMLEditor);
+ }
+
+ // make sure we aren't in an empty block - user will see no cursor. If this
+ // is happening, put a <br> in the block if allowed.
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsCOMPtr<Element> theblock = mHTMLEditor->GetBlock(*selNode);
+
+ if (theblock && mHTMLEditor->IsEditable(theblock)) {
+ bool bIsEmptyNode;
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->IsEmptyNode(theblock, &bIsEmptyNode, false, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // check if br can go into the destination node
+ NS_ENSURE_STATE(mHTMLEditor);
+ if (bIsEmptyNode && mHTMLEditor->CanContainTag(*selNode, *nsGkAtoms::br)) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsCOMPtr<Element> rootNode = mHTMLEditor->GetRoot();
+ NS_ENSURE_TRUE(rootNode, NS_ERROR_FAILURE);
+ if (selNode == rootNode) {
+ // Our root node is completely empty. Don't add a <br> here.
+ // AfterEditInner() will add one for us when it calls
+ // CreateBogusNodeIfNeeded()!
+ return NS_OK;
+ }
+
+ // we know we can skip the rest of this routine given the cirumstance
+ return CreateMozBR(GetAsDOMNode(selNode), selOffset);
+ }
+ }
+
+ // are we in a text node?
+ nsCOMPtr<nsIDOMCharacterData> textNode = do_QueryInterface(selNode);
+ if (textNode)
+ return NS_OK; // we LIKE it when we are in a text node. that RULZ
+
+ // do we need to insert a special mozBR? We do if we are:
+ // 1) prior node is in same block where selection is AND
+ // 2) prior node is a br AND
+ // 3) that br is not visible
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsCOMPtr<nsIContent> nearNode =
+ mHTMLEditor->GetPriorHTMLNode(selNode, selOffset);
+ if (nearNode) {
+ // is nearNode also a descendant of same block?
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsCOMPtr<Element> block = mHTMLEditor->GetBlock(*selNode);
+ nsCOMPtr<Element> nearBlock = mHTMLEditor->GetBlockNodeParent(nearNode);
+ if (block && block == nearBlock) {
+ if (nearNode && TextEditUtils::IsBreak(nearNode)) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ if (!mHTMLEditor->IsVisBreak(nearNode)) {
+ // need to insert special moz BR. Why? Because if we don't
+ // the user will see no new line for the break. Also, things
+ // like table cells won't grow in height.
+ nsCOMPtr<nsIDOMNode> brNode;
+ rv = CreateMozBR(GetAsDOMNode(selNode), selOffset,
+ getter_AddRefs(brNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIDOMNode> brParent =
+ EditorBase::GetNodeLocation(brNode, &selOffset);
+ // selection stays *before* moz-br, sticking to it
+ aSelection->SetInterlinePosition(true);
+ rv = aSelection->Collapse(brParent, selOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsCOMPtr<nsIContent> nextNode =
+ mHTMLEditor->GetNextHTMLNode(nearNode, true);
+ if (nextNode && TextEditUtils::IsMozBR(nextNode)) {
+ // selection between br and mozbr. make it stick to mozbr
+ // so that it will be on blank line.
+ aSelection->SetInterlinePosition(true);
+ }
+ }
+ }
+ }
+ }
+
+ // we aren't in a textnode: are we adjacent to text or a break or an image?
+ NS_ENSURE_STATE(mHTMLEditor);
+ nearNode = mHTMLEditor->GetPriorHTMLNode(selNode, selOffset, true);
+ if (nearNode && (TextEditUtils::IsBreak(nearNode) ||
+ EditorBase::IsTextNode(nearNode) ||
+ HTMLEditUtils::IsImage(nearNode) ||
+ nearNode->IsHTMLElement(nsGkAtoms::hr))) {
+ // this is a good place for the caret to be
+ return NS_OK;
+ }
+ NS_ENSURE_STATE(mHTMLEditor);
+ nearNode = mHTMLEditor->GetNextHTMLNode(selNode, selOffset, true);
+ if (nearNode && (TextEditUtils::IsBreak(nearNode) ||
+ EditorBase::IsTextNode(nearNode) ||
+ nearNode->IsAnyOfHTMLElements(nsGkAtoms::img,
+ nsGkAtoms::hr))) {
+ return NS_OK; // this is a good place for the caret to be
+ }
+
+ // look for a nearby text node.
+ // prefer the correct direction.
+ nsCOMPtr<nsIDOMNode> nearNodeDOM = GetAsDOMNode(nearNode);
+ rv = FindNearSelectableNode(GetAsDOMNode(selNode), selOffset, aAction,
+ address_of(nearNodeDOM));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nearNode = do_QueryInterface(nearNodeDOM);
+
+ if (!nearNode) {
+ return NS_OK;
+ }
+ EditorDOMPoint pt = GetGoodSelPointForNode(*nearNode, aAction);
+ rv = aSelection->Collapse(pt.node, pt.offset);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ return NS_OK;
+}
+
+
+nsresult
+HTMLEditRules::FindNearSelectableNode(nsIDOMNode* aSelNode,
+ int32_t aSelOffset,
+ nsIEditor::EDirection& aDirection,
+ nsCOMPtr<nsIDOMNode>* outSelectableNode)
+{
+ NS_ENSURE_TRUE(aSelNode && outSelectableNode, NS_ERROR_NULL_POINTER);
+ *outSelectableNode = nullptr;
+
+ nsCOMPtr<nsIDOMNode> nearNode, curNode;
+ if (aDirection == nsIEditor::ePrevious) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv =
+ mHTMLEditor->GetPriorHTMLNode(aSelNode, aSelOffset, address_of(nearNode));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv =
+ mHTMLEditor->GetNextHTMLNode(aSelNode, aSelOffset, address_of(nearNode));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ // Try the other direction then.
+ if (!nearNode) {
+ if (aDirection == nsIEditor::ePrevious) {
+ aDirection = nsIEditor::eNext;
+ } else {
+ aDirection = nsIEditor::ePrevious;
+ }
+
+ if (aDirection == nsIEditor::ePrevious) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv = mHTMLEditor->GetPriorHTMLNode(aSelNode, aSelOffset,
+ address_of(nearNode));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv = mHTMLEditor->GetNextHTMLNode(aSelNode, aSelOffset,
+ address_of(nearNode));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+ }
+
+ // scan in the right direction until we find an eligible text node,
+ // but don't cross any breaks, images, or table elements.
+ NS_ENSURE_STATE(mHTMLEditor);
+ while (nearNode && !(mHTMLEditor->IsTextNode(nearNode) ||
+ TextEditUtils::IsBreak(nearNode) ||
+ HTMLEditUtils::IsImage(nearNode))) {
+ curNode = nearNode;
+ if (aDirection == nsIEditor::ePrevious) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv =
+ mHTMLEditor->GetPriorHTMLNode(curNode, address_of(nearNode));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv = mHTMLEditor->GetNextHTMLNode(curNode, address_of(nearNode));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+ NS_ENSURE_STATE(mHTMLEditor);
+ }
+
+ if (nearNode) {
+ // don't cross any table elements
+ if (InDifferentTableElements(nearNode, aSelNode)) {
+ return NS_OK;
+ }
+
+ // otherwise, ok, we have found a good spot to put the selection
+ *outSelectableNode = do_QueryInterface(nearNode);
+ }
+ return NS_OK;
+}
+
+bool
+HTMLEditRules::InDifferentTableElements(nsIDOMNode* aNode1,
+ nsIDOMNode* aNode2)
+{
+ nsCOMPtr<nsINode> node1 = do_QueryInterface(aNode1);
+ nsCOMPtr<nsINode> node2 = do_QueryInterface(aNode2);
+ return InDifferentTableElements(node1, node2);
+}
+
+bool
+HTMLEditRules::InDifferentTableElements(nsINode* aNode1,
+ nsINode* aNode2)
+{
+ MOZ_ASSERT(aNode1 && aNode2);
+
+ while (aNode1 && !HTMLEditUtils::IsTableElement(aNode1)) {
+ aNode1 = aNode1->GetParentNode();
+ }
+
+ while (aNode2 && !HTMLEditUtils::IsTableElement(aNode2)) {
+ aNode2 = aNode2->GetParentNode();
+ }
+
+ return aNode1 != aNode2;
+}
+
+
+nsresult
+HTMLEditRules::RemoveEmptyNodes()
+{
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+
+ // Some general notes on the algorithm used here: the goal is to examine all
+ // the nodes in mDocChangeRange, and remove the empty ones. We do this by
+ // using a content iterator to traverse all the nodes in the range, and
+ // placing the empty nodes into an array. After finishing the iteration, we
+ // delete the empty nodes in the array. (They cannot be deleted as we find
+ // them because that would invalidate the iterator.)
+ //
+ // Since checking to see if a node is empty can be costly for nodes with many
+ // descendants, there are some optimizations made. I rely on the fact that
+ // the iterator is post-order: it will visit children of a node before
+ // visiting the parent node. So if I find that a child node is not empty, I
+ // know that its parent is not empty without even checking. So I put the
+ // parent on a "skipList" which is just a voidArray of nodes I can skip the
+ // empty check on. If I encounter a node on the skiplist, i skip the
+ // processing for that node and replace its slot in the skiplist with that
+ // node's parent.
+ //
+ // An interesting idea is to go ahead and regard parent nodes that are NOT on
+ // the skiplist as being empty (without even doing the IsEmptyNode check) on
+ // the theory that if they weren't empty, we would have encountered a
+ // non-empty child earlier and thus put this parent node on the skiplist.
+ //
+ // Unfortunately I can't use that strategy here, because the range may
+ // include some children of a node while excluding others. Thus I could find
+ // all the _examined_ children empty, but still not have an empty parent.
+
+ // need an iterator
+ nsCOMPtr<nsIContentIterator> iter = NS_NewContentIterator();
+
+ nsresult rv = iter->Init(mDocChangeRange);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<OwningNonNull<nsINode>> arrayOfEmptyNodes, arrayOfEmptyCites, skipList;
+
+ // Check for empty nodes
+ while (!iter->IsDone()) {
+ OwningNonNull<nsINode> node = *iter->GetCurrentNode();
+
+ nsCOMPtr<nsINode> parent = node->GetParentNode();
+
+ size_t idx = skipList.IndexOf(node);
+ if (idx != skipList.NoIndex) {
+ // This node is on our skip list. Skip processing for this node, and
+ // replace its value in the skip list with the value of its parent
+ if (parent) {
+ skipList[idx] = parent;
+ }
+ } else {
+ bool bIsCandidate = false;
+ bool bIsEmptyNode = false;
+ bool bIsMailCite = false;
+
+ if (node->IsElement()) {
+ if (node->IsHTMLElement(nsGkAtoms::body)) {
+ // Don't delete the body
+ } else if ((bIsMailCite = HTMLEditUtils::IsMailCite(node)) ||
+ node->IsHTMLElement(nsGkAtoms::a) ||
+ HTMLEditUtils::IsInlineStyle(node) ||
+ HTMLEditUtils::IsList(node) ||
+ node->IsHTMLElement(nsGkAtoms::div)) {
+ // Only consider certain nodes to be empty for purposes of removal
+ bIsCandidate = true;
+ } else if (HTMLEditUtils::IsFormatNode(node) ||
+ HTMLEditUtils::IsListItem(node) ||
+ node->IsHTMLElement(nsGkAtoms::blockquote)) {
+ // These node types are candidates if selection is not in them. If
+ // it is one of these, don't delete if selection inside. This is so
+ // we can create empty headings, etc., for the user to type into.
+ bool bIsSelInNode;
+ rv = SelectionEndpointInNode(node, &bIsSelInNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!bIsSelInNode) {
+ bIsCandidate = true;
+ }
+ }
+ }
+
+ if (bIsCandidate) {
+ // We delete mailcites even if they have a solo br in them. Other
+ // nodes we require to be empty.
+ rv = htmlEditor->IsEmptyNode(node->AsDOMNode(), &bIsEmptyNode,
+ bIsMailCite, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (bIsEmptyNode) {
+ if (bIsMailCite) {
+ // mailcites go on a separate list from other empty nodes
+ arrayOfEmptyCites.AppendElement(*node);
+ } else {
+ arrayOfEmptyNodes.AppendElement(*node);
+ }
+ }
+ }
+
+ if (!bIsEmptyNode && parent) {
+ // put parent on skip list
+ skipList.AppendElement(*parent);
+ }
+ }
+
+ iter->Next();
+ }
+
+ // now delete the empty nodes
+ for (auto& delNode : arrayOfEmptyNodes) {
+ if (htmlEditor->IsModifiableNode(delNode)) {
+ rv = htmlEditor->DeleteNode(delNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ // Now delete the empty mailcites. This is a separate step because we want
+ // to pull out any br's and preserve them.
+ for (auto& delNode : arrayOfEmptyCites) {
+ bool bIsEmptyNode;
+ rv = htmlEditor->IsEmptyNode(delNode, &bIsEmptyNode, false, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!bIsEmptyNode) {
+ // We are deleting a cite that has just a br. We want to delete cite,
+ // but preserve br.
+ nsCOMPtr<nsINode> parent = delNode->GetParentNode();
+ int32_t offset = parent ? parent->IndexOf(delNode) : -1;
+ nsCOMPtr<Element> br = htmlEditor->CreateBR(parent, offset);
+ NS_ENSURE_STATE(br);
+ }
+ rv = htmlEditor->DeleteNode(delNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+HTMLEditRules::SelectionEndpointInNode(nsINode* aNode,
+ bool* aResult)
+{
+ NS_ENSURE_TRUE(aNode && aResult, NS_ERROR_NULL_POINTER);
+
+ nsIDOMNode* node = aNode->AsDOMNode();
+
+ *aResult = false;
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<Selection> selection = mHTMLEditor->GetSelection();
+ NS_ENSURE_STATE(selection);
+
+ uint32_t rangeCount = selection->RangeCount();
+ for (uint32_t rangeIdx = 0; rangeIdx < rangeCount; ++rangeIdx) {
+ RefPtr<nsRange> range = selection->GetRangeAt(rangeIdx);
+ nsCOMPtr<nsIDOMNode> startParent, endParent;
+ range->GetStartContainer(getter_AddRefs(startParent));
+ if (startParent) {
+ if (node == startParent) {
+ *aResult = true;
+ return NS_OK;
+ }
+ if (EditorUtils::IsDescendantOf(startParent, node)) {
+ *aResult = true;
+ return NS_OK;
+ }
+ }
+ range->GetEndContainer(getter_AddRefs(endParent));
+ if (startParent == endParent) {
+ continue;
+ }
+ if (endParent) {
+ if (node == endParent) {
+ *aResult = true;
+ return NS_OK;
+ }
+ if (EditorUtils::IsDescendantOf(endParent, node)) {
+ *aResult = true;
+ return NS_OK;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+/**
+ * IsEmptyInline: Return true if aNode is an empty inline container
+ */
+bool
+HTMLEditRules::IsEmptyInline(nsINode& aNode)
+{
+ NS_ENSURE_TRUE(mHTMLEditor, false);
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+
+ if (IsInlineNode(aNode) && htmlEditor->IsContainer(&aNode)) {
+ bool isEmpty = true;
+ htmlEditor->IsEmptyNode(&aNode, &isEmpty);
+ return isEmpty;
+ }
+ return false;
+}
+
+
+bool
+HTMLEditRules::ListIsEmptyLine(nsTArray<OwningNonNull<nsINode>>& aArrayOfNodes)
+{
+ // We have a list of nodes which we are candidates for being moved into a new
+ // block. Determine if it's anything more than a blank line. Look for
+ // editable content above and beyond one single BR.
+ NS_ENSURE_TRUE(aArrayOfNodes.Length(), true);
+
+ NS_ENSURE_TRUE(mHTMLEditor, false);
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+
+ int32_t brCount = 0;
+
+ for (auto& node : aArrayOfNodes) {
+ if (!htmlEditor->IsEditable(node)) {
+ continue;
+ }
+ if (TextEditUtils::IsBreak(node)) {
+ // First break doesn't count
+ if (brCount) {
+ return false;
+ }
+ brCount++;
+ } else if (IsEmptyInline(node)) {
+ // Empty inline, keep looking
+ } else {
+ return false;
+ }
+ }
+ return true;
+}
+
+
+nsresult
+HTMLEditRules::PopListItem(nsIDOMNode* aListItem,
+ bool* aOutOfList)
+{
+ nsCOMPtr<Element> listItem = do_QueryInterface(aListItem);
+ // check parms
+ NS_ENSURE_TRUE(listItem && aOutOfList, NS_ERROR_NULL_POINTER);
+
+ // init out params
+ *aOutOfList = false;
+
+ nsCOMPtr<nsINode> curParent = listItem->GetParentNode();
+ int32_t offset = curParent ? curParent->IndexOf(listItem) : -1;
+
+ if (!HTMLEditUtils::IsListItem(listItem)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // if it's first or last list item, don't need to split the list
+ // otherwise we do.
+ nsCOMPtr<nsINode> curParPar = curParent->GetParentNode();
+ int32_t parOffset = curParPar ? curParPar->IndexOf(curParent) : -1;
+
+ bool bIsFirstListItem;
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv =
+ mHTMLEditor->IsFirstEditableChild(aListItem, &bIsFirstListItem);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool bIsLastListItem;
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->IsLastEditableChild(aListItem, &bIsLastListItem);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!bIsFirstListItem && !bIsLastListItem) {
+ // split the list
+ nsCOMPtr<nsIDOMNode> newBlock;
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->SplitNode(GetAsDOMNode(curParent), offset,
+ getter_AddRefs(newBlock));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (!bIsFirstListItem) {
+ parOffset++;
+ }
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->MoveNode(listItem, curParPar, parOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // unwrap list item contents if they are no longer in a list
+ if (!HTMLEditUtils::IsList(curParPar) &&
+ HTMLEditUtils::IsListItem(listItem)) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->RemoveBlockContainer(*listItem);
+ NS_ENSURE_SUCCESS(rv, rv);
+ *aOutOfList = true;
+ }
+ return NS_OK;
+}
+
+nsresult
+HTMLEditRules::RemoveListStructure(Element& aList)
+{
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+ while (aList.GetFirstChild()) {
+ OwningNonNull<nsIContent> child = *aList.GetFirstChild();
+
+ if (HTMLEditUtils::IsListItem(child)) {
+ bool isOutOfList;
+ // Keep popping it out until it's not in a list anymore
+ do {
+ nsresult rv = PopListItem(child->AsDOMNode(), &isOutOfList);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } while (!isOutOfList);
+ } else if (HTMLEditUtils::IsList(child)) {
+ nsresult rv = RemoveListStructure(*child->AsElement());
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // Delete any non-list items for now
+ nsresult rv = htmlEditor->DeleteNode(child);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ // Delete the now-empty list
+ nsresult rv = htmlEditor->RemoveBlockContainer(aList);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+HTMLEditRules::ConfirmSelectionInBody()
+{
+ // get the body
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsCOMPtr<nsIDOMElement> rootElement = do_QueryInterface(mHTMLEditor->GetRoot());
+ NS_ENSURE_TRUE(rootElement, NS_ERROR_UNEXPECTED);
+
+ // get the selection
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<Selection> selection = mHTMLEditor->GetSelection();
+ NS_ENSURE_STATE(selection);
+
+ // get the selection start location
+ nsCOMPtr<nsIDOMNode> selNode, temp, parent;
+ int32_t selOffset;
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv =
+ mHTMLEditor->GetStartNodeAndOffset(selection,
+ getter_AddRefs(selNode), &selOffset);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ temp = selNode;
+
+ // check that selNode is inside body
+ while (temp && !TextEditUtils::IsBody(temp)) {
+ temp->GetParentNode(getter_AddRefs(parent));
+ temp = parent;
+ }
+
+ // if we aren't in the body, force the issue
+ if (!temp) {
+// uncomment this to see when we get bad selections
+// NS_NOTREACHED("selection not in body");
+ selection->Collapse(rootElement, 0);
+ }
+
+ // get the selection end location
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->GetEndNodeAndOffset(selection,
+ getter_AddRefs(selNode), &selOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ temp = selNode;
+
+ // check that selNode is inside body
+ while (temp && !TextEditUtils::IsBody(temp)) {
+ rv = temp->GetParentNode(getter_AddRefs(parent));
+ temp = parent;
+ }
+
+ // if we aren't in the body, force the issue
+ if (!temp) {
+// uncomment this to see when we get bad selections
+// NS_NOTREACHED("selection not in body");
+ selection->Collapse(rootElement, 0);
+ }
+
+ // XXX This is the result of the last call of GetParentNode(), it doesn't
+ // make sense...
+ return rv;
+}
+
+nsresult
+HTMLEditRules::UpdateDocChangeRange(nsRange* aRange)
+{
+ // first make sure aRange is in the document. It might not be if
+ // portions of our editting action involved manipulating nodes
+ // prior to placing them in the document (e.g., populating a list item
+ // before placing it in its list)
+ nsCOMPtr<nsIDOMNode> startNode;
+ nsresult rv = aRange->GetStartContainer(getter_AddRefs(startNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_STATE(mHTMLEditor);
+ if (!mHTMLEditor->IsDescendantOfRoot(startNode)) {
+ // just return - we don't need to adjust mDocChangeRange in this case
+ return NS_OK;
+ }
+
+ if (!mDocChangeRange) {
+ // clone aRange.
+ mDocChangeRange = aRange->CloneRange();
+ } else {
+ int16_t result;
+
+ // compare starts of ranges
+ rv = mDocChangeRange->CompareBoundaryPoints(nsIDOMRange::START_TO_START,
+ aRange, &result);
+ if (rv == NS_ERROR_NOT_INITIALIZED) {
+ // This will happen is mDocChangeRange is non-null, but the range is
+ // uninitialized. In this case we'll set the start to aRange start.
+ // The same test won't be needed further down since after we've set
+ // the start the range will be collapsed to that point.
+ result = 1;
+ rv = NS_OK;
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Positive result means mDocChangeRange start is after aRange start.
+ if (result > 0) {
+ int32_t startOffset;
+ rv = aRange->GetStartOffset(&startOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mDocChangeRange->SetStart(startNode, startOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // compare ends of ranges
+ rv = mDocChangeRange->CompareBoundaryPoints(nsIDOMRange::END_TO_END,
+ aRange, &result);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Negative result means mDocChangeRange end is before aRange end.
+ if (result < 0) {
+ nsCOMPtr<nsIDOMNode> endNode;
+ int32_t endOffset;
+ rv = aRange->GetEndContainer(getter_AddRefs(endNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aRange->GetEndOffset(&endOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mDocChangeRange->SetEnd(endNode, endOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+HTMLEditRules::InsertMozBRIfNeeded(nsINode& aNode)
+{
+ if (!IsBlockNode(aNode)) {
+ return NS_OK;
+ }
+
+ bool isEmpty;
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv = mHTMLEditor->IsEmptyNode(&aNode, &isEmpty);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!isEmpty) {
+ return NS_OK;
+ }
+
+ return CreateMozBR(aNode.AsDOMNode(), 0);
+}
+
+NS_IMETHODIMP
+HTMLEditRules::WillCreateNode(const nsAString& aTag,
+ nsIDOMNode* aParent,
+ int32_t aPosition)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditRules::DidCreateNode(const nsAString& aTag,
+ nsIDOMNode* aNode,
+ nsIDOMNode* aParent,
+ int32_t aPosition,
+ nsresult aResult)
+{
+ if (!mListenerEnabled) {
+ return NS_OK;
+ }
+ // assumption that Join keeps the righthand node
+ nsresult rv = mUtilRange->SelectNode(aNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return UpdateDocChangeRange(mUtilRange);
+}
+
+NS_IMETHODIMP
+HTMLEditRules::WillInsertNode(nsIDOMNode* aNode,
+ nsIDOMNode* aParent,
+ int32_t aPosition)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditRules::DidInsertNode(nsIDOMNode* aNode,
+ nsIDOMNode* aParent,
+ int32_t aPosition,
+ nsresult aResult)
+{
+ if (!mListenerEnabled) {
+ return NS_OK;
+ }
+ nsresult rv = mUtilRange->SelectNode(aNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return UpdateDocChangeRange(mUtilRange);
+}
+
+NS_IMETHODIMP
+HTMLEditRules::WillDeleteNode(nsIDOMNode* aChild)
+{
+ if (!mListenerEnabled) {
+ return NS_OK;
+ }
+ nsresult rv = mUtilRange->SelectNode(aChild);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return UpdateDocChangeRange(mUtilRange);
+}
+
+NS_IMETHODIMP
+HTMLEditRules::DidDeleteNode(nsIDOMNode* aChild,
+ nsresult aResult)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditRules::WillSplitNode(nsIDOMNode* aExistingRightNode,
+ int32_t aOffset)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditRules::DidSplitNode(nsIDOMNode* aExistingRightNode,
+ int32_t aOffset,
+ nsIDOMNode* aNewLeftNode,
+ nsresult aResult)
+{
+ if (!mListenerEnabled) {
+ return NS_OK;
+ }
+ nsresult rv = mUtilRange->SetStart(aNewLeftNode, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mUtilRange->SetEnd(aExistingRightNode, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return UpdateDocChangeRange(mUtilRange);
+}
+
+NS_IMETHODIMP
+HTMLEditRules::WillJoinNodes(nsIDOMNode* aLeftNode,
+ nsIDOMNode* aRightNode,
+ nsIDOMNode* aParent)
+{
+ if (!mListenerEnabled) {
+ return NS_OK;
+ }
+ // remember split point
+ return EditorBase::GetLengthOfDOMNode(aLeftNode, mJoinOffset);
+}
+
+NS_IMETHODIMP
+HTMLEditRules::DidJoinNodes(nsIDOMNode* aLeftNode,
+ nsIDOMNode* aRightNode,
+ nsIDOMNode* aParent,
+ nsresult aResult)
+{
+ if (!mListenerEnabled) {
+ return NS_OK;
+ }
+ // assumption that Join keeps the righthand node
+ nsresult rv = mUtilRange->SetStart(aRightNode, mJoinOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mUtilRange->SetEnd(aRightNode, mJoinOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return UpdateDocChangeRange(mUtilRange);
+}
+
+NS_IMETHODIMP
+HTMLEditRules::WillInsertText(nsIDOMCharacterData* aTextNode,
+ int32_t aOffset,
+ const nsAString& aString)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditRules::DidInsertText(nsIDOMCharacterData* aTextNode,
+ int32_t aOffset,
+ const nsAString& aString,
+ nsresult aResult)
+{
+ if (!mListenerEnabled) {
+ return NS_OK;
+ }
+ int32_t length = aString.Length();
+ nsCOMPtr<nsIDOMNode> theNode = do_QueryInterface(aTextNode);
+ nsresult rv = mUtilRange->SetStart(theNode, aOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mUtilRange->SetEnd(theNode, aOffset+length);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return UpdateDocChangeRange(mUtilRange);
+}
+
+NS_IMETHODIMP
+HTMLEditRules::WillDeleteText(nsIDOMCharacterData* aTextNode,
+ int32_t aOffset,
+ int32_t aLength)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditRules::DidDeleteText(nsIDOMCharacterData* aTextNode,
+ int32_t aOffset,
+ int32_t aLength,
+ nsresult aResult)
+{
+ if (!mListenerEnabled) {
+ return NS_OK;
+ }
+ nsCOMPtr<nsIDOMNode> theNode = do_QueryInterface(aTextNode);
+ nsresult rv = mUtilRange->SetStart(theNode, aOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mUtilRange->SetEnd(theNode, aOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return UpdateDocChangeRange(mUtilRange);
+}
+
+NS_IMETHODIMP
+HTMLEditRules::WillDeleteSelection(nsISelection* aSelection)
+{
+ if (!mListenerEnabled) {
+ return NS_OK;
+ }
+ if (NS_WARN_IF(!aSelection)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ RefPtr<Selection> selection = aSelection->AsSelection();
+ // get the (collapsed) selection location
+ nsCOMPtr<nsIDOMNode> selNode;
+ int32_t selOffset;
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv =
+ mHTMLEditor->GetStartNodeAndOffset(selection,
+ getter_AddRefs(selNode), &selOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mUtilRange->SetStart(selNode, selOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->GetEndNodeAndOffset(selection,
+ getter_AddRefs(selNode), &selOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mUtilRange->SetEnd(selNode, selOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return UpdateDocChangeRange(mUtilRange);
+}
+
+NS_IMETHODIMP
+HTMLEditRules::DidDeleteSelection(nsISelection *aSelection)
+{
+ return NS_OK;
+}
+
+// Let's remove all alignment hints in the children of aNode; it can
+// be an ALIGN attribute (in case we just remove it) or a CENTER
+// element (here we have to remove the container and keep its
+// children). We break on tables and don't look at their children.
+nsresult
+HTMLEditRules::RemoveAlignment(nsIDOMNode* aNode,
+ const nsAString& aAlignType,
+ bool aChildrenOnly)
+{
+ NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER);
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ if (mHTMLEditor->IsTextNode(aNode) || HTMLEditUtils::IsTable(aNode)) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIDOMNode> child = aNode,tmp;
+ if (aChildrenOnly) {
+ aNode->GetFirstChild(getter_AddRefs(child));
+ }
+ NS_ENSURE_STATE(mHTMLEditor);
+ bool useCSS = mHTMLEditor->IsCSSEnabled();
+
+ while (child) {
+ if (aChildrenOnly) {
+ // get the next sibling right now because we could have to remove child
+ child->GetNextSibling(getter_AddRefs(tmp));
+ } else {
+ tmp = nullptr;
+ }
+ bool isBlock;
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv = mHTMLEditor->NodeIsBlockStatic(child, &isBlock);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (EditorBase::NodeIsType(child, nsGkAtoms::center)) {
+ // the current node is a CENTER element
+ // first remove children's alignment
+ rv = RemoveAlignment(child, aAlignType, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // we may have to insert BRs in first and last position of element's children
+ // if the nodes before/after are not blocks and not BRs
+ rv = MakeSureElemStartsOrEndsOnCR(child);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // now remove the CENTER container
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsCOMPtr<Element> childAsElement = do_QueryInterface(child);
+ NS_ENSURE_STATE(childAsElement);
+ rv = mHTMLEditor->RemoveContainer(childAsElement);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else if (isBlock || HTMLEditUtils::IsHR(child)) {
+ // the current node is a block element
+ nsCOMPtr<nsIDOMElement> curElem = do_QueryInterface(child);
+ if (HTMLEditUtils::SupportsAlignAttr(child)) {
+ // remove the ALIGN attribute if this element can have it
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->RemoveAttribute(curElem, NS_LITERAL_STRING("align"));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ if (useCSS) {
+ if (HTMLEditUtils::IsTable(child) || HTMLEditUtils::IsHR(child)) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->SetAttributeOrEquivalent(curElem,
+ NS_LITERAL_STRING("align"),
+ aAlignType, false);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ nsAutoString dummyCssValue;
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->mCSSEditUtils->RemoveCSSInlineStyle(
+ child,
+ nsGkAtoms::textAlign,
+ dummyCssValue);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+ }
+ if (!HTMLEditUtils::IsTable(child)) {
+ // unless this is a table, look at children
+ rv = RemoveAlignment(child, aAlignType, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ child = tmp;
+ }
+ return NS_OK;
+}
+
+// Let's insert a BR as first (resp. last) child of aNode if its
+// first (resp. last) child is not a block nor a BR, and if the
+// previous (resp. next) sibling is not a block nor a BR
+nsresult
+HTMLEditRules::MakeSureElemStartsOrEndsOnCR(nsIDOMNode* aNode,
+ bool aStarts)
+{
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsIDOMNode> child;
+ if (aStarts) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ child = GetAsDOMNode(mHTMLEditor->GetFirstEditableChild(*node));
+ } else {
+ NS_ENSURE_STATE(mHTMLEditor);
+ child = GetAsDOMNode(mHTMLEditor->GetLastEditableChild(*node));
+ }
+ NS_ENSURE_TRUE(child, NS_OK);
+ bool isChildBlock;
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv = mHTMLEditor->NodeIsBlockStatic(child, &isChildBlock);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool foundCR = false;
+ if (isChildBlock || TextEditUtils::IsBreak(child)) {
+ foundCR = true;
+ } else {
+ nsCOMPtr<nsIDOMNode> sibling;
+ if (aStarts) {
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->GetPriorHTMLSibling(aNode, address_of(sibling));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->GetNextHTMLSibling(aNode, address_of(sibling));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+ if (sibling) {
+ bool isBlock;
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->NodeIsBlockStatic(sibling, &isBlock);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (isBlock || TextEditUtils::IsBreak(sibling)) {
+ foundCR = true;
+ }
+ } else {
+ foundCR = true;
+ }
+ }
+ if (!foundCR) {
+ int32_t offset = 0;
+ if (!aStarts) {
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ NS_ENSURE_STATE(node);
+ offset = node->GetChildCount();
+ }
+ nsCOMPtr<nsIDOMNode> brNode;
+ NS_ENSURE_STATE(mHTMLEditor);
+ rv = mHTMLEditor->CreateBR(aNode, offset, address_of(brNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+nsresult
+HTMLEditRules::MakeSureElemStartsOrEndsOnCR(nsIDOMNode* aNode)
+{
+ nsresult rv = MakeSureElemStartsOrEndsOnCR(aNode, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return MakeSureElemStartsOrEndsOnCR(aNode, true);
+}
+
+nsresult
+HTMLEditRules::AlignBlock(Element& aElement,
+ const nsAString& aAlignType,
+ ContentsOnly aContentsOnly)
+{
+ if (!IsBlockNode(aElement) && !aElement.IsHTMLElement(nsGkAtoms::hr)) {
+ // We deal only with blocks; early way out
+ return NS_OK;
+ }
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+
+ nsresult rv = RemoveAlignment(aElement.AsDOMNode(), aAlignType,
+ aContentsOnly == ContentsOnly::yes);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_NAMED_LITERAL_STRING(attr, "align");
+ if (htmlEditor->IsCSSEnabled()) {
+ // Let's use CSS alignment; we use margin-left and margin-right for tables
+ // and text-align for other block-level elements
+ rv = htmlEditor->SetAttributeOrEquivalent(
+ static_cast<nsIDOMElement*>(aElement.AsDOMNode()),
+ attr, aAlignType, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // HTML case; this code is supposed to be called ONLY if the element
+ // supports the align attribute but we'll never know...
+ if (HTMLEditUtils::SupportsAlignAttr(aElement.AsDOMNode())) {
+ rv = htmlEditor->SetAttribute(
+ static_cast<nsIDOMElement*>(aElement.AsDOMNode()),
+ attr, aAlignType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+HTMLEditRules::ChangeIndentation(Element& aElement,
+ Change aChange)
+{
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+
+ nsIAtom& marginProperty =
+ MarginPropertyAtomForIndent(*htmlEditor->mCSSEditUtils, aElement);
+ nsAutoString value;
+ htmlEditor->mCSSEditUtils->GetSpecifiedProperty(aElement, marginProperty,
+ value);
+ float f;
+ nsCOMPtr<nsIAtom> unit;
+ htmlEditor->mCSSEditUtils->ParseLength(value, &f, getter_AddRefs(unit));
+ if (!f) {
+ nsAutoString defaultLengthUnit;
+ htmlEditor->mCSSEditUtils->GetDefaultLengthUnit(defaultLengthUnit);
+ unit = NS_Atomize(defaultLengthUnit);
+ }
+ int8_t multiplier = aChange == Change::plus ? +1 : -1;
+ if (nsGkAtoms::in == unit) {
+ f += NS_EDITOR_INDENT_INCREMENT_IN * multiplier;
+ } else if (nsGkAtoms::cm == unit) {
+ f += NS_EDITOR_INDENT_INCREMENT_CM * multiplier;
+ } else if (nsGkAtoms::mm == unit) {
+ f += NS_EDITOR_INDENT_INCREMENT_MM * multiplier;
+ } else if (nsGkAtoms::pt == unit) {
+ f += NS_EDITOR_INDENT_INCREMENT_PT * multiplier;
+ } else if (nsGkAtoms::pc == unit) {
+ f += NS_EDITOR_INDENT_INCREMENT_PC * multiplier;
+ } else if (nsGkAtoms::em == unit) {
+ f += NS_EDITOR_INDENT_INCREMENT_EM * multiplier;
+ } else if (nsGkAtoms::ex == unit) {
+ f += NS_EDITOR_INDENT_INCREMENT_EX * multiplier;
+ } else if (nsGkAtoms::px == unit) {
+ f += NS_EDITOR_INDENT_INCREMENT_PX * multiplier;
+ } else if (nsGkAtoms::percentage == unit) {
+ f += NS_EDITOR_INDENT_INCREMENT_PERCENT * multiplier;
+ }
+
+ if (0 < f) {
+ nsAutoString newValue;
+ newValue.AppendFloat(f);
+ newValue.Append(nsDependentAtomString(unit));
+ htmlEditor->mCSSEditUtils->SetCSSProperty(aElement, marginProperty,
+ newValue);
+ return NS_OK;
+ }
+
+ htmlEditor->mCSSEditUtils->RemoveCSSProperty(aElement, marginProperty,
+ value);
+
+ // Remove unnecessary divs
+ if (!aElement.IsHTMLElement(nsGkAtoms::div) ||
+ &aElement == htmlEditor->GetActiveEditingHost() ||
+ !htmlEditor->IsDescendantOfEditorRoot(&aElement) ||
+ HTMLEditor::HasAttributes(&aElement)) {
+ return NS_OK;
+ }
+
+ nsresult rv = htmlEditor->RemoveContainer(&aElement);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+HTMLEditRules::WillAbsolutePosition(Selection& aSelection,
+ bool* aCancel,
+ bool* aHandled)
+{
+ MOZ_ASSERT(aCancel && aHandled);
+ NS_ENSURE_STATE(mHTMLEditor);
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+
+ WillInsert(aSelection, aCancel);
+
+ // We want to ignore result of WillInsert()
+ *aCancel = false;
+ *aHandled = true;
+
+ nsCOMPtr<Element> focusElement = htmlEditor->GetSelectionContainer();
+ if (focusElement && HTMLEditUtils::IsImage(focusElement)) {
+ mNewBlock = focusElement;
+ return NS_OK;
+ }
+
+ nsresult rv = NormalizeSelection(&aSelection);
+ NS_ENSURE_SUCCESS(rv, rv);
+ AutoSelectionRestorer selectionRestorer(&aSelection, htmlEditor);
+
+ // Convert the selection ranges into "promoted" selection ranges: this
+ // basically just expands the range to include the immediate block parent,
+ // and then further expands to include any ancestors whose children are all
+ // in the range.
+
+ nsTArray<RefPtr<nsRange>> arrayOfRanges;
+ GetPromotedRanges(aSelection, arrayOfRanges,
+ EditAction::setAbsolutePosition);
+
+ // Use these ranges to contruct a list of nodes to act on.
+ nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
+ rv = GetNodesForOperation(arrayOfRanges, arrayOfNodes,
+ EditAction::setAbsolutePosition);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If nothing visible in list, make an empty block
+ if (ListIsEmptyLine(arrayOfNodes)) {
+ // Get selection location
+ NS_ENSURE_STATE(aSelection.GetRangeAt(0) &&
+ aSelection.GetRangeAt(0)->GetStartParent());
+ OwningNonNull<nsINode> parent =
+ *aSelection.GetRangeAt(0)->GetStartParent();
+ int32_t offset = aSelection.GetRangeAt(0)->StartOffset();
+
+ // Make sure we can put a block here
+ rv = SplitAsNeeded(*nsGkAtoms::div, parent, offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<Element> positionedDiv =
+ htmlEditor->CreateNode(nsGkAtoms::div, parent, offset);
+ NS_ENSURE_STATE(positionedDiv);
+ // Remember our new block for postprocessing
+ mNewBlock = positionedDiv;
+ // Delete anything that was in the list of nodes
+ while (!arrayOfNodes.IsEmpty()) {
+ OwningNonNull<nsINode> curNode = arrayOfNodes[0];
+ rv = htmlEditor->DeleteNode(curNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ arrayOfNodes.RemoveElementAt(0);
+ }
+ // Put selection in new block
+ *aHandled = true;
+ rv = aSelection.Collapse(positionedDiv, 0);
+ // Don't restore the selection
+ selectionRestorer.Abort();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+ }
+
+ // Okay, now go through all the nodes and put them in a blockquote, or
+ // whatever is appropriate. Woohoo!
+ nsCOMPtr<Element> curList, curPositionedDiv, indentedLI;
+ for (uint32_t i = 0; i < arrayOfNodes.Length(); i++) {
+ // Here's where we actually figure out what to do
+ NS_ENSURE_STATE(arrayOfNodes[i]->IsContent());
+ OwningNonNull<nsIContent> curNode = *arrayOfNodes[i]->AsContent();
+
+ // Ignore all non-editable nodes. Leave them be.
+ if (!htmlEditor->IsEditable(curNode)) {
+ continue;
+ }
+
+ nsCOMPtr<nsIContent> sibling;
+
+ nsCOMPtr<nsINode> curParent = curNode->GetParentNode();
+ int32_t offset = curParent ? curParent->IndexOf(curNode) : -1;
+
+ // Some logic for putting list items into nested lists...
+ if (HTMLEditUtils::IsList(curParent)) {
+ // Check to see if curList is still appropriate. Which it is if curNode
+ // is still right after it in the same list.
+ if (curList) {
+ sibling = htmlEditor->GetPriorHTMLSibling(curNode);
+ }
+
+ if (!curList || (sibling && sibling != curList)) {
+ // Create a new nested list of correct type
+ rv =
+ SplitAsNeeded(*curParent->NodeInfo()->NameAtom(), curParent, offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!curPositionedDiv) {
+ nsCOMPtr<nsINode> curParentParent = curParent->GetParentNode();
+ int32_t parentOffset = curParentParent
+ ? curParentParent->IndexOf(curParent) : -1;
+ curPositionedDiv = htmlEditor->CreateNode(nsGkAtoms::div, curParentParent,
+ parentOffset);
+ mNewBlock = curPositionedDiv;
+ }
+ curList = htmlEditor->CreateNode(curParent->NodeInfo()->NameAtom(),
+ curPositionedDiv, -1);
+ NS_ENSURE_STATE(curList);
+ // curList is now the correct thing to put curNode in. Remember our
+ // new block for postprocessing.
+ }
+ // Tuck the node into the end of the active list
+ rv = htmlEditor->MoveNode(curNode, curList, -1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // Not a list item, use blockquote? If we are inside a list item, we
+ // don't want to blockquote, we want to sublist the list item. We may
+ // have several nodes listed in the array of nodes to act on, that are in
+ // the same list item. Since we only want to indent that li once, we
+ // must keep track of the most recent indented list item, and not indent
+ // it if we find another node to act on that is still inside the same li.
+ nsCOMPtr<Element> listItem = IsInListItem(curNode);
+ if (listItem) {
+ if (indentedLI == listItem) {
+ // Already indented this list item
+ continue;
+ }
+ curParent = listItem->GetParentNode();
+ offset = curParent ? curParent->IndexOf(listItem) : -1;
+ // Check to see if curList is still appropriate. Which it is if
+ // curNode is still right after it in the same list.
+ if (curList) {
+ sibling = htmlEditor->GetPriorHTMLSibling(curNode);
+ }
+
+ if (!curList || (sibling && sibling != curList)) {
+ // Create a new nested list of correct type
+ rv = SplitAsNeeded(*curParent->NodeInfo()->NameAtom(), curParent,
+ offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!curPositionedDiv) {
+ nsCOMPtr<nsINode> curParentParent = curParent->GetParentNode();
+ int32_t parentOffset = curParentParent ?
+ curParentParent->IndexOf(curParent) : -1;
+ curPositionedDiv = htmlEditor->CreateNode(nsGkAtoms::div,
+ curParentParent,
+ parentOffset);
+ mNewBlock = curPositionedDiv;
+ }
+ curList = htmlEditor->CreateNode(curParent->NodeInfo()->NameAtom(),
+ curPositionedDiv, -1);
+ NS_ENSURE_STATE(curList);
+ }
+ rv = htmlEditor->MoveNode(listItem, curList, -1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Remember we indented this li
+ indentedLI = listItem;
+ } else {
+ // Need to make a div to put things in if we haven't already
+
+ if (!curPositionedDiv) {
+ if (curNode->IsHTMLElement(nsGkAtoms::div)) {
+ curPositionedDiv = curNode->AsElement();
+ mNewBlock = curPositionedDiv;
+ curList = nullptr;
+ continue;
+ }
+ rv = SplitAsNeeded(*nsGkAtoms::div, curParent, offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ curPositionedDiv = htmlEditor->CreateNode(nsGkAtoms::div, curParent,
+ offset);
+ NS_ENSURE_STATE(curPositionedDiv);
+ // Remember our new block for postprocessing
+ mNewBlock = curPositionedDiv;
+ // curPositionedDiv is now the correct thing to put curNode in
+ }
+
+ // Tuck the node into the end of the active blockquote
+ rv = htmlEditor->MoveNode(curNode, curPositionedDiv, -1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Forget curList, if any
+ curList = nullptr;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+HTMLEditRules::DidAbsolutePosition()
+{
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsCOMPtr<nsIHTMLAbsPosEditor> absPosHTMLEditor = mHTMLEditor;
+ nsCOMPtr<nsIDOMElement> elt =
+ static_cast<nsIDOMElement*>(GetAsDOMNode(mNewBlock));
+ return absPosHTMLEditor->AbsolutelyPositionElement(elt, true);
+}
+
+nsresult
+HTMLEditRules::WillRemoveAbsolutePosition(Selection* aSelection,
+ bool* aCancel,
+ bool* aHandled) {
+ if (!aSelection || !aCancel || !aHandled) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ WillInsert(*aSelection, aCancel);
+
+ // initialize out param
+ // we want to ignore aCancel from WillInsert()
+ *aCancel = false;
+ *aHandled = true;
+
+ nsCOMPtr<nsIDOMElement> elt;
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv =
+ mHTMLEditor->GetAbsolutelyPositionedSelectionContainer(getter_AddRefs(elt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ AutoSelectionRestorer selectionRestorer(aSelection, mHTMLEditor);
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsCOMPtr<nsIHTMLAbsPosEditor> absPosHTMLEditor = mHTMLEditor;
+ return absPosHTMLEditor->AbsolutelyPositionElement(elt, false);
+}
+
+nsresult
+HTMLEditRules::WillRelativeChangeZIndex(Selection* aSelection,
+ int32_t aChange,
+ bool* aCancel,
+ bool* aHandled)
+{
+ if (!aSelection || !aCancel || !aHandled) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ WillInsert(*aSelection, aCancel);
+
+ // initialize out param
+ // we want to ignore aCancel from WillInsert()
+ *aCancel = false;
+ *aHandled = true;
+
+ nsCOMPtr<nsIDOMElement> elt;
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsresult rv =
+ mHTMLEditor->GetAbsolutelyPositionedSelectionContainer(getter_AddRefs(elt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ AutoSelectionRestorer selectionRestorer(aSelection, mHTMLEditor);
+
+ NS_ENSURE_STATE(mHTMLEditor);
+ nsCOMPtr<nsIHTMLAbsPosEditor> absPosHTMLEditor = mHTMLEditor;
+ int32_t zIndex;
+ return absPosHTMLEditor->RelativeChangeElementZIndex(elt, aChange, &zIndex);
+}
+
+NS_IMETHODIMP
+HTMLEditRules::DocumentModified()
+{
+ nsContentUtils::AddScriptRunner(
+ NewRunnableMethod(this, &HTMLEditRules::DocumentModifiedWorker));
+ return NS_OK;
+}
+
+void
+HTMLEditRules::DocumentModifiedWorker()
+{
+ if (!mHTMLEditor) {
+ return;
+ }
+
+ // DeleteNode below may cause a flush, which could destroy the editor
+ nsAutoScriptBlockerSuppressNodeRemoved scriptBlocker;
+
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+ RefPtr<Selection> selection = htmlEditor->GetSelection();
+ if (!selection) {
+ return;
+ }
+
+ // Delete our bogus node, if we have one, since the document might not be
+ // empty any more.
+ if (mBogusNode) {
+ mTextEditor->DeleteNode(mBogusNode);
+ mBogusNode = nullptr;
+ }
+
+ // Try to recreate the bogus node if needed.
+ CreateBogusNodeIfNeeded(selection);
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/HTMLEditRules.h b/editor/libeditor/HTMLEditRules.h
new file mode 100644
index 000000000..40c5e2afd
--- /dev/null
+++ b/editor/libeditor/HTMLEditRules.h
@@ -0,0 +1,377 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef HTMLEditRules_h
+#define HTMLEditRules_h
+
+#include "TypeInState.h"
+#include "mozilla/SelectionState.h"
+#include "mozilla/TextEditRules.h"
+#include "nsCOMPtr.h"
+#include "nsIEditActionListener.h"
+#include "nsIEditor.h"
+#include "nsIHTMLEditor.h"
+#include "nsISupportsImpl.h"
+#include "nsTArray.h"
+#include "nscore.h"
+
+class nsIAtom;
+class nsIDOMCharacterData;
+class nsIDOMDocument;
+class nsIDOMElement;
+class nsIDOMNode;
+class nsIEditor;
+class nsINode;
+class nsRange;
+
+namespace mozilla {
+
+class HTMLEditor;
+class RulesInfo;
+class TextEditor;
+struct EditorDOMPoint;
+namespace dom {
+class Element;
+class Selection;
+} // namespace dom
+
+struct StyleCache final : public PropItem
+{
+ bool mPresent;
+
+ StyleCache()
+ : PropItem()
+ , mPresent(false)
+ {
+ MOZ_COUNT_CTOR(StyleCache);
+ }
+
+ StyleCache(nsIAtom* aTag,
+ const nsAString& aAttr,
+ const nsAString& aValue)
+ : PropItem(aTag, aAttr, aValue)
+ , mPresent(false)
+ {
+ MOZ_COUNT_CTOR(StyleCache);
+ }
+
+ ~StyleCache()
+ {
+ MOZ_COUNT_DTOR(StyleCache);
+ }
+};
+
+#define SIZE_STYLE_TABLE 19
+
+class HTMLEditRules : public TextEditRules
+ , public nsIEditActionListener
+{
+public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLEditRules, TextEditRules)
+
+ HTMLEditRules();
+
+ // nsIEditRules methods
+ NS_IMETHOD Init(TextEditor* aTextEditor) override;
+ NS_IMETHOD DetachEditor() override;
+ NS_IMETHOD BeforeEdit(EditAction action,
+ nsIEditor::EDirection aDirection) override;
+ NS_IMETHOD AfterEdit(EditAction action,
+ nsIEditor::EDirection aDirection) override;
+ NS_IMETHOD WillDoAction(Selection* aSelection, RulesInfo* aInfo,
+ bool* aCancel, bool* aHandled) override;
+ NS_IMETHOD DidDoAction(Selection* aSelection, RulesInfo* aInfo,
+ nsresult aResult) override;
+ NS_IMETHOD DocumentModified() override;
+
+ nsresult GetListState(bool* aMixed, bool* aOL, bool* aUL, bool* aDL);
+ nsresult GetListItemState(bool* aMixed, bool* aLI, bool* aDT, bool* aDD);
+ nsresult GetIndentState(bool* aCanIndent, bool* aCanOutdent);
+ nsresult GetAlignment(bool* aMixed, nsIHTMLEditor::EAlignment* aAlign);
+ nsresult GetParagraphState(bool* aMixed, nsAString& outFormat);
+ nsresult MakeSureElemStartsOrEndsOnCR(nsIDOMNode* aNode);
+
+ // nsIEditActionListener methods
+
+ NS_IMETHOD WillCreateNode(const nsAString& aTag, nsIDOMNode* aParent,
+ int32_t aPosition) override;
+ NS_IMETHOD DidCreateNode(const nsAString& aTag, nsIDOMNode* aNode,
+ nsIDOMNode* aParent, int32_t aPosition,
+ nsresult aResult) override;
+ NS_IMETHOD WillInsertNode(nsIDOMNode* aNode, nsIDOMNode* aParent,
+ int32_t aPosition) override;
+ NS_IMETHOD DidInsertNode(nsIDOMNode* aNode, nsIDOMNode* aParent,
+ int32_t aPosition, nsresult aResult) override;
+ NS_IMETHOD WillDeleteNode(nsIDOMNode* aChild) override;
+ NS_IMETHOD DidDeleteNode(nsIDOMNode* aChild, nsresult aResult) override;
+ NS_IMETHOD WillSplitNode(nsIDOMNode* aExistingRightNode,
+ int32_t aOffset) override;
+ NS_IMETHOD DidSplitNode(nsIDOMNode* aExistingRightNode, int32_t aOffset,
+ nsIDOMNode* aNewLeftNode, nsresult aResult) override;
+ NS_IMETHOD WillJoinNodes(nsIDOMNode* aLeftNode, nsIDOMNode* aRightNode,
+ nsIDOMNode* aParent) override;
+ NS_IMETHOD DidJoinNodes(nsIDOMNode* aLeftNode, nsIDOMNode* aRightNode,
+ nsIDOMNode* aParent, nsresult aResult) override;
+ NS_IMETHOD WillInsertText(nsIDOMCharacterData* aTextNode, int32_t aOffset,
+ const nsAString &aString) override;
+ NS_IMETHOD DidInsertText(nsIDOMCharacterData* aTextNode, int32_t aOffset,
+ const nsAString &aString, nsresult aResult) override;
+ NS_IMETHOD WillDeleteText(nsIDOMCharacterData* aTextNode, int32_t aOffset,
+ int32_t aLength) override;
+ NS_IMETHOD DidDeleteText(nsIDOMCharacterData* aTextNode, int32_t aOffset,
+ int32_t aLength, nsresult aResult) override;
+ NS_IMETHOD WillDeleteSelection(nsISelection* aSelection) override;
+ NS_IMETHOD DidDeleteSelection(nsISelection* aSelection) override;
+ void DeleteNodeIfCollapsedText(nsINode& aNode);
+
+protected:
+ virtual ~HTMLEditRules();
+
+ enum RulesEndpoint
+ {
+ kStart,
+ kEnd
+ };
+
+ void InitFields();
+
+ void WillInsert(Selection& aSelection, bool* aCancel);
+ nsresult WillInsertText(EditAction aAction,
+ Selection* aSelection,
+ bool* aCancel,
+ bool* aHandled,
+ const nsAString* inString,
+ nsAString* outString,
+ int32_t aMaxLength);
+ nsresult WillLoadHTML(Selection* aSelection, bool* aCancel);
+ nsresult WillInsertBreak(Selection& aSelection, bool* aCancel,
+ bool* aHandled);
+ nsresult StandardBreakImpl(nsINode& aNode, int32_t aOffset,
+ Selection& aSelection);
+ nsresult DidInsertBreak(Selection* aSelection, nsresult aResult);
+ nsresult SplitMailCites(Selection* aSelection, bool* aHandled);
+ nsresult WillDeleteSelection(Selection* aSelection,
+ nsIEditor::EDirection aAction,
+ nsIEditor::EStripWrappers aStripWrappers,
+ bool* aCancel, bool* aHandled);
+ nsresult DidDeleteSelection(Selection* aSelection,
+ nsIEditor::EDirection aDir,
+ nsresult aResult);
+ nsresult InsertBRIfNeeded(Selection* aSelection);
+ mozilla::EditorDOMPoint GetGoodSelPointForNode(nsINode& aNode,
+ nsIEditor::EDirection aAction);
+ nsresult JoinBlocks(nsIContent& aLeftNode, nsIContent& aRightNode,
+ bool* aCanceled);
+ nsresult MoveBlock(Element& aLeftBlock, Element& aRightBlock,
+ int32_t aLeftOffset, int32_t aRightOffset);
+ nsresult MoveNodeSmart(nsIContent& aNode, Element& aDestElement,
+ int32_t* aOffset);
+ nsresult MoveContents(Element& aElement, Element& aDestElement,
+ int32_t* aOffset);
+ nsresult DeleteNonTableElements(nsINode* aNode);
+ nsresult WillMakeList(Selection* aSelection,
+ const nsAString* aListType,
+ bool aEntireList,
+ const nsAString* aBulletType,
+ bool* aCancel, bool* aHandled,
+ const nsAString* aItemType = nullptr);
+ nsresult WillRemoveList(Selection* aSelection, bool aOrdered, bool* aCancel,
+ bool* aHandled);
+ nsresult WillIndent(Selection* aSelection, bool* aCancel, bool* aHandled);
+ nsresult WillCSSIndent(Selection* aSelection, bool* aCancel, bool* aHandled);
+ nsresult WillHTMLIndent(Selection* aSelection, bool* aCancel,
+ bool* aHandled);
+ nsresult WillOutdent(Selection& aSelection, bool* aCancel, bool* aHandled);
+ nsresult WillAlign(Selection& aSelection, const nsAString& aAlignType,
+ bool* aCancel, bool* aHandled);
+ nsresult WillAbsolutePosition(Selection& aSelection, bool* aCancel,
+ bool* aHandled);
+ nsresult WillRemoveAbsolutePosition(Selection* aSelection, bool* aCancel,
+ bool* aHandled);
+ nsresult WillRelativeChangeZIndex(Selection* aSelection, int32_t aChange,
+ bool* aCancel, bool* aHandled);
+ nsresult WillMakeDefListItem(Selection* aSelection,
+ const nsAString* aBlockType, bool aEntireList,
+ bool* aCancel, bool* aHandled);
+ nsresult WillMakeBasicBlock(Selection& aSelection,
+ const nsAString& aBlockType,
+ bool* aCancel, bool* aHandled);
+ nsresult DidMakeBasicBlock(Selection* aSelection, RulesInfo* aInfo,
+ nsresult aResult);
+ nsresult DidAbsolutePosition();
+ nsresult AlignInnerBlocks(nsINode& aNode, const nsAString* alignType);
+ nsresult AlignBlockContents(nsIDOMNode* aNode, const nsAString* alignType);
+ nsresult AppendInnerFormatNodes(nsTArray<OwningNonNull<nsINode>>& aArray,
+ nsINode* aNode);
+ nsresult GetFormatString(nsIDOMNode* aNode, nsAString &outFormat);
+ enum class Lists { no, yes };
+ enum class Tables { no, yes };
+ void GetInnerContent(nsINode& aNode,
+ nsTArray<OwningNonNull<nsINode>>& aOutArrayOfNodes,
+ int32_t* aIndex, Lists aLists = Lists::yes,
+ Tables aTables = Tables::yes);
+ Element* IsInListItem(nsINode* aNode);
+ nsresult ReturnInHeader(Selection& aSelection, Element& aHeader,
+ nsINode& aNode, int32_t aOffset);
+ nsresult ReturnInParagraph(Selection* aSelection, nsIDOMNode* aHeader,
+ nsIDOMNode* aTextNode, int32_t aOffset,
+ bool* aCancel, bool* aHandled);
+ nsresult SplitParagraph(nsIDOMNode* aPara,
+ nsIContent* aBRNode,
+ Selection* aSelection,
+ nsCOMPtr<nsIDOMNode>* aSelNode,
+ int32_t* aOffset);
+ nsresult ReturnInListItem(Selection& aSelection, Element& aHeader,
+ nsINode& aNode, int32_t aOffset);
+ nsresult AfterEditInner(EditAction action,
+ nsIEditor::EDirection aDirection);
+ nsresult RemovePartOfBlock(Element& aBlock, nsIContent& aStartChild,
+ nsIContent& aEndChild);
+ void SplitBlock(Element& aBlock,
+ nsIContent& aStartChild,
+ nsIContent& aEndChild,
+ nsIContent** aOutLeftNode = nullptr,
+ nsIContent** aOutRightNode = nullptr,
+ nsIContent** aOutMiddleNode = nullptr);
+ nsresult OutdentPartOfBlock(Element& aBlock,
+ nsIContent& aStartChild,
+ nsIContent& aEndChild,
+ bool aIsBlockIndentedWithCSS,
+ nsIContent** aOutLeftNode,
+ nsIContent** aOutRightNode);
+
+ nsresult ConvertListType(Element* aList, Element** aOutList,
+ nsIAtom* aListType, nsIAtom* aItemType);
+
+ nsresult CreateStyleForInsertText(Selection& aSelection, nsIDocument& aDoc);
+ enum class MozBRCounts { yes, no };
+ nsresult IsEmptyBlock(Element& aNode, bool* aOutIsEmptyBlock,
+ MozBRCounts aMozBRCounts = MozBRCounts::yes);
+ nsresult CheckForEmptyBlock(nsINode* aStartNode, Element* aBodyNode,
+ Selection* aSelection,
+ nsIEditor::EDirection aAction, bool* aHandled);
+ enum class BRLocation { beforeBlock, blockEnd };
+ Element* CheckForInvisibleBR(Element& aBlock, BRLocation aWhere,
+ int32_t aOffset = 0);
+ nsresult ExpandSelectionForDeletion(Selection& aSelection);
+ bool IsFirstNode(nsIDOMNode* aNode);
+ bool IsLastNode(nsIDOMNode* aNode);
+ nsresult NormalizeSelection(Selection* aSelection);
+ void GetPromotedPoint(RulesEndpoint aWhere, nsIDOMNode* aNode,
+ int32_t aOffset, EditAction actionID,
+ nsCOMPtr<nsIDOMNode>* outNode, int32_t* outOffset);
+ void GetPromotedRanges(Selection& aSelection,
+ nsTArray<RefPtr<nsRange>>& outArrayOfRanges,
+ EditAction inOperationType);
+ void PromoteRange(nsRange& aRange, EditAction inOperationType);
+ enum class TouchContent { no, yes };
+ nsresult GetNodesForOperation(
+ nsTArray<RefPtr<nsRange>>& aArrayOfRanges,
+ nsTArray<OwningNonNull<nsINode>>& aOutArrayOfNodes,
+ EditAction aOperationType,
+ TouchContent aTouchContent = TouchContent::yes);
+ void GetChildNodesForOperation(
+ nsINode& aNode,
+ nsTArray<OwningNonNull<nsINode>>& outArrayOfNodes);
+ nsresult GetNodesFromPoint(EditorDOMPoint aPoint,
+ EditAction aOperation,
+ nsTArray<OwningNonNull<nsINode>>& outArrayOfNodes,
+ TouchContent aTouchContent);
+ nsresult GetNodesFromSelection(
+ Selection& aSelection,
+ EditAction aOperation,
+ nsTArray<OwningNonNull<nsINode>>& outArrayOfNodes,
+ TouchContent aTouchContent = TouchContent::yes);
+ enum class EntireList { no, yes };
+ nsresult GetListActionNodes(
+ nsTArray<OwningNonNull<nsINode>>& aOutArrayOfNodes,
+ EntireList aEntireList,
+ TouchContent aTouchContent = TouchContent::yes);
+ void GetDefinitionListItemTypes(Element* aElement, bool* aDT, bool* aDD);
+ nsresult GetParagraphFormatNodes(
+ nsTArray<OwningNonNull<nsINode>>& outArrayOfNodes,
+ TouchContent aTouchContent = TouchContent::yes);
+ void LookInsideDivBQandList(nsTArray<OwningNonNull<nsINode>>& aNodeArray);
+ nsresult BustUpInlinesAtRangeEndpoints(RangeItem& inRange);
+ nsresult BustUpInlinesAtBRs(
+ nsIContent& aNode,
+ nsTArray<OwningNonNull<nsINode>>& aOutArrayOfNodes);
+ nsIContent* GetHighestInlineParent(nsINode& aNode);
+ void MakeTransitionList(nsTArray<OwningNonNull<nsINode>>& aNodeArray,
+ nsTArray<bool>& aTransitionArray);
+ nsresult RemoveBlockStyle(nsTArray<OwningNonNull<nsINode>>& aNodeArray);
+ nsresult ApplyBlockStyle(nsTArray<OwningNonNull<nsINode>>& aNodeArray,
+ nsIAtom& aBlockTag);
+ nsresult MakeBlockquote(nsTArray<OwningNonNull<nsINode>>& aNodeArray);
+ nsresult SplitAsNeeded(nsIAtom& aTag, OwningNonNull<nsINode>& inOutParent,
+ int32_t& inOutOffset);
+ nsresult SplitAsNeeded(nsIAtom& aTag, nsCOMPtr<nsINode>& inOutParent,
+ int32_t& inOutOffset);
+ nsresult AddTerminatingBR(nsIDOMNode *aBlock);
+ EditorDOMPoint JoinNodesSmart(nsIContent& aNodeLeft,
+ nsIContent& aNodeRight);
+ Element* GetTopEnclosingMailCite(nsINode& aNode);
+ nsresult PopListItem(nsIDOMNode* aListItem, bool* aOutOfList);
+ nsresult RemoveListStructure(Element& aList);
+ nsresult CacheInlineStyles(nsIDOMNode* aNode);
+ nsresult ReapplyCachedStyles();
+ void ClearCachedStyles();
+ void AdjustSpecialBreaks();
+ nsresult AdjustWhitespace(Selection* aSelection);
+ nsresult PinSelectionToNewBlock(Selection* aSelection);
+ void CheckInterlinePosition(Selection& aSelection);
+ nsresult AdjustSelection(Selection* aSelection,
+ nsIEditor::EDirection aAction);
+ nsresult FindNearSelectableNode(nsIDOMNode* aSelNode,
+ int32_t aSelOffset,
+ nsIEditor::EDirection& aDirection,
+ nsCOMPtr<nsIDOMNode>* outSelectableNode);
+ /**
+ * Returns true if aNode1 or aNode2 or both is the descendant of some type of
+ * table element, but their nearest table element ancestors differ. "Table
+ * element" here includes not just <table> but also <td>, <tbody>, <tr>, etc.
+ * The nodes count as being their own descendants for this purpose, so a
+ * table element is its own nearest table element ancestor.
+ */
+ bool InDifferentTableElements(nsIDOMNode* aNode1, nsIDOMNode* aNode2);
+ bool InDifferentTableElements(nsINode* aNode1, nsINode* aNode2);
+ nsresult RemoveEmptyNodes();
+ nsresult SelectionEndpointInNode(nsINode* aNode, bool* aResult);
+ nsresult UpdateDocChangeRange(nsRange* aRange);
+ nsresult ConfirmSelectionInBody();
+ nsresult InsertMozBRIfNeeded(nsINode& aNode);
+ bool IsEmptyInline(nsINode& aNode);
+ bool ListIsEmptyLine(nsTArray<OwningNonNull<nsINode>>& arrayOfNodes);
+ nsresult RemoveAlignment(nsIDOMNode* aNode, const nsAString& aAlignType,
+ bool aChildrenOnly);
+ nsresult MakeSureElemStartsOrEndsOnCR(nsIDOMNode* aNode, bool aStarts);
+ enum class ContentsOnly { no, yes };
+ nsresult AlignBlock(Element& aElement,
+ const nsAString& aAlignType, ContentsOnly aContentsOnly);
+ enum class Change { minus, plus };
+ nsresult ChangeIndentation(Element& aElement, Change aChange);
+ void DocumentModifiedWorker();
+
+protected:
+ HTMLEditor* mHTMLEditor;
+ RefPtr<nsRange> mDocChangeRange;
+ bool mListenerEnabled;
+ bool mReturnInEmptyLIKillsList;
+ bool mDidDeleteSelection;
+ bool mDidRangedDelete;
+ bool mRestoreContentEditableCount;
+ RefPtr<nsRange> mUtilRange;
+ // Need to remember an int across willJoin/didJoin...
+ uint32_t mJoinOffset;
+ nsCOMPtr<Element> mNewBlock;
+ RefPtr<RangeItem> mRangeItem;
+ StyleCache mCachedStyles[SIZE_STYLE_TABLE];
+};
+
+} // namespace mozilla
+
+#endif // #ifndef HTMLEditRules_h
+
diff --git a/editor/libeditor/HTMLEditUtils.cpp b/editor/libeditor/HTMLEditUtils.cpp
new file mode 100644
index 000000000..a701c06ec
--- /dev/null
+++ b/editor/libeditor/HTMLEditUtils.cpp
@@ -0,0 +1,842 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HTMLEditUtils.h"
+
+#include "TextEditUtils.h" // for TextEditUtils
+#include "mozilla/ArrayUtils.h" // for ArrayLength
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc.
+#include "mozilla/EditorBase.h" // for EditorBase
+#include "mozilla/dom/Element.h" // for Element, nsINode
+#include "nsAString.h" // for nsAString_internal::IsEmpty
+#include "nsCOMPtr.h" // for nsCOMPtr, operator==, etc.
+#include "nsCaseTreatment.h"
+#include "nsDebug.h" // for NS_PRECONDITION, etc.
+#include "nsError.h" // for NS_SUCCEEDED
+#include "nsGkAtoms.h" // for nsGkAtoms, nsGkAtoms::a, etc.
+#include "nsHTMLTags.h"
+#include "nsIAtom.h" // for nsIAtom
+#include "nsIDOMHTMLAnchorElement.h" // for nsIDOMHTMLAnchorElement
+#include "nsIDOMNode.h" // for nsIDOMNode
+#include "nsNameSpaceManager.h" // for kNameSpaceID_None
+#include "nsLiteralString.h" // for NS_LITERAL_STRING
+#include "nsString.h" // for nsAutoString
+
+namespace mozilla {
+
+/**
+ * IsInlineStyle() returns true if aNode is an inline style.
+ */
+bool
+HTMLEditUtils::IsInlineStyle(nsIDOMNode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ return node && IsInlineStyle(node);
+}
+
+bool
+HTMLEditUtils::IsInlineStyle(nsINode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ return aNode->IsAnyOfHTMLElements(nsGkAtoms::b,
+ nsGkAtoms::i,
+ nsGkAtoms::u,
+ nsGkAtoms::tt,
+ nsGkAtoms::s,
+ nsGkAtoms::strike,
+ nsGkAtoms::big,
+ nsGkAtoms::small,
+ nsGkAtoms::sub,
+ nsGkAtoms::sup,
+ nsGkAtoms::font);
+}
+
+/**
+ * IsFormatNode() returns true if aNode is a format node.
+ */
+bool
+HTMLEditUtils::IsFormatNode(nsIDOMNode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ return node && IsFormatNode(node);
+}
+
+bool
+HTMLEditUtils::IsFormatNode(nsINode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ return aNode->IsAnyOfHTMLElements(nsGkAtoms::p,
+ nsGkAtoms::pre,
+ nsGkAtoms::h1,
+ nsGkAtoms::h2,
+ nsGkAtoms::h3,
+ nsGkAtoms::h4,
+ nsGkAtoms::h5,
+ nsGkAtoms::h6,
+ nsGkAtoms::address);
+}
+
+/**
+ * IsNodeThatCanOutdent() returns true if aNode is a list, list item or
+ * blockquote.
+ */
+bool
+HTMLEditUtils::IsNodeThatCanOutdent(nsIDOMNode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ nsCOMPtr<nsIAtom> nodeAtom = EditorBase::GetTag(aNode);
+ return (nodeAtom == nsGkAtoms::ul)
+ || (nodeAtom == nsGkAtoms::ol)
+ || (nodeAtom == nsGkAtoms::dl)
+ || (nodeAtom == nsGkAtoms::li)
+ || (nodeAtom == nsGkAtoms::dd)
+ || (nodeAtom == nsGkAtoms::dt)
+ || (nodeAtom == nsGkAtoms::blockquote);
+}
+
+/**
+ * IsHeader() returns true if aNode is an html header.
+ */
+bool
+HTMLEditUtils::IsHeader(nsINode& aNode)
+{
+ return aNode.IsAnyOfHTMLElements(nsGkAtoms::h1,
+ nsGkAtoms::h2,
+ nsGkAtoms::h3,
+ nsGkAtoms::h4,
+ nsGkAtoms::h5,
+ nsGkAtoms::h6);
+}
+
+bool
+HTMLEditUtils::IsHeader(nsIDOMNode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ MOZ_ASSERT(node);
+ return IsHeader(*node);
+}
+
+/**
+ * IsParagraph() returns true if aNode is an html paragraph.
+ */
+bool
+HTMLEditUtils::IsParagraph(nsIDOMNode* aNode)
+{
+ return EditorBase::NodeIsType(aNode, nsGkAtoms::p);
+}
+
+/**
+ * IsHR() returns true if aNode is an horizontal rule.
+ */
+bool
+HTMLEditUtils::IsHR(nsIDOMNode* aNode)
+{
+ return EditorBase::NodeIsType(aNode, nsGkAtoms::hr);
+}
+
+/**
+ * IsListItem() returns true if aNode is an html list item.
+ */
+bool
+HTMLEditUtils::IsListItem(nsIDOMNode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ return node && IsListItem(node);
+}
+
+bool
+HTMLEditUtils::IsListItem(nsINode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ return aNode->IsAnyOfHTMLElements(nsGkAtoms::li,
+ nsGkAtoms::dd,
+ nsGkAtoms::dt);
+}
+
+/**
+ * IsTableElement() returns true if aNode is an html table, td, tr, ...
+ */
+bool
+HTMLEditUtils::IsTableElement(nsIDOMNode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ return node && IsTableElement(node);
+}
+
+bool
+HTMLEditUtils::IsTableElement(nsINode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ return aNode->IsAnyOfHTMLElements(nsGkAtoms::table,
+ nsGkAtoms::tr,
+ nsGkAtoms::td,
+ nsGkAtoms::th,
+ nsGkAtoms::thead,
+ nsGkAtoms::tfoot,
+ nsGkAtoms::tbody,
+ nsGkAtoms::caption);
+}
+
+/**
+ * IsTableElementButNotTable() returns true if aNode is an html td, tr, ...
+ * (doesn't include table)
+ */
+bool
+HTMLEditUtils::IsTableElementButNotTable(nsIDOMNode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ return node && IsTableElementButNotTable(node);
+}
+
+bool
+HTMLEditUtils::IsTableElementButNotTable(nsINode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ return aNode->IsAnyOfHTMLElements(nsGkAtoms::tr,
+ nsGkAtoms::td,
+ nsGkAtoms::th,
+ nsGkAtoms::thead,
+ nsGkAtoms::tfoot,
+ nsGkAtoms::tbody,
+ nsGkAtoms::caption);
+}
+
+/**
+ * IsTable() returns true if aNode is an html table.
+ */
+bool
+HTMLEditUtils::IsTable(nsIDOMNode* aNode)
+{
+ return EditorBase::NodeIsType(aNode, nsGkAtoms::table);
+}
+
+bool
+HTMLEditUtils::IsTable(nsINode* aNode)
+{
+ return aNode && aNode->IsHTMLElement(nsGkAtoms::table);
+}
+
+/**
+ * IsTableRow() returns true if aNode is an html tr.
+ */
+bool
+HTMLEditUtils::IsTableRow(nsIDOMNode* aNode)
+{
+ return EditorBase::NodeIsType(aNode, nsGkAtoms::tr);
+}
+
+/**
+ * IsTableCell() returns true if aNode is an html td or th.
+ */
+bool
+HTMLEditUtils::IsTableCell(nsIDOMNode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ return node && IsTableCell(node);
+}
+
+bool
+HTMLEditUtils::IsTableCell(nsINode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ return aNode->IsAnyOfHTMLElements(nsGkAtoms::td, nsGkAtoms::th);
+}
+
+/**
+ * IsTableCellOrCaption() returns true if aNode is an html td or th or caption.
+ */
+bool
+HTMLEditUtils::IsTableCellOrCaption(nsINode& aNode)
+{
+ return aNode.IsAnyOfHTMLElements(nsGkAtoms::td, nsGkAtoms::th,
+ nsGkAtoms::caption);
+}
+
+/**
+ * IsList() returns true if aNode is an html list.
+ */
+bool
+HTMLEditUtils::IsList(nsIDOMNode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ return node && IsList(node);
+}
+
+bool
+HTMLEditUtils::IsList(nsINode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ return aNode->IsAnyOfHTMLElements(nsGkAtoms::ul,
+ nsGkAtoms::ol,
+ nsGkAtoms::dl);
+}
+
+/**
+ * IsOrderedList() returns true if aNode is an html ordered list.
+ */
+bool
+HTMLEditUtils::IsOrderedList(nsIDOMNode* aNode)
+{
+ return EditorBase::NodeIsType(aNode, nsGkAtoms::ol);
+}
+
+
+/**
+ * IsUnorderedList() returns true if aNode is an html unordered list.
+ */
+bool
+HTMLEditUtils::IsUnorderedList(nsIDOMNode* aNode)
+{
+ return EditorBase::NodeIsType(aNode, nsGkAtoms::ul);
+}
+
+/**
+ * IsBlockquote() returns true if aNode is an html blockquote node.
+ */
+bool
+HTMLEditUtils::IsBlockquote(nsIDOMNode* aNode)
+{
+ return EditorBase::NodeIsType(aNode, nsGkAtoms::blockquote);
+}
+
+/**
+ * IsPre() returns true if aNode is an html pre node.
+ */
+bool
+HTMLEditUtils::IsPre(nsIDOMNode* aNode)
+{
+ return EditorBase::NodeIsType(aNode, nsGkAtoms::pre);
+}
+
+/**
+ * IsImage() returns true if aNode is an html image node.
+ */
+bool
+HTMLEditUtils::IsImage(nsINode* aNode)
+{
+ return aNode && aNode->IsHTMLElement(nsGkAtoms::img);
+}
+
+bool
+HTMLEditUtils::IsImage(nsIDOMNode* aNode)
+{
+ return EditorBase::NodeIsType(aNode, nsGkAtoms::img);
+}
+
+bool
+HTMLEditUtils::IsLink(nsIDOMNode *aNode)
+{
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ return node && IsLink(node);
+}
+
+bool
+HTMLEditUtils::IsLink(nsINode* aNode)
+{
+ MOZ_ASSERT(aNode);
+
+ nsCOMPtr<nsIDOMHTMLAnchorElement> anchor = do_QueryInterface(aNode);
+ if (anchor) {
+ nsAutoString tmpText;
+ if (NS_SUCCEEDED(anchor->GetHref(tmpText)) && !tmpText.IsEmpty()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool
+HTMLEditUtils::IsNamedAnchor(nsIDOMNode *aNode)
+{
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ return node && IsNamedAnchor(node);
+}
+
+bool
+HTMLEditUtils::IsNamedAnchor(nsINode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ if (!aNode->IsHTMLElement(nsGkAtoms::a)) {
+ return false;
+ }
+
+ nsAutoString text;
+ return aNode->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::name,
+ text) && !text.IsEmpty();
+}
+
+/**
+ * IsDiv() returns true if aNode is an html div node.
+ */
+bool
+HTMLEditUtils::IsDiv(nsIDOMNode* aNode)
+{
+ return EditorBase::NodeIsType(aNode, nsGkAtoms::div);
+}
+
+/**
+ * IsMozDiv() returns true if aNode is an html div node with |type = _moz|.
+ */
+bool
+HTMLEditUtils::IsMozDiv(nsIDOMNode* aNode)
+{
+ return IsDiv(aNode) && TextEditUtils::HasMozAttr(aNode);
+}
+
+bool
+HTMLEditUtils::IsMozDiv(nsINode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ return aNode->IsHTMLElement(nsGkAtoms::div) &&
+ TextEditUtils::HasMozAttr(GetAsDOMNode(aNode));
+}
+
+/**
+ * IsMailCite() returns true if aNode is an html blockquote with |type=cite|.
+ */
+bool
+HTMLEditUtils::IsMailCite(nsIDOMNode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ return node && IsMailCite(node);
+}
+
+bool
+HTMLEditUtils::IsMailCite(nsINode* aNode)
+{
+ MOZ_ASSERT(aNode);
+
+ // don't ask me why, but our html mailcites are id'd by "type=cite"...
+ if (aNode->IsElement() &&
+ aNode->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+ NS_LITERAL_STRING("cite"),
+ eIgnoreCase)) {
+ return true;
+ }
+
+ // ... but our plaintext mailcites by "_moz_quote=true". go figure.
+ if (aNode->IsElement() &&
+ aNode->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::mozquote,
+ NS_LITERAL_STRING("true"),
+ eIgnoreCase)) {
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * IsFormWidget() returns true if aNode is a form widget of some kind.
+ */
+bool
+HTMLEditUtils::IsFormWidget(nsIDOMNode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ return node && IsFormWidget(node);
+}
+
+bool
+HTMLEditUtils::IsFormWidget(nsINode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ return aNode->IsAnyOfHTMLElements(nsGkAtoms::textarea,
+ nsGkAtoms::select,
+ nsGkAtoms::button,
+ nsGkAtoms::output,
+ nsGkAtoms::keygen,
+ nsGkAtoms::progress,
+ nsGkAtoms::meter,
+ nsGkAtoms::input);
+}
+
+bool
+HTMLEditUtils::SupportsAlignAttr(nsIDOMNode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ nsCOMPtr<nsIAtom> nodeAtom = EditorBase::GetTag(aNode);
+ return (nodeAtom == nsGkAtoms::hr)
+ || (nodeAtom == nsGkAtoms::table)
+ || (nodeAtom == nsGkAtoms::tbody)
+ || (nodeAtom == nsGkAtoms::tfoot)
+ || (nodeAtom == nsGkAtoms::thead)
+ || (nodeAtom == nsGkAtoms::tr)
+ || (nodeAtom == nsGkAtoms::td)
+ || (nodeAtom == nsGkAtoms::th)
+ || (nodeAtom == nsGkAtoms::div)
+ || (nodeAtom == nsGkAtoms::p)
+ || (nodeAtom == nsGkAtoms::h1)
+ || (nodeAtom == nsGkAtoms::h2)
+ || (nodeAtom == nsGkAtoms::h3)
+ || (nodeAtom == nsGkAtoms::h4)
+ || (nodeAtom == nsGkAtoms::h5)
+ || (nodeAtom == nsGkAtoms::h6);
+}
+
+// We use bitmasks to test containment of elements. Elements are marked to be
+// in certain groups by setting the mGroup member of the nsElementInfo struct
+// to the corresponding GROUP_ values (OR'ed together). Similarly, elements are
+// marked to allow containment of certain groups by setting the
+// mCanContainGroups member of the nsElementInfo struct to the corresponding
+// GROUP_ values (OR'ed together).
+// Testing containment then simply consists of checking whether the
+// mCanContainGroups bitmask of an element and the mGroup bitmask of a
+// potential child overlap.
+
+#define GROUP_NONE 0
+
+// body, head, html
+#define GROUP_TOPLEVEL (1 << 1)
+
+// base, link, meta, script, style, title
+#define GROUP_HEAD_CONTENT (1 << 2)
+
+// b, big, i, s, small, strike, tt, u
+#define GROUP_FONTSTYLE (1 << 3)
+
+// abbr, acronym, cite, code, datalist, del, dfn, em, ins, kbd, mark, rb, rp
+// rt, rtc, ruby, samp, strong, var
+#define GROUP_PHRASE (1 << 4)
+
+// a, applet, basefont, bdo, br, font, iframe, img, map, meter, object, output,
+// picture, progress, q, script, span, sub, sup
+#define GROUP_SPECIAL (1 << 5)
+
+// button, form, input, label, select, textarea
+#define GROUP_FORMCONTROL (1 << 6)
+
+// address, applet, article, aside, blockquote, button, center, del, details,
+// dir, div, dl, fieldset, figure, footer, form, h1, h2, h3, h4, h5, h6, header,
+// hgroup, hr, iframe, ins, main, map, menu, nav, noframes, noscript, object,
+// ol, p, pre, table, section, summary, ul
+#define GROUP_BLOCK (1 << 7)
+
+// frame, frameset
+#define GROUP_FRAME (1 << 8)
+
+// col, tbody
+#define GROUP_TABLE_CONTENT (1 << 9)
+
+// tr
+#define GROUP_TBODY_CONTENT (1 << 10)
+
+// td, th
+#define GROUP_TR_CONTENT (1 << 11)
+
+// col
+#define GROUP_COLGROUP_CONTENT (1 << 12)
+
+// param
+#define GROUP_OBJECT_CONTENT (1 << 13)
+
+// li
+#define GROUP_LI (1 << 14)
+
+// area
+#define GROUP_MAP_CONTENT (1 << 15)
+
+// optgroup, option
+#define GROUP_SELECT_CONTENT (1 << 16)
+
+// option
+#define GROUP_OPTIONS (1 << 17)
+
+// dd, dt
+#define GROUP_DL_CONTENT (1 << 18)
+
+// p
+#define GROUP_P (1 << 19)
+
+// text, whitespace, newline, comment
+#define GROUP_LEAF (1 << 20)
+
+// XXX This is because the editor does sublists illegally.
+// ol, ul
+#define GROUP_OL_UL (1 << 21)
+
+// h1, h2, h3, h4, h5, h6
+#define GROUP_HEADING (1 << 22)
+
+// figcaption
+#define GROUP_FIGCAPTION (1 << 23)
+
+// picture members (img, source)
+#define GROUP_PICTURE_CONTENT (1 << 24)
+
+#define GROUP_INLINE_ELEMENT \
+ (GROUP_FONTSTYLE | GROUP_PHRASE | GROUP_SPECIAL | GROUP_FORMCONTROL | \
+ GROUP_LEAF)
+
+#define GROUP_FLOW_ELEMENT (GROUP_INLINE_ELEMENT | GROUP_BLOCK)
+
+struct ElementInfo final
+{
+#ifdef DEBUG
+ eHTMLTags mTag;
+#endif
+ uint32_t mGroup;
+ uint32_t mCanContainGroups;
+ bool mIsContainer;
+ bool mCanContainSelf;
+};
+
+#ifdef DEBUG
+#define ELEM(_tag, _isContainer, _canContainSelf, _group, _canContainGroups) \
+ { eHTMLTag_##_tag, _group, _canContainGroups, _isContainer, _canContainSelf }
+#else
+#define ELEM(_tag, _isContainer, _canContainSelf, _group, _canContainGroups) \
+ { _group, _canContainGroups, _isContainer, _canContainSelf }
+#endif
+
+static const ElementInfo kElements[eHTMLTag_userdefined] = {
+ ELEM(a, true, false, GROUP_SPECIAL, GROUP_INLINE_ELEMENT),
+ ELEM(abbr, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(acronym, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(address, true, true, GROUP_BLOCK,
+ GROUP_INLINE_ELEMENT | GROUP_P),
+ ELEM(applet, true, true, GROUP_SPECIAL | GROUP_BLOCK,
+ GROUP_FLOW_ELEMENT | GROUP_OBJECT_CONTENT),
+ ELEM(area, false, false, GROUP_MAP_CONTENT, GROUP_NONE),
+ ELEM(article, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(aside, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(audio, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(b, true, true, GROUP_FONTSTYLE, GROUP_INLINE_ELEMENT),
+ ELEM(base, false, false, GROUP_HEAD_CONTENT, GROUP_NONE),
+ ELEM(basefont, false, false, GROUP_SPECIAL, GROUP_NONE),
+ ELEM(bdo, true, true, GROUP_SPECIAL, GROUP_INLINE_ELEMENT),
+ ELEM(bgsound, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(big, true, true, GROUP_FONTSTYLE, GROUP_INLINE_ELEMENT),
+ ELEM(blockquote, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(body, true, true, GROUP_TOPLEVEL, GROUP_FLOW_ELEMENT),
+ ELEM(br, false, false, GROUP_SPECIAL, GROUP_NONE),
+ ELEM(button, true, true, GROUP_FORMCONTROL | GROUP_BLOCK,
+ GROUP_FLOW_ELEMENT),
+ ELEM(canvas, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(caption, true, true, GROUP_NONE, GROUP_INLINE_ELEMENT),
+ ELEM(center, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(cite, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(code, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(col, false, false, GROUP_TABLE_CONTENT | GROUP_COLGROUP_CONTENT,
+ GROUP_NONE),
+ ELEM(colgroup, true, false, GROUP_NONE, GROUP_COLGROUP_CONTENT),
+ ELEM(content, true, false, GROUP_NONE, GROUP_INLINE_ELEMENT),
+ ELEM(data, true, false, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(datalist, true, false, GROUP_PHRASE,
+ GROUP_OPTIONS | GROUP_INLINE_ELEMENT),
+ ELEM(dd, true, false, GROUP_DL_CONTENT, GROUP_FLOW_ELEMENT),
+ ELEM(del, true, true, GROUP_PHRASE | GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(details, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(dfn, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(dir, true, false, GROUP_BLOCK, GROUP_LI),
+ ELEM(div, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(dl, true, false, GROUP_BLOCK, GROUP_DL_CONTENT),
+ ELEM(dt, true, true, GROUP_DL_CONTENT, GROUP_INLINE_ELEMENT),
+ ELEM(em, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(embed, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(fieldset, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(figcaption, true, false, GROUP_FIGCAPTION, GROUP_FLOW_ELEMENT),
+ ELEM(figure, true, true, GROUP_BLOCK,
+ GROUP_FLOW_ELEMENT | GROUP_FIGCAPTION),
+ ELEM(font, true, true, GROUP_SPECIAL, GROUP_INLINE_ELEMENT),
+ ELEM(footer, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(form, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(frame, false, false, GROUP_FRAME, GROUP_NONE),
+ ELEM(frameset, true, true, GROUP_FRAME, GROUP_FRAME),
+ ELEM(h1, true, false, GROUP_BLOCK | GROUP_HEADING,
+ GROUP_INLINE_ELEMENT),
+ ELEM(h2, true, false, GROUP_BLOCK | GROUP_HEADING,
+ GROUP_INLINE_ELEMENT),
+ ELEM(h3, true, false, GROUP_BLOCK | GROUP_HEADING,
+ GROUP_INLINE_ELEMENT),
+ ELEM(h4, true, false, GROUP_BLOCK | GROUP_HEADING,
+ GROUP_INLINE_ELEMENT),
+ ELEM(h5, true, false, GROUP_BLOCK | GROUP_HEADING,
+ GROUP_INLINE_ELEMENT),
+ ELEM(h6, true, false, GROUP_BLOCK | GROUP_HEADING,
+ GROUP_INLINE_ELEMENT),
+ ELEM(head, true, false, GROUP_TOPLEVEL, GROUP_HEAD_CONTENT),
+ ELEM(header, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(hgroup, true, false, GROUP_BLOCK, GROUP_HEADING),
+ ELEM(hr, false, false, GROUP_BLOCK, GROUP_NONE),
+ ELEM(html, true, false, GROUP_TOPLEVEL, GROUP_TOPLEVEL),
+ ELEM(i, true, true, GROUP_FONTSTYLE, GROUP_INLINE_ELEMENT),
+ ELEM(iframe, true, true, GROUP_SPECIAL | GROUP_BLOCK,
+ GROUP_FLOW_ELEMENT),
+ ELEM(image, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(img, false, false, GROUP_SPECIAL | GROUP_PICTURE_CONTENT, GROUP_NONE),
+ ELEM(input, false, false, GROUP_FORMCONTROL, GROUP_NONE),
+ ELEM(ins, true, true, GROUP_PHRASE | GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(kbd, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(keygen, false, false, GROUP_FORMCONTROL, GROUP_NONE),
+ ELEM(label, true, false, GROUP_FORMCONTROL, GROUP_INLINE_ELEMENT),
+ ELEM(legend, true, true, GROUP_NONE, GROUP_INLINE_ELEMENT),
+ ELEM(li, true, false, GROUP_LI, GROUP_FLOW_ELEMENT),
+ ELEM(link, false, false, GROUP_HEAD_CONTENT, GROUP_NONE),
+ ELEM(listing, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(main, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(map, true, true, GROUP_SPECIAL, GROUP_BLOCK | GROUP_MAP_CONTENT),
+ ELEM(mark, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(marquee, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(menu, true, true, GROUP_BLOCK, GROUP_LI | GROUP_FLOW_ELEMENT),
+ ELEM(menuitem, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(meta, false, false, GROUP_HEAD_CONTENT, GROUP_NONE),
+ ELEM(meter, true, false, GROUP_SPECIAL, GROUP_FLOW_ELEMENT),
+ ELEM(multicol, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(nav, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(nobr, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(noembed, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(noframes, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(noscript, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(object, true, true, GROUP_SPECIAL | GROUP_BLOCK,
+ GROUP_FLOW_ELEMENT | GROUP_OBJECT_CONTENT),
+ // XXX Can contain self and ul because editor does sublists illegally.
+ ELEM(ol, true, true, GROUP_BLOCK | GROUP_OL_UL,
+ GROUP_LI | GROUP_OL_UL),
+ ELEM(optgroup, true, false, GROUP_SELECT_CONTENT,
+ GROUP_OPTIONS),
+ ELEM(option, true, false,
+ GROUP_SELECT_CONTENT | GROUP_OPTIONS, GROUP_LEAF),
+ ELEM(output, true, true, GROUP_SPECIAL, GROUP_INLINE_ELEMENT),
+ ELEM(p, true, false, GROUP_BLOCK | GROUP_P, GROUP_INLINE_ELEMENT),
+ ELEM(param, false, false, GROUP_OBJECT_CONTENT, GROUP_NONE),
+ ELEM(picture, true, false, GROUP_SPECIAL, GROUP_PICTURE_CONTENT),
+ ELEM(plaintext, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(pre, true, true, GROUP_BLOCK, GROUP_INLINE_ELEMENT),
+ ELEM(progress, true, false, GROUP_SPECIAL, GROUP_FLOW_ELEMENT),
+ ELEM(q, true, true, GROUP_SPECIAL, GROUP_INLINE_ELEMENT),
+ ELEM(rb, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(rp, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(rt, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(rtc, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(ruby, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(s, true, true, GROUP_FONTSTYLE, GROUP_INLINE_ELEMENT),
+ ELEM(samp, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(script, true, false, GROUP_HEAD_CONTENT | GROUP_SPECIAL,
+ GROUP_LEAF),
+ ELEM(section, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(select, true, false, GROUP_FORMCONTROL, GROUP_SELECT_CONTENT),
+ ELEM(shadow, true, false, GROUP_NONE, GROUP_INLINE_ELEMENT),
+ ELEM(small, true, true, GROUP_FONTSTYLE, GROUP_INLINE_ELEMENT),
+ ELEM(source, false, false, GROUP_PICTURE_CONTENT, GROUP_NONE),
+ ELEM(span, true, true, GROUP_SPECIAL, GROUP_INLINE_ELEMENT),
+ ELEM(strike, true, true, GROUP_FONTSTYLE, GROUP_INLINE_ELEMENT),
+ ELEM(strong, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(style, true, false, GROUP_HEAD_CONTENT, GROUP_LEAF),
+ ELEM(sub, true, true, GROUP_SPECIAL, GROUP_INLINE_ELEMENT),
+ ELEM(summary, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(sup, true, true, GROUP_SPECIAL, GROUP_INLINE_ELEMENT),
+ ELEM(table, true, false, GROUP_BLOCK, GROUP_TABLE_CONTENT),
+ ELEM(tbody, true, false, GROUP_TABLE_CONTENT, GROUP_TBODY_CONTENT),
+ ELEM(td, true, false, GROUP_TR_CONTENT, GROUP_FLOW_ELEMENT),
+ ELEM(textarea, true, false, GROUP_FORMCONTROL, GROUP_LEAF),
+ ELEM(tfoot, true, false, GROUP_NONE, GROUP_TBODY_CONTENT),
+ ELEM(th, true, false, GROUP_TR_CONTENT, GROUP_FLOW_ELEMENT),
+ ELEM(thead, true, false, GROUP_NONE, GROUP_TBODY_CONTENT),
+ ELEM(template, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(time, true, false, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(title, true, false, GROUP_HEAD_CONTENT, GROUP_LEAF),
+ ELEM(tr, true, false, GROUP_TBODY_CONTENT, GROUP_TR_CONTENT),
+ ELEM(track, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(tt, true, true, GROUP_FONTSTYLE, GROUP_INLINE_ELEMENT),
+ ELEM(u, true, true, GROUP_FONTSTYLE, GROUP_INLINE_ELEMENT),
+ // XXX Can contain self and ol because editor does sublists illegally.
+ ELEM(ul, true, true, GROUP_BLOCK | GROUP_OL_UL,
+ GROUP_LI | GROUP_OL_UL),
+ ELEM(var, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(video, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(wbr, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(xmp, false, false, GROUP_NONE, GROUP_NONE),
+
+ // These aren't elements.
+ ELEM(text, false, false, GROUP_LEAF, GROUP_NONE),
+ ELEM(whitespace, false, false, GROUP_LEAF, GROUP_NONE),
+ ELEM(newline, false, false, GROUP_LEAF, GROUP_NONE),
+ ELEM(comment, false, false, GROUP_LEAF, GROUP_NONE),
+ ELEM(entity, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(doctypeDecl, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(markupDecl, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(instruction, false, false, GROUP_NONE, GROUP_NONE),
+
+ ELEM(userdefined, true, false, GROUP_NONE, GROUP_FLOW_ELEMENT)
+};
+
+bool
+HTMLEditUtils::CanContain(int32_t aParent, int32_t aChild)
+{
+ NS_ASSERTION(aParent > eHTMLTag_unknown && aParent <= eHTMLTag_userdefined,
+ "aParent out of range!");
+ NS_ASSERTION(aChild > eHTMLTag_unknown && aChild <= eHTMLTag_userdefined,
+ "aChild out of range!");
+
+#ifdef DEBUG
+ static bool checked = false;
+ if (!checked) {
+ checked = true;
+ int32_t i;
+ for (i = 1; i <= eHTMLTag_userdefined; ++i) {
+ NS_ASSERTION(kElements[i - 1].mTag == i,
+ "You need to update kElements (missing tags).");
+ }
+ }
+#endif
+
+ // Special-case button.
+ if (aParent == eHTMLTag_button) {
+ static const eHTMLTags kButtonExcludeKids[] = {
+ eHTMLTag_a,
+ eHTMLTag_fieldset,
+ eHTMLTag_form,
+ eHTMLTag_iframe,
+ eHTMLTag_input,
+ eHTMLTag_select,
+ eHTMLTag_textarea
+ };
+
+ uint32_t j;
+ for (j = 0; j < ArrayLength(kButtonExcludeKids); ++j) {
+ if (kButtonExcludeKids[j] == aChild) {
+ return false;
+ }
+ }
+ }
+
+ // Deprecated elements.
+ if (aChild == eHTMLTag_bgsound) {
+ return false;
+ }
+
+ // Bug #67007, dont strip userdefined tags.
+ if (aChild == eHTMLTag_userdefined) {
+ return true;
+ }
+
+ const ElementInfo& parent = kElements[aParent - 1];
+ if (aParent == aChild) {
+ return parent.mCanContainSelf;
+ }
+
+ const ElementInfo& child = kElements[aChild - 1];
+ return (parent.mCanContainGroups & child.mGroup) != 0;
+}
+
+bool
+HTMLEditUtils::IsContainer(int32_t aTag)
+{
+ NS_ASSERTION(aTag > eHTMLTag_unknown && aTag <= eHTMLTag_userdefined,
+ "aTag out of range!");
+
+ return kElements[aTag - 1].mIsContainer;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/HTMLEditUtils.h b/editor/libeditor/HTMLEditUtils.h
new file mode 100644
index 000000000..4bbb6fdf3
--- /dev/null
+++ b/editor/libeditor/HTMLEditUtils.h
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef HTMLEditUtils_h
+#define HTMLEditUtils_h
+
+#include <stdint.h>
+
+class nsIDOMNode;
+class nsINode;
+
+namespace mozilla {
+
+class HTMLEditUtils final
+{
+public:
+ // from nsHTMLEditRules:
+ static bool IsInlineStyle(nsINode* aNode);
+ static bool IsInlineStyle(nsIDOMNode *aNode);
+ static bool IsFormatNode(nsINode* aNode);
+ static bool IsFormatNode(nsIDOMNode* aNode);
+ static bool IsNodeThatCanOutdent(nsIDOMNode* aNode);
+ static bool IsHeader(nsINode& aNode);
+ static bool IsHeader(nsIDOMNode* aNode);
+ static bool IsParagraph(nsIDOMNode* aNode);
+ static bool IsHR(nsIDOMNode* aNode);
+ static bool IsListItem(nsINode* aNode);
+ static bool IsListItem(nsIDOMNode* aNode);
+ static bool IsTable(nsIDOMNode* aNode);
+ static bool IsTable(nsINode* aNode);
+ static bool IsTableRow(nsIDOMNode* aNode);
+ static bool IsTableElement(nsINode* aNode);
+ static bool IsTableElement(nsIDOMNode* aNode);
+ static bool IsTableElementButNotTable(nsINode* aNode);
+ static bool IsTableElementButNotTable(nsIDOMNode* aNode);
+ static bool IsTableCell(nsINode* node);
+ static bool IsTableCell(nsIDOMNode* aNode);
+ static bool IsTableCellOrCaption(nsINode& aNode);
+ static bool IsList(nsINode* aNode);
+ static bool IsList(nsIDOMNode* aNode);
+ static bool IsOrderedList(nsIDOMNode* aNode);
+ static bool IsUnorderedList(nsIDOMNode* aNode);
+ static bool IsBlockquote(nsIDOMNode* aNode);
+ static bool IsPre(nsIDOMNode* aNode);
+ static bool IsAnchor(nsIDOMNode* aNode);
+ static bool IsImage(nsINode* aNode);
+ static bool IsImage(nsIDOMNode* aNode);
+ static bool IsLink(nsIDOMNode* aNode);
+ static bool IsLink(nsINode* aNode);
+ static bool IsNamedAnchor(nsINode* aNode);
+ static bool IsNamedAnchor(nsIDOMNode* aNode);
+ static bool IsDiv(nsIDOMNode* aNode);
+ static bool IsMozDiv(nsINode* aNode);
+ static bool IsMozDiv(nsIDOMNode* aNode);
+ static bool IsMailCite(nsINode* aNode);
+ static bool IsMailCite(nsIDOMNode* aNode);
+ static bool IsFormWidget(nsINode* aNode);
+ static bool IsFormWidget(nsIDOMNode* aNode);
+ static bool SupportsAlignAttr(nsIDOMNode* aNode);
+ static bool CanContain(int32_t aParent, int32_t aChild);
+ static bool IsContainer(int32_t aTag);
+};
+
+} // namespace mozilla
+
+#endif // #ifndef HTMLEditUtils_h
+
diff --git a/editor/libeditor/HTMLEditor.cpp b/editor/libeditor/HTMLEditor.cpp
new file mode 100644
index 000000000..dd47ffd3c
--- /dev/null
+++ b/editor/libeditor/HTMLEditor.cpp
@@ -0,0 +1,5289 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/HTMLEditor.h"
+
+#include "mozilla/DebugOnly.h"
+#include "mozilla/EventStates.h"
+#include "mozilla/TextEvents.h"
+
+#include "nsCRT.h"
+
+#include "nsUnicharUtils.h"
+
+#include "HTMLEditorEventListener.h"
+#include "HTMLEditRules.h"
+#include "HTMLEditUtils.h"
+#include "HTMLURIRefObject.h"
+#include "SetDocumentTitleTransaction.h"
+#include "StyleSheetTransactions.h"
+#include "TextEditUtils.h"
+#include "TypeInState.h"
+
+#include "nsIDOMText.h"
+#include "nsIDOMMozNamedAttrMap.h"
+#include "nsIDOMNodeList.h"
+#include "nsIDOMDocument.h"
+#include "nsIDOMAttr.h"
+#include "nsIDocumentInlines.h"
+#include "nsIDOMEventTarget.h"
+#include "nsIDOMKeyEvent.h"
+#include "nsIDOMMouseEvent.h"
+#include "nsIDOMHTMLAnchorElement.h"
+#include "nsISelectionController.h"
+#include "nsIDOMHTMLDocument.h"
+#include "nsILinkHandler.h"
+#include "nsIInlineSpellChecker.h"
+
+#include "mozilla/css/Loader.h"
+#include "nsIDOMStyleSheet.h"
+
+#include "nsIContent.h"
+#include "nsIContentIterator.h"
+#include "nsIMutableArray.h"
+#include "nsContentUtils.h"
+#include "nsIDocumentEncoder.h"
+#include "nsIPresShell.h"
+#include "nsPresContext.h"
+#include "nsFocusManager.h"
+#include "nsPIDOMWindow.h"
+
+// netwerk
+#include "nsIURI.h"
+#include "nsNetUtil.h"
+
+// Misc
+#include "mozilla/EditorUtils.h"
+#include "HTMLEditorObjectResizerUtils.h"
+#include "TextEditorTest.h"
+#include "WSRunObject.h"
+#include "nsGkAtoms.h"
+#include "nsIWidget.h"
+
+#include "nsIFrame.h"
+#include "nsIParserService.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/dom/DocumentFragment.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/EventTarget.h"
+#include "mozilla/dom/HTMLBodyElement.h"
+#include "nsTextFragment.h"
+#include "nsContentList.h"
+#include "mozilla/StyleSheet.h"
+#include "mozilla/StyleSheetInlines.h"
+
+namespace mozilla {
+
+using namespace dom;
+using namespace widget;
+
+// Some utilities to handle overloading of "A" tag for link and named anchor.
+static bool
+IsLinkTag(const nsString& s)
+{
+ return s.EqualsIgnoreCase("href");
+}
+
+static bool
+IsNamedAnchorTag(const nsString& s)
+{
+ return s.EqualsIgnoreCase("anchor") || s.EqualsIgnoreCase("namedanchor");
+}
+
+HTMLEditor::HTMLEditor()
+ : mCRInParagraphCreatesParagraph(false)
+ , mCSSAware(false)
+ , mSelectedCellIndex(0)
+ , mIsObjectResizingEnabled(true)
+ , mIsResizing(false)
+ , mPreserveRatio(false)
+ , mResizedObjectIsAnImage(false)
+ , mIsAbsolutelyPositioningEnabled(true)
+ , mResizedObjectIsAbsolutelyPositioned(false)
+ , mGrabberClicked(false)
+ , mIsMoving(false)
+ , mSnapToGridEnabled(false)
+ , mIsInlineTableEditingEnabled(true)
+ , mOriginalX(0)
+ , mOriginalY(0)
+ , mResizedObjectX(0)
+ , mResizedObjectY(0)
+ , mResizedObjectWidth(0)
+ , mResizedObjectHeight(0)
+ , mResizedObjectMarginLeft(0)
+ , mResizedObjectMarginTop(0)
+ , mResizedObjectBorderLeft(0)
+ , mResizedObjectBorderTop(0)
+ , mXIncrementFactor(0)
+ , mYIncrementFactor(0)
+ , mWidthIncrementFactor(0)
+ , mHeightIncrementFactor(0)
+ , mInfoXIncrement(20)
+ , mInfoYIncrement(20)
+ , mPositionedObjectX(0)
+ , mPositionedObjectY(0)
+ , mPositionedObjectWidth(0)
+ , mPositionedObjectHeight(0)
+ , mPositionedObjectMarginLeft(0)
+ , mPositionedObjectMarginTop(0)
+ , mPositionedObjectBorderLeft(0)
+ , mPositionedObjectBorderTop(0)
+ , mGridSize(0)
+{
+}
+
+HTMLEditor::~HTMLEditor()
+{
+ // remove the rules as an action listener. Else we get a bad
+ // ownership loop later on. it's ok if the rules aren't a listener;
+ // we ignore the error.
+ nsCOMPtr<nsIEditActionListener> mListener = do_QueryInterface(mRules);
+ RemoveEditActionListener(mListener);
+
+ //the autopointers will clear themselves up.
+ //but we need to also remove the listeners or we have a leak
+ RefPtr<Selection> selection = GetSelection();
+ // if we don't get the selection, just skip this
+ if (selection) {
+ nsCOMPtr<nsISelectionListener>listener;
+ listener = do_QueryInterface(mTypeInState);
+ if (listener) {
+ selection->RemoveSelectionListener(listener);
+ }
+ listener = do_QueryInterface(mSelectionListenerP);
+ if (listener) {
+ selection->RemoveSelectionListener(listener);
+ }
+ }
+
+ mTypeInState = nullptr;
+ mSelectionListenerP = nullptr;
+
+ // free any default style propItems
+ RemoveAllDefaultProperties();
+
+ if (mLinkHandler && mDocWeak) {
+ nsCOMPtr<nsIPresShell> ps = GetPresShell();
+
+ if (ps && ps->GetPresContext()) {
+ ps->GetPresContext()->SetLinkHandler(mLinkHandler);
+ }
+ }
+
+ RemoveEventListeners();
+
+ HideAnonymousEditingUIs();
+}
+
+void
+HTMLEditor::HideAnonymousEditingUIs()
+{
+ if (mAbsolutelyPositionedObject) {
+ HideGrabber();
+ }
+ if (mInlineEditedCell) {
+ HideInlineTableEditingUI();
+ }
+ if (mResizedObject) {
+ HideResizers();
+ }
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLEditor)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLEditor, TextEditor)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mTypeInState)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mStyleSheets)
+
+ tmp->HideAnonymousEditingUIs();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLEditor, TextEditor)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTypeInState)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStyleSheets)
+
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTopLeftHandle)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTopHandle)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTopRightHandle)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLeftHandle)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRightHandle)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBottomLeftHandle)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBottomHandle)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBottomRightHandle)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mActivatedHandle)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResizingShadow)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResizingInfo)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResizedObject)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMouseMotionListenerP)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelectionListenerP)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResizeEventListenerP)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mObjectResizeEventListeners)
+
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAbsolutelyPositionedObject)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGrabber)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPositioningShadow)
+
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInlineEditedCell)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAddColumnBeforeButton)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRemoveColumnButton)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAddColumnAfterButton)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAddRowBeforeButton)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRemoveRowButton)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAddRowAfterButton)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_ADDREF_INHERITED(HTMLEditor, EditorBase)
+NS_IMPL_RELEASE_INHERITED(HTMLEditor, EditorBase)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(HTMLEditor)
+ NS_INTERFACE_MAP_ENTRY(nsIHTMLEditor)
+ NS_INTERFACE_MAP_ENTRY(nsIHTMLObjectResizer)
+ NS_INTERFACE_MAP_ENTRY(nsIHTMLAbsPosEditor)
+ NS_INTERFACE_MAP_ENTRY(nsIHTMLInlineTableEditor)
+ NS_INTERFACE_MAP_ENTRY(nsITableEditor)
+ NS_INTERFACE_MAP_ENTRY(nsIEditorStyleSheets)
+ NS_INTERFACE_MAP_ENTRY(nsICSSLoaderObserver)
+ NS_INTERFACE_MAP_ENTRY(nsIMutationObserver)
+NS_INTERFACE_MAP_END_INHERITING(TextEditor)
+
+NS_IMETHODIMP
+HTMLEditor::Init(nsIDOMDocument* aDoc,
+ nsIContent* aRoot,
+ nsISelectionController* aSelCon,
+ uint32_t aFlags,
+ const nsAString& aInitialValue)
+{
+ NS_PRECONDITION(aDoc && !aSelCon, "bad arg");
+ NS_ENSURE_TRUE(aDoc, NS_ERROR_NULL_POINTER);
+ MOZ_ASSERT(aInitialValue.IsEmpty(), "Non-empty initial values not supported");
+
+ nsresult rulesRv = NS_OK;
+
+ {
+ // block to scope AutoEditInitRulesTrigger
+ AutoEditInitRulesTrigger rulesTrigger(this, rulesRv);
+
+ // Init the plaintext editor
+ nsresult rv = TextEditor::Init(aDoc, aRoot, nullptr, aFlags, aInitialValue);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Init mutation observer
+ nsCOMPtr<nsINode> document = do_QueryInterface(aDoc);
+ document->AddMutationObserverUnlessExists(this);
+
+ if (!mRootElement) {
+ UpdateRootElement();
+ }
+
+ // disable Composer-only features
+ if (IsMailEditor()) {
+ SetAbsolutePositioningEnabled(false);
+ SetSnapToGridEnabled(false);
+ }
+
+ // Init the HTML-CSS utils
+ mCSSEditUtils = new CSSEditUtils(this);
+
+ // disable links
+ nsCOMPtr<nsIPresShell> presShell = GetPresShell();
+ NS_ENSURE_TRUE(presShell, NS_ERROR_FAILURE);
+ nsPresContext *context = presShell->GetPresContext();
+ NS_ENSURE_TRUE(context, NS_ERROR_NULL_POINTER);
+ if (!IsPlaintextEditor() && !IsInteractionAllowed()) {
+ mLinkHandler = context->GetLinkHandler();
+ context->SetLinkHandler(nullptr);
+ }
+
+ // init the type-in state
+ mTypeInState = new TypeInState();
+
+ // init the selection listener for image resizing
+ mSelectionListenerP = new ResizerSelectionListener(this);
+
+ if (!IsInteractionAllowed()) {
+ // ignore any errors from this in case the file is missing
+ AddOverrideStyleSheet(NS_LITERAL_STRING("resource://gre/res/EditorOverride.css"));
+ }
+
+ RefPtr<Selection> selection = GetSelection();
+ if (selection) {
+ nsCOMPtr<nsISelectionListener>listener;
+ listener = do_QueryInterface(mTypeInState);
+ if (listener) {
+ selection->AddSelectionListener(listener);
+ }
+ listener = do_QueryInterface(mSelectionListenerP);
+ if (listener) {
+ selection->AddSelectionListener(listener);
+ }
+ }
+ }
+ NS_ENSURE_SUCCESS(rulesRv, rulesRv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::PreDestroy(bool aDestroyingFrames)
+{
+ if (mDidPreDestroy) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsINode> document = do_QueryReferent(mDocWeak);
+ if (document) {
+ document->RemoveMutationObserver(this);
+ }
+
+ while (!mStyleSheetURLs.IsEmpty()) {
+ RemoveOverrideStyleSheet(mStyleSheetURLs[0]);
+ }
+
+ // Clean up after our anonymous content -- we don't want these nodes to
+ // stay around (which they would, since the frames have an owning reference).
+ HideAnonymousEditingUIs();
+
+ return TextEditor::PreDestroy(aDestroyingFrames);
+}
+
+void
+HTMLEditor::UpdateRootElement()
+{
+ // Use the HTML documents body element as the editor root if we didn't
+ // get a root element during initialization.
+
+ nsCOMPtr<nsIDOMElement> rootElement;
+ nsCOMPtr<nsIDOMHTMLElement> bodyElement;
+ GetBodyElement(getter_AddRefs(bodyElement));
+ if (bodyElement) {
+ rootElement = bodyElement;
+ } else {
+ // If there is no HTML body element,
+ // we should use the document root element instead.
+ nsCOMPtr<nsIDOMDocument> doc = do_QueryReferent(mDocWeak);
+ if (doc) {
+ doc->GetDocumentElement(getter_AddRefs(rootElement));
+ }
+ }
+
+ mRootElement = do_QueryInterface(rootElement);
+}
+
+already_AddRefed<nsIContent>
+HTMLEditor::FindSelectionRoot(nsINode* aNode)
+{
+ NS_PRECONDITION(aNode->IsNodeOfType(nsINode::eDOCUMENT) ||
+ aNode->IsNodeOfType(nsINode::eCONTENT),
+ "aNode must be content or document node");
+
+ nsCOMPtr<nsIDocument> doc = aNode->GetUncomposedDoc();
+ if (!doc) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIContent> content;
+ if (doc->HasFlag(NODE_IS_EDITABLE) || !aNode->IsContent()) {
+ content = doc->GetRootElement();
+ return content.forget();
+ }
+ content = aNode->AsContent();
+
+ // XXX If we have readonly flag, shouldn't return the element which has
+ // contenteditable="true"? However, such case isn't there without chrome
+ // permission script.
+ if (IsReadonly()) {
+ // We still want to allow selection in a readonly editor.
+ content = do_QueryInterface(GetRoot());
+ return content.forget();
+ }
+
+ if (!content->HasFlag(NODE_IS_EDITABLE)) {
+ // If the content is in read-write state but is not editable itself,
+ // return it as the selection root.
+ if (content->IsElement() &&
+ content->AsElement()->State().HasState(NS_EVENT_STATE_MOZ_READWRITE)) {
+ return content.forget();
+ }
+ return nullptr;
+ }
+
+ // For non-readonly editors we want to find the root of the editable subtree
+ // containing aContent.
+ content = content->GetEditingHost();
+ return content.forget();
+}
+
+void
+HTMLEditor::CreateEventListeners()
+{
+ // Don't create the handler twice
+ if (!mEventListener) {
+ mEventListener = new HTMLEditorEventListener();
+ }
+}
+
+nsresult
+HTMLEditor::InstallEventListeners()
+{
+ NS_ENSURE_TRUE(mDocWeak && mEventListener,
+ NS_ERROR_NOT_INITIALIZED);
+
+ // NOTE: HTMLEditor doesn't need to initialize mEventTarget here because
+ // the target must be document node and it must be referenced as weak pointer.
+
+ HTMLEditorEventListener* listener =
+ reinterpret_cast<HTMLEditorEventListener*>(mEventListener.get());
+ return listener->Connect(this);
+}
+
+void
+HTMLEditor::RemoveEventListeners()
+{
+ if (!mDocWeak) {
+ return;
+ }
+
+ nsCOMPtr<nsIDOMEventTarget> target = GetDOMEventTarget();
+
+ if (target) {
+ // Both mMouseMotionListenerP and mResizeEventListenerP can be
+ // registerd with other targets than the DOM event receiver that
+ // we can reach from here. But nonetheless, unregister the event
+ // listeners with the DOM event reveiver (if it's registerd with
+ // other targets, it'll get unregisterd once the target goes
+ // away).
+
+ if (mMouseMotionListenerP) {
+ // mMouseMotionListenerP might be registerd either as bubbling or
+ // capturing, unregister by both.
+ target->RemoveEventListener(NS_LITERAL_STRING("mousemove"),
+ mMouseMotionListenerP, false);
+ target->RemoveEventListener(NS_LITERAL_STRING("mousemove"),
+ mMouseMotionListenerP, true);
+ }
+
+ if (mResizeEventListenerP) {
+ target->RemoveEventListener(NS_LITERAL_STRING("resize"),
+ mResizeEventListenerP, false);
+ }
+ }
+
+ mMouseMotionListenerP = nullptr;
+ mResizeEventListenerP = nullptr;
+
+ TextEditor::RemoveEventListeners();
+}
+
+NS_IMETHODIMP
+HTMLEditor::SetFlags(uint32_t aFlags)
+{
+ nsresult rv = TextEditor::SetFlags(aFlags);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Sets mCSSAware to correspond to aFlags. This toggles whether CSS is
+ // used to style elements in the editor. Note that the editor is only CSS
+ // aware by default in Composer and in the mail editor.
+ mCSSAware = !NoCSS() && !IsMailEditor();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::InitRules()
+{
+ if (!mRules) {
+ // instantiate the rules for the html editor
+ mRules = new HTMLEditRules();
+ }
+ return mRules->Init(static_cast<TextEditor*>(this));
+}
+
+NS_IMETHODIMP
+HTMLEditor::BeginningOfDocument()
+{
+ if (!mDocWeak) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // Get the selection
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NOT_INITIALIZED);
+
+ // Get the root element.
+ nsCOMPtr<Element> rootElement = GetRoot();
+ if (!rootElement) {
+ NS_WARNING("GetRoot() returned a null pointer (mRootElement is null)");
+ 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);
+ 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->IndexOf(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.
+
+ 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->IndexOf(visNode) : -1;
+ done = true;
+ } else {
+ bool isEmptyBlock;
+ if (NS_SUCCEEDED(IsEmptyNode(visNode, &isEmptyBlock)) &&
+ isEmptyBlock) {
+ // Skip the empty block
+ curNode = visNode->GetParentNode();
+ curOffset = curNode ? curNode->IndexOf(visNode) : -1;
+ curOffset++;
+ } else {
+ curNode = visNode;
+ curOffset = 0;
+ }
+ // Keep looping
+ }
+ } else {
+ // Else we found nothing useful
+ selNode = curNode;
+ selOffset = curOffset;
+ done = true;
+ }
+ }
+ return selection->Collapse(selNode, selOffset);
+}
+
+nsresult
+HTMLEditor::HandleKeyPressEvent(nsIDOMKeyEvent* aKeyEvent)
+{
+ // NOTE: When you change this method, you should also change:
+ // * editor/libeditor/tests/test_htmleditor_keyevent_handling.html
+
+ if (IsReadonly() || IsDisabled()) {
+ // When we're not editable, the events are handled on EditorBase, so, we can
+ // bypass TextEditor.
+ return EditorBase::HandleKeyPressEvent(aKeyEvent);
+ }
+
+ WidgetKeyboardEvent* nativeKeyEvent =
+ aKeyEvent->AsEvent()->WidgetEventPtr()->AsKeyboardEvent();
+ NS_ENSURE_TRUE(nativeKeyEvent, NS_ERROR_UNEXPECTED);
+ NS_ASSERTION(nativeKeyEvent->mMessage == eKeyPress,
+ "HandleKeyPressEvent gets non-keypress event");
+
+ switch (nativeKeyEvent->mKeyCode) {
+ case NS_VK_META:
+ case NS_VK_WIN:
+ case NS_VK_SHIFT:
+ case NS_VK_CONTROL:
+ case NS_VK_ALT:
+ case NS_VK_BACK:
+ case NS_VK_DELETE:
+ // These keys are handled on EditorBase, so, we can bypass
+ // TextEditor.
+ return EditorBase::HandleKeyPressEvent(aKeyEvent);
+ case NS_VK_TAB: {
+ if (IsPlaintextEditor()) {
+ // If this works as plain text editor, e.g., mail editor for plain
+ // text, should be handled on TextEditor.
+ return TextEditor::HandleKeyPressEvent(aKeyEvent);
+ }
+
+ if (IsTabbable()) {
+ return NS_OK; // let it be used for focus switching
+ }
+
+ if (nativeKeyEvent->IsControl() || nativeKeyEvent->IsAlt() ||
+ nativeKeyEvent->IsMeta() || nativeKeyEvent->IsOS()) {
+ return NS_OK;
+ }
+
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection && selection->RangeCount(), NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsINode> node = selection->GetRangeAt(0)->GetStartParent();
+ MOZ_ASSERT(node);
+
+ nsCOMPtr<Element> blockParent = GetBlock(*node);
+
+ if (!blockParent) {
+ break;
+ }
+
+ bool handled = false;
+ nsresult rv = NS_OK;
+ if (HTMLEditUtils::IsTableElement(blockParent)) {
+ rv = TabInTable(nativeKeyEvent->IsShift(), &handled);
+ if (handled) {
+ ScrollSelectionIntoView(false);
+ }
+ } else if (HTMLEditUtils::IsListItem(blockParent)) {
+ rv = Indent(nativeKeyEvent->IsShift()
+ ? NS_LITERAL_STRING("outdent")
+ : NS_LITERAL_STRING("indent"));
+ handled = true;
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (handled) {
+ return aKeyEvent->AsEvent()->PreventDefault(); // consumed
+ }
+ if (nativeKeyEvent->IsShift()) {
+ return NS_OK; // don't type text for shift tabs
+ }
+ aKeyEvent->AsEvent()->PreventDefault();
+ return TypedText(NS_LITERAL_STRING("\t"), eTypedText);
+ }
+ case NS_VK_RETURN:
+ if (nativeKeyEvent->IsControl() || nativeKeyEvent->IsAlt() ||
+ nativeKeyEvent->IsMeta() || nativeKeyEvent->IsOS()) {
+ return NS_OK;
+ }
+ aKeyEvent->AsEvent()->PreventDefault(); // consumed
+ if (nativeKeyEvent->IsShift() && !IsPlaintextEditor()) {
+ // only inserts a br node
+ return TypedText(EmptyString(), eTypedBR);
+ }
+ // uses rules to figure out what to insert
+ return TypedText(EmptyString(), eTypedBreak);
+ }
+
+ // NOTE: On some keyboard layout, some characters are inputted with Control
+ // key or Alt key, but at that time, widget sets FALSE to these keys.
+ if (!nativeKeyEvent->mCharCode || nativeKeyEvent->IsControl() ||
+ nativeKeyEvent->IsAlt() || nativeKeyEvent->IsMeta() ||
+ nativeKeyEvent->IsOS()) {
+ // we don't PreventDefault() here or keybindings like control-x won't work
+ return NS_OK;
+ }
+ aKeyEvent->AsEvent()->PreventDefault();
+ nsAutoString str(nativeKeyEvent->mCharCode);
+ return TypedText(str, eTypedText);
+}
+
+static void
+AssertParserServiceIsCorrect(nsIAtom* aTag, bool aIsBlock)
+{
+#ifdef DEBUG
+ // Check this against what we would have said with the old code:
+ if (aTag == nsGkAtoms::p ||
+ aTag == nsGkAtoms::div ||
+ aTag == nsGkAtoms::blockquote ||
+ aTag == nsGkAtoms::h1 ||
+ aTag == nsGkAtoms::h2 ||
+ aTag == nsGkAtoms::h3 ||
+ aTag == nsGkAtoms::h4 ||
+ aTag == nsGkAtoms::h5 ||
+ aTag == nsGkAtoms::h6 ||
+ aTag == nsGkAtoms::ul ||
+ aTag == nsGkAtoms::ol ||
+ aTag == nsGkAtoms::dl ||
+ aTag == nsGkAtoms::noscript ||
+ aTag == nsGkAtoms::form ||
+ aTag == nsGkAtoms::hr ||
+ aTag == nsGkAtoms::table ||
+ aTag == nsGkAtoms::fieldset ||
+ aTag == nsGkAtoms::address ||
+ aTag == nsGkAtoms::col ||
+ aTag == nsGkAtoms::colgroup ||
+ aTag == nsGkAtoms::li ||
+ aTag == nsGkAtoms::dt ||
+ aTag == nsGkAtoms::dd ||
+ aTag == nsGkAtoms::legend) {
+ if (!aIsBlock) {
+ nsAutoString assertmsg (NS_LITERAL_STRING("Parser and editor disagree on blockness: "));
+
+ nsAutoString tagName;
+ aTag->ToString(tagName);
+ assertmsg.Append(tagName);
+ char* assertstr = ToNewCString(assertmsg);
+ NS_ASSERTION(aIsBlock, assertstr);
+ free(assertstr);
+ }
+ }
+#endif // DEBUG
+}
+
+/**
+ * Returns true if the id represents an element of block type.
+ * Can be used to determine if a new paragraph should be started.
+ */
+bool
+HTMLEditor::NodeIsBlockStatic(const nsINode* aElement)
+{
+ MOZ_ASSERT(aElement);
+
+ // Nodes we know we want to treat as block
+ // even though the parser says they're not:
+ if (aElement->IsAnyOfHTMLElements(nsGkAtoms::body,
+ nsGkAtoms::head,
+ nsGkAtoms::tbody,
+ nsGkAtoms::thead,
+ nsGkAtoms::tfoot,
+ nsGkAtoms::tr,
+ nsGkAtoms::th,
+ nsGkAtoms::td,
+ nsGkAtoms::li,
+ nsGkAtoms::dt,
+ nsGkAtoms::dd,
+ nsGkAtoms::pre)) {
+ return true;
+ }
+
+ bool isBlock;
+#ifdef DEBUG
+ // XXX we can't use DebugOnly here because VC++ is stupid (bug 802884)
+ nsresult rv =
+#endif
+ nsContentUtils::GetParserService()->
+ IsBlock(nsContentUtils::GetParserService()->HTMLAtomTagToId(
+ aElement->NodeInfo()->NameAtom()),
+ isBlock);
+ MOZ_ASSERT(rv == NS_OK);
+
+ AssertParserServiceIsCorrect(aElement->NodeInfo()->NameAtom(), isBlock);
+
+ return isBlock;
+}
+
+nsresult
+HTMLEditor::NodeIsBlockStatic(nsIDOMNode* aNode,
+ bool* aIsBlock)
+{
+ if (!aNode || !aIsBlock) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ nsCOMPtr<dom::Element> element = do_QueryInterface(aNode);
+ *aIsBlock = element && NodeIsBlockStatic(element);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::NodeIsBlock(nsIDOMNode* aNode,
+ bool* aIsBlock)
+{
+ return NodeIsBlockStatic(aNode, aIsBlock);
+}
+
+bool
+HTMLEditor::IsBlockNode(nsINode* aNode)
+{
+ return aNode && NodeIsBlockStatic(aNode);
+}
+
+// Non-static version for the nsIEditor interface and JavaScript
+NS_IMETHODIMP
+HTMLEditor::SetDocumentTitle(const nsAString& aTitle)
+{
+ RefPtr<SetDocumentTitleTransaction> transaction =
+ new SetDocumentTitleTransaction();
+ NS_ENSURE_TRUE(transaction, NS_ERROR_OUT_OF_MEMORY);
+
+ nsresult rv = transaction->Init(this, &aTitle);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ //Don't let Rules System change the selection
+ AutoTransactionsConserveSelection dontChangeSelection(this);
+ return EditorBase::DoTransaction(transaction);
+}
+
+/**
+ * GetBlockNodeParent returns enclosing block level ancestor, if any.
+ */
+Element*
+HTMLEditor::GetBlockNodeParent(nsINode* aNode)
+{
+ MOZ_ASSERT(aNode);
+
+ nsCOMPtr<nsINode> p = aNode->GetParentNode();
+
+ while (p) {
+ if (NodeIsBlockStatic(p)) {
+ return p->AsElement();
+ }
+ p = p->GetParentNode();
+ }
+
+ return nullptr;
+}
+
+nsIDOMNode*
+HTMLEditor::GetBlockNodeParent(nsIDOMNode* aNode)
+{
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+
+ if (!node) {
+ NS_NOTREACHED("null node passed to GetBlockNodeParent()");
+ return nullptr;
+ }
+
+ return GetAsDOMNode(GetBlockNodeParent(node));
+}
+
+/**
+ * Returns the node if it's a block, otherwise GetBlockNodeParent
+ */
+Element*
+HTMLEditor::GetBlock(nsINode& aNode)
+{
+ if (NodeIsBlockStatic(&aNode)) {
+ return aNode.AsElement();
+ }
+ return GetBlockNodeParent(&aNode);
+}
+
+/**
+ * IsNextCharInNodeWhitespace() checks the adjacent content in the same node to
+ * see if following selection is whitespace or nbsp.
+ */
+void
+HTMLEditor::IsNextCharInNodeWhitespace(nsIContent* aContent,
+ int32_t aOffset,
+ bool* outIsSpace,
+ bool* outIsNBSP,
+ nsIContent** outNode,
+ int32_t* outOffset)
+{
+ MOZ_ASSERT(aContent && outIsSpace && outIsNBSP);
+ MOZ_ASSERT((outNode && outOffset) || (!outNode && !outOffset));
+ *outIsSpace = false;
+ *outIsNBSP = false;
+ if (outNode && outOffset) {
+ *outNode = nullptr;
+ *outOffset = -1;
+ }
+
+ if (aContent->IsNodeOfType(nsINode::eTEXT) &&
+ (uint32_t)aOffset < aContent->Length()) {
+ char16_t ch = aContent->GetText()->CharAt(aOffset);
+ *outIsSpace = nsCRT::IsAsciiSpace(ch);
+ *outIsNBSP = (ch == kNBSP);
+ if (outNode && outOffset) {
+ NS_IF_ADDREF(*outNode = aContent);
+ // yes, this is _past_ the character
+ *outOffset = aOffset + 1;
+ }
+ }
+}
+
+
+/**
+ * IsPrevCharInNodeWhitespace() checks the adjacent content in the same node to
+ * see if following selection is whitespace.
+ */
+void
+HTMLEditor::IsPrevCharInNodeWhitespace(nsIContent* aContent,
+ int32_t aOffset,
+ bool* outIsSpace,
+ bool* outIsNBSP,
+ nsIContent** outNode,
+ int32_t* outOffset)
+{
+ MOZ_ASSERT(aContent && outIsSpace && outIsNBSP);
+ MOZ_ASSERT((outNode && outOffset) || (!outNode && !outOffset));
+ *outIsSpace = false;
+ *outIsNBSP = false;
+ if (outNode && outOffset) {
+ *outNode = nullptr;
+ *outOffset = -1;
+ }
+
+ if (aContent->IsNodeOfType(nsINode::eTEXT) && aOffset > 0) {
+ char16_t ch = aContent->GetText()->CharAt(aOffset - 1);
+ *outIsSpace = nsCRT::IsAsciiSpace(ch);
+ *outIsNBSP = (ch == kNBSP);
+ if (outNode && outOffset) {
+ NS_IF_ADDREF(*outNode = aContent);
+ *outOffset = aOffset - 1;
+ }
+ }
+}
+
+bool
+HTMLEditor::IsVisBreak(nsINode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ if (!TextEditUtils::IsBreak(aNode)) {
+ return false;
+ }
+ // Check if there is a later node in block after br
+ nsCOMPtr<nsINode> nextNode = GetNextHTMLNode(aNode, true);
+ if (nextNode && TextEditUtils::IsBreak(nextNode)) {
+ return true;
+ }
+
+ // A single line break before a block boundary is not displayed, so e.g.
+ // foo<p>bar<br></p> and foo<br><p>bar</p> display the same as foo<p>bar</p>.
+ // But if there are multiple <br>s in a row, all but the last are visible.
+ if (!nextNode) {
+ // This break is trailer in block, it's not visible
+ return false;
+ }
+ if (IsBlockNode(nextNode)) {
+ // Break is right before a block, it's not visible
+ return false;
+ }
+
+ // If there's an inline node after this one that's not a break, and also a
+ // prior break, this break must be visible.
+ nsCOMPtr<nsINode> priorNode = GetPriorHTMLNode(aNode, true);
+ if (priorNode && TextEditUtils::IsBreak(priorNode)) {
+ return true;
+ }
+
+ // Sigh. We have to use expensive whitespace calculation code to
+ // determine what is going on
+ int32_t selOffset;
+ nsCOMPtr<nsINode> selNode = GetNodeLocation(aNode, &selOffset);
+ // Let's look after the break
+ selOffset++;
+ WSRunObject wsObj(this, selNode, selOffset);
+ nsCOMPtr<nsINode> unused;
+ int32_t visOffset = 0;
+ WSType visType;
+ wsObj.NextVisibleNode(selNode, selOffset, address_of(unused),
+ &visOffset, &visType);
+ if (visType & WSType::block) {
+ return false;
+ }
+
+ return true;
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetIsDocumentEditable(bool* aIsDocumentEditable)
+{
+ NS_ENSURE_ARG_POINTER(aIsDocumentEditable);
+
+ nsCOMPtr<nsIDOMDocument> doc = GetDOMDocument();
+ *aIsDocumentEditable = doc && IsModifiable();
+
+ return NS_OK;
+}
+
+bool
+HTMLEditor::IsModifiable()
+{
+ return !IsReadonly();
+}
+
+NS_IMETHODIMP
+HTMLEditor::UpdateBaseURL()
+{
+ nsCOMPtr<nsIDocument> doc = GetDocument();
+ NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE);
+
+ // Look for an HTML <base> tag
+ RefPtr<nsContentList> nodeList =
+ doc->GetElementsByTagName(NS_LITERAL_STRING("base"));
+
+ // If no base tag, then set baseURL to the document's URL. This is very
+ // important, else relative URLs for links and images are wrong
+ if (!nodeList || !nodeList->Item(0)) {
+ doc->SetBaseURI(doc->GetDocumentURI());
+ }
+ return NS_OK;
+}
+
+/**
+ * This routine is needed to provide a bottleneck for typing for logging
+ * purposes. Can't use HandleKeyPress() (above) for that since it takes
+ * a nsIDOMKeyEvent* parameter. So instead we pass enough info through
+ * to TypedText() to determine what action to take, but without passing
+ * an event.
+ */
+NS_IMETHODIMP
+HTMLEditor::TypedText(const nsAString& aString,
+ ETypingAction aAction)
+{
+ AutoPlaceHolderBatch batch(this, nsGkAtoms::TypingTxnName);
+
+ if (aAction == eTypedBR) {
+ // only inserts a br node
+ nsCOMPtr<nsIDOMNode> brNode;
+ return InsertBR(address_of(brNode));
+ }
+
+ return TextEditor::TypedText(aString, aAction);
+}
+
+NS_IMETHODIMP
+HTMLEditor::TabInTable(bool inIsShift,
+ bool* outHandled)
+{
+ NS_ENSURE_TRUE(outHandled, NS_ERROR_NULL_POINTER);
+ *outHandled = false;
+
+ // Find enclosing table cell from selection (cell may be selected element)
+ nsCOMPtr<Element> cellElement =
+ GetElementOrParentByTagName(NS_LITERAL_STRING("td"), nullptr);
+ // Do nothing -- we didn't find a table cell
+ NS_ENSURE_TRUE(cellElement, NS_OK);
+
+ // find enclosing table
+ nsCOMPtr<Element> table = GetEnclosingTable(cellElement);
+ NS_ENSURE_TRUE(table, NS_OK);
+
+ // advance to next cell
+ // first create an iterator over the table
+ nsCOMPtr<nsIContentIterator> iter = NS_NewContentIterator();
+ nsresult rv = iter->Init(table);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // position iter at block
+ rv = iter->PositionAt(cellElement);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsINode> node;
+ do {
+ if (inIsShift) {
+ iter->Prev();
+ } else {
+ iter->Next();
+ }
+
+ node = iter->GetCurrentNode();
+
+ if (node && HTMLEditUtils::IsTableCell(node) &&
+ GetEnclosingTable(node) == table) {
+ CollapseSelectionToDeepestNonTableFirstChild(nullptr, node);
+ *outHandled = true;
+ return NS_OK;
+ }
+ } while (!iter->IsDone());
+
+ if (!(*outHandled) && !inIsShift) {
+ // If we haven't handled it yet, then we must have run off the end of the
+ // table. Insert a new row.
+ rv = InsertTableRow(1, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ *outHandled = true;
+ // Put selection in right place. Use table code to get selection and index
+ // to new row...
+ RefPtr<Selection> selection;
+ nsCOMPtr<nsIDOMElement> tblElement, cell;
+ int32_t row;
+ rv = GetCellContext(getter_AddRefs(selection),
+ getter_AddRefs(tblElement),
+ getter_AddRefs(cell),
+ nullptr, nullptr,
+ &row, nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // ...so that we can ask for first cell in that row...
+ rv = GetCellAt(tblElement, row, 0, getter_AddRefs(cell));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // ...and then set selection there. (Note that normally you should use
+ // CollapseSelectionToDeepestNonTableFirstChild(), but we know cell is an
+ // empty new cell, so this works fine)
+ if (cell) {
+ selection->Collapse(cell, 0);
+ }
+ }
+
+ return NS_OK;
+}
+
+already_AddRefed<Element>
+HTMLEditor::CreateBR(nsINode* aNode,
+ int32_t aOffset,
+ EDirection aSelect)
+{
+ nsCOMPtr<nsIDOMNode> parent = GetAsDOMNode(aNode);
+ int32_t offset = aOffset;
+ nsCOMPtr<nsIDOMNode> outBRNode;
+ // We assume everything is fine if the br is not null, irrespective of retval
+ CreateBRImpl(address_of(parent), &offset, address_of(outBRNode), aSelect);
+ nsCOMPtr<Element> ret = do_QueryInterface(outBRNode);
+ return ret.forget();
+}
+
+NS_IMETHODIMP
+HTMLEditor::CreateBR(nsIDOMNode* aNode,
+ int32_t aOffset,
+ nsCOMPtr<nsIDOMNode>* outBRNode,
+ EDirection aSelect)
+{
+ nsCOMPtr<nsIDOMNode> parent = aNode;
+ int32_t offset = aOffset;
+ return CreateBRImpl(address_of(parent), &offset, outBRNode, aSelect);
+}
+
+void
+HTMLEditor::CollapseSelectionToDeepestNonTableFirstChild(Selection* aSelection,
+ nsINode* aNode)
+{
+ MOZ_ASSERT(aNode);
+
+ RefPtr<Selection> selection = aSelection;
+ if (!selection) {
+ selection = GetSelection();
+ }
+ if (!selection) {
+ // Nothing to do
+ return;
+ }
+
+ nsCOMPtr<nsINode> node = aNode;
+
+ for (nsCOMPtr<nsIContent> child = node->GetFirstChild();
+ child;
+ child = child->GetFirstChild()) {
+ // Stop if we find a table, don't want to go into nested tables
+ if (HTMLEditUtils::IsTable(child) || !IsContainer(child)) {
+ break;
+ }
+ node = child;
+ }
+
+ selection->Collapse(node, 0);
+}
+
+
+/**
+ * This is mostly like InsertHTMLWithCharsetAndContext, but we can't use that
+ * because it is selection-based and the rules code won't let us edit under the
+ * <head> node
+ */
+NS_IMETHODIMP
+HTMLEditor::ReplaceHeadContentsWithHTML(const nsAString& aSourceToInsert)
+{
+ // don't do any post processing, rules get confused
+ AutoRules beginRulesSniffing(this, EditAction::ignore, nsIEditor::eNone);
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+
+ ForceCompositionEnd();
+
+ // Do not use AutoRules -- rules code won't let us insert in <head>. Use
+ // the head node as a parent and delete/insert directly.
+ nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak);
+ NS_ENSURE_TRUE(doc, NS_ERROR_NOT_INITIALIZED);
+
+ RefPtr<nsContentList> nodeList =
+ doc->GetElementsByTagName(NS_LITERAL_STRING("head"));
+ NS_ENSURE_TRUE(nodeList, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsIContent> headNode = nodeList->Item(0);
+ NS_ENSURE_TRUE(headNode, NS_ERROR_NULL_POINTER);
+
+ // First, make sure there are no return chars in the source. Bad things
+ // happen if you insert returns (instead of dom newlines, \n) into an editor
+ // document.
+ nsAutoString inputString (aSourceToInsert); // hope this does copy-on-write
+
+ // Windows linebreaks: Map CRLF to LF:
+ inputString.ReplaceSubstring(u"\r\n", u"\n");
+
+ // Mac linebreaks: Map any remaining CR to LF:
+ inputString.ReplaceSubstring(u"\r", u"\n");
+
+ AutoEditBatch beginBatching(this);
+
+ // Get the first range in the selection, for context:
+ RefPtr<nsRange> range = selection->GetRangeAt(0);
+ NS_ENSURE_TRUE(range, NS_ERROR_NULL_POINTER);
+
+ ErrorResult err;
+ RefPtr<DocumentFragment> docfrag =
+ range->CreateContextualFragment(inputString, err);
+
+ // XXXX BUG 50965: This is not returning the text between <title>...</title>
+ // Special code is needed in JS to handle title anyway, so it doesn't matter!
+
+ if (err.Failed()) {
+#ifdef DEBUG
+ printf("Couldn't create contextual fragment: error was %X\n",
+ err.ErrorCodeAsInt());
+#endif
+ return err.StealNSResult();
+ }
+ NS_ENSURE_TRUE(docfrag, NS_ERROR_NULL_POINTER);
+
+ // First delete all children in head
+ while (nsCOMPtr<nsIContent> child = headNode->GetFirstChild()) {
+ nsresult rv = DeleteNode(child);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Now insert the new nodes
+ int32_t offsetOfNewNode = 0;
+
+ // Loop over the contents of the fragment and move into the document
+ while (nsCOMPtr<nsIContent> child = docfrag->GetFirstChild()) {
+ nsresult rv = InsertNode(*child, *headNode, offsetOfNewNode++);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::RebuildDocumentFromSource(const nsAString& aSourceString)
+{
+ ForceCompositionEnd();
+
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<Element> bodyElement = GetRoot();
+ NS_ENSURE_TRUE(bodyElement, NS_ERROR_NULL_POINTER);
+
+ // Find where the <body> tag starts.
+ nsReadingIterator<char16_t> beginbody;
+ nsReadingIterator<char16_t> endbody;
+ aSourceString.BeginReading(beginbody);
+ aSourceString.EndReading(endbody);
+ bool foundbody = CaseInsensitiveFindInReadable(NS_LITERAL_STRING("<body"),
+ beginbody, endbody);
+
+ nsReadingIterator<char16_t> beginhead;
+ nsReadingIterator<char16_t> endhead;
+ aSourceString.BeginReading(beginhead);
+ aSourceString.EndReading(endhead);
+ bool foundhead = CaseInsensitiveFindInReadable(NS_LITERAL_STRING("<head"),
+ beginhead, endhead);
+ // a valid head appears before the body
+ if (foundbody && beginhead.get() > beginbody.get()) {
+ foundhead = false;
+ }
+
+ nsReadingIterator<char16_t> beginclosehead;
+ nsReadingIterator<char16_t> endclosehead;
+ aSourceString.BeginReading(beginclosehead);
+ aSourceString.EndReading(endclosehead);
+
+ // Find the index after "<head>"
+ bool foundclosehead = CaseInsensitiveFindInReadable(
+ NS_LITERAL_STRING("</head>"), beginclosehead, endclosehead);
+ // a valid close head appears after a found head
+ if (foundhead && beginhead.get() > beginclosehead.get()) {
+ foundclosehead = false;
+ }
+ // a valid close head appears before a found body
+ if (foundbody && beginclosehead.get() > beginbody.get()) {
+ foundclosehead = false;
+ }
+
+ // Time to change the document
+ AutoEditBatch beginBatching(this);
+
+ nsReadingIterator<char16_t> endtotal;
+ aSourceString.EndReading(endtotal);
+
+ if (foundhead) {
+ if (foundclosehead) {
+ nsresult rv =
+ ReplaceHeadContentsWithHTML(Substring(beginhead, beginclosehead));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else if (foundbody) {
+ nsresult rv =
+ ReplaceHeadContentsWithHTML(Substring(beginhead, beginbody));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ // XXX Without recourse to some parser/content sink/docshell hackery we
+ // don't really know where the head ends and the body begins so we assume
+ // that there is no body
+ nsresult rv = ReplaceHeadContentsWithHTML(Substring(beginhead, endtotal));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+ } else {
+ nsReadingIterator<char16_t> begintotal;
+ aSourceString.BeginReading(begintotal);
+ NS_NAMED_LITERAL_STRING(head, "<head>");
+ if (foundclosehead) {
+ nsresult rv =
+ ReplaceHeadContentsWithHTML(head + Substring(begintotal,
+ beginclosehead));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else if (foundbody) {
+ nsresult rv = ReplaceHeadContentsWithHTML(head + Substring(begintotal,
+ beginbody));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ // XXX Without recourse to some parser/content sink/docshell hackery we
+ // don't really know where the head ends and the body begins so we assume
+ // that there is no head
+ nsresult rv = ReplaceHeadContentsWithHTML(head);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+ }
+
+ nsresult rv = SelectAll();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!foundbody) {
+ NS_NAMED_LITERAL_STRING(body, "<body>");
+ // XXX Without recourse to some parser/content sink/docshell hackery we
+ // don't really know where the head ends and the body begins
+ if (foundclosehead) {
+ // assume body starts after the head ends
+ nsresult rv = LoadHTML(body + Substring(endclosehead, endtotal));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else if (foundhead) {
+ // assume there is no body
+ nsresult rv = LoadHTML(body);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ // assume there is no head, the entire source is body
+ nsresult rv = LoadHTML(body + aSourceString);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ nsCOMPtr<Element> divElement =
+ CreateElementWithDefaults(NS_LITERAL_STRING("div"));
+ NS_ENSURE_TRUE(divElement, NS_ERROR_FAILURE);
+
+ CloneAttributes(bodyElement, divElement);
+
+ return BeginningOfDocument();
+ }
+
+ rv = LoadHTML(Substring(beginbody, endtotal));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Now we must copy attributes user might have edited on the <body> tag
+ // because InsertHTML (actually, CreateContextualFragment()) will never
+ // return a body node in the DOM fragment
+
+ // We already know where "<body" begins
+ nsReadingIterator<char16_t> beginclosebody = beginbody;
+ nsReadingIterator<char16_t> endclosebody;
+ aSourceString.EndReading(endclosebody);
+ if (!FindInReadable(NS_LITERAL_STRING(">"), beginclosebody, endclosebody)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Truncate at the end of the body tag. Kludge of the year: fool the parser
+ // by replacing "body" with "div" so we get a node
+ nsAutoString bodyTag;
+ bodyTag.AssignLiteral("<div ");
+ bodyTag.Append(Substring(endbody, endclosebody));
+
+ RefPtr<nsRange> range = selection->GetRangeAt(0);
+ NS_ENSURE_TRUE(range, NS_ERROR_FAILURE);
+
+ ErrorResult erv;
+ RefPtr<DocumentFragment> docfrag =
+ range->CreateContextualFragment(bodyTag, erv);
+ NS_ENSURE_TRUE(!erv.Failed(), erv.StealNSResult());
+ NS_ENSURE_TRUE(docfrag, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsIContent> child = docfrag->GetFirstChild();
+ NS_ENSURE_TRUE(child && child->IsElement(), NS_ERROR_NULL_POINTER);
+
+ // Copy all attributes from the div child to current body element
+ CloneAttributes(bodyElement, child->AsElement());
+
+ // place selection at first editable content
+ return BeginningOfDocument();
+}
+
+void
+HTMLEditor::NormalizeEOLInsertPosition(nsINode* firstNodeToInsert,
+ nsCOMPtr<nsIDOMNode>* insertParentNode,
+ int32_t* insertOffset)
+{
+ /*
+ This function will either correct the position passed in,
+ or leave the position unchanged.
+
+ When the (first) item to insert is a block level element,
+ and our insertion position is after the last visible item in a line,
+ i.e. the insertion position is just before a visible line break <br>,
+ we want to skip to the position just after the line break (see bug 68767)
+
+ However, our logic to detect whether we should skip or not
+ needs to be more clever.
+ We must not skip when the caret appears to be positioned at the beginning
+ of a block, in that case skipping the <br> would not insert the <br>
+ at the caret position, but after the current empty line.
+
+ So we have several cases to test:
+
+ 1) We only ever want to skip, if the next visible thing after the current position is a break
+
+ 2) We do not want to skip if there is no previous visible thing at all
+ That is detected if the call to PriorVisibleNode gives us an offset of zero.
+ Because PriorVisibleNode always positions after the prior node, we would
+ see an offset > 0, if there were a prior node.
+
+ 3) We do not want to skip, if both the next and the previous visible things are breaks.
+
+ 4) We do not want to skip if the previous visible thing is in a different block
+ than the insertion position.
+ */
+
+ if (!IsBlockNode(firstNodeToInsert)) {
+ return;
+ }
+
+ WSRunObject wsObj(this, *insertParentNode, *insertOffset);
+ nsCOMPtr<nsINode> nextVisNode, prevVisNode;
+ int32_t nextVisOffset=0;
+ WSType nextVisType;
+ int32_t prevVisOffset=0;
+ WSType prevVisType;
+
+ nsCOMPtr<nsINode> parent(do_QueryInterface(*insertParentNode));
+ wsObj.NextVisibleNode(parent, *insertOffset, address_of(nextVisNode), &nextVisOffset, &nextVisType);
+ if (!nextVisNode) {
+ return;
+ }
+
+ if (!(nextVisType & WSType::br)) {
+ return;
+ }
+
+ wsObj.PriorVisibleNode(parent, *insertOffset, address_of(prevVisNode), &prevVisOffset, &prevVisType);
+ if (!prevVisNode) {
+ return;
+ }
+
+ if (prevVisType & WSType::br) {
+ return;
+ }
+
+ if (prevVisType & WSType::thisBlock) {
+ return;
+ }
+
+ int32_t brOffset=0;
+ nsCOMPtr<nsIDOMNode> brNode = GetNodeLocation(GetAsDOMNode(nextVisNode), &brOffset);
+
+ *insertParentNode = brNode;
+ *insertOffset = brOffset + 1;
+}
+
+NS_IMETHODIMP
+HTMLEditor::InsertElementAtSelection(nsIDOMElement* aElement,
+ bool aDeleteSelection)
+{
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+
+ nsCOMPtr<Element> element = do_QueryInterface(aElement);
+ NS_ENSURE_TRUE(element, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsIDOMNode> node = do_QueryInterface(aElement);
+
+ ForceCompositionEnd();
+ AutoEditBatch beginBatching(this);
+ AutoRules beginRulesSniffing(this, EditAction::insertElement,
+ nsIEditor::eNext);
+
+ RefPtr<Selection> selection = GetSelection();
+ if (!selection) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // hand off to the rules system, see if it has anything to say about this
+ bool cancel, handled;
+ TextRulesInfo ruleInfo(EditAction::insertElement);
+ ruleInfo.insertElement = aElement;
+ nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
+ if (cancel || NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (!handled) {
+ if (aDeleteSelection) {
+ if (!IsBlockNode(element)) {
+ // E.g., inserting an image. In this case we don't need to delete any
+ // inline wrappers before we do the insertion. Otherwise we let
+ // DeleteSelectionAndPrepareToCreateNode do the deletion for us, which
+ // calls DeleteSelection with aStripWrappers = eStrip.
+ rv = DeleteSelection(nsIEditor::eNone, nsIEditor::eNoStrip);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsresult rv = DeleteSelectionAndPrepareToCreateNode();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // If deleting, selection will be collapsed.
+ // so if not, we collapse it
+ if (!aDeleteSelection) {
+ // Named Anchor is a special case,
+ // We collapse to insert element BEFORE the selection
+ // For all other tags, we insert AFTER the selection
+ if (HTMLEditUtils::IsNamedAnchor(node)) {
+ selection->CollapseToStart();
+ } else {
+ selection->CollapseToEnd();
+ }
+ }
+
+ nsCOMPtr<nsIDOMNode> parentSelectedNode;
+ int32_t offsetForInsert;
+ rv = selection->GetAnchorNode(getter_AddRefs(parentSelectedNode));
+ // XXX: ERROR_HANDLING bad XPCOM usage
+ if (NS_SUCCEEDED(rv) &&
+ NS_SUCCEEDED(selection->GetAnchorOffset(&offsetForInsert)) &&
+ parentSelectedNode) {
+ // Adjust position based on the node we are going to insert.
+ NormalizeEOLInsertPosition(element, address_of(parentSelectedNode),
+ &offsetForInsert);
+
+ rv = InsertNodeAtPoint(node, address_of(parentSelectedNode),
+ &offsetForInsert, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Set caret after element, but check for special case
+ // of inserting table-related elements: set in first cell instead
+ if (!SetCaretInTableCell(aElement)) {
+ rv = SetCaretAfterElement(aElement);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // check for inserting a whole table at the end of a block. If so insert a br after it.
+ if (HTMLEditUtils::IsTable(node)) {
+ bool isLast;
+ rv = IsLastEditableChild(node, &isLast);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (isLast) {
+ nsCOMPtr<nsIDOMNode> brNode;
+ rv = CreateBR(parentSelectedNode, offsetForInsert + 1,
+ address_of(brNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+ selection->Collapse(parentSelectedNode, offsetForInsert+1);
+ }
+ }
+ }
+ }
+ rv = rules->DidDoAction(selection, &ruleInfo, rv);
+ return rv;
+}
+
+
+/**
+ * InsertNodeAtPoint() attempts to insert aNode into the document, at a point
+ * specified by {*ioParent,*ioOffset}. Checks with strict dtd to see if
+ * containment is allowed. If not allowed, will attempt to find a parent in
+ * the parent hierarchy of *ioParent that will accept aNode as a child. If
+ * such a parent is found, will split the document tree from
+ * {*ioParent,*ioOffset} up to parent, and then insert aNode.
+ * ioParent & ioOffset are then adjusted to point to the actual location that
+ * aNode was inserted at. aNoEmptyNodes specifies if the splitting process
+ * is allowed to reslt in empty nodes.
+ *
+ * @param aNode Node to insert.
+ * @param ioParent Insertion parent.
+ * @param ioOffset Insertion offset.
+ * @param aNoEmptyNodes Splitting can result in empty nodes?
+ */
+nsresult
+HTMLEditor::InsertNodeAtPoint(nsIDOMNode* aNode,
+ nsCOMPtr<nsIDOMNode>* ioParent,
+ int32_t* ioOffset,
+ bool aNoEmptyNodes)
+{
+ nsCOMPtr<nsIContent> node = do_QueryInterface(aNode);
+ NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER);
+ NS_ENSURE_TRUE(ioParent, NS_ERROR_NULL_POINTER);
+ NS_ENSURE_TRUE(*ioParent, NS_ERROR_NULL_POINTER);
+ NS_ENSURE_TRUE(ioOffset, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsIContent> parent = do_QueryInterface(*ioParent);
+ NS_ENSURE_TRUE(parent, NS_ERROR_NULL_POINTER);
+ nsCOMPtr<nsIContent> topChild = parent;
+ nsCOMPtr<nsIContent> origParent = parent;
+
+ // Search up the parent chain to find a suitable container
+ while (!CanContain(*parent, *node)) {
+ // If the current parent is a root (body or table element)
+ // then go no further - we can't insert
+ if (parent->IsHTMLElement(nsGkAtoms::body) ||
+ HTMLEditUtils::IsTableElement(parent)) {
+ return NS_ERROR_FAILURE;
+ }
+ // Get the next parent
+ NS_ENSURE_TRUE(parent->GetParentNode(), NS_ERROR_FAILURE);
+ if (!IsEditable(parent->GetParentNode())) {
+ // There's no suitable place to put the node in this editing host. Maybe
+ // someone is trying to put block content in a span. So just put it
+ // where we were originally asked.
+ parent = topChild = origParent;
+ break;
+ }
+ topChild = parent;
+ parent = parent->GetParent();
+ }
+ if (parent != topChild) {
+ // we need to split some levels above the original selection parent
+ int32_t offset = SplitNodeDeep(*topChild, *origParent, *ioOffset,
+ aNoEmptyNodes ? EmptyContainers::no
+ : EmptyContainers::yes);
+ NS_ENSURE_STATE(offset != -1);
+ *ioParent = GetAsDOMNode(parent);
+ *ioOffset = offset;
+ }
+ // Now we can insert the new node
+ return InsertNode(*node, *parent, *ioOffset);
+}
+
+NS_IMETHODIMP
+HTMLEditor::SelectElement(nsIDOMElement* aElement)
+{
+ nsCOMPtr<Element> element = do_QueryInterface(aElement);
+ NS_ENSURE_STATE(element || !aElement);
+
+ // Must be sure that element is contained in the document body
+ if (!IsDescendantOfEditorRoot(element)) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+ nsCOMPtr<nsIDOMNode>parent;
+ nsresult rv = aElement->GetParentNode(getter_AddRefs(parent));
+ if (NS_SUCCEEDED(rv) && parent) {
+ int32_t offsetInParent = GetChildOffset(aElement, parent);
+
+ // Collapse selection to just before desired element,
+ rv = selection->Collapse(parent, offsetInParent);
+ if (NS_SUCCEEDED(rv)) {
+ // then extend it to just after
+ rv = selection->Extend(parent, offsetInParent + 1);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+HTMLEditor::SetCaretAfterElement(nsIDOMElement* aElement)
+{
+ nsCOMPtr<Element> element = do_QueryInterface(aElement);
+ NS_ENSURE_STATE(element || !aElement);
+
+ // Be sure the element is contained in the document body
+ if (!aElement || !IsDescendantOfEditorRoot(element)) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+ nsCOMPtr<nsIDOMNode>parent;
+ nsresult rv = aElement->GetParentNode(getter_AddRefs(parent));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(parent, NS_ERROR_NULL_POINTER);
+ int32_t offsetInParent = GetChildOffset(aElement, parent);
+ // Collapse selection to just after desired element,
+ return selection->Collapse(parent, offsetInParent + 1);
+}
+
+NS_IMETHODIMP
+HTMLEditor::SetParagraphFormat(const nsAString& aParagraphFormat)
+{
+ nsAutoString tag; tag.Assign(aParagraphFormat);
+ ToLowerCase(tag);
+ if (tag.EqualsLiteral("dd") || tag.EqualsLiteral("dt")) {
+ return MakeDefinitionItem(tag);
+ }
+ return InsertBasicBlock(tag);
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetParagraphState(bool* aMixed,
+ nsAString& outFormat)
+{
+ if (!mRules) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ NS_ENSURE_TRUE(aMixed, NS_ERROR_NULL_POINTER);
+ RefPtr<HTMLEditRules> htmlRules =
+ static_cast<HTMLEditRules*>(mRules.get());
+
+ return htmlRules->GetParagraphState(aMixed, outFormat);
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetBackgroundColorState(bool* aMixed,
+ nsAString& aOutColor)
+{
+ if (IsCSSEnabled()) {
+ // if we are in CSS mode, we have to check if the containing block defines
+ // a background color
+ return GetCSSBackgroundColorState(aMixed, aOutColor, true);
+ }
+ // in HTML mode, we look only at page's background
+ return GetHTMLBackgroundColorState(aMixed, aOutColor);
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetHighlightColorState(bool* aMixed,
+ nsAString& aOutColor)
+{
+ *aMixed = false;
+ aOutColor.AssignLiteral("transparent");
+ if (!IsCSSEnabled()) {
+ return NS_OK;
+ }
+
+ // in CSS mode, text background can be added by the Text Highlight button
+ // we need to query the background of the selection without looking for
+ // the block container of the ranges in the selection
+ return GetCSSBackgroundColorState(aMixed, aOutColor, false);
+}
+
+nsresult
+HTMLEditor::GetCSSBackgroundColorState(bool* aMixed,
+ nsAString& aOutColor,
+ bool aBlockLevel)
+{
+ NS_ENSURE_TRUE(aMixed, NS_ERROR_NULL_POINTER);
+ *aMixed = false;
+ // the default background color is transparent
+ aOutColor.AssignLiteral("transparent");
+
+ // get selection
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_STATE(selection && selection->GetRangeAt(0));
+
+ // get selection location
+ nsCOMPtr<nsINode> parent = selection->GetRangeAt(0)->GetStartParent();
+ int32_t offset = selection->GetRangeAt(0)->StartOffset();
+ NS_ENSURE_TRUE(parent, NS_ERROR_NULL_POINTER);
+
+ // is the selection collapsed?
+ nsCOMPtr<nsINode> nodeToExamine;
+ if (selection->Collapsed() || IsTextNode(parent)) {
+ // we want to look at the parent and ancestors
+ nodeToExamine = parent;
+ } else {
+ // otherwise we want to look at the first editable node after
+ // {parent,offset} and its ancestors for divs with alignment on them
+ nodeToExamine = parent->GetChildAt(offset);
+ //GetNextNode(parent, offset, true, address_of(nodeToExamine));
+ }
+
+ NS_ENSURE_TRUE(nodeToExamine, NS_ERROR_NULL_POINTER);
+
+ if (aBlockLevel) {
+ // we are querying the block background (and not the text background), let's
+ // climb to the block container
+ nsCOMPtr<Element> blockParent = GetBlock(*nodeToExamine);
+ NS_ENSURE_TRUE(blockParent, NS_OK);
+
+ // Make sure to not walk off onto the Document node
+ do {
+ // retrieve the computed style of background-color for blockParent
+ mCSSEditUtils->GetComputedProperty(*blockParent,
+ *nsGkAtoms::backgroundColor,
+ aOutColor);
+ blockParent = blockParent->GetParentElement();
+ // look at parent if the queried color is transparent and if the node to
+ // examine is not the root of the document
+ } while (aOutColor.EqualsLiteral("transparent") && blockParent);
+ if (aOutColor.EqualsLiteral("transparent")) {
+ // we have hit the root of the document and the color is still transparent !
+ // Grumble... Let's look at the default background color because that's the
+ // color we are looking for
+ mCSSEditUtils->GetDefaultBackgroundColor(aOutColor);
+ }
+ }
+ else {
+ // no, we are querying the text background for the Text Highlight button
+ if (IsTextNode(nodeToExamine)) {
+ // if the node of interest is a text node, let's climb a level
+ nodeToExamine = nodeToExamine->GetParentNode();
+ }
+ do {
+ // is the node to examine a block ?
+ if (NodeIsBlockStatic(nodeToExamine)) {
+ // yes it is a block; in that case, the text background color is transparent
+ aOutColor.AssignLiteral("transparent");
+ break;
+ } else {
+ // no, it's not; let's retrieve the computed style of background-color for the
+ // node to examine
+ mCSSEditUtils->GetComputedProperty(*nodeToExamine,
+ *nsGkAtoms::backgroundColor,
+ aOutColor);
+ if (!aOutColor.EqualsLiteral("transparent")) {
+ break;
+ }
+ }
+ nodeToExamine = nodeToExamine->GetParentNode();
+ } while ( aOutColor.EqualsLiteral("transparent") && nodeToExamine );
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetHTMLBackgroundColorState(bool* aMixed,
+ nsAString& aOutColor)
+{
+ //TODO: We don't handle "mixed" correctly!
+ NS_ENSURE_TRUE(aMixed, NS_ERROR_NULL_POINTER);
+ *aMixed = false;
+ aOutColor.Truncate();
+
+ nsCOMPtr<nsIDOMElement> domElement;
+ int32_t selectedCount;
+ nsAutoString tagName;
+ nsresult rv = GetSelectedOrParentTableElement(tagName,
+ &selectedCount,
+ getter_AddRefs(domElement));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<dom::Element> element = do_QueryInterface(domElement);
+
+ while (element) {
+ // We are in a cell or selected table
+ element->GetAttr(kNameSpaceID_None, nsGkAtoms::bgcolor, aOutColor);
+
+ // Done if we have a color explicitly set
+ if (!aOutColor.IsEmpty()) {
+ return NS_OK;
+ }
+
+ // Once we hit the body, we're done
+ if (element->IsHTMLElement(nsGkAtoms::body)) {
+ return NS_OK;
+ }
+
+ // No color is set, but we need to report visible color inherited
+ // from nested cells/tables, so search up parent chain
+ element = element->GetParentElement();
+ }
+
+ // If no table or cell found, get page body
+ dom::Element* bodyElement = GetRoot();
+ NS_ENSURE_TRUE(bodyElement, NS_ERROR_NULL_POINTER);
+
+ bodyElement->GetAttr(kNameSpaceID_None, nsGkAtoms::bgcolor, aOutColor);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetListState(bool* aMixed,
+ bool* aOL,
+ bool* aUL,
+ bool* aDL)
+{
+ if (!mRules) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ NS_ENSURE_TRUE(aMixed && aOL && aUL && aDL, NS_ERROR_NULL_POINTER);
+ RefPtr<HTMLEditRules> htmlRules =
+ static_cast<HTMLEditRules*>(mRules.get());
+
+ return htmlRules->GetListState(aMixed, aOL, aUL, aDL);
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetListItemState(bool* aMixed,
+ bool* aLI,
+ bool* aDT,
+ bool* aDD)
+{
+ if (!mRules) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ NS_ENSURE_TRUE(aMixed && aLI && aDT && aDD, NS_ERROR_NULL_POINTER);
+
+ RefPtr<HTMLEditRules> htmlRules =
+ static_cast<HTMLEditRules*>(mRules.get());
+
+ return htmlRules->GetListItemState(aMixed, aLI, aDT, aDD);
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetAlignment(bool* aMixed,
+ nsIHTMLEditor::EAlignment* aAlign)
+{
+ if (!mRules) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ NS_ENSURE_TRUE(aMixed && aAlign, NS_ERROR_NULL_POINTER);
+ RefPtr<HTMLEditRules> htmlRules =
+ static_cast<HTMLEditRules*>(mRules.get());
+
+ return htmlRules->GetAlignment(aMixed, aAlign);
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetIndentState(bool* aCanIndent,
+ bool* aCanOutdent)
+{
+ if (!mRules) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ NS_ENSURE_TRUE(aCanIndent && aCanOutdent, NS_ERROR_NULL_POINTER);
+
+ RefPtr<HTMLEditRules> htmlRules =
+ static_cast<HTMLEditRules*>(mRules.get());
+
+ return htmlRules->GetIndentState(aCanIndent, aCanOutdent);
+}
+
+NS_IMETHODIMP
+HTMLEditor::MakeOrChangeList(const nsAString& aListType,
+ bool entireList,
+ const nsAString& aBulletType)
+{
+ if (!mRules) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+
+ bool cancel, handled;
+
+ AutoEditBatch beginBatching(this);
+ AutoRules beginRulesSniffing(this, EditAction::makeList, nsIEditor::eNext);
+
+ // pre-process
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+
+ TextRulesInfo ruleInfo(EditAction::makeList);
+ ruleInfo.blockType = &aListType;
+ ruleInfo.entireList = entireList;
+ ruleInfo.bulletType = &aBulletType;
+ nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
+ if (cancel || NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (!handled) {
+ // Find out if the selection is collapsed:
+ bool isCollapsed = selection->Collapsed();
+
+ NS_ENSURE_TRUE(selection->GetRangeAt(0) &&
+ selection->GetRangeAt(0)->GetStartParent() &&
+ selection->GetRangeAt(0)->GetStartParent()->IsContent(),
+ NS_ERROR_FAILURE);
+ OwningNonNull<nsIContent> node =
+ *selection->GetRangeAt(0)->GetStartParent()->AsContent();
+ int32_t offset = selection->GetRangeAt(0)->StartOffset();
+
+ if (isCollapsed) {
+ // have to find a place to put the list
+ nsCOMPtr<nsIContent> parent = node;
+ nsCOMPtr<nsIContent> topChild = node;
+
+ nsCOMPtr<nsIAtom> listAtom = NS_Atomize(aListType);
+ while (!CanContainTag(*parent, *listAtom)) {
+ topChild = parent;
+ parent = parent->GetParent();
+ }
+
+ if (parent != node) {
+ // we need to split up to the child of parent
+ offset = SplitNodeDeep(*topChild, *node, offset);
+ NS_ENSURE_STATE(offset != -1);
+ }
+
+ // make a list
+ nsCOMPtr<Element> newList = CreateNode(listAtom, parent, offset);
+ NS_ENSURE_STATE(newList);
+ // make a list item
+ nsCOMPtr<Element> newItem = CreateNode(nsGkAtoms::li, newList, 0);
+ NS_ENSURE_STATE(newItem);
+ rv = selection->Collapse(newItem, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ return rules->DidDoAction(selection, &ruleInfo, rv);
+}
+
+NS_IMETHODIMP
+HTMLEditor::RemoveList(const nsAString& aListType)
+{
+ if (!mRules) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+
+ bool cancel, handled;
+
+ AutoEditBatch beginBatching(this);
+ AutoRules beginRulesSniffing(this, EditAction::removeList, nsIEditor::eNext);
+
+ // pre-process
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+
+ TextRulesInfo ruleInfo(EditAction::removeList);
+ if (aListType.LowerCaseEqualsLiteral("ol")) {
+ ruleInfo.bOrdered = true;
+ } else {
+ ruleInfo.bOrdered = false;
+ }
+ nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
+ if (cancel || NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // no default behavior for this yet. what would it mean?
+
+ return rules->DidDoAction(selection, &ruleInfo, rv);
+}
+
+nsresult
+HTMLEditor::MakeDefinitionItem(const nsAString& aItemType)
+{
+ if (!mRules) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+
+ bool cancel, handled;
+
+ AutoEditBatch beginBatching(this);
+ AutoRules beginRulesSniffing(this, EditAction::makeDefListItem,
+ nsIEditor::eNext);
+
+ // pre-process
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+ TextRulesInfo ruleInfo(EditAction::makeDefListItem);
+ ruleInfo.blockType = &aItemType;
+ nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
+ if (cancel || NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (!handled) {
+ // todo: no default for now. we count on rules to handle it.
+ }
+
+ return rules->DidDoAction(selection, &ruleInfo, rv);
+}
+
+nsresult
+HTMLEditor::InsertBasicBlock(const nsAString& aBlockType)
+{
+ if (!mRules) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+
+ bool cancel, handled;
+
+ AutoEditBatch beginBatching(this);
+ AutoRules beginRulesSniffing(this, EditAction::makeBasicBlock,
+ nsIEditor::eNext);
+
+ // pre-process
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+ TextRulesInfo ruleInfo(EditAction::makeBasicBlock);
+ ruleInfo.blockType = &aBlockType;
+ nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
+ if (cancel || NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (!handled) {
+ // Find out if the selection is collapsed:
+ bool isCollapsed = selection->Collapsed();
+
+ NS_ENSURE_TRUE(selection->GetRangeAt(0) &&
+ selection->GetRangeAt(0)->GetStartParent() &&
+ selection->GetRangeAt(0)->GetStartParent()->IsContent(),
+ NS_ERROR_FAILURE);
+ OwningNonNull<nsIContent> node =
+ *selection->GetRangeAt(0)->GetStartParent()->AsContent();
+ int32_t offset = selection->GetRangeAt(0)->StartOffset();
+
+ if (isCollapsed) {
+ // have to find a place to put the block
+ nsCOMPtr<nsIContent> parent = node;
+ nsCOMPtr<nsIContent> topChild = node;
+
+ nsCOMPtr<nsIAtom> blockAtom = NS_Atomize(aBlockType);
+ while (!CanContainTag(*parent, *blockAtom)) {
+ NS_ENSURE_TRUE(parent->GetParent(), NS_ERROR_FAILURE);
+ topChild = parent;
+ parent = parent->GetParent();
+ }
+
+ if (parent != node) {
+ // we need to split up to the child of parent
+ offset = SplitNodeDeep(*topChild, *node, offset);
+ NS_ENSURE_STATE(offset != -1);
+ }
+
+ // make a block
+ nsCOMPtr<Element> newBlock = CreateNode(blockAtom, parent, offset);
+ NS_ENSURE_STATE(newBlock);
+
+ // reposition selection to inside the block
+ rv = selection->Collapse(newBlock, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ return rules->DidDoAction(selection, &ruleInfo, rv);
+}
+
+NS_IMETHODIMP
+HTMLEditor::Indent(const nsAString& aIndent)
+{
+ if (!mRules) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+
+ bool cancel, handled;
+ EditAction opID = EditAction::indent;
+ if (aIndent.LowerCaseEqualsLiteral("outdent")) {
+ opID = EditAction::outdent;
+ }
+ AutoEditBatch beginBatching(this);
+ AutoRules beginRulesSniffing(this, opID, nsIEditor::eNext);
+
+ // pre-process
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+
+ TextRulesInfo ruleInfo(opID);
+ nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
+ if (cancel || NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (!handled) {
+ // Do default - insert a blockquote node if selection collapsed
+ bool isCollapsed = selection->Collapsed();
+
+ NS_ENSURE_TRUE(selection->GetRangeAt(0) &&
+ selection->GetRangeAt(0)->GetStartParent() &&
+ selection->GetRangeAt(0)->GetStartParent()->IsContent(),
+ NS_ERROR_FAILURE);
+ OwningNonNull<nsIContent> node =
+ *selection->GetRangeAt(0)->GetStartParent()->AsContent();
+ int32_t offset = selection->GetRangeAt(0)->StartOffset();
+
+ if (aIndent.EqualsLiteral("indent")) {
+ if (isCollapsed) {
+ // have to find a place to put the blockquote
+ nsCOMPtr<nsIContent> parent = node;
+ nsCOMPtr<nsIContent> topChild = node;
+ while (!CanContainTag(*parent, *nsGkAtoms::blockquote)) {
+ NS_ENSURE_TRUE(parent->GetParent(), NS_ERROR_FAILURE);
+ topChild = parent;
+ parent = parent->GetParent();
+ }
+
+ if (parent != node) {
+ // we need to split up to the child of parent
+ offset = SplitNodeDeep(*topChild, *node, offset);
+ NS_ENSURE_STATE(offset != -1);
+ }
+
+ // make a blockquote
+ nsCOMPtr<Element> newBQ = CreateNode(nsGkAtoms::blockquote, parent, offset);
+ NS_ENSURE_STATE(newBQ);
+ // put a space in it so layout will draw the list item
+ rv = selection->Collapse(newBQ, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = InsertText(NS_LITERAL_STRING(" "));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // reposition selection to before the space character
+ NS_ENSURE_STATE(selection->GetRangeAt(0));
+ rv = selection->Collapse(selection->GetRangeAt(0)->GetStartParent(), 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ }
+ return rules->DidDoAction(selection, &ruleInfo, rv);
+}
+
+//TODO: IMPLEMENT ALIGNMENT!
+
+NS_IMETHODIMP
+HTMLEditor::Align(const nsAString& aAlignType)
+{
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+
+ AutoEditBatch beginBatching(this);
+ AutoRules beginRulesSniffing(this, EditAction::align, nsIEditor::eNext);
+
+ bool cancel, handled;
+
+ // Find out if the selection is collapsed:
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+ TextRulesInfo ruleInfo(EditAction::align);
+ ruleInfo.alignType = &aAlignType;
+ nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
+ if (cancel || NS_FAILED(rv)) {
+ return rv;
+ }
+
+ return rules->DidDoAction(selection, &ruleInfo, rv);
+}
+
+already_AddRefed<Element>
+HTMLEditor::GetElementOrParentByTagName(const nsAString& aTagName,
+ nsINode* aNode)
+{
+ MOZ_ASSERT(!aTagName.IsEmpty());
+
+ nsCOMPtr<nsINode> node = aNode;
+ if (!node) {
+ // If no node supplied, get it from anchor node of current selection
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, nullptr);
+
+ nsCOMPtr<nsINode> anchorNode = selection->GetAnchorNode();
+ NS_ENSURE_TRUE(anchorNode, nullptr);
+
+ // Try to get the actual selected node
+ if (anchorNode->HasChildNodes() && anchorNode->IsContent()) {
+ node = anchorNode->GetChildAt(selection->AnchorOffset());
+ }
+ // Anchor node is probably a text node - just use that
+ if (!node) {
+ node = anchorNode;
+ }
+ }
+
+ nsCOMPtr<Element> current;
+ if (node->IsElement()) {
+ current = node->AsElement();
+ } else if (node->GetParentElement()) {
+ current = node->GetParentElement();
+ } else {
+ // Neither aNode nor its parent is an element, so no ancestor is
+ MOZ_ASSERT(!node->GetParentNode() ||
+ !node->GetParentNode()->GetParentNode());
+ return nullptr;
+ }
+
+ nsAutoString tagName(aTagName);
+ ToLowerCase(tagName);
+ bool getLink = IsLinkTag(tagName);
+ bool getNamedAnchor = IsNamedAnchorTag(tagName);
+ if (getLink || getNamedAnchor) {
+ tagName.Assign('a');
+ }
+ bool findTableCell = tagName.EqualsLiteral("td");
+ bool findList = tagName.EqualsLiteral("list");
+
+ for (; current; current = current->GetParentElement()) {
+ // Test if we have a link (an anchor with href set)
+ if ((getLink && HTMLEditUtils::IsLink(current)) ||
+ (getNamedAnchor && HTMLEditUtils::IsNamedAnchor(current))) {
+ return current.forget();
+ }
+ if (findList) {
+ // Match "ol", "ul", or "dl" for lists
+ if (HTMLEditUtils::IsList(current)) {
+ return current.forget();
+ }
+ } else if (findTableCell) {
+ // Table cells are another special case: match either "td" or "th"
+ if (HTMLEditUtils::IsTableCell(current)) {
+ return current.forget();
+ }
+ } else if (current->NodeName().Equals(tagName,
+ nsCaseInsensitiveStringComparator())) {
+ return current.forget();
+ }
+
+ // Stop searching if parent is a body tag. Note: Originally used IsRoot to
+ // stop at table cells, but that's too messy when you are trying to find
+ // the parent table
+ if (current->GetParentElement() &&
+ current->GetParentElement()->IsHTMLElement(nsGkAtoms::body)) {
+ break;
+ }
+ }
+
+ return nullptr;
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetElementOrParentByTagName(const nsAString& aTagName,
+ nsIDOMNode* aNode,
+ nsIDOMElement** aReturn)
+{
+ NS_ENSURE_TRUE(!aTagName.IsEmpty(), NS_ERROR_NULL_POINTER);
+ NS_ENSURE_TRUE(aReturn, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ nsCOMPtr<Element> parent =
+ GetElementOrParentByTagName(aTagName, node);
+ nsCOMPtr<nsIDOMElement> ret = do_QueryInterface(parent);
+
+ if (!ret) {
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+ }
+
+ ret.forget(aReturn);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetSelectedElement(const nsAString& aTagName,
+ nsIDOMElement** aReturn)
+{
+ NS_ENSURE_TRUE(aReturn , NS_ERROR_NULL_POINTER);
+
+ // default is null - no element found
+ *aReturn = nullptr;
+
+ // First look for a single element in selection
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+
+ bool bNodeFound = false;
+ bool isCollapsed = selection->Collapsed();
+
+ nsAutoString domTagName;
+ nsAutoString TagName(aTagName);
+ ToLowerCase(TagName);
+ // Empty string indicates we should match any element tag
+ bool anyTag = (TagName.IsEmpty());
+ bool isLinkTag = IsLinkTag(TagName);
+ bool isNamedAnchorTag = IsNamedAnchorTag(TagName);
+
+ nsCOMPtr<nsIDOMElement> selectedElement;
+ RefPtr<nsRange> range = selection->GetRangeAt(0);
+ NS_ENSURE_STATE(range);
+
+ nsCOMPtr<nsIDOMNode> startParent;
+ int32_t startOffset, endOffset;
+ nsresult rv = range->GetStartContainer(getter_AddRefs(startParent));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = range->GetStartOffset(&startOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDOMNode> endParent;
+ rv = range->GetEndContainer(getter_AddRefs(endParent));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = range->GetEndOffset(&endOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Optimization for a single selected element
+ if (startParent && startParent == endParent && endOffset - startOffset == 1) {
+ nsCOMPtr<nsIDOMNode> selectedNode = GetChildAt(startParent, startOffset);
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+ if (selectedNode) {
+ selectedNode->GetNodeName(domTagName);
+ ToLowerCase(domTagName);
+
+ // Test for appropriate node type requested
+ if (anyTag || (TagName == domTagName) ||
+ (isLinkTag && HTMLEditUtils::IsLink(selectedNode)) ||
+ (isNamedAnchorTag && HTMLEditUtils::IsNamedAnchor(selectedNode))) {
+ bNodeFound = true;
+ selectedElement = do_QueryInterface(selectedNode);
+ }
+ }
+ }
+
+ if (!bNodeFound) {
+ if (isLinkTag) {
+ // Link tag is a special case - we return the anchor node
+ // found for any selection that is totally within a link,
+ // included a collapsed selection (just a caret in a link)
+ nsCOMPtr<nsIDOMNode> anchorNode;
+ rv = selection->GetAnchorNode(getter_AddRefs(anchorNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+ int32_t anchorOffset = -1;
+ if (anchorNode) {
+ selection->GetAnchorOffset(&anchorOffset);
+ }
+
+ nsCOMPtr<nsIDOMNode> focusNode;
+ rv = selection->GetFocusNode(getter_AddRefs(focusNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+ int32_t focusOffset = -1;
+ if (focusNode) {
+ selection->GetFocusOffset(&focusOffset);
+ }
+
+ // Link node must be the same for both ends of selection
+ if (NS_SUCCEEDED(rv) && anchorNode) {
+ nsCOMPtr<nsIDOMElement> parentLinkOfAnchor;
+ rv = GetElementOrParentByTagName(NS_LITERAL_STRING("href"),
+ anchorNode,
+ getter_AddRefs(parentLinkOfAnchor));
+ // XXX: ERROR_HANDLING can parentLinkOfAnchor be null?
+ if (NS_SUCCEEDED(rv) && parentLinkOfAnchor) {
+ if (isCollapsed) {
+ // We have just a caret in the link
+ bNodeFound = true;
+ } else if (focusNode) {
+ // Link node must be the same for both ends of selection.
+ nsCOMPtr<nsIDOMElement> parentLinkOfFocus;
+ rv = GetElementOrParentByTagName(NS_LITERAL_STRING("href"),
+ focusNode,
+ getter_AddRefs(parentLinkOfFocus));
+ if (NS_SUCCEEDED(rv) && parentLinkOfFocus == parentLinkOfAnchor) {
+ bNodeFound = true;
+ }
+ }
+
+ // We found a link node parent
+ if (bNodeFound) {
+ // GetElementOrParentByTagName addref'd this, so we don't need to do it here
+ *aReturn = parentLinkOfAnchor;
+ NS_IF_ADDREF(*aReturn);
+ return NS_OK;
+ }
+ } else if (anchorOffset >= 0) {
+ // Check if link node is the only thing selected
+ nsCOMPtr<nsIDOMNode> anchorChild;
+ anchorChild = GetChildAt(anchorNode,anchorOffset);
+ if (anchorChild && HTMLEditUtils::IsLink(anchorChild) &&
+ anchorNode == focusNode && focusOffset == anchorOffset + 1) {
+ selectedElement = do_QueryInterface(anchorChild);
+ bNodeFound = true;
+ }
+ }
+ }
+ }
+
+ if (!isCollapsed) {
+ RefPtr<nsRange> currange = selection->GetRangeAt(0);
+ if (currange) {
+ nsCOMPtr<nsIContentIterator> iter =
+ do_CreateInstance("@mozilla.org/content/post-content-iterator;1",
+ &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ iter->Init(currange);
+ // loop through the content iterator for each content node
+ while (!iter->IsDone()) {
+ // Query interface to cast nsIContent to nsIDOMNode
+ // then get tagType to compare to aTagName
+ // Clone node of each desired type and append it to the aDomFrag
+ selectedElement = do_QueryInterface(iter->GetCurrentNode());
+ if (selectedElement) {
+ // If we already found a node, then we have another element,
+ // thus there's not just one element selected
+ if (bNodeFound) {
+ bNodeFound = false;
+ break;
+ }
+
+ selectedElement->GetNodeName(domTagName);
+ ToLowerCase(domTagName);
+
+ if (anyTag) {
+ // Get name of first selected element
+ selectedElement->GetTagName(TagName);
+ ToLowerCase(TagName);
+ anyTag = false;
+ }
+
+ // The "A" tag is a pain,
+ // used for both link(href is set) and "Named Anchor"
+ nsCOMPtr<nsIDOMNode> selectedNode = do_QueryInterface(selectedElement);
+ if ((isLinkTag &&
+ HTMLEditUtils::IsLink(selectedNode)) ||
+ (isNamedAnchorTag &&
+ HTMLEditUtils::IsNamedAnchor(selectedNode))) {
+ bNodeFound = true;
+ } else if (TagName == domTagName) { // All other tag names are handled here
+ bNodeFound = true;
+ }
+ if (!bNodeFound) {
+ // Check if node we have is really part of the selection???
+ break;
+ }
+ }
+ iter->Next();
+ }
+ } else {
+ // Should never get here?
+ isCollapsed = true;
+ NS_WARNING("isCollapsed was FALSE, but no elements found in selection\n");
+ }
+ }
+ }
+
+ if (!bNodeFound) {
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+ }
+
+ *aReturn = selectedElement;
+ if (selectedElement) {
+ // Getters must addref
+ NS_ADDREF(*aReturn);
+ }
+ return rv;
+}
+
+already_AddRefed<Element>
+HTMLEditor::CreateElementWithDefaults(const nsAString& aTagName)
+{
+ MOZ_ASSERT(!aTagName.IsEmpty());
+
+ nsAutoString tagName(aTagName);
+ ToLowerCase(tagName);
+ nsAutoString realTagName;
+
+ if (IsLinkTag(tagName) || IsNamedAnchorTag(tagName)) {
+ realTagName.Assign('a');
+ } else {
+ realTagName = tagName;
+ }
+ // We don't use editor's CreateElement because we don't want to go through
+ // the transaction system
+
+ // New call to use instead to get proper HTML element, bug 39919
+ nsCOMPtr<nsIAtom> realTagAtom = NS_Atomize(realTagName);
+ nsCOMPtr<Element> newElement = CreateHTMLContent(realTagAtom);
+ if (!newElement) {
+ return nullptr;
+ }
+
+ // Mark the new element dirty, so it will be formatted
+ ErrorResult rv;
+ newElement->SetAttribute(NS_LITERAL_STRING("_moz_dirty"), EmptyString(), rv);
+
+ // Set default values for new elements
+ if (tagName.EqualsLiteral("table")) {
+ newElement->SetAttribute(NS_LITERAL_STRING("cellpadding"),
+ NS_LITERAL_STRING("2"), rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ rv.SuppressException();
+ return nullptr;
+ }
+ newElement->SetAttribute(NS_LITERAL_STRING("cellspacing"),
+ NS_LITERAL_STRING("2"), rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ rv.SuppressException();
+ return nullptr;
+ }
+ newElement->SetAttribute(NS_LITERAL_STRING("border"),
+ NS_LITERAL_STRING("1"), rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ rv.SuppressException();
+ return nullptr;
+ }
+ } else if (tagName.EqualsLiteral("td")) {
+ nsresult rv =
+ SetAttributeOrEquivalent(
+ static_cast<nsIDOMElement*>(newElement->AsDOMNode()),
+ NS_LITERAL_STRING("valign"), NS_LITERAL_STRING("top"), true);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ }
+ // ADD OTHER TAGS HERE
+
+ return newElement.forget();
+}
+
+NS_IMETHODIMP
+HTMLEditor::CreateElementWithDefaults(const nsAString& aTagName,
+ nsIDOMElement** aReturn)
+{
+ NS_ENSURE_TRUE(!aTagName.IsEmpty() && aReturn, NS_ERROR_NULL_POINTER);
+ *aReturn = nullptr;
+
+ nsCOMPtr<Element> newElement = CreateElementWithDefaults(aTagName);
+ nsCOMPtr<nsIDOMElement> ret = do_QueryInterface(newElement);
+ NS_ENSURE_TRUE(ret, NS_ERROR_FAILURE);
+
+ ret.forget(aReturn);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::InsertLinkAroundSelection(nsIDOMElement* aAnchorElement)
+{
+ NS_ENSURE_TRUE(aAnchorElement, NS_ERROR_NULL_POINTER);
+
+ // We must have a real selection
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+
+ if (selection->Collapsed()) {
+ NS_WARNING("InsertLinkAroundSelection called but there is no selection!!!");
+ return NS_OK;
+ }
+
+ // Be sure we were given an anchor element
+ nsCOMPtr<nsIDOMHTMLAnchorElement> anchor = do_QueryInterface(aAnchorElement);
+ if (!anchor) {
+ return NS_OK;
+ }
+
+ nsAutoString href;
+ nsresult rv = anchor->GetHref(href);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (href.IsEmpty()) {
+ return NS_OK;
+ }
+
+ AutoEditBatch beginBatching(this);
+
+ // Set all attributes found on the supplied anchor element
+ nsCOMPtr<nsIDOMMozNamedAttrMap> attrMap;
+ aAnchorElement->GetAttributes(getter_AddRefs(attrMap));
+ NS_ENSURE_TRUE(attrMap, NS_ERROR_FAILURE);
+
+ uint32_t count;
+ attrMap->GetLength(&count);
+ nsAutoString name, value;
+
+ for (uint32_t i = 0; i < count; ++i) {
+ nsCOMPtr<nsIDOMAttr> attribute;
+ rv = attrMap->Item(i, getter_AddRefs(attribute));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (attribute) {
+ // We must clear the string buffers
+ // because GetName, GetValue appends to previous string!
+ name.Truncate();
+ value.Truncate();
+
+ rv = attribute->GetName(name);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = attribute->GetValue(value);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = SetInlineProperty(nsGkAtoms::a, name, value);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::SetHTMLBackgroundColor(const nsAString& aColor)
+{
+ NS_PRECONDITION(mDocWeak, "Missing Editor DOM Document");
+
+ // Find a selected or enclosing table element to set background on
+ nsCOMPtr<nsIDOMElement> element;
+ int32_t selectedCount;
+ nsAutoString tagName;
+ nsresult rv = GetSelectedOrParentTableElement(tagName, &selectedCount,
+ getter_AddRefs(element));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool setColor = !aColor.IsEmpty();
+
+ NS_NAMED_LITERAL_STRING(bgcolor, "bgcolor");
+ if (element) {
+ if (selectedCount > 0) {
+ // Traverse all selected cells
+ nsCOMPtr<nsIDOMElement> cell;
+ rv = GetFirstSelectedCell(nullptr, getter_AddRefs(cell));
+ if (NS_SUCCEEDED(rv) && cell) {
+ while (cell) {
+ rv = setColor ? SetAttribute(cell, bgcolor, aColor) :
+ RemoveAttribute(cell, bgcolor);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ GetNextSelectedCell(nullptr, getter_AddRefs(cell));
+ }
+ return NS_OK;
+ }
+ }
+ // If we failed to find a cell, fall through to use originally-found element
+ } else {
+ // No table element -- set the background color on the body tag
+ element = do_QueryInterface(GetRoot());
+ NS_ENSURE_TRUE(element, NS_ERROR_NULL_POINTER);
+ }
+ // Use the editor method that goes through the transaction system
+ return setColor ? SetAttribute(element, bgcolor, aColor) :
+ RemoveAttribute(element, bgcolor);
+}
+
+NS_IMETHODIMP
+HTMLEditor::SetBodyAttribute(const nsAString& aAttribute,
+ const nsAString& aValue)
+{
+ // TODO: Check selection for Cell, Row, Column or table and do color on appropriate level
+
+ NS_ASSERTION(mDocWeak, "Missing Editor DOM Document");
+
+ // Set the background color attribute on the body tag
+ nsCOMPtr<nsIDOMElement> bodyElement = do_QueryInterface(GetRoot());
+ NS_ENSURE_TRUE(bodyElement, NS_ERROR_NULL_POINTER);
+
+ // Use the editor method that goes through the transaction system
+ return SetAttribute(bodyElement, aAttribute, aValue);
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetLinkedObjects(nsIArray** aNodeList)
+{
+ NS_ENSURE_TRUE(aNodeList, NS_ERROR_NULL_POINTER);
+
+ nsresult rv;
+ nsCOMPtr<nsIMutableArray> nodes = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIContentIterator> iter =
+ do_CreateInstance("@mozilla.org/content/post-content-iterator;1", &rv);
+ NS_ENSURE_TRUE(iter, NS_ERROR_NULL_POINTER);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIDocument> doc = GetDocument();
+ NS_ENSURE_TRUE(doc, NS_ERROR_UNEXPECTED);
+
+ iter->Init(doc->GetRootElement());
+
+ // loop through the content iterator for each content node
+ while (!iter->IsDone()) {
+ nsCOMPtr<nsIDOMNode> node (do_QueryInterface(iter->GetCurrentNode()));
+ if (node) {
+ // Let nsURIRefObject make the hard decisions:
+ nsCOMPtr<nsIURIRefObject> refObject;
+ rv = NS_NewHTMLURIRefObject(getter_AddRefs(refObject), node);
+ if (NS_SUCCEEDED(rv)) {
+ nodes->AppendElement(refObject, false);
+ }
+ }
+ iter->Next();
+ }
+ }
+
+ nodes.forget(aNodeList);
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+HTMLEditor::AddStyleSheet(const nsAString& aURL)
+{
+ // Enable existing sheet if already loaded.
+ if (EnableExistingStyleSheet(aURL)) {
+ return NS_OK;
+ }
+
+ // Lose the previously-loaded sheet so there's nothing to replace
+ // This pattern is different from Override methods because
+ // we must wait to remove mLastStyleSheetURL and add new sheet
+ // at the same time (in StyleSheetLoaded callback) so they are undoable together
+ mLastStyleSheetURL.Truncate();
+ return ReplaceStyleSheet(aURL);
+}
+
+NS_IMETHODIMP
+HTMLEditor::ReplaceStyleSheet(const nsAString& aURL)
+{
+ // Enable existing sheet if already loaded.
+ if (EnableExistingStyleSheet(aURL)) {
+ // Disable last sheet if not the same as new one
+ if (!mLastStyleSheetURL.IsEmpty() && !mLastStyleSheetURL.Equals(aURL)) {
+ return EnableStyleSheet(mLastStyleSheetURL, false);
+ }
+ return NS_OK;
+ }
+
+ // Make sure the pres shell doesn't disappear during the load.
+ NS_ENSURE_TRUE(mDocWeak, NS_ERROR_NOT_INITIALIZED);
+ nsCOMPtr<nsIPresShell> ps = GetPresShell();
+ NS_ENSURE_TRUE(ps, NS_ERROR_NOT_INITIALIZED);
+
+ nsCOMPtr<nsIURI> uaURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(uaURI), aURL);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return ps->GetDocument()->CSSLoader()->
+ LoadSheet(uaURI, false, nullptr, EmptyCString(), this);
+}
+
+NS_IMETHODIMP
+HTMLEditor::RemoveStyleSheet(const nsAString& aURL)
+{
+ RefPtr<StyleSheet> sheet = GetStyleSheetForURL(aURL);
+ NS_ENSURE_TRUE(sheet, NS_ERROR_UNEXPECTED);
+
+ RefPtr<RemoveStyleSheetTransaction> transaction;
+ nsresult rv =
+ CreateTxnForRemoveStyleSheet(sheet, getter_AddRefs(transaction));
+ if (!transaction) {
+ rv = NS_ERROR_NULL_POINTER;
+ }
+ if (NS_SUCCEEDED(rv)) {
+ rv = DoTransaction(transaction);
+ if (NS_SUCCEEDED(rv)) {
+ mLastStyleSheetURL.Truncate(); // forget it
+ }
+ // Remove it from our internal list
+ rv = RemoveStyleSheetFromList(aURL);
+ }
+
+ return rv;
+}
+
+
+NS_IMETHODIMP
+HTMLEditor::AddOverrideStyleSheet(const nsAString& aURL)
+{
+ // Enable existing sheet if already loaded.
+ if (EnableExistingStyleSheet(aURL)) {
+ return NS_OK;
+ }
+
+ // Make sure the pres shell doesn't disappear during the load.
+ nsCOMPtr<nsIPresShell> ps = GetPresShell();
+ NS_ENSURE_TRUE(ps, NS_ERROR_NOT_INITIALIZED);
+
+ nsCOMPtr<nsIURI> uaURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(uaURI), aURL);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We MUST ONLY load synchronous local files (no @import)
+ // XXXbz Except this will actually try to load remote files
+ // synchronously, of course..
+ RefPtr<StyleSheet> sheet;
+ // Editor override style sheets may want to style Gecko anonymous boxes
+ rv = ps->GetDocument()->CSSLoader()->
+ LoadSheetSync(uaURI, mozilla::css::eAgentSheetFeatures, true,
+ &sheet);
+
+ // Synchronous loads should ALWAYS return completed
+ NS_ENSURE_TRUE(sheet, NS_ERROR_NULL_POINTER);
+
+ // Add the override style sheet
+ // (This checks if already exists)
+ ps->AddOverrideStyleSheet(sheet);
+
+ ps->RestyleForCSSRuleChanges();
+
+ // Save as the last-loaded sheet
+ mLastOverrideStyleSheetURL = aURL;
+
+ //Add URL and style sheet to our lists
+ return AddNewStyleSheetToList(aURL, sheet);
+}
+
+NS_IMETHODIMP
+HTMLEditor::ReplaceOverrideStyleSheet(const nsAString& aURL)
+{
+ // Enable existing sheet if already loaded.
+ if (EnableExistingStyleSheet(aURL)) {
+ // Disable last sheet if not the same as new one
+ if (!mLastOverrideStyleSheetURL.IsEmpty() &&
+ !mLastOverrideStyleSheetURL.Equals(aURL)) {
+ return EnableStyleSheet(mLastOverrideStyleSheetURL, false);
+ }
+ return NS_OK;
+ }
+ // Remove the previous sheet
+ if (!mLastOverrideStyleSheetURL.IsEmpty()) {
+ RemoveOverrideStyleSheet(mLastOverrideStyleSheetURL);
+ }
+ return AddOverrideStyleSheet(aURL);
+}
+
+// Do NOT use transaction system for override style sheets
+NS_IMETHODIMP
+HTMLEditor::RemoveOverrideStyleSheet(const nsAString& aURL)
+{
+ RefPtr<StyleSheet> sheet = GetStyleSheetForURL(aURL);
+
+ // Make sure we remove the stylesheet from our internal list in all
+ // cases.
+ nsresult rv = RemoveStyleSheetFromList(aURL);
+
+ NS_ENSURE_TRUE(sheet, NS_OK); /// Don't fail if sheet not found
+
+ NS_ENSURE_TRUE(mDocWeak, NS_ERROR_NOT_INITIALIZED);
+ nsCOMPtr<nsIPresShell> ps = GetPresShell();
+ NS_ENSURE_TRUE(ps, NS_ERROR_NOT_INITIALIZED);
+
+ ps->RemoveOverrideStyleSheet(sheet);
+ ps->RestyleForCSSRuleChanges();
+
+ // Remove it from our internal list
+ return rv;
+}
+
+NS_IMETHODIMP
+HTMLEditor::EnableStyleSheet(const nsAString& aURL,
+ bool aEnable)
+{
+ RefPtr<StyleSheet> sheet = GetStyleSheetForURL(aURL);
+ NS_ENSURE_TRUE(sheet, NS_OK); // Don't fail if sheet not found
+
+ // Ensure the style sheet is owned by our document.
+ nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak);
+ sheet->SetOwningDocument(doc);
+
+ if (sheet->IsServo()) {
+ // XXXheycam ServoStyleSheets don't support being enabled/disabled yet.
+ NS_ERROR("stylo: ServoStyleSheets can't be disabled yet");
+ return NS_ERROR_FAILURE;
+ }
+ return sheet->AsGecko()->SetDisabled(!aEnable);
+}
+
+bool
+HTMLEditor::EnableExistingStyleSheet(const nsAString& aURL)
+{
+ RefPtr<StyleSheet> sheet = GetStyleSheetForURL(aURL);
+
+ // Enable sheet if already loaded.
+ if (!sheet) {
+ return false;
+ }
+
+ // Ensure the style sheet is owned by our document.
+ nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak);
+ sheet->SetOwningDocument(doc);
+
+ if (sheet->IsServo()) {
+ // XXXheycam ServoStyleSheets don't support being enabled/disabled yet.
+ NS_ERROR("stylo: ServoStyleSheets can't be disabled yet");
+ return true;
+ }
+ sheet->AsGecko()->SetDisabled(false);
+ return true;
+}
+
+nsresult
+HTMLEditor::AddNewStyleSheetToList(const nsAString& aURL,
+ StyleSheet* aStyleSheet)
+{
+ uint32_t countSS = mStyleSheets.Length();
+ uint32_t countU = mStyleSheetURLs.Length();
+
+ if (countSS != countU) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (!mStyleSheetURLs.AppendElement(aURL)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ return mStyleSheets.AppendElement(aStyleSheet) ? NS_OK : NS_ERROR_UNEXPECTED;
+}
+
+nsresult
+HTMLEditor::RemoveStyleSheetFromList(const nsAString& aURL)
+{
+ // is it already in the list?
+ size_t foundIndex;
+ foundIndex = mStyleSheetURLs.IndexOf(aURL);
+ if (foundIndex == mStyleSheetURLs.NoIndex) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Attempt both removals; if one fails there's not much we can do.
+ mStyleSheets.RemoveElementAt(foundIndex);
+ mStyleSheetURLs.RemoveElementAt(foundIndex);
+
+ return NS_OK;
+}
+
+StyleSheet*
+HTMLEditor::GetStyleSheetForURL(const nsAString& aURL)
+{
+ // is it already in the list?
+ size_t foundIndex;
+ foundIndex = mStyleSheetURLs.IndexOf(aURL);
+ if (foundIndex == mStyleSheetURLs.NoIndex) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(mStyleSheets[foundIndex]);
+ return mStyleSheets[foundIndex];
+}
+
+void
+HTMLEditor::GetURLForStyleSheet(StyleSheet* aStyleSheet,
+ nsAString& aURL)
+{
+ // is it already in the list?
+ int32_t foundIndex = mStyleSheets.IndexOf(aStyleSheet);
+
+ // Don't fail if we don't find it in our list
+ if (foundIndex == -1) {
+ return;
+ }
+
+ // Found it in the list!
+ aURL = mStyleSheetURLs[foundIndex];
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetEmbeddedObjects(nsIArray** aNodeList)
+{
+ NS_ENSURE_TRUE(aNodeList, NS_ERROR_NULL_POINTER);
+
+ nsresult rv;
+ nsCOMPtr<nsIMutableArray> nodes = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIContentIterator> iter =
+ do_CreateInstance("@mozilla.org/content/post-content-iterator;1", &rv);
+ NS_ENSURE_TRUE(iter, NS_ERROR_NULL_POINTER);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDocument> doc = GetDocument();
+ NS_ENSURE_TRUE(doc, NS_ERROR_UNEXPECTED);
+
+ iter->Init(doc->GetRootElement());
+
+ // Loop through the content iterator for each content node.
+ while (!iter->IsDone()) {
+ nsINode* node = iter->GetCurrentNode();
+ if (node->IsElement()) {
+ dom::Element* element = node->AsElement();
+
+ // See if it's an image or an embed and also include all links.
+ // Let mail decide which link to send or not
+ if (element->IsAnyOfHTMLElements(nsGkAtoms::img, nsGkAtoms::embed,
+ nsGkAtoms::a) ||
+ (element->IsHTMLElement(nsGkAtoms::body) &&
+ element->HasAttr(kNameSpaceID_None, nsGkAtoms::background))) {
+ nsCOMPtr<nsIDOMNode> domNode = do_QueryInterface(node);
+ nodes->AppendElement(domNode, false);
+ }
+ }
+ iter->Next();
+ }
+
+ nodes.forget(aNodeList);
+ return rv;
+}
+
+NS_IMETHODIMP
+HTMLEditor::DeleteSelectionImpl(EDirection aAction,
+ EStripWrappers aStripWrappers)
+{
+ MOZ_ASSERT(aStripWrappers == eStrip || aStripWrappers == eNoStrip);
+
+ nsresult rv = EditorBase::DeleteSelectionImpl(aAction, aStripWrappers);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If we weren't asked to strip any wrappers, we're done.
+ if (aStripWrappers == eNoStrip) {
+ return NS_OK;
+ }
+
+ RefPtr<Selection> selection = GetSelection();
+ // Just checking that the selection itself is collapsed doesn't seem to work
+ // right in the multi-range case
+ NS_ENSURE_STATE(selection);
+ NS_ENSURE_STATE(selection->GetAnchorFocusRange());
+ NS_ENSURE_STATE(selection->GetAnchorFocusRange()->Collapsed());
+
+ NS_ENSURE_STATE(selection->GetAnchorNode()->IsContent());
+ nsCOMPtr<nsIContent> content = selection->GetAnchorNode()->AsContent();
+
+ // Don't strip wrappers if this is the only wrapper in the block. Then we'll
+ // add a <br> later, so it won't be an empty wrapper in the end.
+ nsCOMPtr<nsIContent> blockParent = content;
+ while (blockParent && !IsBlockNode(blockParent)) {
+ blockParent = blockParent->GetParent();
+ }
+ if (!blockParent) {
+ return NS_OK;
+ }
+ bool emptyBlockParent;
+ rv = IsEmptyNode(blockParent, &emptyBlockParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (emptyBlockParent) {
+ return NS_OK;
+ }
+
+ if (content && !IsBlockNode(content) && !content->Length() &&
+ content->IsEditable() && content != content->GetEditingHost()) {
+ while (content->GetParent() && !IsBlockNode(content->GetParent()) &&
+ content->GetParent()->Length() == 1 &&
+ content->GetParent()->IsEditable() &&
+ content->GetParent() != content->GetEditingHost()) {
+ content = content->GetParent();
+ }
+ rv = DeleteNode(content);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::DeleteNode(nsINode* aNode)
+{
+ nsCOMPtr<nsIDOMNode> node = do_QueryInterface(aNode);
+ return DeleteNode(node);
+}
+
+NS_IMETHODIMP
+HTMLEditor::DeleteNode(nsIDOMNode* aNode)
+{
+ // do nothing if the node is read-only
+ nsCOMPtr<nsIContent> content = do_QueryInterface(aNode);
+ if (!IsModifiableNode(aNode) && !IsMozEditorBogusNode(content)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return EditorBase::DeleteNode(aNode);
+}
+
+nsresult
+HTMLEditor::DeleteText(nsGenericDOMDataNode& aCharData,
+ uint32_t aOffset,
+ uint32_t aLength)
+{
+ // Do nothing if the node is read-only
+ if (!IsModifiableNode(&aCharData)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return EditorBase::DeleteText(aCharData, aOffset, aLength);
+}
+
+nsresult
+HTMLEditor::InsertTextImpl(const nsAString& aStringToInsert,
+ nsCOMPtr<nsINode>* aInOutNode,
+ int32_t* aInOutOffset,
+ nsIDocument* aDoc)
+{
+ // Do nothing if the node is read-only
+ if (!IsModifiableNode(*aInOutNode)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return EditorBase::InsertTextImpl(aStringToInsert, aInOutNode, aInOutOffset,
+ aDoc);
+}
+
+void
+HTMLEditor::ContentAppended(nsIDocument* aDocument,
+ nsIContent* aContainer,
+ nsIContent* aFirstNewContent,
+ int32_t aIndexInContainer)
+{
+ DoContentInserted(aDocument, aContainer, aFirstNewContent, aIndexInContainer,
+ eAppended);
+}
+
+void
+HTMLEditor::ContentInserted(nsIDocument* aDocument,
+ nsIContent* aContainer,
+ nsIContent* aChild,
+ int32_t aIndexInContainer)
+{
+ DoContentInserted(aDocument, aContainer, aChild, aIndexInContainer,
+ eInserted);
+}
+
+bool
+HTMLEditor::IsInObservedSubtree(nsIDocument* aDocument,
+ nsIContent* aContainer,
+ nsIContent* aChild)
+{
+ if (!aChild) {
+ return false;
+ }
+
+ Element* root = GetRoot();
+ // To be super safe here, check both ChromeOnlyAccess and GetBindingParent.
+ // That catches (also unbound) native anonymous content, XBL and ShadowDOM.
+ if (root &&
+ (root->ChromeOnlyAccess() != aChild->ChromeOnlyAccess() ||
+ root->GetBindingParent() != aChild->GetBindingParent())) {
+ return false;
+ }
+
+ return !aChild->ChromeOnlyAccess() && !aChild->GetBindingParent();
+}
+
+void
+HTMLEditor::DoContentInserted(nsIDocument* aDocument,
+ nsIContent* aContainer,
+ nsIContent* aChild,
+ int32_t aIndexInContainer,
+ InsertedOrAppended aInsertedOrAppended)
+{
+ if (!IsInObservedSubtree(aDocument, aContainer, aChild)) {
+ return;
+ }
+
+ nsCOMPtr<nsIHTMLEditor> kungFuDeathGrip(this);
+
+ if (ShouldReplaceRootElement()) {
+ UpdateRootElement();
+ nsContentUtils::AddScriptRunner(NewRunnableMethod(
+ this, &HTMLEditor::NotifyRootChanged));
+ }
+ // We don't need to handle our own modifications
+ else if (!mAction && (aContainer ? aContainer->IsEditable() : aDocument->IsEditable())) {
+ if (IsMozEditorBogusNode(aChild)) {
+ // Ignore insertion of the bogus node
+ return;
+ }
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+ rules->DocumentModified();
+
+ // Update spellcheck for only the newly-inserted node (bug 743819)
+ if (mInlineSpellChecker) {
+ RefPtr<nsRange> range = new nsRange(aChild);
+ int32_t endIndex = aIndexInContainer + 1;
+ if (aInsertedOrAppended == eAppended) {
+ // Count all the appended nodes
+ nsIContent* sibling = aChild->GetNextSibling();
+ while (sibling) {
+ endIndex++;
+ sibling = sibling->GetNextSibling();
+ }
+ }
+ nsresult rv = range->Set(aContainer, aIndexInContainer,
+ aContainer, endIndex);
+ if (NS_SUCCEEDED(rv)) {
+ mInlineSpellChecker->SpellCheckRange(range);
+ }
+ }
+ }
+}
+
+void
+HTMLEditor::ContentRemoved(nsIDocument* aDocument,
+ nsIContent* aContainer,
+ nsIContent* aChild,
+ int32_t aIndexInContainer,
+ nsIContent* aPreviousSibling)
+{
+ if (!IsInObservedSubtree(aDocument, aContainer, aChild)) {
+ return;
+ }
+
+ nsCOMPtr<nsIHTMLEditor> kungFuDeathGrip(this);
+
+ if (SameCOMIdentity(aChild, mRootElement)) {
+ mRootElement = nullptr;
+ nsContentUtils::AddScriptRunner(NewRunnableMethod(
+ this, &HTMLEditor::NotifyRootChanged));
+ }
+ // We don't need to handle our own modifications
+ else if (!mAction && (aContainer ? aContainer->IsEditable() : aDocument->IsEditable())) {
+ if (aChild && IsMozEditorBogusNode(aChild)) {
+ // Ignore removal of the bogus node
+ return;
+ }
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+ rules->DocumentModified();
+ }
+}
+
+NS_IMETHODIMP_(bool)
+HTMLEditor::IsModifiableNode(nsIDOMNode* aNode)
+{
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ return IsModifiableNode(node);
+}
+
+bool
+HTMLEditor::IsModifiableNode(nsINode* aNode)
+{
+ return !aNode || aNode->IsEditable();
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetIsSelectionEditable(bool* aIsSelectionEditable)
+{
+ MOZ_ASSERT(aIsSelectionEditable);
+
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+
+ // Per the editing spec as of June 2012: we have to have a selection whose
+ // start and end nodes are editable, and which share an ancestor editing
+ // host. (Bug 766387.)
+ *aIsSelectionEditable = selection->RangeCount() &&
+ selection->GetAnchorNode()->IsEditable() &&
+ selection->GetFocusNode()->IsEditable();
+
+ if (*aIsSelectionEditable) {
+ nsINode* commonAncestor =
+ selection->GetAnchorFocusRange()->GetCommonAncestor();
+ while (commonAncestor && !commonAncestor->IsEditable()) {
+ commonAncestor = commonAncestor->GetParentNode();
+ }
+ if (!commonAncestor) {
+ // No editable common ancestor
+ *aIsSelectionEditable = false;
+ }
+ }
+
+ return NS_OK;
+}
+
+static nsresult
+SetSelectionAroundHeadChildren(Selection* aSelection,
+ nsIWeakReference* aDocWeak)
+{
+ // Set selection around <head> node
+ nsCOMPtr<nsIDocument> doc = do_QueryReferent(aDocWeak);
+ NS_ENSURE_TRUE(doc, NS_ERROR_NOT_INITIALIZED);
+
+ dom::Element* headNode = doc->GetHeadElement();
+ NS_ENSURE_STATE(headNode);
+
+ // Collapse selection to before first child of the head,
+ nsresult rv = aSelection->CollapseNative(headNode, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Then extend it to just after.
+ uint32_t childCount = headNode->GetChildCount();
+ return aSelection->ExtendNative(headNode, childCount + 1);
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetHeadContentsAsHTML(nsAString& aOutputString)
+{
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+
+ // Save current selection
+ AutoSelectionRestorer selectionRestorer(selection, this);
+
+ nsresult rv = SetSelectionAroundHeadChildren(selection, mDocWeak);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = OutputToString(NS_LITERAL_STRING("text/html"),
+ nsIDocumentEncoder::OutputSelectionOnly,
+ aOutputString);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Selection always includes <body></body>,
+ // so terminate there
+ nsReadingIterator<char16_t> findIter,endFindIter;
+ aOutputString.BeginReading(findIter);
+ aOutputString.EndReading(endFindIter);
+ //counting on our parser to always lower case!!!
+ if (CaseInsensitiveFindInReadable(NS_LITERAL_STRING("<body"),
+ findIter, endFindIter)) {
+ nsReadingIterator<char16_t> beginIter;
+ aOutputString.BeginReading(beginIter);
+ int32_t offset = Distance(beginIter, findIter);//get the distance
+
+ nsWritingIterator<char16_t> writeIter;
+ aOutputString.BeginWriting(writeIter);
+ // Ensure the string ends in a newline
+ char16_t newline ('\n');
+ findIter.advance(-1);
+ if (!offset || (offset > 0 && (*findIter) != newline)) {
+ writeIter.advance(offset);
+ *writeIter = newline;
+ aOutputString.Truncate(offset+1);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::DebugUnitTests(int32_t* outNumTests,
+ int32_t* outNumTestsFailed)
+{
+#ifdef DEBUG
+ NS_ENSURE_TRUE(outNumTests && outNumTestsFailed, NS_ERROR_NULL_POINTER);
+
+ TextEditorTest *tester = new TextEditorTest();
+ NS_ENSURE_TRUE(tester, NS_ERROR_OUT_OF_MEMORY);
+
+ tester->Run(this, outNumTests, outNumTestsFailed);
+ delete tester;
+ return NS_OK;
+#else
+ return NS_ERROR_NOT_IMPLEMENTED;
+#endif
+}
+
+NS_IMETHODIMP
+HTMLEditor::StyleSheetLoaded(StyleSheet* aSheet,
+ bool aWasAlternate,
+ nsresult aStatus)
+{
+ nsresult rv = NS_OK;
+ AutoEditBatch batchIt(this);
+
+ if (!mLastStyleSheetURL.IsEmpty())
+ RemoveStyleSheet(mLastStyleSheetURL);
+
+ RefPtr<AddStyleSheetTransaction> transaction;
+ rv = CreateTxnForAddStyleSheet(aSheet, getter_AddRefs(transaction));
+ if (!transaction) {
+ rv = NS_ERROR_NULL_POINTER;
+ }
+ if (NS_SUCCEEDED(rv)) {
+ rv = DoTransaction(transaction);
+ if (NS_SUCCEEDED(rv)) {
+ // Get the URI, then url spec from the sheet
+ nsAutoCString spec;
+ rv = aSheet->GetSheetURI()->GetSpec(spec);
+
+ if (NS_SUCCEEDED(rv)) {
+ // Save it so we can remove before applying the next one
+ mLastStyleSheetURL.AssignWithConversion(spec.get());
+
+ // Also save in our arrays of urls and sheets
+ AddNewStyleSheetToList(mLastStyleSheetURL, aSheet);
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+/**
+ * All editor operations which alter the doc should be prefaced
+ * with a call to StartOperation, naming the action and direction.
+ */
+NS_IMETHODIMP
+HTMLEditor::StartOperation(EditAction opID,
+ nsIEditor::EDirection aDirection)
+{
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+
+ EditorBase::StartOperation(opID, aDirection); // will set mAction, mDirection
+ if (rules) {
+ return rules->BeforeEdit(mAction, mDirection);
+ }
+ return NS_OK;
+}
+
+/**
+ * All editor operations which alter the doc should be followed
+ * with a call to EndOperation.
+ */
+NS_IMETHODIMP
+HTMLEditor::EndOperation()
+{
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+
+ // post processing
+ nsresult rv = rules ? rules->AfterEdit(mAction, mDirection) : NS_OK;
+ EditorBase::EndOperation(); // will clear mAction, mDirection
+ return rv;
+}
+
+bool
+HTMLEditor::TagCanContainTag(nsIAtom& aParentTag,
+ nsIAtom& aChildTag)
+{
+ nsIParserService* parserService = nsContentUtils::GetParserService();
+
+ int32_t childTagEnum;
+ // XXX Should this handle #cdata-section too?
+ if (&aChildTag == nsGkAtoms::textTagName) {
+ childTagEnum = eHTMLTag_text;
+ } else {
+ childTagEnum = parserService->HTMLAtomTagToId(&aChildTag);
+ }
+
+ int32_t parentTagEnum = parserService->HTMLAtomTagToId(&aParentTag);
+ return HTMLEditUtils::CanContain(parentTagEnum, childTagEnum);
+}
+
+bool
+HTMLEditor::IsContainer(nsINode* aNode)
+{
+ MOZ_ASSERT(aNode);
+
+ int32_t tagEnum;
+ // XXX Should this handle #cdata-section too?
+ if (aNode->IsNodeOfType(nsINode::eTEXT)) {
+ tagEnum = eHTMLTag_text;
+ } else {
+ tagEnum =
+ nsContentUtils::GetParserService()->HTMLStringTagToId(aNode->NodeName());
+ }
+
+ return HTMLEditUtils::IsContainer(tagEnum);
+}
+
+bool
+HTMLEditor::IsContainer(nsIDOMNode* aNode)
+{
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ if (!node) {
+ return false;
+ }
+ return IsContainer(node);
+}
+
+
+nsresult
+HTMLEditor::SelectEntireDocument(Selection* aSelection)
+{
+ if (!aSelection || !mRules) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+
+ // get editor root node
+ nsCOMPtr<nsIDOMElement> rootElement = do_QueryInterface(GetRoot());
+
+ // is doc empty?
+ bool bDocIsEmpty;
+ nsresult rv = rules->DocumentIsEmpty(&bDocIsEmpty);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (bDocIsEmpty) {
+ // if its empty dont select entire doc - that would select the bogus node
+ return aSelection->Collapse(rootElement, 0);
+ }
+
+ return EditorBase::SelectEntireDocument(aSelection);
+}
+
+NS_IMETHODIMP
+HTMLEditor::SelectAll()
+{
+ ForceCompositionEnd();
+
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_STATE(selection);
+
+ nsCOMPtr<nsIDOMNode> anchorNode;
+ nsresult rv = selection->GetAnchorNode(getter_AddRefs(anchorNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIContent> anchorContent = do_QueryInterface(anchorNode, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsIContent *rootContent;
+ if (anchorContent->HasIndependentSelection()) {
+ rv = selection->SetAncestorLimiter(nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rootContent = mRootElement;
+ } else {
+ nsCOMPtr<nsIPresShell> ps = GetPresShell();
+ rootContent = anchorContent->GetSelectionRootContent(ps);
+ }
+
+ NS_ENSURE_TRUE(rootContent, NS_ERROR_UNEXPECTED);
+
+ nsCOMPtr<nsIDOMNode> rootElement = do_QueryInterface(rootContent, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ Maybe<mozilla::dom::Selection::AutoUserInitiated> userSelection;
+ if (!rootContent->IsEditable()) {
+ userSelection.emplace(selection);
+ }
+ return selection->SelectAllChildren(rootElement);
+}
+
+// this will NOT find aAttribute unless aAttribute has a non-null value
+// so singleton attributes like <Table border> will not be matched!
+bool
+HTMLEditor::IsTextPropertySetByContent(nsINode* aNode,
+ nsIAtom* aProperty,
+ const nsAString* aAttribute,
+ const nsAString* aValue,
+ nsAString* outValue)
+{
+ MOZ_ASSERT(aNode && aProperty);
+ bool isSet;
+ IsTextPropertySetByContent(aNode->AsDOMNode(), aProperty, aAttribute, aValue,
+ isSet, outValue);
+ return isSet;
+}
+
+void
+HTMLEditor::IsTextPropertySetByContent(nsIDOMNode* aNode,
+ nsIAtom* aProperty,
+ const nsAString* aAttribute,
+ const nsAString* aValue,
+ bool& aIsSet,
+ nsAString* outValue)
+{
+ aIsSet = false; // must be initialized to false for code below to work
+ nsAutoString propName;
+ aProperty->ToString(propName);
+ nsCOMPtr<nsIDOMNode>node = aNode;
+
+ while (node) {
+ nsCOMPtr<nsIDOMElement>element;
+ element = do_QueryInterface(node);
+ if (element) {
+ nsAutoString tag, value;
+ element->GetTagName(tag);
+ if (propName.Equals(tag, nsCaseInsensitiveStringComparator())) {
+ bool found = false;
+ if (aAttribute && !aAttribute->IsEmpty()) {
+ element->GetAttribute(*aAttribute, value);
+ if (outValue) {
+ *outValue = value;
+ }
+ if (!value.IsEmpty()) {
+ if (!aValue) {
+ found = true;
+ } else {
+ nsString tString(*aValue);
+ if (tString.Equals(value, nsCaseInsensitiveStringComparator())) {
+ found = true;
+ } else {
+ // We found the prop with the attribute, but the value doesn't
+ // match.
+ break;
+ }
+ }
+ }
+ } else {
+ found = true;
+ }
+ if (found) {
+ aIsSet = true;
+ break;
+ }
+ }
+ }
+ nsCOMPtr<nsIDOMNode>temp;
+ if (NS_SUCCEEDED(node->GetParentNode(getter_AddRefs(temp))) && temp) {
+ node = temp;
+ } else {
+ node = nullptr;
+ }
+ }
+}
+
+bool
+HTMLEditor::SetCaretInTableCell(nsIDOMElement* aElement)
+{
+ nsCOMPtr<dom::Element> element = do_QueryInterface(aElement);
+ if (!element || !element->IsHTMLElement() ||
+ !HTMLEditUtils::IsTableElement(element) ||
+ !IsDescendantOfEditorRoot(element)) {
+ return false;
+ }
+
+ nsIContent* node = element;
+ while (node->HasChildren()) {
+ node = node->GetFirstChild();
+ }
+
+ // Set selection at beginning of the found node
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, false);
+
+ return NS_SUCCEEDED(selection->CollapseNative(node, 0));
+}
+
+/**
+ * GetEnclosingTable() finds ancestor who is a table, if any.
+ */
+Element*
+HTMLEditor::GetEnclosingTable(nsINode* aNode)
+{
+ MOZ_ASSERT(aNode);
+
+ for (nsCOMPtr<Element> block = GetBlockNodeParent(aNode);
+ block;
+ block = GetBlockNodeParent(block)) {
+ if (HTMLEditUtils::IsTable(block)) {
+ return block;
+ }
+ }
+ return nullptr;
+}
+
+nsIDOMNode*
+HTMLEditor::GetEnclosingTable(nsIDOMNode* aNode)
+{
+ NS_PRECONDITION(aNode, "null node passed to HTMLEditor::GetEnclosingTable");
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ NS_ENSURE_TRUE(node, nullptr);
+ nsCOMPtr<Element> table = GetEnclosingTable(node);
+ nsCOMPtr<nsIDOMNode> ret = do_QueryInterface(table);
+ return ret;
+}
+
+
+/**
+ * This method scans the selection for adjacent text nodes
+ * and collapses them into a single text node.
+ * "adjacent" means literally adjacent siblings of the same parent.
+ * Uses EditorBase::JoinNodes so action is undoable.
+ * Should be called within the context of a batch transaction.
+ */
+nsresult
+HTMLEditor::CollapseAdjacentTextNodes(nsRange* aInRange)
+{
+ NS_ENSURE_TRUE(aInRange, NS_ERROR_NULL_POINTER);
+ AutoTransactionsConserveSelection dontSpazMySelection(this);
+ nsTArray<nsCOMPtr<nsIDOMNode> > textNodes;
+ // we can't actually do anything during iteration, so store the text nodes in an array
+ // don't bother ref counting them because we know we can hold them for the
+ // lifetime of this method
+
+
+ // build a list of editable text nodes
+ nsresult rv = NS_ERROR_UNEXPECTED;
+ nsCOMPtr<nsIContentIterator> iter =
+ do_CreateInstance("@mozilla.org/content/subtree-content-iterator;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ iter->Init(aInRange);
+
+ while (!iter->IsDone()) {
+ nsINode* node = iter->GetCurrentNode();
+ if (node->NodeType() == nsIDOMNode::TEXT_NODE &&
+ IsEditable(static_cast<nsIContent*>(node))) {
+ nsCOMPtr<nsIDOMNode> domNode = do_QueryInterface(node);
+ textNodes.AppendElement(domNode);
+ }
+
+ iter->Next();
+ }
+
+ // now that I have a list of text nodes, collapse adjacent text nodes
+ // NOTE: assumption that JoinNodes keeps the righthand node
+ while (textNodes.Length() > 1) {
+ // we assume a textNodes entry can't be nullptr
+ nsIDOMNode *leftTextNode = textNodes[0];
+ nsIDOMNode *rightTextNode = textNodes[1];
+ NS_ASSERTION(leftTextNode && rightTextNode,"left or rightTextNode null in CollapseAdjacentTextNodes");
+
+ // get the prev sibling of the right node, and see if its leftTextNode
+ nsCOMPtr<nsIDOMNode> prevSibOfRightNode;
+ rv = rightTextNode->GetPreviousSibling(getter_AddRefs(prevSibOfRightNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (prevSibOfRightNode && prevSibOfRightNode == leftTextNode) {
+ nsCOMPtr<nsIDOMNode> parent;
+ rv = rightTextNode->GetParentNode(getter_AddRefs(parent));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(parent, NS_ERROR_NULL_POINTER);
+ rv = JoinNodes(leftTextNode, rightTextNode, parent);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ textNodes.RemoveElementAt(0); // remove the leftmost text node from the list
+ }
+
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::SetSelectionAtDocumentStart(Selection* aSelection)
+{
+ dom::Element* rootElement = GetRoot();
+ NS_ENSURE_TRUE(rootElement, NS_ERROR_NULL_POINTER);
+
+ return aSelection->CollapseNative(rootElement, 0);
+}
+
+/**
+ * Remove aNode, reparenting any children into the parent of aNode. In
+ * addition, insert any br's needed to preserve identity of removed block.
+ */
+nsresult
+HTMLEditor::RemoveBlockContainer(nsIContent& aNode)
+{
+ // Two possibilities: the container could be empty of editable content. If
+ // that is the case, we need to compare what is before and after aNode to
+ // determine if we need a br.
+ //
+ // Or it could be not empty, in which case we have to compare previous
+ // sibling and first child to determine if we need a leading br, and compare
+ // following sibling and last child to determine if we need a trailing br.
+
+ nsCOMPtr<nsIContent> child = GetFirstEditableChild(aNode);
+
+ if (child) {
+ // The case of aNode not being empty. We need a br at start unless:
+ // 1) previous sibling of aNode is a block, OR
+ // 2) previous sibling of aNode is a br, OR
+ // 3) first child of aNode is a block OR
+ // 4) either is null
+
+ nsCOMPtr<nsIContent> sibling = GetPriorHTMLSibling(&aNode);
+ if (sibling && !IsBlockNode(sibling) &&
+ !sibling->IsHTMLElement(nsGkAtoms::br) && !IsBlockNode(child)) {
+ // Insert br node
+ nsCOMPtr<Element> br = CreateBR(&aNode, 0);
+ NS_ENSURE_STATE(br);
+ }
+
+ // We need a br at end unless:
+ // 1) following sibling of aNode is a block, OR
+ // 2) last child of aNode is a block, OR
+ // 3) last child of aNode is a br OR
+ // 4) either is null
+
+ sibling = GetNextHTMLSibling(&aNode);
+ if (sibling && !IsBlockNode(sibling)) {
+ child = GetLastEditableChild(aNode);
+ MOZ_ASSERT(child, "aNode has first editable child but not last?");
+ if (!IsBlockNode(child) && !child->IsHTMLElement(nsGkAtoms::br)) {
+ // Insert br node
+ nsCOMPtr<Element> br = CreateBR(&aNode, aNode.Length());
+ NS_ENSURE_STATE(br);
+ }
+ }
+ } else {
+ // The case of aNode being empty. We need a br at start unless:
+ // 1) previous sibling of aNode is a block, OR
+ // 2) previous sibling of aNode is a br, OR
+ // 3) following sibling of aNode is a block, OR
+ // 4) following sibling of aNode is a br OR
+ // 5) either is null
+ nsCOMPtr<nsIContent> sibling = GetPriorHTMLSibling(&aNode);
+ if (sibling && !IsBlockNode(sibling) &&
+ !sibling->IsHTMLElement(nsGkAtoms::br)) {
+ sibling = GetNextHTMLSibling(&aNode);
+ if (sibling && !IsBlockNode(sibling) &&
+ !sibling->IsHTMLElement(nsGkAtoms::br)) {
+ // Insert br node
+ nsCOMPtr<Element> br = CreateBR(&aNode, 0);
+ NS_ENSURE_STATE(br);
+ }
+ }
+ }
+
+ // Now remove container
+ nsresult rv = RemoveContainer(&aNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+/**
+ * GetPriorHTMLSibling() returns the previous editable sibling, if there is
+ * one within the parent.
+ */
+nsIContent*
+HTMLEditor::GetPriorHTMLSibling(nsINode* aNode)
+{
+ MOZ_ASSERT(aNode);
+
+ nsIContent* node = aNode->GetPreviousSibling();
+ while (node && !IsEditable(node)) {
+ node = node->GetPreviousSibling();
+ }
+
+ return node;
+}
+
+nsresult
+HTMLEditor::GetPriorHTMLSibling(nsIDOMNode* inNode,
+ nsCOMPtr<nsIDOMNode>* outNode)
+{
+ NS_ENSURE_TRUE(outNode, NS_ERROR_NULL_POINTER);
+ *outNode = nullptr;
+
+ nsCOMPtr<nsINode> node = do_QueryInterface(inNode);
+ NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER);
+
+ *outNode = do_QueryInterface(GetPriorHTMLSibling(node));
+ return NS_OK;
+}
+
+/**
+ * GetPriorHTMLSibling() returns the previous editable sibling, if there is
+ * one within the parent. just like above routine but takes a parent/offset
+ * instead of a node.
+ */
+nsIContent*
+HTMLEditor::GetPriorHTMLSibling(nsINode* aParent,
+ int32_t aOffset)
+{
+ MOZ_ASSERT(aParent);
+
+ nsIContent* node = aParent->GetChildAt(aOffset - 1);
+ if (!node || IsEditable(node)) {
+ return node;
+ }
+
+ return GetPriorHTMLSibling(node);
+}
+
+nsresult
+HTMLEditor::GetPriorHTMLSibling(nsIDOMNode* inParent,
+ int32_t inOffset,
+ nsCOMPtr<nsIDOMNode>* outNode)
+{
+ NS_ENSURE_TRUE(outNode, NS_ERROR_NULL_POINTER);
+ *outNode = nullptr;
+
+ nsCOMPtr<nsINode> parent = do_QueryInterface(inParent);
+ NS_ENSURE_TRUE(parent, NS_ERROR_NULL_POINTER);
+
+ *outNode = do_QueryInterface(GetPriorHTMLSibling(parent, inOffset));
+ return NS_OK;
+}
+
+/**
+ * GetNextHTMLSibling() returns the next editable sibling, if there is
+ * one within the parent.
+ */
+nsIContent*
+HTMLEditor::GetNextHTMLSibling(nsINode* aNode)
+{
+ MOZ_ASSERT(aNode);
+
+ nsIContent* node = aNode->GetNextSibling();
+ while (node && !IsEditable(node)) {
+ node = node->GetNextSibling();
+ }
+
+ return node;
+}
+
+nsresult
+HTMLEditor::GetNextHTMLSibling(nsIDOMNode* inNode,
+ nsCOMPtr<nsIDOMNode>* outNode)
+{
+ NS_ENSURE_TRUE(outNode, NS_ERROR_NULL_POINTER);
+ *outNode = nullptr;
+
+ nsCOMPtr<nsINode> node = do_QueryInterface(inNode);
+ NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER);
+
+ *outNode = do_QueryInterface(GetNextHTMLSibling(node));
+ return NS_OK;
+}
+
+/**
+ * GetNextHTMLSibling() returns the next editable sibling, if there is
+ * one within the parent. just like above routine but takes a parent/offset
+ * instead of a node.
+ */
+nsIContent*
+HTMLEditor::GetNextHTMLSibling(nsINode* aParent,
+ int32_t aOffset)
+{
+ MOZ_ASSERT(aParent);
+
+ nsIContent* node = aParent->GetChildAt(aOffset + 1);
+ if (!node || IsEditable(node)) {
+ return node;
+ }
+
+ return GetNextHTMLSibling(node);
+}
+
+nsresult
+HTMLEditor::GetNextHTMLSibling(nsIDOMNode* inParent,
+ int32_t inOffset,
+ nsCOMPtr<nsIDOMNode>* outNode)
+{
+ NS_ENSURE_TRUE(outNode, NS_ERROR_NULL_POINTER);
+ *outNode = nullptr;
+
+ nsCOMPtr<nsINode> parent = do_QueryInterface(inParent);
+ NS_ENSURE_TRUE(parent, NS_ERROR_NULL_POINTER);
+
+ *outNode = do_QueryInterface(GetNextHTMLSibling(parent, inOffset));
+ return NS_OK;
+}
+
+/**
+ * GetPriorHTMLNode() returns the previous editable leaf node, if there is
+ * one within the <body>.
+ */
+nsIContent*
+HTMLEditor::GetPriorHTMLNode(nsINode* aNode,
+ bool aNoBlockCrossing)
+{
+ MOZ_ASSERT(aNode);
+
+ if (!GetActiveEditingHost()) {
+ return nullptr;
+ }
+
+ return GetPriorNode(aNode, true, aNoBlockCrossing);
+}
+
+nsresult
+HTMLEditor::GetPriorHTMLNode(nsIDOMNode* aNode,
+ nsCOMPtr<nsIDOMNode>* aResultNode,
+ bool aNoBlockCrossing)
+{
+ NS_ENSURE_TRUE(aResultNode, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER);
+
+ *aResultNode = do_QueryInterface(GetPriorHTMLNode(node, aNoBlockCrossing));
+ return NS_OK;
+}
+
+/**
+ * GetPriorHTMLNode() is same as above but takes {parent,offset} instead of
+ * node.
+ */
+nsIContent*
+HTMLEditor::GetPriorHTMLNode(nsINode* aParent,
+ int32_t aOffset,
+ bool aNoBlockCrossing)
+{
+ MOZ_ASSERT(aParent);
+
+ if (!GetActiveEditingHost()) {
+ return nullptr;
+ }
+
+ return GetPriorNode(aParent, aOffset, true, aNoBlockCrossing);
+}
+
+nsresult
+HTMLEditor::GetPriorHTMLNode(nsIDOMNode* aNode,
+ int32_t aOffset,
+ nsCOMPtr<nsIDOMNode>* aResultNode,
+ bool aNoBlockCrossing)
+{
+ NS_ENSURE_TRUE(aResultNode, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER);
+
+ *aResultNode = do_QueryInterface(GetPriorHTMLNode(node, aOffset,
+ aNoBlockCrossing));
+ return NS_OK;
+}
+
+/**
+ * GetNextHTMLNode() returns the next editable leaf node, if there is
+ * one within the <body>.
+ */
+nsIContent*
+HTMLEditor::GetNextHTMLNode(nsINode* aNode,
+ bool aNoBlockCrossing)
+{
+ MOZ_ASSERT(aNode);
+
+ nsIContent* result = GetNextNode(aNode, true, aNoBlockCrossing);
+
+ if (result && !IsDescendantOfEditorRoot(result)) {
+ return nullptr;
+ }
+
+ return result;
+}
+
+nsresult
+HTMLEditor::GetNextHTMLNode(nsIDOMNode* aNode,
+ nsCOMPtr<nsIDOMNode>* aResultNode,
+ bool aNoBlockCrossing)
+{
+ NS_ENSURE_TRUE(aResultNode, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER);
+
+ *aResultNode = do_QueryInterface(GetNextHTMLNode(node, aNoBlockCrossing));
+ return NS_OK;
+}
+
+/**
+ * GetNextHTMLNode() is same as above but takes {parent,offset} instead of node.
+ */
+nsIContent*
+HTMLEditor::GetNextHTMLNode(nsINode* aParent,
+ int32_t aOffset,
+ bool aNoBlockCrossing)
+{
+ nsIContent* content = GetNextNode(aParent, aOffset, true, aNoBlockCrossing);
+ if (content && !IsDescendantOfEditorRoot(content)) {
+ return nullptr;
+ }
+ return content;
+}
+
+nsresult
+HTMLEditor::GetNextHTMLNode(nsIDOMNode* aNode,
+ int32_t aOffset,
+ nsCOMPtr<nsIDOMNode>* aResultNode,
+ bool aNoBlockCrossing)
+{
+ NS_ENSURE_TRUE(aResultNode, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER);
+
+ *aResultNode = do_QueryInterface(GetNextHTMLNode(node, aOffset,
+ aNoBlockCrossing));
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::IsFirstEditableChild(nsIDOMNode* aNode,
+ bool* aOutIsFirst)
+{
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ NS_ENSURE_TRUE(aOutIsFirst && node, NS_ERROR_NULL_POINTER);
+
+ // init out parms
+ *aOutIsFirst = false;
+
+ // find first editable child and compare it to aNode
+ nsCOMPtr<nsINode> parent = node->GetParentNode();
+ NS_ENSURE_TRUE(parent, NS_ERROR_FAILURE);
+
+ *aOutIsFirst = (GetFirstEditableChild(*parent) == node);
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::IsLastEditableChild(nsIDOMNode* aNode,
+ bool* aOutIsLast)
+{
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ NS_ENSURE_TRUE(aOutIsLast && node, NS_ERROR_NULL_POINTER);
+
+ // init out parms
+ *aOutIsLast = false;
+
+ // find last editable child and compare it to aNode
+ nsCOMPtr<nsINode> parent = node->GetParentNode();
+ NS_ENSURE_TRUE(parent, NS_ERROR_FAILURE);
+
+ *aOutIsLast = (GetLastEditableChild(*parent) == node);
+ return NS_OK;
+}
+
+nsIContent*
+HTMLEditor::GetFirstEditableChild(nsINode& aNode)
+{
+ nsCOMPtr<nsIContent> child = aNode.GetFirstChild();
+
+ while (child && !IsEditable(child)) {
+ child = child->GetNextSibling();
+ }
+
+ return child;
+}
+
+nsIContent*
+HTMLEditor::GetLastEditableChild(nsINode& aNode)
+{
+ nsCOMPtr<nsIContent> child = aNode.GetLastChild();
+
+ while (child && !IsEditable(child)) {
+ child = child->GetPreviousSibling();
+ }
+
+ return child;
+}
+
+nsIContent*
+HTMLEditor::GetFirstEditableLeaf(nsINode& aNode)
+{
+ nsCOMPtr<nsIContent> child = GetLeftmostChild(&aNode);
+ while (child && (!IsEditable(child) || child->HasChildren())) {
+ child = GetNextHTMLNode(child);
+
+ // Only accept nodes that are descendants of aNode
+ if (!aNode.Contains(child)) {
+ return nullptr;
+ }
+ }
+
+ return child;
+}
+
+nsIContent*
+HTMLEditor::GetLastEditableLeaf(nsINode& aNode)
+{
+ nsCOMPtr<nsIContent> child = GetRightmostChild(&aNode, false);
+ while (child && (!IsEditable(child) || child->HasChildren())) {
+ child = GetPriorHTMLNode(child);
+
+ // Only accept nodes that are descendants of aNode
+ if (!aNode.Contains(child)) {
+ return nullptr;
+ }
+ }
+
+ return child;
+}
+
+/**
+ * IsVisTextNode() figures out if textnode aTextNode has any visible content.
+ */
+nsresult
+HTMLEditor::IsVisTextNode(nsIContent* aNode,
+ bool* outIsEmptyNode,
+ bool aSafeToAskFrames)
+{
+ MOZ_ASSERT(aNode);
+ MOZ_ASSERT(aNode->NodeType() == nsIDOMNode::TEXT_NODE);
+ MOZ_ASSERT(outIsEmptyNode);
+
+ *outIsEmptyNode = true;
+
+ uint32_t length = aNode->TextLength();
+ if (aSafeToAskFrames) {
+ nsCOMPtr<nsISelectionController> selCon;
+ nsresult rv = GetSelectionController(getter_AddRefs(selCon));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(selCon, NS_ERROR_FAILURE);
+ bool isVisible = false;
+ // ask the selection controller for information about whether any
+ // of the data in the node is really rendered. This is really
+ // something that frames know about, but we aren't supposed to talk to frames.
+ // So we put a call in the selection controller interface, since it's already
+ // in bed with frames anyway. (this is a fix for bug 22227, and a
+ // partial fix for bug 46209)
+ rv = selCon->CheckVisibilityContent(aNode, 0, length, &isVisible);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (isVisible) {
+ *outIsEmptyNode = false;
+ }
+ } else if (length) {
+ if (aNode->TextIsOnlyWhitespace()) {
+ WSRunObject wsRunObj(this, aNode, 0);
+ nsCOMPtr<nsINode> visNode;
+ int32_t outVisOffset=0;
+ WSType visType;
+ wsRunObj.NextVisibleNode(aNode, 0, address_of(visNode),
+ &outVisOffset, &visType);
+ if (visType == WSType::normalWS || visType == WSType::text) {
+ *outIsEmptyNode = (aNode != visNode);
+ }
+ } else {
+ *outIsEmptyNode = false;
+ }
+ }
+ return NS_OK;
+}
+
+/**
+ * IsEmptyNode() figures out if aNode is an empty node. A block can have
+ * children and still be considered empty, if the children are empty or
+ * non-editable.
+ */
+nsresult
+HTMLEditor::IsEmptyNode(nsIDOMNode*aNode,
+ bool* outIsEmptyNode,
+ bool aSingleBRDoesntCount,
+ bool aListOrCellNotEmpty,
+ bool aSafeToAskFrames)
+{
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ return IsEmptyNode(node, outIsEmptyNode, aSingleBRDoesntCount,
+ aListOrCellNotEmpty, aSafeToAskFrames);
+}
+
+nsresult
+HTMLEditor::IsEmptyNode(nsINode* aNode,
+ bool* outIsEmptyNode,
+ bool aSingleBRDoesntCount,
+ bool aListOrCellNotEmpty,
+ bool aSafeToAskFrames)
+{
+ NS_ENSURE_TRUE(aNode && outIsEmptyNode, NS_ERROR_NULL_POINTER);
+ *outIsEmptyNode = true;
+ bool seenBR = false;
+ return IsEmptyNodeImpl(aNode, outIsEmptyNode, aSingleBRDoesntCount,
+ aListOrCellNotEmpty, aSafeToAskFrames, &seenBR);
+}
+
+/**
+ * IsEmptyNodeImpl() is workhorse for IsEmptyNode().
+ */
+nsresult
+HTMLEditor::IsEmptyNodeImpl(nsINode* aNode,
+ bool* outIsEmptyNode,
+ bool aSingleBRDoesntCount,
+ bool aListOrCellNotEmpty,
+ bool aSafeToAskFrames,
+ bool* aSeenBR)
+{
+ NS_ENSURE_TRUE(aNode && outIsEmptyNode && aSeenBR, NS_ERROR_NULL_POINTER);
+
+ if (aNode->NodeType() == nsIDOMNode::TEXT_NODE) {
+ return IsVisTextNode(static_cast<nsIContent*>(aNode), outIsEmptyNode, aSafeToAskFrames);
+ }
+
+ // if it's not a text node (handled above) and it's not a container,
+ // then we don't call it empty (it's an <hr>, or <br>, etc.).
+ // Also, if it's an anchor then don't treat it as empty - even though
+ // anchors are containers, named anchors are "empty" but we don't
+ // want to treat them as such. Also, don't call ListItems or table
+ // cells empty if caller desires. Form Widgets not empty.
+ if (!IsContainer(aNode->AsDOMNode()) ||
+ (HTMLEditUtils::IsNamedAnchor(aNode) ||
+ HTMLEditUtils::IsFormWidget(aNode) ||
+ (aListOrCellNotEmpty &&
+ (HTMLEditUtils::IsListItem(aNode) ||
+ HTMLEditUtils::IsTableCell(aNode))))) {
+ *outIsEmptyNode = false;
+ return NS_OK;
+ }
+
+ // need this for later
+ bool isListItemOrCell = HTMLEditUtils::IsListItem(aNode) ||
+ HTMLEditUtils::IsTableCell(aNode);
+
+ // loop over children of node. if no children, or all children are either
+ // empty text nodes or non-editable, then node qualifies as empty
+ for (nsCOMPtr<nsIContent> child = aNode->GetFirstChild();
+ child;
+ child = child->GetNextSibling()) {
+ // Is the child editable and non-empty? if so, return false
+ if (EditorBase::IsEditable(child)) {
+ if (child->NodeType() == nsIDOMNode::TEXT_NODE) {
+ nsresult rv = IsVisTextNode(child, outIsEmptyNode, aSafeToAskFrames);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // break out if we find we aren't emtpy
+ if (!*outIsEmptyNode) {
+ return NS_OK;
+ }
+ } else {
+ // An editable, non-text node. We need to check its content.
+ // Is it the node we are iterating over?
+ if (child == aNode) {
+ break;
+ }
+
+ if (aSingleBRDoesntCount && !*aSeenBR && child->IsHTMLElement(nsGkAtoms::br)) {
+ // the first br in a block doesn't count if the caller so indicated
+ *aSeenBR = true;
+ } else {
+ // is it an empty node of some sort?
+ // note: list items or table cells are not considered empty
+ // if they contain other lists or tables
+ if (child->IsElement()) {
+ if (isListItemOrCell) {
+ if (HTMLEditUtils::IsList(child) ||
+ child->IsHTMLElement(nsGkAtoms::table)) {
+ // break out if we find we aren't empty
+ *outIsEmptyNode = false;
+ return NS_OK;
+ }
+ } else if (HTMLEditUtils::IsFormWidget(child)) {
+ // is it a form widget?
+ // break out if we find we aren't empty
+ *outIsEmptyNode = false;
+ return NS_OK;
+ }
+ }
+
+ bool isEmptyNode = true;
+ nsresult rv = IsEmptyNodeImpl(child, &isEmptyNode,
+ aSingleBRDoesntCount,
+ aListOrCellNotEmpty, aSafeToAskFrames,
+ aSeenBR);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!isEmptyNode) {
+ // otherwise it ain't empty
+ *outIsEmptyNode = false;
+ return NS_OK;
+ }
+ }
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+// add to aElement the CSS inline styles corresponding to the HTML attribute
+// aAttribute with its value aValue
+nsresult
+HTMLEditor::SetAttributeOrEquivalent(nsIDOMElement* aElement,
+ const nsAString& aAttribute,
+ const nsAString& aValue,
+ bool aSuppressTransaction)
+{
+ nsAutoScriptBlocker scriptBlocker;
+
+ if (IsCSSEnabled() && mCSSEditUtils) {
+ int32_t count;
+ nsresult rv =
+ mCSSEditUtils->SetCSSEquivalentToHTMLStyle(aElement, nullptr,
+ &aAttribute, &aValue,
+ &count,
+ aSuppressTransaction);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (count) {
+ // we found an equivalence ; let's remove the HTML attribute itself if it is set
+ nsAutoString existingValue;
+ bool wasSet = false;
+ rv = GetAttributeValue(aElement, aAttribute, existingValue, &wasSet);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!wasSet) {
+ return NS_OK;
+ }
+ return aSuppressTransaction ? aElement->RemoveAttribute(aAttribute) :
+ RemoveAttribute(aElement, aAttribute);
+ }
+
+ // count is an integer that represents the number of CSS declarations applied to the
+ // element. If it is zero, we found no equivalence in this implementation for the
+ // attribute
+ if (aAttribute.EqualsLiteral("style")) {
+ // if it is the style attribute, just add the new value to the existing style
+ // attribute's value
+ nsAutoString existingValue;
+ bool wasSet = false;
+ nsresult rv = GetAttributeValue(aElement, NS_LITERAL_STRING("style"),
+ existingValue, &wasSet);
+ NS_ENSURE_SUCCESS(rv, rv);
+ existingValue.Append(' ');
+ existingValue.Append(aValue);
+ return aSuppressTransaction ?
+ aElement->SetAttribute(aAttribute, existingValue) :
+ SetAttribute(aElement, aAttribute, existingValue);
+ }
+
+ // we have no CSS equivalence for this attribute and it is not the style
+ // attribute; let's set it the good'n'old HTML way
+ return aSuppressTransaction ? aElement->SetAttribute(aAttribute, aValue) :
+ SetAttribute(aElement, aAttribute, aValue);
+ }
+
+ // we are not in an HTML+CSS editor; let's set the attribute the HTML way
+ return aSuppressTransaction ? aElement->SetAttribute(aAttribute, aValue) :
+ SetAttribute(aElement, aAttribute, aValue);
+}
+
+nsresult
+HTMLEditor::RemoveAttributeOrEquivalent(nsIDOMElement* aElement,
+ const nsAString& aAttribute,
+ bool aSuppressTransaction)
+{
+ nsCOMPtr<dom::Element> element = do_QueryInterface(aElement);
+ NS_ENSURE_TRUE(element, NS_OK);
+
+ nsCOMPtr<nsIAtom> attribute = NS_Atomize(aAttribute);
+ MOZ_ASSERT(attribute);
+
+ if (IsCSSEnabled() && mCSSEditUtils) {
+ nsresult rv =
+ mCSSEditUtils->RemoveCSSEquivalentToHTMLStyle(
+ element, nullptr, &aAttribute, nullptr, aSuppressTransaction);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (!element->HasAttr(kNameSpaceID_None, attribute)) {
+ return NS_OK;
+ }
+
+ return aSuppressTransaction ?
+ element->UnsetAttr(kNameSpaceID_None, attribute, /* aNotify = */ true) :
+ RemoveAttribute(aElement, aAttribute);
+}
+
+nsresult
+HTMLEditor::SetIsCSSEnabled(bool aIsCSSPrefChecked)
+{
+ if (!mCSSEditUtils) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ mCSSEditUtils->SetCSSEnabled(aIsCSSPrefChecked);
+
+ // Disable the eEditorNoCSSMask flag if we're enabling StyleWithCSS.
+ uint32_t flags = mFlags;
+ if (aIsCSSPrefChecked) {
+ // Turn off NoCSS as we're enabling CSS
+ flags &= ~eEditorNoCSSMask;
+ } else {
+ // Turn on NoCSS, as we're disabling CSS.
+ flags |= eEditorNoCSSMask;
+ }
+
+ return SetFlags(flags);
+}
+
+// Set the block background color
+nsresult
+HTMLEditor::SetCSSBackgroundColor(const nsAString& aColor)
+{
+ NS_ENSURE_TRUE(mRules, NS_ERROR_NOT_INITIALIZED);
+ ForceCompositionEnd();
+
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_STATE(selection);
+
+ bool isCollapsed = selection->Collapsed();
+
+ AutoEditBatch batchIt(this);
+ AutoRules beginRulesSniffing(this, EditAction::insertElement,
+ nsIEditor::eNext);
+ AutoSelectionRestorer selectionRestorer(selection, this);
+ AutoTransactionsConserveSelection dontSpazMySelection(this);
+
+ bool cancel, handled;
+ TextRulesInfo ruleInfo(EditAction::setTextProperty);
+ nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!cancel && !handled) {
+ // Loop through the ranges in the selection
+ NS_NAMED_LITERAL_STRING(bgcolor, "bgcolor");
+ for (uint32_t i = 0; i < selection->RangeCount(); i++) {
+ RefPtr<nsRange> range = selection->GetRangeAt(i);
+ NS_ENSURE_TRUE(range, NS_ERROR_FAILURE);
+
+ nsCOMPtr<Element> cachedBlockParent;
+
+ // Check for easy case: both range endpoints in same text node
+ nsCOMPtr<nsINode> startNode = range->GetStartParent();
+ int32_t startOffset = range->StartOffset();
+ nsCOMPtr<nsINode> endNode = range->GetEndParent();
+ int32_t endOffset = range->EndOffset();
+ if (startNode == endNode && IsTextNode(startNode)) {
+ // Let's find the block container of the text node
+ nsCOMPtr<Element> blockParent = GetBlockNodeParent(startNode);
+ // And apply the background color to that block container
+ if (blockParent && cachedBlockParent != blockParent) {
+ cachedBlockParent = blockParent;
+ mCSSEditUtils->SetCSSEquivalentToHTMLStyle(blockParent, nullptr,
+ &bgcolor, &aColor, false);
+ }
+ } else if (startNode == endNode &&
+ startNode->IsHTMLElement(nsGkAtoms::body) && isCollapsed) {
+ // No block in the document, let's apply the background to the body
+ mCSSEditUtils->SetCSSEquivalentToHTMLStyle(startNode->AsElement(),
+ nullptr, &bgcolor, &aColor,
+ false);
+ } else if (startNode == endNode && (endOffset - startOffset == 1 ||
+ (!startOffset && !endOffset))) {
+ // A unique node is selected, let's also apply the background color to
+ // the containing block, possibly the node itself
+ nsCOMPtr<nsIContent> selectedNode = startNode->GetChildAt(startOffset);
+ nsCOMPtr<Element> blockParent = GetBlock(*selectedNode);
+ if (blockParent && cachedBlockParent != blockParent) {
+ cachedBlockParent = blockParent;
+ mCSSEditUtils->SetCSSEquivalentToHTMLStyle(blockParent, nullptr,
+ &bgcolor, &aColor, false);
+ }
+ } else {
+ // Not the easy case. Range not contained in single text node. There
+ // are up to three phases here. There are all the nodes reported by
+ // the subtree iterator to be processed. And there are potentially a
+ // starting textnode and an ending textnode which are only partially
+ // contained by the range.
+
+ // Let's handle the nodes reported by the iterator. These nodes are
+ // entirely contained in the selection range. We build up a list of
+ // them (since doing operations on the document during iteration would
+ // perturb the iterator).
+
+ OwningNonNull<nsIContentIterator> iter =
+ NS_NewContentSubtreeIterator();
+
+ nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
+ nsCOMPtr<nsINode> node;
+
+ // Iterate range and build up array
+ rv = iter->Init(range);
+ // Init returns an error if no nodes in range. This can easily happen
+ // with the subtree iterator if the selection doesn't contain any
+ // *whole* nodes.
+ if (NS_SUCCEEDED(rv)) {
+ for (; !iter->IsDone(); iter->Next()) {
+ node = do_QueryInterface(iter->GetCurrentNode());
+ NS_ENSURE_TRUE(node, NS_ERROR_FAILURE);
+
+ if (IsEditable(node)) {
+ arrayOfNodes.AppendElement(*node);
+ }
+ }
+ }
+ // First check the start parent of the range to see if it needs to be
+ // separately handled (it does if it's a text node, due to how the
+ // subtree iterator works - it will not have reported it).
+ if (IsTextNode(startNode) && IsEditable(startNode)) {
+ nsCOMPtr<Element> blockParent = GetBlockNodeParent(startNode);
+ if (blockParent && cachedBlockParent != blockParent) {
+ cachedBlockParent = blockParent;
+ mCSSEditUtils->SetCSSEquivalentToHTMLStyle(blockParent, nullptr,
+ &bgcolor, &aColor,
+ false);
+ }
+ }
+
+ // Then loop through the list, set the property on each node
+ for (auto& node : arrayOfNodes) {
+ nsCOMPtr<Element> blockParent = GetBlock(node);
+ if (blockParent && cachedBlockParent != blockParent) {
+ cachedBlockParent = blockParent;
+ mCSSEditUtils->SetCSSEquivalentToHTMLStyle(blockParent, nullptr,
+ &bgcolor, &aColor,
+ false);
+ }
+ }
+ arrayOfNodes.Clear();
+
+ // Last, check the end parent of the range to see if it needs to be
+ // separately handled (it does if it's a text node, due to how the
+ // subtree iterator works - it will not have reported it).
+ if (IsTextNode(endNode) && IsEditable(endNode)) {
+ nsCOMPtr<Element> blockParent = GetBlockNodeParent(endNode);
+ if (blockParent && cachedBlockParent != blockParent) {
+ cachedBlockParent = blockParent;
+ mCSSEditUtils->SetCSSEquivalentToHTMLStyle(blockParent, nullptr,
+ &bgcolor, &aColor,
+ false);
+ }
+ }
+ }
+ }
+ }
+ if (!cancel) {
+ // Post-process
+ rv = rules->DidDoAction(selection, &ruleInfo, rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::SetBackgroundColor(const nsAString& aColor)
+{
+ if (IsCSSEnabled()) {
+ // if we are in CSS mode, we have to apply the background color to the
+ // containing block (or the body if we have no block-level element in
+ // the document)
+ return SetCSSBackgroundColor(aColor);
+ }
+
+ // but in HTML mode, we can only set the document's background color
+ return SetHTMLBackgroundColor(aColor);
+}
+
+/**
+ * NodesSameType() does these nodes have the same tag?
+ */
+bool
+HTMLEditor::AreNodesSameType(nsIContent* aNode1,
+ nsIContent* aNode2)
+{
+ MOZ_ASSERT(aNode1);
+ MOZ_ASSERT(aNode2);
+
+ if (aNode1->NodeInfo()->NameAtom() != aNode2->NodeInfo()->NameAtom()) {
+ return false;
+ }
+
+ if (!IsCSSEnabled() || !aNode1->IsHTMLElement(nsGkAtoms::span)) {
+ return true;
+ }
+
+ // If CSS is enabled, we are stricter about span nodes.
+ return mCSSEditUtils->ElementsSameStyle(aNode1->AsDOMNode(),
+ aNode2->AsDOMNode());
+}
+
+nsresult
+HTMLEditor::CopyLastEditableChildStyles(nsIDOMNode* aPreviousBlock,
+ nsIDOMNode* aNewBlock,
+ Element** aOutBrNode)
+{
+ nsCOMPtr<nsINode> newBlock = do_QueryInterface(aNewBlock);
+ NS_ENSURE_STATE(newBlock || !aNewBlock);
+ *aOutBrNode = nullptr;
+ nsCOMPtr<nsIDOMNode> child, tmp;
+ // first, clear out aNewBlock. Contract is that we want only the styles from previousBlock.
+ nsresult rv = aNewBlock->GetFirstChild(getter_AddRefs(child));
+ while (NS_SUCCEEDED(rv) && child) {
+ rv = DeleteNode(child);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aNewBlock->GetFirstChild(getter_AddRefs(child));
+ }
+ // now find and clone the styles
+ child = aPreviousBlock;
+ tmp = aPreviousBlock;
+ while (tmp) {
+ child = tmp;
+ nsCOMPtr<nsINode> child_ = do_QueryInterface(child);
+ NS_ENSURE_STATE(child_ || !child);
+ tmp = GetAsDOMNode(GetLastEditableChild(*child_));
+ }
+ while (child && TextEditUtils::IsBreak(child)) {
+ nsCOMPtr<nsIDOMNode> priorNode;
+ rv = GetPriorHTMLNode(child, address_of(priorNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+ child = priorNode;
+ }
+ nsCOMPtr<Element> newStyles, deepestStyle;
+ nsCOMPtr<nsINode> childNode = do_QueryInterface(child);
+ nsCOMPtr<Element> childElement;
+ if (childNode) {
+ childElement = childNode->IsElement() ? childNode->AsElement()
+ : childNode->GetParentElement();
+ }
+ while (childElement && (childElement->AsDOMNode() != aPreviousBlock)) {
+ if (HTMLEditUtils::IsInlineStyle(childElement) ||
+ childElement->IsHTMLElement(nsGkAtoms::span)) {
+ if (newStyles) {
+ newStyles = InsertContainerAbove(newStyles,
+ childElement->NodeInfo()->NameAtom());
+ NS_ENSURE_STATE(newStyles);
+ } else {
+ deepestStyle = newStyles =
+ CreateNode(childElement->NodeInfo()->NameAtom(), newBlock, 0);
+ NS_ENSURE_STATE(newStyles);
+ }
+ CloneAttributes(newStyles, childElement);
+ }
+ childElement = childElement->GetParentElement();
+ }
+ if (deepestStyle) {
+ RefPtr<Element> retVal = CreateBR(deepestStyle, 0);
+ retVal.forget(aOutBrNode);
+ NS_ENSURE_STATE(*aOutBrNode);
+ }
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::GetElementOrigin(nsIDOMElement* aElement,
+ int32_t& aX,
+ int32_t& aY)
+{
+ aX = 0;
+ aY = 0;
+
+ NS_ENSURE_TRUE(mDocWeak, NS_ERROR_NOT_INITIALIZED);
+ nsCOMPtr<nsIPresShell> ps = GetPresShell();
+ NS_ENSURE_TRUE(ps, NS_ERROR_NOT_INITIALIZED);
+
+ nsCOMPtr<nsIContent> content = do_QueryInterface(aElement);
+ nsIFrame *frame = content->GetPrimaryFrame();
+ NS_ENSURE_TRUE(frame, NS_OK);
+
+ nsIFrame *container = ps->GetAbsoluteContainingBlock(frame);
+ NS_ENSURE_TRUE(container, NS_OK);
+ nsPoint off = frame->GetOffsetTo(container);
+ aX = nsPresContext::AppUnitsToIntCSSPixels(off.x);
+ aY = nsPresContext::AppUnitsToIntCSSPixels(off.y);
+
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::EndUpdateViewBatch()
+{
+ nsresult rv = EditorBase::EndUpdateViewBatch();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mUpdateCount) {
+ return NS_OK;
+ }
+
+ // We may need to show resizing handles or update existing ones after
+ // all transactions are done. This way of doing is preferred to DOM
+ // mutation events listeners because all the changes the user can apply
+ // to a document may result in multiple events, some of them quite hard
+ // to listen too (in particular when an ancestor of the selection is
+ // changed but the selection itself is not changed).
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NOT_INITIALIZED);
+ return CheckSelectionStateForAnonymousButtons(selection);
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetSelectionContainer(nsIDOMElement** aReturn)
+{
+ nsCOMPtr<nsIDOMElement> container =
+ static_cast<nsIDOMElement*>(GetAsDOMNode(GetSelectionContainer()));
+ NS_ENSURE_TRUE(container, NS_ERROR_FAILURE);
+ container.forget(aReturn);
+ return NS_OK;
+}
+
+Element*
+HTMLEditor::GetSelectionContainer()
+{
+ // If we don't get the selection, just skip this
+ NS_ENSURE_TRUE(GetSelection(), nullptr);
+
+ OwningNonNull<Selection> selection = *GetSelection();
+
+ nsCOMPtr<nsINode> focusNode;
+
+ if (selection->Collapsed()) {
+ focusNode = selection->GetFocusNode();
+ } else {
+ int32_t rangeCount = selection->RangeCount();
+
+ if (rangeCount == 1) {
+ RefPtr<nsRange> range = selection->GetRangeAt(0);
+
+ nsCOMPtr<nsINode> startContainer = range->GetStartParent();
+ int32_t startOffset = range->StartOffset();
+ nsCOMPtr<nsINode> endContainer = range->GetEndParent();
+ int32_t endOffset = range->EndOffset();
+
+ if (startContainer == endContainer && startOffset + 1 == endOffset) {
+ nsCOMPtr<nsIDOMElement> focusElement;
+ nsresult rv = GetSelectedElement(EmptyString(),
+ getter_AddRefs(focusElement));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ if (focusElement) {
+ focusNode = do_QueryInterface(focusElement);
+ }
+ }
+ if (!focusNode) {
+ focusNode = range->GetCommonAncestor();
+ }
+ } else {
+ for (int32_t i = 0; i < rangeCount; i++) {
+ RefPtr<nsRange> range = selection->GetRangeAt(i);
+
+ nsCOMPtr<nsINode> startContainer = range->GetStartParent();
+ if (!focusNode) {
+ focusNode = startContainer;
+ } else if (focusNode != startContainer) {
+ focusNode = startContainer->GetParentNode();
+ break;
+ }
+ }
+ }
+ }
+
+ if (focusNode && focusNode->GetAsText()) {
+ focusNode = focusNode->GetParentNode();
+ }
+
+ if (focusNode && focusNode->IsElement()) {
+ return focusNode->AsElement();
+ }
+
+ return nullptr;
+}
+
+NS_IMETHODIMP
+HTMLEditor::IsAnonymousElement(nsIDOMElement* aElement,
+ bool* aReturn)
+{
+ NS_ENSURE_TRUE(aElement, NS_ERROR_NULL_POINTER);
+ nsCOMPtr<nsIContent> content = do_QueryInterface(aElement);
+ *aReturn = content->IsRootOfNativeAnonymousSubtree();
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::SetReturnInParagraphCreatesNewParagraph(bool aCreatesNewParagraph)
+{
+ mCRInParagraphCreatesParagraph = aCreatesNewParagraph;
+ return NS_OK;
+}
+
+bool
+HTMLEditor::GetReturnInParagraphCreatesNewParagraph()
+{
+ return mCRInParagraphCreatesParagraph;
+}
+
+nsresult
+HTMLEditor::GetReturnInParagraphCreatesNewParagraph(bool* aCreatesNewParagraph)
+{
+ *aCreatesNewParagraph = mCRInParagraphCreatesParagraph;
+ return NS_OK;
+}
+
+already_AddRefed<nsIContent>
+HTMLEditor::GetFocusedContent()
+{
+ NS_ENSURE_TRUE(mDocWeak, nullptr);
+
+ nsFocusManager* fm = nsFocusManager::GetFocusManager();
+ NS_ENSURE_TRUE(fm, nullptr);
+
+ nsCOMPtr<nsIContent> focusedContent = fm->GetFocusedContent();
+
+ nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak);
+ bool inDesignMode = doc->HasFlag(NODE_IS_EDITABLE);
+ if (!focusedContent) {
+ // in designMode, nobody gets focus in most cases.
+ if (inDesignMode && OurWindowHasFocus()) {
+ nsCOMPtr<nsIContent> docRoot = doc->GetRootElement();
+ return docRoot.forget();
+ }
+ return nullptr;
+ }
+
+ if (inDesignMode) {
+ return OurWindowHasFocus() &&
+ nsContentUtils::ContentIsDescendantOf(focusedContent, doc) ?
+ focusedContent.forget() : nullptr;
+ }
+
+ // We're HTML editor for contenteditable
+
+ // If the focused content isn't editable, or it has independent selection,
+ // we don't have focus.
+ if (!focusedContent->HasFlag(NODE_IS_EDITABLE) ||
+ focusedContent->HasIndependentSelection()) {
+ return nullptr;
+ }
+ // If our window is focused, we're focused.
+ return OurWindowHasFocus() ? focusedContent.forget() : nullptr;
+}
+
+already_AddRefed<nsIContent>
+HTMLEditor::GetFocusedContentForIME()
+{
+ nsCOMPtr<nsIContent> focusedContent = GetFocusedContent();
+ if (!focusedContent) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak);
+ NS_ENSURE_TRUE(doc, nullptr);
+ return doc->HasFlag(NODE_IS_EDITABLE) ? nullptr : focusedContent.forget();
+}
+
+bool
+HTMLEditor::IsActiveInDOMWindow()
+{
+ NS_ENSURE_TRUE(mDocWeak, false);
+
+ nsFocusManager* fm = nsFocusManager::GetFocusManager();
+ NS_ENSURE_TRUE(fm, false);
+
+ nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak);
+ bool inDesignMode = doc->HasFlag(NODE_IS_EDITABLE);
+
+ // If we're in designMode, we're always active in the DOM window.
+ if (inDesignMode) {
+ return true;
+ }
+
+ nsPIDOMWindowOuter* ourWindow = doc->GetWindow();
+ nsCOMPtr<nsPIDOMWindowOuter> win;
+ nsIContent* content =
+ nsFocusManager::GetFocusedDescendant(ourWindow, false,
+ getter_AddRefs(win));
+ if (!content) {
+ return false;
+ }
+
+ // We're HTML editor for contenteditable
+
+ // If the active content isn't editable, or it has independent selection,
+ // we're not active).
+ if (!content->HasFlag(NODE_IS_EDITABLE) ||
+ content->HasIndependentSelection()) {
+ return false;
+ }
+ return true;
+}
+
+Element*
+HTMLEditor::GetActiveEditingHost()
+{
+ NS_ENSURE_TRUE(mDocWeak, nullptr);
+
+ nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak);
+ NS_ENSURE_TRUE(doc, nullptr);
+ if (doc->HasFlag(NODE_IS_EDITABLE)) {
+ return doc->GetBodyElement();
+ }
+
+ // We're HTML editor for contenteditable
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, nullptr);
+ nsCOMPtr<nsIDOMNode> focusNode;
+ nsresult rv = selection->GetFocusNode(getter_AddRefs(focusNode));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ nsCOMPtr<nsIContent> content = do_QueryInterface(focusNode);
+ if (!content) {
+ return nullptr;
+ }
+
+ // If the active content isn't editable, or it has independent selection,
+ // we're not active.
+ if (!content->HasFlag(NODE_IS_EDITABLE) ||
+ content->HasIndependentSelection()) {
+ return nullptr;
+ }
+ return content->GetEditingHost();
+}
+
+already_AddRefed<EventTarget>
+HTMLEditor::GetDOMEventTarget()
+{
+ // Don't use getDocument here, because we have no way of knowing
+ // whether Init() was ever called. So we need to get the document
+ // ourselves, if it exists.
+ NS_PRECONDITION(mDocWeak, "This editor has not been initialized yet");
+ nsCOMPtr<mozilla::dom::EventTarget> target = do_QueryReferent(mDocWeak);
+ return target.forget();
+}
+
+bool
+HTMLEditor::ShouldReplaceRootElement()
+{
+ if (!mRootElement) {
+ // If we don't know what is our root element, we should find our root.
+ return true;
+ }
+
+ // If we temporary set document root element to mRootElement, but there is
+ // body element now, we should replace the root element by the body element.
+ nsCOMPtr<nsIDOMHTMLElement> docBody;
+ GetBodyElement(getter_AddRefs(docBody));
+ return !SameCOMIdentity(docBody, mRootElement);
+}
+
+void
+HTMLEditor::NotifyRootChanged()
+{
+ nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this);
+
+ RemoveEventListeners();
+ nsresult rv = InstallEventListeners();
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ UpdateRootElement();
+ if (!mRootElement) {
+ return;
+ }
+
+ rv = BeginningOfDocument();
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ // When this editor has focus, we need to reset the selection limiter to
+ // new root. Otherwise, that is going to be done when this gets focus.
+ nsCOMPtr<nsINode> node = GetFocusedNode();
+ nsCOMPtr<nsIDOMEventTarget> target = do_QueryInterface(node);
+ if (target) {
+ InitializeSelection(target);
+ }
+
+ SyncRealTimeSpell();
+}
+
+nsresult
+HTMLEditor::GetBodyElement(nsIDOMHTMLElement** aBody)
+{
+ NS_PRECONDITION(mDocWeak, "bad state, null mDocWeak");
+ nsCOMPtr<nsIDOMHTMLDocument> htmlDoc = do_QueryReferent(mDocWeak);
+ if (!htmlDoc) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ return htmlDoc->GetBody(aBody);
+}
+
+already_AddRefed<nsINode>
+HTMLEditor::GetFocusedNode()
+{
+ nsCOMPtr<nsIContent> focusedContent = GetFocusedContent();
+ if (!focusedContent) {
+ return nullptr;
+ }
+
+ nsIFocusManager* fm = nsFocusManager::GetFocusManager();
+ NS_ASSERTION(fm, "Focus manager is null");
+ nsCOMPtr<nsIDOMElement> focusedElement;
+ fm->GetFocusedElement(getter_AddRefs(focusedElement));
+ if (focusedElement) {
+ nsCOMPtr<nsINode> node = do_QueryInterface(focusedElement);
+ return node.forget();
+ }
+
+ nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak);
+ return doc.forget();
+}
+
+bool
+HTMLEditor::OurWindowHasFocus()
+{
+ NS_ENSURE_TRUE(mDocWeak, false);
+ nsIFocusManager* fm = nsFocusManager::GetFocusManager();
+ NS_ENSURE_TRUE(fm, false);
+ nsCOMPtr<mozIDOMWindowProxy> focusedWindow;
+ fm->GetFocusedWindow(getter_AddRefs(focusedWindow));
+ if (!focusedWindow) {
+ return false;
+ }
+ nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak);
+ nsPIDOMWindowOuter* ourWindow = doc->GetWindow();
+ return ourWindow == focusedWindow;
+}
+
+bool
+HTMLEditor::IsAcceptableInputEvent(nsIDOMEvent* aEvent)
+{
+ if (!EditorBase::IsAcceptableInputEvent(aEvent)) {
+ return false;
+ }
+
+ // While there is composition, all composition events in its top level window
+ // are always fired on the composing editor. Therefore, if this editor has
+ // composition, the composition events should be handled in this editor.
+ if (mComposition && aEvent->WidgetEventPtr()->AsCompositionEvent()) {
+ return true;
+ }
+
+ NS_ENSURE_TRUE(mDocWeak, false);
+
+ nsCOMPtr<nsIDOMEventTarget> target;
+ aEvent->GetTarget(getter_AddRefs(target));
+ NS_ENSURE_TRUE(target, false);
+
+ nsCOMPtr<nsIDocument> document = do_QueryReferent(mDocWeak);
+ if (document->HasFlag(NODE_IS_EDITABLE)) {
+ // If this editor is in designMode and the event target is the document,
+ // the event is for this editor.
+ nsCOMPtr<nsIDocument> targetDocument = do_QueryInterface(target);
+ if (targetDocument) {
+ return targetDocument == document;
+ }
+ // Otherwise, check whether the event target is in this document or not.
+ nsCOMPtr<nsIContent> targetContent = do_QueryInterface(target);
+ NS_ENSURE_TRUE(targetContent, false);
+ return document == targetContent->GetUncomposedDoc();
+ }
+
+ // This HTML editor is for contenteditable. We need to check the validity of
+ // the target.
+ nsCOMPtr<nsIContent> targetContent = do_QueryInterface(target);
+ NS_ENSURE_TRUE(targetContent, false);
+
+ // If the event is a mouse event, we need to check if the target content is
+ // the focused editing host or its descendant.
+ nsCOMPtr<nsIDOMMouseEvent> mouseEvent = do_QueryInterface(aEvent);
+ if (mouseEvent) {
+ nsIContent* editingHost = GetActiveEditingHost();
+ // If there is no active editing host, we cannot handle the mouse event
+ // correctly.
+ if (!editingHost) {
+ return false;
+ }
+ // If clicked on non-editable root element but the body element is the
+ // active editing host, we should assume that the click event is targetted.
+ if (targetContent == document->GetRootElement() &&
+ !targetContent->HasFlag(NODE_IS_EDITABLE) &&
+ editingHost == document->GetBodyElement()) {
+ targetContent = editingHost;
+ }
+ // If the target element is neither the active editing host nor a descendant
+ // of it, we may not be able to handle the event.
+ if (!nsContentUtils::ContentIsDescendantOf(targetContent, editingHost)) {
+ return false;
+ }
+ // If the clicked element has an independent selection, we shouldn't
+ // handle this click event.
+ if (targetContent->HasIndependentSelection()) {
+ return false;
+ }
+ // If the target content is editable, we should handle this event.
+ return targetContent->HasFlag(NODE_IS_EDITABLE);
+ }
+
+ // If the target of the other events which target focused element isn't
+ // editable or has an independent selection, this editor shouldn't handle the
+ // event.
+ if (!targetContent->HasFlag(NODE_IS_EDITABLE) ||
+ targetContent->HasIndependentSelection()) {
+ return false;
+ }
+
+ // Finally, check whether we're actually focused or not. When we're not
+ // focused, we should ignore the dispatched event by script (or something)
+ // because content editable element needs selection in itself for editing.
+ // However, when we're not focused, it's not guaranteed.
+ return IsActiveInDOMWindow();
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetPreferredIMEState(IMEState* aState)
+{
+ // HTML editor don't prefer the CSS ime-mode because IE didn't do so too.
+ aState->mOpen = IMEState::DONT_CHANGE_OPEN_STATE;
+ if (IsReadonly() || IsDisabled()) {
+ aState->mEnabled = IMEState::DISABLED;
+ } else {
+ aState->mEnabled = IMEState::ENABLED;
+ }
+ return NS_OK;
+}
+
+already_AddRefed<nsIContent>
+HTMLEditor::GetInputEventTargetContent()
+{
+ nsCOMPtr<nsIContent> target = GetActiveEditingHost();
+ return target.forget();
+}
+
+bool
+HTMLEditor::IsEditable(nsINode* aNode)
+{
+ if (!TextEditor::IsEditable(aNode)) {
+ return false;
+ }
+ if (aNode->IsElement()) {
+ // If we're dealing with an element, then ask it whether it's editable.
+ return aNode->IsEditable();
+ }
+ // We might be dealing with a text node for example, which we always consider
+ // to be editable.
+ return true;
+}
+
+Element*
+HTMLEditor::GetEditorRoot()
+{
+ return GetActiveEditingHost();
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/HTMLEditor.h b/editor/libeditor/HTMLEditor.h
new file mode 100644
index 000000000..477ec9741
--- /dev/null
+++ b/editor/libeditor/HTMLEditor.h
@@ -0,0 +1,1109 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_HTMLEditor_h
+#define mozilla_HTMLEditor_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/CSSEditUtils.h"
+#include "mozilla/StyleSheet.h"
+#include "mozilla/TextEditor.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/File.h"
+
+#include "nsAttrName.h"
+#include "nsAutoPtr.h"
+#include "nsCOMPtr.h"
+#include "nsIContentFilter.h"
+#include "nsICSSLoaderObserver.h"
+#include "nsIDocumentObserver.h"
+#include "nsIDOMElement.h"
+#include "nsIDOMEventListener.h"
+#include "nsIEditor.h"
+#include "nsIEditorMailSupport.h"
+#include "nsIEditorStyleSheets.h"
+#include "nsIEditorUtils.h"
+#include "nsIEditRules.h"
+#include "nsIHTMLAbsPosEditor.h"
+#include "nsIHTMLEditor.h"
+#include "nsIHTMLInlineTableEditor.h"
+#include "nsIHTMLObjectResizeListener.h"
+#include "nsIHTMLObjectResizer.h"
+#include "nsISelectionListener.h"
+#include "nsITableEditor.h"
+#include "nsPoint.h"
+#include "nsStubMutationObserver.h"
+#include "nsTArray.h"
+
+class nsDocumentFragment;
+class nsIDOMKeyEvent;
+class nsITransferable;
+class nsIClipboard;
+class nsILinkHandler;
+class nsTableWrapperFrame;
+class nsIDOMRange;
+class nsRange;
+
+namespace mozilla {
+
+class HTMLEditorEventListener;
+class HTMLEditRules;
+class TextEditRules;
+class TypeInState;
+class WSRunObject;
+struct PropItem;
+template<class T> class OwningNonNull;
+namespace dom {
+class DocumentFragment;
+} // namespace dom
+namespace widget {
+struct IMEState;
+} // namespace widget
+
+/**
+ * The HTML editor implementation.<br>
+ * Use to edit HTML document represented as a DOM tree.
+ */
+class HTMLEditor final : public TextEditor
+ , public nsIHTMLEditor
+ , public nsIHTMLObjectResizer
+ , public nsIHTMLAbsPosEditor
+ , public nsITableEditor
+ , public nsIHTMLInlineTableEditor
+ , public nsIEditorStyleSheets
+ , public nsICSSLoaderObserver
+ , public nsStubMutationObserver
+{
+private:
+ enum BlockTransformationType
+ {
+ eNoOp,
+ eReplaceParent = 1,
+ eInsertParent = 2
+ };
+
+ const char16_t kNBSP = 160;
+
+public:
+ enum ResizingRequestID
+ {
+ kX = 0,
+ kY = 1,
+ kWidth = 2,
+ kHeight = 3
+ };
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLEditor, TextEditor)
+
+ HTMLEditor();
+
+ bool GetReturnInParagraphCreatesNewParagraph();
+ Element* GetSelectionContainer();
+
+ // TextEditor overrides
+ NS_IMETHOD GetIsDocumentEditable(bool* aIsDocumentEditable) override;
+ NS_IMETHOD BeginningOfDocument() override;
+ virtual nsresult HandleKeyPressEvent(nsIDOMKeyEvent* aKeyEvent) override;
+ virtual already_AddRefed<nsIContent> GetFocusedContent() override;
+ virtual already_AddRefed<nsIContent> GetFocusedContentForIME() override;
+ virtual bool IsActiveInDOMWindow() override;
+ virtual already_AddRefed<dom::EventTarget> GetDOMEventTarget() override;
+ virtual Element* GetEditorRoot() override;
+ virtual already_AddRefed<nsIContent> FindSelectionRoot(
+ nsINode *aNode) override;
+ virtual bool IsAcceptableInputEvent(nsIDOMEvent* aEvent) override;
+ virtual already_AddRefed<nsIContent> GetInputEventTargetContent() override;
+ virtual bool IsEditable(nsINode* aNode) override;
+ using EditorBase::IsEditable;
+
+ // nsStubMutationObserver overrides
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
+
+ // nsIEditorIMESupport overrides
+ NS_IMETHOD GetPreferredIMEState(widget::IMEState* aState) override;
+
+ // nsIHTMLEditor methods
+ NS_DECL_NSIHTMLEDITOR
+
+ // nsIHTMLObjectResizer methods (implemented in HTMLObjectResizer.cpp)
+ NS_DECL_NSIHTMLOBJECTRESIZER
+
+ // nsIHTMLAbsPosEditor methods (implemented in HTMLAbsPositionEditor.cpp)
+ NS_DECL_NSIHTMLABSPOSEDITOR
+
+ // nsIHTMLInlineTableEditor methods (implemented in HTMLInlineTableEditor.cpp)
+ NS_DECL_NSIHTMLINLINETABLEEDITOR
+
+ // XXX Following methods are not overriding but defined here...
+ nsresult CopyLastEditableChildStyles(nsIDOMNode* aPreviousBlock,
+ nsIDOMNode* aNewBlock,
+ Element** aOutBrNode);
+
+ nsresult LoadHTML(const nsAString& aInputString);
+
+ nsresult GetCSSBackgroundColorState(bool* aMixed, nsAString& aOutColor,
+ bool aBlockLevel);
+ NS_IMETHOD GetHTMLBackgroundColorState(bool* aMixed, nsAString& outColor);
+
+ // nsIEditorStyleSheets methods
+ NS_IMETHOD AddStyleSheet(const nsAString& aURL) override;
+ NS_IMETHOD ReplaceStyleSheet(const nsAString& aURL) override;
+ NS_IMETHOD RemoveStyleSheet(const nsAString &aURL) override;
+
+ NS_IMETHOD AddOverrideStyleSheet(const nsAString& aURL) override;
+ NS_IMETHOD ReplaceOverrideStyleSheet(const nsAString& aURL) override;
+ NS_IMETHOD RemoveOverrideStyleSheet(const nsAString &aURL) override;
+
+ NS_IMETHOD EnableStyleSheet(const nsAString& aURL, bool aEnable) override;
+
+ // nsIEditorMailSupport methods
+ NS_DECL_NSIEDITORMAILSUPPORT
+
+ // nsITableEditor methods
+ NS_IMETHOD InsertTableCell(int32_t aNumber, bool aAfter) override;
+ NS_IMETHOD InsertTableColumn(int32_t aNumber, bool aAfter) override;
+ NS_IMETHOD InsertTableRow(int32_t aNumber, bool aAfter) override;
+ NS_IMETHOD DeleteTable() override;
+ NS_IMETHOD DeleteTableCell(int32_t aNumber) override;
+ NS_IMETHOD DeleteTableCellContents() override;
+ NS_IMETHOD DeleteTableColumn(int32_t aNumber) override;
+ NS_IMETHOD DeleteTableRow(int32_t aNumber) override;
+ NS_IMETHOD SelectTableCell() override;
+ NS_IMETHOD SelectBlockOfCells(nsIDOMElement* aStartCell,
+ nsIDOMElement* aEndCell) override;
+ NS_IMETHOD SelectTableRow() override;
+ NS_IMETHOD SelectTableColumn() override;
+ NS_IMETHOD SelectTable() override;
+ NS_IMETHOD SelectAllTableCells() override;
+ NS_IMETHOD SwitchTableCellHeaderType(nsIDOMElement* aSourceCell,
+ nsIDOMElement** aNewCell) override;
+ NS_IMETHOD JoinTableCells(bool aMergeNonContiguousContents) override;
+ NS_IMETHOD SplitTableCell() override;
+ NS_IMETHOD NormalizeTable(nsIDOMElement* aTable) override;
+ NS_IMETHOD GetCellIndexes(nsIDOMElement* aCell,
+ int32_t* aRowIndex, int32_t* aColIndex) override;
+ NS_IMETHOD GetTableSize(nsIDOMElement* aTable,
+ int32_t* aRowCount, int32_t* aColCount) override;
+ NS_IMETHOD GetCellAt(nsIDOMElement* aTable, int32_t aRowIndex,
+ int32_t aColIndex, nsIDOMElement **aCell) override;
+ NS_IMETHOD GetCellDataAt(nsIDOMElement* aTable,
+ int32_t aRowIndex, int32_t aColIndex,
+ nsIDOMElement** aCell,
+ int32_t* aStartRowIndex, int32_t* aStartColIndex,
+ int32_t* aRowSpan, int32_t* aColSpan,
+ int32_t* aActualRowSpan, int32_t* aActualColSpan,
+ bool* aIsSelected) override;
+ NS_IMETHOD GetFirstRow(nsIDOMElement* aTableElement,
+ nsIDOMNode** aRowNode) override;
+ NS_IMETHOD GetNextRow(nsIDOMNode* aCurrentRowNode,
+ nsIDOMNode** aRowNode) override;
+ nsresult GetLastCellInRow(nsIDOMNode* aRowNode,
+ nsIDOMNode** aCellNode);
+
+ NS_IMETHOD SetSelectionAfterTableEdit(nsIDOMElement* aTable, int32_t aRow,
+ int32_t aCol, int32_t aDirection,
+ bool aSelected) override;
+ NS_IMETHOD GetSelectedOrParentTableElement(
+ nsAString& aTagName, int32_t* aSelectedCount,
+ nsIDOMElement** aTableElement) override;
+ NS_IMETHOD GetSelectedCellsType(nsIDOMElement* aElement,
+ uint32_t* aSelectionType) override;
+
+ nsresult GetCellFromRange(nsRange* aRange, nsIDOMElement** aCell);
+
+ /**
+ * Finds the first selected cell in first range of selection
+ * This is in the *order of selection*, not order in the table
+ * (i.e., each cell added to selection is added in another range
+ * in the selection's rangelist, independent of location in table)
+ * aRange is optional: returns the range around the cell.
+ */
+ NS_IMETHOD GetFirstSelectedCell(nsIDOMRange** aRange,
+ nsIDOMElement** aCell) override;
+ /**
+ * Get next cell until no more are found. Always use GetFirstSelected cell
+ * first aRange is optional: returns the range around the cell.
+ */
+ NS_IMETHOD GetNextSelectedCell(nsIDOMRange** aRange,
+ nsIDOMElement** aCell) override;
+
+ /**
+ * Upper-left-most selected cell in table.
+ */
+ NS_IMETHOD GetFirstSelectedCellInTable(int32_t* aRowIndex, int32_t* aColIndex,
+ nsIDOMElement** aCell) override;
+
+ // Miscellaneous
+
+ /**
+ * This sets background on the appropriate container element (table, cell,)
+ * or calls into nsTextEditor to set the page background.
+ */
+ nsresult SetCSSBackgroundColor(const nsAString& aColor);
+ nsresult SetHTMLBackgroundColor(const nsAString& aColor);
+
+ // Block methods moved from EditorBase
+ static Element* GetBlockNodeParent(nsINode* aNode);
+ static nsIDOMNode* GetBlockNodeParent(nsIDOMNode* aNode);
+ static Element* GetBlock(nsINode& aNode);
+
+ void IsNextCharInNodeWhitespace(nsIContent* aContent,
+ int32_t aOffset,
+ bool* outIsSpace,
+ bool* outIsNBSP,
+ nsIContent** outNode = nullptr,
+ int32_t* outOffset = 0);
+ void IsPrevCharInNodeWhitespace(nsIContent* aContent,
+ int32_t aOffset,
+ bool* outIsSpace,
+ bool* outIsNBSP,
+ nsIContent** outNode = nullptr,
+ int32_t* outOffset = 0);
+
+ // Overrides of EditorBase interface methods
+ nsresult EndUpdateViewBatch() override;
+
+ NS_IMETHOD Init(nsIDOMDocument* aDoc, nsIContent* aRoot,
+ nsISelectionController* aSelCon, uint32_t aFlags,
+ const nsAString& aValue) override;
+ NS_IMETHOD PreDestroy(bool aDestroyingFrames) override;
+
+ /**
+ * @param aElement Must not be null.
+ */
+ static bool NodeIsBlockStatic(const nsINode* aElement);
+ static nsresult NodeIsBlockStatic(nsIDOMNode *aNode, bool *aIsBlock);
+
+protected:
+ virtual ~HTMLEditor();
+
+ using EditorBase::IsBlockNode;
+ virtual bool IsBlockNode(nsINode *aNode) override;
+
+public:
+ // XXX Why don't we move following methods above for grouping by the origins?
+ NS_IMETHOD SetFlags(uint32_t aFlags) override;
+
+ NS_IMETHOD Paste(int32_t aSelectionType) override;
+ NS_IMETHOD CanPaste(int32_t aSelectionType, bool* aCanPaste) override;
+
+ NS_IMETHOD PasteTransferable(nsITransferable* aTransferable) override;
+ NS_IMETHOD CanPasteTransferable(nsITransferable* aTransferable,
+ bool* aCanPaste) override;
+
+ NS_IMETHOD DebugUnitTests(int32_t* outNumTests,
+ int32_t* outNumTestsFailed) override;
+
+ /**
+ * All editor operations which alter the doc should be prefaced
+ * with a call to StartOperation, naming the action and direction.
+ */
+ NS_IMETHOD StartOperation(EditAction opID,
+ nsIEditor::EDirection aDirection) override;
+
+ /**
+ * All editor operations which alter the doc should be followed
+ * with a call to EndOperation.
+ */
+ NS_IMETHOD EndOperation() override;
+
+ /**
+ * returns true if aParentTag can contain a child of type aChildTag.
+ */
+ virtual bool TagCanContainTag(nsIAtom& aParentTag,
+ nsIAtom& aChildTag) override;
+
+ /**
+ * Returns true if aNode is a container.
+ */
+ virtual bool IsContainer(nsINode* aNode) override;
+ virtual bool IsContainer(nsIDOMNode* aNode) override;
+
+ /**
+ * Make the given selection span the entire document.
+ */
+ virtual nsresult SelectEntireDocument(Selection* aSelection) override;
+
+ NS_IMETHOD SetAttributeOrEquivalent(nsIDOMElement* aElement,
+ const nsAString& aAttribute,
+ const nsAString& aValue,
+ bool aSuppressTransaction) override;
+ NS_IMETHOD RemoveAttributeOrEquivalent(nsIDOMElement* aElement,
+ const nsAString& aAttribute,
+ bool aSuppressTransaction) override;
+
+ /**
+ * Join together any adjacent editable text nodes in the range.
+ */
+ nsresult CollapseAdjacentTextNodes(nsRange* aRange);
+
+ virtual bool AreNodesSameType(nsIContent* aNode1,
+ nsIContent* aNode2) override;
+
+ NS_IMETHOD DeleteSelectionImpl(EDirection aAction,
+ EStripWrappers aStripWrappers) override;
+ nsresult DeleteNode(nsINode* aNode);
+ NS_IMETHOD DeleteNode(nsIDOMNode* aNode) override;
+ nsresult DeleteText(nsGenericDOMDataNode& aTextNode, uint32_t aOffset,
+ uint32_t aLength);
+ virtual nsresult InsertTextImpl(const nsAString& aStringToInsert,
+ nsCOMPtr<nsINode>* aInOutNode,
+ int32_t* aInOutOffset,
+ nsIDocument* aDoc) override;
+ NS_IMETHOD_(bool) IsModifiableNode(nsIDOMNode* aNode) override;
+ virtual bool IsModifiableNode(nsINode* aNode) override;
+
+ NS_IMETHOD GetIsSelectionEditable(bool* aIsSelectionEditable) override;
+
+ NS_IMETHOD SelectAll() override;
+
+ // nsICSSLoaderObserver
+ NS_IMETHOD StyleSheetLoaded(StyleSheet* aSheet,
+ bool aWasAlternate, nsresult aStatus) override;
+
+ // Utility Routines, not part of public API
+ NS_IMETHOD TypedText(const nsAString& aString,
+ ETypingAction aAction) override;
+ nsresult InsertNodeAtPoint(nsIDOMNode* aNode,
+ nsCOMPtr<nsIDOMNode>* ioParent,
+ int32_t* ioOffset,
+ bool aNoEmptyNodes);
+
+ /**
+ * Use this to assure that selection is set after attribute nodes when
+ * trying to collapse selection at begining of a block node
+ * e.g., when setting at beginning of a table cell
+ * This will stop at a table, however, since we don't want to
+ * "drill down" into nested tables.
+ * @param aSelection Optional. If null, we get current selection.
+ */
+ void CollapseSelectionToDeepestNonTableFirstChild(Selection* aSelection,
+ nsINode* aNode);
+
+ /**
+ * aNode must be a non-null text node.
+ * outIsEmptyNode must be non-null.
+ */
+ nsresult IsVisTextNode(nsIContent* aNode,
+ bool* outIsEmptyNode,
+ bool aSafeToAskFrames);
+ nsresult IsEmptyNode(nsIDOMNode* aNode, bool* outIsEmptyBlock,
+ bool aMozBRDoesntCount = false,
+ bool aListOrCellNotEmpty = false,
+ bool aSafeToAskFrames = false);
+ nsresult IsEmptyNode(nsINode* aNode, bool* outIsEmptyBlock,
+ bool aMozBRDoesntCount = false,
+ bool aListOrCellNotEmpty = false,
+ bool aSafeToAskFrames = false);
+ nsresult IsEmptyNodeImpl(nsINode* aNode,
+ bool* outIsEmptyBlock,
+ bool aMozBRDoesntCount,
+ bool aListOrCellNotEmpty,
+ bool aSafeToAskFrames,
+ bool* aSeenBR);
+
+ /**
+ * Returns TRUE if sheet was loaded, false if it wasn't.
+ */
+ bool EnableExistingStyleSheet(const nsAString& aURL);
+
+ /**
+ * Dealing with the internal style sheet lists.
+ */
+ StyleSheet* GetStyleSheetForURL(const nsAString& aURL);
+ void GetURLForStyleSheet(StyleSheet* aStyleSheet,
+ nsAString& aURL);
+
+ /**
+ * Add a url + known style sheet to the internal lists.
+ */
+ nsresult AddNewStyleSheetToList(const nsAString &aURL,
+ StyleSheet* aStyleSheet);
+ nsresult RemoveStyleSheetFromList(const nsAString &aURL);
+
+ bool IsCSSEnabled()
+ {
+ // TODO: removal of mCSSAware and use only the presence of mCSSEditUtils
+ return mCSSAware && mCSSEditUtils && mCSSEditUtils->IsCSSPrefChecked();
+ }
+
+ static bool HasAttributes(Element* aElement)
+ {
+ MOZ_ASSERT(aElement);
+ uint32_t attrCount = aElement->GetAttrCount();
+ return attrCount > 1 ||
+ (1 == attrCount &&
+ !aElement->GetAttrNameAt(0)->Equals(nsGkAtoms::mozdirty));
+ }
+
+protected:
+ class BlobReader final : public nsIEditorBlobListener
+ {
+ public:
+ BlobReader(dom::BlobImpl* aBlob, HTMLEditor* aHTMLEditor,
+ bool aIsSafe, nsIDOMDocument* aSourceDoc,
+ nsIDOMNode* aDestinationNode, int32_t aDestOffset,
+ bool aDoDeleteSelection);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIEDITORBLOBLISTENER
+
+ private:
+ ~BlobReader()
+ {
+ }
+
+ RefPtr<dom::BlobImpl> mBlob;
+ RefPtr<HTMLEditor> mHTMLEditor;
+ bool mIsSafe;
+ nsCOMPtr<nsIDOMDocument> mSourceDoc;
+ nsCOMPtr<nsIDOMNode> mDestinationNode;
+ int32_t mDestOffset;
+ bool mDoDeleteSelection;
+ };
+
+ NS_IMETHOD InitRules() override;
+
+ virtual void CreateEventListeners() override;
+ virtual nsresult InstallEventListeners() override;
+ virtual void RemoveEventListeners() override;
+
+ bool ShouldReplaceRootElement();
+ void NotifyRootChanged();
+ nsresult GetBodyElement(nsIDOMHTMLElement** aBody);
+
+ /**
+ * Get the focused node of this editor.
+ * @return If the editor has focus, this returns the focused node.
+ * Otherwise, returns null.
+ */
+ already_AddRefed<nsINode> GetFocusedNode();
+
+ /**
+ * Return TRUE if aElement is a table-related elemet and caret was set.
+ */
+ bool SetCaretInTableCell(nsIDOMElement* aElement);
+
+ NS_IMETHOD TabInTable(bool inIsShift, bool* outHandled);
+ already_AddRefed<Element> CreateBR(nsINode* aNode, int32_t aOffset,
+ EDirection aSelect = eNone);
+ NS_IMETHOD CreateBR(
+ nsIDOMNode* aNode, int32_t aOffset,
+ nsCOMPtr<nsIDOMNode>* outBRNode,
+ nsIEditor::EDirection aSelect = nsIEditor::eNone) override;
+
+ // Table Editing (implemented in nsTableEditor.cpp)
+
+ /**
+ * Insert a new cell after or before supplied aCell.
+ * Optional: If aNewCell supplied, returns the newly-created cell (addref'd,
+ * of course)
+ * This doesn't change or use the current selection.
+ */
+ NS_IMETHOD InsertCell(nsIDOMElement* aCell, int32_t aRowSpan,
+ int32_t aColSpan, bool aAfter, bool aIsHeader,
+ nsIDOMElement** aNewCell);
+
+ /**
+ * Helpers that don't touch the selection or do batch transactions.
+ */
+ NS_IMETHOD DeleteRow(nsIDOMElement* aTable, int32_t aRowIndex);
+ NS_IMETHOD DeleteColumn(nsIDOMElement* aTable, int32_t aColIndex);
+ NS_IMETHOD DeleteCellContents(nsIDOMElement* aCell);
+
+ /**
+ * Move all contents from aCellToMerge into aTargetCell (append at end).
+ */
+ NS_IMETHOD MergeCells(nsCOMPtr<nsIDOMElement> aTargetCell,
+ nsCOMPtr<nsIDOMElement> aCellToMerge,
+ bool aDeleteCellToMerge);
+
+ nsresult DeleteTable2(nsIDOMElement* aTable, Selection* aSelection);
+ NS_IMETHOD SetColSpan(nsIDOMElement* aCell, int32_t aColSpan);
+ NS_IMETHOD SetRowSpan(nsIDOMElement* aCell, int32_t aRowSpan);
+
+ /**
+ * Helper used to get nsTableWrapperFrame for a table.
+ */
+ nsTableWrapperFrame* GetTableFrame(nsIDOMElement* aTable);
+
+ /**
+ * Needed to do appropriate deleting when last cell or row is about to be
+ * deleted. This doesn't count cells that don't start in the given row (are
+ * spanning from row above).
+ */
+ int32_t GetNumberOfCellsInRow(nsIDOMElement* aTable, int32_t rowIndex);
+
+ /**
+ * Test if all cells in row or column at given index are selected.
+ */
+ bool AllCellsInRowSelected(nsIDOMElement* aTable, int32_t aRowIndex,
+ int32_t aNumberOfColumns);
+ bool AllCellsInColumnSelected(nsIDOMElement* aTable, int32_t aColIndex,
+ int32_t aNumberOfRows);
+
+ bool IsEmptyCell(Element* aCell);
+
+ /**
+ * Most insert methods need to get the same basic context data.
+ * Any of the pointers may be null if you don't need that datum (for more
+ * efficiency).
+ * Input: *aCell is a known cell,
+ * if null, cell is obtained from the anchor node of the selection.
+ * Returns NS_EDITOR_ELEMENT_NOT_FOUND if cell is not found even if aCell is
+ * null.
+ */
+ nsresult GetCellContext(Selection** aSelection, nsIDOMElement** aTable,
+ nsIDOMElement** aCell, nsIDOMNode** aCellParent,
+ int32_t* aCellOffset, int32_t* aRowIndex,
+ int32_t* aColIndex);
+
+ NS_IMETHOD GetCellSpansAt(nsIDOMElement* aTable, int32_t aRowIndex,
+ int32_t aColIndex, int32_t& aActualRowSpan,
+ int32_t& aActualColSpan);
+
+ NS_IMETHOD SplitCellIntoColumns(nsIDOMElement* aTable, int32_t aRowIndex,
+ int32_t aColIndex, int32_t aColSpanLeft,
+ int32_t aColSpanRight,
+ nsIDOMElement** aNewCell);
+
+ NS_IMETHOD SplitCellIntoRows(nsIDOMElement* aTable, int32_t aRowIndex,
+ int32_t aColIndex, int32_t aRowSpanAbove,
+ int32_t aRowSpanBelow, nsIDOMElement** aNewCell);
+
+ nsresult CopyCellBackgroundColor(nsIDOMElement* destCell,
+ nsIDOMElement* sourceCell);
+
+ /**
+ * Reduce rowspan/colspan when cells span into nonexistent rows/columns.
+ */
+ NS_IMETHOD FixBadRowSpan(nsIDOMElement* aTable, int32_t aRowIndex,
+ int32_t& aNewRowCount);
+ NS_IMETHOD FixBadColSpan(nsIDOMElement* aTable, int32_t aColIndex,
+ int32_t& aNewColCount);
+
+ /**
+ * Fallback method: Call this after using ClearSelection() and you
+ * failed to set selection to some other content in the document.
+ */
+ nsresult SetSelectionAtDocumentStart(Selection* aSelection);
+
+ // End of Table Editing utilities
+
+ static Element* GetEnclosingTable(nsINode* aNode);
+ static nsIDOMNode* GetEnclosingTable(nsIDOMNode* aNode);
+
+ /**
+ * Content-based query returns true if <aProperty aAttribute=aValue> effects
+ * aNode. If <aProperty aAttribute=aValue> contains aNode, but
+ * <aProperty aAttribute=SomeOtherValue> also contains aNode and the second is
+ * more deeply nested than the first, then the first does not effect aNode.
+ *
+ * @param aNode The target of the query
+ * @param aProperty The property that we are querying for
+ * @param aAttribute The attribute of aProperty, example: color in
+ * <FONT color="blue"> May be null.
+ * @param aValue The value of aAttribute, example: blue in
+ * <FONT color="blue"> May be null. Ignored if aAttribute
+ * is null.
+ * @param aIsSet [OUT] true if <aProperty aAttribute=aValue> effects
+ * aNode.
+ * @param outValue [OUT] the value of the attribute, if aIsSet is true
+ *
+ * The nsIContent variant returns aIsSet instead of using an out parameter.
+ */
+ bool IsTextPropertySetByContent(nsINode* aNode,
+ nsIAtom* aProperty,
+ const nsAString* aAttribute,
+ const nsAString* aValue,
+ nsAString* outValue = nullptr);
+
+ void IsTextPropertySetByContent(nsIDOMNode* aNode,
+ nsIAtom* aProperty,
+ const nsAString* aAttribute,
+ const nsAString* aValue,
+ bool& aIsSet,
+ nsAString* outValue = nullptr);
+
+ // Methods for handling plaintext quotations
+ NS_IMETHOD PasteAsPlaintextQuotation(int32_t aSelectionType);
+
+ /**
+ * Insert a string as quoted text, replacing the selected text (if any).
+ * @param aQuotedText The string to insert.
+ * @param aAddCites Whether to prepend extra ">" to each line
+ * (usually true, unless those characters
+ * have already been added.)
+ * @return aNodeInserted The node spanning the insertion, if applicable.
+ * If aAddCites is false, this will be null.
+ */
+ NS_IMETHOD InsertAsPlaintextQuotation(const nsAString& aQuotedText,
+ bool aAddCites,
+ nsIDOMNode** aNodeInserted);
+
+ nsresult InsertObject(const nsACString& aType, nsISupports* aObject,
+ bool aIsSafe,
+ nsIDOMDocument* aSourceDoc,
+ nsIDOMNode* aDestinationNode,
+ int32_t aDestOffset,
+ bool aDoDeleteSelection);
+
+ // factored methods for handling insertion of data from transferables
+ // (drag&drop or clipboard)
+ NS_IMETHOD PrepareTransferable(nsITransferable** transferable) override;
+ nsresult PrepareHTMLTransferable(nsITransferable** transferable);
+ nsresult InsertFromTransferable(nsITransferable* transferable,
+ nsIDOMDocument* aSourceDoc,
+ const nsAString& aContextStr,
+ const nsAString& aInfoStr,
+ bool havePrivateHTMLFlavor,
+ nsIDOMNode *aDestinationNode,
+ int32_t aDestinationOffset,
+ bool aDoDeleteSelection);
+ nsresult InsertFromDataTransfer(dom::DataTransfer* aDataTransfer,
+ int32_t aIndex,
+ nsIDOMDocument* aSourceDoc,
+ nsIDOMNode* aDestinationNode,
+ int32_t aDestOffset,
+ bool aDoDeleteSelection) override;
+ bool HavePrivateHTMLFlavor(nsIClipboard* clipboard );
+ nsresult ParseCFHTML(nsCString& aCfhtml, char16_t** aStuffToPaste,
+ char16_t** aCfcontext);
+ nsresult DoContentFilterCallback(const nsAString& aFlavor,
+ nsIDOMDocument* aSourceDoc,
+ bool aWillDeleteSelection,
+ nsIDOMNode** aFragmentAsNode,
+ nsIDOMNode** aFragStartNode,
+ int32_t* aFragStartOffset,
+ nsIDOMNode** aFragEndNode,
+ int32_t* aFragEndOffset,
+ nsIDOMNode** aTargetNode,
+ int32_t* aTargetOffset,
+ bool* aDoContinue);
+
+ bool IsInLink(nsIDOMNode* aNode, nsCOMPtr<nsIDOMNode>* outLink = nullptr);
+ nsresult StripFormattingNodes(nsIContent& aNode, bool aOnlyList = false);
+ nsresult CreateDOMFragmentFromPaste(const nsAString& aInputString,
+ const nsAString& aContextStr,
+ const nsAString& aInfoStr,
+ nsCOMPtr<nsIDOMNode>* outFragNode,
+ nsCOMPtr<nsIDOMNode>* outStartNode,
+ nsCOMPtr<nsIDOMNode>* outEndNode,
+ int32_t* outStartOffset,
+ int32_t* outEndOffset,
+ bool aTrustedInput);
+ nsresult ParseFragment(const nsAString& aStr, nsIAtom* aContextLocalName,
+ nsIDocument* aTargetDoc,
+ dom::DocumentFragment** aFragment, bool aTrustedInput);
+ void CreateListOfNodesToPaste(dom::DocumentFragment& aFragment,
+ nsTArray<OwningNonNull<nsINode>>& outNodeList,
+ nsINode* aStartNode,
+ int32_t aStartOffset,
+ nsINode* aEndNode,
+ int32_t aEndOffset);
+ nsresult CreateTagStack(nsTArray<nsString>& aTagStack,
+ nsIDOMNode* aNode);
+ enum class StartOrEnd { start, end };
+ void GetListAndTableParents(StartOrEnd aStartOrEnd,
+ nsTArray<OwningNonNull<nsINode>>& aNodeList,
+ nsTArray<OwningNonNull<Element>>& outArray);
+ int32_t DiscoverPartialListsAndTables(
+ nsTArray<OwningNonNull<nsINode>>& aPasteNodes,
+ nsTArray<OwningNonNull<Element>>& aListsAndTables);
+ nsINode* ScanForListAndTableStructure(
+ StartOrEnd aStartOrEnd,
+ nsTArray<OwningNonNull<nsINode>>& aNodes,
+ Element& aListOrTable);
+ void ReplaceOrphanedStructure(
+ StartOrEnd aStartOrEnd,
+ nsTArray<OwningNonNull<nsINode>>& aNodeArray,
+ nsTArray<OwningNonNull<Element>>& aListAndTableArray,
+ int32_t aHighWaterMark);
+
+ /**
+ * Small utility routine to test if a break node is visible to user.
+ */
+ bool IsVisBreak(nsINode* aNode);
+
+ /**
+ * Utility routine to possibly adjust the insertion position when
+ * inserting a block level element.
+ */
+ void NormalizeEOLInsertPosition(nsINode* firstNodeToInsert,
+ nsCOMPtr<nsIDOMNode>* insertParentNode,
+ int32_t* insertOffset);
+
+ /**
+ * Small utility routine to test the eEditorReadonly bit.
+ */
+ bool IsModifiable();
+
+ /**
+ * Helpers for block transformations.
+ */
+ nsresult MakeDefinitionItem(const nsAString& aItemType);
+ nsresult InsertBasicBlock(const nsAString& aBlockType);
+
+ /**
+ * Increase/decrease the font size of selection.
+ */
+ enum class FontSize { incr, decr };
+ nsresult RelativeFontChange(FontSize aDir);
+
+ /**
+ * Helper routines for font size changing.
+ */
+ nsresult RelativeFontChangeOnTextNode(FontSize aDir,
+ Text& aTextNode,
+ int32_t aStartOffset,
+ int32_t aEndOffset);
+ nsresult RelativeFontChangeOnNode(int32_t aSizeChange, nsIContent* aNode);
+ nsresult RelativeFontChangeHelper(int32_t aSizeChange, nsINode* aNode);
+
+ /**
+ * Helper routines for inline style.
+ */
+ nsresult SetInlinePropertyOnTextNode(Text& aData,
+ int32_t aStartOffset,
+ int32_t aEndOffset,
+ nsIAtom& aProperty,
+ const nsAString* aAttribute,
+ const nsAString& aValue);
+ nsresult SetInlinePropertyOnNode(nsIContent& aNode,
+ nsIAtom& aProperty,
+ const nsAString* aAttribute,
+ const nsAString& aValue);
+
+ nsresult PromoteInlineRange(nsRange& aRange);
+ nsresult PromoteRangeIfStartsOrEndsInNamedAnchor(nsRange& aRange);
+ nsresult SplitStyleAboveRange(nsRange* aRange,
+ nsIAtom* aProperty,
+ const nsAString* aAttribute);
+ nsresult SplitStyleAbovePoint(nsCOMPtr<nsINode>* aNode, int32_t* aOffset,
+ nsIAtom* aProperty,
+ const nsAString* aAttribute,
+ nsIContent** aOutLeftNode = nullptr,
+ nsIContent** aOutRightNode = nullptr);
+ nsresult ApplyDefaultProperties();
+ nsresult RemoveStyleInside(nsIContent& aNode,
+ nsIAtom* aProperty,
+ const nsAString* aAttribute,
+ const bool aChildrenOnly = false);
+ nsresult RemoveInlinePropertyImpl(nsIAtom* aProperty,
+ const nsAString* aAttribute);
+
+ bool NodeIsProperty(nsINode& aNode);
+ bool IsAtFrontOfNode(nsINode& aNode, int32_t aOffset);
+ bool IsAtEndOfNode(nsINode& aNode, int32_t aOffset);
+ bool IsOnlyAttribute(const nsIContent* aElement, const nsAString& aAttribute);
+
+ nsresult RemoveBlockContainer(nsIContent& aNode);
+
+ nsIContent* GetPriorHTMLSibling(nsINode* aNode);
+ nsresult GetPriorHTMLSibling(nsIDOMNode*inNode,
+ nsCOMPtr<nsIDOMNode>* outNode);
+ nsIContent* GetPriorHTMLSibling(nsINode* aParent, int32_t aOffset);
+ nsresult GetPriorHTMLSibling(nsIDOMNode* inParent, int32_t inOffset,
+ nsCOMPtr<nsIDOMNode>* outNode);
+
+ nsIContent* GetNextHTMLSibling(nsINode* aNode);
+ nsresult GetNextHTMLSibling(nsIDOMNode* inNode,
+ nsCOMPtr<nsIDOMNode>* outNode);
+ nsIContent* GetNextHTMLSibling(nsINode* aParent, int32_t aOffset);
+ nsresult GetNextHTMLSibling(nsIDOMNode* inParent, int32_t inOffset,
+ nsCOMPtr<nsIDOMNode>* outNode);
+
+ nsIContent* GetPriorHTMLNode(nsINode* aNode, bool aNoBlockCrossing = false);
+ nsresult GetPriorHTMLNode(nsIDOMNode* inNode, nsCOMPtr<nsIDOMNode>* outNode,
+ bool bNoBlockCrossing = false);
+ nsIContent* GetPriorHTMLNode(nsINode* aParent, int32_t aOffset,
+ bool aNoBlockCrossing = false);
+ nsresult GetPriorHTMLNode(nsIDOMNode* inParent, int32_t inOffset,
+ nsCOMPtr<nsIDOMNode>* outNode,
+ bool bNoBlockCrossing = false);
+
+ nsIContent* GetNextHTMLNode(nsINode* aNode, bool aNoBlockCrossing = false);
+ nsresult GetNextHTMLNode(nsIDOMNode* inNode, nsCOMPtr<nsIDOMNode>* outNode,
+ bool bNoBlockCrossing = false);
+ nsIContent* GetNextHTMLNode(nsINode* aParent, int32_t aOffset,
+ bool aNoBlockCrossing = false);
+ nsresult GetNextHTMLNode(nsIDOMNode* inParent, int32_t inOffset,
+ nsCOMPtr<nsIDOMNode>* outNode,
+ bool bNoBlockCrossing = false);
+
+ nsresult IsFirstEditableChild(nsIDOMNode* aNode, bool* aOutIsFirst);
+ nsresult IsLastEditableChild(nsIDOMNode* aNode, bool* aOutIsLast);
+ nsIContent* GetFirstEditableChild(nsINode& aNode);
+ nsIContent* GetLastEditableChild(nsINode& aNode);
+
+ nsIContent* GetFirstEditableLeaf(nsINode& aNode);
+ nsIContent* GetLastEditableLeaf(nsINode& aNode);
+
+ nsresult GetInlinePropertyBase(nsIAtom& aProperty,
+ const nsAString* aAttribute,
+ const nsAString* aValue,
+ bool* aFirst,
+ bool* aAny,
+ bool* aAll,
+ nsAString* outValue,
+ bool aCheckDefaults = true);
+ bool HasStyleOrIdOrClass(Element* aElement);
+ nsresult RemoveElementIfNoStyleOrIdOrClass(Element& aElement);
+
+ /**
+ * Whether the outer window of the DOM event target has focus or not.
+ */
+ bool OurWindowHasFocus();
+
+ /**
+ * This function is used to insert a string of HTML input optionally with some
+ * context information into the editable field. The HTML input either comes
+ * from a transferable object created as part of a drop/paste operation, or
+ * from the InsertHTML method. We may want the HTML input to be sanitized
+ * (for example, if it's coming from a transferable object), in which case
+ * aTrustedInput should be set to false, otherwise, the caller should set it
+ * to true, which means that the HTML will be inserted in the DOM verbatim.
+ *
+ * aClearStyle should be set to false if you want the paste to be affected by
+ * local style (e.g., for the insertHTML command).
+ */
+ nsresult DoInsertHTMLWithContext(const nsAString& aInputString,
+ const nsAString& aContextStr,
+ const nsAString& aInfoStr,
+ const nsAString& aFlavor,
+ nsIDOMDocument* aSourceDoc,
+ nsIDOMNode* aDestNode,
+ int32_t aDestOffset,
+ bool aDeleteSelection,
+ bool aTrustedInput,
+ bool aClearStyle = true);
+
+ nsresult ClearStyle(nsCOMPtr<nsINode>* aNode, int32_t* aOffset,
+ nsIAtom* aProperty, const nsAString* aAttribute);
+
+ void SetElementPosition(Element& aElement, int32_t aX, int32_t aY);
+
+protected:
+ nsTArray<OwningNonNull<nsIContentFilter>> mContentFilters;
+
+ RefPtr<TypeInState> mTypeInState;
+
+ bool mCRInParagraphCreatesParagraph;
+
+ bool mCSSAware;
+ nsAutoPtr<CSSEditUtils> mCSSEditUtils;
+
+ // Used by GetFirstSelectedCell and GetNextSelectedCell
+ int32_t mSelectedCellIndex;
+
+ nsString mLastStyleSheetURL;
+ nsString mLastOverrideStyleSheetURL;
+
+ // Maintain a list of associated style sheets and their urls.
+ nsTArray<nsString> mStyleSheetURLs;
+ nsTArray<RefPtr<StyleSheet>> mStyleSheets;
+
+ // an array for holding default style settings
+ nsTArray<PropItem*> mDefaultStyles;
+
+protected:
+ // ANONYMOUS UTILS
+ void RemoveListenerAndDeleteRef(const nsAString& aEvent,
+ nsIDOMEventListener* aListener,
+ bool aUseCapture,
+ Element* aElement,
+ nsIContent* aParentContent,
+ nsIPresShell* aShell);
+ void DeleteRefToAnonymousNode(nsIDOMElement* aElement,
+ nsIContent* aParentContent,
+ nsIPresShell* aShell);
+
+ nsresult ShowResizersInner(nsIDOMElement *aResizedElement);
+
+ /**
+ * Returns the offset of an element's frame to its absolute containing block.
+ */
+ nsresult GetElementOrigin(nsIDOMElement* aElement,
+ int32_t& aX, int32_t& aY);
+ nsresult GetPositionAndDimensions(nsIDOMElement* aElement,
+ int32_t& aX, int32_t& aY,
+ int32_t& aW, int32_t& aH,
+ int32_t& aBorderLeft,
+ int32_t& aBorderTop,
+ int32_t& aMarginLeft,
+ int32_t& aMarginTop);
+
+ bool IsInObservedSubtree(nsIDocument* aDocument,
+ nsIContent* aContainer,
+ nsIContent* aChild);
+
+ void UpdateRootElement();
+
+ // resizing
+ bool mIsObjectResizingEnabled;
+ bool mIsResizing;
+ bool mPreserveRatio;
+ bool mResizedObjectIsAnImage;
+
+ // absolute positioning
+ bool mIsAbsolutelyPositioningEnabled;
+ bool mResizedObjectIsAbsolutelyPositioned;
+
+ bool mGrabberClicked;
+ bool mIsMoving;
+
+ bool mSnapToGridEnabled;
+
+ // inline table editing
+ bool mIsInlineTableEditingEnabled;
+
+ // resizing
+ nsCOMPtr<Element> mTopLeftHandle;
+ nsCOMPtr<Element> mTopHandle;
+ nsCOMPtr<Element> mTopRightHandle;
+ nsCOMPtr<Element> mLeftHandle;
+ nsCOMPtr<Element> mRightHandle;
+ nsCOMPtr<Element> mBottomLeftHandle;
+ nsCOMPtr<Element> mBottomHandle;
+ nsCOMPtr<Element> mBottomRightHandle;
+
+ nsCOMPtr<Element> mActivatedHandle;
+
+ nsCOMPtr<Element> mResizingShadow;
+ nsCOMPtr<Element> mResizingInfo;
+
+ nsCOMPtr<Element> mResizedObject;
+
+ nsCOMPtr<nsIDOMEventListener> mMouseMotionListenerP;
+ nsCOMPtr<nsISelectionListener> mSelectionListenerP;
+ nsCOMPtr<nsIDOMEventListener> mResizeEventListenerP;
+
+ nsTArray<OwningNonNull<nsIHTMLObjectResizeListener>> mObjectResizeEventListeners;
+
+ int32_t mOriginalX;
+ int32_t mOriginalY;
+
+ int32_t mResizedObjectX;
+ int32_t mResizedObjectY;
+ int32_t mResizedObjectWidth;
+ int32_t mResizedObjectHeight;
+
+ int32_t mResizedObjectMarginLeft;
+ int32_t mResizedObjectMarginTop;
+ int32_t mResizedObjectBorderLeft;
+ int32_t mResizedObjectBorderTop;
+
+ int32_t mXIncrementFactor;
+ int32_t mYIncrementFactor;
+ int32_t mWidthIncrementFactor;
+ int32_t mHeightIncrementFactor;
+
+ int8_t mInfoXIncrement;
+ int8_t mInfoYIncrement;
+
+ nsresult SetAllResizersPosition();
+
+ already_AddRefed<Element> CreateResizer(int16_t aLocation,
+ nsIDOMNode* aParentNode);
+ void SetAnonymousElementPosition(int32_t aX, int32_t aY,
+ nsIDOMElement* aResizer);
+
+ already_AddRefed<Element> CreateShadow(nsIDOMNode* aParentNode,
+ nsIDOMElement* aOriginalObject);
+ nsresult SetShadowPosition(Element* aShadow, Element* aOriginalObject,
+ int32_t aOriginalObjectX,
+ int32_t aOriginalObjectY);
+
+ already_AddRefed<Element> CreateResizingInfo(nsIDOMNode* aParentNode);
+ nsresult SetResizingInfoPosition(int32_t aX, int32_t aY,
+ int32_t aW, int32_t aH);
+
+ int32_t GetNewResizingIncrement(int32_t aX, int32_t aY, int32_t aID);
+ nsresult StartResizing(nsIDOMElement* aHandle);
+ int32_t GetNewResizingX(int32_t aX, int32_t aY);
+ int32_t GetNewResizingY(int32_t aX, int32_t aY);
+ int32_t GetNewResizingWidth(int32_t aX, int32_t aY);
+ int32_t GetNewResizingHeight(int32_t aX, int32_t aY);
+ void HideShadowAndInfo();
+ void SetFinalSize(int32_t aX, int32_t aY);
+ void DeleteRefToAnonymousNode(nsIDOMNode* aNode);
+ void SetResizeIncrements(int32_t aX, int32_t aY, int32_t aW, int32_t aH,
+ bool aPreserveRatio);
+ void HideAnonymousEditingUIs();
+
+ // absolute positioning
+ int32_t mPositionedObjectX;
+ int32_t mPositionedObjectY;
+ int32_t mPositionedObjectWidth;
+ int32_t mPositionedObjectHeight;
+
+ int32_t mPositionedObjectMarginLeft;
+ int32_t mPositionedObjectMarginTop;
+ int32_t mPositionedObjectBorderLeft;
+ int32_t mPositionedObjectBorderTop;
+
+ nsCOMPtr<Element> mAbsolutelyPositionedObject;
+ nsCOMPtr<Element> mGrabber;
+ nsCOMPtr<Element> mPositioningShadow;
+
+ int32_t mGridSize;
+
+ already_AddRefed<Element> CreateGrabber(nsINode* aParentNode);
+ nsresult StartMoving(nsIDOMElement* aHandle);
+ nsresult SetFinalPosition(int32_t aX, int32_t aY);
+ void AddPositioningOffset(int32_t& aX, int32_t& aY);
+ void SnapToGrid(int32_t& newX, int32_t& newY);
+ nsresult GrabberClicked();
+ nsresult EndMoving();
+ nsresult CheckPositionedElementBGandFG(nsIDOMElement* aElement,
+ nsAString& aReturn);
+
+ // inline table editing
+ nsCOMPtr<nsIDOMElement> mInlineEditedCell;
+
+ nsCOMPtr<nsIDOMElement> mAddColumnBeforeButton;
+ nsCOMPtr<nsIDOMElement> mRemoveColumnButton;
+ nsCOMPtr<nsIDOMElement> mAddColumnAfterButton;
+
+ nsCOMPtr<nsIDOMElement> mAddRowBeforeButton;
+ nsCOMPtr<nsIDOMElement> mRemoveRowButton;
+ nsCOMPtr<nsIDOMElement> mAddRowAfterButton;
+
+ void AddMouseClickListener(nsIDOMElement* aElement);
+ void RemoveMouseClickListener(nsIDOMElement* aElement);
+
+ nsCOMPtr<nsILinkHandler> mLinkHandler;
+
+public:
+ friend class HTMLEditorEventListener;
+ friend class HTMLEditRules;
+ friend class TextEditRules;
+ friend class WSRunObject;
+
+private:
+ bool IsSimpleModifiableNode(nsIContent* aContent,
+ nsIAtom* aProperty,
+ const nsAString* aAttribute,
+ const nsAString* aValue);
+ nsresult SetInlinePropertyOnNodeImpl(nsIContent& aNode,
+ nsIAtom& aProperty,
+ const nsAString* aAttribute,
+ const nsAString& aValue);
+ typedef enum { eInserted, eAppended } InsertedOrAppended;
+ void DoContentInserted(nsIDocument* aDocument, nsIContent* aContainer,
+ nsIContent* aChild, int32_t aIndexInContainer,
+ InsertedOrAppended aInsertedOrAppended);
+ already_AddRefed<Element> GetElementOrParentByTagName(
+ const nsAString& aTagName, nsINode* aNode);
+ already_AddRefed<Element> CreateElementWithDefaults(
+ const nsAString& aTagName);
+};
+
+} // namespace mozilla
+
+#endif // #ifndef mozilla_HTMLEditor_h
diff --git a/editor/libeditor/HTMLEditorDataTransfer.cpp b/editor/libeditor/HTMLEditorDataTransfer.cpp
new file mode 100644
index 000000000..b9cd8adb9
--- /dev/null
+++ b/editor/libeditor/HTMLEditorDataTransfer.cpp
@@ -0,0 +1,2409 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sw=2 et tw=78: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/HTMLEditor.h"
+
+#include <string.h>
+
+#include "HTMLEditUtils.h"
+#include "TextEditUtils.h"
+#include "WSRunObject.h"
+#include "mozilla/dom/DataTransfer.h"
+#include "mozilla/dom/DocumentFragment.h"
+#include "mozilla/dom/DOMStringList.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Base64.h"
+#include "mozilla/BasicEvents.h"
+#include "mozilla/EditorUtils.h"
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/SelectionState.h"
+#include "nsAString.h"
+#include "nsCOMPtr.h"
+#include "nsCRTGlue.h" // for CRLF
+#include "nsComponentManagerUtils.h"
+#include "nsIScriptError.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsDependentSubstring.h"
+#include "nsError.h"
+#include "nsGkAtoms.h"
+#include "nsIClipboard.h"
+#include "nsIContent.h"
+#include "nsIContentFilter.h"
+#include "nsIDOMComment.h"
+#include "nsIDOMDocument.h"
+#include "nsIDOMDocumentFragment.h"
+#include "nsIDOMElement.h"
+#include "nsIDOMHTMLAnchorElement.h"
+#include "nsIDOMHTMLEmbedElement.h"
+#include "nsIDOMHTMLFrameElement.h"
+#include "nsIDOMHTMLIFrameElement.h"
+#include "nsIDOMHTMLImageElement.h"
+#include "nsIDOMHTMLInputElement.h"
+#include "nsIDOMHTMLLinkElement.h"
+#include "nsIDOMHTMLObjectElement.h"
+#include "nsIDOMHTMLScriptElement.h"
+#include "nsIDOMNode.h"
+#include "nsIDocument.h"
+#include "nsIEditor.h"
+#include "nsIEditorIMESupport.h"
+#include "nsIEditorMailSupport.h"
+#include "nsIEditRules.h"
+#include "nsIFile.h"
+#include "nsIInputStream.h"
+#include "nsIMIMEService.h"
+#include "nsNameSpaceManager.h"
+#include "nsINode.h"
+#include "nsIParserUtils.h"
+#include "nsIPlaintextEditor.h"
+#include "nsISupportsImpl.h"
+#include "nsISupportsPrimitives.h"
+#include "nsISupportsUtils.h"
+#include "nsITransferable.h"
+#include "nsIURI.h"
+#include "nsIVariant.h"
+#include "nsLinebreakConverter.h"
+#include "nsLiteralString.h"
+#include "nsNetUtil.h"
+#include "nsRange.h"
+#include "nsReadableUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsStreamUtils.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsStringIterator.h"
+#include "nsSubstringTuple.h"
+#include "nsTreeSanitizer.h"
+#include "nsXPCOM.h"
+#include "nscore.h"
+#include "nsContentUtils.h"
+
+class nsIAtom;
+class nsILoadContext;
+class nsISupports;
+
+namespace mozilla {
+
+using namespace dom;
+
+#define kInsertCookie "_moz_Insert Here_moz_"
+
+// some little helpers
+static bool FindIntegerAfterString(const char* aLeadingString,
+ nsCString& aCStr, int32_t& foundNumber);
+static nsresult RemoveFragComments(nsCString& theStr);
+static void RemoveBodyAndHead(nsINode& aNode);
+static nsresult FindTargetNode(nsIDOMNode* aStart,
+ nsCOMPtr<nsIDOMNode>& aResult);
+
+nsresult
+HTMLEditor::LoadHTML(const nsAString& aInputString)
+{
+ NS_ENSURE_TRUE(mRules, NS_ERROR_NOT_INITIALIZED);
+
+ // force IME commit; set up rules sniffing and batching
+ ForceCompositionEnd();
+ AutoEditBatch beginBatching(this);
+ AutoRules beginRulesSniffing(this, EditAction::loadHTML, nsIEditor::eNext);
+
+ // Get selection
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_STATE(selection);
+
+ TextRulesInfo ruleInfo(EditAction::loadHTML);
+ bool cancel, handled;
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+ nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (cancel) {
+ return NS_OK; // rules canceled the operation
+ }
+
+ if (!handled) {
+ // Delete Selection, but only if it isn't collapsed, see bug #106269
+ if (!selection->Collapsed()) {
+ rv = DeleteSelection(eNone, eStrip);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Get the first range in the selection, for context:
+ RefPtr<nsRange> range = selection->GetRangeAt(0);
+ NS_ENSURE_TRUE(range, NS_ERROR_NULL_POINTER);
+
+ // create fragment for pasted html
+ nsCOMPtr<nsIDOMDocumentFragment> docfrag;
+ rv = range->CreateContextualFragment(aInputString, getter_AddRefs(docfrag));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // put the fragment into the document
+ nsCOMPtr<nsIDOMNode> parent;
+ rv = range->GetStartContainer(getter_AddRefs(parent));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(parent, NS_ERROR_NULL_POINTER);
+ int32_t childOffset;
+ rv = range->GetStartOffset(&childOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDOMNode> nodeToInsert;
+ docfrag->GetFirstChild(getter_AddRefs(nodeToInsert));
+ while (nodeToInsert) {
+ rv = InsertNode(nodeToInsert, parent, childOffset++);
+ NS_ENSURE_SUCCESS(rv, rv);
+ docfrag->GetFirstChild(getter_AddRefs(nodeToInsert));
+ }
+ }
+
+ return rules->DidDoAction(selection, &ruleInfo, rv);
+}
+
+NS_IMETHODIMP
+HTMLEditor::InsertHTML(const nsAString& aInString)
+{
+ const nsAFlatString& empty = EmptyString();
+
+ return InsertHTMLWithContext(aInString, empty, empty, empty,
+ nullptr, nullptr, 0, true);
+}
+
+nsresult
+HTMLEditor::InsertHTMLWithContext(const nsAString& aInputString,
+ const nsAString& aContextStr,
+ const nsAString& aInfoStr,
+ const nsAString& aFlavor,
+ nsIDOMDocument* aSourceDoc,
+ nsIDOMNode* aDestNode,
+ int32_t aDestOffset,
+ bool aDeleteSelection)
+{
+ return DoInsertHTMLWithContext(aInputString, aContextStr, aInfoStr,
+ aFlavor, aSourceDoc, aDestNode, aDestOffset, aDeleteSelection,
+ /* trusted input */ true, /* clear style */ false);
+}
+
+nsresult
+HTMLEditor::DoInsertHTMLWithContext(const nsAString& aInputString,
+ const nsAString& aContextStr,
+ const nsAString& aInfoStr,
+ const nsAString& aFlavor,
+ nsIDOMDocument* aSourceDoc,
+ nsIDOMNode* aDestNode,
+ int32_t aDestOffset,
+ bool aDeleteSelection,
+ bool aTrustedInput,
+ bool aClearStyle)
+{
+ NS_ENSURE_TRUE(mRules, NS_ERROR_NOT_INITIALIZED);
+
+ // Prevent the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+
+ // force IME commit; set up rules sniffing and batching
+ ForceCompositionEnd();
+ AutoEditBatch beginBatching(this);
+ AutoRules beginRulesSniffing(this, EditAction::htmlPaste, nsIEditor::eNext);
+
+ // Get selection
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_STATE(selection);
+
+ // create a dom document fragment that represents the structure to paste
+ nsCOMPtr<nsIDOMNode> fragmentAsNode, streamStartParent, streamEndParent;
+ int32_t streamStartOffset = 0, streamEndOffset = 0;
+
+ nsresult rv = CreateDOMFragmentFromPaste(aInputString, aContextStr, aInfoStr,
+ address_of(fragmentAsNode),
+ address_of(streamStartParent),
+ address_of(streamEndParent),
+ &streamStartOffset,
+ &streamEndOffset,
+ aTrustedInput);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDOMNode> targetNode;
+ int32_t targetOffset=0;
+
+ if (!aDestNode) {
+ // if caller didn't provide the destination/target node,
+ // fetch the paste insertion point from our selection
+ rv = GetStartNodeAndOffset(selection, getter_AddRefs(targetNode), &targetOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!targetNode || !IsEditable(targetNode)) {
+ return NS_ERROR_FAILURE;
+ }
+ } else {
+ targetNode = aDestNode;
+ targetOffset = aDestOffset;
+ }
+
+ bool doContinue = true;
+
+ rv = DoContentFilterCallback(aFlavor, aSourceDoc, aDeleteSelection,
+ (nsIDOMNode **)address_of(fragmentAsNode),
+ (nsIDOMNode **)address_of(streamStartParent),
+ &streamStartOffset,
+ (nsIDOMNode **)address_of(streamEndParent),
+ &streamEndOffset,
+ (nsIDOMNode **)address_of(targetNode),
+ &targetOffset, &doContinue);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(doContinue, NS_OK);
+
+ // if we have a destination / target node, we want to insert there
+ // rather than in place of the selection
+ // ignore aDeleteSelection here if no aDestNode since deletion will
+ // also occur later; this block is intended to cover the various
+ // scenarios where we are dropping in an editor (and may want to delete
+ // the selection before collapsing the selection in the new destination)
+ if (aDestNode) {
+ if (aDeleteSelection) {
+ // Use an auto tracker so that our drop point is correctly
+ // positioned after the delete.
+ AutoTrackDOMPoint tracker(mRangeUpdater, &targetNode, &targetOffset);
+ rv = DeleteSelection(eNone, eStrip);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = selection->Collapse(targetNode, targetOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // we need to recalculate various things based on potentially new offsets
+ // this is work to be completed at a later date (probably by jfrancis)
+
+ // make a list of what nodes in docFrag we need to move
+ nsTArray<OwningNonNull<nsINode>> nodeList;
+ nsCOMPtr<nsINode> fragmentAsNodeNode = do_QueryInterface(fragmentAsNode);
+ NS_ENSURE_STATE(fragmentAsNodeNode || !fragmentAsNode);
+ nsCOMPtr<nsINode> streamStartParentNode =
+ do_QueryInterface(streamStartParent);
+ NS_ENSURE_STATE(streamStartParentNode || !streamStartParent);
+ nsCOMPtr<nsINode> streamEndParentNode =
+ do_QueryInterface(streamEndParent);
+ NS_ENSURE_STATE(streamEndParentNode || !streamEndParent);
+ CreateListOfNodesToPaste(*static_cast<DocumentFragment*>(fragmentAsNodeNode.get()),
+ nodeList,
+ streamStartParentNode, streamStartOffset,
+ streamEndParentNode, streamEndOffset);
+
+ if (nodeList.IsEmpty()) {
+ // We aren't inserting anything, but if aDeleteSelection is set, we do want
+ // to delete everything.
+ if (aDeleteSelection) {
+ return DeleteSelection(eNone, eStrip);
+ }
+ return NS_OK;
+ }
+
+ // Are there any table elements in the list?
+ // node and offset for insertion
+ nsCOMPtr<nsIDOMNode> parentNode;
+ int32_t offsetOfNewNode;
+
+ // check for table cell selection mode
+ bool cellSelectionMode = false;
+ nsCOMPtr<nsIDOMElement> cell;
+ rv = GetFirstSelectedCell(nullptr, getter_AddRefs(cell));
+ if (NS_SUCCEEDED(rv) && cell) {
+ cellSelectionMode = true;
+ }
+
+ if (cellSelectionMode) {
+ // do we have table content to paste? If so, we want to delete
+ // the selected table cells and replace with new table elements;
+ // but if not we want to delete _contents_ of cells and replace
+ // with non-table elements. Use cellSelectionMode bool to
+ // indicate results.
+ if (!HTMLEditUtils::IsTableElement(nodeList[0])) {
+ cellSelectionMode = false;
+ }
+ }
+
+ if (!cellSelectionMode) {
+ rv = DeleteSelectionAndPrepareToCreateNode();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aClearStyle) {
+ // pasting does not inherit local inline styles
+ nsCOMPtr<nsINode> tmpNode = selection->GetAnchorNode();
+ int32_t tmpOffset = static_cast<int32_t>(selection->AnchorOffset());
+ rv = ClearStyle(address_of(tmpNode), &tmpOffset, nullptr, nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ } else {
+ // Delete whole cells: we will replace with new table content.
+
+ // Braces for artificial block to scope AutoSelectionRestorer.
+ // Save current selection since DeleteTableCell() perturbs it.
+ {
+ AutoSelectionRestorer selectionRestorer(selection, this);
+ rv = DeleteTableCell(1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // collapse selection to beginning of deleted table content
+ selection->CollapseToStart();
+ }
+
+ // give rules a chance to handle or cancel
+ TextRulesInfo ruleInfo(EditAction::insertElement);
+ bool cancel, handled;
+ rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (cancel) {
+ return NS_OK; // rules canceled the operation
+ }
+
+ if (!handled) {
+ // The rules code (WillDoAction above) might have changed the selection.
+ // refresh our memory...
+ rv = GetStartNodeAndOffset(selection, getter_AddRefs(parentNode), &offsetOfNewNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(parentNode, NS_ERROR_FAILURE);
+
+ // Adjust position based on the first node we are going to insert.
+ NormalizeEOLInsertPosition(nodeList[0], address_of(parentNode),
+ &offsetOfNewNode);
+
+ // if there are any invisible br's after our insertion point, remove them.
+ // this is because if there is a br at end of what we paste, it will make
+ // the invisible br visible.
+ WSRunObject wsObj(this, parentNode, offsetOfNewNode);
+ if (wsObj.mEndReasonNode &&
+ TextEditUtils::IsBreak(wsObj.mEndReasonNode) &&
+ !IsVisBreak(wsObj.mEndReasonNode)) {
+ rv = DeleteNode(wsObj.mEndReasonNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Remember if we are in a link.
+ bool bStartedInLink = IsInLink(parentNode);
+
+ // Are we in a text node? If so, split it.
+ if (IsTextNode(parentNode)) {
+ nsCOMPtr<nsIContent> parentContent = do_QueryInterface(parentNode);
+ NS_ENSURE_STATE(parentContent || !parentNode);
+ offsetOfNewNode = SplitNodeDeep(*parentContent, *parentContent,
+ offsetOfNewNode);
+ NS_ENSURE_STATE(offsetOfNewNode != -1);
+ nsCOMPtr<nsIDOMNode> temp;
+ rv = parentNode->GetParentNode(getter_AddRefs(temp));
+ NS_ENSURE_SUCCESS(rv, rv);
+ parentNode = temp;
+ }
+
+ // build up list of parents of first node in list that are either
+ // lists or tables. First examine front of paste node list.
+ nsTArray<OwningNonNull<Element>> startListAndTableArray;
+ GetListAndTableParents(StartOrEnd::start, nodeList,
+ startListAndTableArray);
+
+ // remember number of lists and tables above us
+ int32_t highWaterMark = -1;
+ if (!startListAndTableArray.IsEmpty()) {
+ highWaterMark = DiscoverPartialListsAndTables(nodeList,
+ startListAndTableArray);
+ }
+
+ // if we have pieces of tables or lists to be inserted, let's force the paste
+ // to deal with table elements right away, so that it doesn't orphan some
+ // table or list contents outside the table or list.
+ if (highWaterMark >= 0) {
+ ReplaceOrphanedStructure(StartOrEnd::start, nodeList,
+ startListAndTableArray, highWaterMark);
+ }
+
+ // Now go through the same process again for the end of the paste node list.
+ nsTArray<OwningNonNull<Element>> endListAndTableArray;
+ GetListAndTableParents(StartOrEnd::end, nodeList, endListAndTableArray);
+ highWaterMark = -1;
+
+ // remember number of lists and tables above us
+ if (!endListAndTableArray.IsEmpty()) {
+ highWaterMark = DiscoverPartialListsAndTables(nodeList,
+ endListAndTableArray);
+ }
+
+ // don't orphan partial list or table structure
+ if (highWaterMark >= 0) {
+ ReplaceOrphanedStructure(StartOrEnd::end, nodeList,
+ endListAndTableArray, highWaterMark);
+ }
+
+ // Loop over the node list and paste the nodes:
+ nsCOMPtr<nsIDOMNode> parentBlock, lastInsertNode, insertedContextParent;
+ nsCOMPtr<nsINode> parentNodeNode = do_QueryInterface(parentNode);
+ NS_ENSURE_STATE(parentNodeNode || !parentNode);
+ if (IsBlockNode(parentNodeNode)) {
+ parentBlock = parentNode;
+ } else {
+ parentBlock = GetBlockNodeParent(parentNode);
+ }
+
+ int32_t listCount = nodeList.Length();
+ for (int32_t j = 0; j < listCount; j++) {
+ bool bDidInsert = false;
+ nsCOMPtr<nsIDOMNode> curNode = nodeList[j]->AsDOMNode();
+
+ NS_ENSURE_TRUE(curNode, NS_ERROR_FAILURE);
+ NS_ENSURE_TRUE(curNode != fragmentAsNode, NS_ERROR_FAILURE);
+ NS_ENSURE_TRUE(!TextEditUtils::IsBody(curNode), NS_ERROR_FAILURE);
+
+ if (insertedContextParent) {
+ // if we had to insert something higher up in the paste hierarchy, we want to
+ // skip any further paste nodes that descend from that. Else we will paste twice.
+ if (EditorUtils::IsDescendantOf(curNode, insertedContextParent)) {
+ continue;
+ }
+ }
+
+ // give the user a hand on table element insertion. if they have
+ // a table or table row on the clipboard, and are trying to insert
+ // into a table or table row, insert the appropriate children instead.
+ if (HTMLEditUtils::IsTableRow(curNode) &&
+ HTMLEditUtils::IsTableRow(parentNode) &&
+ (HTMLEditUtils::IsTable(curNode) ||
+ HTMLEditUtils::IsTable(parentNode))) {
+ nsCOMPtr<nsIDOMNode> child;
+ curNode->GetFirstChild(getter_AddRefs(child));
+ while (child) {
+ rv = InsertNodeAtPoint(child, address_of(parentNode), &offsetOfNewNode, true);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+
+ bDidInsert = true;
+ lastInsertNode = child;
+ offsetOfNewNode++;
+
+ curNode->GetFirstChild(getter_AddRefs(child));
+ }
+ }
+ // give the user a hand on list insertion. if they have
+ // a list on the clipboard, and are trying to insert
+ // into a list or list item, insert the appropriate children instead,
+ // ie, merge the lists instead of pasting in a sublist.
+ else if (HTMLEditUtils::IsList(curNode) &&
+ (HTMLEditUtils::IsList(parentNode) ||
+ HTMLEditUtils::IsListItem(parentNode))) {
+ nsCOMPtr<nsIDOMNode> child, tmp;
+ curNode->GetFirstChild(getter_AddRefs(child));
+ while (child) {
+ if (HTMLEditUtils::IsListItem(child) ||
+ HTMLEditUtils::IsList(child)) {
+ // Check if we are pasting into empty list item. If so
+ // delete it and paste into parent list instead.
+ if (HTMLEditUtils::IsListItem(parentNode)) {
+ bool isEmpty;
+ rv = IsEmptyNode(parentNode, &isEmpty, true);
+ if (NS_SUCCEEDED(rv) && isEmpty) {
+ int32_t newOffset;
+ nsCOMPtr<nsIDOMNode> listNode = GetNodeLocation(parentNode, &newOffset);
+ if (listNode) {
+ DeleteNode(parentNode);
+ parentNode = listNode;
+ offsetOfNewNode = newOffset;
+ }
+ }
+ }
+ rv = InsertNodeAtPoint(child, address_of(parentNode), &offsetOfNewNode, true);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+
+ bDidInsert = true;
+ lastInsertNode = child;
+ offsetOfNewNode++;
+ } else {
+ curNode->RemoveChild(child, getter_AddRefs(tmp));
+ }
+ curNode->GetFirstChild(getter_AddRefs(child));
+ }
+ } else if (parentBlock && HTMLEditUtils::IsPre(parentBlock) &&
+ HTMLEditUtils::IsPre(curNode)) {
+ // Check for pre's going into pre's.
+ nsCOMPtr<nsIDOMNode> child;
+ curNode->GetFirstChild(getter_AddRefs(child));
+ while (child) {
+ rv = InsertNodeAtPoint(child, address_of(parentNode), &offsetOfNewNode, true);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+
+ bDidInsert = true;
+ lastInsertNode = child;
+ offsetOfNewNode++;
+
+ curNode->GetFirstChild(getter_AddRefs(child));
+ }
+ }
+
+ if (!bDidInsert || NS_FAILED(rv)) {
+ // try to insert
+ rv = InsertNodeAtPoint(curNode, address_of(parentNode), &offsetOfNewNode, true);
+ if (NS_SUCCEEDED(rv)) {
+ bDidInsert = true;
+ lastInsertNode = curNode;
+ }
+
+ // Assume failure means no legal parent in the document hierarchy,
+ // try again with the parent of curNode in the paste hierarchy.
+ nsCOMPtr<nsIDOMNode> parent;
+ while (NS_FAILED(rv) && curNode) {
+ curNode->GetParentNode(getter_AddRefs(parent));
+ if (parent && !TextEditUtils::IsBody(parent)) {
+ rv = InsertNodeAtPoint(parent, address_of(parentNode), &offsetOfNewNode, true);
+ if (NS_SUCCEEDED(rv)) {
+ bDidInsert = true;
+ insertedContextParent = parent;
+ lastInsertNode = GetChildAt(parentNode, offsetOfNewNode);
+ }
+ }
+ curNode = parent;
+ }
+ }
+ if (lastInsertNode) {
+ parentNode = GetNodeLocation(lastInsertNode, &offsetOfNewNode);
+ offsetOfNewNode++;
+ }
+ }
+
+ // Now collapse the selection to the end of what we just inserted:
+ if (lastInsertNode) {
+ // set selection to the end of what we just pasted.
+ nsCOMPtr<nsIDOMNode> selNode, tmp, highTable;
+ int32_t selOffset;
+
+ // but don't cross tables
+ if (!HTMLEditUtils::IsTable(lastInsertNode)) {
+ nsCOMPtr<nsINode> lastInsertNode_ = do_QueryInterface(lastInsertNode);
+ NS_ENSURE_STATE(lastInsertNode_ || !lastInsertNode);
+ selNode = GetAsDOMNode(GetLastEditableLeaf(*lastInsertNode_));
+ tmp = selNode;
+ while (tmp && tmp != lastInsertNode) {
+ if (HTMLEditUtils::IsTable(tmp)) {
+ highTable = tmp;
+ }
+ nsCOMPtr<nsIDOMNode> parent = tmp;
+ tmp->GetParentNode(getter_AddRefs(parent));
+ tmp = parent;
+ }
+ if (highTable) {
+ selNode = highTable;
+ }
+ }
+ if (!selNode) {
+ selNode = lastInsertNode;
+ }
+ if (IsTextNode(selNode) ||
+ (IsContainer(selNode) && !HTMLEditUtils::IsTable(selNode))) {
+ rv = GetLengthOfDOMNode(selNode, (uint32_t&)selOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // We need to find a container for selection. Look up.
+ tmp = selNode;
+ selNode = GetNodeLocation(tmp, &selOffset);
+ // selNode might be null in case a mutation listener removed
+ // the stuff we just inserted from the DOM.
+ NS_ENSURE_STATE(selNode);
+ ++selOffset; // want to be *after* last leaf node in paste
+ }
+
+ // make sure we don't end up with selection collapsed after an invisible break node
+ WSRunObject wsRunObj(this, selNode, selOffset);
+ nsCOMPtr<nsINode> visNode;
+ int32_t outVisOffset=0;
+ WSType visType;
+ nsCOMPtr<nsINode> selNode_(do_QueryInterface(selNode));
+ wsRunObj.PriorVisibleNode(selNode_, selOffset, address_of(visNode),
+ &outVisOffset, &visType);
+ if (visType == WSType::br) {
+ // we are after a break. Is it visible? Despite the name,
+ // PriorVisibleNode does not make that determination for breaks.
+ // It also may not return the break in visNode. We have to pull it
+ // out of the WSRunObject's state.
+ if (!IsVisBreak(wsRunObj.mStartReasonNode)) {
+ // don't leave selection past an invisible break;
+ // reset {selNode,selOffset} to point before break
+ selNode = GetNodeLocation(GetAsDOMNode(wsRunObj.mStartReasonNode), &selOffset);
+ // we want to be inside any inline style prior to break
+ WSRunObject wsRunObj(this, selNode, selOffset);
+ selNode_ = do_QueryInterface(selNode);
+ wsRunObj.PriorVisibleNode(selNode_, selOffset, address_of(visNode),
+ &outVisOffset, &visType);
+ if (visType == WSType::text || visType == WSType::normalWS) {
+ selNode = GetAsDOMNode(visNode);
+ selOffset = outVisOffset; // PriorVisibleNode already set offset to _after_ the text or ws
+ } else if (visType == WSType::special) {
+ // prior visible thing is an image or some other non-text thingy.
+ // We want to be right after it.
+ selNode = GetNodeLocation(GetAsDOMNode(wsRunObj.mStartReasonNode), &selOffset);
+ ++selOffset;
+ }
+ }
+ }
+ selection->Collapse(selNode, selOffset);
+
+ // if we just pasted a link, discontinue link style
+ nsCOMPtr<nsIDOMNode> link;
+ if (!bStartedInLink && IsInLink(selNode, address_of(link))) {
+ // so, if we just pasted a link, I split it. Why do that instead of just
+ // nudging selection point beyond it? Because it might have ended in a BR
+ // that is not visible. If so, the code above just placed selection
+ // inside that. So I split it instead.
+ nsCOMPtr<nsIContent> linkContent = do_QueryInterface(link);
+ NS_ENSURE_STATE(linkContent || !link);
+ nsCOMPtr<nsIContent> selContent = do_QueryInterface(selNode);
+ NS_ENSURE_STATE(selContent || !selNode);
+ nsCOMPtr<nsIContent> leftLink;
+ SplitNodeDeep(*linkContent, *selContent, selOffset,
+ EmptyContainers::no, getter_AddRefs(leftLink));
+ if (leftLink) {
+ selNode = GetNodeLocation(GetAsDOMNode(leftLink), &selOffset);
+ selection->Collapse(selNode, selOffset+1);
+ }
+ }
+ }
+ }
+
+ return rules->DidDoAction(selection, &ruleInfo, rv);
+}
+
+NS_IMETHODIMP
+HTMLEditor::AddInsertionListener(nsIContentFilter* aListener)
+{
+ NS_ENSURE_TRUE(aListener, NS_ERROR_NULL_POINTER);
+
+ // don't let a listener be added more than once
+ if (!mContentFilters.Contains(aListener)) {
+ mContentFilters.AppendElement(*aListener);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::RemoveInsertionListener(nsIContentFilter* aListener)
+{
+ NS_ENSURE_TRUE(aListener, NS_ERROR_FAILURE);
+
+ mContentFilters.RemoveElement(aListener);
+
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::DoContentFilterCallback(const nsAString& aFlavor,
+ nsIDOMDocument* sourceDoc,
+ bool aWillDeleteSelection,
+ nsIDOMNode** aFragmentAsNode,
+ nsIDOMNode** aFragStartNode,
+ int32_t* aFragStartOffset,
+ nsIDOMNode** aFragEndNode,
+ int32_t* aFragEndOffset,
+ nsIDOMNode** aTargetNode,
+ int32_t* aTargetOffset,
+ bool* aDoContinue)
+{
+ *aDoContinue = true;
+
+ for (auto& listener : mContentFilters) {
+ if (!*aDoContinue) {
+ break;
+ }
+ listener->NotifyOfInsertion(aFlavor, nullptr, sourceDoc,
+ aWillDeleteSelection, aFragmentAsNode,
+ aFragStartNode, aFragStartOffset,
+ aFragEndNode, aFragEndOffset, aTargetNode,
+ aTargetOffset, aDoContinue);
+ }
+
+ return NS_OK;
+}
+
+bool
+HTMLEditor::IsInLink(nsIDOMNode* aNode,
+ nsCOMPtr<nsIDOMNode>* outLink)
+{
+ NS_ENSURE_TRUE(aNode, false);
+ if (outLink) {
+ *outLink = nullptr;
+ }
+ nsCOMPtr<nsIDOMNode> tmp, node = aNode;
+ while (node) {
+ if (HTMLEditUtils::IsLink(node)) {
+ if (outLink) {
+ *outLink = node;
+ }
+ return true;
+ }
+ tmp = node;
+ tmp->GetParentNode(getter_AddRefs(node));
+ }
+ return false;
+}
+
+nsresult
+HTMLEditor::StripFormattingNodes(nsIContent& aNode,
+ bool aListOnly)
+{
+ if (aNode.TextIsOnlyWhitespace()) {
+ nsCOMPtr<nsINode> parent = aNode.GetParentNode();
+ if (parent) {
+ if (!aListOnly || HTMLEditUtils::IsList(parent)) {
+ ErrorResult rv;
+ parent->RemoveChild(aNode, rv);
+ return rv.StealNSResult();
+ }
+ return NS_OK;
+ }
+ }
+
+ if (!aNode.IsHTMLElement(nsGkAtoms::pre)) {
+ nsCOMPtr<nsIContent> child = aNode.GetLastChild();
+ while (child) {
+ nsCOMPtr<nsIContent> previous = child->GetPreviousSibling();
+ nsresult rv = StripFormattingNodes(*child, aListOnly);
+ NS_ENSURE_SUCCESS(rv, rv);
+ child = previous.forget();
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::PrepareTransferable(nsITransferable** aTransferable)
+{
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::PrepareHTMLTransferable(nsITransferable** aTransferable)
+{
+ // Create generic Transferable for getting the data
+ nsresult rv = CallCreateInstance("@mozilla.org/widget/transferable;1", aTransferable);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the nsITransferable interface for getting the data from the clipboard
+ if (aTransferable) {
+ nsCOMPtr<nsIDocument> destdoc = GetDocument();
+ nsILoadContext* loadContext = destdoc ? destdoc->GetLoadContext() : nullptr;
+ (*aTransferable)->Init(loadContext);
+
+ // Create the desired DataFlavor for the type of data
+ // we want to get out of the transferable
+ // This should only happen in html editors, not plaintext
+ if (!IsPlaintextEditor()) {
+ (*aTransferable)->AddDataFlavor(kNativeHTMLMime);
+ (*aTransferable)->AddDataFlavor(kHTMLMime);
+ (*aTransferable)->AddDataFlavor(kFileMime);
+
+ switch (Preferences::GetInt("clipboard.paste_image_type", 1)) {
+ case 0: // prefer JPEG over PNG over GIF encoding
+ (*aTransferable)->AddDataFlavor(kJPEGImageMime);
+ (*aTransferable)->AddDataFlavor(kJPGImageMime);
+ (*aTransferable)->AddDataFlavor(kPNGImageMime);
+ (*aTransferable)->AddDataFlavor(kGIFImageMime);
+ break;
+ case 1: // prefer PNG over JPEG over GIF encoding (default)
+ default:
+ (*aTransferable)->AddDataFlavor(kPNGImageMime);
+ (*aTransferable)->AddDataFlavor(kJPEGImageMime);
+ (*aTransferable)->AddDataFlavor(kJPGImageMime);
+ (*aTransferable)->AddDataFlavor(kGIFImageMime);
+ break;
+ case 2: // prefer GIF over JPEG over PNG encoding
+ (*aTransferable)->AddDataFlavor(kGIFImageMime);
+ (*aTransferable)->AddDataFlavor(kJPEGImageMime);
+ (*aTransferable)->AddDataFlavor(kJPGImageMime);
+ (*aTransferable)->AddDataFlavor(kPNGImageMime);
+ break;
+ }
+ }
+ (*aTransferable)->AddDataFlavor(kUnicodeMime);
+ (*aTransferable)->AddDataFlavor(kMozTextInternal);
+ }
+
+ return NS_OK;
+}
+
+bool
+FindIntegerAfterString(const char* aLeadingString,
+ nsCString& aCStr,
+ int32_t& foundNumber)
+{
+ // first obtain offsets from cfhtml str
+ int32_t numFront = aCStr.Find(aLeadingString);
+ if (numFront == -1) {
+ return false;
+ }
+ numFront += strlen(aLeadingString);
+
+ int32_t numBack = aCStr.FindCharInSet(CRLF, numFront);
+ if (numBack == -1) {
+ return false;
+ }
+
+ nsAutoCString numStr(Substring(aCStr, numFront, numBack-numFront));
+ nsresult errorCode;
+ foundNumber = numStr.ToInteger(&errorCode);
+ return true;
+}
+
+nsresult
+RemoveFragComments(nsCString& aStr)
+{
+ // remove the StartFragment/EndFragment comments from the str, if present
+ int32_t startCommentIndx = aStr.Find("<!--StartFragment");
+ if (startCommentIndx >= 0) {
+ int32_t startCommentEnd = aStr.Find("-->", false, startCommentIndx);
+ if (startCommentEnd > startCommentIndx) {
+ aStr.Cut(startCommentIndx, (startCommentEnd + 3) - startCommentIndx);
+ }
+ }
+ int32_t endCommentIndx = aStr.Find("<!--EndFragment");
+ if (endCommentIndx >= 0) {
+ int32_t endCommentEnd = aStr.Find("-->", false, endCommentIndx);
+ if (endCommentEnd > endCommentIndx) {
+ aStr.Cut(endCommentIndx, (endCommentEnd + 3) - endCommentIndx);
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::ParseCFHTML(nsCString& aCfhtml,
+ char16_t** aStuffToPaste,
+ char16_t** aCfcontext)
+{
+ // First obtain offsets from cfhtml str.
+ int32_t startHTML, endHTML, startFragment, endFragment;
+ if (!FindIntegerAfterString("StartHTML:", aCfhtml, startHTML) ||
+ startHTML < -1) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!FindIntegerAfterString("EndHTML:", aCfhtml, endHTML) ||
+ endHTML < -1) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!FindIntegerAfterString("StartFragment:", aCfhtml, startFragment) ||
+ startFragment < 0) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!FindIntegerAfterString("EndFragment:", aCfhtml, endFragment) ||
+ startFragment < 0) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // The StartHTML and EndHTML markers are allowed to be -1 to include everything.
+ // See Reference: MSDN doc entitled "HTML Clipboard Format"
+ // http://msdn.microsoft.com/en-us/library/aa767917(VS.85).aspx#unknown_854
+ if (startHTML == -1) {
+ startHTML = aCfhtml.Find("<!--StartFragment-->");
+ if (startHTML == -1) {
+ return NS_OK;
+ }
+ }
+ if (endHTML == -1) {
+ const char endFragmentMarker[] = "<!--EndFragment-->";
+ endHTML = aCfhtml.Find(endFragmentMarker);
+ if (endHTML == -1) {
+ return NS_OK;
+ }
+ endHTML += ArrayLength(endFragmentMarker) - 1;
+ }
+
+ // create context string
+ nsAutoCString contextUTF8(Substring(aCfhtml, startHTML, startFragment - startHTML) +
+ NS_LITERAL_CSTRING("<!--" kInsertCookie "-->") +
+ Substring(aCfhtml, endFragment, endHTML - endFragment));
+
+ // validate startFragment
+ // make sure it's not in the middle of a HTML tag
+ // see bug #228879 for more details
+ int32_t curPos = startFragment;
+ while (curPos > startHTML) {
+ if (aCfhtml[curPos] == '>') {
+ // working backwards, the first thing we see is the end of a tag
+ // so StartFragment is good, so do nothing.
+ break;
+ }
+ if (aCfhtml[curPos] == '<') {
+ // if we are at the start, then we want to see the '<'
+ if (curPos != startFragment) {
+ // working backwards, the first thing we see is the start of a tag
+ // so StartFragment is bad, so we need to update it.
+ NS_ERROR("StartFragment byte count in the clipboard looks bad, see bug #228879");
+ startFragment = curPos - 1;
+ }
+ break;
+ }
+ curPos--;
+ }
+
+ // create fragment string
+ nsAutoCString fragmentUTF8(Substring(aCfhtml, startFragment, endFragment-startFragment));
+
+ // remove the StartFragment/EndFragment comments from the fragment, if present
+ RemoveFragComments(fragmentUTF8);
+
+ // remove the StartFragment/EndFragment comments from the context, if present
+ RemoveFragComments(contextUTF8);
+
+ // convert both strings to usc2
+ const nsAFlatString& fragUcs2Str = NS_ConvertUTF8toUTF16(fragmentUTF8);
+ const nsAFlatString& cntxtUcs2Str = NS_ConvertUTF8toUTF16(contextUTF8);
+
+ // translate platform linebreaks for fragment
+ int32_t oldLengthInChars = fragUcs2Str.Length() + 1; // +1 to include null terminator
+ int32_t newLengthInChars = 0;
+ *aStuffToPaste = nsLinebreakConverter::ConvertUnicharLineBreaks(fragUcs2Str.get(),
+ nsLinebreakConverter::eLinebreakAny,
+ nsLinebreakConverter::eLinebreakContent,
+ oldLengthInChars, &newLengthInChars);
+ NS_ENSURE_TRUE(*aStuffToPaste, NS_ERROR_FAILURE);
+
+ // translate platform linebreaks for context
+ oldLengthInChars = cntxtUcs2Str.Length() + 1; // +1 to include null terminator
+ newLengthInChars = 0;
+ *aCfcontext = nsLinebreakConverter::ConvertUnicharLineBreaks(cntxtUcs2Str.get(),
+ nsLinebreakConverter::eLinebreakAny,
+ nsLinebreakConverter::eLinebreakContent,
+ oldLengthInChars, &newLengthInChars);
+ // it's ok for context to be empty. frag might be whole doc and contain all its context.
+
+ // we're done!
+ return NS_OK;
+}
+
+static nsresult
+ImgFromData(const nsACString& aType, const nsACString& aData, nsString& aOutput)
+{
+ nsAutoCString data64;
+ nsresult rv = Base64Encode(aData, data64);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aOutput.AssignLiteral("<IMG src=\"data:");
+ AppendUTF8toUTF16(aType, aOutput);
+ aOutput.AppendLiteral(";base64,");
+ if (!AppendASCIItoUTF16(data64, aOutput, fallible_t())) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ aOutput.AppendLiteral("\" alt=\"\" >");
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(HTMLEditor::BlobReader, nsIEditorBlobListener)
+
+HTMLEditor::BlobReader::BlobReader(BlobImpl* aBlob,
+ HTMLEditor* aHTMLEditor,
+ bool aIsSafe,
+ nsIDOMDocument* aSourceDoc,
+ nsIDOMNode* aDestinationNode,
+ int32_t aDestOffset,
+ bool aDoDeleteSelection)
+ : mBlob(aBlob)
+ , mHTMLEditor(aHTMLEditor)
+ , mIsSafe(aIsSafe)
+ , mSourceDoc(aSourceDoc)
+ , mDestinationNode(aDestinationNode)
+ , mDestOffset(aDestOffset)
+ , mDoDeleteSelection(aDoDeleteSelection)
+{
+ MOZ_ASSERT(mBlob);
+ MOZ_ASSERT(mHTMLEditor);
+ MOZ_ASSERT(mDestinationNode);
+}
+
+NS_IMETHODIMP
+HTMLEditor::BlobReader::OnResult(const nsACString& aResult)
+{
+ nsString blobType;
+ mBlob->GetType(blobType);
+
+ NS_ConvertUTF16toUTF8 type(blobType);
+ nsAutoString stuffToPaste;
+ nsresult rv = ImgFromData(type, aResult, stuffToPaste);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ AutoEditBatch beginBatching(mHTMLEditor);
+ rv = mHTMLEditor->DoInsertHTMLWithContext(stuffToPaste, EmptyString(),
+ EmptyString(),
+ NS_LITERAL_STRING(kFileMime),
+ mSourceDoc,
+ mDestinationNode, mDestOffset,
+ mDoDeleteSelection,
+ mIsSafe, false);
+ return rv;
+}
+
+NS_IMETHODIMP
+HTMLEditor::BlobReader::OnError(const nsAString& aError)
+{
+ nsCOMPtr<nsINode> destNode = do_QueryInterface(mDestinationNode);
+ const nsPromiseFlatString& flat = PromiseFlatString(aError);
+ const char16_t* error = flat.get();
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
+ NS_LITERAL_CSTRING("Editor"),
+ destNode->OwnerDoc(),
+ nsContentUtils::eDOM_PROPERTIES,
+ "EditorFileDropFailed",
+ &error, 1);
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::InsertObject(const nsACString& aType,
+ nsISupports* aObject,
+ bool aIsSafe,
+ nsIDOMDocument* aSourceDoc,
+ nsIDOMNode* aDestinationNode,
+ int32_t aDestOffset,
+ bool aDoDeleteSelection)
+{
+ nsresult rv;
+
+ if (nsCOMPtr<BlobImpl> blob = do_QueryInterface(aObject)) {
+ RefPtr<BlobReader> br = new BlobReader(blob, this, aIsSafe, aSourceDoc,
+ aDestinationNode, aDestOffset,
+ aDoDeleteSelection);
+ nsCOMPtr<nsIEditorUtils> utils =
+ do_GetService("@mozilla.org/editor-utils;1");
+ NS_ENSURE_TRUE(utils, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsINode> node = do_QueryInterface(aDestinationNode);
+ MOZ_ASSERT(node);
+
+ nsCOMPtr<nsIDOMBlob> domBlob = Blob::Create(node->GetOwnerGlobal(), blob);
+ NS_ENSURE_TRUE(domBlob, NS_ERROR_FAILURE);
+
+ return utils->SlurpBlob(domBlob, node->OwnerDoc()->GetWindow(), br);
+ }
+
+ nsAutoCString type(aType);
+
+ // Check to see if we can insert an image file
+ bool insertAsImage = false;
+ nsCOMPtr<nsIFile> fileObj;
+ if (type.EqualsLiteral(kFileMime)) {
+ fileObj = do_QueryInterface(aObject);
+ if (fileObj) {
+ // Accept any image type fed to us
+ if (nsContentUtils::IsFileImage(fileObj, type)) {
+ insertAsImage = true;
+ } else {
+ // Reset type.
+ type.AssignLiteral(kFileMime);
+ }
+ }
+ }
+
+ if (type.EqualsLiteral(kJPEGImageMime) ||
+ type.EqualsLiteral(kJPGImageMime) ||
+ type.EqualsLiteral(kPNGImageMime) ||
+ type.EqualsLiteral(kGIFImageMime) ||
+ insertAsImage) {
+ nsCString imageData;
+ if (insertAsImage) {
+ rv = nsContentUtils::SlurpFileToString(fileObj, imageData);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ nsCOMPtr<nsIInputStream> imageStream = do_QueryInterface(aObject);
+ NS_ENSURE_TRUE(imageStream, NS_ERROR_FAILURE);
+
+ rv = NS_ConsumeStream(imageStream, UINT32_MAX, imageData);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = imageStream->Close();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsAutoString stuffToPaste;
+ rv = ImgFromData(type, imageData, stuffToPaste);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ AutoEditBatch beginBatching(this);
+ rv = DoInsertHTMLWithContext(stuffToPaste, EmptyString(), EmptyString(),
+ NS_LITERAL_STRING(kFileMime),
+ aSourceDoc,
+ aDestinationNode, aDestOffset,
+ aDoDeleteSelection,
+ aIsSafe, false);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::InsertFromTransferable(nsITransferable* transferable,
+ nsIDOMDocument* aSourceDoc,
+ const nsAString& aContextStr,
+ const nsAString& aInfoStr,
+ bool havePrivateHTMLFlavor,
+ nsIDOMNode* aDestinationNode,
+ int32_t aDestOffset,
+ bool aDoDeleteSelection)
+{
+ nsresult rv = NS_OK;
+ nsAutoCString bestFlavor;
+ nsCOMPtr<nsISupports> genericDataObj;
+ uint32_t len = 0;
+ if (NS_SUCCEEDED(
+ transferable->GetAnyTransferData(bestFlavor,
+ getter_AddRefs(genericDataObj),
+ &len))) {
+ AutoTransactionsConserveSelection dontSpazMySelection(this);
+ nsAutoString flavor;
+ flavor.AssignWithConversion(bestFlavor);
+ nsAutoString stuffToPaste;
+ bool isSafe = IsSafeToInsertData(aSourceDoc);
+
+ if (bestFlavor.EqualsLiteral(kFileMime) ||
+ bestFlavor.EqualsLiteral(kJPEGImageMime) ||
+ bestFlavor.EqualsLiteral(kJPGImageMime) ||
+ bestFlavor.EqualsLiteral(kPNGImageMime) ||
+ bestFlavor.EqualsLiteral(kGIFImageMime)) {
+ rv = InsertObject(bestFlavor, genericDataObj, isSafe,
+ aSourceDoc, aDestinationNode, aDestOffset, aDoDeleteSelection);
+ } else if (bestFlavor.EqualsLiteral(kNativeHTMLMime)) {
+ // note cf_html uses utf8, hence use length = len, not len/2 as in flavors below
+ nsCOMPtr<nsISupportsCString> textDataObj = do_QueryInterface(genericDataObj);
+ if (textDataObj && len > 0) {
+ nsAutoCString cfhtml;
+ textDataObj->GetData(cfhtml);
+ NS_ASSERTION(cfhtml.Length() <= (len), "Invalid length!");
+ nsXPIDLString cfcontext, cffragment, cfselection; // cfselection left emtpy for now
+
+ rv = ParseCFHTML(cfhtml, getter_Copies(cffragment), getter_Copies(cfcontext));
+ if (NS_SUCCEEDED(rv) && !cffragment.IsEmpty()) {
+ AutoEditBatch beginBatching(this);
+ // If we have our private HTML flavor, we will only use the fragment
+ // from the CF_HTML. The rest comes from the clipboard.
+ if (havePrivateHTMLFlavor) {
+ rv = DoInsertHTMLWithContext(cffragment,
+ aContextStr, aInfoStr, flavor,
+ aSourceDoc,
+ aDestinationNode, aDestOffset,
+ aDoDeleteSelection,
+ isSafe);
+ } else {
+ rv = DoInsertHTMLWithContext(cffragment,
+ cfcontext, cfselection, flavor,
+ aSourceDoc,
+ aDestinationNode, aDestOffset,
+ aDoDeleteSelection,
+ isSafe);
+
+ }
+ } else {
+ // In some platforms (like Linux), the clipboard might return data
+ // requested for unknown flavors (for example:
+ // application/x-moz-nativehtml). In this case, treat the data
+ // to be pasted as mere HTML to get the best chance of pasting it
+ // correctly.
+ bestFlavor.AssignLiteral(kHTMLMime);
+ // Fall through the next case
+ }
+ }
+ }
+ if (bestFlavor.EqualsLiteral(kHTMLMime) ||
+ bestFlavor.EqualsLiteral(kUnicodeMime) ||
+ bestFlavor.EqualsLiteral(kMozTextInternal)) {
+ nsCOMPtr<nsISupportsString> textDataObj = do_QueryInterface(genericDataObj);
+ if (textDataObj && len > 0) {
+ nsAutoString text;
+ textDataObj->GetData(text);
+ NS_ASSERTION(text.Length() <= (len/2), "Invalid length!");
+ stuffToPaste.Assign(text.get(), len / 2);
+ } else {
+ nsCOMPtr<nsISupportsCString> textDataObj(do_QueryInterface(genericDataObj));
+ if (textDataObj && len > 0) {
+ nsAutoCString text;
+ textDataObj->GetData(text);
+ NS_ASSERTION(text.Length() <= len, "Invalid length!");
+ stuffToPaste.Assign(NS_ConvertUTF8toUTF16(Substring(text, 0, len)));
+ }
+ }
+
+ if (!stuffToPaste.IsEmpty()) {
+ AutoEditBatch beginBatching(this);
+ if (bestFlavor.EqualsLiteral(kHTMLMime)) {
+ rv = DoInsertHTMLWithContext(stuffToPaste,
+ aContextStr, aInfoStr, flavor,
+ aSourceDoc,
+ aDestinationNode, aDestOffset,
+ aDoDeleteSelection,
+ isSafe);
+ } else {
+ rv = InsertTextAt(stuffToPaste, aDestinationNode, aDestOffset, aDoDeleteSelection);
+ }
+ }
+ }
+ }
+
+ // Try to scroll the selection into view if the paste succeeded
+ if (NS_SUCCEEDED(rv)) {
+ ScrollSelectionIntoView(false);
+ }
+ return rv;
+}
+
+static void
+GetStringFromDataTransfer(nsIDOMDataTransfer* aDataTransfer,
+ const nsAString& aType,
+ int32_t aIndex,
+ nsAString& aOutputString)
+{
+ nsCOMPtr<nsIVariant> variant;
+ DataTransfer::Cast(aDataTransfer)->GetDataAtNoSecurityCheck(aType, aIndex, getter_AddRefs(variant));
+ if (variant) {
+ variant->GetAsAString(aOutputString);
+ }
+}
+
+nsresult
+HTMLEditor::InsertFromDataTransfer(DataTransfer* aDataTransfer,
+ int32_t aIndex,
+ nsIDOMDocument* aSourceDoc,
+ nsIDOMNode* aDestinationNode,
+ int32_t aDestOffset,
+ bool aDoDeleteSelection)
+{
+ ErrorResult rv;
+ RefPtr<DOMStringList> types = aDataTransfer->MozTypesAt(aIndex, rv);
+ if (rv.Failed()) {
+ return rv.StealNSResult();
+ }
+
+ bool hasPrivateHTMLFlavor = types->Contains(NS_LITERAL_STRING(kHTMLContext));
+
+ bool isText = IsPlaintextEditor();
+ bool isSafe = IsSafeToInsertData(aSourceDoc);
+
+ uint32_t length = types->Length();
+ for (uint32_t t = 0; t < length; t++) {
+ nsAutoString type;
+ types->Item(t, type);
+
+ if (!isText) {
+ if (type.EqualsLiteral(kFileMime) ||
+ type.EqualsLiteral(kJPEGImageMime) ||
+ type.EqualsLiteral(kJPGImageMime) ||
+ type.EqualsLiteral(kPNGImageMime) ||
+ type.EqualsLiteral(kGIFImageMime)) {
+ nsCOMPtr<nsIVariant> variant;
+ DataTransfer::Cast(aDataTransfer)->GetDataAtNoSecurityCheck(type, aIndex, getter_AddRefs(variant));
+ if (variant) {
+ nsCOMPtr<nsISupports> object;
+ variant->GetAsISupports(getter_AddRefs(object));
+ return InsertObject(NS_ConvertUTF16toUTF8(type), object, isSafe,
+ aSourceDoc, aDestinationNode, aDestOffset, aDoDeleteSelection);
+ }
+ } else if (type.EqualsLiteral(kNativeHTMLMime)) {
+ // Windows only clipboard parsing.
+ nsAutoString text;
+ GetStringFromDataTransfer(aDataTransfer, type, aIndex, text);
+ NS_ConvertUTF16toUTF8 cfhtml(text);
+
+ nsXPIDLString cfcontext, cffragment, cfselection; // cfselection left emtpy for now
+
+ nsresult rv = ParseCFHTML(cfhtml, getter_Copies(cffragment), getter_Copies(cfcontext));
+ if (NS_SUCCEEDED(rv) && !cffragment.IsEmpty()) {
+ AutoEditBatch beginBatching(this);
+
+ if (hasPrivateHTMLFlavor) {
+ // If we have our private HTML flavor, we will only use the fragment
+ // from the CF_HTML. The rest comes from the clipboard.
+ nsAutoString contextString, infoString;
+ GetStringFromDataTransfer(aDataTransfer, NS_LITERAL_STRING(kHTMLContext), aIndex, contextString);
+ GetStringFromDataTransfer(aDataTransfer, NS_LITERAL_STRING(kHTMLInfo), aIndex, infoString);
+ return DoInsertHTMLWithContext(cffragment,
+ contextString, infoString, type,
+ aSourceDoc,
+ aDestinationNode, aDestOffset,
+ aDoDeleteSelection,
+ isSafe);
+ } else {
+ return DoInsertHTMLWithContext(cffragment,
+ cfcontext, cfselection, type,
+ aSourceDoc,
+ aDestinationNode, aDestOffset,
+ aDoDeleteSelection,
+ isSafe);
+ }
+ }
+ } else if (type.EqualsLiteral(kHTMLMime)) {
+ nsAutoString text, contextString, infoString;
+ GetStringFromDataTransfer(aDataTransfer, type, aIndex, text);
+ GetStringFromDataTransfer(aDataTransfer, NS_LITERAL_STRING(kHTMLContext), aIndex, contextString);
+ GetStringFromDataTransfer(aDataTransfer, NS_LITERAL_STRING(kHTMLInfo), aIndex, infoString);
+
+ AutoEditBatch beginBatching(this);
+ if (type.EqualsLiteral(kHTMLMime)) {
+ return DoInsertHTMLWithContext(text,
+ contextString, infoString, type,
+ aSourceDoc,
+ aDestinationNode, aDestOffset,
+ aDoDeleteSelection,
+ isSafe);
+ }
+ }
+ }
+
+ if (type.EqualsLiteral(kTextMime) ||
+ type.EqualsLiteral(kMozTextInternal)) {
+ nsAutoString text;
+ GetStringFromDataTransfer(aDataTransfer, type, aIndex, text);
+
+ AutoEditBatch beginBatching(this);
+ return InsertTextAt(text, aDestinationNode, aDestOffset, aDoDeleteSelection);
+ }
+ }
+
+ return NS_OK;
+}
+
+bool
+HTMLEditor::HavePrivateHTMLFlavor(nsIClipboard* aClipboard)
+{
+ // check the clipboard for our special kHTMLContext flavor. If that is there, we know
+ // we have our own internal html format on clipboard.
+
+ NS_ENSURE_TRUE(aClipboard, false);
+ bool bHavePrivateHTMLFlavor = false;
+
+ const char* flavArray[] = { kHTMLContext };
+
+ if (NS_SUCCEEDED(
+ aClipboard->HasDataMatchingFlavors(flavArray,
+ ArrayLength(flavArray),
+ nsIClipboard::kGlobalClipboard,
+ &bHavePrivateHTMLFlavor))) {
+ return bHavePrivateHTMLFlavor;
+ }
+
+ return false;
+}
+
+
+NS_IMETHODIMP
+HTMLEditor::Paste(int32_t aSelectionType)
+{
+ if (!FireClipboardEvent(ePaste, aSelectionType)) {
+ return NS_OK;
+ }
+
+ // Get Clipboard Service
+ nsresult rv;
+ nsCOMPtr<nsIClipboard> clipboard(do_GetService("@mozilla.org/widget/clipboard;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the nsITransferable interface for getting the data from the clipboard
+ nsCOMPtr<nsITransferable> trans;
+ rv = PrepareHTMLTransferable(getter_AddRefs(trans));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(trans, NS_ERROR_FAILURE);
+ // Get the Data from the clipboard
+ rv = clipboard->GetData(trans, aSelectionType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!IsModifiable()) {
+ return NS_OK;
+ }
+
+ // also get additional html copy hints, if present
+ nsAutoString contextStr, infoStr;
+
+ // If we have our internal html flavor on the clipboard, there is special
+ // context to use instead of cfhtml context.
+ bool bHavePrivateHTMLFlavor = HavePrivateHTMLFlavor(clipboard);
+ if (bHavePrivateHTMLFlavor) {
+ nsCOMPtr<nsISupports> contextDataObj, infoDataObj;
+ uint32_t contextLen, infoLen;
+ nsCOMPtr<nsISupportsString> textDataObj;
+
+ nsCOMPtr<nsITransferable> contextTrans =
+ do_CreateInstance("@mozilla.org/widget/transferable;1");
+ NS_ENSURE_TRUE(contextTrans, NS_ERROR_NULL_POINTER);
+ contextTrans->Init(nullptr);
+ contextTrans->AddDataFlavor(kHTMLContext);
+ clipboard->GetData(contextTrans, aSelectionType);
+ contextTrans->GetTransferData(kHTMLContext, getter_AddRefs(contextDataObj), &contextLen);
+
+ nsCOMPtr<nsITransferable> infoTrans =
+ do_CreateInstance("@mozilla.org/widget/transferable;1");
+ NS_ENSURE_TRUE(infoTrans, NS_ERROR_NULL_POINTER);
+ infoTrans->Init(nullptr);
+ infoTrans->AddDataFlavor(kHTMLInfo);
+ clipboard->GetData(infoTrans, aSelectionType);
+ infoTrans->GetTransferData(kHTMLInfo, getter_AddRefs(infoDataObj), &infoLen);
+
+ if (contextDataObj) {
+ nsAutoString text;
+ textDataObj = do_QueryInterface(contextDataObj);
+ textDataObj->GetData(text);
+ NS_ASSERTION(text.Length() <= (contextLen/2), "Invalid length!");
+ contextStr.Assign(text.get(), contextLen / 2);
+ }
+
+ if (infoDataObj) {
+ nsAutoString text;
+ textDataObj = do_QueryInterface(infoDataObj);
+ textDataObj->GetData(text);
+ NS_ASSERTION(text.Length() <= (infoLen/2), "Invalid length!");
+ infoStr.Assign(text.get(), infoLen / 2);
+ }
+ }
+
+ // handle transferable hooks
+ nsCOMPtr<nsIDOMDocument> domdoc;
+ GetDocument(getter_AddRefs(domdoc));
+ if (!EditorHookUtils::DoInsertionHook(domdoc, nullptr, trans)) {
+ return NS_OK;
+ }
+
+ return InsertFromTransferable(trans, nullptr, contextStr, infoStr, bHavePrivateHTMLFlavor,
+ nullptr, 0, true);
+}
+
+NS_IMETHODIMP
+HTMLEditor::PasteTransferable(nsITransferable* aTransferable)
+{
+ // Use an invalid value for the clipboard type as data comes from aTransferable
+ // and we don't currently implement a way to put that in the data transfer yet.
+ if (!FireClipboardEvent(ePaste, nsIClipboard::kGlobalClipboard)) {
+ return NS_OK;
+ }
+
+ // handle transferable hooks
+ nsCOMPtr<nsIDOMDocument> domdoc = GetDOMDocument();
+ if (!EditorHookUtils::DoInsertionHook(domdoc, nullptr, aTransferable)) {
+ return NS_OK;
+ }
+
+ nsAutoString contextStr, infoStr;
+ return InsertFromTransferable(aTransferable, nullptr, contextStr, infoStr, false,
+ nullptr, 0, true);
+}
+
+/**
+ * HTML PasteNoFormatting. Ignore any HTML styles and formating in paste source.
+ */
+NS_IMETHODIMP
+HTMLEditor::PasteNoFormatting(int32_t aSelectionType)
+{
+ if (!FireClipboardEvent(ePaste, aSelectionType)) {
+ return NS_OK;
+ }
+
+ ForceCompositionEnd();
+
+ // Get Clipboard Service
+ nsresult rv;
+ nsCOMPtr<nsIClipboard> clipboard(do_GetService("@mozilla.org/widget/clipboard;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the nsITransferable interface for getting the data from the clipboard.
+ // use TextEditor::PrepareTransferable() to force unicode plaintext data.
+ nsCOMPtr<nsITransferable> trans;
+ rv = TextEditor::PrepareTransferable(getter_AddRefs(trans));
+ if (NS_SUCCEEDED(rv) && trans) {
+ // Get the Data from the clipboard
+ if (NS_SUCCEEDED(clipboard->GetData(trans, aSelectionType)) &&
+ IsModifiable()) {
+ const nsAFlatString& empty = EmptyString();
+ rv = InsertFromTransferable(trans, nullptr, empty, empty, false, nullptr, 0,
+ true);
+ }
+ }
+
+ return rv;
+}
+
+// The following arrays contain the MIME types that we can paste. The arrays
+// are used by CanPaste() and CanPasteTransferable() below.
+
+static const char* textEditorFlavors[] = { kUnicodeMime };
+static const char* textHtmlEditorFlavors[] = { kUnicodeMime, kHTMLMime,
+ kJPEGImageMime, kJPGImageMime,
+ kPNGImageMime, kGIFImageMime };
+
+NS_IMETHODIMP
+HTMLEditor::CanPaste(int32_t aSelectionType,
+ bool* aCanPaste)
+{
+ NS_ENSURE_ARG_POINTER(aCanPaste);
+ *aCanPaste = false;
+
+ // can't paste if readonly
+ if (!IsModifiable()) {
+ return NS_OK;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIClipboard> clipboard(do_GetService("@mozilla.org/widget/clipboard;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool haveFlavors;
+
+ // Use the flavors depending on the current editor mask
+ if (IsPlaintextEditor()) {
+ rv = clipboard->HasDataMatchingFlavors(textEditorFlavors,
+ ArrayLength(textEditorFlavors),
+ aSelectionType, &haveFlavors);
+ } else {
+ rv = clipboard->HasDataMatchingFlavors(textHtmlEditorFlavors,
+ ArrayLength(textHtmlEditorFlavors),
+ aSelectionType, &haveFlavors);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aCanPaste = haveFlavors;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::CanPasteTransferable(nsITransferable* aTransferable,
+ bool* aCanPaste)
+{
+ NS_ENSURE_ARG_POINTER(aCanPaste);
+
+ // can't paste if readonly
+ if (!IsModifiable()) {
+ *aCanPaste = false;
+ return NS_OK;
+ }
+
+ // If |aTransferable| is null, assume that a paste will succeed.
+ if (!aTransferable) {
+ *aCanPaste = true;
+ return NS_OK;
+ }
+
+ // Peek in |aTransferable| to see if it contains a supported MIME type.
+
+ // Use the flavors depending on the current editor mask
+ const char ** flavors;
+ unsigned length;
+ if (IsPlaintextEditor()) {
+ flavors = textEditorFlavors;
+ length = ArrayLength(textEditorFlavors);
+ } else {
+ flavors = textHtmlEditorFlavors;
+ length = ArrayLength(textHtmlEditorFlavors);
+ }
+
+ for (unsigned int i = 0; i < length; i++, flavors++) {
+ nsCOMPtr<nsISupports> data;
+ uint32_t dataLen;
+ nsresult rv = aTransferable->GetTransferData(*flavors,
+ getter_AddRefs(data),
+ &dataLen);
+ if (NS_SUCCEEDED(rv) && data) {
+ *aCanPaste = true;
+ return NS_OK;
+ }
+ }
+
+ *aCanPaste = false;
+ return NS_OK;
+}
+
+/**
+ * HTML PasteAsQuotation: Paste in a blockquote type=cite.
+ */
+NS_IMETHODIMP
+HTMLEditor::PasteAsQuotation(int32_t aSelectionType)
+{
+ if (IsPlaintextEditor()) {
+ return PasteAsPlaintextQuotation(aSelectionType);
+ }
+
+ nsAutoString citation;
+ return PasteAsCitedQuotation(citation, aSelectionType);
+}
+
+NS_IMETHODIMP
+HTMLEditor::PasteAsCitedQuotation(const nsAString& aCitation,
+ int32_t aSelectionType)
+{
+ AutoEditBatch beginBatching(this);
+ AutoRules beginRulesSniffing(this, EditAction::insertQuotation,
+ nsIEditor::eNext);
+
+ // get selection
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+
+ // give rules a chance to handle or cancel
+ TextRulesInfo ruleInfo(EditAction::insertElement);
+ bool cancel, handled;
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+ nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (cancel || handled) {
+ return NS_OK; // rules canceled the operation
+ }
+
+ nsCOMPtr<Element> newNode =
+ DeleteSelectionAndCreateElement(*nsGkAtoms::blockquote);
+ NS_ENSURE_TRUE(newNode, NS_ERROR_NULL_POINTER);
+
+ // Try to set type=cite. Ignore it if this fails.
+ newNode->SetAttr(kNameSpaceID_None, nsGkAtoms::type,
+ NS_LITERAL_STRING("cite"), true);
+
+ // Set the selection to the underneath the node we just inserted:
+ rv = selection->Collapse(newNode, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Ensure that the inserted <blockquote> has a frame to make it IsEditable.
+ FlushFrames();
+
+ return Paste(aSelectionType);
+}
+
+/**
+ * Paste a plaintext quotation.
+ */
+NS_IMETHODIMP
+HTMLEditor::PasteAsPlaintextQuotation(int32_t aSelectionType)
+{
+ // Get Clipboard Service
+ nsresult rv;
+ nsCOMPtr<nsIClipboard> clipboard(do_GetService("@mozilla.org/widget/clipboard;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Create generic Transferable for getting the data
+ nsCOMPtr<nsITransferable> trans =
+ do_CreateInstance("@mozilla.org/widget/transferable;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(trans, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIDocument> destdoc = GetDocument();
+ nsILoadContext* loadContext = destdoc ? destdoc->GetLoadContext() : nullptr;
+ trans->Init(loadContext);
+
+ // We only handle plaintext pastes here
+ trans->AddDataFlavor(kUnicodeMime);
+
+ // Get the Data from the clipboard
+ clipboard->GetData(trans, aSelectionType);
+
+ // Now we ask the transferable for the data
+ // it still owns the data, we just have a pointer to it.
+ // If it can't support a "text" output of the data the call will fail
+ nsCOMPtr<nsISupports> genericDataObj;
+ uint32_t len = 0;
+ nsAutoCString flav;
+ rv = trans->GetAnyTransferData(flav, getter_AddRefs(genericDataObj), &len);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (flav.EqualsLiteral(kUnicodeMime)) {
+ nsCOMPtr<nsISupportsString> textDataObj = do_QueryInterface(genericDataObj);
+ if (textDataObj && len > 0) {
+ nsAutoString stuffToPaste;
+ textDataObj->GetData(stuffToPaste);
+ NS_ASSERTION(stuffToPaste.Length() <= (len/2), "Invalid length!");
+ AutoEditBatch beginBatching(this);
+ rv = InsertAsPlaintextQuotation(stuffToPaste, true, 0);
+ }
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+HTMLEditor::InsertTextWithQuotations(const nsAString& aStringToInsert)
+{
+ AutoEditBatch beginBatching(this);
+ // The whole operation should be undoable in one transaction:
+ BeginTransaction();
+
+ // We're going to loop over the string, collecting up a "hunk"
+ // that's all the same type (quoted or not),
+ // Whenever the quotedness changes (or we reach the string's end)
+ // we will insert the hunk all at once, quoted or non.
+
+ static const char16_t cite('>');
+ bool curHunkIsQuoted = (aStringToInsert.First() == cite);
+
+ nsAString::const_iterator hunkStart, strEnd;
+ aStringToInsert.BeginReading(hunkStart);
+ aStringToInsert.EndReading(strEnd);
+
+ // In the loop below, we only look for DOM newlines (\n),
+ // because we don't have a FindChars method that can look
+ // for both \r and \n. \r is illegal in the dom anyway,
+ // but in debug builds, let's take the time to verify that
+ // there aren't any there:
+#ifdef DEBUG
+ nsAString::const_iterator dbgStart (hunkStart);
+ if (FindCharInReadable('\r', dbgStart, strEnd)) {
+ NS_ASSERTION(false,
+ "Return characters in DOM! InsertTextWithQuotations may be wrong");
+ }
+#endif /* DEBUG */
+
+ // Loop over lines:
+ nsresult rv = NS_OK;
+ nsAString::const_iterator lineStart (hunkStart);
+ // We will break from inside when we run out of newlines.
+ for (;;) {
+ // Search for the end of this line (dom newlines, see above):
+ bool found = FindCharInReadable('\n', lineStart, strEnd);
+ bool quoted = false;
+ if (found) {
+ // if there's another newline, lineStart now points there.
+ // Loop over any consecutive newline chars:
+ nsAString::const_iterator firstNewline (lineStart);
+ while (*lineStart == '\n') {
+ ++lineStart;
+ }
+ quoted = (*lineStart == cite);
+ if (quoted == curHunkIsQuoted) {
+ continue;
+ }
+ // else we're changing state, so we need to insert
+ // from curHunk to lineStart then loop around.
+
+ // But if the current hunk is quoted, then we want to make sure
+ // that any extra newlines on the end do not get included in
+ // the quoted section: blank lines flaking a quoted section
+ // should be considered unquoted, so that if the user clicks
+ // there and starts typing, the new text will be outside of
+ // the quoted block.
+ if (curHunkIsQuoted) {
+ lineStart = firstNewline;
+
+ // 'firstNewline' points to the first '\n'. We want to
+ // ensure that this first newline goes into the hunk
+ // since quoted hunks can be displayed as blocks
+ // (and the newline should become invisible in this case).
+ // So the next line needs to start at the next character.
+ lineStart++;
+ }
+ }
+
+ // If no newline found, lineStart is now strEnd and we can finish up,
+ // inserting from curHunk to lineStart then returning.
+ const nsAString &curHunk = Substring(hunkStart, lineStart);
+ nsCOMPtr<nsIDOMNode> dummyNode;
+ if (curHunkIsQuoted) {
+ rv = InsertAsPlaintextQuotation(curHunk, false,
+ getter_AddRefs(dummyNode));
+ } else {
+ rv = InsertText(curHunk);
+ }
+ if (!found) {
+ break;
+ }
+ curHunkIsQuoted = quoted;
+ hunkStart = lineStart;
+ }
+
+ EndTransaction();
+
+ return rv;
+}
+
+NS_IMETHODIMP
+HTMLEditor::InsertAsQuotation(const nsAString& aQuotedText,
+ nsIDOMNode** aNodeInserted)
+{
+ if (IsPlaintextEditor()) {
+ return InsertAsPlaintextQuotation(aQuotedText, true, aNodeInserted);
+ }
+
+ nsAutoString citation;
+ return InsertAsCitedQuotation(aQuotedText, citation, false,
+ aNodeInserted);
+}
+
+// Insert plaintext as a quotation, with cite marks (e.g. "> ").
+// This differs from its corresponding method in TextEditor
+// in that here, quoted material is enclosed in a <pre> tag
+// in order to preserve the original line wrapping.
+NS_IMETHODIMP
+HTMLEditor::InsertAsPlaintextQuotation(const nsAString& aQuotedText,
+ bool aAddCites,
+ nsIDOMNode** aNodeInserted)
+{
+ // get selection
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+
+ AutoEditBatch beginBatching(this);
+ AutoRules beginRulesSniffing(this, EditAction::insertQuotation,
+ nsIEditor::eNext);
+
+ // give rules a chance to handle or cancel
+ TextRulesInfo ruleInfo(EditAction::insertElement);
+ bool cancel, handled;
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+ nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (cancel || handled) {
+ return NS_OK; // rules canceled the operation
+ }
+
+ // Wrap the inserted quote in a <span> so we can distinguish it. If we're
+ // inserting into the <body>, we use a <span> which is displayed as a block
+ // and sized to the screen using 98 viewport width units.
+ // We could use 100vw, but 98vw avoids a horizontal scroll bar where possible.
+ // All this is done to wrap overlong lines to the screen and not to the
+ // container element, the width-restricted body.
+ nsCOMPtr<Element> newNode =
+ DeleteSelectionAndCreateElement(*nsGkAtoms::span);
+
+ // If this succeeded, then set selection inside the pre
+ // so the inserted text will end up there.
+ // If it failed, we don't care what the return value was,
+ // but we'll fall through and try to insert the text anyway.
+ if (newNode) {
+ // Add an attribute on the pre node so we'll know it's a quotation.
+ newNode->SetAttr(kNameSpaceID_None, nsGkAtoms::mozquote,
+ NS_LITERAL_STRING("true"), true);
+ // Allow wrapping on spans so long lines get wrapped to the screen.
+ nsCOMPtr<nsINode> parent = newNode->GetParentNode();
+ if (parent && parent->IsHTMLElement(nsGkAtoms::body)) {
+ newNode->SetAttr(kNameSpaceID_None, nsGkAtoms::style,
+ NS_LITERAL_STRING("white-space: pre-wrap; display: block; width: 98vw;"),
+ true);
+ } else {
+ newNode->SetAttr(kNameSpaceID_None, nsGkAtoms::style,
+ NS_LITERAL_STRING("white-space: pre-wrap;"), true);
+ }
+
+ // and set the selection inside it:
+ selection->Collapse(newNode, 0);
+ }
+
+ // Ensure that the inserted <span> has a frame to make it IsEditable.
+ FlushFrames();
+
+ if (aAddCites) {
+ rv = TextEditor::InsertAsQuotation(aQuotedText, aNodeInserted);
+ } else {
+ rv = TextEditor::InsertText(aQuotedText);
+ }
+ // Note that if !aAddCites, aNodeInserted isn't set.
+ // That's okay because the routines that use aAddCites
+ // don't need to know the inserted node.
+
+ if (aNodeInserted && NS_SUCCEEDED(rv)) {
+ *aNodeInserted = GetAsDOMNode(newNode);
+ NS_IF_ADDREF(*aNodeInserted);
+ }
+
+ // Set the selection to just after the inserted node:
+ if (NS_SUCCEEDED(rv) && newNode) {
+ nsCOMPtr<nsINode> parent = newNode->GetParentNode();
+ int32_t offset = parent ? parent->IndexOf(newNode) : -1;
+ if (parent) {
+ selection->Collapse(parent, offset + 1);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+HTMLEditor::StripCites()
+{
+ return TextEditor::StripCites();
+}
+
+NS_IMETHODIMP
+HTMLEditor::Rewrap(bool aRespectNewlines)
+{
+ return TextEditor::Rewrap(aRespectNewlines);
+}
+
+NS_IMETHODIMP
+HTMLEditor::InsertAsCitedQuotation(const nsAString& aQuotedText,
+ const nsAString& aCitation,
+ bool aInsertHTML,
+ nsIDOMNode** aNodeInserted)
+{
+ // Don't let anyone insert html into a "plaintext" editor:
+ if (IsPlaintextEditor()) {
+ NS_ASSERTION(!aInsertHTML, "InsertAsCitedQuotation: trying to insert html into plaintext editor");
+ return InsertAsPlaintextQuotation(aQuotedText, true, aNodeInserted);
+ }
+
+ // get selection
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+
+ AutoEditBatch beginBatching(this);
+ AutoRules beginRulesSniffing(this, EditAction::insertQuotation,
+ nsIEditor::eNext);
+
+ // give rules a chance to handle or cancel
+ TextRulesInfo ruleInfo(EditAction::insertElement);
+ bool cancel, handled;
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+ nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (cancel || handled) {
+ return NS_OK; // rules canceled the operation
+ }
+
+ nsCOMPtr<Element> newNode =
+ DeleteSelectionAndCreateElement(*nsGkAtoms::blockquote);
+ NS_ENSURE_TRUE(newNode, NS_ERROR_NULL_POINTER);
+
+ // Try to set type=cite. Ignore it if this fails.
+ newNode->SetAttr(kNameSpaceID_None, nsGkAtoms::type,
+ NS_LITERAL_STRING("cite"), true);
+
+ if (!aCitation.IsEmpty()) {
+ newNode->SetAttr(kNameSpaceID_None, nsGkAtoms::cite, aCitation, true);
+ }
+
+ // Set the selection inside the blockquote so aQuotedText will go there:
+ selection->Collapse(newNode, 0);
+
+ // Ensure that the inserted <blockquote> has a frame to make it IsEditable.
+ FlushFrames();
+
+ if (aInsertHTML) {
+ rv = LoadHTML(aQuotedText);
+ } else {
+ rv = InsertText(aQuotedText); // XXX ignore charset
+ }
+
+ if (aNodeInserted && NS_SUCCEEDED(rv)) {
+ *aNodeInserted = GetAsDOMNode(newNode);
+ NS_IF_ADDREF(*aNodeInserted);
+ }
+
+ // Set the selection to just after the inserted node:
+ if (NS_SUCCEEDED(rv) && newNode) {
+ nsCOMPtr<nsINode> parent = newNode->GetParentNode();
+ int32_t offset = parent ? parent->IndexOf(newNode) : -1;
+ if (parent) {
+ selection->Collapse(parent, offset + 1);
+ }
+ }
+ return rv;
+}
+
+
+void RemoveBodyAndHead(nsINode& aNode)
+{
+ nsCOMPtr<nsIContent> body, head;
+ // find the body and head nodes if any.
+ // look only at immediate children of aNode.
+ for (nsCOMPtr<nsIContent> child = aNode.GetFirstChild();
+ child;
+ child = child->GetNextSibling()) {
+ if (child->IsHTMLElement(nsGkAtoms::body)) {
+ body = child;
+ } else if (child->IsHTMLElement(nsGkAtoms::head)) {
+ head = child;
+ }
+ }
+ if (head) {
+ ErrorResult ignored;
+ aNode.RemoveChild(*head, ignored);
+ }
+ if (body) {
+ nsCOMPtr<nsIContent> child = body->GetFirstChild();
+ while (child) {
+ ErrorResult ignored;
+ aNode.InsertBefore(*child, body, ignored);
+ child = body->GetFirstChild();
+ }
+
+ ErrorResult ignored;
+ aNode.RemoveChild(*body, ignored);
+ }
+}
+
+/**
+ * This function finds the target node that we will be pasting into. aStart is
+ * the context that we're given and aResult will be the target. Initially,
+ * *aResult must be nullptr.
+ *
+ * The target for a paste is found by either finding the node that contains
+ * the magical comment node containing kInsertCookie or, failing that, the
+ * firstChild of the firstChild (until we reach a leaf).
+ */
+nsresult FindTargetNode(nsIDOMNode *aStart, nsCOMPtr<nsIDOMNode> &aResult)
+{
+ NS_ENSURE_TRUE(aStart, NS_OK);
+
+ nsCOMPtr<nsIDOMNode> child, tmp;
+
+ nsresult rv = aStart->GetFirstChild(getter_AddRefs(child));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!child) {
+ // If the current result is nullptr, then aStart is a leaf, and is the
+ // fallback result.
+ if (!aResult) {
+ aResult = aStart;
+ }
+ return NS_OK;
+ }
+
+ do {
+ // Is this child the magical cookie?
+ nsCOMPtr<nsIDOMComment> comment = do_QueryInterface(child);
+ if (comment) {
+ nsAutoString data;
+ rv = comment->GetData(data);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (data.EqualsLiteral(kInsertCookie)) {
+ // Yes it is! Return an error so we bubble out and short-circuit the
+ // search.
+ aResult = aStart;
+
+ // Note: it doesn't matter if this fails.
+ aStart->RemoveChild(child, getter_AddRefs(tmp));
+
+ return NS_SUCCESS_EDITOR_FOUND_TARGET;
+ }
+ }
+
+ rv = FindTargetNode(child, aResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (rv == NS_SUCCESS_EDITOR_FOUND_TARGET) {
+ return NS_SUCCESS_EDITOR_FOUND_TARGET;
+ }
+
+ rv = child->GetNextSibling(getter_AddRefs(tmp));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ child = tmp;
+ } while (child);
+
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::CreateDOMFragmentFromPaste(const nsAString& aInputString,
+ const nsAString& aContextStr,
+ const nsAString& aInfoStr,
+ nsCOMPtr<nsIDOMNode>* outFragNode,
+ nsCOMPtr<nsIDOMNode>* outStartNode,
+ nsCOMPtr<nsIDOMNode>* outEndNode,
+ int32_t* outStartOffset,
+ int32_t* outEndOffset,
+ bool aTrustedInput)
+{
+ NS_ENSURE_TRUE(outFragNode && outStartNode && outEndNode, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsIDocument> doc = GetDocument();
+ NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE);
+
+ // if we have context info, create a fragment for that
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIDOMNode> contextLeaf;
+ RefPtr<DocumentFragment> contextAsNode;
+ if (!aContextStr.IsEmpty()) {
+ rv = ParseFragment(aContextStr, nullptr, doc, getter_AddRefs(contextAsNode),
+ aTrustedInput);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(contextAsNode, NS_ERROR_FAILURE);
+
+ rv = StripFormattingNodes(*contextAsNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RemoveBodyAndHead(*contextAsNode);
+
+ rv = FindTargetNode(contextAsNode, contextLeaf);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsIContent> contextLeafAsContent = do_QueryInterface(contextLeaf);
+
+ // create fragment for pasted html
+ nsIAtom* contextAtom;
+ if (contextLeafAsContent) {
+ contextAtom = contextLeafAsContent->NodeInfo()->NameAtom();
+ if (contextLeafAsContent->IsHTMLElement(nsGkAtoms::html)) {
+ contextAtom = nsGkAtoms::body;
+ }
+ } else {
+ contextAtom = nsGkAtoms::body;
+ }
+ RefPtr<DocumentFragment> fragment;
+ rv = ParseFragment(aInputString,
+ contextAtom,
+ doc,
+ getter_AddRefs(fragment),
+ aTrustedInput);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(fragment, NS_ERROR_FAILURE);
+
+ RemoveBodyAndHead(*fragment);
+
+ if (contextAsNode) {
+ // unite the two trees
+ nsCOMPtr<nsIDOMNode> junk;
+ contextLeaf->AppendChild(fragment, getter_AddRefs(junk));
+ fragment = contextAsNode;
+ }
+
+ rv = StripFormattingNodes(*fragment, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If there was no context, then treat all of the data we did get as the
+ // pasted data.
+ if (contextLeaf) {
+ *outEndNode = *outStartNode = contextLeaf;
+ } else {
+ *outEndNode = *outStartNode = fragment;
+ }
+
+ *outFragNode = fragment.forget();
+ *outStartOffset = 0;
+
+ // get the infoString contents
+ if (!aInfoStr.IsEmpty()) {
+ int32_t sep = aInfoStr.FindChar((char16_t)',');
+ nsAutoString numstr1(Substring(aInfoStr, 0, sep));
+ nsAutoString numstr2(Substring(aInfoStr, sep+1, aInfoStr.Length() - (sep+1)));
+
+ // Move the start and end children.
+ nsresult err;
+ int32_t num = numstr1.ToInteger(&err);
+
+ nsCOMPtr<nsIDOMNode> tmp;
+ while (num--) {
+ (*outStartNode)->GetFirstChild(getter_AddRefs(tmp));
+ NS_ENSURE_TRUE(tmp, NS_ERROR_FAILURE);
+ tmp.swap(*outStartNode);
+ }
+
+ num = numstr2.ToInteger(&err);
+ while (num--) {
+ (*outEndNode)->GetLastChild(getter_AddRefs(tmp));
+ NS_ENSURE_TRUE(tmp, NS_ERROR_FAILURE);
+ tmp.swap(*outEndNode);
+ }
+ }
+
+ nsCOMPtr<nsINode> node = do_QueryInterface(*outEndNode);
+ *outEndOffset = node->Length();
+ return NS_OK;
+}
+
+
+nsresult
+HTMLEditor::ParseFragment(const nsAString& aFragStr,
+ nsIAtom* aContextLocalName,
+ nsIDocument* aTargetDocument,
+ DocumentFragment** aFragment,
+ bool aTrustedInput)
+{
+ nsAutoScriptBlockerSuppressNodeRemoved autoBlocker;
+
+ RefPtr<DocumentFragment> fragment =
+ new DocumentFragment(aTargetDocument->NodeInfoManager());
+ nsresult rv = nsContentUtils::ParseFragmentHTML(aFragStr,
+ fragment,
+ aContextLocalName ?
+ aContextLocalName : nsGkAtoms::body,
+ kNameSpaceID_XHTML,
+ false,
+ true);
+ if (!aTrustedInput) {
+ nsTreeSanitizer sanitizer(aContextLocalName ?
+ nsIParserUtils::SanitizerAllowStyle :
+ nsIParserUtils::SanitizerAllowComments);
+ sanitizer.Sanitize(fragment);
+ }
+ fragment.forget(aFragment);
+ return rv;
+}
+
+void
+HTMLEditor::CreateListOfNodesToPaste(
+ DocumentFragment& aFragment,
+ nsTArray<OwningNonNull<nsINode>>& outNodeList,
+ nsINode* aStartNode,
+ int32_t aStartOffset,
+ nsINode* aEndNode,
+ int32_t aEndOffset)
+{
+ // If no info was provided about the boundary between context and stream,
+ // then assume all is stream.
+ if (!aStartNode) {
+ aStartNode = &aFragment;
+ aStartOffset = 0;
+ aEndNode = &aFragment;
+ aEndOffset = aFragment.Length();
+ }
+
+ RefPtr<nsRange> docFragRange;
+ nsresult rv = nsRange::CreateRange(aStartNode, aStartOffset,
+ aEndNode, aEndOffset,
+ getter_AddRefs(docFragRange));
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ NS_ENSURE_SUCCESS(rv, );
+
+ // Now use a subtree iterator over the range to create a list of nodes
+ TrivialFunctor functor;
+ DOMSubtreeIterator iter;
+ rv = iter.Init(*docFragRange);
+ NS_ENSURE_SUCCESS(rv, );
+ iter.AppendList(functor, outNodeList);
+}
+
+void
+HTMLEditor::GetListAndTableParents(StartOrEnd aStartOrEnd,
+ nsTArray<OwningNonNull<nsINode>>& aNodeList,
+ nsTArray<OwningNonNull<Element>>& outArray)
+{
+ MOZ_ASSERT(aNodeList.Length());
+
+ // Build up list of parents of first (or last) node in list that are either
+ // lists, or tables.
+ int32_t idx = aStartOrEnd == StartOrEnd::end ? aNodeList.Length() - 1 : 0;
+
+ for (nsCOMPtr<nsINode> node = aNodeList[idx]; node;
+ node = node->GetParentNode()) {
+ if (HTMLEditUtils::IsList(node) || HTMLEditUtils::IsTable(node)) {
+ outArray.AppendElement(*node->AsElement());
+ }
+ }
+}
+
+int32_t
+HTMLEditor::DiscoverPartialListsAndTables(
+ nsTArray<OwningNonNull<nsINode>>& aPasteNodes,
+ nsTArray<OwningNonNull<Element>>& aListsAndTables)
+{
+ int32_t ret = -1;
+ int32_t listAndTableParents = aListsAndTables.Length();
+
+ // Scan insertion list for table elements (other than table).
+ for (auto& curNode : aPasteNodes) {
+ if (HTMLEditUtils::IsTableElement(curNode) &&
+ !curNode->IsHTMLElement(nsGkAtoms::table)) {
+ nsCOMPtr<Element> table = curNode->GetParentElement();
+ while (table && !table->IsHTMLElement(nsGkAtoms::table)) {
+ table = table->GetParentElement();
+ }
+ if (table) {
+ int32_t idx = aListsAndTables.IndexOf(table);
+ if (idx == -1) {
+ return ret;
+ }
+ ret = idx;
+ if (ret == listAndTableParents - 1) {
+ return ret;
+ }
+ }
+ }
+ if (HTMLEditUtils::IsListItem(curNode)) {
+ nsCOMPtr<Element> list = curNode->GetParentElement();
+ while (list && !HTMLEditUtils::IsList(list)) {
+ list = list->GetParentElement();
+ }
+ if (list) {
+ int32_t idx = aListsAndTables.IndexOf(list);
+ if (idx == -1) {
+ return ret;
+ }
+ ret = idx;
+ if (ret == listAndTableParents - 1) {
+ return ret;
+ }
+ }
+ }
+ }
+ return ret;
+}
+
+nsINode*
+HTMLEditor::ScanForListAndTableStructure(
+ StartOrEnd aStartOrEnd,
+ nsTArray<OwningNonNull<nsINode>>& aNodes,
+ Element& aListOrTable)
+{
+ // Look upward from first/last paste node for a piece of this list/table
+ int32_t idx = aStartOrEnd == StartOrEnd::end ? aNodes.Length() - 1 : 0;
+ bool isList = HTMLEditUtils::IsList(&aListOrTable);
+
+ for (nsCOMPtr<nsINode> node = aNodes[idx]; node;
+ node = node->GetParentNode()) {
+ if ((isList && HTMLEditUtils::IsListItem(node)) ||
+ (!isList && HTMLEditUtils::IsTableElement(node) &&
+ !node->IsHTMLElement(nsGkAtoms::table))) {
+ nsCOMPtr<Element> structureNode = node->GetParentElement();
+ if (isList) {
+ while (structureNode && !HTMLEditUtils::IsList(structureNode)) {
+ structureNode = structureNode->GetParentElement();
+ }
+ } else {
+ while (structureNode &&
+ !structureNode->IsHTMLElement(nsGkAtoms::table)) {
+ structureNode = structureNode->GetParentElement();
+ }
+ }
+ if (structureNode == &aListOrTable) {
+ if (isList) {
+ return structureNode;
+ }
+ return node;
+ }
+ }
+ }
+ return nullptr;
+}
+
+void
+HTMLEditor::ReplaceOrphanedStructure(
+ StartOrEnd aStartOrEnd,
+ nsTArray<OwningNonNull<nsINode>>& aNodeArray,
+ nsTArray<OwningNonNull<Element>>& aListAndTableArray,
+ int32_t aHighWaterMark)
+{
+ OwningNonNull<Element> curNode = aListAndTableArray[aHighWaterMark];
+
+ // Find substructure of list or table that must be included in paste.
+ nsCOMPtr<nsINode> replaceNode =
+ ScanForListAndTableStructure(aStartOrEnd, aNodeArray, curNode);
+
+ if (!replaceNode) {
+ return;
+ }
+
+ // If we found substructure, paste it instead of its descendants.
+ // Only replace with the substructure if all the nodes in the list are
+ // descendants.
+ bool shouldReplaceNodes = true;
+ for (uint32_t i = 0; i < aNodeArray.Length(); i++) {
+ uint32_t idx = aStartOrEnd == StartOrEnd::start ?
+ i : (aNodeArray.Length() - i - 1);
+ OwningNonNull<nsINode> endpoint = aNodeArray[idx];
+ if (!EditorUtils::IsDescendantOf(endpoint, replaceNode)) {
+ shouldReplaceNodes = false;
+ break;
+ }
+ }
+
+ if (shouldReplaceNodes) {
+ // Now replace the removed nodes with the structural parent
+ aNodeArray.Clear();
+ if (aStartOrEnd == StartOrEnd::end) {
+ aNodeArray.AppendElement(*replaceNode);
+ } else {
+ aNodeArray.InsertElementAt(0, *replaceNode);
+ }
+ }
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/HTMLEditorEventListener.cpp b/editor/libeditor/HTMLEditorEventListener.cpp
new file mode 100644
index 000000000..8fb9459c2
--- /dev/null
+++ b/editor/libeditor/HTMLEditorEventListener.cpp
@@ -0,0 +1,215 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HTMLEditorEventListener.h"
+
+#include "HTMLEditUtils.h"
+#include "mozilla/HTMLEditor.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/Selection.h"
+#include "nsCOMPtr.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsIDOMElement.h"
+#include "nsIDOMEvent.h"
+#include "nsIDOMEventTarget.h"
+#include "nsIDOMMouseEvent.h"
+#include "nsIDOMNode.h"
+#include "nsIEditor.h"
+#include "nsIHTMLInlineTableEditor.h"
+#include "nsIHTMLObjectResizer.h"
+#include "nsISupportsImpl.h"
+#include "nsLiteralString.h"
+#include "nsQueryObject.h"
+#include "nsRange.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+#ifdef DEBUG
+nsresult
+HTMLEditorEventListener::Connect(EditorBase* aEditorBase)
+{
+ nsCOMPtr<nsIHTMLEditor> htmlEditor = do_QueryObject(aEditorBase);
+ nsCOMPtr<nsIHTMLInlineTableEditor> htmlInlineTableEditor =
+ do_QueryObject(aEditorBase);
+ NS_PRECONDITION(htmlEditor && htmlInlineTableEditor,
+ "Set HTMLEditor or its sub class");
+ return EditorEventListener::Connect(aEditorBase);
+}
+#endif
+
+HTMLEditor*
+HTMLEditorEventListener::GetHTMLEditor()
+{
+ // mEditor must be HTMLEditor or its subclass.
+ return static_cast<HTMLEditor*>(mEditorBase);
+}
+
+nsresult
+HTMLEditorEventListener::MouseUp(nsIDOMMouseEvent* aMouseEvent)
+{
+ HTMLEditor* htmlEditor = GetHTMLEditor();
+
+ nsCOMPtr<nsIDOMEventTarget> target;
+ nsresult rv = aMouseEvent->AsEvent()->GetTarget(getter_AddRefs(target));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(target, NS_ERROR_NULL_POINTER);
+ nsCOMPtr<nsIDOMElement> element = do_QueryInterface(target);
+
+ int32_t clientX, clientY;
+ aMouseEvent->GetClientX(&clientX);
+ aMouseEvent->GetClientY(&clientY);
+ htmlEditor->MouseUp(clientX, clientY, element);
+
+ return EditorEventListener::MouseUp(aMouseEvent);
+}
+
+nsresult
+HTMLEditorEventListener::MouseDown(nsIDOMMouseEvent* aMouseEvent)
+{
+ HTMLEditor* htmlEditor = GetHTMLEditor();
+ // Contenteditable should disregard mousedowns outside it.
+ // IsAcceptableInputEvent() checks it for a mouse event.
+ if (!htmlEditor->IsAcceptableInputEvent(aMouseEvent->AsEvent())) {
+ // If it's not acceptable mousedown event (including when mousedown event
+ // is fired outside of the active editing host), we need to commit
+ // composition because it will be change the selection to the clicked
+ // point. Then, we won't be able to commit the composition.
+ return EditorEventListener::MouseDown(aMouseEvent);
+ }
+
+ // Detect only "context menu" click
+ // XXX This should be easier to do!
+ // But eDOMEvents_contextmenu and eContextMenu is not exposed in any event
+ // interface :-(
+ int16_t buttonNumber;
+ nsresult rv = aMouseEvent->GetButton(&buttonNumber);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool isContextClick = buttonNumber == 2;
+
+ int32_t clickCount;
+ rv = aMouseEvent->GetDetail(&clickCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDOMEventTarget> target;
+ rv = aMouseEvent->AsEvent()->GetExplicitOriginalTarget(getter_AddRefs(target));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(target, NS_ERROR_NULL_POINTER);
+ nsCOMPtr<nsIDOMElement> element = do_QueryInterface(target);
+
+ if (isContextClick || (buttonNumber == 0 && clickCount == 2)) {
+ RefPtr<Selection> selection = mEditorBase->GetSelection();
+ NS_ENSURE_TRUE(selection, NS_OK);
+
+ // Get location of mouse within target node
+ nsCOMPtr<nsIDOMNode> parent;
+ rv = aMouseEvent->GetRangeParent(getter_AddRefs(parent));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(parent, NS_ERROR_FAILURE);
+
+ int32_t offset = 0;
+ rv = aMouseEvent->GetRangeOffset(&offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Detect if mouse point is within current selection for context click
+ bool nodeIsInSelection = false;
+ if (isContextClick && !selection->Collapsed()) {
+ int32_t rangeCount;
+ rv = selection->GetRangeCount(&rangeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (int32_t i = 0; i < rangeCount; i++) {
+ RefPtr<nsRange> range = selection->GetRangeAt(i);
+ if (!range) {
+ // Don't bail yet, iterate through them all
+ continue;
+ }
+
+ range->IsPointInRange(parent, offset, &nodeIsInSelection);
+
+ // Done when we find a range that we are in
+ if (nodeIsInSelection) {
+ break;
+ }
+ }
+ }
+ nsCOMPtr<nsIDOMNode> node = do_QueryInterface(target);
+ if (node && !nodeIsInSelection) {
+ if (!element) {
+ if (isContextClick) {
+ // Set the selection to the point under the mouse cursor:
+ selection->Collapse(parent, offset);
+ } else {
+ // Get enclosing link if in text so we can select the link
+ nsCOMPtr<nsIDOMElement> linkElement;
+ rv = htmlEditor->GetElementOrParentByTagName(
+ NS_LITERAL_STRING("href"), node,
+ getter_AddRefs(linkElement));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (linkElement) {
+ element = linkElement;
+ }
+ }
+ }
+ // Select entire element clicked on if NOT within an existing selection
+ // and not the entire body, or table-related elements
+ if (element) {
+ nsCOMPtr<nsIDOMNode> selectAllNode =
+ htmlEditor->FindUserSelectAllNode(element);
+
+ if (selectAllNode) {
+ nsCOMPtr<nsIDOMElement> newElement = do_QueryInterface(selectAllNode);
+ if (newElement) {
+ node = selectAllNode;
+ element = newElement;
+ }
+ }
+
+ if (isContextClick && !HTMLEditUtils::IsImage(node)) {
+ selection->Collapse(parent, offset);
+ } else {
+ htmlEditor->SelectElement(element);
+ }
+ }
+ }
+ // HACK !!! Context click places the caret but the context menu consumes
+ // the event; so we need to check resizing state ourselves
+ htmlEditor->CheckSelectionStateForAnonymousButtons(selection);
+
+ // Prevent bubbling if we changed selection or
+ // for all context clicks
+ if (element || isContextClick) {
+ aMouseEvent->AsEvent()->PreventDefault();
+ return NS_OK;
+ }
+ } else if (!isContextClick && buttonNumber == 0 && clickCount == 1) {
+ // if the target element is an image, we have to display resizers
+ int32_t clientX, clientY;
+ aMouseEvent->GetClientX(&clientX);
+ aMouseEvent->GetClientY(&clientY);
+ htmlEditor->MouseDown(clientX, clientY, element, aMouseEvent->AsEvent());
+ }
+
+ return EditorEventListener::MouseDown(aMouseEvent);
+}
+
+nsresult
+HTMLEditorEventListener::MouseClick(nsIDOMMouseEvent* aMouseEvent)
+{
+ nsCOMPtr<nsIDOMEventTarget> target;
+ nsresult rv = aMouseEvent->AsEvent()->GetTarget(getter_AddRefs(target));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(target, NS_ERROR_NULL_POINTER);
+ nsCOMPtr<nsIDOMElement> element = do_QueryInterface(target);
+
+ GetHTMLEditor()->DoInlineTableEditingAction(element);
+
+ return EditorEventListener::MouseClick(aMouseEvent);
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/HTMLEditorEventListener.h b/editor/libeditor/HTMLEditorEventListener.h
new file mode 100644
index 000000000..b97b675b5
--- /dev/null
+++ b/editor/libeditor/HTMLEditorEventListener.h
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef HTMLEditorEventListener_h
+#define HTMLEditorEventListener_h
+
+#include "EditorEventListener.h"
+#include "nscore.h"
+
+namespace mozilla {
+
+class EditorBase;
+class HTMLEditor;
+
+class HTMLEditorEventListener final : public EditorEventListener
+{
+public:
+ HTMLEditorEventListener()
+ {
+ }
+
+ virtual ~HTMLEditorEventListener()
+ {
+ }
+
+#ifdef DEBUG
+ // WARNING: You must be use HTMLEditor or its sub class for this class.
+ virtual nsresult Connect(EditorBase* aEditorBase) override;
+#endif
+
+protected:
+ virtual nsresult MouseDown(nsIDOMMouseEvent* aMouseEvent) override;
+ virtual nsresult MouseUp(nsIDOMMouseEvent* aMouseEvent) override;
+ virtual nsresult MouseClick(nsIDOMMouseEvent* aMouseEvent) override;
+
+ inline HTMLEditor* GetHTMLEditor();
+};
+
+} // namespace mozilla
+
+#endif // #ifndef HTMLEditorEventListener_h
diff --git a/editor/libeditor/HTMLEditorObjectResizer.cpp b/editor/libeditor/HTMLEditorObjectResizer.cpp
new file mode 100644
index 000000000..111a3f975
--- /dev/null
+++ b/editor/libeditor/HTMLEditorObjectResizer.cpp
@@ -0,0 +1,1059 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/HTMLEditor.h"
+#include "HTMLEditorObjectResizerUtils.h"
+
+#include "HTMLEditUtils.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/EditorUtils.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/MathAlgorithms.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/mozalloc.h"
+#include "nsAString.h"
+#include "nsAlgorithm.h"
+#include "nsCOMPtr.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsGkAtoms.h"
+#include "nsIAtom.h"
+#include "nsIContent.h"
+#include "nsID.h"
+#include "nsIDOMDocument.h"
+#include "nsIDOMElement.h"
+#include "nsIDOMEvent.h"
+#include "nsIDOMEventTarget.h"
+#include "nsIDOMMouseEvent.h"
+#include "nsIDOMNode.h"
+#include "nsIDOMText.h"
+#include "nsIDocument.h"
+#include "nsIEditor.h"
+#include "nsIHTMLObjectResizeListener.h"
+#include "nsIHTMLObjectResizer.h"
+#include "nsIPresShell.h"
+#include "nsISupportsUtils.h"
+#include "nsPIDOMWindow.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsSubstringTuple.h"
+#include "nscore.h"
+#include <algorithm>
+
+class nsISelection;
+
+namespace mozilla {
+
+using namespace dom;
+
+/******************************************************************************
+ * mozilla::DocumentResizeEventListener
+ ******************************************************************************/
+
+NS_IMPL_ISUPPORTS(DocumentResizeEventListener, nsIDOMEventListener)
+
+DocumentResizeEventListener::DocumentResizeEventListener(nsIHTMLEditor* aEditor)
+{
+ mEditor = do_GetWeakReference(aEditor);
+}
+
+NS_IMETHODIMP
+DocumentResizeEventListener::HandleEvent(nsIDOMEvent* aMouseEvent)
+{
+ nsCOMPtr<nsIHTMLObjectResizer> objectResizer = do_QueryReferent(mEditor);
+ if (objectResizer) {
+ return objectResizer->RefreshResizers();
+ }
+ return NS_OK;
+}
+
+/******************************************************************************
+ * mozilla::ResizerSelectionListener
+ ******************************************************************************/
+
+NS_IMPL_ISUPPORTS(ResizerSelectionListener, nsISelectionListener)
+
+ResizerSelectionListener::ResizerSelectionListener(nsIHTMLEditor* aEditor)
+{
+ mEditor = do_GetWeakReference(aEditor);
+}
+
+NS_IMETHODIMP
+ResizerSelectionListener::NotifySelectionChanged(nsIDOMDocument* aDOMDocument,
+ nsISelection* aSelection,
+ int16_t aReason)
+{
+ if ((aReason & (nsISelectionListener::MOUSEDOWN_REASON |
+ nsISelectionListener::KEYPRESS_REASON |
+ nsISelectionListener::SELECTALL_REASON)) && aSelection) {
+ // the selection changed and we need to check if we have to
+ // hide and/or redisplay resizing handles
+ nsCOMPtr<nsIHTMLEditor> editor = do_QueryReferent(mEditor);
+ if (editor) {
+ editor->CheckSelectionStateForAnonymousButtons(aSelection);
+ }
+ }
+
+ return NS_OK;
+}
+
+/******************************************************************************
+ * mozilla::ResizerMouseMotionListener
+ ******************************************************************************/
+
+NS_IMPL_ISUPPORTS(ResizerMouseMotionListener, nsIDOMEventListener)
+
+ResizerMouseMotionListener::ResizerMouseMotionListener(nsIHTMLEditor* aEditor)
+{
+ mEditor = do_GetWeakReference(aEditor);
+}
+
+NS_IMETHODIMP
+ResizerMouseMotionListener::HandleEvent(nsIDOMEvent* aMouseEvent)
+{
+ nsCOMPtr<nsIDOMMouseEvent> mouseEvent ( do_QueryInterface(aMouseEvent) );
+ if (!mouseEvent) {
+ //non-ui event passed in. bad things.
+ return NS_OK;
+ }
+
+ // Don't do anything special if not an HTML object resizer editor
+ nsCOMPtr<nsIHTMLObjectResizer> objectResizer = do_QueryReferent(mEditor);
+ if (objectResizer) {
+ // check if we have to redisplay a resizing shadow
+ objectResizer->MouseMove(aMouseEvent);
+ }
+
+ return NS_OK;
+}
+
+/******************************************************************************
+ * mozilla::HTMLEditor
+ ******************************************************************************/
+
+already_AddRefed<Element>
+HTMLEditor::CreateResizer(int16_t aLocation,
+ nsIDOMNode* aParentNode)
+{
+ nsCOMPtr<nsIDOMElement> retDOM;
+ nsresult rv = CreateAnonymousElement(NS_LITERAL_STRING("span"),
+ aParentNode,
+ NS_LITERAL_STRING("mozResizer"),
+ false,
+ getter_AddRefs(retDOM));
+
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ NS_ENSURE_TRUE(retDOM, nullptr);
+
+ // add the mouse listener so we can detect a click on a resizer
+ nsCOMPtr<nsIDOMEventTarget> evtTarget = do_QueryInterface(retDOM);
+ evtTarget->AddEventListener(NS_LITERAL_STRING("mousedown"), mEventListener,
+ true);
+
+ nsAutoString locationStr;
+ switch (aLocation) {
+ case nsIHTMLObjectResizer::eTopLeft:
+ locationStr = kTopLeft;
+ break;
+ case nsIHTMLObjectResizer::eTop:
+ locationStr = kTop;
+ break;
+ case nsIHTMLObjectResizer::eTopRight:
+ locationStr = kTopRight;
+ break;
+
+ case nsIHTMLObjectResizer::eLeft:
+ locationStr = kLeft;
+ break;
+ case nsIHTMLObjectResizer::eRight:
+ locationStr = kRight;
+ break;
+
+ case nsIHTMLObjectResizer::eBottomLeft:
+ locationStr = kBottomLeft;
+ break;
+ case nsIHTMLObjectResizer::eBottom:
+ locationStr = kBottom;
+ break;
+ case nsIHTMLObjectResizer::eBottomRight:
+ locationStr = kBottomRight;
+ break;
+ }
+
+ nsCOMPtr<Element> ret = do_QueryInterface(retDOM);
+ rv = ret->SetAttr(kNameSpaceID_None, nsGkAtoms::anonlocation, locationStr,
+ true);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ return ret.forget();
+}
+
+already_AddRefed<Element>
+HTMLEditor::CreateShadow(nsIDOMNode* aParentNode,
+ nsIDOMElement* aOriginalObject)
+{
+ // let's create an image through the element factory
+ nsAutoString name;
+ if (HTMLEditUtils::IsImage(aOriginalObject)) {
+ name.AssignLiteral("img");
+ } else {
+ name.AssignLiteral("span");
+ }
+ nsCOMPtr<nsIDOMElement> retDOM;
+ CreateAnonymousElement(name, aParentNode,
+ NS_LITERAL_STRING("mozResizingShadow"), true,
+ getter_AddRefs(retDOM));
+
+ NS_ENSURE_TRUE(retDOM, nullptr);
+
+ nsCOMPtr<Element> ret = do_QueryInterface(retDOM);
+ return ret.forget();
+}
+
+already_AddRefed<Element>
+HTMLEditor::CreateResizingInfo(nsIDOMNode* aParentNode)
+{
+ // let's create an info box through the element factory
+ nsCOMPtr<nsIDOMElement> retDOM;
+ CreateAnonymousElement(NS_LITERAL_STRING("span"), aParentNode,
+ NS_LITERAL_STRING("mozResizingInfo"), true,
+ getter_AddRefs(retDOM));
+
+ nsCOMPtr<Element> ret = do_QueryInterface(retDOM);
+ return ret.forget();
+}
+
+nsresult
+HTMLEditor::SetAllResizersPosition()
+{
+ NS_ENSURE_TRUE(mTopLeftHandle, NS_ERROR_FAILURE);
+
+ int32_t x = mResizedObjectX;
+ int32_t y = mResizedObjectY;
+ int32_t w = mResizedObjectWidth;
+ int32_t h = mResizedObjectHeight;
+
+ // now let's place all the resizers around the image
+
+ // get the size of resizers
+ nsAutoString value;
+ float resizerWidth, resizerHeight;
+ nsCOMPtr<nsIAtom> dummyUnit;
+ mCSSEditUtils->GetComputedProperty(*mTopLeftHandle, *nsGkAtoms::width,
+ value);
+ mCSSEditUtils->ParseLength(value, &resizerWidth, getter_AddRefs(dummyUnit));
+ mCSSEditUtils->GetComputedProperty(*mTopLeftHandle, *nsGkAtoms::height,
+ value);
+ mCSSEditUtils->ParseLength(value, &resizerHeight, getter_AddRefs(dummyUnit));
+
+ int32_t rw = (int32_t)((resizerWidth + 1) / 2);
+ int32_t rh = (int32_t)((resizerHeight+ 1) / 2);
+
+ SetAnonymousElementPosition(x-rw, y-rh, static_cast<nsIDOMElement*>(GetAsDOMNode(mTopLeftHandle)));
+ SetAnonymousElementPosition(x+w/2-rw, y-rh, static_cast<nsIDOMElement*>(GetAsDOMNode(mTopHandle)));
+ SetAnonymousElementPosition(x+w-rw-1, y-rh, static_cast<nsIDOMElement*>(GetAsDOMNode(mTopRightHandle)));
+
+ SetAnonymousElementPosition(x-rw, y+h/2-rh, static_cast<nsIDOMElement*>(GetAsDOMNode(mLeftHandle)));
+ SetAnonymousElementPosition(x+w-rw-1, y+h/2-rh, static_cast<nsIDOMElement*>(GetAsDOMNode(mRightHandle)));
+
+ SetAnonymousElementPosition(x-rw, y+h-rh-1, static_cast<nsIDOMElement*>(GetAsDOMNode(mBottomLeftHandle)));
+ SetAnonymousElementPosition(x+w/2-rw, y+h-rh-1, static_cast<nsIDOMElement*>(GetAsDOMNode(mBottomHandle)));
+ SetAnonymousElementPosition(x+w-rw-1, y+h-rh-1, static_cast<nsIDOMElement*>(GetAsDOMNode(mBottomRightHandle)));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::RefreshResizers()
+{
+ // nothing to do if resizers are not displayed...
+ NS_ENSURE_TRUE(mResizedObject, NS_OK);
+
+ nsresult rv =
+ GetPositionAndDimensions(
+ static_cast<nsIDOMElement*>(GetAsDOMNode(mResizedObject)),
+ mResizedObjectX,
+ mResizedObjectY,
+ mResizedObjectWidth,
+ mResizedObjectHeight,
+ mResizedObjectBorderLeft,
+ mResizedObjectBorderTop,
+ mResizedObjectMarginLeft,
+ mResizedObjectMarginTop);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetAllResizersPosition();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return SetShadowPosition(mResizingShadow, mResizedObject,
+ mResizedObjectX, mResizedObjectY);
+}
+
+NS_IMETHODIMP
+HTMLEditor::ShowResizers(nsIDOMElement* aResizedElement)
+{
+ nsresult rv = ShowResizersInner(aResizedElement);
+ if (NS_FAILED(rv)) {
+ HideResizers();
+ }
+ return rv;
+}
+
+nsresult
+HTMLEditor::ShowResizersInner(nsIDOMElement* aResizedElement)
+{
+ NS_ENSURE_ARG_POINTER(aResizedElement);
+
+ nsCOMPtr<nsIDOMNode> parentNode;
+ nsresult rv = aResizedElement->GetParentNode(getter_AddRefs(parentNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mResizedObject) {
+ NS_ERROR("call HideResizers first");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsCOMPtr<nsINode> resizedNode = do_QueryInterface(aResizedElement);
+ if (NS_WARN_IF(!IsDescendantOfEditorRoot(resizedNode))) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ mResizedObject = do_QueryInterface(aResizedElement);
+ NS_ENSURE_STATE(mResizedObject);
+
+ // The resizers and the shadow will be anonymous siblings of the element.
+ mTopLeftHandle = CreateResizer(nsIHTMLObjectResizer::eTopLeft, parentNode);
+ NS_ENSURE_TRUE(mTopLeftHandle, NS_ERROR_FAILURE);
+ mTopHandle = CreateResizer(nsIHTMLObjectResizer::eTop, parentNode);
+ NS_ENSURE_TRUE(mTopHandle, NS_ERROR_FAILURE);
+ mTopRightHandle = CreateResizer(nsIHTMLObjectResizer::eTopRight, parentNode);
+ NS_ENSURE_TRUE(mTopRightHandle, NS_ERROR_FAILURE);
+
+ mLeftHandle = CreateResizer(nsIHTMLObjectResizer::eLeft, parentNode);
+ NS_ENSURE_TRUE(mLeftHandle, NS_ERROR_FAILURE);
+ mRightHandle = CreateResizer(nsIHTMLObjectResizer::eRight, parentNode);
+ NS_ENSURE_TRUE(mRightHandle, NS_ERROR_FAILURE);
+
+ mBottomLeftHandle = CreateResizer(nsIHTMLObjectResizer::eBottomLeft, parentNode);
+ NS_ENSURE_TRUE(mBottomLeftHandle, NS_ERROR_FAILURE);
+ mBottomHandle = CreateResizer(nsIHTMLObjectResizer::eBottom, parentNode);
+ NS_ENSURE_TRUE(mBottomHandle, NS_ERROR_FAILURE);
+ mBottomRightHandle = CreateResizer(nsIHTMLObjectResizer::eBottomRight, parentNode);
+ NS_ENSURE_TRUE(mBottomRightHandle, NS_ERROR_FAILURE);
+
+ rv = GetPositionAndDimensions(aResizedElement,
+ mResizedObjectX,
+ mResizedObjectY,
+ mResizedObjectWidth,
+ mResizedObjectHeight,
+ mResizedObjectBorderLeft,
+ mResizedObjectBorderTop,
+ mResizedObjectMarginLeft,
+ mResizedObjectMarginTop);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // and let's set their absolute positions in the document
+ rv = SetAllResizersPosition();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // now, let's create the resizing shadow
+ mResizingShadow = CreateShadow(parentNode, aResizedElement);
+ NS_ENSURE_TRUE(mResizingShadow, NS_ERROR_FAILURE);
+ // and set its position
+ rv = SetShadowPosition(mResizingShadow, mResizedObject,
+ mResizedObjectX, mResizedObjectY);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // and then the resizing info tooltip
+ mResizingInfo = CreateResizingInfo(parentNode);
+ NS_ENSURE_TRUE(mResizingInfo, NS_ERROR_FAILURE);
+
+ // and listen to the "resize" event on the window first, get the
+ // window from the document...
+ nsCOMPtr<nsIDocument> doc = GetDocument();
+ NS_ENSURE_TRUE(doc, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsIDOMEventTarget> target = do_QueryInterface(doc->GetWindow());
+ if (!target) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ mResizeEventListenerP = new DocumentResizeEventListener(this);
+ if (!mResizeEventListenerP) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ rv = target->AddEventListener(NS_LITERAL_STRING("resize"),
+ mResizeEventListenerP, false);
+ // XXX Even when it failed to add event listener, should we need to set
+ // _moz_resizing attribute?
+ aResizedElement->SetAttribute(NS_LITERAL_STRING("_moz_resizing"), NS_LITERAL_STRING("true"));
+ return rv;
+}
+
+NS_IMETHODIMP
+HTMLEditor::HideResizers()
+{
+ NS_ENSURE_TRUE(mResizedObject, NS_OK);
+
+ // get the presshell's document observer interface.
+ nsCOMPtr<nsIPresShell> ps = GetPresShell();
+ // We allow the pres shell to be null; when it is, we presume there
+ // are no document observers to notify, but we still want to
+ // UnbindFromTree.
+
+ nsCOMPtr<nsIContent> parentContent;
+
+ if (mTopLeftHandle) {
+ parentContent = mTopLeftHandle->GetParent();
+ }
+
+ NS_NAMED_LITERAL_STRING(mousedown, "mousedown");
+
+ RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
+ mTopLeftHandle, parentContent, ps);
+ mTopLeftHandle = nullptr;
+
+ RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
+ mTopHandle, parentContent, ps);
+ mTopHandle = nullptr;
+
+ RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
+ mTopRightHandle, parentContent, ps);
+ mTopRightHandle = nullptr;
+
+ RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
+ mLeftHandle, parentContent, ps);
+ mLeftHandle = nullptr;
+
+ RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
+ mRightHandle, parentContent, ps);
+ mRightHandle = nullptr;
+
+ RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
+ mBottomLeftHandle, parentContent, ps);
+ mBottomLeftHandle = nullptr;
+
+ RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
+ mBottomHandle, parentContent, ps);
+ mBottomHandle = nullptr;
+
+ RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
+ mBottomRightHandle, parentContent, ps);
+ mBottomRightHandle = nullptr;
+
+ RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
+ mResizingShadow, parentContent, ps);
+ mResizingShadow = nullptr;
+
+ RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
+ mResizingInfo, parentContent, ps);
+ mResizingInfo = nullptr;
+
+ if (mActivatedHandle) {
+ mActivatedHandle->UnsetAttr(kNameSpaceID_None, nsGkAtoms::_moz_activated,
+ true);
+ mActivatedHandle = nullptr;
+ }
+
+ // don't forget to remove the listeners !
+
+ nsCOMPtr<nsIDOMEventTarget> target = GetDOMEventTarget();
+
+ if (target && mMouseMotionListenerP) {
+ DebugOnly<nsresult> rv =
+ target->RemoveEventListener(NS_LITERAL_STRING("mousemove"),
+ mMouseMotionListenerP, true);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to remove mouse motion listener");
+ }
+ mMouseMotionListenerP = nullptr;
+
+ nsCOMPtr<nsIDocument> doc = GetDocument();
+ if (!doc) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ target = do_QueryInterface(doc->GetWindow());
+ if (!target) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ if (mResizeEventListenerP) {
+ DebugOnly<nsresult> rv =
+ target->RemoveEventListener(NS_LITERAL_STRING("resize"),
+ mResizeEventListenerP, false);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to remove resize event listener");
+ }
+ mResizeEventListenerP = nullptr;
+
+ mResizedObject->UnsetAttr(kNameSpaceID_None, nsGkAtoms::_moz_resizing, true);
+ mResizedObject = nullptr;
+
+ return NS_OK;
+}
+
+void
+HTMLEditor::HideShadowAndInfo()
+{
+ if (mResizingShadow) {
+ mResizingShadow->SetAttr(kNameSpaceID_None, nsGkAtoms::_class,
+ NS_LITERAL_STRING("hidden"), true);
+ }
+ if (mResizingInfo) {
+ mResizingInfo->SetAttr(kNameSpaceID_None, nsGkAtoms::_class,
+ NS_LITERAL_STRING("hidden"), true);
+ }
+}
+
+nsresult
+HTMLEditor::StartResizing(nsIDOMElement* aHandle)
+{
+ // First notify the listeners if any
+ for (auto& listener : mObjectResizeEventListeners) {
+ listener->OnStartResizing(static_cast<nsIDOMElement*>(GetAsDOMNode(mResizedObject)));
+ }
+
+ mIsResizing = true;
+ mActivatedHandle = do_QueryInterface(aHandle);
+ NS_ENSURE_STATE(mActivatedHandle || !aHandle);
+ mActivatedHandle->SetAttr(kNameSpaceID_None, nsGkAtoms::_moz_activated,
+ NS_LITERAL_STRING("true"), true);
+
+ // do we want to preserve ratio or not?
+ bool preserveRatio = HTMLEditUtils::IsImage(mResizedObject) &&
+ Preferences::GetBool("editor.resizing.preserve_ratio", true);
+
+ // the way we change the position/size of the shadow depends on
+ // the handle
+ nsAutoString locationStr;
+ aHandle->GetAttribute(NS_LITERAL_STRING("anonlocation"), locationStr);
+ if (locationStr.Equals(kTopLeft)) {
+ SetResizeIncrements(1, 1, -1, -1, preserveRatio);
+ } else if (locationStr.Equals(kTop)) {
+ SetResizeIncrements(0, 1, 0, -1, false);
+ } else if (locationStr.Equals(kTopRight)) {
+ SetResizeIncrements(0, 1, 1, -1, preserveRatio);
+ } else if (locationStr.Equals(kLeft)) {
+ SetResizeIncrements(1, 0, -1, 0, false);
+ } else if (locationStr.Equals(kRight)) {
+ SetResizeIncrements(0, 0, 1, 0, false);
+ } else if (locationStr.Equals(kBottomLeft)) {
+ SetResizeIncrements(1, 0, -1, 1, preserveRatio);
+ } else if (locationStr.Equals(kBottom)) {
+ SetResizeIncrements(0, 0, 0, 1, false);
+ } else if (locationStr.Equals(kBottomRight)) {
+ SetResizeIncrements(0, 0, 1, 1, preserveRatio);
+ }
+
+ // make the shadow appear
+ mResizingShadow->UnsetAttr(kNameSpaceID_None, nsGkAtoms::_class, true);
+
+ // position it
+ mCSSEditUtils->SetCSSPropertyPixels(*mResizingShadow, *nsGkAtoms::width,
+ mResizedObjectWidth);
+ mCSSEditUtils->SetCSSPropertyPixels(*mResizingShadow, *nsGkAtoms::height,
+ mResizedObjectHeight);
+
+ // add a mouse move listener to the editor
+ nsresult result = NS_OK;
+ if (!mMouseMotionListenerP) {
+ mMouseMotionListenerP = new ResizerMouseMotionListener(this);
+ if (!mMouseMotionListenerP) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ nsCOMPtr<nsIDOMEventTarget> target = GetDOMEventTarget();
+ NS_ENSURE_TRUE(target, NS_ERROR_FAILURE);
+
+ result = target->AddEventListener(NS_LITERAL_STRING("mousemove"),
+ mMouseMotionListenerP, true);
+ NS_ASSERTION(NS_SUCCEEDED(result),
+ "failed to register mouse motion listener");
+ }
+ return result;
+}
+
+NS_IMETHODIMP
+HTMLEditor::MouseDown(int32_t aClientX,
+ int32_t aClientY,
+ nsIDOMElement* aTarget,
+ nsIDOMEvent* aEvent)
+{
+ bool anonElement = false;
+ if (aTarget && NS_SUCCEEDED(aTarget->HasAttribute(NS_LITERAL_STRING("_moz_anonclass"), &anonElement)))
+ // we caught a click on an anonymous element
+ if (anonElement) {
+ nsAutoString anonclass;
+ nsresult rv =
+ aTarget->GetAttribute(NS_LITERAL_STRING("_moz_anonclass"), anonclass);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (anonclass.EqualsLiteral("mozResizer")) {
+ // and that element is a resizer, let's start resizing!
+ aEvent->PreventDefault();
+
+ mOriginalX = aClientX;
+ mOriginalY = aClientY;
+ return StartResizing(aTarget);
+ }
+ if (anonclass.EqualsLiteral("mozGrabber")) {
+ // and that element is a grabber, let's start moving the element!
+ mOriginalX = aClientX;
+ mOriginalY = aClientY;
+ return GrabberClicked();
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::MouseUp(int32_t aClientX,
+ int32_t aClientY,
+ nsIDOMElement* aTarget)
+{
+ if (mIsResizing) {
+ // we are resizing and release the mouse button, so let's
+ // end the resizing process
+ mIsResizing = false;
+ HideShadowAndInfo();
+ SetFinalSize(aClientX, aClientY);
+ } else if (mIsMoving || mGrabberClicked) {
+ if (mIsMoving) {
+ mPositioningShadow->SetAttr(kNameSpaceID_None, nsGkAtoms::_class,
+ NS_LITERAL_STRING("hidden"), true);
+ SetFinalPosition(aClientX, aClientY);
+ }
+ if (mGrabberClicked) {
+ EndMoving();
+ }
+ }
+ return NS_OK;
+}
+
+void
+HTMLEditor::SetResizeIncrements(int32_t aX,
+ int32_t aY,
+ int32_t aW,
+ int32_t aH,
+ bool aPreserveRatio)
+{
+ mXIncrementFactor = aX;
+ mYIncrementFactor = aY;
+ mWidthIncrementFactor = aW;
+ mHeightIncrementFactor = aH;
+ mPreserveRatio = aPreserveRatio;
+}
+
+nsresult
+HTMLEditor::SetResizingInfoPosition(int32_t aX,
+ int32_t aY,
+ int32_t aW,
+ int32_t aH)
+{
+ nsCOMPtr<nsIDOMDocument> domdoc = GetDOMDocument();
+
+ // Determine the position of the resizing info box based upon the new
+ // position and size of the element (aX, aY, aW, aH), and which
+ // resizer is the "activated handle". For example, place the resizing
+ // info box at the bottom-right corner of the new element, if the element
+ // is being resized by the bottom-right resizer.
+ int32_t infoXPosition;
+ int32_t infoYPosition;
+
+ if (mActivatedHandle == mTopLeftHandle ||
+ mActivatedHandle == mLeftHandle ||
+ mActivatedHandle == mBottomLeftHandle) {
+ infoXPosition = aX;
+ } else if (mActivatedHandle == mTopHandle ||
+ mActivatedHandle == mBottomHandle) {
+ infoXPosition = aX + (aW / 2);
+ } else {
+ // should only occur when mActivatedHandle is one of the 3 right-side
+ // handles, but this is a reasonable default if it isn't any of them (?)
+ infoXPosition = aX + aW;
+ }
+
+ if (mActivatedHandle == mTopLeftHandle ||
+ mActivatedHandle == mTopHandle ||
+ mActivatedHandle == mTopRightHandle) {
+ infoYPosition = aY;
+ } else if (mActivatedHandle == mLeftHandle ||
+ mActivatedHandle == mRightHandle) {
+ infoYPosition = aY + (aH / 2);
+ } else {
+ // should only occur when mActivatedHandle is one of the 3 bottom-side
+ // handles, but this is a reasonable default if it isn't any of them (?)
+ infoYPosition = aY + aH;
+ }
+
+ // Offset info box by 20 so it's not directly under the mouse cursor.
+ const int mouseCursorOffset = 20;
+ mCSSEditUtils->SetCSSPropertyPixels(*mResizingInfo, *nsGkAtoms::left,
+ infoXPosition + mouseCursorOffset);
+ mCSSEditUtils->SetCSSPropertyPixels(*mResizingInfo, *nsGkAtoms::top,
+ infoYPosition + mouseCursorOffset);
+
+ nsCOMPtr<nsIContent> textInfo = mResizingInfo->GetFirstChild();
+ ErrorResult erv;
+ if (textInfo) {
+ mResizingInfo->RemoveChild(*textInfo, erv);
+ NS_ENSURE_TRUE(!erv.Failed(), erv.StealNSResult());
+ textInfo = nullptr;
+ }
+
+ nsAutoString widthStr, heightStr, diffWidthStr, diffHeightStr;
+ widthStr.AppendInt(aW);
+ heightStr.AppendInt(aH);
+ int32_t diffWidth = aW - mResizedObjectWidth;
+ int32_t diffHeight = aH - mResizedObjectHeight;
+ if (diffWidth > 0) {
+ diffWidthStr.Assign('+');
+ }
+ if (diffHeight > 0) {
+ diffHeightStr.Assign('+');
+ }
+ diffWidthStr.AppendInt(diffWidth);
+ diffHeightStr.AppendInt(diffHeight);
+
+ nsAutoString info(widthStr + NS_LITERAL_STRING(" x ") + heightStr +
+ NS_LITERAL_STRING(" (") + diffWidthStr +
+ NS_LITERAL_STRING(", ") + diffHeightStr +
+ NS_LITERAL_STRING(")"));
+
+ nsCOMPtr<nsIDOMText> nodeAsText;
+ nsresult rv = domdoc->CreateTextNode(info, getter_AddRefs(nodeAsText));
+ NS_ENSURE_SUCCESS(rv, rv);
+ textInfo = do_QueryInterface(nodeAsText);
+ mResizingInfo->AppendChild(*textInfo, erv);
+ NS_ENSURE_TRUE(!erv.Failed(), erv.StealNSResult());
+
+ return mResizingInfo->UnsetAttr(kNameSpaceID_None, nsGkAtoms::_class, true);
+}
+
+nsresult
+HTMLEditor::SetShadowPosition(Element* aShadow,
+ Element* aOriginalObject,
+ int32_t aOriginalObjectX,
+ int32_t aOriginalObjectY)
+{
+ SetAnonymousElementPosition(aOriginalObjectX, aOriginalObjectY, static_cast<nsIDOMElement*>(GetAsDOMNode(aShadow)));
+
+ if (HTMLEditUtils::IsImage(aOriginalObject)) {
+ nsAutoString imageSource;
+ aOriginalObject->GetAttr(kNameSpaceID_None, nsGkAtoms::src, imageSource);
+ nsresult rv = aShadow->SetAttr(kNameSpaceID_None, nsGkAtoms::src,
+ imageSource, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+int32_t
+HTMLEditor::GetNewResizingIncrement(int32_t aX,
+ int32_t aY,
+ int32_t aID)
+{
+ int32_t result = 0;
+ if (!mPreserveRatio) {
+ switch (aID) {
+ case kX:
+ case kWidth:
+ result = aX - mOriginalX;
+ break;
+ case kY:
+ case kHeight:
+ result = aY - mOriginalY;
+ break;
+ }
+ return result;
+ }
+
+ int32_t xi = (aX - mOriginalX) * mWidthIncrementFactor;
+ int32_t yi = (aY - mOriginalY) * mHeightIncrementFactor;
+ float objectSizeRatio =
+ ((float)mResizedObjectWidth) / ((float)mResizedObjectHeight);
+ result = (xi > yi) ? xi : yi;
+ switch (aID) {
+ case kX:
+ case kWidth:
+ if (result == yi)
+ result = (int32_t) (((float) result) * objectSizeRatio);
+ result = (int32_t) (((float) result) * mWidthIncrementFactor);
+ break;
+ case kY:
+ case kHeight:
+ if (result == xi)
+ result = (int32_t) (((float) result) / objectSizeRatio);
+ result = (int32_t) (((float) result) * mHeightIncrementFactor);
+ break;
+ }
+ return result;
+}
+
+int32_t
+HTMLEditor::GetNewResizingX(int32_t aX,
+ int32_t aY)
+{
+ int32_t resized = mResizedObjectX +
+ GetNewResizingIncrement(aX, aY, kX) * mXIncrementFactor;
+ int32_t max = mResizedObjectX + mResizedObjectWidth;
+ return std::min(resized, max);
+}
+
+int32_t
+HTMLEditor::GetNewResizingY(int32_t aX,
+ int32_t aY)
+{
+ int32_t resized = mResizedObjectY +
+ GetNewResizingIncrement(aX, aY, kY) * mYIncrementFactor;
+ int32_t max = mResizedObjectY + mResizedObjectHeight;
+ return std::min(resized, max);
+}
+
+int32_t
+HTMLEditor::GetNewResizingWidth(int32_t aX,
+ int32_t aY)
+{
+ int32_t resized = mResizedObjectWidth +
+ GetNewResizingIncrement(aX, aY, kWidth) *
+ mWidthIncrementFactor;
+ return std::max(resized, 1);
+}
+
+int32_t
+HTMLEditor::GetNewResizingHeight(int32_t aX,
+ int32_t aY)
+{
+ int32_t resized = mResizedObjectHeight +
+ GetNewResizingIncrement(aX, aY, kHeight) *
+ mHeightIncrementFactor;
+ return std::max(resized, 1);
+}
+
+NS_IMETHODIMP
+HTMLEditor::MouseMove(nsIDOMEvent* aMouseEvent)
+{
+ NS_NAMED_LITERAL_STRING(leftStr, "left");
+ NS_NAMED_LITERAL_STRING(topStr, "top");
+
+ if (mIsResizing) {
+ // we are resizing and the mouse pointer's position has changed
+ // we have to resdisplay the shadow
+ nsCOMPtr<nsIDOMMouseEvent> mouseEvent ( do_QueryInterface(aMouseEvent) );
+ int32_t clientX, clientY;
+ mouseEvent->GetClientX(&clientX);
+ mouseEvent->GetClientY(&clientY);
+
+ int32_t newX = GetNewResizingX(clientX, clientY);
+ int32_t newY = GetNewResizingY(clientX, clientY);
+ int32_t newWidth = GetNewResizingWidth(clientX, clientY);
+ int32_t newHeight = GetNewResizingHeight(clientX, clientY);
+
+ mCSSEditUtils->SetCSSPropertyPixels(*mResizingShadow, *nsGkAtoms::left,
+ newX);
+ mCSSEditUtils->SetCSSPropertyPixels(*mResizingShadow, *nsGkAtoms::top,
+ newY);
+ mCSSEditUtils->SetCSSPropertyPixels(*mResizingShadow, *nsGkAtoms::width,
+ newWidth);
+ mCSSEditUtils->SetCSSPropertyPixels(*mResizingShadow, *nsGkAtoms::height,
+ newHeight);
+
+ return SetResizingInfoPosition(newX, newY, newWidth, newHeight);
+ }
+
+ if (mGrabberClicked) {
+ nsCOMPtr<nsIDOMMouseEvent> mouseEvent ( do_QueryInterface(aMouseEvent) );
+ int32_t clientX, clientY;
+ mouseEvent->GetClientX(&clientX);
+ mouseEvent->GetClientY(&clientY);
+
+ int32_t xThreshold =
+ LookAndFeel::GetInt(LookAndFeel::eIntID_DragThresholdX, 1);
+ int32_t yThreshold =
+ LookAndFeel::GetInt(LookAndFeel::eIntID_DragThresholdY, 1);
+
+ if (DeprecatedAbs(clientX - mOriginalX) * 2 >= xThreshold ||
+ DeprecatedAbs(clientY - mOriginalY) * 2 >= yThreshold) {
+ mGrabberClicked = false;
+ StartMoving(nullptr);
+ }
+ }
+ if (mIsMoving) {
+ nsCOMPtr<nsIDOMMouseEvent> mouseEvent ( do_QueryInterface(aMouseEvent) );
+ int32_t clientX, clientY;
+ mouseEvent->GetClientX(&clientX);
+ mouseEvent->GetClientY(&clientY);
+
+ int32_t newX = mPositionedObjectX + clientX - mOriginalX;
+ int32_t newY = mPositionedObjectY + clientY - mOriginalY;
+
+ SnapToGrid(newX, newY);
+
+ mCSSEditUtils->SetCSSPropertyPixels(*mPositioningShadow, *nsGkAtoms::left,
+ newX);
+ mCSSEditUtils->SetCSSPropertyPixels(*mPositioningShadow, *nsGkAtoms::top,
+ newY);
+ }
+ return NS_OK;
+}
+
+void
+HTMLEditor::SetFinalSize(int32_t aX,
+ int32_t aY)
+{
+ if (!mResizedObject) {
+ // paranoia
+ return;
+ }
+
+ if (mActivatedHandle) {
+ mActivatedHandle->UnsetAttr(kNameSpaceID_None, nsGkAtoms::_moz_activated, true);
+ mActivatedHandle = nullptr;
+ }
+
+ // we have now to set the new width and height of the resized object
+ // we don't set the x and y position because we don't control that in
+ // a normal HTML layout
+ int32_t left = GetNewResizingX(aX, aY);
+ int32_t top = GetNewResizingY(aX, aY);
+ int32_t width = GetNewResizingWidth(aX, aY);
+ int32_t height = GetNewResizingHeight(aX, aY);
+ bool setWidth = !mResizedObjectIsAbsolutelyPositioned || (width != mResizedObjectWidth);
+ bool setHeight = !mResizedObjectIsAbsolutelyPositioned || (height != mResizedObjectHeight);
+
+ int32_t x, y;
+ x = left - ((mResizedObjectIsAbsolutelyPositioned) ? mResizedObjectBorderLeft+mResizedObjectMarginLeft : 0);
+ y = top - ((mResizedObjectIsAbsolutelyPositioned) ? mResizedObjectBorderTop+mResizedObjectMarginTop : 0);
+
+ // we want one transaction only from a user's point of view
+ AutoEditBatch batchIt(this);
+
+ NS_NAMED_LITERAL_STRING(widthStr, "width");
+ NS_NAMED_LITERAL_STRING(heightStr, "height");
+
+ nsCOMPtr<Element> resizedObject = do_QueryInterface(mResizedObject);
+ NS_ENSURE_TRUE(resizedObject, );
+ if (mResizedObjectIsAbsolutelyPositioned) {
+ if (setHeight) {
+ mCSSEditUtils->SetCSSPropertyPixels(*resizedObject, *nsGkAtoms::top, y);
+ }
+ if (setWidth) {
+ mCSSEditUtils->SetCSSPropertyPixels(*resizedObject, *nsGkAtoms::left, x);
+ }
+ }
+ if (IsCSSEnabled() || mResizedObjectIsAbsolutelyPositioned) {
+ if (setWidth && mResizedObject->HasAttr(kNameSpaceID_None, nsGkAtoms::width)) {
+ RemoveAttribute(static_cast<nsIDOMElement*>(GetAsDOMNode(mResizedObject)), widthStr);
+ }
+
+ if (setHeight && mResizedObject->HasAttr(kNameSpaceID_None,
+ nsGkAtoms::height)) {
+ RemoveAttribute(static_cast<nsIDOMElement*>(GetAsDOMNode(mResizedObject)), heightStr);
+ }
+
+ if (setWidth) {
+ mCSSEditUtils->SetCSSPropertyPixels(*resizedObject, *nsGkAtoms::width,
+ width);
+ }
+ if (setHeight) {
+ mCSSEditUtils->SetCSSPropertyPixels(*resizedObject, *nsGkAtoms::height,
+ height);
+ }
+ } else {
+ // we use HTML size and remove all equivalent CSS properties
+
+ // we set the CSS width and height to remove it later,
+ // triggering an immediate reflow; otherwise, we have problems
+ // with asynchronous reflow
+ if (setWidth) {
+ mCSSEditUtils->SetCSSPropertyPixels(*resizedObject, *nsGkAtoms::width,
+ width);
+ }
+ if (setHeight) {
+ mCSSEditUtils->SetCSSPropertyPixels(*resizedObject, *nsGkAtoms::height,
+ height);
+ }
+ if (setWidth) {
+ nsAutoString w;
+ w.AppendInt(width);
+ SetAttribute(static_cast<nsIDOMElement*>(GetAsDOMNode(mResizedObject)), widthStr, w);
+ }
+ if (setHeight) {
+ nsAutoString h;
+ h.AppendInt(height);
+ SetAttribute(static_cast<nsIDOMElement*>(GetAsDOMNode(mResizedObject)), heightStr, h);
+ }
+
+ if (setWidth) {
+ mCSSEditUtils->RemoveCSSProperty(*resizedObject, *nsGkAtoms::width,
+ EmptyString());
+ }
+ if (setHeight) {
+ mCSSEditUtils->RemoveCSSProperty(*resizedObject, *nsGkAtoms::height,
+ EmptyString());
+ }
+ }
+ // finally notify the listeners if any
+ for (auto& listener : mObjectResizeEventListeners) {
+ listener->OnEndResizing(static_cast<nsIDOMElement*>(GetAsDOMNode(mResizedObject)),
+ mResizedObjectWidth, mResizedObjectHeight, width,
+ height);
+ }
+
+ // keep track of that size
+ mResizedObjectWidth = width;
+ mResizedObjectHeight = height;
+
+ RefreshResizers();
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetResizedObject(nsIDOMElement** aResizedObject)
+{
+ nsCOMPtr<nsIDOMElement> ret = static_cast<nsIDOMElement*>(GetAsDOMNode(mResizedObject));
+ ret.forget(aResizedObject);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetObjectResizingEnabled(bool* aIsObjectResizingEnabled)
+{
+ *aIsObjectResizingEnabled = mIsObjectResizingEnabled;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::SetObjectResizingEnabled(bool aObjectResizingEnabled)
+{
+ mIsObjectResizingEnabled = aObjectResizingEnabled;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::AddObjectResizeEventListener(nsIHTMLObjectResizeListener* aListener)
+{
+ NS_ENSURE_ARG_POINTER(aListener);
+ if (mObjectResizeEventListeners.Contains(aListener)) {
+ /* listener already registered */
+ NS_ASSERTION(false,
+ "trying to register an already registered object resize event listener");
+ return NS_OK;
+ }
+ mObjectResizeEventListeners.AppendElement(*aListener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::RemoveObjectResizeEventListener(
+ nsIHTMLObjectResizeListener* aListener)
+{
+ NS_ENSURE_ARG_POINTER(aListener);
+ if (!mObjectResizeEventListeners.Contains(aListener)) {
+ /* listener was not registered */
+ NS_ASSERTION(false,
+ "trying to remove an object resize event listener that was not already registered");
+ return NS_OK;
+ }
+ mObjectResizeEventListeners.RemoveElement(aListener);
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/HTMLEditorObjectResizerUtils.h b/editor/libeditor/HTMLEditorObjectResizerUtils.h
new file mode 100644
index 000000000..2bebbe76b
--- /dev/null
+++ b/editor/libeditor/HTMLEditorObjectResizerUtils.h
@@ -0,0 +1,82 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef HTMLEditorObjectResizerUtils_h
+#define HTMLEditorObjectResizerUtils_h
+
+#include "nsIDOMEventListener.h"
+#include "nsISelectionListener.h"
+#include "nsISupportsImpl.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsLiteralString.h"
+
+class nsIHTMLEditor;
+
+namespace mozilla {
+
+#define kTopLeft NS_LITERAL_STRING("nw")
+#define kTop NS_LITERAL_STRING("n")
+#define kTopRight NS_LITERAL_STRING("ne")
+#define kLeft NS_LITERAL_STRING("w")
+#define kRight NS_LITERAL_STRING("e")
+#define kBottomLeft NS_LITERAL_STRING("sw")
+#define kBottom NS_LITERAL_STRING("s")
+#define kBottomRight NS_LITERAL_STRING("se")
+
+/******************************************************************************
+ * mozilla::ResizerSelectionListener
+ ******************************************************************************/
+
+class ResizerSelectionListener final : public nsISelectionListener
+{
+public:
+ explicit ResizerSelectionListener(nsIHTMLEditor* aEditor);
+ void Reset();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISELECTIONLISTENER
+
+protected:
+ virtual ~ResizerSelectionListener() {}
+ nsWeakPtr mEditor;
+};
+
+/******************************************************************************
+ * mozilla::ResizerMouseMotionListener
+ ******************************************************************************/
+
+class ResizerMouseMotionListener final : public nsIDOMEventListener
+{
+public:
+ explicit ResizerMouseMotionListener(nsIHTMLEditor* aEditor);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIDOMEVENTLISTENER
+
+protected:
+ virtual ~ResizerMouseMotionListener() {}
+ nsWeakPtr mEditor;
+};
+
+/******************************************************************************
+ * mozilla::DocumentResizeEventListener
+ ******************************************************************************/
+
+class DocumentResizeEventListener final : public nsIDOMEventListener
+{
+public:
+ explicit DocumentResizeEventListener(nsIHTMLEditor* aEditor);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIDOMEVENTLISTENER
+
+protected:
+ virtual ~DocumentResizeEventListener() {}
+ nsWeakPtr mEditor;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef HTMLEditorObjectResizerUtils_h
diff --git a/editor/libeditor/HTMLInlineTableEditor.cpp b/editor/libeditor/HTMLInlineTableEditor.cpp
new file mode 100644
index 000000000..3da1876ec
--- /dev/null
+++ b/editor/libeditor/HTMLInlineTableEditor.cpp
@@ -0,0 +1,283 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/HTMLEditor.h"
+
+#include "HTMLEditUtils.h"
+#include "mozilla/dom/Element.h"
+#include "nsAString.h"
+#include "nsCOMPtr.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsIContent.h"
+#include "nsIDOMElement.h"
+#include "nsIDOMEventTarget.h"
+#include "nsIDOMHTMLElement.h"
+#include "nsIDOMNode.h"
+#include "nsIHTMLObjectResizer.h"
+#include "nsIPresShell.h"
+#include "nsLiteralString.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nscore.h"
+
+namespace mozilla {
+
+// Uncomment the following line if you want to disable
+// table deletion when the only column/row is removed
+// #define DISABLE_TABLE_DELETION 1
+
+NS_IMETHODIMP
+HTMLEditor::SetInlineTableEditingEnabled(bool aIsEnabled)
+{
+ mIsInlineTableEditingEnabled = aIsEnabled;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetInlineTableEditingEnabled(bool* aIsEnabled)
+{
+ *aIsEnabled = mIsInlineTableEditingEnabled;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::ShowInlineTableEditingUI(nsIDOMElement* aCell)
+{
+ NS_ENSURE_ARG_POINTER(aCell);
+
+ // do nothing if aCell is not a table cell...
+ nsCOMPtr<Element> cell = do_QueryInterface(aCell);
+ if (!cell || !HTMLEditUtils::IsTableCell(cell)) {
+ return NS_OK;
+ }
+
+ if (NS_WARN_IF(!IsDescendantOfEditorRoot(cell))) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (mInlineEditedCell) {
+ NS_ERROR("call HideInlineTableEditingUI first");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ // the resizers and the shadow will be anonymous children of the body
+ nsCOMPtr<nsIDOMElement> bodyElement = do_QueryInterface(GetRoot());
+ NS_ENSURE_TRUE(bodyElement, NS_ERROR_NULL_POINTER);
+
+ CreateAnonymousElement(NS_LITERAL_STRING("a"), bodyElement,
+ NS_LITERAL_STRING("mozTableAddColumnBefore"),
+ false, getter_AddRefs(mAddColumnBeforeButton));
+ CreateAnonymousElement(NS_LITERAL_STRING("a"), bodyElement,
+ NS_LITERAL_STRING("mozTableRemoveColumn"),
+ false, getter_AddRefs(mRemoveColumnButton));
+ CreateAnonymousElement(NS_LITERAL_STRING("a"), bodyElement,
+ NS_LITERAL_STRING("mozTableAddColumnAfter"),
+ false, getter_AddRefs(mAddColumnAfterButton));
+
+ CreateAnonymousElement(NS_LITERAL_STRING("a"), bodyElement,
+ NS_LITERAL_STRING("mozTableAddRowBefore"),
+ false, getter_AddRefs(mAddRowBeforeButton));
+ CreateAnonymousElement(NS_LITERAL_STRING("a"), bodyElement,
+ NS_LITERAL_STRING("mozTableRemoveRow"),
+ false, getter_AddRefs(mRemoveRowButton));
+ CreateAnonymousElement(NS_LITERAL_STRING("a"), bodyElement,
+ NS_LITERAL_STRING("mozTableAddRowAfter"),
+ false, getter_AddRefs(mAddRowAfterButton));
+
+ AddMouseClickListener(mAddColumnBeforeButton);
+ AddMouseClickListener(mRemoveColumnButton);
+ AddMouseClickListener(mAddColumnAfterButton);
+ AddMouseClickListener(mAddRowBeforeButton);
+ AddMouseClickListener(mRemoveRowButton);
+ AddMouseClickListener(mAddRowAfterButton);
+
+ mInlineEditedCell = aCell;
+ return RefreshInlineTableEditingUI();
+}
+
+NS_IMETHODIMP
+HTMLEditor::HideInlineTableEditingUI()
+{
+ mInlineEditedCell = nullptr;
+
+ RemoveMouseClickListener(mAddColumnBeforeButton);
+ RemoveMouseClickListener(mRemoveColumnButton);
+ RemoveMouseClickListener(mAddColumnAfterButton);
+ RemoveMouseClickListener(mAddRowBeforeButton);
+ RemoveMouseClickListener(mRemoveRowButton);
+ RemoveMouseClickListener(mAddRowAfterButton);
+
+ // get the presshell's document observer interface.
+ nsCOMPtr<nsIPresShell> ps = GetPresShell();
+ // We allow the pres shell to be null; when it is, we presume there
+ // are no document observers to notify, but we still want to
+ // UnbindFromTree.
+
+ // get the root content node.
+ nsCOMPtr<nsIContent> bodyContent = GetRoot();
+
+ DeleteRefToAnonymousNode(mAddColumnBeforeButton, bodyContent, ps);
+ mAddColumnBeforeButton = nullptr;
+ DeleteRefToAnonymousNode(mRemoveColumnButton, bodyContent, ps);
+ mRemoveColumnButton = nullptr;
+ DeleteRefToAnonymousNode(mAddColumnAfterButton, bodyContent, ps);
+ mAddColumnAfterButton = nullptr;
+ DeleteRefToAnonymousNode(mAddRowBeforeButton, bodyContent, ps);
+ mAddRowBeforeButton = nullptr;
+ DeleteRefToAnonymousNode(mRemoveRowButton, bodyContent, ps);
+ mRemoveRowButton = nullptr;
+ DeleteRefToAnonymousNode(mAddRowAfterButton, bodyContent, ps);
+ mAddRowAfterButton = nullptr;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::DoInlineTableEditingAction(nsIDOMElement* aElement)
+{
+ NS_ENSURE_ARG_POINTER(aElement);
+ bool anonElement = false;
+ if (aElement &&
+ NS_SUCCEEDED(aElement->HasAttribute(NS_LITERAL_STRING("_moz_anonclass"), &anonElement)) &&
+ anonElement) {
+ nsAutoString anonclass;
+ nsresult rv =
+ aElement->GetAttribute(NS_LITERAL_STRING("_moz_anonclass"), anonclass);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!StringBeginsWith(anonclass, NS_LITERAL_STRING("mozTable")))
+ return NS_OK;
+
+ nsCOMPtr<nsIDOMNode> tableNode = GetEnclosingTable(mInlineEditedCell);
+ nsCOMPtr<nsIDOMElement> tableElement = do_QueryInterface(tableNode);
+ int32_t rowCount, colCount;
+ rv = GetTableSize(tableElement, &rowCount, &colCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hideUI = false;
+ bool hideResizersWithInlineTableUI = (GetAsDOMNode(mResizedObject) == tableElement);
+
+ if (anonclass.EqualsLiteral("mozTableAddColumnBefore"))
+ InsertTableColumn(1, false);
+ else if (anonclass.EqualsLiteral("mozTableAddColumnAfter"))
+ InsertTableColumn(1, true);
+ else if (anonclass.EqualsLiteral("mozTableAddRowBefore"))
+ InsertTableRow(1, false);
+ else if (anonclass.EqualsLiteral("mozTableAddRowAfter"))
+ InsertTableRow(1, true);
+ else if (anonclass.EqualsLiteral("mozTableRemoveColumn")) {
+ DeleteTableColumn(1);
+#ifndef DISABLE_TABLE_DELETION
+ hideUI = (colCount == 1);
+#endif
+ }
+ else if (anonclass.EqualsLiteral("mozTableRemoveRow")) {
+ DeleteTableRow(1);
+#ifndef DISABLE_TABLE_DELETION
+ hideUI = (rowCount == 1);
+#endif
+ }
+ else
+ return NS_OK;
+
+ if (hideUI) {
+ HideInlineTableEditingUI();
+ if (hideResizersWithInlineTableUI)
+ HideResizers();
+ }
+ }
+
+ return NS_OK;
+}
+
+void
+HTMLEditor::AddMouseClickListener(nsIDOMElement* aElement)
+{
+ nsCOMPtr<nsIDOMEventTarget> evtTarget(do_QueryInterface(aElement));
+ if (evtTarget) {
+ evtTarget->AddEventListener(NS_LITERAL_STRING("click"),
+ mEventListener, true);
+ }
+}
+
+void
+HTMLEditor::RemoveMouseClickListener(nsIDOMElement* aElement)
+{
+ nsCOMPtr<nsIDOMEventTarget> evtTarget(do_QueryInterface(aElement));
+ if (evtTarget) {
+ evtTarget->RemoveEventListener(NS_LITERAL_STRING("click"),
+ mEventListener, true);
+ }
+}
+
+NS_IMETHODIMP
+HTMLEditor::RefreshInlineTableEditingUI()
+{
+ nsCOMPtr<nsIDOMHTMLElement> htmlElement = do_QueryInterface(mInlineEditedCell);
+ if (!htmlElement) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ int32_t xCell, yCell, wCell, hCell;
+ GetElementOrigin(mInlineEditedCell, xCell, yCell);
+
+ nsresult rv = htmlElement->GetOffsetWidth(&wCell);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = htmlElement->GetOffsetHeight(&hCell);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t xHoriz = xCell + wCell/2;
+ int32_t yVert = yCell + hCell/2;
+
+ nsCOMPtr<nsIDOMNode> tableNode = GetEnclosingTable(mInlineEditedCell);
+ nsCOMPtr<nsIDOMElement> tableElement = do_QueryInterface(tableNode);
+ int32_t rowCount, colCount;
+ rv = GetTableSize(tableElement, &rowCount, &colCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ SetAnonymousElementPosition(xHoriz-10, yCell-7, mAddColumnBeforeButton);
+#ifdef DISABLE_TABLE_DELETION
+ NS_NAMED_LITERAL_STRING(classStr, "class");
+
+ if (colCount== 1) {
+ mRemoveColumnButton->SetAttribute(classStr,
+ NS_LITERAL_STRING("hidden"));
+ }
+ else {
+ bool hasClass = false;
+ rv = mRemoveColumnButton->HasAttribute(classStr, &hasClass);
+ if (NS_SUCCEEDED(rv) && hasClass) {
+ mRemoveColumnButton->RemoveAttribute(classStr);
+ }
+#endif
+ SetAnonymousElementPosition(xHoriz-4, yCell-7, mRemoveColumnButton);
+#ifdef DISABLE_TABLE_DELETION
+ }
+#endif
+ SetAnonymousElementPosition(xHoriz+6, yCell-7, mAddColumnAfterButton);
+
+ SetAnonymousElementPosition(xCell-7, yVert-10, mAddRowBeforeButton);
+#ifdef DISABLE_TABLE_DELETION
+ if (rowCount== 1) {
+ mRemoveRowButton->SetAttribute(classStr,
+ NS_LITERAL_STRING("hidden"));
+ }
+ else {
+ bool hasClass = false;
+ rv = mRemoveRowButton->HasAttribute(classStr, &hasClass);
+ if (NS_SUCCEEDED(rv) && hasClass) {
+ mRemoveRowButton->RemoveAttribute(classStr);
+ }
+#endif
+ SetAnonymousElementPosition(xCell-7, yVert-4, mRemoveRowButton);
+#ifdef DISABLE_TABLE_DELETION
+ }
+#endif
+ SetAnonymousElementPosition(xCell-7, yVert+6, mAddRowAfterButton);
+
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/HTMLStyleEditor.cpp b/editor/libeditor/HTMLStyleEditor.cpp
new file mode 100644
index 000000000..bc7141ad3
--- /dev/null
+++ b/editor/libeditor/HTMLStyleEditor.cpp
@@ -0,0 +1,1752 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/HTMLEditor.h"
+
+#include "HTMLEditUtils.h"
+#include "TextEditUtils.h"
+#include "TypeInState.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/EditorUtils.h"
+#include "mozilla/SelectionState.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/mozalloc.h"
+#include "nsAString.h"
+#include "nsAttrName.h"
+#include "nsCOMPtr.h"
+#include "nsCaseTreatment.h"
+#include "nsComponentManagerUtils.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsGkAtoms.h"
+#include "nsIAtom.h"
+#include "nsIContent.h"
+#include "nsIContentIterator.h"
+#include "nsIDOMElement.h"
+#include "nsIEditor.h"
+#include "nsIEditorIMESupport.h"
+#include "nsIEditRules.h"
+#include "nsNameSpaceManager.h"
+#include "nsINode.h"
+#include "nsISupportsImpl.h"
+#include "nsLiteralString.h"
+#include "nsRange.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+#include "nsUnicharUtils.h"
+#include "nscore.h"
+
+class nsISupports;
+
+namespace mozilla {
+
+using namespace dom;
+
+static bool
+IsEmptyTextNode(HTMLEditor* aThis, nsINode* aNode)
+{
+ bool isEmptyTextNode = false;
+ return EditorBase::IsTextNode(aNode) &&
+ NS_SUCCEEDED(aThis->IsEmptyNode(aNode, &isEmptyTextNode)) &&
+ isEmptyTextNode;
+}
+
+NS_IMETHODIMP
+HTMLEditor::AddDefaultProperty(nsIAtom* aProperty,
+ const nsAString& aAttribute,
+ const nsAString& aValue)
+{
+ nsString outValue;
+ int32_t index;
+ nsString attr(aAttribute);
+ if (TypeInState::FindPropInList(aProperty, attr, &outValue,
+ mDefaultStyles, index)) {
+ PropItem *item = mDefaultStyles[index];
+ item->value = aValue;
+ } else {
+ nsString value(aValue);
+ PropItem *propItem = new PropItem(aProperty, attr, value);
+ mDefaultStyles.AppendElement(propItem);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::RemoveDefaultProperty(nsIAtom* aProperty,
+ const nsAString& aAttribute,
+ const nsAString& aValue)
+{
+ nsString outValue;
+ int32_t index;
+ nsString attr(aAttribute);
+ if (TypeInState::FindPropInList(aProperty, attr, &outValue,
+ mDefaultStyles, index)) {
+ delete mDefaultStyles[index];
+ mDefaultStyles.RemoveElementAt(index);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::RemoveAllDefaultProperties()
+{
+ size_t defcon = mDefaultStyles.Length();
+ for (size_t j = 0; j < defcon; j++) {
+ delete mDefaultStyles[j];
+ }
+ mDefaultStyles.Clear();
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+HTMLEditor::SetInlineProperty(nsIAtom* aProperty,
+ const nsAString& aAttribute,
+ const nsAString& aValue)
+{
+ NS_ENSURE_TRUE(aProperty, NS_ERROR_NULL_POINTER);
+ NS_ENSURE_TRUE(mRules, NS_ERROR_NOT_INITIALIZED);
+ nsCOMPtr<nsIEditRules> rules(mRules);
+ ForceCompositionEnd();
+
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+
+ if (selection->Collapsed()) {
+ // Manipulating text attributes on a collapsed selection only sets state
+ // for the next text insertion
+ mTypeInState->SetProp(aProperty, aAttribute, aValue);
+ return NS_OK;
+ }
+
+ AutoEditBatch batchIt(this);
+ AutoRules beginRulesSniffing(this, EditAction::insertElement,
+ nsIEditor::eNext);
+ AutoSelectionRestorer selectionRestorer(selection, this);
+ AutoTransactionsConserveSelection dontSpazMySelection(this);
+
+ bool cancel, handled;
+ TextRulesInfo ruleInfo(EditAction::setTextProperty);
+ // Protect the edit rules object from dying
+ nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!cancel && !handled) {
+ // Loop through the ranges in the selection
+ uint32_t rangeCount = selection->RangeCount();
+ for (uint32_t rangeIdx = 0; rangeIdx < rangeCount; rangeIdx++) {
+ RefPtr<nsRange> range = selection->GetRangeAt(rangeIdx);
+
+ // Adjust range to include any ancestors whose children are entirely
+ // selected
+ rv = PromoteInlineRange(*range);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Check for easy case: both range endpoints in same text node
+ nsCOMPtr<nsINode> startNode = range->GetStartParent();
+ nsCOMPtr<nsINode> endNode = range->GetEndParent();
+ if (startNode && startNode == endNode && startNode->GetAsText()) {
+ rv = SetInlinePropertyOnTextNode(*startNode->GetAsText(),
+ range->StartOffset(),
+ range->EndOffset(),
+ *aProperty, &aAttribute, aValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+ continue;
+ }
+
+ // Not the easy case. Range not contained in single text node. There
+ // are up to three phases here. There are all the nodes reported by the
+ // subtree iterator to be processed. And there are potentially a
+ // starting textnode and an ending textnode which are only partially
+ // contained by the range.
+
+ // Let's handle the nodes reported by the iterator. These nodes are
+ // entirely contained in the selection range. We build up a list of them
+ // (since doing operations on the document during iteration would perturb
+ // the iterator).
+
+ OwningNonNull<nsIContentIterator> iter = NS_NewContentSubtreeIterator();
+
+ nsTArray<OwningNonNull<nsIContent>> arrayOfNodes;
+
+ // Iterate range and build up array
+ rv = iter->Init(range);
+ // Init returns an error if there are no nodes in range. This can easily
+ // happen with the subtree iterator if the selection doesn't contain any
+ // *whole* nodes.
+ if (NS_SUCCEEDED(rv)) {
+ for (; !iter->IsDone(); iter->Next()) {
+ OwningNonNull<nsINode> node = *iter->GetCurrentNode();
+
+ if (node->IsContent() && IsEditable(node)) {
+ arrayOfNodes.AppendElement(*node->AsContent());
+ }
+ }
+ }
+ // First check the start parent of the range to see if it needs to be
+ // separately handled (it does if it's a text node, due to how the
+ // subtree iterator works - it will not have reported it).
+ if (startNode && startNode->GetAsText() && IsEditable(startNode)) {
+ rv = SetInlinePropertyOnTextNode(*startNode->GetAsText(),
+ range->StartOffset(),
+ startNode->Length(), *aProperty,
+ &aAttribute, aValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Then loop through the list, set the property on each node
+ for (auto& node : arrayOfNodes) {
+ rv = SetInlinePropertyOnNode(*node, *aProperty, &aAttribute, aValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Last check the end parent of the range to see if it needs to be
+ // separately handled (it does if it's a text node, due to how the
+ // subtree iterator works - it will not have reported it).
+ if (endNode && endNode->GetAsText() && IsEditable(endNode)) {
+ rv = SetInlinePropertyOnTextNode(*endNode->GetAsText(), 0,
+ range->EndOffset(), *aProperty,
+ &aAttribute, aValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ }
+ if (!cancel) {
+ // Post-process
+ return rules->DidDoAction(selection, &ruleInfo, rv);
+ }
+ return NS_OK;
+}
+
+// Helper function for SetInlinePropertyOn*: is aNode a simple old <b>, <font>,
+// <span style="">, etc. that we can reuse instead of creating a new one?
+bool
+HTMLEditor::IsSimpleModifiableNode(nsIContent* aContent,
+ nsIAtom* aProperty,
+ const nsAString* aAttribute,
+ const nsAString* aValue)
+{
+ // aContent can be null, in which case we'll return false in a few lines
+ MOZ_ASSERT(aProperty);
+ MOZ_ASSERT_IF(aAttribute, aValue);
+
+ nsCOMPtr<dom::Element> element = do_QueryInterface(aContent);
+ if (!element) {
+ return false;
+ }
+
+ // First check for <b>, <i>, etc.
+ if (element->IsHTMLElement(aProperty) && !element->GetAttrCount() &&
+ (!aAttribute || aAttribute->IsEmpty())) {
+ return true;
+ }
+
+ // Special cases for various equivalencies: <strong>, <em>, <s>
+ if (!element->GetAttrCount() &&
+ ((aProperty == nsGkAtoms::b &&
+ element->IsHTMLElement(nsGkAtoms::strong)) ||
+ (aProperty == nsGkAtoms::i &&
+ element->IsHTMLElement(nsGkAtoms::em)) ||
+ (aProperty == nsGkAtoms::strike &&
+ element->IsHTMLElement(nsGkAtoms::s)))) {
+ return true;
+ }
+
+ // Now look for things like <font>
+ if (aAttribute && !aAttribute->IsEmpty()) {
+ nsCOMPtr<nsIAtom> atom = NS_Atomize(*aAttribute);
+ MOZ_ASSERT(atom);
+
+ nsString attrValue;
+ if (element->IsHTMLElement(aProperty) &&
+ IsOnlyAttribute(element, *aAttribute) &&
+ element->GetAttr(kNameSpaceID_None, atom, attrValue) &&
+ attrValue.Equals(*aValue, nsCaseInsensitiveStringComparator())) {
+ // This is not quite correct, because it excludes cases like
+ // <font face=000> being the same as <font face=#000000>.
+ // Property-specific handling is needed (bug 760211).
+ return true;
+ }
+ }
+
+ // No luck so far. Now we check for a <span> with a single style=""
+ // attribute that sets only the style we're looking for, if this type of
+ // style supports it
+ if (!mCSSEditUtils->IsCSSEditableProperty(element, aProperty, aAttribute) ||
+ !element->IsHTMLElement(nsGkAtoms::span) ||
+ element->GetAttrCount() != 1 ||
+ !element->HasAttr(kNameSpaceID_None, nsGkAtoms::style)) {
+ return false;
+ }
+
+ // Some CSS styles are not so simple. For instance, underline is
+ // "text-decoration: underline", which decomposes into four different text-*
+ // properties. So for now, we just create a span, add the desired style, and
+ // see if it matches.
+ nsCOMPtr<Element> newSpan = CreateHTMLContent(nsGkAtoms::span);
+ NS_ASSERTION(newSpan, "CreateHTMLContent failed");
+ NS_ENSURE_TRUE(newSpan, false);
+ mCSSEditUtils->SetCSSEquivalentToHTMLStyle(newSpan, aProperty,
+ aAttribute, aValue,
+ /*suppress transaction*/ true);
+
+ return mCSSEditUtils->ElementsSameStyle(newSpan, element);
+}
+
+nsresult
+HTMLEditor::SetInlinePropertyOnTextNode(Text& aText,
+ int32_t aStartOffset,
+ int32_t aEndOffset,
+ nsIAtom& aProperty,
+ const nsAString* aAttribute,
+ const nsAString& aValue)
+{
+ if (!aText.GetParentNode() ||
+ !CanContainTag(*aText.GetParentNode(), aProperty)) {
+ return NS_OK;
+ }
+
+ // Don't need to do anything if no characters actually selected
+ if (aStartOffset == aEndOffset) {
+ return NS_OK;
+ }
+
+ // Don't need to do anything if property already set on node
+ if (mCSSEditUtils->IsCSSEditableProperty(&aText, &aProperty, aAttribute)) {
+ // The HTML styles defined by aProperty/aAttribute have a CSS equivalence
+ // for node; let's check if it carries those CSS styles
+ if (mCSSEditUtils->IsCSSEquivalentToHTMLInlineStyleSet(&aText, &aProperty,
+ aAttribute, aValue, CSSEditUtils::eComputed)) {
+ return NS_OK;
+ }
+ } else if (IsTextPropertySetByContent(&aText, &aProperty, aAttribute,
+ &aValue)) {
+ return NS_OK;
+ }
+
+ // Do we need to split the text node?
+ ErrorResult rv;
+ RefPtr<Text> text = &aText;
+ if (uint32_t(aEndOffset) != aText.Length()) {
+ // We need to split off back of text node
+ text = SplitNode(aText, aEndOffset, rv)->GetAsText();
+ NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult());
+ }
+
+ if (aStartOffset) {
+ // We need to split off front of text node
+ SplitNode(*text, aStartOffset, rv);
+ NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult());
+ }
+
+ if (aAttribute) {
+ // Look for siblings that are correct type of node
+ nsIContent* sibling = GetPriorHTMLSibling(text);
+ if (IsSimpleModifiableNode(sibling, &aProperty, aAttribute, &aValue)) {
+ // Previous sib is already right kind of inline node; slide this over
+ return MoveNode(text, sibling, -1);
+ }
+ sibling = GetNextHTMLSibling(text);
+ if (IsSimpleModifiableNode(sibling, &aProperty, aAttribute, &aValue)) {
+ // Following sib is already right kind of inline node; slide this over
+ return MoveNode(text, sibling, 0);
+ }
+ }
+
+ // Reparent the node inside inline node with appropriate {attribute,value}
+ return SetInlinePropertyOnNode(*text, aProperty, aAttribute, aValue);
+}
+
+nsresult
+HTMLEditor::SetInlinePropertyOnNodeImpl(nsIContent& aNode,
+ nsIAtom& aProperty,
+ const nsAString* aAttribute,
+ const nsAString& aValue)
+{
+ nsCOMPtr<nsIAtom> attrAtom = aAttribute ? NS_Atomize(*aAttribute) : nullptr;
+
+ // If this is an element that can't be contained in a span, we have to
+ // recurse to its children.
+ if (!TagCanContain(*nsGkAtoms::span, aNode)) {
+ if (aNode.HasChildren()) {
+ nsTArray<OwningNonNull<nsIContent>> arrayOfNodes;
+
+ // Populate the list.
+ for (nsCOMPtr<nsIContent> child = aNode.GetFirstChild();
+ child;
+ child = child->GetNextSibling()) {
+ if (IsEditable(child) && !IsEmptyTextNode(this, child)) {
+ arrayOfNodes.AppendElement(*child);
+ }
+ }
+
+ // Then loop through the list, set the property on each node.
+ for (auto& node : arrayOfNodes) {
+ nsresult rv = SetInlinePropertyOnNode(node, aProperty, aAttribute,
+ aValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ return NS_OK;
+ }
+
+ // First check if there's an adjacent sibling we can put our node into.
+ nsCOMPtr<nsIContent> previousSibling = GetPriorHTMLSibling(&aNode);
+ nsCOMPtr<nsIContent> nextSibling = GetNextHTMLSibling(&aNode);
+ if (IsSimpleModifiableNode(previousSibling, &aProperty, aAttribute, &aValue)) {
+ nsresult rv = MoveNode(&aNode, previousSibling, -1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (IsSimpleModifiableNode(nextSibling, &aProperty, aAttribute, &aValue)) {
+ rv = JoinNodes(*previousSibling, *nextSibling);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+ }
+ if (IsSimpleModifiableNode(nextSibling, &aProperty, aAttribute, &aValue)) {
+ nsresult rv = MoveNode(&aNode, nextSibling, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+ }
+
+ // Don't need to do anything if property already set on node
+ if (mCSSEditUtils->IsCSSEditableProperty(&aNode, &aProperty, aAttribute)) {
+ if (mCSSEditUtils->IsCSSEquivalentToHTMLInlineStyleSet(
+ &aNode, &aProperty, aAttribute, aValue, CSSEditUtils::eComputed)) {
+ return NS_OK;
+ }
+ } else if (IsTextPropertySetByContent(&aNode, &aProperty,
+ aAttribute, &aValue)) {
+ return NS_OK;
+ }
+
+ bool useCSS = (IsCSSEnabled() &&
+ mCSSEditUtils->IsCSSEditableProperty(&aNode, &aProperty,
+ aAttribute)) ||
+ // bgcolor is always done using CSS
+ aAttribute->EqualsLiteral("bgcolor");
+
+ if (useCSS) {
+ nsCOMPtr<dom::Element> tmp;
+ // We only add style="" to <span>s with no attributes (bug 746515). If we
+ // don't have one, we need to make one.
+ if (aNode.IsHTMLElement(nsGkAtoms::span) &&
+ !aNode.AsElement()->GetAttrCount()) {
+ tmp = aNode.AsElement();
+ } else {
+ tmp = InsertContainerAbove(&aNode, nsGkAtoms::span);
+ NS_ENSURE_STATE(tmp);
+ }
+
+ // Add the CSS styles corresponding to the HTML style request
+ int32_t count;
+ nsresult rv =
+ mCSSEditUtils->SetCSSEquivalentToHTMLStyle(tmp->AsDOMNode(),
+ &aProperty, aAttribute,
+ &aValue, &count, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+ }
+
+ // is it already the right kind of node, but with wrong attribute?
+ if (aNode.IsHTMLElement(&aProperty)) {
+ // Just set the attribute on it.
+ nsCOMPtr<nsIDOMElement> elem = do_QueryInterface(&aNode);
+ return SetAttribute(elem, *aAttribute, aValue);
+ }
+
+ // ok, chuck it in its very own container
+ nsCOMPtr<Element> tmp = InsertContainerAbove(&aNode, &aProperty, attrAtom,
+ &aValue);
+ NS_ENSURE_STATE(tmp);
+
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::SetInlinePropertyOnNode(nsIContent& aNode,
+ nsIAtom& aProperty,
+ const nsAString* aAttribute,
+ const nsAString& aValue)
+{
+ nsCOMPtr<nsIContent> previousSibling = aNode.GetPreviousSibling(),
+ nextSibling = aNode.GetNextSibling();
+ NS_ENSURE_STATE(aNode.GetParentNode());
+ OwningNonNull<nsINode> parent = *aNode.GetParentNode();
+
+ nsresult rv = RemoveStyleInside(aNode, &aProperty, aAttribute);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aNode.GetParentNode()) {
+ // The node is still where it was
+ return SetInlinePropertyOnNodeImpl(aNode, aProperty,
+ aAttribute, aValue);
+ }
+
+ // It's vanished. Use the old siblings for reference to construct a
+ // list. But first, verify that the previous/next siblings are still
+ // where we expect them; otherwise we have to give up.
+ if ((previousSibling && previousSibling->GetParentNode() != parent) ||
+ (nextSibling && nextSibling->GetParentNode() != parent)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ nsTArray<OwningNonNull<nsIContent>> nodesToSet;
+ nsCOMPtr<nsIContent> cur = previousSibling
+ ? previousSibling->GetNextSibling() : parent->GetFirstChild();
+ for (; cur && cur != nextSibling; cur = cur->GetNextSibling()) {
+ if (IsEditable(cur)) {
+ nodesToSet.AppendElement(*cur);
+ }
+ }
+
+ for (auto& node : nodesToSet) {
+ rv = SetInlinePropertyOnNodeImpl(node, aProperty, aAttribute, aValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::SplitStyleAboveRange(nsRange* inRange,
+ nsIAtom* aProperty,
+ const nsAString* aAttribute)
+{
+ NS_ENSURE_TRUE(inRange, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsINode> startNode = inRange->GetStartParent();
+ int32_t startOffset = inRange->StartOffset();
+ nsCOMPtr<nsINode> endNode = inRange->GetEndParent();
+ int32_t endOffset = inRange->EndOffset();
+
+ nsCOMPtr<nsINode> origStartNode = startNode;
+
+ // split any matching style nodes above the start of range
+ {
+ AutoTrackDOMPoint tracker(mRangeUpdater, address_of(endNode), &endOffset);
+ nsresult rv =
+ SplitStyleAbovePoint(address_of(startNode), &startOffset, aProperty,
+ aAttribute);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // second verse, same as the first...
+ nsresult rv =
+ SplitStyleAbovePoint(address_of(endNode), &endOffset, aProperty,
+ aAttribute);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // reset the range
+ rv = inRange->SetStart(startNode, startOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return inRange->SetEnd(endNode, endOffset);
+}
+
+nsresult
+HTMLEditor::SplitStyleAbovePoint(nsCOMPtr<nsINode>* aNode,
+ int32_t* aOffset,
+ // null here means we split all properties
+ nsIAtom* aProperty,
+ const nsAString* aAttribute,
+ nsIContent** aOutLeftNode,
+ nsIContent** aOutRightNode)
+{
+ NS_ENSURE_TRUE(aNode && *aNode && aOffset, NS_ERROR_NULL_POINTER);
+ NS_ENSURE_TRUE((*aNode)->IsContent(), NS_OK);
+
+ // Split any matching style nodes above the node/offset
+ OwningNonNull<nsIContent> node = *(*aNode)->AsContent();
+
+ bool useCSS = IsCSSEnabled();
+
+ bool isSet;
+ while (!IsBlockNode(node) && node->GetParent() &&
+ IsEditable(node->GetParent())) {
+ isSet = false;
+ if (useCSS && mCSSEditUtils->IsCSSEditableProperty(node, aProperty,
+ aAttribute)) {
+ // The HTML style defined by aProperty/aAttribute has a CSS equivalence
+ // in this implementation for the node; let's check if it carries those
+ // CSS styles
+ nsAutoString firstValue;
+ mCSSEditUtils->IsCSSEquivalentToHTMLInlineStyleSet(GetAsDOMNode(node),
+ aProperty, aAttribute, isSet, firstValue, CSSEditUtils::eSpecified);
+ }
+ if (// node is the correct inline prop
+ (aProperty && node->IsHTMLElement(aProperty)) ||
+ // node is href - test if really <a href=...
+ (aProperty == nsGkAtoms::href && HTMLEditUtils::IsLink(node)) ||
+ // or node is any prop, and we asked to split them all
+ (!aProperty && NodeIsProperty(node)) ||
+ // or the style is specified in the style attribute
+ isSet) {
+ // Found a style node we need to split
+ int32_t offset = SplitNodeDeep(*node, *(*aNode)->AsContent(), *aOffset,
+ EmptyContainers::yes, aOutLeftNode,
+ aOutRightNode);
+ NS_ENSURE_TRUE(offset != -1, NS_ERROR_FAILURE);
+ // reset startNode/startOffset
+ *aNode = node->GetParent();
+ *aOffset = offset;
+ }
+ node = node->GetParent();
+ }
+
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::ClearStyle(nsCOMPtr<nsINode>* aNode,
+ int32_t* aOffset,
+ nsIAtom* aProperty,
+ const nsAString* aAttribute)
+{
+ nsCOMPtr<nsIContent> leftNode, rightNode;
+ nsresult rv = SplitStyleAbovePoint(aNode, aOffset, aProperty,
+ aAttribute, getter_AddRefs(leftNode),
+ getter_AddRefs(rightNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (leftNode) {
+ bool bIsEmptyNode;
+ IsEmptyNode(leftNode, &bIsEmptyNode, false, true);
+ if (bIsEmptyNode) {
+ // delete leftNode if it became empty
+ rv = DeleteNode(leftNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ if (rightNode) {
+ nsCOMPtr<nsINode> secondSplitParent = GetLeftmostChild(rightNode);
+ // don't try to split non-containers (br's, images, hr's, etc.)
+ if (!secondSplitParent) {
+ secondSplitParent = rightNode;
+ }
+ nsCOMPtr<Element> savedBR;
+ if (!IsContainer(secondSplitParent)) {
+ if (TextEditUtils::IsBreak(secondSplitParent)) {
+ savedBR = do_QueryInterface(secondSplitParent);
+ NS_ENSURE_STATE(savedBR);
+ }
+
+ secondSplitParent = secondSplitParent->GetParentNode();
+ }
+ *aOffset = 0;
+ rv = SplitStyleAbovePoint(address_of(secondSplitParent),
+ aOffset, aProperty, aAttribute,
+ getter_AddRefs(leftNode),
+ getter_AddRefs(rightNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (rightNode) {
+ bool bIsEmptyNode;
+ IsEmptyNode(rightNode, &bIsEmptyNode, false, true);
+ if (bIsEmptyNode) {
+ // delete rightNode if it became empty
+ rv = DeleteNode(rightNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ if (!leftNode) {
+ return NS_OK;
+ }
+
+ // should be impossible to not get a new leftnode here
+ nsCOMPtr<nsINode> newSelParent = GetLeftmostChild(leftNode);
+ if (!newSelParent) {
+ newSelParent = leftNode;
+ }
+ // If rightNode starts with a br, suck it out of right node and into
+ // leftNode. This is so we you don't revert back to the previous style
+ // if you happen to click at the end of a line.
+ if (savedBR) {
+ rv = MoveNode(savedBR, newSelParent, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // remove the style on this new hierarchy
+ int32_t newSelOffset = 0;
+ {
+ // Track the point at the new hierarchy. This is so we can know where
+ // to put the selection after we call RemoveStyleInside().
+ // RemoveStyleInside() could remove any and all of those nodes, so I
+ // have to use the range tracking system to find the right spot to put
+ // selection.
+ AutoTrackDOMPoint tracker(mRangeUpdater,
+ address_of(newSelParent), &newSelOffset);
+ rv = RemoveStyleInside(*leftNode, aProperty, aAttribute);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // reset our node offset values to the resulting new sel point
+ *aNode = newSelParent;
+ *aOffset = newSelOffset;
+ }
+
+ return NS_OK;
+}
+
+bool
+HTMLEditor::NodeIsProperty(nsINode& aNode)
+{
+ return IsContainer(&aNode) && IsEditable(&aNode) && !IsBlockNode(&aNode) &&
+ !aNode.IsHTMLElement(nsGkAtoms::a);
+}
+
+nsresult
+HTMLEditor::ApplyDefaultProperties()
+{
+ size_t defcon = mDefaultStyles.Length();
+ for (size_t j = 0; j < defcon; j++) {
+ PropItem *propItem = mDefaultStyles[j];
+ NS_ENSURE_TRUE(propItem, NS_ERROR_NULL_POINTER);
+ nsresult rv =
+ SetInlineProperty(propItem->tag, propItem->attr, propItem->value);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::RemoveStyleInside(nsIContent& aNode,
+ nsIAtom* aProperty,
+ const nsAString* aAttribute,
+ const bool aChildrenOnly /* = false */)
+{
+ if (aNode.GetAsText()) {
+ return NS_OK;
+ }
+
+ // first process the children
+ RefPtr<nsIContent> child = aNode.GetFirstChild();
+ while (child) {
+ // cache next sibling since we might remove child
+ nsCOMPtr<nsIContent> next = child->GetNextSibling();
+ nsresult rv = RemoveStyleInside(*child, aProperty, aAttribute);
+ NS_ENSURE_SUCCESS(rv, rv);
+ child = next.forget();
+ }
+
+ // then process the node itself
+ if (!aChildrenOnly &&
+ // node is prop we asked for
+ ((aProperty && aNode.NodeInfo()->NameAtom() == aProperty) ||
+ // but check for link (<a href=...)
+ (aProperty == nsGkAtoms::href && HTMLEditUtils::IsLink(&aNode)) ||
+ // and for named anchors
+ (aProperty == nsGkAtoms::name && HTMLEditUtils::IsNamedAnchor(&aNode)) ||
+ // or node is any prop and we asked for that
+ (!aProperty && NodeIsProperty(aNode)))) {
+ // if we weren't passed an attribute, then we want to
+ // remove any matching inlinestyles entirely
+ if (!aAttribute || aAttribute->IsEmpty()) {
+ NS_NAMED_LITERAL_STRING(styleAttr, "style");
+ NS_NAMED_LITERAL_STRING(classAttr, "class");
+
+ bool hasStyleAttr = aNode.HasAttr(kNameSpaceID_None, nsGkAtoms::style);
+ bool hasClassAttr = aNode.HasAttr(kNameSpaceID_None, nsGkAtoms::_class);
+ if (aProperty && (hasStyleAttr || hasClassAttr)) {
+ // aNode carries inline styles or a class attribute so we can't
+ // just remove the element... We need to create above the element
+ // a span that will carry those styles or class, then we can delete
+ // the node.
+ nsCOMPtr<Element> spanNode =
+ InsertContainerAbove(&aNode, nsGkAtoms::span);
+ NS_ENSURE_STATE(spanNode);
+ nsresult rv =
+ CloneAttribute(styleAttr, spanNode->AsDOMNode(), aNode.AsDOMNode());
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv =
+ CloneAttribute(classAttr, spanNode->AsDOMNode(), aNode.AsDOMNode());
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ nsresult rv = RemoveContainer(&aNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // otherwise we just want to eliminate the attribute
+ nsCOMPtr<nsIAtom> attribute = NS_Atomize(*aAttribute);
+ if (aNode.HasAttr(kNameSpaceID_None, attribute)) {
+ // if this matching attribute is the ONLY one on the node,
+ // then remove the whole node. Otherwise just nix the attribute.
+ if (IsOnlyAttribute(&aNode, *aAttribute)) {
+ nsresult rv = RemoveContainer(&aNode);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ nsCOMPtr<nsIDOMElement> elem = do_QueryInterface(&aNode);
+ NS_ENSURE_TRUE(elem, NS_ERROR_NULL_POINTER);
+ nsresult rv = RemoveAttribute(elem, *aAttribute);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+ }
+ }
+ }
+
+ if (!aChildrenOnly &&
+ mCSSEditUtils->IsCSSEditableProperty(&aNode, aProperty, aAttribute)) {
+ // the HTML style defined by aProperty/aAttribute has a CSS equivalence in
+ // this implementation for the node aNode; let's check if it carries those
+ // css styles
+ nsAutoString propertyValue;
+ bool isSet = mCSSEditUtils->IsCSSEquivalentToHTMLInlineStyleSet(&aNode,
+ aProperty, aAttribute, propertyValue, CSSEditUtils::eSpecified);
+ if (isSet && aNode.IsElement()) {
+ // yes, tmp has the corresponding css declarations in its style attribute
+ // let's remove them
+ mCSSEditUtils->RemoveCSSEquivalentToHTMLStyle(aNode.AsElement(),
+ aProperty,
+ aAttribute,
+ &propertyValue,
+ false);
+ // remove the node if it is a span or font, if its style attribute is
+ // empty or absent, and if it does not have a class nor an id
+ RemoveElementIfNoStyleOrIdOrClass(*aNode.AsElement());
+ }
+ }
+
+ // Or node is big or small and we are setting font size
+ if (aChildrenOnly) {
+ return NS_OK;
+ }
+ if (aProperty == nsGkAtoms::font &&
+ (aNode.IsHTMLElement(nsGkAtoms::big) ||
+ aNode.IsHTMLElement(nsGkAtoms::small)) &&
+ aAttribute && aAttribute->LowerCaseEqualsLiteral("size")) {
+ // if we are setting font size, remove any nested bigs and smalls
+ return RemoveContainer(&aNode);
+ }
+ return NS_OK;
+}
+
+bool
+HTMLEditor::IsOnlyAttribute(const nsIContent* aContent,
+ const nsAString& aAttribute)
+{
+ MOZ_ASSERT(aContent);
+
+ uint32_t attrCount = aContent->GetAttrCount();
+ for (uint32_t i = 0; i < attrCount; ++i) {
+ const nsAttrName* name = aContent->GetAttrNameAt(i);
+ if (!name->NamespaceEquals(kNameSpaceID_None)) {
+ return false;
+ }
+
+ nsAutoString attrString;
+ name->LocalName()->ToString(attrString);
+ // if it's the attribute we know about, or a special _moz attribute,
+ // keep looking
+ if (!attrString.Equals(aAttribute, nsCaseInsensitiveStringComparator()) &&
+ !StringBeginsWith(attrString, NS_LITERAL_STRING("_moz"))) {
+ return false;
+ }
+ }
+ // if we made it through all of them without finding a real attribute
+ // other than aAttribute, then return true
+ return true;
+}
+
+nsresult
+HTMLEditor::PromoteRangeIfStartsOrEndsInNamedAnchor(nsRange& aRange)
+{
+ // We assume that <a> is not nested.
+ nsCOMPtr<nsINode> startNode = aRange.GetStartParent();
+ int32_t startOffset = aRange.StartOffset();
+ nsCOMPtr<nsINode> endNode = aRange.GetEndParent();
+ int32_t endOffset = aRange.EndOffset();
+
+ nsCOMPtr<nsINode> parent = startNode;
+
+ while (parent && !parent->IsHTMLElement(nsGkAtoms::body) &&
+ !HTMLEditUtils::IsNamedAnchor(parent)) {
+ parent = parent->GetParentNode();
+ }
+ NS_ENSURE_TRUE(parent, NS_ERROR_NULL_POINTER);
+
+ if (HTMLEditUtils::IsNamedAnchor(parent)) {
+ startNode = parent->GetParentNode();
+ startOffset = startNode ? startNode->IndexOf(parent) : -1;
+ }
+
+ parent = endNode;
+ while (parent && !parent->IsHTMLElement(nsGkAtoms::body) &&
+ !HTMLEditUtils::IsNamedAnchor(parent)) {
+ parent = parent->GetParentNode();
+ }
+ NS_ENSURE_TRUE(parent, NS_ERROR_NULL_POINTER);
+
+ if (HTMLEditUtils::IsNamedAnchor(parent)) {
+ endNode = parent->GetParentNode();
+ endOffset = endNode ? endNode->IndexOf(parent) + 1 : 0;
+ }
+
+ nsresult rv = aRange.SetStart(startNode, startOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aRange.SetEnd(endNode, endOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::PromoteInlineRange(nsRange& aRange)
+{
+ nsCOMPtr<nsINode> startNode = aRange.GetStartParent();
+ int32_t startOffset = aRange.StartOffset();
+ nsCOMPtr<nsINode> endNode = aRange.GetEndParent();
+ int32_t endOffset = aRange.EndOffset();
+
+ while (startNode && !startNode->IsHTMLElement(nsGkAtoms::body) &&
+ IsEditable(startNode) && IsAtFrontOfNode(*startNode, startOffset)) {
+ nsCOMPtr<nsINode> parent = startNode->GetParentNode();
+ NS_ENSURE_TRUE(parent, NS_ERROR_NULL_POINTER);
+ startOffset = parent->IndexOf(startNode);
+ startNode = parent;
+ }
+
+ while (endNode && !endNode->IsHTMLElement(nsGkAtoms::body) &&
+ IsEditable(endNode) && IsAtEndOfNode(*endNode, endOffset)) {
+ nsCOMPtr<nsINode> parent = endNode->GetParentNode();
+ NS_ENSURE_TRUE(parent, NS_ERROR_NULL_POINTER);
+ // We are AFTER this node
+ endOffset = 1 + parent->IndexOf(endNode);
+ endNode = parent;
+ }
+
+ nsresult rv = aRange.SetStart(startNode, startOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aRange.SetEnd(endNode, endOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+bool
+HTMLEditor::IsAtFrontOfNode(nsINode& aNode,
+ int32_t aOffset)
+{
+ if (!aOffset) {
+ return true;
+ }
+
+ if (IsTextNode(&aNode)) {
+ return false;
+ }
+
+ nsCOMPtr<nsIContent> firstNode = GetFirstEditableChild(aNode);
+ NS_ENSURE_TRUE(firstNode, true);
+ if (aNode.IndexOf(firstNode) < aOffset) {
+ return false;
+ }
+ return true;
+}
+
+bool
+HTMLEditor::IsAtEndOfNode(nsINode& aNode,
+ int32_t aOffset)
+{
+ if (aOffset == (int32_t)aNode.Length()) {
+ return true;
+ }
+
+ if (IsTextNode(&aNode)) {
+ return false;
+ }
+
+ nsCOMPtr<nsIContent> lastNode = GetLastEditableChild(aNode);
+ NS_ENSURE_TRUE(lastNode, true);
+ if (aNode.IndexOf(lastNode) < aOffset) {
+ return true;
+ }
+ return false;
+}
+
+
+nsresult
+HTMLEditor::GetInlinePropertyBase(nsIAtom& aProperty,
+ const nsAString* aAttribute,
+ const nsAString* aValue,
+ bool* aFirst,
+ bool* aAny,
+ bool* aAll,
+ nsAString* outValue,
+ bool aCheckDefaults)
+{
+ *aAny = false;
+ *aAll = true;
+ *aFirst = false;
+ bool first = true;
+
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+
+ bool isCollapsed = selection->Collapsed();
+ RefPtr<nsRange> range = selection->GetRangeAt(0);
+ // XXX: Should be a while loop, to get each separate range
+ // XXX: ERROR_HANDLING can currentItem be null?
+ if (range) {
+ // For each range, set a flag
+ bool firstNodeInRange = true;
+
+ if (isCollapsed) {
+ nsCOMPtr<nsINode> collapsedNode = range->GetStartParent();
+ NS_ENSURE_TRUE(collapsedNode, NS_ERROR_FAILURE);
+ bool isSet, theSetting;
+ nsString tOutString;
+ if (aAttribute) {
+ nsString tString(*aAttribute);
+ mTypeInState->GetTypingState(isSet, theSetting, &aProperty, tString,
+ &tOutString);
+ if (outValue) {
+ outValue->Assign(tOutString);
+ }
+ } else {
+ mTypeInState->GetTypingState(isSet, theSetting, &aProperty);
+ }
+ if (isSet) {
+ *aFirst = *aAny = *aAll = theSetting;
+ return NS_OK;
+ }
+
+ if (mCSSEditUtils->IsCSSEditableProperty(collapsedNode, &aProperty,
+ aAttribute)) {
+ if (aValue) {
+ tOutString.Assign(*aValue);
+ }
+ *aFirst = *aAny = *aAll =
+ mCSSEditUtils->IsCSSEquivalentToHTMLInlineStyleSet(collapsedNode,
+ &aProperty, aAttribute, tOutString, CSSEditUtils::eComputed);
+ if (outValue) {
+ outValue->Assign(tOutString);
+ }
+ return NS_OK;
+ }
+
+ isSet = IsTextPropertySetByContent(collapsedNode, &aProperty,
+ aAttribute, aValue, outValue);
+ *aFirst = *aAny = *aAll = isSet;
+
+ if (!isSet && aCheckDefaults) {
+ // Style not set, but if it is a default then it will appear if content
+ // is inserted, so we should report it as set (analogous to
+ // TypeInState).
+ int32_t index;
+ if (aAttribute && TypeInState::FindPropInList(&aProperty, *aAttribute,
+ outValue, mDefaultStyles,
+ index)) {
+ *aFirst = *aAny = *aAll = true;
+ if (outValue) {
+ outValue->Assign(mDefaultStyles[index]->value);
+ }
+ }
+ }
+ return NS_OK;
+ }
+
+ // Non-collapsed selection
+ nsCOMPtr<nsIContentIterator> iter = NS_NewContentIterator();
+
+ nsAutoString firstValue, theValue;
+
+ nsCOMPtr<nsINode> endNode = range->GetEndParent();
+ int32_t endOffset = range->EndOffset();
+
+ for (iter->Init(range); !iter->IsDone(); iter->Next()) {
+ if (!iter->GetCurrentNode()->IsContent()) {
+ continue;
+ }
+ nsCOMPtr<nsIContent> content = iter->GetCurrentNode()->AsContent();
+
+ if (content->IsHTMLElement(nsGkAtoms::body)) {
+ break;
+ }
+
+ // just ignore any non-editable nodes
+ if (content->GetAsText() && (!IsEditable(content) ||
+ IsEmptyTextNode(this, content))) {
+ continue;
+ }
+ if (content->GetAsText()) {
+ if (!isCollapsed && first && firstNodeInRange) {
+ firstNodeInRange = false;
+ if (range->StartOffset() == (int32_t)content->Length()) {
+ continue;
+ }
+ } else if (content == endNode && !endOffset) {
+ continue;
+ }
+ } else if (content->IsElement()) {
+ // handle non-text leaf nodes here
+ continue;
+ }
+
+ bool isSet = false;
+ if (first) {
+ if (mCSSEditUtils->IsCSSEditableProperty(content, &aProperty,
+ aAttribute)) {
+ // The HTML styles defined by aProperty/aAttribute have a CSS
+ // equivalence in this implementation for node; let's check if it
+ // carries those CSS styles
+ if (aValue) {
+ firstValue.Assign(*aValue);
+ }
+ isSet = mCSSEditUtils->IsCSSEquivalentToHTMLInlineStyleSet(content,
+ &aProperty, aAttribute, firstValue, CSSEditUtils::eComputed);
+ } else {
+ isSet = IsTextPropertySetByContent(content, &aProperty, aAttribute,
+ aValue, &firstValue);
+ }
+ *aFirst = isSet;
+ first = false;
+ if (outValue) {
+ *outValue = firstValue;
+ }
+ } else {
+ if (mCSSEditUtils->IsCSSEditableProperty(content, &aProperty,
+ aAttribute)) {
+ // The HTML styles defined by aProperty/aAttribute have a CSS
+ // equivalence in this implementation for node; let's check if it
+ // carries those CSS styles
+ if (aValue) {
+ theValue.Assign(*aValue);
+ }
+ isSet = mCSSEditUtils->IsCSSEquivalentToHTMLInlineStyleSet(content,
+ &aProperty, aAttribute, theValue, CSSEditUtils::eComputed);
+ } else {
+ isSet = IsTextPropertySetByContent(content, &aProperty, aAttribute,
+ aValue, &theValue);
+ }
+ if (firstValue != theValue) {
+ *aAll = false;
+ }
+ }
+
+ if (isSet) {
+ *aAny = true;
+ } else {
+ *aAll = false;
+ }
+ }
+ }
+ if (!*aAny) {
+ // make sure that if none of the selection is set, we don't report all is
+ // set
+ *aAll = false;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetInlineProperty(nsIAtom* aProperty,
+ const nsAString& aAttribute,
+ const nsAString& aValue,
+ bool* aFirst,
+ bool* aAny,
+ bool* aAll)
+{
+ NS_ENSURE_TRUE(aProperty && aFirst && aAny && aAll, NS_ERROR_NULL_POINTER);
+ const nsAString *att = nullptr;
+ if (!aAttribute.IsEmpty())
+ att = &aAttribute;
+ const nsAString *val = nullptr;
+ if (!aValue.IsEmpty())
+ val = &aValue;
+ return GetInlinePropertyBase(*aProperty, att, val, aFirst, aAny, aAll, nullptr);
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetInlinePropertyWithAttrValue(nsIAtom* aProperty,
+ const nsAString& aAttribute,
+ const nsAString& aValue,
+ bool* aFirst,
+ bool* aAny,
+ bool* aAll,
+ nsAString& outValue)
+{
+ NS_ENSURE_TRUE(aProperty && aFirst && aAny && aAll, NS_ERROR_NULL_POINTER);
+ const nsAString *att = nullptr;
+ if (!aAttribute.IsEmpty())
+ att = &aAttribute;
+ const nsAString *val = nullptr;
+ if (!aValue.IsEmpty())
+ val = &aValue;
+ return GetInlinePropertyBase(*aProperty, att, val, aFirst, aAny, aAll, &outValue);
+}
+
+NS_IMETHODIMP
+HTMLEditor::RemoveAllInlineProperties()
+{
+ AutoEditBatch batchIt(this);
+ AutoRules beginRulesSniffing(this, EditAction::resetTextProperties,
+ nsIEditor::eNext);
+
+ nsresult rv = RemoveInlinePropertyImpl(nullptr, nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return ApplyDefaultProperties();
+}
+
+NS_IMETHODIMP
+HTMLEditor::RemoveInlineProperty(nsIAtom* aProperty,
+ const nsAString& aAttribute)
+{
+ return RemoveInlinePropertyImpl(aProperty, &aAttribute);
+}
+
+nsresult
+HTMLEditor::RemoveInlinePropertyImpl(nsIAtom* aProperty,
+ const nsAString* aAttribute)
+{
+ MOZ_ASSERT_IF(aProperty, aAttribute);
+ NS_ENSURE_TRUE(mRules, NS_ERROR_NOT_INITIALIZED);
+ ForceCompositionEnd();
+
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+
+ if (selection->Collapsed()) {
+ // Manipulating text attributes on a collapsed selection only sets state
+ // for the next text insertion
+
+ // For links, aProperty uses "href", use "a" instead
+ if (aProperty == nsGkAtoms::href || aProperty == nsGkAtoms::name) {
+ aProperty = nsGkAtoms::a;
+ }
+
+ if (aProperty) {
+ mTypeInState->ClearProp(aProperty, *aAttribute);
+ } else {
+ mTypeInState->ClearAllProps();
+ }
+ return NS_OK;
+ }
+
+ AutoEditBatch batchIt(this);
+ AutoRules beginRulesSniffing(this, EditAction::removeTextProperty,
+ nsIEditor::eNext);
+ AutoSelectionRestorer selectionRestorer(selection, this);
+ AutoTransactionsConserveSelection dontSpazMySelection(this);
+
+ bool cancel, handled;
+ TextRulesInfo ruleInfo(EditAction::removeTextProperty);
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+ nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!cancel && !handled) {
+ // Loop through the ranges in the selection
+ uint32_t rangeCount = selection->RangeCount();
+ // Since ranges might be modified by SplitStyleAboveRange, we need hold
+ // current ranges
+ AutoTArray<OwningNonNull<nsRange>, 8> arrayOfRanges;
+ for (uint32_t rangeIdx = 0; rangeIdx < rangeCount; ++rangeIdx) {
+ arrayOfRanges.AppendElement(*selection->GetRangeAt(rangeIdx));
+ }
+ for (auto& range : arrayOfRanges) {
+ if (aProperty == nsGkAtoms::name) {
+ // Promote range if it starts or end in a named anchor and we want to
+ // remove named anchors
+ rv = PromoteRangeIfStartsOrEndsInNamedAnchor(range);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ // Adjust range to include any ancestors whose children are entirely
+ // selected
+ rv = PromoteInlineRange(range);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ // Remove this style from ancestors of our range endpoints, splitting
+ // them as appropriate
+ rv = SplitStyleAboveRange(range, aProperty, aAttribute);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Check for easy case: both range endpoints in same text node
+ nsCOMPtr<nsINode> startNode = range->GetStartParent();
+ nsCOMPtr<nsINode> endNode = range->GetEndParent();
+ if (startNode && startNode == endNode && startNode->GetAsText()) {
+ // We're done with this range!
+ if (IsCSSEnabled() &&
+ mCSSEditUtils->IsCSSEditableProperty(startNode, aProperty,
+ aAttribute)) {
+ // The HTML style defined by aProperty/aAttribute has a CSS
+ // equivalence in this implementation for startNode
+ if (mCSSEditUtils->IsCSSEquivalentToHTMLInlineStyleSet(startNode,
+ aProperty, aAttribute, EmptyString(),
+ CSSEditUtils::eComputed)) {
+ // startNode's computed style indicates the CSS equivalence to the
+ // HTML style to remove is applied; but we found no element in the
+ // ancestors of startNode carrying specified styles; assume it
+ // comes from a rule and try to insert a span "inverting" the style
+ if (mCSSEditUtils->IsCSSInvertible(*aProperty, aAttribute)) {
+ NS_NAMED_LITERAL_STRING(value, "-moz-editor-invert-value");
+ SetInlinePropertyOnTextNode(*startNode->GetAsText(),
+ range->StartOffset(),
+ range->EndOffset(), *aProperty,
+ aAttribute, value);
+ }
+ }
+ }
+ } else {
+ // Not the easy case. Range not contained in single text node.
+ nsCOMPtr<nsIContentIterator> iter = NS_NewContentSubtreeIterator();
+
+ nsTArray<OwningNonNull<nsIContent>> arrayOfNodes;
+
+ // Iterate range and build up array
+ for (iter->Init(range); !iter->IsDone(); iter->Next()) {
+ nsCOMPtr<nsINode> node = iter->GetCurrentNode();
+ NS_ENSURE_TRUE(node, NS_ERROR_FAILURE);
+
+ if (IsEditable(node) && node->IsContent()) {
+ arrayOfNodes.AppendElement(*node->AsContent());
+ }
+ }
+
+ // Loop through the list, remove the property on each node
+ for (auto& node : arrayOfNodes) {
+ rv = RemoveStyleInside(node, aProperty, aAttribute);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (IsCSSEnabled() &&
+ mCSSEditUtils->IsCSSEditableProperty(node, aProperty,
+ aAttribute) &&
+ mCSSEditUtils->IsCSSEquivalentToHTMLInlineStyleSet(node,
+ aProperty, aAttribute, EmptyString(),
+ CSSEditUtils::eComputed) &&
+ // startNode's computed style indicates the CSS equivalence to
+ // the HTML style to remove is applied; but we found no element
+ // in the ancestors of startNode carrying specified styles;
+ // assume it comes from a rule and let's try to insert a span
+ // "inverting" the style
+ mCSSEditUtils->IsCSSInvertible(*aProperty, aAttribute)) {
+ NS_NAMED_LITERAL_STRING(value, "-moz-editor-invert-value");
+ SetInlinePropertyOnNode(node, *aProperty, aAttribute, value);
+ }
+ }
+ }
+ }
+ }
+ if (!cancel) {
+ // Post-process
+ rv = rules->DidDoAction(selection, &ruleInfo, rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::IncreaseFontSize()
+{
+ return RelativeFontChange(FontSize::incr);
+}
+
+NS_IMETHODIMP
+HTMLEditor::DecreaseFontSize()
+{
+ return RelativeFontChange(FontSize::decr);
+}
+
+nsresult
+HTMLEditor::RelativeFontChange(FontSize aDir)
+{
+ ForceCompositionEnd();
+
+ // Get the selection
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE);
+ // If selection is collapsed, set typing state
+ if (selection->Collapsed()) {
+ nsIAtom& atom = aDir == FontSize::incr ? *nsGkAtoms::big :
+ *nsGkAtoms::small;
+
+ // Let's see in what kind of element the selection is
+ NS_ENSURE_TRUE(selection->RangeCount() &&
+ selection->GetRangeAt(0)->GetStartParent(), NS_OK);
+ OwningNonNull<nsINode> selectedNode =
+ *selection->GetRangeAt(0)->GetStartParent();
+ if (IsTextNode(selectedNode)) {
+ NS_ENSURE_TRUE(selectedNode->GetParentNode(), NS_OK);
+ selectedNode = *selectedNode->GetParentNode();
+ }
+ if (!CanContainTag(selectedNode, atom)) {
+ return NS_OK;
+ }
+
+ // Manipulating text attributes on a collapsed selection only sets state
+ // for the next text insertion
+ mTypeInState->SetProp(&atom, EmptyString(), EmptyString());
+ return NS_OK;
+ }
+
+ // Wrap with txn batching, rules sniffing, and selection preservation code
+ AutoEditBatch batchIt(this);
+ AutoRules beginRulesSniffing(this, EditAction::setTextProperty,
+ nsIEditor::eNext);
+ AutoSelectionRestorer selectionRestorer(selection, this);
+ AutoTransactionsConserveSelection dontSpazMySelection(this);
+
+ // Loop through the ranges in the selection
+ uint32_t rangeCount = selection->RangeCount();
+ for (uint32_t rangeIdx = 0; rangeIdx < rangeCount; ++rangeIdx) {
+ RefPtr<nsRange> range = selection->GetRangeAt(rangeIdx);
+
+ // Adjust range to include any ancestors with entirely selected children
+ nsresult rv = PromoteInlineRange(*range);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Check for easy case: both range endpoints in same text node
+ nsCOMPtr<nsINode> startNode = range->GetStartParent();
+ nsCOMPtr<nsINode> endNode = range->GetEndParent();
+ if (startNode == endNode && IsTextNode(startNode)) {
+ rv = RelativeFontChangeOnTextNode(aDir, *startNode->GetAsText(),
+ range->StartOffset(),
+ range->EndOffset());
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // Not the easy case. Range not contained in single text node. There
+ // are up to three phases here. There are all the nodes reported by the
+ // subtree iterator to be processed. And there are potentially a
+ // starting textnode and an ending textnode which are only partially
+ // contained by the range.
+
+ // Let's handle the nodes reported by the iterator. These nodes are
+ // entirely contained in the selection range. We build up a list of them
+ // (since doing operations on the document during iteration would perturb
+ // the iterator).
+
+ OwningNonNull<nsIContentIterator> iter = NS_NewContentSubtreeIterator();
+
+ // Iterate range and build up array
+ rv = iter->Init(range);
+ if (NS_SUCCEEDED(rv)) {
+ nsTArray<OwningNonNull<nsIContent>> arrayOfNodes;
+ for (; !iter->IsDone(); iter->Next()) {
+ NS_ENSURE_TRUE(iter->GetCurrentNode()->IsContent(), NS_ERROR_FAILURE);
+ OwningNonNull<nsIContent> node = *iter->GetCurrentNode()->AsContent();
+
+ if (IsEditable(node)) {
+ arrayOfNodes.AppendElement(node);
+ }
+ }
+
+ // Now that we have the list, do the font size change on each node
+ for (auto& node : arrayOfNodes) {
+ rv = RelativeFontChangeOnNode(aDir == FontSize::incr ? +1 : -1, node);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ // Now check the start and end parents of the range to see if they need
+ // to be separately handled (they do if they are text nodes, due to how
+ // the subtree iterator works - it will not have reported them).
+ if (IsTextNode(startNode) && IsEditable(startNode)) {
+ rv = RelativeFontChangeOnTextNode(aDir, *startNode->GetAsText(),
+ range->StartOffset(),
+ startNode->Length());
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ if (IsTextNode(endNode) && IsEditable(endNode)) {
+ rv = RelativeFontChangeOnTextNode(aDir, *endNode->GetAsText(), 0,
+ range->EndOffset());
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::RelativeFontChangeOnTextNode(FontSize aDir,
+ Text& aTextNode,
+ int32_t aStartOffset,
+ int32_t aEndOffset)
+{
+ // Don't need to do anything if no characters actually selected
+ if (aStartOffset == aEndOffset) {
+ return NS_OK;
+ }
+
+ if (!aTextNode.GetParentNode() ||
+ !CanContainTag(*aTextNode.GetParentNode(), *nsGkAtoms::big)) {
+ return NS_OK;
+ }
+
+ OwningNonNull<nsIContent> node = aTextNode;
+
+ // Do we need to split the text node?
+
+ // -1 is a magic value meaning to the end of node
+ if (aEndOffset == -1) {
+ aEndOffset = aTextNode.Length();
+ }
+
+ ErrorResult rv;
+ if ((uint32_t)aEndOffset != aTextNode.Length()) {
+ // We need to split off back of text node
+ node = SplitNode(node, aEndOffset, rv);
+ NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult());
+ }
+ if (aStartOffset) {
+ // We need to split off front of text node
+ SplitNode(node, aStartOffset, rv);
+ NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult());
+ }
+
+ // Look for siblings that are correct type of node
+ nsIAtom* nodeType = aDir == FontSize::incr ? nsGkAtoms::big
+ : nsGkAtoms::small;
+ nsCOMPtr<nsIContent> sibling = GetPriorHTMLSibling(node);
+ if (sibling && sibling->IsHTMLElement(nodeType)) {
+ // Previous sib is already right kind of inline node; slide this over
+ nsresult rv = MoveNode(node, sibling, -1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+ }
+ sibling = GetNextHTMLSibling(node);
+ if (sibling && sibling->IsHTMLElement(nodeType)) {
+ // Following sib is already right kind of inline node; slide this over
+ nsresult rv = MoveNode(node, sibling, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+ }
+
+ // Else reparent the node inside font node with appropriate relative size
+ nsCOMPtr<Element> newElement = InsertContainerAbove(node, nodeType);
+ NS_ENSURE_STATE(newElement);
+
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::RelativeFontChangeHelper(int32_t aSizeChange,
+ nsINode* aNode)
+{
+ MOZ_ASSERT(aNode);
+
+ /* This routine looks for all the font nodes in the tree rooted by aNode,
+ including aNode itself, looking for font nodes that have the size attr
+ set. Any such nodes need to have big or small put inside them, since
+ they override any big/small that are above them.
+ */
+
+ // Can only change font size by + or - 1
+ if (aSizeChange != 1 && aSizeChange != -1) {
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ // If this is a font node with size, put big/small inside it.
+ if (aNode->IsHTMLElement(nsGkAtoms::font) &&
+ aNode->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::size)) {
+ // Cycle through children and adjust relative font size.
+ for (uint32_t i = aNode->GetChildCount(); i--; ) {
+ nsresult rv = RelativeFontChangeOnNode(aSizeChange, aNode->GetChildAt(i));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // RelativeFontChangeOnNode already calls us recursively,
+ // so we don't need to check our children again.
+ return NS_OK;
+ }
+
+ // Otherwise cycle through the children.
+ for (uint32_t i = aNode->GetChildCount(); i--; ) {
+ nsresult rv = RelativeFontChangeHelper(aSizeChange, aNode->GetChildAt(i));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::RelativeFontChangeOnNode(int32_t aSizeChange,
+ nsIContent* aNode)
+{
+ MOZ_ASSERT(aNode);
+ // Can only change font size by + or - 1
+ if (aSizeChange != 1 && aSizeChange != -1) {
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ nsIAtom* atom;
+ if (aSizeChange == 1) {
+ atom = nsGkAtoms::big;
+ } else {
+ atom = nsGkAtoms::small;
+ }
+
+ // Is it the opposite of what we want?
+ if ((aSizeChange == 1 && aNode->IsHTMLElement(nsGkAtoms::small)) ||
+ (aSizeChange == -1 && aNode->IsHTMLElement(nsGkAtoms::big))) {
+ // first populate any nested font tags that have the size attr set
+ nsresult rv = RelativeFontChangeHelper(aSizeChange, aNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // in that case, just remove this node and pull up the children
+ return RemoveContainer(aNode);
+ }
+
+ // can it be put inside a "big" or "small"?
+ if (TagCanContain(*atom, *aNode)) {
+ // first populate any nested font tags that have the size attr set
+ nsresult rv = RelativeFontChangeHelper(aSizeChange, aNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // ok, chuck it in.
+ // first look at siblings of aNode for matching bigs or smalls.
+ // if we find one, move aNode into it.
+ nsIContent* sibling = GetPriorHTMLSibling(aNode);
+ if (sibling && sibling->IsHTMLElement(atom)) {
+ // previous sib is already right kind of inline node; slide this over into it
+ return MoveNode(aNode, sibling, -1);
+ }
+
+ sibling = GetNextHTMLSibling(aNode);
+ if (sibling && sibling->IsHTMLElement(atom)) {
+ // following sib is already right kind of inline node; slide this over into it
+ return MoveNode(aNode, sibling, 0);
+ }
+
+ // else insert it above aNode
+ nsCOMPtr<Element> newElement = InsertContainerAbove(aNode, atom);
+ NS_ENSURE_STATE(newElement);
+
+ return NS_OK;
+ }
+
+ // none of the above? then cycle through the children.
+ // MOOSE: we should group the children together if possible
+ // into a single "big" or "small". For the moment they are
+ // each getting their own.
+ for (uint32_t i = aNode->GetChildCount(); i--; ) {
+ nsresult rv = RelativeFontChangeOnNode(aSizeChange, aNode->GetChildAt(i));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetFontFaceState(bool* aMixed,
+ nsAString& outFace)
+{
+ NS_ENSURE_TRUE(aMixed, NS_ERROR_FAILURE);
+ *aMixed = true;
+ outFace.Truncate();
+
+ bool first, any, all;
+
+ NS_NAMED_LITERAL_STRING(attr, "face");
+ nsresult rv =
+ GetInlinePropertyBase(*nsGkAtoms::font, &attr, nullptr, &first, &any,
+ &all, &outFace);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (any && !all) {
+ return NS_OK; // mixed
+ }
+ if (all) {
+ *aMixed = false;
+ return NS_OK;
+ }
+
+ // if there is no font face, check for tt
+ rv = GetInlinePropertyBase(*nsGkAtoms::tt, nullptr, nullptr, &first, &any,
+ &all,nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (any && !all) {
+ return rv; // mixed
+ }
+ if (all) {
+ *aMixed = false;
+ outFace.AssignLiteral("tt");
+ }
+
+ if (!any) {
+ // there was no font face attrs of any kind. We are in normal font.
+ outFace.Truncate();
+ *aMixed = false;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetFontColorState(bool* aMixed,
+ nsAString& aOutColor)
+{
+ NS_ENSURE_TRUE(aMixed, NS_ERROR_NULL_POINTER);
+ *aMixed = true;
+ aOutColor.Truncate();
+
+ NS_NAMED_LITERAL_STRING(colorStr, "color");
+ bool first, any, all;
+
+ nsresult rv =
+ GetInlinePropertyBase(*nsGkAtoms::font, &colorStr, nullptr, &first,
+ &any, &all, &aOutColor);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (any && !all) {
+ return NS_OK; // mixed
+ }
+ if (all) {
+ *aMixed = false;
+ return NS_OK;
+ }
+
+ if (!any) {
+ // there was no font color attrs of any kind..
+ aOutColor.Truncate();
+ *aMixed = false;
+ }
+ return NS_OK;
+}
+
+// the return value is true only if the instance of the HTML editor we created
+// can handle CSS styles (for instance, Composer can, Messenger can't) and if
+// the CSS preference is checked
+nsresult
+HTMLEditor::GetIsCSSEnabled(bool* aIsCSSEnabled)
+{
+ *aIsCSSEnabled = IsCSSEnabled();
+ return NS_OK;
+}
+
+static bool
+HasNonEmptyAttribute(Element* aElement,
+ nsIAtom* aName)
+{
+ MOZ_ASSERT(aElement);
+
+ nsAutoString value;
+ return aElement->GetAttr(kNameSpaceID_None, aName, value) && !value.IsEmpty();
+}
+
+bool
+HTMLEditor::HasStyleOrIdOrClass(Element* aElement)
+{
+ MOZ_ASSERT(aElement);
+
+ // remove the node if its style attribute is empty or absent,
+ // and if it does not have a class nor an id
+ return HasNonEmptyAttribute(aElement, nsGkAtoms::style) ||
+ HasNonEmptyAttribute(aElement, nsGkAtoms::_class) ||
+ HasNonEmptyAttribute(aElement, nsGkAtoms::id);
+}
+
+nsresult
+HTMLEditor::RemoveElementIfNoStyleOrIdOrClass(Element& aElement)
+{
+ // early way out if node is not the right kind of element
+ if ((!aElement.IsHTMLElement(nsGkAtoms::span) &&
+ !aElement.IsHTMLElement(nsGkAtoms::font)) ||
+ HasStyleOrIdOrClass(&aElement)) {
+ return NS_OK;
+ }
+
+ return RemoveContainer(&aElement);
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/HTMLTableEditor.cpp b/editor/libeditor/HTMLTableEditor.cpp
new file mode 100644
index 000000000..3da0cfe0c
--- /dev/null
+++ b/editor/libeditor/HTMLTableEditor.cpp
@@ -0,0 +1,3458 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <stdio.h>
+
+#include "mozilla/HTMLEditor.h"
+
+#include "HTMLEditUtils.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/EditorUtils.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/dom/Element.h"
+#include "nsAString.h"
+#include "nsAlgorithm.h"
+#include "nsCOMPtr.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsGkAtoms.h"
+#include "nsIAtom.h"
+#include "nsIContent.h"
+#include "nsIDOMElement.h"
+#include "nsIDOMNode.h"
+#include "nsIEditor.h"
+#include "nsIFrame.h"
+#include "nsINode.h"
+#include "nsIPresShell.h"
+#include "nsISupportsUtils.h"
+#include "nsITableCellLayout.h" // For efficient access to table cell
+#include "nsITableEditor.h"
+#include "nsLiteralString.h"
+#include "nsQueryFrame.h"
+#include "nsRange.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsTableCellFrame.h"
+#include "nsTableWrapperFrame.h"
+#include "nscore.h"
+#include <algorithm>
+
+namespace mozilla {
+
+using namespace dom;
+
+/**
+ * Stack based helper class for restoring selection after table edit.
+ */
+class MOZ_STACK_CLASS AutoSelectionSetterAfterTableEdit final
+{
+private:
+ nsCOMPtr<nsITableEditor> mTableEditor;
+ nsCOMPtr<nsIDOMElement> mTable;
+ int32_t mCol, mRow, mDirection, mSelected;
+
+public:
+ AutoSelectionSetterAfterTableEdit(nsITableEditor* aTableEditor,
+ nsIDOMElement* aTable,
+ int32_t aRow,
+ int32_t aCol,
+ int32_t aDirection,
+ bool aSelected)
+ : mTableEditor(aTableEditor)
+ , mTable(aTable)
+ , mCol(aCol)
+ , mRow(aRow)
+ , mDirection(aDirection)
+ , mSelected(aSelected)
+ {
+ }
+
+ ~AutoSelectionSetterAfterTableEdit()
+ {
+ if (mTableEditor) {
+ mTableEditor->SetSelectionAfterTableEdit(mTable, mRow, mCol, mDirection,
+ mSelected);
+ }
+ }
+
+ // This is needed to abort the caret reset in the destructor
+ // when one method yields control to another
+ void CancelSetCaret()
+ {
+ mTableEditor = nullptr;
+ mTable = nullptr;
+ }
+};
+
+NS_IMETHODIMP
+HTMLEditor::InsertCell(nsIDOMElement* aCell,
+ int32_t aRowSpan,
+ int32_t aColSpan,
+ bool aAfter,
+ bool aIsHeader,
+ nsIDOMElement** aNewCell)
+{
+ NS_ENSURE_TRUE(aCell, NS_ERROR_NULL_POINTER);
+ if (aNewCell) {
+ *aNewCell = nullptr;
+ }
+
+ // And the parent and offsets needed to do an insert
+ nsCOMPtr<nsIDOMNode> cellParent;
+ nsresult rv = aCell->GetParentNode(getter_AddRefs(cellParent));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(cellParent, NS_ERROR_NULL_POINTER);
+
+ int32_t cellOffset = GetChildOffset(aCell, cellParent);
+
+ nsCOMPtr<nsIDOMElement> newCell;
+ rv = CreateElementWithDefaults(aIsHeader ? NS_LITERAL_STRING("th") :
+ NS_LITERAL_STRING("tb"),
+ getter_AddRefs(newCell));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (!newCell) {
+ return NS_ERROR_FAILURE;
+ }
+
+ //Optional: return new cell created
+ if (aNewCell) {
+ *aNewCell = newCell.get();
+ NS_ADDREF(*aNewCell);
+ }
+
+ if (aRowSpan > 1) {
+ // Note: Do NOT use editor transaction for this
+ nsAutoString newRowSpan;
+ newRowSpan.AppendInt(aRowSpan, 10);
+ newCell->SetAttribute(NS_LITERAL_STRING("rowspan"), newRowSpan);
+ }
+ if (aColSpan > 1) {
+ // Note: Do NOT use editor transaction for this
+ nsAutoString newColSpan;
+ newColSpan.AppendInt(aColSpan, 10);
+ newCell->SetAttribute(NS_LITERAL_STRING("colspan"), newColSpan);
+ }
+ if (aAfter) {
+ cellOffset++;
+ }
+
+ //Don't let Rules System change the selection
+ AutoTransactionsConserveSelection dontChangeSelection(this);
+ return InsertNode(newCell, cellParent, cellOffset);
+}
+
+NS_IMETHODIMP
+HTMLEditor::SetColSpan(nsIDOMElement* aCell,
+ int32_t aColSpan)
+{
+ NS_ENSURE_TRUE(aCell, NS_ERROR_NULL_POINTER);
+ nsAutoString newSpan;
+ newSpan.AppendInt(aColSpan, 10);
+ return SetAttribute(aCell, NS_LITERAL_STRING("colspan"), newSpan);
+}
+
+NS_IMETHODIMP
+HTMLEditor::SetRowSpan(nsIDOMElement* aCell,
+ int32_t aRowSpan)
+{
+ NS_ENSURE_TRUE(aCell, NS_ERROR_NULL_POINTER);
+ nsAutoString newSpan;
+ newSpan.AppendInt(aRowSpan, 10);
+ return SetAttribute(aCell, NS_LITERAL_STRING("rowspan"), newSpan);
+}
+
+NS_IMETHODIMP
+HTMLEditor::InsertTableCell(int32_t aNumber,
+ bool aAfter)
+{
+ nsCOMPtr<nsIDOMElement> table;
+ nsCOMPtr<nsIDOMElement> curCell;
+ nsCOMPtr<nsIDOMNode> cellParent;
+ int32_t cellOffset, startRowIndex, startColIndex;
+ nsresult rv = GetCellContext(nullptr,
+ getter_AddRefs(table),
+ getter_AddRefs(curCell),
+ getter_AddRefs(cellParent), &cellOffset,
+ &startRowIndex, &startColIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Don't fail if no cell found
+ NS_ENSURE_TRUE(curCell, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND);
+
+ // Get more data for current cell in row we are inserting at (we need COLSPAN)
+ int32_t curStartRowIndex, curStartColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan;
+ bool isSelected;
+ rv = GetCellDataAt(table, startRowIndex, startColIndex,
+ getter_AddRefs(curCell),
+ &curStartRowIndex, &curStartColIndex, &rowSpan, &colSpan,
+ &actualRowSpan, &actualColSpan, &isSelected);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(curCell, NS_ERROR_FAILURE);
+ int32_t newCellIndex = aAfter ? (startColIndex+colSpan) : startColIndex;
+ //We control selection resetting after the insert...
+ AutoSelectionSetterAfterTableEdit setCaret(this, table, startRowIndex,
+ newCellIndex, ePreviousColumn,
+ false);
+ //...so suppress Rules System selection munging
+ AutoTransactionsConserveSelection dontChangeSelection(this);
+
+ for (int32_t i = 0; i < aNumber; i++) {
+ nsCOMPtr<nsIDOMElement> newCell;
+ rv = CreateElementWithDefaults(NS_LITERAL_STRING("td"),
+ getter_AddRefs(newCell));
+ if (NS_SUCCEEDED(rv) && newCell) {
+ if (aAfter) {
+ cellOffset++;
+ }
+ rv = InsertNode(newCell, cellParent, cellOffset);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ }
+ }
+ // XXX This is perhaps the result of the last call of InsertNode() or
+ // CreateElementWithDefaults().
+ return rv;
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetFirstRow(nsIDOMElement* aTableElement,
+ nsIDOMNode** aRowNode)
+{
+ NS_ENSURE_TRUE(aRowNode, NS_ERROR_NULL_POINTER);
+
+ *aRowNode = nullptr;
+
+ NS_ENSURE_TRUE(aTableElement, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsIDOMElement> tableElement;
+ nsresult rv = GetElementOrParentByTagName(NS_LITERAL_STRING("table"),
+ aTableElement,
+ getter_AddRefs(tableElement));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(tableElement, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsIDOMNode> tableChild;
+ rv = tableElement->GetFirstChild(getter_AddRefs(tableChild));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ while (tableChild) {
+ nsCOMPtr<nsIContent> content = do_QueryInterface(tableChild);
+ if (content) {
+ if (content->IsHTMLElement(nsGkAtoms::tr)) {
+ // Found a row directly under <table>
+ *aRowNode = tableChild;
+ NS_ADDREF(*aRowNode);
+ return NS_OK;
+ }
+ // Look for row in one of the row container elements
+ if (content->IsAnyOfHTMLElements(nsGkAtoms::tbody,
+ nsGkAtoms::thead,
+ nsGkAtoms::tfoot)) {
+ nsCOMPtr<nsIDOMNode> rowNode;
+ rv = tableChild->GetFirstChild(getter_AddRefs(rowNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We can encounter textnodes here -- must find a row
+ while (rowNode && !HTMLEditUtils::IsTableRow(rowNode)) {
+ nsCOMPtr<nsIDOMNode> nextNode;
+ rv = rowNode->GetNextSibling(getter_AddRefs(nextNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rowNode = nextNode;
+ }
+ if (rowNode) {
+ *aRowNode = rowNode.get();
+ NS_ADDREF(*aRowNode);
+ return NS_OK;
+ }
+ }
+ }
+ // Here if table child was a CAPTION or COLGROUP
+ // or child of a row parent wasn't a row (bad HTML?),
+ // or first child was a textnode
+ // Look in next table child
+ nsCOMPtr<nsIDOMNode> nextChild;
+ rv = tableChild->GetNextSibling(getter_AddRefs(nextChild));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ tableChild = nextChild;
+ }
+ // If here, row was not found
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetNextRow(nsIDOMNode* aCurrentRowNode,
+ nsIDOMNode** aRowNode)
+{
+ NS_ENSURE_TRUE(aRowNode, NS_ERROR_NULL_POINTER);
+
+ *aRowNode = nullptr;
+
+ NS_ENSURE_TRUE(aCurrentRowNode, NS_ERROR_NULL_POINTER);
+
+ if (!HTMLEditUtils::IsTableRow(aCurrentRowNode)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIDOMNode> nextRow;
+ nsresult rv = aCurrentRowNode->GetNextSibling(getter_AddRefs(nextRow));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDOMNode> nextNode;
+
+ // Skip over any textnodes here
+ while (nextRow && !HTMLEditUtils::IsTableRow(nextRow)) {
+ rv = nextRow->GetNextSibling(getter_AddRefs(nextNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nextRow = nextNode;
+ }
+ if (nextRow) {
+ *aRowNode = nextRow.get();
+ NS_ADDREF(*aRowNode);
+ return NS_OK;
+ }
+
+ // No row found, search for rows in other table sections
+ nsCOMPtr<nsIDOMNode> rowParent;
+ rv = aCurrentRowNode->GetParentNode(getter_AddRefs(rowParent));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(rowParent, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsIDOMNode> parentSibling;
+ rv = rowParent->GetNextSibling(getter_AddRefs(parentSibling));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ while (parentSibling) {
+ rv = parentSibling->GetFirstChild(getter_AddRefs(nextRow));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We can encounter textnodes here -- must find a row
+ while (nextRow && !HTMLEditUtils::IsTableRow(nextRow)) {
+ rv = nextRow->GetNextSibling(getter_AddRefs(nextNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nextRow = nextNode;
+ }
+ if (nextRow) {
+ *aRowNode = nextRow.get();
+ NS_ADDREF(*aRowNode);
+ return NS_OK;
+ }
+
+ // We arrive here only if a table section has no children
+ // or first child of section is not a row (bad HTML or more "_moz_text" nodes!)
+ // So look for another section sibling
+ rv = parentSibling->GetNextSibling(getter_AddRefs(nextNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ parentSibling = nextNode;
+ }
+ // If here, row was not found
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+}
+
+nsresult
+HTMLEditor::GetLastCellInRow(nsIDOMNode* aRowNode,
+ nsIDOMNode** aCellNode)
+{
+ NS_ENSURE_TRUE(aCellNode, NS_ERROR_NULL_POINTER);
+
+ *aCellNode = nullptr;
+
+ NS_ENSURE_TRUE(aRowNode, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsIDOMNode> rowChild;
+ nsresult rv = aRowNode->GetLastChild(getter_AddRefs(rowChild));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ while (rowChild && !HTMLEditUtils::IsTableCell(rowChild)) {
+ // Skip over textnodes
+ nsCOMPtr<nsIDOMNode> previousChild;
+ rv = rowChild->GetPreviousSibling(getter_AddRefs(previousChild));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rowChild = previousChild;
+ }
+ if (rowChild) {
+ *aCellNode = rowChild.get();
+ NS_ADDREF(*aCellNode);
+ return NS_OK;
+ }
+ // If here, cell was not found
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+}
+
+NS_IMETHODIMP
+HTMLEditor::InsertTableColumn(int32_t aNumber,
+ bool aAfter)
+{
+ RefPtr<Selection> selection;
+ nsCOMPtr<nsIDOMElement> table;
+ nsCOMPtr<nsIDOMElement> curCell;
+ int32_t startRowIndex, startColIndex;
+ nsresult rv = GetCellContext(getter_AddRefs(selection),
+ getter_AddRefs(table),
+ getter_AddRefs(curCell),
+ nullptr, nullptr,
+ &startRowIndex, &startColIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Don't fail if no cell found
+ NS_ENSURE_TRUE(curCell, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND);
+
+ // Get more data for current cell (we need ROWSPAN)
+ int32_t curStartRowIndex, curStartColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan;
+ bool isSelected;
+ rv = GetCellDataAt(table, startRowIndex, startColIndex,
+ getter_AddRefs(curCell),
+ &curStartRowIndex, &curStartColIndex,
+ &rowSpan, &colSpan,
+ &actualRowSpan, &actualColSpan, &isSelected);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(curCell, NS_ERROR_FAILURE);
+
+ AutoEditBatch beginBatching(this);
+ // Prevent auto insertion of BR in new cell until we're done
+ AutoRules beginRulesSniffing(this, EditAction::insertNode, nsIEditor::eNext);
+
+ // Use column after current cell if requested
+ if (aAfter) {
+ startColIndex += actualColSpan;
+ //Detect when user is adding after a COLSPAN=0 case
+ // Assume they want to stop the "0" behavior and
+ // really add a new column. Thus we set the
+ // colspan to its true value
+ if (!colSpan) {
+ SetColSpan(curCell, actualColSpan);
+ }
+ }
+
+ int32_t rowCount, colCount, rowIndex;
+ rv = GetTableSize(table, &rowCount, &colCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ //We reset caret in destructor...
+ AutoSelectionSetterAfterTableEdit setCaret(this, table, startRowIndex,
+ startColIndex, ePreviousRow,
+ false);
+ //.. so suppress Rules System selection munging
+ AutoTransactionsConserveSelection dontChangeSelection(this);
+
+ // If we are inserting after all existing columns
+ // Make sure table is "well formed"
+ // before appending new column
+ if (startColIndex >= colCount) {
+ NormalizeTable(table);
+ }
+
+ nsCOMPtr<nsIDOMNode> rowNode;
+ for (rowIndex = 0; rowIndex < rowCount; rowIndex++) {
+ if (startColIndex < colCount) {
+ // We are inserting before an existing column
+ rv = GetCellDataAt(table, rowIndex, startColIndex,
+ getter_AddRefs(curCell),
+ &curStartRowIndex, &curStartColIndex,
+ &rowSpan, &colSpan,
+ &actualRowSpan, &actualColSpan, &isSelected);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Don't fail entire process if we fail to find a cell
+ // (may fail just in particular rows with < adequate cells per row)
+ if (curCell) {
+ if (curStartColIndex < startColIndex) {
+ // We have a cell spanning this location
+ // Simply increase its colspan to keep table rectangular
+ // Note: we do nothing if colsSpan=0,
+ // since it should automatically span the new column
+ if (colSpan > 0) {
+ SetColSpan(curCell, colSpan+aNumber);
+ }
+ } else {
+ // Simply set selection to the current cell
+ // so we can let InsertTableCell() do the work
+ // Insert a new cell before current one
+ selection->Collapse(curCell, 0);
+ rv = InsertTableCell(aNumber, false);
+ }
+ }
+ } else {
+ // Get current row and append new cells after last cell in row
+ if (!rowIndex) {
+ rv = GetFirstRow(table.get(), getter_AddRefs(rowNode));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ nsCOMPtr<nsIDOMNode> nextRow;
+ rv = GetNextRow(rowNode.get(), getter_AddRefs(nextRow));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ rowNode = nextRow;
+ }
+
+ if (rowNode) {
+ nsCOMPtr<nsIDOMNode> lastCell;
+ rv = GetLastCellInRow(rowNode, getter_AddRefs(lastCell));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(lastCell, NS_ERROR_FAILURE);
+
+ curCell = do_QueryInterface(lastCell);
+ if (curCell) {
+ // Simply add same number of cells to each row
+ // Although tempted to check cell indexes for curCell,
+ // the effects of COLSPAN>1 in some cells makes this futile!
+ // We must use NormalizeTable first to assure
+ // that there are cells in each cellmap location
+ selection->Collapse(curCell, 0);
+ rv = InsertTableCell(aNumber, true);
+ }
+ }
+ }
+ }
+ // XXX This is perhaps the result of the last call of InsertTableCell().
+ return rv;
+}
+
+NS_IMETHODIMP
+HTMLEditor::InsertTableRow(int32_t aNumber,
+ bool aAfter)
+{
+ nsCOMPtr<nsIDOMElement> table;
+ nsCOMPtr<nsIDOMElement> curCell;
+
+ int32_t startRowIndex, startColIndex;
+ nsresult rv = GetCellContext(nullptr,
+ getter_AddRefs(table),
+ getter_AddRefs(curCell),
+ nullptr, nullptr,
+ &startRowIndex, &startColIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Don't fail if no cell found
+ NS_ENSURE_TRUE(curCell, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND);
+
+ // Get more data for current cell in row we are inserting at (we need COLSPAN)
+ int32_t curStartRowIndex, curStartColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan;
+ bool isSelected;
+ rv = GetCellDataAt(table, startRowIndex, startColIndex,
+ getter_AddRefs(curCell),
+ &curStartRowIndex, &curStartColIndex,
+ &rowSpan, &colSpan,
+ &actualRowSpan, &actualColSpan, &isSelected);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(curCell, NS_ERROR_FAILURE);
+
+ int32_t rowCount, colCount;
+ rv = GetTableSize(table, &rowCount, &colCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ AutoEditBatch beginBatching(this);
+ // Prevent auto insertion of BR in new cell until we're done
+ AutoRules beginRulesSniffing(this, EditAction::insertNode, nsIEditor::eNext);
+
+ if (aAfter) {
+ // Use row after current cell
+ startRowIndex += actualRowSpan;
+
+ //Detect when user is adding after a ROWSPAN=0 case
+ // Assume they want to stop the "0" behavior and
+ // really add a new row. Thus we set the
+ // rowspan to its true value
+ if (!rowSpan) {
+ SetRowSpan(curCell, actualRowSpan);
+ }
+ }
+
+ //We control selection resetting after the insert...
+ AutoSelectionSetterAfterTableEdit setCaret(this, table, startRowIndex,
+ startColIndex, ePreviousColumn,
+ false);
+ //...so suppress Rules System selection munging
+ AutoTransactionsConserveSelection dontChangeSelection(this);
+
+ nsCOMPtr<nsIDOMElement> cellForRowParent;
+ int32_t cellsInRow = 0;
+ if (startRowIndex < rowCount) {
+ // We are inserting above an existing row
+ // Get each cell in the insert row to adjust for COLSPAN effects while we
+ // count how many cells are needed
+ int32_t colIndex = 0;
+ while (NS_SUCCEEDED(GetCellDataAt(table, startRowIndex, colIndex,
+ getter_AddRefs(curCell),
+ &curStartRowIndex, &curStartColIndex,
+ &rowSpan, &colSpan,
+ &actualRowSpan, &actualColSpan,
+ &isSelected))) {
+ if (curCell) {
+ if (curStartRowIndex < startRowIndex) {
+ // We have a cell spanning this location
+ // Simply increase its rowspan
+ //Note that if rowSpan == 0, we do nothing,
+ // since that cell should automatically extend into the new row
+ if (rowSpan > 0) {
+ SetRowSpan(curCell, rowSpan+aNumber);
+ }
+ } else {
+ // We have a cell in the insert row
+
+ // Count the number of cells we need to add to the new row
+ cellsInRow += actualColSpan;
+
+ // Save cell we will use below
+ if (!cellForRowParent) {
+ cellForRowParent = curCell;
+ }
+ }
+ // Next cell in row
+ colIndex += actualColSpan;
+ } else {
+ colIndex++;
+ }
+ }
+ } else {
+ // We are adding a new row after all others
+ // If it weren't for colspan=0 effect,
+ // we could simply use colCount for number of new cells...
+ // XXX colspan=0 support has now been removed in table layout so maybe this can be cleaned up now? (bug 1243183)
+ cellsInRow = colCount;
+
+ // ...but we must compensate for all cells with rowSpan = 0 in the last row
+ int32_t lastRow = rowCount-1;
+ int32_t tempColIndex = 0;
+ while (NS_SUCCEEDED(GetCellDataAt(table, lastRow, tempColIndex,
+ getter_AddRefs(curCell),
+ &curStartRowIndex, &curStartColIndex,
+ &rowSpan, &colSpan,
+ &actualRowSpan, &actualColSpan,
+ &isSelected))) {
+ if (!rowSpan) {
+ cellsInRow -= actualColSpan;
+ }
+
+ tempColIndex += actualColSpan;
+
+ // Save cell from the last row that we will use below
+ if (!cellForRowParent && curStartRowIndex == lastRow) {
+ cellForRowParent = curCell;
+ }
+ }
+ }
+
+ if (cellsInRow > 0) {
+ // The row parent and offset where we will insert new row
+ nsCOMPtr<nsIDOMNode> parentOfRow;
+ int32_t newRowOffset;
+
+ NS_NAMED_LITERAL_STRING(trStr, "tr");
+ if (!cellForRowParent) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIDOMElement> parentRow;
+ rv = GetElementOrParentByTagName(trStr, cellForRowParent,
+ getter_AddRefs(parentRow));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(parentRow, NS_ERROR_NULL_POINTER);
+
+ parentRow->GetParentNode(getter_AddRefs(parentOfRow));
+ NS_ENSURE_TRUE(parentOfRow, NS_ERROR_NULL_POINTER);
+
+ newRowOffset = GetChildOffset(parentRow, parentOfRow);
+
+ // Adjust for when adding past the end
+ if (aAfter && startRowIndex >= rowCount) {
+ newRowOffset++;
+ }
+
+ for (int32_t row = 0; row < aNumber; row++) {
+ // Create a new row
+ nsCOMPtr<nsIDOMElement> newRow;
+ rv = CreateElementWithDefaults(trStr, getter_AddRefs(newRow));
+ if (NS_SUCCEEDED(rv)) {
+ NS_ENSURE_TRUE(newRow, NS_ERROR_FAILURE);
+
+ for (int32_t i = 0; i < cellsInRow; i++) {
+ nsCOMPtr<nsIDOMElement> newCell;
+ rv = CreateElementWithDefaults(NS_LITERAL_STRING("td"),
+ getter_AddRefs(newCell));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(newCell, NS_ERROR_FAILURE);
+
+ // Don't use transaction system yet! (not until entire row is inserted)
+ nsCOMPtr<nsIDOMNode>resultNode;
+ rv = newRow->AppendChild(newCell, getter_AddRefs(resultNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // Use transaction system to insert the entire row+cells
+ // (Note that rows are inserted at same childoffset each time)
+ rv = InsertNode(newRow, parentOfRow, newRowOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ }
+ // XXX This might be the result of the last call of
+ // CreateElementWithDefaults(), otherwise, NS_OK.
+ return rv;
+}
+
+// Editor helper only
+// XXX Code changed for bug 217717 and now we don't need aSelection param
+// TODO: Remove aSelection param
+nsresult
+HTMLEditor::DeleteTable2(nsIDOMElement* aTable,
+ Selection* aSelection)
+{
+ NS_ENSURE_TRUE(aTable, NS_ERROR_NULL_POINTER);
+
+ // Select the table
+ nsresult rv = ClearSelection();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ rv = AppendNodeToSelectionAsRange(aTable);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return DeleteSelection(nsIEditor::eNext, nsIEditor::eStrip);
+}
+
+NS_IMETHODIMP
+HTMLEditor::DeleteTable()
+{
+ RefPtr<Selection> selection;
+ nsCOMPtr<nsIDOMElement> table;
+ nsresult rv = GetCellContext(getter_AddRefs(selection),
+ getter_AddRefs(table),
+ nullptr, nullptr, nullptr, nullptr, nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ AutoEditBatch beginBatching(this);
+ return DeleteTable2(table, selection);
+}
+
+NS_IMETHODIMP
+HTMLEditor::DeleteTableCell(int32_t aNumber)
+{
+ RefPtr<Selection> selection;
+ nsCOMPtr<nsIDOMElement> table;
+ nsCOMPtr<nsIDOMElement> cell;
+ int32_t startRowIndex, startColIndex;
+
+
+ nsresult rv = GetCellContext(getter_AddRefs(selection),
+ getter_AddRefs(table),
+ getter_AddRefs(cell),
+ nullptr, nullptr,
+ &startRowIndex, &startColIndex);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Don't fail if we didn't find a table or cell
+ NS_ENSURE_TRUE(table && cell, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND);
+
+ AutoEditBatch beginBatching(this);
+ // Prevent rules testing until we're done
+ AutoRules beginRulesSniffing(this, EditAction::deleteNode, nsIEditor::eNext);
+
+ nsCOMPtr<nsIDOMElement> firstCell;
+ nsCOMPtr<nsIDOMRange> range;
+ rv = GetFirstSelectedCell(getter_AddRefs(range), getter_AddRefs(firstCell));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t rangeCount;
+ rv = selection->GetRangeCount(&rangeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (firstCell && rangeCount > 1) {
+ // When > 1 selected cell,
+ // ignore aNumber and use selected cells
+ cell = firstCell;
+
+ int32_t rowCount, colCount;
+ rv = GetTableSize(table, &rowCount, &colCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get indexes -- may be different than original cell
+ rv = GetCellIndexes(cell, &startRowIndex, &startColIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // The setCaret object will call AutoSelectionSetterAfterTableEdit in its
+ // destructor
+ AutoSelectionSetterAfterTableEdit setCaret(this, table, startRowIndex,
+ startColIndex, ePreviousColumn,
+ false);
+ AutoTransactionsConserveSelection dontChangeSelection(this);
+
+ bool checkToDeleteRow = true;
+ bool checkToDeleteColumn = true;
+ while (cell) {
+ bool deleteRow = false;
+ bool deleteCol = false;
+
+ if (checkToDeleteRow) {
+ // Optimize to delete an entire row
+ // Clear so we don't repeat AllCellsInRowSelected within the same row
+ checkToDeleteRow = false;
+
+ deleteRow = AllCellsInRowSelected(table, startRowIndex, colCount);
+ if (deleteRow) {
+ // First, find the next cell in a different row
+ // to continue after we delete this row
+ int32_t nextRow = startRowIndex;
+ while (nextRow == startRowIndex) {
+ rv = GetNextSelectedCell(nullptr, getter_AddRefs(cell));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!cell) {
+ break;
+ }
+ rv = GetCellIndexes(cell, &nextRow, &startColIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // Delete entire row
+ rv = DeleteRow(table, startRowIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (cell) {
+ // For the next cell: Subtract 1 for row we deleted
+ startRowIndex = nextRow - 1;
+ // Set true since we know we will look at a new row next
+ checkToDeleteRow = true;
+ }
+ }
+ }
+ if (!deleteRow) {
+ if (checkToDeleteColumn) {
+ // Optimize to delete an entire column
+ // Clear this so we don't repeat AllCellsInColSelected within the same Col
+ checkToDeleteColumn = false;
+
+ deleteCol = AllCellsInColumnSelected(table, startColIndex, colCount);
+ if (deleteCol) {
+ // First, find the next cell in a different column
+ // to continue after we delete this column
+ int32_t nextCol = startColIndex;
+ while (nextCol == startColIndex) {
+ rv = GetNextSelectedCell(nullptr, getter_AddRefs(cell));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!cell) {
+ break;
+ }
+ rv = GetCellIndexes(cell, &startRowIndex, &nextCol);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // Delete entire Col
+ rv = DeleteColumn(table, startColIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (cell) {
+ // For the next cell, subtract 1 for col. deleted
+ startColIndex = nextCol - 1;
+ // Set true since we know we will look at a new column next
+ checkToDeleteColumn = true;
+ }
+ }
+ }
+ if (!deleteCol) {
+ // First get the next cell to delete
+ nsCOMPtr<nsIDOMElement> nextCell;
+ rv = GetNextSelectedCell(getter_AddRefs(range),
+ getter_AddRefs(nextCell));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Then delete the cell
+ rv = DeleteNode(cell);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // The next cell to delete
+ cell = nextCell;
+ if (cell) {
+ rv = GetCellIndexes(cell, &startRowIndex, &startColIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ }
+ }
+ } else {
+ for (int32_t i = 0; i < aNumber; i++) {
+ rv = GetCellContext(getter_AddRefs(selection),
+ getter_AddRefs(table),
+ getter_AddRefs(cell),
+ nullptr, nullptr,
+ &startRowIndex, &startColIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Don't fail if no cell found
+ NS_ENSURE_TRUE(cell, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND);
+
+ if (GetNumberOfCellsInRow(table, startRowIndex) == 1) {
+ nsCOMPtr<nsIDOMElement> parentRow;
+ rv = GetElementOrParentByTagName(NS_LITERAL_STRING("tr"), cell,
+ getter_AddRefs(parentRow));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(parentRow, NS_ERROR_NULL_POINTER);
+
+ // We should delete the row instead,
+ // but first check if its the only row left
+ // so we can delete the entire table
+ int32_t rowCount, colCount;
+ rv = GetTableSize(table, &rowCount, &colCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (rowCount == 1) {
+ return DeleteTable2(table, selection);
+ }
+
+ // We need to call DeleteTableRow to handle cells with rowspan
+ rv = DeleteTableRow(1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // More than 1 cell in the row
+
+ // The setCaret object will call AutoSelectionSetterAfterTableEdit in its
+ // destructor
+ AutoSelectionSetterAfterTableEdit setCaret(this, table, startRowIndex,
+ startColIndex, ePreviousColumn,
+ false);
+ AutoTransactionsConserveSelection dontChangeSelection(this);
+
+ rv = DeleteNode(cell);
+ // If we fail, don't try to delete any more cells???
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::DeleteTableCellContents()
+{
+ RefPtr<Selection> selection;
+ nsCOMPtr<nsIDOMElement> table;
+ nsCOMPtr<nsIDOMElement> cell;
+ int32_t startRowIndex, startColIndex;
+ nsresult rv = GetCellContext(getter_AddRefs(selection),
+ getter_AddRefs(table),
+ getter_AddRefs(cell),
+ nullptr, nullptr,
+ &startRowIndex, &startColIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Don't fail if no cell found
+ NS_ENSURE_TRUE(cell, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND);
+
+
+ AutoEditBatch beginBatching(this);
+ // Prevent rules testing until we're done
+ AutoRules beginRulesSniffing(this, EditAction::deleteNode, nsIEditor::eNext);
+ //Don't let Rules System change the selection
+ AutoTransactionsConserveSelection dontChangeSelection(this);
+
+
+ nsCOMPtr<nsIDOMElement> firstCell;
+ nsCOMPtr<nsIDOMRange> range;
+ rv = GetFirstSelectedCell(getter_AddRefs(range), getter_AddRefs(firstCell));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+
+ if (firstCell) {
+ cell = firstCell;
+ rv = GetCellIndexes(cell, &startRowIndex, &startColIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ AutoSelectionSetterAfterTableEdit setCaret(this, table, startRowIndex,
+ startColIndex, ePreviousColumn,
+ false);
+
+ while (cell) {
+ DeleteCellContents(cell);
+ if (firstCell) {
+ // We doing a selected cells, so do all of them
+ rv = GetNextSelectedCell(nullptr, getter_AddRefs(cell));
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ cell = nullptr;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::DeleteCellContents(nsIDOMElement* aCell)
+{
+ NS_ENSURE_TRUE(aCell, NS_ERROR_NULL_POINTER);
+
+ // Prevent rules testing until we're done
+ AutoRules beginRulesSniffing(this, EditAction::deleteNode, nsIEditor::eNext);
+
+ nsCOMPtr<nsIDOMNode> child;
+ bool hasChild;
+ aCell->HasChildNodes(&hasChild);
+
+ while (hasChild) {
+ aCell->GetLastChild(getter_AddRefs(child));
+ nsresult rv = DeleteNode(child);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aCell->HasChildNodes(&hasChild);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::DeleteTableColumn(int32_t aNumber)
+{
+ RefPtr<Selection> selection;
+ nsCOMPtr<nsIDOMElement> table;
+ nsCOMPtr<nsIDOMElement> cell;
+ int32_t startRowIndex, startColIndex, rowCount, colCount;
+ nsresult rv = GetCellContext(getter_AddRefs(selection),
+ getter_AddRefs(table),
+ getter_AddRefs(cell),
+ nullptr, nullptr,
+ &startRowIndex, &startColIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Don't fail if no cell found
+ NS_ENSURE_TRUE(table && cell, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND);
+
+ rv = GetTableSize(table, &rowCount, &colCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Shortcut the case of deleting all columns in table
+ if (!startColIndex && aNumber >= colCount) {
+ return DeleteTable2(table, selection);
+ }
+
+ // Check for counts too high
+ aNumber = std::min(aNumber,(colCount-startColIndex));
+
+ AutoEditBatch beginBatching(this);
+ // Prevent rules testing until we're done
+ AutoRules beginRulesSniffing(this, EditAction::deleteNode, nsIEditor::eNext);
+
+ // Test if deletion is controlled by selected cells
+ nsCOMPtr<nsIDOMElement> firstCell;
+ nsCOMPtr<nsIDOMRange> range;
+ rv = GetFirstSelectedCell(getter_AddRefs(range), getter_AddRefs(firstCell));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t rangeCount;
+ rv = selection->GetRangeCount(&rangeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (firstCell && rangeCount > 1) {
+ // Fetch indexes again - may be different for selected cells
+ rv = GetCellIndexes(firstCell, &startRowIndex, &startColIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ //We control selection resetting after the insert...
+ AutoSelectionSetterAfterTableEdit setCaret(this, table, startRowIndex,
+ startColIndex, ePreviousRow,
+ false);
+
+ if (firstCell && rangeCount > 1) {
+ // Use selected cells to determine what rows to delete
+ cell = firstCell;
+
+ while (cell) {
+ if (cell != firstCell) {
+ rv = GetCellIndexes(cell, &startRowIndex, &startColIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // Find the next cell in a different column
+ // to continue after we delete this column
+ int32_t nextCol = startColIndex;
+ while (nextCol == startColIndex) {
+ rv = GetNextSelectedCell(getter_AddRefs(range), getter_AddRefs(cell));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!cell) {
+ break;
+ }
+ rv = GetCellIndexes(cell, &startRowIndex, &nextCol);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ rv = DeleteColumn(table, startColIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ } else {
+ for (int32_t i = 0; i < aNumber; i++) {
+ rv = DeleteColumn(table, startColIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::DeleteColumn(nsIDOMElement* aTable,
+ int32_t aColIndex)
+{
+ NS_ENSURE_TRUE(aTable, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsIDOMElement> cell;
+ int32_t startRowIndex, startColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan;
+ bool isSelected;
+ int32_t rowIndex = 0;
+
+ do {
+ nsresult rv =
+ GetCellDataAt(aTable, rowIndex, aColIndex, getter_AddRefs(cell),
+ &startRowIndex, &startColIndex, &rowSpan, &colSpan,
+ &actualRowSpan, &actualColSpan, &isSelected);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (cell) {
+ // Find cells that don't start in column we are deleting
+ if (startColIndex < aColIndex || colSpan > 1 || !colSpan) {
+ // We have a cell spanning this location
+ // Decrease its colspan to keep table rectangular,
+ // but if colSpan=0, it will adjust automatically
+ if (colSpan > 0) {
+ NS_ASSERTION((colSpan > 1),"Bad COLSPAN in DeleteTableColumn");
+ SetColSpan(cell, colSpan-1);
+ }
+ if (startColIndex == aColIndex) {
+ // Cell is in column to be deleted, but must have colspan > 1,
+ // so delete contents of cell instead of cell itself
+ // (We must have reset colspan above)
+ DeleteCellContents(cell);
+ }
+ // To next cell in column
+ rowIndex += actualRowSpan;
+ } else {
+ // Delete the cell
+ if (GetNumberOfCellsInRow(aTable, rowIndex) == 1) {
+ // Only 1 cell in row - delete the row
+ nsCOMPtr<nsIDOMElement> parentRow;
+ rv = GetElementOrParentByTagName(NS_LITERAL_STRING("tr"), cell,
+ getter_AddRefs(parentRow));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!parentRow) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ // But first check if its the only row left
+ // so we can delete the entire table
+ // (This should never happen but it's the safe thing to do)
+ int32_t rowCount, colCount;
+ rv = GetTableSize(aTable, &rowCount, &colCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (rowCount == 1) {
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE);
+ return DeleteTable2(aTable, selection);
+ }
+
+ // Delete the row by placing caret in cell we were to delete
+ // We need to call DeleteTableRow to handle cells with rowspan
+ rv = DeleteRow(aTable, startRowIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Note that we don't incremenet rowIndex
+ // since a row was deleted and "next"
+ // row now has current rowIndex
+ } else {
+ // A more "normal" deletion
+ rv = DeleteNode(cell);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ //Skip over any rows spanned by this cell
+ rowIndex += actualRowSpan;
+ }
+ }
+ }
+ } while (cell);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::DeleteTableRow(int32_t aNumber)
+{
+ RefPtr<Selection> selection;
+ nsCOMPtr<nsIDOMElement> table;
+ nsCOMPtr<nsIDOMElement> cell;
+ int32_t startRowIndex, startColIndex;
+ int32_t rowCount, colCount;
+ nsresult rv = GetCellContext(getter_AddRefs(selection),
+ getter_AddRefs(table),
+ getter_AddRefs(cell),
+ nullptr, nullptr,
+ &startRowIndex, &startColIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Don't fail if no cell found
+ NS_ENSURE_TRUE(cell, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND);
+
+ rv = GetTableSize(table, &rowCount, &colCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Shortcut the case of deleting all rows in table
+ if (!startRowIndex && aNumber >= rowCount) {
+ return DeleteTable2(table, selection);
+ }
+
+ AutoEditBatch beginBatching(this);
+ // Prevent rules testing until we're done
+ AutoRules beginRulesSniffing(this, EditAction::deleteNode, nsIEditor::eNext);
+
+ nsCOMPtr<nsIDOMElement> firstCell;
+ nsCOMPtr<nsIDOMRange> range;
+ rv = GetFirstSelectedCell(getter_AddRefs(range), getter_AddRefs(firstCell));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t rangeCount;
+ rv = selection->GetRangeCount(&rangeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (firstCell && rangeCount > 1) {
+ // Fetch indexes again - may be different for selected cells
+ rv = GetCellIndexes(firstCell, &startRowIndex, &startColIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ //We control selection resetting after the insert...
+ AutoSelectionSetterAfterTableEdit setCaret(this, table, startRowIndex,
+ startColIndex, ePreviousRow,
+ false);
+ // Don't change selection during deletions
+ AutoTransactionsConserveSelection dontChangeSelection(this);
+
+ if (firstCell && rangeCount > 1) {
+ // Use selected cells to determine what rows to delete
+ cell = firstCell;
+
+ while (cell) {
+ if (cell != firstCell) {
+ rv = GetCellIndexes(cell, &startRowIndex, &startColIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // Find the next cell in a different row
+ // to continue after we delete this row
+ int32_t nextRow = startRowIndex;
+ while (nextRow == startRowIndex) {
+ rv = GetNextSelectedCell(getter_AddRefs(range), getter_AddRefs(cell));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!cell) break;
+ rv = GetCellIndexes(cell, &nextRow, &startColIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // Delete entire row
+ rv = DeleteRow(table, startRowIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ } else {
+ // Check for counts too high
+ aNumber = std::min(aNumber,(rowCount-startRowIndex));
+ for (int32_t i = 0; i < aNumber; i++) {
+ rv = DeleteRow(table, startRowIndex);
+ // If failed in current row, try the next
+ if (NS_FAILED(rv)) {
+ startRowIndex++;
+ }
+
+ // Check if there's a cell in the "next" row
+ rv = GetCellAt(table, startRowIndex, startColIndex, getter_AddRefs(cell));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!cell) {
+ break;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+// Helper that doesn't batch or change the selection
+NS_IMETHODIMP
+HTMLEditor::DeleteRow(nsIDOMElement* aTable,
+ int32_t aRowIndex)
+{
+ NS_ENSURE_TRUE(aTable, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsIDOMElement> cell;
+ nsCOMPtr<nsIDOMElement> cellInDeleteRow;
+ int32_t startRowIndex, startColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan;
+ bool isSelected;
+ int32_t colIndex = 0;
+
+ // Prevent rules testing until we're done
+ AutoRules beginRulesSniffing(this, EditAction::deleteNode, nsIEditor::eNext);
+
+ // The list of cells we will change rowspan in
+ // and the new rowspan values for each
+ nsTArray<nsCOMPtr<nsIDOMElement> > spanCellList;
+ nsTArray<int32_t> newSpanList;
+
+ int32_t rowCount, colCount;
+ nsresult rv = GetTableSize(aTable, &rowCount, &colCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Scan through cells in row to do rowspan adjustments
+ // Note that after we delete row, startRowIndex will point to the
+ // cells in the next row to be deleted
+ do {
+ if (aRowIndex >= rowCount || colIndex >= colCount) {
+ break;
+ }
+
+ rv = GetCellDataAt(aTable, aRowIndex, colIndex, getter_AddRefs(cell),
+ &startRowIndex, &startColIndex, &rowSpan, &colSpan,
+ &actualRowSpan, &actualColSpan, &isSelected);
+ // We don't fail if we don't find a cell, so this must be real bad
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Compensate for cells that don't start or extend below the row we are deleting
+ if (cell) {
+ if (startRowIndex < aRowIndex) {
+ // Cell starts in row above us
+ // Decrease its rowspan to keep table rectangular
+ // but we don't need to do this if rowspan=0,
+ // since it will automatically adjust
+ if (rowSpan > 0) {
+ // Build list of cells to change rowspan
+ // We can't do it now since it upsets cell map,
+ // so we will do it after deleting the row
+ spanCellList.AppendElement(cell);
+ newSpanList.AppendElement(std::max((aRowIndex - startRowIndex), actualRowSpan-1));
+ }
+ } else {
+ if (rowSpan > 1) {
+ // Cell spans below row to delete, so we must insert new cells to
+ // keep rows below. Note that we test "rowSpan" so we don't do this
+ // if rowSpan = 0 (automatic readjustment).
+ int32_t aboveRowToInsertNewCellInto = aRowIndex - startRowIndex + 1;
+ int32_t numOfRawSpanRemainingBelow = actualRowSpan - 1;
+ rv = SplitCellIntoRows(aTable, startRowIndex, startColIndex,
+ aboveRowToInsertNewCellInto,
+ numOfRawSpanRemainingBelow, nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ if (!cellInDeleteRow) {
+ cellInDeleteRow = cell; // Reference cell to find row to delete
+ }
+ }
+ // Skip over other columns spanned by this cell
+ colIndex += actualColSpan;
+ }
+ } while (cell);
+
+ // Things are messed up if we didn't find a cell in the row!
+ NS_ENSURE_TRUE(cellInDeleteRow, NS_ERROR_FAILURE);
+
+ // Delete the entire row
+ nsCOMPtr<nsIDOMElement> parentRow;
+ rv = GetElementOrParentByTagName(NS_LITERAL_STRING("tr"), cellInDeleteRow,
+ getter_AddRefs(parentRow));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (parentRow) {
+ rv = DeleteNode(parentRow);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Now we can set new rowspans for cells stored above
+ for (uint32_t i = 0, n = spanCellList.Length(); i < n; i++) {
+ nsIDOMElement *cellPtr = spanCellList[i];
+ if (cellPtr) {
+ rv = SetRowSpan(cellPtr, newSpanList[i]);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+HTMLEditor::SelectTable()
+{
+ nsCOMPtr<nsIDOMElement> table;
+ nsresult rv = GetElementOrParentByTagName(NS_LITERAL_STRING("table"), nullptr,
+ getter_AddRefs(table));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Don't fail if we didn't find a table
+ NS_ENSURE_TRUE(table, NS_OK);
+
+ rv = ClearSelection();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ return AppendNodeToSelectionAsRange(table);
+}
+
+NS_IMETHODIMP
+HTMLEditor::SelectTableCell()
+{
+ nsCOMPtr<nsIDOMElement> cell;
+ nsresult rv = GetElementOrParentByTagName(NS_LITERAL_STRING("td"), nullptr,
+ getter_AddRefs(cell));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(cell, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND);
+
+ rv = ClearSelection();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ return AppendNodeToSelectionAsRange(cell);
+}
+
+NS_IMETHODIMP
+HTMLEditor::SelectBlockOfCells(nsIDOMElement* aStartCell,
+ nsIDOMElement* aEndCell)
+{
+ NS_ENSURE_TRUE(aStartCell && aEndCell, NS_ERROR_NULL_POINTER);
+
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE);
+
+ NS_NAMED_LITERAL_STRING(tableStr, "table");
+ nsCOMPtr<nsIDOMElement> table;
+ nsresult rv = GetElementOrParentByTagName(tableStr, aStartCell,
+ getter_AddRefs(table));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(table, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIDOMElement> endTable;
+ rv = GetElementOrParentByTagName(tableStr, aEndCell,
+ getter_AddRefs(endTable));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(endTable, NS_ERROR_FAILURE);
+
+ // We can only select a block if within the same table,
+ // so do nothing if not within one table
+ if (table != endTable) {
+ return NS_OK;
+ }
+
+ int32_t startRowIndex, startColIndex, endRowIndex, endColIndex;
+
+ // Get starting and ending cells' location in the cellmap
+ rv = GetCellIndexes(aStartCell, &startRowIndex, &startColIndex);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ rv = GetCellIndexes(aEndCell, &endRowIndex, &endColIndex);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Suppress nsISelectionListener notification
+ // until all selection changes are finished
+ SelectionBatcher selectionBatcher(selection);
+
+ // Examine all cell nodes in current selection and
+ // remove those outside the new block cell region
+ int32_t minColumn = std::min(startColIndex, endColIndex);
+ int32_t minRow = std::min(startRowIndex, endRowIndex);
+ int32_t maxColumn = std::max(startColIndex, endColIndex);
+ int32_t maxRow = std::max(startRowIndex, endRowIndex);
+
+ nsCOMPtr<nsIDOMElement> cell;
+ int32_t currentRowIndex, currentColIndex;
+ nsCOMPtr<nsIDOMRange> range;
+ rv = GetFirstSelectedCell(getter_AddRefs(range), getter_AddRefs(cell));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (rv == NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND) {
+ return NS_OK;
+ }
+
+ while (cell) {
+ rv = GetCellIndexes(cell, &currentRowIndex, &currentColIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (currentRowIndex < maxRow || currentRowIndex > maxRow ||
+ currentColIndex < maxColumn || currentColIndex > maxColumn) {
+ selection->RemoveRange(range);
+ // Since we've removed the range, decrement pointer to next range
+ mSelectedCellIndex--;
+ }
+ rv = GetNextSelectedCell(getter_AddRefs(range), getter_AddRefs(cell));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ int32_t rowSpan, colSpan, actualRowSpan, actualColSpan;
+ bool isSelected;
+ for (int32_t row = minRow; row <= maxRow; row++) {
+ for (int32_t col = minColumn; col <= maxColumn;
+ col += std::max(actualColSpan, 1)) {
+ rv = GetCellDataAt(table, row, col, getter_AddRefs(cell),
+ &currentRowIndex, &currentColIndex,
+ &rowSpan, &colSpan,
+ &actualRowSpan, &actualColSpan, &isSelected);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ // Skip cells that already selected or are spanned from previous locations
+ if (!isSelected && cell &&
+ row == currentRowIndex && col == currentColIndex) {
+ rv = AppendNodeToSelectionAsRange(cell);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ }
+ }
+ }
+ // NS_OK, otherwise, the last failure of GetCellDataAt() or
+ // AppendNodeToSelectionAsRange().
+ return rv;
+}
+
+NS_IMETHODIMP
+HTMLEditor::SelectAllTableCells()
+{
+ nsCOMPtr<nsIDOMElement> cell;
+ nsresult rv = GetElementOrParentByTagName(NS_LITERAL_STRING("td"), nullptr,
+ getter_AddRefs(cell));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Don't fail if we didn't find a cell
+ NS_ENSURE_TRUE(cell, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND);
+
+ nsCOMPtr<nsIDOMElement> startCell = cell;
+
+ // Get parent table
+ nsCOMPtr<nsIDOMElement> table;
+ rv = GetElementOrParentByTagName(NS_LITERAL_STRING("table"), cell,
+ getter_AddRefs(table));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!table) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ int32_t rowCount, colCount;
+ rv = GetTableSize(table, &rowCount, &colCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE);
+
+ // Suppress nsISelectionListener notification
+ // until all selection changes are finished
+ SelectionBatcher selectionBatcher(selection);
+
+ // It is now safe to clear the selection
+ // BE SURE TO RESET IT BEFORE LEAVING!
+ rv = ClearSelection();
+
+ // Select all cells in the same column as current cell
+ bool cellSelected = false;
+ int32_t rowSpan, colSpan, actualRowSpan, actualColSpan, currentRowIndex, currentColIndex;
+ bool isSelected;
+ for (int32_t row = 0; row < rowCount; row++) {
+ for (int32_t col = 0; col < colCount; col += std::max(actualColSpan, 1)) {
+ rv = GetCellDataAt(table, row, col, getter_AddRefs(cell),
+ &currentRowIndex, &currentColIndex,
+ &rowSpan, &colSpan,
+ &actualRowSpan, &actualColSpan, &isSelected);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ // Skip cells that are spanned from previous rows or columns
+ if (cell && row == currentRowIndex && col == currentColIndex) {
+ rv = AppendNodeToSelectionAsRange(cell);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ cellSelected = true;
+ }
+ }
+ }
+ // Safety code to select starting cell if nothing else was selected
+ if (!cellSelected) {
+ return AppendNodeToSelectionAsRange(startCell);
+ }
+ // NS_OK, otherwise, the error of ClearSelection() when there is no column or
+ // the last failure of GetCellDataAt() or AppendNodeToSelectionAsRange().
+ return rv;
+}
+
+NS_IMETHODIMP
+HTMLEditor::SelectTableRow()
+{
+ nsCOMPtr<nsIDOMElement> cell;
+ nsresult rv = GetElementOrParentByTagName(NS_LITERAL_STRING("td"), nullptr,
+ getter_AddRefs(cell));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Don't fail if we didn't find a cell
+ NS_ENSURE_TRUE(cell, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND);
+ nsCOMPtr<nsIDOMElement> startCell = cell;
+
+ // Get table and location of cell:
+ RefPtr<Selection> selection;
+ nsCOMPtr<nsIDOMElement> table;
+ int32_t startRowIndex, startColIndex;
+
+ rv = GetCellContext(getter_AddRefs(selection),
+ getter_AddRefs(table),
+ getter_AddRefs(cell),
+ nullptr, nullptr,
+ &startRowIndex, &startColIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(table, NS_ERROR_FAILURE);
+
+ int32_t rowCount, colCount;
+ rv = GetTableSize(table, &rowCount, &colCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ //Note: At this point, we could get first and last cells in row,
+ // then call SelectBlockOfCells, but that would take just
+ // a little less code, so the following is more efficient
+
+ // Suppress nsISelectionListener notification
+ // until all selection changes are finished
+ SelectionBatcher selectionBatcher(selection);
+
+ // It is now safe to clear the selection
+ // BE SURE TO RESET IT BEFORE LEAVING!
+ rv = ClearSelection();
+
+ // Select all cells in the same row as current cell
+ bool cellSelected = false;
+ int32_t rowSpan, colSpan, actualRowSpan, actualColSpan, currentRowIndex, currentColIndex;
+ bool isSelected;
+ for (int32_t col = 0; col < colCount; col += std::max(actualColSpan, 1)) {
+ rv = GetCellDataAt(table, startRowIndex, col, getter_AddRefs(cell),
+ &currentRowIndex, &currentColIndex, &rowSpan, &colSpan,
+ &actualRowSpan, &actualColSpan, &isSelected);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ // Skip cells that are spanned from previous rows or columns
+ if (cell && currentRowIndex == startRowIndex && currentColIndex == col) {
+ rv = AppendNodeToSelectionAsRange(cell);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ cellSelected = true;
+ }
+ }
+ // Safety code to select starting cell if nothing else was selected
+ if (!cellSelected) {
+ return AppendNodeToSelectionAsRange(startCell);
+ }
+ // NS_OK, otherwise, the error of ClearSelection() when there is no column or
+ // the last failure of GetCellDataAt() or AppendNodeToSelectionAsRange().
+ return rv;
+}
+
+NS_IMETHODIMP
+HTMLEditor::SelectTableColumn()
+{
+ nsCOMPtr<nsIDOMElement> cell;
+ nsresult rv = GetElementOrParentByTagName(NS_LITERAL_STRING("td"), nullptr,
+ getter_AddRefs(cell));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Don't fail if we didn't find a cell
+ NS_ENSURE_TRUE(cell, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND);
+
+ nsCOMPtr<nsIDOMElement> startCell = cell;
+
+ // Get location of cell:
+ RefPtr<Selection> selection;
+ nsCOMPtr<nsIDOMElement> table;
+ int32_t startRowIndex, startColIndex;
+
+ rv = GetCellContext(getter_AddRefs(selection),
+ getter_AddRefs(table),
+ getter_AddRefs(cell),
+ nullptr, nullptr,
+ &startRowIndex, &startColIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(table, NS_ERROR_FAILURE);
+
+ int32_t rowCount, colCount;
+ rv = GetTableSize(table, &rowCount, &colCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Suppress nsISelectionListener notification
+ // until all selection changes are finished
+ SelectionBatcher selectionBatcher(selection);
+
+ // It is now safe to clear the selection
+ // BE SURE TO RESET IT BEFORE LEAVING!
+ rv = ClearSelection();
+
+ // Select all cells in the same column as current cell
+ bool cellSelected = false;
+ int32_t rowSpan, colSpan, actualRowSpan, actualColSpan, currentRowIndex, currentColIndex;
+ bool isSelected;
+ for (int32_t row = 0; row < rowCount; row += std::max(actualRowSpan, 1)) {
+ rv = GetCellDataAt(table, row, startColIndex, getter_AddRefs(cell),
+ &currentRowIndex, &currentColIndex, &rowSpan, &colSpan,
+ &actualRowSpan, &actualColSpan, &isSelected);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ // Skip cells that are spanned from previous rows or columns
+ if (cell && currentRowIndex == row && currentColIndex == startColIndex) {
+ rv = AppendNodeToSelectionAsRange(cell);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ cellSelected = true;
+ }
+ }
+ // Safety code to select starting cell if nothing else was selected
+ if (!cellSelected) {
+ return AppendNodeToSelectionAsRange(startCell);
+ }
+ // NS_OK, otherwise, the error of ClearSelection() when there is no row or
+ // the last failure of GetCellDataAt() or AppendNodeToSelectionAsRange().
+ return rv;
+}
+
+NS_IMETHODIMP
+HTMLEditor::SplitTableCell()
+{
+ nsCOMPtr<nsIDOMElement> table;
+ nsCOMPtr<nsIDOMElement> cell;
+ int32_t startRowIndex, startColIndex, actualRowSpan, actualColSpan;
+ // Get cell, table, etc. at selection anchor node
+ nsresult rv = GetCellContext(nullptr,
+ getter_AddRefs(table),
+ getter_AddRefs(cell),
+ nullptr, nullptr,
+ &startRowIndex, &startColIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!table || !cell) {
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+ }
+
+ // We need rowspan and colspan data
+ rv = GetCellSpansAt(table, startRowIndex, startColIndex,
+ actualRowSpan, actualColSpan);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Must have some span to split
+ if (actualRowSpan <= 1 && actualColSpan <= 1) {
+ return NS_OK;
+ }
+
+ AutoEditBatch beginBatching(this);
+ // Prevent auto insertion of BR in new cell until we're done
+ AutoRules beginRulesSniffing(this, EditAction::insertNode, nsIEditor::eNext);
+
+ // We reset selection
+ AutoSelectionSetterAfterTableEdit setCaret(this, table, startRowIndex,
+ startColIndex, ePreviousColumn,
+ false);
+ //...so suppress Rules System selection munging
+ AutoTransactionsConserveSelection dontChangeSelection(this);
+
+ nsCOMPtr<nsIDOMElement> newCell;
+ int32_t rowIndex = startRowIndex;
+ int32_t rowSpanBelow, colSpanAfter;
+
+ // Split up cell row-wise first into rowspan=1 above, and the rest below,
+ // whittling away at the cell below until no more extra span
+ for (rowSpanBelow = actualRowSpan-1; rowSpanBelow >= 0; rowSpanBelow--) {
+ // We really split row-wise only if we had rowspan > 1
+ if (rowSpanBelow > 0) {
+ rv = SplitCellIntoRows(table, rowIndex, startColIndex, 1, rowSpanBelow,
+ getter_AddRefs(newCell));
+ NS_ENSURE_SUCCESS(rv, rv);
+ CopyCellBackgroundColor(newCell, cell);
+ }
+ int32_t colIndex = startColIndex;
+ // Now split the cell with rowspan = 1 into cells if it has colSpan > 1
+ for (colSpanAfter = actualColSpan-1; colSpanAfter > 0; colSpanAfter--) {
+ rv = SplitCellIntoColumns(table, rowIndex, colIndex, 1, colSpanAfter,
+ getter_AddRefs(newCell));
+ NS_ENSURE_SUCCESS(rv, rv);
+ CopyCellBackgroundColor(newCell, cell);
+ colIndex++;
+ }
+ // Point to the new cell and repeat
+ rowIndex++;
+ }
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::CopyCellBackgroundColor(nsIDOMElement* destCell,
+ nsIDOMElement* sourceCell)
+{
+ NS_ENSURE_TRUE(destCell && sourceCell, NS_ERROR_NULL_POINTER);
+
+ // Copy backgournd color to new cell
+ NS_NAMED_LITERAL_STRING(bgcolor, "bgcolor");
+ nsAutoString color;
+ bool isSet;
+ nsresult rv = GetAttributeValue(sourceCell, bgcolor, color, &isSet);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (!isSet) {
+ return NS_OK;
+ }
+ return SetAttribute(destCell, bgcolor, color);
+}
+
+NS_IMETHODIMP
+HTMLEditor::SplitCellIntoColumns(nsIDOMElement* aTable,
+ int32_t aRowIndex,
+ int32_t aColIndex,
+ int32_t aColSpanLeft,
+ int32_t aColSpanRight,
+ nsIDOMElement** aNewCell)
+{
+ NS_ENSURE_TRUE(aTable, NS_ERROR_NULL_POINTER);
+ if (aNewCell) {
+ *aNewCell = nullptr;
+ }
+
+ nsCOMPtr<nsIDOMElement> cell;
+ int32_t startRowIndex, startColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan;
+ bool isSelected;
+ nsresult rv =
+ GetCellDataAt(aTable, aRowIndex, aColIndex, getter_AddRefs(cell),
+ &startRowIndex, &startColIndex,
+ &rowSpan, &colSpan,
+ &actualRowSpan, &actualColSpan, &isSelected);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(cell, NS_ERROR_NULL_POINTER);
+
+ // We can't split!
+ if (actualColSpan <= 1 || (aColSpanLeft + aColSpanRight) > actualColSpan) {
+ return NS_OK;
+ }
+
+ // Reduce colspan of cell to split
+ rv = SetColSpan(cell, aColSpanLeft);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Insert new cell after using the remaining span
+ // and always get the new cell so we can copy the background color;
+ nsCOMPtr<nsIDOMElement> newCell;
+ rv = InsertCell(cell, actualRowSpan, aColSpanRight, true, false,
+ getter_AddRefs(newCell));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!newCell) {
+ return NS_OK;
+ }
+ if (aNewCell) {
+ NS_ADDREF(*aNewCell = newCell.get());
+ }
+ return CopyCellBackgroundColor(newCell, cell);
+}
+
+NS_IMETHODIMP
+HTMLEditor::SplitCellIntoRows(nsIDOMElement* aTable,
+ int32_t aRowIndex,
+ int32_t aColIndex,
+ int32_t aRowSpanAbove,
+ int32_t aRowSpanBelow,
+ nsIDOMElement** aNewCell)
+{
+ NS_ENSURE_TRUE(aTable, NS_ERROR_NULL_POINTER);
+ if (aNewCell) *aNewCell = nullptr;
+
+ nsCOMPtr<nsIDOMElement> cell;
+ int32_t startRowIndex, startColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan;
+ bool isSelected;
+ nsresult rv =
+ GetCellDataAt(aTable, aRowIndex, aColIndex, getter_AddRefs(cell),
+ &startRowIndex, &startColIndex,
+ &rowSpan, &colSpan,
+ &actualRowSpan, &actualColSpan, &isSelected);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(cell, NS_ERROR_NULL_POINTER);
+
+ // We can't split!
+ if (actualRowSpan <= 1 || (aRowSpanAbove + aRowSpanBelow) > actualRowSpan) {
+ return NS_OK;
+ }
+
+ int32_t rowCount, colCount;
+ rv = GetTableSize(aTable, &rowCount, &colCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDOMElement> cell2;
+ nsCOMPtr<nsIDOMElement> lastCellFound;
+ int32_t startRowIndex2, startColIndex2, rowSpan2, colSpan2, actualRowSpan2, actualColSpan2;
+ bool isSelected2;
+ int32_t colIndex = 0;
+ bool insertAfter = (startColIndex > 0);
+ // This is the row we will insert new cell into
+ int32_t rowBelowIndex = startRowIndex+aRowSpanAbove;
+
+ // Find a cell to insert before or after
+ for (;;) {
+ // Search for a cell to insert before
+ rv = GetCellDataAt(aTable, rowBelowIndex,
+ colIndex, getter_AddRefs(cell2),
+ &startRowIndex2, &startColIndex2, &rowSpan2, &colSpan2,
+ &actualRowSpan2, &actualColSpan2, &isSelected2);
+ // If we fail here, it could be because row has bad rowspan values,
+ // such as all cells having rowspan > 1 (Call FixRowSpan first!)
+ if (NS_FAILED(rv) || !cell) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Skip over cells spanned from above (like the one we are splitting!)
+ if (cell2 && startRowIndex2 == rowBelowIndex) {
+ if (!insertAfter) {
+ // Inserting before, so stop at first cell in row we want to insert
+ // into.
+ break;
+ }
+ // New cell isn't first in row,
+ // so stop after we find the cell just before new cell's column
+ if (startColIndex2 + actualColSpan2 == startColIndex) {
+ break;
+ }
+ // If cell found is AFTER desired new cell colum,
+ // we have multiple cells with rowspan > 1 that
+ // prevented us from finding a cell to insert after...
+ if (startColIndex2 > startColIndex) {
+ // ... so instead insert before the cell we found
+ insertAfter = false;
+ break;
+ }
+ lastCellFound = cell2;
+ }
+ // Skip to next available cellmap location
+ colIndex += std::max(actualColSpan2, 1);
+
+ // Done when past end of total number of columns
+ if (colIndex > colCount) {
+ break;
+ }
+ }
+
+ if (!cell2 && lastCellFound) {
+ // Edge case where we didn't find a cell to insert after
+ // or before because column(s) before desired column
+ // and all columns after it are spanned from above.
+ // We can insert after the last cell we found
+ cell2 = lastCellFound;
+ insertAfter = true; // Should always be true, but let's be sure
+ }
+
+ // Reduce rowspan of cell to split
+ rv = SetRowSpan(cell, aRowSpanAbove);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+
+ // Insert new cell after using the remaining span
+ // and always get the new cell so we can copy the background color;
+ nsCOMPtr<nsIDOMElement> newCell;
+ rv = InsertCell(cell2, aRowSpanBelow, actualColSpan, insertAfter, false,
+ getter_AddRefs(newCell));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!newCell) {
+ return NS_OK;
+ }
+ if (aNewCell) {
+ NS_ADDREF(*aNewCell = newCell.get());
+ }
+ return CopyCellBackgroundColor(newCell, cell2);
+}
+
+NS_IMETHODIMP
+HTMLEditor::SwitchTableCellHeaderType(nsIDOMElement* aSourceCell,
+ nsIDOMElement** aNewCell)
+{
+ nsCOMPtr<Element> sourceCell = do_QueryInterface(aSourceCell);
+ NS_ENSURE_TRUE(sourceCell, NS_ERROR_NULL_POINTER);
+
+ AutoEditBatch beginBatching(this);
+ // Prevent auto insertion of BR in new cell created by ReplaceContainer
+ AutoRules beginRulesSniffing(this, EditAction::insertNode, nsIEditor::eNext);
+
+ // Save current selection to restore when done
+ // This is needed so ReplaceContainer can monitor selection
+ // when replacing nodes
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE);
+ AutoSelectionRestorer selectionRestorer(selection, this);
+
+ // Set to the opposite of current type
+ nsCOMPtr<nsIAtom> atom = EditorBase::GetTag(aSourceCell);
+ nsIAtom* newCellType = atom == nsGkAtoms::td ? nsGkAtoms::th : nsGkAtoms::td;
+
+ // This creates new node, moves children, copies attributes (true)
+ // and manages the selection!
+ nsCOMPtr<Element> newNode = ReplaceContainer(sourceCell, newCellType,
+ nullptr, nullptr, EditorBase::eCloneAttributes);
+ NS_ENSURE_TRUE(newNode, NS_ERROR_FAILURE);
+
+ // Return the new cell
+ if (aNewCell) {
+ nsCOMPtr<nsIDOMElement> newElement = do_QueryInterface(newNode);
+ *aNewCell = newElement.get();
+ NS_ADDREF(*aNewCell);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::JoinTableCells(bool aMergeNonContiguousContents)
+{
+ nsCOMPtr<nsIDOMElement> table;
+ nsCOMPtr<nsIDOMElement> targetCell;
+ int32_t startRowIndex, startColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan;
+ bool isSelected;
+ nsCOMPtr<nsIDOMElement> cell2;
+ int32_t startRowIndex2, startColIndex2, rowSpan2, colSpan2, actualRowSpan2, actualColSpan2;
+ bool isSelected2;
+
+ // Get cell, table, etc. at selection anchor node
+ nsresult rv = GetCellContext(nullptr,
+ getter_AddRefs(table),
+ getter_AddRefs(targetCell),
+ nullptr, nullptr,
+ &startRowIndex, &startColIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!table || !targetCell) {
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+ }
+
+ AutoEditBatch beginBatching(this);
+ //Don't let Rules System change the selection
+ AutoTransactionsConserveSelection dontChangeSelection(this);
+
+ // Note: We dont' use AutoSelectionSetterAfterTableEdit here so the selection
+ // is retained after joining. This leaves the target cell selected
+ // as well as the "non-contiguous" cells, so user can see what happened.
+
+ nsCOMPtr<nsIDOMElement> firstCell;
+ int32_t firstRowIndex, firstColIndex;
+ rv = GetFirstSelectedCellInTable(&firstRowIndex, &firstColIndex,
+ getter_AddRefs(firstCell));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool joinSelectedCells = false;
+ if (firstCell) {
+ nsCOMPtr<nsIDOMElement> secondCell;
+ rv = GetNextSelectedCell(nullptr, getter_AddRefs(secondCell));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If only one cell is selected, join with cell to the right
+ joinSelectedCells = (secondCell != nullptr);
+ }
+
+ if (joinSelectedCells) {
+ // We have selected cells: Join just contiguous cells
+ // and just merge contents if not contiguous
+
+ int32_t rowCount, colCount;
+ rv = GetTableSize(table, &rowCount, &colCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get spans for cell we will merge into
+ int32_t firstRowSpan, firstColSpan;
+ rv = GetCellSpansAt(table, firstRowIndex, firstColIndex,
+ firstRowSpan, firstColSpan);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // This defines the last indexes along the "edges"
+ // of the contiguous block of cells, telling us
+ // that we can join adjacent cells to the block
+ // Start with same as the first values,
+ // then expand as we find adjacent selected cells
+ int32_t lastRowIndex = firstRowIndex;
+ int32_t lastColIndex = firstColIndex;
+ int32_t rowIndex, colIndex;
+
+ // First pass: Determine boundaries of contiguous rectangular block
+ // that we will join into one cell,
+ // favoring adjacent cells in the same row
+ for (rowIndex = firstRowIndex; rowIndex <= lastRowIndex; rowIndex++) {
+ int32_t currentRowCount = rowCount;
+ // Be sure each row doesn't have rowspan errors
+ rv = FixBadRowSpan(table, rowIndex, rowCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Adjust rowcount by number of rows we removed
+ lastRowIndex -= (currentRowCount-rowCount);
+
+ bool cellFoundInRow = false;
+ bool lastRowIsSet = false;
+ int32_t lastColInRow = 0;
+ int32_t firstColInRow = firstColIndex;
+ for (colIndex = firstColIndex; colIndex < colCount;
+ colIndex += std::max(actualColSpan2, 1)) {
+ rv = GetCellDataAt(table, rowIndex, colIndex, getter_AddRefs(cell2),
+ &startRowIndex2, &startColIndex2,
+ &rowSpan2, &colSpan2,
+ &actualRowSpan2, &actualColSpan2, &isSelected2);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (isSelected2) {
+ if (!cellFoundInRow) {
+ // We've just found the first selected cell in this row
+ firstColInRow = colIndex;
+ }
+ if (rowIndex > firstRowIndex && firstColInRow != firstColIndex) {
+ // We're in at least the second row,
+ // but left boundary is "ragged" (not the same as 1st row's start)
+ //Let's just end block on previous row
+ // and keep previous lastColIndex
+ //TODO: We could try to find the Maximum firstColInRow
+ // so our block can still extend down more rows?
+ lastRowIndex = std::max(0,rowIndex - 1);
+ lastRowIsSet = true;
+ break;
+ }
+ // Save max selected column in this row, including extra colspan
+ lastColInRow = colIndex + (actualColSpan2-1);
+ cellFoundInRow = true;
+ } else if (cellFoundInRow) {
+ // No cell or not selected, but at least one cell in row was found
+ if (rowIndex > (firstRowIndex + 1) && colIndex <= lastColIndex) {
+ // Cell is in a column less than current right border in
+ // the third or higher selected row, so stop block at the previous row
+ lastRowIndex = std::max(0,rowIndex - 1);
+ lastRowIsSet = true;
+ }
+ // We're done with this row
+ break;
+ }
+ } // End of column loop
+
+ // Done with this row
+ if (cellFoundInRow) {
+ if (rowIndex == firstRowIndex) {
+ // First row always initializes the right boundary
+ lastColIndex = lastColInRow;
+ }
+
+ // If we didn't determine last row above...
+ if (!lastRowIsSet) {
+ if (colIndex < lastColIndex) {
+ // (don't think we ever get here?)
+ // Cell is in a column less than current right boundary,
+ // so stop block at the previous row
+ lastRowIndex = std::max(0,rowIndex - 1);
+ } else {
+ // Go on to examine next row
+ lastRowIndex = rowIndex+1;
+ }
+ }
+ // Use the minimum col we found so far for right boundary
+ lastColIndex = std::min(lastColIndex, lastColInRow);
+ } else {
+ // No selected cells in this row -- stop at row above
+ // and leave last column at its previous value
+ lastRowIndex = std::max(0,rowIndex - 1);
+ }
+ }
+
+ // The list of cells we will delete after joining
+ nsTArray<nsCOMPtr<nsIDOMElement> > deleteList;
+
+ // 2nd pass: Do the joining and merging
+ for (rowIndex = 0; rowIndex < rowCount; rowIndex++) {
+ for (colIndex = 0; colIndex < colCount;
+ colIndex += std::max(actualColSpan2, 1)) {
+ rv = GetCellDataAt(table, rowIndex, colIndex, getter_AddRefs(cell2),
+ &startRowIndex2, &startColIndex2,
+ &rowSpan2, &colSpan2,
+ &actualRowSpan2, &actualColSpan2, &isSelected2);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If this is 0, we are past last cell in row, so exit the loop
+ if (!actualColSpan2) {
+ break;
+ }
+
+ // Merge only selected cells (skip cell we're merging into, of course)
+ if (isSelected2 && cell2 != firstCell) {
+ if (rowIndex >= firstRowIndex && rowIndex <= lastRowIndex &&
+ colIndex >= firstColIndex && colIndex <= lastColIndex) {
+ // We are within the join region
+ // Problem: It is very tricky to delete cells as we merge,
+ // since that will upset the cellmap
+ // Instead, build a list of cells to delete and do it later
+ NS_ASSERTION(startRowIndex2 == rowIndex, "JoinTableCells: StartRowIndex is in row above");
+
+ if (actualColSpan2 > 1) {
+ //Check if cell "hangs" off the boundary because of colspan > 1
+ // Use split methods to chop off excess
+ int32_t extraColSpan = (startColIndex2 + actualColSpan2) - (lastColIndex+1);
+ if ( extraColSpan > 0) {
+ rv = SplitCellIntoColumns(table, startRowIndex2, startColIndex2,
+ actualColSpan2 - extraColSpan,
+ extraColSpan, nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ rv = MergeCells(firstCell, cell2, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Add cell to list to delete
+ deleteList.AppendElement(cell2.get());
+ } else if (aMergeNonContiguousContents) {
+ // Cell is outside join region -- just merge the contents
+ rv = MergeCells(firstCell, cell2, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ }
+ }
+
+ // All cell contents are merged. Delete the empty cells we accumulated
+ // Prevent rules testing until we're done
+ AutoRules beginRulesSniffing(this, EditAction::deleteNode,
+ nsIEditor::eNext);
+
+ for (uint32_t i = 0, n = deleteList.Length(); i < n; i++) {
+ nsIDOMElement *elementPtr = deleteList[i];
+ if (elementPtr) {
+ nsCOMPtr<nsIDOMNode> node = do_QueryInterface(elementPtr);
+ rv = DeleteNode(node);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ // Cleanup selection: remove ranges where cells were deleted
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE);
+
+ int32_t rangeCount;
+ rv = selection->GetRangeCount(&rangeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<nsRange> range;
+ for (int32_t i = 0; i < rangeCount; i++) {
+ range = selection->GetRangeAt(i);
+ NS_ENSURE_TRUE(range, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIDOMElement> deletedCell;
+ GetCellFromRange(range, getter_AddRefs(deletedCell));
+ if (!deletedCell) {
+ selection->RemoveRange(range);
+ rangeCount--;
+ i--;
+ }
+ }
+
+ // Set spans for the cell everthing merged into
+ rv = SetRowSpan(firstCell, lastRowIndex-firstRowIndex+1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetColSpan(firstCell, lastColIndex-firstColIndex+1);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+
+ // Fixup disturbances in table layout
+ NormalizeTable(table);
+ } else {
+ // Joining with cell to the right -- get rowspan and colspan data of target cell
+ rv = GetCellDataAt(table, startRowIndex, startColIndex,
+ getter_AddRefs(targetCell),
+ &startRowIndex, &startColIndex, &rowSpan, &colSpan,
+ &actualRowSpan, &actualColSpan, &isSelected);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(targetCell, NS_ERROR_NULL_POINTER);
+
+ // Get data for cell to the right
+ rv = GetCellDataAt(table, startRowIndex, startColIndex + actualColSpan,
+ getter_AddRefs(cell2),
+ &startRowIndex2, &startColIndex2, &rowSpan2, &colSpan2,
+ &actualRowSpan2, &actualColSpan2, &isSelected2);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!cell2) {
+ return NS_OK; // Don't fail if there's no cell
+ }
+
+ // sanity check
+ NS_ASSERTION((startRowIndex >= startRowIndex2),"JoinCells: startRowIndex < startRowIndex2");
+
+ // Figure out span of merged cell starting from target's starting row
+ // to handle case of merged cell starting in a row above
+ int32_t spanAboveMergedCell = startRowIndex - startRowIndex2;
+ int32_t effectiveRowSpan2 = actualRowSpan2 - spanAboveMergedCell;
+
+ if (effectiveRowSpan2 > actualRowSpan) {
+ // Cell to the right spans into row below target
+ // Split off portion below target cell's bottom-most row
+ rv = SplitCellIntoRows(table, startRowIndex2, startColIndex2,
+ spanAboveMergedCell+actualRowSpan,
+ effectiveRowSpan2-actualRowSpan, nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Move contents from cell to the right
+ // Delete the cell now only if it starts in the same row
+ // and has enough row "height"
+ rv = MergeCells(targetCell, cell2,
+ (startRowIndex2 == startRowIndex) &&
+ (effectiveRowSpan2 >= actualRowSpan));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (effectiveRowSpan2 < actualRowSpan) {
+ // Merged cell is "shorter"
+ // (there are cells(s) below it that are row-spanned by target cell)
+ // We could try splitting those cells, but that's REAL messy,
+ // so the safest thing to do is NOT really join the cells
+ return NS_OK;
+ }
+
+ if (spanAboveMergedCell > 0) {
+ // Cell we merged started in a row above the target cell
+ // Reduce rowspan to give room where target cell will extend its colspan
+ rv = SetRowSpan(cell2, spanAboveMergedCell);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Reset target cell's colspan to encompass cell to the right
+ rv = SetColSpan(targetCell, actualColSpan+actualColSpan2);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::MergeCells(nsCOMPtr<nsIDOMElement> aTargetCell,
+ nsCOMPtr<nsIDOMElement> aCellToMerge,
+ bool aDeleteCellToMerge)
+{
+ nsCOMPtr<dom::Element> targetCell = do_QueryInterface(aTargetCell);
+ nsCOMPtr<dom::Element> cellToMerge = do_QueryInterface(aCellToMerge);
+ NS_ENSURE_TRUE(targetCell && cellToMerge, NS_ERROR_NULL_POINTER);
+
+ // Prevent rules testing until we're done
+ AutoRules beginRulesSniffing(this, EditAction::deleteNode, nsIEditor::eNext);
+
+ // Don't need to merge if cell is empty
+ if (!IsEmptyCell(cellToMerge)) {
+ // Get index of last child in target cell
+ // If we fail or don't have children,
+ // we insert at index 0
+ int32_t insertIndex = 0;
+
+ // Start inserting just after last child
+ uint32_t len = targetCell->GetChildCount();
+ if (len == 1 && IsEmptyCell(targetCell)) {
+ // Delete the empty node
+ nsIContent* cellChild = targetCell->GetFirstChild();
+ nsresult rv = DeleteNode(cellChild->AsDOMNode());
+ NS_ENSURE_SUCCESS(rv, rv);
+ insertIndex = 0;
+ } else {
+ insertIndex = (int32_t)len;
+ }
+
+ // Move the contents
+ while (cellToMerge->HasChildren()) {
+ nsCOMPtr<nsIDOMNode> cellChild = cellToMerge->GetLastChild()->AsDOMNode();
+ nsresult rv = DeleteNode(cellChild);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = InsertNode(cellChild, aTargetCell, insertIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ // Delete cells whose contents were moved
+ if (aDeleteCellToMerge) {
+ return DeleteNode(aCellToMerge);
+ }
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+HTMLEditor::FixBadRowSpan(nsIDOMElement* aTable,
+ int32_t aRowIndex,
+ int32_t& aNewRowCount)
+{
+ NS_ENSURE_TRUE(aTable, NS_ERROR_NULL_POINTER);
+
+ int32_t rowCount, colCount;
+ nsresult rv = GetTableSize(aTable, &rowCount, &colCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDOMElement>cell;
+ int32_t startRowIndex, startColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan;
+ bool isSelected;
+
+ int32_t minRowSpan = -1;
+ int32_t colIndex;
+
+ for (colIndex = 0; colIndex < colCount;
+ colIndex += std::max(actualColSpan, 1)) {
+ rv = GetCellDataAt(aTable, aRowIndex, colIndex, getter_AddRefs(cell),
+ &startRowIndex, &startColIndex, &rowSpan, &colSpan,
+ &actualRowSpan, &actualColSpan, &isSelected);
+ // NOTE: This is a *real* failure.
+ // GetCellDataAt passes if cell is missing from cellmap
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (!cell) {
+ break;
+ }
+ if (rowSpan > 0 &&
+ startRowIndex == aRowIndex &&
+ (rowSpan < minRowSpan || minRowSpan == -1)) {
+ minRowSpan = rowSpan;
+ }
+ NS_ASSERTION((actualColSpan > 0),"ActualColSpan = 0 in FixBadRowSpan");
+ }
+ if (minRowSpan > 1) {
+ // The amount to reduce everyone's rowspan
+ // so at least one cell has rowspan = 1
+ int32_t rowsReduced = minRowSpan - 1;
+ for (colIndex = 0; colIndex < colCount;
+ colIndex += std::max(actualColSpan, 1)) {
+ rv = GetCellDataAt(aTable, aRowIndex, colIndex, getter_AddRefs(cell),
+ &startRowIndex, &startColIndex, &rowSpan, &colSpan,
+ &actualRowSpan, &actualColSpan, &isSelected);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ // Fixup rowspans only for cells starting in current row
+ if (cell && rowSpan > 0 &&
+ startRowIndex == aRowIndex &&
+ startColIndex == colIndex ) {
+ rv = SetRowSpan(cell, rowSpan-rowsReduced);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+ NS_ASSERTION((actualColSpan > 0),"ActualColSpan = 0 in FixBadRowSpan");
+ }
+ }
+ return GetTableSize(aTable, &aNewRowCount, &colCount);
+}
+
+NS_IMETHODIMP
+HTMLEditor::FixBadColSpan(nsIDOMElement* aTable,
+ int32_t aColIndex,
+ int32_t& aNewColCount)
+{
+ NS_ENSURE_TRUE(aTable, NS_ERROR_NULL_POINTER);
+
+ int32_t rowCount, colCount;
+ nsresult rv = GetTableSize(aTable, &rowCount, &colCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDOMElement> cell;
+ int32_t startRowIndex, startColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan;
+ bool isSelected;
+
+ int32_t minColSpan = -1;
+ int32_t rowIndex;
+
+ for (rowIndex = 0; rowIndex < rowCount;
+ rowIndex += std::max(actualRowSpan, 1)) {
+ rv = GetCellDataAt(aTable, rowIndex, aColIndex, getter_AddRefs(cell),
+ &startRowIndex, &startColIndex, &rowSpan, &colSpan,
+ &actualRowSpan, &actualColSpan, &isSelected);
+ // NOTE: This is a *real* failure.
+ // GetCellDataAt passes if cell is missing from cellmap
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (!cell) {
+ break;
+ }
+ if (colSpan > 0 &&
+ startColIndex == aColIndex &&
+ (colSpan < minColSpan || minColSpan == -1)) {
+ minColSpan = colSpan;
+ }
+ NS_ASSERTION((actualRowSpan > 0),"ActualRowSpan = 0 in FixBadColSpan");
+ }
+ if (minColSpan > 1) {
+ // The amount to reduce everyone's colspan
+ // so at least one cell has colspan = 1
+ int32_t colsReduced = minColSpan - 1;
+ for (rowIndex = 0; rowIndex < rowCount;
+ rowIndex += std::max(actualRowSpan, 1)) {
+ rv = GetCellDataAt(aTable, rowIndex, aColIndex, getter_AddRefs(cell),
+ &startRowIndex, &startColIndex, &rowSpan, &colSpan,
+ &actualRowSpan, &actualColSpan, &isSelected);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ // Fixup colspans only for cells starting in current column
+ if (cell && colSpan > 0 &&
+ startColIndex == aColIndex &&
+ startRowIndex == rowIndex) {
+ rv = SetColSpan(cell, colSpan-colsReduced);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+ NS_ASSERTION((actualRowSpan > 0),"ActualRowSpan = 0 in FixBadColSpan");
+ }
+ }
+ return GetTableSize(aTable, &rowCount, &aNewColCount);
+}
+
+NS_IMETHODIMP
+HTMLEditor::NormalizeTable(nsIDOMElement* aTable)
+{
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIDOMElement> table;
+ nsresult rv = GetElementOrParentByTagName(NS_LITERAL_STRING("table"),
+ aTable, getter_AddRefs(table));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Don't fail if we didn't find a table
+ NS_ENSURE_TRUE(table, NS_OK);
+
+ int32_t rowCount, colCount, rowIndex, colIndex;
+ rv = GetTableSize(table, &rowCount, &colCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Save current selection
+ AutoSelectionRestorer selectionRestorer(selection, this);
+
+ AutoEditBatch beginBatching(this);
+ // Prevent auto insertion of BR in new cell until we're done
+ AutoRules beginRulesSniffing(this, EditAction::insertNode, nsIEditor::eNext);
+
+ nsCOMPtr<nsIDOMElement> cell;
+ int32_t startRowIndex, startColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan;
+ bool isSelected;
+
+ // Scan all cells in each row to detect bad rowspan values
+ for (rowIndex = 0; rowIndex < rowCount; rowIndex++) {
+ rv = FixBadRowSpan(table, rowIndex, rowCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // and same for colspans
+ for (colIndex = 0; colIndex < colCount; colIndex++) {
+ rv = FixBadColSpan(table, colIndex, colCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Fill in missing cellmap locations with empty cells
+ for (rowIndex = 0; rowIndex < rowCount; rowIndex++) {
+ nsCOMPtr<nsIDOMElement> previousCellInRow;
+ for (colIndex = 0; colIndex < colCount; colIndex++) {
+ rv = GetCellDataAt(table, rowIndex, colIndex, getter_AddRefs(cell),
+ &startRowIndex, &startColIndex, &rowSpan, &colSpan,
+ &actualRowSpan, &actualColSpan, &isSelected);
+ // NOTE: This is a *real* failure.
+ // GetCellDataAt passes if cell is missing from cellmap
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (!cell) {
+ //We are missing a cell at a cellmap location
+#ifdef DEBUG
+ printf("NormalizeTable found missing cell at row=%d, col=%d\n",
+ rowIndex, colIndex);
+#endif
+ // Add a cell after the previous Cell in the current row
+ if (!previousCellInRow) {
+ // We don't have any cells in this row -- We are really messed up!
+#ifdef DEBUG
+ printf("NormalizeTable found no cells in row=%d, col=%d\n",
+ rowIndex, colIndex);
+#endif
+ return NS_ERROR_FAILURE;
+ }
+
+ // Insert a new cell after (true), and return the new cell to us
+ rv = InsertCell(previousCellInRow, 1, 1, true, false,
+ getter_AddRefs(cell));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Set this so we use returned new "cell" to set previousCellInRow below
+ if (cell) {
+ startRowIndex = rowIndex;
+ }
+ }
+ // Save the last cell found in the same row we are scanning
+ if (startRowIndex == rowIndex) {
+ previousCellInRow = cell;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetCellIndexes(nsIDOMElement* aCell,
+ int32_t* aRowIndex,
+ int32_t* aColIndex)
+{
+ NS_ENSURE_ARG_POINTER(aRowIndex);
+ *aColIndex=0; // initialize out params
+ NS_ENSURE_ARG_POINTER(aColIndex);
+ *aRowIndex=0;
+ if (!aCell) {
+ // Get the selected cell or the cell enclosing the selection anchor
+ nsCOMPtr<nsIDOMElement> cell;
+ nsresult rv = GetElementOrParentByTagName(NS_LITERAL_STRING("td"), nullptr,
+ getter_AddRefs(cell));
+ if (NS_FAILED(rv) || !cell) {
+ return NS_ERROR_FAILURE;
+ }
+ aCell = cell;
+ }
+
+ NS_ENSURE_TRUE(mDocWeak, NS_ERROR_NOT_INITIALIZED);
+ nsCOMPtr<nsIPresShell> ps = GetPresShell();
+ NS_ENSURE_TRUE(ps, NS_ERROR_NOT_INITIALIZED);
+
+ nsCOMPtr<nsIContent> nodeAsContent( do_QueryInterface(aCell) );
+ NS_ENSURE_TRUE(nodeAsContent, NS_ERROR_FAILURE);
+ // frames are not ref counted, so don't use an nsCOMPtr
+ nsIFrame *layoutObject = nodeAsContent->GetPrimaryFrame();
+ NS_ENSURE_TRUE(layoutObject, NS_ERROR_FAILURE);
+
+ nsITableCellLayout *cellLayoutObject = do_QueryFrame(layoutObject);
+ NS_ENSURE_TRUE(cellLayoutObject, NS_ERROR_FAILURE);
+ return cellLayoutObject->GetCellIndexes(*aRowIndex, *aColIndex);
+}
+
+nsTableWrapperFrame*
+HTMLEditor::GetTableFrame(nsIDOMElement* aTable)
+{
+ NS_ENSURE_TRUE(aTable, nullptr);
+
+ nsCOMPtr<nsIContent> nodeAsContent( do_QueryInterface(aTable) );
+ NS_ENSURE_TRUE(nodeAsContent, nullptr);
+ return do_QueryFrame(nodeAsContent->GetPrimaryFrame());
+}
+
+//Return actual number of cells (a cell with colspan > 1 counts as just 1)
+int32_t
+HTMLEditor::GetNumberOfCellsInRow(nsIDOMElement* aTable,
+ int32_t rowIndex)
+{
+ int32_t cellCount = 0;
+ nsCOMPtr<nsIDOMElement> cell;
+ int32_t colIndex = 0;
+ do {
+ int32_t startRowIndex, startColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan;
+ bool isSelected;
+ nsresult rv =
+ GetCellDataAt(aTable, rowIndex, colIndex, getter_AddRefs(cell),
+ &startRowIndex, &startColIndex, &rowSpan, &colSpan,
+ &actualRowSpan, &actualColSpan, &isSelected);
+ NS_ENSURE_SUCCESS(rv, 0);
+ if (cell) {
+ // Only count cells that start in row we are working with
+ if (startRowIndex == rowIndex) {
+ cellCount++;
+ }
+ //Next possible location for a cell
+ colIndex += actualColSpan;
+ } else {
+ colIndex++;
+ }
+ } while (cell);
+
+ return cellCount;
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetTableSize(nsIDOMElement* aTable,
+ int32_t* aRowCount,
+ int32_t* aColCount)
+{
+ NS_ENSURE_ARG_POINTER(aRowCount);
+ NS_ENSURE_ARG_POINTER(aColCount);
+ *aRowCount = 0;
+ *aColCount = 0;
+ nsCOMPtr<nsIDOMElement> table;
+ // Get the selected talbe or the table enclosing the selection anchor
+ nsresult rv = GetElementOrParentByTagName(NS_LITERAL_STRING("table"), aTable,
+ getter_AddRefs(table));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(table, NS_ERROR_FAILURE);
+
+ nsTableWrapperFrame* tableFrame = GetTableFrame(table.get());
+ NS_ENSURE_TRUE(tableFrame, NS_ERROR_FAILURE);
+
+ *aRowCount = tableFrame->GetRowCount();
+ *aColCount = tableFrame->GetColCount();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetCellDataAt(nsIDOMElement* aTable,
+ int32_t aRowIndex,
+ int32_t aColIndex,
+ nsIDOMElement** aCell,
+ int32_t* aStartRowIndex,
+ int32_t* aStartColIndex,
+ int32_t* aRowSpan,
+ int32_t* aColSpan,
+ int32_t* aActualRowSpan,
+ int32_t* aActualColSpan,
+ bool* aIsSelected)
+{
+ NS_ENSURE_ARG_POINTER(aStartRowIndex);
+ NS_ENSURE_ARG_POINTER(aStartColIndex);
+ NS_ENSURE_ARG_POINTER(aRowSpan);
+ NS_ENSURE_ARG_POINTER(aColSpan);
+ NS_ENSURE_ARG_POINTER(aActualRowSpan);
+ NS_ENSURE_ARG_POINTER(aActualColSpan);
+ NS_ENSURE_ARG_POINTER(aIsSelected);
+ NS_ENSURE_TRUE(aCell, NS_ERROR_NULL_POINTER);
+
+ *aStartRowIndex = 0;
+ *aStartColIndex = 0;
+ *aRowSpan = 0;
+ *aColSpan = 0;
+ *aActualRowSpan = 0;
+ *aActualColSpan = 0;
+ *aIsSelected = false;
+
+ *aCell = nullptr;
+
+ if (!aTable) {
+ // Get the selected table or the table enclosing the selection anchor
+ nsCOMPtr<nsIDOMElement> table;
+ nsresult rv =
+ GetElementOrParentByTagName(NS_LITERAL_STRING("table"), nullptr,
+ getter_AddRefs(table));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!table) {
+ return NS_ERROR_FAILURE;
+ }
+ aTable = table;
+ }
+
+ nsTableWrapperFrame* tableFrame = GetTableFrame(aTable);
+ NS_ENSURE_TRUE(tableFrame, NS_ERROR_FAILURE);
+
+ nsTableCellFrame* cellFrame =
+ tableFrame->GetCellFrameAt(aRowIndex, aColIndex);
+ if (!cellFrame) {
+ return NS_ERROR_FAILURE;
+ }
+
+ *aIsSelected = cellFrame->IsSelected();
+ cellFrame->GetRowIndex(*aStartRowIndex);
+ cellFrame->GetColIndex(*aStartColIndex);
+ *aRowSpan = cellFrame->GetRowSpan();
+ *aColSpan = cellFrame->GetColSpan();
+ *aActualRowSpan = tableFrame->GetEffectiveRowSpanAt(aRowIndex, aColIndex);
+ *aActualColSpan = tableFrame->GetEffectiveColSpanAt(aRowIndex, aColIndex);
+ nsCOMPtr<nsIDOMElement> domCell = do_QueryInterface(cellFrame->GetContent());
+ domCell.forget(aCell);
+
+ return NS_OK;
+}
+
+// When all you want is the cell
+NS_IMETHODIMP
+HTMLEditor::GetCellAt(nsIDOMElement* aTable,
+ int32_t aRowIndex,
+ int32_t aColIndex,
+ nsIDOMElement** aCell)
+{
+ NS_ENSURE_ARG_POINTER(aCell);
+ *aCell = nullptr;
+
+ if (!aTable) {
+ // Get the selected table or the table enclosing the selection anchor
+ nsCOMPtr<nsIDOMElement> table;
+ nsresult rv =
+ GetElementOrParentByTagName(NS_LITERAL_STRING("table"), nullptr,
+ getter_AddRefs(table));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(table, NS_ERROR_FAILURE);
+ aTable = table;
+ }
+
+ nsTableWrapperFrame* tableFrame = GetTableFrame(aTable);
+ if (!tableFrame) {
+ *aCell = nullptr;
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+ }
+
+ nsCOMPtr<nsIDOMElement> domCell =
+ do_QueryInterface(tableFrame->GetCellAt(aRowIndex, aColIndex));
+ domCell.forget(aCell);
+
+ return NS_OK;
+}
+
+// When all you want are the rowspan and colspan (not exposed in nsITableEditor)
+NS_IMETHODIMP
+HTMLEditor::GetCellSpansAt(nsIDOMElement* aTable,
+ int32_t aRowIndex,
+ int32_t aColIndex,
+ int32_t& aActualRowSpan,
+ int32_t& aActualColSpan)
+{
+ nsTableWrapperFrame* tableFrame = GetTableFrame(aTable);
+ if (!tableFrame) {
+ return NS_ERROR_FAILURE;
+ }
+ aActualRowSpan = tableFrame->GetEffectiveRowSpanAt(aRowIndex, aColIndex);
+ aActualColSpan = tableFrame->GetEffectiveColSpanAt(aRowIndex, aColIndex);
+
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::GetCellContext(Selection** aSelection,
+ nsIDOMElement** aTable,
+ nsIDOMElement** aCell,
+ nsIDOMNode** aCellParent,
+ int32_t* aCellOffset,
+ int32_t* aRowIndex,
+ int32_t* aColIndex)
+{
+ // Initialize return pointers
+ if (aSelection) {
+ *aSelection = nullptr;
+ }
+ if (aTable) {
+ *aTable = nullptr;
+ }
+ if (aCell) {
+ *aCell = nullptr;
+ }
+ if (aCellParent) {
+ *aCellParent = nullptr;
+ }
+ if (aCellOffset) {
+ *aCellOffset = 0;
+ }
+ if (aRowIndex) {
+ *aRowIndex = 0;
+ }
+ if (aColIndex) {
+ *aColIndex = 0;
+ }
+
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE);
+
+ if (aSelection) {
+ *aSelection = selection.get();
+ NS_ADDREF(*aSelection);
+ }
+ nsCOMPtr <nsIDOMElement> table;
+ nsCOMPtr <nsIDOMElement> cell;
+
+ // Caller may supply the cell...
+ if (aCell && *aCell) {
+ cell = *aCell;
+ }
+
+ // ...but if not supplied,
+ // get cell if it's the child of selection anchor node,
+ // or get the enclosing by a cell
+ if (!cell) {
+ // Find a selected or enclosing table element
+ nsCOMPtr<nsIDOMElement> cellOrTableElement;
+ int32_t selectedCount;
+ nsAutoString tagName;
+ nsresult rv =
+ GetSelectedOrParentTableElement(tagName, &selectedCount,
+ getter_AddRefs(cellOrTableElement));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (tagName.EqualsLiteral("table")) {
+ // We have a selected table, not a cell
+ if (aTable) {
+ *aTable = cellOrTableElement.get();
+ NS_ADDREF(*aTable);
+ }
+ return NS_OK;
+ }
+ if (!tagName.EqualsLiteral("td")) {
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+ }
+
+ // We found a cell
+ cell = cellOrTableElement;
+ }
+ if (aCell) {
+ *aCell = cell.get();
+ NS_ADDREF(*aCell);
+ }
+
+ // Get containing table
+ nsresult rv = GetElementOrParentByTagName(NS_LITERAL_STRING("table"), cell,
+ getter_AddRefs(table));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Cell must be in a table, so fail if not found
+ NS_ENSURE_TRUE(table, NS_ERROR_FAILURE);
+ if (aTable) {
+ *aTable = table.get();
+ NS_ADDREF(*aTable);
+ }
+
+ // Get the rest of the related data only if requested
+ if (aRowIndex || aColIndex) {
+ int32_t rowIndex, colIndex;
+ // Get current cell location so we can put caret back there when done
+ rv = GetCellIndexes(cell, &rowIndex, &colIndex);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (aRowIndex) {
+ *aRowIndex = rowIndex;
+ }
+ if (aColIndex) {
+ *aColIndex = colIndex;
+ }
+ }
+ if (aCellParent) {
+ nsCOMPtr <nsIDOMNode> cellParent;
+ // Get the immediate parent of the cell
+ rv = cell->GetParentNode(getter_AddRefs(cellParent));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Cell has to have a parent, so fail if not found
+ NS_ENSURE_TRUE(cellParent, NS_ERROR_FAILURE);
+
+ *aCellParent = cellParent.get();
+ NS_ADDREF(*aCellParent);
+
+ if (aCellOffset) {
+ *aCellOffset = GetChildOffset(cell, cellParent);
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::GetCellFromRange(nsRange* aRange,
+ nsIDOMElement** aCell)
+{
+ // Note: this might return a node that is outside of the range.
+ // Use carefully.
+ NS_ENSURE_TRUE(aRange && aCell, NS_ERROR_NULL_POINTER);
+
+ *aCell = nullptr;
+
+ nsCOMPtr<nsIDOMNode> startParent;
+ nsresult rv = aRange->GetStartContainer(getter_AddRefs(startParent));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(startParent, NS_ERROR_FAILURE);
+
+ int32_t startOffset;
+ rv = aRange->GetStartOffset(&startOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDOMNode> childNode = GetChildAt(startParent, startOffset);
+ // This means selection is probably at a text node (or end of doc?)
+ if (!childNode) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIDOMNode> endParent;
+ rv = aRange->GetEndContainer(getter_AddRefs(endParent));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(startParent, NS_ERROR_FAILURE);
+
+ int32_t endOffset;
+ rv = aRange->GetEndOffset(&endOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If a cell is deleted, the range is collapse
+ // (startOffset == endOffset)
+ // so tell caller the cell wasn't found
+ if (startParent == endParent &&
+ endOffset == startOffset+1 &&
+ HTMLEditUtils::IsTableCell(childNode)) {
+ // Should we also test if frame is selected? (Use GetCellDataAt())
+ // (Let's not for now -- more efficient)
+ nsCOMPtr<nsIDOMElement> cellElement = do_QueryInterface(childNode);
+ *aCell = cellElement.get();
+ NS_ADDREF(*aCell);
+ return NS_OK;
+ }
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetFirstSelectedCell(nsIDOMRange** aRange,
+ nsIDOMElement** aCell)
+{
+ NS_ENSURE_TRUE(aCell, NS_ERROR_NULL_POINTER);
+ *aCell = nullptr;
+ if (aRange) {
+ *aRange = nullptr;
+ }
+
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE);
+
+ RefPtr<nsRange> range = selection->GetRangeAt(0);
+ NS_ENSURE_TRUE(range, NS_ERROR_FAILURE);
+
+ mSelectedCellIndex = 0;
+
+ nsresult rv = GetCellFromRange(range, aCell);
+ // Failure here probably means selection is in a text node,
+ // so there's no selected cell
+ if (NS_FAILED(rv)) {
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+ }
+ // No cell means range was collapsed (cell was deleted)
+ if (!*aCell) {
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+ }
+
+ if (aRange) {
+ *aRange = range.get();
+ NS_ADDREF(*aRange);
+ }
+
+ // Setup for next cell
+ mSelectedCellIndex = 1;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetNextSelectedCell(nsIDOMRange** aRange,
+ nsIDOMElement** aCell)
+{
+ NS_ENSURE_TRUE(aCell, NS_ERROR_NULL_POINTER);
+ *aCell = nullptr;
+ if (aRange) {
+ *aRange = nullptr;
+ }
+
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE);
+
+ int32_t rangeCount = selection->RangeCount();
+
+ // Don't even try if index exceeds range count
+ if (mSelectedCellIndex >= rangeCount) {
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+ }
+
+ // Scan through ranges to find next valid selected cell
+ RefPtr<nsRange> range;
+ for (; mSelectedCellIndex < rangeCount; mSelectedCellIndex++) {
+ range = selection->GetRangeAt(mSelectedCellIndex);
+ NS_ENSURE_TRUE(range, NS_ERROR_FAILURE);
+
+ nsresult rv = GetCellFromRange(range, aCell);
+ // Failure here means the range doesn't contain a cell
+ NS_ENSURE_SUCCESS(rv, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND);
+
+ // We found a selected cell
+ if (*aCell) {
+ break;
+ }
+
+ // If we didn't find a cell, continue to next range in selection
+ }
+ // No cell means all remaining ranges were collapsed (cells were deleted)
+ NS_ENSURE_TRUE(*aCell, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND);
+
+ if (aRange) {
+ *aRange = range.get();
+ NS_ADDREF(*aRange);
+ }
+
+ // Setup for next cell
+ mSelectedCellIndex++;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetFirstSelectedCellInTable(int32_t* aRowIndex,
+ int32_t* aColIndex,
+ nsIDOMElement** aCell)
+{
+ NS_ENSURE_TRUE(aCell, NS_ERROR_NULL_POINTER);
+ *aCell = nullptr;
+ if (aRowIndex) {
+ *aRowIndex = 0;
+ }
+ if (aColIndex) {
+ *aColIndex = 0;
+ }
+
+ nsCOMPtr<nsIDOMElement> cell;
+ nsresult rv = GetFirstSelectedCell(nullptr, getter_AddRefs(cell));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(cell, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND);
+
+ *aCell = cell.get();
+ NS_ADDREF(*aCell);
+
+ // Also return the row and/or column if requested
+ if (aRowIndex || aColIndex) {
+ int32_t startRowIndex, startColIndex;
+ rv = GetCellIndexes(cell, &startRowIndex, &startColIndex);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (aRowIndex) {
+ *aRowIndex = startRowIndex;
+ }
+ if (aColIndex) {
+ *aColIndex = startColIndex;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::SetSelectionAfterTableEdit(nsIDOMElement* aTable,
+ int32_t aRow,
+ int32_t aCol,
+ int32_t aDirection,
+ bool aSelected)
+{
+ NS_ENSURE_TRUE(aTable, NS_ERROR_NOT_INITIALIZED);
+
+ RefPtr<Selection> selection = GetSelection();
+
+ if (!selection) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIDOMElement> cell;
+ bool done = false;
+ do {
+ nsresult rv = GetCellAt(aTable, aRow, aCol, getter_AddRefs(cell));
+ if (NS_FAILED(rv)) {
+ break;
+ }
+
+ if (cell) {
+ if (aSelected) {
+ // Reselect the cell
+ return SelectElement(cell);
+ } else {
+ // Set the caret to deepest first child
+ // but don't go into nested tables
+ // TODO: Should we really be placing the caret at the END
+ // of the cell content?
+ nsCOMPtr<nsINode> cellNode = do_QueryInterface(cell);
+ if (cellNode) {
+ CollapseSelectionToDeepestNonTableFirstChild(selection, cellNode);
+ }
+ return NS_OK;
+ }
+ } else {
+ // Setup index to find another cell in the
+ // direction requested, but move in
+ // other direction if already at beginning of row or column
+ switch (aDirection) {
+ case ePreviousColumn:
+ if (!aCol) {
+ if (aRow > 0) {
+ aRow--;
+ } else {
+ done = true;
+ }
+ } else {
+ aCol--;
+ }
+ break;
+ case ePreviousRow:
+ if (!aRow) {
+ if (aCol > 0) {
+ aCol--;
+ } else {
+ done = true;
+ }
+ } else {
+ aRow--;
+ }
+ break;
+ default:
+ done = true;
+ }
+ }
+ } while (!done);
+
+ // We didn't find a cell
+ // Set selection to just before the table
+ nsCOMPtr<nsIDOMNode> tableParent;
+ nsresult rv = aTable->GetParentNode(getter_AddRefs(tableParent));
+ if (NS_SUCCEEDED(rv) && tableParent) {
+ int32_t tableOffset = GetChildOffset(aTable, tableParent);
+ return selection->Collapse(tableParent, tableOffset);
+ }
+ // Last resort: Set selection to start of doc
+ // (it's very bad to not have a valid selection!)
+ return SetSelectionAtDocumentStart(selection);
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetSelectedOrParentTableElement(nsAString& aTagName,
+ int32_t* aSelectedCount,
+ nsIDOMElement** aTableElement)
+{
+ NS_ENSURE_ARG_POINTER(aTableElement);
+ NS_ENSURE_ARG_POINTER(aSelectedCount);
+ *aTableElement = nullptr;
+ aTagName.Truncate();
+ *aSelectedCount = 0;
+
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE);
+
+ // Try to get the first selected cell
+ nsCOMPtr<nsIDOMElement> tableOrCellElement;
+ nsresult rv = GetFirstSelectedCell(nullptr,
+ getter_AddRefs(tableOrCellElement));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_NAMED_LITERAL_STRING(tdName, "td");
+
+ if (tableOrCellElement) {
+ // Each cell is in its own selection range,
+ // so count signals multiple-cell selection
+ rv = selection->GetRangeCount(aSelectedCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aTagName = tdName;
+ } else {
+ nsCOMPtr<nsIDOMNode> anchorNode;
+ rv = selection->GetAnchorNode(getter_AddRefs(anchorNode));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ NS_ENSURE_TRUE(anchorNode, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIDOMNode> selectedNode;
+
+ // Get child of anchor node, if exists
+ bool hasChildren;
+ anchorNode->HasChildNodes(&hasChildren);
+
+ if (hasChildren) {
+ int32_t anchorOffset;
+ rv = selection->GetAnchorOffset(&anchorOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ selectedNode = GetChildAt(anchorNode, anchorOffset);
+ if (!selectedNode) {
+ selectedNode = anchorNode;
+ // If anchor doesn't have a child, we can't be selecting a table element,
+ // so don't do the following:
+ } else {
+ nsCOMPtr<nsIAtom> atom = EditorBase::GetTag(selectedNode);
+
+ if (atom == nsGkAtoms::td) {
+ tableOrCellElement = do_QueryInterface(selectedNode);
+ aTagName = tdName;
+ // Each cell is in its own selection range,
+ // so count signals multiple-cell selection
+ rv = selection->GetRangeCount(aSelectedCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else if (atom == nsGkAtoms::table) {
+ tableOrCellElement = do_QueryInterface(selectedNode);
+ aTagName.AssignLiteral("table");
+ *aSelectedCount = 1;
+ } else if (atom == nsGkAtoms::tr) {
+ tableOrCellElement = do_QueryInterface(selectedNode);
+ aTagName.AssignLiteral("tr");
+ *aSelectedCount = 1;
+ }
+ }
+ }
+ if (!tableOrCellElement) {
+ // Didn't find a table element -- find a cell parent
+ rv = GetElementOrParentByTagName(tdName, anchorNode,
+ getter_AddRefs(tableOrCellElement));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (tableOrCellElement) {
+ aTagName = tdName;
+ }
+ }
+ }
+ if (tableOrCellElement) {
+ *aTableElement = tableOrCellElement.get();
+ NS_ADDREF(*aTableElement);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::GetSelectedCellsType(nsIDOMElement* aElement,
+ uint32_t* aSelectionType)
+{
+ NS_ENSURE_ARG_POINTER(aSelectionType);
+ *aSelectionType = 0;
+
+ // Be sure we have a table element
+ // (if aElement is null, this uses selection's anchor node)
+ nsCOMPtr<nsIDOMElement> table;
+
+ nsresult rv =
+ GetElementOrParentByTagName(NS_LITERAL_STRING("table"), aElement,
+ getter_AddRefs(table));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t rowCount, colCount;
+ rv = GetTableSize(table, &rowCount, &colCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Traverse all selected cells
+ nsCOMPtr<nsIDOMElement> selectedCell;
+ rv = GetFirstSelectedCell(nullptr, getter_AddRefs(selectedCell));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (rv == NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND) {
+ return NS_OK;
+ }
+
+ // We have at least one selected cell, so set return value
+ *aSelectionType = nsISelectionPrivate::TABLESELECTION_CELL;
+
+ // Store indexes of each row/col to avoid duplication of searches
+ nsTArray<int32_t> indexArray;
+
+ bool allCellsInRowAreSelected = false;
+ bool allCellsInColAreSelected = false;
+ while (NS_SUCCEEDED(rv) && selectedCell) {
+ // Get the cell's location in the cellmap
+ int32_t startRowIndex, startColIndex;
+ rv = GetCellIndexes(selectedCell, &startRowIndex, &startColIndex);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (!indexArray.Contains(startColIndex)) {
+ indexArray.AppendElement(startColIndex);
+ allCellsInRowAreSelected = AllCellsInRowSelected(table, startRowIndex, colCount);
+ // We're done as soon as we fail for any row
+ if (!allCellsInRowAreSelected) {
+ break;
+ }
+ }
+ rv = GetNextSelectedCell(nullptr, getter_AddRefs(selectedCell));
+ }
+
+ if (allCellsInRowAreSelected) {
+ *aSelectionType = nsISelectionPrivate::TABLESELECTION_ROW;
+ return NS_OK;
+ }
+ // Test for columns
+
+ // Empty the indexArray
+ indexArray.Clear();
+
+ // Start at first cell again
+ rv = GetFirstSelectedCell(nullptr, getter_AddRefs(selectedCell));
+ while (NS_SUCCEEDED(rv) && selectedCell) {
+ // Get the cell's location in the cellmap
+ int32_t startRowIndex, startColIndex;
+ rv = GetCellIndexes(selectedCell, &startRowIndex, &startColIndex);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (!indexArray.Contains(startRowIndex)) {
+ indexArray.AppendElement(startColIndex);
+ allCellsInColAreSelected = AllCellsInColumnSelected(table, startColIndex, rowCount);
+ // We're done as soon as we fail for any column
+ if (!allCellsInRowAreSelected) {
+ break;
+ }
+ }
+ rv = GetNextSelectedCell(nullptr, getter_AddRefs(selectedCell));
+ }
+ if (allCellsInColAreSelected) {
+ *aSelectionType = nsISelectionPrivate::TABLESELECTION_COLUMN;
+ }
+
+ return NS_OK;
+}
+
+bool
+HTMLEditor::AllCellsInRowSelected(nsIDOMElement* aTable,
+ int32_t aRowIndex,
+ int32_t aNumberOfColumns)
+{
+ NS_ENSURE_TRUE(aTable, false);
+
+ int32_t curStartRowIndex, curStartColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan;
+ bool isSelected;
+
+ for (int32_t col = 0; col < aNumberOfColumns;
+ col += std::max(actualColSpan, 1)) {
+ nsCOMPtr<nsIDOMElement> cell;
+ nsresult rv = GetCellDataAt(aTable, aRowIndex, col, getter_AddRefs(cell),
+ &curStartRowIndex, &curStartColIndex,
+ &rowSpan, &colSpan,
+ &actualRowSpan, &actualColSpan, &isSelected);
+
+ NS_ENSURE_SUCCESS(rv, false);
+ // If no cell, we may have a "ragged" right edge,
+ // so return TRUE only if we already found a cell in the row
+ NS_ENSURE_TRUE(cell, (col > 0) ? true : false);
+
+ // Return as soon as a non-selected cell is found
+ NS_ENSURE_TRUE(isSelected, false);
+
+ NS_ASSERTION((actualColSpan > 0),"ActualColSpan = 0 in AllCellsInRowSelected");
+ }
+ return true;
+}
+
+bool
+HTMLEditor::AllCellsInColumnSelected(nsIDOMElement* aTable,
+ int32_t aColIndex,
+ int32_t aNumberOfRows)
+{
+ NS_ENSURE_TRUE(aTable, false);
+
+ int32_t curStartRowIndex, curStartColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan;
+ bool isSelected;
+
+ for (int32_t row = 0; row < aNumberOfRows;
+ row += std::max(actualRowSpan, 1)) {
+ nsCOMPtr<nsIDOMElement> cell;
+ nsresult rv = GetCellDataAt(aTable, row, aColIndex, getter_AddRefs(cell),
+ &curStartRowIndex, &curStartColIndex,
+ &rowSpan, &colSpan,
+ &actualRowSpan, &actualColSpan, &isSelected);
+
+ NS_ENSURE_SUCCESS(rv, false);
+ // If no cell, we must have a "ragged" right edge on the last column
+ // so return TRUE only if we already found a cell in the row
+ NS_ENSURE_TRUE(cell, (row > 0) ? true : false);
+
+ // Return as soon as a non-selected cell is found
+ NS_ENSURE_TRUE(isSelected, false);
+ }
+ return true;
+}
+
+bool
+HTMLEditor::IsEmptyCell(dom::Element* aCell)
+{
+ MOZ_ASSERT(aCell);
+
+ // Check if target only contains empty text node or <br>
+ nsCOMPtr<nsINode> cellChild = aCell->GetFirstChild();
+ if (!cellChild) {
+ return false;
+ }
+
+ nsCOMPtr<nsINode> nextChild = cellChild->GetNextSibling();
+ if (nextChild) {
+ return false;
+ }
+
+ // We insert a single break into a cell by default
+ // to have some place to locate a cursor -- it is dispensable
+ if (cellChild->IsHTMLElement(nsGkAtoms::br)) {
+ return true;
+ }
+
+ bool isEmpty;
+ // Or check if no real content
+ nsresult rv = IsEmptyNode(cellChild, &isEmpty, false, false);
+ NS_ENSURE_SUCCESS(rv, false);
+ return isEmpty;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/HTMLURIRefObject.cpp b/editor/libeditor/HTMLURIRefObject.cpp
new file mode 100644
index 000000000..612451d85
--- /dev/null
+++ b/editor/libeditor/HTMLURIRefObject.cpp
@@ -0,0 +1,262 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Here is the list, from beppe and glazman:
+ href >> A, AREA, BASE, LINK
+ src >> FRAME, IFRAME, IMG, INPUT, SCRIPT
+ <META http-equiv="refresh" content="3,http://www.acme.com/intro.html">
+ longdesc >> FRAME, IFRAME, IMG
+ usemap >> IMG, INPUT, OBJECT
+ action >> FORM
+ background >> BODY
+ codebase >> OBJECT, APPLET
+ classid >> OBJECT
+ data >> OBJECT
+ cite >> BLOCKQUOTE, DEL, INS, Q
+ profile >> HEAD
+ ARCHIVE attribute on APPLET ; warning, it contains a list of URIs.
+
+ Easier way of organizing the list:
+ a: href
+ area: href
+ base: href
+ body: background
+ blockquote: cite (not normally rewritable)
+ link: href
+ frame: src, longdesc
+ iframe: src, longdesc
+ input: src, usemap
+ form: action
+ img: src, longdesc, usemap
+ script: src
+ applet: codebase, archive <list>
+ object: codebase, data, classid, usemap
+ head: profile
+ del: cite
+ ins: cite
+ q: cite
+ */
+
+#include "HTMLURIRefObject.h"
+
+#include "mozilla/mozalloc.h"
+#include "nsAString.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsID.h"
+#include "nsIDOMAttr.h"
+#include "nsIDOMElement.h"
+#include "nsIDOMMozNamedAttrMap.h"
+#include "nsIDOMNode.h"
+#include "nsISupportsUtils.h"
+#include "nsString.h"
+
+namespace mozilla {
+
+// String classes change too often and I can't keep up.
+// Set this macro to this week's approved case-insensitive compare routine.
+#define MATCHES(tagName, str) tagName.EqualsIgnoreCase(str)
+
+HTMLURIRefObject::HTMLURIRefObject()
+ : mCurAttrIndex(0)
+ , mAttributeCnt(0)
+{
+}
+
+HTMLURIRefObject::~HTMLURIRefObject()
+{
+}
+
+//Interfaces for addref and release and queryinterface
+NS_IMPL_ISUPPORTS(HTMLURIRefObject, nsIURIRefObject)
+
+NS_IMETHODIMP
+HTMLURIRefObject::Reset()
+{
+ mCurAttrIndex = 0;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLURIRefObject::GetNextURI(nsAString& aURI)
+{
+ NS_ENSURE_TRUE(mNode, NS_ERROR_NOT_INITIALIZED);
+
+ // XXX Why don't you use nsIAtom for comparing the tag name a lot?
+ nsAutoString tagName;
+ nsresult rv = mNode->GetNodeName(tagName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Loop over attribute list:
+ if (!mAttributes) {
+ nsCOMPtr<nsIDOMElement> element (do_QueryInterface(mNode));
+ NS_ENSURE_TRUE(element, NS_ERROR_INVALID_ARG);
+
+ mCurAttrIndex = 0;
+ element->GetAttributes(getter_AddRefs(mAttributes));
+ NS_ENSURE_TRUE(mAttributes, NS_ERROR_NOT_INITIALIZED);
+
+ rv = mAttributes->GetLength(&mAttributeCnt);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(mAttributeCnt, NS_ERROR_FAILURE);
+ mCurAttrIndex = 0;
+ }
+
+ while (mCurAttrIndex < mAttributeCnt) {
+ nsCOMPtr<nsIDOMAttr> attrNode;
+ rv = mAttributes->Item(mCurAttrIndex++, getter_AddRefs(attrNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_ARG_POINTER(attrNode);
+ nsString curAttr;
+ rv = attrNode->GetName(curAttr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // href >> A, AREA, BASE, LINK
+ if (MATCHES(curAttr, "href")) {
+ if (!MATCHES(tagName, "a") && !MATCHES(tagName, "area") &&
+ !MATCHES(tagName, "base") && !MATCHES(tagName, "link")) {
+ continue;
+ }
+ rv = attrNode->GetValue(aURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsString uri (aURI);
+ // href pointing to a named anchor doesn't count
+ if (aURI.First() != char16_t('#')) {
+ return NS_OK;
+ }
+ aURI.Truncate();
+ return NS_ERROR_INVALID_ARG;
+ }
+ // src >> FRAME, IFRAME, IMG, INPUT, SCRIPT
+ else if (MATCHES(curAttr, "src")) {
+ if (!MATCHES(tagName, "img") &&
+ !MATCHES(tagName, "frame") && !MATCHES(tagName, "iframe") &&
+ !MATCHES(tagName, "input") && !MATCHES(tagName, "script")) {
+ continue;
+ }
+ return attrNode->GetValue(aURI);
+ }
+ //<META http-equiv="refresh" content="3,http://www.acme.com/intro.html">
+ else if (MATCHES(curAttr, "content")) {
+ if (!MATCHES(tagName, "meta")) {
+ continue;
+ }
+ }
+ // longdesc >> FRAME, IFRAME, IMG
+ else if (MATCHES(curAttr, "longdesc")) {
+ if (!MATCHES(tagName, "img") &&
+ !MATCHES(tagName, "frame") && !MATCHES(tagName, "iframe")) {
+ continue;
+ }
+ }
+ // usemap >> IMG, INPUT, OBJECT
+ else if (MATCHES(curAttr, "usemap")) {
+ if (!MATCHES(tagName, "img") &&
+ !MATCHES(tagName, "input") && !MATCHES(tagName, "object")) {
+ continue;
+ }
+ }
+ // action >> FORM
+ else if (MATCHES(curAttr, "action")) {
+ if (!MATCHES(tagName, "form")) {
+ continue;
+ }
+ }
+ // background >> BODY
+ else if (MATCHES(curAttr, "background")) {
+ if (!MATCHES(tagName, "body")) {
+ continue;
+ }
+ }
+ // codebase >> OBJECT, APPLET
+ else if (MATCHES(curAttr, "codebase")) {
+ if (!MATCHES(tagName, "meta")) {
+ continue;
+ }
+ }
+ // classid >> OBJECT
+ else if (MATCHES(curAttr, "classid")) {
+ if (!MATCHES(tagName, "object")) {
+ continue;
+ }
+ }
+ // data >> OBJECT
+ else if (MATCHES(curAttr, "data")) {
+ if (!MATCHES(tagName, "object")) {
+ continue;
+ }
+ }
+ // cite >> BLOCKQUOTE, DEL, INS, Q
+ else if (MATCHES(curAttr, "cite")) {
+ if (!MATCHES(tagName, "blockquote") && !MATCHES(tagName, "q") &&
+ !MATCHES(tagName, "del") && !MATCHES(tagName, "ins")) {
+ continue;
+ }
+ }
+ // profile >> HEAD
+ else if (MATCHES(curAttr, "profile")) {
+ if (!MATCHES(tagName, "head")) {
+ continue;
+ }
+ }
+ // archive attribute on APPLET; warning, it contains a list of URIs.
+ else if (MATCHES(curAttr, "archive")) {
+ if (!MATCHES(tagName, "applet")) {
+ continue;
+ }
+ }
+ }
+ // Return a code to indicate that there are no more,
+ // to distinguish that case from real errors.
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP
+HTMLURIRefObject::RewriteAllURIs(const nsAString& aOldPat,
+ const nsAString& aNewPat,
+ bool aMakeRel)
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+HTMLURIRefObject::GetNode(nsIDOMNode** aNode)
+{
+ NS_ENSURE_TRUE(mNode, NS_ERROR_NOT_INITIALIZED);
+ NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER);
+ *aNode = mNode.get();
+ NS_ADDREF(*aNode);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLURIRefObject::SetNode(nsIDOMNode* aNode)
+{
+ mNode = aNode;
+ nsAutoString dummyURI;
+ if (NS_SUCCEEDED(GetNextURI(dummyURI))) {
+ mCurAttrIndex = 0; // Reset so we'll get the first node next time
+ return NS_OK;
+ }
+
+ // If there weren't any URIs in the attributes,
+ // then don't accept this node.
+ mNode = nullptr;
+ return NS_ERROR_INVALID_ARG;
+}
+
+} // namespace mozilla
+
+nsresult NS_NewHTMLURIRefObject(nsIURIRefObject** aResult, nsIDOMNode* aNode)
+{
+ RefPtr<mozilla::HTMLURIRefObject> refObject = new mozilla::HTMLURIRefObject();
+ nsresult rv = refObject->SetNode(aNode);
+ if (NS_FAILED(rv)) {
+ *aResult = 0;
+ return rv;
+ }
+ refObject.forget(aResult);
+ return NS_OK;
+}
diff --git a/editor/libeditor/HTMLURIRefObject.h b/editor/libeditor/HTMLURIRefObject.h
new file mode 100644
index 000000000..47f7f1868
--- /dev/null
+++ b/editor/libeditor/HTMLURIRefObject.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef HTMLURIRefObject_h
+#define HTMLURIRefObject_h
+
+#include "nsCOMPtr.h"
+#include "nsISupportsImpl.h"
+#include "nsIURIRefObject.h"
+#include "nscore.h"
+
+#define NS_URI_REF_OBJECT_CID \
+{ /* {bdd79df6-1dd1-11b2-b29c-c3d63a58f1d2} */ \
+ 0xbdd79df6, 0x1dd1, 0x11b2, \
+ { 0xb2, 0x9c, 0xc3, 0xd6, 0x3a, 0x58, 0xf1, 0xd2 } \
+}
+
+class nsIDOMMozNamedAttrMap;
+class nsIDOMNode;
+
+namespace mozilla {
+
+class HTMLURIRefObject final : public nsIURIRefObject
+{
+public:
+ HTMLURIRefObject();
+
+ // Interfaces for addref and release and queryinterface
+ NS_DECL_ISUPPORTS
+
+ NS_DECL_NSIURIREFOBJECT
+
+protected:
+ virtual ~HTMLURIRefObject();
+
+ nsCOMPtr<nsIDOMNode> mNode;
+ nsCOMPtr<nsIDOMMozNamedAttrMap> mAttributes;
+ uint32_t mCurAttrIndex;
+ uint32_t mAttributeCnt;
+};
+
+} // namespace mozilla
+
+nsresult NS_NewHTMLURIRefObject(nsIURIRefObject** aResult, nsIDOMNode* aNode);
+
+#endif // #ifndef HTMLURIRefObject_h
diff --git a/editor/libeditor/InsertNodeTransaction.cpp b/editor/libeditor/InsertNodeTransaction.cpp
new file mode 100644
index 000000000..6af33b74f
--- /dev/null
+++ b/editor/libeditor/InsertNodeTransaction.cpp
@@ -0,0 +1,97 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "InsertNodeTransaction.h"
+
+#include "mozilla/EditorBase.h" // for EditorBase
+
+#include "mozilla/dom/Selection.h" // for Selection
+
+#include "nsAString.h"
+#include "nsDebug.h" // for NS_ENSURE_TRUE, etc.
+#include "nsError.h" // for NS_ERROR_NULL_POINTER, etc.
+#include "nsIContent.h" // for nsIContent
+#include "nsMemory.h" // for nsMemory
+#include "nsReadableUtils.h" // for ToNewCString
+#include "nsString.h" // for nsString
+
+namespace mozilla {
+
+using namespace dom;
+
+InsertNodeTransaction::InsertNodeTransaction(nsIContent& aNode,
+ nsINode& aParent,
+ int32_t aOffset,
+ EditorBase& aEditorBase)
+ : mNode(&aNode)
+ , mParent(&aParent)
+ , mOffset(aOffset)
+ , mEditorBase(aEditorBase)
+{
+}
+
+InsertNodeTransaction::~InsertNodeTransaction()
+{
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(InsertNodeTransaction, EditTransactionBase,
+ mNode,
+ mParent)
+
+NS_IMPL_ADDREF_INHERITED(InsertNodeTransaction, EditTransactionBase)
+NS_IMPL_RELEASE_INHERITED(InsertNodeTransaction, EditTransactionBase)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(InsertNodeTransaction)
+NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
+
+NS_IMETHODIMP
+InsertNodeTransaction::DoTransaction()
+{
+ MOZ_ASSERT(mNode && mParent);
+
+ uint32_t count = mParent->GetChildCount();
+ if (mOffset > static_cast<int32_t>(count) || mOffset == -1) {
+ // -1 is sentinel value meaning "append at end"
+ mOffset = count;
+ }
+
+ // Note, it's ok for ref to be null. That means append.
+ nsCOMPtr<nsIContent> ref = mParent->GetChildAt(mOffset);
+
+ mEditorBase.MarkNodeDirty(GetAsDOMNode(mNode));
+
+ ErrorResult rv;
+ mParent->InsertBefore(*mNode, ref, rv);
+ NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult());
+
+ // Only set selection to insertion point if editor gives permission
+ if (mEditorBase.GetShouldTxnSetSelection()) {
+ RefPtr<Selection> selection = mEditorBase.GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+ // Place the selection just after the inserted element
+ selection->Collapse(mParent, mOffset + 1);
+ } else {
+ // Do nothing - DOM Range gravity will adjust selection
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+InsertNodeTransaction::UndoTransaction()
+{
+ MOZ_ASSERT(mNode && mParent);
+
+ ErrorResult rv;
+ mParent->RemoveChild(*mNode, rv);
+ return rv.StealNSResult();
+}
+
+NS_IMETHODIMP
+InsertNodeTransaction::GetTxnDescription(nsAString& aString)
+{
+ aString.AssignLiteral("InsertNodeTransaction");
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/InsertNodeTransaction.h b/editor/libeditor/InsertNodeTransaction.h
new file mode 100644
index 000000000..5af7b8aff
--- /dev/null
+++ b/editor/libeditor/InsertNodeTransaction.h
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef InsertNodeTransaction_h
+#define InsertNodeTransaction_h
+
+#include "mozilla/EditTransactionBase.h" // for EditTransactionBase, etc.
+#include "nsCOMPtr.h" // for nsCOMPtr
+#include "nsCycleCollectionParticipant.h"
+#include "nsIContent.h" // for nsIContent
+#include "nsISupportsImpl.h" // for NS_DECL_ISUPPORTS_INHERITED
+
+namespace mozilla {
+
+class EditorBase;
+
+/**
+ * A transaction that inserts a single element
+ */
+class InsertNodeTransaction final : public EditTransactionBase
+{
+public:
+ /**
+ * Initialize the transaction.
+ * @param aNode The node to insert.
+ * @param aParent The node to insert into.
+ * @param aOffset The offset in aParent to insert aNode.
+ */
+ InsertNodeTransaction(nsIContent& aNode, nsINode& aParent, int32_t aOffset,
+ EditorBase& aEditorBase);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(InsertNodeTransaction,
+ EditTransactionBase)
+
+ NS_DECL_EDITTRANSACTIONBASE
+
+protected:
+ virtual ~InsertNodeTransaction();
+
+ // The element to insert.
+ nsCOMPtr<nsIContent> mNode;
+
+ // The node into which the new node will be inserted.
+ nsCOMPtr<nsINode> mParent;
+
+ // The index in mParent for the new node.
+ int32_t mOffset;
+
+ // The editor for this transaction.
+ EditorBase& mEditorBase;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef InsertNodeTransaction_h
diff --git a/editor/libeditor/InsertTextTransaction.cpp b/editor/libeditor/InsertTextTransaction.cpp
new file mode 100644
index 000000000..009b2d023
--- /dev/null
+++ b/editor/libeditor/InsertTextTransaction.cpp
@@ -0,0 +1,125 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "InsertTextTransaction.h"
+
+#include "mozilla/EditorBase.h" // mEditorBase
+#include "mozilla/SelectionState.h" // RangeUpdater
+#include "mozilla/dom/Selection.h" // Selection local var
+#include "mozilla/dom/Text.h" // mTextNode
+#include "nsAString.h" // nsAString parameter
+#include "nsDebug.h" // for NS_ASSERTION, etc.
+#include "nsError.h" // for NS_OK, etc.
+#include "nsQueryObject.h" // for do_QueryObject
+
+namespace mozilla {
+
+using namespace dom;
+
+InsertTextTransaction::InsertTextTransaction(Text& aTextNode,
+ uint32_t aOffset,
+ const nsAString& aStringToInsert,
+ EditorBase& aEditorBase,
+ RangeUpdater* aRangeUpdater)
+ : mTextNode(&aTextNode)
+ , mOffset(aOffset)
+ , mStringToInsert(aStringToInsert)
+ , mEditorBase(aEditorBase)
+ , mRangeUpdater(aRangeUpdater)
+{
+}
+
+InsertTextTransaction::~InsertTextTransaction()
+{
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(InsertTextTransaction, EditTransactionBase,
+ mTextNode)
+
+NS_IMPL_ADDREF_INHERITED(InsertTextTransaction, EditTransactionBase)
+NS_IMPL_RELEASE_INHERITED(InsertTextTransaction, EditTransactionBase)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(InsertTextTransaction)
+ if (aIID.Equals(NS_GET_IID(InsertTextTransaction))) {
+ foundInterface = static_cast<nsITransaction*>(this);
+ } else
+NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
+
+
+NS_IMETHODIMP
+InsertTextTransaction::DoTransaction()
+{
+ nsresult rv = mTextNode->InsertData(mOffset, mStringToInsert);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Only set selection to insertion point if editor gives permission
+ if (mEditorBase.GetShouldTxnSetSelection()) {
+ RefPtr<Selection> selection = mEditorBase.GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+ DebugOnly<nsresult> rv =
+ selection->Collapse(mTextNode, mOffset + mStringToInsert.Length());
+ NS_ASSERTION(NS_SUCCEEDED(rv),
+ "Selection could not be collapsed after insert");
+ } else {
+ // Do nothing - DOM Range gravity will adjust selection
+ }
+ mRangeUpdater->SelAdjInsertText(*mTextNode, mOffset, mStringToInsert);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+InsertTextTransaction::UndoTransaction()
+{
+ return mTextNode->DeleteData(mOffset, mStringToInsert.Length());
+}
+
+NS_IMETHODIMP
+InsertTextTransaction::Merge(nsITransaction* aTransaction,
+ bool* aDidMerge)
+{
+ if (!aTransaction || !aDidMerge) {
+ return NS_OK;
+ }
+ // Set out param default value
+ *aDidMerge = false;
+
+ // If aTransaction is a InsertTextTransaction, and if the selection hasn't
+ // changed, then absorb it.
+ RefPtr<InsertTextTransaction> otherTransaction = do_QueryObject(aTransaction);
+ if (otherTransaction && IsSequentialInsert(*otherTransaction)) {
+ nsAutoString otherData;
+ otherTransaction->GetData(otherData);
+ mStringToInsert += otherData;
+ *aDidMerge = true;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+InsertTextTransaction::GetTxnDescription(nsAString& aString)
+{
+ aString.AssignLiteral("InsertTextTransaction: ");
+ aString += mStringToInsert;
+ return NS_OK;
+}
+
+/* ============ private methods ================== */
+
+void
+InsertTextTransaction::GetData(nsString& aResult)
+{
+ aResult = mStringToInsert;
+}
+
+bool
+InsertTextTransaction::IsSequentialInsert(
+ InsertTextTransaction& aOtherTransaction)
+{
+ return aOtherTransaction.mTextNode == mTextNode &&
+ aOtherTransaction.mOffset == mOffset + mStringToInsert.Length();
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/InsertTextTransaction.h b/editor/libeditor/InsertTextTransaction.h
new file mode 100644
index 000000000..f97f20e37
--- /dev/null
+++ b/editor/libeditor/InsertTextTransaction.h
@@ -0,0 +1,88 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef InsertTextTransaction_h
+#define InsertTextTransaction_h
+
+#include "mozilla/EditTransactionBase.h" // base class
+#include "nsCycleCollectionParticipant.h" // various macros
+#include "nsID.h" // NS_DECLARE_STATIC_IID_ACCESSOR
+#include "nsISupportsImpl.h" // NS_DECL_ISUPPORTS_INHERITED
+#include "nsString.h" // nsString members
+#include "nscore.h" // NS_IMETHOD, nsAString
+
+class nsITransaction;
+
+#define NS_INSERTTEXTTXN_IID \
+{ 0x8c9ad77f, 0x22a7, 0x4d01, \
+ { 0xb1, 0x59, 0x8a, 0x0f, 0xdb, 0x1d, 0x08, 0xe9 } }
+
+namespace mozilla {
+
+class EditorBase;
+class RangeUpdater;
+
+namespace dom {
+class Text;
+} // namespace dom
+
+/**
+ * A transaction that inserts text into a content node.
+ */
+class InsertTextTransaction final : public EditTransactionBase
+{
+public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_INSERTTEXTTXN_IID)
+
+ /**
+ * @param aElement The text content node.
+ * @param aOffset The location in aElement to do the insertion.
+ * @param aString The new text to insert.
+ * @param aPresShell Used to get and set the selection.
+ * @param aRangeUpdater The range updater
+ */
+ InsertTextTransaction(dom::Text& aTextNode, uint32_t aOffset,
+ const nsAString& aString, EditorBase& aEditorBase,
+ RangeUpdater* aRangeUpdater);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(InsertTextTransaction,
+ EditTransactionBase)
+
+ NS_DECL_EDITTRANSACTIONBASE
+
+ NS_IMETHOD Merge(nsITransaction* aTransaction, bool* aDidMerge) override;
+
+ /**
+ * Return the string data associated with this transaction.
+ */
+ void GetData(nsString& aResult);
+
+private:
+ virtual ~InsertTextTransaction();
+
+ // Return true if aOtherTransaction immediately follows this transaction.
+ bool IsSequentialInsert(InsertTextTransaction& aOtherTrasaction);
+
+ // The Text node to operate upon.
+ RefPtr<dom::Text> mTextNode;
+
+ // The offset into mTextNode where the insertion is to take place.
+ uint32_t mOffset;
+
+ // The text to insert into mTextNode at mOffset.
+ nsString mStringToInsert;
+
+ // The editor, which we'll need to get the selection.
+ EditorBase& mEditorBase;
+
+ RangeUpdater* mRangeUpdater;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(InsertTextTransaction, NS_INSERTTEXTTXN_IID)
+
+} // namespace mozilla
+
+#endif // #ifndef InsertTextTransaction_h
diff --git a/editor/libeditor/InternetCiter.cpp b/editor/libeditor/InternetCiter.cpp
new file mode 100644
index 000000000..ed9fa5549
--- /dev/null
+++ b/editor/libeditor/InternetCiter.cpp
@@ -0,0 +1,366 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "InternetCiter.h"
+
+#include "nsAString.h"
+#include "nsCOMPtr.h"
+#include "nsCRT.h"
+#include "nsDebug.h"
+#include "nsDependentSubstring.h"
+#include "nsError.h"
+#include "nsILineBreaker.h"
+#include "nsLWBrkCIID.h"
+#include "nsServiceManagerUtils.h"
+#include "nsString.h"
+#include "nsStringIterator.h"
+
+namespace mozilla {
+
+const char16_t gt ('>');
+const char16_t space (' ');
+const char16_t nl ('\n');
+const char16_t cr('\r');
+
+/**
+ * Mail citations using the Internet style: > This is a citation.
+ */
+
+nsresult
+InternetCiter::GetCiteString(const nsAString& aInString,
+ nsAString& aOutString)
+{
+ aOutString.Truncate();
+ char16_t uch = nl;
+
+ // Strip trailing new lines which will otherwise turn up
+ // as ugly quoted empty lines.
+ nsReadingIterator <char16_t> beginIter,endIter;
+ aInString.BeginReading(beginIter);
+ aInString.EndReading(endIter);
+ while(beginIter!= endIter &&
+ (*endIter == cr || *endIter == nl)) {
+ --endIter;
+ }
+
+ // Loop over the string:
+ while (beginIter != endIter) {
+ if (uch == nl) {
+ aOutString.Append(gt);
+ // No space between >: this is ">>> " style quoting, for
+ // compatibility with RFC 2646 and format=flowed.
+ if (*beginIter != gt) {
+ aOutString.Append(space);
+ }
+ }
+
+ uch = *beginIter;
+ ++beginIter;
+
+ aOutString += uch;
+ }
+
+ if (uch != nl) {
+ aOutString += nl;
+ }
+ return NS_OK;
+}
+
+nsresult
+InternetCiter::StripCitesAndLinebreaks(const nsAString& aInString,
+ nsAString& aOutString,
+ bool aLinebreaksToo,
+ int32_t* aCiteLevel)
+{
+ if (aCiteLevel) {
+ *aCiteLevel = 0;
+ }
+
+ aOutString.Truncate();
+ nsReadingIterator <char16_t> beginIter,endIter;
+ aInString.BeginReading(beginIter);
+ aInString.EndReading(endIter);
+ while (beginIter!= endIter) { // loop over lines
+ // Clear out cites first, at the beginning of the line:
+ int32_t thisLineCiteLevel = 0;
+ while (beginIter!= endIter &&
+ (*beginIter == gt || nsCRT::IsAsciiSpace(*beginIter))) {
+ if (*beginIter == gt) {
+ ++thisLineCiteLevel;
+ }
+ ++beginIter;
+ }
+ // Now copy characters until line end:
+ while (beginIter != endIter && (*beginIter != '\r' && *beginIter != '\n')) {
+ aOutString.Append(*beginIter);
+ ++beginIter;
+ }
+ if (aLinebreaksToo) {
+ aOutString.Append(char16_t(' '));
+ } else {
+ aOutString.Append(char16_t('\n')); // DOM linebreaks, not NS_LINEBREAK
+ }
+ // Skip over any more consecutive linebreak-like characters:
+ while (beginIter != endIter && (*beginIter == '\r' || *beginIter == '\n')) {
+ ++beginIter;
+ }
+ // Done with this line -- update cite level
+ if (aCiteLevel && (thisLineCiteLevel > *aCiteLevel)) {
+ *aCiteLevel = thisLineCiteLevel;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+InternetCiter::StripCites(const nsAString& aInString,
+ nsAString& aOutString)
+{
+ return StripCitesAndLinebreaks(aInString, aOutString, false, 0);
+}
+
+static void AddCite(nsAString& aOutString, int32_t citeLevel)
+{
+ for (int32_t i = 0; i < citeLevel; ++i) {
+ aOutString.Append(gt);
+ }
+ if (citeLevel > 0) {
+ aOutString.Append(space);
+ }
+}
+
+static inline void
+BreakLine(nsAString& aOutString, uint32_t& outStringCol,
+ uint32_t citeLevel)
+{
+ aOutString.Append(nl);
+ if (citeLevel > 0) {
+ AddCite(aOutString, citeLevel);
+ outStringCol = citeLevel + 1;
+ } else {
+ outStringCol = 0;
+ }
+}
+
+static inline bool IsSpace(char16_t c)
+{
+ const char16_t nbsp (0xa0);
+ return (nsCRT::IsAsciiSpace(c) || (c == nl) || (c == cr) || (c == nbsp));
+}
+
+nsresult
+InternetCiter::Rewrap(const nsAString& aInString,
+ uint32_t aWrapCol,
+ uint32_t aFirstLineOffset,
+ bool aRespectNewlines,
+ nsAString& aOutString)
+{
+ // There shouldn't be returns in this string, only dom newlines.
+ // Check to make sure:
+#ifdef DEBUG
+ int32_t cr = aInString.FindChar(char16_t('\r'));
+ NS_ASSERTION((cr < 0), "Rewrap: CR in string gotten from DOM!\n");
+#endif /* DEBUG */
+
+ aOutString.Truncate();
+
+ nsresult rv;
+ nsCOMPtr<nsILineBreaker> lineBreaker = do_GetService(NS_LBRK_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Loop over lines in the input string, rewrapping each one.
+ uint32_t length;
+ uint32_t posInString = 0;
+ uint32_t outStringCol = 0;
+ uint32_t citeLevel = 0;
+ const nsPromiseFlatString &tString = PromiseFlatString(aInString);
+ length = tString.Length();
+ while (posInString < length) {
+ // Get the new cite level here since we're at the beginning of a line
+ uint32_t newCiteLevel = 0;
+ while (posInString < length && tString[posInString] == gt) {
+ ++newCiteLevel;
+ ++posInString;
+ while (posInString < length && tString[posInString] == space) {
+ ++posInString;
+ }
+ }
+ if (posInString >= length) {
+ break;
+ }
+
+ // Special case: if this is a blank line, maintain a blank line
+ // (retain the original paragraph breaks)
+ if (tString[posInString] == nl && !aOutString.IsEmpty()) {
+ if (aOutString.Last() != nl) {
+ aOutString.Append(nl);
+ }
+ AddCite(aOutString, newCiteLevel);
+ aOutString.Append(nl);
+
+ ++posInString;
+ outStringCol = 0;
+ continue;
+ }
+
+ // If the cite level has changed, then start a new line with the
+ // new cite level (but if we're at the beginning of the string,
+ // don't bother).
+ if (newCiteLevel != citeLevel && posInString > newCiteLevel+1 &&
+ outStringCol) {
+ BreakLine(aOutString, outStringCol, 0);
+ }
+ citeLevel = newCiteLevel;
+
+ // Prepend the quote level to the out string if appropriate
+ if (!outStringCol) {
+ AddCite(aOutString, citeLevel);
+ outStringCol = citeLevel + (citeLevel ? 1 : 0);
+ }
+ // If it's not a cite, and we're not at the beginning of a line in
+ // the output string, add a space to separate new text from the
+ // previous text.
+ else if (outStringCol > citeLevel) {
+ aOutString.Append(space);
+ ++outStringCol;
+ }
+
+ // find the next newline -- don't want to go farther than that
+ int32_t nextNewline = tString.FindChar(nl, posInString);
+ if (nextNewline < 0) {
+ nextNewline = length;
+ }
+
+ // For now, don't wrap unquoted lines at all.
+ // This is because the plaintext edit window has already wrapped them
+ // by the time we get them for rewrap, yet when we call the line
+ // breaker, it will refuse to break backwards, and we'll end up
+ // with a line that's too long and gets displayed as a lone word
+ // on a line by itself. Need special logic to detect this case
+ // and break it ourselves without resorting to the line breaker.
+ if (!citeLevel) {
+ aOutString.Append(Substring(tString, posInString,
+ nextNewline-posInString));
+ outStringCol += nextNewline - posInString;
+ if (nextNewline != (int32_t)length) {
+ aOutString.Append(nl);
+ outStringCol = 0;
+ }
+ posInString = nextNewline+1;
+ continue;
+ }
+
+ // Otherwise we have to use the line breaker and loop
+ // over this line of the input string to get all of it:
+ while ((int32_t)posInString < nextNewline) {
+ // Skip over initial spaces:
+ while ((int32_t)posInString < nextNewline &&
+ nsCRT::IsAsciiSpace(tString[posInString])) {
+ ++posInString;
+ }
+
+ // If this is a short line, just append it and continue:
+ if (outStringCol + nextNewline - posInString <= aWrapCol-citeLevel-1) {
+ // If this short line is the final one in the in string,
+ // then we need to include the final newline, if any:
+ if (nextNewline+1 == (int32_t)length && tString[nextNewline-1] == nl) {
+ ++nextNewline;
+ }
+ // Trim trailing spaces:
+ int32_t lastRealChar = nextNewline;
+ while ((uint32_t)lastRealChar > posInString &&
+ nsCRT::IsAsciiSpace(tString[lastRealChar-1])) {
+ --lastRealChar;
+ }
+
+ aOutString += Substring(tString,
+ posInString, lastRealChar - posInString);
+ outStringCol += lastRealChar - posInString;
+ posInString = nextNewline + 1;
+ continue;
+ }
+
+ int32_t eol = posInString + aWrapCol - citeLevel - outStringCol;
+ // eol is the prospective end of line.
+ // We'll first look backwards from there for a place to break.
+ // If it's already less than our current position,
+ // then our line is already too long, so break now.
+ if (eol <= (int32_t)posInString) {
+ BreakLine(aOutString, outStringCol, citeLevel);
+ continue; // continue inner loop, with outStringCol now at bol
+ }
+
+ int32_t breakPt = 0;
+ // XXX Why this uses NS_ERROR_"BASE"?
+ rv = NS_ERROR_BASE;
+ if (lineBreaker) {
+ breakPt = lineBreaker->Prev(tString.get() + posInString,
+ length - posInString, eol + 1 - posInString);
+ if (breakPt == NS_LINEBREAKER_NEED_MORE_TEXT) {
+ // if we couldn't find a breakpoint looking backwards,
+ // and we're not starting a new line, then end this line
+ // and loop around again:
+ if (outStringCol > citeLevel + 1) {
+ BreakLine(aOutString, outStringCol, citeLevel);
+ continue; // continue inner loop, with outStringCol now at bol
+ }
+
+ // Else try looking forwards:
+ breakPt = lineBreaker->Next(tString.get() + posInString,
+ length - posInString, eol - posInString);
+
+ rv = breakPt == NS_LINEBREAKER_NEED_MORE_TEXT ? NS_ERROR_BASE :
+ NS_OK;
+ } else {
+ rv = NS_OK;
+ }
+ }
+ // If rv is okay, then breakPt is the place to break.
+ // If we get out here and rv is set, something went wrong with line
+ // breaker. Just break the line, hard.
+ if (NS_FAILED(rv)) {
+ breakPt = eol;
+ }
+
+ // Special case: maybe we should have wrapped last time.
+ // If the first breakpoint here makes the current line too long,
+ // then if we already have text on the current line,
+ // break and loop around again.
+ // If we're at the beginning of the current line, though,
+ // don't force a break since the long word might be a url
+ // and breaking it would make it unclickable on the other end.
+ const int SLOP = 6;
+ if (outStringCol + breakPt > aWrapCol + SLOP &&
+ outStringCol > citeLevel+1) {
+ BreakLine(aOutString, outStringCol, citeLevel);
+ continue;
+ }
+
+ nsAutoString sub (Substring(tString, posInString, breakPt));
+ // skip newlines or whitespace at the end of the string
+ int32_t subend = sub.Length();
+ while (subend > 0 && IsSpace(sub[subend-1])) {
+ --subend;
+ }
+ sub.Left(sub, subend);
+ aOutString += sub;
+ outStringCol += sub.Length();
+ // Advance past the whitespace which caused the wrap:
+ posInString += breakPt;
+ while (posInString < length && IsSpace(tString[posInString])) {
+ ++posInString;
+ }
+
+ // Add a newline and the quote level to the out string
+ if (posInString < length) { // not for the last line, though
+ BreakLine(aOutString, outStringCol, citeLevel);
+ }
+ } // end inner loop within one line of aInString
+ } // end outer loop over lines of aInString
+
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/InternetCiter.h b/editor/libeditor/InternetCiter.h
new file mode 100644
index 000000000..d1a861678
--- /dev/null
+++ b/editor/libeditor/InternetCiter.h
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef InternetCiter_h
+#define InternetCiter_h
+
+#include "nscore.h"
+#include "nsStringFwd.h"
+
+namespace mozilla {
+
+/**
+ * Mail citations using standard Internet style.
+ */
+class InternetCiter final
+{
+public:
+ static nsresult GetCiteString(const nsAString& aInString,
+ nsAString& aOutString);
+
+ static nsresult StripCites(const nsAString& aInString,
+ nsAString& aOutString);
+
+ static nsresult Rewrap(const nsAString& aInString,
+ uint32_t aWrapCol, uint32_t aFirstLineOffset,
+ bool aRespectNewlines,
+ nsAString& aOutString);
+
+protected:
+ static nsresult StripCitesAndLinebreaks(const nsAString& aInString,
+ nsAString& aOutString,
+ bool aLinebreaksToo,
+ int32_t* aCiteLevel);
+};
+
+} // namespace mozilla
+
+#endif // #ifndef InternetCiter_h
diff --git a/editor/libeditor/JoinNodeTransaction.cpp b/editor/libeditor/JoinNodeTransaction.cpp
new file mode 100644
index 000000000..228d1f4d6
--- /dev/null
+++ b/editor/libeditor/JoinNodeTransaction.cpp
@@ -0,0 +1,109 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "JoinNodeTransaction.h"
+
+#include "mozilla/EditorBase.h" // for EditorBase
+#include "nsAString.h"
+#include "nsDebug.h" // for NS_ASSERTION, etc.
+#include "nsError.h" // for NS_ERROR_NULL_POINTER, etc.
+#include "nsIContent.h" // for nsIContent
+#include "nsIDOMCharacterData.h" // for nsIDOMCharacterData
+#include "nsIEditor.h" // for EditorBase::IsModifiableNode
+#include "nsISupportsImpl.h" // for QueryInterface, etc.
+
+namespace mozilla {
+
+using namespace dom;
+
+JoinNodeTransaction::JoinNodeTransaction(EditorBase& aEditorBase,
+ nsINode& aLeftNode,
+ nsINode& aRightNode)
+ : mEditorBase(aEditorBase)
+ , mLeftNode(&aLeftNode)
+ , mRightNode(&aRightNode)
+ , mOffset(0)
+{
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(JoinNodeTransaction, EditTransactionBase,
+ mLeftNode,
+ mRightNode,
+ mParent)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(JoinNodeTransaction)
+NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
+
+nsresult
+JoinNodeTransaction::CheckValidity()
+{
+ if (!mEditorBase.IsModifiableNode(mLeftNode->GetParentNode())) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+// After DoTransaction() and RedoTransaction(), the left node is removed from
+// the content tree and right node remains.
+NS_IMETHODIMP
+JoinNodeTransaction::DoTransaction()
+{
+ // Get the parent node
+ nsCOMPtr<nsINode> leftParent = mLeftNode->GetParentNode();
+ NS_ENSURE_TRUE(leftParent, NS_ERROR_NULL_POINTER);
+
+ // Verify that mLeftNode and mRightNode have the same parent
+ if (leftParent != mRightNode->GetParentNode()) {
+ NS_ASSERTION(false, "Nodes do not have same parent");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // Set this instance's mParent. Other methods will see a non-null mParent
+ // and know all is well
+ mParent = leftParent;
+ mOffset = mLeftNode->Length();
+
+ return mEditorBase.JoinNodesImpl(mRightNode, mLeftNode, mParent);
+}
+
+//XXX: What if instead of split, we just deleted the unneeded children of
+// mRight and re-inserted mLeft?
+NS_IMETHODIMP
+JoinNodeTransaction::UndoTransaction()
+{
+ MOZ_ASSERT(mParent);
+
+ // First, massage the existing node so it is in its post-split state
+ ErrorResult rv;
+ if (mRightNode->GetAsText()) {
+ rv = mRightNode->GetAsText()->DeleteData(0, mOffset);
+ } else {
+ nsCOMPtr<nsIContent> child = mRightNode->GetFirstChild();
+ for (uint32_t i = 0; i < mOffset; i++) {
+ if (rv.Failed()) {
+ return rv.StealNSResult();
+ }
+ if (!child) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ nsCOMPtr<nsIContent> nextSibling = child->GetNextSibling();
+ mLeftNode->AppendChild(*child, rv);
+ child = nextSibling;
+ }
+ }
+ // Second, re-insert the left node into the tree
+ nsCOMPtr<nsINode> refNode = mRightNode;
+ mParent->InsertBefore(*mLeftNode, refNode, rv);
+ return rv.StealNSResult();
+}
+
+NS_IMETHODIMP
+JoinNodeTransaction::GetTxnDescription(nsAString& aString)
+{
+ aString.AssignLiteral("JoinNodeTransaction");
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/JoinNodeTransaction.h b/editor/libeditor/JoinNodeTransaction.h
new file mode 100644
index 000000000..84208cb45
--- /dev/null
+++ b/editor/libeditor/JoinNodeTransaction.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef JoinNodeTransaction_h
+#define JoinNodeTransaction_h
+
+#include "mozilla/EditTransactionBase.h" // for EditTransactionBase, etc.
+#include "nsCOMPtr.h" // for nsCOMPtr
+#include "nsCycleCollectionParticipant.h"
+#include "nsID.h" // for REFNSIID
+#include "nscore.h" // for NS_IMETHOD
+
+class nsINode;
+
+namespace mozilla {
+
+class EditorBase;
+
+/**
+ * A transaction that joins two nodes E1 (left node) and E2 (right node) into a
+ * single node E. The children of E are the children of E1 followed by the
+ * children of E2. After DoTransaction() and RedoTransaction(), E1 is removed
+ * from the content tree and E2 remains.
+ */
+class JoinNodeTransaction final : public EditTransactionBase
+{
+public:
+ /**
+ * @param aEditorBase The provider of core editing operations.
+ * @param aLeftNode The first of two nodes to join.
+ * @param aRightNode The second of two nodes to join.
+ */
+ JoinNodeTransaction(EditorBase& aEditorBase,
+ nsINode& aLeftNode, nsINode& aRightNode);
+
+ /**
+ * Call this after constructing to ensure the inputs are correct.
+ */
+ nsresult CheckValidity();
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(JoinNodeTransaction,
+ EditTransactionBase)
+ NS_IMETHOD QueryInterface(REFNSIID aIID, void** aInstancePtr) override;
+
+ NS_DECL_EDITTRANSACTIONBASE
+
+protected:
+ EditorBase& mEditorBase;
+
+ // The nodes to operate upon. After the merge, mRightNode remains and
+ // mLeftNode is removed from the content tree.
+ nsCOMPtr<nsINode> mLeftNode;
+ nsCOMPtr<nsINode> mRightNode;
+
+ // The offset into mNode where the children of mElement are split (for
+ // undo). mOffset is the index of the first child in the right node. -1
+ // means the left node had no children.
+ uint32_t mOffset;
+
+ // The parent node containing mLeftNode and mRightNode.
+ nsCOMPtr<nsINode> mParent;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef JoinNodeTransaction_h
diff --git a/editor/libeditor/PlaceholderTransaction.cpp b/editor/libeditor/PlaceholderTransaction.cpp
new file mode 100644
index 000000000..1031b45ab
--- /dev/null
+++ b/editor/libeditor/PlaceholderTransaction.cpp
@@ -0,0 +1,270 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "PlaceholderTransaction.h"
+
+#include "CompositionTransaction.h"
+#include "mozilla/EditorBase.h"
+#include "mozilla/dom/Selection.h"
+#include "nsGkAtoms.h"
+#include "nsQueryObject.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+PlaceholderTransaction::PlaceholderTransaction()
+ : mAbsorb(true)
+ , mForwarding(nullptr)
+ , mCompositionTransaction(nullptr)
+ , mCommitted(false)
+ , mEditorBase(nullptr)
+{
+}
+
+PlaceholderTransaction::~PlaceholderTransaction()
+{
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(PlaceholderTransaction)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(PlaceholderTransaction,
+ EditAggregateTransaction)
+ if (tmp->mStartSel) {
+ ImplCycleCollectionUnlink(*tmp->mStartSel);
+ }
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mEndSel);
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(PlaceholderTransaction,
+ EditAggregateTransaction)
+ if (tmp->mStartSel) {
+ ImplCycleCollectionTraverse(cb, *tmp->mStartSel, "mStartSel", 0);
+ }
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEndSel);
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PlaceholderTransaction)
+ NS_INTERFACE_MAP_ENTRY(nsIAbsorbingTransaction)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+NS_INTERFACE_MAP_END_INHERITING(EditAggregateTransaction)
+
+NS_IMPL_ADDREF_INHERITED(PlaceholderTransaction, EditAggregateTransaction)
+NS_IMPL_RELEASE_INHERITED(PlaceholderTransaction, EditAggregateTransaction)
+
+NS_IMETHODIMP
+PlaceholderTransaction::Init(nsIAtom* aName,
+ SelectionState* aSelState,
+ EditorBase* aEditorBase)
+{
+ NS_ENSURE_TRUE(aEditorBase && aSelState, NS_ERROR_NULL_POINTER);
+
+ mName = aName;
+ mStartSel = aSelState;
+ mEditorBase = aEditorBase;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PlaceholderTransaction::DoTransaction()
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PlaceholderTransaction::UndoTransaction()
+{
+ // Undo transactions.
+ nsresult rv = EditAggregateTransaction::UndoTransaction();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ENSURE_TRUE(mStartSel, NS_ERROR_NULL_POINTER);
+
+ // now restore selection
+ RefPtr<Selection> selection = mEditorBase->GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+ return mStartSel->RestoreSelection(selection);
+}
+
+NS_IMETHODIMP
+PlaceholderTransaction::RedoTransaction()
+{
+ // Redo transactions.
+ nsresult rv = EditAggregateTransaction::RedoTransaction();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // now restore selection
+ RefPtr<Selection> selection = mEditorBase->GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+ return mEndSel.RestoreSelection(selection);
+}
+
+
+NS_IMETHODIMP
+PlaceholderTransaction::Merge(nsITransaction* aTransaction,
+ bool* aDidMerge)
+{
+ NS_ENSURE_TRUE(aDidMerge && aTransaction, NS_ERROR_NULL_POINTER);
+
+ // set out param default value
+ *aDidMerge=false;
+
+ if (mForwarding) {
+ NS_NOTREACHED("tried to merge into a placeholder that was in forwarding mode!");
+ return NS_ERROR_FAILURE;
+ }
+
+ // check to see if aTransaction is one of the editor's
+ // private transactions. If not, we want to avoid merging
+ // the foreign transaction into our placeholder since we
+ // don't know what it does.
+
+ nsCOMPtr<nsPIEditorTransaction> pTxn = do_QueryInterface(aTransaction);
+ NS_ENSURE_TRUE(pTxn, NS_OK); // it's foreign so just bail!
+
+ // XXX: hack, not safe! need nsIEditTransaction!
+ EditTransactionBase* editTransactionBase = (EditTransactionBase*)aTransaction;
+ // determine if this incoming txn is a placeholder txn
+ nsCOMPtr<nsIAbsorbingTransaction> absorbingTransaction =
+ do_QueryObject(editTransactionBase);
+
+ // We are absorbing all transactions if mAbsorb is lit.
+ if (mAbsorb) {
+ RefPtr<CompositionTransaction> otherTransaction =
+ do_QueryObject(aTransaction);
+ if (otherTransaction) {
+ // special handling for CompositionTransaction's: they need to merge with
+ // any previous CompositionTransaction in this placeholder, if possible.
+ if (!mCompositionTransaction) {
+ // this is the first IME txn in the placeholder
+ mCompositionTransaction = otherTransaction;
+ AppendChild(editTransactionBase);
+ } else {
+ bool didMerge;
+ mCompositionTransaction->Merge(otherTransaction, &didMerge);
+ if (!didMerge) {
+ // it wouldn't merge. Earlier IME txn is already committed and will
+ // not absorb further IME txns. So just stack this one after it
+ // and remember it as a candidate for further merges.
+ mCompositionTransaction = otherTransaction;
+ AppendChild(editTransactionBase);
+ }
+ }
+ } else if (!absorbingTransaction) {
+ // See bug 171243: just drop incoming placeholders on the floor.
+ // Their children will be swallowed by this preexisting one.
+ AppendChild(editTransactionBase);
+ }
+ *aDidMerge = true;
+// RememberEndingSelection();
+// efficiency hack: no need to remember selection here, as we haven't yet
+// finished the initial batch and we know we will be told when the batch ends.
+// we can remeber the selection then.
+ } else {
+ // merge typing or IME or deletion transactions if the selection matches
+ if ((mName.get() == nsGkAtoms::TypingTxnName ||
+ mName.get() == nsGkAtoms::IMETxnName ||
+ mName.get() == nsGkAtoms::DeleteTxnName) && !mCommitted) {
+ if (absorbingTransaction) {
+ nsCOMPtr<nsIAtom> atom;
+ absorbingTransaction->GetTxnName(getter_AddRefs(atom));
+ if (atom && atom == mName) {
+ // check if start selection of next placeholder matches
+ // end selection of this placeholder
+ bool isSame;
+ absorbingTransaction->StartSelectionEquals(&mEndSel, &isSame);
+ if (isSame) {
+ mAbsorb = true; // we need to start absorbing again
+ absorbingTransaction->ForwardEndBatchTo(this);
+ // AppendChild(editTransactionBase);
+ // see bug 171243: we don't need to merge placeholders
+ // into placeholders. We just reactivate merging in the pre-existing
+ // placeholder and drop the new one on the floor. The EndPlaceHolderBatch()
+ // call on the new placeholder will be forwarded to this older one.
+ RememberEndingSelection();
+ *aDidMerge = true;
+ }
+ }
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PlaceholderTransaction::GetTxnDescription(nsAString& aString)
+{
+ aString.AssignLiteral("PlaceholderTransaction: ");
+
+ if (mName) {
+ nsAutoString name;
+ mName->ToString(name);
+ aString += name;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PlaceholderTransaction::GetTxnName(nsIAtom** aName)
+{
+ return GetName(aName);
+}
+
+NS_IMETHODIMP
+PlaceholderTransaction::StartSelectionEquals(SelectionState* aSelState,
+ bool* aResult)
+{
+ // determine if starting selection matches the given selection state.
+ // note that we only care about collapsed selections.
+ NS_ENSURE_TRUE(aResult && aSelState, NS_ERROR_NULL_POINTER);
+ if (!mStartSel->IsCollapsed() || !aSelState->IsCollapsed()) {
+ *aResult = false;
+ return NS_OK;
+ }
+ *aResult = mStartSel->IsEqual(aSelState);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PlaceholderTransaction::EndPlaceHolderBatch()
+{
+ mAbsorb = false;
+
+ if (mForwarding) {
+ nsCOMPtr<nsIAbsorbingTransaction> plcTxn = do_QueryReferent(mForwarding);
+ if (plcTxn) {
+ plcTxn->EndPlaceHolderBatch();
+ }
+ }
+ // remember our selection state.
+ return RememberEndingSelection();
+}
+
+NS_IMETHODIMP
+PlaceholderTransaction::ForwardEndBatchTo(
+ nsIAbsorbingTransaction* aForwardingAddress)
+{
+ mForwarding = do_GetWeakReference(aForwardingAddress);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PlaceholderTransaction::Commit()
+{
+ mCommitted = true;
+ return NS_OK;
+}
+
+nsresult
+PlaceholderTransaction::RememberEndingSelection()
+{
+ RefPtr<Selection> selection = mEditorBase->GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+ mEndSel.SaveSelection(selection);
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/PlaceholderTransaction.h b/editor/libeditor/PlaceholderTransaction.h
new file mode 100644
index 000000000..867a82ce3
--- /dev/null
+++ b/editor/libeditor/PlaceholderTransaction.h
@@ -0,0 +1,92 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef PlaceholderTransaction_h
+#define PlaceholderTransaction_h
+
+#include "EditAggregateTransaction.h"
+#include "mozilla/EditorUtils.h"
+#include "nsIAbsorbingTransaction.h"
+#include "nsIDOMNode.h"
+#include "nsCOMPtr.h"
+#include "nsWeakPtr.h"
+#include "nsWeakReference.h"
+#include "nsAutoPtr.h"
+
+namespace mozilla {
+
+class CompositionTransaction;
+
+/**
+ * An aggregate transaction that knows how to absorb all subsequent
+ * transactions with the same name. This transaction does not "Do" anything.
+ * But it absorbs other transactions via merge, and can undo/redo the
+ * transactions it has absorbed.
+ */
+
+class PlaceholderTransaction final : public EditAggregateTransaction,
+ public nsIAbsorbingTransaction,
+ public nsSupportsWeakReference
+{
+public:
+ NS_DECL_ISUPPORTS_INHERITED
+
+ PlaceholderTransaction();
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(PlaceholderTransaction,
+ EditAggregateTransaction)
+// ------------ EditAggregateTransaction -----------------------
+
+ NS_DECL_EDITTRANSACTIONBASE
+
+ NS_IMETHOD RedoTransaction() override;
+ NS_IMETHOD Merge(nsITransaction* aTransaction, bool* aDidMerge) override;
+
+// ------------ nsIAbsorbingTransaction -----------------------
+
+ NS_IMETHOD Init(nsIAtom* aName, SelectionState* aSelState,
+ EditorBase* aEditorBase) override;
+
+ NS_IMETHOD GetTxnName(nsIAtom** aName) override;
+
+ NS_IMETHOD StartSelectionEquals(SelectionState* aSelState,
+ bool* aResult) override;
+
+ NS_IMETHOD EndPlaceHolderBatch() override;
+
+ NS_IMETHOD ForwardEndBatchTo(
+ nsIAbsorbingTransaction* aForwardingAddress) override;
+
+ NS_IMETHOD Commit() override;
+
+ nsresult RememberEndingSelection();
+
+protected:
+ virtual ~PlaceholderTransaction();
+
+ // Do we auto absorb any and all transaction?
+ bool mAbsorb;
+ nsWeakPtr mForwarding;
+ // First IME txn in this placeholder - used for IME merging.
+ mozilla::CompositionTransaction* mCompositionTransaction;
+ // Do we stop auto absorbing any matching placeholder transactions?
+ bool mCommitted;
+
+ // These next two members store the state of the selection in a safe way.
+ // Selection at the start of the transaction is stored, as is the selection
+ // at the end. This is so that UndoTransaction() and RedoTransaction() can
+ // restore the selection properly.
+
+ // Use a pointer because this is constructed before we exist.
+ nsAutoPtr<SelectionState> mStartSel;
+ SelectionState mEndSel;
+
+ // The editor for this transaction.
+ EditorBase* mEditorBase;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef PlaceholderTransaction_h
diff --git a/editor/libeditor/SelectionState.cpp b/editor/libeditor/SelectionState.cpp
new file mode 100644
index 000000000..f9ad5947a
--- /dev/null
+++ b/editor/libeditor/SelectionState.cpp
@@ -0,0 +1,695 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/SelectionState.h"
+
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc.
+#include "mozilla/EditorUtils.h" // for EditorUtils
+#include "mozilla/dom/Selection.h" // for Selection
+#include "nsAString.h" // for nsAString_internal::Length
+#include "nsCycleCollectionParticipant.h"
+#include "nsDebug.h" // for NS_ENSURE_TRUE, etc.
+#include "nsError.h" // for NS_OK, etc.
+#include "nsIContent.h" // for nsIContent
+#include "nsIDOMCharacterData.h" // for nsIDOMCharacterData
+#include "nsIDOMNode.h" // for nsIDOMNode
+#include "nsISupportsImpl.h" // for nsRange::Release
+#include "nsRange.h" // for nsRange
+
+namespace mozilla {
+
+using namespace dom;
+
+/******************************************************************************
+ * mozilla::SelectionState
+ *
+ * Class for recording selection info. Stores selection as collection of
+ * { {startnode, startoffset} , {endnode, endoffset} } tuples. Can't store
+ * ranges since dom gravity will possibly change the ranges.
+ ******************************************************************************/
+SelectionState::SelectionState()
+{
+}
+
+SelectionState::~SelectionState()
+{
+ MakeEmpty();
+}
+
+void
+SelectionState::SaveSelection(Selection* aSel)
+{
+ MOZ_ASSERT(aSel);
+ int32_t arrayCount = mArray.Length();
+ int32_t rangeCount = aSel->RangeCount();
+
+ // if we need more items in the array, new them
+ if (arrayCount < rangeCount) {
+ for (int32_t i = arrayCount; i < rangeCount; i++) {
+ mArray.AppendElement();
+ mArray[i] = new RangeItem();
+ }
+ } else if (arrayCount > rangeCount) {
+ // else if we have too many, delete them
+ for (int32_t i = arrayCount - 1; i >= rangeCount; i--) {
+ mArray.RemoveElementAt(i);
+ }
+ }
+
+ // now store the selection ranges
+ for (int32_t i = 0; i < rangeCount; i++) {
+ mArray[i]->StoreRange(aSel->GetRangeAt(i));
+ }
+}
+
+nsresult
+SelectionState::RestoreSelection(Selection* aSel)
+{
+ NS_ENSURE_TRUE(aSel, NS_ERROR_NULL_POINTER);
+
+ // clear out selection
+ aSel->RemoveAllRanges();
+
+ // set the selection ranges anew
+ size_t arrayCount = mArray.Length();
+ for (size_t i = 0; i < arrayCount; i++) {
+ RefPtr<nsRange> range = mArray[i]->GetRange();
+ NS_ENSURE_TRUE(range, NS_ERROR_UNEXPECTED);
+
+ nsresult rv = aSel->AddRange(range);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+bool
+SelectionState::IsCollapsed()
+{
+ if (mArray.Length() != 1) {
+ return false;
+ }
+ RefPtr<nsRange> range = mArray[0]->GetRange();
+ NS_ENSURE_TRUE(range, false);
+ bool bIsCollapsed = false;
+ range->GetCollapsed(&bIsCollapsed);
+ return bIsCollapsed;
+}
+
+bool
+SelectionState::IsEqual(SelectionState* aSelState)
+{
+ NS_ENSURE_TRUE(aSelState, false);
+ size_t myCount = mArray.Length(), itsCount = aSelState->mArray.Length();
+ if (myCount != itsCount) {
+ return false;
+ }
+ if (!myCount) {
+ return false;
+ }
+
+ for (size_t i = 0; i < myCount; i++) {
+ RefPtr<nsRange> myRange = mArray[i]->GetRange();
+ RefPtr<nsRange> itsRange = aSelState->mArray[i]->GetRange();
+ NS_ENSURE_TRUE(myRange && itsRange, false);
+
+ int16_t compResult;
+ nsresult rv;
+ rv = myRange->CompareBoundaryPoints(nsIDOMRange::START_TO_START, itsRange, &compResult);
+ if (NS_FAILED(rv) || compResult) {
+ return false;
+ }
+ rv = myRange->CompareBoundaryPoints(nsIDOMRange::END_TO_END, itsRange, &compResult);
+ if (NS_FAILED(rv) || compResult) {
+ return false;
+ }
+ }
+ // if we got here, they are equal
+ return true;
+}
+
+void
+SelectionState::MakeEmpty()
+{
+ // free any items in the array
+ mArray.Clear();
+}
+
+bool
+SelectionState::IsEmpty()
+{
+ return mArray.IsEmpty();
+}
+
+/******************************************************************************
+ * mozilla::RangeUpdater
+ *
+ * Class for updating nsRanges in response to editor actions.
+ ******************************************************************************/
+
+RangeUpdater::RangeUpdater()
+ : mLock(false)
+{
+}
+
+RangeUpdater::~RangeUpdater()
+{
+ // nothing to do, we don't own the items in our array.
+}
+
+void
+RangeUpdater::RegisterRangeItem(RangeItem* aRangeItem)
+{
+ if (!aRangeItem) {
+ return;
+ }
+ if (mArray.Contains(aRangeItem)) {
+ NS_ERROR("tried to register an already registered range");
+ return; // don't register it again. It would get doubly adjusted.
+ }
+ mArray.AppendElement(aRangeItem);
+}
+
+void
+RangeUpdater::DropRangeItem(RangeItem* aRangeItem)
+{
+ if (!aRangeItem) {
+ return;
+ }
+ mArray.RemoveElement(aRangeItem);
+}
+
+nsresult
+RangeUpdater::RegisterSelectionState(SelectionState& aSelState)
+{
+ size_t theCount = aSelState.mArray.Length();
+ if (theCount < 1) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (size_t i = 0; i < theCount; i++) {
+ RegisterRangeItem(aSelState.mArray[i]);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+RangeUpdater::DropSelectionState(SelectionState& aSelState)
+{
+ size_t theCount = aSelState.mArray.Length();
+ if (theCount < 1) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (size_t i = 0; i < theCount; i++) {
+ DropRangeItem(aSelState.mArray[i]);
+ }
+
+ return NS_OK;
+}
+
+// gravity methods:
+
+nsresult
+RangeUpdater::SelAdjCreateNode(nsINode* aParent,
+ int32_t aPosition)
+{
+ if (mLock) {
+ // lock set by Will/DidReplaceParent, etc...
+ return NS_OK;
+ }
+ NS_ENSURE_TRUE(aParent, NS_ERROR_NULL_POINTER);
+ size_t count = mArray.Length();
+ if (!count) {
+ return NS_OK;
+ }
+
+ for (size_t i = 0; i < count; i++) {
+ RangeItem* item = mArray[i];
+ NS_ENSURE_TRUE(item, NS_ERROR_NULL_POINTER);
+
+ if (item->startNode == aParent && item->startOffset > aPosition) {
+ item->startOffset++;
+ }
+ if (item->endNode == aParent && item->endOffset > aPosition) {
+ item->endOffset++;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+RangeUpdater::SelAdjCreateNode(nsIDOMNode* aParent,
+ int32_t aPosition)
+{
+ nsCOMPtr<nsINode> parent = do_QueryInterface(aParent);
+ return SelAdjCreateNode(parent, aPosition);
+}
+
+nsresult
+RangeUpdater::SelAdjInsertNode(nsINode* aParent,
+ int32_t aPosition)
+{
+ return SelAdjCreateNode(aParent, aPosition);
+}
+
+nsresult
+RangeUpdater::SelAdjInsertNode(nsIDOMNode* aParent,
+ int32_t aPosition)
+{
+ return SelAdjCreateNode(aParent, aPosition);
+}
+
+void
+RangeUpdater::SelAdjDeleteNode(nsINode* aNode)
+{
+ if (mLock) {
+ // lock set by Will/DidReplaceParent, etc...
+ return;
+ }
+ MOZ_ASSERT(aNode);
+ size_t count = mArray.Length();
+ if (!count) {
+ return;
+ }
+
+ nsCOMPtr<nsINode> parent = aNode->GetParentNode();
+ int32_t offset = parent ? parent->IndexOf(aNode) : -1;
+
+ // check for range endpoints that are after aNode and in the same parent
+ for (size_t i = 0; i < count; i++) {
+ RangeItem* item = mArray[i];
+ MOZ_ASSERT(item);
+
+ if (item->startNode == parent && item->startOffset > offset) {
+ item->startOffset--;
+ }
+ if (item->endNode == parent && item->endOffset > offset) {
+ item->endOffset--;
+ }
+
+ // check for range endpoints that are in aNode
+ if (item->startNode == aNode) {
+ item->startNode = parent;
+ item->startOffset = offset;
+ }
+ if (item->endNode == aNode) {
+ item->endNode = parent;
+ item->endOffset = offset;
+ }
+
+ // check for range endpoints that are in descendants of aNode
+ nsCOMPtr<nsINode> oldStart;
+ if (EditorUtils::IsDescendantOf(item->startNode, aNode)) {
+ oldStart = item->startNode; // save for efficiency hack below.
+ item->startNode = parent;
+ item->startOffset = offset;
+ }
+
+ // avoid having to call IsDescendantOf() for common case of range startnode == range endnode.
+ if (item->endNode == oldStart ||
+ EditorUtils::IsDescendantOf(item->endNode, aNode)) {
+ item->endNode = parent;
+ item->endOffset = offset;
+ }
+ }
+}
+
+void
+RangeUpdater::SelAdjDeleteNode(nsIDOMNode* aNode)
+{
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ NS_ENSURE_TRUE_VOID(node);
+ return SelAdjDeleteNode(node);
+}
+
+nsresult
+RangeUpdater::SelAdjSplitNode(nsIContent& aOldRightNode,
+ int32_t aOffset,
+ nsIContent* aNewLeftNode)
+{
+ if (mLock) {
+ // lock set by Will/DidReplaceParent, etc...
+ return NS_OK;
+ }
+ NS_ENSURE_TRUE(aNewLeftNode, NS_ERROR_NULL_POINTER);
+ size_t count = mArray.Length();
+ if (!count) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsINode> parent = aOldRightNode.GetParentNode();
+ int32_t offset = parent ? parent->IndexOf(&aOldRightNode) : -1;
+
+ // first part is same as inserting aNewLeftnode
+ nsresult rv = SelAdjInsertNode(parent, offset - 1);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // next step is to check for range enpoints inside aOldRightNode
+ for (size_t i = 0; i < count; i++) {
+ RangeItem* item = mArray[i];
+ NS_ENSURE_TRUE(item, NS_ERROR_NULL_POINTER);
+
+ if (item->startNode == &aOldRightNode) {
+ if (item->startOffset > aOffset) {
+ item->startOffset -= aOffset;
+ } else {
+ item->startNode = aNewLeftNode;
+ }
+ }
+ if (item->endNode == &aOldRightNode) {
+ if (item->endOffset > aOffset) {
+ item->endOffset -= aOffset;
+ } else {
+ item->endNode = aNewLeftNode;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+RangeUpdater::SelAdjJoinNodes(nsINode& aLeftNode,
+ nsINode& aRightNode,
+ nsINode& aParent,
+ int32_t aOffset,
+ int32_t aOldLeftNodeLength)
+{
+ if (mLock) {
+ // lock set by Will/DidReplaceParent, etc...
+ return NS_OK;
+ }
+ size_t count = mArray.Length();
+ if (!count) {
+ return NS_OK;
+ }
+
+ for (size_t i = 0; i < count; i++) {
+ RangeItem* item = mArray[i];
+ NS_ENSURE_TRUE(item, NS_ERROR_NULL_POINTER);
+
+ if (item->startNode == &aParent) {
+ // adjust start point in aParent
+ if (item->startOffset > aOffset) {
+ item->startOffset--;
+ } else if (item->startOffset == aOffset) {
+ // join keeps right hand node
+ item->startNode = &aRightNode;
+ item->startOffset = aOldLeftNodeLength;
+ }
+ } else if (item->startNode == &aRightNode) {
+ // adjust start point in aRightNode
+ item->startOffset += aOldLeftNodeLength;
+ } else if (item->startNode == &aLeftNode) {
+ // adjust start point in aLeftNode
+ item->startNode = &aRightNode;
+ }
+
+ if (item->endNode == &aParent) {
+ // adjust end point in aParent
+ if (item->endOffset > aOffset) {
+ item->endOffset--;
+ } else if (item->endOffset == aOffset) {
+ // join keeps right hand node
+ item->endNode = &aRightNode;
+ item->endOffset = aOldLeftNodeLength;
+ }
+ } else if (item->endNode == &aRightNode) {
+ // adjust end point in aRightNode
+ item->endOffset += aOldLeftNodeLength;
+ } else if (item->endNode == &aLeftNode) {
+ // adjust end point in aLeftNode
+ item->endNode = &aRightNode;
+ }
+ }
+
+ return NS_OK;
+}
+
+void
+RangeUpdater::SelAdjInsertText(Text& aTextNode,
+ int32_t aOffset,
+ const nsAString& aString)
+{
+ if (mLock) {
+ // lock set by Will/DidReplaceParent, etc...
+ return;
+ }
+
+ size_t count = mArray.Length();
+ if (!count) {
+ return;
+ }
+
+ size_t len = aString.Length();
+ for (size_t i = 0; i < count; i++) {
+ RangeItem* item = mArray[i];
+ MOZ_ASSERT(item);
+
+ if (item->startNode == &aTextNode && item->startOffset > aOffset) {
+ item->startOffset += len;
+ }
+ if (item->endNode == &aTextNode && item->endOffset > aOffset) {
+ item->endOffset += len;
+ }
+ }
+ return;
+}
+
+nsresult
+RangeUpdater::SelAdjDeleteText(nsIContent* aTextNode,
+ int32_t aOffset,
+ int32_t aLength)
+{
+ if (mLock) {
+ // lock set by Will/DidReplaceParent, etc...
+ return NS_OK;
+ }
+
+ size_t count = mArray.Length();
+ if (!count) {
+ return NS_OK;
+ }
+ NS_ENSURE_TRUE(aTextNode, NS_ERROR_NULL_POINTER);
+
+ for (size_t i = 0; i < count; i++) {
+ RangeItem* item = mArray[i];
+ NS_ENSURE_TRUE(item, NS_ERROR_NULL_POINTER);
+
+ if (item->startNode == aTextNode && item->startOffset > aOffset) {
+ item->startOffset -= aLength;
+ if (item->startOffset < 0) {
+ item->startOffset = 0;
+ }
+ }
+ if (item->endNode == aTextNode && item->endOffset > aOffset) {
+ item->endOffset -= aLength;
+ if (item->endOffset < 0) {
+ item->endOffset = 0;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+RangeUpdater::SelAdjDeleteText(nsIDOMCharacterData* aTextNode,
+ int32_t aOffset,
+ int32_t aLength)
+{
+ nsCOMPtr<nsIContent> textNode = do_QueryInterface(aTextNode);
+ return SelAdjDeleteText(textNode, aOffset, aLength);
+}
+
+nsresult
+RangeUpdater::WillReplaceContainer()
+{
+ if (mLock) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ mLock = true;
+ return NS_OK;
+}
+
+nsresult
+RangeUpdater::DidReplaceContainer(Element* aOriginalNode,
+ Element* aNewNode)
+{
+ NS_ENSURE_TRUE(mLock, NS_ERROR_UNEXPECTED);
+ mLock = false;
+
+ NS_ENSURE_TRUE(aOriginalNode && aNewNode, NS_ERROR_NULL_POINTER);
+ size_t count = mArray.Length();
+ if (!count) {
+ return NS_OK;
+ }
+
+ for (size_t i = 0; i < count; i++) {
+ RangeItem* item = mArray[i];
+ NS_ENSURE_TRUE(item, NS_ERROR_NULL_POINTER);
+
+ if (item->startNode == aOriginalNode) {
+ item->startNode = aNewNode;
+ }
+ if (item->endNode == aOriginalNode) {
+ item->endNode = aNewNode;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+RangeUpdater::WillRemoveContainer()
+{
+ if (mLock) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ mLock = true;
+ return NS_OK;
+}
+
+nsresult
+RangeUpdater::DidRemoveContainer(nsINode* aNode,
+ nsINode* aParent,
+ int32_t aOffset,
+ uint32_t aNodeOrigLen)
+{
+ NS_ENSURE_TRUE(mLock, NS_ERROR_UNEXPECTED);
+ mLock = false;
+
+ NS_ENSURE_TRUE(aNode && aParent, NS_ERROR_NULL_POINTER);
+ size_t count = mArray.Length();
+ if (!count) {
+ return NS_OK;
+ }
+
+ for (size_t i = 0; i < count; i++) {
+ RangeItem* item = mArray[i];
+ NS_ENSURE_TRUE(item, NS_ERROR_NULL_POINTER);
+
+ if (item->startNode == aNode) {
+ item->startNode = aParent;
+ item->startOffset += aOffset;
+ } else if (item->startNode == aParent && item->startOffset > aOffset) {
+ item->startOffset += (int32_t)aNodeOrigLen - 1;
+ }
+
+ if (item->endNode == aNode) {
+ item->endNode = aParent;
+ item->endOffset += aOffset;
+ } else if (item->endNode == aParent && item->endOffset > aOffset) {
+ item->endOffset += (int32_t)aNodeOrigLen - 1;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+RangeUpdater::DidRemoveContainer(nsIDOMNode* aNode,
+ nsIDOMNode* aParent,
+ int32_t aOffset,
+ uint32_t aNodeOrigLen)
+{
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ nsCOMPtr<nsINode> parent = do_QueryInterface(aParent);
+ return DidRemoveContainer(node, parent, aOffset, aNodeOrigLen);
+}
+
+nsresult
+RangeUpdater::WillInsertContainer()
+{
+ if (mLock) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ mLock = true;
+ return NS_OK;
+}
+
+nsresult
+RangeUpdater::DidInsertContainer()
+{
+ NS_ENSURE_TRUE(mLock, NS_ERROR_UNEXPECTED);
+ mLock = false;
+ return NS_OK;
+}
+
+void
+RangeUpdater::WillMoveNode()
+{
+ mLock = true;
+}
+
+void
+RangeUpdater::DidMoveNode(nsINode* aOldParent, int32_t aOldOffset,
+ nsINode* aNewParent, int32_t aNewOffset)
+{
+ MOZ_ASSERT(aOldParent);
+ MOZ_ASSERT(aNewParent);
+ NS_ENSURE_TRUE_VOID(mLock);
+ mLock = false;
+
+ for (size_t i = 0, count = mArray.Length(); i < count; ++i) {
+ RangeItem* item = mArray[i];
+ NS_ENSURE_TRUE_VOID(item);
+
+ // like a delete in aOldParent
+ if (item->startNode == aOldParent && item->startOffset > aOldOffset) {
+ item->startOffset--;
+ }
+ if (item->endNode == aOldParent && item->endOffset > aOldOffset) {
+ item->endOffset--;
+ }
+
+ // and like an insert in aNewParent
+ if (item->startNode == aNewParent && item->startOffset > aNewOffset) {
+ item->startOffset++;
+ }
+ if (item->endNode == aNewParent && item->endOffset > aNewOffset) {
+ item->endOffset++;
+ }
+ }
+}
+
+/******************************************************************************
+ * mozilla::RangeItem
+ *
+ * Helper struct for SelectionState. This stores range endpoints.
+ ******************************************************************************/
+
+RangeItem::RangeItem()
+{
+}
+
+RangeItem::~RangeItem()
+{
+}
+
+NS_IMPL_CYCLE_COLLECTION(RangeItem, startNode, endNode)
+NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(RangeItem, AddRef)
+NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(RangeItem, Release)
+
+void
+RangeItem::StoreRange(nsRange* aRange)
+{
+ MOZ_ASSERT(aRange);
+ startNode = aRange->GetStartParent();
+ startOffset = aRange->StartOffset();
+ endNode = aRange->GetEndParent();
+ endOffset = aRange->EndOffset();
+}
+
+already_AddRefed<nsRange>
+RangeItem::GetRange()
+{
+ RefPtr<nsRange> range = new nsRange(startNode);
+ if (NS_FAILED(range->Set(startNode, startOffset, endNode, endOffset))) {
+ return nullptr;
+ }
+ return range.forget();
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/SelectionState.h b/editor/libeditor/SelectionState.h
new file mode 100644
index 000000000..36e7b7769
--- /dev/null
+++ b/editor/libeditor/SelectionState.h
@@ -0,0 +1,357 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_SelectionState_h
+#define mozilla_SelectionState_h
+
+#include "nsCOMPtr.h"
+#include "nsIDOMNode.h"
+#include "nsINode.h"
+#include "nsTArray.h"
+#include "nscore.h"
+
+class nsCycleCollectionTraversalCallback;
+class nsIDOMCharacterData;
+class nsRange;
+namespace mozilla {
+class RangeUpdater;
+namespace dom {
+class Selection;
+class Text;
+} // namespace dom
+
+/**
+ * A helper struct for saving/setting ranges.
+ */
+struct RangeItem final
+{
+ RangeItem();
+
+private:
+ // Private destructor, to discourage deletion outside of Release():
+ ~RangeItem();
+
+public:
+ void StoreRange(nsRange* aRange);
+ already_AddRefed<nsRange> GetRange();
+
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(RangeItem)
+ NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(RangeItem)
+
+ nsCOMPtr<nsINode> startNode;
+ int32_t startOffset;
+ nsCOMPtr<nsINode> endNode;
+ int32_t endOffset;
+};
+
+/**
+ * mozilla::SelectionState
+ *
+ * Class for recording selection info. Stores selection as collection of
+ * { {startnode, startoffset} , {endnode, endoffset} } tuples. Can't store
+ * ranges since dom gravity will possibly change the ranges.
+ */
+
+class SelectionState final
+{
+public:
+ SelectionState();
+ ~SelectionState();
+
+ void SaveSelection(dom::Selection *aSel);
+ nsresult RestoreSelection(dom::Selection* aSel);
+ bool IsCollapsed();
+ bool IsEqual(SelectionState *aSelState);
+ void MakeEmpty();
+ bool IsEmpty();
+private:
+ nsTArray<RefPtr<RangeItem>> mArray;
+
+ friend class RangeUpdater;
+ friend void ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback&,
+ SelectionState&,
+ const char*,
+ uint32_t);
+ friend void ImplCycleCollectionUnlink(SelectionState&);
+};
+
+inline void
+ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback& aCallback,
+ SelectionState& aField,
+ const char* aName,
+ uint32_t aFlags = 0)
+{
+ ImplCycleCollectionTraverse(aCallback, aField.mArray, aName, aFlags);
+}
+
+inline void
+ImplCycleCollectionUnlink(SelectionState& aField)
+{
+ ImplCycleCollectionUnlink(aField.mArray);
+}
+
+class RangeUpdater final
+{
+public:
+ RangeUpdater();
+ ~RangeUpdater();
+
+ void RegisterRangeItem(RangeItem* aRangeItem);
+ void DropRangeItem(RangeItem* aRangeItem);
+ nsresult RegisterSelectionState(SelectionState& aSelState);
+ nsresult DropSelectionState(SelectionState& aSelState);
+
+ // editor selection gravity routines. Note that we can't always depend on
+ // DOM Range gravity to do what we want to the "real" selection. For instance,
+ // if you move a node, that corresponds to deleting it and reinserting it.
+ // DOM Range gravity will promote the selection out of the node on deletion,
+ // which is not what you want if you know you are reinserting it.
+ nsresult SelAdjCreateNode(nsINode* aParent, int32_t aPosition);
+ nsresult SelAdjCreateNode(nsIDOMNode* aParent, int32_t aPosition);
+ nsresult SelAdjInsertNode(nsINode* aParent, int32_t aPosition);
+ nsresult SelAdjInsertNode(nsIDOMNode* aParent, int32_t aPosition);
+ void SelAdjDeleteNode(nsINode* aNode);
+ void SelAdjDeleteNode(nsIDOMNode* aNode);
+ nsresult SelAdjSplitNode(nsIContent& aOldRightNode, int32_t aOffset,
+ nsIContent* aNewLeftNode);
+ nsresult SelAdjJoinNodes(nsINode& aLeftNode,
+ nsINode& aRightNode,
+ nsINode& aParent,
+ int32_t aOffset,
+ int32_t aOldLeftNodeLength);
+ void SelAdjInsertText(dom::Text& aTextNode, int32_t aOffset,
+ const nsAString &aString);
+ nsresult SelAdjDeleteText(nsIContent* aTextNode, int32_t aOffset,
+ int32_t aLength);
+ nsresult SelAdjDeleteText(nsIDOMCharacterData* aTextNode,
+ int32_t aOffset, int32_t aLength);
+ // the following gravity routines need will/did sandwiches, because the other
+ // gravity routines will be called inside of these sandwiches, but should be
+ // ignored.
+ nsresult WillReplaceContainer();
+ nsresult DidReplaceContainer(dom::Element* aOriginalNode,
+ dom::Element* aNewNode);
+ nsresult WillRemoveContainer();
+ nsresult DidRemoveContainer(nsINode* aNode, nsINode* aParent,
+ int32_t aOffset, uint32_t aNodeOrigLen);
+ nsresult DidRemoveContainer(nsIDOMNode* aNode, nsIDOMNode* aParent,
+ int32_t aOffset, uint32_t aNodeOrigLen);
+ nsresult WillInsertContainer();
+ nsresult DidInsertContainer();
+ void WillMoveNode();
+ void DidMoveNode(nsINode* aOldParent, int32_t aOldOffset,
+ nsINode* aNewParent, int32_t aNewOffset);
+
+private:
+ friend void ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback&,
+ RangeUpdater&,
+ const char*,
+ uint32_t);
+ friend void ImplCycleCollectionUnlink(RangeUpdater& aField);
+
+ nsTArray<RefPtr<RangeItem>> mArray;
+ bool mLock;
+};
+
+inline void
+ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback& aCallback,
+ RangeUpdater& aField,
+ const char* aName,
+ uint32_t aFlags = 0)
+{
+ ImplCycleCollectionTraverse(aCallback, aField.mArray, aName, aFlags);
+}
+
+inline void
+ImplCycleCollectionUnlink(RangeUpdater& aField)
+{
+ ImplCycleCollectionUnlink(aField.mArray);
+}
+
+/**
+ * Helper class for using SelectionState. Stack based class for doing
+ * preservation of dom points across editor actions.
+ */
+
+class MOZ_STACK_CLASS AutoTrackDOMPoint final
+{
+private:
+ RangeUpdater& mRangeUpdater;
+ // Allow tracking either nsIDOMNode or nsINode until nsIDOMNode is gone
+ nsCOMPtr<nsINode>* mNode;
+ nsCOMPtr<nsIDOMNode>* mDOMNode;
+ int32_t* mOffset;
+ RefPtr<RangeItem> mRangeItem;
+
+public:
+ AutoTrackDOMPoint(RangeUpdater& aRangeUpdater,
+ nsCOMPtr<nsINode>* aNode, int32_t* aOffset)
+ : mRangeUpdater(aRangeUpdater)
+ , mNode(aNode)
+ , mDOMNode(nullptr)
+ , mOffset(aOffset)
+ {
+ mRangeItem = new RangeItem();
+ mRangeItem->startNode = *mNode;
+ mRangeItem->endNode = *mNode;
+ mRangeItem->startOffset = *mOffset;
+ mRangeItem->endOffset = *mOffset;
+ mRangeUpdater.RegisterRangeItem(mRangeItem);
+ }
+
+ AutoTrackDOMPoint(RangeUpdater& aRangeUpdater,
+ nsCOMPtr<nsIDOMNode>* aNode, int32_t* aOffset)
+ : mRangeUpdater(aRangeUpdater)
+ , mNode(nullptr)
+ , mDOMNode(aNode)
+ , mOffset(aOffset)
+ {
+ mRangeItem = new RangeItem();
+ mRangeItem->startNode = do_QueryInterface(*mDOMNode);
+ mRangeItem->endNode = do_QueryInterface(*mDOMNode);
+ mRangeItem->startOffset = *mOffset;
+ mRangeItem->endOffset = *mOffset;
+ mRangeUpdater.RegisterRangeItem(mRangeItem);
+ }
+
+ ~AutoTrackDOMPoint()
+ {
+ mRangeUpdater.DropRangeItem(mRangeItem);
+ if (mNode) {
+ *mNode = mRangeItem->startNode;
+ } else {
+ *mDOMNode = GetAsDOMNode(mRangeItem->startNode);
+ }
+ *mOffset = mRangeItem->startOffset;
+ }
+};
+
+/**
+ * Another helper class for SelectionState. Stack based class for doing
+ * Will/DidReplaceContainer()
+ */
+
+class MOZ_STACK_CLASS AutoReplaceContainerSelNotify final
+{
+private:
+ RangeUpdater& mRangeUpdater;
+ dom::Element* mOriginalElement;
+ dom::Element* mNewElement;
+
+public:
+ AutoReplaceContainerSelNotify(RangeUpdater& aRangeUpdater,
+ dom::Element* aOriginalElement,
+ dom::Element* aNewElement)
+ : mRangeUpdater(aRangeUpdater)
+ , mOriginalElement(aOriginalElement)
+ , mNewElement(aNewElement)
+ {
+ mRangeUpdater.WillReplaceContainer();
+ }
+
+ ~AutoReplaceContainerSelNotify()
+ {
+ mRangeUpdater.DidReplaceContainer(mOriginalElement, mNewElement);
+ }
+};
+
+/**
+ * Another helper class for SelectionState. Stack based class for doing
+ * Will/DidRemoveContainer()
+ */
+
+class MOZ_STACK_CLASS AutoRemoveContainerSelNotify final
+{
+private:
+ RangeUpdater& mRangeUpdater;
+ nsIDOMNode* mNode;
+ nsIDOMNode* mParent;
+ int32_t mOffset;
+ uint32_t mNodeOrigLen;
+
+public:
+ AutoRemoveContainerSelNotify(RangeUpdater& aRangeUpdater,
+ nsINode* aNode,
+ nsINode* aParent,
+ int32_t aOffset,
+ uint32_t aNodeOrigLen)
+ : mRangeUpdater(aRangeUpdater)
+ , mNode(aNode->AsDOMNode())
+ , mParent(aParent->AsDOMNode())
+ , mOffset(aOffset)
+ , mNodeOrigLen(aNodeOrigLen)
+ {
+ mRangeUpdater.WillRemoveContainer();
+ }
+
+ ~AutoRemoveContainerSelNotify()
+ {
+ mRangeUpdater.DidRemoveContainer(mNode, mParent, mOffset, mNodeOrigLen);
+ }
+};
+
+/**
+ * Another helper class for SelectionState. Stack based class for doing
+ * Will/DidInsertContainer()
+ */
+
+class MOZ_STACK_CLASS AutoInsertContainerSelNotify final
+{
+private:
+ RangeUpdater& mRangeUpdater;
+
+public:
+ explicit AutoInsertContainerSelNotify(RangeUpdater& aRangeUpdater)
+ : mRangeUpdater(aRangeUpdater)
+ {
+ mRangeUpdater.WillInsertContainer();
+ }
+
+ ~AutoInsertContainerSelNotify()
+ {
+ mRangeUpdater.DidInsertContainer();
+ }
+};
+
+/**
+ * Another helper class for SelectionState. Stack based class for doing
+ * Will/DidMoveNode()
+ */
+
+class MOZ_STACK_CLASS AutoMoveNodeSelNotify final
+{
+private:
+ RangeUpdater& mRangeUpdater;
+ nsINode* mOldParent;
+ nsINode* mNewParent;
+ int32_t mOldOffset;
+ int32_t mNewOffset;
+
+public:
+ AutoMoveNodeSelNotify(RangeUpdater& aRangeUpdater,
+ nsINode* aOldParent,
+ int32_t aOldOffset,
+ nsINode* aNewParent,
+ int32_t aNewOffset)
+ : mRangeUpdater(aRangeUpdater)
+ , mOldParent(aOldParent)
+ , mNewParent(aNewParent)
+ , mOldOffset(aOldOffset)
+ , mNewOffset(aNewOffset)
+ {
+ MOZ_ASSERT(aOldParent);
+ MOZ_ASSERT(aNewParent);
+ mRangeUpdater.WillMoveNode();
+ }
+
+ ~AutoMoveNodeSelNotify()
+ {
+ mRangeUpdater.DidMoveNode(mOldParent, mOldOffset, mNewParent, mNewOffset);
+ }
+};
+
+} // namespace mozilla
+
+#endif // #ifndef mozilla_SelectionState_h
diff --git a/editor/libeditor/SetDocumentTitleTransaction.cpp b/editor/libeditor/SetDocumentTitleTransaction.cpp
new file mode 100644
index 000000000..88403c0ad
--- /dev/null
+++ b/editor/libeditor/SetDocumentTitleTransaction.cpp
@@ -0,0 +1,229 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SetDocumentTitleTransaction.h"
+#include "mozilla/dom/Element.h" // for Element
+#include "nsAString.h"
+#include "nsCOMPtr.h" // for nsCOMPtr, getter_AddRefs, etc.
+#include "nsDebug.h" // for NS_ENSURE_SUCCESS, etc.
+#include "nsError.h" // for NS_OK, NS_ERROR_FAILURE, etc.
+#include "nsIDOMCharacterData.h" // for nsIDOMCharacterData
+#include "nsIDOMDocument.h" // for nsIDOMDocument
+#include "nsIDOMElement.h" // for nsIDOMElement
+#include "nsIDOMNode.h" // for nsIDOMNode
+#include "nsIDOMNodeList.h" // for nsIDOMNodeList
+#include "nsIDOMText.h" // for nsIDOMText
+#include "nsIDocument.h" // for nsIDocument
+#include "nsIEditor.h" // for nsIEditor
+#include "nsIHTMLEditor.h" // for nsIHTMLEditor
+#include "nsLiteralString.h" // for NS_LITERAL_STRING
+
+namespace mozilla {
+
+// Note that aEditor is not refcounted.
+SetDocumentTitleTransaction::SetDocumentTitleTransaction()
+ : mEditor(nullptr)
+ , mIsTransient(false)
+{
+}
+
+NS_IMETHODIMP
+SetDocumentTitleTransaction::Init(nsIHTMLEditor* aEditor,
+ const nsAString* aValue)
+
+{
+ NS_ASSERTION(aEditor && aValue, "null args");
+ if (!aEditor || !aValue) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ mEditor = aEditor;
+ mValue = *aValue;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SetDocumentTitleTransaction::DoTransaction()
+{
+ return SetDomTitle(mValue);
+}
+
+NS_IMETHODIMP
+SetDocumentTitleTransaction::UndoTransaction()
+{
+ // No extra work required; the DOM changes alone are enough
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SetDocumentTitleTransaction::RedoTransaction()
+{
+ // No extra work required; the DOM changes alone are enough
+ return NS_OK;
+}
+
+nsresult
+SetDocumentTitleTransaction::SetDomTitle(const nsAString& aTitle)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(mEditor);
+ if (NS_WARN_IF(!editor)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIDOMDocument> domDoc;
+ nsresult rv = editor->GetDocument(getter_AddRefs(domDoc));
+ if (NS_WARN_IF(!domDoc)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIDOMNodeList> titleList;
+ rv = domDoc->GetElementsByTagName(NS_LITERAL_STRING("title"),
+ getter_AddRefs(titleList));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // First assume we will NOT really do anything
+ // (transaction will not be pushed on stack)
+ mIsTransient = true;
+
+ nsCOMPtr<nsIDOMNode> titleNode;
+ if(titleList) {
+ rv = titleList->Item(0, getter_AddRefs(titleNode));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (titleNode) {
+ // Delete existing child textnode of title node
+ // (Note: all contents under a TITLE node are always in a single text node)
+ nsCOMPtr<nsIDOMNode> child;
+ rv = titleNode->GetFirstChild(getter_AddRefs(child));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if(child) {
+ // Save current text as the undo value
+ nsCOMPtr<nsIDOMCharacterData> textNode = do_QueryInterface(child);
+ if(textNode) {
+ textNode->GetData(mUndoValue);
+
+ // If title text is identical to what already exists,
+ // quit now (mIsTransient is now TRUE)
+ if (mUndoValue == aTitle) {
+ return NS_OK;
+ }
+ }
+ rv = editor->DeleteNode(child);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+ }
+ }
+
+ // We didn't return above, thus we really will be changing the title
+ mIsTransient = false;
+
+ // Get the <HEAD> node, create a <TITLE> and insert it under the HEAD
+ nsCOMPtr<nsIDocument> document = do_QueryInterface(domDoc);
+ if (NS_WARN_IF(!document)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ RefPtr<dom::Element> headElement = document->GetHeadElement();
+ if (NS_WARN_IF(!headElement)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ bool newTitleNode = false;
+ uint32_t newTitleIndex = 0;
+
+ if (!titleNode) {
+ // Didn't find one above: Create a new one
+ nsCOMPtr<nsIDOMElement>titleElement;
+ rv = domDoc->CreateElement(NS_LITERAL_STRING("title"),
+ getter_AddRefs(titleElement));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ if (NS_WARN_IF(!titleElement)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ titleNode = do_QueryInterface(titleElement);
+ newTitleNode = true;
+
+ // Get index so we append new title node after all existing HEAD children.
+ newTitleIndex = headElement->GetChildCount();
+ }
+
+ // Append a text node under the TITLE only if the title text isn't empty.
+ if (titleNode && !aTitle.IsEmpty()) {
+ nsCOMPtr<nsIDOMText> textNode;
+ rv = domDoc->CreateTextNode(aTitle, getter_AddRefs(textNode));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIDOMNode> newNode = do_QueryInterface(textNode);
+ if (NS_WARN_IF(!newNode)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (newTitleNode) {
+ // Not undoable: We will insert newTitleNode below
+ nsCOMPtr<nsIDOMNode> resultNode;
+ rv = titleNode->AppendChild(newNode, getter_AddRefs(resultNode));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ // This is an undoable transaction
+ rv = editor->InsertNode(newNode, titleNode, 0);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+ // Calling AppendChild() or InsertNode() could cause removing the head
+ // element. So, let's mark it dirty.
+ headElement = nullptr;
+ }
+
+ if (newTitleNode) {
+ if (!headElement) {
+ headElement = document->GetHeadElement();
+ if (NS_WARN_IF(!headElement)) {
+ // XXX Can we return NS_OK when there is no head element?
+ return NS_ERROR_UNEXPECTED;
+ }
+ }
+ // Undoable transaction to insert title+text together
+ rv = editor->InsertNode(titleNode, headElement->AsDOMNode(), newTitleIndex);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+SetDocumentTitleTransaction::GetTxnDescription(nsAString& aString)
+{
+ aString.AssignLiteral("SetDocumentTitleTransaction: ");
+ aString += mValue;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SetDocumentTitleTransaction::GetIsTransient(bool* aIsTransient)
+{
+ if (NS_WARN_IF(!aIsTransient)) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ *aIsTransient = mIsTransient;
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/SetDocumentTitleTransaction.h b/editor/libeditor/SetDocumentTitleTransaction.h
new file mode 100644
index 000000000..0f17fe8d2
--- /dev/null
+++ b/editor/libeditor/SetDocumentTitleTransaction.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef SetDocumentTitleTransaction_h
+#define SetDocumentTitleTransaction_h
+
+#include "mozilla/EditTransactionBase.h" // for EditTransactionBase, etc.
+#include "nsString.h" // for nsString
+#include "nscore.h" // for NS_IMETHOD, nsAString, etc.
+
+class nsIHTMLEditor;
+
+namespace mozilla {
+
+/**
+ * A transaction that changes the document's title,
+ * which is a text node under the <title> tag in a page's <head> section
+ * provides default concrete behavior for all nsITransaction methods.
+ */
+class SetDocumentTitleTransaction final : public EditTransactionBase
+{
+public:
+ /**
+ * Initialize the transaction.
+ * @param aEditor The object providing core editing operations.
+ * @param aValue The new value for document title.
+ */
+ NS_IMETHOD Init(nsIHTMLEditor* aEditor,
+ const nsAString* aValue);
+ SetDocumentTitleTransaction();
+
+private:
+ nsresult SetDomTitle(const nsAString& aTitle);
+
+public:
+ NS_DECL_EDITTRANSACTIONBASE
+
+ NS_IMETHOD RedoTransaction() override;
+ NS_IMETHOD GetIsTransient(bool *aIsTransient) override;
+
+protected:
+
+ // The editor that created this transaction.
+ nsIHTMLEditor* mEditor;
+
+ // The new title string.
+ nsString mValue;
+
+ // The previous title string to use for undo.
+ nsString mUndoValue;
+
+ // Set true if we dont' really change the title during Do().
+ bool mIsTransient;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef SetDocumentTitleTransaction_h
diff --git a/editor/libeditor/SplitNodeTransaction.cpp b/editor/libeditor/SplitNodeTransaction.cpp
new file mode 100644
index 000000000..113ff7a61
--- /dev/null
+++ b/editor/libeditor/SplitNodeTransaction.cpp
@@ -0,0 +1,128 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SplitNodeTransaction.h"
+
+#include "mozilla/EditorBase.h" // for EditorBase
+#include "mozilla/dom/Selection.h"
+#include "nsAString.h"
+#include "nsDebug.h" // for NS_ASSERTION, etc.
+#include "nsError.h" // for NS_ERROR_NOT_INITIALIZED, etc.
+#include "nsIContent.h" // for nsIContent
+
+namespace mozilla {
+
+using namespace dom;
+
+SplitNodeTransaction::SplitNodeTransaction(EditorBase& aEditorBase,
+ nsIContent& aNode,
+ int32_t aOffset)
+ : mEditorBase(aEditorBase)
+ , mExistingRightNode(&aNode)
+ , mOffset(aOffset)
+{
+}
+
+SplitNodeTransaction::~SplitNodeTransaction()
+{
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(SplitNodeTransaction, EditTransactionBase,
+ mParent,
+ mNewLeftNode)
+
+NS_IMPL_ADDREF_INHERITED(SplitNodeTransaction, EditTransactionBase)
+NS_IMPL_RELEASE_INHERITED(SplitNodeTransaction, EditTransactionBase)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SplitNodeTransaction)
+NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
+
+NS_IMETHODIMP
+SplitNodeTransaction::DoTransaction()
+{
+ // Create a new node
+ ErrorResult rv;
+ // Don't use .downcast directly because AsContent has an assertion we want
+ nsCOMPtr<nsINode> clone = mExistingRightNode->CloneNode(false, rv);
+ NS_ASSERTION(!rv.Failed() && clone, "Could not create clone");
+ NS_ENSURE_TRUE(!rv.Failed() && clone, rv.StealNSResult());
+ mNewLeftNode = dont_AddRef(clone.forget().take()->AsContent());
+ mEditorBase.MarkNodeDirty(mExistingRightNode->AsDOMNode());
+
+ // Get the parent node
+ mParent = mExistingRightNode->GetParentNode();
+ NS_ENSURE_TRUE(mParent, NS_ERROR_NULL_POINTER);
+
+ // Insert the new node
+ rv = mEditorBase.SplitNodeImpl(*mExistingRightNode, mOffset, *mNewLeftNode);
+ if (mEditorBase.GetShouldTxnSetSelection()) {
+ RefPtr<Selection> selection = mEditorBase.GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+ rv = selection->Collapse(mNewLeftNode, mOffset);
+ }
+ return rv.StealNSResult();
+}
+
+NS_IMETHODIMP
+SplitNodeTransaction::UndoTransaction()
+{
+ MOZ_ASSERT(mNewLeftNode && mParent);
+
+ // This assumes Do inserted the new node in front of the prior existing node
+ return mEditorBase.JoinNodesImpl(mExistingRightNode, mNewLeftNode, mParent);
+}
+
+/* Redo cannot simply resplit the right node, because subsequent transactions
+ * on the redo stack may depend on the left node existing in its previous
+ * state.
+ */
+NS_IMETHODIMP
+SplitNodeTransaction::RedoTransaction()
+{
+ MOZ_ASSERT(mNewLeftNode && mParent);
+
+ ErrorResult rv;
+ // First, massage the existing node so it is in its post-split state
+ if (mExistingRightNode->GetAsText()) {
+ rv = mExistingRightNode->GetAsText()->DeleteData(0, mOffset);
+ NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult());
+ } else {
+ nsCOMPtr<nsIContent> child = mExistingRightNode->GetFirstChild();
+ nsCOMPtr<nsIContent> nextSibling;
+ for (int32_t i=0; i < mOffset; i++) {
+ if (rv.Failed()) {
+ return rv.StealNSResult();
+ }
+ if (!child) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ nextSibling = child->GetNextSibling();
+ mExistingRightNode->RemoveChild(*child, rv);
+ if (!rv.Failed()) {
+ mNewLeftNode->AppendChild(*child, rv);
+ }
+ child = nextSibling;
+ }
+ }
+ // Second, re-insert the left node into the tree
+ nsCOMPtr<nsIContent> refNode = mExistingRightNode;
+ mParent->InsertBefore(*mNewLeftNode, refNode, rv);
+ return rv.StealNSResult();
+}
+
+
+NS_IMETHODIMP
+SplitNodeTransaction::GetTxnDescription(nsAString& aString)
+{
+ aString.AssignLiteral("SplitNodeTransaction");
+ return NS_OK;
+}
+
+nsIContent*
+SplitNodeTransaction::GetNewNode()
+{
+ return mNewLeftNode;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/SplitNodeTransaction.h b/editor/libeditor/SplitNodeTransaction.h
new file mode 100644
index 000000000..36119518b
--- /dev/null
+++ b/editor/libeditor/SplitNodeTransaction.h
@@ -0,0 +1,71 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef SplitNodeTransaction_h
+#define SplitNodeTransaction_h
+
+#include "mozilla/EditTransactionBase.h" // for EditTxn, etc.
+#include "nsCOMPtr.h" // for nsCOMPtr
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupportsImpl.h" // for NS_DECL_ISUPPORTS_INHERITED
+#include "nscore.h" // for NS_IMETHOD
+
+class nsIContent;
+class nsINode;
+
+namespace mozilla {
+
+class EditorBase;
+
+/**
+ * A transaction that splits a node into two identical nodes, with the children
+ * divided between the new nodes.
+ */
+class SplitNodeTransaction final : public EditTransactionBase
+{
+public:
+ /**
+ * @param aEditorBase The provider of core editing operations
+ * @param aNode The node to split
+ * @param aOffset The location within aNode to do the split. aOffset may
+ * refer to children of aNode, or content of aNode. The
+ * left node will have child|content 0..aOffset-1.
+ */
+ SplitNodeTransaction(EditorBase& aEditorBase, nsIContent& aNode,
+ int32_t aOffset);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(SplitNodeTransaction,
+ EditTransactionBase)
+
+ NS_DECL_EDITTRANSACTIONBASE
+
+ NS_IMETHOD RedoTransaction() override;
+
+ nsIContent* GetNewNode();
+
+protected:
+ virtual ~SplitNodeTransaction();
+
+ EditorBase& mEditorBase;
+
+ // The node to operate upon.
+ nsCOMPtr<nsIContent> mExistingRightNode;
+
+ // The offset into mExistingRightNode where its children are split. mOffset
+ // is the index of the first child in the right node. -1 means the new node
+ // gets no children.
+ int32_t mOffset;
+
+ // The node we create when splitting mExistingRightNode.
+ nsCOMPtr<nsIContent> mNewLeftNode;
+
+ // The parent shared by mExistingRightNode and mNewLeftNode.
+ nsCOMPtr<nsINode> mParent;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef SplitNodeTransaction_h
diff --git a/editor/libeditor/StyleSheetTransactions.cpp b/editor/libeditor/StyleSheetTransactions.cpp
new file mode 100644
index 000000000..6a31a16e2
--- /dev/null
+++ b/editor/libeditor/StyleSheetTransactions.cpp
@@ -0,0 +1,157 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "StyleSheetTransactions.h"
+
+#include <stddef.h> // for nullptr
+
+#include "nsAString.h"
+#include "nsCOMPtr.h" // for nsCOMPtr, do_QueryInterface, etc.
+#include "mozilla/StyleSheet.h" // for mozilla::StyleSheet
+#include "mozilla/StyleSheetInlines.h"
+#include "nsDebug.h" // for NS_ENSURE_TRUE
+#include "nsError.h" // for NS_OK, etc.
+#include "nsIDOMDocument.h" // for nsIDOMDocument
+#include "nsIDocument.h" // for nsIDocument
+#include "nsIDocumentObserver.h" // for UPDATE_STYLE
+#include "nsIEditor.h" // for nsIEditor
+
+namespace mozilla {
+
+static void
+AddStyleSheet(nsIEditor* aEditor, StyleSheet* aSheet)
+{
+ nsCOMPtr<nsIDOMDocument> domDoc;
+ aEditor->GetDocument(getter_AddRefs(domDoc));
+ nsCOMPtr<nsIDocument> doc = do_QueryInterface(domDoc);
+ if (doc) {
+ doc->BeginUpdate(UPDATE_STYLE);
+ doc->AddStyleSheet(aSheet);
+ doc->EndUpdate(UPDATE_STYLE);
+ }
+}
+
+static void
+RemoveStyleSheet(nsIEditor* aEditor, StyleSheet* aSheet)
+{
+ nsCOMPtr<nsIDOMDocument> domDoc;
+ aEditor->GetDocument(getter_AddRefs(domDoc));
+ nsCOMPtr<nsIDocument> doc = do_QueryInterface(domDoc);
+ if (doc) {
+ doc->BeginUpdate(UPDATE_STYLE);
+ doc->RemoveStyleSheet(aSheet);
+ doc->EndUpdate(UPDATE_STYLE);
+ }
+}
+
+/******************************************************************************
+ * AddStyleSheetTransaction
+ ******************************************************************************/
+
+AddStyleSheetTransaction::AddStyleSheetTransaction()
+ : mEditor(nullptr)
+{
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(AddStyleSheetTransaction,
+ EditTransactionBase,
+ mSheet)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AddStyleSheetTransaction)
+NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
+
+NS_IMETHODIMP
+AddStyleSheetTransaction::Init(nsIEditor* aEditor,
+ StyleSheet* aSheet)
+{
+ NS_ENSURE_TRUE(aEditor && aSheet, NS_ERROR_INVALID_ARG);
+
+ mEditor = aEditor;
+ mSheet = aSheet;
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+AddStyleSheetTransaction::DoTransaction()
+{
+ NS_ENSURE_TRUE(mEditor && mSheet, NS_ERROR_NOT_INITIALIZED);
+
+ AddStyleSheet(mEditor, mSheet);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AddStyleSheetTransaction::UndoTransaction()
+{
+ NS_ENSURE_TRUE(mEditor && mSheet, NS_ERROR_NOT_INITIALIZED);
+
+ RemoveStyleSheet(mEditor, mSheet);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AddStyleSheetTransaction::GetTxnDescription(nsAString& aString)
+{
+ aString.AssignLiteral("AddStyleSheetTransaction");
+ return NS_OK;
+}
+
+/******************************************************************************
+ * RemoveStyleSheetTransaction
+ ******************************************************************************/
+
+RemoveStyleSheetTransaction::RemoveStyleSheetTransaction()
+ : mEditor(nullptr)
+{
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(RemoveStyleSheetTransaction,
+ EditTransactionBase,
+ mSheet)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(RemoveStyleSheetTransaction)
+NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
+
+NS_IMETHODIMP
+RemoveStyleSheetTransaction::Init(nsIEditor* aEditor,
+ StyleSheet* aSheet)
+{
+ NS_ENSURE_TRUE(aEditor && aSheet, NS_ERROR_INVALID_ARG);
+
+ mEditor = aEditor;
+ mSheet = aSheet;
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+RemoveStyleSheetTransaction::DoTransaction()
+{
+ NS_ENSURE_TRUE(mEditor && mSheet, NS_ERROR_NOT_INITIALIZED);
+
+ RemoveStyleSheet(mEditor, mSheet);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+RemoveStyleSheetTransaction::UndoTransaction()
+{
+ NS_ENSURE_TRUE(mEditor && mSheet, NS_ERROR_NOT_INITIALIZED);
+
+ AddStyleSheet(mEditor, mSheet);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+RemoveStyleSheetTransaction::GetTxnDescription(nsAString& aString)
+{
+ aString.AssignLiteral("RemoveStyleSheetTransaction");
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/StyleSheetTransactions.h b/editor/libeditor/StyleSheetTransactions.h
new file mode 100644
index 000000000..bf615b263
--- /dev/null
+++ b/editor/libeditor/StyleSheetTransactions.h
@@ -0,0 +1,73 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef StylesheetTransactions_h
+#define StylesheetTransactions_h
+
+#include "mozilla/EditTransactionBase.h" // for EditTransactionBase, etc.
+#include "mozilla/StyleSheet.h" // for mozilla::StyleSheet
+#include "nsCycleCollectionParticipant.h"
+#include "nsID.h" // for REFNSIID
+#include "nscore.h" // for NS_IMETHOD
+
+class nsIEditor;
+
+namespace mozilla {
+
+class AddStyleSheetTransaction final : public EditTransactionBase
+{
+public:
+ /**
+ * Initialize the transaction.
+ * @param aEditor The object providing core editing operations
+ * @param aSheet The stylesheet to add
+ */
+ NS_IMETHOD Init(nsIEditor* aEditor, StyleSheet* aSheet);
+
+ AddStyleSheetTransaction();
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(AddStyleSheetTransaction,
+ EditTransactionBase)
+ NS_IMETHOD QueryInterface(REFNSIID aIID, void** aInstancePtr) override;
+
+ NS_DECL_EDITTRANSACTIONBASE
+
+protected:
+ // The editor that created this transaction.
+ nsIEditor* mEditor;
+ // The style sheet to add.
+ RefPtr<mozilla::StyleSheet> mSheet;
+};
+
+
+class RemoveStyleSheetTransaction final : public EditTransactionBase
+{
+public:
+ /**
+ * Initialize the transaction.
+ * @param aEditor The object providing core editing operations.
+ * @param aSheet The stylesheet to remove.
+ */
+ NS_IMETHOD Init(nsIEditor* aEditor, StyleSheet* aSheet);
+
+ RemoveStyleSheetTransaction();
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(RemoveStyleSheetTransaction,
+ EditTransactionBase)
+ NS_IMETHOD QueryInterface(REFNSIID aIID, void** aInstancePtr) override;
+
+ NS_DECL_EDITTRANSACTIONBASE
+
+protected:
+ // The editor that created this transaction.
+ nsIEditor* mEditor;
+ // The style sheet to remove.
+ RefPtr<StyleSheet> mSheet;
+
+};
+
+} // namespace mozilla
+
+#endif // #ifndef StylesheetTransactions_h
diff --git a/editor/libeditor/TextEditRules.cpp b/editor/libeditor/TextEditRules.cpp
new file mode 100644
index 000000000..8f8f34e8b
--- /dev/null
+++ b/editor/libeditor/TextEditRules.cpp
@@ -0,0 +1,1494 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/TextEditRules.h"
+
+#include "TextEditUtils.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/EditorUtils.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/TextComposition.h"
+#include "mozilla/TextEditor.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/NodeIterator.h"
+#include "mozilla/dom/Selection.h"
+#include "nsAString.h"
+#include "nsCOMPtr.h"
+#include "nsCRT.h"
+#include "nsCRTGlue.h"
+#include "nsComponentManagerUtils.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsGkAtoms.h"
+#include "nsIContent.h"
+#include "nsIDOMCharacterData.h"
+#include "nsIDOMDocument.h"
+#include "nsIDOMElement.h"
+#include "nsIDOMNode.h"
+#include "nsIDOMNodeFilter.h"
+#include "nsIDOMNodeIterator.h"
+#include "nsIDOMNodeList.h"
+#include "nsIDOMText.h"
+#include "nsNameSpaceManager.h"
+#include "nsINode.h"
+#include "nsIPlaintextEditor.h"
+#include "nsISupportsBase.h"
+#include "nsLiteralString.h"
+#include "nsUnicharUtils.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+#define CANCEL_OPERATION_IF_READONLY_OR_DISABLED \
+ if (IsReadonly() || IsDisabled()) \
+ { \
+ *aCancel = true; \
+ return NS_OK; \
+ };
+
+/********************************************************
+ * mozilla::TextEditRules
+ ********************************************************/
+
+TextEditRules::TextEditRules()
+ : mTextEditor(nullptr)
+ , mPasswordIMEIndex(0)
+ , mCachedSelectionOffset(0)
+ , mActionNesting(0)
+ , mLockRulesSniffing(false)
+ , mDidExplicitlySetInterline(false)
+ , mDeleteBidiImmediately(false)
+ , mTheAction(EditAction::none)
+ , mLastStart(0)
+ , mLastLength(0)
+{
+ InitFields();
+}
+
+void
+TextEditRules::InitFields()
+{
+ mTextEditor = nullptr;
+ mPasswordText.Truncate();
+ mPasswordIMEText.Truncate();
+ mPasswordIMEIndex = 0;
+ mBogusNode = nullptr;
+ mCachedSelectionNode = nullptr;
+ mCachedSelectionOffset = 0;
+ mActionNesting = 0;
+ mLockRulesSniffing = false;
+ mDidExplicitlySetInterline = false;
+ mDeleteBidiImmediately = false;
+ mTheAction = EditAction::none;
+ mTimer = nullptr;
+ mLastStart = 0;
+ mLastLength = 0;
+}
+
+TextEditRules::~TextEditRules()
+{
+ // do NOT delete mTextEditor here. We do not hold a ref count to
+ // mTextEditor. mTextEditor owns our lifespan.
+
+ if (mTimer) {
+ mTimer->Cancel();
+ }
+}
+
+NS_IMPL_CYCLE_COLLECTION(TextEditRules, mBogusNode, mCachedSelectionNode)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TextEditRules)
+ NS_INTERFACE_MAP_ENTRY(nsIEditRules)
+ NS_INTERFACE_MAP_ENTRY(nsITimerCallback)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIEditRules)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(TextEditRules)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(TextEditRules)
+
+NS_IMETHODIMP
+TextEditRules::Init(TextEditor* aTextEditor)
+{
+ if (!aTextEditor) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ InitFields();
+
+ // We hold a non-refcounted reference back to our editor.
+ mTextEditor = aTextEditor;
+ RefPtr<Selection> selection = mTextEditor->GetSelection();
+ NS_WARNING_ASSERTION(selection, "editor cannot get selection");
+
+ // Put in a magic br if needed. This method handles null selection,
+ // which should never happen anyway
+ nsresult rv = CreateBogusNodeIfNeeded(selection);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If the selection hasn't been set up yet, set it up collapsed to the end of
+ // our editable content.
+ int32_t rangeCount;
+ rv = selection->GetRangeCount(&rangeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!rangeCount) {
+ rv = mTextEditor->EndOfDocument();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (IsPlaintextEditor()) {
+ // ensure trailing br node
+ rv = CreateTrailingBRIfNeeded();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ mDeleteBidiImmediately =
+ Preferences::GetBool("bidi.edit.delete_immediately", false);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextEditRules::SetInitialValue(const nsAString& aValue)
+{
+ if (IsPasswordEditor()) {
+ mPasswordText = aValue;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextEditRules::DetachEditor()
+{
+ if (mTimer) {
+ mTimer->Cancel();
+ }
+ mTextEditor = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextEditRules::BeforeEdit(EditAction action,
+ nsIEditor::EDirection aDirection)
+{
+ if (mLockRulesSniffing) {
+ return NS_OK;
+ }
+
+ AutoLockRulesSniffing lockIt(this);
+ mDidExplicitlySetInterline = false;
+ if (!mActionNesting) {
+ // let rules remember the top level action
+ mTheAction = action;
+ }
+ mActionNesting++;
+
+ // get the selection and cache the position before editing
+ NS_ENSURE_STATE(mTextEditor);
+ RefPtr<Selection> selection = mTextEditor->GetSelection();
+ NS_ENSURE_STATE(selection);
+
+ selection->GetAnchorNode(getter_AddRefs(mCachedSelectionNode));
+ selection->GetAnchorOffset(&mCachedSelectionOffset);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextEditRules::AfterEdit(EditAction action,
+ nsIEditor::EDirection aDirection)
+{
+ if (mLockRulesSniffing) {
+ return NS_OK;
+ }
+
+ AutoLockRulesSniffing lockIt(this);
+
+ NS_PRECONDITION(mActionNesting>0, "bad action nesting!");
+ if (!--mActionNesting) {
+ NS_ENSURE_STATE(mTextEditor);
+ RefPtr<Selection> selection = mTextEditor->GetSelection();
+ NS_ENSURE_STATE(selection);
+
+ NS_ENSURE_STATE(mTextEditor);
+ nsresult rv =
+ mTextEditor->HandleInlineSpellCheck(action, selection,
+ mCachedSelectionNode,
+ mCachedSelectionOffset,
+ nullptr, 0, nullptr, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // if only trailing <br> remaining remove it
+ rv = RemoveRedundantTrailingBR();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // detect empty doc
+ rv = CreateBogusNodeIfNeeded(selection);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // ensure trailing br node
+ rv = CreateTrailingBRIfNeeded();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // collapse the selection to the trailing BR if it's at the end of our text node
+ CollapseSelectionToTrailingBRIfNeeded(selection);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextEditRules::WillDoAction(Selection* aSelection,
+ RulesInfo* aInfo,
+ bool* aCancel,
+ bool* aHandled)
+{
+ // null selection is legal
+ MOZ_ASSERT(aInfo && aCancel && aHandled);
+
+ *aCancel = false;
+ *aHandled = false;
+
+ // my kingdom for dynamic cast
+ TextRulesInfo* info = static_cast<TextRulesInfo*>(aInfo);
+
+ switch (info->action) {
+ case EditAction::insertBreak:
+ UndefineCaretBidiLevel(aSelection);
+ return WillInsertBreak(aSelection, aCancel, aHandled, info->maxLength);
+ case EditAction::insertText:
+ case EditAction::insertIMEText:
+ UndefineCaretBidiLevel(aSelection);
+ return WillInsertText(info->action, aSelection, aCancel, aHandled,
+ info->inString, info->outString, info->maxLength);
+ case EditAction::deleteSelection:
+ return WillDeleteSelection(aSelection, info->collapsedAction,
+ aCancel, aHandled);
+ case EditAction::undo:
+ return WillUndo(aSelection, aCancel, aHandled);
+ case EditAction::redo:
+ return WillRedo(aSelection, aCancel, aHandled);
+ case EditAction::setTextProperty:
+ return WillSetTextProperty(aSelection, aCancel, aHandled);
+ case EditAction::removeTextProperty:
+ return WillRemoveTextProperty(aSelection, aCancel, aHandled);
+ case EditAction::outputText:
+ return WillOutputText(aSelection, info->outputFormat, info->outString,
+ aCancel, aHandled);
+ case EditAction::insertElement:
+ // i had thought this would be html rules only. but we put pre elements
+ // into plaintext mail when doing quoting for reply! doh!
+ WillInsert(*aSelection, aCancel);
+ return NS_OK;
+ default:
+ return NS_ERROR_FAILURE;
+ }
+}
+
+NS_IMETHODIMP
+TextEditRules::DidDoAction(Selection* aSelection,
+ RulesInfo* aInfo,
+ nsresult aResult)
+{
+ NS_ENSURE_STATE(mTextEditor);
+ // don't let any txns in here move the selection around behind our back.
+ // Note that this won't prevent explicit selection setting from working.
+ AutoTransactionsConserveSelection dontSpazMySelection(mTextEditor);
+
+ NS_ENSURE_TRUE(aSelection && aInfo, NS_ERROR_NULL_POINTER);
+
+ // my kingdom for dynamic cast
+ TextRulesInfo* info = static_cast<TextRulesInfo*>(aInfo);
+
+ switch (info->action) {
+ case EditAction::insertBreak:
+ return DidInsertBreak(aSelection, aResult);
+ case EditAction::insertText:
+ case EditAction::insertIMEText:
+ return DidInsertText(aSelection, aResult);
+ case EditAction::deleteSelection:
+ return DidDeleteSelection(aSelection, info->collapsedAction, aResult);
+ case EditAction::undo:
+ return DidUndo(aSelection, aResult);
+ case EditAction::redo:
+ return DidRedo(aSelection, aResult);
+ case EditAction::setTextProperty:
+ return DidSetTextProperty(aSelection, aResult);
+ case EditAction::removeTextProperty:
+ return DidRemoveTextProperty(aSelection, aResult);
+ case EditAction::outputText:
+ return DidOutputText(aSelection, aResult);
+ default:
+ // Don't fail on transactions we don't handle here!
+ return NS_OK;
+ }
+}
+
+NS_IMETHODIMP
+TextEditRules::DocumentIsEmpty(bool* aDocumentIsEmpty)
+{
+ NS_ENSURE_TRUE(aDocumentIsEmpty, NS_ERROR_NULL_POINTER);
+
+ *aDocumentIsEmpty = (mBogusNode != nullptr);
+ return NS_OK;
+}
+
+void
+TextEditRules::WillInsert(Selection& aSelection, bool* aCancel)
+{
+ MOZ_ASSERT(aCancel);
+
+ if (IsReadonly() || IsDisabled()) {
+ *aCancel = true;
+ return;
+ }
+
+ // initialize out param
+ *aCancel = false;
+
+ // check for the magic content node and delete it if it exists
+ if (mBogusNode) {
+ NS_ENSURE_TRUE_VOID(mTextEditor);
+ mTextEditor->DeleteNode(mBogusNode);
+ mBogusNode = nullptr;
+ }
+}
+
+nsresult
+TextEditRules::DidInsert(Selection* aSelection,
+ nsresult aResult)
+{
+ return NS_OK;
+}
+
+nsresult
+TextEditRules::WillInsertBreak(Selection* aSelection,
+ bool* aCancel,
+ bool* aHandled,
+ int32_t aMaxLength)
+{
+ if (!aSelection || !aCancel || !aHandled) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ CANCEL_OPERATION_IF_READONLY_OR_DISABLED
+ *aHandled = false;
+ if (IsSingleLineEditor()) {
+ *aCancel = true;
+ } else {
+ // handle docs with a max length
+ // NOTE, this function copies inString into outString for us.
+ NS_NAMED_LITERAL_STRING(inString, "\n");
+ nsAutoString outString;
+ bool didTruncate;
+ nsresult rv = TruncateInsertionIfNeeded(aSelection, &inString, &outString,
+ aMaxLength, &didTruncate);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (didTruncate) {
+ *aCancel = true;
+ return NS_OK;
+ }
+
+ *aCancel = false;
+
+ // if the selection isn't collapsed, delete it.
+ bool bCollapsed;
+ rv = aSelection->GetIsCollapsed(&bCollapsed);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!bCollapsed) {
+ NS_ENSURE_STATE(mTextEditor);
+ rv = mTextEditor->DeleteSelection(nsIEditor::eNone, nsIEditor::eStrip);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ WillInsert(*aSelection, aCancel);
+ // initialize out param
+ // we want to ignore result of WillInsert()
+ *aCancel = false;
+ }
+ return NS_OK;
+}
+
+nsresult
+TextEditRules::DidInsertBreak(Selection* aSelection,
+ nsresult aResult)
+{
+ return NS_OK;
+}
+
+nsresult
+TextEditRules::CollapseSelectionToTrailingBRIfNeeded(Selection* aSelection)
+{
+ // we only need to execute the stuff below if we are a plaintext editor.
+ // html editors have a different mechanism for putting in mozBR's
+ // (because there are a bunch more places you have to worry about it in html)
+ if (!IsPlaintextEditor()) {
+ return NS_OK;
+ }
+
+ NS_ENSURE_STATE(mTextEditor);
+
+ // If there is no selection ranges, we should set to the end of the editor.
+ // This is usually performed in TextEditRules::Init(), however, if the
+ // editor is reframed, this may be called by AfterEdit().
+ if (!aSelection->RangeCount()) {
+ mTextEditor->EndOfDocument();
+ }
+
+ // if we are at the end of the textarea, we need to set the
+ // selection to stick to the mozBR at the end of the textarea.
+ int32_t selOffset;
+ nsCOMPtr<nsIDOMNode> selNode;
+ nsresult rv =
+ mTextEditor->GetStartNodeAndOffset(aSelection,
+ getter_AddRefs(selNode), &selOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDOMText> nodeAsText = do_QueryInterface(selNode);
+ if (!nodeAsText) {
+ return NS_OK; // Nothing to do if we're not at a text node.
+ }
+
+ uint32_t length;
+ rv = nodeAsText->GetLength(&length);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // nothing to do if we're not at the end of the text node
+ if (selOffset != int32_t(length)) {
+ return NS_OK;
+ }
+
+ int32_t parentOffset;
+ nsCOMPtr<nsIDOMNode> parentNode =
+ EditorBase::GetNodeLocation(selNode, &parentOffset);
+
+ NS_ENSURE_STATE(mTextEditor);
+ nsCOMPtr<nsIDOMNode> root = do_QueryInterface(mTextEditor->GetRoot());
+ NS_ENSURE_TRUE(root, NS_ERROR_NULL_POINTER);
+ if (parentNode != root) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIDOMNode> nextNode = mTextEditor->GetChildAt(parentNode,
+ parentOffset + 1);
+ if (nextNode && TextEditUtils::IsMozBR(nextNode)) {
+ rv = aSelection->Collapse(parentNode, parentOffset + 1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+static inline already_AddRefed<nsIDOMNode>
+GetTextNode(Selection* selection,
+ EditorBase* editor)
+{
+ int32_t selOffset;
+ nsCOMPtr<nsIDOMNode> selNode;
+ nsresult rv =
+ editor->GetStartNodeAndOffset(selection,
+ getter_AddRefs(selNode), &selOffset);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ if (!editor->IsTextNode(selNode)) {
+ // Get an nsINode from the nsIDOMNode
+ nsCOMPtr<nsINode> node = do_QueryInterface(selNode);
+ // if node is null, return it to indicate there's no text
+ NS_ENSURE_TRUE(node, nullptr);
+ // This should be the root node, walk the tree looking for text nodes
+ RefPtr<NodeIterator> iter =
+ new NodeIterator(node, nsIDOMNodeFilter::SHOW_TEXT, NodeFilterHolder());
+ while (!editor->IsTextNode(selNode)) {
+ if (NS_FAILED(iter->NextNode(getter_AddRefs(selNode))) || !selNode) {
+ return nullptr;
+ }
+ }
+ }
+ return selNode.forget();
+}
+#ifdef DEBUG
+#define ASSERT_PASSWORD_LENGTHS_EQUAL() \
+ if (IsPasswordEditor() && mTextEditor->GetRoot()) { \
+ int32_t txtLen; \
+ mTextEditor->GetTextLength(&txtLen); \
+ NS_ASSERTION(mPasswordText.Length() == uint32_t(txtLen), \
+ "password length not equal to number of asterisks"); \
+ }
+#else
+#define ASSERT_PASSWORD_LENGTHS_EQUAL()
+#endif
+
+// static
+void
+TextEditRules::HandleNewLines(nsString& aString,
+ int32_t aNewlineHandling)
+{
+ if (aNewlineHandling < 0) {
+ int32_t caretStyle;
+ TextEditor::GetDefaultEditorPrefs(aNewlineHandling, caretStyle);
+ }
+
+ switch(aNewlineHandling) {
+ case nsIPlaintextEditor::eNewlinesReplaceWithSpaces:
+ // Strip trailing newlines first so we don't wind up with trailing spaces
+ aString.Trim(CRLF, false, true);
+ aString.ReplaceChar(CRLF, ' ');
+ break;
+ case nsIPlaintextEditor::eNewlinesStrip:
+ aString.StripChars(CRLF);
+ break;
+ case nsIPlaintextEditor::eNewlinesPasteToFirst:
+ default: {
+ int32_t firstCRLF = aString.FindCharInSet(CRLF);
+
+ // we get first *non-empty* line.
+ int32_t offset = 0;
+ while (firstCRLF == offset) {
+ offset++;
+ firstCRLF = aString.FindCharInSet(CRLF, offset);
+ }
+ if (firstCRLF > 0) {
+ aString.Truncate(firstCRLF);
+ }
+ if (offset > 0) {
+ aString.Cut(0, offset);
+ }
+ break;
+ }
+ case nsIPlaintextEditor::eNewlinesReplaceWithCommas:
+ aString.Trim(CRLF, true, true);
+ aString.ReplaceChar(CRLF, ',');
+ break;
+ case nsIPlaintextEditor::eNewlinesStripSurroundingWhitespace: {
+ nsAutoString result;
+ uint32_t offset = 0;
+ while (offset < aString.Length()) {
+ int32_t nextCRLF = aString.FindCharInSet(CRLF, offset);
+ if (nextCRLF < 0) {
+ result.Append(nsDependentSubstring(aString, offset));
+ break;
+ }
+ uint32_t wsBegin = nextCRLF;
+ // look backwards for the first non-whitespace char
+ while (wsBegin > offset && NS_IS_SPACE(aString[wsBegin - 1])) {
+ --wsBegin;
+ }
+ result.Append(nsDependentSubstring(aString, offset, wsBegin - offset));
+ offset = nextCRLF + 1;
+ while (offset < aString.Length() && NS_IS_SPACE(aString[offset])) {
+ ++offset;
+ }
+ }
+ aString = result;
+ break;
+ }
+ case nsIPlaintextEditor::eNewlinesPasteIntact:
+ // even if we're pasting newlines, don't paste leading/trailing ones
+ aString.Trim(CRLF, true, true);
+ break;
+ }
+}
+
+nsresult
+TextEditRules::WillInsertText(EditAction aAction,
+ Selection* aSelection,
+ bool* aCancel,
+ bool* aHandled,
+ const nsAString* inString,
+ nsAString* outString,
+ int32_t aMaxLength)
+{
+ if (!aSelection || !aCancel || !aHandled) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ if (inString->IsEmpty() && aAction != EditAction::insertIMEText) {
+ // HACK: this is a fix for bug 19395
+ // I can't outlaw all empty insertions
+ // because IME transaction depend on them
+ // There is more work to do to make the
+ // world safe for IME.
+ *aCancel = true;
+ *aHandled = false;
+ return NS_OK;
+ }
+
+ // initialize out param
+ *aCancel = false;
+ *aHandled = true;
+
+ // handle docs with a max length
+ // NOTE, this function copies inString into outString for us.
+ bool truncated = false;
+ nsresult rv = TruncateInsertionIfNeeded(aSelection, inString, outString,
+ aMaxLength, &truncated);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // If we're exceeding the maxlength when composing IME, we need to clean up
+ // the composing text, so we shouldn't return early.
+ if (truncated && outString->IsEmpty() &&
+ aAction != EditAction::insertIMEText) {
+ *aCancel = true;
+ return NS_OK;
+ }
+
+ int32_t start = 0;
+ int32_t end = 0;
+
+ // handle password field docs
+ if (IsPasswordEditor()) {
+ NS_ENSURE_STATE(mTextEditor);
+ nsContentUtils::GetSelectionInTextControl(aSelection,
+ mTextEditor->GetRoot(),
+ start, end);
+ }
+
+ // if the selection isn't collapsed, delete it.
+ bool bCollapsed;
+ rv = aSelection->GetIsCollapsed(&bCollapsed);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!bCollapsed) {
+ NS_ENSURE_STATE(mTextEditor);
+ rv = mTextEditor->DeleteSelection(nsIEditor::eNone, nsIEditor::eStrip);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ WillInsert(*aSelection, aCancel);
+ // initialize out param
+ // we want to ignore result of WillInsert()
+ *aCancel = false;
+
+ // handle password field data
+ // this has the side effect of changing all the characters in aOutString
+ // to the replacement character
+ if (IsPasswordEditor() &&
+ aAction == EditAction::insertIMEText) {
+ RemoveIMETextFromPWBuf(start, outString);
+ }
+
+ // People have lots of different ideas about what text fields
+ // should do with multiline pastes. See bugs 21032, 23485, 23485, 50935.
+ // The six possible options are:
+ // 0. paste newlines intact
+ // 1. paste up to the first newline (default)
+ // 2. replace newlines with spaces
+ // 3. strip newlines
+ // 4. replace with commas
+ // 5. strip newlines and surrounding whitespace
+ // So find out what we're expected to do:
+ if (IsSingleLineEditor()) {
+ nsAutoString tString(*outString);
+
+ NS_ENSURE_STATE(mTextEditor);
+ HandleNewLines(tString, mTextEditor->mNewlineHandling);
+
+ outString->Assign(tString);
+ }
+
+ if (IsPasswordEditor()) {
+ // manage the password buffer
+ mPasswordText.Insert(*outString, start);
+
+ if (LookAndFeel::GetEchoPassword() && !DontEchoPassword()) {
+ HideLastPWInput();
+ mLastStart = start;
+ mLastLength = outString->Length();
+ if (mTimer) {
+ mTimer->Cancel();
+ } else {
+ mTimer = do_CreateInstance("@mozilla.org/timer;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ mTimer->InitWithCallback(this, LookAndFeel::GetPasswordMaskDelay(),
+ nsITimer::TYPE_ONE_SHOT);
+ } else {
+ FillBufWithPWChars(outString, outString->Length());
+ }
+ }
+
+ // get the (collapsed) selection location
+ NS_ENSURE_STATE(aSelection->GetRangeAt(0));
+ nsCOMPtr<nsINode> selNode = aSelection->GetRangeAt(0)->GetStartParent();
+ int32_t selOffset = aSelection->GetRangeAt(0)->StartOffset();
+ NS_ENSURE_STATE(selNode);
+
+ // don't put text in places that can't have it
+ NS_ENSURE_STATE(mTextEditor);
+ if (!mTextEditor->IsTextNode(selNode) &&
+ !mTextEditor->CanContainTag(*selNode, *nsGkAtoms::textTagName)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // we need to get the doc
+ NS_ENSURE_STATE(mTextEditor);
+ nsCOMPtr<nsIDocument> doc = mTextEditor->GetDocument();
+ NS_ENSURE_TRUE(doc, NS_ERROR_NOT_INITIALIZED);
+
+ if (aAction == EditAction::insertIMEText) {
+ NS_ENSURE_STATE(mTextEditor);
+ // Find better insertion point to insert text.
+ mTextEditor->FindBetterInsertionPoint(selNode, selOffset);
+ // If there is one or more IME selections, its minimum offset should be
+ // the insertion point.
+ int32_t IMESelectionOffset =
+ mTextEditor->GetIMESelectionStartOffsetIn(selNode);
+ if (IMESelectionOffset >= 0) {
+ selOffset = IMESelectionOffset;
+ }
+ rv = mTextEditor->InsertTextImpl(*outString, address_of(selNode),
+ &selOffset, doc);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // aAction == EditAction::insertText; find where we are
+ nsCOMPtr<nsINode> curNode = selNode;
+ int32_t curOffset = selOffset;
+
+ // don't spaz my selection in subtransactions
+ NS_ENSURE_STATE(mTextEditor);
+ AutoTransactionsConserveSelection dontSpazMySelection(mTextEditor);
+
+ rv = mTextEditor->InsertTextImpl(*outString, address_of(curNode),
+ &curOffset, doc);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (curNode) {
+ // Make the caret attach to the inserted text, unless this text ends with a LF,
+ // in which case make the caret attach to the next line.
+ bool endsWithLF =
+ !outString->IsEmpty() && outString->Last() == nsCRT::LF;
+ aSelection->SetInterlinePosition(endsWithLF);
+
+ aSelection->Collapse(curNode, curOffset);
+ }
+ }
+ ASSERT_PASSWORD_LENGTHS_EQUAL()
+ return NS_OK;
+}
+
+nsresult
+TextEditRules::DidInsertText(Selection* aSelection,
+ nsresult aResult)
+{
+ return DidInsert(aSelection, aResult);
+}
+
+nsresult
+TextEditRules::WillSetTextProperty(Selection* aSelection,
+ bool* aCancel,
+ bool* aHandled)
+{
+ if (!aSelection || !aCancel || !aHandled) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ // XXX: should probably return a success value other than NS_OK that means "not allowed"
+ if (IsPlaintextEditor()) {
+ *aCancel = true;
+ }
+ return NS_OK;
+}
+
+nsresult
+TextEditRules::DidSetTextProperty(Selection* aSelection,
+ nsresult aResult)
+{
+ return NS_OK;
+}
+
+nsresult
+TextEditRules::WillRemoveTextProperty(Selection* aSelection,
+ bool* aCancel,
+ bool* aHandled)
+{
+ if (!aSelection || !aCancel || !aHandled) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ // XXX: should probably return a success value other than NS_OK that means "not allowed"
+ if (IsPlaintextEditor()) {
+ *aCancel = true;
+ }
+ return NS_OK;
+}
+
+nsresult
+TextEditRules::DidRemoveTextProperty(Selection* aSelection,
+ nsresult aResult)
+{
+ return NS_OK;
+}
+
+nsresult
+TextEditRules::WillDeleteSelection(Selection* aSelection,
+ nsIEditor::EDirection aCollapsedAction,
+ bool* aCancel,
+ bool* aHandled)
+{
+ if (!aSelection || !aCancel || !aHandled) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ CANCEL_OPERATION_IF_READONLY_OR_DISABLED
+
+ // initialize out param
+ *aCancel = false;
+ *aHandled = false;
+
+ // if there is only bogus content, cancel the operation
+ if (mBogusNode) {
+ *aCancel = true;
+ return NS_OK;
+ }
+
+ // If the current selection is empty (e.g the user presses backspace with
+ // a collapsed selection), then we want to avoid sending the selectstart
+ // event to the user, so we hide selection changes. However, we still
+ // want to send a single selectionchange event to the document, so we
+ // batch the selectionchange events, such that a single event fires after
+ // the AutoHideSelectionChanges destructor has been run.
+ SelectionBatcher selectionBatcher(aSelection);
+ AutoHideSelectionChanges hideSelection(aSelection);
+ nsAutoScriptBlocker scriptBlocker;
+
+ if (IsPasswordEditor()) {
+ NS_ENSURE_STATE(mTextEditor);
+ nsresult rv =
+ mTextEditor->ExtendSelectionForDelete(aSelection, &aCollapsedAction);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // manage the password buffer
+ int32_t start, end;
+ nsContentUtils::GetSelectionInTextControl(aSelection,
+ mTextEditor->GetRoot(),
+ start, end);
+
+ if (LookAndFeel::GetEchoPassword()) {
+ HideLastPWInput();
+ mLastStart = start;
+ mLastLength = 0;
+ if (mTimer) {
+ mTimer->Cancel();
+ }
+ }
+
+ // Collapsed selection.
+ if (end == start) {
+ // Deleting back.
+ if (nsIEditor::ePrevious == aCollapsedAction && 0<start) {
+ mPasswordText.Cut(start-1, 1);
+ }
+ // Deleting forward.
+ else if (nsIEditor::eNext == aCollapsedAction) {
+ mPasswordText.Cut(start, 1);
+ }
+ // Otherwise nothing to do for this collapsed selection.
+ }
+ // Extended selection.
+ else {
+ mPasswordText.Cut(start, end-start);
+ }
+ } else {
+ nsCOMPtr<nsIDOMNode> startNode;
+ int32_t startOffset;
+ NS_ENSURE_STATE(mTextEditor);
+ nsresult rv =
+ mTextEditor->GetStartNodeAndOffset(aSelection, getter_AddRefs(startNode),
+ &startOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(startNode, NS_ERROR_FAILURE);
+
+ bool bCollapsed;
+ rv = aSelection->GetIsCollapsed(&bCollapsed);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!bCollapsed) {
+ return NS_OK;
+ }
+
+ // Test for distance between caret and text that will be deleted
+ rv = CheckBidiLevelForDeletion(aSelection, startNode, startOffset,
+ aCollapsedAction, aCancel);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (*aCancel) {
+ return NS_OK;
+ }
+
+ NS_ENSURE_STATE(mTextEditor);
+ rv = mTextEditor->ExtendSelectionForDelete(aSelection, &aCollapsedAction);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ NS_ENSURE_STATE(mTextEditor);
+ nsresult rv =
+ mTextEditor->DeleteSelectionImpl(aCollapsedAction, nsIEditor::eStrip);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aHandled = true;
+ ASSERT_PASSWORD_LENGTHS_EQUAL()
+ return NS_OK;
+}
+
+nsresult
+TextEditRules::DidDeleteSelection(Selection* aSelection,
+ nsIEditor::EDirection aCollapsedAction,
+ nsresult aResult)
+{
+ nsCOMPtr<nsIDOMNode> startNode;
+ int32_t startOffset;
+ NS_ENSURE_STATE(mTextEditor);
+ nsresult rv =
+ mTextEditor->GetStartNodeAndOffset(aSelection,
+ getter_AddRefs(startNode), &startOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(startNode, NS_ERROR_FAILURE);
+
+ // delete empty text nodes at selection
+ if (mTextEditor->IsTextNode(startNode)) {
+ nsCOMPtr<nsIDOMText> textNode = do_QueryInterface(startNode);
+ uint32_t strLength;
+ rv = textNode->GetLength(&strLength);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // are we in an empty text node?
+ if (!strLength) {
+ rv = mTextEditor->DeleteNode(startNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ if (mDidExplicitlySetInterline) {
+ return NS_OK;
+ }
+ // We prevent the caret from sticking on the left of prior BR
+ // (i.e. the end of previous line) after this deletion. Bug 92124
+ return aSelection->SetInterlinePosition(true);
+}
+
+nsresult
+TextEditRules::WillUndo(Selection* aSelection,
+ bool* aCancel,
+ bool* aHandled)
+{
+ if (!aSelection || !aCancel || !aHandled) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ CANCEL_OPERATION_IF_READONLY_OR_DISABLED
+ // initialize out param
+ *aCancel = false;
+ *aHandled = false;
+ return NS_OK;
+}
+
+/**
+ * The idea here is to see if the magic empty node has suddenly reappeared as
+ * the result of the undo. If it has, set our state so we remember it.
+ * There is a tradeoff between doing here and at redo, or doing it everywhere
+ * else that might care. Since undo and redo are relatively rare, it makes
+ * sense to take the (small) performance hit here.
+ */
+nsresult
+TextEditRules::DidUndo(Selection* aSelection,
+ nsresult aResult)
+{
+ NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER);
+ // If aResult is an error, we return it.
+ NS_ENSURE_SUCCESS(aResult, aResult);
+
+ NS_ENSURE_STATE(mTextEditor);
+ dom::Element* theRoot = mTextEditor->GetRoot();
+ NS_ENSURE_TRUE(theRoot, NS_ERROR_FAILURE);
+ nsIContent* node = mTextEditor->GetLeftmostChild(theRoot);
+ if (node && mTextEditor->IsMozEditorBogusNode(node)) {
+ mBogusNode = do_QueryInterface(node);
+ } else {
+ mBogusNode = nullptr;
+ }
+ return aResult;
+}
+
+nsresult
+TextEditRules::WillRedo(Selection* aSelection,
+ bool* aCancel,
+ bool* aHandled)
+{
+ if (!aSelection || !aCancel || !aHandled) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ CANCEL_OPERATION_IF_READONLY_OR_DISABLED
+ // initialize out param
+ *aCancel = false;
+ *aHandled = false;
+ return NS_OK;
+}
+
+nsresult
+TextEditRules::DidRedo(Selection* aSelection,
+ nsresult aResult)
+{
+ if (!aSelection) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ if (NS_FAILED(aResult)) {
+ return aResult; // if aResult is an error, we return it.
+ }
+
+ NS_ENSURE_STATE(mTextEditor);
+ nsCOMPtr<nsIDOMElement> theRoot = do_QueryInterface(mTextEditor->GetRoot());
+ NS_ENSURE_TRUE(theRoot, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIDOMHTMLCollection> nodeList;
+ nsresult rv = theRoot->GetElementsByTagName(NS_LITERAL_STRING("br"),
+ getter_AddRefs(nodeList));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (nodeList) {
+ uint32_t len;
+ nodeList->GetLength(&len);
+
+ if (len != 1) {
+ // only in the case of one br could there be the bogus node
+ mBogusNode = nullptr;
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIDOMNode> node;
+ nodeList->Item(0, getter_AddRefs(node));
+ nsCOMPtr<nsIContent> content = do_QueryInterface(node);
+ MOZ_ASSERT(content);
+ if (mTextEditor->IsMozEditorBogusNode(content)) {
+ mBogusNode = node;
+ } else {
+ mBogusNode = nullptr;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+TextEditRules::WillOutputText(Selection* aSelection,
+ const nsAString* aOutputFormat,
+ nsAString* aOutString,
+ bool* aCancel,
+ bool* aHandled)
+{
+ // null selection ok
+ if (!aOutString || !aOutputFormat || !aCancel || !aHandled) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ // initialize out param
+ *aCancel = false;
+ *aHandled = false;
+
+ nsAutoString outputFormat(*aOutputFormat);
+ ToLowerCase(outputFormat);
+ if (outputFormat.EqualsLiteral("text/plain")) {
+ // Only use these rules for plain text output.
+ if (IsPasswordEditor()) {
+ *aOutString = mPasswordText;
+ *aHandled = true;
+ } else if (mBogusNode) {
+ // This means there's no content, so output null string.
+ aOutString->Truncate();
+ *aHandled = true;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+TextEditRules::DidOutputText(Selection* aSelection,
+ nsresult aResult)
+{
+ return NS_OK;
+}
+
+nsresult
+TextEditRules::RemoveRedundantTrailingBR()
+{
+ // If the bogus node exists, we have no work to do
+ if (mBogusNode) {
+ return NS_OK;
+ }
+
+ // Likewise, nothing to be done if we could never have inserted a trailing br
+ if (IsSingleLineEditor()) {
+ return NS_OK;
+ }
+
+ NS_ENSURE_STATE(mTextEditor);
+ RefPtr<dom::Element> body = mTextEditor->GetRoot();
+ if (!body) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ uint32_t childCount = body->GetChildCount();
+ if (childCount > 1) {
+ // The trailing br is redundant if it is the only remaining child node
+ return NS_OK;
+ }
+
+ RefPtr<nsIContent> child = body->GetFirstChild();
+ if (!child || !child->IsElement()) {
+ return NS_OK;
+ }
+
+ dom::Element* elem = child->AsElement();
+ if (!TextEditUtils::IsMozBR(elem)) {
+ return NS_OK;
+ }
+
+ // Rather than deleting this node from the DOM tree we should instead
+ // morph this br into the bogus node
+ elem->UnsetAttr(kNameSpaceID_None, nsGkAtoms::type, true);
+
+ // set mBogusNode to be this <br>
+ mBogusNode = do_QueryInterface(elem);
+
+ // give it the bogus node attribute
+ elem->SetAttr(kNameSpaceID_None, kMOZEditorBogusNodeAttrAtom,
+ kMOZEditorBogusNodeValue, false);
+ return NS_OK;
+}
+
+nsresult
+TextEditRules::CreateTrailingBRIfNeeded()
+{
+ // but only if we aren't a single line edit field
+ if (IsSingleLineEditor()) {
+ return NS_OK;
+ }
+
+ NS_ENSURE_STATE(mTextEditor);
+ dom::Element* body = mTextEditor->GetRoot();
+ NS_ENSURE_TRUE(body, NS_ERROR_NULL_POINTER);
+
+ nsIContent* lastChild = body->GetLastChild();
+ // assuming CreateBogusNodeIfNeeded() has been called first
+ NS_ENSURE_TRUE(lastChild, NS_ERROR_NULL_POINTER);
+
+ if (!lastChild->IsHTMLElement(nsGkAtoms::br)) {
+ AutoTransactionsConserveSelection dontSpazMySelection(mTextEditor);
+ nsCOMPtr<nsIDOMNode> domBody = do_QueryInterface(body);
+ return CreateMozBR(domBody, body->Length());
+ }
+
+ // Check to see if the trailing BR is a former bogus node - this will have
+ // stuck around if we previously morphed a trailing node into a bogus node.
+ if (!mTextEditor->IsMozEditorBogusNode(lastChild)) {
+ return NS_OK;
+ }
+
+ // Morph it back to a mozBR
+ lastChild->UnsetAttr(kNameSpaceID_None, kMOZEditorBogusNodeAttrAtom, false);
+ lastChild->SetAttr(kNameSpaceID_None, nsGkAtoms::type,
+ NS_LITERAL_STRING("_moz"), true);
+ return NS_OK;
+}
+
+nsresult
+TextEditRules::CreateBogusNodeIfNeeded(Selection* aSelection)
+{
+ NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER);
+ NS_ENSURE_TRUE(mTextEditor, NS_ERROR_NULL_POINTER);
+
+ if (mBogusNode) {
+ // Let's not create more than one, ok?
+ return NS_OK;
+ }
+
+ // tell rules system to not do any post-processing
+ AutoRules beginRulesSniffing(mTextEditor, EditAction::ignore,
+ nsIEditor::eNone);
+
+ nsCOMPtr<dom::Element> body = mTextEditor->GetRoot();
+ if (!body) {
+ // We don't even have a body yet, don't insert any bogus nodes at
+ // this point.
+ return NS_OK;
+ }
+
+ // Now we've got the body element. Iterate over the body element's children,
+ // looking for editable content. If no editable content is found, insert the
+ // bogus node.
+ for (nsCOMPtr<nsIContent> bodyChild = body->GetFirstChild();
+ bodyChild;
+ bodyChild = bodyChild->GetNextSibling()) {
+ if (mTextEditor->IsMozEditorBogusNode(bodyChild) ||
+ !mTextEditor->IsEditable(body) || // XXX hoist out of the loop?
+ mTextEditor->IsEditable(bodyChild) ||
+ mTextEditor->IsBlockNode(bodyChild)) {
+ return NS_OK;
+ }
+ }
+
+ // Skip adding the bogus node if body is read-only.
+ if (!mTextEditor->IsModifiableNode(body)) {
+ return NS_OK;
+ }
+
+ // Create a br.
+ nsCOMPtr<Element> newContent = mTextEditor->CreateHTMLContent(nsGkAtoms::br);
+ NS_ENSURE_STATE(newContent);
+
+ // set mBogusNode to be the newly created <br>
+ mBogusNode = do_QueryInterface(newContent);
+ NS_ENSURE_TRUE(mBogusNode, NS_ERROR_NULL_POINTER);
+
+ // Give it a special attribute.
+ newContent->SetAttr(kNameSpaceID_None, kMOZEditorBogusNodeAttrAtom,
+ kMOZEditorBogusNodeValue, false);
+
+ // Put the node in the document.
+ nsCOMPtr<nsIDOMNode> bodyNode = do_QueryInterface(body);
+ nsresult rv = mTextEditor->InsertNode(mBogusNode, bodyNode, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Set selection.
+ aSelection->CollapseNative(body, 0);
+ return NS_OK;
+}
+
+
+nsresult
+TextEditRules::TruncateInsertionIfNeeded(Selection* aSelection,
+ const nsAString* aInString,
+ nsAString* aOutString,
+ int32_t aMaxLength,
+ bool* aTruncated)
+{
+ if (!aSelection || !aInString || !aOutString) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ if (!aOutString->Assign(*aInString, mozilla::fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ if (aTruncated) {
+ *aTruncated = false;
+ }
+
+ NS_ENSURE_STATE(mTextEditor);
+ if (-1 != aMaxLength && IsPlaintextEditor() &&
+ !mTextEditor->IsIMEComposing()) {
+ // Get the current text length.
+ // Get the length of inString.
+ // Get the length of the selection.
+ // If selection is collapsed, it is length 0.
+ // Subtract the length of the selection from the len(doc)
+ // since we'll delete the selection on insert.
+ // This is resultingDocLength.
+ // Get old length of IME composing string
+ // which will be replaced by new one.
+ // If (resultingDocLength) is at or over max, cancel the insert
+ // If (resultingDocLength) + (length of input) > max,
+ // set aOutString to subset of inString so length = max
+ int32_t docLength;
+ nsresult rv = mTextEditor->GetTextLength(&docLength);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ int32_t start, end;
+ nsContentUtils::GetSelectionInTextControl(aSelection,
+ mTextEditor->GetRoot(),
+ start, end);
+
+ TextComposition* composition = mTextEditor->GetComposition();
+ int32_t oldCompStrLength = composition ? composition->String().Length() : 0;
+
+ const int32_t selectionLength = end - start;
+ const int32_t resultingDocLength = docLength - selectionLength - oldCompStrLength;
+ if (resultingDocLength >= aMaxLength) {
+ // This call is guaranteed to reduce the capacity of the string, so it
+ // cannot cause an OOM.
+ aOutString->Truncate();
+ if (aTruncated) {
+ *aTruncated = true;
+ }
+ } else {
+ int32_t oldLength = aOutString->Length();
+ if (oldLength + resultingDocLength > aMaxLength) {
+ int32_t newLength = aMaxLength - resultingDocLength;
+ MOZ_ASSERT(newLength > 0);
+ char16_t newLastChar = aOutString->CharAt(newLength - 1);
+ char16_t removingFirstChar = aOutString->CharAt(newLength);
+ // Don't separate the string between a surrogate pair.
+ if (NS_IS_HIGH_SURROGATE(newLastChar) &&
+ NS_IS_LOW_SURROGATE(removingFirstChar)) {
+ newLength--;
+ }
+ // XXX What should we do if we're removing IVS and its preceding
+ // character won't be removed?
+ // This call is guaranteed to reduce the capacity of the string, so it
+ // cannot cause an OOM.
+ aOutString->Truncate(newLength);
+ if (aTruncated) {
+ *aTruncated = true;
+ }
+ }
+ }
+ }
+ return NS_OK;
+}
+
+void
+TextEditRules::ResetIMETextPWBuf()
+{
+ mPasswordIMEText.Truncate();
+}
+
+void
+TextEditRules::RemoveIMETextFromPWBuf(int32_t& aStart,
+ nsAString* aIMEString)
+{
+ MOZ_ASSERT(aIMEString);
+
+ // initialize PasswordIME
+ if (mPasswordIMEText.IsEmpty()) {
+ mPasswordIMEIndex = aStart;
+ } else {
+ // manage the password buffer
+ mPasswordText.Cut(mPasswordIMEIndex, mPasswordIMEText.Length());
+ aStart = mPasswordIMEIndex;
+ }
+
+ mPasswordIMEText.Assign(*aIMEString);
+}
+
+NS_IMETHODIMP
+TextEditRules::Notify(nsITimer* aTimer)
+{
+ MOZ_ASSERT(mTimer);
+
+ // Check whether our text editor's password flag was changed before this
+ // "hide password character" timer actually fires.
+ nsresult rv = IsPasswordEditor() ? HideLastPWInput() : NS_OK;
+ ASSERT_PASSWORD_LENGTHS_EQUAL();
+ mLastLength = 0;
+ return rv;
+}
+
+nsresult
+TextEditRules::HideLastPWInput()
+{
+ if (!mLastLength) {
+ // Special case, we're trying to replace a range that no longer exists
+ return NS_OK;
+ }
+
+ nsAutoString hiddenText;
+ FillBufWithPWChars(&hiddenText, mLastLength);
+
+ NS_ENSURE_STATE(mTextEditor);
+ RefPtr<Selection> selection = mTextEditor->GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+ int32_t start, end;
+ nsContentUtils::GetSelectionInTextControl(selection, mTextEditor->GetRoot(),
+ start, end);
+
+ nsCOMPtr<nsIDOMNode> selNode = GetTextNode(selection, mTextEditor);
+ NS_ENSURE_TRUE(selNode, NS_OK);
+
+ nsCOMPtr<nsIDOMCharacterData> nodeAsText(do_QueryInterface(selNode));
+ NS_ENSURE_TRUE(nodeAsText, NS_OK);
+
+ nodeAsText->ReplaceData(mLastStart, mLastLength, hiddenText);
+ selection->Collapse(selNode, start);
+ if (start != end) {
+ selection->Extend(selNode, end);
+ }
+ return NS_OK;
+}
+
+// static
+void
+TextEditRules::FillBufWithPWChars(nsAString* aOutString,
+ int32_t aLength)
+{
+ MOZ_ASSERT(aOutString);
+
+ // change the output to the platform password character
+ char16_t passwordChar = LookAndFeel::GetPasswordCharacter();
+
+ aOutString->Truncate();
+ for (int32_t i = 0; i < aLength; i++) {
+ aOutString->Append(passwordChar);
+ }
+}
+
+/**
+ * CreateMozBR() puts a BR node with moz attribute at {inParent, inOffset}.
+ */
+nsresult
+TextEditRules::CreateMozBR(nsIDOMNode* inParent,
+ int32_t inOffset,
+ nsIDOMNode** outBRNode)
+{
+ NS_ENSURE_TRUE(inParent, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsIDOMNode> brNode;
+ NS_ENSURE_STATE(mTextEditor);
+ nsresult rv = mTextEditor->CreateBR(inParent, inOffset, address_of(brNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // give it special moz attr
+ nsCOMPtr<nsIDOMElement> brElem = do_QueryInterface(brNode);
+ if (brElem) {
+ rv = mTextEditor->SetAttribute(brElem, NS_LITERAL_STRING("type"),
+ NS_LITERAL_STRING("_moz"));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (outBRNode) {
+ brNode.forget(outBRNode);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextEditRules::DocumentModified()
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+bool
+TextEditRules::IsPasswordEditor() const
+{
+ return mTextEditor ? mTextEditor->IsPasswordEditor() : false;
+}
+
+bool
+TextEditRules::IsSingleLineEditor() const
+{
+ return mTextEditor ? mTextEditor->IsSingleLineEditor() : false;
+}
+
+bool
+TextEditRules::IsPlaintextEditor() const
+{
+ return mTextEditor ? mTextEditor->IsPlaintextEditor() : false;
+}
+
+bool
+TextEditRules::IsReadonly() const
+{
+ return mTextEditor ? mTextEditor->IsReadonly() : false;
+}
+
+bool
+TextEditRules::IsDisabled() const
+{
+ return mTextEditor ? mTextEditor->IsDisabled() : false;
+}
+bool
+TextEditRules::IsMailEditor() const
+{
+ return mTextEditor ? mTextEditor->IsMailEditor() : false;
+}
+
+bool
+TextEditRules::DontEchoPassword() const
+{
+ return mTextEditor ? mTextEditor->DontEchoPassword() : false;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/TextEditRules.h b/editor/libeditor/TextEditRules.h
new file mode 100644
index 000000000..6d4915f15
--- /dev/null
+++ b/editor/libeditor/TextEditRules.h
@@ -0,0 +1,357 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_TextEditRules_h
+#define mozilla_TextEditRules_h
+
+#include "mozilla/EditorBase.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIEditRules.h"
+#include "nsIEditor.h"
+#include "nsISupportsImpl.h"
+#include "nsITimer.h"
+#include "nsString.h"
+#include "nscore.h"
+
+class nsIDOMElement;
+class nsIDOMNode;
+
+namespace mozilla {
+
+class AutoLockRulesSniffing;
+class TextEditor;
+namespace dom {
+class Selection;
+} // namespace dom
+
+/**
+ * Object that encapsulates HTML text-specific editing rules.
+ *
+ * To be a good citizen, edit rules must live by these restrictions:
+ * 1. All data manipulation is through the editor.
+ * Content nodes in the document tree must <B>not</B> be manipulated
+ * directly. Content nodes in document fragments that are not part of the
+ * document itself may be manipulated at will. Operations on document
+ * fragments must <B>not</B> go through the editor.
+ * 2. Selection must not be explicitly set by the rule method.
+ * Any manipulation of Selection must be done by the editor.
+ */
+class TextEditRules : public nsIEditRules
+ , public nsITimerCallback
+{
+public:
+ typedef dom::Element Element;
+ typedef dom::Selection Selection;
+ typedef dom::Text Text;
+ template<typename T> using OwningNonNull = OwningNonNull<T>;
+
+ NS_DECL_NSITIMERCALLBACK
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(TextEditRules, nsIEditRules)
+
+ TextEditRules();
+
+ // nsIEditRules methods
+ NS_IMETHOD Init(TextEditor* aTextEditor) override;
+ NS_IMETHOD SetInitialValue(const nsAString& aValue) override;
+ NS_IMETHOD DetachEditor() override;
+ NS_IMETHOD BeforeEdit(EditAction action,
+ nsIEditor::EDirection aDirection) override;
+ NS_IMETHOD AfterEdit(EditAction action,
+ nsIEditor::EDirection aDirection) override;
+ NS_IMETHOD WillDoAction(Selection* aSelection, RulesInfo* aInfo,
+ bool* aCancel, bool* aHandled) override;
+ NS_IMETHOD DidDoAction(Selection* aSelection, RulesInfo* aInfo,
+ nsresult aResult) override;
+ NS_IMETHOD DocumentIsEmpty(bool* aDocumentIsEmpty) override;
+ NS_IMETHOD DocumentModified() override;
+
+protected:
+ virtual ~TextEditRules();
+
+public:
+ void ResetIMETextPWBuf();
+
+ /**
+ * Handles the newline characters either according to aNewLineHandling
+ * or to the default system prefs if aNewLineHandling is negative.
+ *
+ * @param aString the string to be modified in place.
+ * @param aNewLineHandling determine the desired type of newline handling:
+ * * negative values:
+ * handle newlines according to platform defaults.
+ * * nsIPlaintextEditor::eNewlinesReplaceWithSpaces:
+ * replace newlines with spaces.
+ * * nsIPlaintextEditor::eNewlinesStrip:
+ * remove newlines from the string.
+ * * nsIPlaintextEditor::eNewlinesReplaceWithCommas:
+ * replace newlines with commas.
+ * * nsIPlaintextEditor::eNewlinesStripSurroundingWhitespace:
+ * collapse newlines and surrounding whitespace characters and
+ * remove them from the string.
+ * * nsIPlaintextEditor::eNewlinesPasteIntact:
+ * only remove the leading and trailing newlines.
+ * * nsIPlaintextEditor::eNewlinesPasteToFirst or any other value:
+ * remove the first newline and all characters following it.
+ */
+ static void HandleNewLines(nsString& aString, int32_t aNewLineHandling);
+
+ /**
+ * Prepare a string buffer for being displayed as the contents of a password
+ * field. This function uses the platform-specific character for representing
+ * characters entered into password fields.
+ *
+ * @param aOutString the output string. When this function returns,
+ * aOutString will contain aLength password characters.
+ * @param aLength the number of password characters that aOutString should
+ * contain.
+ */
+ static void FillBufWithPWChars(nsAString* aOutString, int32_t aLength);
+
+protected:
+
+ void InitFields();
+
+ // TextEditRules implementation methods
+ nsresult WillInsertText(EditAction aAction,
+ Selection* aSelection,
+ bool* aCancel,
+ bool* aHandled,
+ const nsAString* inString,
+ nsAString* outString,
+ int32_t aMaxLength);
+ nsresult DidInsertText(Selection* aSelection, nsresult aResult);
+ nsresult GetTopEnclosingPre(nsIDOMNode* aNode, nsIDOMNode** aOutPreNode);
+
+ nsresult WillInsertBreak(Selection* aSelection, bool* aCancel,
+ bool* aHandled, int32_t aMaxLength);
+ nsresult DidInsertBreak(Selection* aSelection, nsresult aResult);
+
+ void WillInsert(Selection& aSelection, bool* aCancel);
+ nsresult DidInsert(Selection* aSelection, nsresult aResult);
+
+ nsresult WillDeleteSelection(Selection* aSelection,
+ nsIEditor::EDirection aCollapsedAction,
+ bool* aCancel,
+ bool* aHandled);
+ nsresult DidDeleteSelection(Selection* aSelection,
+ nsIEditor::EDirection aCollapsedAction,
+ nsresult aResult);
+
+ nsresult WillSetTextProperty(Selection* aSelection, bool* aCancel,
+ bool* aHandled);
+ nsresult DidSetTextProperty(Selection* aSelection, nsresult aResult);
+
+ nsresult WillRemoveTextProperty(Selection* aSelection, bool* aCancel,
+ bool* aHandled);
+ nsresult DidRemoveTextProperty(Selection* aSelection, nsresult aResult);
+
+ nsresult WillUndo(Selection* aSelection, bool* aCancel, bool* aHandled);
+ nsresult DidUndo(Selection* aSelection, nsresult aResult);
+
+ nsresult WillRedo(Selection* aSelection, bool* aCancel, bool* aHandled);
+ nsresult DidRedo(Selection* aSelection, nsresult aResult);
+
+ /**
+ * Called prior to nsIEditor::OutputToString.
+ * @param aSelection
+ * @param aInFormat The format requested for the output, a MIME type.
+ * @param aOutText The string to use for output, if aCancel is set to true.
+ * @param aOutCancel If set to true, the caller should cancel the operation
+ * and use aOutText as the result.
+ */
+ nsresult WillOutputText(Selection* aSelection,
+ const nsAString* aInFormat,
+ nsAString* aOutText,
+ bool* aOutCancel,
+ bool* aHandled);
+
+ nsresult DidOutputText(Selection* aSelection, nsresult aResult);
+
+ /**
+ * Check for and replace a redundant trailing break.
+ */
+ nsresult RemoveRedundantTrailingBR();
+
+ /**
+ * Creates a trailing break in the text doc if there is not one already.
+ */
+ nsresult CreateTrailingBRIfNeeded();
+
+ /**
+ * Creates a bogus text node if the document has no editable content.
+ */
+ nsresult CreateBogusNodeIfNeeded(Selection* aSelection);
+
+ /**
+ * Returns a truncated insertion string if insertion would place us over
+ * aMaxLength
+ */
+ nsresult TruncateInsertionIfNeeded(Selection* aSelection,
+ const nsAString* aInString,
+ nsAString* aOutString,
+ int32_t aMaxLength,
+ bool* aTruncated);
+
+ /**
+ * Remove IME composition text from password buffer.
+ */
+ void RemoveIMETextFromPWBuf(int32_t& aStart, nsAString* aIMEString);
+
+ nsresult CreateMozBR(nsIDOMNode* inParent, int32_t inOffset,
+ nsIDOMNode** outBRNode = nullptr);
+
+ void UndefineCaretBidiLevel(Selection* aSelection);
+
+ nsresult CheckBidiLevelForDeletion(Selection* aSelection,
+ nsIDOMNode* aSelNode,
+ int32_t aSelOffset,
+ nsIEditor::EDirection aAction,
+ bool* aCancel);
+
+ nsresult HideLastPWInput();
+
+ nsresult CollapseSelectionToTrailingBRIfNeeded(Selection* aSelection);
+
+ bool IsPasswordEditor() const;
+ bool IsSingleLineEditor() const;
+ bool IsPlaintextEditor() const;
+ bool IsReadonly() const;
+ bool IsDisabled() const;
+ bool IsMailEditor() const;
+ bool DontEchoPassword() const;
+
+ // Note that we do not refcount the editor.
+ TextEditor* mTextEditor;
+ // A buffer we use to store the real value of password editors.
+ nsString mPasswordText;
+ // A buffer we use to track the IME composition string.
+ nsString mPasswordIMEText;
+ uint32_t mPasswordIMEIndex;
+ // Magic node acts as placeholder in empty doc.
+ nsCOMPtr<nsIDOMNode> mBogusNode;
+ // Cached selected node.
+ nsCOMPtr<nsIDOMNode> mCachedSelectionNode;
+ // Cached selected offset.
+ int32_t mCachedSelectionOffset;
+ uint32_t mActionNesting;
+ bool mLockRulesSniffing;
+ bool mDidExplicitlySetInterline;
+ // In bidirectional text, delete characters not visually adjacent to the
+ // caret without moving the caret first.
+ bool mDeleteBidiImmediately;
+ // The top level editor action.
+ EditAction mTheAction;
+ nsCOMPtr<nsITimer> mTimer;
+ uint32_t mLastStart;
+ uint32_t mLastLength;
+
+ // friends
+ friend class AutoLockRulesSniffing;
+};
+
+class TextRulesInfo final : public RulesInfo
+{
+public:
+ explicit TextRulesInfo(EditAction aAction)
+ : RulesInfo(aAction)
+ , inString(nullptr)
+ , outString(nullptr)
+ , outputFormat(nullptr)
+ , maxLength(-1)
+ , collapsedAction(nsIEditor::eNext)
+ , stripWrappers(nsIEditor::eStrip)
+ , bOrdered(false)
+ , entireList(false)
+ , bulletType(nullptr)
+ , alignType(nullptr)
+ , blockType(nullptr)
+ , insertElement(nullptr)
+ {}
+
+ // kInsertText
+ const nsAString* inString;
+ nsAString* outString;
+ const nsAString* outputFormat;
+ int32_t maxLength;
+
+ // kDeleteSelection
+ nsIEditor::EDirection collapsedAction;
+ nsIEditor::EStripWrappers stripWrappers;
+
+ // kMakeList
+ bool bOrdered;
+ bool entireList;
+ const nsAString* bulletType;
+
+ // kAlign
+ const nsAString* alignType;
+
+ // kMakeBasicBlock
+ const nsAString* blockType;
+
+ // kInsertElement
+ const nsIDOMElement* insertElement;
+};
+
+/**
+ * Stack based helper class for StartOperation()/EndOperation() sandwich.
+ * This class sets a bool letting us know to ignore any rules sniffing
+ * that tries to occur reentrantly.
+ */
+class MOZ_STACK_CLASS AutoLockRulesSniffing final
+{
+public:
+ explicit AutoLockRulesSniffing(TextEditRules* aRules)
+ : mRules(aRules)
+ {
+ if (mRules) {
+ mRules->mLockRulesSniffing = true;
+ }
+ }
+
+ ~AutoLockRulesSniffing()
+ {
+ if (mRules) {
+ mRules->mLockRulesSniffing = false;
+ }
+ }
+
+protected:
+ TextEditRules* mRules;
+};
+
+/**
+ * Stack based helper class for turning on/off the edit listener.
+ */
+class MOZ_STACK_CLASS AutoLockListener final
+{
+public:
+ explicit AutoLockListener(bool* aEnabled)
+ : mEnabled(aEnabled)
+ , mOldState(false)
+ {
+ if (mEnabled) {
+ mOldState = *mEnabled;
+ *mEnabled = false;
+ }
+ }
+
+ ~AutoLockListener()
+ {
+ if (mEnabled) {
+ *mEnabled = mOldState;
+ }
+ }
+
+protected:
+ bool* mEnabled;
+ bool mOldState;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef mozilla_TextEditRules_h
diff --git a/editor/libeditor/TextEditRulesBidi.cpp b/editor/libeditor/TextEditRulesBidi.cpp
new file mode 100644
index 000000000..f6b8b7120
--- /dev/null
+++ b/editor/libeditor/TextEditRulesBidi.cpp
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/TextEditRules.h"
+
+#include "mozilla/TextEditor.h"
+#include "mozilla/dom/Selection.h"
+#include "nsCOMPtr.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsFrameSelection.h"
+#include "nsIContent.h"
+#include "nsIDOMNode.h"
+#include "nsIEditor.h"
+#include "nsIPresShell.h"
+#include "nsISupportsImpl.h"
+#include "nsPresContext.h"
+#include "nscore.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+// Test for distance between caret and text that will be deleted
+nsresult
+TextEditRules::CheckBidiLevelForDeletion(Selection* aSelection,
+ nsIDOMNode* aSelNode,
+ int32_t aSelOffset,
+ nsIEditor::EDirection aAction,
+ bool* aCancel)
+{
+ NS_ENSURE_ARG_POINTER(aCancel);
+ *aCancel = false;
+
+ nsCOMPtr<nsIPresShell> shell = mTextEditor->GetPresShell();
+ NS_ENSURE_TRUE(shell, NS_ERROR_NOT_INITIALIZED);
+
+ nsPresContext *context = shell->GetPresContext();
+ NS_ENSURE_TRUE(context, NS_ERROR_NULL_POINTER);
+
+ if (!context->BidiEnabled()) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIContent> content = do_QueryInterface(aSelNode);
+ NS_ENSURE_TRUE(content, NS_ERROR_NULL_POINTER);
+
+ nsBidiLevel levelBefore;
+ nsBidiLevel levelAfter;
+ RefPtr<nsFrameSelection> frameSelection =
+ aSelection->AsSelection()->GetFrameSelection();
+ NS_ENSURE_TRUE(frameSelection, NS_ERROR_NULL_POINTER);
+
+ nsPrevNextBidiLevels levels = frameSelection->
+ GetPrevNextBidiLevels(content, aSelOffset, true);
+
+ levelBefore = levels.mLevelBefore;
+ levelAfter = levels.mLevelAfter;
+
+ nsBidiLevel currentCaretLevel = frameSelection->GetCaretBidiLevel();
+
+ nsBidiLevel levelOfDeletion;
+ levelOfDeletion =
+ (nsIEditor::eNext==aAction || nsIEditor::eNextWord==aAction) ?
+ levelAfter : levelBefore;
+
+ if (currentCaretLevel == levelOfDeletion) {
+ return NS_OK; // perform the deletion
+ }
+
+ if (!mDeleteBidiImmediately && levelBefore != levelAfter) {
+ *aCancel = true;
+ }
+
+ // Set the bidi level of the caret to that of the
+ // character that will be (or would have been) deleted
+ frameSelection->SetCaretBidiLevel(levelOfDeletion);
+ return NS_OK;
+}
+
+void
+TextEditRules::UndefineCaretBidiLevel(Selection* aSelection)
+{
+ /**
+ * After inserting text the caret Bidi level must be set to the level of the
+ * inserted text.This is difficult, because we cannot know what the level is
+ * until after the Bidi algorithm is applied to the whole paragraph.
+ *
+ * So we set the caret Bidi level to UNDEFINED here, and the caret code will
+ * set it correctly later
+ */
+ RefPtr<nsFrameSelection> frameSelection = aSelection->GetFrameSelection();
+ if (frameSelection) {
+ frameSelection->UndefineCaretBidiLevel();
+ }
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/TextEditUtils.cpp b/editor/libeditor/TextEditUtils.cpp
new file mode 100644
index 000000000..7c4f2ec12
--- /dev/null
+++ b/editor/libeditor/TextEditUtils.cpp
@@ -0,0 +1,113 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TextEditUtils.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/TextEditor.h"
+#include "mozilla/dom/Element.h"
+#include "nsAString.h"
+#include "nsCOMPtr.h"
+#include "nsCaseTreatment.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsGkAtoms.h"
+#include "nsIDOMElement.h"
+#include "nsIDOMNode.h"
+#include "nsNameSpaceManager.h"
+#include "nsLiteralString.h"
+#include "nsString.h"
+
+namespace mozilla {
+
+/******************************************************************************
+ * TextEditUtils
+ ******************************************************************************/
+
+/**
+ * IsBody() returns true if aNode is an html body node.
+ */
+bool
+TextEditUtils::IsBody(nsIDOMNode* aNode)
+{
+ return EditorBase::NodeIsType(aNode, nsGkAtoms::body);
+}
+
+/**
+ * IsBreak() returns true if aNode is an html break node.
+ */
+bool
+TextEditUtils::IsBreak(nsIDOMNode* aNode)
+{
+ return EditorBase::NodeIsType(aNode, nsGkAtoms::br);
+}
+
+bool
+TextEditUtils::IsBreak(nsINode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ return aNode->IsHTMLElement(nsGkAtoms::br);
+}
+
+
+/**
+ * IsMozBR() returns true if aNode is an html br node with |type = _moz|.
+ */
+bool
+TextEditUtils::IsMozBR(nsIDOMNode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ return IsBreak(aNode) && HasMozAttr(aNode);
+}
+
+bool
+TextEditUtils::IsMozBR(nsINode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ return aNode->IsHTMLElement(nsGkAtoms::br) &&
+ aNode->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+ NS_LITERAL_STRING("_moz"),
+ eIgnoreCase);
+}
+
+/**
+ * HasMozAttr() returns true if aNode has type attribute and its value is
+ * |_moz|. (Used to indicate div's and br's we use in mail compose rules)
+ */
+bool
+TextEditUtils::HasMozAttr(nsIDOMNode* aNode)
+{
+ MOZ_ASSERT(aNode);
+ nsCOMPtr<nsIDOMElement> element = do_QueryInterface(aNode);
+ if (!element) {
+ return false;
+ }
+ nsAutoString typeAttrVal;
+ nsresult rv = element->GetAttribute(NS_LITERAL_STRING("type"), typeAttrVal);
+ return NS_SUCCEEDED(rv) && typeAttrVal.LowerCaseEqualsLiteral("_moz");
+}
+
+/******************************************************************************
+ * AutoEditInitRulesTrigger
+ ******************************************************************************/
+
+AutoEditInitRulesTrigger::AutoEditInitRulesTrigger(TextEditor* aTextEditor,
+ nsresult& aResult)
+ : mTextEditor(aTextEditor)
+ , mResult(aResult)
+{
+ if (mTextEditor) {
+ mTextEditor->BeginEditorInit();
+ }
+}
+
+AutoEditInitRulesTrigger::~AutoEditInitRulesTrigger()
+{
+ if (mTextEditor) {
+ mResult = mTextEditor->EndEditorInit();
+ }
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/TextEditUtils.h b/editor/libeditor/TextEditUtils.h
new file mode 100644
index 000000000..cfdbc928f
--- /dev/null
+++ b/editor/libeditor/TextEditUtils.h
@@ -0,0 +1,47 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef TextEditUtils_h
+#define TextEditUtils_h
+
+#include "nscore.h"
+
+class nsIDOMNode;
+class nsINode;
+
+namespace mozilla {
+
+class TextEditor;
+
+class TextEditUtils final
+{
+public:
+ // from TextEditRules:
+ static bool IsBody(nsIDOMNode* aNode);
+ static bool IsBreak(nsIDOMNode* aNode);
+ static bool IsBreak(nsINode* aNode);
+ static bool IsMozBR(nsIDOMNode* aNode);
+ static bool IsMozBR(nsINode* aNode);
+ static bool HasMozAttr(nsIDOMNode* aNode);
+};
+
+/***************************************************************************
+ * stack based helper class for detecting end of editor initialization, in
+ * order to trigger "end of init" initialization of the edit rules.
+ */
+class AutoEditInitRulesTrigger final
+{
+private:
+ TextEditor* mTextEditor;
+ nsresult& mResult;
+
+public:
+ AutoEditInitRulesTrigger(TextEditor* aTextEditor, nsresult& aResult);
+ ~AutoEditInitRulesTrigger();
+};
+
+} // naemspace mozilla
+
+#endif // #ifndef TextEditUtils_h
diff --git a/editor/libeditor/TextEditor.cpp b/editor/libeditor/TextEditor.cpp
new file mode 100644
index 000000000..8fe824e11
--- /dev/null
+++ b/editor/libeditor/TextEditor.cpp
@@ -0,0 +1,1638 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/TextEditor.h"
+
+#include "InternetCiter.h"
+#include "TextEditUtils.h"
+#include "gfxFontUtils.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/EditorUtils.h" // AutoEditBatch, AutoRules
+#include "mozilla/mozalloc.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/TextEditRules.h"
+#include "mozilla/TextComposition.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/Element.h"
+#include "nsAString.h"
+#include "nsCRT.h"
+#include "nsCaret.h"
+#include "nsCharTraits.h"
+#include "nsComponentManagerUtils.h"
+#include "nsContentCID.h"
+#include "nsCopySupport.h"
+#include "nsDebug.h"
+#include "nsDependentSubstring.h"
+#include "nsError.h"
+#include "nsGkAtoms.h"
+#include "nsIClipboard.h"
+#include "nsIContent.h"
+#include "nsIContentIterator.h"
+#include "nsIDOMCharacterData.h"
+#include "nsIDOMDocument.h"
+#include "nsIDOMElement.h"
+#include "nsIDOMEventTarget.h"
+#include "nsIDOMKeyEvent.h"
+#include "nsIDOMNode.h"
+#include "nsIDOMNodeList.h"
+#include "nsIDocumentEncoder.h"
+#include "nsIEditorIMESupport.h"
+#include "nsIEditRules.h"
+#include "nsINode.h"
+#include "nsIPresShell.h"
+#include "nsISelectionController.h"
+#include "nsISupportsPrimitives.h"
+#include "nsITransferable.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsNameSpaceManager.h"
+#include "nsLiteralString.h"
+#include "nsReadableUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsSubstringTuple.h"
+#include "nsUnicharUtils.h"
+#include "nsXPCOM.h"
+
+class nsIOutputStream;
+class nsISupports;
+
+namespace mozilla {
+
+using namespace dom;
+
+TextEditor::TextEditor()
+ : mWrapColumn(0)
+ , mMaxTextLength(-1)
+ , mInitTriggerCounter(0)
+ , mNewlineHandling(nsIPlaintextEditor::eNewlinesPasteToFirst)
+#ifdef XP_WIN
+ , mCaretStyle(1)
+#else
+ , mCaretStyle(0)
+#endif
+{
+ // check the "single line editor newline handling"
+ // and "caret behaviour in selection" prefs
+ GetDefaultEditorPrefs(mNewlineHandling, mCaretStyle);
+}
+
+TextEditor::~TextEditor()
+{
+ // Remove event listeners. Note that if we had an HTML editor,
+ // it installed its own instead of these
+ RemoveEventListeners();
+
+ if (mRules)
+ mRules->DetachEditor();
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(TextEditor)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(TextEditor, EditorBase)
+ if (tmp->mRules)
+ tmp->mRules->DetachEditor();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mRules)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(TextEditor, EditorBase)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRules)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_ADDREF_INHERITED(TextEditor, EditorBase)
+NS_IMPL_RELEASE_INHERITED(TextEditor, EditorBase)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(TextEditor)
+ NS_INTERFACE_MAP_ENTRY(nsIPlaintextEditor)
+ NS_INTERFACE_MAP_ENTRY(nsIEditorMailSupport)
+NS_INTERFACE_MAP_END_INHERITING(EditorBase)
+
+
+NS_IMETHODIMP
+TextEditor::Init(nsIDOMDocument* aDoc,
+ nsIContent* aRoot,
+ nsISelectionController* aSelCon,
+ uint32_t aFlags,
+ const nsAString& aInitialValue)
+{
+ NS_PRECONDITION(aDoc, "bad arg");
+ NS_ENSURE_TRUE(aDoc, NS_ERROR_NULL_POINTER);
+
+ if (mRules) {
+ mRules->DetachEditor();
+ }
+
+ nsresult rulesRv = NS_OK;
+ {
+ // block to scope AutoEditInitRulesTrigger
+ AutoEditInitRulesTrigger rulesTrigger(this, rulesRv);
+
+ // Init the base editor
+ nsresult rv = EditorBase::Init(aDoc, aRoot, aSelCon, aFlags, aInitialValue);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+ NS_ENSURE_SUCCESS(rulesRv, rulesRv);
+
+ // mRules may not have been initialized yet, when this is called via
+ // HTMLEditor::Init.
+ if (mRules) {
+ mRules->SetInitialValue(aInitialValue);
+ }
+
+ return NS_OK;
+}
+
+static int32_t sNewlineHandlingPref = -1,
+ sCaretStylePref = -1;
+
+static void
+EditorPrefsChangedCallback(const char* aPrefName, void *)
+{
+ if (!nsCRT::strcmp(aPrefName, "editor.singleLine.pasteNewlines")) {
+ sNewlineHandlingPref =
+ Preferences::GetInt("editor.singleLine.pasteNewlines",
+ nsIPlaintextEditor::eNewlinesPasteToFirst);
+ } else if (!nsCRT::strcmp(aPrefName, "layout.selection.caret_style")) {
+ sCaretStylePref = Preferences::GetInt("layout.selection.caret_style",
+#ifdef XP_WIN
+ 1);
+ if (!sCaretStylePref) {
+ sCaretStylePref = 1;
+ }
+#else
+ 0);
+#endif
+ }
+}
+
+// static
+void
+TextEditor::GetDefaultEditorPrefs(int32_t& aNewlineHandling,
+ int32_t& aCaretStyle)
+{
+ if (sNewlineHandlingPref == -1) {
+ Preferences::RegisterCallback(EditorPrefsChangedCallback,
+ "editor.singleLine.pasteNewlines");
+ EditorPrefsChangedCallback("editor.singleLine.pasteNewlines", nullptr);
+ Preferences::RegisterCallback(EditorPrefsChangedCallback,
+ "layout.selection.caret_style");
+ EditorPrefsChangedCallback("layout.selection.caret_style", nullptr);
+ }
+
+ aNewlineHandling = sNewlineHandlingPref;
+ aCaretStyle = sCaretStylePref;
+}
+
+void
+TextEditor::BeginEditorInit()
+{
+ mInitTriggerCounter++;
+}
+
+nsresult
+TextEditor::EndEditorInit()
+{
+ NS_PRECONDITION(mInitTriggerCounter > 0, "ended editor init before we began?");
+ mInitTriggerCounter--;
+ if (mInitTriggerCounter) {
+ return NS_OK;
+ }
+
+ nsresult rv = InitRules();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ // Throw away the old transaction manager if this is not the first time that
+ // we're initializing the editor.
+ EnableUndo(false);
+ EnableUndo(true);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextEditor::SetDocumentCharacterSet(const nsACString& characterSet)
+{
+ nsresult rv = EditorBase::SetDocumentCharacterSet(characterSet);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Update META charset element.
+ nsCOMPtr<nsIDOMDocument> domdoc = GetDOMDocument();
+ NS_ENSURE_TRUE(domdoc, NS_ERROR_NOT_INITIALIZED);
+
+ if (UpdateMetaCharset(domdoc, characterSet)) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIDOMNodeList> headList;
+ rv = domdoc->GetElementsByTagName(NS_LITERAL_STRING("head"), getter_AddRefs(headList));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(headList, NS_OK);
+
+ nsCOMPtr<nsIDOMNode> headNode;
+ headList->Item(0, getter_AddRefs(headNode));
+ NS_ENSURE_TRUE(headNode, NS_OK);
+
+ // Create a new meta charset tag
+ nsCOMPtr<nsIDOMNode> resultNode;
+ rv = CreateNode(NS_LITERAL_STRING("meta"), headNode, 0, getter_AddRefs(resultNode));
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE);
+ NS_ENSURE_TRUE(resultNode, NS_OK);
+
+ // Set attributes to the created element
+ if (characterSet.IsEmpty()) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<dom::Element> metaElement = do_QueryInterface(resultNode);
+ if (!metaElement) {
+ return NS_OK;
+ }
+
+ // not undoable, undo should undo CreateNode
+ metaElement->SetAttr(kNameSpaceID_None, nsGkAtoms::httpEquiv,
+ NS_LITERAL_STRING("Content-Type"), true);
+ metaElement->SetAttr(kNameSpaceID_None, nsGkAtoms::content,
+ NS_LITERAL_STRING("text/html;charset=") +
+ NS_ConvertASCIItoUTF16(characterSet),
+ true);
+ return NS_OK;
+}
+
+bool
+TextEditor::UpdateMetaCharset(nsIDOMDocument* aDocument,
+ const nsACString& aCharacterSet)
+{
+ MOZ_ASSERT(aDocument);
+ // get a list of META tags
+ nsCOMPtr<nsIDOMNodeList> list;
+ nsresult rv = aDocument->GetElementsByTagName(NS_LITERAL_STRING("meta"),
+ getter_AddRefs(list));
+ NS_ENSURE_SUCCESS(rv, false);
+ NS_ENSURE_TRUE(list, false);
+
+ nsCOMPtr<nsINodeList> metaList = do_QueryInterface(list);
+
+ uint32_t listLength = 0;
+ metaList->GetLength(&listLength);
+
+ for (uint32_t i = 0; i < listLength; ++i) {
+ nsCOMPtr<nsIContent> metaNode = metaList->Item(i);
+ MOZ_ASSERT(metaNode);
+
+ if (!metaNode->IsElement()) {
+ continue;
+ }
+
+ nsAutoString currentValue;
+ metaNode->GetAttr(kNameSpaceID_None, nsGkAtoms::httpEquiv, currentValue);
+
+ if (!FindInReadable(NS_LITERAL_STRING("content-type"),
+ currentValue,
+ nsCaseInsensitiveStringComparator())) {
+ continue;
+ }
+
+ metaNode->GetAttr(kNameSpaceID_None, nsGkAtoms::content, currentValue);
+
+ NS_NAMED_LITERAL_STRING(charsetEquals, "charset=");
+ nsAString::const_iterator originalStart, start, end;
+ originalStart = currentValue.BeginReading(start);
+ currentValue.EndReading(end);
+ if (!FindInReadable(charsetEquals, start, end,
+ nsCaseInsensitiveStringComparator())) {
+ continue;
+ }
+
+ // set attribute to <original prefix> charset=text/html
+ nsCOMPtr<nsIDOMElement> metaElement = do_QueryInterface(metaNode);
+ MOZ_ASSERT(metaElement);
+ rv = EditorBase::SetAttribute(metaElement, NS_LITERAL_STRING("content"),
+ Substring(originalStart, start) +
+ charsetEquals +
+ NS_ConvertASCIItoUTF16(aCharacterSet));
+ return NS_SUCCEEDED(rv);
+ }
+ return false;
+}
+
+NS_IMETHODIMP
+TextEditor::InitRules()
+{
+ if (!mRules) {
+ // instantiate the rules for this text editor
+ mRules = new TextEditRules();
+ }
+ return mRules->Init(this);
+}
+
+
+NS_IMETHODIMP
+TextEditor::GetIsDocumentEditable(bool* aIsDocumentEditable)
+{
+ NS_ENSURE_ARG_POINTER(aIsDocumentEditable);
+
+ nsCOMPtr<nsIDOMDocument> doc = GetDOMDocument();
+ *aIsDocumentEditable = doc && IsModifiable();
+
+ return NS_OK;
+}
+
+bool
+TextEditor::IsModifiable()
+{
+ return !IsReadonly();
+}
+
+nsresult
+TextEditor::HandleKeyPressEvent(nsIDOMKeyEvent* aKeyEvent)
+{
+ // NOTE: When you change this method, you should also change:
+ // * editor/libeditor/tests/test_texteditor_keyevent_handling.html
+ // * editor/libeditor/tests/test_htmleditor_keyevent_handling.html
+ //
+ // And also when you add new key handling, you need to change the subclass's
+ // HandleKeyPressEvent()'s switch statement.
+
+ if (IsReadonly() || IsDisabled()) {
+ // When we're not editable, the events handled on EditorBase.
+ return EditorBase::HandleKeyPressEvent(aKeyEvent);
+ }
+
+ WidgetKeyboardEvent* nativeKeyEvent =
+ aKeyEvent->AsEvent()->WidgetEventPtr()->AsKeyboardEvent();
+ NS_ENSURE_TRUE(nativeKeyEvent, NS_ERROR_UNEXPECTED);
+ NS_ASSERTION(nativeKeyEvent->mMessage == eKeyPress,
+ "HandleKeyPressEvent gets non-keypress event");
+
+ switch (nativeKeyEvent->mKeyCode) {
+ case NS_VK_META:
+ case NS_VK_WIN:
+ case NS_VK_SHIFT:
+ case NS_VK_CONTROL:
+ case NS_VK_ALT:
+ case NS_VK_BACK:
+ case NS_VK_DELETE:
+ // These keys are handled on EditorBase
+ return EditorBase::HandleKeyPressEvent(aKeyEvent);
+ case NS_VK_TAB: {
+ if (IsTabbable()) {
+ return NS_OK; // let it be used for focus switching
+ }
+
+ if (nativeKeyEvent->IsShift() || nativeKeyEvent->IsControl() ||
+ nativeKeyEvent->IsAlt() || nativeKeyEvent->IsMeta() ||
+ nativeKeyEvent->IsOS()) {
+ return NS_OK;
+ }
+
+ // else we insert the tab straight through
+ aKeyEvent->AsEvent()->PreventDefault();
+ return TypedText(NS_LITERAL_STRING("\t"), eTypedText);
+ }
+ case NS_VK_RETURN:
+ if (IsSingleLineEditor() || nativeKeyEvent->IsControl() ||
+ nativeKeyEvent->IsAlt() || nativeKeyEvent->IsMeta() ||
+ nativeKeyEvent->IsOS()) {
+ return NS_OK;
+ }
+ aKeyEvent->AsEvent()->PreventDefault();
+ return TypedText(EmptyString(), eTypedBreak);
+ }
+
+ // NOTE: On some keyboard layout, some characters are inputted with Control
+ // key or Alt key, but at that time, widget sets FALSE to these keys.
+ if (!nativeKeyEvent->mCharCode || nativeKeyEvent->IsControl() ||
+ nativeKeyEvent->IsAlt() || nativeKeyEvent->IsMeta() ||
+ nativeKeyEvent->IsOS()) {
+ // we don't PreventDefault() here or keybindings like control-x won't work
+ return NS_OK;
+ }
+ aKeyEvent->AsEvent()->PreventDefault();
+ nsAutoString str(nativeKeyEvent->mCharCode);
+ return TypedText(str, eTypedText);
+}
+
+/* This routine is needed to provide a bottleneck for typing for logging
+ purposes. Can't use HandleKeyPress() (above) for that since it takes
+ a nsIDOMKeyEvent* parameter. So instead we pass enough info through
+ to TypedText() to determine what action to take, but without passing
+ an event.
+ */
+NS_IMETHODIMP
+TextEditor::TypedText(const nsAString& aString, ETypingAction aAction)
+{
+ AutoPlaceHolderBatch batch(this, nsGkAtoms::TypingTxnName);
+
+ switch (aAction) {
+ case eTypedText:
+ return InsertText(aString);
+ case eTypedBreak:
+ return InsertLineBreak();
+ default:
+ // eTypedBR is only for HTML
+ return NS_ERROR_FAILURE;
+ }
+}
+
+already_AddRefed<Element>
+TextEditor::CreateBRImpl(nsCOMPtr<nsINode>* aInOutParent,
+ int32_t* aInOutOffset,
+ EDirection aSelect)
+{
+ nsCOMPtr<nsIDOMNode> parent(GetAsDOMNode(*aInOutParent));
+ nsCOMPtr<nsIDOMNode> br;
+ // We ignore the retval, and assume it's fine if the br is non-null
+ CreateBRImpl(address_of(parent), aInOutOffset, address_of(br), aSelect);
+ *aInOutParent = do_QueryInterface(parent);
+ nsCOMPtr<Element> ret(do_QueryInterface(br));
+ return ret.forget();
+}
+
+nsresult
+TextEditor::CreateBRImpl(nsCOMPtr<nsIDOMNode>* aInOutParent,
+ int32_t* aInOutOffset,
+ nsCOMPtr<nsIDOMNode>* outBRNode,
+ EDirection aSelect)
+{
+ NS_ENSURE_TRUE(aInOutParent && *aInOutParent && aInOutOffset && outBRNode, NS_ERROR_NULL_POINTER);
+ *outBRNode = nullptr;
+
+ // we need to insert a br. unfortunately, we may have to split a text node to do it.
+ nsCOMPtr<nsIDOMNode> node = *aInOutParent;
+ int32_t theOffset = *aInOutOffset;
+ nsCOMPtr<nsIDOMCharacterData> nodeAsText = do_QueryInterface(node);
+ NS_NAMED_LITERAL_STRING(brType, "br");
+ nsCOMPtr<nsIDOMNode> brNode;
+ if (nodeAsText) {
+ int32_t offset;
+ uint32_t len;
+ nodeAsText->GetLength(&len);
+ nsCOMPtr<nsIDOMNode> tmp = GetNodeLocation(node, &offset);
+ NS_ENSURE_TRUE(tmp, NS_ERROR_FAILURE);
+ if (!theOffset) {
+ // we are already set to go
+ } else if (theOffset == (int32_t)len) {
+ // update offset to point AFTER the text node
+ offset++;
+ } else {
+ // split the text node
+ nsresult rv = SplitNode(node, theOffset, getter_AddRefs(tmp));
+ NS_ENSURE_SUCCESS(rv, rv);
+ tmp = GetNodeLocation(node, &offset);
+ }
+ // create br
+ nsresult rv = CreateNode(brType, tmp, offset, getter_AddRefs(brNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+ *aInOutParent = tmp;
+ *aInOutOffset = offset+1;
+ } else {
+ nsresult rv = CreateNode(brType, node, theOffset, getter_AddRefs(brNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+ (*aInOutOffset)++;
+ }
+
+ *outBRNode = brNode;
+ if (*outBRNode && (aSelect != eNone)) {
+ int32_t offset;
+ nsCOMPtr<nsIDOMNode> parent = GetNodeLocation(*outBRNode, &offset);
+
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_STATE(selection);
+ if (aSelect == eNext) {
+ // position selection after br
+ selection->SetInterlinePosition(true);
+ selection->Collapse(parent, offset + 1);
+ } else if (aSelect == ePrevious) {
+ // position selection before br
+ selection->SetInterlinePosition(true);
+ selection->Collapse(parent, offset);
+ }
+ }
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+TextEditor::CreateBR(nsIDOMNode* aNode,
+ int32_t aOffset,
+ nsCOMPtr<nsIDOMNode>* outBRNode,
+ EDirection aSelect)
+{
+ nsCOMPtr<nsIDOMNode> parent = aNode;
+ int32_t offset = aOffset;
+ return CreateBRImpl(address_of(parent), &offset, outBRNode, aSelect);
+}
+
+nsresult
+TextEditor::InsertBR(nsCOMPtr<nsIDOMNode>* outBRNode)
+{
+ NS_ENSURE_TRUE(outBRNode, NS_ERROR_NULL_POINTER);
+ *outBRNode = nullptr;
+
+ // calling it text insertion to trigger moz br treatment by rules
+ AutoRules beginRulesSniffing(this, EditAction::insertText, nsIEditor::eNext);
+
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_STATE(selection);
+
+ if (!selection->Collapsed()) {
+ nsresult rv = DeleteSelection(nsIEditor::eNone, nsIEditor::eStrip);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsIDOMNode> selNode;
+ int32_t selOffset;
+ nsresult rv =
+ GetStartNodeAndOffset(selection, getter_AddRefs(selNode), &selOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = CreateBR(selNode, selOffset, outBRNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // position selection after br
+ selNode = GetNodeLocation(*outBRNode, &selOffset);
+ selection->SetInterlinePosition(true);
+ return selection->Collapse(selNode, selOffset+1);
+}
+
+nsresult
+TextEditor::ExtendSelectionForDelete(Selection* aSelection,
+ nsIEditor::EDirection* aAction)
+{
+ bool bCollapsed = aSelection->Collapsed();
+
+ if (*aAction == eNextWord ||
+ *aAction == ePreviousWord ||
+ (*aAction == eNext && bCollapsed) ||
+ (*aAction == ePrevious && bCollapsed) ||
+ *aAction == eToBeginningOfLine ||
+ *aAction == eToEndOfLine) {
+ nsCOMPtr<nsISelectionController> selCont;
+ GetSelectionController(getter_AddRefs(selCont));
+ NS_ENSURE_TRUE(selCont, NS_ERROR_NO_INTERFACE);
+
+ nsresult rv;
+ switch (*aAction) {
+ case eNextWord:
+ rv = selCont->WordExtendForDelete(true);
+ // DeleteSelectionImpl doesn't handle these actions
+ // because it's inside batching, so don't confuse it:
+ *aAction = eNone;
+ break;
+ case ePreviousWord:
+ rv = selCont->WordExtendForDelete(false);
+ *aAction = eNone;
+ break;
+ case eNext:
+ rv = selCont->CharacterExtendForDelete();
+ // Don't set aAction to eNone (see Bug 502259)
+ break;
+ case ePrevious: {
+ // Only extend the selection where the selection is after a UTF-16
+ // surrogate pair or a variation selector.
+ // For other cases we don't want to do that, in order
+ // to make sure that pressing backspace will only delete the last
+ // typed character.
+ nsCOMPtr<nsIDOMNode> node;
+ int32_t offset;
+ rv = GetStartNodeAndOffset(aSelection, getter_AddRefs(node), &offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(node, NS_ERROR_FAILURE);
+
+ // node might be anonymous DIV, so we find better text node
+ FindBetterInsertionPoint(node, offset);
+
+ if (IsTextNode(node)) {
+ nsCOMPtr<nsIDOMCharacterData> charData = do_QueryInterface(node);
+ if (charData) {
+ nsAutoString data;
+ rv = charData->GetData(data);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if ((offset > 1 &&
+ NS_IS_LOW_SURROGATE(data[offset - 1]) &&
+ NS_IS_HIGH_SURROGATE(data[offset - 2])) ||
+ (offset > 0 &&
+ gfxFontUtils::IsVarSelector(data[offset - 1]))) {
+ rv = selCont->CharacterExtendForBackspace();
+ }
+ }
+ }
+ break;
+ }
+ case eToBeginningOfLine:
+ selCont->IntraLineMove(true, false); // try to move to end
+ rv = selCont->IntraLineMove(false, true); // select to beginning
+ *aAction = eNone;
+ break;
+ case eToEndOfLine:
+ rv = selCont->IntraLineMove(true, true);
+ *aAction = eNext;
+ break;
+ default: // avoid several compiler warnings
+ rv = NS_OK;
+ break;
+ }
+ return rv;
+ }
+ return NS_OK;
+}
+
+nsresult
+TextEditor::DeleteSelection(EDirection aAction,
+ EStripWrappers aStripWrappers)
+{
+ MOZ_ASSERT(aStripWrappers == eStrip || aStripWrappers == eNoStrip);
+
+ if (!mRules) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+
+ // delete placeholder txns merge.
+ AutoPlaceHolderBatch batch(this, nsGkAtoms::DeleteTxnName);
+ AutoRules beginRulesSniffing(this, EditAction::deleteSelection, aAction);
+
+ // pre-process
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+
+ // If there is an existing selection when an extended delete is requested,
+ // platforms that use "caret-style" caret positioning collapse the
+ // selection to the start and then create a new selection.
+ // Platforms that use "selection-style" caret positioning just delete the
+ // existing selection without extending it.
+ if (!selection->Collapsed() &&
+ (aAction == eNextWord || aAction == ePreviousWord ||
+ aAction == eToBeginningOfLine || aAction == eToEndOfLine)) {
+ if (mCaretStyle == 1) {
+ nsresult rv = selection->CollapseToStart();
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ aAction = eNone;
+ }
+ }
+
+ TextRulesInfo ruleInfo(EditAction::deleteSelection);
+ ruleInfo.collapsedAction = aAction;
+ ruleInfo.stripWrappers = aStripWrappers;
+ bool cancel, handled;
+ nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!cancel && !handled) {
+ rv = DeleteSelectionImpl(aAction, aStripWrappers);
+ }
+ if (!cancel) {
+ // post-process
+ rv = rules->DidDoAction(selection, &ruleInfo, rv);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+TextEditor::InsertText(const nsAString& aStringToInsert)
+{
+ if (!mRules) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+
+ EditAction opID = EditAction::insertText;
+ if (ShouldHandleIMEComposition()) {
+ opID = EditAction::insertIMEText;
+ }
+ AutoPlaceHolderBatch batch(this, nullptr);
+ AutoRules beginRulesSniffing(this, opID, nsIEditor::eNext);
+
+ // pre-process
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+ nsAutoString resultString;
+ // XXX can we trust instring to outlive ruleInfo,
+ // XXX and ruleInfo not to refer to instring in its dtor?
+ //nsAutoString instring(aStringToInsert);
+ TextRulesInfo ruleInfo(opID);
+ ruleInfo.inString = &aStringToInsert;
+ ruleInfo.outString = &resultString;
+ ruleInfo.maxLength = mMaxTextLength;
+
+ bool cancel, handled;
+ nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!cancel && !handled) {
+ // we rely on rules code for now - no default implementation
+ }
+ if (cancel) {
+ return NS_OK;
+ }
+ // post-process
+ return rules->DidDoAction(selection, &ruleInfo, rv);
+}
+
+NS_IMETHODIMP
+TextEditor::InsertLineBreak()
+{
+ if (!mRules) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+
+ AutoEditBatch beginBatching(this);
+ AutoRules beginRulesSniffing(this, EditAction::insertBreak, nsIEditor::eNext);
+
+ // pre-process
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+
+ TextRulesInfo ruleInfo(EditAction::insertBreak);
+ ruleInfo.maxLength = mMaxTextLength;
+ bool cancel, handled;
+ nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!cancel && !handled) {
+ // get the (collapsed) selection location
+ NS_ENSURE_STATE(selection->GetRangeAt(0));
+ nsCOMPtr<nsINode> selNode = selection->GetRangeAt(0)->GetStartParent();
+ int32_t selOffset = selection->GetRangeAt(0)->StartOffset();
+ NS_ENSURE_STATE(selNode);
+
+ // don't put text in places that can't have it
+ if (!IsTextNode(selNode) && !CanContainTag(*selNode,
+ *nsGkAtoms::textTagName)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // we need to get the doc
+ nsCOMPtr<nsIDocument> doc = GetDocument();
+ NS_ENSURE_TRUE(doc, NS_ERROR_NOT_INITIALIZED);
+
+ // don't spaz my selection in subtransactions
+ AutoTransactionsConserveSelection dontSpazMySelection(this);
+
+ // insert a linefeed character
+ rv = InsertTextImpl(NS_LITERAL_STRING("\n"), address_of(selNode),
+ &selOffset, doc);
+ if (!selNode) {
+ rv = NS_ERROR_NULL_POINTER; // don't return here, so DidDoAction is called
+ }
+ if (NS_SUCCEEDED(rv)) {
+ // set the selection to the correct location
+ rv = selection->Collapse(selNode, selOffset);
+ if (NS_SUCCEEDED(rv)) {
+ // see if we're at the end of the editor range
+ nsCOMPtr<nsIDOMNode> endNode;
+ int32_t endOffset;
+ rv = GetEndNodeAndOffset(selection,
+ getter_AddRefs(endNode), &endOffset);
+
+ if (NS_SUCCEEDED(rv) &&
+ endNode == GetAsDOMNode(selNode) && endOffset == selOffset) {
+ // SetInterlinePosition(true) means we want the caret to stick to the content on the "right".
+ // We want the caret to stick to whatever is past the break. This is
+ // because the break is on the same line we were on, but the next content
+ // will be on the following line.
+ selection->SetInterlinePosition(true);
+ }
+ }
+ }
+ }
+
+ if (!cancel) {
+ // post-process, always called if WillInsertBreak didn't return cancel==true
+ rv = rules->DidDoAction(selection, &ruleInfo, rv);
+ }
+ return rv;
+}
+
+nsresult
+TextEditor::BeginIMEComposition(WidgetCompositionEvent* aEvent)
+{
+ NS_ENSURE_TRUE(!mComposition, NS_OK);
+
+ if (IsPasswordEditor()) {
+ NS_ENSURE_TRUE(mRules, NS_ERROR_NULL_POINTER);
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+
+ TextEditRules* textEditRules = static_cast<TextEditRules*>(rules.get());
+ textEditRules->ResetIMETextPWBuf();
+ }
+
+ return EditorBase::BeginIMEComposition(aEvent);
+}
+
+nsresult
+TextEditor::UpdateIMEComposition(nsIDOMEvent* aDOMTextEvent)
+{
+ MOZ_ASSERT(aDOMTextEvent, "aDOMTextEvent must not be nullptr");
+
+ WidgetCompositionEvent* compositionChangeEvent =
+ aDOMTextEvent->WidgetEventPtr()->AsCompositionEvent();
+ NS_ENSURE_TRUE(compositionChangeEvent, NS_ERROR_INVALID_ARG);
+ MOZ_ASSERT(compositionChangeEvent->mMessage == eCompositionChange,
+ "The internal event should be eCompositionChange");
+
+ if (!EnsureComposition(compositionChangeEvent)) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIPresShell> ps = GetPresShell();
+ NS_ENSURE_TRUE(ps, NS_ERROR_NOT_INITIALIZED);
+
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_STATE(selection);
+
+ // NOTE: TextComposition should receive selection change notification before
+ // CompositionChangeEventHandlingMarker notifies TextComposition of the
+ // end of handling compositionchange event because TextComposition may
+ // need to ignore selection changes caused by composition. Therefore,
+ // CompositionChangeEventHandlingMarker must be destroyed after a call
+ // of NotifiyEditorObservers(eNotifyEditorObserversOfEnd) or
+ // NotifiyEditorObservers(eNotifyEditorObserversOfCancel) which notifies
+ // TextComposition of a selection change.
+ MOZ_ASSERT(!mPlaceHolderBatch,
+ "UpdateIMEComposition() must be called without place holder batch");
+ TextComposition::CompositionChangeEventHandlingMarker
+ compositionChangeEventHandlingMarker(mComposition, compositionChangeEvent);
+
+ NotifyEditorObservers(eNotifyEditorObserversOfBefore);
+
+ RefPtr<nsCaret> caretP = ps->GetCaret();
+
+ nsresult rv;
+ {
+ AutoPlaceHolderBatch batch(this, nsGkAtoms::IMETxnName);
+
+ rv = InsertText(compositionChangeEvent->mData);
+
+ if (caretP) {
+ caretP->SetSelection(selection);
+ }
+ }
+
+ // If still composing, we should fire input event via observer.
+ // Note that if the composition will be committed by the following
+ // compositionend event, we don't need to notify editor observes of this
+ // change.
+ // NOTE: We must notify after the auto batch will be gone.
+ if (!compositionChangeEvent->IsFollowedByCompositionEnd()) {
+ NotifyEditorObservers(eNotifyEditorObserversOfEnd);
+ }
+
+ return rv;
+}
+
+already_AddRefed<nsIContent>
+TextEditor::GetInputEventTargetContent()
+{
+ nsCOMPtr<nsIContent> target = do_QueryInterface(mEventTarget);
+ return target.forget();
+}
+
+NS_IMETHODIMP
+TextEditor::GetDocumentIsEmpty(bool* aDocumentIsEmpty)
+{
+ NS_ENSURE_TRUE(aDocumentIsEmpty, NS_ERROR_NULL_POINTER);
+
+ NS_ENSURE_TRUE(mRules, NS_ERROR_NOT_INITIALIZED);
+
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+
+ return rules->DocumentIsEmpty(aDocumentIsEmpty);
+}
+
+NS_IMETHODIMP
+TextEditor::GetTextLength(int32_t* aCount)
+{
+ NS_ASSERTION(aCount, "null pointer");
+
+ // initialize out params
+ *aCount = 0;
+
+ // special-case for empty document, to account for the bogus node
+ bool docEmpty;
+ nsresult rv = GetDocumentIsEmpty(&docEmpty);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (docEmpty) {
+ return NS_OK;
+ }
+
+ dom::Element *rootElement = GetRoot();
+ NS_ENSURE_TRUE(rootElement, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsIContentIterator> iter =
+ do_CreateInstance("@mozilla.org/content/post-content-iterator;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t totalLength = 0;
+ iter->Init(rootElement);
+ for (; !iter->IsDone(); iter->Next()) {
+ nsCOMPtr<nsIDOMNode> currentNode = do_QueryInterface(iter->GetCurrentNode());
+ nsCOMPtr<nsIDOMCharacterData> textNode = do_QueryInterface(currentNode);
+ if (textNode && IsEditable(currentNode)) {
+ uint32_t length;
+ textNode->GetLength(&length);
+ totalLength += length;
+ }
+ }
+
+ *aCount = totalLength;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextEditor::SetMaxTextLength(int32_t aMaxTextLength)
+{
+ mMaxTextLength = aMaxTextLength;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextEditor::GetMaxTextLength(int32_t* aMaxTextLength)
+{
+ NS_ENSURE_TRUE(aMaxTextLength, NS_ERROR_INVALID_POINTER);
+ *aMaxTextLength = mMaxTextLength;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextEditor::GetWrapWidth(int32_t* aWrapColumn)
+{
+ NS_ENSURE_TRUE( aWrapColumn, NS_ERROR_NULL_POINTER);
+
+ *aWrapColumn = mWrapColumn;
+ return NS_OK;
+}
+
+//
+// See if the style value includes this attribute, and if it does,
+// cut out everything from the attribute to the next semicolon.
+//
+static void CutStyle(const char* stylename, nsString& styleValue)
+{
+ // Find the current wrapping type:
+ int32_t styleStart = styleValue.Find(stylename, true);
+ if (styleStart >= 0) {
+ int32_t styleEnd = styleValue.Find(";", false, styleStart);
+ if (styleEnd > styleStart) {
+ styleValue.Cut(styleStart, styleEnd - styleStart + 1);
+ } else {
+ styleValue.Cut(styleStart, styleValue.Length() - styleStart);
+ }
+ }
+}
+
+NS_IMETHODIMP
+TextEditor::SetWrapWidth(int32_t aWrapColumn)
+{
+ SetWrapColumn(aWrapColumn);
+
+ // Make sure we're a plaintext editor, otherwise we shouldn't
+ // do the rest of this.
+ if (!IsPlaintextEditor()) {
+ return NS_OK;
+ }
+
+ // Ought to set a style sheet here ...
+ // Probably should keep around an mPlaintextStyleSheet for this purpose.
+ dom::Element *rootElement = GetRoot();
+ NS_ENSURE_TRUE(rootElement, NS_ERROR_NULL_POINTER);
+
+ // Get the current style for this root element:
+ nsAutoString styleValue;
+ rootElement->GetAttr(kNameSpaceID_None, nsGkAtoms::style, styleValue);
+
+ // We'll replace styles for these values:
+ CutStyle("white-space", styleValue);
+ CutStyle("width", styleValue);
+ CutStyle("font-family", styleValue);
+
+ // If we have other style left, trim off any existing semicolons
+ // or whitespace, then add a known semicolon-space:
+ if (!styleValue.IsEmpty()) {
+ styleValue.Trim("; \t", false, true);
+ styleValue.AppendLiteral("; ");
+ }
+
+ // Make sure we have fixed-width font. This should be done for us,
+ // but it isn't, see bug 22502, so we have to add "font: -moz-fixed;".
+ // Only do this if we're wrapping.
+ if (IsWrapHackEnabled() && aWrapColumn >= 0) {
+ styleValue.AppendLiteral("font-family: -moz-fixed; ");
+ }
+
+ // and now we're ready to set the new whitespace/wrapping style.
+ if (aWrapColumn > 0) {
+ // Wrap to a fixed column.
+ styleValue.AppendLiteral("white-space: pre-wrap; width: ");
+ styleValue.AppendInt(aWrapColumn);
+ styleValue.AppendLiteral("ch;");
+ } else if (!aWrapColumn) {
+ styleValue.AppendLiteral("white-space: pre-wrap;");
+ } else {
+ styleValue.AppendLiteral("white-space: pre;");
+ }
+
+ return rootElement->SetAttr(kNameSpaceID_None, nsGkAtoms::style, styleValue, true);
+}
+
+NS_IMETHODIMP
+TextEditor::SetWrapColumn(int32_t aWrapColumn)
+{
+ mWrapColumn = aWrapColumn;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextEditor::GetNewlineHandling(int32_t* aNewlineHandling)
+{
+ NS_ENSURE_ARG_POINTER(aNewlineHandling);
+
+ *aNewlineHandling = mNewlineHandling;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextEditor::SetNewlineHandling(int32_t aNewlineHandling)
+{
+ mNewlineHandling = aNewlineHandling;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextEditor::Undo(uint32_t aCount)
+{
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+
+ AutoUpdateViewBatch beginViewBatching(this);
+
+ ForceCompositionEnd();
+
+ NotifyEditorObservers(eNotifyEditorObserversOfBefore);
+
+ AutoRules beginRulesSniffing(this, EditAction::undo, nsIEditor::eNone);
+
+ TextRulesInfo ruleInfo(EditAction::undo);
+ RefPtr<Selection> selection = GetSelection();
+ bool cancel, handled;
+ nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
+
+ if (!cancel && NS_SUCCEEDED(rv)) {
+ rv = EditorBase::Undo(aCount);
+ rv = rules->DidDoAction(selection, &ruleInfo, rv);
+ }
+
+ NotifyEditorObservers(eNotifyEditorObserversOfEnd);
+ return rv;
+}
+
+NS_IMETHODIMP
+TextEditor::Redo(uint32_t aCount)
+{
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+
+ AutoUpdateViewBatch beginViewBatching(this);
+
+ ForceCompositionEnd();
+
+ NotifyEditorObservers(eNotifyEditorObserversOfBefore);
+
+ AutoRules beginRulesSniffing(this, EditAction::redo, nsIEditor::eNone);
+
+ TextRulesInfo ruleInfo(EditAction::redo);
+ RefPtr<Selection> selection = GetSelection();
+ bool cancel, handled;
+ nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
+
+ if (!cancel && NS_SUCCEEDED(rv)) {
+ rv = EditorBase::Redo(aCount);
+ rv = rules->DidDoAction(selection, &ruleInfo, rv);
+ }
+
+ NotifyEditorObservers(eNotifyEditorObserversOfEnd);
+ return rv;
+}
+
+bool
+TextEditor::CanCutOrCopy(PasswordFieldAllowed aPasswordFieldAllowed)
+{
+ RefPtr<Selection> selection = GetSelection();
+ if (!selection) {
+ return false;
+ }
+
+ if (aPasswordFieldAllowed == ePasswordFieldNotAllowed &&
+ IsPasswordEditor()) {
+ return false;
+ }
+
+ return !selection->Collapsed();
+}
+
+bool
+TextEditor::FireClipboardEvent(EventMessage aEventMessage,
+ int32_t aSelectionType,
+ bool* aActionTaken)
+{
+ if (aEventMessage == ePaste) {
+ ForceCompositionEnd();
+ }
+
+ nsCOMPtr<nsIPresShell> presShell = GetPresShell();
+ NS_ENSURE_TRUE(presShell, false);
+
+ RefPtr<Selection> selection = GetSelection();
+ if (!selection) {
+ return false;
+ }
+
+ if (!nsCopySupport::FireClipboardEvent(aEventMessage, aSelectionType,
+ presShell, selection, aActionTaken)) {
+ return false;
+ }
+
+ // If the event handler caused the editor to be destroyed, return false.
+ // Otherwise return true to indicate that the event was not cancelled.
+ return !mDidPreDestroy;
+}
+
+NS_IMETHODIMP
+TextEditor::Cut()
+{
+ bool actionTaken = false;
+ if (FireClipboardEvent(eCut, nsIClipboard::kGlobalClipboard, &actionTaken)) {
+ DeleteSelection(eNone, eStrip);
+ }
+ return actionTaken ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+TextEditor::CanCut(bool* aCanCut)
+{
+ NS_ENSURE_ARG_POINTER(aCanCut);
+ // Cut is always enabled in HTML documents
+ nsCOMPtr<nsIDocument> doc = GetDocument();
+ *aCanCut = (doc && doc->IsHTMLOrXHTML()) ||
+ (IsModifiable() && CanCutOrCopy(ePasswordFieldNotAllowed));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextEditor::Copy()
+{
+ bool actionTaken = false;
+ FireClipboardEvent(eCopy, nsIClipboard::kGlobalClipboard, &actionTaken);
+
+ return actionTaken ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+TextEditor::CanCopy(bool* aCanCopy)
+{
+ NS_ENSURE_ARG_POINTER(aCanCopy);
+ // Copy is always enabled in HTML documents
+ nsCOMPtr<nsIDocument> doc = GetDocument();
+ *aCanCopy = (doc && doc->IsHTMLOrXHTML()) ||
+ CanCutOrCopy(ePasswordFieldNotAllowed);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextEditor::CanDelete(bool* aCanDelete)
+{
+ NS_ENSURE_ARG_POINTER(aCanDelete);
+ *aCanDelete = IsModifiable() && CanCutOrCopy(ePasswordFieldAllowed);
+ return NS_OK;
+}
+
+// Shared between OutputToString and OutputToStream
+NS_IMETHODIMP
+TextEditor::GetAndInitDocEncoder(const nsAString& aFormatType,
+ uint32_t aFlags,
+ const nsACString& aCharset,
+ nsIDocumentEncoder** encoder)
+{
+ nsresult rv = NS_OK;
+
+ nsAutoCString formatType(NS_DOC_ENCODER_CONTRACTID_BASE);
+ LossyAppendUTF16toASCII(aFormatType, formatType);
+ nsCOMPtr<nsIDocumentEncoder> docEncoder (do_CreateInstance(formatType.get(), &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDOMDocument> domDoc = do_QueryReferent(mDocWeak);
+ NS_ASSERTION(domDoc, "Need a document");
+
+ rv = docEncoder->Init(domDoc, aFormatType, aFlags);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!aCharset.IsEmpty() && !aCharset.EqualsLiteral("null")) {
+ docEncoder->SetCharset(aCharset);
+ }
+
+ int32_t wc;
+ (void) GetWrapWidth(&wc);
+ if (wc >= 0) {
+ (void) docEncoder->SetWrapColumn(wc);
+ }
+
+ // Set the selection, if appropriate.
+ // We do this either if the OutputSelectionOnly flag is set,
+ // in which case we use our existing selection ...
+ if (aFlags & nsIDocumentEncoder::OutputSelectionOnly) {
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_STATE(selection);
+ rv = docEncoder->SetSelection(selection);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // ... or if the root element is not a body,
+ // in which case we set the selection to encompass the root.
+ else {
+ dom::Element* rootElement = GetRoot();
+ NS_ENSURE_TRUE(rootElement, NS_ERROR_FAILURE);
+ if (!rootElement->IsHTMLElement(nsGkAtoms::body)) {
+ rv = docEncoder->SetNativeContainerNode(rootElement);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ docEncoder.forget(encoder);
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+TextEditor::OutputToString(const nsAString& aFormatType,
+ uint32_t aFlags,
+ nsAString& aOutputString)
+{
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+
+ nsString resultString;
+ TextRulesInfo ruleInfo(EditAction::outputText);
+ ruleInfo.outString = &resultString;
+ // XXX Struct should store a nsAReadable*
+ nsAutoString str(aFormatType);
+ ruleInfo.outputFormat = &str;
+ bool cancel, handled;
+ nsresult rv = rules->WillDoAction(nullptr, &ruleInfo, &cancel, &handled);
+ if (cancel || NS_FAILED(rv)) {
+ return rv;
+ }
+ if (handled) {
+ // This case will get triggered by password fields.
+ aOutputString.Assign(*(ruleInfo.outString));
+ return rv;
+ }
+
+ nsAutoCString charsetStr;
+ rv = GetDocumentCharacterSet(charsetStr);
+ if (NS_FAILED(rv) || charsetStr.IsEmpty()) {
+ charsetStr.AssignLiteral("ISO-8859-1");
+ }
+
+ nsCOMPtr<nsIDocumentEncoder> encoder;
+ rv = GetAndInitDocEncoder(aFormatType, aFlags, charsetStr, getter_AddRefs(encoder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return encoder->EncodeToString(aOutputString);
+}
+
+NS_IMETHODIMP
+TextEditor::OutputToStream(nsIOutputStream* aOutputStream,
+ const nsAString& aFormatType,
+ const nsACString& aCharset,
+ uint32_t aFlags)
+{
+ nsresult rv;
+
+ // special-case for empty document when requesting plain text,
+ // to account for the bogus text node.
+ // XXX Should there be a similar test in OutputToString?
+ if (aFormatType.EqualsLiteral("text/plain")) {
+ bool docEmpty;
+ rv = GetDocumentIsEmpty(&docEmpty);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (docEmpty) {
+ return NS_OK; // Output nothing.
+ }
+ }
+
+ nsCOMPtr<nsIDocumentEncoder> encoder;
+ rv = GetAndInitDocEncoder(aFormatType, aFlags, aCharset,
+ getter_AddRefs(encoder));
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return encoder->EncodeToStream(aOutputStream);
+}
+
+NS_IMETHODIMP
+TextEditor::InsertTextWithQuotations(const nsAString& aStringToInsert)
+{
+ return InsertText(aStringToInsert);
+}
+
+NS_IMETHODIMP
+TextEditor::PasteAsQuotation(int32_t aSelectionType)
+{
+ // Get Clipboard Service
+ nsresult rv;
+ nsCOMPtr<nsIClipboard> clipboard(do_GetService("@mozilla.org/widget/clipboard;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the nsITransferable interface for getting the data from the clipboard
+ nsCOMPtr<nsITransferable> trans;
+ rv = PrepareTransferable(getter_AddRefs(trans));
+ if (NS_SUCCEEDED(rv) && trans) {
+ // Get the Data from the clipboard
+ clipboard->GetData(trans, aSelectionType);
+
+ // Now we ask the transferable for the data
+ // it still owns the data, we just have a pointer to it.
+ // If it can't support a "text" output of the data the call will fail
+ nsCOMPtr<nsISupports> genericDataObj;
+ uint32_t len;
+ nsAutoCString flav;
+ rv = trans->GetAnyTransferData(flav, getter_AddRefs(genericDataObj),
+ &len);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (flav.EqualsLiteral(kUnicodeMime) ||
+ flav.EqualsLiteral(kMozTextInternal)) {
+ nsCOMPtr<nsISupportsString> textDataObj ( do_QueryInterface(genericDataObj) );
+ if (textDataObj && len > 0) {
+ nsAutoString stuffToPaste;
+ textDataObj->GetData ( stuffToPaste );
+ AutoEditBatch beginBatching(this);
+ rv = InsertAsQuotation(stuffToPaste, 0);
+ }
+ }
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+TextEditor::InsertAsQuotation(const nsAString& aQuotedText,
+ nsIDOMNode** aNodeInserted)
+{
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+
+ // Let the citer quote it for us:
+ nsString quotedStuff;
+ nsresult rv = InternetCiter::GetCiteString(aQuotedText, quotedStuff);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // It's best to put a blank line after the quoted text so that mails
+ // written without thinking won't be so ugly.
+ if (!aQuotedText.IsEmpty() && (aQuotedText.Last() != char16_t('\n'))) {
+ quotedStuff.Append(char16_t('\n'));
+ }
+
+ // get selection
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+
+ AutoEditBatch beginBatching(this);
+ AutoRules beginRulesSniffing(this, EditAction::insertText, nsIEditor::eNext);
+
+ // give rules a chance to handle or cancel
+ TextRulesInfo ruleInfo(EditAction::insertElement);
+ bool cancel, handled;
+ rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (cancel) {
+ return NS_OK; // Rules canceled the operation.
+ }
+ if (!handled) {
+ rv = InsertText(quotedStuff);
+
+ // XXX Should set *aNodeInserted to the first node inserted
+ if (aNodeInserted && NS_SUCCEEDED(rv)) {
+ *aNodeInserted = nullptr;
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+TextEditor::PasteAsCitedQuotation(const nsAString& aCitation,
+ int32_t aSelectionType)
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+TextEditor::InsertAsCitedQuotation(const nsAString& aQuotedText,
+ const nsAString& aCitation,
+ bool aInsertHTML,
+ nsIDOMNode** aNodeInserted)
+{
+ return InsertAsQuotation(aQuotedText, aNodeInserted);
+}
+
+nsresult
+TextEditor::SharedOutputString(uint32_t aFlags,
+ bool* aIsCollapsed,
+ nsAString& aResult)
+{
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NOT_INITIALIZED);
+
+ *aIsCollapsed = selection->Collapsed();
+
+ if (!*aIsCollapsed) {
+ aFlags |= nsIDocumentEncoder::OutputSelectionOnly;
+ }
+ // If the selection isn't collapsed, we'll use the whole document.
+
+ return OutputToString(NS_LITERAL_STRING("text/plain"), aFlags, aResult);
+}
+
+NS_IMETHODIMP
+TextEditor::Rewrap(bool aRespectNewlines)
+{
+ int32_t wrapCol;
+ nsresult rv = GetWrapWidth(&wrapCol);
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ // Rewrap makes no sense if there's no wrap column; default to 72.
+ if (wrapCol <= 0) {
+ wrapCol = 72;
+ }
+
+ nsAutoString current;
+ bool isCollapsed;
+ rv = SharedOutputString(nsIDocumentEncoder::OutputFormatted
+ | nsIDocumentEncoder::OutputLFLineBreak,
+ &isCollapsed, current);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString wrapped;
+ uint32_t firstLineOffset = 0; // XXX need to reset this if there is a selection
+ rv = InternetCiter::Rewrap(current, wrapCol, firstLineOffset,
+ aRespectNewlines, wrapped);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (isCollapsed) {
+ SelectAll();
+ }
+
+ return InsertTextWithQuotations(wrapped);
+}
+
+NS_IMETHODIMP
+TextEditor::StripCites()
+{
+ nsAutoString current;
+ bool isCollapsed;
+ nsresult rv = SharedOutputString(nsIDocumentEncoder::OutputFormatted,
+ &isCollapsed, current);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString stripped;
+ rv = InternetCiter::StripCites(current, stripped);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (isCollapsed) {
+ rv = SelectAll();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return InsertText(stripped);
+}
+
+NS_IMETHODIMP
+TextEditor::GetEmbeddedObjects(nsIArray** aNodeList)
+{
+ if (NS_WARN_IF(!aNodeList)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ *aNodeList = nullptr;
+ return NS_OK;
+}
+
+/**
+ * All editor operations which alter the doc should be prefaced
+ * with a call to StartOperation, naming the action and direction.
+ */
+NS_IMETHODIMP
+TextEditor::StartOperation(EditAction opID,
+ nsIEditor::EDirection aDirection)
+{
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+
+ EditorBase::StartOperation(opID, aDirection); // will set mAction, mDirection
+ if (rules) {
+ return rules->BeforeEdit(mAction, mDirection);
+ }
+ return NS_OK;
+}
+
+/**
+ * All editor operations which alter the doc should be followed
+ * with a call to EndOperation.
+ */
+NS_IMETHODIMP
+TextEditor::EndOperation()
+{
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+
+ // post processing
+ nsresult rv = rules ? rules->AfterEdit(mAction, mDirection) : NS_OK;
+ EditorBase::EndOperation(); // will clear mAction, mDirection
+ return rv;
+}
+
+nsresult
+TextEditor::SelectEntireDocument(Selection* aSelection)
+{
+ if (!aSelection || !mRules) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+
+ // is doc empty?
+ bool bDocIsEmpty;
+ if (NS_SUCCEEDED(rules->DocumentIsEmpty(&bDocIsEmpty)) && bDocIsEmpty) {
+ // get root node
+ nsCOMPtr<nsIDOMElement> rootElement = do_QueryInterface(GetRoot());
+ NS_ENSURE_TRUE(rootElement, NS_ERROR_FAILURE);
+
+ // if it's empty don't select entire doc - that would select the bogus node
+ return aSelection->Collapse(rootElement, 0);
+ }
+
+ SelectionBatcher selectionBatcher(aSelection);
+ nsresult rv = EditorBase::SelectEntireDocument(aSelection);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Don't select the trailing BR node if we have one
+ int32_t selOffset;
+ nsCOMPtr<nsIDOMNode> selNode;
+ rv = GetEndNodeAndOffset(aSelection, getter_AddRefs(selNode), &selOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDOMNode> childNode = GetChildAt(selNode, selOffset - 1);
+
+ if (childNode && TextEditUtils::IsMozBR(childNode)) {
+ int32_t parentOffset;
+ nsCOMPtr<nsIDOMNode> parentNode = GetNodeLocation(childNode, &parentOffset);
+
+ return aSelection->Extend(parentNode, parentOffset);
+ }
+
+ return NS_OK;
+}
+
+already_AddRefed<EventTarget>
+TextEditor::GetDOMEventTarget()
+{
+ nsCOMPtr<EventTarget> copy = mEventTarget;
+ return copy.forget();
+}
+
+
+nsresult
+TextEditor::SetAttributeOrEquivalent(nsIDOMElement* aElement,
+ const nsAString& aAttribute,
+ const nsAString& aValue,
+ bool aSuppressTransaction)
+{
+ return EditorBase::SetAttribute(aElement, aAttribute, aValue);
+}
+
+nsresult
+TextEditor::RemoveAttributeOrEquivalent(nsIDOMElement* aElement,
+ const nsAString& aAttribute,
+ bool aSuppressTransaction)
+{
+ return EditorBase::RemoveAttribute(aElement, aAttribute);
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/TextEditor.h b/editor/libeditor/TextEditor.h
new file mode 100644
index 000000000..872cd91d3
--- /dev/null
+++ b/editor/libeditor/TextEditor.h
@@ -0,0 +1,246 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_TextEditor_h
+#define mozilla_TextEditor_h
+
+#include "mozilla/EditorBase.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIEditor.h"
+#include "nsIEditorMailSupport.h"
+#include "nsIPlaintextEditor.h"
+#include "nsISupportsImpl.h"
+#include "nscore.h"
+
+class nsIContent;
+class nsIDOMDocument;
+class nsIDOMElement;
+class nsIDOMEvent;
+class nsIDOMKeyEvent;
+class nsIDOMNode;
+class nsIDocumentEncoder;
+class nsIEditRules;
+class nsIOutputStream;
+class nsISelectionController;
+class nsITransferable;
+
+namespace mozilla {
+
+class AutoEditInitRulesTrigger;
+class HTMLEditRules;
+class TextEditRules;
+namespace dom {
+class Selection;
+} // namespace dom
+
+/**
+ * The text editor implementation.
+ * Use to edit text document represented as a DOM tree.
+ */
+class TextEditor : public EditorBase
+ , public nsIPlaintextEditor
+ , public nsIEditorMailSupport
+{
+public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(TextEditor, EditorBase)
+
+ enum ETypingAction
+ {
+ eTypedText, /* user typed text */
+ eTypedBR, /* user typed shift-enter to get a br */
+ eTypedBreak /* user typed enter */
+ };
+
+ TextEditor();
+
+ // nsIPlaintextEditor methods
+ NS_DECL_NSIPLAINTEXTEDITOR
+
+ // nsIEditorMailSupport overrides
+ NS_DECL_NSIEDITORMAILSUPPORT
+
+ // Overrides of EditorBase interface methods
+ NS_IMETHOD SetAttributeOrEquivalent(nsIDOMElement* aElement,
+ const nsAString& aAttribute,
+ const nsAString& aValue,
+ bool aSuppressTransaction) override;
+ NS_IMETHOD RemoveAttributeOrEquivalent(nsIDOMElement* aElement,
+ const nsAString& aAttribute,
+ bool aSuppressTransaction) override;
+
+ NS_IMETHOD Init(nsIDOMDocument* aDoc, nsIContent* aRoot,
+ nsISelectionController* aSelCon, uint32_t aFlags,
+ const nsAString& aValue) override;
+
+ NS_IMETHOD GetDocumentIsEmpty(bool* aDocumentIsEmpty) override;
+ NS_IMETHOD GetIsDocumentEditable(bool* aIsDocumentEditable) override;
+
+ NS_IMETHOD DeleteSelection(EDirection aAction,
+ EStripWrappers aStripWrappers) override;
+
+ NS_IMETHOD SetDocumentCharacterSet(const nsACString& characterSet) override;
+
+ NS_IMETHOD Undo(uint32_t aCount) override;
+ NS_IMETHOD Redo(uint32_t aCount) override;
+
+ NS_IMETHOD Cut() override;
+ NS_IMETHOD CanCut(bool* aCanCut) override;
+ NS_IMETHOD Copy() override;
+ NS_IMETHOD CanCopy(bool* aCanCopy) override;
+ NS_IMETHOD CanDelete(bool* aCanDelete) override;
+ NS_IMETHOD Paste(int32_t aSelectionType) override;
+ NS_IMETHOD CanPaste(int32_t aSelectionType, bool* aCanPaste) override;
+ NS_IMETHOD PasteTransferable(nsITransferable* aTransferable) override;
+ NS_IMETHOD CanPasteTransferable(nsITransferable* aTransferable,
+ bool* aCanPaste) override;
+
+ NS_IMETHOD OutputToString(const nsAString& aFormatType,
+ uint32_t aFlags,
+ nsAString& aOutputString) override;
+
+ NS_IMETHOD OutputToStream(nsIOutputStream* aOutputStream,
+ const nsAString& aFormatType,
+ const nsACString& aCharsetOverride,
+ uint32_t aFlags) override;
+
+ /**
+ * All editor operations which alter the doc should be prefaced
+ * with a call to StartOperation, naming the action and direction.
+ */
+ NS_IMETHOD StartOperation(EditAction opID,
+ nsIEditor::EDirection aDirection) override;
+
+ /**
+ * All editor operations which alter the doc should be followed
+ * with a call to EndOperation.
+ */
+ NS_IMETHOD EndOperation() override;
+
+ /**
+ * Make the given selection span the entire document.
+ */
+ virtual nsresult SelectEntireDocument(Selection* aSelection) override;
+
+ virtual nsresult HandleKeyPressEvent(nsIDOMKeyEvent* aKeyEvent) override;
+
+ virtual already_AddRefed<dom::EventTarget> GetDOMEventTarget() override;
+
+ virtual nsresult BeginIMEComposition(WidgetCompositionEvent* aEvent) override;
+ virtual nsresult UpdateIMEComposition(nsIDOMEvent* aTextEvent) override;
+
+ virtual already_AddRefed<nsIContent> GetInputEventTargetContent() override;
+
+ // Utility Routines, not part of public API
+ NS_IMETHOD TypedText(const nsAString& aString, ETypingAction aAction);
+
+ nsresult InsertTextAt(const nsAString& aStringToInsert,
+ nsIDOMNode* aDestinationNode,
+ int32_t aDestOffset,
+ bool aDoDeleteSelection);
+
+ virtual nsresult InsertFromDataTransfer(dom::DataTransfer* aDataTransfer,
+ int32_t aIndex,
+ nsIDOMDocument* aSourceDoc,
+ nsIDOMNode* aDestinationNode,
+ int32_t aDestOffset,
+ bool aDoDeleteSelection) override;
+
+ virtual nsresult InsertFromDrop(nsIDOMEvent* aDropEvent) override;
+
+ /**
+ * Extends the selection for given deletion operation
+ * If done, also update aAction to what's actually left to do after the
+ * extension.
+ */
+ nsresult ExtendSelectionForDelete(Selection* aSelection,
+ nsIEditor::EDirection* aAction);
+
+ /**
+ * Return true if the data is safe to insert as the source and destination
+ * principals match, or we are in a editor context where this doesn't matter.
+ * Otherwise, the data must be sanitized first.
+ */
+ bool IsSafeToInsertData(nsIDOMDocument* aSourceDoc);
+
+ static void GetDefaultEditorPrefs(int32_t& aNewLineHandling,
+ int32_t& aCaretStyle);
+
+protected:
+ virtual ~TextEditor();
+
+ NS_IMETHOD InitRules();
+ void BeginEditorInit();
+ nsresult EndEditorInit();
+
+ NS_IMETHOD GetAndInitDocEncoder(const nsAString& aFormatType,
+ uint32_t aFlags,
+ const nsACString& aCharset,
+ nsIDocumentEncoder** encoder);
+
+ NS_IMETHOD CreateBR(nsIDOMNode* aNode, int32_t aOffset,
+ nsCOMPtr<nsIDOMNode>* outBRNode,
+ EDirection aSelect = eNone);
+ already_AddRefed<Element> CreateBRImpl(nsCOMPtr<nsINode>* aInOutParent,
+ int32_t* aInOutOffset,
+ EDirection aSelect);
+ nsresult CreateBRImpl(nsCOMPtr<nsIDOMNode>* aInOutParent,
+ int32_t* aInOutOffset,
+ nsCOMPtr<nsIDOMNode>* outBRNode,
+ EDirection aSelect);
+ nsresult InsertBR(nsCOMPtr<nsIDOMNode>* outBRNode);
+
+ /**
+ * Factored methods for handling insertion of data from transferables
+ * (drag&drop or clipboard).
+ */
+ NS_IMETHOD PrepareTransferable(nsITransferable** transferable);
+ NS_IMETHOD InsertTextFromTransferable(nsITransferable* transferable,
+ nsIDOMNode* aDestinationNode,
+ int32_t aDestOffset,
+ bool aDoDeleteSelection);
+
+ /**
+ * Shared outputstring; returns whether selection is collapsed and resulting
+ * string.
+ */
+ nsresult SharedOutputString(uint32_t aFlags, bool* aIsCollapsed,
+ nsAString& aResult);
+
+ /**
+ * Small utility routine to test the eEditorReadonly bit.
+ */
+ bool IsModifiable();
+
+ enum PasswordFieldAllowed
+ {
+ ePasswordFieldAllowed,
+ ePasswordFieldNotAllowed
+ };
+ bool CanCutOrCopy(PasswordFieldAllowed aPasswordFieldAllowed);
+ bool FireClipboardEvent(EventMessage aEventMessage,
+ int32_t aSelectionType,
+ bool* aActionTaken = nullptr);
+
+ bool UpdateMetaCharset(nsIDOMDocument* aDocument,
+ const nsACString& aCharacterSet);
+
+protected:
+ nsCOMPtr<nsIEditRules> mRules;
+ int32_t mWrapColumn;
+ int32_t mMaxTextLength;
+ int32_t mInitTriggerCounter;
+ int32_t mNewlineHandling;
+ int32_t mCaretStyle;
+
+ friend class AutoEditInitRulesTrigger;
+ friend class HTMLEditRules;
+ friend class TextEditRules;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef mozilla_TextEditor_h
diff --git a/editor/libeditor/TextEditorDataTransfer.cpp b/editor/libeditor/TextEditorDataTransfer.cpp
new file mode 100644
index 000000000..0388aa4a8
--- /dev/null
+++ b/editor/libeditor/TextEditorDataTransfer.cpp
@@ -0,0 +1,472 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/TextEditor.h"
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/EditorUtils.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/SelectionState.h"
+#include "mozilla/dom/Selection.h"
+#include "nsAString.h"
+#include "nsCOMPtr.h"
+#include "nsComponentManagerUtils.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsIClipboard.h"
+#include "nsIContent.h"
+#include "nsIDOMDataTransfer.h"
+#include "nsIDOMDocument.h"
+#include "nsIDOMDragEvent.h"
+#include "nsIDOMEvent.h"
+#include "nsIDOMNode.h"
+#include "nsIDOMUIEvent.h"
+#include "nsIDocument.h"
+#include "nsIDragService.h"
+#include "nsIDragSession.h"
+#include "nsIEditor.h"
+#include "nsIEditorIMESupport.h"
+#include "nsIDocShell.h"
+#include "nsIDocShellTreeItem.h"
+#include "nsIPrincipal.h"
+#include "nsIFormControl.h"
+#include "nsIPlaintextEditor.h"
+#include "nsISupportsPrimitives.h"
+#include "nsITransferable.h"
+#include "nsIVariant.h"
+#include "nsLiteralString.h"
+#include "nsRange.h"
+#include "nsServiceManagerUtils.h"
+#include "nsString.h"
+#include "nsXPCOM.h"
+#include "nscore.h"
+
+class nsILoadContext;
+class nsISupports;
+
+namespace mozilla {
+
+using namespace dom;
+
+NS_IMETHODIMP
+TextEditor::PrepareTransferable(nsITransferable** transferable)
+{
+ // Create generic Transferable for getting the data
+ nsresult rv = CallCreateInstance("@mozilla.org/widget/transferable;1", transferable);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the nsITransferable interface for getting the data from the clipboard
+ if (transferable) {
+ nsCOMPtr<nsIDocument> destdoc = GetDocument();
+ nsILoadContext* loadContext = destdoc ? destdoc->GetLoadContext() : nullptr;
+ (*transferable)->Init(loadContext);
+
+ (*transferable)->AddDataFlavor(kUnicodeMime);
+ (*transferable)->AddDataFlavor(kMozTextInternal);
+ };
+ return NS_OK;
+}
+
+nsresult
+TextEditor::InsertTextAt(const nsAString& aStringToInsert,
+ nsIDOMNode* aDestinationNode,
+ int32_t aDestOffset,
+ bool aDoDeleteSelection)
+{
+ if (aDestinationNode) {
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_STATE(selection);
+
+ nsCOMPtr<nsIDOMNode> targetNode = aDestinationNode;
+ int32_t targetOffset = aDestOffset;
+
+ if (aDoDeleteSelection) {
+ // Use an auto tracker so that our drop point is correctly
+ // positioned after the delete.
+ AutoTrackDOMPoint tracker(mRangeUpdater, &targetNode, &targetOffset);
+ nsresult rv = DeleteSelection(eNone, eStrip);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsresult rv = selection->Collapse(targetNode, targetOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return InsertText(aStringToInsert);
+}
+
+NS_IMETHODIMP
+TextEditor::InsertTextFromTransferable(nsITransferable* aTransferable,
+ nsIDOMNode* aDestinationNode,
+ int32_t aDestOffset,
+ bool aDoDeleteSelection)
+{
+ nsresult rv = NS_OK;
+ nsAutoCString bestFlavor;
+ nsCOMPtr<nsISupports> genericDataObj;
+ uint32_t len = 0;
+ if (NS_SUCCEEDED(
+ aTransferable->GetAnyTransferData(bestFlavor,
+ getter_AddRefs(genericDataObj),
+ &len)) &&
+ (bestFlavor.EqualsLiteral(kUnicodeMime) ||
+ bestFlavor.EqualsLiteral(kMozTextInternal))) {
+ AutoTransactionsConserveSelection dontSpazMySelection(this);
+ nsCOMPtr<nsISupportsString> textDataObj ( do_QueryInterface(genericDataObj) );
+ if (textDataObj && len > 0) {
+ nsAutoString stuffToPaste;
+ textDataObj->GetData(stuffToPaste);
+ NS_ASSERTION(stuffToPaste.Length() <= (len/2), "Invalid length!");
+
+ // Sanitize possible carriage returns in the string to be inserted
+ nsContentUtils::PlatformToDOMLineBreaks(stuffToPaste);
+
+ AutoEditBatch beginBatching(this);
+ rv = InsertTextAt(stuffToPaste, aDestinationNode, aDestOffset, aDoDeleteSelection);
+ }
+ }
+
+ // Try to scroll the selection into view if the paste/drop succeeded
+
+ if (NS_SUCCEEDED(rv)) {
+ ScrollSelectionIntoView(false);
+ }
+
+ return rv;
+}
+
+nsresult
+TextEditor::InsertFromDataTransfer(DataTransfer* aDataTransfer,
+ int32_t aIndex,
+ nsIDOMDocument* aSourceDoc,
+ nsIDOMNode* aDestinationNode,
+ int32_t aDestOffset,
+ bool aDoDeleteSelection)
+{
+ nsCOMPtr<nsIVariant> data;
+ DataTransfer::Cast(aDataTransfer)->GetDataAtNoSecurityCheck(NS_LITERAL_STRING("text/plain"), aIndex,
+ getter_AddRefs(data));
+ if (data) {
+ nsAutoString insertText;
+ data->GetAsAString(insertText);
+ nsContentUtils::PlatformToDOMLineBreaks(insertText);
+
+ AutoEditBatch beginBatching(this);
+ return InsertTextAt(insertText, aDestinationNode, aDestOffset, aDoDeleteSelection);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+TextEditor::InsertFromDrop(nsIDOMEvent* aDropEvent)
+{
+ ForceCompositionEnd();
+
+ nsCOMPtr<nsIDOMDragEvent> dragEvent(do_QueryInterface(aDropEvent));
+ NS_ENSURE_TRUE(dragEvent, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIDOMDataTransfer> domDataTransfer;
+ dragEvent->GetDataTransfer(getter_AddRefs(domDataTransfer));
+ nsCOMPtr<DataTransfer> dataTransfer = do_QueryInterface(domDataTransfer);
+ NS_ENSURE_TRUE(dataTransfer, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIDragSession> dragSession = nsContentUtils::GetDragSession();
+ NS_ASSERTION(dragSession, "No drag session");
+
+ nsCOMPtr<nsIDOMNode> sourceNode;
+ dataTransfer->GetMozSourceNode(getter_AddRefs(sourceNode));
+
+ nsCOMPtr<nsIDOMDocument> srcdomdoc;
+ if (sourceNode) {
+ sourceNode->GetOwnerDocument(getter_AddRefs(srcdomdoc));
+ NS_ENSURE_TRUE(sourceNode, NS_ERROR_FAILURE);
+ }
+
+ if (nsContentUtils::CheckForSubFrameDrop(dragSession,
+ aDropEvent->WidgetEventPtr()->AsDragEvent())) {
+ // Don't allow drags from subframe documents with different origins than
+ // the drop destination.
+ if (srcdomdoc && !IsSafeToInsertData(srcdomdoc)) {
+ return NS_OK;
+ }
+ }
+
+ // Current doc is destination
+ nsCOMPtr<nsIDOMDocument> destdomdoc = GetDOMDocument();
+ NS_ENSURE_TRUE(destdomdoc, NS_ERROR_NOT_INITIALIZED);
+
+ uint32_t numItems = 0;
+ nsresult rv = dataTransfer->GetMozItemCount(&numItems);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (numItems < 1) {
+ return NS_ERROR_FAILURE; // Nothing to drop?
+ }
+
+ // Combine any deletion and drop insertion into one transaction
+ AutoEditBatch beginBatching(this);
+
+ bool deleteSelection = false;
+
+ // We have to figure out whether to delete and relocate caret only once
+ // Parent and offset are under the mouse cursor
+ nsCOMPtr<nsIDOMUIEvent> uiEvent = do_QueryInterface(aDropEvent);
+ NS_ENSURE_TRUE(uiEvent, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIDOMNode> newSelectionParent;
+ rv = uiEvent->GetRangeParent(getter_AddRefs(newSelectionParent));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(newSelectionParent, NS_ERROR_FAILURE);
+
+ int32_t newSelectionOffset;
+ rv = uiEvent->GetRangeOffset(&newSelectionOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE);
+
+ bool isCollapsed = selection->Collapsed();
+
+ // Only the HTMLEditor::FindUserSelectAllNode returns a node.
+ nsCOMPtr<nsIDOMNode> userSelectNode = FindUserSelectAllNode(newSelectionParent);
+ if (userSelectNode) {
+ // The drop is happening over a "-moz-user-select: all"
+ // subtree so make sure the content we insert goes before
+ // the root of the subtree.
+ //
+ // XXX: Note that inserting before the subtree matches the
+ // current behavior when dropping on top of an image.
+ // The decision for dropping before or after the
+ // subtree should really be done based on coordinates.
+
+ newSelectionParent = GetNodeLocation(userSelectNode, &newSelectionOffset);
+
+ NS_ENSURE_TRUE(newSelectionParent, NS_ERROR_FAILURE);
+ }
+
+ // Check if mouse is in the selection
+ // if so, jump through some hoops to determine if mouse is over selection (bail)
+ // and whether user wants to copy selection or delete it
+ if (!isCollapsed) {
+ // We never have to delete if selection is already collapsed
+ bool cursorIsInSelection = false;
+
+ int32_t rangeCount;
+ rv = selection->GetRangeCount(&rangeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (int32_t j = 0; j < rangeCount; j++) {
+ RefPtr<nsRange> range = selection->GetRangeAt(j);
+ if (!range) {
+ // don't bail yet, iterate through them all
+ continue;
+ }
+
+ rv = range->IsPointInRange(newSelectionParent, newSelectionOffset, &cursorIsInSelection);
+ if (cursorIsInSelection) {
+ break;
+ }
+ }
+
+ if (cursorIsInSelection) {
+ // Dragging within same doc can't drop on itself -- leave!
+ if (srcdomdoc == destdomdoc) {
+ return NS_OK;
+ }
+
+ // Dragging from another window onto a selection
+ // XXX Decision made to NOT do this,
+ // note that 4.x does replace if dropped on
+ //deleteSelection = true;
+ } else {
+ // We are NOT over the selection
+ if (srcdomdoc == destdomdoc) {
+ // Within the same doc: delete if user doesn't want to copy
+ uint32_t dropEffect;
+ dataTransfer->GetDropEffectInt(&dropEffect);
+ deleteSelection = !(dropEffect & nsIDragService::DRAGDROP_ACTION_COPY);
+ } else {
+ // Different source doc: Don't delete
+ deleteSelection = false;
+ }
+ }
+ }
+
+ if (IsPlaintextEditor()) {
+ nsCOMPtr<nsIContent> content = do_QueryInterface(newSelectionParent);
+ while (content) {
+ nsCOMPtr<nsIFormControl> formControl(do_QueryInterface(content));
+ if (formControl && !formControl->AllowDrop()) {
+ // Don't allow dropping into a form control that doesn't allow being
+ // dropped into.
+ return NS_OK;
+ }
+ content = content->GetParent();
+ }
+ }
+
+ for (uint32_t i = 0; i < numItems; ++i) {
+ InsertFromDataTransfer(dataTransfer, i, srcdomdoc, newSelectionParent,
+ newSelectionOffset, deleteSelection);
+ }
+
+ if (NS_SUCCEEDED(rv)) {
+ ScrollSelectionIntoView(false);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+TextEditor::Paste(int32_t aSelectionType)
+{
+ if (!FireClipboardEvent(ePaste, aSelectionType)) {
+ return NS_OK;
+ }
+
+ // Get Clipboard Service
+ nsresult rv;
+ nsCOMPtr<nsIClipboard> clipboard(do_GetService("@mozilla.org/widget/clipboard;1", &rv));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Get the nsITransferable interface for getting the data from the clipboard
+ nsCOMPtr<nsITransferable> trans;
+ rv = PrepareTransferable(getter_AddRefs(trans));
+ if (NS_SUCCEEDED(rv) && trans) {
+ // Get the Data from the clipboard
+ if (NS_SUCCEEDED(clipboard->GetData(trans, aSelectionType)) &&
+ IsModifiable()) {
+ // handle transferable hooks
+ nsCOMPtr<nsIDOMDocument> domdoc = GetDOMDocument();
+ if (!EditorHookUtils::DoInsertionHook(domdoc, nullptr, trans)) {
+ return NS_OK;
+ }
+
+ rv = InsertTextFromTransferable(trans, nullptr, 0, true);
+ }
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+TextEditor::PasteTransferable(nsITransferable* aTransferable)
+{
+ // Use an invalid value for the clipboard type as data comes from aTransferable
+ // and we don't currently implement a way to put that in the data transfer yet.
+ if (!FireClipboardEvent(ePaste, -1)) {
+ return NS_OK;
+ }
+
+ if (!IsModifiable()) {
+ return NS_OK;
+ }
+
+ // handle transferable hooks
+ nsCOMPtr<nsIDOMDocument> domdoc = GetDOMDocument();
+ if (!EditorHookUtils::DoInsertionHook(domdoc, nullptr, aTransferable)) {
+ return NS_OK;
+ }
+
+ return InsertTextFromTransferable(aTransferable, nullptr, 0, true);
+}
+
+NS_IMETHODIMP
+TextEditor::CanPaste(int32_t aSelectionType,
+ bool* aCanPaste)
+{
+ NS_ENSURE_ARG_POINTER(aCanPaste);
+ *aCanPaste = false;
+
+ // can't paste if readonly
+ if (!IsModifiable()) {
+ return NS_OK;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIClipboard> clipboard(do_GetService("@mozilla.org/widget/clipboard;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // the flavors that we can deal with
+ const char* textEditorFlavors[] = { kUnicodeMime };
+
+ bool haveFlavors;
+ rv = clipboard->HasDataMatchingFlavors(textEditorFlavors,
+ ArrayLength(textEditorFlavors),
+ aSelectionType, &haveFlavors);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aCanPaste = haveFlavors;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+TextEditor::CanPasteTransferable(nsITransferable* aTransferable,
+ bool* aCanPaste)
+{
+ NS_ENSURE_ARG_POINTER(aCanPaste);
+
+ // can't paste if readonly
+ if (!IsModifiable()) {
+ *aCanPaste = false;
+ return NS_OK;
+ }
+
+ // If |aTransferable| is null, assume that a paste will succeed.
+ if (!aTransferable) {
+ *aCanPaste = true;
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsISupports> data;
+ uint32_t dataLen;
+ nsresult rv = aTransferable->GetTransferData(kUnicodeMime,
+ getter_AddRefs(data),
+ &dataLen);
+ if (NS_SUCCEEDED(rv) && data) {
+ *aCanPaste = true;
+ } else {
+ *aCanPaste = false;
+ }
+
+ return NS_OK;
+}
+
+bool
+TextEditor::IsSafeToInsertData(nsIDOMDocument* aSourceDoc)
+{
+ // Try to determine whether we should use a sanitizing fragment sink
+ bool isSafe = false;
+
+ nsCOMPtr<nsIDocument> destdoc = GetDocument();
+ NS_ASSERTION(destdoc, "Where is our destination doc?");
+ nsCOMPtr<nsIDocShellTreeItem> dsti = destdoc->GetDocShell();
+ nsCOMPtr<nsIDocShellTreeItem> root;
+ if (dsti) {
+ dsti->GetRootTreeItem(getter_AddRefs(root));
+ }
+ nsCOMPtr<nsIDocShell> docShell = do_QueryInterface(root);
+ uint32_t appType;
+ if (docShell && NS_SUCCEEDED(docShell->GetAppType(&appType))) {
+ isSafe = appType == nsIDocShell::APP_TYPE_EDITOR;
+ }
+ if (!isSafe && aSourceDoc) {
+ nsCOMPtr<nsIDocument> srcdoc = do_QueryInterface(aSourceDoc);
+ NS_ASSERTION(srcdoc, "Where is our source doc?");
+
+ nsIPrincipal* srcPrincipal = srcdoc->NodePrincipal();
+ nsIPrincipal* destPrincipal = destdoc->NodePrincipal();
+ NS_ASSERTION(srcPrincipal && destPrincipal, "How come we don't have a principal?");
+ srcPrincipal->Subsumes(destPrincipal, &isSafe);
+ }
+
+ return isSafe;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/TextEditorTest.cpp b/editor/libeditor/TextEditorTest.cpp
new file mode 100644
index 000000000..5378bc33b
--- /dev/null
+++ b/editor/libeditor/TextEditorTest.cpp
@@ -0,0 +1,259 @@
+/* -*- Mode: C++ tab-width: 2 indent-tabs-mode: nil c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifdef DEBUG
+
+#include "TextEditorTest.h"
+
+#include <stdio.h>
+
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsGkAtoms.h"
+#include "nsIDOMCharacterData.h"
+#include "nsIDOMDocument.h"
+#include "nsIDOMNode.h"
+#include "nsIDOMNodeList.h"
+#include "nsIEditor.h"
+#include "nsIHTMLEditor.h"
+#include "nsIPlaintextEditor.h"
+#include "nsISelection.h"
+#include "nsLiteralString.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+
+#define TEST_RESULT(r) { if (NS_FAILED(r)) {printf("FAILURE result=%X\n", static_cast<uint32_t>(r)); return r; } }
+#define TEST_POINTER(p) { if (!p) {printf("FAILURE null pointer\n"); return NS_ERROR_NULL_POINTER; } }
+
+TextEditorTest::TextEditorTest()
+{
+ printf("constructed a TextEditorTest\n");
+}
+
+TextEditorTest::~TextEditorTest()
+{
+ printf("destroyed a TextEditorTest\n");
+}
+
+void TextEditorTest::Run(nsIEditor *aEditor, int32_t *outNumTests, int32_t *outNumTestsFailed)
+{
+ if (!aEditor) return;
+ mTextEditor = do_QueryInterface(aEditor);
+ mEditor = do_QueryInterface(aEditor);
+ RunUnitTest(outNumTests, outNumTestsFailed);
+}
+
+nsresult TextEditorTest::RunUnitTest(int32_t *outNumTests, int32_t *outNumTestsFailed)
+{
+ NS_ENSURE_TRUE(outNumTests && outNumTestsFailed, NS_ERROR_NULL_POINTER);
+
+ *outNumTests = 0;
+ *outNumTestsFailed = 0;
+
+ nsresult rv = InitDoc();
+ TEST_RESULT(rv);
+ // shouldn't we just bail on error here?
+
+ // insert some simple text
+ rv = mTextEditor->InsertText(NS_LITERAL_STRING("1234567890abcdefghij1234567890"));
+ TEST_RESULT(rv);
+ (*outNumTests)++;
+ if (NS_FAILED(rv)) {
+ ++(*outNumTestsFailed);
+ }
+
+ // insert some more text
+ rv = mTextEditor->InsertText(NS_LITERAL_STRING("Moreover, I am cognizant of the interrelatedness of all communities and states. I cannot sit idly by in Atlanta and not be concerned about what happens in Birmingham. Injustice anywhere is a threat to justice everywhere"));
+ TEST_RESULT(rv);
+ (*outNumTests)++;
+ if (NS_FAILED(rv)) {
+ ++(*outNumTestsFailed);
+ }
+
+ rv = TestInsertBreak();
+ TEST_RESULT(rv);
+ (*outNumTests)++;
+ if (NS_FAILED(rv)) {
+ ++(*outNumTestsFailed);
+ }
+
+ rv = TestTextProperties();
+ TEST_RESULT(rv);
+ (*outNumTests)++;
+ if (NS_FAILED(rv)) {
+ ++(*outNumTestsFailed);
+ }
+
+ // get us back to the original document
+ rv = mEditor->Undo(12);
+ TEST_RESULT(rv);
+
+ return rv;
+}
+
+nsresult TextEditorTest::InitDoc()
+{
+ nsresult rv = mEditor->SelectAll();
+ TEST_RESULT(rv);
+ rv = mEditor->DeleteSelection(nsIEditor::eNext, nsIEditor::eStrip);
+ TEST_RESULT(rv);
+ return rv;
+}
+
+nsresult TextEditorTest::TestInsertBreak()
+{
+ nsCOMPtr<nsISelection>selection;
+ nsresult rv = mEditor->GetSelection(getter_AddRefs(selection));
+ TEST_RESULT(rv);
+ TEST_POINTER(selection.get());
+ nsCOMPtr<nsIDOMNode>anchor;
+ rv = selection->GetAnchorNode(getter_AddRefs(anchor));
+ TEST_RESULT(rv);
+ TEST_POINTER(anchor.get());
+ selection->Collapse(anchor, 0);
+ // insert one break
+ printf("inserting a break\n");
+ rv = mTextEditor->InsertLineBreak();
+ TEST_RESULT(rv);
+ mEditor->DebugDumpContent();
+
+ // insert a second break adjacent to the first
+ printf("inserting a second break\n");
+ rv = mTextEditor->InsertLineBreak();
+ TEST_RESULT(rv);
+ mEditor->DebugDumpContent();
+
+ return rv;
+}
+
+nsresult TextEditorTest::TestTextProperties()
+{
+ nsCOMPtr<nsIDOMDocument>doc;
+ nsresult rv = mEditor->GetDocument(getter_AddRefs(doc));
+ TEST_RESULT(rv);
+ TEST_POINTER(doc.get());
+ nsCOMPtr<nsIDOMNodeList>nodeList;
+ // XXX This is broken, text nodes are not elements.
+ nsAutoString textTag(NS_LITERAL_STRING("#text"));
+ rv = doc->GetElementsByTagName(textTag, getter_AddRefs(nodeList));
+ TEST_RESULT(rv);
+ TEST_POINTER(nodeList.get());
+ uint32_t count;
+ nodeList->GetLength(&count);
+ NS_ASSERTION(0!=count, "there are no text nodes in the document!");
+ nsCOMPtr<nsIDOMNode>textNode;
+ rv = nodeList->Item(count - 1, getter_AddRefs(textNode));
+ TEST_RESULT(rv);
+ TEST_POINTER(textNode.get());
+
+ // set the whole text node to bold
+ printf("set the whole first text node to bold\n");
+ nsCOMPtr<nsISelection>selection;
+ rv = mEditor->GetSelection(getter_AddRefs(selection));
+ TEST_RESULT(rv);
+ TEST_POINTER(selection.get());
+ nsCOMPtr<nsIDOMCharacterData>textData;
+ textData = do_QueryInterface(textNode);
+ uint32_t length;
+ textData->GetLength(&length);
+ selection->Collapse(textNode, 0);
+ selection->Extend(textNode, length);
+
+ nsCOMPtr<nsIHTMLEditor> htmlEditor (do_QueryInterface(mTextEditor));
+ NS_ENSURE_TRUE(htmlEditor, NS_ERROR_FAILURE);
+
+ bool any = false;
+ bool all = false;
+ bool first=false;
+
+ const nsAFlatString& empty = EmptyString();
+
+ rv = htmlEditor->GetInlineProperty(nsGkAtoms::b, empty, empty, &first,
+ &any, &all);
+ TEST_RESULT(rv);
+ NS_ASSERTION(false==first, "first should be false");
+ NS_ASSERTION(false==any, "any should be false");
+ NS_ASSERTION(false==all, "all should be false");
+ rv = htmlEditor->SetInlineProperty(nsGkAtoms::b, empty, empty);
+ TEST_RESULT(rv);
+ rv = htmlEditor->GetInlineProperty(nsGkAtoms::b, empty, empty, &first,
+ &any, &all);
+ TEST_RESULT(rv);
+ NS_ASSERTION(true==first, "first should be true");
+ NS_ASSERTION(true==any, "any should be true");
+ NS_ASSERTION(true==all, "all should be true");
+ mEditor->DebugDumpContent();
+
+ // remove the bold we just set
+ printf("set the whole first text node to not bold\n");
+ rv = htmlEditor->RemoveInlineProperty(nsGkAtoms::b, empty);
+ TEST_RESULT(rv);
+ rv = htmlEditor->GetInlineProperty(nsGkAtoms::b, empty, empty, &first,
+ &any, &all);
+ TEST_RESULT(rv);
+ NS_ASSERTION(false==first, "first should be false");
+ NS_ASSERTION(false==any, "any should be false");
+ NS_ASSERTION(false==all, "all should be false");
+ mEditor->DebugDumpContent();
+
+ // set all but the first and last character to bold
+ printf("set the first text node (1, length-1) to bold and italic, and (2, length-1) to underline.\n");
+ selection->Collapse(textNode, 1);
+ selection->Extend(textNode, length-1);
+ rv = htmlEditor->SetInlineProperty(nsGkAtoms::b, empty, empty);
+ TEST_RESULT(rv);
+ rv = htmlEditor->GetInlineProperty(nsGkAtoms::b, empty, empty, &first,
+ &any, &all);
+ TEST_RESULT(rv);
+ NS_ASSERTION(true==first, "first should be true");
+ NS_ASSERTION(true==any, "any should be true");
+ NS_ASSERTION(true==all, "all should be true");
+ mEditor->DebugDumpContent();
+ // make all that same text italic
+ rv = htmlEditor->SetInlineProperty(nsGkAtoms::i, empty, empty);
+ TEST_RESULT(rv);
+ rv = htmlEditor->GetInlineProperty(nsGkAtoms::i, empty, empty, &first,
+ &any, &all);
+ TEST_RESULT(rv);
+ NS_ASSERTION(true==first, "first should be true");
+ NS_ASSERTION(true==any, "any should be true");
+ NS_ASSERTION(true==all, "all should be true");
+ rv = htmlEditor->GetInlineProperty(nsGkAtoms::b, empty, empty, &first,
+ &any, &all);
+ TEST_RESULT(rv);
+ NS_ASSERTION(true==first, "first should be true");
+ NS_ASSERTION(true==any, "any should be true");
+ NS_ASSERTION(true==all, "all should be true");
+ mEditor->DebugDumpContent();
+
+ // make all the text underlined, except for the first 2 and last 2 characters
+ rv = doc->GetElementsByTagName(textTag, getter_AddRefs(nodeList));
+ TEST_RESULT(rv);
+ TEST_POINTER(nodeList.get());
+ nodeList->GetLength(&count);
+ NS_ASSERTION(0!=count, "there are no text nodes in the document!");
+ rv = nodeList->Item(count-2, getter_AddRefs(textNode));
+ TEST_RESULT(rv);
+ TEST_POINTER(textNode.get());
+ textData = do_QueryInterface(textNode);
+ textData->GetLength(&length);
+ NS_ASSERTION(length==915, "wrong text node");
+ selection->Collapse(textNode, 1);
+ selection->Extend(textNode, length-2);
+ rv = htmlEditor->SetInlineProperty(nsGkAtoms::u, empty, empty);
+ TEST_RESULT(rv);
+ rv = htmlEditor->GetInlineProperty(nsGkAtoms::u, empty, empty, &first,
+ &any, &all);
+ TEST_RESULT(rv);
+ NS_ASSERTION(true==first, "first should be true");
+ NS_ASSERTION(true==any, "any should be true");
+ NS_ASSERTION(true==all, "all should be true");
+ mEditor->DebugDumpContent();
+
+ return rv;
+}
+
+#endif
diff --git a/editor/libeditor/TextEditorTest.h b/editor/libeditor/TextEditorTest.h
new file mode 100644
index 000000000..0483da463
--- /dev/null
+++ b/editor/libeditor/TextEditorTest.h
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __TextEditorTest_h__
+#define __TextEditorTest_h__
+
+#include "nscore.h"
+
+class nsIEditor;
+class nsIPlaintextEditor;
+#ifdef DEBUG
+
+#include "nsCOMPtr.h"
+
+class TextEditorTest
+{
+public:
+
+ void Run(nsIEditor *aEditor, int32_t *outNumTests, int32_t *outNumTestsFailed);
+ TextEditorTest();
+ ~TextEditorTest();
+
+protected:
+
+ /** create an empty document */
+ nsresult InitDoc();
+
+ nsresult RunUnitTest(int32_t *outNumTests, int32_t *outNumTestsFailed);
+
+ nsresult TestInsertBreak();
+
+ nsresult TestTextProperties();
+
+ nsCOMPtr<nsIPlaintextEditor> mTextEditor;
+ nsCOMPtr<nsIEditor> mEditor;
+};
+
+#endif /* DEBUG */
+
+#endif
diff --git a/editor/libeditor/TypeInState.cpp b/editor/libeditor/TypeInState.cpp
new file mode 100644
index 000000000..ce43e5e4d
--- /dev/null
+++ b/editor/libeditor/TypeInState.cpp
@@ -0,0 +1,397 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TypeInState.h"
+
+#include <stddef.h>
+
+#include "nsError.h"
+#include "mozilla/EditorBase.h"
+#include "mozilla/mozalloc.h"
+#include "mozilla/dom/Selection.h"
+#include "nsAString.h"
+#include "nsDebug.h"
+#include "nsGkAtoms.h"
+#include "nsIDOMNode.h"
+#include "nsISupportsBase.h"
+#include "nsISupportsImpl.h"
+#include "nsReadableUtils.h"
+#include "nsStringFwd.h"
+
+class nsIAtom;
+class nsIDOMDocument;
+
+namespace mozilla {
+
+using namespace dom;
+
+/********************************************************************
+ * mozilla::TypeInState
+ *******************************************************************/
+
+NS_IMPL_CYCLE_COLLECTION(TypeInState, mLastSelectionContainer)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(TypeInState)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(TypeInState)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TypeInState)
+ NS_INTERFACE_MAP_ENTRY(nsISelectionListener)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+TypeInState::TypeInState()
+ : mRelativeFontSize(0)
+ , mLastSelectionOffset(0)
+{
+ Reset();
+}
+
+TypeInState::~TypeInState()
+{
+ // Call Reset() to release any data that may be in
+ // mClearedArray and mSetArray.
+
+ Reset();
+}
+
+nsresult
+TypeInState::UpdateSelState(Selection* aSelection)
+{
+ NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER);
+
+ if (!aSelection->Collapsed()) {
+ return NS_OK;
+ }
+
+ return EditorBase::GetStartNodeAndOffset(
+ aSelection, getter_AddRefs(mLastSelectionContainer),
+ &mLastSelectionOffset);
+}
+
+
+NS_IMETHODIMP
+TypeInState::NotifySelectionChanged(nsIDOMDocument* aDOMDocument,
+ nsISelection* aSelection,
+ int16_t aReason)
+{
+ // XXX: Selection currently generates bogus selection changed notifications
+ // XXX: (bug 140303). It can notify us when the selection hasn't actually
+ // XXX: changed, and it notifies us more than once for the same change.
+ // XXX:
+ // XXX: The following code attempts to work around the bogus notifications,
+ // XXX: and should probably be removed once bug 140303 is fixed.
+ // XXX:
+ // XXX: This code temporarily fixes the problem where clicking the mouse in
+ // XXX: the same location clears the type-in-state.
+ RefPtr<Selection> selection =
+ aSelection ? aSelection->AsSelection() : nullptr;
+
+ if (aSelection) {
+ int32_t rangeCount = selection->RangeCount();
+
+ if (selection->Collapsed() && rangeCount) {
+ nsCOMPtr<nsIDOMNode> selNode;
+ int32_t selOffset = 0;
+
+ nsresult rv =
+ EditorBase::GetStartNodeAndOffset(selection, getter_AddRefs(selNode),
+ &selOffset);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (selNode &&
+ selNode == mLastSelectionContainer &&
+ selOffset == mLastSelectionOffset) {
+ // We got a bogus selection changed notification!
+ return NS_OK;
+ }
+
+ mLastSelectionContainer = selNode;
+ mLastSelectionOffset = selOffset;
+ } else {
+ mLastSelectionContainer = nullptr;
+ mLastSelectionOffset = 0;
+ }
+ }
+
+ Reset();
+ return NS_OK;
+}
+
+void
+TypeInState::Reset()
+{
+ for (size_t i = 0, n = mClearedArray.Length(); i < n; i++) {
+ delete mClearedArray[i];
+ }
+ mClearedArray.Clear();
+ for (size_t i = 0, n = mSetArray.Length(); i < n; i++) {
+ delete mSetArray[i];
+ }
+ mSetArray.Clear();
+}
+
+
+void
+TypeInState::SetProp(nsIAtom* aProp,
+ const nsAString& aAttr,
+ const nsAString& aValue)
+{
+ // special case for big/small, these nest
+ if (nsGkAtoms::big == aProp) {
+ mRelativeFontSize++;
+ return;
+ }
+ if (nsGkAtoms::small == aProp) {
+ mRelativeFontSize--;
+ return;
+ }
+
+ int32_t index;
+ if (IsPropSet(aProp, aAttr, nullptr, index)) {
+ // if it's already set, update the value
+ mSetArray[index]->value = aValue;
+ return;
+ }
+
+ // Make a new propitem and add it to the list of set properties.
+ mSetArray.AppendElement(new PropItem(aProp, aAttr, aValue));
+
+ // remove it from the list of cleared properties, if we have a match
+ RemovePropFromClearedList(aProp, aAttr);
+}
+
+
+void
+TypeInState::ClearAllProps()
+{
+ // null prop means "all" props
+ ClearProp(nullptr, EmptyString());
+}
+
+void
+TypeInState::ClearProp(nsIAtom* aProp,
+ const nsAString& aAttr)
+{
+ // if it's already cleared we are done
+ if (IsPropCleared(aProp, aAttr)) {
+ return;
+ }
+
+ // make a new propitem
+ PropItem* item = new PropItem(aProp, aAttr, EmptyString());
+
+ // remove it from the list of set properties, if we have a match
+ RemovePropFromSetList(aProp, aAttr);
+
+ // add it to the list of cleared properties
+ mClearedArray.AppendElement(item);
+}
+
+
+/**
+ * TakeClearProperty() hands back next property item on the clear list.
+ * Caller assumes ownership of PropItem and must delete it.
+ */
+PropItem*
+TypeInState::TakeClearProperty()
+{
+ size_t count = mClearedArray.Length();
+ if (!count) {
+ return nullptr;
+ }
+
+ --count; // indices are zero based
+ PropItem* propItem = mClearedArray[count];
+ mClearedArray.RemoveElementAt(count);
+ return propItem;
+}
+
+/**
+ * TakeSetProperty() hands back next poroperty item on the set list.
+ * Caller assumes ownership of PropItem and must delete it.
+ */
+PropItem*
+TypeInState::TakeSetProperty()
+{
+ size_t count = mSetArray.Length();
+ if (!count) {
+ return nullptr;
+ }
+ count--; // indices are zero based
+ PropItem* propItem = mSetArray[count];
+ mSetArray.RemoveElementAt(count);
+ return propItem;
+}
+
+/**
+ * TakeRelativeFontSize() hands back relative font value, which is then
+ * cleared out.
+ */
+int32_t
+TypeInState::TakeRelativeFontSize()
+{
+ int32_t relSize = mRelativeFontSize;
+ mRelativeFontSize = 0;
+ return relSize;
+}
+
+void
+TypeInState::GetTypingState(bool& isSet,
+ bool& theSetting,
+ nsIAtom* aProp)
+{
+ GetTypingState(isSet, theSetting, aProp, EmptyString(), nullptr);
+}
+
+void
+TypeInState::GetTypingState(bool& isSet,
+ bool& theSetting,
+ nsIAtom* aProp,
+ const nsString& aAttr,
+ nsString* aValue)
+{
+ if (IsPropSet(aProp, aAttr, aValue)) {
+ isSet = true;
+ theSetting = true;
+ } else if (IsPropCleared(aProp, aAttr)) {
+ isSet = true;
+ theSetting = false;
+ } else {
+ isSet = false;
+ }
+}
+
+void
+TypeInState::RemovePropFromSetList(nsIAtom* aProp,
+ const nsAString& aAttr)
+{
+ int32_t index;
+ if (!aProp) {
+ // clear _all_ props
+ for (size_t i = 0, n = mSetArray.Length(); i < n; i++) {
+ delete mSetArray[i];
+ }
+ mSetArray.Clear();
+ mRelativeFontSize=0;
+ } else if (FindPropInList(aProp, aAttr, nullptr, mSetArray, index)) {
+ delete mSetArray[index];
+ mSetArray.RemoveElementAt(index);
+ }
+}
+
+void
+TypeInState::RemovePropFromClearedList(nsIAtom* aProp,
+ const nsAString& aAttr)
+{
+ int32_t index;
+ if (FindPropInList(aProp, aAttr, nullptr, mClearedArray, index)) {
+ delete mClearedArray[index];
+ mClearedArray.RemoveElementAt(index);
+ }
+}
+
+bool
+TypeInState::IsPropSet(nsIAtom* aProp,
+ const nsAString& aAttr,
+ nsAString* outValue)
+{
+ int32_t i;
+ return IsPropSet(aProp, aAttr, outValue, i);
+}
+
+bool
+TypeInState::IsPropSet(nsIAtom* aProp,
+ const nsAString& aAttr,
+ nsAString* outValue,
+ int32_t& outIndex)
+{
+ // linear search. list should be short.
+ size_t count = mSetArray.Length();
+ for (size_t i = 0; i < count; i++) {
+ PropItem *item = mSetArray[i];
+ if (item->tag == aProp && item->attr == aAttr) {
+ if (outValue) {
+ *outValue = item->value;
+ }
+ outIndex = i;
+ return true;
+ }
+ }
+ return false;
+}
+
+
+bool
+TypeInState::IsPropCleared(nsIAtom* aProp,
+ const nsAString& aAttr)
+{
+ int32_t i;
+ return IsPropCleared(aProp, aAttr, i);
+}
+
+
+bool
+TypeInState::IsPropCleared(nsIAtom* aProp,
+ const nsAString& aAttr,
+ int32_t& outIndex)
+{
+ if (FindPropInList(aProp, aAttr, nullptr, mClearedArray, outIndex)) {
+ return true;
+ }
+ if (FindPropInList(0, EmptyString(), nullptr, mClearedArray, outIndex)) {
+ // special case for all props cleared
+ outIndex = -1;
+ return true;
+ }
+ return false;
+}
+
+bool
+TypeInState::FindPropInList(nsIAtom* aProp,
+ const nsAString& aAttr,
+ nsAString* outValue,
+ nsTArray<PropItem*>& aList,
+ int32_t& outIndex)
+{
+ // linear search. list should be short.
+ size_t count = aList.Length();
+ for (size_t i = 0; i < count; i++) {
+ PropItem *item = aList[i];
+ if (item->tag == aProp && item->attr == aAttr) {
+ if (outValue) {
+ *outValue = item->value;
+ }
+ outIndex = i;
+ return true;
+ }
+ }
+ return false;
+}
+
+/********************************************************************
+ * mozilla::PropItem: helper struct for mozilla::TypeInState
+ *******************************************************************/
+
+PropItem::PropItem()
+ : tag(nullptr)
+{
+ MOZ_COUNT_CTOR(PropItem);
+}
+
+PropItem::PropItem(nsIAtom* aTag,
+ const nsAString& aAttr,
+ const nsAString &aValue)
+ : tag(aTag)
+ , attr(aAttr)
+ , value(aValue)
+{
+ MOZ_COUNT_CTOR(PropItem);
+}
+
+PropItem::~PropItem()
+{
+ MOZ_COUNT_DTOR(PropItem);
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/TypeInState.h b/editor/libeditor/TypeInState.h
new file mode 100644
index 000000000..540b2d9c1
--- /dev/null
+++ b/editor/libeditor/TypeInState.h
@@ -0,0 +1,111 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef TypeInState_h
+#define TypeInState_h
+
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISelectionListener.h"
+#include "nsISupportsImpl.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nscore.h"
+
+// Workaround for windows headers
+#ifdef SetProp
+#undef SetProp
+#endif
+
+class nsIAtom;
+class nsIDOMNode;
+
+namespace mozilla {
+
+class HTMLEditRules;
+namespace dom {
+class Selection;
+} // namespace dom
+
+struct PropItem
+{
+ nsIAtom* tag;
+ nsString attr;
+ nsString value;
+
+ PropItem();
+ PropItem(nsIAtom* aTag, const nsAString& aAttr, const nsAString& aValue);
+ ~PropItem();
+};
+
+class TypeInState final : public nsISelectionListener
+{
+public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(TypeInState)
+
+ TypeInState();
+ void Reset();
+
+ nsresult UpdateSelState(dom::Selection* aSelection);
+
+ // nsISelectionListener
+ NS_DECL_NSISELECTIONLISTENER
+
+ void SetProp(nsIAtom* aProp, const nsAString& aAttr, const nsAString& aValue);
+
+ void ClearAllProps();
+ void ClearProp(nsIAtom* aProp, const nsAString& aAttr);
+
+ /**
+ * TakeClearProperty() hands back next property item on the clear list.
+ * Caller assumes ownership of PropItem and must delete it.
+ */
+ PropItem* TakeClearProperty();
+
+ /**
+ * TakeSetProperty() hands back next property item on the set list.
+ * Caller assumes ownership of PropItem and must delete it.
+ */
+ PropItem* TakeSetProperty();
+
+ /**
+ * TakeRelativeFontSize() hands back relative font value, which is then
+ * cleared out.
+ */
+ int32_t TakeRelativeFontSize();
+
+ void GetTypingState(bool& isSet, bool& theSetting, nsIAtom* aProp);
+ void GetTypingState(bool& isSet, bool& theSetting, nsIAtom* aProp,
+ const nsString& aAttr, nsString* outValue);
+
+ static bool FindPropInList(nsIAtom* aProp, const nsAString& aAttr,
+ nsAString* outValue, nsTArray<PropItem*>& aList,
+ int32_t& outIndex);
+
+protected:
+ virtual ~TypeInState();
+
+ void RemovePropFromSetList(nsIAtom* aProp, const nsAString& aAttr);
+ void RemovePropFromClearedList(nsIAtom* aProp, const nsAString& aAttr);
+ bool IsPropSet(nsIAtom* aProp, const nsAString& aAttr, nsAString* outValue);
+ bool IsPropSet(nsIAtom* aProp, const nsAString& aAttr, nsAString* outValue,
+ int32_t& outIndex);
+ bool IsPropCleared(nsIAtom* aProp, const nsAString& aAttr);
+ bool IsPropCleared(nsIAtom* aProp, const nsAString& aAttr, int32_t& outIndex);
+
+ nsTArray<PropItem*> mSetArray;
+ nsTArray<PropItem*> mClearedArray;
+ int32_t mRelativeFontSize;
+ nsCOMPtr<nsIDOMNode> mLastSelectionContainer;
+ int32_t mLastSelectionOffset;
+
+ friend class HTMLEditRules;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef TypeInState_h
+
diff --git a/editor/libeditor/WSRunObject.cpp b/editor/libeditor/WSRunObject.cpp
new file mode 100644
index 000000000..39ac3fee8
--- /dev/null
+++ b/editor/libeditor/WSRunObject.cpp
@@ -0,0 +1,1926 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WSRunObject.h"
+
+#include "TextEditUtils.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Casting.h"
+#include "mozilla/EditorUtils.h"
+#include "mozilla/HTMLEditor.h"
+#include "mozilla/mozalloc.h"
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/SelectionState.h"
+
+#include "nsAString.h"
+#include "nsCRT.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsIContent.h"
+#include "nsIDOMDocument.h"
+#include "nsIDOMNode.h"
+#include "nsISupportsImpl.h"
+#include "nsRange.h"
+#include "nsString.h"
+#include "nsTextFragment.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+const char16_t nbsp = 160;
+
+WSRunObject::WSRunObject(HTMLEditor* aHTMLEditor,
+ nsINode* aNode,
+ int32_t aOffset)
+ : mNode(aNode)
+ , mOffset(aOffset)
+ , mPRE(false)
+ , mStartOffset(0)
+ , mEndOffset(0)
+ , mFirstNBSPOffset(0)
+ , mLastNBSPOffset(0)
+ , mStartRun(nullptr)
+ , mEndRun(nullptr)
+ , mHTMLEditor(aHTMLEditor)
+{
+ GetWSNodes();
+ GetRuns();
+}
+
+WSRunObject::WSRunObject(HTMLEditor* aHTMLEditor,
+ nsIDOMNode* aNode,
+ int32_t aOffset)
+ : mNode(do_QueryInterface(aNode))
+ , mOffset(aOffset)
+ , mPRE(false)
+ , mStartOffset(0)
+ , mEndOffset(0)
+ , mFirstNBSPOffset(0)
+ , mLastNBSPOffset(0)
+ , mStartRun(nullptr)
+ , mEndRun(nullptr)
+ , mHTMLEditor(aHTMLEditor)
+{
+ GetWSNodes();
+ GetRuns();
+}
+
+WSRunObject::~WSRunObject()
+{
+ ClearRuns();
+}
+
+nsresult
+WSRunObject::ScrubBlockBoundary(HTMLEditor* aHTMLEditor,
+ BlockBoundary aBoundary,
+ nsINode* aBlock,
+ int32_t aOffset)
+{
+ NS_ENSURE_TRUE(aHTMLEditor && aBlock, NS_ERROR_NULL_POINTER);
+
+ int32_t offset;
+ if (aBoundary == kBlockStart) {
+ offset = 0;
+ } else if (aBoundary == kBlockEnd) {
+ offset = aBlock->Length();
+ } else {
+ // Else we are scrubbing an outer boundary - just before or after a block
+ // element.
+ NS_ENSURE_STATE(aOffset >= 0);
+ offset = aOffset;
+ }
+
+ WSRunObject theWSObj(aHTMLEditor, aBlock, offset);
+ return theWSObj.Scrub();
+}
+
+nsresult
+WSRunObject::PrepareToJoinBlocks(HTMLEditor* aHTMLEditor,
+ Element* aLeftBlock,
+ Element* aRightBlock)
+{
+ NS_ENSURE_TRUE(aLeftBlock && aRightBlock && aHTMLEditor,
+ NS_ERROR_NULL_POINTER);
+
+ WSRunObject leftWSObj(aHTMLEditor, aLeftBlock, aLeftBlock->Length());
+ WSRunObject rightWSObj(aHTMLEditor, aRightBlock, 0);
+
+ return leftWSObj.PrepareToDeleteRangePriv(&rightWSObj);
+}
+
+nsresult
+WSRunObject::PrepareToDeleteRange(HTMLEditor* aHTMLEditor,
+ nsCOMPtr<nsINode>* aStartNode,
+ int32_t* aStartOffset,
+ nsCOMPtr<nsINode>* aEndNode,
+ int32_t* aEndOffset)
+{
+ NS_ENSURE_TRUE(aHTMLEditor && aStartNode && *aStartNode && aStartOffset &&
+ aEndNode && *aEndNode && aEndOffset, NS_ERROR_NULL_POINTER);
+
+ AutoTrackDOMPoint trackerStart(aHTMLEditor->mRangeUpdater,
+ aStartNode, aStartOffset);
+ AutoTrackDOMPoint trackerEnd(aHTMLEditor->mRangeUpdater,
+ aEndNode, aEndOffset);
+
+ WSRunObject leftWSObj(aHTMLEditor, *aStartNode, *aStartOffset);
+ WSRunObject rightWSObj(aHTMLEditor, *aEndNode, *aEndOffset);
+
+ return leftWSObj.PrepareToDeleteRangePriv(&rightWSObj);
+}
+
+nsresult
+WSRunObject::PrepareToDeleteNode(HTMLEditor* aHTMLEditor,
+ nsIContent* aContent)
+{
+ NS_ENSURE_TRUE(aContent && aHTMLEditor, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsINode> parent = aContent->GetParentNode();
+ NS_ENSURE_STATE(parent);
+ int32_t offset = parent->IndexOf(aContent);
+
+ WSRunObject leftWSObj(aHTMLEditor, parent, offset);
+ WSRunObject rightWSObj(aHTMLEditor, parent, offset + 1);
+
+ return leftWSObj.PrepareToDeleteRangePriv(&rightWSObj);
+}
+
+nsresult
+WSRunObject::PrepareToSplitAcrossBlocks(HTMLEditor* aHTMLEditor,
+ nsCOMPtr<nsINode>* aSplitNode,
+ int32_t* aSplitOffset)
+{
+ NS_ENSURE_TRUE(aHTMLEditor && aSplitNode && *aSplitNode && aSplitOffset,
+ NS_ERROR_NULL_POINTER);
+
+ AutoTrackDOMPoint tracker(aHTMLEditor->mRangeUpdater,
+ aSplitNode, aSplitOffset);
+
+ WSRunObject wsObj(aHTMLEditor, *aSplitNode, *aSplitOffset);
+
+ return wsObj.PrepareToSplitAcrossBlocksPriv();
+}
+
+already_AddRefed<Element>
+WSRunObject::InsertBreak(nsCOMPtr<nsINode>* aInOutParent,
+ int32_t* aInOutOffset,
+ nsIEditor::EDirection aSelect)
+{
+ // MOOSE: for now, we always assume non-PRE formatting. Fix this later.
+ // meanwhile, the pre case is handled in WillInsertText in
+ // HTMLEditRules.cpp
+ NS_ENSURE_TRUE(aInOutParent && aInOutOffset, nullptr);
+
+ WSFragment *beforeRun, *afterRun;
+ FindRun(*aInOutParent, *aInOutOffset, &beforeRun, false);
+ FindRun(*aInOutParent, *aInOutOffset, &afterRun, true);
+
+ {
+ // Some scoping for AutoTrackDOMPoint. This will track our insertion
+ // point while we tweak any surrounding whitespace
+ AutoTrackDOMPoint tracker(mHTMLEditor->mRangeUpdater, aInOutParent,
+ aInOutOffset);
+
+ // Handle any changes needed to ws run after inserted br
+ if (!afterRun || (afterRun->mType & WSType::trailingWS)) {
+ // Don't need to do anything. Just insert break. ws won't change.
+ } else if (afterRun->mType & WSType::leadingWS) {
+ // Delete the leading ws that is after insertion point. We don't
+ // have to (it would still not be significant after br), but it's
+ // just more aesthetically pleasing to.
+ nsresult rv = DeleteChars(*aInOutParent, *aInOutOffset,
+ afterRun->mEndNode, afterRun->mEndOffset,
+ eOutsideUserSelectAll);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ } else if (afterRun->mType == WSType::normalWS) {
+ // Need to determine if break at front of non-nbsp run. If so, convert
+ // run to nbsp.
+ WSPoint thePoint = GetCharAfter(*aInOutParent, *aInOutOffset);
+ if (thePoint.mTextNode && nsCRT::IsAsciiSpace(thePoint.mChar)) {
+ WSPoint prevPoint = GetCharBefore(thePoint);
+ if (prevPoint.mTextNode && !nsCRT::IsAsciiSpace(prevPoint.mChar)) {
+ // We are at start of non-nbsps. Convert to a single nbsp.
+ nsresult rv = ConvertToNBSP(thePoint);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ }
+ }
+ }
+
+ // Handle any changes needed to ws run before inserted br
+ if (!beforeRun || (beforeRun->mType & WSType::leadingWS)) {
+ // Don't need to do anything. Just insert break. ws won't change.
+ } else if (beforeRun->mType & WSType::trailingWS) {
+ // Need to delete the trailing ws that is before insertion point, because it
+ // would become significant after break inserted.
+ nsresult rv = DeleteChars(beforeRun->mStartNode, beforeRun->mStartOffset,
+ *aInOutParent, *aInOutOffset,
+ eOutsideUserSelectAll);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ } else if (beforeRun->mType == WSType::normalWS) {
+ // Try to change an nbsp to a space, just to prevent nbsp proliferation
+ nsresult rv = CheckTrailingNBSP(beforeRun, *aInOutParent, *aInOutOffset);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ }
+ }
+
+ // ready, aim, fire!
+ return mHTMLEditor->CreateBRImpl(aInOutParent, aInOutOffset, aSelect);
+}
+
+nsresult
+WSRunObject::InsertText(const nsAString& aStringToInsert,
+ nsCOMPtr<nsINode>* aInOutParent,
+ int32_t* aInOutOffset,
+ nsIDocument* aDoc)
+{
+ // MOOSE: for now, we always assume non-PRE formatting. Fix this later.
+ // meanwhile, the pre case is handled in WillInsertText in
+ // HTMLEditRules.cpp
+
+ // MOOSE: for now, just getting the ws logic straight. This implementation
+ // is very slow. Will need to replace edit rules impl with a more efficient
+ // text sink here that does the minimal amount of searching/replacing/copying
+
+ NS_ENSURE_TRUE(aInOutParent && aInOutOffset && aDoc, NS_ERROR_NULL_POINTER);
+
+ if (aStringToInsert.IsEmpty()) {
+ return NS_OK;
+ }
+
+ nsAutoString theString(aStringToInsert);
+
+ WSFragment *beforeRun, *afterRun;
+ FindRun(*aInOutParent, *aInOutOffset, &beforeRun, false);
+ FindRun(*aInOutParent, *aInOutOffset, &afterRun, true);
+
+ {
+ // Some scoping for AutoTrackDOMPoint. This will track our insertion
+ // point while we tweak any surrounding whitespace
+ AutoTrackDOMPoint tracker(mHTMLEditor->mRangeUpdater, aInOutParent,
+ aInOutOffset);
+
+ // Handle any changes needed to ws run after inserted text
+ if (!afterRun || afterRun->mType & WSType::trailingWS) {
+ // Don't need to do anything. Just insert text. ws won't change.
+ } else if (afterRun->mType & WSType::leadingWS) {
+ // Delete the leading ws that is after insertion point, because it
+ // would become significant after text inserted.
+ nsresult rv =
+ DeleteChars(*aInOutParent, *aInOutOffset, afterRun->mEndNode,
+ afterRun->mEndOffset, eOutsideUserSelectAll);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else if (afterRun->mType == WSType::normalWS) {
+ // Try to change an nbsp to a space, if possible, just to prevent nbsp
+ // proliferation
+ nsresult rv = CheckLeadingNBSP(afterRun, *aInOutParent, *aInOutOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Handle any changes needed to ws run before inserted text
+ if (!beforeRun || beforeRun->mType & WSType::leadingWS) {
+ // Don't need to do anything. Just insert text. ws won't change.
+ } else if (beforeRun->mType & WSType::trailingWS) {
+ // Need to delete the trailing ws that is before insertion point, because
+ // it would become significant after text inserted.
+ nsresult rv =
+ DeleteChars(beforeRun->mStartNode, beforeRun->mStartOffset,
+ *aInOutParent, *aInOutOffset, eOutsideUserSelectAll);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else if (beforeRun->mType == WSType::normalWS) {
+ // Try to change an nbsp to a space, if possible, just to prevent nbsp
+ // proliferation
+ nsresult rv = CheckTrailingNBSP(beforeRun, *aInOutParent, *aInOutOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ // Next up, tweak head and tail of string as needed. First the head: there
+ // are a variety of circumstances that would require us to convert a leading
+ // ws char into an nbsp:
+
+ if (nsCRT::IsAsciiSpace(theString[0])) {
+ // We have a leading space
+ if (beforeRun) {
+ if (beforeRun->mType & WSType::leadingWS) {
+ theString.SetCharAt(nbsp, 0);
+ } else if (beforeRun->mType & WSType::normalWS) {
+ WSPoint wspoint = GetCharBefore(*aInOutParent, *aInOutOffset);
+ if (wspoint.mTextNode && nsCRT::IsAsciiSpace(wspoint.mChar)) {
+ theString.SetCharAt(nbsp, 0);
+ }
+ }
+ } else if (mStartReason & WSType::block || mStartReason == WSType::br) {
+ theString.SetCharAt(nbsp, 0);
+ }
+ }
+
+ // Then the tail
+ uint32_t lastCharIndex = theString.Length() - 1;
+
+ if (nsCRT::IsAsciiSpace(theString[lastCharIndex])) {
+ // We have a leading space
+ if (afterRun) {
+ if (afterRun->mType & WSType::trailingWS) {
+ theString.SetCharAt(nbsp, lastCharIndex);
+ } else if (afterRun->mType & WSType::normalWS) {
+ WSPoint wspoint = GetCharAfter(*aInOutParent, *aInOutOffset);
+ if (wspoint.mTextNode && nsCRT::IsAsciiSpace(wspoint.mChar)) {
+ theString.SetCharAt(nbsp, lastCharIndex);
+ }
+ }
+ } else if (mEndReason & WSType::block) {
+ theString.SetCharAt(nbsp, lastCharIndex);
+ }
+ }
+
+ // Next, scan string for adjacent ws and convert to nbsp/space combos
+ // MOOSE: don't need to convert tabs here since that is done by
+ // WillInsertText() before we are called. Eventually, all that logic will be
+ // pushed down into here and made more efficient.
+ bool prevWS = false;
+ for (uint32_t i = 0; i <= lastCharIndex; i++) {
+ if (nsCRT::IsAsciiSpace(theString[i])) {
+ if (prevWS) {
+ // i - 1 can't be negative because prevWS starts out false
+ theString.SetCharAt(nbsp, i - 1);
+ } else {
+ prevWS = true;
+ }
+ } else {
+ prevWS = false;
+ }
+ }
+
+ // Ready, aim, fire!
+ mHTMLEditor->InsertTextImpl(theString, aInOutParent, aInOutOffset, aDoc);
+ return NS_OK;
+}
+
+nsresult
+WSRunObject::DeleteWSBackward()
+{
+ WSPoint point = GetCharBefore(mNode, mOffset);
+ NS_ENSURE_TRUE(point.mTextNode, NS_OK); // nothing to delete
+
+ // Easy case, preformatted ws.
+ if (mPRE && (nsCRT::IsAsciiSpace(point.mChar) || point.mChar == nbsp)) {
+ return DeleteChars(point.mTextNode, point.mOffset,
+ point.mTextNode, point.mOffset + 1);
+ }
+
+ // Caller's job to ensure that previous char is really ws. If it is normal
+ // ws, we need to delete the whole run.
+ if (nsCRT::IsAsciiSpace(point.mChar)) {
+ RefPtr<Text> startNodeText, endNodeText;
+ int32_t startOffset, endOffset;
+ GetAsciiWSBounds(eBoth, point.mTextNode, point.mOffset + 1,
+ getter_AddRefs(startNodeText), &startOffset,
+ getter_AddRefs(endNodeText), &endOffset);
+
+ // adjust surrounding ws
+ nsCOMPtr<nsINode> startNode = startNodeText.get();
+ nsCOMPtr<nsINode> endNode = endNodeText.get();
+ nsresult rv =
+ WSRunObject::PrepareToDeleteRange(mHTMLEditor,
+ address_of(startNode), &startOffset,
+ address_of(endNode), &endOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // finally, delete that ws
+ return DeleteChars(startNode, startOffset, endNode, endOffset);
+ }
+
+ if (point.mChar == nbsp) {
+ nsCOMPtr<nsINode> node(point.mTextNode);
+ // adjust surrounding ws
+ int32_t startOffset = point.mOffset;
+ int32_t endOffset = point.mOffset + 1;
+ nsresult rv =
+ WSRunObject::PrepareToDeleteRange(mHTMLEditor,
+ address_of(node), &startOffset,
+ address_of(node), &endOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // finally, delete that ws
+ return DeleteChars(node, startOffset, node, endOffset);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+WSRunObject::DeleteWSForward()
+{
+ WSPoint point = GetCharAfter(mNode, mOffset);
+ NS_ENSURE_TRUE(point.mTextNode, NS_OK); // nothing to delete
+
+ // Easy case, preformatted ws.
+ if (mPRE && (nsCRT::IsAsciiSpace(point.mChar) || point.mChar == nbsp)) {
+ return DeleteChars(point.mTextNode, point.mOffset,
+ point.mTextNode, point.mOffset + 1);
+ }
+
+ // Caller's job to ensure that next char is really ws. If it is normal ws,
+ // we need to delete the whole run.
+ if (nsCRT::IsAsciiSpace(point.mChar)) {
+ RefPtr<Text> startNodeText, endNodeText;
+ int32_t startOffset, endOffset;
+ GetAsciiWSBounds(eBoth, point.mTextNode, point.mOffset + 1,
+ getter_AddRefs(startNodeText), &startOffset,
+ getter_AddRefs(endNodeText), &endOffset);
+
+ // Adjust surrounding ws
+ nsCOMPtr<nsINode> startNode(startNodeText), endNode(endNodeText);
+ nsresult rv =
+ WSRunObject::PrepareToDeleteRange(mHTMLEditor,
+ address_of(startNode), &startOffset,
+ address_of(endNode), &endOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Finally, delete that ws
+ return DeleteChars(startNode, startOffset, endNode, endOffset);
+ }
+
+ if (point.mChar == nbsp) {
+ nsCOMPtr<nsINode> node(point.mTextNode);
+ // Adjust surrounding ws
+ int32_t startOffset = point.mOffset;
+ int32_t endOffset = point.mOffset+1;
+ nsresult rv =
+ WSRunObject::PrepareToDeleteRange(mHTMLEditor,
+ address_of(node), &startOffset,
+ address_of(node), &endOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Finally, delete that ws
+ return DeleteChars(node, startOffset, node, endOffset);
+ }
+
+ return NS_OK;
+}
+
+void
+WSRunObject::PriorVisibleNode(nsINode* aNode,
+ int32_t aOffset,
+ nsCOMPtr<nsINode>* outVisNode,
+ int32_t* outVisOffset,
+ WSType* outType)
+{
+ // Find first visible thing before the point. Position
+ // outVisNode/outVisOffset just _after_ that thing. If we don't find
+ // anything return start of ws.
+ MOZ_ASSERT(aNode && outVisNode && outVisOffset && outType);
+
+ WSFragment* run;
+ FindRun(aNode, aOffset, &run, false);
+
+ // Is there a visible run there or earlier?
+ for (; run; run = run->mLeft) {
+ if (run->mType == WSType::normalWS) {
+ WSPoint point = GetCharBefore(aNode, aOffset);
+ // When it's a non-empty text node, return it.
+ if (point.mTextNode && point.mTextNode->Length()) {
+ *outVisNode = point.mTextNode;
+ *outVisOffset = point.mOffset + 1;
+ if (nsCRT::IsAsciiSpace(point.mChar) || point.mChar == nbsp) {
+ *outType = WSType::normalWS;
+ } else {
+ *outType = WSType::text;
+ }
+ return;
+ }
+ // If no text node, keep looking. We should eventually fall out of loop
+ }
+ }
+
+ // If we get here, then nothing in ws data to find. Return start reason.
+ *outVisNode = mStartReasonNode;
+ // This really isn't meaningful if mStartReasonNode != mStartNode
+ *outVisOffset = mStartOffset;
+ *outType = mStartReason;
+}
+
+
+void
+WSRunObject::NextVisibleNode(nsINode* aNode,
+ int32_t aOffset,
+ nsCOMPtr<nsINode>* outVisNode,
+ int32_t* outVisOffset,
+ WSType* outType)
+{
+ // Find first visible thing after the point. Position
+ // outVisNode/outVisOffset just _before_ that thing. If we don't find
+ // anything return end of ws.
+ MOZ_ASSERT(aNode && outVisNode && outVisOffset && outType);
+
+ WSFragment* run;
+ FindRun(aNode, aOffset, &run, true);
+
+ // Is there a visible run there or later?
+ for (; run; run = run->mRight) {
+ if (run->mType == WSType::normalWS) {
+ WSPoint point = GetCharAfter(aNode, aOffset);
+ // When it's a non-empty text node, return it.
+ if (point.mTextNode && point.mTextNode->Length()) {
+ *outVisNode = point.mTextNode;
+ *outVisOffset = point.mOffset;
+ if (nsCRT::IsAsciiSpace(point.mChar) || point.mChar == nbsp) {
+ *outType = WSType::normalWS;
+ } else {
+ *outType = WSType::text;
+ }
+ return;
+ }
+ // If no text node, keep looking. We should eventually fall out of loop
+ }
+ }
+
+ // If we get here, then nothing in ws data to find. Return end reason
+ *outVisNode = mEndReasonNode;
+ // This really isn't meaningful if mEndReasonNode != mEndNode
+ *outVisOffset = mEndOffset;
+ *outType = mEndReason;
+}
+
+nsresult
+WSRunObject::AdjustWhitespace()
+{
+ // this routine examines a run of ws and tries to get rid of some unneeded nbsp's,
+ // replacing them with regualr ascii space if possible. Keeping things simple
+ // for now and just trying to fix up the trailing ws in the run.
+ if (!mLastNBSPNode) {
+ // nothing to do!
+ return NS_OK;
+ }
+ WSFragment *curRun = mStartRun;
+ while (curRun) {
+ // look for normal ws run
+ if (curRun->mType == WSType::normalWS) {
+ nsresult rv = CheckTrailingNBSPOfRun(curRun);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+ curRun = curRun->mRight;
+ }
+ return NS_OK;
+}
+
+
+//--------------------------------------------------------------------------------------------
+// protected methods
+//--------------------------------------------------------------------------------------------
+
+nsINode*
+WSRunObject::GetWSBoundingParent()
+{
+ NS_ENSURE_TRUE(mNode, nullptr);
+ OwningNonNull<nsINode> wsBoundingParent = *mNode;
+ while (!IsBlockNode(wsBoundingParent)) {
+ nsCOMPtr<nsINode> parent = wsBoundingParent->GetParentNode();
+ if (!parent || !mHTMLEditor->IsEditable(parent)) {
+ break;
+ }
+ wsBoundingParent = parent;
+ }
+ return wsBoundingParent;
+}
+
+nsresult
+WSRunObject::GetWSNodes()
+{
+ // collect up an array of nodes that are contiguous with the insertion point
+ // and which contain only whitespace. Stop if you reach non-ws text or a new
+ // block boundary.
+ EditorDOMPoint start(mNode, mOffset), end(mNode, mOffset);
+ nsCOMPtr<nsINode> wsBoundingParent = GetWSBoundingParent();
+
+ // first look backwards to find preceding ws nodes
+ if (RefPtr<Text> textNode = mNode->GetAsText()) {
+ const nsTextFragment* textFrag = textNode->GetText();
+
+ mNodeArray.InsertElementAt(0, textNode);
+ if (mOffset) {
+ for (int32_t pos = mOffset - 1; pos >= 0; pos--) {
+ // sanity bounds check the char position. bug 136165
+ if (uint32_t(pos) >= textFrag->GetLength()) {
+ NS_NOTREACHED("looking beyond end of text fragment");
+ continue;
+ }
+ char16_t theChar = textFrag->CharAt(pos);
+ if (!nsCRT::IsAsciiSpace(theChar)) {
+ if (theChar != nbsp) {
+ mStartNode = textNode;
+ mStartOffset = pos + 1;
+ mStartReason = WSType::text;
+ mStartReasonNode = textNode;
+ break;
+ }
+ // as we look backwards update our earliest found nbsp
+ mFirstNBSPNode = textNode;
+ mFirstNBSPOffset = pos;
+ // also keep track of latest nbsp so far
+ if (!mLastNBSPNode) {
+ mLastNBSPNode = textNode;
+ mLastNBSPOffset = pos;
+ }
+ }
+ start.node = textNode;
+ start.offset = pos;
+ }
+ }
+ }
+
+ while (!mStartNode) {
+ // we haven't found the start of ws yet. Keep looking
+ nsCOMPtr<nsIContent> priorNode = GetPreviousWSNode(start, wsBoundingParent);
+ if (priorNode) {
+ if (IsBlockNode(priorNode)) {
+ mStartNode = start.node;
+ mStartOffset = start.offset;
+ mStartReason = WSType::otherBlock;
+ mStartReasonNode = priorNode;
+ } else if (RefPtr<Text> textNode = priorNode->GetAsText()) {
+ mNodeArray.InsertElementAt(0, textNode);
+ const nsTextFragment *textFrag;
+ if (!textNode || !(textFrag = textNode->GetText())) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ uint32_t len = textNode->TextLength();
+
+ if (len < 1) {
+ // Zero length text node. Set start point to it
+ // so we can get past it!
+ start.SetPoint(priorNode, 0);
+ } else {
+ for (int32_t pos = len - 1; pos >= 0; pos--) {
+ // sanity bounds check the char position. bug 136165
+ if (uint32_t(pos) >= textFrag->GetLength()) {
+ NS_NOTREACHED("looking beyond end of text fragment");
+ continue;
+ }
+ char16_t theChar = textFrag->CharAt(pos);
+ if (!nsCRT::IsAsciiSpace(theChar)) {
+ if (theChar != nbsp) {
+ mStartNode = textNode;
+ mStartOffset = pos + 1;
+ mStartReason = WSType::text;
+ mStartReasonNode = textNode;
+ break;
+ }
+ // as we look backwards update our earliest found nbsp
+ mFirstNBSPNode = textNode;
+ mFirstNBSPOffset = pos;
+ // also keep track of latest nbsp so far
+ if (!mLastNBSPNode) {
+ mLastNBSPNode = textNode;
+ mLastNBSPOffset = pos;
+ }
+ }
+ start.SetPoint(textNode, pos);
+ }
+ }
+ } else {
+ // it's a break or a special node, like <img>, that is not a block and not
+ // a break but still serves as a terminator to ws runs.
+ mStartNode = start.node;
+ mStartOffset = start.offset;
+ if (TextEditUtils::IsBreak(priorNode)) {
+ mStartReason = WSType::br;
+ } else {
+ mStartReason = WSType::special;
+ }
+ mStartReasonNode = priorNode;
+ }
+ } else {
+ // no prior node means we exhausted wsBoundingParent
+ mStartNode = start.node;
+ mStartOffset = start.offset;
+ mStartReason = WSType::thisBlock;
+ mStartReasonNode = wsBoundingParent;
+ }
+ }
+
+ // then look ahead to find following ws nodes
+ if (RefPtr<Text> textNode = mNode->GetAsText()) {
+ // don't need to put it on list. it already is from code above
+ const nsTextFragment *textFrag = textNode->GetText();
+
+ uint32_t len = textNode->TextLength();
+ if (uint16_t(mOffset)<len) {
+ for (uint32_t pos = mOffset; pos < len; pos++) {
+ // sanity bounds check the char position. bug 136165
+ if (pos >= textFrag->GetLength()) {
+ NS_NOTREACHED("looking beyond end of text fragment");
+ continue;
+ }
+ char16_t theChar = textFrag->CharAt(pos);
+ if (!nsCRT::IsAsciiSpace(theChar)) {
+ if (theChar != nbsp) {
+ mEndNode = textNode;
+ mEndOffset = pos;
+ mEndReason = WSType::text;
+ mEndReasonNode = textNode;
+ break;
+ }
+ // as we look forwards update our latest found nbsp
+ mLastNBSPNode = textNode;
+ mLastNBSPOffset = pos;
+ // also keep track of earliest nbsp so far
+ if (!mFirstNBSPNode) {
+ mFirstNBSPNode = textNode;
+ mFirstNBSPOffset = pos;
+ }
+ }
+ end.SetPoint(textNode, pos + 1);
+ }
+ }
+ }
+
+ while (!mEndNode) {
+ // we haven't found the end of ws yet. Keep looking
+ nsCOMPtr<nsIContent> nextNode = GetNextWSNode(end, wsBoundingParent);
+ if (nextNode) {
+ if (IsBlockNode(nextNode)) {
+ // we encountered a new block. therefore no more ws.
+ mEndNode = end.node;
+ mEndOffset = end.offset;
+ mEndReason = WSType::otherBlock;
+ mEndReasonNode = nextNode;
+ } else if (RefPtr<Text> textNode = nextNode->GetAsText()) {
+ mNodeArray.AppendElement(textNode);
+ const nsTextFragment *textFrag;
+ if (!textNode || !(textFrag = textNode->GetText())) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ uint32_t len = textNode->TextLength();
+
+ if (len < 1) {
+ // Zero length text node. Set end point to it
+ // so we can get past it!
+ end.SetPoint(textNode, 0);
+ } else {
+ for (uint32_t pos = 0; pos < len; pos++) {
+ // sanity bounds check the char position. bug 136165
+ if (pos >= textFrag->GetLength()) {
+ NS_NOTREACHED("looking beyond end of text fragment");
+ continue;
+ }
+ char16_t theChar = textFrag->CharAt(pos);
+ if (!nsCRT::IsAsciiSpace(theChar)) {
+ if (theChar != nbsp) {
+ mEndNode = textNode;
+ mEndOffset = pos;
+ mEndReason = WSType::text;
+ mEndReasonNode = textNode;
+ break;
+ }
+ // as we look forwards update our latest found nbsp
+ mLastNBSPNode = textNode;
+ mLastNBSPOffset = pos;
+ // also keep track of earliest nbsp so far
+ if (!mFirstNBSPNode) {
+ mFirstNBSPNode = textNode;
+ mFirstNBSPOffset = pos;
+ }
+ }
+ end.SetPoint(textNode, pos + 1);
+ }
+ }
+ } else {
+ // we encountered a break or a special node, like <img>,
+ // that is not a block and not a break but still
+ // serves as a terminator to ws runs.
+ mEndNode = end.node;
+ mEndOffset = end.offset;
+ if (TextEditUtils::IsBreak(nextNode)) {
+ mEndReason = WSType::br;
+ } else {
+ mEndReason = WSType::special;
+ }
+ mEndReasonNode = nextNode;
+ }
+ } else {
+ // no next node means we exhausted wsBoundingParent
+ mEndNode = end.node;
+ mEndOffset = end.offset;
+ mEndReason = WSType::thisBlock;
+ mEndReasonNode = wsBoundingParent;
+ }
+ }
+
+ return NS_OK;
+}
+
+void
+WSRunObject::GetRuns()
+{
+ ClearRuns();
+
+ // handle some easy cases first
+ mHTMLEditor->IsPreformatted(GetAsDOMNode(mNode), &mPRE);
+ // if it's preformatedd, or if we are surrounded by text or special, it's all one
+ // big normal ws run
+ if (mPRE ||
+ ((mStartReason == WSType::text || mStartReason == WSType::special) &&
+ (mEndReason == WSType::text || mEndReason == WSType::special ||
+ mEndReason == WSType::br))) {
+ MakeSingleWSRun(WSType::normalWS);
+ return;
+ }
+
+ // if we are before or after a block (or after a break), and there are no nbsp's,
+ // then it's all non-rendering ws.
+ if (!mFirstNBSPNode && !mLastNBSPNode &&
+ ((mStartReason & WSType::block) || mStartReason == WSType::br ||
+ (mEndReason & WSType::block))) {
+ WSType wstype;
+ if ((mStartReason & WSType::block) || mStartReason == WSType::br) {
+ wstype = WSType::leadingWS;
+ }
+ if (mEndReason & WSType::block) {
+ wstype |= WSType::trailingWS;
+ }
+ MakeSingleWSRun(wstype);
+ return;
+ }
+
+ // otherwise a little trickier. shucks.
+ mStartRun = new WSFragment();
+ mStartRun->mStartNode = mStartNode;
+ mStartRun->mStartOffset = mStartOffset;
+
+ if (mStartReason & WSType::block || mStartReason == WSType::br) {
+ // set up mStartRun
+ mStartRun->mType = WSType::leadingWS;
+ mStartRun->mEndNode = mFirstNBSPNode;
+ mStartRun->mEndOffset = mFirstNBSPOffset;
+ mStartRun->mLeftType = mStartReason;
+ mStartRun->mRightType = WSType::normalWS;
+
+ // set up next run
+ WSFragment *normalRun = new WSFragment();
+ mStartRun->mRight = normalRun;
+ normalRun->mType = WSType::normalWS;
+ normalRun->mStartNode = mFirstNBSPNode;
+ normalRun->mStartOffset = mFirstNBSPOffset;
+ normalRun->mLeftType = WSType::leadingWS;
+ normalRun->mLeft = mStartRun;
+ if (mEndReason != WSType::block) {
+ // then no trailing ws. this normal run ends the overall ws run.
+ normalRun->mRightType = mEndReason;
+ normalRun->mEndNode = mEndNode;
+ normalRun->mEndOffset = mEndOffset;
+ mEndRun = normalRun;
+ } else {
+ // we might have trailing ws.
+ // it so happens that *if* there is an nbsp at end, {mEndNode,mEndOffset-1}
+ // will point to it, even though in general start/end points not
+ // guaranteed to be in text nodes.
+ if (mLastNBSPNode == mEndNode && mLastNBSPOffset == mEndOffset - 1) {
+ // normal ws runs right up to adjacent block (nbsp next to block)
+ normalRun->mRightType = mEndReason;
+ normalRun->mEndNode = mEndNode;
+ normalRun->mEndOffset = mEndOffset;
+ mEndRun = normalRun;
+ } else {
+ normalRun->mEndNode = mLastNBSPNode;
+ normalRun->mEndOffset = mLastNBSPOffset+1;
+ normalRun->mRightType = WSType::trailingWS;
+
+ // set up next run
+ WSFragment *lastRun = new WSFragment();
+ lastRun->mType = WSType::trailingWS;
+ lastRun->mStartNode = mLastNBSPNode;
+ lastRun->mStartOffset = mLastNBSPOffset+1;
+ lastRun->mEndNode = mEndNode;
+ lastRun->mEndOffset = mEndOffset;
+ lastRun->mLeftType = WSType::normalWS;
+ lastRun->mLeft = normalRun;
+ lastRun->mRightType = mEndReason;
+ mEndRun = lastRun;
+ normalRun->mRight = lastRun;
+ }
+ }
+ } else {
+ // mStartReason is not WSType::block or WSType::br; set up mStartRun
+ mStartRun->mType = WSType::normalWS;
+ mStartRun->mEndNode = mLastNBSPNode;
+ mStartRun->mEndOffset = mLastNBSPOffset+1;
+ mStartRun->mLeftType = mStartReason;
+
+ // we might have trailing ws.
+ // it so happens that *if* there is an nbsp at end, {mEndNode,mEndOffset-1}
+ // will point to it, even though in general start/end points not
+ // guaranteed to be in text nodes.
+ if (mLastNBSPNode == mEndNode && mLastNBSPOffset == (mEndOffset - 1)) {
+ mStartRun->mRightType = mEndReason;
+ mStartRun->mEndNode = mEndNode;
+ mStartRun->mEndOffset = mEndOffset;
+ mEndRun = mStartRun;
+ } else {
+ // set up next run
+ WSFragment *lastRun = new WSFragment();
+ lastRun->mType = WSType::trailingWS;
+ lastRun->mStartNode = mLastNBSPNode;
+ lastRun->mStartOffset = mLastNBSPOffset+1;
+ lastRun->mLeftType = WSType::normalWS;
+ lastRun->mLeft = mStartRun;
+ lastRun->mRightType = mEndReason;
+ mEndRun = lastRun;
+ mStartRun->mRight = lastRun;
+ mStartRun->mRightType = WSType::trailingWS;
+ }
+ }
+}
+
+void
+WSRunObject::ClearRuns()
+{
+ WSFragment *tmp, *run;
+ run = mStartRun;
+ while (run) {
+ tmp = run->mRight;
+ delete run;
+ run = tmp;
+ }
+ mStartRun = 0;
+ mEndRun = 0;
+}
+
+void
+WSRunObject::MakeSingleWSRun(WSType aType)
+{
+ mStartRun = new WSFragment();
+
+ mStartRun->mStartNode = mStartNode;
+ mStartRun->mStartOffset = mStartOffset;
+ mStartRun->mType = aType;
+ mStartRun->mEndNode = mEndNode;
+ mStartRun->mEndOffset = mEndOffset;
+ mStartRun->mLeftType = mStartReason;
+ mStartRun->mRightType = mEndReason;
+
+ mEndRun = mStartRun;
+}
+
+nsIContent*
+WSRunObject::GetPreviousWSNodeInner(nsINode* aStartNode,
+ nsINode* aBlockParent)
+{
+ // Can't really recycle various getnext/prior routines because we have
+ // special needs here. Need to step into inline containers but not block
+ // containers.
+ MOZ_ASSERT(aStartNode && aBlockParent);
+
+ nsCOMPtr<nsIContent> priorNode = aStartNode->GetPreviousSibling();
+ OwningNonNull<nsINode> curNode = *aStartNode;
+ while (!priorNode) {
+ // We have exhausted nodes in parent of aStartNode.
+ nsCOMPtr<nsINode> curParent = curNode->GetParentNode();
+ NS_ENSURE_TRUE(curParent, nullptr);
+ if (curParent == aBlockParent) {
+ // We have exhausted nodes in the block parent. The convention here is
+ // to return null.
+ return nullptr;
+ }
+ // We have a parent: look for previous sibling
+ priorNode = curParent->GetPreviousSibling();
+ curNode = curParent;
+ }
+ // We have a prior node. If it's a block, return it.
+ if (IsBlockNode(priorNode)) {
+ return priorNode;
+ }
+ if (mHTMLEditor->IsContainer(priorNode)) {
+ // Else if it's a container, get deep rightmost child
+ nsCOMPtr<nsIContent> child = mHTMLEditor->GetRightmostChild(priorNode);
+ if (child) {
+ return child;
+ }
+ }
+ // Else return the node itself
+ return priorNode;
+}
+
+nsIContent*
+WSRunObject::GetPreviousWSNode(EditorDOMPoint aPoint,
+ nsINode* aBlockParent)
+{
+ // Can't really recycle various getnext/prior routines because we
+ // have special needs here. Need to step into inline containers but
+ // not block containers.
+ MOZ_ASSERT(aPoint.node && aBlockParent);
+
+ if (aPoint.node->NodeType() == nsIDOMNode::TEXT_NODE) {
+ return GetPreviousWSNodeInner(aPoint.node, aBlockParent);
+ }
+ if (!mHTMLEditor->IsContainer(aPoint.node)) {
+ return GetPreviousWSNodeInner(aPoint.node, aBlockParent);
+ }
+
+ if (!aPoint.offset) {
+ if (aPoint.node == aBlockParent) {
+ // We are at start of the block.
+ return nullptr;
+ }
+
+ // We are at start of non-block container
+ return GetPreviousWSNodeInner(aPoint.node, aBlockParent);
+ }
+
+ nsCOMPtr<nsIContent> startContent = do_QueryInterface(aPoint.node);
+ NS_ENSURE_TRUE(startContent, nullptr);
+ nsCOMPtr<nsIContent> priorNode = startContent->GetChildAt(aPoint.offset - 1);
+ NS_ENSURE_TRUE(priorNode, nullptr);
+ // We have a prior node. If it's a block, return it.
+ if (IsBlockNode(priorNode)) {
+ return priorNode;
+ }
+ if (mHTMLEditor->IsContainer(priorNode)) {
+ // Else if it's a container, get deep rightmost child
+ nsCOMPtr<nsIContent> child = mHTMLEditor->GetRightmostChild(priorNode);
+ if (child) {
+ return child;
+ }
+ }
+ // Else return the node itself
+ return priorNode;
+}
+
+nsIContent*
+WSRunObject::GetNextWSNodeInner(nsINode* aStartNode,
+ nsINode* aBlockParent)
+{
+ // Can't really recycle various getnext/prior routines because we have
+ // special needs here. Need to step into inline containers but not block
+ // containers.
+ MOZ_ASSERT(aStartNode && aBlockParent);
+
+ nsCOMPtr<nsIContent> nextNode = aStartNode->GetNextSibling();
+ nsCOMPtr<nsINode> curNode = aStartNode;
+ while (!nextNode) {
+ // We have exhausted nodes in parent of aStartNode.
+ nsCOMPtr<nsINode> curParent = curNode->GetParentNode();
+ NS_ENSURE_TRUE(curParent, nullptr);
+ if (curParent == aBlockParent) {
+ // We have exhausted nodes in the block parent. The convention here is
+ // to return null.
+ return nullptr;
+ }
+ // We have a parent: look for next sibling
+ nextNode = curParent->GetNextSibling();
+ curNode = curParent;
+ }
+ // We have a next node. If it's a block, return it.
+ if (IsBlockNode(nextNode)) {
+ return nextNode;
+ }
+ if (mHTMLEditor->IsContainer(nextNode)) {
+ // Else if it's a container, get deep leftmost child
+ nsCOMPtr<nsIContent> child = mHTMLEditor->GetLeftmostChild(nextNode);
+ if (child) {
+ return child;
+ }
+ }
+ // Else return the node itself
+ return nextNode;
+}
+
+nsIContent*
+WSRunObject::GetNextWSNode(EditorDOMPoint aPoint,
+ nsINode* aBlockParent)
+{
+ // Can't really recycle various getnext/prior routines because we have
+ // special needs here. Need to step into inline containers but not block
+ // containers.
+ MOZ_ASSERT(aPoint.node && aBlockParent);
+
+ if (aPoint.node->NodeType() == nsIDOMNode::TEXT_NODE) {
+ return GetNextWSNodeInner(aPoint.node, aBlockParent);
+ }
+ if (!mHTMLEditor->IsContainer(aPoint.node)) {
+ return GetNextWSNodeInner(aPoint.node, aBlockParent);
+ }
+
+ nsCOMPtr<nsIContent> startContent = do_QueryInterface(aPoint.node);
+ NS_ENSURE_TRUE(startContent, nullptr);
+
+ nsCOMPtr<nsIContent> nextNode = startContent->GetChildAt(aPoint.offset);
+ if (!nextNode) {
+ if (aPoint.node == aBlockParent) {
+ // We are at end of the block.
+ return nullptr;
+ }
+
+ // We are at end of non-block container
+ return GetNextWSNodeInner(aPoint.node, aBlockParent);
+ }
+
+ // We have a next node. If it's a block, return it.
+ if (IsBlockNode(nextNode)) {
+ return nextNode;
+ }
+ if (mHTMLEditor->IsContainer(nextNode)) {
+ // else if it's a container, get deep leftmost child
+ nsCOMPtr<nsIContent> child = mHTMLEditor->GetLeftmostChild(nextNode);
+ if (child) {
+ return child;
+ }
+ }
+ // Else return the node itself
+ return nextNode;
+}
+
+nsresult
+WSRunObject::PrepareToDeleteRangePriv(WSRunObject* aEndObject)
+{
+ // this routine adjust whitespace before *this* and after aEndObject
+ // in preperation for the two areas to become adjacent after the
+ // intervening content is deleted. It's overly agressive right
+ // now. There might be a block boundary remaining between them after
+ // the deletion, in which case these adjstments are unneeded (though
+ // I don't think they can ever be harmful?)
+
+ NS_ENSURE_TRUE(aEndObject, NS_ERROR_NULL_POINTER);
+
+ // get the runs before and after selection
+ WSFragment *beforeRun, *afterRun;
+ FindRun(mNode, mOffset, &beforeRun, false);
+ aEndObject->FindRun(aEndObject->mNode, aEndObject->mOffset, &afterRun, true);
+
+ // trim after run of any leading ws
+ if (afterRun && (afterRun->mType & WSType::leadingWS)) {
+ nsresult rv =
+ aEndObject->DeleteChars(aEndObject->mNode, aEndObject->mOffset,
+ afterRun->mEndNode, afterRun->mEndOffset,
+ eOutsideUserSelectAll);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // adjust normal ws in afterRun if needed
+ if (afterRun && afterRun->mType == WSType::normalWS && !aEndObject->mPRE) {
+ if ((beforeRun && (beforeRun->mType & WSType::leadingWS)) ||
+ (!beforeRun && ((mStartReason & WSType::block) ||
+ mStartReason == WSType::br))) {
+ // make sure leading char of following ws is an nbsp, so that it will show up
+ WSPoint point = aEndObject->GetCharAfter(aEndObject->mNode,
+ aEndObject->mOffset);
+ if (point.mTextNode && nsCRT::IsAsciiSpace(point.mChar)) {
+ nsresult rv = aEndObject->ConvertToNBSP(point, eOutsideUserSelectAll);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ }
+ // trim before run of any trailing ws
+ if (beforeRun && (beforeRun->mType & WSType::trailingWS)) {
+ nsresult rv = DeleteChars(beforeRun->mStartNode, beforeRun->mStartOffset,
+ mNode, mOffset, eOutsideUserSelectAll);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else if (beforeRun && beforeRun->mType == WSType::normalWS && !mPRE) {
+ if ((afterRun && (afterRun->mType & WSType::trailingWS)) ||
+ (afterRun && afterRun->mType == WSType::normalWS) ||
+ (!afterRun && (aEndObject->mEndReason & WSType::block))) {
+ // make sure trailing char of starting ws is an nbsp, so that it will show up
+ WSPoint point = GetCharBefore(mNode, mOffset);
+ if (point.mTextNode && nsCRT::IsAsciiSpace(point.mChar)) {
+ RefPtr<Text> wsStartNode, wsEndNode;
+ int32_t wsStartOffset, wsEndOffset;
+ GetAsciiWSBounds(eBoth, mNode, mOffset,
+ getter_AddRefs(wsStartNode), &wsStartOffset,
+ getter_AddRefs(wsEndNode), &wsEndOffset);
+ point.mTextNode = wsStartNode;
+ point.mOffset = wsStartOffset;
+ nsresult rv = ConvertToNBSP(point, eOutsideUserSelectAll);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+WSRunObject::PrepareToSplitAcrossBlocksPriv()
+{
+ // used to prepare ws to be split across two blocks. The main issue
+ // here is make sure normalWS doesn't end up becoming non-significant
+ // leading or trailing ws after the split.
+
+ // get the runs before and after selection
+ WSFragment *beforeRun, *afterRun;
+ FindRun(mNode, mOffset, &beforeRun, false);
+ FindRun(mNode, mOffset, &afterRun, true);
+
+ // adjust normal ws in afterRun if needed
+ if (afterRun && afterRun->mType == WSType::normalWS) {
+ // make sure leading char of following ws is an nbsp, so that it will show up
+ WSPoint point = GetCharAfter(mNode, mOffset);
+ if (point.mTextNode && nsCRT::IsAsciiSpace(point.mChar)) {
+ nsresult rv = ConvertToNBSP(point);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ // adjust normal ws in beforeRun if needed
+ if (beforeRun && beforeRun->mType == WSType::normalWS) {
+ // make sure trailing char of starting ws is an nbsp, so that it will show up
+ WSPoint point = GetCharBefore(mNode, mOffset);
+ if (point.mTextNode && nsCRT::IsAsciiSpace(point.mChar)) {
+ RefPtr<Text> wsStartNode, wsEndNode;
+ int32_t wsStartOffset, wsEndOffset;
+ GetAsciiWSBounds(eBoth, mNode, mOffset,
+ getter_AddRefs(wsStartNode), &wsStartOffset,
+ getter_AddRefs(wsEndNode), &wsEndOffset);
+ point.mTextNode = wsStartNode;
+ point.mOffset = wsStartOffset;
+ nsresult rv = ConvertToNBSP(point);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+WSRunObject::DeleteChars(nsINode* aStartNode,
+ int32_t aStartOffset,
+ nsINode* aEndNode,
+ int32_t aEndOffset,
+ AreaRestriction aAR)
+{
+ // MOOSE: this routine needs to be modified to preserve the integrity of the
+ // wsFragment info.
+ NS_ENSURE_TRUE(aStartNode && aEndNode, NS_ERROR_NULL_POINTER);
+
+ if (aAR == eOutsideUserSelectAll) {
+ nsCOMPtr<nsIDOMNode> san =
+ mHTMLEditor->FindUserSelectAllNode(GetAsDOMNode(aStartNode));
+ if (san) {
+ return NS_OK;
+ }
+
+ if (aStartNode != aEndNode) {
+ san = mHTMLEditor->FindUserSelectAllNode(GetAsDOMNode(aEndNode));
+ if (san) {
+ return NS_OK;
+ }
+ }
+ }
+
+ if (aStartNode == aEndNode && aStartOffset == aEndOffset) {
+ // Nothing to delete
+ return NS_OK;
+ }
+
+ int32_t idx = mNodeArray.IndexOf(aStartNode);
+ if (idx == -1) {
+ // If our strarting point wasn't one of our ws text nodes, then just go
+ // through them from the beginning.
+ idx = 0;
+ }
+
+ if (aStartNode == aEndNode && aStartNode->GetAsText()) {
+ return mHTMLEditor->DeleteText(*aStartNode->GetAsText(),
+ static_cast<uint32_t>(aStartOffset),
+ static_cast<uint32_t>(aEndOffset - aStartOffset));
+ }
+
+ RefPtr<nsRange> range;
+ int32_t count = mNodeArray.Length();
+ for (; idx < count; idx++) {
+ RefPtr<Text> node = mNodeArray[idx];
+ if (!node) {
+ // We ran out of ws nodes; must have been deleting to end
+ return NS_OK;
+ }
+ if (node == aStartNode) {
+ uint32_t len = node->Length();
+ if (uint32_t(aStartOffset) < len) {
+ nsresult rv =
+ mHTMLEditor->DeleteText(*node, AssertedCast<uint32_t>(aStartOffset),
+ len - aStartOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ } else if (node == aEndNode) {
+ if (aEndOffset) {
+ nsresult rv =
+ mHTMLEditor->DeleteText(*node, 0, AssertedCast<uint32_t>(aEndOffset));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ break;
+ } else {
+ if (!range) {
+ range = new nsRange(aStartNode);
+ nsresult rv =
+ range->Set(aStartNode, aStartOffset, aEndNode, aEndOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ bool nodeBefore, nodeAfter;
+ nsresult rv =
+ nsRange::CompareNodeToRange(node, range, &nodeBefore, &nodeAfter);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (nodeAfter) {
+ break;
+ }
+ if (!nodeBefore) {
+ rv = mHTMLEditor->DeleteNode(node);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mNodeArray.RemoveElement(node);
+ --count;
+ --idx;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+WSRunObject::WSPoint
+WSRunObject::GetCharAfter(nsINode* aNode,
+ int32_t aOffset)
+{
+ MOZ_ASSERT(aNode);
+
+ int32_t idx = mNodeArray.IndexOf(aNode);
+ if (idx == -1) {
+ // Use range comparisons to get right ws node
+ return GetWSPointAfter(aNode, aOffset);
+ }
+ // Use WSPoint version of GetCharAfter()
+ return GetCharAfter(WSPoint(mNodeArray[idx], aOffset, 0));
+}
+
+WSRunObject::WSPoint
+WSRunObject::GetCharBefore(nsINode* aNode,
+ int32_t aOffset)
+{
+ MOZ_ASSERT(aNode);
+
+ int32_t idx = mNodeArray.IndexOf(aNode);
+ if (idx == -1) {
+ // Use range comparisons to get right ws node
+ return GetWSPointBefore(aNode, aOffset);
+ }
+ // Use WSPoint version of GetCharBefore()
+ return GetCharBefore(WSPoint(mNodeArray[idx], aOffset, 0));
+}
+
+WSRunObject::WSPoint
+WSRunObject::GetCharAfter(const WSPoint &aPoint)
+{
+ MOZ_ASSERT(aPoint.mTextNode);
+
+ WSPoint outPoint;
+ outPoint.mTextNode = nullptr;
+ outPoint.mOffset = 0;
+ outPoint.mChar = 0;
+
+ int32_t idx = mNodeArray.IndexOf(aPoint.mTextNode);
+ if (idx == -1) {
+ // Can't find point, but it's not an error
+ return outPoint;
+ }
+
+ if (static_cast<uint16_t>(aPoint.mOffset) < aPoint.mTextNode->TextLength()) {
+ outPoint = aPoint;
+ outPoint.mChar = GetCharAt(aPoint.mTextNode, aPoint.mOffset);
+ return outPoint;
+ }
+
+ int32_t numNodes = mNodeArray.Length();
+ if (idx + 1 < numNodes) {
+ outPoint.mTextNode = mNodeArray[idx + 1];
+ MOZ_ASSERT(outPoint.mTextNode);
+ outPoint.mOffset = 0;
+ outPoint.mChar = GetCharAt(outPoint.mTextNode, 0);
+ }
+
+ return outPoint;
+}
+
+WSRunObject::WSPoint
+WSRunObject::GetCharBefore(const WSPoint &aPoint)
+{
+ MOZ_ASSERT(aPoint.mTextNode);
+
+ WSPoint outPoint;
+ outPoint.mTextNode = nullptr;
+ outPoint.mOffset = 0;
+ outPoint.mChar = 0;
+
+ int32_t idx = mNodeArray.IndexOf(aPoint.mTextNode);
+ if (idx == -1) {
+ // Can't find point, but it's not an error
+ return outPoint;
+ }
+
+ if (aPoint.mOffset) {
+ outPoint = aPoint;
+ outPoint.mOffset--;
+ outPoint.mChar = GetCharAt(aPoint.mTextNode, aPoint.mOffset - 1);
+ return outPoint;
+ }
+
+ if (idx) {
+ outPoint.mTextNode = mNodeArray[idx - 1];
+
+ uint32_t len = outPoint.mTextNode->TextLength();
+ if (len) {
+ outPoint.mOffset = len - 1;
+ outPoint.mChar = GetCharAt(outPoint.mTextNode, len - 1);
+ }
+ }
+ return outPoint;
+}
+
+nsresult
+WSRunObject::ConvertToNBSP(WSPoint aPoint, AreaRestriction aAR)
+{
+ // MOOSE: this routine needs to be modified to preserve the integrity of the
+ // wsFragment info.
+ NS_ENSURE_TRUE(aPoint.mTextNode, NS_ERROR_NULL_POINTER);
+
+ if (aAR == eOutsideUserSelectAll) {
+ nsCOMPtr<nsIDOMNode> san =
+ mHTMLEditor->FindUserSelectAllNode(GetAsDOMNode(aPoint.mTextNode));
+ if (san) {
+ return NS_OK;
+ }
+ }
+
+ // First, insert an nbsp
+ AutoTransactionsConserveSelection dontSpazMySelection(mHTMLEditor);
+ nsAutoString nbspStr(nbsp);
+ nsresult rv =
+ mHTMLEditor->InsertTextIntoTextNodeImpl(nbspStr, *aPoint.mTextNode,
+ aPoint.mOffset, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Next, find range of ws it will replace
+ RefPtr<Text> startNode, endNode;
+ int32_t startOffset = 0, endOffset = 0;
+
+ GetAsciiWSBounds(eAfter, aPoint.mTextNode, aPoint.mOffset + 1,
+ getter_AddRefs(startNode), &startOffset,
+ getter_AddRefs(endNode), &endOffset);
+
+ // Finally, delete that replaced ws, if any
+ if (startNode) {
+ rv = DeleteChars(startNode, startOffset, endNode, endOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+void
+WSRunObject::GetAsciiWSBounds(int16_t aDir,
+ nsINode* aNode,
+ int32_t aOffset,
+ Text** outStartNode,
+ int32_t* outStartOffset,
+ Text** outEndNode,
+ int32_t* outEndOffset)
+{
+ MOZ_ASSERT(aNode && outStartNode && outStartOffset && outEndNode &&
+ outEndOffset);
+
+ RefPtr<Text> startNode, endNode;
+ int32_t startOffset = 0, endOffset = 0;
+
+ if (aDir & eAfter) {
+ WSPoint point = GetCharAfter(aNode, aOffset);
+ if (point.mTextNode) {
+ // We found a text node, at least
+ startNode = endNode = point.mTextNode;
+ startOffset = endOffset = point.mOffset;
+
+ // Scan ahead to end of ASCII ws
+ for (; nsCRT::IsAsciiSpace(point.mChar) && point.mTextNode;
+ point = GetCharAfter(point)) {
+ endNode = point.mTextNode;
+ // endOffset is _after_ ws
+ point.mOffset++;
+ endOffset = point.mOffset;
+ }
+ }
+ }
+
+ if (aDir & eBefore) {
+ WSPoint point = GetCharBefore(aNode, aOffset);
+ if (point.mTextNode) {
+ // We found a text node, at least
+ startNode = point.mTextNode;
+ startOffset = point.mOffset + 1;
+ if (!endNode) {
+ endNode = startNode;
+ endOffset = startOffset;
+ }
+
+ // Scan back to start of ASCII ws
+ for (; nsCRT::IsAsciiSpace(point.mChar) && point.mTextNode;
+ point = GetCharBefore(point)) {
+ startNode = point.mTextNode;
+ startOffset = point.mOffset;
+ }
+ }
+ }
+
+ startNode.forget(outStartNode);
+ *outStartOffset = startOffset;
+ endNode.forget(outEndNode);
+ *outEndOffset = endOffset;
+}
+
+/**
+ * Given a dompoint, find the ws run that is before or after it, as caller
+ * needs
+ */
+void
+WSRunObject::FindRun(nsINode* aNode,
+ int32_t aOffset,
+ WSFragment** outRun,
+ bool after)
+{
+ MOZ_ASSERT(aNode && outRun);
+ *outRun = nullptr;
+
+ for (WSFragment* run = mStartRun; run; run = run->mRight) {
+ int32_t comp = run->mStartNode ? nsContentUtils::ComparePoints(aNode,
+ aOffset, run->mStartNode, run->mStartOffset) : -1;
+ if (comp <= 0) {
+ if (after) {
+ *outRun = run;
+ } else {
+ // before
+ *outRun = nullptr;
+ }
+ return;
+ }
+ comp = run->mEndNode ? nsContentUtils::ComparePoints(aNode, aOffset,
+ run->mEndNode, run->mEndOffset) : -1;
+ if (comp < 0) {
+ *outRun = run;
+ return;
+ } else if (!comp) {
+ if (after) {
+ *outRun = run->mRight;
+ } else {
+ // before
+ *outRun = run;
+ }
+ return;
+ }
+ if (!run->mRight) {
+ if (after) {
+ *outRun = nullptr;
+ } else {
+ // before
+ *outRun = run;
+ }
+ return;
+ }
+ }
+}
+
+char16_t
+WSRunObject::GetCharAt(Text* aTextNode,
+ int32_t aOffset)
+{
+ // return 0 if we can't get a char, for whatever reason
+ NS_ENSURE_TRUE(aTextNode, 0);
+
+ int32_t len = int32_t(aTextNode->TextLength());
+ if (aOffset < 0 || aOffset >= len) {
+ return 0;
+ }
+ return aTextNode->GetText()->CharAt(aOffset);
+}
+
+WSRunObject::WSPoint
+WSRunObject::GetWSPointAfter(nsINode* aNode,
+ int32_t aOffset)
+{
+ // Note: only to be called if aNode is not a ws node.
+
+ // Binary search on wsnodes
+ uint32_t numNodes = mNodeArray.Length();
+
+ if (!numNodes) {
+ // Do nothing if there are no nodes to search
+ WSPoint outPoint;
+ return outPoint;
+ }
+
+ uint32_t firstNum = 0, curNum = numNodes/2, lastNum = numNodes;
+ int16_t cmp = 0;
+ RefPtr<Text> curNode;
+
+ // Begin binary search. We do this because we need to minimize calls to
+ // ComparePoints(), which is expensive.
+ while (curNum != lastNum) {
+ curNode = mNodeArray[curNum];
+ cmp = nsContentUtils::ComparePoints(aNode, aOffset, curNode, 0);
+ if (cmp < 0) {
+ lastNum = curNum;
+ } else {
+ firstNum = curNum + 1;
+ }
+ curNum = (lastNum - firstNum)/2 + firstNum;
+ MOZ_ASSERT(firstNum <= curNum && curNum <= lastNum, "Bad binary search");
+ }
+
+ // When the binary search is complete, we always know that the current node
+ // is the same as the end node, which is always past our range. Therefore,
+ // we've found the node immediately after the point of interest.
+ if (curNum == mNodeArray.Length()) {
+ // hey asked for past our range (it's after the last node). GetCharAfter
+ // will do the work for us when we pass it the last index of the last node.
+ RefPtr<Text> textNode(mNodeArray[curNum - 1]);
+ WSPoint point(textNode, textNode->TextLength(), 0);
+ return GetCharAfter(point);
+ } else {
+ // The char after the point is the first character of our range.
+ RefPtr<Text> textNode(mNodeArray[curNum]);
+ WSPoint point(textNode, 0, 0);
+ return GetCharAfter(point);
+ }
+}
+
+WSRunObject::WSPoint
+WSRunObject::GetWSPointBefore(nsINode* aNode,
+ int32_t aOffset)
+{
+ // Note: only to be called if aNode is not a ws node.
+
+ // Binary search on wsnodes
+ uint32_t numNodes = mNodeArray.Length();
+
+ if (!numNodes) {
+ // Do nothing if there are no nodes to search
+ WSPoint outPoint;
+ return outPoint;
+ }
+
+ uint32_t firstNum = 0, curNum = numNodes/2, lastNum = numNodes;
+ int16_t cmp = 0;
+ RefPtr<Text> curNode;
+
+ // Begin binary search. We do this because we need to minimize calls to
+ // ComparePoints(), which is expensive.
+ while (curNum != lastNum) {
+ curNode = mNodeArray[curNum];
+ cmp = nsContentUtils::ComparePoints(aNode, aOffset, curNode, 0);
+ if (cmp < 0) {
+ lastNum = curNum;
+ } else {
+ firstNum = curNum + 1;
+ }
+ curNum = (lastNum - firstNum)/2 + firstNum;
+ MOZ_ASSERT(firstNum <= curNum && curNum <= lastNum, "Bad binary search");
+ }
+
+ // When the binary search is complete, we always know that the current node
+ // is the same as the end node, which is always past our range. Therefore,
+ // we've found the node immediately after the point of interest.
+ if (curNum == mNodeArray.Length()) {
+ // Get the point before the end of the last node, we can pass the length of
+ // the node into GetCharBefore, and it will return the last character.
+ RefPtr<Text> textNode(mNodeArray[curNum - 1]);
+ WSPoint point(textNode, textNode->TextLength(), 0);
+ return GetCharBefore(point);
+ } else {
+ // We can just ask the current node for the point immediately before it,
+ // it will handle moving to the previous node (if any) and returning the
+ // appropriate character
+ RefPtr<Text> textNode(mNodeArray[curNum]);
+ WSPoint point(textNode, 0, 0);
+ return GetCharBefore(point);
+ }
+}
+
+nsresult
+WSRunObject::CheckTrailingNBSPOfRun(WSFragment *aRun)
+{
+ // Try to change an nbsp to a space, if possible, just to prevent nbsp
+ // proliferation. Examine what is before and after the trailing nbsp, if
+ // any.
+ NS_ENSURE_TRUE(aRun, NS_ERROR_NULL_POINTER);
+ bool leftCheck = false;
+ bool spaceNBSP = false;
+ bool rightCheck = false;
+
+ // confirm run is normalWS
+ if (aRun->mType != WSType::normalWS) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // first check for trailing nbsp
+ WSPoint thePoint = GetCharBefore(aRun->mEndNode, aRun->mEndOffset);
+ if (thePoint.mTextNode && thePoint.mChar == nbsp) {
+ // now check that what is to the left of it is compatible with replacing nbsp with space
+ WSPoint prevPoint = GetCharBefore(thePoint);
+ if (prevPoint.mTextNode) {
+ if (!nsCRT::IsAsciiSpace(prevPoint.mChar)) {
+ leftCheck = true;
+ } else {
+ spaceNBSP = true;
+ }
+ } else if (aRun->mLeftType == WSType::text ||
+ aRun->mLeftType == WSType::special) {
+ leftCheck = true;
+ }
+ if (leftCheck || spaceNBSP) {
+ // now check that what is to the right of it is compatible with replacing
+ // nbsp with space
+ if (aRun->mRightType == WSType::text ||
+ aRun->mRightType == WSType::special ||
+ aRun->mRightType == WSType::br) {
+ rightCheck = true;
+ }
+ if ((aRun->mRightType & WSType::block) &&
+ IsBlockNode(GetWSBoundingParent())) {
+ // We are at a block boundary. Insert a <br>. Why? Well, first note
+ // that the br will have no visible effect since it is up against a
+ // block boundary. |foo<br><p>bar| renders like |foo<p>bar| and
+ // similarly |<p>foo<br></p>bar| renders like |<p>foo</p>bar|. What
+ // this <br> addition gets us is the ability to convert a trailing nbsp
+ // to a space. Consider: |<body>foo. '</body>|, where ' represents
+ // selection. User types space attempting to put 2 spaces after the
+ // end of their sentence. We used to do this as: |<body>foo.
+ // &nbsp</body>| This caused problems with soft wrapping: the nbsp
+ // would wrap to the next line, which looked attrocious. If you try to
+ // do: |<body>foo.&nbsp </body>| instead, the trailing space is
+ // invisible because it is against a block boundary. If you do:
+ // |<body>foo.&nbsp&nbsp</body>| then you get an even uglier soft
+ // wrapping problem, where foo is on one line until you type the final
+ // space, and then "foo " jumps down to the next line. Ugh. The best
+ // way I can find out of this is to throw in a harmless <br> here,
+ // which allows us to do: |<body>foo.&nbsp <br></body>|, which doesn't
+ // cause foo to jump lines, doesn't cause spaces to show up at the
+ // beginning of soft wrapped lines, and lets the user see 2 spaces when
+ // they type 2 spaces.
+
+ nsCOMPtr<Element> brNode =
+ mHTMLEditor->CreateBR(aRun->mEndNode, aRun->mEndOffset);
+ NS_ENSURE_TRUE(brNode, NS_ERROR_FAILURE);
+
+ // Refresh thePoint, prevPoint
+ thePoint = GetCharBefore(aRun->mEndNode, aRun->mEndOffset);
+ prevPoint = GetCharBefore(thePoint);
+ rightCheck = true;
+ }
+ }
+ if (leftCheck && rightCheck) {
+ // Now replace nbsp with space. First, insert a space
+ AutoTransactionsConserveSelection dontSpazMySelection(mHTMLEditor);
+ nsAutoString spaceStr(char16_t(32));
+ nsresult rv =
+ mHTMLEditor->InsertTextIntoTextNodeImpl(spaceStr, *thePoint.mTextNode,
+ thePoint.mOffset, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Finally, delete that nbsp
+ rv = DeleteChars(thePoint.mTextNode, thePoint.mOffset + 1,
+ thePoint.mTextNode, thePoint.mOffset + 2);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else if (!mPRE && spaceNBSP && rightCheck) {
+ // Don't mess with this preformatted for now. We have a run of ASCII
+ // whitespace (which will render as one space) followed by an nbsp (which
+ // is at the end of the whitespace run). Let's switch their order. This
+ // will ensure that if someone types two spaces after a sentence, and the
+ // editor softwraps at this point, the spaces won't be split across lines,
+ // which looks ugly and is bad for the moose.
+
+ RefPtr<Text> startNode, endNode;
+ int32_t startOffset, endOffset;
+ GetAsciiWSBounds(eBoth, prevPoint.mTextNode, prevPoint.mOffset + 1,
+ getter_AddRefs(startNode), &startOffset,
+ getter_AddRefs(endNode), &endOffset);
+
+ // Delete that nbsp
+ nsresult rv = DeleteChars(thePoint.mTextNode, thePoint.mOffset,
+ thePoint.mTextNode, thePoint.mOffset + 1);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Finally, insert that nbsp before the ASCII ws run
+ AutoTransactionsConserveSelection dontSpazMySelection(mHTMLEditor);
+ nsAutoString nbspStr(nbsp);
+ rv = mHTMLEditor->InsertTextIntoTextNodeImpl(nbspStr, *startNode,
+ startOffset, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+WSRunObject::CheckTrailingNBSP(WSFragment* aRun,
+ nsINode* aNode,
+ int32_t aOffset)
+{
+ // Try to change an nbsp to a space, if possible, just to prevent nbsp
+ // proliferation. This routine is called when we are about to make this
+ // point in the ws abut an inserted break or text, so we don't have to worry
+ // about what is after it. What is after it now will end up after the
+ // inserted object.
+ NS_ENSURE_TRUE(aRun && aNode, NS_ERROR_NULL_POINTER);
+ bool canConvert = false;
+ WSPoint thePoint = GetCharBefore(aNode, aOffset);
+ if (thePoint.mTextNode && thePoint.mChar == nbsp) {
+ WSPoint prevPoint = GetCharBefore(thePoint);
+ if (prevPoint.mTextNode) {
+ if (!nsCRT::IsAsciiSpace(prevPoint.mChar)) {
+ canConvert = true;
+ }
+ } else if (aRun->mLeftType == WSType::text ||
+ aRun->mLeftType == WSType::special) {
+ canConvert = true;
+ }
+ }
+ if (canConvert) {
+ // First, insert a space
+ AutoTransactionsConserveSelection dontSpazMySelection(mHTMLEditor);
+ nsAutoString spaceStr(char16_t(32));
+ nsresult rv =
+ mHTMLEditor->InsertTextIntoTextNodeImpl(spaceStr, *thePoint.mTextNode,
+ thePoint.mOffset, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Finally, delete that nbsp
+ rv = DeleteChars(thePoint.mTextNode, thePoint.mOffset + 1,
+ thePoint.mTextNode, thePoint.mOffset + 2);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+nsresult
+WSRunObject::CheckLeadingNBSP(WSFragment* aRun,
+ nsINode* aNode,
+ int32_t aOffset)
+{
+ // Try to change an nbsp to a space, if possible, just to prevent nbsp
+ // proliferation This routine is called when we are about to make this point
+ // in the ws abut an inserted text, so we don't have to worry about what is
+ // before it. What is before it now will end up before the inserted text.
+ bool canConvert = false;
+ WSPoint thePoint = GetCharAfter(aNode, aOffset);
+ if (thePoint.mChar == nbsp) {
+ WSPoint tmp = thePoint;
+ // we want to be after thePoint
+ tmp.mOffset++;
+ WSPoint nextPoint = GetCharAfter(tmp);
+ if (nextPoint.mTextNode) {
+ if (!nsCRT::IsAsciiSpace(nextPoint.mChar)) {
+ canConvert = true;
+ }
+ } else if (aRun->mRightType == WSType::text ||
+ aRun->mRightType == WSType::special ||
+ aRun->mRightType == WSType::br) {
+ canConvert = true;
+ }
+ }
+ if (canConvert) {
+ // First, insert a space
+ AutoTransactionsConserveSelection dontSpazMySelection(mHTMLEditor);
+ nsAutoString spaceStr(char16_t(32));
+ nsresult rv =
+ mHTMLEditor->InsertTextIntoTextNodeImpl(spaceStr, *thePoint.mTextNode,
+ thePoint.mOffset, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Finally, delete that nbsp
+ rv = DeleteChars(thePoint.mTextNode, thePoint.mOffset + 1,
+ thePoint.mTextNode, thePoint.mOffset + 2);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+
+nsresult
+WSRunObject::Scrub()
+{
+ WSFragment *run = mStartRun;
+ while (run) {
+ if (run->mType & (WSType::leadingWS | WSType::trailingWS)) {
+ nsresult rv = DeleteChars(run->mStartNode, run->mStartOffset,
+ run->mEndNode, run->mEndOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ run = run->mRight;
+ }
+ return NS_OK;
+}
+
+bool
+WSRunObject::IsBlockNode(nsINode* aNode)
+{
+ return aNode && aNode->IsElement() &&
+ HTMLEditor::NodeIsBlockStatic(aNode->AsElement());
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/WSRunObject.h b/editor/libeditor/WSRunObject.h
new file mode 100644
index 000000000..215e3eb2f
--- /dev/null
+++ b/editor/libeditor/WSRunObject.h
@@ -0,0 +1,411 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef WSRunObject_h
+#define WSRunObject_h
+
+#include "nsCOMPtr.h"
+#include "nsIEditor.h" // for EDirection
+#include "nsINode.h"
+#include "nscore.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/Text.h"
+
+class nsIDOMNode;
+
+namespace mozilla {
+
+class HTMLEditor;
+class HTMLEditRules;
+struct EditorDOMPoint;
+
+// class WSRunObject represents the entire whitespace situation
+// around a given point. It collects up a list of nodes that contain
+// whitespace and categorizes in up to 3 different WSFragments (detailed
+// below). Each WSFragment is a collection of whitespace that is
+// either all insignificant, or that is significant. A WSFragment could
+// consist of insignificant whitespace because it is after a block
+// boundary or after a break. Or it could be insignificant because it
+// is before a block. Or it could be significant because it is
+// surrounded by text, or starts and ends with nbsps, etc.
+
+// Throughout I refer to LeadingWS, NormalWS, TrailingWS. LeadingWS & TrailingWS
+// are runs of ascii ws that are insignificant (do not render) because they
+// are adjacent to block boundaries, or after a break. NormalWS is ws that
+// does cause soem rendering. Note that not all the ws in a NormalWS run need
+// render. For example, two ascii spaces surrounded by text on both sides
+// will only render as one space (in non-preformatted stlye html), yet both
+// spaces count as NormalWS. Together, they render as the one visible space.
+
+/**
+ * A type-safe bitfield indicating various types of whitespace or other things.
+ * Used as a member variable in WSRunObject and WSFragment.
+ *
+ * XXX: If this idea is useful in other places, we should generalize it using a
+ * template.
+ */
+class WSType
+{
+public:
+ enum Enum
+ {
+ none = 0,
+ leadingWS = 1, // leading insignificant ws, ie, after block or br
+ trailingWS = 1 << 1, // trailing insignificant ws, ie, before block
+ normalWS = 1 << 2, // normal significant ws, ie, after text, image, ...
+ text = 1 << 3, // indicates regular (non-ws) text
+ special = 1 << 4, // indicates an inline non-container, like image
+ br = 1 << 5, // indicates a br node
+ otherBlock = 1 << 6, // indicates a block other than one ws run is in
+ thisBlock = 1 << 7, // indicates the block ws run is in
+ block = otherBlock | thisBlock // block found
+ };
+
+ /**
+ * Implicit constructor, because the enums are logically just WSTypes
+ * themselves, and are only a separate type because there's no other obvious
+ * way to name specific WSType values.
+ */
+ MOZ_IMPLICIT WSType(const Enum& aEnum = none)
+ : mEnum(aEnum)
+ {}
+
+ // operator==, &, and | need to access mEnum
+ friend bool operator==(const WSType& aLeft, const WSType& aRight);
+ friend const WSType operator&(const WSType& aLeft, const WSType& aRight);
+ friend const WSType operator|(const WSType& aLeft, const WSType& aRight);
+ WSType& operator=(const WSType& aOther)
+ {
+ // This handles self-assignment fine
+ mEnum = aOther.mEnum;
+ return *this;
+ }
+ WSType& operator&=(const WSType& aOther)
+ {
+ mEnum &= aOther.mEnum;
+ return *this;
+ }
+ WSType& operator|=(const WSType& aOther)
+ {
+ mEnum |= aOther.mEnum;
+ return *this;
+ }
+
+private:
+ uint16_t mEnum;
+ void bool_conversion_helper() {}
+
+public:
+ // Allow boolean conversion with no numeric conversion
+ typedef void (WSType::*bool_type)();
+ operator bool_type() const
+ {
+ return mEnum ? &WSType::bool_conversion_helper : nullptr;
+ }
+};
+
+/**
+ * These are declared as global functions so "WSType::Enum == WSType" et al.
+ * will work using the implicit constructor.
+ */
+inline bool operator==(const WSType& aLeft, const WSType& aRight)
+{
+ return aLeft.mEnum == aRight.mEnum;
+}
+
+inline bool operator!=(const WSType& aLeft, const WSType& aRight)
+{
+ return !(aLeft == aRight);
+}
+
+inline const WSType operator&(const WSType& aLeft, const WSType& aRight)
+{
+ WSType ret;
+ ret.mEnum = aLeft.mEnum & aRight.mEnum;
+ return ret;
+}
+
+inline const WSType operator|(const WSType& aLeft, const WSType& aRight)
+{
+ WSType ret;
+ ret.mEnum = aLeft.mEnum | aRight.mEnum;
+ return ret;
+}
+
+/**
+ * Make sure that & and | of WSType::Enum creates a WSType instead of an int,
+ * because operators between WSType and int shouldn't work
+ */
+inline const WSType operator&(const WSType::Enum& aLeft,
+ const WSType::Enum& aRight)
+{
+ return WSType(aLeft) & WSType(aRight);
+}
+
+inline const WSType operator|(const WSType::Enum& aLeft,
+ const WSType::Enum& aRight)
+{
+ return WSType(aLeft) | WSType(aRight);
+}
+
+class MOZ_STACK_CLASS WSRunObject final
+{
+public:
+ enum BlockBoundary
+ {
+ kBeforeBlock,
+ kBlockStart,
+ kBlockEnd,
+ kAfterBlock
+ };
+
+ enum {eBefore = 1};
+ enum {eAfter = 1 << 1};
+ enum {eBoth = eBefore | eAfter};
+
+ WSRunObject(HTMLEditor* aHTMLEditor, nsINode* aNode, int32_t aOffset);
+ WSRunObject(HTMLEditor* aHTMLEditor, nsIDOMNode* aNode, int32_t aOffset);
+ ~WSRunObject();
+
+ // ScrubBlockBoundary removes any non-visible whitespace at the specified
+ // location relative to a block node.
+ static nsresult ScrubBlockBoundary(HTMLEditor* aHTMLEditor,
+ BlockBoundary aBoundary,
+ nsINode* aBlock,
+ int32_t aOffset = -1);
+
+ // PrepareToJoinBlocks fixes up ws at the end of aLeftBlock and the
+ // beginning of aRightBlock in preperation for them to be joined. Example
+ // of fixup: trailingws in aLeftBlock needs to be removed.
+ static nsresult PrepareToJoinBlocks(HTMLEditor* aHTMLEditor,
+ dom::Element* aLeftBlock,
+ dom::Element* aRightBlock);
+
+ // PrepareToDeleteRange fixes up ws before {aStartNode,aStartOffset}
+ // and after {aEndNode,aEndOffset} in preperation for content
+ // in that range to be deleted. Note that the nodes and offsets
+ // are adjusted in response to any dom changes we make while
+ // adjusting ws.
+ // example of fixup: trailingws before {aStartNode,aStartOffset}
+ // needs to be removed.
+ static nsresult PrepareToDeleteRange(HTMLEditor* aHTMLEditor,
+ nsCOMPtr<nsINode>* aStartNode,
+ int32_t* aStartOffset,
+ nsCOMPtr<nsINode>* aEndNode,
+ int32_t* aEndOffset);
+
+ // PrepareToDeleteNode fixes up ws before and after aContent in preparation
+ // for aContent to be deleted. Example of fixup: trailingws before
+ // aContent needs to be removed.
+ static nsresult PrepareToDeleteNode(HTMLEditor* aHTMLEditor,
+ nsIContent* aContent);
+
+ // PrepareToSplitAcrossBlocks fixes up ws before and after
+ // {aSplitNode,aSplitOffset} in preparation for a block parent to be split.
+ // Note that the aSplitNode and aSplitOffset are adjusted in response to
+ // any DOM changes we make while adjusting ws. Example of fixup: normalws
+ // before {aSplitNode,aSplitOffset} needs to end with nbsp.
+ static nsresult PrepareToSplitAcrossBlocks(HTMLEditor* aHTMLEditor,
+ nsCOMPtr<nsINode>* aSplitNode,
+ int32_t* aSplitOffset);
+
+ // InsertBreak inserts a br node at {aInOutParent,aInOutOffset}
+ // and makes any needed adjustments to ws around that point.
+ // example of fixup: normalws after {aInOutParent,aInOutOffset}
+ // needs to begin with nbsp.
+ already_AddRefed<dom::Element> InsertBreak(nsCOMPtr<nsINode>* aInOutParent,
+ int32_t* aInOutOffset,
+ nsIEditor::EDirection aSelect);
+
+ // InsertText inserts a string at {aInOutParent,aInOutOffset} and makes any
+ // needed adjustments to ws around that point. Example of fixup:
+ // trailingws before {aInOutParent,aInOutOffset} needs to be removed.
+ nsresult InsertText(const nsAString& aStringToInsert,
+ nsCOMPtr<nsINode>* aInOutNode,
+ int32_t* aInOutOffset,
+ nsIDocument* aDoc);
+
+ // DeleteWSBackward deletes a single visible piece of ws before the ws
+ // point (the point to create the wsRunObject, passed to its constructor).
+ // It makes any needed conversion to adjacent ws to retain its
+ // significance.
+ nsresult DeleteWSBackward();
+
+ // DeleteWSForward deletes a single visible piece of ws after the ws point
+ // (the point to create the wsRunObject, passed to its constructor). It
+ // makes any needed conversion to adjacent ws to retain its significance.
+ nsresult DeleteWSForward();
+
+ // PriorVisibleNode returns the first piece of visible thing before
+ // {aNode,aOffset}. If there is no visible ws qualifying it returns what
+ // is before the ws run. Note that {outVisNode,outVisOffset} is set to
+ // just AFTER the visible object.
+ void PriorVisibleNode(nsINode* aNode,
+ int32_t aOffset,
+ nsCOMPtr<nsINode>* outVisNode,
+ int32_t* outVisOffset,
+ WSType* outType);
+
+ // NextVisibleNode returns the first piece of visible thing after
+ // {aNode,aOffset}. If there is no visible ws qualifying it returns what
+ // is after the ws run. Note that {outVisNode,outVisOffset} is set to just
+ // BEFORE the visible object.
+ void NextVisibleNode(nsINode* aNode,
+ int32_t aOffset,
+ nsCOMPtr<nsINode>* outVisNode,
+ int32_t* outVisOffset,
+ WSType* outType);
+
+ // AdjustWhitespace examines the ws object for nbsp's that can
+ // be safely converted to regular ascii space and converts them.
+ nsresult AdjustWhitespace();
+
+protected:
+ // WSFragment represents a single run of ws (all leadingws, or all normalws,
+ // or all trailingws, or all leading+trailingws). Note that this single run
+ // may still span multiple nodes.
+ struct WSFragment final
+ {
+ nsCOMPtr<nsINode> mStartNode; // node where ws run starts
+ nsCOMPtr<nsINode> mEndNode; // node where ws run ends
+ int32_t mStartOffset; // offset where ws run starts
+ int32_t mEndOffset; // offset where ws run ends
+ // type of ws, and what is to left and right of it
+ WSType mType, mLeftType, mRightType;
+ // other ws runs to left or right. may be null.
+ WSFragment *mLeft, *mRight;
+
+ WSFragment()
+ : mStartOffset(0)
+ , mEndOffset(0)
+ , mLeft(nullptr)
+ , mRight(nullptr)
+ {}
+ };
+
+ // A WSPoint struct represents a unique location within the ws run. It is
+ // always within a textnode that is one of the nodes stored in the list
+ // in the wsRunObject. For convenience, the character at that point is also
+ // stored in the struct.
+ struct MOZ_STACK_CLASS WSPoint final
+ {
+ RefPtr<dom::Text> mTextNode;
+ uint32_t mOffset;
+ char16_t mChar;
+
+ WSPoint()
+ : mTextNode(nullptr)
+ , mOffset(0)
+ , mChar(0)
+ {}
+
+ WSPoint(dom::Text* aTextNode, int32_t aOffset, char16_t aChar)
+ : mTextNode(aTextNode)
+ , mOffset(aOffset)
+ , mChar(aChar)
+ {}
+ };
+
+ enum AreaRestriction
+ {
+ eAnywhere, eOutsideUserSelectAll
+ };
+
+ /**
+ * Return the node which we will handle white-space under. This is the
+ * closest block within the DOM subtree we're editing, or if none is
+ * found, the (inline) root of the editable subtree.
+ */
+ nsINode* GetWSBoundingParent();
+
+ nsresult GetWSNodes();
+ void GetRuns();
+ void ClearRuns();
+ void MakeSingleWSRun(WSType aType);
+ nsIContent* GetPreviousWSNodeInner(nsINode* aStartNode,
+ nsINode* aBlockParent);
+ nsIContent* GetPreviousWSNode(EditorDOMPoint aPoint, nsINode* aBlockParent);
+ nsIContent* GetNextWSNodeInner(nsINode* aStartNode, nsINode* aBlockParent);
+ nsIContent* GetNextWSNode(EditorDOMPoint aPoint, nsINode* aBlockParent);
+ nsresult PrepareToDeleteRangePriv(WSRunObject* aEndObject);
+ nsresult PrepareToSplitAcrossBlocksPriv();
+ nsresult DeleteChars(nsINode* aStartNode, int32_t aStartOffset,
+ nsINode* aEndNode, int32_t aEndOffset,
+ AreaRestriction aAR = eAnywhere);
+ WSPoint GetCharAfter(nsINode* aNode, int32_t aOffset);
+ WSPoint GetCharBefore(nsINode* aNode, int32_t aOffset);
+ WSPoint GetCharAfter(const WSPoint& aPoint);
+ WSPoint GetCharBefore(const WSPoint& aPoint);
+ nsresult ConvertToNBSP(WSPoint aPoint,
+ AreaRestriction aAR = eAnywhere);
+ void GetAsciiWSBounds(int16_t aDir, nsINode* aNode, int32_t aOffset,
+ dom::Text** outStartNode, int32_t* outStartOffset,
+ dom::Text** outEndNode, int32_t* outEndOffset);
+ void FindRun(nsINode* aNode, int32_t aOffset, WSFragment** outRun,
+ bool after);
+ char16_t GetCharAt(dom::Text* aTextNode, int32_t aOffset);
+ WSPoint GetWSPointAfter(nsINode* aNode, int32_t aOffset);
+ WSPoint GetWSPointBefore(nsINode* aNode, int32_t aOffset);
+ nsresult CheckTrailingNBSPOfRun(WSFragment *aRun);
+ nsresult CheckTrailingNBSP(WSFragment* aRun, nsINode* aNode,
+ int32_t aOffset);
+ nsresult CheckLeadingNBSP(WSFragment* aRun, nsINode* aNode,
+ int32_t aOffset);
+
+ nsresult Scrub();
+ bool IsBlockNode(nsINode* aNode);
+
+ // The node passed to our constructor.
+ nsCOMPtr<nsINode> mNode;
+ // The offset passed to our contructor.
+ int32_t mOffset;
+ // Together, the above represent the point at which we are building up ws info.
+
+ // true if we are in preformatted whitespace context.
+ bool mPRE;
+ // Node/offset where ws starts.
+ nsCOMPtr<nsINode> mStartNode;
+ int32_t mStartOffset;
+ // Reason why ws starts (eText, eOtherBlock, etc.).
+ WSType mStartReason;
+ // The node that implicated by start reason.
+ nsCOMPtr<nsINode> mStartReasonNode;
+
+ // Node/offset where ws ends.
+ nsCOMPtr<nsINode> mEndNode;
+ int32_t mEndOffset;
+ // Reason why ws ends (eText, eOtherBlock, etc.).
+ WSType mEndReason;
+ // The node that implicated by end reason.
+ nsCOMPtr<nsINode> mEndReasonNode;
+
+ // Location of first nbsp in ws run, if any.
+ RefPtr<dom::Text> mFirstNBSPNode;
+ int32_t mFirstNBSPOffset;
+
+ // Location of last nbsp in ws run, if any.
+ RefPtr<dom::Text> mLastNBSPNode;
+ int32_t mLastNBSPOffset;
+
+ // The list of nodes containing ws in this run.
+ nsTArray<RefPtr<dom::Text>> mNodeArray;
+
+ // The first WSFragment in the run.
+ WSFragment* mStartRun;
+ // The last WSFragment in the run, may be same as first.
+ WSFragment* mEndRun;
+
+ // Non-owning.
+ HTMLEditor* mHTMLEditor;
+
+ // Opening this class up for pillaging.
+ friend class HTMLEditRules;
+ // Opening this class up for more pillaging.
+ friend class HTMLEditor;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef WSRunObject_h
diff --git a/editor/libeditor/crashtests/1057677.html b/editor/libeditor/crashtests/1057677.html
new file mode 100644
index 000000000..d0b9497a5
--- /dev/null
+++ b/editor/libeditor/crashtests/1057677.html
@@ -0,0 +1,9 @@
+<html><body></body><script>
+document.designMode = "on";
+var hrElem = document.createElement("HR");
+var select = window.getSelection();
+document.body.appendChild(hrElem);
+select.collapse(hrElem,0);
+document.execCommand("InsertHTML", false, "<div>foo</div><div>bar</div>");
+</script>
+</html>
diff --git a/editor/libeditor/crashtests/1128787.html b/editor/libeditor/crashtests/1128787.html
new file mode 100644
index 000000000..fc6bff097
--- /dev/null
+++ b/editor/libeditor/crashtests/1128787.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Bug 1128787</title>
+</head>
+<body>
+ <input type="button"/>
+ <script>
+ window.onload = function () {
+ document.designMode = "on";
+ }
+ var input = document.getElementsByTagName("input")[0];
+ input.focus();
+ input.type = "text";
+ </script>
+</body>
+</html>
diff --git a/editor/libeditor/crashtests/1134545.html b/editor/libeditor/crashtests/1134545.html
new file mode 100644
index 000000000..4e871804f
--- /dev/null
+++ b/editor/libeditor/crashtests/1134545.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<!-- saved from url=(0065)https://bug1134545.bugzilla.mozilla.org/attachment.cgi?id=8566418 -->
+<html><head><meta http-equiv="Content-Type" content="text/html; charset=windows-1252">
+<script>
+
+function boom()
+{
+ textNode = document.createTextNode(" ");
+ x.appendChild(textNode);
+ x.setAttribute('contenteditable', "true");
+ textNode.remove();
+ window.getSelection().selectAllChildren(textNode);
+ document.execCommand("increasefontsize", false, null);
+}
+
+</script>
+</head>
+<body onload="boom();">
+<div id="x" contenteditable="true"></div>
+
+
+</body></html> \ No newline at end of file
diff --git a/editor/libeditor/crashtests/1158452.html b/editor/libeditor/crashtests/1158452.html
new file mode 100644
index 000000000..56c74abd6
--- /dev/null
+++ b/editor/libeditor/crashtests/1158452.html
@@ -0,0 +1,10 @@
+
+<div>
+<div>
+aaaaaaa
+</script>
+<script type="text/javascript">
+document.designMode = "on"
+window.getSelection().modify("extend", "backward", "line")
+document.execCommand("increasefontsize","",null);
+</script>
diff --git a/editor/libeditor/crashtests/1158651.html b/editor/libeditor/crashtests/1158651.html
new file mode 100644
index 000000000..27278b523
--- /dev/null
+++ b/editor/libeditor/crashtests/1158651.html
@@ -0,0 +1,18 @@
+<script>
+onload = function() {
+ var testContainer = document.createElement("span");
+ testContainer.contentEditable = true;
+ document.body.appendChild(testContainer);
+
+ function queryFormatBlock(content)
+ {
+ testContainer.innerHTML = content;
+ while (testContainer.firstChild)
+ testContainer = testContainer.firstChild;
+ window.getSelection().collapse(testContainer, 0);
+ document.queryCommandValue('formatBlock');
+ }
+
+ queryFormatBlock('<ol>hello</ol>');
+};
+</script>
diff --git a/editor/libeditor/crashtests/1244894.xhtml b/editor/libeditor/crashtests/1244894.xhtml
new file mode 100644
index 000000000..89a24751e
--- /dev/null
+++ b/editor/libeditor/crashtests/1244894.xhtml
@@ -0,0 +1,21 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<script>
+
+function boom()
+{
+ document.designMode = 'on';
+ document.execCommand("indent", false, null);
+ document.execCommand("insertText", false, "a");
+ document.execCommand("forwardDelete", false, null);
+ document.execCommand("justifyfull", false, null);
+}
+
+window.addEventListener("load", boom, false);
+
+</script>
+</head>
+
+<body> <span class="v"></span></body><body><input type="file" /></body>
+
+</html>
diff --git a/editor/libeditor/crashtests/1272490.html b/editor/libeditor/crashtests/1272490.html
new file mode 100644
index 000000000..a3c8ecd81
--- /dev/null
+++ b/editor/libeditor/crashtests/1272490.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<script>
+window.onload = function () {
+ var childDocument = document.getElementsByTagName("iframe")[0].contentDocument;
+ childDocument.designMode = "on";
+ function onAttrModified(aEvent) {
+ childDocument.removeEventListener("DOMAttrModified", onAttrModified, false);
+ // Remove the editor from document during executing "insertOrderedList".
+ document.body.innerHTML = "";
+ }
+ childDocument.addEventListener("DOMAttrModified", onAttrModified, false);
+ childDocument.execCommand("insertOrderedList", false, "1");
+}
+</script>
+</head>
+<body><iframe></iframe></body>
+</html>
diff --git a/editor/libeditor/crashtests/1317704.html b/editor/libeditor/crashtests/1317704.html
new file mode 100644
index 000000000..64359c796
--- /dev/null
+++ b/editor/libeditor/crashtests/1317704.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<script>
+addEventListener('DOMContentLoaded', function(){
+ document.documentElement.className = 'lizard';
+ setTimeout(function(){
+ document.execCommand('selectAll', false, null);
+ document.designMode = 'on';
+ document.execCommand('removeformat', false, null);
+ }, 0);
+});
+</script>
+<style>
+.lizard{
+ -webkit-user-select:all;
+}
+*{
+ position:fixed;
+ display:table-column;
+}
+</style>
+</head>
+<body>
+<span>
+<span contenteditable>
+<span class=lizard></span>
+<span class=lizard></span>
+<span />
+</span>
+</span>
+</span>
+</span>
+</body>
+</html>
diff --git a/editor/libeditor/crashtests/336081-1.xhtml b/editor/libeditor/crashtests/336081-1.xhtml
new file mode 100644
index 000000000..da653c601
--- /dev/null
+++ b/editor/libeditor/crashtests/336081-1.xhtml
@@ -0,0 +1,52 @@
+<html xmlns="http://www.w3.org/1999/xhtml" class="reftest-wait">
+<head>
+<script>
+<![CDATA[
+
+function foop(targetWindow)
+{
+ var targetDocument = targetWindow.document;
+
+ var r1 = targetDocument.createRange();
+ r1.setStart(targetDocument.getElementById("out1"), 0);
+ r1.setEnd (targetDocument.getElementById("out2"), 0);
+ targetWindow.getSelection().addRange(r1);
+
+ var r2 = targetDocument.createRange();
+ r2.setStart(targetDocument.getElementById("in1"), 0);
+ r2.setEnd (targetDocument.getElementById("in2"), 0);
+ targetWindow.getSelection().addRange(r2);
+
+ targetDocument.execCommand('removeformat', false, null);
+ targetDocument.execCommand('outdent', false, null);
+}
+
+function init()
+{
+ setTimeout(function()
+ {
+ var fd = window.frames[0].document;
+ fd.body.appendChild(fd.importNode(document.getElementById('rootish'), true));
+ fd.designMode = 'on';
+ foop(window.frames[0]);
+ document.documentElement.removeAttribute("class");
+ }, 100);
+}
+
+]]>
+</script>
+</head>
+
+<body onload="init()">
+
+<iframe src="data:text/html," style="width: 95%; height: 500px;"/>
+
+<div id="rootish">
+<div id="out1"/>
+<div id="in1"/>
+<div id="in2"/>
+<div id="out2"/>
+</div>
+
+</body>
+</html> \ No newline at end of file
diff --git a/editor/libeditor/crashtests/336104.html b/editor/libeditor/crashtests/336104.html
new file mode 100644
index 000000000..32f11b745
--- /dev/null
+++ b/editor/libeditor/crashtests/336104.html
@@ -0,0 +1,37 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<script>
+function init()
+{
+ var targetWindow = window.frames[0];
+ var targetDocument = targetWindow.document;
+ var rootish = document.getElementById('rootish');
+
+ targetDocument.body.appendChild(targetDocument.adoptNode(rootish));
+ targetDocument.designMode = 'on';
+
+ targetWindow.getSelection().removeAllRanges();
+
+ var r = targetDocument.createRange();
+ r.setStart(targetDocument.getElementById("start"), 0);
+ r.setEnd (targetDocument.getElementById("endparent").firstChild, 0);
+ targetWindow.getSelection().addRange(r);
+
+ targetDocument.execCommand('outdent', false, null);
+}
+</script>
+
+</head>
+
+<body onload="setTimeout(init, 300);">
+
+<iframe src="data:text/html," style="width: 95%; height: 500px;"></iframe>
+
+<div id="rootish">
+ <div id="start"></div>
+ <p>Huh</p>
+ <svg xmlns="http://www.w3.org/2000/svg" id="endparent"> </svg>
+</div>
+
+</body>
+</html>
diff --git a/editor/libeditor/crashtests/382527-1.html b/editor/libeditor/crashtests/382527-1.html
new file mode 100644
index 000000000..2441dcd87
--- /dev/null
+++ b/editor/libeditor/crashtests/382527-1.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<script>
+
+
+function init1()
+{
+ targetIframe = document.createElementNS('http://www.w3.org/1999/xhtml', 'iframe');
+ targetIframe.src = "data:text/html,";
+ targetIframe.setAttribute("style", "width: 300px; height: 200px; border: 1px dotted green;");
+ targetIframe.addEventListener("load", init2, false);
+ document.body.appendChild(targetIframe);
+}
+
+
+function init2()
+{
+ targetWindow = targetIframe.contentWindow;
+ targetDocument = targetWindow.document;
+
+ var div = document.getElementById("div");
+ textNode = div.firstChild;
+
+ targetDocument.body.appendChild(targetDocument.adoptNode(div, true));
+
+ targetDocument.designMode = 'on';
+ setTimeout(init3, 0);
+}
+
+
+function init3()
+{
+ var rng = targetDocument.createRange();
+ rng.setStart(textNode, 1);
+ rng.setEnd(textNode, 1);
+ targetWindow.getSelection().addRange(rng);
+
+ try {
+ targetDocument.execCommand("inserthtml", false, "<p>");
+ } catch(e) {}
+
+ document.documentElement.removeAttribute("class");
+}
+
+
+</script>
+
+</head>
+
+<body onload="init1();">
+
+<div id="div"> </div>
+
+<script>
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/crashtests/382778-1.html b/editor/libeditor/crashtests/382778-1.html
new file mode 100644
index 000000000..960b16630
--- /dev/null
+++ b/editor/libeditor/crashtests/382778-1.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<script>
+
+function init1()
+{
+ // Create an html:iframe in HTML mode (so designMode can be used 320092)
+ targetIframe = document.createElementNS('http://www.w3.org/1999/xhtml', 'iframe');
+ targetIframe.src = "data:text/html,";
+ targetIframe.setAttribute("style", "width: 700px; height: 500px; border: 1px dotted green;");
+ targetIframe.addEventListener("load", init2, false);
+ document.body.appendChild(targetIframe);
+}
+
+
+function init2()
+{
+ targetWindow = targetIframe.contentWindow;
+ targetDocument = targetWindow.document;
+
+ p = document.getElementById("p");
+ pText = p.firstChild;
+
+ targetDocument.body.appendChild(targetDocument.adoptNode(p, true));
+
+ targetDocument.designMode = 'on';
+
+ setTimeout(boom, 0);
+}
+
+
+function boom()
+{
+ var rng = targetDocument.createRange();
+ rng.setStart(pText, 3);
+ rng.setEnd(pText, 3);
+
+ targetWindow.getSelection().addRange(rng);
+
+ targetDocument.execCommand("insertorderedlist", false, null);
+
+ document.documentElement.removeAttribute("class")
+}
+
+</script>
+</head>
+
+<body onload="init1();">
+<p id="p">word word</p>
+</body>
+
+</html>
diff --git a/editor/libeditor/crashtests/402172-1.html b/editor/libeditor/crashtests/402172-1.html
new file mode 100644
index 000000000..4022523fa
--- /dev/null
+++ b/editor/libeditor/crashtests/402172-1.html
@@ -0,0 +1,23 @@
+<html>
+<head>
+<script>
+
+function boom()
+{
+ document.getElementById("div").contentEditable = "true";
+ document.getElementById("div").focus();
+ document.getElementById("div").contentEditable = "false";
+
+ document.getElementById("table").contentEditable = "true";
+}
+
+</script>
+</head>
+
+<body onload="boom();">
+
+<table id="table"><td></td></table><div id="div"></div>
+
+</body>
+
+</html>
diff --git a/editor/libeditor/crashtests/403965-1.xhtml b/editor/libeditor/crashtests/403965-1.xhtml
new file mode 100644
index 000000000..02993914d
--- /dev/null
+++ b/editor/libeditor/crashtests/403965-1.xhtml
@@ -0,0 +1,7 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:html="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<tbody contenteditable="true"/>
+<frameset/>
+<xul:box onbeforecopy="event.explicitOriginalTarget.parentNode.parentNode.removeChild(event.explicitOriginalTarget.parentNode)">
+<xul:box/>
+</xul:box>
+</html> \ No newline at end of file
diff --git a/editor/libeditor/crashtests/407074-1.html b/editor/libeditor/crashtests/407074-1.html
new file mode 100644
index 000000000..22f172856
--- /dev/null
+++ b/editor/libeditor/crashtests/407074-1.html
@@ -0,0 +1,7 @@
+<html>
+<head>
+</head>
+
+<body onload="try { document.execCommand('inserthtml', false, '0'); } catch(e) { }"><span id="textarea" contenteditable="true">is</span></body>
+
+</html>
diff --git a/editor/libeditor/crashtests/407079-1.html b/editor/libeditor/crashtests/407079-1.html
new file mode 100644
index 000000000..8b0e36cd6
--- /dev/null
+++ b/editor/libeditor/crashtests/407079-1.html
@@ -0,0 +1,15 @@
+<html>
+<head>
+<script type="text/javascript">
+
+function boom()
+{
+ document.execCommand("selectAll", false, null);
+ document.execCommand("inserthtml", false, "<p>");
+}
+
+</script>
+</head>
+
+<body onload="boom();"><textarea contenteditable="true"></textarea></body>
+</html>
diff --git a/editor/libeditor/crashtests/407256-1.html b/editor/libeditor/crashtests/407256-1.html
new file mode 100644
index 000000000..824162ac5
--- /dev/null
+++ b/editor/libeditor/crashtests/407256-1.html
@@ -0,0 +1,23 @@
+<html>
+<head>
+<script type="text/javascript">
+
+function boom()
+{
+ document.addEventListener("DOMNodeInserted", x, false);
+
+ function x()
+ {
+ document.removeEventListener("DOMNodeInserted", x, false);
+ document.execCommand("insertParagraph", false, "");
+ }
+
+ document.execCommand("insertorderedlist", false, "");
+}
+
+</script>
+</head>
+
+<body contenteditable="true" onload="boom()"></body>
+
+</html>
diff --git a/editor/libeditor/crashtests/407277-1.html b/editor/libeditor/crashtests/407277-1.html
new file mode 100644
index 000000000..41c6bf280
--- /dev/null
+++ b/editor/libeditor/crashtests/407277-1.html
@@ -0,0 +1,7 @@
+<html>
+<head>
+</head>
+<body style="margin: initial;"
+ contenteditable="true"
+ onload="document.execCommand('outdent', false, null);"></body>
+</html>
diff --git a/editor/libeditor/crashtests/414178-1.html b/editor/libeditor/crashtests/414178-1.html
new file mode 100644
index 000000000..19cc205b9
--- /dev/null
+++ b/editor/libeditor/crashtests/414178-1.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script type="text/javascript">
+
+function boom()
+{
+ var table = document.createElement("table");
+ document.body.appendChild(table);
+ table.contentEditable = "true";
+ table.focus();
+ try {
+ // This will throw, since it's attempting to inject a list inside a table
+ document.execCommand("insertunorderedlist", false, null);
+ } catch (e) {}
+}
+
+</script>
+</head>
+
+<body onload="boom();"></body>
+</html>
diff --git a/editor/libeditor/crashtests/418923-1.html b/editor/libeditor/crashtests/418923-1.html
new file mode 100644
index 000000000..786ea25d9
--- /dev/null
+++ b/editor/libeditor/crashtests/418923-1.html
@@ -0,0 +1,19 @@
+<html><head><script type="text/javascript">
+
+function boom()
+{
+ var dE = document.documentElement;
+ var head = document.getElementsByTagName("head")[0];
+ dE.removeChild(document.body);
+ dE.contentEditable = "true";
+ dE.focus();
+ dE.contentEditable = "false";
+ head.focus();
+ head.contentEditable = "true";
+ try {
+ document.execCommand("selectAll", false, "");
+ } catch(e) {
+ }
+}
+
+</script></head><body onload="boom();"></body></html> \ No newline at end of file
diff --git a/editor/libeditor/crashtests/420439.html b/editor/libeditor/crashtests/420439.html
new file mode 100644
index 000000000..e1303307d
--- /dev/null
+++ b/editor/libeditor/crashtests/420439.html
@@ -0,0 +1,30 @@
+<html>
+<head>
+<script type="text/javascript">
+
+function boom()
+{
+ function x()
+ {
+ document.removeEventListener("DOMAttrModified", x, false);
+ document.execCommand("backcolor", false, "green");
+ }
+
+ document.getElementById("td").focus();
+
+ document.addEventListener("DOMAttrModified", x, false);
+ try {
+ document.execCommand("subscript", false, null);
+ } catch(e) {
+ }
+ document.removeEventListener("DOMAttrModified", x, false);
+}
+
+</script>
+</head>
+
+<body contenteditable="true" onload="setTimeout(boom, 30);">
+<table><tbody contenteditable="false"><tr><td contenteditable="true" id="td"></td></tr></tbody></table>
+</body>
+
+</html>
diff --git a/editor/libeditor/crashtests/428489-1.html b/editor/libeditor/crashtests/428489-1.html
new file mode 100644
index 000000000..8eec1268b
--- /dev/null
+++ b/editor/libeditor/crashtests/428489-1.html
@@ -0,0 +1,8 @@
+<html>
+<head>
+<title>Crash [@ nsHTMLEditor::GetPositionAndDimensions] when window gets removed during click on contenteditable absolute positioned element</title>
+</head>
+<body>
+<iframe id="content" src="data:text/html;charset=utf-8,%3Chtml%3E%3Chead%3E%0A%3Cscript%3E%0Awindow.addEventListener%28%27DOMAttrModified%27%2C%20function%28e%29%20%7Bdump%28%27DOMAttrModified\n%27%29%3Bwindow.frameElement.parentNode.removeChild%28window.frameElement%29%3B%7D%2C%20true%29%3B%0A%3C/script%3E%0A%3C/head%3E%0A%3Cbody%3E%0A%3Cdiv%20style%3D%22position%3A%20absolute%3B%22%20contenteditable%3D%22true%22%3EClicking%20on%20this%20should%20not%20crash%20Mozilla%0A%3C/body%3E%0A%3C/html%3E" style="width:1000px;height: 300px;"></iframe>
+</body>
+</html>
diff --git a/editor/libeditor/crashtests/429586-1.html b/editor/libeditor/crashtests/429586-1.html
new file mode 100644
index 000000000..a32df3b72
--- /dev/null
+++ b/editor/libeditor/crashtests/429586-1.html
@@ -0,0 +1,8 @@
+<html>
+<head>
+<title>Bug 429586 - Crash [@ nsEditor::EndUpdateViewBatch] with pasting and domattrmodified removing iframe</title>
+</head>
+<body>
+<iframe id="content" src="data:text/html;charset=utf-8,%3Chtml%3E%0A%3Chead%3E%0A%3C/head%3E%0A%3Cbody%20contenteditable%3D%22true%22%3E%0A%0A%3Cscript%3E%0Afunction%20dokey%28%29%7B%0Adocument.body.focus%28%29%3B%0Adocument.execCommand%28%27insertParagraph%27%2C%20false%2C%20%27%27%29%3B%0A%7D%0AsetTimeout%28dokey%2C200%29%3B%0A%0Adocument.addEventListener%28%27DOMAttrModified%27%2C%20function%28%29%20%7Bwindow.frameElement.parentNode.removeChild%28window.frameElement%29%7D%2C%20true%29%3B%0A%3C/script%3E%0A%3C/body%3E%0A%3C/html%3E"></iframe>
+</body>
+</html>
diff --git a/editor/libeditor/crashtests/430624-1.html b/editor/libeditor/crashtests/430624-1.html
new file mode 100644
index 000000000..bfa95c662
--- /dev/null
+++ b/editor/libeditor/crashtests/430624-1.html
@@ -0,0 +1,14 @@
+<html>
+<head>
+<script>
+function crash() {
+ window.frames[0].onload = null;
+ window.frames[0].location = 'data:text/html;charset=utf-8,2nd%20page';
+}
+</script>
+</head>
+<body onload="crash()">
+ <!-- iframe contents: <html><body onload="document.body.setAttribute('spellcheck', true);"></body></html> -->
+ <iframe src="data:text/html;charset=utf-8;base64,PGh0bWw%2BPGJvZHkgb25sb2FkPSJkb2N1bWVudC5ib2R5LnNldEF0dHJpYnV0ZSgnc3BlbGxjaGVjaycsIHRydWUpOyI%2BPC9ib2R5PjwvaHRtbD4%3D"></iframe>
+</body>
+</html> \ No newline at end of file
diff --git a/editor/libeditor/crashtests/431086-1.xhtml b/editor/libeditor/crashtests/431086-1.xhtml
new file mode 100644
index 000000000..c6c5d8d99
--- /dev/null
+++ b/editor/libeditor/crashtests/431086-1.xhtml
@@ -0,0 +1,22 @@
+<div xmlns="http://www.w3.org/1999/xhtml">
+
+<script type="text/javascript">
+
+function boom()
+{
+ var r = document.documentElement;
+ r.style.position = "absolute";
+ r.contentEditable = "true";
+ r.focus();
+ r.contentEditable = "false";
+ r.focus();
+ r.contentEditable = "true";
+ document.execCommand("subscript", false, null);
+ r.contentEditable = "false";
+}
+
+window.addEventListener("load", boom, false);
+
+</script>
+
+</div>
diff --git a/editor/libeditor/crashtests/448329-1.html b/editor/libeditor/crashtests/448329-1.html
new file mode 100644
index 000000000..99d0f63b3
--- /dev/null
+++ b/editor/libeditor/crashtests/448329-1.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html><head>
+ <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
+ <title>Testcase for bug 448329</title>
+</head>
+<body>
+
+<iframe id="frame448329"></iframe>
+
+<script>
+
+function test448329(id,cmd) {
+
+ var elm = document.getElementById(id);
+ var doc = elm.contentDocument;
+ doc.designMode = "On";
+
+ // Work around getSelection depending on a presshell but not flushing to get
+ // one.
+ doc.body.offsetWidth;
+ var s = doc.defaultView.getSelection();
+
+ // Test document node
+ if (s.rangeCount > 0)
+ s.removeAllRanges();
+ var range = doc.createRange();
+ range.setStart(doc, 0);
+ range.setEnd(doc, 0);
+ s.addRange(range);
+ doc.queryCommandIndeterm(cmd);
+
+ // Test HTML node
+ if (s.rangeCount > 0)
+ s.removeAllRanges();
+ range = doc.createRange();
+ range.setStart(doc.documentElement, 0);
+ range.setEnd(doc.documentElement, 0);
+ s.addRange(range);
+ doc.queryCommandIndeterm(cmd);
+
+ // Test BODY node
+ if (s.rangeCount > 0)
+ s.removeAllRanges();
+ range = doc.createRange();
+ var body = doc.documentElement.childNodes[1];
+ range.setStart(body, 0);
+ range.setEnd(body, 0);
+ s.addRange(range);
+ doc.queryCommandIndeterm(cmd);
+
+ var text = doc.createTextNode("Hello Kitty");
+ body.insertBefore(text, null)
+
+ // Test TEXT node
+ if (s.rangeCount > 0)
+ s.removeAllRanges();
+ range = doc.createRange();
+ range.setStart(text, 0);
+ range.setEnd(text, 1);
+ s.addRange(range);
+ doc.queryCommandIndeterm(cmd);
+
+}
+
+test448329("frame448329", "backcolor")
+test448329("frame448329", "hilitecolor")
+
+</script>
+
+
+</body>
+</html>
diff --git a/editor/libeditor/crashtests/448329-2.html b/editor/libeditor/crashtests/448329-2.html
new file mode 100644
index 000000000..fd4707b54
--- /dev/null
+++ b/editor/libeditor/crashtests/448329-2.html
@@ -0,0 +1,21 @@
+<html>
+<head>
+ <title>Testcase for bug 448329</title>
+<script>
+function go() {
+ test("myFrame", "backcolor");
+}
+function test(id,cmd) {
+ var doc = document.getElementById(id).contentDocument;
+ doc.designMode = "On";
+
+ var s = doc.defaultView.getSelection();
+ s.removeAllRanges();
+ s.addRange(doc.createRange());
+
+ doc.queryCommandIndeterm(cmd);
+}
+</script>
+</head>
+<body onload="go()"><iframe id="myFrame"></iframe></body>
+</html>
diff --git a/editor/libeditor/crashtests/448329-3.html b/editor/libeditor/crashtests/448329-3.html
new file mode 100644
index 000000000..0a48c1818
--- /dev/null
+++ b/editor/libeditor/crashtests/448329-3.html
@@ -0,0 +1,112 @@
+<!DOCTYPE HTML>
+<html><head>
+ <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
+ <title>Testcase #3 for bug 448329</title>
+</head>
+<body>
+
+<iframe id="frame448329"></iframe>
+
+<script>
+
+function test448329(id,cmd,val) {
+
+ var elm = document.getElementById(id);
+ var doc = elm.contentDocument;
+ doc.designMode = "On";
+
+ // Work around getSelection depending on a presshell but not flushing to get
+ // one.
+ doc.body.offsetWidth;
+ var s = doc.defaultView.getSelection();
+
+ // Test document node
+ if (s.rangeCount > 0)
+ s.removeAllRanges();
+ var range = doc.createRange();
+ range.setStart(doc, 0);
+ range.setEnd(doc, 0);
+ s.addRange(range);
+ doc.execCommand(cmd,false,val);
+
+ // Test HTML node
+ if (s.rangeCount > 0)
+ s.removeAllRanges();
+ range = doc.createRange();
+ range.setStart(doc.documentElement, 0);
+ range.setEnd(doc.documentElement, 0);
+ s.addRange(range);
+ doc.execCommand(cmd,false,val);
+
+ // Test BODY node
+ if (s.rangeCount > 0)
+ s.removeAllRanges();
+ range = doc.createRange();
+ var body = doc.documentElement.childNodes[1];
+ range.setStart(body, 0);
+ range.setEnd(body, 0);
+ s.addRange(range);
+ doc.execCommand(cmd,false,val);
+
+ var text = doc.createTextNode("Hello Kitty");
+ body.insertBefore(text, null)
+
+ // Test TEXT node
+ if (s.rangeCount > 0)
+ s.removeAllRanges();
+ range = doc.createRange();
+ range.setStart(text, 0);
+ range.setEnd(text, 1);
+ s.addRange(range);
+ doc.execCommand(cmd,false,val);
+
+ // Test BODY[0,0] + TEXT node
+ if (s.rangeCount > 0)
+ s.removeAllRanges();
+ range = doc.createRange();
+ range.setStart(body, 0);
+ range.setEnd(body, 0);
+ s.addRange(range);
+ range = doc.createRange();
+ range.setStart(text, 0);
+ range.setEnd(text, 1);
+ s.addRange(range);
+ doc.execCommand(cmd,false,val);
+
+ // Test BODY[0,1] + TEXT node
+ if (s.rangeCount > 0)
+ s.removeAllRanges();
+ range = doc.createRange();
+ range.setStart(body, 0);
+ range.setEnd(body, 1);
+ s.addRange(range);
+ range = doc.createRange();
+ range.setStart(text, 0);
+ range.setEnd(text, 1);
+ s.addRange(range);
+ doc.execCommand(cmd,false,val);
+
+ // Test BODY[0,1] + TEXT node without a parent
+ if (s.rangeCount > 0)
+ s.removeAllRanges();
+ range = doc.createRange();
+ range.setStart(body, 0);
+ range.setEnd(body, 1);
+ s.addRange(range);
+ range = doc.createRange();
+ text = doc.createTextNode("Hello Kitty"); // not in doc
+ range.setStart(text, 0);
+ range.setEnd(text, 1);
+ s.addRange(range);
+ doc.execCommand(cmd,false,val);
+
+}
+
+test448329("frame448329", "backcolor", "green")
+test448329("frame448329", "hilitecolor", "green")
+
+</script>
+
+
+</body>
+</html>
diff --git a/editor/libeditor/crashtests/456727-1.html b/editor/libeditor/crashtests/456727-1.html
new file mode 100644
index 000000000..d14422c93
--- /dev/null
+++ b/editor/libeditor/crashtests/456727-1.html
@@ -0,0 +1,8 @@
+<html>
+<BODY onload="
+document.designMode='on';
+document.replaceChild(document.createElement('HTML'), document.firstChild);
+document.queryCommandValue('backcolor');
+">
+</body>
+</html> \ No newline at end of file
diff --git a/editor/libeditor/crashtests/456727-2.html b/editor/libeditor/crashtests/456727-2.html
new file mode 100644
index 000000000..1c8fe5db9
--- /dev/null
+++ b/editor/libeditor/crashtests/456727-2.html
@@ -0,0 +1,8 @@
+<html>
+<BODY onload="
+document.designMode='on';
+document.removeChild(document.firstChild);
+document.queryCommandState('BackColor');
+">
+</body>
+</html>
diff --git a/editor/libeditor/crashtests/459613-iframe.html b/editor/libeditor/crashtests/459613-iframe.html
new file mode 100644
index 000000000..9f6e558d0
--- /dev/null
+++ b/editor/libeditor/crashtests/459613-iframe.html
@@ -0,0 +1 @@
+<html><body><textarea>notaword</textarea></body></html>
diff --git a/editor/libeditor/crashtests/459613.html b/editor/libeditor/crashtests/459613.html
new file mode 100644
index 000000000..7a335f22e
--- /dev/null
+++ b/editor/libeditor/crashtests/459613.html
@@ -0,0 +1,17 @@
+<html class="reftest-wait">
+<head>
+<script type="text/javascript">
+
+function finish() {
+ document.documentElement.removeAttribute("class");
+}
+
+</script>
+</head>
+
+<body>
+
+<iframe src="459613-iframe.html" onload="finish();"></iframe>
+
+</body>
+</html>
diff --git a/editor/libeditor/crashtests/467647-1.html b/editor/libeditor/crashtests/467647-1.html
new file mode 100644
index 000000000..7bb4271d7
--- /dev/null
+++ b/editor/libeditor/crashtests/467647-1.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script type="text/javascript">
+
+function boom()
+{
+ document.getElementById("s").focus();
+ try {
+ document.execCommand("insertorderedlist", false, null);
+ } catch(e) { }
+}
+
+</script>
+</head>
+
+<body onload="boom();"><span id="s" contenteditable="true">One<div></div></span><marquee></marquee></body>
+
+</html>
diff --git a/editor/libeditor/crashtests/475132-1.xhtml b/editor/libeditor/crashtests/475132-1.xhtml
new file mode 100644
index 000000000..8b4dd688c
--- /dev/null
+++ b/editor/libeditor/crashtests/475132-1.xhtml
@@ -0,0 +1,20 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<script type="text/javascript">
+
+function boom()
+{
+ document.getElementsByTagName("td")[0].contentEditable = "true";
+ document.getElementsByTagName("td")[0].focus();
+ document.documentElement.contentEditable = "true";
+ document.documentElement.focus();
+ document.execCommand("indent", false, null);
+ document.execCommand("insertParagraph", false, null);
+}
+
+</script>
+</head>
+
+<body onload="boom();" contenteditable="false"><td></td></body>
+
+</html>
diff --git a/editor/libeditor/crashtests/499844-1.html b/editor/libeditor/crashtests/499844-1.html
new file mode 100644
index 000000000..4fa509c4a
--- /dev/null
+++ b/editor/libeditor/crashtests/499844-1.html
@@ -0,0 +1,15 @@
+<html>
+<head>
+<script type="text/javascript">
+
+function boom()
+{
+ document.body.contentEditable = "true";
+ document.execCommand("outdent", false, null);
+}
+
+</script>
+</head>
+
+<body style="word-spacing: 3px;" onload="boom();"> &#x0301;</body>
+</html>
diff --git a/editor/libeditor/crashtests/503709-1.xhtml b/editor/libeditor/crashtests/503709-1.xhtml
new file mode 100644
index 000000000..867bebf1a
--- /dev/null
+++ b/editor/libeditor/crashtests/503709-1.xhtml
@@ -0,0 +1,11 @@
+<html contenteditable="true" xmlns="http://www.w3.org/1999/xhtml"><head><script>
+
+function boom()
+{
+ document.execCommand("selectAll", false, "");
+ try { document.execCommand("justifyfull", false, null); } catch(e) { }
+ try { document.execCommand("inserthorizontalrule", false, "false"); } catch(e) { }
+ document.execCommand("delete", false, null);
+}
+
+</script></head>x y z<body onload="boom();"><div/></body></html>
diff --git a/editor/libeditor/crashtests/513375-1.xhtml b/editor/libeditor/crashtests/513375-1.xhtml
new file mode 100644
index 000000000..25e5e1c34
--- /dev/null
+++ b/editor/libeditor/crashtests/513375-1.xhtml
@@ -0,0 +1,19 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head contenteditable="true">
+<script type="text/javascript">
+<![CDATA[
+
+function boom()
+{
+ var r = document.createRange();
+ r.selectNode(document.body);
+ r.deleteContents();
+ try { document.execCommand("selectAll", false, null); } catch(e) { }
+}
+
+]]>
+</script>
+</head>
+
+<body onload="boom();" contenteditable="true"></body>
+</html>
diff --git a/editor/libeditor/crashtests/535632-1.xhtml b/editor/libeditor/crashtests/535632-1.xhtml
new file mode 100644
index 000000000..92470b825
--- /dev/null
+++ b/editor/libeditor/crashtests/535632-1.xhtml
@@ -0,0 +1 @@
+<body xmlns="http://www.w3.org/1999/xhtml" style="margin: 200px;" contenteditable="true" onload="document.execCommand('outdent', false, null);" /> \ No newline at end of file
diff --git a/editor/libeditor/crashtests/574558-1.xhtml b/editor/libeditor/crashtests/574558-1.xhtml
new file mode 100644
index 000000000..6aac47072
--- /dev/null
+++ b/editor/libeditor/crashtests/574558-1.xhtml
@@ -0,0 +1,15 @@
+<html xmlns="http://www.w3.org/1999/xhtml"><head><script>
+<![CDATA[
+
+function boom()
+{
+ document.execCommand("selectAll", false, null);
+ document.execCommand("selectAll", false, null);
+ document.execCommand("inserthtml", false, "<span><div>");
+ var textarea = document.getElementById("textarea");
+ var span = document.createElementNS("http://www.w3.org/1999/xhtml", "span");
+ textarea.appendChild(span);
+}
+
+]]>
+</script></head><div contenteditable="true"></div><body onload="boom();"><textarea id="textarea">f</textarea></body></html>
diff --git a/editor/libeditor/crashtests/580151-1.xhtml b/editor/libeditor/crashtests/580151-1.xhtml
new file mode 100644
index 000000000..379941111
--- /dev/null
+++ b/editor/libeditor/crashtests/580151-1.xhtml
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+var t;
+
+function boom()
+{
+ var b = document.createElementNS("http://www.w3.org/1999/xhtml", "body");
+ t = document.createElementNS("http://www.w3.org/1999/xhtml", "textarea");
+ b.appendChild(t);
+ document.removeChild(document.documentElement)
+ document.appendChild(b)
+ document.removeChild(document.documentElement)
+ var ns = document.createElementNS("http://www.w3.org/1999/xhtml", "script");
+ var nt = document.createTextNode("t.appendChild(document.createTextNode(' '));");
+ ns.appendChild(nt);
+ b.appendChild(ns);
+ document.appendChild(b);
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/editor/libeditor/crashtests/582138-1.xhtml b/editor/libeditor/crashtests/582138-1.xhtml
new file mode 100644
index 000000000..afcec2eba
--- /dev/null
+++ b/editor/libeditor/crashtests/582138-1.xhtml
@@ -0,0 +1,10 @@
+<html xmlns="http://www.w3.org/1999/xhtml"><mtr xmlns="http://www.w3.org/1998/Math/MathML"><td id="cell" xmlns="http://www.w3.org/1999/xhtml"></td></mtr><script>
+function boom()
+{
+ document.getElementById("cell").contentEditable = true;
+ document.getElementById("cell").focus();
+ document.execCommand("inserthtml", false, "x");
+}
+
+window.addEventListener("load", boom, false);
+</script></html>
diff --git a/editor/libeditor/crashtests/612565-1.html b/editor/libeditor/crashtests/612565-1.html
new file mode 100644
index 000000000..1b059aa66
--- /dev/null
+++ b/editor/libeditor/crashtests/612565-1.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <iframe></iframe>
+ </body>
+ <script>
+ onload = function() {
+ var i = document.querySelector("iframe");
+ var doc = i.contentDocument;
+ doc.body.appendChild(doc.createTextNode("foo"));
+ doc.designMode = "on";
+ while (doc.body.firstChild) {
+ doc.body.removeChild(doc.body.firstChild);
+ }
+ };
+ </script>
+</html>
diff --git a/editor/libeditor/crashtests/615015-1.html b/editor/libeditor/crashtests/615015-1.html
new file mode 100644
index 000000000..a383f9e75
--- /dev/null
+++ b/editor/libeditor/crashtests/615015-1.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ document.getElementById("j").focus();
+ try {
+ document.execCommand("insertunorderedlist", false, null);
+ } catch(e) { }
+}
+
+</script>
+</head>
+<body onload="boom();"><span><span contenteditable id="j"></span>T</span></body>
+</html>
diff --git a/editor/libeditor/crashtests/615450-1.html b/editor/libeditor/crashtests/615450-1.html
new file mode 100644
index 000000000..fb36bddc9
--- /dev/null
+++ b/editor/libeditor/crashtests/615450-1.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html contenteditable="true">
+<head>
+<script>
+
+function boom()
+{
+ document.documentElement.appendChild(document.body);
+ document.documentElement.contentEditable = "false";
+ try { document.execCommand("outdent", false, null); } catch(e) { }
+ document.body.contentEditable = "true";
+ try { document.execCommand("inserthtml", false, "x"); } catch(e) { }
+}
+
+</script>
+</head>
+<body onload="boom();"><div style="margin-left: 40px;"><span contenteditable="true">p q r s</span> T</div></body></html>
diff --git a/editor/libeditor/crashtests/633709.xhtml b/editor/libeditor/crashtests/633709.xhtml
new file mode 100644
index 000000000..139389001
--- /dev/null
+++ b/editor/libeditor/crashtests/633709.xhtml
@@ -0,0 +1,36 @@
+<html xmlns="http://www.w3.org/1999/xhtml" class="reftest-wait">
+
+<body><div contenteditable="true"></div><div><input id="i"><div></div></input></div></body>
+
+<script id="s">
+<![CDATA[
+
+function boom()
+{
+ document.getElementById("i").focus();
+
+ try { document.execCommand("stylewithcss", false, "true") } catch(e) { }
+ try { document.execCommand("inserthtml", false, "<x>X</x>"); } catch(e) { }
+ try { document.execCommand("underline", false, null); } catch(e) { }
+ try { document.execCommand("justifyfull", false, null); } catch(e) { }
+ try { document.execCommand("underline", false, null); } catch(e) { }
+ try { document.execCommand("insertParagraph", false, null); } catch(e) { }
+ try { document.execCommand("delete", false, null); } catch(e) { }
+
+ try { document.execCommand("stylewithcss", false, "false") } catch(e) { }
+ try { document.execCommand("inserthtml", false, "<x>X</x>"); } catch(e) { }
+ try { document.execCommand("underline", false, null); } catch(e) { }
+ try { document.execCommand("justifyfull", false, null); } catch(e) { }
+ try { document.execCommand("underline", false, null); } catch(e) { }
+ try { document.execCommand("insertParagraph", false, null); } catch(e) { }
+ try { document.execCommand("delete", false, null); } catch(e) { }
+
+ document.documentElement.removeAttribute("class");
+}
+
+setTimeout(boom, 10);
+
+]]>
+</script>
+
+</html>
diff --git a/editor/libeditor/crashtests/636074-1.html b/editor/libeditor/crashtests/636074-1.html
new file mode 100644
index 000000000..e99c42ea6
--- /dev/null
+++ b/editor/libeditor/crashtests/636074-1.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ document.getElementById("i").focus();
+ document.documentElement.contentEditable = "true";
+ document.execCommand("inserthtml", false, "<table>");
+ document.execCommand("indent", false, null);
+ document.execCommand("delete", false, null);
+}
+
+</script>
+</head>
+<body onload="boom();"><input id="i"></body>
+</html>
diff --git a/editor/libeditor/crashtests/639736-1.xhtml b/editor/libeditor/crashtests/639736-1.xhtml
new file mode 100644
index 000000000..4692daee7
--- /dev/null
+++ b/editor/libeditor/crashtests/639736-1.xhtml
@@ -0,0 +1,14 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<script>
+
+function boom()
+{
+ try { document.execCommand("removeformat", false, null); } catch(e) { }
+ document.adoptNode(document.documentElement);
+}
+
+</script>
+</head>
+<body onload="boom();"><td contenteditable="true" /></body>
+</html>
diff --git a/editor/libeditor/crashtests/643786-1.html b/editor/libeditor/crashtests/643786-1.html
new file mode 100644
index 000000000..3f0b27a54
--- /dev/null
+++ b/editor/libeditor/crashtests/643786-1.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var f = document.getElementById("f");
+ var fw = f.contentWindow;
+ fw.document.designMode = 'on';
+ f.style.content = "'m'";
+ fw.document.removeChild(fw.document.documentElement)
+}
+
+</script>
+</head>
+<body onload="boom();"><iframe id="f" src="data:text/html,"></iframe></body>
+</html>
diff --git a/editor/libeditor/crashtests/650572-1.html b/editor/libeditor/crashtests/650572-1.html
new file mode 100644
index 000000000..a86f6e618
--- /dev/null
+++ b/editor/libeditor/crashtests/650572-1.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ document.documentElement.offsetHeight;
+ document.getElementById("d").focus();
+ document.getElementById("b").style.display = "inline";
+ document.getElementById("c").contentEditable = "false";
+}
+
+</script>
+</head>
+<body contenteditable="true" id="b" onload="setTimeout(boom, 200);"><div id="c"><input id="d"></div></body>
+</html>
+
diff --git a/editor/libeditor/crashtests/667321-1.html b/editor/libeditor/crashtests/667321-1.html
new file mode 100644
index 000000000..275269522
--- /dev/null
+++ b/editor/libeditor/crashtests/667321-1.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ document.body.style.cssFloat = "left";
+ document.createElement("div").appendChild(document.querySelector("legend"));
+}
+
+</script>
+</head>
+<body onload="boom();"><fieldset><legend></legend><textarea></textarea></fieldset></body>
+</html>
diff --git a/editor/libeditor/crashtests/682650-1.html b/editor/libeditor/crashtests/682650-1.html
new file mode 100644
index 000000000..66ebc2f62
--- /dev/null
+++ b/editor/libeditor/crashtests/682650-1.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function repeatChar(s, n)
+{
+ while (s.length < n)
+ s += s;
+ return s.substr(0, n);
+}
+
+function boom()
+{
+ document.documentElement.contentEditable = "true";
+ document.execCommand("inserthtml", false, "<button><\/button>");
+ document.execCommand("inserthtml", false, repeatChar("i", 34646));
+ document.execCommand("contentReadOnly", false, null);
+ document.execCommand("removeformat", false, null);
+ document.execCommand("hilitecolor", false, "red");
+ document.execCommand("inserthtml", false, "a");
+ document.execCommand("delete", false, null);
+}
+
+</script>
+</head>
+
+<body onload="boom();"></body>
+
+</html>
diff --git a/editor/libeditor/crashtests/713427-1.html b/editor/libeditor/crashtests/713427-1.html
new file mode 100644
index 000000000..21da24693
--- /dev/null
+++ b/editor/libeditor/crashtests/713427-1.html
@@ -0,0 +1,9 @@
+<span>
+<script contenteditable="true"></script>
+<blockquote>
+<input>
+<code style="display: table-row;">
+<html contenteditable="true">
+</blockquote>
+
+
diff --git a/editor/libeditor/crashtests/713427-2.xhtml b/editor/libeditor/crashtests/713427-2.xhtml
new file mode 100644
index 000000000..b04a5d773
--- /dev/null
+++ b/editor/libeditor/crashtests/713427-2.xhtml
@@ -0,0 +1,28 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<script>
+<![CDATA[
+
+function boom()
+{
+ while (document.documentElement.firstChild) {
+ document.documentElement.removeChild(document.documentElement.firstChild);
+ }
+
+ var td = document.createElementNS("http://www.w3.org/1999/xhtml", "td");
+ td.setAttributeNS(null, "contenteditable", "true");
+ (document.documentElement).appendChild(td);
+ var head = document.createElementNS("http://www.w3.org/1999/xhtml", "head");
+ (document.documentElement).appendChild(head);
+
+ head.appendChild(td);
+}
+
+window.addEventListener("load", boom, false);
+
+]]>
+</script>
+</head>
+
+<body></body>
+</html>
diff --git a/editor/libeditor/crashtests/716456-1.html b/editor/libeditor/crashtests/716456-1.html
new file mode 100644
index 000000000..a5ef5a2cc
--- /dev/null
+++ b/editor/libeditor/crashtests/716456-1.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<script>
+
+function boom()
+{
+ var div = document.querySelector("div");
+ div.contentEditable = "true";
+ div.focus();
+
+ var r = document.documentElement;
+ document["removeChild"](r);
+ document["appendChild"](r);
+
+ setTimeout(function() {
+ getSelection().collapse(div, 0);
+ document.execCommand("inserthtml", false, "a");
+ setTimeout(function() {
+ document.documentElement.removeAttribute("class");
+ }, 0);
+ }, 0);
+}
+
+</script>
+</head>
+
+<body onload="boom();"><div></div></body>
+</html>
diff --git a/editor/libeditor/crashtests/759748.html b/editor/libeditor/crashtests/759748.html
new file mode 100644
index 000000000..1e85a3877
--- /dev/null
+++ b/editor/libeditor/crashtests/759748.html
@@ -0,0 +1,58 @@
+<!doctype html>
+<body>
+<script>
+var cmds = {
+ bold: "",
+ italic: "",
+ underline: "",
+ strikethrough: "",
+ subscript: "",
+ superscript: "",
+ cut: "",
+ copy: "",
+ paste: "",
+ delete: "",
+ forwarddelete: "",
+ selectall: "",
+ undo: "",
+ redo: "",
+ indent: "",
+ outdent: "",
+ backcolor: "#888888",
+ forecolor: "#888888",
+ hilitecolor: "#888888",
+ fontname: "Courier",
+ fontsize: "6",
+ increasefontsize: "",
+ decreasefontsize: "",
+ inserthorizontalrule: "",
+ createlink: "foo",
+ insertimage: "foo",
+ inserthtml: "foo",
+ inserttext: "foo",
+ insertparagraph: "",
+ gethtml: "",
+ justifyleft: "",
+ justifyright: "",
+ justifycenter: "",
+ justifyfull: "",
+ removeformat: "",
+ unlink: "",
+ insertorderedlist: "",
+ insertunorderedlist: "",
+ formatblock: "h1",
+ heading: "h1",
+ stylewithcss: "true",
+ usecss: "true",
+ contentreadonly: "true",
+ readonly: "true",
+ insertbronreturn: "true",
+ enableobjectresizing: "true",
+ enableinlinetableediting: "true",
+};
+for (var k in cmds) {
+ document.body.innerHTML = "<div contenteditable>abc</div>";
+ getSelection().removeAllRanges();
+ try { document.execCommand(k, false, cmds[k]) } catch(e) {}
+}
+</script>
diff --git a/editor/libeditor/crashtests/761861.html b/editor/libeditor/crashtests/761861.html
new file mode 100644
index 000000000..0c1f3f521
--- /dev/null
+++ b/editor/libeditor/crashtests/761861.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<script>
+function boom() {
+ var r = document.documentElement;
+ while (r.firstChild) {
+ r.removeChild(r.firstChild);
+ }
+
+ document.documentElement.contentEditable = "true";
+ document.documentElement.appendChild(document.createElement("span"));
+ document.documentElement.firstChild.appendChild(document.createTextNode("_"));
+ document.execCommand("forwarddelete");
+}
+</script>
+<body onload="boom()">
diff --git a/editor/libeditor/crashtests/762183.html b/editor/libeditor/crashtests/762183.html
new file mode 100644
index 000000000..1916ac6fb
--- /dev/null
+++ b/editor/libeditor/crashtests/762183.html
@@ -0,0 +1,6 @@
+<body contenteditable=true>x y
+<script>
+document.body.firstChild.splitText(2).splitText(1).splitText(1);
+getSelection().collapse(document.body, 1);
+document.execCommand("forwardDelete", false, null);
+</script>
diff --git a/editor/libeditor/crashtests/766305.html b/editor/libeditor/crashtests/766305.html
new file mode 100644
index 000000000..a5000fe73
--- /dev/null
+++ b/editor/libeditor/crashtests/766305.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var s = "x";
+ for (var i = 0; i < 15; ++i)
+ s = s + s;
+ var t = document.createTextNode(s);
+ document.body.appendChild(t);
+ window.getSelection().collapse(t, s.length);
+ document.execCommand("insertText", false, "a");
+}
+
+</script>
+</head>
+
+<body contenteditable="true" onload="boom();"></body>
+</html>
diff --git a/editor/libeditor/crashtests/766360.html b/editor/libeditor/crashtests/766360.html
new file mode 100644
index 000000000..76c30456d
--- /dev/null
+++ b/editor/libeditor/crashtests/766360.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var r = document.createRange();
+ r.setEnd(document.createTextNode("x"), 0);
+ window.getSelection().addRange(r);
+ document.execCommand("inserthtml", false, "y");
+}
+
+</script>
+</head>
+
+<body contenteditable="true" onload="boom();"></body>
+</html>
diff --git a/editor/libeditor/crashtests/766387.html b/editor/libeditor/crashtests/766387.html
new file mode 100644
index 000000000..20ccc60d4
--- /dev/null
+++ b/editor/libeditor/crashtests/766387.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ window.getSelection().removeAllRanges();
+ var r = document.createRange();
+ r.setStart(document.getElementById("x"), 1);
+ r.setEnd(document.getElementById("y"), 0);
+ window.getSelection().addRange(r);
+ document.execCommand("insertorderedlist", false, null);
+}
+
+</script>
+</head>
+
+<body onload="boom();"><div id="x" contenteditable="true">a</div><div id="y" contenteditable="true"></div></body>
+</html>
diff --git a/editor/libeditor/crashtests/766413.html b/editor/libeditor/crashtests/766413.html
new file mode 100644
index 000000000..c5d9835e3
--- /dev/null
+++ b/editor/libeditor/crashtests/766413.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var root = document.documentElement;
+ while (root.firstChild) {
+ root.removeChild(root.firstChild);
+ }
+
+ var space = document.createTextNode(" ");
+ var body = document.createElementNS("http://www.w3.org/1999/xhtml", "body");
+ root.contentEditable = "true";
+ root.focus();
+ document.execCommand("contentReadOnly", false, null);
+ root.appendChild(body);
+ root.contentEditable = "false";
+ root.appendChild(space);
+ root.removeChild(body);
+ root.contentEditable = "true";
+
+ window.getSelection().removeAllRanges();
+ var r1 = document.createRange();
+ r1.setStart(root, 0);
+ r1.setEnd(root, 0);
+ window.getSelection().addRange(r1);
+ looseText = document.createTextNode("c");
+ var r2 = document.createRange();
+ r2.setStart(looseText, 0);
+ r2.setEnd(looseText, 0);
+ window.getSelection().addRange(r2);
+
+ document.execCommand("forwardDelete", false, null);
+}
+
+</script>
+</head>
+
+<body onload="boom();"></body>
+</html>
diff --git a/editor/libeditor/crashtests/766795.html b/editor/libeditor/crashtests/766795.html
new file mode 100644
index 000000000..b4ade3020
--- /dev/null
+++ b/editor/libeditor/crashtests/766795.html
@@ -0,0 +1,21 @@
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var fragEl = document.createElement("span");
+ fragEl.setAttribute("contenteditable", "true");
+ fragEl.setAttribute("style", "position: absolute;");
+
+ var frag = document.createDocumentFragment();
+ frag.appendChild(fragEl);
+
+ window.getSelection().selectAllChildren(fragEl);
+}
+
+</script>
+</head>
+
+<body contenteditable="true" onload="boom();"></body>
+</html>
diff --git a/editor/libeditor/crashtests/766845.xhtml b/editor/libeditor/crashtests/766845.xhtml
new file mode 100644
index 000000000..409e21010
--- /dev/null
+++ b/editor/libeditor/crashtests/766845.xhtml
@@ -0,0 +1,27 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<script>
+<![CDATA[
+
+function boom()
+{
+ window.getSelection().removeAllRanges();
+ var r1 = document.createRange();
+ r1.setStart(document.body, 0);
+ r1.setEnd (document.body, 1);
+ window.getSelection().addRange(r1);
+ var r2 = document.createRange();
+ r2.setStart(document.body, 1);
+ r2.setEnd (document.body, 2);
+ window.getSelection().addRange(r2);
+ if (document.queryCommandEnabled("inserthtml"))
+ document.execCommand("inserthtml", false, "1");
+}
+
+]]>
+</script>
+</head>
+
+<body contenteditable="true" onload="boom();"><div></div><div></div></body>
+
+</html>
diff --git a/editor/libeditor/crashtests/767169.html b/editor/libeditor/crashtests/767169.html
new file mode 100644
index 000000000..3dfad160c
--- /dev/null
+++ b/editor/libeditor/crashtests/767169.html
@@ -0,0 +1,23 @@
+<html>
+<head>
+<script>
+
+// Document must not have a doctype to trigger the bug
+
+function boom()
+{
+ var root = document.documentElement;
+ while (root.firstChild) { root.removeChild(root.firstChild); }
+ root.contentEditable = "true";
+ document.removeChild(root);
+ document.appendChild(root);
+ window.getSelection().collapse(root, 0);
+ window.getSelection().extend(document, 1);
+ document.removeChild(root);
+}
+
+</script>
+</head>
+
+<body onload="boom();"></body>
+</html>
diff --git a/editor/libeditor/crashtests/768748.html b/editor/libeditor/crashtests/768748.html
new file mode 100644
index 000000000..09206dce3
--- /dev/null
+++ b/editor/libeditor/crashtests/768748.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html contenteditable="true">
+<head>
+<script>
+
+function boom()
+{
+ var looseText = document.createTextNode("x");
+ window.getSelection().collapse(looseText, 0);
+ document.queryCommandState("insertorderedlist");
+}
+
+</script>
+</head>
+<body onload="setTimeout(boom, 0)"></body>
+</html>
diff --git a/editor/libeditor/crashtests/768765.html b/editor/libeditor/crashtests/768765.html
new file mode 100644
index 000000000..060e5161b
--- /dev/null
+++ b/editor/libeditor/crashtests/768765.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var root = document.documentElement;
+
+ while (root.firstChild) { root.removeChild(root.firstChild); }
+
+ var body = document.createElementNS("http://www.w3.org/1999/xhtml", "body");
+ var div = document.createElementNS("http://www.w3.org/1999/xhtml", "div");
+ root.contentEditable = "true";
+ root.appendChild(div);
+ root.removeChild(div);
+ root.insertBefore(body, root.firstChild);
+
+ window.getSelection().removeAllRanges();
+ var r0 = document.createRange();
+ r0.setStart(body, 0);
+ r0.setEnd(body, 0);
+ window.getSelection().addRange(r0);
+ var r1 = document.createRange();
+ r1.setStart(div, 0);
+ r1.setEnd(div, 0);
+ window.getSelection().addRange(r1);
+
+ document.execCommand("inserthtml", false, "1");
+}
+
+</script>
+</head>
+
+<body onload="boom();"></body>
+</html>
diff --git a/editor/libeditor/crashtests/769008-1.html b/editor/libeditor/crashtests/769008-1.html
new file mode 100644
index 000000000..8ea8a3601
--- /dev/null
+++ b/editor/libeditor/crashtests/769008-1.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var x = document.getElementById("x");
+
+ window.getSelection().removeAllRanges();
+
+ var range = document.createRange();
+ range.setStart(x, 0);
+ range.setEnd(x, 0);
+ window.getSelection().addRange(range);
+
+ document.execCommand("delete", false, "null");
+}
+
+</script>
+</head>
+<body contenteditable="true" onload="boom();"><div></div><span id="x"></span></body>
+</html>
diff --git a/editor/libeditor/crashtests/769967.xhtml b/editor/libeditor/crashtests/769967.xhtml
new file mode 100644
index 000000000..724f6b899
--- /dev/null
+++ b/editor/libeditor/crashtests/769967.xhtml
@@ -0,0 +1,16 @@
+<html xmlns="http://www.w3.org/1999/xhtml" contenteditable="true" style="-moz-user-select: all;"><sub>x</sub><script>
+function boom()
+{
+ window.getSelection().removeAllRanges();
+ var r = document.createRange();
+ r.setStart(document.documentElement, 0);
+ r.setEnd(document.documentElement, 0);
+ window.getSelection().addRange(r);
+
+ document.execCommand("subscript", false, null);
+ document.execCommand("insertText", false, "y");
+}
+
+window.addEventListener("load", boom, false);
+
+</script></html>
diff --git a/editor/libeditor/crashtests/771749.html b/editor/libeditor/crashtests/771749.html
new file mode 100644
index 000000000..9237364f2
--- /dev/null
+++ b/editor/libeditor/crashtests/771749.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var root = document.documentElement;
+ root.contentEditable = "true";
+ document.removeChild(root);
+ document.appendChild(root);
+ document.execCommand("insertunorderedlist", false, null);
+ document.execCommand("inserthtml", false, "<span></span>");
+ document.execCommand("outdent", false, null);
+}
+
+</script>
+</head>
+
+<body onload="boom();"></body>
+</html>
diff --git a/editor/libeditor/crashtests/772282.html b/editor/libeditor/crashtests/772282.html
new file mode 100644
index 000000000..f6259b344
--- /dev/null
+++ b/editor/libeditor/crashtests/772282.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var root = document.documentElement;
+ while(root.firstChild) { root.removeChild(root.firstChild); }
+ var body = document.createElementNS("http://www.w3.org/1999/xhtml", "body");
+ body.setAttributeNS(null, "contenteditable", "true");
+ var img = document.createElementNS("http://www.w3.org/1999/xhtml", "img");
+ body.appendChild(img);
+ root.appendChild(body);
+ document.removeChild(root);
+ document.appendChild(root);
+ document.execCommand("insertText", false, "5");
+ document.execCommand("selectAll", false, null);
+ document.execCommand("insertParagraph", false, null);
+ document.execCommand("increasefontsize", false, null);
+}
+
+</script>
+</head>
+
+<body onload="boom();"></body>
+</html>
diff --git a/editor/libeditor/crashtests/776323.html b/editor/libeditor/crashtests/776323.html
new file mode 100644
index 000000000..9fc2776c3
--- /dev/null
+++ b/editor/libeditor/crashtests/776323.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html contenteditable="true">
+<head>
+<script>
+
+function boom()
+{
+ document.execCommand("inserthtml", false, "b");
+ var myrange = document.createRange();
+ myrange.selectNodeContents(document.getElementsByTagName("img")[0]);
+ window.getSelection().addRange(myrange);
+ document.execCommand("strikethrough", false, null);
+}
+
+</script>
+</head>
+<body onload="boom();"><img></body>
+</html>
diff --git a/editor/libeditor/crashtests/793866.html b/editor/libeditor/crashtests/793866.html
new file mode 100644
index 000000000..4984474db
--- /dev/null
+++ b/editor/libeditor/crashtests/793866.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var b = document.body;
+ b.contentEditable = "true";
+ document.execCommand("contentReadOnly", false, null);
+ b.focus();
+ b.contentEditable = "false";
+ document.documentElement.contentEditable = "true";
+ document.createDocumentFragment().appendChild(b);
+ document.documentElement.focus();
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/editor/libeditor/crashtests/crashtests.list b/editor/libeditor/crashtests/crashtests.list
new file mode 100644
index 000000000..3fbc6b196
--- /dev/null
+++ b/editor/libeditor/crashtests/crashtests.list
@@ -0,0 +1,71 @@
+load 336081-1.xhtml
+load 336104.html
+load 382527-1.html
+load 382778-1.html
+load 402172-1.html
+load 403965-1.xhtml
+load 407074-1.html
+load 407079-1.html
+load 407256-1.html
+load 407277-1.html
+load 414178-1.html
+load 418923-1.html
+load 420439.html
+load 428489-1.html
+load 429586-1.html
+load 430624-1.html
+load 431086-1.xhtml
+load 448329-1.html
+load 448329-2.html
+load 448329-3.html
+load 456727-1.html
+load 456727-2.html
+load 459613.html
+needs-focus load 467647-1.html
+load 475132-1.xhtml
+load 499844-1.html
+load 503709-1.xhtml
+load 513375-1.xhtml
+load 535632-1.xhtml
+load 574558-1.xhtml
+load 580151-1.xhtml
+load 582138-1.xhtml
+load 612565-1.html
+load 615015-1.html
+load 615450-1.html
+load 633709.xhtml
+load 636074-1.html
+load 639736-1.xhtml
+load 643786-1.html
+load 650572-1.html
+load 667321-1.html
+load 682650-1.html
+load 713427-1.html
+load 713427-2.xhtml
+load 716456-1.html
+load 759748.html
+load 761861.html
+load 762183.html
+load 766305.html
+load 766360.html
+load 766387.html
+load 766413.html
+load 766795.html
+load 766845.xhtml
+load 767169.html
+load 768748.html
+load 768765.html
+load 769008-1.html
+load 769967.xhtml
+needs-focus load 771749.html
+load 772282.html
+load 776323.html
+needs-focus load 793866.html
+load 1057677.html
+needs-focus load 1128787.html
+load 1134545.html
+load 1158452.html
+load 1158651.html
+load 1244894.xhtml
+load 1272490.html
+load 1317704.html
diff --git a/editor/libeditor/moz.build b/editor/libeditor/moz.build
new file mode 100644
index 000000000..998ef3d39
--- /dev/null
+++ b/editor/libeditor/moz.build
@@ -0,0 +1,100 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+MOCHITEST_MANIFESTS += [
+ 'tests/browserscope/mochitest.ini',
+ 'tests/mochitest.ini',
+]
+
+MOCHITEST_CHROME_MANIFESTS += ['tests/chrome.ini']
+
+BROWSER_CHROME_MANIFESTS += ['tests/browser.ini']
+
+EXPORTS += [
+ 'nsIEditRules.h',
+]
+
+EXPORTS.mozilla += [
+ 'ChangeStyleTransaction.h',
+ 'CSSEditUtils.h',
+ 'EditorBase.h',
+ 'EditorController.h',
+ 'EditorUtils.h',
+ 'EditTransactionBase.h',
+ 'HTMLEditor.h',
+ 'SelectionState.h',
+ 'TextEditor.h',
+ 'TextEditRules.h',
+]
+
+UNIFIED_SOURCES += [
+ 'ChangeAttributeTransaction.cpp',
+ 'ChangeStyleTransaction.cpp',
+ 'CompositionTransaction.cpp',
+ 'CreateElementTransaction.cpp',
+ 'CSSEditUtils.cpp',
+ 'DeleteNodeTransaction.cpp',
+ 'DeleteRangeTransaction.cpp',
+ 'DeleteTextTransaction.cpp',
+ 'EditAggregateTransaction.cpp',
+ 'EditorBase.cpp',
+ 'EditorCommands.cpp',
+ 'EditorController.cpp',
+ 'EditorEventListener.cpp',
+ 'EditorUtils.cpp',
+ 'EditTransactionBase.cpp',
+ 'HTMLAbsPositionEditor.cpp',
+ 'HTMLAnonymousNodeEditor.cpp',
+ 'HTMLEditor.cpp',
+ 'HTMLEditorDataTransfer.cpp',
+ 'HTMLEditorEventListener.cpp',
+ 'HTMLEditorObjectResizer.cpp',
+ 'HTMLEditRules.cpp',
+ 'HTMLEditUtils.cpp',
+ 'HTMLInlineTableEditor.cpp',
+ 'HTMLStyleEditor.cpp',
+ 'HTMLTableEditor.cpp',
+ 'HTMLURIRefObject.cpp',
+ 'InsertNodeTransaction.cpp',
+ 'InsertTextTransaction.cpp',
+ 'InternetCiter.cpp',
+ 'JoinNodeTransaction.cpp',
+ 'PlaceholderTransaction.cpp',
+ 'SelectionState.cpp',
+ 'SetDocumentTitleTransaction.cpp',
+ 'SplitNodeTransaction.cpp',
+ 'StyleSheetTransactions.cpp',
+ 'TextEditor.cpp',
+ 'TextEditorDataTransfer.cpp',
+ 'TextEditorTest.cpp',
+ 'TextEditRules.cpp',
+ 'TextEditRulesBidi.cpp',
+ 'TextEditUtils.cpp',
+ 'TypeInState.cpp',
+ 'WSRunObject.cpp',
+]
+
+LOCAL_INCLUDES += [
+ '/dom/base',
+ '/editor/txmgr',
+ '/extensions/spellcheck/src',
+ '/layout/generic',
+ '/layout/style',
+ '/layout/tables',
+ '/layout/xul',
+]
+
+EXTRA_COMPONENTS += [
+ 'EditorUtils.js',
+ 'EditorUtils.manifest',
+]
+
+include('/ipc/chromium/chromium-config.mozbuild')
+
+FINAL_LIBRARY = 'xul'
+
+if CONFIG['GNU_CXX']:
+ CXXFLAGS += ['-Wno-error=shadow']
diff --git a/editor/libeditor/nsIAbsorbingTransaction.h b/editor/libeditor/nsIAbsorbingTransaction.h
new file mode 100644
index 000000000..e22caed4a
--- /dev/null
+++ b/editor/libeditor/nsIAbsorbingTransaction.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsIAbsorbingTransaction_h__
+#define nsIAbsorbingTransaction_h__
+
+#include "nsISupports.h"
+
+/*
+Transaction interface to outside world
+*/
+
+#define NS_IABSORBINGTRANSACTION_IID \
+{ /* a6cf9116-15b3-11d2-932e-00805f8add32 */ \
+ 0xa6cf9116, \
+ 0x15b3, \
+ 0x11d2, \
+ {0x93, 0x2e, 0x00, 0x80, 0x5f, 0x8a, 0xdd, 0x32} }
+
+class nsIAtom;
+
+namespace mozilla {
+class EditorBase;
+class SelectionState;
+} // namespace mozilla
+
+/**
+ * A transaction interface mixin - for transactions that can support.
+ * the placeholder absorbtion idiom.
+ */
+class nsIAbsorbingTransaction : public nsISupports{
+public:
+
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IABSORBINGTRANSACTION_IID)
+
+ NS_IMETHOD Init(nsIAtom* aName, mozilla::SelectionState* aSelState,
+ mozilla::EditorBase* aEditorBase) = 0;
+
+ NS_IMETHOD EndPlaceHolderBatch()=0;
+
+ NS_IMETHOD GetTxnName(nsIAtom **aName)=0;
+
+ NS_IMETHOD StartSelectionEquals(mozilla::SelectionState* aSelState,
+ bool* aResult) = 0;
+
+ NS_IMETHOD ForwardEndBatchTo(nsIAbsorbingTransaction *aForwardingAddress)=0;
+
+ NS_IMETHOD Commit()=0;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIAbsorbingTransaction,
+ NS_IABSORBINGTRANSACTION_IID)
+
+#endif // nsIAbsorbingTransaction_h__
+
diff --git a/editor/libeditor/nsIEditRules.h b/editor/libeditor/nsIEditRules.h
new file mode 100644
index 000000000..b186895ae
--- /dev/null
+++ b/editor/libeditor/nsIEditRules.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsIEditRules_h
+#define nsIEditRules_h
+
+#define NS_IEDITRULES_IID \
+{ 0x3836386d, 0x806a, 0x488d, \
+ { 0x8b, 0xab, 0xaf, 0x42, 0xbb, 0x4c, 0x90, 0x66 } }
+
+#include "mozilla/EditorBase.h" // for EditAction enum
+
+namespace mozilla {
+
+class TextEditor;
+namespace dom {
+class Selection;
+} // namespace dom
+
+/**
+ * Base for an object to encapsulate any additional info needed to be passed
+ * to rules system by the editor.
+ */
+class RulesInfo
+{
+public:
+ explicit RulesInfo(EditAction aAction)
+ : action(aAction)
+ {}
+ virtual ~RulesInfo() {}
+
+ EditAction action;
+};
+
+} // namespace mozilla
+
+/**
+ * Interface of editing rules.
+ */
+class nsIEditRules : public nsISupports
+{
+public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IEDITRULES_IID)
+
+//Interfaces for addref and release and queryinterface
+//NOTE: Use NS_DECL_ISUPPORTS_INHERITED in any class inherited from nsIEditRules
+
+ NS_IMETHOD Init(mozilla::TextEditor* aTextEditor) = 0;
+ NS_IMETHOD SetInitialValue(const nsAString& aValue) = 0;
+ NS_IMETHOD DetachEditor() = 0;
+ NS_IMETHOD BeforeEdit(EditAction action,
+ nsIEditor::EDirection aDirection) = 0;
+ NS_IMETHOD AfterEdit(EditAction action,
+ nsIEditor::EDirection aDirection) = 0;
+ NS_IMETHOD WillDoAction(mozilla::dom::Selection* aSelection,
+ mozilla::RulesInfo* aInfo, bool* aCancel,
+ bool* aHandled) = 0;
+ NS_IMETHOD DidDoAction(mozilla::dom::Selection* aSelection,
+ mozilla::RulesInfo* aInfo, nsresult aResult) = 0;
+ NS_IMETHOD DocumentIsEmpty(bool* aDocumentIsEmpty) = 0;
+ NS_IMETHOD DocumentModified() = 0;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIEditRules, NS_IEDITRULES_IID)
+
+#endif // #ifndef nsIEditRules_h
diff --git a/editor/libeditor/tests/browser.ini b/editor/libeditor/tests/browser.ini
new file mode 100644
index 000000000..249f59aa8
--- /dev/null
+++ b/editor/libeditor/tests/browser.ini
@@ -0,0 +1,6 @@
+[browser_bug527935.js]
+skip-if = toolkit == 'android'
+support-files = bug527935.html
+[browser_bug629172.js]
+skip-if = toolkit == 'android'
+support-files = bug629172.html
diff --git a/editor/libeditor/tests/browser_bug527935.js b/editor/libeditor/tests/browser_bug527935.js
new file mode 100644
index 000000000..dc6e74d3e
--- /dev/null
+++ b/editor/libeditor/tests/browser_bug527935.js
@@ -0,0 +1,63 @@
+add_task(function*() {
+ yield new Promise(resolve => waitForFocus(resolve, window));
+
+ const kPageURL = "http://example.org/browser/editor/libeditor/tests/bug527935.html";
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: kPageURL
+ }, function*(aBrowser) {
+ var popupShown = false;
+ function listener() {
+ popupShown = true;
+ }
+ SpecialPowers.addAutoCompletePopupEventListener(window, "popupshowing", listener);
+
+ yield ContentTask.spawn(aBrowser, {}, function*() {
+ var window = content.window.wrappedJSObject;
+ var document = window.document;
+ var formTarget = document.getElementById("formTarget");
+ var initValue = document.getElementById("initValue");
+
+ window.loadPromise = new Promise(resolve => {
+ formTarget.onload = resolve;
+ });
+
+ initValue.focus();
+ initValue.value = "foo";
+ });
+
+ EventUtils.synthesizeKey("VK_RETURN", {});
+
+ yield ContentTask.spawn(aBrowser, {}, function*() {
+ var window = content.window.wrappedJSObject;
+ var document = window.document;
+
+ yield window.loadPromise;
+
+ var newInput = document.createElement("input");
+ newInput.setAttribute("name", "test");
+ document.body.appendChild(newInput);
+
+ var event = document.createEvent("KeyboardEvent");
+
+ event.initKeyEvent("keypress", true, true, null, false, false,
+ false, false, 0, "f".charCodeAt(0));
+ newInput.value = "";
+ newInput.focus();
+ newInput.dispatchEvent(event);
+ });
+
+ yield new Promise(resolve => hitEventLoop(resolve, 100));
+
+ ok(!popupShown, "Popup must not be opened");
+ SpecialPowers.removeAutoCompletePopupEventListener(window, "popupshowing", listener);
+ });
+});
+
+function hitEventLoop(func, times) {
+ if (times > 0) {
+ setTimeout(hitEventLoop, 0, func, times - 1);
+ } else {
+ setTimeout(func, 0);
+ }
+}
diff --git a/editor/libeditor/tests/browser_bug629172.js b/editor/libeditor/tests/browser_bug629172.js
new file mode 100644
index 000000000..0c4f34069
--- /dev/null
+++ b/editor/libeditor/tests/browser_bug629172.js
@@ -0,0 +1,106 @@
+add_task(function*() {
+ yield new Promise(resolve => waitForFocus(resolve, window));
+
+ const kPageURL = "http://example.org/browser/editor/libeditor/tests/bug629172.html";
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: kPageURL
+ }, function*(aBrowser) {
+ yield ContentTask.spawn(aBrowser, {}, function*() {
+ var window = content.window.wrappedJSObject;
+ var document = window.document;
+
+ // Note: Using the with keyword, we would have been able to write this as:
+ //
+ // with (window) {
+ // Screenshots = {};
+ // // so on
+ // }
+ //
+ // However, browser-chrome tests are somehow forced to run in strict mode,
+ // which doesn't permit the usage of the with keyword, turning the following
+ // into the ugliest test you've ever seen. :(
+ var LTRRef = document.getElementById("ltr-ref");
+ var RTLRef = document.getElementById("rtl-ref");
+ window.Screenshots = {};
+
+ // generate the reference screenshots
+ LTRRef.style.display = "";
+ document.body.clientWidth;
+ window.Screenshots.ltr = window.snapshotWindow(window);
+ LTRRef.parentNode.removeChild(LTRRef);
+ RTLRef.style.display = "";
+ document.body.clientWidth;
+ window.Screenshots.rtl = window.snapshotWindow(window);
+ RTLRef.parentNode.removeChild(RTLRef);
+ window.Screenshots.get = function(dir, flip) {
+ if (flip) {
+ return this[dir == "rtl" ? "ltr" : "rtl"];
+ } else {
+ return this[dir];
+ }
+ };
+ });
+
+ function simulateCtrlShiftX(aBrowser) {
+ // In e10s, the keypress event will be dispatched to the content process,
+ // but in non-e10s it is handled by the browser UI code and hence won't
+ // reach the web page. As a result, we need to observe the event in
+ // the content process only in e10s mode.
+ var waitForKeypressContent = BrowserTestUtils.waitForContentEvent(aBrowser, "keypress");
+ EventUtils.synthesizeKey("x", {accelKey: true, shiftKey: true});
+ if (gMultiProcessBrowser) {
+ return waitForKeypressContent;
+ }
+ return Promise.resolve();
+ }
+
+ function* testDirection(initialDir, aBrowser) {
+ yield ContentTask.spawn(aBrowser, {initialDir}, function({initialDir}) {
+ var window = content.window.wrappedJSObject;
+ var document = window.document;
+
+ var t = window.t = document.createElement("textarea");
+ t.setAttribute("dir", initialDir);
+ t.value = "test.";
+ window.inputEventCount = 0;
+ t.oninput = function() { window.inputEventCount++; };
+ document.getElementById("content").appendChild(t);
+ document.body.clientWidth;
+ var s1 = window.snapshotWindow(window);
+ ok(window.compareSnapshots(s1, window.Screenshots.get(initialDir, false), true)[0],
+ "Textarea should appear correctly before switching the direction (" + initialDir + ")");
+ t.focus();
+ is(window.inputEventCount, 0, "input event count must be 0 before");
+ });
+ yield simulateCtrlShiftX(aBrowser);
+ yield ContentTask.spawn(aBrowser, {initialDir}, function({initialDir}) {
+ var window = content.window.wrappedJSObject;
+
+ is(window.t.getAttribute("dir"), initialDir == "ltr" ? "rtl" : "ltr", "The dir attribute must be correctly updated");
+ is(window.inputEventCount, 1, "input event count must be 1 after");
+ window.t.blur();
+ var s2 = window.snapshotWindow(window);
+ ok(window.compareSnapshots(s2, window.Screenshots.get(initialDir, true), true)[0],
+ "Textarea should appear correctly after switching the direction (" + initialDir + ")");
+ window.t.focus();
+ is(window.inputEventCount, 1, "input event count must be 1 before");
+ });
+ yield simulateCtrlShiftX(aBrowser);
+ yield ContentTask.spawn(aBrowser, {initialDir}, function({initialDir}) {
+ var window = content.window.wrappedJSObject;
+
+ is(window.inputEventCount, 2, "input event count must be 2 after");
+ is(window.t.getAttribute("dir"), initialDir == "ltr" ? "ltr" : "rtl", "The dir attribute must be correctly updated");
+ window.t.blur();
+ var s3 = window.snapshotWindow(window);
+ ok(window.compareSnapshots(s3, window.Screenshots.get(initialDir, false), true)[0],
+ "Textarea should appear correctly after switching back the direction (" + initialDir + ")");
+ window.t.parentNode.removeChild(window.t);
+ });
+ }
+
+ yield testDirection("ltr", aBrowser);
+ yield testDirection("rtl", aBrowser);
+ });
+});
diff --git a/editor/libeditor/tests/browserscope/lib/richtext/LICENSE b/editor/libeditor/tests/browserscope/lib/richtext/LICENSE
new file mode 100644
index 000000000..57bc88a15
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext/LICENSE
@@ -0,0 +1,202 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext/README b/editor/libeditor/tests/browserscope/lib/richtext/README
new file mode 100644
index 000000000..a3bc3110f
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext/README
@@ -0,0 +1,58 @@
+README FOR BROWSERSCOPE
+-----------------------
+
+Hey there - thanks for downloading the code. This file has instructions
+for getting setup so that you can run the codebase locally.
+
+This project is built on Google App Engine using the
+Django web application framework and written in Python.
+
+To get started, you'll need to first download the App Engine SDK at:
+http://code.google.com/appengine/downloads.html
+
+For local development, just startup the server:
+./pathto/google_appengine/dev_appserver.py --port=8080 browserscope
+
+You should then be able to access the local application at:
+http://localhost:8080/
+
+Note: the first time you hit the homepage it may take a little
+while - that's because it's trying to read out median times for all
+of the tests from a nonexistent datastore and write to memcache.
+Just be a lil patient.
+
+You can run the unit tests at:
+ http://localhost:8080/test
+
+
+CONTRIBUTING
+------------------
+
+Most likely you are interested in adding new tests or creating
+a new test category. If you are interested in adding tests to an existing
+"category" you may want to get in touch with the maintainer for that
+branch of the tree. We are really looking forward to receiving your
+code in patch format. Currently the category maintainers are:
+Network: Steve Souders <souders@gmail.com>
+Reflow: Lindsey Simon <elsigh@gmail.com>
+Security: Adam Barth <adam@adambarth.com> and Collin Jackson <collin@collinjackson.com>
+
+
+To create a completely new test category:
+ * Copy one of the existing directories in categories/
+ * Edit your test_set.py, handlers.py
+ * Add your files in templates/ and static/
+ * Update urls.py and settings.CATEGORIES
+ * Follow the examples of other tests re:
+ * beaconing using/testdriver_base
+ * your GetScoreAndDisplayValue method
+ * your GetRowScoreAndDisplayValue method
+
+References:
+ * App Engine Docs - http://code.google.com/appengine/docs/python/overview.html
+ * App Engine Group - http://groups.google.com/group/google-appengine
+ * Python Docs - http://www.python.org/doc/
+ * Django - http://www.djangoproject.com/
+
+
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext/README.Mozilla b/editor/libeditor/tests/browserscope/lib/richtext/README.Mozilla
new file mode 100644
index 000000000..5d304943f
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext/README.Mozilla
@@ -0,0 +1,17 @@
+The BrowserScope project provides a set of cross-browser HTML editor tests,
+which we import in our test suite in order to run them as part of our
+continuous integration system.
+
+We pull tests occasionally from their Subversion repository using the pull
+script which can be found in this directory. We also record the revision ID
+which we've used in the current_revision file inside this directory.
+
+Using the pull script is quite easy, just switch to this directory, and say:
+
+sh update_from_upstream
+
+There are tests which we're currently failing on, and there will probably be
+more of those in the future. We should maintain a list of the failing tests
+manually in currentStatus.js (which can also be found in this directory), to
+make sure that the suite passes entirely, with failing tests marked as todo
+items.
diff --git a/editor/libeditor/tests/browserscope/lib/richtext/currentStatus.js b/editor/libeditor/tests/browserscope/lib/richtext/currentStatus.js
new file mode 100644
index 000000000..b30775d04
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext/currentStatus.js
@@ -0,0 +1,46 @@
+/**
+ * This file lists the tests in the BrowserScope suite which we are currently
+ * failing. We mark them as todo items to keep track of them.
+ */
+
+var knownFailures = {
+ // Dummy result items. There is one for each category.
+ 'apply' : {
+ '0-undefined' : true
+ },
+ 'unapply' : {
+ '0-undefined' : true
+ },
+ 'change' : {
+ '0-undefined' : true
+ },
+ 'query' : {
+ '0-undefined' : true
+ },
+ 'a' : {
+ 'createbookmark-0' : true,
+ 'fontsize-1' : true,
+ 'subscript-1' : true,
+ 'superscript-1' : true,
+ },
+ 'u': {
+ 'removeformat-1' : true,
+ 'removeformat-2' : true,
+ 'strikethrough-2' : true,
+ 'subscript-1' : true,
+ 'superscript-1' : true,
+ 'unbookmark-0' : true,
+ },
+ 'q': {
+ 'fontsize-1' : true,
+ 'fontsize-2' : true,
+ },
+ 'c': {
+ 'fontsize-1' : true,
+ 'fontsize-2' : true,
+ },
+};
+
+function isKnownFailure(type, test, param) {
+ return (type in knownFailures) && ((test + "-" + param) in knownFailures[type]);
+}
diff --git a/editor/libeditor/tests/browserscope/lib/richtext/current_revision b/editor/libeditor/tests/browserscope/lib/richtext/current_revision
new file mode 100644
index 000000000..1e2569914
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext/current_revision
@@ -0,0 +1 @@
+775
diff --git a/editor/libeditor/tests/browserscope/lib/richtext/richtext/editable.html b/editor/libeditor/tests/browserscope/lib/richtext/richtext/editable.html
new file mode 100644
index 000000000..a294f0b56
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext/richtext/editable.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+ <script>
+ function load(){
+ window.document.designMode = "On";
+ }
+ </script>
+</head>
+<body contentEditable="true" onload="load()">
+</body>
+</html> \ No newline at end of file
diff --git a/editor/libeditor/tests/browserscope/lib/richtext/richtext/js/range.js b/editor/libeditor/tests/browserscope/lib/richtext/richtext/js/range.js
new file mode 100644
index 000000000..3e4463e11
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext/richtext/js/range.js
@@ -0,0 +1,1069 @@
+var goog$global = this, goog$isString = function(val) {
+ return typeof val == "string"
+};
+Math.floor(Math.random() * 2147483648).toString(36);
+var goog$now = Date.now || function() {
+ return(new Date).getTime()
+}, goog$inherits = function(childCtor, parentCtor) {
+ function tempCtor() {
+ }
+ tempCtor.prototype = parentCtor.prototype;
+ childCtor.superClass_ = parentCtor.prototype;
+ childCtor.prototype = new tempCtor
+};var goog$array$peek = function(array) {
+ return array[array.length - 1]
+}, goog$array$indexOf = function(arr, obj, opt_fromIndex) {
+ if(arr.indexOf)return arr.indexOf(obj, opt_fromIndex);
+ if(Array.indexOf)return Array.indexOf(arr, obj, opt_fromIndex);
+ for(var fromIndex = opt_fromIndex == null ? 0 : opt_fromIndex < 0 ? Math.max(0, arr.length + opt_fromIndex) : opt_fromIndex, i = fromIndex;i < arr.length;i++)if(i in arr && arr[i] === obj)return i;
+ return-1
+}, goog$array$map = function(arr, f, opt_obj) {
+ if(arr.map)return arr.map(f, opt_obj);
+ if(Array.map)return Array.map(arr, f, opt_obj);
+ for(var l = arr.length, res = [], resLength = 0, arr2 = goog$isString(arr) ? arr.split("") : arr, i = 0;i < l;i++)if(i in arr2)res[resLength++] = f.call(opt_obj, arr2[i], i, arr);
+ return res
+}, goog$array$some = function(arr, f, opt_obj) {
+ if(arr.some)return arr.some(f, opt_obj);
+ if(Array.some)return Array.some(arr, f, opt_obj);
+ for(var l = arr.length, arr2 = goog$isString(arr) ? arr.split("") : arr, i = 0;i < l;i++)if(i in arr2 && f.call(opt_obj, arr2[i], i, arr))return true;
+ return false
+}, goog$array$every = function(arr, f, opt_obj) {
+ if(arr.every)return arr.every(f, opt_obj);
+ if(Array.every)return Array.every(arr, f, opt_obj);
+ for(var l = arr.length, arr2 = goog$isString(arr) ? arr.split("") : arr, i = 0;i < l;i++)if(i in arr2 && !f.call(opt_obj, arr2[i], i, arr))return false;
+ return true
+}, goog$array$find = function(arr, f, opt_obj) {
+ var i;
+ JSCompiler_inline_label_goog$array$findIndex_12: {
+ for(var JSCompiler_inline_l = arr.length, JSCompiler_inline_arr2 = goog$isString(arr) ? arr.split("") : arr, JSCompiler_inline_i = 0;JSCompiler_inline_i < JSCompiler_inline_l;JSCompiler_inline_i++)if(JSCompiler_inline_i in JSCompiler_inline_arr2 && f.call(opt_obj, JSCompiler_inline_arr2[JSCompiler_inline_i], JSCompiler_inline_i, arr)) {
+ i = JSCompiler_inline_i;
+ break JSCompiler_inline_label_goog$array$findIndex_12
+ }i = -1
+ }return i < 0 ? null : goog$isString(arr) ? arr.charAt(i) : arr[i]
+};var goog$string$trim = function(str) {
+ return str.replace(/^[\s\xa0]+|[\s\xa0]+$/g, "")
+}, goog$string$htmlEscape = function(str, opt_isLikelyToContainHtmlChars) {
+ if(opt_isLikelyToContainHtmlChars)return str.replace(goog$string$amperRe_, "&amp;").replace(goog$string$ltRe_, "&lt;").replace(goog$string$gtRe_, "&gt;").replace(goog$string$quotRe_, "&quot;");
+ else {
+ if(!goog$string$allRe_.test(str))return str;
+ if(str.indexOf("&") != -1)str = str.replace(goog$string$amperRe_, "&amp;");
+ if(str.indexOf("<") != -1)str = str.replace(goog$string$ltRe_, "&lt;");
+ if(str.indexOf(">") != -1)str = str.replace(goog$string$gtRe_, "&gt;");
+ if(str.indexOf('"') != -1)str = str.replace(goog$string$quotRe_, "&quot;");
+ return str
+ }
+}, goog$string$amperRe_ = /&/g, goog$string$ltRe_ = /</g, goog$string$gtRe_ = />/g, goog$string$quotRe_ = /\"/g, goog$string$allRe_ = /[&<>\"]/, goog$string$contains = function(s, ss) {
+ return s.indexOf(ss) != -1
+}, goog$string$compareVersions = function(version1, version2) {
+ for(var order = 0, v1Subs = goog$string$trim(String(version1)).split("."), v2Subs = goog$string$trim(String(version2)).split("."), subCount = Math.max(v1Subs.length, v2Subs.length), subIdx = 0;order == 0 && subIdx < subCount;subIdx++) {
+ var v1Sub = v1Subs[subIdx] || "", v2Sub = v2Subs[subIdx] || "", v1CompParser = new RegExp("(\\d*)(\\D*)", "g"), v2CompParser = new RegExp("(\\d*)(\\D*)", "g");
+ do {
+ var v1Comp = v1CompParser.exec(v1Sub) || ["", "", ""], v2Comp = v2CompParser.exec(v2Sub) || ["", "", ""];
+ if(v1Comp[0].length == 0 && v2Comp[0].length == 0)break;
+ var v1CompNum = v1Comp[1].length == 0 ? 0 : parseInt(v1Comp[1], 10), v2CompNum = v2Comp[1].length == 0 ? 0 : parseInt(v2Comp[1], 10);
+ order = goog$string$compareElements_(v1CompNum, v2CompNum) || goog$string$compareElements_(v1Comp[2].length == 0, v2Comp[2].length == 0) || goog$string$compareElements_(v1Comp[2], v2Comp[2])
+ }while(order == 0)
+ }return order
+}, goog$string$compareElements_ = function(left, right) {
+ if(left < right)return-1;
+ else if(left > right)return 1;
+ return 0
+};
+goog$now();var goog$userAgent$detectedOpera_, goog$userAgent$detectedIe_, goog$userAgent$detectedWebkit_, goog$userAgent$detectedMobile_, goog$userAgent$detectedGecko_, goog$userAgent$detectedCamino_, goog$userAgent$detectedMac_, goog$userAgent$detectedWindows_, goog$userAgent$detectedLinux_, goog$userAgent$detectedX11_, goog$userAgent$getUserAgentString = function() {
+ return goog$global.navigator ? goog$global.navigator.userAgent : null
+}, goog$userAgent$getNavigator = function() {
+ return goog$global.navigator
+};
+goog$userAgent$detectedCamino_ = goog$userAgent$detectedGecko_ = goog$userAgent$detectedMobile_ = goog$userAgent$detectedWebkit_ = goog$userAgent$detectedIe_ = goog$userAgent$detectedOpera_ = false;
+var JSCompiler_inline_ua_15;
+if(JSCompiler_inline_ua_15 = goog$userAgent$getUserAgentString()) {
+ var JSCompiler_inline_navigator$$1_16 = goog$userAgent$getNavigator();
+ goog$userAgent$detectedOpera_ = JSCompiler_inline_ua_15.indexOf("Opera") == 0;
+ goog$userAgent$detectedIe_ = !goog$userAgent$detectedOpera_ && JSCompiler_inline_ua_15.indexOf("MSIE") != -1;
+ goog$userAgent$detectedMobile_ = (goog$userAgent$detectedWebkit_ = !goog$userAgent$detectedOpera_ && JSCompiler_inline_ua_15.indexOf("WebKit") != -1) && JSCompiler_inline_ua_15.indexOf("Mobile") != -1;
+ goog$userAgent$detectedCamino_ = (goog$userAgent$detectedGecko_ = !goog$userAgent$detectedOpera_ && !goog$userAgent$detectedWebkit_ && JSCompiler_inline_navigator$$1_16.product == "Gecko") && JSCompiler_inline_navigator$$1_16.vendor == "Camino"
+}var goog$userAgent$OPERA = goog$userAgent$detectedOpera_, goog$userAgent$IE = goog$userAgent$detectedIe_, goog$userAgent$GECKO = goog$userAgent$detectedGecko_, goog$userAgent$WEBKIT = goog$userAgent$detectedWebkit_, goog$userAgent$MOBILE = goog$userAgent$detectedMobile_, goog$userAgent$PLATFORM, JSCompiler_inline_navigator$$2_19 = goog$userAgent$getNavigator();
+goog$userAgent$PLATFORM = JSCompiler_inline_navigator$$2_19 && JSCompiler_inline_navigator$$2_19.platform || "";
+goog$userAgent$detectedMac_ = goog$string$contains(goog$userAgent$PLATFORM, "Mac");
+goog$userAgent$detectedWindows_ = goog$string$contains(goog$userAgent$PLATFORM, "Win");
+goog$userAgent$detectedLinux_ = goog$string$contains(goog$userAgent$PLATFORM, "Linux");
+goog$userAgent$detectedX11_ = !!goog$userAgent$getNavigator() && goog$string$contains(goog$userAgent$getNavigator().appVersion || "", "X11");
+var goog$userAgent$VERSION, JSCompiler_inline_version$$6_26 = "", JSCompiler_inline_re$$2_27;
+if(goog$userAgent$OPERA && goog$global.opera) {
+ var JSCompiler_inline_operaVersion_28 = goog$global.opera.version;
+ JSCompiler_inline_version$$6_26 = typeof JSCompiler_inline_operaVersion_28 == "function" ? JSCompiler_inline_operaVersion_28() : JSCompiler_inline_operaVersion_28
+}else {
+ if(goog$userAgent$GECKO)JSCompiler_inline_re$$2_27 = /rv\:([^\);]+)(\)|;)/;
+ else if(goog$userAgent$IE)JSCompiler_inline_re$$2_27 = /MSIE\s+([^\);]+)(\)|;)/;
+ else if(goog$userAgent$WEBKIT)JSCompiler_inline_re$$2_27 = /WebKit\/(\S+)/;
+ if(JSCompiler_inline_re$$2_27) {
+ var JSCompiler_inline_arr$$41_29 = JSCompiler_inline_re$$2_27.exec(goog$userAgent$getUserAgentString());
+ JSCompiler_inline_version$$6_26 = JSCompiler_inline_arr$$41_29 ? JSCompiler_inline_arr$$41_29[1] : ""
+ }
+}goog$userAgent$VERSION = JSCompiler_inline_version$$6_26;
+var goog$userAgent$isVersionCache_ = {}, goog$userAgent$isVersion = function(version) {
+ return goog$userAgent$isVersionCache_[version] || (goog$userAgent$isVersionCache_[version] = goog$string$compareVersions(goog$userAgent$VERSION, version) >= 0)
+};var goog$dom$getWindow = function(opt_doc) {
+ return opt_doc ? goog$dom$getWindow_(opt_doc) : window
+}, goog$dom$getWindow_ = function(doc) {
+ if(doc.parentWindow)return doc.parentWindow;
+ if(goog$userAgent$WEBKIT && !goog$userAgent$isVersion("500") && !goog$userAgent$MOBILE) {
+ var scriptElement = doc.createElement("script");
+ scriptElement.innerHTML = "document.parentWindow=window";
+ var parentElement = doc.documentElement;
+ parentElement.appendChild(scriptElement);
+ parentElement.removeChild(scriptElement);
+ return doc.parentWindow
+ }return doc.defaultView
+}, goog$dom$appendChild = function(parent, child) {
+ parent.appendChild(child)
+}, goog$dom$BAD_CONTAINS_WEBKIT_ = goog$userAgent$WEBKIT && goog$userAgent$isVersion("522"), goog$dom$contains = function(parent, descendant) {
+ if(typeof parent.contains != "undefined" && !goog$dom$BAD_CONTAINS_WEBKIT_ && descendant.nodeType == 1)return parent == descendant || parent.contains(descendant);
+ if(typeof parent.compareDocumentPosition != "undefined")return parent == descendant || Boolean(parent.compareDocumentPosition(descendant) & 16);
+ for(;descendant && parent != descendant;)descendant = descendant.parentNode;
+ return descendant == parent
+}, goog$dom$compareNodeOrder = function(node1, node2) {
+ if(node1 == node2)return 0;
+ if(node1.compareDocumentPosition)return node1.compareDocumentPosition(node2) & 2 ? 1 : -1;
+ if("sourceIndex" in node1 || node1.parentNode && "sourceIndex" in node1.parentNode) {
+ var isElement1 = node1.nodeType == 1, isElement2 = node2.nodeType == 1;
+ if(isElement1 && isElement2)return node1.sourceIndex - node2.sourceIndex;
+ else {
+ var parent1 = node1.parentNode, parent2 = node2.parentNode;
+ if(parent1 == parent2)return goog$dom$compareSiblingOrder_(node1, node2);
+ if(!isElement1 && goog$dom$contains(parent1, node2))return-1 * goog$dom$compareParentsDescendantNodeIe_(node1, node2);
+ if(!isElement2 && goog$dom$contains(parent2, node1))return goog$dom$compareParentsDescendantNodeIe_(node2, node1);
+ return(isElement1 ? node1.sourceIndex : parent1.sourceIndex) - (isElement2 ? node2.sourceIndex : parent2.sourceIndex)
+ }
+ }var doc = goog$dom$getOwnerDocument(node1), range1, range2;
+ range1 = doc.createRange();
+ range1.selectNode(node1);
+ range1.collapse(true);
+ range2 = doc.createRange();
+ range2.selectNode(node2);
+ range2.collapse(true);
+ return range1.compareBoundaryPoints(goog$global.Range.START_TO_END, range2)
+}, goog$dom$compareParentsDescendantNodeIe_ = function(textNode, node) {
+ var parent = textNode.parentNode;
+ if(parent == node)return-1;
+ for(var sibling = node;sibling.parentNode != parent;)sibling = sibling.parentNode;
+ return goog$dom$compareSiblingOrder_(sibling, textNode)
+}, goog$dom$compareSiblingOrder_ = function(node1, node2) {
+ for(var s = node2;s = s.previousSibling;)if(s == node1)return-1;
+ return 1
+}, goog$dom$findCommonAncestor = function() {
+ var i, count = arguments.length;
+ if(count) {
+ if(count == 1)return arguments[0]
+ }else return null;
+ var paths = [], minLength = Infinity;
+ for(i = 0;i < count;i++) {
+ for(var ancestors = [], node = arguments[i];node;) {
+ ancestors.unshift(node);
+ node = node.parentNode
+ }paths.push(ancestors);
+ minLength = Math.min(minLength, ancestors.length)
+ }var output = null;
+ for(i = 0;i < minLength;i++) {
+ for(var first = paths[0][i], j = 1;j < count;j++)if(first != paths[j][i])return output;
+ output = first
+ }return output
+}, goog$dom$getOwnerDocument = function(node) {
+ // Added 'editorDoc' as hack for browsers that don't support node.ownerDocument
+ return node.nodeType == 9 ? node : node.ownerDocument || node.document || editorDoc
+}, goog$dom$DomHelper = function(opt_document) {
+ this.document_ = opt_document || goog$global.document || document
+};
+goog$dom$DomHelper.prototype.getDocument = function() {
+ return this.document_
+};
+goog$dom$DomHelper.prototype.createElement = function(name) {
+ return this.document_.createElement(name)
+};
+goog$dom$DomHelper.prototype.getWindow = function() {
+ return goog$dom$getWindow_(this.document_)
+};
+goog$dom$DomHelper.prototype.appendChild = goog$dom$appendChild;
+goog$dom$DomHelper.prototype.contains = goog$dom$contains;var goog$Disposable = function() {
+};if("StopIteration" in goog$global)var goog$iter$StopIteration = goog$global.StopIteration;
+else goog$iter$StopIteration = Error("StopIteration");
+var goog$iter$Iterator = function() {
+};
+goog$iter$Iterator.prototype.next = function() {
+ throw goog$iter$StopIteration;
+};
+goog$iter$Iterator.prototype.__iterator__ = function() {
+ return this
+};var goog$debug$exposeException = function(err, opt_fn) {
+ try {
+ var e, JSCompiler_inline_href_34;
+ JSCompiler_inline_label_goog$getObjectByName_61: {
+ for(var JSCompiler_inline_parts = "window.location.href".split("."), JSCompiler_inline_cur = goog$global, JSCompiler_inline_part;JSCompiler_inline_part = JSCompiler_inline_parts.shift();)if(JSCompiler_inline_cur[JSCompiler_inline_part])JSCompiler_inline_cur = JSCompiler_inline_cur[JSCompiler_inline_part];
+ else {
+ JSCompiler_inline_href_34 = null;
+ break JSCompiler_inline_label_goog$getObjectByName_61
+ }JSCompiler_inline_href_34 = JSCompiler_inline_cur
+ }e = typeof err == "string" ? {message:err, name:"Unknown error", lineNumber:"Not available", fileName:JSCompiler_inline_href_34, stack:"Not available"} : !err.lineNumber || !err.fileName || !err.stack ? {message:err.message, name:err.name, lineNumber:err.lineNumber || err.line || "Not available", fileName:err.fileName || err.filename || err.sourceURL || JSCompiler_inline_href_34, stack:err.stack || "Not available"} : err;
+ var error = "Message: " + goog$string$htmlEscape(e.message) + '\nUrl: <a href="view-source:' + e.fileName + '" target="_new">' + e.fileName + "</a>\nLine: " + e.lineNumber + "\n\nBrowser stack:\n" + goog$string$htmlEscape(e.stack + "-> ") + "[end]\n\nJS stack traversal:\n" + goog$string$htmlEscape(goog$debug$getStacktrace(opt_fn) + "-> ");
+ return error
+ }catch(e2) {
+ return"Exception trying to expose exception! You win, we lose. " + e2
+ }
+}, goog$debug$getStacktrace = function(opt_fn) {
+ return goog$debug$getStacktraceHelper_(opt_fn || arguments.callee.caller, [])
+}, goog$debug$getStacktraceHelper_ = function(fn, visited) {
+ var sb = [], JSCompiler_inline_result_36;
+ JSCompiler_inline_label_goog$array$contains_41:JSCompiler_inline_result_36 = visited.contains ? visited.contains(fn) : goog$array$indexOf(visited, fn) > -1;
+ if(JSCompiler_inline_result_36)sb.push("[...circular reference...]");
+ else if(fn && visited.length < 50) {
+ sb.push(goog$debug$getFunctionName(fn) + "(");
+ for(var args = fn.arguments, i = 0;i < args.length;i++) {
+ i > 0 && sb.push(", ");
+ var argDesc, arg = args[i];
+ switch(typeof arg) {
+ case "object":
+ argDesc = arg ? "object" : "null";
+ break;
+ case "string":
+ argDesc = arg;
+ break;
+ case "number":
+ argDesc = String(arg);
+ break;
+ case "boolean":
+ argDesc = arg ? "true" : "false";
+ break;
+ case "function":
+ argDesc = (argDesc = goog$debug$getFunctionName(arg)) ? argDesc : "[fn]";
+ break;
+ case "undefined":
+ ;
+ default:
+ argDesc = typeof arg;
+ break
+ }
+ if(argDesc.length > 40)argDesc = argDesc.substr(0, 40) + "...";
+ sb.push(argDesc)
+ }visited.push(fn);
+ sb.push(")\n");
+ try {
+ sb.push(goog$debug$getStacktraceHelper_(fn.caller, visited))
+ }catch(e) {
+ sb.push("[exception trying to get caller]\n")
+ }
+ }else fn ? sb.push("[...long stack...]") : sb.push("[end]");
+ return sb.join("")
+}, goog$debug$getFunctionName = function(fn) {
+ var functionSource = String(fn);
+ if(!goog$debug$fnNameCache_[functionSource]) {
+ var matches = /function ([^\(]+)/.exec(functionSource);
+ if(matches) {
+ var method = matches[1];
+ goog$debug$fnNameCache_[functionSource] = method
+ }else goog$debug$fnNameCache_[functionSource] = "[Anonymous]"
+ }return goog$debug$fnNameCache_[functionSource]
+}, goog$debug$fnNameCache_ = {};var goog$debug$LogRecord = function(level, msg, loggerName, opt_time, opt_sequenceNumber) {
+ this.sequenceNumber_ = typeof opt_sequenceNumber == "number" ? opt_sequenceNumber : goog$debug$LogRecord$nextSequenceNumber_++;
+ this.time_ = opt_time || goog$now();
+ this.level_ = level;
+ this.msg_ = msg;
+ this.loggerName_ = loggerName
+};
+goog$debug$LogRecord.prototype.exception_ = null;
+goog$debug$LogRecord.prototype.exceptionText_ = null;
+var goog$debug$LogRecord$nextSequenceNumber_ = 0;
+goog$debug$LogRecord.prototype.setException = function(exception) {
+ this.exception_ = exception
+};
+goog$debug$LogRecord.prototype.setExceptionText = function(text) {
+ this.exceptionText_ = text
+};
+goog$debug$LogRecord.prototype.setLevel = function(level) {
+ this.level_ = level
+};var goog$debug$Logger = function(name) {
+ this.name_ = name;
+ this.parent_ = null;
+ this.children_ = {};
+ this.handlers_ = []
+};
+goog$debug$Logger.prototype.level_ = null;
+var goog$debug$Logger$Level = function(name, value) {
+ this.name = name;
+ this.value = value
+};
+goog$debug$Logger$Level.prototype.toString = function() {
+ return this.name
+};
+new goog$debug$Logger$Level("OFF", Infinity);
+new goog$debug$Logger$Level("SHOUT", 1200);
+var goog$debug$Logger$Level$SEVERE = new goog$debug$Logger$Level("SEVERE", 1000), goog$debug$Logger$Level$WARNING = new goog$debug$Logger$Level("WARNING", 900);
+new goog$debug$Logger$Level("INFO", 800);
+var goog$debug$Logger$Level$CONFIG = new goog$debug$Logger$Level("CONFIG", 700);
+new goog$debug$Logger$Level("FINE", 500);
+new goog$debug$Logger$Level("FINER", 400);
+new goog$debug$Logger$Level("FINEST", 300);
+new goog$debug$Logger$Level("ALL", 0);
+goog$debug$Logger.prototype.setLevel = function(level) {
+ this.level_ = level
+};
+goog$debug$Logger.prototype.isLoggable = function(level) {
+ if(this.level_)return level.value >= this.level_.value;
+ if(this.parent_)return this.parent_.isLoggable(level);
+ return false
+};
+goog$debug$Logger.prototype.log = function(level, msg, opt_exception) {
+ this.isLoggable(level) && this.logRecord(this.getLogRecord(level, msg, opt_exception))
+};
+goog$debug$Logger.prototype.getLogRecord = function(level, msg, opt_exception) {
+ var logRecord = new goog$debug$LogRecord(level, String(msg), this.name_);
+ if(opt_exception) {
+ logRecord.setException(opt_exception);
+ logRecord.setExceptionText(goog$debug$exposeException(opt_exception, arguments.callee.caller))
+ }return logRecord
+};
+goog$debug$Logger.prototype.severe = function(msg, opt_exception) {
+ this.log(goog$debug$Logger$Level$SEVERE, msg, opt_exception)
+};
+goog$debug$Logger.prototype.warning = function(msg, opt_exception) {
+ this.log(goog$debug$Logger$Level$WARNING, msg, opt_exception)
+};
+goog$debug$Logger.prototype.logRecord = function(logRecord) {
+ if(this.isLoggable(logRecord.level_))for(var target = this;target;) {
+ target.callPublish_(logRecord);
+ target = target.parent_
+ }
+};
+goog$debug$Logger.prototype.callPublish_ = function(logRecord) {
+ for(var i = 0;i < this.handlers_.length;i++)this.handlers_[i](logRecord)
+};
+goog$debug$Logger.prototype.setParent_ = function(parent) {
+ this.parent_ = parent
+};
+goog$debug$Logger.prototype.addChild_ = function(name, logger) {
+ this.children_[name] = logger
+};
+var goog$debug$LogManager$loggers_ = {}, goog$debug$LogManager$rootLogger_ = null, goog$debug$LogManager$getLogger = function(name) {
+ if(!goog$debug$LogManager$rootLogger_) {
+ goog$debug$LogManager$rootLogger_ = new goog$debug$Logger("");
+ goog$debug$LogManager$loggers_[""] = goog$debug$LogManager$rootLogger_;
+ goog$debug$LogManager$rootLogger_.setLevel(goog$debug$Logger$Level$CONFIG)
+ }return name in goog$debug$LogManager$loggers_ ? goog$debug$LogManager$loggers_[name] : goog$debug$LogManager$createLogger_(name)
+}, goog$debug$LogManager$createLogger_ = function(name) {
+ var logger = new goog$debug$Logger(name), parts = name.split("."), leafName = parts[parts.length - 1];
+ parts.length = parts.length - 1;
+ var parentName = parts.join("."), parentLogger = goog$debug$LogManager$getLogger(parentName);
+ parentLogger.addChild_(leafName, logger);
+ logger.setParent_(parentLogger);
+ return goog$debug$LogManager$loggers_[name] = logger
+};var goog$dom$SavedRange = function() {
+ goog$Disposable.call(this)
+};
+goog$inherits(goog$dom$SavedRange, goog$Disposable);
+goog$debug$LogManager$getLogger("goog.dom.SavedRange");var goog$dom$TagIterator = function(opt_node, opt_reversed, opt_unconstrained, opt_tagType, opt_depth) {
+ this.reversed = !!opt_reversed;
+ opt_node && this.setPosition(opt_node, opt_tagType);
+ this.depth = opt_depth != undefined ? opt_depth : this.tagType || 0;
+ if(this.reversed)this.depth *= -1;
+ this.constrained = !opt_unconstrained
+};
+goog$inherits(goog$dom$TagIterator, goog$iter$Iterator);
+goog$dom$TagIterator.prototype.node = null;
+goog$dom$TagIterator.prototype.tagType = null;
+goog$dom$TagIterator.prototype.started_ = false;
+goog$dom$TagIterator.prototype.setPosition = function(node, opt_tagType, opt_depth) {
+ if(this.node = node)this.tagType = typeof opt_tagType == "number" ? opt_tagType : this.node.nodeType != 1 ? 0 : this.reversed ? -1 : 1;
+ if(typeof opt_depth == "number")this.depth = opt_depth
+};
+goog$dom$TagIterator.prototype.next = function() {
+ var node;
+ if(this.started_) {
+ if(!this.node || this.constrained && this.depth == 0)throw goog$iter$StopIteration;node = this.node;
+ var startType = this.reversed ? -1 : 1;
+ if(this.tagType == startType) {
+ var child = this.reversed ? node.lastChild : node.firstChild;
+ child ? this.setPosition(child) : this.setPosition(node, startType * -1)
+ }else {
+ var sibling = this.reversed ? node.previousSibling : node.nextSibling;
+ sibling ? this.setPosition(sibling) : this.setPosition(node.parentNode, startType * -1)
+ }this.depth += this.tagType * (this.reversed ? -1 : 1)
+ }else this.started_ = true;
+ node = this.node;
+ if(!this.node)throw goog$iter$StopIteration;return node
+};
+goog$dom$TagIterator.prototype.isStartTag = function() {
+ return this.tagType == 1
+};var goog$dom$AbstractRange = function() {
+};
+goog$dom$AbstractRange.prototype.getTextRanges = function() {
+ for(var output = [], i = 0, len = this.getTextRangeCount();i < len;i++)output.push(this.getTextRange(i));
+ return output
+};
+goog$dom$AbstractRange.prototype.getAnchorNode = function() {
+ return this.isReversed() ? this.getEndNode() : this.getStartNode()
+};
+goog$dom$AbstractRange.prototype.getAnchorOffset = function() {
+ return this.isReversed() ? this.getEndOffset() : this.getStartOffset()
+};
+goog$dom$AbstractRange.prototype.getFocusNode = function() {
+ return this.isReversed() ? this.getStartNode() : this.getEndNode()
+};
+goog$dom$AbstractRange.prototype.getFocusOffset = function() {
+ return this.isReversed() ? this.getStartOffset() : this.getEndOffset()
+};
+goog$dom$AbstractRange.prototype.isReversed = function() {
+ return false
+};
+goog$dom$AbstractRange.prototype.getDocument = function() {
+ return goog$dom$getOwnerDocument(goog$userAgent$IE ? this.getContainer() : this.getStartNode())
+};
+goog$dom$AbstractRange.prototype.getWindow = function() {
+ return goog$dom$getWindow(this.getDocument())
+};
+goog$dom$AbstractRange.prototype.containsNode = function(node, opt_allowPartial) {
+ return this.containsRange(goog$dom$TextRange$createFromNodeContents(node, undefined), opt_allowPartial)
+};
+var goog$dom$RangeIterator = function(node, opt_reverse) {
+ goog$dom$TagIterator.call(this, node, opt_reverse, true)
+};
+goog$inherits(goog$dom$RangeIterator, goog$dom$TagIterator);var goog$dom$AbstractMultiRange = function() {
+};
+goog$inherits(goog$dom$AbstractMultiRange, goog$dom$AbstractRange);
+goog$dom$AbstractMultiRange.prototype.containsRange = function(otherRange, opt_allowPartial) {
+ var ranges = this.getTextRanges(), otherRanges = otherRange.getTextRanges(), fn = opt_allowPartial ? goog$array$some : goog$array$every;
+ return fn(otherRanges, function(otherRange) {
+ return goog$array$some(ranges, function(range) {
+ return range.containsRange(otherRange, opt_allowPartial)
+ })
+ })
+};var goog$dom$TextRangeIterator = function(startNode, startOffset, endNode, endOffset, opt_reverse) {
+ var goNext;
+ if(startNode) {
+ this.startNode_ = startNode;
+ this.startOffset_ = startOffset;
+ this.endNode_ = endNode;
+ this.endOffset_ = endOffset;
+ if(startNode.nodeType == 1 && startNode.tagName != "BR") {
+ var startChildren = startNode.childNodes, candidate = startChildren[startOffset];
+ if(candidate) {
+ this.startNode_ = candidate;
+ this.startOffset_ = 0
+ }else {
+ if(startChildren.length)this.startNode_ = goog$array$peek(startChildren);
+ goNext = true
+ }
+ }if(endNode.nodeType == 1)if(this.endNode_ = endNode.childNodes[endOffset])this.endOffset_ = 0;
+ else this.endNode_ = endNode
+ }goog$dom$RangeIterator.call(this, opt_reverse ? this.endNode_ : this.startNode_, opt_reverse);
+ if(goNext)try {
+ this.next()
+ }catch(e) {
+ if(e != goog$iter$StopIteration)throw e;
+ }
+};
+goog$inherits(goog$dom$TextRangeIterator, goog$dom$RangeIterator);
+goog$dom$TextRangeIterator.prototype.startNode_ = null;
+goog$dom$TextRangeIterator.prototype.endNode_ = null;
+goog$dom$TextRangeIterator.prototype.startOffset_ = 0;
+goog$dom$TextRangeIterator.prototype.endOffset_ = 0;
+goog$dom$TextRangeIterator.prototype.getStartNode = function() {
+ return this.startNode_
+};
+goog$dom$TextRangeIterator.prototype.getEndNode = function() {
+ return this.endNode_
+};
+goog$dom$TextRangeIterator.prototype.isLast = function() {
+ return this.started_ && this.node == this.endNode_ && (!this.endOffset_ || !this.isStartTag())
+};
+goog$dom$TextRangeIterator.prototype.next = function() {
+ if(this.isLast())throw goog$iter$StopIteration;return goog$dom$TextRangeIterator.superClass_.next.call(this)
+};var goog$userAgent$jscript$DETECTED_HAS_JSCRIPT_, goog$userAgent$jscript$DETECTED_VERSION_, JSCompiler_inline_hasScriptEngine_44 = "ScriptEngine" in goog$global;
+goog$userAgent$jscript$DETECTED_VERSION_ = (goog$userAgent$jscript$DETECTED_HAS_JSCRIPT_ = JSCompiler_inline_hasScriptEngine_44 && goog$global.ScriptEngine() == "JScript") ? goog$global.ScriptEngineMajorVersion() + "." + goog$global.ScriptEngineMinorVersion() + "." + goog$global.ScriptEngineBuildVersion() : "0";var goog$dom$browserrange$AbstractRange = function() {
+};
+goog$dom$browserrange$AbstractRange.prototype.containsRange = function(range, opt_allowPartial) {
+ return this.containsBrowserRange(range.range_, opt_allowPartial)
+};
+goog$dom$browserrange$AbstractRange.prototype.containsBrowserRange = function(range, opt_allowPartial) {
+ try {
+ return opt_allowPartial ? this.compareBrowserRangeEndpoints(range, 0, 1) >= 0 && this.compareBrowserRangeEndpoints(range, 1, 0) <= 0 : this.compareBrowserRangeEndpoints(range, 0, 0) >= 0 && this.compareBrowserRangeEndpoints(range, 1, 1) <= 0
+ }catch(e) {
+ if(!goog$userAgent$IE)throw e;return false
+ }
+};
+goog$dom$browserrange$AbstractRange.prototype.containsNode = function(node, opt_allowPartial) {
+ return this.containsRange(goog$userAgent$IE ? goog$dom$browserrange$IeRange$createFromNodeContents(node) : goog$userAgent$WEBKIT ? new goog$dom$browserrange$WebKitRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)) : goog$userAgent$GECKO ? new goog$dom$browserrange$GeckoRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)) : new goog$dom$browserrange$W3cRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)), opt_allowPartial)
+};
+goog$dom$browserrange$AbstractRange.prototype.__iterator__ = function() {
+ return new goog$dom$TextRangeIterator(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset())
+};var goog$dom$browserrange$W3cRange = function(range) {
+ this.range_ = range
+};
+goog$inherits(goog$dom$browserrange$W3cRange, goog$dom$browserrange$AbstractRange);
+var goog$dom$browserrange$W3cRange$getBrowserRangeForNode = function(node) {
+ var nodeRange = goog$dom$getOwnerDocument(node).createRange();
+ if(node.nodeType == 3) {
+ nodeRange.setStart(node, 0);
+ nodeRange.setEnd(node, node.length)
+ }else {
+ for(var tempNode, leaf = node;tempNode = leaf.firstChild;)leaf = tempNode;
+ nodeRange.setStart(leaf, 0);
+ for(leaf = node;tempNode = leaf.lastChild;)leaf = tempNode;
+ nodeRange.setEnd(leaf, leaf.nodeType == 1 ? leaf.childNodes.length : leaf.length)
+ }return nodeRange
+}, goog$dom$browserrange$W3cRange$getBrowserRangeForNodes_ = function(startNode, startOffset, endNode, endOffset) {
+ var nodeRange = goog$dom$getOwnerDocument(startNode).createRange();
+ nodeRange.setStart(startNode, startOffset);
+ nodeRange.setEnd(endNode, endOffset);
+ return nodeRange
+};
+goog$dom$browserrange$W3cRange.prototype.getContainer = function() {
+ return this.range_.commonAncestorContainer
+};
+goog$dom$browserrange$W3cRange.prototype.getStartNode = function() {
+ return this.range_.startContainer
+};
+goog$dom$browserrange$W3cRange.prototype.getStartOffset = function() {
+ return this.range_.startOffset
+};
+goog$dom$browserrange$W3cRange.prototype.getEndNode = function() {
+ return this.range_.endContainer
+};
+goog$dom$browserrange$W3cRange.prototype.getEndOffset = function() {
+ return this.range_.endOffset
+};
+goog$dom$browserrange$W3cRange.prototype.compareBrowserRangeEndpoints = function(range, thisEndpoint, otherEndpoint) {
+ return this.range_.compareBoundaryPoints(otherEndpoint == 1 ? thisEndpoint == 1 ? goog$global.Range.START_TO_START : goog$global.Range.START_TO_END : thisEndpoint == 1 ? goog$global.Range.END_TO_START : goog$global.Range.END_TO_END, range)
+};
+goog$dom$browserrange$W3cRange.prototype.isCollapsed = function() {
+ return this.range_.collapsed
+};
+goog$dom$browserrange$W3cRange.prototype.select = function(reverse) {
+ var win = goog$dom$getWindow(goog$dom$getOwnerDocument(this.getStartNode()));
+ this.selectInternal(win.getSelection(), reverse)
+};
+goog$dom$browserrange$W3cRange.prototype.selectInternal = function(selection) {
+ selection.addRange(this.range_)
+};
+goog$dom$browserrange$W3cRange.prototype.collapse = function(toStart) {
+ this.range_.collapse(toStart)
+};var goog$dom$browserrange$GeckoRange = function(range) {
+ goog$dom$browserrange$W3cRange.call(this, range)
+};
+goog$inherits(goog$dom$browserrange$GeckoRange, goog$dom$browserrange$W3cRange);
+goog$dom$browserrange$GeckoRange.prototype.selectInternal = function(selection, reversed) {
+ var anchorNode = reversed ? this.getEndNode() : this.getStartNode(), anchorOffset = reversed ? this.getEndOffset() : this.getStartOffset(), focusNode = reversed ? this.getStartNode() : this.getEndNode(), focusOffset = reversed ? this.getStartOffset() : this.getEndOffset();
+ selection.collapse(anchorNode, anchorOffset);
+ if(anchorNode != focusNode || anchorOffset != focusOffset)selection.extend(focusNode, focusOffset)
+};var goog$dom$browserrange$IeRange = function(range, doc) {
+ this.range_ = range;
+ this.doc_ = doc
+};
+goog$inherits(goog$dom$browserrange$IeRange, goog$dom$browserrange$AbstractRange);
+var goog$dom$browserrange$IeRange$logger_ = goog$debug$LogManager$getLogger("goog.dom.browserrange.IeRange"), goog$dom$browserrange$IeRange$getBrowserRangeForNode_ = function(node) {
+ var nodeRange = goog$dom$getOwnerDocument(node).body.createTextRange();
+ if(node.nodeType == 1)nodeRange.moveToElementText(node);
+ else {
+ for(var offset = 0, sibling = node;sibling = sibling.previousSibling;) {
+ var nodeType = sibling.nodeType;
+ if(nodeType == 3)offset += sibling.length;
+ else if(nodeType == 1) {
+ nodeRange.moveToElementText(sibling);
+ break
+ }
+ }sibling || nodeRange.moveToElementText(node.parentNode);
+ nodeRange.collapse(!sibling);
+ offset && nodeRange.move("character", offset);
+ nodeRange.moveEnd("character", node.length)
+ }return nodeRange
+}, goog$dom$browserrange$IeRange$getBrowserRangeForNodes_ = function(startNode, startOffset, endNode, endOffset) {
+ var child, collapse = false;
+ if(startNode.nodeType == 1) {
+ startOffset > startNode.childNodes.length && goog$dom$browserrange$IeRange$logger_.severe("Cannot have startOffset > startNode child count");
+ child = startNode.childNodes[startOffset];
+ collapse = !child;
+ startNode = child || startNode;
+ startOffset = 0
+ }var leftRange = goog$dom$browserrange$IeRange$getBrowserRangeForNode_(startNode);
+ startOffset && leftRange.move("character", startOffset);
+ collapse && leftRange.collapse(false);
+ collapse = false;
+ if(endNode.nodeType == 1) {
+ startOffset > startNode.childNodes.length && goog$dom$browserrange$IeRange$logger_.severe("Cannot have endOffset > endNode child count");
+ endNode = (child = endNode.childNodes[endOffset]) || endNode;
+ if(endNode.tagName == "BR")endOffset = 1;
+ else {
+ endOffset = 0;
+ collapse = !child
+ }
+ }var rightRange = goog$dom$browserrange$IeRange$getBrowserRangeForNode_(endNode);
+ rightRange.collapse(!collapse);
+ endOffset && rightRange.moveEnd("character", endOffset);
+ leftRange.setEndPoint("EndToEnd", rightRange);
+ return leftRange
+}, goog$dom$browserrange$IeRange$createFromNodeContents = function(node) {
+ var range = new goog$dom$browserrange$IeRange(goog$dom$browserrange$IeRange$getBrowserRangeForNode_(node), goog$dom$getOwnerDocument(node));
+ range.parentNode_ = node;
+ return range
+};
+goog$dom$browserrange$IeRange.prototype.parentNode_ = null;
+goog$dom$browserrange$IeRange.prototype.startNode_ = null;
+goog$dom$browserrange$IeRange.prototype.endNode_ = null;
+goog$dom$browserrange$IeRange.prototype.clearCachedValues_ = function() {
+ this.parentNode_ = this.startNode_ = this.endNode_ = null
+};
+goog$dom$browserrange$IeRange.prototype.getContainer = function() {
+ if(!this.parentNode_) {
+ for(var selectText = this.range_.text, i = 1;selectText.charAt(selectText.length - i) == " ";i++)this.range_.moveEnd("character", -1);
+ for(var parent = this.range_.parentElement(), htmlText = this.range_.htmlText.replace(/(\r\n|\r|\n)+/g, " ");htmlText.length > parent.outerHTML.replace(/(\r\n|\r|\n)+/g, " ").length;)parent = parent.parentNode;
+ for(;parent.childNodes.length == 1 && parent.innerText == (parent.firstChild.nodeType == 3 ? parent.firstChild.nodeValue : parent.firstChild.innerText);) {
+ if(parent.firstChild.tagName == "IMG")break;
+ parent = parent.firstChild
+ }if(selectText.length == 0)parent = this.findDeepestContainer_(parent);
+ this.parentNode_ = parent
+ }return this.parentNode_
+};
+goog$dom$browserrange$IeRange.prototype.findDeepestContainer_ = function(node) {
+ for(var childNodes = node.childNodes, i = 0, len = childNodes.length;i < len;i++) {
+ var child = childNodes[i];
+ if(child.nodeType == 1)if(this.range_.inRange(goog$dom$browserrange$IeRange$getBrowserRangeForNode_(child)))return this.findDeepestContainer_(child)
+ }return node
+};
+goog$dom$browserrange$IeRange.prototype.getStartNode = function() {
+ return this.startNode_ || (this.startNode_ = this.getEndpointNode_(1))
+};
+goog$dom$browserrange$IeRange.prototype.getStartOffset = function() {
+ return this.getOffset_(1)
+};
+goog$dom$browserrange$IeRange.prototype.getEndNode = function() {
+ return this.endNode_ || (this.endNode_ = this.getEndpointNode_(0))
+};
+goog$dom$browserrange$IeRange.prototype.getEndOffset = function() {
+ return this.getOffset_(0)
+};
+goog$dom$browserrange$IeRange.prototype.containsRange = function(range, opt_allowPartial) {
+ return this.containsBrowserRange(range.range_, opt_allowPartial)
+};
+goog$dom$browserrange$IeRange.prototype.compareBrowserRangeEndpoints = function(range, thisEndpoint, otherEndpoint) {
+ return this.range_.compareEndPoints((thisEndpoint == 1 ? "Start" : "End") + "To" + (otherEndpoint == 1 ? "Start" : "End"), range)
+};
+goog$dom$browserrange$IeRange.prototype.getEndpointNode_ = function(endpoint, opt_node) {
+ var node = opt_node || this.getContainer();
+ if(!node || !node.firstChild) {
+ if(endpoint == 0 && node.previousSibling && node.previousSibling.tagName == "BR" && this.getOffset_(endpoint, node) == 0)node = node.previousSibling;
+ return node.tagName == "BR" ? node.parentNode : node
+ }for(var child = endpoint == 1 ? node.firstChild : node.lastChild;child;) {
+ if(this.containsNode(child, true))return this.getEndpointNode_(endpoint, child);
+ child = endpoint == 1 ? child.nextSibling : child.previousSibling
+ }return node
+};
+goog$dom$browserrange$IeRange.prototype.getOffset_ = function(endpoint, opt_container) {
+ var container = opt_container || (endpoint == 1 ? this.getStartNode() : this.getEndNode());
+ if(container.nodeType == 1) {
+ for(var children = container.childNodes, len = children.length, i = endpoint == 1 ? 0 : len - 1;i >= 0 && i < len;) {
+ var child = children[i];
+ if(this.containsNode(child, true)) {
+ endpoint == 0 && child.previousSibling && child.previousSibling.tagName == "BR" && this.getOffset_(endpoint, child) == 0 && i--;
+ break
+ }i += endpoint == 1 ? 1 : -1
+ }return i == -1 ? 0 : i
+ }else {
+ var range = this.range_.duplicate(), nodeRange = goog$dom$browserrange$IeRange$getBrowserRangeForNode_(container);
+ range.setEndPoint(endpoint == 1 ? "EndToEnd" : "StartToStart", nodeRange);
+ var rangeLength = range.text.length;
+ return endpoint == 0 ? rangeLength : container.length - rangeLength
+ }
+};
+goog$dom$browserrange$IeRange.prototype.isCollapsed = function() {
+ return this.range_.text == ""
+};
+goog$dom$browserrange$IeRange.prototype.select = function() {
+ this.range_.select()
+};
+goog$dom$browserrange$IeRange.prototype.collapse = function(toStart) {
+ this.range_.collapse(toStart);
+ if(toStart)this.endNode_ = this.startNode_;
+ else this.startNode_ = this.endNode_
+};var goog$dom$browserrange$WebKitRange = function(range) {
+ goog$dom$browserrange$W3cRange.call(this, range)
+};
+goog$inherits(goog$dom$browserrange$WebKitRange, goog$dom$browserrange$W3cRange);
+goog$dom$browserrange$WebKitRange.prototype.compareBrowserRangeEndpoints = function(range, thisEndpoint, otherEndpoint) {
+ if(goog$userAgent$isVersion("528"))return goog$dom$browserrange$WebKitRange.superClass_.compareBrowserRangeEndpoints.call(this, range, thisEndpoint, otherEndpoint);
+ return this.range_.compareBoundaryPoints(otherEndpoint == 1 ? thisEndpoint == 1 ? goog$global.Range.START_TO_START : goog$global.Range.END_TO_START : thisEndpoint == 1 ? goog$global.Range.START_TO_END : goog$global.Range.END_TO_END, range)
+};
+goog$dom$browserrange$WebKitRange.prototype.selectInternal = function(selection, reversed) {
+ selection.removeAllRanges();
+ reversed ? selection.setBaseAndExtent(this.getEndNode(), this.getEndOffset(), this.getStartNode(), this.getStartOffset()) : selection.setBaseAndExtent(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset())
+};var goog$dom$browserrange$createRangeFromNodes = function(startNode, startOffset, endNode, endOffset) {
+ return goog$userAgent$IE ? new goog$dom$browserrange$IeRange(goog$dom$browserrange$IeRange$getBrowserRangeForNodes_(startNode, startOffset, endNode, endOffset), goog$dom$getOwnerDocument(startNode)) : goog$userAgent$WEBKIT ? new goog$dom$browserrange$WebKitRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNodes_(startNode, startOffset, endNode, endOffset)) : goog$userAgent$GECKO ? new goog$dom$browserrange$GeckoRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNodes_(startNode, startOffset,
+ endNode, endOffset)) : new goog$dom$browserrange$W3cRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNodes_(startNode, startOffset, endNode, endOffset))
+};var goog$dom$TextRange = function() {
+};
+goog$inherits(goog$dom$TextRange, goog$dom$AbstractRange);
+var goog$dom$TextRange$createFromBrowserRangeWrapper_ = function(browserRange, opt_isReversed) {
+ var range = new goog$dom$TextRange;
+ range.browserRangeWrapper_ = browserRange;
+ range.isReversed_ = !!opt_isReversed;
+ return range
+}, goog$dom$TextRange$createFromNodeContents = function(node, opt_isReversed) {
+ return goog$dom$TextRange$createFromBrowserRangeWrapper_(goog$userAgent$IE ? goog$dom$browserrange$IeRange$createFromNodeContents(node) : goog$userAgent$WEBKIT ? new goog$dom$browserrange$WebKitRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)) : goog$userAgent$GECKO ? new goog$dom$browserrange$GeckoRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)) : new goog$dom$browserrange$W3cRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)), opt_isReversed)
+}, goog$dom$TextRange$createFromNodes = function(anchorNode, anchorOffset, focusNode, focusOffset) {
+ var range = new goog$dom$TextRange;
+ range.isReversed_ = goog$dom$Range$isReversed(anchorNode, anchorOffset, focusNode, focusOffset);
+ if(anchorNode.tagName == "BR") {
+ var parent = anchorNode.parentNode;
+ anchorOffset = goog$array$indexOf(parent.childNodes, anchorNode);
+ anchorNode = parent
+ }if(focusNode.tagName == "BR") {
+ parent = focusNode.parentNode;
+ focusOffset = goog$array$indexOf(parent.childNodes, focusNode);
+ focusNode = parent
+ }if(range.isReversed_) {
+ range.startNode_ = focusNode;
+ range.startOffset_ = focusOffset;
+ range.endNode_ = anchorNode;
+ range.endOffset_ = anchorOffset
+ }else {
+ range.startNode_ = anchorNode;
+ range.startOffset_ = anchorOffset;
+ range.endNode_ = focusNode;
+ range.endOffset_ = focusOffset
+ }return range
+};
+goog$dom$TextRange.prototype.browserRangeWrapper_ = null;
+goog$dom$TextRange.prototype.startNode_ = null;
+goog$dom$TextRange.prototype.startOffset_ = null;
+goog$dom$TextRange.prototype.endNode_ = null;
+goog$dom$TextRange.prototype.endOffset_ = null;
+goog$dom$TextRange.prototype.isReversed_ = false;
+goog$dom$TextRange.prototype.getType = function() {
+ return"text"
+};
+goog$dom$TextRange.prototype.getBrowserRangeObject = function() {
+ return this.getBrowserRangeWrapper_().range_
+};
+goog$dom$TextRange.prototype.clearCachedValues_ = function() {
+ this.startNode_ = this.startOffset_ = this.endNode_ = this.endOffset_ = null
+};
+goog$dom$TextRange.prototype.getTextRangeCount = function() {
+ return 1
+};
+goog$dom$TextRange.prototype.getTextRange = function() {
+ return this
+};
+goog$dom$TextRange.prototype.getBrowserRangeWrapper_ = function() {
+ return this.browserRangeWrapper_ || (this.browserRangeWrapper_ = goog$dom$browserrange$createRangeFromNodes(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset()))
+};
+goog$dom$TextRange.prototype.getContainer = function() {
+ return this.getBrowserRangeWrapper_().getContainer()
+};
+goog$dom$TextRange.prototype.getStartNode = function() {
+ return this.startNode_ || (this.startNode_ = this.getBrowserRangeWrapper_().getStartNode())
+};
+goog$dom$TextRange.prototype.getStartOffset = function() {
+ return this.startOffset_ != null ? this.startOffset_ : (this.startOffset_ = this.getBrowserRangeWrapper_().getStartOffset())
+};
+goog$dom$TextRange.prototype.getEndNode = function() {
+ return this.endNode_ || (this.endNode_ = this.getBrowserRangeWrapper_().getEndNode())
+};
+goog$dom$TextRange.prototype.getEndOffset = function() {
+ return this.endOffset_ != null ? this.endOffset_ : (this.endOffset_ = this.getBrowserRangeWrapper_().getEndOffset())
+};
+goog$dom$TextRange.prototype.isReversed = function() {
+ return this.isReversed_
+};
+goog$dom$TextRange.prototype.containsRange = function(otherRange, opt_allowPartial) {
+ var otherRangeType = otherRange.getType();
+ if(otherRangeType == "text")return this.getBrowserRangeWrapper_().containsRange(otherRange.getBrowserRangeWrapper_(), opt_allowPartial);
+ else if(otherRangeType == "control") {
+ var elements = otherRange.getElements(), fn = opt_allowPartial ? goog$array$some : goog$array$every;
+ return fn(elements, function(el) {
+ return this.containsNode(el, opt_allowPartial)
+ }, this)
+ }
+};
+goog$dom$TextRange.prototype.isCollapsed = function() {
+ return this.getBrowserRangeWrapper_().isCollapsed()
+};
+goog$dom$TextRange.prototype.__iterator__ = function() {
+ return new goog$dom$TextRangeIterator(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset())
+};
+goog$dom$TextRange.prototype.select = function() {
+ this.getBrowserRangeWrapper_().select(this.isReversed_)
+};
+goog$dom$TextRange.prototype.saveUsingDom = function() {
+ return new goog$dom$DomSavedTextRange_(this)
+};
+goog$dom$TextRange.prototype.collapse = function(toAnchor) {
+ var toStart = this.isReversed() ? !toAnchor : toAnchor;
+ this.browserRangeWrapper_ && this.browserRangeWrapper_.collapse(toStart);
+ if(toStart) {
+ this.endNode_ = this.startNode_;
+ this.endOffset_ = this.startOffset_
+ }else {
+ this.startNode_ = this.endNode_;
+ this.startOffset_ = this.endOffset_
+ }this.isReversed_ = false
+};
+var goog$dom$DomSavedTextRange_ = function(range) {
+ this.anchorNode_ = range.getAnchorNode();
+ this.anchorOffset_ = range.getAnchorOffset();
+ this.focusNode_ = range.getFocusNode();
+ this.focusOffset_ = range.getFocusOffset()
+};
+goog$inherits(goog$dom$DomSavedTextRange_, goog$dom$SavedRange);var goog$dom$ControlRange = function() {
+};
+goog$inherits(goog$dom$ControlRange, goog$dom$AbstractMultiRange);
+goog$dom$ControlRange.prototype.range_ = null;
+goog$dom$ControlRange.prototype.elements_ = null;
+goog$dom$ControlRange.prototype.sortedElements_ = null;
+goog$dom$ControlRange.prototype.clearCachedValues_ = function() {
+ this.sortedElements_ = this.elements_ = null
+};
+goog$dom$ControlRange.prototype.getType = function() {
+ return"control"
+};
+goog$dom$ControlRange.prototype.getBrowserRangeObject = function() {
+ return this.range_ || document.body.createControlRange()
+};
+goog$dom$ControlRange.prototype.getTextRangeCount = function() {
+ return this.range_ ? this.range_.length : 0
+};
+goog$dom$ControlRange.prototype.getTextRange = function(i) {
+ return goog$dom$TextRange$createFromNodeContents(this.range_.item(i))
+};
+goog$dom$ControlRange.prototype.getContainer = function() {
+ return goog$dom$findCommonAncestor.apply(null, this.getElements())
+};
+goog$dom$ControlRange.prototype.getStartNode = function() {
+ return this.getSortedElements()[0]
+};
+goog$dom$ControlRange.prototype.getStartOffset = function() {
+ return 0
+};
+goog$dom$ControlRange.prototype.getEndNode = function() {
+ var sorted = this.getSortedElements(), startsLast = goog$array$peek(sorted);
+ return goog$array$find(sorted, function(el) {
+ return goog$dom$contains(el, startsLast)
+ })
+};
+goog$dom$ControlRange.prototype.getEndOffset = function() {
+ return this.getEndNode().childNodes.length
+};
+goog$dom$ControlRange.prototype.getElements = function() {
+ if(!this.elements_) {
+ this.elements_ = [];
+ if(this.range_)for(var i = 0;i < this.range_.length;i++)this.elements_.push(this.range_.item(i))
+ }return this.elements_
+};
+goog$dom$ControlRange.prototype.getSortedElements = function() {
+ if(!this.sortedElements_) {
+ this.sortedElements_ = this.getElements().concat();
+ this.sortedElements_.sort(function(a, b) {
+ return a.sourceIndex - b.sourceIndex
+ })
+ }return this.sortedElements_
+};
+goog$dom$ControlRange.prototype.isCollapsed = function() {
+ return!this.range_ || !this.range_.length
+};
+goog$dom$ControlRange.prototype.__iterator__ = function() {
+ return new goog$dom$ControlRangeIterator(this)
+};
+goog$dom$ControlRange.prototype.select = function() {
+ this.range_ && this.range_.select()
+};
+goog$dom$ControlRange.prototype.saveUsingDom = function() {
+ return new goog$dom$DomSavedControlRange_(this)
+};
+goog$dom$ControlRange.prototype.collapse = function() {
+ this.range_ = null;
+ this.clearCachedValues_()
+};
+var goog$dom$DomSavedControlRange_ = function(range) {
+ this.elements_ = range.getElements()
+};
+goog$inherits(goog$dom$DomSavedControlRange_, goog$dom$SavedRange);
+var goog$dom$ControlRangeIterator = function(range) {
+ if(range) {
+ this.elements_ = range.getSortedElements();
+ this.startNode_ = this.elements_.shift();
+ this.endNode_ = goog$array$peek(this.elements_) || this.startNode_
+ }goog$dom$RangeIterator.call(this, this.startNode_, false)
+};
+goog$inherits(goog$dom$ControlRangeIterator, goog$dom$RangeIterator);
+goog$dom$ControlRangeIterator.prototype.startNode_ = null;
+goog$dom$ControlRangeIterator.prototype.endNode_ = null;
+goog$dom$ControlRangeIterator.prototype.elements_ = null;
+goog$dom$ControlRangeIterator.prototype.getStartNode = function() {
+ return this.startNode_
+};
+goog$dom$ControlRangeIterator.prototype.getEndNode = function() {
+ return this.endNode_
+};
+goog$dom$ControlRangeIterator.prototype.isLast = function() {
+ return!this.depth && !this.elements_.length
+};
+goog$dom$ControlRangeIterator.prototype.next = function() {
+ if(this.isLast())throw goog$iter$StopIteration;else if(!this.depth) {
+ var el = this.elements_.shift();
+ this.setPosition(el, 1, 1);
+ return el
+ }return goog$dom$ControlRangeIterator.superClass_.next.call(this)
+};var goog$dom$MultiRange = function() {
+ this.browserRanges_ = [];
+ this.ranges_ = [];
+ this.container_ = this.sortedRanges_ = null
+};
+goog$inherits(goog$dom$MultiRange, goog$dom$AbstractMultiRange);
+goog$dom$MultiRange.prototype.logger_ = goog$debug$LogManager$getLogger("goog.dom.MultiRange");
+goog$dom$MultiRange.prototype.clearCachedValues_ = function() {
+ this.ranges_ = [];
+ this.container_ = this.sortedRanges_ = null
+};
+goog$dom$MultiRange.prototype.getType = function() {
+ return"mutli"
+};
+goog$dom$MultiRange.prototype.getBrowserRangeObject = function() {
+ this.browserRanges_.length > 1 && this.logger_.warning("getBrowserRangeObject called on MultiRange with more than 1 range");
+ return this.browserRanges_[0]
+};
+goog$dom$MultiRange.prototype.getTextRangeCount = function() {
+ return this.browserRanges_.length
+};
+goog$dom$MultiRange.prototype.getTextRange = function(i) {
+ this.ranges_[i] || (this.ranges_[i] = goog$dom$TextRange$createFromBrowserRangeWrapper_(goog$userAgent$IE ? new goog$dom$browserrange$IeRange(this.browserRanges_[i], goog$dom$getOwnerDocument(this.browserRanges_[i].parentElement())) : goog$userAgent$WEBKIT ? new goog$dom$browserrange$WebKitRange(this.browserRanges_[i]) : goog$userAgent$GECKO ? new goog$dom$browserrange$GeckoRange(this.browserRanges_[i]) : new goog$dom$browserrange$W3cRange(this.browserRanges_[i]), undefined));
+ return this.ranges_[i]
+};
+goog$dom$MultiRange.prototype.getContainer = function() {
+ if(!this.container_) {
+ for(var nodes = [], i = 0, len = this.getTextRangeCount();i < len;i++)nodes.push(this.getTextRange(i).getContainer());
+ this.container_ = goog$dom$findCommonAncestor.apply(null, nodes)
+ }return this.container_
+};
+goog$dom$MultiRange.prototype.getSortedRanges = function() {
+ if(!this.sortedRanges_) {
+ this.sortedRanges_ = this.getTextRanges();
+ this.sortedRanges_.sort(function(a, b) {
+ var aStartNode = a.getStartNode(), aStartOffset = a.getStartOffset(), bStartNode = b.getStartNode(), bStartOffset = b.getStartOffset();
+ if(aStartNode == bStartNode && aStartOffset == bStartOffset)return 0;
+ return goog$dom$Range$isReversed(aStartNode, aStartOffset, bStartNode, bStartOffset) ? 1 : -1
+ })
+ }return this.sortedRanges_
+};
+goog$dom$MultiRange.prototype.getStartNode = function() {
+ return this.getSortedRanges()[0].getStartNode()
+};
+goog$dom$MultiRange.prototype.getStartOffset = function() {
+ return this.getSortedRanges()[0].getStartOffset()
+};
+goog$dom$MultiRange.prototype.getEndNode = function() {
+ return goog$array$peek(this.getSortedRanges()).getEndNode()
+};
+goog$dom$MultiRange.prototype.getEndOffset = function() {
+ return goog$array$peek(this.getSortedRanges()).getEndOffset()
+};
+goog$dom$MultiRange.prototype.isCollapsed = function() {
+ return this.browserRanges_.length == 0 || this.browserRanges_.length == 1 && this.getTextRange(0).isCollapsed()
+};
+goog$dom$MultiRange.prototype.__iterator__ = function() {
+ return new goog$dom$MultiRangeIterator(this)
+};
+goog$dom$MultiRange.prototype.select = function() {
+ var selection;
+ JSCompiler_inline_label_goog$dom$AbstractRange$getBrowserSelectionForWindow_50: {
+ var JSCompiler_inline_win = this.getWindow();
+ if(JSCompiler_inline_win.getSelection)selection = JSCompiler_inline_win.getSelection();
+ else {
+ var JSCompiler_inline_doc = JSCompiler_inline_win.document;
+ selection = JSCompiler_inline_doc.selection || JSCompiler_inline_doc.getSelection && JSCompiler_inline_doc.getSelection()
+ }
+ }selection.removeAllRanges();
+ for(var i = 0, len = this.getTextRangeCount();i < len;i++)selection.addRange(this.getTextRange(i).getBrowserRangeObject())
+};
+goog$dom$MultiRange.prototype.saveUsingDom = function() {
+ return new goog$dom$DomSavedMultiRange_(this)
+};
+goog$dom$MultiRange.prototype.collapse = function(toAnchor) {
+ if(!this.isCollapsed()) {
+ var range = toAnchor ? this.getTextRange(0) : this.getTextRange(this.getTextRangeCount() - 1);
+ this.clearCachedValues_();
+ range.collapse(toAnchor);
+ this.ranges_ = [range];
+ this.sortedRanges_ = [range];
+ this.browserRanges_ = [range.getBrowserRangeObject()]
+ }
+};
+var goog$dom$DomSavedMultiRange_ = function(range) {
+ this.savedRanges_ = goog$array$map(range.getTextRanges(), function(range) {
+ return range.saveUsingDom()
+ })
+};
+goog$inherits(goog$dom$DomSavedMultiRange_, goog$dom$SavedRange);
+var goog$dom$MultiRangeIterator = function(range) {
+ if(range) {
+ this.ranges_ = range.getSortedRanges();
+ if(this.ranges_.length) {
+ this.startNode_ = this.ranges_[0].getStartNode();
+ this.endNode_ = goog$array$peek(this.ranges_).getEndNode()
+ }
+ }goog$dom$RangeIterator.call(this, this.startNode_, false)
+};
+goog$inherits(goog$dom$MultiRangeIterator, goog$dom$RangeIterator);
+goog$dom$MultiRangeIterator.prototype.startNode_ = null;
+goog$dom$MultiRangeIterator.prototype.endNode_ = null;
+goog$dom$MultiRangeIterator.prototype.ranges_ = null;
+goog$dom$MultiRangeIterator.prototype.getStartNode = function() {
+ return this.startNode_
+};
+goog$dom$MultiRangeIterator.prototype.getEndNode = function() {
+ return this.endNode_
+};
+goog$dom$MultiRangeIterator.prototype.isLast = function() {
+ return this.ranges_.length == 1 && this.ranges_[0].isLast()
+};
+goog$dom$MultiRangeIterator.prototype.next = function() {
+ do try {
+ this.ranges_[0].next();
+ break
+ }catch(ex) {
+ if(ex != goog$iter$StopIteration)throw ex;this.ranges_.shift()
+ }while(this.ranges_.length);
+ if(this.ranges_.length) {
+ var range = this.ranges_[0];
+ this.setPosition(range.node, range.tagType, range.depth);
+ return range.node
+ }else throw goog$iter$StopIteration;
+};var goog$dom$Range$createCaret = function(node, offset) {
+ return goog$dom$TextRange$createFromNodes(node, offset, node, offset)
+}, goog$dom$Range$createFromNodes = function(startNode, startOffset, endNode, endOffset) {
+ return goog$dom$TextRange$createFromNodes(startNode, startOffset, endNode, endOffset)
+}, goog$dom$Range$isReversed = function(anchorNode, anchorOffset, focusNode, focusOffset) {
+ if(anchorNode == focusNode)return focusOffset < anchorOffset;
+ var child;
+ if(anchorNode.nodeType == 1 && anchorOffset)if(child = anchorNode.childNodes[anchorOffset]) {
+ anchorNode = child;
+ anchorOffset = 0
+ }else if(goog$dom$contains(anchorNode, focusNode))return true;
+ if(focusNode.nodeType == 1 && focusOffset)if(child = focusNode.childNodes[focusOffset]) {
+ focusNode = child;
+ focusOffset = 0
+ }else if(goog$dom$contains(focusNode, anchorNode))return false;
+ return(goog$dom$compareNodeOrder(anchorNode, focusNode) || anchorOffset - focusOffset) > 0
+};window.createCaret = goog$dom$Range$createCaret;
+window.createFromNodes = goog$dom$Range$createFromNodes;
+try {
+ goog$dom$Range$createCaret(document.body, 0).select()
+}catch(e$$13) {
+};
+
+/**************************************************
+ Trace:
+ 56.427 Start Handling request
+ 0 56.427 Start Building cUnit
+ 1 56.428 Done 1 ms Building cUnit
+ 0 56.428 Start Checking memcacheg
+ 0 56.428 Start Connecting to memcacheg
+ 8 56.436 Done 8 ms Connecting to memcacheg
+ 1 56.437 Done 9 ms Checking memcacheg
+ 0 56.437 Done 10 ms Handling request
+**************************************************/
diff --git a/editor/libeditor/tests/browserscope/lib/richtext/richtext/richtext.html b/editor/libeditor/tests/browserscope/lib/richtext/richtext/richtext.html
new file mode 100644
index 000000000..ef0e22f2a
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext/richtext/richtext.html
@@ -0,0 +1,1081 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <title>Rich Text Tests</title>
+ <script src="js/range.js"></script>
+ <script>
+ /**
+ * Color class allows cross-browser comparison of values, which can
+ * be returned from queryCommandValue in several formats:
+ * 0xff00ff
+ * rgb(255, 0, 0)
+ * Number containing the hex value
+ */
+ function Color(value) {
+ this.compare = function(other) {
+ if (!this.valid || !other.valid) {
+ return false;
+ }
+ return this.red == other.red && this.green == other.green && this.blue == other.blue;
+ }
+ this.parse = function(value) {
+ var hexMatch = String(value).match(/#([0-9a-f]{6})/i);
+ if (hexMatch) {
+ this.red = parseInt(hexMatch[1].substring(0, 2), 16);
+ this.green = parseInt(hexMatch[1].substring(2, 4), 16);
+ this.blue = parseInt(hexMatch[1].substring(4, 6), 16);
+ return true;
+ }
+ var rgbMatch = String(value).match(/rgb\(([0-9]{1,3}),\s*([0-9]{1,3}),\s*([0-9]{1,3})\)/i);
+ if (rgbMatch) {
+ this.red = Number(rgbMatch[1]);
+ this.green = Number(rgbMatch[2]);
+ this.blue = Number(rgbMatch[3]);
+ return true;
+ }
+ if (Number(value)) {
+ this.red = value & 0xFF;
+ this.green = (value & 0xFF00) >> 8;
+ this.blue = (value & 0xFF0000) >> 16;
+ return true;
+ }
+ return false;
+ }
+ this.toString = function() {
+ return this.red + ',' + this.green + ',' + this.blue;
+ }
+ this.valid = this.parse(value);
+ }
+
+ /**
+ * Utility class for converting font sizes to the size
+ * attribute in a font tag. Currently only converts px because
+ * only the sizes and px ever come from queryCommandValue.
+ */
+ function Size(value) {
+ var pxMatch = String(value).match(/([0-9]+)px/);
+ if (pxMatch) {
+ var px = Number(pxMatch[1]);
+ if (px <= 10) {
+ this.size = 1;
+ } else if (px <= 13) {
+ this.size = 2;
+ } else if (px <= 16) {
+ this.size = 3;
+ } else if (px <= 18) {
+ this.size = 4;
+ } else if (px <= 24) {
+ this.size = 5;
+ } else if (px <= 32) {
+ this.size = 6;
+ } else if (px <= 47) {
+ this.size = 7;
+ } else {
+ this.size = NaN;
+ }
+ } else if (Number(value)) {
+ this.size = Number(value);
+ } else {
+ switch (value) {
+ case 'x-small':
+ this.size = 1;
+ break;
+ case 'small':
+ this.size = 2;
+ break;
+ case 'medium':
+ this.size = 3;
+ break;
+ case 'large':
+ this.size = 4;
+ break;
+ case 'x-large':
+ this.size = 5;
+ break;
+ case 'xx-large':
+ this.size = 6;
+ break;
+ case 'xxx-large':
+ case '-webkit-xxx-large':
+ this.size = 7;
+ break;
+ default:
+ this.size = null;
+ }
+ }
+ this.compare = function(other) {
+ return this.size == other.size;
+ }
+ this.toString = function() {
+ return String(this.size);
+ }
+ }
+
+ var IMAGE_URI = '/tests/editor/libeditor/tests/green.png';
+
+ var APPLY_TESTS = {
+ 'backcolor' : {
+ opt_arg: '#FF0000',
+ styleWithCSS: 'background-color'},
+ 'bold' : {
+ opt_arg: null,
+ styleWithCSS: 'font-weight'},
+ 'createbookmark' : {
+ opt_arg: 'bookmark_name'},
+ 'createlink' : {
+ opt_arg: 'http://www.openweb.org'},
+ 'decreasefontsize' : {
+ opt_arg: null},
+ 'fontname' : {
+ opt_arg: 'Arial',
+ styleWithCSS: 'font-family'},
+ 'fontsize' : {
+ opt_arg: 4,
+ styleWithCSS: 'font-size'},
+ 'forecolor' : {
+ opt_arg: '#FF0000',
+ styleWithCSS: 'color'},
+ 'formatblock' : {
+ opt_arg: 'h1',
+ wholeline: true},
+ 'hilitecolor' : {
+ opt_arg: '#FF0000',
+ styleWithCSS: 'background-color'},
+ 'indent' : {
+ opt_arg: null,
+ wholeline: true,
+ styleWithCSS: 'margin'},
+ 'inserthorizontalrule' : {
+ opt_arg: null,
+ collapse: true},
+ 'inserthtml': {
+ opt_arg: '<br>',
+ collapse: true},
+ 'insertimage': {
+ opt_arg: IMAGE_URI,
+ collapse: true},
+ 'insertorderedlist' : {
+ opt_arg: null,
+ wholeline: true},
+ 'insertunorderedlist' : {
+ opt_arg: null,
+ wholeline: true},
+ 'italic' : {
+ opt_arg: null,
+ styleWithCSS: 'font-style'},
+ 'justifycenter' : {
+ opt_arg: null,
+ wholeline: true,
+ styleWithCSS: 'text-align'},
+ 'justifyfull' : {
+ opt_arg: null,
+ wholeline: true,
+ styleWithCSS: 'text-align'},
+ 'justifyleft' : {
+ opt_arg: null,
+ wholeline: true,
+ styleWithCSS: 'text-align'},
+ 'justifyright' : {
+ opt_arg: null,
+ wholeline: true,
+ styleWithCSS: 'text-align'},
+ 'strikethrough' : {
+ opt_arg: null,
+ styleWithCSS: 'text-decoration'},
+ 'subscript' : {
+ opt_arg: null,
+ styleWithCSS: 'vertical-align'},
+ 'superscript' : {
+ opt_arg: null,
+ styleWithCSS: 'vertical-align'},
+ 'underline' : {
+ opt_arg: null,
+ styleWithCSS: 'text-decoration'}};
+
+ var UNAPPLY_TESTS = {
+ 'bold' : {
+ tags: [
+ ['<b>', '</b>'],
+ ['<STRONG>', '</STRONG>'],
+ ['<span style="font-weight: bold;">', '</span>']]},
+ 'italic' : {
+ tags: [
+ ['<i>', '</i>'],
+ ['<EM>', '</EM>'],
+ ['<span style="font-style: italic;">', '</span>']]},
+ 'outdent' : {
+ unapply: 'indent',
+ block: true,
+ tags: [
+ ['<blockquote>', '</blockquote>'],
+ ['<blockquote class="webkit-indent-blockquote" style="margin: 0 0 0 40px; border: none; padding: 0px;">', '</blockquote>'],
+ ['<ul><li>', '</li></ul>'],
+ ['<ol><li>', '</li></ol>'],
+ ['<div style="margin-left: 40px;">', '</div>']]},
+ 'removeformat' : {
+ unapply: '*',
+ block: true,
+ tags: [
+ ['<b>', '</b>'],
+ ['<a href="http://www.foo.com">', '</a>'],
+ ['<table><tr><td>', '</td></tr></table>']]},
+ 'strikethrough' : {
+ tags: [
+ ['<strike>', '</strike>'],
+ ['<s>', '</s>'],
+ ['<del>', '</del>'],
+ ['<span style="text-decoration: line-through;">', '</span>']]},
+ 'subscript' : {
+ tags: [
+ ['<sub>', '</sub>'],
+ ['<span style="vertical-align: sub;">', '</span>']]},
+ 'superscript' : {
+ tags: [
+ ['<sup>', '</sup>'],
+ ['<span style="vertical-align: super;">', '</span>']]},
+ 'unbookmark' : {
+ unapply: 'createbookmark',
+ tags: [
+ ['<a name="bookmark">', '</a>']]},
+ 'underline' : {
+ tags: [
+ ['<u>', '</u>'],
+ ['<span style="text-decoration: underline;">', '</span>']]},
+ 'unlink' : {
+ unapply: 'createbookmark',
+ tags: [
+ ['<a href="http://www.foo.com">', '</a>']]}};
+
+ var QUERY_TESTS = {
+ 'backcolor' : {
+ type: 'value',
+ tests: [
+ {html: '<FONT style="BACKGROUND-COLOR: #ffccaa">foo bar baz</FONT>', expected: new Color('#ffccaa')},
+ {html: '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0);">foo bar baz</span>', expected: new Color('#ff0000')},
+ {html: '<span style="background-color: #ff0000">foo bar baz</span>', expected: new Color('#ff0000')}
+ ]
+ },
+ 'bold' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<b>foo bar baz</b>', expected: true},
+ {html: '<STRONG>foo bar baz</STRONG>', expected: true},
+ {html: '<span style="font-weight:bold">foo bar baz</span>', expected: true},
+ {html: '<b style="font-weight:normal">foo bar baz</b>', expected: false},
+ {html: '<b><span style="font-weight:normal;">foo bar baz</span>', expected: false}
+ ]
+ },
+ 'fontname' : {
+ type: 'value',
+ tests: [
+ {html: '<font face="Arial">foo bar baz</font>', expected: 'Arial'},
+ {html: '<span style="font-family:Arial">foo bar baz</span>', expected: 'Arial'},
+ {html: '<font face="Arial" style="font-family:Courier">foo bar baz</font>', expected: 'Courier'},
+ {html: '<font face="Courier"><font face="Arial">foo bar baz</font></font>', expected: 'Arial'},
+ {html: '<span style="font-family:Courier"><font face="Arial">foo bar baz</font></span>', expected: 'Arial'}
+ ]
+ },
+ 'fontsize' : {
+ type: 'value',
+ tests: [
+ {html: '<font size=4>foo bar baz</font>', expected: new Size(4)},
+ // IE adds +1 to font size from font-size style attributes.
+ // This is hard to correct for since it does NOT add +1 to size attribute from font tag.
+ {html: '<span class="Apple-style-span" style="font-size: large;">foo bar baz</span>', expected: new Size(4)},
+ {html: '<font size=1 style="font-size:x-large;">foo bar baz</font>', expected: new Size(5)}
+ ]
+ },
+ 'forecolor' : {
+ type: 'value',
+ tests: [
+ {html: '<font color="#ff0000">foo bar baz</font>', expected: new Color('#ff0000')},
+ {html: '<span style="color:#ff0000">foo bar baz</span>', expected: new Color('#ff0000')},
+ {html: '<font color="#0000ff" style="color:#ff0000">foo bar baz</span>', expected: new Color('#ff0000')}
+ ]
+ },
+ 'hilitecolor' : {
+ type: 'value',
+ tests: [
+ {html: '<FONT style="BACKGROUND-COLOR: #ffccaa">foo bar baz</FONT>', expected: new Color('#ffccaa')},
+ {html: '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0);">foo bar baz</span>', expected: new Color('#ff0000')},
+ {html: '<span style="background-color: #ff0000">foo bar baz</span>', expected: new Color('#ff0000')}
+ ]
+ },
+ 'insertorderedlist' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<ol><li>foo bar baz</li></ol>', expected: true},
+ {html: '<ul><li>foo bar baz</li></ul>', expected: false}
+ ]
+ },
+ 'insertunorderedlist' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<ol><li>foo bar baz</li></ol>', expected: false},
+ {html: '<ul><li>foo bar baz</li></ul>', expected: true}
+ ]
+ },
+ 'italic' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<i>foo bar baz</i>', expected: true},
+ {html: '<EM>foo bar baz</EM>', expected: true},
+ {html: '<span style="font-style:italic">foo bar baz</span>', expected: true},
+ {html: '<i><span style="font-style:normal">foo bar baz</span></i>', expected: false}
+ ]
+ },
+ 'justifycenter' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<div align="center">foo bar baz</div>', expected: true},
+ {html: '<p align="center">foo bar baz</p>', expected: true},
+ {html: '<div style="text-align: center;">foo bar baz</div>', expected: true}
+ ]
+ },
+ 'justifyfull' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<div align="justify">foo bar baz</div>', expected: true},
+ {html: '<p align="justify">foo bar baz</p>', expected: true},
+ {html: '<div style="text-align: justify;">foo bar baz</div>', expected: true}
+ ]
+ },
+ 'justifyleft' : {
+ type: 'state',
+ tests: [
+ {html: '<div align="left">foo bar baz</div>', expected: true},
+ {html: '<p align="left">foo bar baz</p>', expected: true},
+ {html: '<div style="text-align: left;">foo bar baz</div>', expected: true}
+ ]
+ },
+ 'justifyright' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<div align="right">foo bar baz</div>', expected: true},
+ {html: '<p align="right">foo bar baz</p>', expected: true},
+ {html: '<div style="text-align: right;">foo bar baz</div>', expected: true}
+ ]
+ },
+ 'strikethrough' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<strike>foo bar baz</strike>', expected: true},
+ {html: '<strike style="text-decoration: none">foo bar baz</strike>', expected: false},
+ {html: '<s>foo bar baz</s>', expected: true},
+ {html: '<del>foo bar baz</del>', expected: true},
+ {html: '<span style="text-decoration:line-through">foo bar baz</span>', expected: true}
+ ]
+ },
+ 'subscript' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<sub>foo bar baz</sub>', expected: true}
+ ]
+ },
+ 'superscript' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<sup>foo bar baz</sup>', expected: true}
+ ]
+ },
+ 'underline' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<u>foo bar baz</u>', expected: true},
+ {html: '<a href="http://www.foo.com">foo bar baz</a>', expected: true},
+ {html: '<span style="text-decoration:underline">foo bar baz</span>', expected: true},
+ {html: '<u style="text-decoration:none">foo bar baz</u>', expected: false},
+ {html: '<a style="text-decoration:none" href="http://www.foo.com">foo bar baz</a>', expected: false}
+ ]
+ }
+ };
+
+ var CHANGE_TESTS = {
+ 'backcolor' : {
+ type: 'value',
+ tests: [
+ {html: '<FONT style="BACKGROUND-COLOR: #ffccaa">foo bar baz</FONT>', opt_arg: '#884422'},
+ {html: '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0);">foo bar baz</span>', opt_arg: '#0000ff'},
+ {html: '<span style="background-color: #ff0000">foo bar baz</span>', opt_arg: '#0000ff'}
+ ]
+ },
+ 'fontname' : {
+ type: 'value',
+ tests: [
+ {html: '<font face="Arial">foo bar baz</font>', opt_arg: 'Courier'},
+ {html: '<span style="font-family:Arial">foo bar baz</span>', opt_arg: 'Courier'},
+ {html: '<font face="Arial" style="font-family:Verdana">foo bar baz</font>', opt_arg: 'Courier'},
+ {html: '<font face="Verdana"><font face="Arial">foo bar baz</font></font>', opt_arg: 'Courier'},
+ {html: '<span style="font-family:Verdana"><font face="Arial">foo bar baz</font></span>', opt_arg: 'Courier'}
+ ]
+ },
+ 'fontsize' : {
+ type: 'value',
+ tests: [
+ {html: '<font size=4>foo bar baz</font>', opt_arg: 1},
+ {html: '<span class="Apple-style-span" style="font-size: large;">foo bar baz</span>', opt_arg: 1},
+ {html: '<font size=1 style="font-size:x-small;">foo bar baz</font>', opt_arg: 5}
+ ]
+ },
+ 'forecolor' : {
+ type: 'value',
+ tests: [
+ {html: '<font color="#ff0000">foo bar baz</font>', opt_arg: '#00ff00'},
+ {html: '<span style="color:#ff0000">foo bar baz</span>', opt_arg: '#00ff00'},
+ {html: '<font color="#0000ff" style="color:#ff0000">foo bar baz</span>', opt_arg: '#00ff00'}
+ ]
+ },
+ 'hilitecolor' : {
+ type: 'value',
+ tests: [
+ {html: '<FONT style="BACKGROUND-COLOR: #ffccaa">foo bar baz</FONT>', opt_arg: '#884422'},
+ {html: '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0);">foo bar baz</span>', opt_arg: '#00ff00'},
+ {html: '<span style="background-color: #ff0000">foo bar baz</span>', opt_arg: '#00ff00'}
+ ]
+ }
+ };
+
+ /** The document of the editable iframe */
+ var editorDoc = null;
+ /** Dummy text to apply and unapply formatting to */
+ var TEST_CONTENT = 'foo bar baz';
+ /**
+ * Word in dummy text that should change. Formatting is sometimes applied
+ * to a single word instead of the entire text node because sometimes a
+ * style might get applied to the body node instead of wrapped around
+ * the text, and that's not what's being tested.
+ */
+ var TEST_WORD = 'bar';
+ /** Constant for indicating an action is unsupported (threw exception) */
+ var UNSUPPORTED = 'UNSUPPORTED';
+ /** <br> and <p> are acceptable HTML to be left over from block elements */
+ var BLOCK_REMOVE_TAGS = [/\s*<br>\s*/i, /\s*<p>\s*/i];
+ /** Array used to accumulate test results */
+ // Tack on the actual display tests with bogus data
+ // otherwise the beacon will fail.
+ var results = ['apply=0', 'unapply=0', 'change=0', 'query=0'];
+
+ /**
+ *
+ */
+ function resetIframe(newHtml) {
+ // These attributes can get set on the iframe by some errant execCommands
+ editorDoc.body.setAttribute('style', '');
+ editorDoc.body.setAttribute('bgcolor', '');
+ editorDoc.body.innerHTML = newHtml;
+ }
+
+ /**
+ * Finds the text node in the given node containing the given word.
+ * Returns null if not found.
+ */
+ function findTextNode(word, node) {
+ if (node.nodeType == 3) {
+ // Text node, check value.
+ if (node.data.indexOf(word) != -1) {
+ return node;
+ }
+ } else if (node.nodeType == 1) {
+ // Element node, check children.
+ for (var i = 0; i < node.childNodes.length; i++) {
+ var result = findTextNode(word, node.childNodes[i]);
+ if (result) {
+ return result;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Sets the selection to be collapsed at the start of the word,
+ * or the start of the editor if no word is passed in.
+ */
+ function selectStart(word) {
+ var textNode = findTextNode(word || '', editorDoc.body);
+ var startOffset = 0;
+ if (word) {
+ startOffset = textNode.data.indexOf(word);
+ }
+ var range = createCaret(textNode, startOffset);
+ range.select();
+ }
+
+ /**
+ * Selects the given word in the editor iframe.
+ */
+ function selectWord(word) {
+ var textNode = findTextNode(word, editorDoc.body);
+ if (!textNode) {
+ return;
+ }
+ var start = textNode.data.indexOf(word);
+ var range = createFromNodes(textNode, start, textNode, start + word.length);
+ range.select();
+ }
+
+ /**
+ * Gets the HTML before the text, so that we know how the browser
+ * applied a style
+ */
+ function getSurroundingTags(text) {
+ var html = editorDoc.body.innerHTML;
+ var tagStart = html.indexOf('<');
+ var index = editorDoc.body.innerHTML.indexOf(text);
+ if (tagStart == -1 || index == -1) {
+ return '';
+ }
+ return editorDoc.body.innerHTML.substring(tagStart, index);
+ }
+
+ /**
+ * Does the test for an apply execCommand.
+ */
+ function doApplyTest(command, styleWithCSS) {
+ try {
+ // Set styleWithCSS
+ try {
+ editorDoc.execCommand('styleWithCSS', false, styleWithCSS);
+ } catch (ex) {
+ // Ignore errors
+ }
+ resetIframe(TEST_CONTENT);
+ if (APPLY_TESTS[command].collapse) {
+ selectStart(TEST_WORD);
+ } else {
+ selectWord(TEST_WORD);
+ }
+ try {
+ editorDoc.execCommand(command, false, APPLY_TESTS[command].opt_arg);
+ } catch (ex) {
+ return UNSUPPORTED;
+ }
+ return getSurroundingTags(APPLY_TESTS[command].wholeline? TEST_CONTENT : TEST_WORD);
+ } catch (ex) {
+ return UNSUPPORTED;
+ }
+ }
+
+ /**
+ * Outputs the result of the apply command to a table.
+ * @return {boolean} success
+ */
+ function outputApplyResult(command, result, styleWithCSS) {
+ // The apply command "succeeded" if HTML was generated.
+ var success = (result != UNSUPPORTED) && result;
+ // Except for styleWithCSS commands, which only succeed if the
+ // expected style was applied.
+ if (styleWithCSS) {
+ success = result && result.toLowerCase().indexOf(APPLY_TESTS[command].styleWithCSS) != -1;
+ }
+ results.push('a-' + command + '-' + (styleWithCSS ? 1 : 0) + '=' + (success ? '1' : '0'));
+
+ // Each command is displayed as a table row with 3 columns
+ var tr = document.createElement('TR');
+ tr.className = success ? 'success' : 'fail';
+
+ // Column 1: command name
+ var td = document.createElement('TD');
+ td.innerHTML = command;
+ tr.appendChild(td);
+
+ // Column 2: styleWithCSS
+ var td = document.createElement('TD');
+ td.innerHTML = styleWithCSS ? 'true' : 'false';
+ tr.appendChild(td);
+
+ // Column 3: pass/fail
+ td = document.createElement('TD');
+ td.innerHTML = success ? 'PASS' : 'FAIL';
+ tr.appendChild(td);
+
+ // Column 4: generated HTML (for passing commands)
+ td = document.createElement('TD');
+ // Escape the HTML in the result for printing.
+ result = result.replace(/\</g, '&lt;').replace(/\>/g, '&gt;');
+ td.innerHTML = result;
+ tr.appendChild(td);
+ var table = document.getElementById('apply_output');
+ table.appendChild(tr);
+ return success;
+ }
+
+ /**
+ * Does the test for an unapply execCommand.
+ */
+ function doUnapplyTest(command, index) {
+ try {
+ var wordStart = TEST_CONTENT.indexOf(TEST_WORD);
+ resetIframe(
+ TEST_CONTENT.substring(0, wordStart) +
+ UNAPPLY_TESTS[command].tags[index][0] +
+ TEST_WORD +
+ UNAPPLY_TESTS[command].tags[index][1] +
+ TEST_CONTENT.substring(wordStart + TEST_WORD.length));
+ selectWord(TEST_WORD);
+ try {
+ editorDoc.execCommand(command, false, UNAPPLY_TESTS[command].opt_arg || null);
+ } catch (ex) {
+ return UNSUPPORTED;
+ }
+ return getSurroundingTags(TEST_WORD);
+ } catch (ex) {
+ return UNSUPPORTED;
+ }
+ }
+
+ /**
+ * Check if the given unapply execCommand succeeded. It succeeded if
+ * the following conditions are true:
+ * - The execCommand did not throw an exception
+ * - One of the following:
+ * - The html was removed after the execCommand
+ * - The html was block and the html was replaced with <p> or <br>
+ */
+ function unapplyCommandSucceeded(command, result) {
+ if (result != UNSUPPORTED) {
+ if (!result) {
+ return true;
+ } else if (UNAPPLY_TESTS[command].block) {
+ for (var i = 0; i < BLOCK_REMOVE_TAGS.length; i++) {
+ if (result.match(BLOCK_REMOVE_TAGS[i])) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Outputs the result of the unapply command to a table.
+ * @return {boolean} success
+ */
+ function outputUnapplyResult(command, result, index) {
+ // The apply command "succeeded" if HTML was removed.
+ var success = unapplyCommandSucceeded(command, result);
+ results.push('u-' + command + '-' + index + '=' + (success ? '1' : '0'));
+
+ // Each command is displayed as a table row with 5 columns
+ var tr = document.createElement('TR');
+ tr.className = success ? 'success' : 'fail';
+
+ // Column 1: command name
+ var td = document.createElement('TD');
+ td.innerHTML = command;
+ tr.appendChild(td);
+
+ // Column 2: command name being unapplied
+ var td = document.createElement('TD');
+ td.innerHTML = UNAPPLY_TESTS[command].unapply || command;
+ tr.appendChild(td);
+
+ // Column 3: pass/fail
+ td = document.createElement('TD');
+ td.innerHTML = success ? 'PASS' : 'FAIL';
+ tr.appendChild(td);
+
+ // Column 4: html being removed
+ td = document.createElement('TD');
+ // Escape the html for printing.
+ var htmlToRemove = UNAPPLY_TESTS[command].tags[index][0].replace(/\</g, '&lt;').replace(/\>/g, '&gt;');
+ td.innerHTML = htmlToRemove;
+ tr.appendChild(td);
+
+ // Column 5: resulting html
+ td = document.createElement('TD');
+ // Escape the HTML in the result for printing.
+ result = result.replace(/\</g, '&lt;').replace(/\>/g, '&gt;');
+ td.innerHTML = success ? '&nbsp;' : result;
+ tr.appendChild(td);
+ var table = document.getElementById('unapply_output');
+ table.appendChild(tr);
+ return success;
+ }
+
+ /**
+ * Does a queryCommandState or queryCommandValue test for an execCommand.
+ */
+ function doQueryTest(command, index) {
+ try {
+ resetIframe(QUERY_TESTS[command].tests[index].html);
+ selectWord(TEST_WORD);
+ // Dummy val that won't match any expected vals, including false.
+ var result = UNSUPPORTED;
+ if (QUERY_TESTS[command].type == 'state') {
+ try {
+ result = editorDoc.queryCommandState(command);
+ } catch (ex) {
+ result = UNSUPPORTED;
+ }
+ } else {
+ try {
+ // A return value of false indicates the command is not supported.
+ result = editorDoc.queryCommandValue(command) || UNSUPPORTED;
+ } catch (ex) {
+ result = UNSUPPORTED;
+ }
+ }
+ return result;
+ } catch (ex) {
+ return UNSUPPORTED;
+ }
+ }
+
+ /**
+ * Check if the given queryCommandState or queryCommandValue succeeded.
+ */
+ function queryCommandSucceeded(command, index, result) {
+ var expected = QUERY_TESTS[command].tests[index].expected;
+ if (expected instanceof Color) {
+ return expected.compare(new Color(result));
+ } else if (expected instanceof Size) {
+ return expected.compare(new Size(result));
+ } else {
+ return (result == expected);
+ }
+ }
+
+ /**
+ * @return {boolean} success
+ */
+ function outputQueryResult(command, index, result) {
+ // Create table row for results.
+ var tr = document.createElement('TR');
+ var success = queryCommandSucceeded(command, index, result);
+ tr.className = success ? 'success' : 'fail';
+ results.push('q-' + command + '-' + index + '=' + (success ? '1' : '0'));
+
+ // Column 1: command name
+ var td = document.createElement('TD');
+ td.innerHTML = command;
+ tr.appendChild(td);
+
+ // Column 2: pass/fail
+ td = document.createElement('TD');
+ td.innerHTML = success ? 'PASS' : 'FAIL';
+ tr.appendChild(td);
+
+ // Column 3: test HTML
+ td = document.createElement('TD');
+ var testHtml = QUERY_TESTS[command].tests[index].html.replace(/</g, '&lt;').replace(/>/g, '&gt;');
+ td.innerHTML = testHtml.substring(0, testHtml.indexOf(TEST_CONTENT));
+ tr.appendChild(td);
+
+ // Column 4: Expected result
+ td = document.createElement('TD');
+ td.innerHTML = QUERY_TESTS[command].tests[index].expected;
+ tr.appendChild(td);
+
+ // Column 5: Actual result
+ td = document.createElement('TD');
+ td.innerHTML = result;
+ tr.appendChild(td);
+
+ // Append result to the state or value table, depending on what
+ // type of command this is.
+ var table = document.getElementById(
+ QUERY_TESTS[command].type == 'state' ? 'querystate_output' : 'queryvalue_output');
+ table.appendChild(tr);
+ return success;
+ }
+
+ function doChangeTest(command, index) {
+ try {
+ resetIframe(CHANGE_TESTS[command].tests[index].html);
+ selectWord(TEST_CONTENT);
+ try {
+ editorDoc.execCommand(command, false, CHANGE_TESTS[command].tests[index].opt_arg);
+ } catch (ex) {
+ return UNSUPPORTED;
+ }
+ } catch (ex) {
+ return UNSUPPORTED;
+ }
+ }
+
+ function checkChangeSuccess(command, index) {
+ var textNode = findTextNode(TEST_CONTENT, editorDoc.body);
+ if (!textNode) {
+ // The text has been removed from the document, or split up for no reason.
+ return false;
+ }
+ var expected = null, attributeName = null, styleName = null;
+ switch (command) {
+ case 'backcolor':
+ case 'hilitecolor':
+ expected = new Color(CHANGE_TESTS[command].tests[index].opt_arg);
+ styleName = 'backgroundColor';
+ break;
+ case 'fontname':
+ expected = CHANGE_TESTS[command].tests[index].opt_arg;
+ attributeName = 'face';
+ styleName = 'fontFamily';
+ break;
+ case 'fontsize':
+ expected = new Size(CHANGE_TESTS[command].tests[index].opt_arg);
+ attributeName = 'size';
+ styleName = 'fontSize';
+ break;
+ case 'forecolor':
+ expected = new Color(CHANGE_TESTS[command].tests[index].opt_arg);
+ attributeName = 'color';
+ styleName = 'color';
+ }
+ var foundExpected = false;
+
+ // Loop through all the parent nodes that format the text node,
+ // checking that there is exactly one font attribute or
+ // style, and that it's set correctly.
+ var currentNode = textNode.parentNode;
+ while(currentNode && currentNode.nodeName != 'BODY') {
+ // Check font attribute.
+ if (attributeName && currentNode.nodeName == 'FONT' && currentNode.getAttribute(attributeName)) {
+ var foundAttribute = false;
+ switch(command) {
+ case 'backcolor':
+ case 'forecolor':
+ case 'hilitecolor':
+ foundAttribute = new Color(currentNode.getAttribute(attributeName)).compare(expected);
+ break;
+ case 'fontsize':
+ foundAttribute = new Size(currentNode.getAttribute(attributeName)).compare(expected);
+ break;
+ case 'fontname':
+ foundAttribute = (currentNode.getAttribute(attributeName).toLowerCase() == expected.toLowerCase());
+ }
+ if (foundAttribute && foundExpected) {
+ // This is the correct attribute, but the style has been applied
+ // twice. This makes it hard for other browsers to remove the
+ // style.
+ return false;
+ } else if (!foundAttribute) {
+ // This node has an incorrect font attribute.
+ return false;
+ }
+ // The expected font attribute was found.
+ foundExpected = true;
+ }
+ // Check node style.
+ if (currentNode.style[styleName]) {
+ var foundStyle = false;
+ switch(command) {
+ case 'backcolor':
+ case 'forecolor':
+ case 'hilitecolor':
+ foundStyle = new Color(currentNode.style[styleName]).compare(expected);
+ break;
+ case 'fontsize':
+ foundStyle = new Size(currentNode.style[styleName]).compare(expected);
+ break;
+ case 'fontname':
+ foundStyle = (currentNode.style[styleName].toLowerCase() == expected.toLowerCase());
+ }
+ if (foundStyle && foundExpected) {
+ // This is the correct style, but the style has been
+ // applied twice. This makes it hard for other browsers to
+ // remove the style.
+ return false;
+ } else if (!foundStyle) {
+ // This node has an incorrect font style.
+ return false;
+ }
+ foundExpected = true;
+ }
+ currentNode = currentNode.parentNode;
+ }
+ return foundExpected;
+ }
+
+ /**
+ * @return {boolean} success
+ */
+ function outputChangeResult(command, index) {
+ // Each command is displayed as a table row with 4 columns
+ var tr = document.createElement('TR');
+ var success = checkChangeSuccess(command, index);
+ tr.className = success ? 'success' : 'fail';
+ results.push('c-' + command + '-' + index + '=' + (success ? '1' : '0'));
+
+ // Column 1: command name
+ var td = document.createElement('TD');
+ td.innerHTML = command;
+ tr.appendChild(td);
+
+ // Column 2: status
+ td = document.createElement('TD');
+ td.innerHTML = (success == null) ? '?' : (success == true ? 'PASS' : 'FAIL');
+ tr.appendChild(td);
+
+ // Column 3: opt_arg
+ td = document.createElement('TD');
+ td.innerHTML = CHANGE_TESTS[command].tests[index].opt_arg;
+ tr.appendChild(td);
+
+ // Column 4: original html
+ td = document.createElement('TD');
+ td.innerHTML = CHANGE_TESTS[command].tests[index].html.replace(/\</g, '&lt;').replace(/\>/g, '&gt;');;
+ tr.appendChild(td);
+
+ // Column 5: resulting html
+ td = document.createElement('TD');
+ td.innerHTML = editorDoc.body.innerHTML.replace(/\</g, '&lt;').replace(/\>/g, '&gt;');;
+ tr.appendChild(td);
+
+ var table = document.getElementById('change_output');
+ table.appendChild(tr);
+ return success;
+ }
+
+ function runTests() {
+ // Wrap initialization code in a try/catch so we can fail gracefully
+ // on older browsers.
+ try {
+ editorDoc = document.getElementById('editor').contentWindow.document;
+ // Default styleWithCSS to false, since it's not supported by IE.
+ try {
+ editorDoc.execCommand('styleWithCSS', false, false);
+ } catch (ex) {
+ // Not supported by IE.
+ }
+ } catch (ex) {}
+
+ // Apply tests
+ var apply_score = 0;
+ var apply_count = 0;
+ var unapply_score= 0;
+ var unapply_count = 0;
+ var change_score = 0;
+ var change_count = 0;
+ var query_score = 0;
+ var query_count = 0;
+ for (var command in APPLY_TESTS) {
+ try {
+ var result = doApplyTest(command, false);
+ var success = outputApplyResult(command, result, false);
+ apply_score += success ? 1 : 0;
+ } catch (ex) {
+ // An exception is counted as a failed test, don't increment success.
+ }
+ apply_count++;
+ if (APPLY_TESTS[command].styleWithCSS) {
+ try {
+ var result = doApplyTest(command, true);
+ var success = outputApplyResult(command, result, true);
+ apply_score += success ? 1 : 0;
+ } catch (ex) {
+ // An exception is counted as a failed test, don't increment success.
+ }
+ apply_count++;
+ }
+ }
+
+ // Unapply tests
+ for (var command in UNAPPLY_TESTS) {
+ for (var i = 0; i < UNAPPLY_TESTS[command].tags.length; i++) {
+ try {
+ var result = doUnapplyTest(command, i);
+ var success = outputUnapplyResult(command, result, i);
+ unapply_score += success ? 1 : 0;
+ } catch (ex) {
+ // An exception is counted as a failed test, don't increment success.
+ }
+ unapply_count++;
+ }
+ }
+
+ // Query tests
+ for (var command in QUERY_TESTS) {
+ for (var i = 0; i < QUERY_TESTS[command].tests.length; i++) {
+ try {
+ var result = doQueryTest(command, i);
+ var success = outputQueryResult(command, i, result);
+ query_score += success ? 1 : 0;
+ } catch (ex) {
+ // An exception is counted as a failed test, don't increment success.
+ }
+ query_count++;
+ }
+ }
+
+ // Change tests
+ for (var command in CHANGE_TESTS) {
+ for (var i = 0; i < CHANGE_TESTS[command].tests.length; i++) {
+ try {
+ doChangeTest(command, i);
+ var success = outputChangeResult(command, i);
+ change_score += success ? 1 : 0;
+ } catch (ex) {
+ // An exception is counted as a failed test, don't increment success.
+ }
+ change_count++;
+ }
+ }
+
+ // Beacon all test results.
+ // and construct a shorter version for the results page.
+ try {
+ document.getElementById('apply-score').innerHTML =
+ apply_score + '/' + apply_count;
+ document.getElementById('unapply-score').innerHTML =
+ unapply_score + '/' + unapply_count;
+ document.getElementById('query-score').innerHTML =
+ query_score + '/' + query_count;
+ document.getElementById('change-score').innerHTML =
+ change_score + '/' + change_count;
+ } catch (ex) {}
+ var continueParams = [
+ 'apply=' + apply_score,
+ 'unapply=' + unapply_score,
+ 'query=' + query_score,
+ 'change=' + change_score
+ ];
+ parent.sendScore(results, continueParams);
+ }
+ </script>
+ <style>
+ .success {
+ background-color: #93c47d;
+ }
+ .fail {
+ background-color: #ea9999;
+ }
+ .score {
+ color: #666;
+ }
+ </style>
+</head>
+<body onload="runTests()">
+ <h1>Apply Formatting <span id="apply-score" class="score"></span></h1>
+ <table id="apply"><tbody id="apply_output"><tr><th>Command</th><th>styleWithCSS</th><th>Status</th><th>Output</th></tr></tbody></table>
+ <h1>Unapply Formatting <span id="unapply-score" class="score"></span></h1>
+ <table id="unapply">
+ <thead><tr><th>Command</th><th>Command unapplied</th><th>Status</th><th>HTML Attempted to Unapply</th><th>Resulting HTML</th></tr></thead>
+ <tbody id="unapply_output"></tbody></table>
+ <h1>Query Formatting State <span id="query-score" class="score"></span></h1>
+ <table id="querystate">
+ <thead><tr><th>Command</th><th>Status</th><th>HTML</th><th>Expected</th><th>Actual</th></tr></thead>
+ <tbody id="querystate_output"></tbody></table>
+ <h1>Query Formatting Value </h1>
+ <table id="queryvalue">
+ <thead><tr><th>Command</th><th>Status</th><th>HTML</th><th>Expected</th><th>Actual</th></tr></thead>
+ <tbody id="queryvalue_output"></tbody></table>
+ <h1>Change Formatting <span id="change-score" class="score"></span></h1>
+ <table id="change">
+ <thead><tr><th>Command</th><th>Status</th><th>Argument</th><th>Original HTML</th><th>Resulting HTML</th></tr></thead>
+ <tbody id="change_output"></tbody></table>
+ <iframe name="editor" id="editor" src="editable.html"></iframe>
+</body>
+</html>
diff --git a/editor/libeditor/tests/browserscope/lib/richtext/update_from_upstream b/editor/libeditor/tests/browserscope/lib/richtext/update_from_upstream
new file mode 100644
index 000000000..2071454a8
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext/update_from_upstream
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+set -x
+
+if test -d richtext; then
+ rm -drf richtext;
+fi
+
+svn checkout http://browserscope.googlecode.com/svn/trunk/categories/richtext/static richtext | tail -1 | sed 's/[^0-9]//g' > current_revision
+
+find richtext -type d -name .svn -exec rm -drf \{\} \; 2> /dev/null
+
+hg add current_revision richtext
+
+hg stat .
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/LICENSE b/editor/libeditor/tests/browserscope/lib/richtext2/LICENSE
new file mode 100644
index 000000000..57bc88a15
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/LICENSE
@@ -0,0 +1,202 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/README b/editor/libeditor/tests/browserscope/lib/richtext2/README
new file mode 100644
index 000000000..a3bc3110f
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/README
@@ -0,0 +1,58 @@
+README FOR BROWSERSCOPE
+-----------------------
+
+Hey there - thanks for downloading the code. This file has instructions
+for getting setup so that you can run the codebase locally.
+
+This project is built on Google App Engine using the
+Django web application framework and written in Python.
+
+To get started, you'll need to first download the App Engine SDK at:
+http://code.google.com/appengine/downloads.html
+
+For local development, just startup the server:
+./pathto/google_appengine/dev_appserver.py --port=8080 browserscope
+
+You should then be able to access the local application at:
+http://localhost:8080/
+
+Note: the first time you hit the homepage it may take a little
+while - that's because it's trying to read out median times for all
+of the tests from a nonexistent datastore and write to memcache.
+Just be a lil patient.
+
+You can run the unit tests at:
+ http://localhost:8080/test
+
+
+CONTRIBUTING
+------------------
+
+Most likely you are interested in adding new tests or creating
+a new test category. If you are interested in adding tests to an existing
+"category" you may want to get in touch with the maintainer for that
+branch of the tree. We are really looking forward to receiving your
+code in patch format. Currently the category maintainers are:
+Network: Steve Souders <souders@gmail.com>
+Reflow: Lindsey Simon <elsigh@gmail.com>
+Security: Adam Barth <adam@adambarth.com> and Collin Jackson <collin@collinjackson.com>
+
+
+To create a completely new test category:
+ * Copy one of the existing directories in categories/
+ * Edit your test_set.py, handlers.py
+ * Add your files in templates/ and static/
+ * Update urls.py and settings.CATEGORIES
+ * Follow the examples of other tests re:
+ * beaconing using/testdriver_base
+ * your GetScoreAndDisplayValue method
+ * your GetRowScoreAndDisplayValue method
+
+References:
+ * App Engine Docs - http://code.google.com/appengine/docs/python/overview.html
+ * App Engine Group - http://groups.google.com/group/google-appengine
+ * Python Docs - http://www.python.org/doc/
+ * Django - http://www.djangoproject.com/
+
+
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/README.Mozilla b/editor/libeditor/tests/browserscope/lib/richtext2/README.Mozilla
new file mode 100644
index 000000000..3e667a0b7
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/README.Mozilla
@@ -0,0 +1,23 @@
+The BrowserScope project provides a set of cross-browser HTML editor tests,
+which we import in our test suite in order to run them as part of our
+continuous integration system.
+
+We pull tests occasionally from their Subversion repository using the pull
+script which can be found in this directory. We also record the revision ID
+which we've used in the current_revision file inside this directory.
+
+Using the pull script is quite easy, just switch to this directory, and say:
+
+sh update_from_upstream
+
+There are tests which we're currently failing on, and there will probably be
+more of those in the future. We should maintain a list of the failing tests
+manually in currentStatus.js (which can also be found in this directory), to
+make sure that the suite passes entirely, with failing tests marked as todo
+items.
+
+The current status of the test suite needs to be updated whenever an editor
+bug gets fixed, which makes us pass one of the tests. When that happens,
+you should set the UPDATE_TEST_RESULTS constant to true in test_richtext2.html,
+run the test suite, paste the result JSON string in a JSON beautifier (such
+as http://jsbeautifier.org/), and use the result to update currentStatus.js.
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/currentStatus.js b/editor/libeditor/tests/browserscope/lib/richtext2/currentStatus.js
new file mode 100644
index 000000000..570853afa
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/currentStatus.js
@@ -0,0 +1,1850 @@
+/**
+ * The current status of the test suite.
+ *
+ * See README.Mozilla for details on how to generate this.
+ */
+const knownFailures = {
+ "value": {
+ "A-Proposed-FS:18px_TEXT-1_SI-dM": true,
+ "A-Proposed-FS:18px_TEXT-1_SI-body": true,
+ "A-Proposed-FS:18px_TEXT-1_SI-div": true,
+ "A-Proposed-FS:large_TEXT-1_SI-dM": true,
+ "A-Proposed-FS:large_TEXT-1_SI-body": true,
+ "A-Proposed-FS:large_TEXT-1_SI-div": true,
+ "A-Proposed-CB:name_TEXT-1_SI-dM": true,
+ "A-Proposed-CB:name_TEXT-1_SI-body": true,
+ "A-Proposed-CB:name_TEXT-1_SI-div": true,
+ "AC-Proposed-SUB_TEXT-1_SI-dM": true,
+ "AC-Proposed-SUB_TEXT-1_SI-body": true,
+ "AC-Proposed-SUB_TEXT-1_SI-div": true,
+ "AC-Proposed-SUP_TEXT-1_SI-dM": true,
+ "AC-Proposed-SUP_TEXT-1_SI-body": true,
+ "AC-Proposed-SUP_TEXT-1_SI-div": true,
+ "AC-Proposed-FS:2_TEXT-1_SI-dM": true,
+ "AC-Proposed-FS:2_TEXT-1_SI-body": true,
+ "AC-Proposed-FS:2_TEXT-1_SI-div": true,
+ "AC-Proposed-FS:18px_TEXT-1_SI-dM": true,
+ "AC-Proposed-FS:18px_TEXT-1_SI-body": true,
+ "AC-Proposed-FS:18px_TEXT-1_SI-div": true,
+ "AC-Proposed-FS:large_TEXT-1_SI-dM": true,
+ "AC-Proposed-FS:large_TEXT-1_SI-body": true,
+ "AC-Proposed-FS:large_TEXT-1_SI-div": true,
+ "C-Proposed-BC:ace_FONT.ass.s:bc:rgb-1_SW-dM": true,
+ "C-Proposed-BC:ace_FONT.ass.s:bc:rgb-1_SW-body": true,
+ "C-Proposed-BC:ace_FONT.ass.s:bc:rgb-1_SW-div": true,
+ "C-Proposed-HC:g_SPAN.ass.s:c:rgb-1_SW-dM": true,
+ "C-Proposed-HC:g_SPAN.ass.s:c:rgb-1_SW-body": true,
+ "C-Proposed-HC:g_SPAN.ass.s:c:rgb-1_SW-div": true,
+ "C-Proposed-FN:c_FONTf:a-1_SI-dM": true,
+ "C-Proposed-FN:c_FONTf:a-1_SI-body": true,
+ "C-Proposed-FN:c_FONTf:a-1_SI-div": true,
+ "C-Proposed-FN:c_FONTf:a-2_SL-dM": true,
+ "C-Proposed-FN:c_FONTf:a-2_SL-body": true,
+ "C-Proposed-FN:c_FONTf:a-2_SL-div": true,
+ "C-Proposed-FS:1_SPAN.ass.s:fs:large-1_SW-dM": true,
+ "C-Proposed-FS:1_SPAN.ass.s:fs:large-1_SW-body": true,
+ "C-Proposed-FS:1_SPAN.ass.s:fs:large-1_SW-div": true,
+ "C-Proposed-FS:5_FONTsz:1.s:fs:xs-1_SW-dM": true,
+ "C-Proposed-FS:5_FONTsz:1.s:fs:xs-1_SW-body": true,
+ "C-Proposed-FS:5_FONTsz:1.s:fs:xs-1_SW-div": true,
+ "C-Proposed-FS:larger_FONTsz:4-dM": true,
+ "C-Proposed-FS:larger_FONTsz:4-body": true,
+ "C-Proposed-FS:larger_FONTsz:4-div": true,
+ "C-Proposed-FS:smaller_FONTsz:4-dM": true,
+ "C-Proposed-FS:smaller_FONTsz:4-body": true,
+ "C-Proposed-FS:smaller_FONTsz:4-div": true,
+ "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SO-dM": true,
+ "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SO-body": true,
+ "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SO-div": true,
+ "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SW-dM": true,
+ "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SW-body": true,
+ "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SW-div": true,
+ "C-Proposed-FB:h1_ADDRESS-FONT.ass.sz:4-1_SW-dM": true,
+ "C-Proposed-FB:h1_ADDRESS-FONT.ass.sz:4-1_SW-body": true,
+ "C-Proposed-FB:h1_ADDRESS-FONT.ass.sz:4-1_SW-div": true,
+ "CC-Proposed-I_B-1_SW-dM": true,
+ "CC-Proposed-I_B-1_SW-body": true,
+ "CC-Proposed-I_B-1_SW-div": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-1_SI-dM": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-1_SI-body": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-1_SI-div": true,
+ "CC-Proposed-BC:gray_P-SPANs:bc:b-3_SL-dM": true,
+ "CC-Proposed-BC:gray_P-SPANs:bc:b-3_SL-body": true,
+ "CC-Proposed-BC:gray_P-SPANs:bc:b-3_SL-div": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-2_SL-dM": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-2_SL-body": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-2_SL-div": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-2_SR-dM": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-2_SR-body": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-2_SR-div": true,
+ "CC-Proposed-FN:c_FONTf:a-1_SI-dM": true,
+ "CC-Proposed-FN:c_FONTf:a-1_SI-body": true,
+ "CC-Proposed-FN:c_FONTf:a-1_SI-div": true,
+ "CC-Proposed-FN:c_FONTf:a-2_SL-dM": true,
+ "CC-Proposed-FN:c_FONTf:a-2_SL-body": true,
+ "CC-Proposed-FN:c_FONTf:a-2_SL-div": true,
+ "CC-Proposed-FS:1_SPANs:fs:l-1_SW-dM": true,
+ "CC-Proposed-FS:1_SPANs:fs:l-1_SW-body": true,
+ "CC-Proposed-FS:1_SPANs:fs:l-1_SW-div": true,
+ "CC-Proposed-FS:18px_SPANs:fs:l-1_SW-dM": true,
+ "CC-Proposed-FS:18px_SPANs:fs:l-1_SW-body": true,
+ "CC-Proposed-FS:18px_SPANs:fs:l-1_SW-div": true,
+ "CC-Proposed-FS:4_SPANs:fs:l-1_SW-dM": true,
+ "CC-Proposed-FS:4_SPANs:fs:l-1_SW-body": true,
+ "CC-Proposed-FS:4_SPANs:fs:l-1_SW-div": true,
+ "CC-Proposed-FS:4_SPANs:fs:18px-1_SW-dM": true,
+ "CC-Proposed-FS:4_SPANs:fs:18px-1_SW-body": true,
+ "CC-Proposed-FS:4_SPANs:fs:18px-1_SW-div": true,
+ "CC-Proposed-FS:larger_SPANs:fs:l-1_SI-dM": true,
+ "CC-Proposed-FS:larger_SPANs:fs:l-1_SI-body": true,
+ "CC-Proposed-FS:larger_SPANs:fs:l-1_SI-div": true,
+ "CC-Proposed-FS:smaller_SPANs:fs:l-1_SI-dM": true,
+ "CC-Proposed-FS:smaller_SPANs:fs:l-1_SI-body": true,
+ "CC-Proposed-FS:smaller_SPANs:fs:l-1_SI-div": true,
+ "U-RFC-UNLINK_A-1_SO-dM": true,
+ "U-RFC-UNLINK_A-1_SO-body": true,
+ "U-RFC-UNLINK_A-1_SO-div": true,
+ "U-RFC-UNLINK_A-1_SW-dM": true,
+ "U-RFC-UNLINK_A-1_SW-body": true,
+ "U-RFC-UNLINK_A-1_SW-div": true,
+ "U-RFC-UNLINK_A-2_SO-dM": true,
+ "U-RFC-UNLINK_A-2_SO-body": true,
+ "U-RFC-UNLINK_A-2_SO-div": true,
+ "U-RFC-UNLINK_A2-1_SO-dM": true,
+ "U-RFC-UNLINK_A2-1_SO-body": true,
+ "U-RFC-UNLINK_A2-1_SO-div": true,
+ "U-Proposed-B_B-P-I..P-1_SO-I-dM": true,
+ "U-Proposed-B_B-P-I..P-1_SO-I-body": true,
+ "U-Proposed-B_B-P-I..P-1_SO-I-div": true,
+ "U-Proposed-B_B-2_SL-dM": true,
+ "U-Proposed-B_B-2_SL-body": true,
+ "U-Proposed-B_B-2_SL-div": true,
+ "U-Proposed-B_B-2_SR-dM": true,
+ "U-Proposed-B_B-2_SR-body": true,
+ "U-Proposed-B_B-2_SR-div": true,
+ "U-Proposed-U_U-S-2_SI-dM": true,
+ "U-Proposed-U_U-S-2_SI-body": true,
+ "U-Proposed-U_U-S-2_SI-div": true,
+ "U-Proposed-S_DEL-1_SW-dM": true,
+ "U-Proposed-S_DEL-1_SW-body": true,
+ "U-Proposed-S_DEL-1_SW-div": true,
+ "U-Proposed-SUB_SPANs:va:sub-1_SW-dM": true,
+ "U-Proposed-SUB_SPANs:va:sub-1_SW-body": true,
+ "U-Proposed-SUB_SPANs:va:sub-1_SW-div": true,
+ "U-Proposed-SUP_SPANs:va:super-1_SW-dM": true,
+ "U-Proposed-SUP_SPANs:va:super-1_SW-body": true,
+ "U-Proposed-SUP_SPANs:va:super-1_SW-div": true,
+ "U-Proposed-UNLINK_A-1_SC-dM": true,
+ "U-Proposed-UNLINK_A-1_SC-body": true,
+ "U-Proposed-UNLINK_A-1_SC-div": true,
+ "U-Proposed-UNLINK_A-1_SI-dM": true,
+ "U-Proposed-UNLINK_A-1_SI-body": true,
+ "U-Proposed-UNLINK_A-1_SI-div": true,
+ "U-Proposed-UNLINK_A-2_SL-dM": true,
+ "U-Proposed-UNLINK_A-2_SL-body": true,
+ "U-Proposed-UNLINK_A-2_SL-div": true,
+ "U-Proposed-UNLINK_A-3_SR-dM": true,
+ "U-Proposed-UNLINK_A-3_SR-body": true,
+ "U-Proposed-UNLINK_A-3_SR-div": true,
+ "U-Proposed-OUTDENT_BQ-1_SW-dM": true,
+ "U-Proposed-OUTDENT_BQ-1_SW-body": true,
+ "U-Proposed-OUTDENT_BQ-1_SW-div": true,
+ "U-Proposed-OUTDENT_BQ.wibq.s:m:00040.b:n.p:0-1_SW-dM": true,
+ "U-Proposed-OUTDENT_BQ.wibq.s:m:00040.b:n.p:0-1_SW-body": true,
+ "U-Proposed-OUTDENT_BQ.wibq.s:m:00040.b:n.p:0-1_SW-div": true,
+ "U-Proposed-OUTDENT_OL-LI-1_SW-dM": true,
+ "U-Proposed-OUTDENT_OL-LI-1_SW-body": true,
+ "U-Proposed-OUTDENT_OL-LI-1_SW-div": true,
+ "U-Proposed-OUTDENT_UL-LI-1_SW-dM": true,
+ "U-Proposed-OUTDENT_UL-LI-1_SW-body": true,
+ "U-Proposed-OUTDENT_UL-LI-1_SW-div": true,
+ "U-Proposed-OUTDENT_DIV-1_SW-dM": true,
+ "U-Proposed-OUTDENT_DIV-1_SW-body": true,
+ "U-Proposed-OUTDENT_DIV-1_SW-div": true,
+ "U-Proposed-REMOVEFORMAT_Ahref:url-1_SW-dM": true,
+ "U-Proposed-REMOVEFORMAT_Ahref:url-1_SW-body": true,
+ "U-Proposed-REMOVEFORMAT_Ahref:url-1_SW-div": true,
+ "U-Proposed-REMOVEFORMAT_TABLE-TBODY-TR-TD-1_SW-dM": true,
+ "U-Proposed-REMOVEFORMAT_TABLE-TBODY-TR-TD-1_SW-body": true,
+ "U-Proposed-REMOVEFORMAT_TABLE-TBODY-TR-TD-1_SW-div": true,
+ "U-Proposed-UNBOOKMARK_An:name-1_SW-dM": true,
+ "U-Proposed-UNBOOKMARK_An:name-1_SW-body": true,
+ "U-Proposed-UNBOOKMARK_An:name-1_SW-div": true,
+ "UC-Proposed-S_SPANc:s-1_SW-dM": true,
+ "UC-Proposed-S_SPANc:s-1_SW-body": true,
+ "UC-Proposed-S_SPANc:s-1_SW-div": true,
+ "UC-Proposed-S_SPANc:s-2_SI-dM": true,
+ "UC-Proposed-S_SPANc:s-2_SI-body": true,
+ "UC-Proposed-S_SPANc:s-2_SI-div": true,
+ "D-Proposed-CHAR-3_SC-dM": true,
+ "D-Proposed-CHAR-3_SC-body": true,
+ "D-Proposed-CHAR-3_SC-div": true,
+ "D-Proposed-CHAR-4_SC-dM": true,
+ "D-Proposed-CHAR-4_SC-body": true,
+ "D-Proposed-CHAR-4_SC-div": true,
+ "D-Proposed-CHAR-5_SC-dM": true,
+ "D-Proposed-CHAR-5_SC-body": true,
+ "D-Proposed-CHAR-5_SC-div": true,
+ "D-Proposed-CHAR-5_SI-1-dM": true,
+ "D-Proposed-CHAR-5_SI-1-body": true,
+ "D-Proposed-CHAR-5_SI-1-div": true,
+ "D-Proposed-CHAR-5_SI-2-dM": true,
+ "D-Proposed-CHAR-5_SI-2-body": true,
+ "D-Proposed-CHAR-5_SI-2-div": true,
+ "D-Proposed-CHAR-5_SR-dM": true,
+ "D-Proposed-CHAR-5_SR-body": true,
+ "D-Proposed-CHAR-5_SR-div": true,
+ "D-Proposed-CHAR-6_SC-dM": true,
+ "D-Proposed-CHAR-6_SC-body": true,
+ "D-Proposed-CHAR-6_SC-div": true,
+ "D-Proposed-CHAR-7_SC-dM": true,
+ "D-Proposed-CHAR-7_SC-body": true,
+ "D-Proposed-CHAR-7_SC-div": true,
+ "D-Proposed-OL-LI-1_SW-dM": true,
+ "D-Proposed-OL-LI-1_SW-body": true,
+ "D-Proposed-OL-LI-1_SW-div": true,
+ "D-Proposed-TR2rs:2-1_SO1-dM": true,
+ "D-Proposed-TR2rs:2-1_SO1-body": true,
+ "D-Proposed-TR2rs:2-1_SO1-div": true,
+ "D-Proposed-TR2rs:2-1_SO2-dM": true,
+ "D-Proposed-TR2rs:2-1_SO2-body": true,
+ "D-Proposed-TR2rs:2-1_SO2-div": true,
+ "D-Proposed-TR3rs:3-1_SO1-dM": true,
+ "D-Proposed-TR3rs:3-1_SO1-body": true,
+ "D-Proposed-TR3rs:3-1_SO1-div": true,
+ "D-Proposed-TR3rs:3-1_SO2-dM": true,
+ "D-Proposed-TR3rs:3-1_SO2-body": true,
+ "D-Proposed-TR3rs:3-1_SO2-div": true,
+ "D-Proposed-TR3rs:3-1_SO3-dM": true,
+ "D-Proposed-TR3rs:3-1_SO3-body": true,
+ "D-Proposed-TR3rs:3-1_SO3-div": true,
+ "D-Proposed-DIV:ce:false-1_SB-dM": true,
+ "D-Proposed-DIV:ce:false-1_SB-body": true,
+ "D-Proposed-DIV:ce:false-1_SB-div": true,
+ "D-Proposed-DIV:ce:false-1_SL-dM": true,
+ "D-Proposed-DIV:ce:false-1_SL-body": true,
+ "D-Proposed-DIV:ce:false-1_SL-div": true,
+ "D-Proposed-DIV:ce:false-1_SR-dM": true,
+ "D-Proposed-DIV:ce:false-1_SR-body": true,
+ "D-Proposed-DIV:ce:false-1_SR-div": true,
+ "D-Proposed-DIV:ce:false-1_SI-dM": true,
+ "FD-Proposed-OL-LI-1_SW-dM": true,
+ "FD-Proposed-OL-LI-1_SW-body": true,
+ "FD-Proposed-OL-LI-1_SW-div": true,
+ "FD-Proposed-TR2rs:2-1_SO1-dM": true,
+ "FD-Proposed-TR2rs:2-1_SO1-body": true,
+ "FD-Proposed-TR2rs:2-1_SO1-div": true,
+ "FD-Proposed-TR2rs:2-1_SO2-dM": true,
+ "FD-Proposed-TR2rs:2-1_SO2-body": true,
+ "FD-Proposed-TR2rs:2-1_SO2-div": true,
+ "FD-Proposed-TR3rs:3-1_SO1-dM": true,
+ "FD-Proposed-TR3rs:3-1_SO1-body": true,
+ "FD-Proposed-TR3rs:3-1_SO1-div": true,
+ "FD-Proposed-TR3rs:3-1_SO2-dM": true,
+ "FD-Proposed-TR3rs:3-1_SO2-body": true,
+ "FD-Proposed-TR3rs:3-1_SO2-div": true,
+ "FD-Proposed-TR3rs:3-1_SO3-dM": true,
+ "FD-Proposed-TR3rs:3-1_SO3-body": true,
+ "FD-Proposed-TR3rs:3-1_SO3-div": true,
+ "FD-Proposed-DIV:ce:false-1_SB-dM": true,
+ "FD-Proposed-DIV:ce:false-1_SB-body": true,
+ "FD-Proposed-DIV:ce:false-1_SB-div": true,
+ "FD-Proposed-DIV:ce:false-1_SL-dM": true,
+ "FD-Proposed-DIV:ce:false-1_SL-body": true,
+ "FD-Proposed-DIV:ce:false-1_SL-div": true,
+ "FD-Proposed-DIV:ce:false-1_SR-dM": true,
+ "FD-Proposed-DIV:ce:false-1_SR-body": true,
+ "FD-Proposed-DIV:ce:false-1_SR-div": true,
+ "FD-Proposed-DIV:ce:false-1_SI-dM": true,
+ "I-Proposed-IIMG:._SPAN-IMG-1_SO-dM": true,
+ "I-Proposed-IIMG:._SPAN-IMG-1_SO-body": true,
+ "I-Proposed-IIMG:._SPAN-IMG-1_SO-div": true,
+ "I-Proposed-IIMG:._IMG-1_SO-dM": true,
+ "I-Proposed-IIMG:._IMG-1_SO-body": true,
+ "I-Proposed-IIMG:._IMG-1_SO-div": true,
+ "Q-Proposed-UNSELECT_TEXT-1-dM": true,
+ "Q-Proposed-UNSELECT_TEXT-1-body": true,
+ "Q-Proposed-UNSELECT_TEXT-1-div": true,
+ "Q-Proposed-CREATEBOOKMARK_TEXT-1-dM": true,
+ "Q-Proposed-CREATEBOOKMARK_TEXT-1-body": true,
+ "Q-Proposed-CREATEBOOKMARK_TEXT-1-div": true,
+ "Q-Proposed-UNBOOKMARK_TEXT-1-dM": true,
+ "Q-Proposed-UNBOOKMARK_TEXT-1-body": true,
+ "Q-Proposed-UNBOOKMARK_TEXT-1-div": true,
+ "Q-Proposed-PASTE_TEXT-1-dM": true,
+ "Q-Proposed-PASTE_TEXT-1-body": true,
+ "Q-Proposed-PASTE_TEXT-1-div": true,
+ "QE-Proposed-UNSELECT_TEXT-1-dM": true,
+ "QE-Proposed-UNSELECT_TEXT-1-body": true,
+ "QE-Proposed-UNSELECT_TEXT-1-div": true,
+ "QE-Proposed-REDO_TEXT-1-dM": true,
+ "QE-Proposed-REDO_TEXT-1-body": true,
+ "QE-Proposed-REDO_TEXT-1-div": true,
+ "QE-Proposed-CREATEBOOKMARK_TEXT-1-dM": true,
+ "QE-Proposed-CREATEBOOKMARK_TEXT-1-body": true,
+ "QE-Proposed-CREATEBOOKMARK_TEXT-1-div": true,
+ "QE-Proposed-UNBOOKMARK_TEXT-1-dM": true,
+ "QE-Proposed-UNBOOKMARK_TEXT-1-body": true,
+ "QE-Proposed-UNBOOKMARK_TEXT-1-div": true,
+ "QE-Proposed-COPY_TEXT-1-dM": true,
+ "QE-Proposed-COPY_TEXT-1-body": true,
+ "QE-Proposed-COPY_TEXT-1-div": true,
+ "QE-Proposed-CUT_TEXT-1-dM": true,
+ "QE-Proposed-CUT_TEXT-1-body": true,
+ "QE-Proposed-CUT_TEXT-1-div": true,
+ "QE-Proposed-PASTE_TEXT-1-dM": true,
+ "QE-Proposed-PASTE_TEXT-1-body": true,
+ "QE-Proposed-PASTE_TEXT-1-div": true,
+ "QS-Proposed-SUB_SPAN.sub-1-SI-dM": true,
+ "QS-Proposed-SUB_SPAN.sub-1-SI-body": true,
+ "QS-Proposed-SUB_SPAN.sub-1-SI-div": true,
+ "QS-Proposed-SUB_MYSUB-1-SI-dM": true,
+ "QS-Proposed-SUB_MYSUB-1-SI-body": true,
+ "QS-Proposed-SUB_MYSUB-1-SI-div": true,
+ "QS-Proposed-SUP_SPAN.sup-1-SI-dM": true,
+ "QS-Proposed-SUP_SPAN.sup-1-SI-body": true,
+ "QS-Proposed-SUP_SPAN.sup-1-SI-div": true,
+ "QS-Proposed-SUP_MYSUP-1-SI-dM": true,
+ "QS-Proposed-SUP_MYSUP-1-SI-body": true,
+ "QS-Proposed-SUP_MYSUP-1-SI-div": true,
+ "QS-Proposed-JC_SPANs:ta:c-1_SI-dM": true,
+ "QS-Proposed-JC_SPANs:ta:c-1_SI-body": true,
+ "QS-Proposed-JC_SPANs:ta:c-1_SI-div": true,
+ "QS-Proposed-JC_SPAN.jc-1-SI-dM": true,
+ "QS-Proposed-JC_SPAN.jc-1-SI-body": true,
+ "QS-Proposed-JC_SPAN.jc-1-SI-div": true,
+ "QS-Proposed-JC_MYJC-1-SI-dM": true,
+ "QS-Proposed-JC_MYJC-1-SI-body": true,
+ "QS-Proposed-JC_MYJC-1-SI-div": true,
+ "QS-Proposed-JF_SPANs:ta:j-1_SI-dM": true,
+ "QS-Proposed-JF_SPANs:ta:j-1_SI-body": true,
+ "QS-Proposed-JF_SPANs:ta:j-1_SI-div": true,
+ "QS-Proposed-JF_SPAN.jf-1-SI-dM": true,
+ "QS-Proposed-JF_SPAN.jf-1-SI-body": true,
+ "QS-Proposed-JF_SPAN.jf-1-SI-div": true,
+ "QS-Proposed-JF_MYJF-1-SI-dM": true,
+ "QS-Proposed-JF_MYJF-1-SI-body": true,
+ "QS-Proposed-JF_MYJF-1-SI-div": true,
+ "QS-Proposed-JL_TEXT_SI-dM": true,
+ "QS-Proposed-JL_TEXT_SI-body": true,
+ "QS-Proposed-JL_TEXT_SI-div": true,
+ "QS-Proposed-JR_SPANs:ta:r-1_SI-dM": true,
+ "QS-Proposed-JR_SPANs:ta:r-1_SI-body": true,
+ "QS-Proposed-JR_SPANs:ta:r-1_SI-div": true,
+ "QS-Proposed-JR_SPAN.jr-1-SI-dM": true,
+ "QS-Proposed-JR_SPAN.jr-1-SI-body": true,
+ "QS-Proposed-JR_SPAN.jr-1-SI-div": true,
+ "QS-Proposed-JR_MYJR-1-SI-dM": true,
+ "QS-Proposed-JR_MYJR-1-SI-body": true,
+ "QS-Proposed-JR_MYJR-1-SI-div": true,
+ "QV-Proposed-B_TEXT_SI-dM": true,
+ "QV-Proposed-B_TEXT_SI-body": true,
+ "QV-Proposed-B_TEXT_SI-div": true,
+ "QV-Proposed-B_B-1_SI-dM": true,
+ "QV-Proposed-B_B-1_SI-body": true,
+ "QV-Proposed-B_B-1_SI-div": true,
+ "QV-Proposed-B_STRONG-1_SI-dM": true,
+ "QV-Proposed-B_STRONG-1_SI-body": true,
+ "QV-Proposed-B_STRONG-1_SI-div": true,
+ "QV-Proposed-B_SPANs:fw:b-1_SI-dM": true,
+ "QV-Proposed-B_SPANs:fw:b-1_SI-body": true,
+ "QV-Proposed-B_SPANs:fw:b-1_SI-div": true,
+ "QV-Proposed-B_SPANs:fw:n-1_SI-dM": true,
+ "QV-Proposed-B_SPANs:fw:n-1_SI-body": true,
+ "QV-Proposed-B_SPANs:fw:n-1_SI-div": true,
+ "QV-Proposed-B_Bs:fw:n-1_SI-dM": true,
+ "QV-Proposed-B_Bs:fw:n-1_SI-body": true,
+ "QV-Proposed-B_Bs:fw:n-1_SI-div": true,
+ "QV-Proposed-B_SPAN.b-1_SI-dM": true,
+ "QV-Proposed-B_SPAN.b-1_SI-body": true,
+ "QV-Proposed-B_SPAN.b-1_SI-div": true,
+ "QV-Proposed-B_MYB-1-SI-dM": true,
+ "QV-Proposed-B_MYB-1-SI-body": true,
+ "QV-Proposed-B_MYB-1-SI-div": true,
+ "QV-Proposed-I_TEXT_SI-dM": true,
+ "QV-Proposed-I_TEXT_SI-body": true,
+ "QV-Proposed-I_TEXT_SI-div": true,
+ "QV-Proposed-I_I-1_SI-dM": true,
+ "QV-Proposed-I_I-1_SI-body": true,
+ "QV-Proposed-I_I-1_SI-div": true,
+ "QV-Proposed-I_EM-1_SI-dM": true,
+ "QV-Proposed-I_EM-1_SI-body": true,
+ "QV-Proposed-I_EM-1_SI-div": true,
+ "QV-Proposed-I_SPANs:fs:i-1_SI-dM": true,
+ "QV-Proposed-I_SPANs:fs:i-1_SI-body": true,
+ "QV-Proposed-I_SPANs:fs:i-1_SI-div": true,
+ "QV-Proposed-I_SPANs:fs:n-1_SI-dM": true,
+ "QV-Proposed-I_SPANs:fs:n-1_SI-body": true,
+ "QV-Proposed-I_SPANs:fs:n-1_SI-div": true,
+ "QV-Proposed-I_I-SPANs:fs:n-1_SI-dM": true,
+ "QV-Proposed-I_I-SPANs:fs:n-1_SI-body": true,
+ "QV-Proposed-I_I-SPANs:fs:n-1_SI-div": true,
+ "QV-Proposed-I_SPAN.i-1_SI-dM": true,
+ "QV-Proposed-I_SPAN.i-1_SI-body": true,
+ "QV-Proposed-I_SPAN.i-1_SI-div": true,
+ "QV-Proposed-I_MYI-1-SI-dM": true,
+ "QV-Proposed-I_MYI-1-SI-body": true,
+ "QV-Proposed-I_MYI-1-SI-div": true,
+ "QV-Proposed-FB_BQ-1_SC-dM": true,
+ "QV-Proposed-FB_BQ-1_SC-body": true,
+ "QV-Proposed-FB_BQ-1_SC-div": true,
+ "QV-Proposed-FB_H1-H2-1_SL-dM": true,
+ "QV-Proposed-FB_H1-H2-1_SL-body": true,
+ "QV-Proposed-FB_H1-H2-1_SL-div": true,
+ "QV-Proposed-FB_H1-H2-1_SR-dM": true,
+ "QV-Proposed-FB_H1-H2-1_SR-body": true,
+ "QV-Proposed-FB_H1-H2-1_SR-div": true,
+ "QV-Proposed-FB_TEXT-ADDRESS-1_SL-dM": true,
+ "QV-Proposed-FB_TEXT-ADDRESS-1_SL-body": true,
+ "QV-Proposed-FB_TEXT-ADDRESS-1_SL-div": true,
+ "QV-Proposed-FB_H1-H2.TEXT.H2-1_SM-dM": true,
+ "QV-Proposed-FB_H1-H2.TEXT.H2-1_SM-body": true,
+ "QV-Proposed-FB_H1-H2.TEXT.H2-1_SM-div": true,
+ "QV-Proposed-H_P-1_SC-dM": true,
+ "QV-Proposed-H_P-1_SC-body": true,
+ "QV-Proposed-H_P-1_SC-div": true,
+ "QV-Proposed-FS_FONTs:fs:l-1_SI-dM": true,
+ "QV-Proposed-FS_FONTs:fs:l-1_SI-body": true,
+ "QV-Proposed-FS_FONTs:fs:l-1_SI-div": true,
+ "QV-Proposed-FS_FONT.ass.s:fs:l-1_SI-dM": true,
+ "QV-Proposed-FS_FONT.ass.s:fs:l-1_SI-body": true,
+ "QV-Proposed-FS_FONT.ass.s:fs:l-1_SI-div": true,
+ "QV-Proposed-FS_FONTsz:1.s:fs:xl-1_SI-dM": true,
+ "QV-Proposed-FS_FONTsz:1.s:fs:xl-1_SI-body": true,
+ "QV-Proposed-FS_FONTsz:1.s:fs:xl-1_SI-div": true,
+ "QV-Proposed-FS_SPAN.large-1_SI-dM": true,
+ "QV-Proposed-FS_SPAN.large-1_SI-body": true,
+ "QV-Proposed-FS_SPAN.large-1_SI-div": true,
+ "QV-Proposed-FS_SPAN.fs18px-1_SI-dM": true,
+ "QV-Proposed-FS_SPAN.fs18px-1_SI-body": true,
+ "QV-Proposed-FS_SPAN.fs18px-1_SI-div": true,
+ "QV-Proposed-FA_MYLARGE-1-SI-dM": true,
+ "QV-Proposed-FA_MYLARGE-1-SI-body": true,
+ "QV-Proposed-FA_MYLARGE-1-SI-div": true,
+ "QV-Proposed-FA_MYFS18PX-1-SI-dM": true,
+ "QV-Proposed-FA_MYFS18PX-1-SI-body": true,
+ "QV-Proposed-FA_MYFS18PX-1-SI-div": true,
+ "QV-Proposed-BC_FONTs:bc:fca-1_SI-dM": true,
+ "QV-Proposed-BC_FONTs:bc:fca-1_SI-body": true,
+ "QV-Proposed-BC_FONTs:bc:fca-1_SI-div": true,
+ "QV-Proposed-BC_SPANs:bc:abc-1_SI-dM": true,
+ "QV-Proposed-BC_SPANs:bc:abc-1_SI-body": true,
+ "QV-Proposed-BC_SPANs:bc:abc-1_SI-div": true,
+ "QV-Proposed-BC_FONTs:bc:084-SPAN-1_SI-dM": true,
+ "QV-Proposed-BC_FONTs:bc:084-SPAN-1_SI-body": true,
+ "QV-Proposed-BC_FONTs:bc:084-SPAN-1_SI-div": true,
+ "QV-Proposed-BC_SPANs:bc:cde-SPAN-1_SI-dM": true,
+ "QV-Proposed-BC_SPANs:bc:cde-SPAN-1_SI-body": true,
+ "QV-Proposed-BC_SPANs:bc:cde-SPAN-1_SI-div": true,
+ "QV-Proposed-BC_SPAN.ass.s:bc:rgb-1_SI-dM": true,
+ "QV-Proposed-BC_SPAN.ass.s:bc:rgb-1_SI-body": true,
+ "QV-Proposed-BC_SPAN.ass.s:bc:rgb-1_SI-div": true,
+ "QV-Proposed-BC_SPAN.bcred-1_SI-dM": true,
+ "QV-Proposed-BC_SPAN.bcred-1_SI-body": true,
+ "QV-Proposed-BC_SPAN.bcred-1_SI-div": true,
+ "QV-Proposed-BC_MYBCRED-1-SI-dM": true,
+ "QV-Proposed-BC_MYBCRED-1-SI-body": true,
+ "QV-Proposed-BC_MYBCRED-1-SI-div": true,
+ "QV-Proposed-HC_FONTs:bc:fc0-1_SI-dM": true,
+ "QV-Proposed-HC_FONTs:bc:fc0-1_SI-body": true,
+ "QV-Proposed-HC_FONTs:bc:fc0-1_SI-div": true,
+ "QV-Proposed-HC_SPANs:bc:a0c-1_SI-dM": true,
+ "QV-Proposed-HC_SPANs:bc:a0c-1_SI-body": true,
+ "QV-Proposed-HC_SPANs:bc:a0c-1_SI-div": true,
+ "QV-Proposed-HC_SPAN.ass.s:bc:rgb-1_SI-dM": true,
+ "QV-Proposed-HC_SPAN.ass.s:bc:rgb-1_SI-body": true,
+ "QV-Proposed-HC_SPAN.ass.s:bc:rgb-1_SI-div": true,
+ "QV-Proposed-HC_FONTs:bc:83e-SPAN-1_SI-dM": true,
+ "QV-Proposed-HC_FONTs:bc:83e-SPAN-1_SI-body": true,
+ "QV-Proposed-HC_FONTs:bc:83e-SPAN-1_SI-div": true,
+ "QV-Proposed-HC_SPANs:bc:b12-SPAN-1_SI-dM": true,
+ "QV-Proposed-HC_SPANs:bc:b12-SPAN-1_SI-body": true,
+ "QV-Proposed-HC_SPANs:bc:b12-SPAN-1_SI-div": true,
+ "QV-Proposed-HC_SPAN.bcred-1_SI-dM": true,
+ "QV-Proposed-HC_SPAN.bcred-1_SI-body": true,
+ "QV-Proposed-HC_SPAN.bcred-1_SI-div": true,
+ "QV-Proposed-HC_MYBCRED-1-SI-dM": true,
+ "QV-Proposed-HC_MYBCRED-1-SI-body": true,
+ "QV-Proposed-HC_MYBCRED-1-SI-div": true
+ },
+ "select": {
+ "S-Proposed-UNSEL_TEXT-1_SI-dM": true,
+ "S-Proposed-UNSEL_TEXT-1_SI-body": true,
+ "S-Proposed-UNSEL_TEXT-1_SI-div": true,
+ "S-Proposed-SM:m.f.c_TEXT-1_SI-1-dM": true,
+ "S-Proposed-SM:m.f.c_TEXT-1_SI-1-body": true,
+ "S-Proposed-SM:m.f.c_TEXT-1_SI-1-div": true,
+ "S-Proposed-SM:m.b.c_TEXT-1_SI-1-dM": true,
+ "S-Proposed-SM:m.b.c_TEXT-1_SI-1-body": true,
+ "S-Proposed-SM:m.b.c_TEXT-1_SI-1-div": true,
+ "S-Proposed-SM:m.b.w_TEXT-1_SI-1-dM": true,
+ "S-Proposed-SM:m.b.w_TEXT-1_SI-1-body": true,
+ "S-Proposed-SM:m.b.w_TEXT-1_SI-1-div": true,
+ "S-Proposed-SM:m.f.c_CHAR-5_SI-2-dM": true,
+ "S-Proposed-SM:m.f.c_CHAR-5_SI-2-body": true,
+ "S-Proposed-SM:m.f.c_CHAR-5_SI-2-div": true,
+ "S-Proposed-SM:m.f.c_CHAR-5_SR-dM": true,
+ "S-Proposed-SM:m.f.c_CHAR-5_SR-body": true,
+ "S-Proposed-SM:m.f.c_CHAR-5_SR-div": true,
+ "S-Proposed-SM:m.b.c_CHAR-5_SR-dM": true,
+ "S-Proposed-SM:m.b.c_CHAR-5_SR-body": true,
+ "S-Proposed-SM:m.b.c_CHAR-5_SR-div": true,
+ "S-Proposed-SM:m.f.w_TEXT-jp_SC-1-dM": true,
+ "S-Proposed-SM:m.f.w_TEXT-jp_SC-1-body": true,
+ "S-Proposed-SM:m.f.w_TEXT-jp_SC-1-div": true,
+ "S-Proposed-SM:m.f.w_TEXT-jp_SC-2-dM": true,
+ "S-Proposed-SM:m.f.w_TEXT-jp_SC-2-body": true,
+ "S-Proposed-SM:m.f.w_TEXT-jp_SC-2-div": true,
+ "S-Proposed-SM:m.f.w_TEXT-jp_SC-5-dM": true,
+ "S-Proposed-SM:m.f.w_TEXT-jp_SC-5-body": true,
+ "S-Proposed-SM:m.f.w_TEXT-jp_SC-5-div": true,
+ "S-Proposed-SM:e.b.w_TEXT-1_SI-3-dM": true,
+ "S-Proposed-SM:e.b.w_TEXT-1_SI-3-body": true,
+ "S-Proposed-SM:e.b.w_TEXT-1_SI-3-div": true,
+ "S-Proposed-SM:e.b.w_TEXT-1_SI-4-dM": true,
+ "S-Proposed-SM:e.b.w_TEXT-1_SI-4-body": true,
+ "S-Proposed-SM:e.b.w_TEXT-1_SI-4-div": true,
+ "S-Proposed-SM:e.b.w_TEXT-1_SI-5-dM": true,
+ "S-Proposed-SM:e.b.w_TEXT-1_SI-5-body": true,
+ "S-Proposed-SM:e.b.w_TEXT-1_SI-5-div": true,
+ "S-Proposed-SM:e.f.w_TEXT-1_SIR-1-dM": true,
+ "S-Proposed-SM:e.f.w_TEXT-1_SIR-1-body": true,
+ "S-Proposed-SM:e.f.w_TEXT-1_SIR-1-div": true,
+ "S-Proposed-SM:e.f.w_TEXT-1_SIR-3-dM": true,
+ "S-Proposed-SM:e.f.w_TEXT-1_SIR-3-body": true,
+ "S-Proposed-SM:e.f.w_TEXT-1_SIR-3-div": true,
+ "S-Proposed-SM:e.f.lb_BR.BR-1_SI-1-dM": true,
+ "S-Proposed-SM:e.f.lb_BR.BR-1_SI-1-body": true,
+ "S-Proposed-SM:e.f.lb_BR.BR-1_SI-1-div": true,
+ "S-Proposed-SM:e.f.lb_P.P.P-1_SI-1-dM": true,
+ "S-Proposed-SM:e.f.lb_P.P.P-1_SI-1-body": true,
+ "S-Proposed-SM:e.f.lb_P.P.P-1_SI-1-div": true,
+ "S-Proposed-SM:e.b.lb_BR.BR-1_SIR-2-dM": true,
+ "S-Proposed-SM:e.b.lb_BR.BR-1_SIR-2-body": true,
+ "S-Proposed-SM:e.b.lb_BR.BR-1_SIR-2-div": true,
+ "S-Proposed-SM:e.b.lb_P.P.P-1_SIR-2-dM": true,
+ "S-Proposed-SM:e.b.lb_P.P.P-1_SIR-2-body": true,
+ "S-Proposed-SM:e.b.lb_P.P.P-1_SIR-2-div": true,
+ "S-Proposed-SM:e.f.l_BR.BR-2_SI-1-dM": true,
+ "S-Proposed-SM:e.f.l_BR.BR-2_SI-1-body": true,
+ "S-Proposed-SM:e.f.l_BR.BR-2_SI-1-div": true,
+ "A-Proposed-B_TEXT-1_SI-dM": true,
+ "A-Proposed-B_TEXT-1_SI-body": true,
+ "A-Proposed-B_TEXT-1_SI-div": true,
+ "A-Proposed-B_TEXT-1_SIR-dM": true,
+ "A-Proposed-B_TEXT-1_SIR-body": true,
+ "A-Proposed-B_TEXT-1_SIR-div": true,
+ "A-Proposed-B_I-1_SL-dM": true,
+ "A-Proposed-B_I-1_SL-body": true,
+ "A-Proposed-B_I-1_SL-div": true,
+ "A-Proposed-I_TEXT-1_SI-dM": true,
+ "A-Proposed-I_TEXT-1_SI-body": true,
+ "A-Proposed-I_TEXT-1_SI-div": true,
+ "A-Proposed-U_TEXT-1_SI-dM": true,
+ "A-Proposed-U_TEXT-1_SI-body": true,
+ "A-Proposed-U_TEXT-1_SI-div": true,
+ "A-Proposed-S_TEXT-1_SI-dM": true,
+ "A-Proposed-S_TEXT-1_SI-body": true,
+ "A-Proposed-S_TEXT-1_SI-div": true,
+ "A-Proposed-SUB_TEXT-1_SI-dM": true,
+ "A-Proposed-SUB_TEXT-1_SI-body": true,
+ "A-Proposed-SUB_TEXT-1_SI-div": true,
+ "A-Proposed-SUP_TEXT-1_SI-dM": true,
+ "A-Proposed-SUP_TEXT-1_SI-body": true,
+ "A-Proposed-SUP_TEXT-1_SI-div": true,
+ "A-Proposed-CL:url_TEXT-1_SI-dM": true,
+ "A-Proposed-CL:url_TEXT-1_SI-body": true,
+ "A-Proposed-CL:url_TEXT-1_SI-div": true,
+ "A-Proposed-BC:blue_TEXT-1_SI-dM": true,
+ "A-Proposed-BC:blue_TEXT-1_SI-body": true,
+ "A-Proposed-BC:blue_TEXT-1_SI-div": true,
+ "A-Proposed-FC:blue_TEXT-1_SI-dM": true,
+ "A-Proposed-FC:blue_TEXT-1_SI-body": true,
+ "A-Proposed-FC:blue_TEXT-1_SI-div": true,
+ "A-Proposed-HC:blue_TEXT-1_SI-dM": true,
+ "A-Proposed-HC:blue_TEXT-1_SI-body": true,
+ "A-Proposed-HC:blue_TEXT-1_SI-div": true,
+ "A-Proposed-FN:a_TEXT-1_SI-dM": true,
+ "A-Proposed-FN:a_TEXT-1_SI-body": true,
+ "A-Proposed-FN:a_TEXT-1_SI-div": true,
+ "A-Proposed-FS:2_TEXT-1_SI-dM": true,
+ "A-Proposed-FS:2_TEXT-1_SI-body": true,
+ "A-Proposed-FS:2_TEXT-1_SI-div": true,
+ "A-Proposed-FS:18px_TEXT-1_SI-dM": true,
+ "A-Proposed-FS:18px_TEXT-1_SI-body": true,
+ "A-Proposed-FS:18px_TEXT-1_SI-div": true,
+ "A-Proposed-FS:large_TEXT-1_SI-dM": true,
+ "A-Proposed-FS:large_TEXT-1_SI-body": true,
+ "A-Proposed-FS:large_TEXT-1_SI-div": true,
+ "A-Proposed-INCFS:2_TEXT-1_SI-dM": true,
+ "A-Proposed-INCFS:2_TEXT-1_SI-body": true,
+ "A-Proposed-INCFS:2_TEXT-1_SI-div": true,
+ "A-Proposed-DECFS:2_TEXT-1_SI-dM": true,
+ "A-Proposed-DECFS:2_TEXT-1_SI-body": true,
+ "A-Proposed-DECFS:2_TEXT-1_SI-div": true,
+ "A-Proposed-CB:name_TEXT-1_SI-dM": true,
+ "A-Proposed-CB:name_TEXT-1_SI-body": true,
+ "A-Proposed-CB:name_TEXT-1_SI-div": true,
+ "AC-Proposed-B_TEXT-1_SI-dM": true,
+ "AC-Proposed-B_TEXT-1_SI-body": true,
+ "AC-Proposed-B_TEXT-1_SI-div": true,
+ "AC-Proposed-I_TEXT-1_SI-dM": true,
+ "AC-Proposed-I_TEXT-1_SI-body": true,
+ "AC-Proposed-I_TEXT-1_SI-div": true,
+ "AC-Proposed-U_TEXT-1_SI-dM": true,
+ "AC-Proposed-U_TEXT-1_SI-body": true,
+ "AC-Proposed-U_TEXT-1_SI-div": true,
+ "AC-Proposed-S_TEXT-1_SI-dM": true,
+ "AC-Proposed-S_TEXT-1_SI-body": true,
+ "AC-Proposed-S_TEXT-1_SI-div": true,
+ "AC-Proposed-SUB_TEXT-1_SI-dM": true,
+ "AC-Proposed-SUB_TEXT-1_SI-body": true,
+ "AC-Proposed-SUB_TEXT-1_SI-div": true,
+ "AC-Proposed-SUP_TEXT-1_SI-dM": true,
+ "AC-Proposed-SUP_TEXT-1_SI-body": true,
+ "AC-Proposed-SUP_TEXT-1_SI-div": true,
+ "AC-Proposed-BC:blue_TEXT-1_SI-dM": true,
+ "AC-Proposed-BC:blue_TEXT-1_SI-body": true,
+ "AC-Proposed-BC:blue_TEXT-1_SI-div": true,
+ "AC-Proposed-FC:blue_TEXT-1_SI-dM": true,
+ "AC-Proposed-FC:blue_TEXT-1_SI-body": true,
+ "AC-Proposed-FC:blue_TEXT-1_SI-div": true,
+ "AC-Proposed-HC:blue_TEXT-1_SI-dM": true,
+ "AC-Proposed-HC:blue_TEXT-1_SI-body": true,
+ "AC-Proposed-HC:blue_TEXT-1_SI-div": true,
+ "AC-Proposed-FN:a_TEXT-1_SI-dM": true,
+ "AC-Proposed-FN:a_TEXT-1_SI-body": true,
+ "AC-Proposed-FN:a_TEXT-1_SI-div": true,
+ "AC-Proposed-FS:2_TEXT-1_SI-dM": true,
+ "AC-Proposed-FS:2_TEXT-1_SI-body": true,
+ "AC-Proposed-FS:2_TEXT-1_SI-div": true,
+ "AC-Proposed-FS:18px_TEXT-1_SI-dM": true,
+ "AC-Proposed-FS:18px_TEXT-1_SI-body": true,
+ "AC-Proposed-FS:18px_TEXT-1_SI-div": true,
+ "AC-Proposed-FS:large_TEXT-1_SI-dM": true,
+ "AC-Proposed-FS:large_TEXT-1_SI-body": true,
+ "AC-Proposed-FS:large_TEXT-1_SI-div": true,
+ "C-Proposed-I_I-1_SL-dM": true,
+ "C-Proposed-I_I-1_SL-body": true,
+ "C-Proposed-I_I-1_SL-div": true,
+ "C-Proposed-I_B-I-1_SO-dM": true,
+ "C-Proposed-I_B-I-1_SO-body": true,
+ "C-Proposed-I_B-I-1_SO-div": true,
+ "C-Proposed-U_U-1_SO-dM": true,
+ "C-Proposed-U_U-1_SO-body": true,
+ "C-Proposed-U_U-1_SO-div": true,
+ "C-Proposed-U_U-1_SL-dM": true,
+ "C-Proposed-U_U-1_SL-body": true,
+ "C-Proposed-U_U-1_SL-div": true,
+ "C-Proposed-U_S-U-1_SO-dM": true,
+ "C-Proposed-U_S-U-1_SO-body": true,
+ "C-Proposed-U_S-U-1_SO-div": true,
+ "C-Proposed-BC:ace_FONT.ass.s:bc:rgb-1_SW-dM": true,
+ "C-Proposed-BC:ace_FONT.ass.s:bc:rgb-1_SW-body": true,
+ "C-Proposed-BC:ace_FONT.ass.s:bc:rgb-1_SW-div": true,
+ "C-Proposed-FC:g_FONTc:b.sz:6-1_SI-dM": true,
+ "C-Proposed-FC:g_FONTc:b.sz:6-1_SI-body": true,
+ "C-Proposed-FC:g_FONTc:b.sz:6-1_SI-div": true,
+ "C-Proposed-HC:g_SPAN.ass.s:c:rgb-1_SW-dM": true,
+ "C-Proposed-HC:g_SPAN.ass.s:c:rgb-1_SW-body": true,
+ "C-Proposed-HC:g_SPAN.ass.s:c:rgb-1_SW-div": true,
+ "C-Proposed-FN:c_FONTf:a-1_SI-dM": true,
+ "C-Proposed-FN:c_FONTf:a-1_SI-body": true,
+ "C-Proposed-FN:c_FONTf:a-1_SI-div": true,
+ "C-Proposed-FN:c_FONTf:a-2_SL-dM": true,
+ "C-Proposed-FN:c_FONTf:a-2_SL-body": true,
+ "C-Proposed-FN:c_FONTf:a-2_SL-div": true,
+ "C-Proposed-FS:1_SPAN.ass.s:fs:large-1_SW-dM": true,
+ "C-Proposed-FS:1_SPAN.ass.s:fs:large-1_SW-body": true,
+ "C-Proposed-FS:1_SPAN.ass.s:fs:large-1_SW-div": true,
+ "C-Proposed-FS:5_FONTsz:1.s:fs:xs-1_SW-dM": true,
+ "C-Proposed-FS:5_FONTsz:1.s:fs:xs-1_SW-body": true,
+ "C-Proposed-FS:5_FONTsz:1.s:fs:xs-1_SW-div": true,
+ "C-Proposed-FS:2_FONTc:b.sz:6-1_SI-dM": true,
+ "C-Proposed-FS:2_FONTc:b.sz:6-1_SI-body": true,
+ "C-Proposed-FS:2_FONTc:b.sz:6-1_SI-div": true,
+ "C-Proposed-FS:larger_FONTsz:4-dM": true,
+ "C-Proposed-FS:larger_FONTsz:4-body": true,
+ "C-Proposed-FS:larger_FONTsz:4-div": true,
+ "C-Proposed-FS:smaller_FONTsz:4-dM": true,
+ "C-Proposed-FS:smaller_FONTsz:4-body": true,
+ "C-Proposed-FS:smaller_FONTsz:4-div": true,
+ "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SO-dM": true,
+ "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SO-body": true,
+ "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SO-div": true,
+ "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SW-dM": true,
+ "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SW-body": true,
+ "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SW-div": true,
+ "C-Proposed-FB:h1_ADDRESS-FONT.ass.sz:4-1_SW-dM": true,
+ "C-Proposed-FB:h1_ADDRESS-FONT.ass.sz:4-1_SW-body": true,
+ "C-Proposed-FB:h1_ADDRESS-FONT.ass.sz:4-1_SW-div": true,
+ "CC-Proposed-I_I-1_SL-dM": true,
+ "CC-Proposed-I_I-1_SL-body": true,
+ "CC-Proposed-I_I-1_SL-div": true,
+ "CC-Proposed-I_B-1_SL-dM": true,
+ "CC-Proposed-I_B-1_SL-body": true,
+ "CC-Proposed-I_B-1_SL-div": true,
+ "CC-Proposed-I_B-1_SW-dM": true,
+ "CC-Proposed-I_B-1_SW-body": true,
+ "CC-Proposed-I_B-1_SW-div": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-1_SI-dM": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-1_SI-body": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-1_SI-div": true,
+ "CC-Proposed-BC:gray_P-SPANs:bc:b-3_SL-dM": true,
+ "CC-Proposed-BC:gray_P-SPANs:bc:b-3_SL-body": true,
+ "CC-Proposed-BC:gray_P-SPANs:bc:b-3_SL-div": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-2_SL-dM": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-2_SL-body": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-2_SL-div": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-2_SR-dM": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-2_SR-body": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-2_SR-div": true,
+ "CC-Proposed-FN:c_FONTf:a-1_SI-dM": true,
+ "CC-Proposed-FN:c_FONTf:a-1_SI-body": true,
+ "CC-Proposed-FN:c_FONTf:a-1_SI-div": true,
+ "CC-Proposed-FN:c_FONTf:a-2_SL-dM": true,
+ "CC-Proposed-FN:c_FONTf:a-2_SL-body": true,
+ "CC-Proposed-FN:c_FONTf:a-2_SL-div": true,
+ "CC-Proposed-FS:1_SPANs:fs:l-1_SW-dM": true,
+ "CC-Proposed-FS:1_SPANs:fs:l-1_SW-body": true,
+ "CC-Proposed-FS:1_SPANs:fs:l-1_SW-div": true,
+ "CC-Proposed-FS:18px_SPANs:fs:l-1_SW-dM": true,
+ "CC-Proposed-FS:18px_SPANs:fs:l-1_SW-body": true,
+ "CC-Proposed-FS:18px_SPANs:fs:l-1_SW-div": true,
+ "CC-Proposed-FS:4_SPANs:fs:l-1_SW-dM": true,
+ "CC-Proposed-FS:4_SPANs:fs:l-1_SW-body": true,
+ "CC-Proposed-FS:4_SPANs:fs:l-1_SW-div": true,
+ "CC-Proposed-FS:4_SPANs:fs:18px-1_SW-dM": true,
+ "CC-Proposed-FS:4_SPANs:fs:18px-1_SW-body": true,
+ "CC-Proposed-FS:4_SPANs:fs:18px-1_SW-div": true,
+ "CC-Proposed-FS:larger_SPANs:fs:l-1_SI-dM": true,
+ "CC-Proposed-FS:larger_SPANs:fs:l-1_SI-body": true,
+ "CC-Proposed-FS:larger_SPANs:fs:l-1_SI-div": true,
+ "CC-Proposed-FS:smaller_SPANs:fs:l-1_SI-dM": true,
+ "CC-Proposed-FS:smaller_SPANs:fs:l-1_SI-body": true,
+ "CC-Proposed-FS:smaller_SPANs:fs:l-1_SI-div": true,
+ "U-RFC-UNLINK_A-1_SO-dM": true,
+ "U-RFC-UNLINK_A-1_SO-body": true,
+ "U-RFC-UNLINK_A-1_SO-div": true,
+ "U-RFC-UNLINK_A-1_SW-dM": true,
+ "U-RFC-UNLINK_A-1_SW-body": true,
+ "U-RFC-UNLINK_A-1_SW-div": true,
+ "U-RFC-UNLINK_A-2_SO-dM": true,
+ "U-RFC-UNLINK_A-2_SO-body": true,
+ "U-RFC-UNLINK_A-2_SO-div": true,
+ "U-RFC-UNLINK_A2-1_SO-dM": true,
+ "U-RFC-UNLINK_A2-1_SO-body": true,
+ "U-RFC-UNLINK_A2-1_SO-div": true,
+ "U-Proposed-B_B-P3-1_SO12-dM": true,
+ "U-Proposed-B_B-P3-1_SO12-body": true,
+ "U-Proposed-B_B-P3-1_SO12-div": true,
+ "U-Proposed-B_B-P-I..P-1_SO-I-dM": true,
+ "U-Proposed-B_B-P-I..P-1_SO-I-body": true,
+ "U-Proposed-B_B-P-I..P-1_SO-I-div": true,
+ "U-Proposed-B_B-2_SL-dM": true,
+ "U-Proposed-B_B-2_SL-body": true,
+ "U-Proposed-B_B-2_SL-div": true,
+ "U-Proposed-B_B-2_SR-dM": true,
+ "U-Proposed-B_B-2_SR-body": true,
+ "U-Proposed-B_B-2_SR-div": true,
+ "U-Proposed-I_I-P3-1_SO2-dM": true,
+ "U-Proposed-I_I-P3-1_SO2-body": true,
+ "U-Proposed-I_I-P3-1_SO2-div": true,
+ "U-Proposed-U_U-S-1_SO-dM": true,
+ "U-Proposed-U_U-S-1_SO-body": true,
+ "U-Proposed-U_U-S-1_SO-div": true,
+ "U-Proposed-U_U-S-2_SI-dM": true,
+ "U-Proposed-U_U-S-2_SI-body": true,
+ "U-Proposed-U_U-S-2_SI-div": true,
+ "U-Proposed-U_U-P3-1_SO-dM": true,
+ "U-Proposed-U_U-P3-1_SO-body": true,
+ "U-Proposed-U_U-P3-1_SO-div": true,
+ "U-Proposed-S_DEL-1_SW-dM": true,
+ "U-Proposed-S_DEL-1_SW-body": true,
+ "U-Proposed-S_DEL-1_SW-div": true,
+ "U-Proposed-S_S-U-1_SI-dM": true,
+ "U-Proposed-S_S-U-1_SI-body": true,
+ "U-Proposed-S_S-U-1_SI-div": true,
+ "U-Proposed-S_U-S-1_SI-dM": true,
+ "U-Proposed-S_U-S-1_SI-body": true,
+ "U-Proposed-S_U-S-1_SI-div": true,
+ "U-Proposed-SUB_SPANs:va:sub-1_SW-dM": true,
+ "U-Proposed-SUB_SPANs:va:sub-1_SW-body": true,
+ "U-Proposed-SUB_SPANs:va:sub-1_SW-div": true,
+ "U-Proposed-SUP_SPANs:va:super-1_SW-dM": true,
+ "U-Proposed-SUP_SPANs:va:super-1_SW-body": true,
+ "U-Proposed-SUP_SPANs:va:super-1_SW-div": true,
+ "U-Proposed-UNLINK_A-1_SC-dM": true,
+ "U-Proposed-UNLINK_A-1_SC-body": true,
+ "U-Proposed-UNLINK_A-1_SC-div": true,
+ "U-Proposed-UNLINK_A-1_SI-dM": true,
+ "U-Proposed-UNLINK_A-1_SI-body": true,
+ "U-Proposed-UNLINK_A-1_SI-div": true,
+ "U-Proposed-UNLINK_A-2_SL-dM": true,
+ "U-Proposed-UNLINK_A-2_SL-body": true,
+ "U-Proposed-UNLINK_A-2_SL-div": true,
+ "U-Proposed-UNLINK_A-3_SR-dM": true,
+ "U-Proposed-UNLINK_A-3_SR-body": true,
+ "U-Proposed-UNLINK_A-3_SR-div": true,
+ "U-Proposed-OUTDENT_DIV-1_SW-dM": true,
+ "U-Proposed-OUTDENT_DIV-1_SW-body": true,
+ "U-Proposed-OUTDENT_DIV-1_SW-div": true,
+ "U-Proposed-REMOVEFORMAT_Ahref:url-1_SW-dM": true,
+ "U-Proposed-REMOVEFORMAT_Ahref:url-1_SW-body": true,
+ "U-Proposed-REMOVEFORMAT_Ahref:url-1_SW-div": true,
+ "U-Proposed-REMOVEFORMAT_TABLE-TBODY-TR-TD-1_SW-dM": true,
+ "U-Proposed-REMOVEFORMAT_TABLE-TBODY-TR-TD-1_SW-body": true,
+ "U-Proposed-REMOVEFORMAT_TABLE-TBODY-TR-TD-1_SW-div": true,
+ "U-Proposed-UNBOOKMARK_An:name-1_SW-dM": true,
+ "U-Proposed-UNBOOKMARK_An:name-1_SW-body": true,
+ "U-Proposed-UNBOOKMARK_An:name-1_SW-div": true,
+ "UC-Proposed-S_SPANc:s-1_SW-dM": true,
+ "UC-Proposed-S_SPANc:s-1_SW-body": true,
+ "UC-Proposed-S_SPANc:s-1_SW-div": true,
+ "UC-Proposed-S_SPANc:s-2_SI-dM": true,
+ "UC-Proposed-S_SPANc:s-2_SI-body": true,
+ "UC-Proposed-S_SPANc:s-2_SI-div": true,
+ "D-Proposed-CHAR-3_SC-dM": true,
+ "D-Proposed-CHAR-3_SC-body": true,
+ "D-Proposed-CHAR-3_SC-div": true,
+ "D-Proposed-CHAR-4_SC-dM": true,
+ "D-Proposed-CHAR-4_SC-body": true,
+ "D-Proposed-CHAR-4_SC-div": true,
+ "D-Proposed-CHAR-5_SC-dM": true,
+ "D-Proposed-CHAR-5_SC-body": true,
+ "D-Proposed-CHAR-5_SC-div": true,
+ "D-Proposed-CHAR-5_SI-1-dM": true,
+ "D-Proposed-CHAR-5_SI-1-body": true,
+ "D-Proposed-CHAR-5_SI-1-div": true,
+ "D-Proposed-CHAR-5_SI-2-dM": true,
+ "D-Proposed-CHAR-5_SI-2-body": true,
+ "D-Proposed-CHAR-5_SI-2-div": true,
+ "D-Proposed-CHAR-5_SR-dM": true,
+ "D-Proposed-CHAR-5_SR-body": true,
+ "D-Proposed-CHAR-5_SR-div": true,
+ "D-Proposed-CHAR-6_SC-dM": true,
+ "D-Proposed-CHAR-6_SC-body": true,
+ "D-Proposed-CHAR-6_SC-div": true,
+ "D-Proposed-CHAR-7_SC-dM": true,
+ "D-Proposed-CHAR-7_SC-body": true,
+ "D-Proposed-CHAR-7_SC-div": true,
+ "D-Proposed-B-1_SW-div": true,
+ "D-Proposed-B-1_SL-dM": true,
+ "D-Proposed-B-1_SL-body": true,
+ "D-Proposed-B-1_SL-div": true,
+ "D-Proposed-B-1_SR-dM": true,
+ "D-Proposed-B-1_SR-body": true,
+ "D-Proposed-B-1_SR-div": true,
+ "D-Proposed-B.I-1_SM-dM": true,
+ "D-Proposed-B.I-1_SM-body": true,
+ "D-Proposed-B.I-1_SM-div": true,
+ "D-Proposed-OL-LI2-1_SO1-dM": true,
+ "D-Proposed-OL-LI2-1_SO1-body": true,
+ "D-Proposed-OL-LI2-1_SO1-div": true,
+ "D-Proposed-OL-LI-1_SW-dM": true,
+ "D-Proposed-OL-LI-1_SW-body": true,
+ "D-Proposed-OL-LI-1_SW-div": true,
+ "D-Proposed-OL-LI-1_SO-dM": true,
+ "D-Proposed-OL-LI-1_SO-body": true,
+ "D-Proposed-OL-LI-1_SO-div": true,
+ "D-Proposed-HR.BR-1_SM-dM": true,
+ "D-Proposed-HR.BR-1_SM-body": true,
+ "D-Proposed-HR.BR-1_SM-div": true,
+ "D-Proposed-TR2rs:2-1_SO1-dM": true,
+ "D-Proposed-TR2rs:2-1_SO1-body": true,
+ "D-Proposed-TR2rs:2-1_SO1-div": true,
+ "D-Proposed-TR2rs:2-1_SO2-dM": true,
+ "D-Proposed-TR2rs:2-1_SO2-body": true,
+ "D-Proposed-TR2rs:2-1_SO2-div": true,
+ "D-Proposed-TR3rs:3-1_SO1-dM": true,
+ "D-Proposed-TR3rs:3-1_SO1-body": true,
+ "D-Proposed-TR3rs:3-1_SO1-div": true,
+ "D-Proposed-TR3rs:3-1_SO2-dM": true,
+ "D-Proposed-TR3rs:3-1_SO2-body": true,
+ "D-Proposed-TR3rs:3-1_SO2-div": true,
+ "D-Proposed-TR3rs:3-1_SO3-dM": true,
+ "D-Proposed-TR3rs:3-1_SO3-body": true,
+ "D-Proposed-TR3rs:3-1_SO3-div": true,
+ "D-Proposed-DIV:ce:false-1_SB-dM": true,
+ "D-Proposed-DIV:ce:false-1_SB-body": true,
+ "D-Proposed-DIV:ce:false-1_SB-div": true,
+ "D-Proposed-DIV:ce:false-1_SL-dM": true,
+ "D-Proposed-DIV:ce:false-1_SL-body": true,
+ "D-Proposed-DIV:ce:false-1_SL-div": true,
+ "D-Proposed-DIV:ce:false-1_SR-dM": true,
+ "D-Proposed-DIV:ce:false-1_SR-body": true,
+ "D-Proposed-DIV:ce:false-1_SR-div": true,
+ "D-Proposed-DIV:ce:false-1_SI-dM": true,
+ "D-Proposed-SPAN:d:ib-2_SL-dM": true,
+ "D-Proposed-SPAN:d:ib-2_SL-body": true,
+ "D-Proposed-SPAN:d:ib-2_SL-div": true,
+ "D-Proposed-SPAN:d:ib-3_SR-dM": true,
+ "D-Proposed-SPAN:d:ib-3_SR-body": true,
+ "D-Proposed-SPAN:d:ib-3_SR-div": true,
+ "FD-Proposed-B-1_SW-div": true,
+ "FD-Proposed-OL-LI-1_SW-dM": true,
+ "FD-Proposed-OL-LI-1_SW-body": true,
+ "FD-Proposed-OL-LI-1_SW-div": true,
+ "FD-Proposed-OL-LI-1_SO-dM": true,
+ "FD-Proposed-OL-LI-1_SO-body": true,
+ "FD-Proposed-OL-LI-1_SO-div": true,
+ "FD-Proposed-TABLE-1_SB-dM": true,
+ "FD-Proposed-TABLE-1_SB-body": true,
+ "FD-Proposed-TABLE-1_SB-div": true,
+ "FD-Proposed-TD-1_SE-dM": true,
+ "FD-Proposed-TD-1_SE-body": true,
+ "FD-Proposed-TD-1_SE-div": true,
+ "FD-Proposed-TD2-1_SE1-dM": true,
+ "FD-Proposed-TD2-1_SE1-body": true,
+ "FD-Proposed-TD2-1_SE1-div": true,
+ "FD-Proposed-TD2-1_SM-dM": true,
+ "FD-Proposed-TD2-1_SM-body": true,
+ "FD-Proposed-TD2-1_SM-div": true,
+ "FD-Proposed-TR2rs:2-1_SO1-dM": true,
+ "FD-Proposed-TR2rs:2-1_SO1-body": true,
+ "FD-Proposed-TR2rs:2-1_SO1-div": true,
+ "FD-Proposed-TR2rs:2-1_SO2-dM": true,
+ "FD-Proposed-TR2rs:2-1_SO2-body": true,
+ "FD-Proposed-TR2rs:2-1_SO2-div": true,
+ "FD-Proposed-TR3rs:3-1_SO1-dM": true,
+ "FD-Proposed-TR3rs:3-1_SO1-body": true,
+ "FD-Proposed-TR3rs:3-1_SO1-div": true,
+ "FD-Proposed-TR3rs:3-1_SO2-dM": true,
+ "FD-Proposed-TR3rs:3-1_SO2-body": true,
+ "FD-Proposed-TR3rs:3-1_SO2-div": true,
+ "FD-Proposed-TR3rs:3-1_SO3-dM": true,
+ "FD-Proposed-TR3rs:3-1_SO3-body": true,
+ "FD-Proposed-TR3rs:3-1_SO3-div": true,
+ "FD-Proposed-DIV:ce:false-1_SB-dM": true,
+ "FD-Proposed-DIV:ce:false-1_SB-body": true,
+ "FD-Proposed-DIV:ce:false-1_SB-div": true,
+ "FD-Proposed-DIV:ce:false-1_SL-dM": true,
+ "FD-Proposed-DIV:ce:false-1_SL-body": true,
+ "FD-Proposed-DIV:ce:false-1_SL-div": true,
+ "FD-Proposed-DIV:ce:false-1_SR-dM": true,
+ "FD-Proposed-DIV:ce:false-1_SR-body": true,
+ "FD-Proposed-DIV:ce:false-1_SR-div": true,
+ "FD-Proposed-DIV:ce:false-1_SI-dM": true,
+ "I-Proposed-IHR_TEXT-1_SC-dM": true,
+ "I-Proposed-IHR_TEXT-1_SC-body": true,
+ "I-Proposed-IHR_TEXT-1_SC-div": true,
+ "I-Proposed-IHR_TEXT-1_SI-dM": true,
+ "I-Proposed-IHR_TEXT-1_SI-body": true,
+ "I-Proposed-IHR_TEXT-1_SI-div": true,
+ "I-Proposed-IHR_B-1_SC-dM": true,
+ "I-Proposed-IHR_B-1_SC-body": true,
+ "I-Proposed-IHR_B-1_SC-div": true,
+ "I-Proposed-IHR_B-1_SS-dM": true,
+ "I-Proposed-IHR_B-1_SS-body": true,
+ "I-Proposed-IHR_B-1_SS-div": true,
+ "I-Proposed-IHR_B-I-1_SMR-dM": true,
+ "I-Proposed-IHR_B-I-1_SMR-body": true,
+ "I-Proposed-IHR_B-I-1_SMR-div": true,
+ "I-Proposed-IBR_LI-1_SC-dM": true,
+ "I-Proposed-IBR_LI-1_SC-body": true,
+ "I-Proposed-IBR_LI-1_SC-div": true,
+ "I-Proposed-IIMG:._SPAN-IMG-1_SO-dM": true,
+ "I-Proposed-IIMG:._SPAN-IMG-1_SO-body": true,
+ "I-Proposed-IIMG:._SPAN-IMG-1_SO-div": true,
+ "I-Proposed-IIMG:._IMG-1_SO-dM": true,
+ "I-Proposed-IIMG:._IMG-1_SO-body": true,
+ "I-Proposed-IIMG:._IMG-1_SO-div": true,
+ "I-Proposed-IHTML:BR_TEXT-1_SC-dM": true,
+ "I-Proposed-IHTML:BR_TEXT-1_SC-body": true,
+ "I-Proposed-IHTML:BR_TEXT-1_SC-div": true,
+ "I-Proposed-IHTML:S_TEXT-1_SI-dM": true,
+ "I-Proposed-IHTML:S_TEXT-1_SI-body": true,
+ "I-Proposed-IHTML:S_TEXT-1_SI-div": true,
+ "I-Proposed-IHTML:H1.H2_TEXT-1_SI-dM": true,
+ "I-Proposed-IHTML:H1.H2_TEXT-1_SI-body": true,
+ "I-Proposed-IHTML:H1.H2_TEXT-1_SI-div": true,
+ "I-Proposed-IHTML:P-B_TEXT-1_SI-dM": true,
+ "I-Proposed-IHTML:P-B_TEXT-1_SI-body": true,
+ "I-Proposed-IHTML:P-B_TEXT-1_SI-div": true,
+ "Q-Proposed-SELECTALL_TEXT-1-dM": true,
+ "Q-Proposed-SELECTALL_TEXT-1-body": true,
+ "Q-Proposed-SELECTALL_TEXT-1-div": true,
+ "Q-Proposed-UNSELECT_TEXT-1-dM": true,
+ "Q-Proposed-UNSELECT_TEXT-1-body": true,
+ "Q-Proposed-UNSELECT_TEXT-1-div": true,
+ "Q-Proposed-UNDO_TEXT-1-dM": true,
+ "Q-Proposed-UNDO_TEXT-1-body": true,
+ "Q-Proposed-UNDO_TEXT-1-div": true,
+ "Q-Proposed-REDO_TEXT-1-dM": true,
+ "Q-Proposed-REDO_TEXT-1-body": true,
+ "Q-Proposed-REDO_TEXT-1-div": true,
+ "Q-Proposed-BOLD_TEXT-1-dM": true,
+ "Q-Proposed-BOLD_TEXT-1-body": true,
+ "Q-Proposed-BOLD_TEXT-1-div": true,
+ "Q-Proposed-BOLD_B-dM": true,
+ "Q-Proposed-BOLD_B-body": true,
+ "Q-Proposed-BOLD_B-div": true,
+ "Q-Proposed-ITALIC_TEXT-1-dM": true,
+ "Q-Proposed-ITALIC_TEXT-1-body": true,
+ "Q-Proposed-ITALIC_TEXT-1-div": true,
+ "Q-Proposed-ITALIC_I-dM": true,
+ "Q-Proposed-ITALIC_I-body": true,
+ "Q-Proposed-ITALIC_I-div": true,
+ "Q-Proposed-UNDERLINE_TEXT-1-dM": true,
+ "Q-Proposed-UNDERLINE_TEXT-1-body": true,
+ "Q-Proposed-UNDERLINE_TEXT-1-div": true,
+ "Q-Proposed-STRIKETHROUGH_TEXT-1-dM": true,
+ "Q-Proposed-STRIKETHROUGH_TEXT-1-body": true,
+ "Q-Proposed-STRIKETHROUGH_TEXT-1-div": true,
+ "Q-Proposed-SUBSCRIPT_TEXT-1-dM": true,
+ "Q-Proposed-SUBSCRIPT_TEXT-1-body": true,
+ "Q-Proposed-SUBSCRIPT_TEXT-1-div": true,
+ "Q-Proposed-SUPERSCRIPT_TEXT-1-dM": true,
+ "Q-Proposed-SUPERSCRIPT_TEXT-1-body": true,
+ "Q-Proposed-SUPERSCRIPT_TEXT-1-div": true,
+ "Q-Proposed-FORMATBLOCK_TEXT-1-dM": true,
+ "Q-Proposed-FORMATBLOCK_TEXT-1-body": true,
+ "Q-Proposed-FORMATBLOCK_TEXT-1-div": true,
+ "Q-Proposed-CREATELINK_TEXT-1-dM": true,
+ "Q-Proposed-CREATELINK_TEXT-1-body": true,
+ "Q-Proposed-CREATELINK_TEXT-1-div": true,
+ "Q-Proposed-UNLINK_TEXT-1-dM": true,
+ "Q-Proposed-UNLINK_TEXT-1-body": true,
+ "Q-Proposed-UNLINK_TEXT-1-div": true,
+ "Q-Proposed-INSERTHTML_TEXT-1-dM": true,
+ "Q-Proposed-INSERTHTML_TEXT-1-body": true,
+ "Q-Proposed-INSERTHTML_TEXT-1-div": true,
+ "Q-Proposed-INSERTHORIZONTALRULE_TEXT-1-dM": true,
+ "Q-Proposed-INSERTHORIZONTALRULE_TEXT-1-body": true,
+ "Q-Proposed-INSERTHORIZONTALRULE_TEXT-1-div": true,
+ "Q-Proposed-INSERTIMAGE_TEXT-1-dM": true,
+ "Q-Proposed-INSERTIMAGE_TEXT-1-body": true,
+ "Q-Proposed-INSERTIMAGE_TEXT-1-div": true,
+ "Q-Proposed-INSERTLINEBREAK_TEXT-1-dM": true,
+ "Q-Proposed-INSERTLINEBREAK_TEXT-1-body": true,
+ "Q-Proposed-INSERTLINEBREAK_TEXT-1-div": true,
+ "Q-Proposed-INSERTPARAGRAPH_TEXT-1-dM": true,
+ "Q-Proposed-INSERTPARAGRAPH_TEXT-1-body": true,
+ "Q-Proposed-INSERTPARAGRAPH_TEXT-1-div": true,
+ "Q-Proposed-INSERTORDEREDLIST_TEXT-1-dM": true,
+ "Q-Proposed-INSERTORDEREDLIST_TEXT-1-body": true,
+ "Q-Proposed-INSERTORDEREDLIST_TEXT-1-div": true,
+ "Q-Proposed-INSERTUNORDEREDLIST_TEXT-1-dM": true,
+ "Q-Proposed-INSERTUNORDEREDLIST_TEXT-1-body": true,
+ "Q-Proposed-INSERTUNORDEREDLIST_TEXT-1-div": true,
+ "Q-Proposed-INSERTTEXT_TEXT-1-dM": true,
+ "Q-Proposed-INSERTTEXT_TEXT-1-body": true,
+ "Q-Proposed-INSERTTEXT_TEXT-1-div": true,
+ "Q-Proposed-DELETE_TEXT-1-dM": true,
+ "Q-Proposed-DELETE_TEXT-1-body": true,
+ "Q-Proposed-DELETE_TEXT-1-div": true,
+ "Q-Proposed-FORWARDDELETE_TEXT-1-dM": true,
+ "Q-Proposed-FORWARDDELETE_TEXT-1-body": true,
+ "Q-Proposed-FORWARDDELETE_TEXT-1-div": true,
+ "Q-Proposed-STYLEWITHCSS_TEXT-1-dM": true,
+ "Q-Proposed-STYLEWITHCSS_TEXT-1-body": true,
+ "Q-Proposed-STYLEWITHCSS_TEXT-1-div": true,
+ "Q-Proposed-CONTENTREADONLY_TEXT-1-dM": true,
+ "Q-Proposed-CONTENTREADONLY_TEXT-1-body": true,
+ "Q-Proposed-CONTENTREADONLY_TEXT-1-div": true,
+ "Q-Proposed-BACKCOLOR_TEXT-1-dM": true,
+ "Q-Proposed-BACKCOLOR_TEXT-1-body": true,
+ "Q-Proposed-BACKCOLOR_TEXT-1-div": true,
+ "Q-Proposed-FORECOLOR_TEXT-1-dM": true,
+ "Q-Proposed-FORECOLOR_TEXT-1-body": true,
+ "Q-Proposed-FORECOLOR_TEXT-1-div": true,
+ "Q-Proposed-HILITECOLOR_TEXT-1-dM": true,
+ "Q-Proposed-HILITECOLOR_TEXT-1-body": true,
+ "Q-Proposed-HILITECOLOR_TEXT-1-div": true,
+ "Q-Proposed-FONTNAME_TEXT-1-dM": true,
+ "Q-Proposed-FONTNAME_TEXT-1-body": true,
+ "Q-Proposed-FONTNAME_TEXT-1-div": true,
+ "Q-Proposed-FONTSIZE_TEXT-1-dM": true,
+ "Q-Proposed-FONTSIZE_TEXT-1-body": true,
+ "Q-Proposed-FONTSIZE_TEXT-1-div": true,
+ "Q-Proposed-INCREASEFONTSIZE_TEXT-1-dM": true,
+ "Q-Proposed-INCREASEFONTSIZE_TEXT-1-body": true,
+ "Q-Proposed-INCREASEFONTSIZE_TEXT-1-div": true,
+ "Q-Proposed-DECREASEFONTSIZE_TEXT-1-dM": true,
+ "Q-Proposed-DECREASEFONTSIZE_TEXT-1-body": true,
+ "Q-Proposed-DECREASEFONTSIZE_TEXT-1-div": true,
+ "Q-Proposed-HEADING_TEXT-1-dM": true,
+ "Q-Proposed-HEADING_TEXT-1-body": true,
+ "Q-Proposed-HEADING_TEXT-1-div": true,
+ "Q-Proposed-INDENT_TEXT-1-dM": true,
+ "Q-Proposed-INDENT_TEXT-1-body": true,
+ "Q-Proposed-INDENT_TEXT-1-div": true,
+ "Q-Proposed-OUTDENT_TEXT-1-dM": true,
+ "Q-Proposed-OUTDENT_TEXT-1-body": true,
+ "Q-Proposed-OUTDENT_TEXT-1-div": true,
+ "Q-Proposed-CREATEBOOKMARK_TEXT-1-dM": true,
+ "Q-Proposed-CREATEBOOKMARK_TEXT-1-body": true,
+ "Q-Proposed-CREATEBOOKMARK_TEXT-1-div": true,
+ "Q-Proposed-UNBOOKMARK_TEXT-1-dM": true,
+ "Q-Proposed-UNBOOKMARK_TEXT-1-body": true,
+ "Q-Proposed-UNBOOKMARK_TEXT-1-div": true,
+ "Q-Proposed-JUSTIFYCENTER_TEXT-1-dM": true,
+ "Q-Proposed-JUSTIFYCENTER_TEXT-1-body": true,
+ "Q-Proposed-JUSTIFYCENTER_TEXT-1-div": true,
+ "Q-Proposed-JUSTIFYFULL_TEXT-1-dM": true,
+ "Q-Proposed-JUSTIFYFULL_TEXT-1-body": true,
+ "Q-Proposed-JUSTIFYFULL_TEXT-1-div": true,
+ "Q-Proposed-JUSTIFYLEFT_TEXT-1-dM": true,
+ "Q-Proposed-JUSTIFYLEFT_TEXT-1-body": true,
+ "Q-Proposed-JUSTIFYLEFT_TEXT-1-div": true,
+ "Q-Proposed-JUSTIFYRIGHT_TEXT-1-dM": true,
+ "Q-Proposed-JUSTIFYRIGHT_TEXT-1-body": true,
+ "Q-Proposed-JUSTIFYRIGHT_TEXT-1-div": true,
+ "Q-Proposed-REMOVEFORMAT_TEXT-1-dM": true,
+ "Q-Proposed-REMOVEFORMAT_TEXT-1-body": true,
+ "Q-Proposed-REMOVEFORMAT_TEXT-1-div": true,
+ "Q-Proposed-COPY_TEXT-1-dM": true,
+ "Q-Proposed-COPY_TEXT-1-body": true,
+ "Q-Proposed-COPY_TEXT-1-div": true,
+ "Q-Proposed-CUT_TEXT-1-dM": true,
+ "Q-Proposed-CUT_TEXT-1-body": true,
+ "Q-Proposed-CUT_TEXT-1-div": true,
+ "Q-Proposed-PASTE_TEXT-1-dM": true,
+ "Q-Proposed-PASTE_TEXT-1-body": true,
+ "Q-Proposed-PASTE_TEXT-1-div": true,
+ "Q-Proposed-garbage-1_TEXT-1-dM": true,
+ "Q-Proposed-garbage-1_TEXT-1-body": true,
+ "Q-Proposed-garbage-1_TEXT-1-div": true,
+ "QE-Proposed-SELECTALL_TEXT-1-dM": true,
+ "QE-Proposed-SELECTALL_TEXT-1-body": true,
+ "QE-Proposed-SELECTALL_TEXT-1-div": true,
+ "QE-Proposed-UNSELECT_TEXT-1-dM": true,
+ "QE-Proposed-UNSELECT_TEXT-1-body": true,
+ "QE-Proposed-UNSELECT_TEXT-1-div": true,
+ "QE-Proposed-UNDO_TEXT-1-dM": true,
+ "QE-Proposed-UNDO_TEXT-1-body": true,
+ "QE-Proposed-UNDO_TEXT-1-div": true,
+ "QE-Proposed-REDO_TEXT-1-dM": true,
+ "QE-Proposed-REDO_TEXT-1-body": true,
+ "QE-Proposed-REDO_TEXT-1-div": true,
+ "QE-Proposed-BOLD_TEXT-1-dM": true,
+ "QE-Proposed-BOLD_TEXT-1-body": true,
+ "QE-Proposed-BOLD_TEXT-1-div": true,
+ "QE-Proposed-ITALIC_TEXT-1-dM": true,
+ "QE-Proposed-ITALIC_TEXT-1-body": true,
+ "QE-Proposed-ITALIC_TEXT-1-div": true,
+ "QE-Proposed-UNDERLINE_TEXT-1-dM": true,
+ "QE-Proposed-UNDERLINE_TEXT-1-body": true,
+ "QE-Proposed-UNDERLINE_TEXT-1-div": true,
+ "QE-Proposed-STRIKETHROUGH_TEXT-1-dM": true,
+ "QE-Proposed-STRIKETHROUGH_TEXT-1-body": true,
+ "QE-Proposed-STRIKETHROUGH_TEXT-1-div": true,
+ "QE-Proposed-SUBSCRIPT_TEXT-1-dM": true,
+ "QE-Proposed-SUBSCRIPT_TEXT-1-body": true,
+ "QE-Proposed-SUBSCRIPT_TEXT-1-div": true,
+ "QE-Proposed-SUPERSCRIPT_TEXT-1-dM": true,
+ "QE-Proposed-SUPERSCRIPT_TEXT-1-body": true,
+ "QE-Proposed-SUPERSCRIPT_TEXT-1-div": true,
+ "QE-Proposed-FORMATBLOCK_TEXT-1-dM": true,
+ "QE-Proposed-FORMATBLOCK_TEXT-1-body": true,
+ "QE-Proposed-FORMATBLOCK_TEXT-1-div": true,
+ "QE-Proposed-CREATELINK_TEXT-1-dM": true,
+ "QE-Proposed-CREATELINK_TEXT-1-body": true,
+ "QE-Proposed-CREATELINK_TEXT-1-div": true,
+ "QE-Proposed-UNLINK_TEXT-1-dM": true,
+ "QE-Proposed-UNLINK_TEXT-1-body": true,
+ "QE-Proposed-UNLINK_TEXT-1-div": true,
+ "QE-Proposed-INSERTHTML_TEXT-1-dM": true,
+ "QE-Proposed-INSERTHTML_TEXT-1-body": true,
+ "QE-Proposed-INSERTHTML_TEXT-1-div": true,
+ "QE-Proposed-INSERTHORIZONTALRULE_TEXT-1-dM": true,
+ "QE-Proposed-INSERTHORIZONTALRULE_TEXT-1-body": true,
+ "QE-Proposed-INSERTHORIZONTALRULE_TEXT-1-div": true,
+ "QE-Proposed-INSERTIMAGE_TEXT-1-dM": true,
+ "QE-Proposed-INSERTIMAGE_TEXT-1-body": true,
+ "QE-Proposed-INSERTIMAGE_TEXT-1-div": true,
+ "QE-Proposed-INSERTLINEBREAK_TEXT-1-dM": true,
+ "QE-Proposed-INSERTLINEBREAK_TEXT-1-body": true,
+ "QE-Proposed-INSERTLINEBREAK_TEXT-1-div": true,
+ "QE-Proposed-INSERTPARAGRAPH_TEXT-1-dM": true,
+ "QE-Proposed-INSERTPARAGRAPH_TEXT-1-body": true,
+ "QE-Proposed-INSERTPARAGRAPH_TEXT-1-div": true,
+ "QE-Proposed-INSERTORDEREDLIST_TEXT-1-dM": true,
+ "QE-Proposed-INSERTORDEREDLIST_TEXT-1-body": true,
+ "QE-Proposed-INSERTORDEREDLIST_TEXT-1-div": true,
+ "QE-Proposed-INSERTUNORDEREDLIST_TEXT-1-dM": true,
+ "QE-Proposed-INSERTUNORDEREDLIST_TEXT-1-body": true,
+ "QE-Proposed-INSERTUNORDEREDLIST_TEXT-1-div": true,
+ "QE-Proposed-INSERTTEXT_TEXT-1-dM": true,
+ "QE-Proposed-INSERTTEXT_TEXT-1-body": true,
+ "QE-Proposed-INSERTTEXT_TEXT-1-div": true,
+ "QE-Proposed-DELETE_TEXT-1-dM": true,
+ "QE-Proposed-DELETE_TEXT-1-body": true,
+ "QE-Proposed-DELETE_TEXT-1-div": true,
+ "QE-Proposed-FORWARDDELETE_TEXT-1-dM": true,
+ "QE-Proposed-FORWARDDELETE_TEXT-1-body": true,
+ "QE-Proposed-FORWARDDELETE_TEXT-1-div": true,
+ "QE-Proposed-STYLEWITHCSS_TEXT-1-dM": true,
+ "QE-Proposed-STYLEWITHCSS_TEXT-1-body": true,
+ "QE-Proposed-STYLEWITHCSS_TEXT-1-div": true,
+ "QE-Proposed-CONTENTREADONLY_TEXT-1-dM": true,
+ "QE-Proposed-CONTENTREADONLY_TEXT-1-body": true,
+ "QE-Proposed-CONTENTREADONLY_TEXT-1-div": true,
+ "QE-Proposed-BACKCOLOR_TEXT-1-dM": true,
+ "QE-Proposed-BACKCOLOR_TEXT-1-body": true,
+ "QE-Proposed-BACKCOLOR_TEXT-1-div": true,
+ "QE-Proposed-FORECOLOR_TEXT-1-dM": true,
+ "QE-Proposed-FORECOLOR_TEXT-1-body": true,
+ "QE-Proposed-FORECOLOR_TEXT-1-div": true,
+ "QE-Proposed-HILITECOLOR_TEXT-1-dM": true,
+ "QE-Proposed-HILITECOLOR_TEXT-1-body": true,
+ "QE-Proposed-HILITECOLOR_TEXT-1-div": true,
+ "QE-Proposed-FONTNAME_TEXT-1-dM": true,
+ "QE-Proposed-FONTNAME_TEXT-1-body": true,
+ "QE-Proposed-FONTNAME_TEXT-1-div": true,
+ "QE-Proposed-FONTSIZE_TEXT-1-dM": true,
+ "QE-Proposed-FONTSIZE_TEXT-1-body": true,
+ "QE-Proposed-FONTSIZE_TEXT-1-div": true,
+ "QE-Proposed-INCREASEFONTSIZE_TEXT-1-dM": true,
+ "QE-Proposed-INCREASEFONTSIZE_TEXT-1-body": true,
+ "QE-Proposed-INCREASEFONTSIZE_TEXT-1-div": true,
+ "QE-Proposed-DECREASEFONTSIZE_TEXT-1-dM": true,
+ "QE-Proposed-DECREASEFONTSIZE_TEXT-1-body": true,
+ "QE-Proposed-DECREASEFONTSIZE_TEXT-1-div": true,
+ "QE-Proposed-HEADING_TEXT-1-dM": true,
+ "QE-Proposed-HEADING_TEXT-1-body": true,
+ "QE-Proposed-HEADING_TEXT-1-div": true,
+ "QE-Proposed-INDENT_TEXT-1-dM": true,
+ "QE-Proposed-INDENT_TEXT-1-body": true,
+ "QE-Proposed-INDENT_TEXT-1-div": true,
+ "QE-Proposed-OUTDENT_TEXT-1-dM": true,
+ "QE-Proposed-OUTDENT_TEXT-1-body": true,
+ "QE-Proposed-OUTDENT_TEXT-1-div": true,
+ "QE-Proposed-CREATEBOOKMARK_TEXT-1-dM": true,
+ "QE-Proposed-CREATEBOOKMARK_TEXT-1-body": true,
+ "QE-Proposed-CREATEBOOKMARK_TEXT-1-div": true,
+ "QE-Proposed-UNBOOKMARK_TEXT-1-dM": true,
+ "QE-Proposed-UNBOOKMARK_TEXT-1-body": true,
+ "QE-Proposed-UNBOOKMARK_TEXT-1-div": true,
+ "QE-Proposed-JUSTIFYCENTER_TEXT-1-dM": true,
+ "QE-Proposed-JUSTIFYCENTER_TEXT-1-body": true,
+ "QE-Proposed-JUSTIFYCENTER_TEXT-1-div": true,
+ "QE-Proposed-JUSTIFYFULL_TEXT-1-dM": true,
+ "QE-Proposed-JUSTIFYFULL_TEXT-1-body": true,
+ "QE-Proposed-JUSTIFYFULL_TEXT-1-div": true,
+ "QE-Proposed-JUSTIFYLEFT_TEXT-1-dM": true,
+ "QE-Proposed-JUSTIFYLEFT_TEXT-1-body": true,
+ "QE-Proposed-JUSTIFYLEFT_TEXT-1-div": true,
+ "QE-Proposed-JUSTIFYRIGHT_TEXT-1-dM": true,
+ "QE-Proposed-JUSTIFYRIGHT_TEXT-1-body": true,
+ "QE-Proposed-JUSTIFYRIGHT_TEXT-1-div": true,
+ "QE-Proposed-REMOVEFORMAT_TEXT-1-dM": true,
+ "QE-Proposed-REMOVEFORMAT_TEXT-1-body": true,
+ "QE-Proposed-REMOVEFORMAT_TEXT-1-div": true,
+ "QE-Proposed-COPY_TEXT-1-dM": true,
+ "QE-Proposed-COPY_TEXT-1-body": true,
+ "QE-Proposed-COPY_TEXT-1-div": true,
+ "QE-Proposed-CUT_TEXT-1-dM": true,
+ "QE-Proposed-CUT_TEXT-1-body": true,
+ "QE-Proposed-CUT_TEXT-1-div": true,
+ "QE-Proposed-PASTE_TEXT-1-dM": true,
+ "QE-Proposed-PASTE_TEXT-1-body": true,
+ "QE-Proposed-PASTE_TEXT-1-div": true,
+ "QE-Proposed-garbage-1_TEXT-1-dM": true,
+ "QE-Proposed-garbage-1_TEXT-1-body": true,
+ "QE-Proposed-garbage-1_TEXT-1-div": true,
+ "QI-Proposed-SELECTALL_TEXT-1-dM": true,
+ "QI-Proposed-SELECTALL_TEXT-1-body": true,
+ "QI-Proposed-SELECTALL_TEXT-1-div": true,
+ "QI-Proposed-UNSELECT_TEXT-1-dM": true,
+ "QI-Proposed-UNSELECT_TEXT-1-body": true,
+ "QI-Proposed-UNSELECT_TEXT-1-div": true,
+ "QI-Proposed-UNDO_TEXT-1-dM": true,
+ "QI-Proposed-UNDO_TEXT-1-body": true,
+ "QI-Proposed-UNDO_TEXT-1-div": true,
+ "QI-Proposed-REDO_TEXT-1-dM": true,
+ "QI-Proposed-REDO_TEXT-1-body": true,
+ "QI-Proposed-REDO_TEXT-1-div": true,
+ "QI-Proposed-BOLD_TEXT-1-dM": true,
+ "QI-Proposed-BOLD_TEXT-1-body": true,
+ "QI-Proposed-BOLD_TEXT-1-div": true,
+ "QI-Proposed-ITALIC_TEXT-1-dM": true,
+ "QI-Proposed-ITALIC_TEXT-1-body": true,
+ "QI-Proposed-ITALIC_TEXT-1-div": true,
+ "QI-Proposed-UNDERLINE_TEXT-1-dM": true,
+ "QI-Proposed-UNDERLINE_TEXT-1-body": true,
+ "QI-Proposed-UNDERLINE_TEXT-1-div": true,
+ "QI-Proposed-STRIKETHROUGH_TEXT-1-dM": true,
+ "QI-Proposed-STRIKETHROUGH_TEXT-1-body": true,
+ "QI-Proposed-STRIKETHROUGH_TEXT-1-div": true,
+ "QI-Proposed-SUBSCRIPT_TEXT-1-dM": true,
+ "QI-Proposed-SUBSCRIPT_TEXT-1-body": true,
+ "QI-Proposed-SUBSCRIPT_TEXT-1-div": true,
+ "QI-Proposed-SUPERSCRIPT_TEXT-1-dM": true,
+ "QI-Proposed-SUPERSCRIPT_TEXT-1-body": true,
+ "QI-Proposed-SUPERSCRIPT_TEXT-1-div": true,
+ "QI-Proposed-FORMATBLOCK_TEXT-1-dM": true,
+ "QI-Proposed-FORMATBLOCK_TEXT-1-body": true,
+ "QI-Proposed-FORMATBLOCK_TEXT-1-div": true,
+ "QI-Proposed-CREATELINK_TEXT-1-dM": true,
+ "QI-Proposed-CREATELINK_TEXT-1-body": true,
+ "QI-Proposed-CREATELINK_TEXT-1-div": true,
+ "QI-Proposed-UNLINK_TEXT-1-dM": true,
+ "QI-Proposed-UNLINK_TEXT-1-body": true,
+ "QI-Proposed-UNLINK_TEXT-1-div": true,
+ "QI-Proposed-INSERTHTML_TEXT-1-dM": true,
+ "QI-Proposed-INSERTHTML_TEXT-1-body": true,
+ "QI-Proposed-INSERTHTML_TEXT-1-div": true,
+ "QI-Proposed-INSERTHORIZONTALRULE_TEXT-1-dM": true,
+ "QI-Proposed-INSERTHORIZONTALRULE_TEXT-1-body": true,
+ "QI-Proposed-INSERTHORIZONTALRULE_TEXT-1-div": true,
+ "QI-Proposed-INSERTIMAGE_TEXT-1-dM": true,
+ "QI-Proposed-INSERTIMAGE_TEXT-1-body": true,
+ "QI-Proposed-INSERTIMAGE_TEXT-1-div": true,
+ "QI-Proposed-INSERTLINEBREAK_TEXT-1-dM": true,
+ "QI-Proposed-INSERTLINEBREAK_TEXT-1-body": true,
+ "QI-Proposed-INSERTLINEBREAK_TEXT-1-div": true,
+ "QI-Proposed-INSERTPARAGRAPH_TEXT-1-dM": true,
+ "QI-Proposed-INSERTPARAGRAPH_TEXT-1-body": true,
+ "QI-Proposed-INSERTPARAGRAPH_TEXT-1-div": true,
+ "QI-Proposed-INSERTORDEREDLIST_TEXT-1-dM": true,
+ "QI-Proposed-INSERTORDEREDLIST_TEXT-1-body": true,
+ "QI-Proposed-INSERTORDEREDLIST_TEXT-1-div": true,
+ "QI-Proposed-INSERTUNORDEREDLIST_TEXT-1-dM": true,
+ "QI-Proposed-INSERTUNORDEREDLIST_TEXT-1-body": true,
+ "QI-Proposed-INSERTUNORDEREDLIST_TEXT-1-div": true,
+ "QI-Proposed-INSERTTEXT_TEXT-1-dM": true,
+ "QI-Proposed-INSERTTEXT_TEXT-1-body": true,
+ "QI-Proposed-INSERTTEXT_TEXT-1-div": true,
+ "QI-Proposed-DELETE_TEXT-1-dM": true,
+ "QI-Proposed-DELETE_TEXT-1-body": true,
+ "QI-Proposed-DELETE_TEXT-1-div": true,
+ "QI-Proposed-FORWARDDELETE_TEXT-1-dM": true,
+ "QI-Proposed-FORWARDDELETE_TEXT-1-body": true,
+ "QI-Proposed-FORWARDDELETE_TEXT-1-div": true,
+ "QI-Proposed-STYLEWITHCSS_TEXT-1-dM": true,
+ "QI-Proposed-STYLEWITHCSS_TEXT-1-body": true,
+ "QI-Proposed-STYLEWITHCSS_TEXT-1-div": true,
+ "QI-Proposed-CONTENTREADONLY_TEXT-1-dM": true,
+ "QI-Proposed-CONTENTREADONLY_TEXT-1-body": true,
+ "QI-Proposed-CONTENTREADONLY_TEXT-1-div": true,
+ "QI-Proposed-BACKCOLOR_TEXT-1-dM": true,
+ "QI-Proposed-BACKCOLOR_TEXT-1-body": true,
+ "QI-Proposed-BACKCOLOR_TEXT-1-div": true,
+ "QI-Proposed-FORECOLOR_TEXT-1-dM": true,
+ "QI-Proposed-FORECOLOR_TEXT-1-body": true,
+ "QI-Proposed-FORECOLOR_TEXT-1-div": true,
+ "QI-Proposed-HILITECOLOR_TEXT-1-dM": true,
+ "QI-Proposed-HILITECOLOR_TEXT-1-body": true,
+ "QI-Proposed-HILITECOLOR_TEXT-1-div": true,
+ "QI-Proposed-FONTNAME_TEXT-1-dM": true,
+ "QI-Proposed-FONTNAME_TEXT-1-body": true,
+ "QI-Proposed-FONTNAME_TEXT-1-div": true,
+ "QI-Proposed-FONTSIZE_TEXT-1-dM": true,
+ "QI-Proposed-FONTSIZE_TEXT-1-body": true,
+ "QI-Proposed-FONTSIZE_TEXT-1-div": true,
+ "QI-Proposed-INCREASEFONTSIZE_TEXT-1-dM": true,
+ "QI-Proposed-INCREASEFONTSIZE_TEXT-1-body": true,
+ "QI-Proposed-INCREASEFONTSIZE_TEXT-1-div": true,
+ "QI-Proposed-DECREASEFONTSIZE_TEXT-1-dM": true,
+ "QI-Proposed-DECREASEFONTSIZE_TEXT-1-body": true,
+ "QI-Proposed-DECREASEFONTSIZE_TEXT-1-div": true,
+ "QI-Proposed-HEADING_TEXT-1-dM": true,
+ "QI-Proposed-HEADING_TEXT-1-body": true,
+ "QI-Proposed-HEADING_TEXT-1-div": true,
+ "QI-Proposed-INDENT_TEXT-1-dM": true,
+ "QI-Proposed-INDENT_TEXT-1-body": true,
+ "QI-Proposed-INDENT_TEXT-1-div": true,
+ "QI-Proposed-OUTDENT_TEXT-1-dM": true,
+ "QI-Proposed-OUTDENT_TEXT-1-body": true,
+ "QI-Proposed-OUTDENT_TEXT-1-div": true,
+ "QI-Proposed-CREATEBOOKMARK_TEXT-1-dM": true,
+ "QI-Proposed-CREATEBOOKMARK_TEXT-1-body": true,
+ "QI-Proposed-CREATEBOOKMARK_TEXT-1-div": true,
+ "QI-Proposed-UNBOOKMARK_TEXT-1-dM": true,
+ "QI-Proposed-UNBOOKMARK_TEXT-1-body": true,
+ "QI-Proposed-UNBOOKMARK_TEXT-1-div": true,
+ "QI-Proposed-JUSTIFYCENTER_TEXT-1-dM": true,
+ "QI-Proposed-JUSTIFYCENTER_TEXT-1-body": true,
+ "QI-Proposed-JUSTIFYCENTER_TEXT-1-div": true,
+ "QI-Proposed-JUSTIFYFULL_TEXT-1-dM": true,
+ "QI-Proposed-JUSTIFYFULL_TEXT-1-body": true,
+ "QI-Proposed-JUSTIFYFULL_TEXT-1-div": true,
+ "QI-Proposed-JUSTIFYLEFT_TEXT-1-dM": true,
+ "QI-Proposed-JUSTIFYLEFT_TEXT-1-body": true,
+ "QI-Proposed-JUSTIFYLEFT_TEXT-1-div": true,
+ "QI-Proposed-JUSTIFYRIGHT_TEXT-1-dM": true,
+ "QI-Proposed-JUSTIFYRIGHT_TEXT-1-body": true,
+ "QI-Proposed-JUSTIFYRIGHT_TEXT-1-div": true,
+ "QI-Proposed-REMOVEFORMAT_TEXT-1-dM": true,
+ "QI-Proposed-REMOVEFORMAT_TEXT-1-body": true,
+ "QI-Proposed-REMOVEFORMAT_TEXT-1-div": true,
+ "QI-Proposed-COPY_TEXT-1-dM": true,
+ "QI-Proposed-COPY_TEXT-1-body": true,
+ "QI-Proposed-COPY_TEXT-1-div": true,
+ "QI-Proposed-CUT_TEXT-1-dM": true,
+ "QI-Proposed-CUT_TEXT-1-body": true,
+ "QI-Proposed-CUT_TEXT-1-div": true,
+ "QI-Proposed-PASTE_TEXT-1-dM": true,
+ "QI-Proposed-PASTE_TEXT-1-body": true,
+ "QI-Proposed-PASTE_TEXT-1-div": true,
+ "QI-Proposed-garbage-1_TEXT-1-dM": true,
+ "QI-Proposed-garbage-1_TEXT-1-body": true,
+ "QI-Proposed-garbage-1_TEXT-1-div": true,
+ "QS-Proposed-B_TEXT_SI-dM": true,
+ "QS-Proposed-B_TEXT_SI-body": true,
+ "QS-Proposed-B_TEXT_SI-div": true,
+ "QS-Proposed-B_B-1_SI-dM": true,
+ "QS-Proposed-B_B-1_SI-body": true,
+ "QS-Proposed-B_B-1_SI-div": true,
+ "QS-Proposed-B_STRONG-1_SI-dM": true,
+ "QS-Proposed-B_STRONG-1_SI-body": true,
+ "QS-Proposed-B_STRONG-1_SI-div": true,
+ "QS-Proposed-B_SPANs:fw:b-1_SI-dM": true,
+ "QS-Proposed-B_SPANs:fw:b-1_SI-body": true,
+ "QS-Proposed-B_SPANs:fw:b-1_SI-div": true,
+ "QS-Proposed-B_SPANs:fw:n-1_SI-dM": true,
+ "QS-Proposed-B_SPANs:fw:n-1_SI-body": true,
+ "QS-Proposed-B_SPANs:fw:n-1_SI-div": true,
+ "QS-Proposed-B_Bs:fw:n-1_SI-dM": true,
+ "QS-Proposed-B_Bs:fw:n-1_SI-body": true,
+ "QS-Proposed-B_Bs:fw:n-1_SI-div": true,
+ "QS-Proposed-B_B-SPANs:fw:n-1_SI-dM": true,
+ "QS-Proposed-B_B-SPANs:fw:n-1_SI-body": true,
+ "QS-Proposed-B_B-SPANs:fw:n-1_SI-div": true,
+ "QS-Proposed-B_SPAN.b-1-SI-dM": true,
+ "QS-Proposed-B_SPAN.b-1-SI-body": true,
+ "QS-Proposed-B_SPAN.b-1-SI-div": true,
+ "QS-Proposed-B_MYB-1-SI-dM": true,
+ "QS-Proposed-B_MYB-1-SI-body": true,
+ "QS-Proposed-B_MYB-1-SI-div": true,
+ "QS-Proposed-B_B-I-1_SC-dM": true,
+ "QS-Proposed-B_B-I-1_SC-body": true,
+ "QS-Proposed-B_B-I-1_SC-div": true,
+ "QS-Proposed-B_B-I-1_SL-dM": true,
+ "QS-Proposed-B_B-I-1_SL-body": true,
+ "QS-Proposed-B_B-I-1_SL-div": true,
+ "QS-Proposed-B_B-I-1_SR-dM": true,
+ "QS-Proposed-B_B-I-1_SR-body": true,
+ "QS-Proposed-B_B-I-1_SR-div": true,
+ "QS-Proposed-B_STRONG-I-1_SC-dM": true,
+ "QS-Proposed-B_STRONG-I-1_SC-body": true,
+ "QS-Proposed-B_STRONG-I-1_SC-div": true,
+ "QS-Proposed-B_B-I-U-1_SC-dM": true,
+ "QS-Proposed-B_B-I-U-1_SC-body": true,
+ "QS-Proposed-B_B-I-U-1_SC-div": true,
+ "QS-Proposed-B_B-I-U-1_SM-dM": true,
+ "QS-Proposed-B_B-I-U-1_SM-body": true,
+ "QS-Proposed-B_B-I-U-1_SM-div": true,
+ "QS-Proposed-B_TEXT-B-1_SO-1-dM": true,
+ "QS-Proposed-B_TEXT-B-1_SO-1-body": true,
+ "QS-Proposed-B_TEXT-B-1_SO-1-div": true,
+ "QS-Proposed-B_TEXT-B-1_SO-2-dM": true,
+ "QS-Proposed-B_TEXT-B-1_SO-2-body": true,
+ "QS-Proposed-B_TEXT-B-1_SO-2-div": true,
+ "QS-Proposed-B_TEXT-B-1_SL-dM": true,
+ "QS-Proposed-B_TEXT-B-1_SL-body": true,
+ "QS-Proposed-B_TEXT-B-1_SL-div": true,
+ "QS-Proposed-B_TEXT-B-1_SR-dM": true,
+ "QS-Proposed-B_TEXT-B-1_SR-body": true,
+ "QS-Proposed-B_TEXT-B-1_SR-div": true,
+ "QS-Proposed-B_TEXT-B-1_SO-3-dM": true,
+ "QS-Proposed-B_TEXT-B-1_SO-3-body": true,
+ "QS-Proposed-B_TEXT-B-1_SO-3-div": true,
+ "QS-Proposed-B_B.TEXT.B-1_SM-dM": true,
+ "QS-Proposed-B_B.TEXT.B-1_SM-body": true,
+ "QS-Proposed-B_B.TEXT.B-1_SM-div": true,
+ "QS-Proposed-B_B.B.B-1_SM-dM": true,
+ "QS-Proposed-B_B.B.B-1_SM-body": true,
+ "QS-Proposed-B_B.B.B-1_SM-div": true,
+ "QS-Proposed-B_B.STRONG.B-1_SM-dM": true,
+ "QS-Proposed-B_B.STRONG.B-1_SM-body": true,
+ "QS-Proposed-B_B.STRONG.B-1_SM-div": true,
+ "QS-Proposed-B_SPAN.b.MYB.SPANs:fw:b-1_SM-dM": true,
+ "QS-Proposed-B_SPAN.b.MYB.SPANs:fw:b-1_SM-body": true,
+ "QS-Proposed-B_SPAN.b.MYB.SPANs:fw:b-1_SM-div": true,
+ "QS-Proposed-I_TEXT_SI-dM": true,
+ "QS-Proposed-I_TEXT_SI-body": true,
+ "QS-Proposed-I_TEXT_SI-div": true,
+ "QS-Proposed-I_I-1_SI-dM": true,
+ "QS-Proposed-I_I-1_SI-body": true,
+ "QS-Proposed-I_I-1_SI-div": true,
+ "QS-Proposed-I_EM-1_SI-dM": true,
+ "QS-Proposed-I_EM-1_SI-body": true,
+ "QS-Proposed-I_EM-1_SI-div": true,
+ "QS-Proposed-I_SPANs:fs:i-1_SI-dM": true,
+ "QS-Proposed-I_SPANs:fs:i-1_SI-body": true,
+ "QS-Proposed-I_SPANs:fs:i-1_SI-div": true,
+ "QS-Proposed-I_SPANs:fs:n-1_SI-dM": true,
+ "QS-Proposed-I_SPANs:fs:n-1_SI-body": true,
+ "QS-Proposed-I_SPANs:fs:n-1_SI-div": true,
+ "QS-Proposed-I_I-SPANs:fs:n-1_SI-dM": true,
+ "QS-Proposed-I_I-SPANs:fs:n-1_SI-body": true,
+ "QS-Proposed-I_I-SPANs:fs:n-1_SI-div": true,
+ "QS-Proposed-I_SPAN.i-1-SI-dM": true,
+ "QS-Proposed-I_SPAN.i-1-SI-body": true,
+ "QS-Proposed-I_SPAN.i-1-SI-div": true,
+ "QS-Proposed-I_MYI-1-SI-dM": true,
+ "QS-Proposed-I_MYI-1-SI-body": true,
+ "QS-Proposed-I_MYI-1-SI-div": true,
+ "QS-Proposed-U_TEXT_SI-dM": true,
+ "QS-Proposed-U_TEXT_SI-body": true,
+ "QS-Proposed-U_TEXT_SI-div": true,
+ "QS-Proposed-U_U-1_SI-dM": true,
+ "QS-Proposed-U_U-1_SI-body": true,
+ "QS-Proposed-U_U-1_SI-div": true,
+ "QS-Proposed-U_Us:td:n-1_SI-dM": true,
+ "QS-Proposed-U_Us:td:n-1_SI-body": true,
+ "QS-Proposed-U_Us:td:n-1_SI-div": true,
+ "QS-Proposed-U_Ah:url-1_SI-dM": true,
+ "QS-Proposed-U_Ah:url-1_SI-body": true,
+ "QS-Proposed-U_Ah:url-1_SI-div": true,
+ "QS-Proposed-U_Ah:url.s:td:n-1_SI-dM": true,
+ "QS-Proposed-U_Ah:url.s:td:n-1_SI-body": true,
+ "QS-Proposed-U_Ah:url.s:td:n-1_SI-div": true,
+ "QS-Proposed-U_SPANs:td:u-1_SI-dM": true,
+ "QS-Proposed-U_SPANs:td:u-1_SI-body": true,
+ "QS-Proposed-U_SPANs:td:u-1_SI-div": true,
+ "QS-Proposed-U_SPAN.u-1-SI-dM": true,
+ "QS-Proposed-U_SPAN.u-1-SI-body": true,
+ "QS-Proposed-U_SPAN.u-1-SI-div": true,
+ "QS-Proposed-U_MYU-1-SI-dM": true,
+ "QS-Proposed-U_MYU-1-SI-body": true,
+ "QS-Proposed-U_MYU-1-SI-div": true,
+ "QS-Proposed-S_TEXT_SI-dM": true,
+ "QS-Proposed-S_TEXT_SI-body": true,
+ "QS-Proposed-S_TEXT_SI-div": true,
+ "QS-Proposed-S_S-1_SI-dM": true,
+ "QS-Proposed-S_S-1_SI-body": true,
+ "QS-Proposed-S_S-1_SI-div": true,
+ "QS-Proposed-S_STRIKE-1_SI-dM": true,
+ "QS-Proposed-S_STRIKE-1_SI-body": true,
+ "QS-Proposed-S_STRIKE-1_SI-div": true,
+ "QS-Proposed-S_STRIKEs:td:n-1_SI-dM": true,
+ "QS-Proposed-S_STRIKEs:td:n-1_SI-body": true,
+ "QS-Proposed-S_STRIKEs:td:n-1_SI-div": true,
+ "QS-Proposed-S_DEL-1_SI-dM": true,
+ "QS-Proposed-S_DEL-1_SI-body": true,
+ "QS-Proposed-S_DEL-1_SI-div": true,
+ "QS-Proposed-S_SPANs:td:lt-1_SI-dM": true,
+ "QS-Proposed-S_SPANs:td:lt-1_SI-body": true,
+ "QS-Proposed-S_SPANs:td:lt-1_SI-div": true,
+ "QS-Proposed-S_SPAN.s-1-SI-dM": true,
+ "QS-Proposed-S_SPAN.s-1-SI-body": true,
+ "QS-Proposed-S_SPAN.s-1-SI-div": true,
+ "QS-Proposed-S_MYS-1-SI-dM": true,
+ "QS-Proposed-S_MYS-1-SI-body": true,
+ "QS-Proposed-S_MYS-1-SI-div": true,
+ "QS-Proposed-S_S.STRIKE.DEL-1_SM-dM": true,
+ "QS-Proposed-S_S.STRIKE.DEL-1_SM-body": true,
+ "QS-Proposed-S_S.STRIKE.DEL-1_SM-div": true,
+ "QS-Proposed-SUB_TEXT_SI-dM": true,
+ "QS-Proposed-SUB_TEXT_SI-body": true,
+ "QS-Proposed-SUB_TEXT_SI-div": true,
+ "QS-Proposed-SUB_SUB-1_SI-dM": true,
+ "QS-Proposed-SUB_SUB-1_SI-body": true,
+ "QS-Proposed-SUB_SUB-1_SI-div": true,
+ "QS-Proposed-SUB_SPAN.sub-1-SI-dM": true,
+ "QS-Proposed-SUB_SPAN.sub-1-SI-body": true,
+ "QS-Proposed-SUB_SPAN.sub-1-SI-div": true,
+ "QS-Proposed-SUB_MYSUB-1-SI-dM": true,
+ "QS-Proposed-SUB_MYSUB-1-SI-body": true,
+ "QS-Proposed-SUB_MYSUB-1-SI-div": true,
+ "QS-Proposed-SUP_TEXT_SI-dM": true,
+ "QS-Proposed-SUP_TEXT_SI-body": true,
+ "QS-Proposed-SUP_TEXT_SI-div": true,
+ "QS-Proposed-SUP_SUP-1_SI-dM": true,
+ "QS-Proposed-SUP_SUP-1_SI-body": true,
+ "QS-Proposed-SUP_SUP-1_SI-div": true,
+ "QS-Proposed-IOL_TEXT_SI-dM": true,
+ "QS-Proposed-IOL_TEXT_SI-body": true,
+ "QS-Proposed-IOL_TEXT_SI-div": true,
+ "QS-Proposed-SUP_SPAN.sup-1-SI-dM": true,
+ "QS-Proposed-SUP_SPAN.sup-1-SI-body": true,
+ "QS-Proposed-SUP_SPAN.sup-1-SI-div": true,
+ "QS-Proposed-SUP_MYSUP-1-SI-dM": true,
+ "QS-Proposed-SUP_MYSUP-1-SI-body": true,
+ "QS-Proposed-SUP_MYSUP-1-SI-div": true,
+ "QS-Proposed-IOL_TEXT-1_SI-dM": true,
+ "QS-Proposed-IOL_TEXT-1_SI-body": true,
+ "QS-Proposed-IOL_TEXT-1_SI-div": true,
+ "QS-Proposed-IOL_OL-LI-1_SI-dM": true,
+ "QS-Proposed-IOL_OL-LI-1_SI-body": true,
+ "QS-Proposed-IOL_OL-LI-1_SI-div": true,
+ "QS-Proposed-IOL_UL_LI-1_SI-dM": true,
+ "QS-Proposed-IOL_UL_LI-1_SI-body": true,
+ "QS-Proposed-IOL_UL_LI-1_SI-div": true,
+ "QS-Proposed-IUL_TEXT_SI-dM": true,
+ "QS-Proposed-IUL_TEXT_SI-body": true,
+ "QS-Proposed-IUL_TEXT_SI-div": true,
+ "QS-Proposed-IUL_OL-LI-1_SI-dM": true,
+ "QS-Proposed-IUL_OL-LI-1_SI-body": true,
+ "QS-Proposed-IUL_OL-LI-1_SI-div": true,
+ "QS-Proposed-IUL_UL-LI-1_SI-dM": true,
+ "QS-Proposed-IUL_UL-LI-1_SI-body": true,
+ "QS-Proposed-IUL_UL-LI-1_SI-div": true,
+ "QS-Proposed-JC_TEXT_SI-dM": true,
+ "QS-Proposed-JC_TEXT_SI-body": true,
+ "QS-Proposed-JC_TEXT_SI-div": true,
+ "QS-Proposed-JC_DIVa:c-1_SI-dM": true,
+ "QS-Proposed-JC_DIVa:c-1_SI-body": true,
+ "QS-Proposed-JC_DIVa:c-1_SI-div": true,
+ "QS-Proposed-JC_Pa:c-1_SI-dM": true,
+ "QS-Proposed-JC_Pa:c-1_SI-body": true,
+ "QS-Proposed-JC_Pa:c-1_SI-div": true,
+ "QS-Proposed-JC_SPANs:ta:c-1_SI-dM": true,
+ "QS-Proposed-JC_SPANs:ta:c-1_SI-body": true,
+ "QS-Proposed-JC_SPANs:ta:c-1_SI-div": true,
+ "QS-Proposed-JC_SPAN.jc-1-SI-dM": true,
+ "QS-Proposed-JC_SPAN.jc-1-SI-body": true,
+ "QS-Proposed-JC_SPAN.jc-1-SI-div": true,
+ "QS-Proposed-JC_MYJC-1-SI-dM": true,
+ "QS-Proposed-JC_MYJC-1-SI-body": true,
+ "QS-Proposed-JC_MYJC-1-SI-div": true,
+ "QS-Proposed-JF_TEXT_SI-dM": true,
+ "QS-Proposed-JF_TEXT_SI-body": true,
+ "QS-Proposed-JF_TEXT_SI-div": true,
+ "QS-Proposed-JF_DIVa:j-1_SI-dM": true,
+ "QS-Proposed-JF_DIVa:j-1_SI-body": true,
+ "QS-Proposed-JF_DIVa:j-1_SI-div": true,
+ "QS-Proposed-JF_Pa:j-1_SI-dM": true,
+ "QS-Proposed-JF_Pa:j-1_SI-body": true,
+ "QS-Proposed-JF_Pa:j-1_SI-div": true,
+ "QS-Proposed-JF_SPANs:ta:j-1_SI-dM": true,
+ "QS-Proposed-JF_SPANs:ta:j-1_SI-body": true,
+ "QS-Proposed-JF_SPANs:ta:j-1_SI-div": true,
+ "QS-Proposed-JF_SPAN.jf-1-SI-dM": true,
+ "QS-Proposed-JF_SPAN.jf-1-SI-body": true,
+ "QS-Proposed-JF_SPAN.jf-1-SI-div": true,
+ "QS-Proposed-JF_MYJF-1-SI-dM": true,
+ "QS-Proposed-JF_MYJF-1-SI-body": true,
+ "QS-Proposed-JF_MYJF-1-SI-div": true,
+ "QS-Proposed-JL_TEXT_SI-dM": true,
+ "QS-Proposed-JL_TEXT_SI-body": true,
+ "QS-Proposed-JL_TEXT_SI-div": true,
+ "QS-Proposed-JL_DIVa:l-1_SI-dM": true,
+ "QS-Proposed-JL_DIVa:l-1_SI-body": true,
+ "QS-Proposed-JL_DIVa:l-1_SI-div": true,
+ "QS-Proposed-JL_Pa:l-1_SI-dM": true,
+ "QS-Proposed-JL_Pa:l-1_SI-body": true,
+ "QS-Proposed-JL_Pa:l-1_SI-div": true,
+ "QS-Proposed-JL_SPANs:ta:l-1_SI-dM": true,
+ "QS-Proposed-JL_SPANs:ta:l-1_SI-body": true,
+ "QS-Proposed-JL_SPANs:ta:l-1_SI-div": true,
+ "QS-Proposed-JL_SPAN.jl-1-SI-dM": true,
+ "QS-Proposed-JL_SPAN.jl-1-SI-body": true,
+ "QS-Proposed-JL_SPAN.jl-1-SI-div": true,
+ "QS-Proposed-JL_MYJL-1-SI-dM": true,
+ "QS-Proposed-JL_MYJL-1-SI-body": true,
+ "QS-Proposed-JL_MYJL-1-SI-div": true,
+ "QS-Proposed-JR_TEXT_SI-dM": true,
+ "QS-Proposed-JR_TEXT_SI-body": true,
+ "QS-Proposed-JR_TEXT_SI-div": true,
+ "QS-Proposed-JR_DIVa:r-1_SI-dM": true,
+ "QS-Proposed-JR_DIVa:r-1_SI-body": true,
+ "QS-Proposed-JR_DIVa:r-1_SI-div": true,
+ "QS-Proposed-JR_Pa:r-1_SI-dM": true,
+ "QS-Proposed-JR_Pa:r-1_SI-body": true,
+ "QS-Proposed-JR_Pa:r-1_SI-div": true,
+ "QS-Proposed-JR_SPANs:ta:r-1_SI-dM": true,
+ "QS-Proposed-JR_SPANs:ta:r-1_SI-body": true,
+ "QS-Proposed-JR_SPANs:ta:r-1_SI-div": true,
+ "QS-Proposed-JR_SPAN.jr-1-SI-dM": true,
+ "QS-Proposed-JR_SPAN.jr-1-SI-body": true,
+ "QS-Proposed-JR_SPAN.jr-1-SI-div": true,
+ "QS-Proposed-JR_MYJR-1-SI-dM": true,
+ "QS-Proposed-JR_MYJR-1-SI-body": true,
+ "QS-Proposed-JR_MYJR-1-SI-div": true,
+ "QV-Proposed-B_TEXT_SI-dM": true,
+ "QV-Proposed-B_TEXT_SI-body": true,
+ "QV-Proposed-B_TEXT_SI-div": true,
+ "QV-Proposed-B_B-1_SI-dM": true,
+ "QV-Proposed-B_B-1_SI-body": true,
+ "QV-Proposed-B_B-1_SI-div": true,
+ "QV-Proposed-B_STRONG-1_SI-dM": true,
+ "QV-Proposed-B_STRONG-1_SI-body": true,
+ "QV-Proposed-B_STRONG-1_SI-div": true,
+ "QV-Proposed-B_SPANs:fw:b-1_SI-dM": true,
+ "QV-Proposed-B_SPANs:fw:b-1_SI-body": true,
+ "QV-Proposed-B_SPANs:fw:b-1_SI-div": true,
+ "QV-Proposed-B_SPANs:fw:n-1_SI-dM": true,
+ "QV-Proposed-B_SPANs:fw:n-1_SI-body": true,
+ "QV-Proposed-B_SPANs:fw:n-1_SI-div": true,
+ "QV-Proposed-B_Bs:fw:n-1_SI-dM": true,
+ "QV-Proposed-B_Bs:fw:n-1_SI-body": true,
+ "QV-Proposed-B_Bs:fw:n-1_SI-div": true,
+ "QV-Proposed-B_SPAN.b-1_SI-dM": true,
+ "QV-Proposed-B_SPAN.b-1_SI-body": true,
+ "QV-Proposed-B_SPAN.b-1_SI-div": true,
+ "QV-Proposed-B_MYB-1-SI-dM": true,
+ "QV-Proposed-B_MYB-1-SI-body": true,
+ "QV-Proposed-B_MYB-1-SI-div": true,
+ "QV-Proposed-I_TEXT_SI-dM": true,
+ "QV-Proposed-I_TEXT_SI-body": true,
+ "QV-Proposed-I_TEXT_SI-div": true,
+ "QV-Proposed-I_I-1_SI-dM": true,
+ "QV-Proposed-I_I-1_SI-body": true,
+ "QV-Proposed-I_I-1_SI-div": true,
+ "QV-Proposed-I_EM-1_SI-dM": true,
+ "QV-Proposed-I_EM-1_SI-body": true,
+ "QV-Proposed-I_EM-1_SI-div": true,
+ "QV-Proposed-I_SPANs:fs:i-1_SI-dM": true,
+ "QV-Proposed-I_SPANs:fs:i-1_SI-body": true,
+ "QV-Proposed-I_SPANs:fs:i-1_SI-div": true,
+ "QV-Proposed-I_SPANs:fs:n-1_SI-dM": true,
+ "QV-Proposed-I_SPANs:fs:n-1_SI-body": true,
+ "QV-Proposed-I_SPANs:fs:n-1_SI-div": true,
+ "QV-Proposed-I_I-SPANs:fs:n-1_SI-dM": true,
+ "QV-Proposed-I_I-SPANs:fs:n-1_SI-body": true,
+ "QV-Proposed-I_I-SPANs:fs:n-1_SI-div": true,
+ "QV-Proposed-I_SPAN.i-1_SI-dM": true,
+ "QV-Proposed-I_SPAN.i-1_SI-body": true,
+ "QV-Proposed-I_SPAN.i-1_SI-div": true,
+ "QV-Proposed-I_MYI-1-SI-dM": true,
+ "QV-Proposed-I_MYI-1-SI-body": true,
+ "QV-Proposed-I_MYI-1-SI-div": true,
+ "QV-Proposed-FB_TEXT-1_SC-dM": true,
+ "QV-Proposed-FB_TEXT-1_SC-body": true,
+ "QV-Proposed-FB_TEXT-1_SC-div": true,
+ "QV-Proposed-FB_H1-1_SC-dM": true,
+ "QV-Proposed-FB_H1-1_SC-body": true,
+ "QV-Proposed-FB_H1-1_SC-div": true,
+ "QV-Proposed-FB_PRE-1_SC-dM": true,
+ "QV-Proposed-FB_PRE-1_SC-body": true,
+ "QV-Proposed-FB_PRE-1_SC-div": true,
+ "QV-Proposed-FB_BQ-1_SC-dM": true,
+ "QV-Proposed-FB_BQ-1_SC-body": true,
+ "QV-Proposed-FB_BQ-1_SC-div": true,
+ "QV-Proposed-FB_ADDRESS-1_SC-dM": true,
+ "QV-Proposed-FB_ADDRESS-1_SC-body": true,
+ "QV-Proposed-FB_ADDRESS-1_SC-div": true,
+ "QV-Proposed-FB_H1-H2-1_SC-dM": true,
+ "QV-Proposed-FB_H1-H2-1_SC-body": true,
+ "QV-Proposed-FB_H1-H2-1_SC-div": true,
+ "QV-Proposed-FB_H1-H2-1_SL-dM": true,
+ "QV-Proposed-FB_H1-H2-1_SL-body": true,
+ "QV-Proposed-FB_H1-H2-1_SL-div": true,
+ "QV-Proposed-FB_H1-H2-1_SR-dM": true,
+ "QV-Proposed-FB_H1-H2-1_SR-body": true,
+ "QV-Proposed-FB_H1-H2-1_SR-div": true,
+ "QV-Proposed-FB_TEXT-ADDRESS-1_SL-dM": true,
+ "QV-Proposed-FB_TEXT-ADDRESS-1_SL-body": true,
+ "QV-Proposed-FB_TEXT-ADDRESS-1_SL-div": true,
+ "QV-Proposed-FB_TEXT-ADDRESS-1_SR-dM": true,
+ "QV-Proposed-FB_TEXT-ADDRESS-1_SR-body": true,
+ "QV-Proposed-FB_TEXT-ADDRESS-1_SR-div": true,
+ "QV-Proposed-FB_H1-H2.TEXT.H2-1_SM-dM": true,
+ "QV-Proposed-FB_H1-H2.TEXT.H2-1_SM-body": true,
+ "QV-Proposed-FB_H1-H2.TEXT.H2-1_SM-div": true,
+ "QV-Proposed-H_H1-1_SC-dM": true,
+ "QV-Proposed-H_H1-1_SC-body": true,
+ "QV-Proposed-H_H1-1_SC-div": true,
+ "QV-Proposed-H_H3-1_SC-dM": true,
+ "QV-Proposed-H_H3-1_SC-body": true,
+ "QV-Proposed-H_H3-1_SC-div": true,
+ "QV-Proposed-H_H1-H2-H3-H4-1_SC-dM": true,
+ "QV-Proposed-H_H1-H2-H3-H4-1_SC-body": true,
+ "QV-Proposed-H_H1-H2-H3-H4-1_SC-div": true,
+ "QV-Proposed-H_P-1_SC-dM": true,
+ "QV-Proposed-H_P-1_SC-body": true,
+ "QV-Proposed-H_P-1_SC-div": true,
+ "QV-Proposed-FN_FONTf:a-1_SI-dM": true,
+ "QV-Proposed-FN_FONTf:a-1_SI-body": true,
+ "QV-Proposed-FN_FONTf:a-1_SI-div": true,
+ "QV-Proposed-FN_SPANs:ff:a-1_SI-dM": true,
+ "QV-Proposed-FN_SPANs:ff:a-1_SI-body": true,
+ "QV-Proposed-FN_SPANs:ff:a-1_SI-div": true,
+ "QV-Proposed-FN_FONTf:a.s:ff:c-1_SI-dM": true,
+ "QV-Proposed-FN_FONTf:a.s:ff:c-1_SI-body": true,
+ "QV-Proposed-FN_FONTf:a.s:ff:c-1_SI-div": true,
+ "QV-Proposed-FN_FONTf:a-FONTf:c-1_SI-dM": true,
+ "QV-Proposed-FN_FONTf:a-FONTf:c-1_SI-body": true,
+ "QV-Proposed-FN_FONTf:a-FONTf:c-1_SI-div": true,
+ "QV-Proposed-FN_SPANs:ff:c-FONTf:a-1_SI-dM": true,
+ "QV-Proposed-FN_SPANs:ff:c-FONTf:a-1_SI-body": true,
+ "QV-Proposed-FN_SPANs:ff:c-FONTf:a-1_SI-div": true,
+ "QV-Proposed-FN_SPAN.fs18px-1_SI-dM": true,
+ "QV-Proposed-FN_SPAN.fs18px-1_SI-body": true,
+ "QV-Proposed-FN_SPAN.fs18px-1_SI-div": true,
+ "QV-Proposed-FN_MYCOURIER-1-SI-dM": true,
+ "QV-Proposed-FN_MYCOURIER-1-SI-body": true,
+ "QV-Proposed-FN_MYCOURIER-1-SI-div": true,
+ "QV-Proposed-FS_FONTsz:4-1_SI-dM": true,
+ "QV-Proposed-FS_FONTsz:4-1_SI-body": true,
+ "QV-Proposed-FS_FONTsz:4-1_SI-div": true,
+ "QV-Proposed-FS_FONTs:fs:l-1_SI-dM": true,
+ "QV-Proposed-FS_FONTs:fs:l-1_SI-body": true,
+ "QV-Proposed-FS_FONTs:fs:l-1_SI-div": true,
+ "QV-Proposed-FS_FONT.ass.s:fs:l-1_SI-dM": true,
+ "QV-Proposed-FS_FONT.ass.s:fs:l-1_SI-body": true,
+ "QV-Proposed-FS_FONT.ass.s:fs:l-1_SI-div": true,
+ "QV-Proposed-FS_FONTsz:1.s:fs:xl-1_SI-dM": true,
+ "QV-Proposed-FS_FONTsz:1.s:fs:xl-1_SI-body": true,
+ "QV-Proposed-FS_FONTsz:1.s:fs:xl-1_SI-div": true,
+ "QV-Proposed-FS_SPAN.large-1_SI-dM": true,
+ "QV-Proposed-FS_SPAN.large-1_SI-body": true,
+ "QV-Proposed-FS_SPAN.large-1_SI-div": true,
+ "QV-Proposed-FS_SPAN.fs18px-1_SI-dM": true,
+ "QV-Proposed-FS_SPAN.fs18px-1_SI-body": true,
+ "QV-Proposed-FS_SPAN.fs18px-1_SI-div": true,
+ "QV-Proposed-FA_MYLARGE-1-SI-dM": true,
+ "QV-Proposed-FA_MYLARGE-1-SI-body": true,
+ "QV-Proposed-FA_MYLARGE-1-SI-div": true,
+ "QV-Proposed-FA_MYFS18PX-1-SI-dM": true,
+ "QV-Proposed-FA_MYFS18PX-1-SI-body": true,
+ "QV-Proposed-FA_MYFS18PX-1-SI-div": true,
+ "QV-Proposed-BC_FONTs:bc:fca-1_SI-dM": true,
+ "QV-Proposed-BC_FONTs:bc:fca-1_SI-body": true,
+ "QV-Proposed-BC_FONTs:bc:fca-1_SI-div": true,
+ "QV-Proposed-BC_SPANs:bc:abc-1_SI-dM": true,
+ "QV-Proposed-BC_SPANs:bc:abc-1_SI-body": true,
+ "QV-Proposed-BC_SPANs:bc:abc-1_SI-div": true,
+ "QV-Proposed-BC_FONTs:bc:084-SPAN-1_SI-dM": true,
+ "QV-Proposed-BC_FONTs:bc:084-SPAN-1_SI-body": true,
+ "QV-Proposed-BC_FONTs:bc:084-SPAN-1_SI-div": true,
+ "QV-Proposed-BC_SPANs:bc:cde-SPAN-1_SI-dM": true,
+ "QV-Proposed-BC_SPANs:bc:cde-SPAN-1_SI-body": true,
+ "QV-Proposed-BC_SPANs:bc:cde-SPAN-1_SI-div": true,
+ "QV-Proposed-BC_SPAN.ass.s:bc:rgb-1_SI-dM": true,
+ "QV-Proposed-BC_SPAN.ass.s:bc:rgb-1_SI-body": true,
+ "QV-Proposed-BC_SPAN.ass.s:bc:rgb-1_SI-div": true,
+ "QV-Proposed-BC_SPAN.bcred-1_SI-dM": true,
+ "QV-Proposed-BC_SPAN.bcred-1_SI-body": true,
+ "QV-Proposed-BC_SPAN.bcred-1_SI-div": true,
+ "QV-Proposed-BC_MYBCRED-1-SI-dM": true,
+ "QV-Proposed-BC_MYBCRED-1-SI-body": true,
+ "QV-Proposed-BC_MYBCRED-1-SI-div": true,
+ "QV-Proposed-FC_FONTc:f00-1_SI-dM": true,
+ "QV-Proposed-FC_FONTc:f00-1_SI-body": true,
+ "QV-Proposed-FC_FONTc:f00-1_SI-div": true,
+ "QV-Proposed-FC_SPANs:c:0f0-1_SI-dM": true,
+ "QV-Proposed-FC_SPANs:c:0f0-1_SI-body": true,
+ "QV-Proposed-FC_SPANs:c:0f0-1_SI-div": true,
+ "QV-Proposed-FC_FONTc:333.s:c:999-1_SI-dM": true,
+ "QV-Proposed-FC_FONTc:333.s:c:999-1_SI-body": true,
+ "QV-Proposed-FC_FONTc:333.s:c:999-1_SI-div": true,
+ "QV-Proposed-FC_FONTc:641-SPAN-1_SI-dM": true,
+ "QV-Proposed-FC_FONTc:641-SPAN-1_SI-body": true,
+ "QV-Proposed-FC_FONTc:641-SPAN-1_SI-div": true,
+ "QV-Proposed-FC_SPANs:c:d95-SPAN-1_SI-dM": true,
+ "QV-Proposed-FC_SPANs:c:d95-SPAN-1_SI-body": true,
+ "QV-Proposed-FC_SPANs:c:d95-SPAN-1_SI-div": true,
+ "QV-Proposed-FC_SPAN.red-1_SI-dM": true,
+ "QV-Proposed-FC_SPAN.red-1_SI-body": true,
+ "QV-Proposed-FC_SPAN.red-1_SI-div": true,
+ "QV-Proposed-FC_MYRED-1-SI-dM": true,
+ "QV-Proposed-FC_MYRED-1-SI-body": true,
+ "QV-Proposed-FC_MYRED-1-SI-div": true,
+ "QV-Proposed-HC_FONTs:bc:fc0-1_SI-dM": true,
+ "QV-Proposed-HC_FONTs:bc:fc0-1_SI-body": true,
+ "QV-Proposed-HC_FONTs:bc:fc0-1_SI-div": true,
+ "QV-Proposed-HC_SPANs:bc:a0c-1_SI-dM": true,
+ "QV-Proposed-HC_SPANs:bc:a0c-1_SI-body": true,
+ "QV-Proposed-HC_SPANs:bc:a0c-1_SI-div": true,
+ "QV-Proposed-HC_SPAN.ass.s:bc:rgb-1_SI-dM": true,
+ "QV-Proposed-HC_SPAN.ass.s:bc:rgb-1_SI-body": true,
+ "QV-Proposed-HC_SPAN.ass.s:bc:rgb-1_SI-div": true,
+ "QV-Proposed-HC_FONTs:bc:83e-SPAN-1_SI-dM": true,
+ "QV-Proposed-HC_FONTs:bc:83e-SPAN-1_SI-body": true,
+ "QV-Proposed-HC_FONTs:bc:83e-SPAN-1_SI-div": true,
+ "QV-Proposed-HC_SPANs:bc:b12-SPAN-1_SI-dM": true,
+ "QV-Proposed-HC_SPANs:bc:b12-SPAN-1_SI-body": true,
+ "QV-Proposed-HC_SPANs:bc:b12-SPAN-1_SI-div": true,
+ "QV-Proposed-HC_SPAN.bcred-1_SI-dM": true,
+ "QV-Proposed-HC_SPAN.bcred-1_SI-body": true,
+ "QV-Proposed-HC_SPAN.bcred-1_SI-div": true,
+ "QV-Proposed-HC_MYBCRED-1-SI-dM": true,
+ "QV-Proposed-HC_MYBCRED-1-SI-body": true,
+ "QV-Proposed-HC_MYBCRED-1-SI-div": true
+ }
+}
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/current_revision b/editor/libeditor/tests/browserscope/lib/richtext2/current_revision
new file mode 100644
index 000000000..cc34bb397
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/current_revision
@@ -0,0 +1 @@
+805
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/__init__.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/__init__.py
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/common.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/common.py
new file mode 100644
index 000000000..345f9bbb0
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/common.py
@@ -0,0 +1,25 @@
+#!/usr/bin/python2.5
+#
+# Copyright 2010 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the 'License')
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an 'AS IS' BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Common constants"""
+
+__author__ = 'rolandsteiner@google.com (Roland Steiner)'
+
+CATEGORY = 'richtext2'
+
+TEST_ID_PREFIX = 'RTE2'
+
+CLASSES = ['Finalized', 'RFC', 'Proposed'] \ No newline at end of file
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/handlers.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/handlers.py
new file mode 100644
index 000000000..2ee1e79ad
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/handlers.py
@@ -0,0 +1,107 @@
+#!/usr/bin/python2.5
+#
+# Copyright 2010 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the 'License')
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an 'AS IS' BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Handlers for New Rich Text Tests"""
+
+__author__ = 'rolandsteiner@google.com (Roland Steiner)'
+
+from google.appengine.api import users
+from google.appengine.ext import db
+from google.appengine.api import memcache
+from google.appengine.ext import webapp
+from google.appengine.ext.webapp import template
+
+import django
+from django import http
+from django import shortcuts
+
+from django.template import add_to_builtins
+add_to_builtins('base.custom_filters')
+
+# Shared stuff
+from categories import all_test_sets
+from base import decorators
+from base import util
+
+# common to the RichText2 suite
+from categories.richtext2 import common
+
+# tests
+from categories.richtext2.tests.apply import APPLY_TESTS
+from categories.richtext2.tests.applyCSS import APPLY_TESTS_CSS
+from categories.richtext2.tests.change import CHANGE_TESTS
+from categories.richtext2.tests.changeCSS import CHANGE_TESTS_CSS
+from categories.richtext2.tests.delete import DELETE_TESTS
+from categories.richtext2.tests.forwarddelete import FORWARDDELETE_TESTS
+from categories.richtext2.tests.insert import INSERT_TESTS
+from categories.richtext2.tests.selection import SELECTION_TESTS
+from categories.richtext2.tests.unapply import UNAPPLY_TESTS
+from categories.richtext2.tests.unapplyCSS import UNAPPLY_TESTS_CSS
+
+from categories.richtext2.tests.querySupported import QUERYSUPPORTED_TESTS
+from categories.richtext2.tests.queryEnabled import QUERYENABLED_TESTS
+from categories.richtext2.tests.queryIndeterm import QUERYINDETERM_TESTS
+from categories.richtext2.tests.queryState import QUERYSTATE_TESTS, QUERYSTATE_TESTS_CSS
+from categories.richtext2.tests.queryValue import QUERYVALUE_TESTS, QUERYVALUE_TESTS_CSS
+
+
+def About(request):
+ """About page."""
+ overview = """These tests cover browers' implementations of
+ <a href="http://blog.whatwg.org/the-road-to-html-5-contenteditable">contenteditable</a>
+ for basic rich text formatting commands. Most browser implementations do very
+ well at editing the HTML which is generated by their own execCommands. But a
+ big problem happens when developers try to make cross-browser web
+ applications using contenteditable - most browsers are not able to correctly
+ change formatting generated by other browsers. On top of that, most browsers
+ allow users to to paste arbitrary HTML from other webpages into a
+ contenteditable region, which is even harder for browsers to properly
+ format. These tests check how well the execCommand, queryCommandState,
+ and queryCommandValue functions work with different types of HTML."""
+ return util.About(request, common.CATEGORY, category_title='Rich Text',
+ overview=overview, show_hidden=False)
+
+
+def RunRichText2Tests(request):
+ params = {
+ 'classes': common.CLASSES,
+ 'commonIDPrefix': common.TEST_ID_PREFIX,
+ 'strict': False,
+ 'suites': [
+ SELECTION_TESTS,
+ APPLY_TESTS,
+ APPLY_TESTS_CSS,
+ CHANGE_TESTS,
+ CHANGE_TESTS_CSS,
+ UNAPPLY_TESTS,
+ UNAPPLY_TESTS_CSS,
+ DELETE_TESTS,
+ FORWARDDELETE_TESTS,
+ INSERT_TESTS,
+
+ QUERYSUPPORTED_TESTS,
+ QUERYENABLED_TESTS,
+ QUERYINDETERM_TESTS,
+ QUERYSTATE_TESTS,
+ QUERYSTATE_TESTS_CSS,
+ QUERYVALUE_TESTS,
+ QUERYVALUE_TESTS_CSS
+ ]
+ }
+ return shortcuts.render_to_response('%s/templates/richtext2.html' % common.CATEGORY, params)
+
+
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/common.css b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/common.css
new file mode 100644
index 000000000..77c6bb872
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/common.css
@@ -0,0 +1,116 @@
+.framed {
+ vertical-align: top;
+ margin: 8px;
+ border: 1px solid black;
+}
+
+.legend {
+ padding: 12px;
+ background-color: #f8f8ff;
+}
+
+.legendHdr {
+ font-size: large;
+ text-decoration: underline;
+}
+
+table.legend {
+ display: inline-table;
+}
+
+.suite-thead {
+ text-align: left;
+}
+
+.lo {
+ background-color: #dddddd;
+}
+.hi {
+ background-color: #eeeeee;
+}
+
+.lo .grey {
+ background-color: #dddddd;
+}
+.lo .na {
+ background-color: #dddddd;
+}
+.lo .pass {
+ background-color: #d4ffc0;
+}
+.lo .canary {
+ background-color: #ffcccc;
+}
+.lo .fail {
+ background-color: #ffcccc;
+}
+.lo .accept {
+ background-color: #ffffc0;
+}
+.lo .exception {
+ background-color: #f0d0f4;
+}
+.lo .unsupported {
+ background-color: #f0d0f4;
+}
+
+.hi .grey {
+ background-color: #eeeeee;
+}
+.hi .na {
+ background-color: #eeeeee;
+}
+.hi .pass {
+ background-color: #e0ffdc;
+}
+.hi .canary {
+ background-color: #ffd8d8;
+}
+.hi .fail {
+ background-color: #ffd8d8;
+}
+.hi .accept {
+ background-color: #ffffd8;
+}
+.hi .exception {
+ background-color: #f4dcf8;
+}
+.hi .unsupported {
+ background-color: #f4dcf8;
+}
+
+
+.sel {
+ color: blue;
+}
+
+.txt {
+ padding: 1px;
+ margin: 1px;
+ border: 1px solid #b0b0b0;
+}
+
+.idLabel {
+ font-size: small;
+}
+
+.fade {
+ color: grey;
+}
+.accexp {
+ color: #606070;
+}
+.comment {
+ color: grey;
+}
+
+.score {
+ color: #666666;
+}
+
+.fatalerror {
+ color: red;
+ font-size: large;
+ font-weight: bold;
+}
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-body.html b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-body.html
new file mode 100644
index 000000000..a254adc03
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-body.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+
+ <link rel="stylesheet" href="editable.css" type="text/css">
+</head>
+<body contentEditable="true">
+</body>
+</html>
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-dM.html b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-dM.html
new file mode 100644
index 000000000..e16de3ab9
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-dM.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+
+ <link rel="stylesheet" href="editable.css" type="text/css">
+
+ <script>
+ function setDesignMode() {
+ window.document.designMode = "On";
+ }
+ </script>
+</head>
+<body onload="setDesignMode()">
+</body>
+</html>
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-div.html b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-div.html
new file mode 100644
index 000000000..7dd600dbd
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-div.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+
+ <link rel="stylesheet" href="editable.css" type="text/css">
+</head>
+<body>
+</body>
+</html>
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable.css b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable.css
new file mode 100644
index 000000000..99fec4950
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable.css
@@ -0,0 +1,66 @@
+.b, myb {
+ font-weight: bold;
+}
+
+.i, myi {
+ font-style: italic;
+}
+
+.s, mys {
+ text-decoration: line-through;
+}
+
+.u, myu {
+ text-decoration: underline;
+}
+
+.sub, mysub {
+ vertical-align: sub;
+}
+
+.sup, mysup {
+ vertical-align: super;
+}
+
+.jc, myjc {
+ text-align: center;
+}
+
+.jf, myjf {
+ text-align: justify;
+}
+
+.jl, myjl {
+ text-align: left;
+}
+
+.jr, myjr {
+ text-align: right;
+}
+
+.red, myred {
+ color: red;
+}
+
+.bcred, mybcred {
+ background-color: red;
+}
+
+.large, mylarge {
+ font-size: large;
+}
+
+.fs18px, myfs18px {
+ font-size: 18px;
+}
+
+.courier, mycourier {
+ font-family: courier;
+}
+
+gen::before {
+ content: "[BEFORE]";
+}
+gen::after {
+ content: "[AFTER]";
+}
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/canonicalize.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/canonicalize.js
new file mode 100644
index 000000000..2236d9dfc
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/canonicalize.js
@@ -0,0 +1,436 @@
+/**
+ * @fileoverview
+ * Canonicalization functions used in the RTE test suite.
+ *
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License')
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @version 0.1
+ * @author rolandsteiner@google.com
+ */
+
+/**
+ * Canonicalize HTML entities to their actual character
+ *
+ * @param str {String} the HTML string to be canonicalized
+ * @return {String} the canonicalized string
+ */
+
+function canonicalizeEntities(str) {
+ // TODO(rolandsteiner): this function is very much not optimized, but that shouldn't
+ // theoretically matter too much - look into it at some point.
+ var match;
+ while (match = str.match(/&#x([0-9A-F]+);/i)) {
+ str = str.replace('&#x' + match[1] + ';', String.fromCharCode(parseInt(match[1], 16)));
+ }
+ while (match = str.match(/&#([0-9]+);/)) {
+ str = str.replace('&#' + match[1] + ';', String.fromCharCode(Number(match[1])));
+ }
+ return str;
+}
+
+/**
+ * Canonicalize the contents of the HTML 'style' attribute.
+ * I.e. sorts the CSS attributes alphabetically and canonicalizes the values
+ * CSS attributes where necessary.
+ *
+ * If this would return an empty string, return null instead to suppress the
+ * whole 'style' attribute.
+ *
+ * Avoid tests that contain {, } or : within CSS values!
+ *
+ * Note that this function relies on the spaces of the input string already
+ * having been normalized by canonicalizeSpaces!
+ *
+ * FIXME: does not canonicalize the contents of compound attributes
+ * (e.g., 'border').
+ *
+ * @param str {String} contents of the 'style' attribute
+ * @param emitFlags {Object} flags used for this output
+ * @return {String/null} canonicalized string, null instead of the empty string
+ */
+function canonicalizeStyle(str, emitFlags) {
+ // Remove any enclosing curly brackets
+ str = str.replace(/ ?[\{\}] ?/g, '');
+
+ var attributes = str.split(';');
+ var count = attributes.length;
+ var resultArr = [];
+
+ for (var a = 0; a < count; ++a) {
+ // Retrieve "name: value" pair
+ // Note: may expectedly fail if the last pair was terminated with ';'
+ var avPair = attributes[a].match(/ ?([^ :]+) ?: ?(.+)/);
+ if (!avPair)
+ continue;
+
+ var name = avPair[1];
+ var value = avPair[2].replace(/ $/, ''); // Remove any trailing space.
+
+ switch (name) {
+ case 'color':
+ case 'background-color':
+ case 'border-color':
+ if (emitFlags.canonicalizeUnits) {
+ resultArr.push(name + ': #' + new Color(value).toHexString());
+ } else {
+ resultArr.push(name + ': ' + value);
+ }
+ break;
+
+ case 'font-family':
+ if (emitFlags.canonicalizeUnits) {
+ resultArr.push(name + ': ' + new FontName(value).toString());
+ } else {
+ resultArr.push(name + ': ' + value);
+ }
+ break;
+
+ case 'font-size':
+ if (emitFlags.canonicalizeUnits) {
+ resultArr.push(name + ': ' + new FontSize(value).toString());
+ } else {
+ resultArr.push(name + ': ' + value);
+ }
+ break;
+
+ default:
+ resultArr.push(name + ': ' + value);
+ }
+ }
+
+ // Sort by name, assuming no duplicate CSS attribute names.
+ resultArr.sort();
+
+ return resultArr.join('; ') || null;
+}
+
+/**
+ * Canonicalize a single attribute value.
+ *
+ * Note that this function relies on the spaces of the input string already
+ * having been normalized by canonicalizeSpaces!
+ *
+ * @param elemName {String} the name of the element
+ * @param attrName {String} the name of the attribute
+ * @param attrValue {String} the value of the attribute
+ * @param emitFlags {Object} flags used for this output
+ * @return {String/null} the canonicalized value, or null if the attribute should be skipped.
+ */
+function canonicalizeSingleAttribute(elemName, attrName, attrValue, emitFlags) {
+ // We emit attributes as name="value", so change any contained apostrophes
+ // to quote marks.
+ attrValue = attrValue.replace(/\x22/, '\x27');
+
+ switch (attrName) {
+ case 'class':
+ return emitFlags.emitClass ? attrValue : null;
+
+ case 'id':
+ if (!emitFlags.emitID) {
+ return null;
+ }
+ if (attrValue && attrValue.substr(0, 7) == 'editor-') {
+ return null;
+ }
+ return attrValue;
+
+ // Remove empty style attributes, canonicalize the contents otherwise,
+ // provided the test cares for styles.
+ case 'style':
+ return (emitFlags.emitStyle && attrValue)
+ ? canonicalizeStyle(attrValue, emitFlags)
+ : null;
+
+ // Never output onload handlers as they are set by the test environment.
+ case 'onload':
+ return null;
+
+ // Canonicalize colors.
+ case 'bgcolor':
+ case 'color':
+ if (!attrValue) {
+ return null;
+ }
+ return emitFlags.canonicalizeUnits ? new Color(attrValue).toString() : attrValue;
+
+ // Canonicalize font names.
+ case 'face':
+ return emitFlags.canonicalizeUnits ? new FontName(attrValue).toString() : attrValue;
+
+ // Canonicalize font sizes (leave other 'size' attributes as-is).
+ case 'size':
+ if (!attrValue) {
+ return null;
+ }
+ switch (elemName) {
+ case 'basefont':
+ case 'font':
+ return emitFlags.canonicalizeUnits ? new FontSize(attrValue).toString() : attrValue;
+ }
+ return attrValue;
+
+ // Remove spans with value 1. Retain spans with other values, even if
+ // empty or with a value 0, since those indicate a flawed implementation.
+ case 'colspan':
+ case 'rowspan':
+ case 'span':
+ return (attrValue == '1' || attrValue === '') ? null : attrValue;
+
+ // Boolean attributes: presence equals true. If present, the value must be
+ // the empty string or the attribute's canonical name.
+ // (http://www.whatwg.org/specs/web-apps/current-work/#boolean-attributes)
+ // Below we only normalize empty string to the canonical name for
+ // comparison purposes. All other values are not touched and will therefore
+ // in all likelihood result in a failed test (even if they may be accepted
+ // by the UA).
+ case 'async':
+ case 'autofocus':
+ case 'checked':
+ case 'compact':
+ case 'declare':
+ case 'defer':
+ case 'disabled':
+ case 'formnovalidate':
+ case 'frameborder':
+ case 'ismap':
+ case 'loop':
+ case 'multiple':
+ case 'nohref':
+ case 'nosize':
+ case 'noshade':
+ case 'novalidate':
+ case 'nowrap':
+ case 'open':
+ case 'readonly':
+ case 'required':
+ case 'reversed':
+ case 'seamless':
+ case 'selected':
+ return attrValue ? attrValue : attrName;
+
+ default:
+ return attrValue;
+ }
+}
+
+/**
+ * Canonicalize the contents of an element tag.
+ *
+ * I.e. sorts the attributes alphabetically and canonicalizes their
+ * values where necessary. Also removes attributes we're not interested in.
+ *
+ * Note that this function relies on the spaces of the input string already
+ * having been normalized by canonicalizeSpaces!
+ *
+ * @param str {String} the contens of the element tag, excluding < and >.
+ * @param emitFlags {Object} flags used for this output
+ * @return {String} the canonicalized contents.
+ */
+function canonicalizeElementTag(str, emitFlags) {
+ // FIXME: lowercase only if emitFlags.lowercase is set
+ str = str.toLowerCase();
+
+ var pos = str.search(' ');
+
+ // element name only
+ if (pos == -1) {
+ return str;
+ }
+
+ var elemName = str.substr(0, pos);
+ str = str.substr(pos + 1);
+
+ // Even if emitFlags.emitAttrs is not set, we must iterate over the
+ // attributes to catch the special selection attribute and/or selection
+ // markers. :(
+
+ // Iterate over attributes, add them to an array, canonicalize their
+ // contents, and finally output the (remaining) attributes in sorted order.
+ // Note: We can't do a simple split on space here, because the value of,
+ // e.g., 'style' attributes may also contain spaces.
+ var attrs = [];
+ var selStartInTag = false;
+ var selEndInTag = false;
+
+ while (str) {
+ var attrName;
+ var attrValue = '';
+
+ pos = str.search(/[ =]/);
+ if (pos >= 0) {
+ attrName = str.substr(0, pos);
+ if (str.charAt(pos) == ' ') {
+ ++pos;
+ }
+ if (str.charAt(pos) == '=') {
+ ++pos;
+ if (str.charAt(pos) == ' ') {
+ ++pos;
+ }
+ str = str.substr(pos);
+ switch (str.charAt(0)) {
+ case '"':
+ case "'":
+ pos = str.indexOf(str.charAt(0), 1);
+ pos = (pos < 0) ? str.length : pos;
+ attrValue = str.substring(1, pos);
+ ++pos;
+ break;
+
+ default:
+ pos = str.indexOf(' ', 0);
+ pos = (pos < 0) ? str.length : pos;
+ attrValue = (pos == -1) ? str : str.substr(0, pos);
+ break;
+ }
+ attrValue = attrValue.replace(/^ /, '');
+ attrValue = attrValue.replace(/ $/, '');
+ }
+ } else {
+ attrName = str;
+ }
+ str = (pos == -1 || pos >= str.length) ? '' : str.substr(pos + 1);
+
+ // Remove special selection attributes.
+ switch (attrName) {
+ case ATTRNAME_SEL_START:
+ selStartInTag = true;
+ continue;
+
+ case ATTRNAME_SEL_END:
+ selEndInTag = true;
+ continue;
+ }
+
+ switch (attrName) {
+ case '':
+ case 'onload':
+ case 'xmlns':
+ break;
+
+ default:
+ if (!emitFlags.emitAttrs) {
+ break;
+ }
+ // >>> fall through >>>
+
+ case 'contenteditable':
+ attrValue = canonicalizeEntities(attrValue);
+ attrValue = canonicalizeSingleAttribute(elemName, attrName, attrValue, emitFlags);
+ if (attrValue !== null) {
+ attrs.push(attrName + '="' + attrValue + '"');
+ }
+ }
+ }
+
+ var result = elemName;
+
+ // Sort alphabetically (on full string rather than just attribute value for
+ // simplicity. Also, attribute names will differ when encountering the '=').
+ if (attrs.length > 0) {
+ attrs.sort();
+ result += ' ' + attrs.join(' ');
+ }
+
+ // Add intra-tag selection marker(s) or attribute(s), if any, at the end.
+ if (selStartInTag && selEndInTag) {
+ result += ' |';
+ } else if (selStartInTag) {
+ result += ' {';
+ } else if (selEndInTag) {
+ result += ' }';
+ }
+
+ return result;
+}
+
+/**
+ * Canonicalize elements and attributes to facilitate comparison to the
+ * expectation string: sort attributes, canonicalize values and remove chaff.
+ *
+ * Note that this function relies on the spaces of the input string already
+ * having been normalized by canonicalizeSpaces!
+ *
+ * @param str {String} the HTML string to be canonicalized
+ * @param emitFlags {Object} flags used for this output
+ * @return {String} the canonicalized string
+ */
+function canonicalizeElementsAndAttributes(str, emitFlags) {
+ var tagStart = str.indexOf('<');
+ var tagEnd = 0;
+ var result = '';
+
+ while (tagStart >= 0) {
+ ++tagStart;
+ if (str.charAt(tagStart) == '/') {
+ ++tagStart;
+ }
+ result = result + canonicalizeEntities(str.substring(tagEnd, tagStart));
+ tagEnd = str.indexOf('>', tagStart);
+ if (tagEnd < 0) {
+ tagEnd = str.length - 1;
+ }
+ if (str.charAt(tagEnd - 1) == '/') {
+ --tagEnd;
+ }
+ var elemStr = str.substring(tagStart, tagEnd);
+ elemStr = canonicalizeElementTag(elemStr, emitFlags);
+ result = result + elemStr;
+ tagStart = str.indexOf('<', tagEnd);
+ }
+ return result + canonicalizeEntities(str.substring(tagEnd));
+}
+
+/**
+ * Canonicalize an innerHTML string to uniform single whitespaces.
+ *
+ * FIXME: running this prevents testing for pre-formatted content
+ * and the CSS 'white-space' attribute.
+ *
+ * @param str {String} the HTML string to be canonicalized
+ * @return {String} the canonicalized string
+ */
+function canonicalizeSpaces(str) {
+ // Collapse sequential whitespace.
+ str = str.replace(/\s+/g, ' ');
+
+ // Remove spaces immediately inside angle brackets <, >, </ and />.
+ // While doing this also canonicalize <.../> to <...>.
+ str = str.replace(/\< ?/g, '<');
+ str = str.replace(/\<\/ ?/g, '</');
+ str = str.replace(/ ?\/?\>/g, '>');
+
+ return str;
+}
+
+/**
+ * Canonicalize an innerHTML string to uniform single whitespaces.
+ * Also remove comments to retain only embedded selection markers, and
+ * remove </br> and </hr> if present.
+ *
+ * FIXME: running this prevents testing for pre-formatted content
+ * and the CSS 'white-space' attribute.
+ *
+ * @param str {String} the HTML string to be canonicalized
+ * @return {String} the canonicalized string
+ */
+function initialCanonicalizationOf(str) {
+ str = canonicalizeSpaces(str);
+ str = str.replace(/ ?<!-- ?/g, '');
+ str = str.replace(/ ?--> ?/g, '');
+ str = str.replace(/<\/[bh]r>/g, '');
+
+ return str;
+}
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/compare.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/compare.js
new file mode 100644
index 000000000..be059cfc8
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/compare.js
@@ -0,0 +1,489 @@
+/**
+ * @fileoverview
+ * Comparison functions used in the RTE test suite.
+ *
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License')
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @version 0.1
+ * @author rolandsteiner@google.com
+ */
+
+/**
+ * constants used only in the compare functions.
+ */
+var RESULT_DIFF = 0; // actual result doesn't match expectation
+var RESULT_SEL = 1; // actual result matches expectation in HTML only
+var RESULT_EQUAL = 2; // actual result matches expectation in both HTML and selection
+
+/**
+ * Gets the test expectations as an array from the passed-in field.
+ *
+ * @param {Array|String} the test expectation(s) as string or array.
+ * @return {Array} test expectations as an array.
+ */
+function getExpectationArray(expected) {
+ if (expected === undefined) {
+ return [];
+ }
+ if (expected === null) {
+ return [null];
+ }
+ switch (typeof expected) {
+ case 'string':
+ case 'boolean':
+ case 'number':
+ return [expected];
+ }
+ // Assume it's already an array.
+ return expected;
+}
+
+/**
+ * Compare a test result to a single expectation string.
+ *
+ * FIXME: add support for optional elements/attributes.
+ *
+ * @param expected {String} the already canonicalized (with the exception of selection marks) expectation string
+ * @param actual {String} the already canonicalized (with the exception of selection marks) actual result
+ * @return {Integer} one of the RESULT_... return values
+ * @see variables.js for return values
+ */
+function compareHTMLToSingleExpectation(expected, actual) {
+ // If the test checks the selection, then the actual string must match the
+ // expectation exactly.
+ if (expected == actual) {
+ return RESULT_EQUAL;
+ }
+
+ // Remove selection markers and see if strings match then.
+ expected = expected.replace(/ [{}\|]>/g, '>'); // intra-tag
+ expected = expected.replace(/[\[\]\^{}\|]/g, ''); // outside tag
+ actual = actual.replace(/ [{}\|]>/g, '>'); // intra-tag
+ actual = actual.replace(/[\[\]\^{}\|]/g, ''); // outside tag
+
+ return (expected == actual) ? RESULT_SEL : RESULT_DIFF;
+}
+
+/**
+ * Compare the current HTMLtest result to the expectation string(s).
+ *
+ * @param actual {String/Boolean} actual value
+ * @param expected {String/Array} expectation(s)
+ * @param emitFlags {Object} flags to use for canonicalization
+ * @return {Integer} one of the RESULT_... return values
+ * @see variables.js for return values
+ */
+function compareHTMLToExpectation(actual, expected, emitFlags) {
+ // Find the most favorable result among the possible expectation strings.
+ var expectedArr = getExpectationArray(expected);
+ var count = expectedArr ? expectedArr.length : 0;
+ var best = RESULT_DIFF;
+
+ for (var idx = 0; idx < count && best < RESULT_EQUAL; ++idx) {
+ var expected = expectedArr[idx];
+ expected = canonicalizeSpaces(expected);
+ expected = canonicalizeElementsAndAttributes(expected, emitFlags);
+
+ var singleResult = compareHTMLToSingleExpectation(expected, actual);
+
+ best = Math.max(best, singleResult);
+ }
+ return best;
+}
+
+/**
+ * Compare the current HTMLtest result to expected and acceptable results
+ *
+ * @param expected {String/Array} expected result(s)
+ * @param accepted {String/Array} accepted result(s)
+ * @param actual {String} actual result
+ * @param emitFlags {Object} how to canonicalize the HTML strings
+ * @param result {Object} [out] object recieving the result of the comparison.
+ */
+function compareHTMLTestResultTo(expected, accepted, actual, emitFlags, result) {
+ actual = actual.replace(/[\x60\xb4]/g, '');
+ actual = canonicalizeElementsAndAttributes(actual, emitFlags);
+
+ var bestExpected = compareHTMLToExpectation(actual, expected, emitFlags);
+
+ if (bestExpected == RESULT_EQUAL) {
+ // Shortcut - it doesn't get any better
+ result.valresult = VALRESULT_EQUAL;
+ result.selresult = SELRESULT_EQUAL;
+ return;
+ }
+
+ var bestAccepted = compareHTMLToExpectation(actual, accepted, emitFlags);
+
+ switch (bestExpected) {
+ case RESULT_SEL:
+ switch (bestAccepted) {
+ case RESULT_EQUAL:
+ // The HTML was equal to the/an expected HTML result as well
+ // (just not the selection there), therefore the difference
+ // between expected and accepted can only lie in the selection.
+ result.valresult = VALRESULT_EQUAL;
+ result.selresult = SELRESULT_ACCEPT;
+ return;
+
+ case RESULT_SEL:
+ case RESULT_DIFF:
+ // The acceptable expectations did not yield a better result
+ // -> stay with the original (i.e., comparison to 'expected') result.
+ result.valresult = VALRESULT_EQUAL;
+ result.selresult = SELRESULT_DIFF;
+ return;
+ }
+ break;
+
+ case RESULT_DIFF:
+ switch (bestAccepted) {
+ case RESULT_EQUAL:
+ result.valresult = VALRESULT_ACCEPT;
+ result.selresult = SELRESULT_EQUAL;
+ return;
+
+ case RESULT_SEL:
+ result.valresult = VALRESULT_ACCEPT;
+ result.selresult = SELRESULT_DIFF;
+ return;
+
+ case RESULT_DIFF:
+ result.valresult = VALRESULT_DIFF;
+ result.selresult = SELRESULT_NA;
+ return;
+ }
+ break;
+ }
+
+ throw INTERNAL_ERR + HTML_COMPARISON;
+}
+
+/**
+ * Verify that the canaries are unviolated.
+ *
+ * @param container {Object} the test container descriptor as object reference
+ * @param result {Object} object reference that contains the result data
+ * @return {Boolean} whether the canaries' HTML is OK (selection flagged, but not fatal)
+ */
+function verifyCanaries(container, result) {
+ if (!container.canary) {
+ return true;
+ }
+
+ var str = canonicalizeElementsAndAttributes(result.bodyInnerHTML, emitFlagsForCanary);
+
+ if (str.length < 2 * container.canary.length) {
+ result.valresult = VALRESULT_CANARY;
+ result.selresult = SELRESULT_NA;
+ result.output = result.bodyOuterHTML;
+ return false;
+ }
+
+ var strBefore = str.substr(0, container.canary.length);
+ var strAfter = str.substr(str.length - container.canary.length);
+
+ // Verify that the canary stretch doesn't contain any selection markers
+ if (SELECTION_MARKERS.test(strBefore) || SELECTION_MARKERS.test(strAfter)) {
+ str = str.replace(SELECTION_MARKERS, '');
+ if (str.length < 2 * container.canary.length) {
+ result.valresult = VALRESULT_CANARY;
+ result.selresult = SELRESULT_NA;
+ result.output = result.bodyOuterHTML;
+ return false;
+ }
+
+ // Selection escaped contentEditable element, but HTML may still be ok.
+ result.selresult = SELRESULT_CANARY;
+ strBefore = str.substr(0, container.canary.length);
+ strAfter = str.substr(str.length - container.canary.length);
+ }
+
+ if (strBefore !== container.canary || strAfter !== container.canary) {
+ result.valresult = VALRESULT_CANARY;
+ result.selresult = SELRESULT_NA;
+ result.output = result.bodyOuterHTML;
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Compare the current HTMLtest result to the expectation string(s).
+ * Sets the global result variables.
+ *
+ * @param suite {Object} the test suite as object reference
+ * @param group {Object} group of tests within the suite the test belongs to
+ * @param test {Object} the test as object reference
+ * @param container {Object} the test container description
+ * @param result {Object} [in/out] the result description, incl. HTML strings
+ * @see variables.js for result values
+ */
+function compareHTMLTestResult(suite, group, test, container, result) {
+ if (!verifyCanaries(container, result)) {
+ return;
+ }
+
+ var emitFlags = {
+ emitAttrs: getTestParameter(suite, group, test, PARAM_CHECK_ATTRIBUTES),
+ emitStyle: getTestParameter(suite, group, test, PARAM_CHECK_STYLE),
+ emitClass: getTestParameter(suite, group, test, PARAM_CHECK_CLASS),
+ emitID: getTestParameter(suite, group, test, PARAM_CHECK_ID),
+ lowercase: true,
+ canonicalizeUnits: true
+ };
+
+ // 2a.) Compare opening tag -
+ // decide whether to compare vs. outer or inner HTML based on this.
+ var openingTagEnd = result.outerHTML.indexOf('>') + 1;
+ var openingTag = result.outerHTML.substr(0, openingTagEnd);
+
+ openingTag = canonicalizeElementsAndAttributes(openingTag, emitFlags);
+ var tagCmp = compareHTMLToExpectation(openingTag, container.tagOpen, emitFlags);
+
+ if (tagCmp == RESULT_EQUAL) {
+ result.output = result.innerHTML;
+ compareHTMLTestResultTo(
+ getTestParameter(suite, group, test, PARAM_EXPECTED),
+ getTestParameter(suite, group, test, PARAM_ACCEPT),
+ result.innerHTML,
+ emitFlags,
+ result)
+ } else {
+ result.output = result.outerHTML;
+ compareHTMLTestResultTo(
+ getContainerParameter(suite, group, test, container, PARAM_EXPECTED_OUTER),
+ getContainerParameter(suite, group, test, container, PARAM_ACCEPT_OUTER),
+ result.outerHTML,
+ emitFlags,
+ result)
+ }
+}
+
+/**
+ * Insert a selection position indicator.
+ *
+ * @param node {DOMNode} the node where to insert the selection indicator
+ * @param offs {Integer} the offset of the selection indicator
+ * @param textInd {String} the indicator to use if the node is a text node
+ * @param elemInd {String} the indicator to use if the node is an element node
+ */
+function insertSelectionIndicator(node, offs, textInd, elemInd) {
+ switch (node.nodeType) {
+ case DOM_NODE_TYPE_TEXT:
+ // Insert selection marker for text node into text content.
+ var text = node.data;
+ node.data = text.substring(0, offs) + textInd + text.substring(offs);
+ break;
+
+ case DOM_NODE_TYPE_ELEMENT:
+ var child = node.firstChild;
+ try {
+ // node has other children: insert marker as comment node
+ var comment = document.createComment(elemInd);
+ while (child && offs) {
+ --offs;
+ child = child.nextSibling;
+ }
+ if (child) {
+ node.insertBefore(comment, child);
+ } else {
+ node.appendChild(comment);
+ }
+ } catch (ex) {
+ // can't append child comment -> insert as special attribute(s)
+ switch (elemInd) {
+ case '|':
+ node.setAttribute(ATTRNAME_SEL_START, '1');
+ node.setAttribute(ATTRNAME_SEL_END, '1');
+ break;
+
+ case '{':
+ node.setAttribute(ATTRNAME_SEL_START, '1');
+ break;
+
+ case '}':
+ node.setAttribute(ATTRNAME_SEL_END, '1');
+ break;
+ }
+ }
+ break;
+ }
+}
+
+/**
+ * Adds quotes around all text nodes to show cases with non-normalized
+ * text nodes. Those are not a bug, but may still be usefil in helping to
+ * debug erroneous cases.
+ *
+ * @param node {DOMNode} root node from which to descend
+ */
+function encloseTextNodesWithQuotes(node) {
+ switch (node.nodeType) {
+ case DOM_NODE_TYPE_ELEMENT:
+ for (var i = 0; i < node.childNodes.length; ++i) {
+ encloseTextNodesWithQuotes(node.childNodes[i]);
+ }
+ break;
+
+ case DOM_NODE_TYPE_TEXT:
+ node.data = '\x60' + node.data + '\xb4';
+ break;
+ }
+}
+
+/**
+ * Retrieve the result of a test run and do some preliminary canonicalization.
+ *
+ * @param container {Object} the container where to retrieve the result from as object reference
+ * @param result {Object} object reference that contains the result data
+ * @return {String} a preliminarily canonicalized innerHTML with selection markers
+ */
+function prepareHTMLTestResult(container, result) {
+ // Start with empty strings in case any of the below throws.
+ result.innerHTML = '';
+ result.outerHTML = '';
+
+ // 1.) insert selection markers
+ var selRange = createFromWindow(container.win);
+ if (selRange) {
+ // save values, since range object gets auto-modified
+ var node1 = selRange.getAnchorNode();
+ var offs1 = selRange.getAnchorOffset();
+ var node2 = selRange.getFocusNode();
+ var offs2 = selRange.getFocusOffset();
+
+ // add markers
+ if (node1 && node1 == node2 && offs1 == offs2) {
+ // collapsed selection
+ insertSelectionIndicator(node1, offs1, '^', '|');
+ } else {
+ // Start point and end point are different
+ if (node1) {
+ insertSelectionIndicator(node1, offs1, '[', '{');
+ }
+
+ if (node2) {
+ if (node1 == node2 && offs1 < offs2) {
+ // Anchor indicator was inserted under the same node, so we need
+ // to shift the offset by 1
+ ++offs2;
+ }
+ insertSelectionIndicator(node2, offs2, ']', '}');
+ }
+ }
+ }
+
+ // 2.) insert markers for text node boundaries;
+ encloseTextNodesWithQuotes(container.editor);
+
+ // 3.) retrieve inner and outer HTML
+ result.innerHTML = initialCanonicalizationOf(container.editor.innerHTML);
+ result.bodyInnerHTML = initialCanonicalizationOf(container.body.innerHTML);
+ if (goog.userAgent.IE) {
+ result.outerHTML = initialCanonicalizationOf(container.editor.outerHTML);
+ result.bodyOuterHTML = initialCanonicalizationOf(container.body.outerHTML);
+ result.outerHTML = result.outerHTML.replace(/^\s+/, '');
+ result.outerHTML = result.outerHTML.replace(/\s+$/, '');
+ result.bodyOuterHTML = result.bodyOuterHTML.replace(/^\s+/, '');
+ result.bodyOuterHTML = result.bodyOuterHTML.replace(/\s+$/, '');
+ } else {
+ result.outerHTML = initialCanonicalizationOf(new XMLSerializer().serializeToString(container.editor));
+ result.bodyOuterHTML = initialCanonicalizationOf(new XMLSerializer().serializeToString(container.body));
+ }
+}
+
+/**
+ * Compare a text test result to the expectation string(s).
+ *
+ * @param suite {Object} the test suite as object reference
+ * @param group {Object} group of tests within the suite the test belongs to
+ * @param test {Object} the test as object reference
+ * @param actual {String/Boolean} actual value
+ * @param expected {String/Array} expectation(s)
+ * @return {Boolean} whether we found a match
+ */
+function compareTextTestResultWith(suite, group, test, actual, expected) {
+ var expectedArr = getExpectationArray(expected);
+ // Find the most favorable result among the possible expectation strings.
+ var count = expectedArr.length;
+
+ // If the value matches the expectation exactly, then we're fine.
+ for (var idx = 0; idx < count; ++idx) {
+ if (actual === expectedArr[idx])
+ return true;
+ }
+
+ // Otherwise see if we should canonicalize specific value types.
+ //
+ // We only need to look at font name, color and size units if the originating
+ // test was both a) queryCommandValue and b) querying a font name/color/size
+ // specific criterion.
+ //
+ // TODO(rolandsteiner): This is ugly! Refactor!
+ switch (getTestParameter(suite, group, test, PARAM_QUERYCOMMANDVALUE)) {
+ case 'backcolor':
+ case 'forecolor':
+ case 'hilitecolor':
+ for (var idx = 0; idx < count; ++idx) {
+ if (new Color(actual).compare(new Color(expectedArr[idx])))
+ return true;
+ }
+ return false;
+
+ case 'fontname':
+ for (var idx = 0; idx < count; ++idx) {
+ if (new FontName(actual).compare(new FontName(expectedArr[idx])))
+ return true;
+ }
+ return false;
+
+ case 'fontsize':
+ for (var idx = 0; idx < count; ++idx) {
+ if (new FontSize(actual).compare(new FontSize(expectedArr[idx])))
+ return true;
+ }
+ return false;
+ }
+
+ return false;
+}
+
+/**
+ * Compare the passed-in text test result to the expectation string(s).
+ * Sets the global result variables.
+ *
+ * @param suite {Object} the test suite as object reference
+ * @param group {Object} group of tests within the suite the test belongs to
+ * @param test {Object} the test as object reference
+ * @param actual {String/Boolean} actual value
+ * @return {Integer} a RESUTLHTML... result value
+ * @see variables.js for result values
+ */
+function compareTextTestResult(suite, group, test, result) {
+ var expected = getTestParameter(suite, group, test, PARAM_EXPECTED);
+ if (compareTextTestResultWith(suite, group, test, result.output, expected)) {
+ result.valresult = VALRESULT_EQUAL;
+ return;
+ }
+ var accepted = getTestParameter(suite, group, test, PARAM_ACCEPT);
+ if (accepted && compareTextTestResultWith(suite, group, test, result.output, accepted)) {
+ result.valresult = VALRESULT_ACCEPT;
+ return;
+ }
+ result.valresult = VALRESULT_DIFF;
+}
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/output.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/output.js
new file mode 100644
index 000000000..897efa011
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/output.js
@@ -0,0 +1,456 @@
+/**
+ * @fileoverview
+ * Functions used to format the test result output.
+ *
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License')
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @version 0.1
+ * @author rolandsteiner@google.com
+ */
+
+/**
+ * Writes a fatal error to the output (replaces alert box)
+ *
+ * @param text {String} text to output
+ */
+function writeFatalError(text) {
+ var errorsStart = document.getElementById('errors');
+ var divider = document.getElementById('divider');
+ if (!errorsStart) {
+ errorsStart = document.createElement('hr');
+ errorsStart.id = 'errors';
+ divider.parentNode.insertBefore(errorsStart, divider);
+ }
+ var error = document.createElement('div');
+ error.className = 'fatalerror';
+ error.innerHTML = 'FATAL ERROR: ' + escapeOutput(text);
+ errorsStart.parentNode.insertBefore(error, divider);
+}
+
+/**
+ * Generates a unique ID for a given single test out of the suite ID and
+ * test ID.
+ *
+ * @param suiteID {string} ID string of the suite
+ * @param testID {string} ID string of the individual tests
+ * @return {string} globally unique ID
+ */
+function generateOutputID(suiteID, testID) {
+ return commonIDPrefix + '-' + suiteID + '_' + testID;
+}
+
+/**
+ * Function to highlight the selection markers
+ *
+ * @param str {String} a HTML string containing selection markers
+ * @return {String} the HTML string with highlighting tags around the markers
+ */
+function highlightSelectionMarkers(str) {
+ str = str.replace(/\[/g, '<span class="sel">[</span>');
+ str = str.replace(/\]/g, '<span class="sel">]</span>');
+ str = str.replace(/\^/g, '<span class="sel">^</span>');
+ str = str.replace(/{/g, '<span class="sel">{</span>');
+ str = str.replace(/}/g, '<span class="sel">}</span>');
+ str = str.replace(/\|/g, '<b class="sel">|</b>');
+ return str;
+}
+
+/**
+ * Function to highlight the selection markers
+ *
+ * @param str {String} a HTML string containing selection markers
+ * @return {String} the HTML string with highlighting tags around the markers
+ */
+function highlightSelectionMarkersAndTextNodes(str) {
+ str = highlightSelectionMarkers(str);
+ str = str.replace(/\x60/g, '<span class="txt">');
+ str = str.replace(/\xb4/g, '</span>');
+ return str;
+}
+
+/**
+ * Function to format output according to type
+ *
+ * @param value {String/Boolean} string or value to format
+ * @return {String} HTML-formatted string
+ */
+function formatValueOrString(value) {
+ if (value === undefined)
+ return '<i>undefined</i>';
+ if (value === null)
+ return '<i>null</i>';
+
+ switch (typeof value) {
+ case 'boolean':
+ return '<i>' + value.toString() + '</i>';
+
+ case 'number':
+ return value.toString();
+
+ case 'string':
+ return "'" + escapeOutput(value) + "'";
+
+ default:
+ return '<i>(' + escapeOutput(value.toString()) + ')</i>';
+ }
+}
+
+/**
+ * Function to highlight text nodes
+ *
+ * @param suite {Object} the suite the test belongs to
+ * @param group {Object} the group within the suite the test belongs to
+ * @param test {Object} the test description as object reference
+ * @param actual {String} a HTML string containing text nodes with markers
+ * @return {String} string with highlighting tags around the text node parts
+ */
+function formatActualResult(suite, group, test, actual) {
+ if (typeof actual != 'string')
+ return formatValueOrString(actual);
+
+ actual = escapeOutput(actual);
+
+ // Fade attributes (or just style) if not actually tested for
+ if (!getTestParameter(suite, group, test, PARAM_CHECK_ATTRIBUTES)) {
+ actual = actual.replace(/([^ =]+)=\x22([^\x22]*)\x22/g, '<span class="fade">$1="$2"</span>');
+ } else {
+ // NOTE: convert 'class="..."' first, before adding other <span class="fade">...</span> !!!
+ if (!getTestParameter(suite, group, test, PARAM_CHECK_CLASS)) {
+ actual = actual.replace(/class=\x22([^\x22]*)\x22/g, '<span class="fade">class="$1"</span>');
+ }
+ if (!getTestParameter(suite, group, test, PARAM_CHECK_STYLE)) {
+ actual = actual.replace(/style=\x22([^\x22]*)\x22/g, '<span class="fade">style="$1"</span>');
+ }
+ if (!getTestParameter(suite, group, test, PARAM_CHECK_ID)) {
+ actual = actual.replace(/id=\x22([^\x22]*)\x22/g, '<span class="fade">id="$1"</span>');
+ } else {
+ // fade out contenteditable host element's 'editor-<xyz>' ID.
+ actual = actual.replace(/id=\x22editor-([^\x22]*)\x22/g, '<span class="fade">id="editor-$1"</span>');
+ }
+ // grey out 'xmlns'
+ actual = actual.replace(/xmlns=\x22([^\x22]*)\x22/g, '<span class="fade">xmlns="$1"</span>');
+ // remove 'onload'
+ actual = actual.replace(/onload=\x22[^\x22]*\x22 ?/g, '');
+ }
+ // Highlight selection markers and text nodes.
+ actual = highlightSelectionMarkersAndTextNodes(actual);
+
+ return actual;
+}
+
+/**
+ * Escape text content for use with .innerHTML.
+ *
+ * @param str {String} HTML text to displayed
+ * @return {String} the escaped HTML
+ */
+function escapeOutput(str) {
+ return str ? str.replace(/\</g, '&lt;').replace(/\>/g, '&gt;') : '';
+}
+
+/**
+ * Fills in a single output table cell
+ *
+ * @param id {String} ID of the table cell
+ * @param val {String} inner HTML to set
+ * @param ttl {String, optional} value of the 'title' attribute
+ * @param cls {String, optional} class name for the cell
+ */
+function setTD(id, val, ttl, cls) {
+ var td = document.getElementById(id);
+ if (td) {
+ td.innerHTML = val;
+ if (ttl) {
+ td.title = ttl;
+ }
+ if (cls) {
+ td.className = cls;
+ }
+ }
+}
+
+/**
+ * Outputs the results of a single test suite
+ *
+ * @param suite {Object} test suite as object reference
+ * @param clsID {String} test class ID ('Proposed', 'RFC', 'Final')
+ * @param group {Object} the group of tests within the suite the test belongs to
+ * @param testIdx {Object} the test as object reference
+ */
+function outputTestResults(suite, clsID, group, test) {
+ var suiteID = suite.id;
+ var cls = suite[clsID];
+ var trID = generateOutputID(suiteID, test.id);
+ var testResult = results[suiteID][clsID][test.id];
+ var testValOut = VALOUTPUT[testResult.valresult];
+ var testSelOut = SELOUTPUT[testResult.selresult];
+
+ var suiteChecksSelOnly = !suiteChecksHTMLOrText(suite);
+ var testUsesHTML = !!getTestParameter(suite, group, test, PARAM_EXECCOMMAND) ||
+ !!getTestParameter(suite, group, test, PARAM_FUNCTION);
+
+ // Set background color for test ID
+ var td = document.getElementById(trID + IDOUT_TESTID);
+ if (td) {
+ td.className = (suiteChecksSelOnly && testResult.selresult != SELRESULT_NA) ? testSelOut.css : testValOut.css;
+ }
+
+ // Fill in "Command" and "Value" cells
+ var cmd;
+ var cmdOutput = '&nbsp;';
+ var valOutput = '&nbsp;';
+
+ if (cmd = getTestParameter(suite, group, test, PARAM_EXECCOMMAND)) {
+ cmdOutput = escapeOutput(cmd);
+ var val = getTestParameter(suite, group, test, PARAM_VALUE);
+ if (val !== undefined) {
+ valOutput = formatValueOrString(val);
+ }
+ } else if (cmd = getTestParameter(suite, group, test, PARAM_FUNCTION)) {
+ cmdOutput = '<i>' + escapeOutput(cmd) + '</i>';
+ } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDSUPPORTED)) {
+ cmdOutput = '<i>queryCommandSupported</i>';
+ valOutput = escapeOutput(cmd);
+ } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDENABLED)) {
+ cmdOutput = '<i>queryCommandEnabled</i>';
+ valOutput = escapeOutput(cmd);
+ } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDINDETERM)) {
+ cmdOutput = '<i>queryCommandIndeterm</i>';
+ valOutput = escapeOutput(cmd);
+ } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDSTATE)) {
+ cmdOutput = '<i>queryCommandState</i>';
+ valOutput = escapeOutput(cmd);
+ } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDVALUE)) {
+ cmdOutput = '<i>queryCommandValue</i>';
+ valOutput = escapeOutput(cmd);
+ } else {
+ cmdOutput = '<i>(none)</i>';
+ }
+ setTD(trID + IDOUT_COMMAND, cmdOutput);
+ setTD(trID + IDOUT_VALUE, valOutput);
+
+ // Fill in "Attribute checked?" and "Style checked?" cells
+ if (testUsesHTML) {
+ var checkAttrs = getTestParameter(suite, group, test, PARAM_CHECK_ATTRIBUTES);
+ var checkStyle = getTestParameter(suite, group, test, PARAM_CHECK_STYLE);
+
+ setTD(trID + IDOUT_CHECKATTRS,
+ checkAttrs ? OUTSTR_YES : OUTSTR_NO,
+ checkAttrs ? 'attributes must match' : 'attributes are ignored');
+
+ if (checkAttrs && checkStyle) {
+ setTD(trID + IDOUT_CHECKSTYLE, OUTSTR_YES, 'style attribute contents must match');
+ } else if (checkAttrs) {
+ setTD(trID + IDOUT_CHECKSTYLE, OUTSTR_NO, 'style attribute contents is ignored');
+ } else {
+ setTD(trID + IDOUT_CHECKSTYLE, OUTSTR_NO, 'all attributes (incl. style) are ignored');
+ }
+ } else {
+ setTD(trID + IDOUT_CHECKATTRS, OUTSTR_NA, 'attributes not applicable');
+ setTD(trID + IDOUT_CHECKSTYLE, OUTSTR_NA, 'style not applicable');
+ }
+
+ // Fill in test pad specification cell (initial HTML + selection markers)
+ setTD(trID + IDOUT_PAD, highlightSelectionMarkers(escapeOutput(getTestParameter(suite, group, test, PARAM_PAD))));
+
+ // Fill in expected result(s) cell
+ var expectedOutput = '';
+ var expectedArr = getExpectationArray(getTestParameter(suite, group, test, PARAM_EXPECTED));
+ for (var idx = 0; idx < expectedArr.length; ++idx) {
+ if (expectedOutput) {
+ expectedOutput += '\xA0\xA0\xA0<i>or</i><br>';
+ }
+ expectedOutput += testUsesHTML ? highlightSelectionMarkers(escapeOutput(expectedArr[idx]))
+ : formatValueOrString(expectedArr[idx]);
+ }
+ var acceptedArr = getExpectationArray(getTestParameter(suite, group, test, PARAM_ACCEPT));
+ for (var idx = 0; idx < acceptedArr.length; ++idx) {
+ expectedOutput += '<span class="accexp">\xA0\xA0\xA0<i>or</i></span><br><span class="accexp">';
+ expectedOutput += testUsesHTML ? highlightSelectionMarkers(escapeOutput(acceptedArr[idx]))
+ : formatValueOrString(acceptedArr[idx]);
+ expectedOutput += '</span>';
+ }
+ // TODO(rolandsteiner): THIS IS UGLY, relying on 'div' container being index 2,
+ // AND not allowing other containers to have 'outer' results - change!!!
+ var outerOutput = '';
+ expectedArr = getExpectationArray(getContainerParameter(suite, group, test, containers[2], PARAM_EXPECTED_OUTER));
+ for (var idx = 0; idx < expectedArr.length; ++idx) {
+ if (outerOutput) {
+ outerOutput += '\xA0\xA0\xA0<i>or</i><br>';
+ }
+ outerOutput += testUsesHTML ? highlightSelectionMarkers(escapeOutput(expectedArr[idx]))
+ : formatValueOrString(expectedArr[idx]);
+ }
+ acceptedArr = getExpectationArray(getContainerParameter(suite, group, test, containers[2], PARAM_ACCEPT_OUTER));
+ for (var idx = 0; idx < acceptedArr.length; ++idx) {
+ if (outerOutput) {
+ outerOutput += '<span class="accexp">\xA0\xA0\xA0<i>or</i></span><br>';
+ }
+ outerOutput += '<span class="accexp">';
+ outerOutput += testUsesHTML ? highlightSelectionMarkers(escapeOutput(acceptedArr[idx]))
+ : formatValueOrString(acceptedArr[idx]);
+ outerOutput += '</span>';
+ }
+ if (outerOutput) {
+ expectedOutput += '<hr>' + outerOutput;
+ }
+ setTD(trID + IDOUT_EXPECTED, expectedOutput);
+
+ // Iterate over the individual container results
+ for (var cntIdx = 0; cntIdx < containers.length; ++cntIdx) {
+ var cntID = containers[cntIdx].id;
+ var cntTD = document.getElementById(trID + IDOUT_CONTAINER + cntID);
+ var cntResult = testResult[cntID];
+ var cntValOut = VALOUTPUT[cntResult.valresult];
+ var cntSelOut = SELOUTPUT[cntResult.selresult];
+ var cssVal = cntValOut.css;
+ var cssSel = (!suiteChecksSelOnly || cntResult.selresult != SELRESULT_NA) ? cntSelOut.css : cssVal;
+ var cssCnt = cssVal;
+
+ // Fill in result status cell ("PASS", "ACC.", "FAIL", "EXC.", etc.)
+ setTD(trID + IDOUT_STATUSVAL + cntID, cntValOut.output, cntValOut.title, cssVal);
+
+ // Fill in selection status cell ("PASS", "ACC.", "FAIL", "N/A")
+ setTD(trID + IDOUT_STATUSSEL + cntID, cntSelOut.output, cntSelOut.title, cssSel);
+
+ // Fill in actual result
+ switch (cntResult.valresult) {
+ case VALRESULT_SETUP_EXCEPTION:
+ setTD(trID + IDOUT_ACTUAL + cntID,
+ SETUP_EXCEPTION + '(mouseover)',
+ escapeOutput(cntResult.output),
+ cssVal);
+ break;
+
+ case VALRESULT_EXECUTION_EXCEPTION:
+ setTD(trID + IDOUT_ACTUAL + cntID,
+ EXECUTION_EXCEPTION + '(mouseover)',
+ escapeOutput(cntResult.output.toString()),
+ cssVal);
+ break;
+
+ case VALRESULT_VERIFICATION_EXCEPTION:
+ setTD(trID + IDOUT_ACTUAL + cntID,
+ VERIFICATION_EXCEPTION + '(mouseover)',
+ escapeOutput(cntResult.output.toString()),
+ cssVal);
+ break;
+
+ case VALRESULT_UNSUPPORTED:
+ setTD(trID + IDOUT_ACTUAL + cntID,
+ escapeOutput(cntResult.output),
+ '',
+ cssVal);
+ break;
+
+ case VALRESULT_CANARY:
+ setTD(trID + IDOUT_ACTUAL + cntID,
+ highlightSelectionMarkersAndTextNodes(escapeOutput(cntResult.output)),
+ '',
+ cssVal);
+ break;
+
+ case VALRESULT_DIFF:
+ case VALRESULT_ACCEPT:
+ case VALRESULT_EQUAL:
+ if (!testUsesHTML) {
+ setTD(trID + IDOUT_ACTUAL + cntID,
+ formatValueOrString(cntResult.output),
+ '',
+ cssVal);
+ } else if (cntResult.selresult == SELRESULT_CANARY) {
+ cssCnt = suiteChecksSelOnly ? cssSel : cssVal;
+ setTD(trID + IDOUT_ACTUAL + cntID,
+ highlightSelectionMarkersAndTextNodes(escapeOutput(cntResult.output)),
+ '',
+ cssCnt);
+ } else {
+ cssCnt = suiteChecksSelOnly ? cssSel : cssVal;
+ setTD(trID + IDOUT_ACTUAL + cntID,
+ formatActualResult(suite, group, test, cntResult.output),
+ '',
+ cssCnt);
+ }
+ break;
+
+ default:
+ cssCnt = 'exception';
+ setTD(trID + IDOUT_ACTUAL + cntID,
+ INTERNAL_ERR + 'UNKNOWN RESULT VALUE',
+ '',
+ cssCnt);
+ }
+
+ if (cntTD) {
+ cntTD.className = cssCnt;
+ }
+ }
+}
+
+/**
+ * Outputs the results of a single test suite
+ *
+ * @param {Object} suite as object reference
+ */
+function outputTestSuiteResults(suite) {
+ var suiteID = suite.id;
+ var span;
+
+ span = document.getElementById(suiteID + '-score');
+ if (span) {
+ span.innerHTML = results[suiteID].valscore + '/' + results[suiteID].count;
+ }
+ span = document.getElementById(suiteID + '-selscore');
+ if (span) {
+ span.innerHTML = results[suiteID].selscore + '/' + results[suiteID].count;
+ }
+ span = document.getElementById(suiteID + '-time');
+ if (span) {
+ span.innerHTML = results[suiteID].time;
+ }
+ span = document.getElementById(suiteID + '-progress');
+ if (span) {
+ span.style.color = 'green';
+ }
+
+ for (var clsIdx = 0; clsIdx < testClassCount; ++clsIdx) {
+ var clsID = testClassIDs[clsIdx];
+ var cls = suite[clsID];
+ if (!cls)
+ continue;
+
+ span = document.getElementById(suiteID + '-' + clsID + '-score');
+ if (span) {
+ span.innerHTML = results[suiteID][clsID].valscore + '/' + results[suiteID][clsID].count;
+ }
+ span = document.getElementById(suiteID + '-' + clsID + '-selscore');
+ if (span) {
+ span.innerHTML = results[suiteID][clsID].selscore + '/' + results[suiteID][clsID].count;
+ }
+
+ var groupCount = cls.length;
+
+ for (var groupIdx = 0; groupIdx < groupCount; ++groupIdx) {
+ var group = cls[groupIdx];
+ var testCount = group.tests.length;
+
+ for (var testIdx = 0; testIdx < testCount; ++testIdx) {
+ var test = group.tests[testIdx];
+
+ outputTestResults(suite, clsID, group, test);
+ }
+ }
+ }
+}
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/pad.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/pad.js
new file mode 100644
index 000000000..282f0d907
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/pad.js
@@ -0,0 +1,269 @@
+/**
+ * @fileoverview
+ * Functions used to handle test and expectation strings.
+ *
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License')
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @version 0.1
+ * @author rolandsteiner@google.com
+ */
+
+/**
+ * Normalize text selection indicators and convert inter-element selection
+ * indicators to comments.
+ *
+ * Note that this function relies on the spaces of the input string already
+ * having been normalized by canonicalizeSpaces!
+ *
+ * @param pad {String} HTML string that includes selection marker characters
+ * @return {String} the HTML string with the selection markers converted
+ */
+function convertSelectionIndicators(pad) {
+ // Sanity check: Markers { } | only directly before or after an element,
+ // or just before a closing > (i.e., not within a text node).
+ // Note that intra-tag selection markers have already been converted to the
+ // special selection attribute(s) above.
+ if (/[^>][{}\|][^<>]/.test(pad) ||
+ /^[{}\|][^<]/.test(pad) ||
+ /[^>][{}\|]$/.test(pad) ||
+ /^[{}\|]*$/.test(pad)) {
+ throw SETUP_BAD_SELECTION_SPEC;
+ }
+
+ // Convert intra-tag selection markers to special attributes.
+ pad = pad.replace(/\{\>/g, ATTRNAME_SEL_START + '="1">');
+ pad = pad.replace(/\}\>/g, ATTRNAME_SEL_END + '="1">');
+ pad = pad.replace(/\|\>/g, ATTRNAME_SEL_START + '="1" ' +
+ ATTRNAME_SEL_END + '="1">');
+
+ // Convert remaining {, }, | to comments with '[' and ']' data.
+ pad = pad.replace('{', '<!--[-->');
+ pad = pad.replace('}', '<!--]-->');
+ pad = pad.replace('|', '<!--[--><!--]-->');
+
+ // Convert caret indicator ^ to empty selection indicator []
+ // (this simplifies further processing).
+ pad = pad.replace(/\^/, '[]');
+
+ return pad;
+}
+
+/**
+ * Derives one point of the selection from the indicators with the HTML tree:
+ * '[' or ']' within a text or comment node, or the special selection
+ * attributes within an element node.
+ *
+ * @param root {DOMNode} root node of the recursive search
+ * @param marker {String} which marker to look for: '[' or ']'
+ * @return {Object} a pair object: {node: {DOMNode}/null, offset: {Integer}}
+ */
+function deriveSelectionPoint(root, marker) {
+ switch (root.nodeType) {
+ case DOM_NODE_TYPE_ELEMENT:
+ if (root.attributes) {
+ // Note: getAttribute() is necessary for this to work on all browsers!
+ if (marker == '[' && root.getAttribute(ATTRNAME_SEL_START)) {
+ root.removeAttribute(ATTRNAME_SEL_START);
+ return {node: root, offs: 0};
+ }
+ if (marker == ']' && root.getAttribute(ATTRNAME_SEL_END)) {
+ root.removeAttribute(ATTRNAME_SEL_END);
+ return {node: root, offs: 0};
+ }
+ }
+ for (var i = 0; i < root.childNodes.length; ++i) {
+ var pair = deriveSelectionPoint(root.childNodes[i], marker);
+ if (pair.node) {
+ return pair;
+ }
+ }
+ break;
+
+ case DOM_NODE_TYPE_TEXT:
+ var pos = root.data.indexOf(marker);
+ if (pos != -1) {
+ // Remove selection marker from text.
+ var nodeText = root.data;
+ root.data = nodeText.substr(0, pos) + nodeText.substr(pos + 1);
+ return {node: root, offs: pos };
+ }
+ break;
+
+ case DOM_NODE_TYPE_COMMENT:
+ var pos = root.data.indexOf(marker);
+ if (pos != -1) {
+ // Remove comment node from parent.
+ var helper = root.previousSibling;
+
+ for (pos = 0; helper; ++pos ) {
+ helper = helper.previousSibling;
+ }
+ helper = root;
+ root = root.parentNode;
+ root.removeChild(helper);
+ return {node: root, offs: pos };
+ }
+ break;
+ }
+
+ return {node: null, offs: 0 };
+}
+
+/**
+ * Initialize the test HTML with the starting state specified in the test.
+ *
+ * The selection is specified "inline", using special characters:
+ * ^ a collapsed text caret selection (same as [])
+ * [ the selection start within a text node
+ * ] the selection end within a text node
+ * | collapsed selection between elements (same as {})
+ * { selection starting with the following element
+ * } selection ending with the preceding element
+ * {, } and | can also be used within an element tag, just before the closing
+ * angle bracket > to specify a selection [element, 0] where the element
+ * doesn't otherwise have any children. Ex.: <hr {>foobarbaz<hr }>
+ *
+ * Explicit and implicit specification can also be mixed between the 2 points.
+ *
+ * A pad string must only contain at most ONE of the above that is suitable for
+ * that start or end point, respectively, and must contain either both or none.
+ *
+ * @param suite {Object} suite that test originates in as object reference
+ * @param group {Object} group of tests within the suite the test belongs to
+ * @param test {Object} test to be run as object reference
+ * @param container {Object} container descriptor as object reference
+ */
+function initContainer(suite, group, test, container) {
+ var pad = getTestParameter(suite, group, test, PARAM_PAD);
+ pad = canonicalizeSpaces(pad);
+ pad = convertSelectionIndicators(pad);
+
+ if (container.editorID) {
+ container.body.innerHTML = container.canary + container.tagOpen + pad + container.tagClose + container.canary;
+ container.editor = container.doc.getElementById(container.editorID);
+ } else {
+ container.body.innerHTML = pad;
+ container.editor = container.body;
+ }
+
+ win = container.win;
+ doc = container.doc;
+ body = container.body;
+ editor = container.editor;
+ sel = null;
+
+ if (!editor) {
+ throw SETUP_CONTAINER;
+ }
+
+ if (getTestParameter(suite, group, test, PARAM_STYLE_WITH_CSS)) {
+ try {
+ container.doc.execCommand('styleWithCSS', false, true);
+ } catch (ex) {
+ // ignore exception if unsupported
+ }
+ }
+
+ var selAnchor = deriveSelectionPoint(editor, '[');
+ var selFocus = deriveSelectionPoint(editor, ']');
+
+ // sanity check
+ if (!selAnchor || !selFocus) {
+ throw SETUP_SELECTION;
+ }
+
+ if (!selAnchor.node || !selFocus.node) {
+ if (selAnchor.node || selFocus.node) {
+ // Broken test: only one selection point was specified
+ throw SETUP_BAD_SELECTION_SPEC;
+ }
+ sel = null;
+ return;
+ }
+
+ if (selAnchor.node === selFocus.node) {
+ if (selAnchor.offs > selFocus.offs) {
+ // Both selection points are within the same node, the selection was
+ // specified inline (thus the end indicator element or character was
+ // removed), and the end point is before the start (reversed selection).
+ // Start offset that was derived is now off by 1 and needs adjustment.
+ --selAnchor.offs;
+ }
+
+ if (selAnchor.offs === selFocus.offs) {
+ createCaret(selAnchor.node, selAnchor.offs).select();
+ try {
+ sel = win.getSelection();
+ } catch (ex) {
+ sel = undefined;
+ }
+ return;
+ }
+ }
+
+ createFromNodes(selAnchor.node, selAnchor.offs, selFocus.node, selFocus.offs).select();
+
+ try {
+ sel = win.getSelection();
+ } catch (ex) {
+ sel = undefined;
+ }
+}
+
+/**
+ * Reset the editor element after a test is run.
+ *
+ * @param container {Object} container descriptor as object reference
+ */
+function resetContainer(container) {
+ // Remove errant styles and attributes that may have been set on the <body>.
+ container.body.removeAttribute('style');
+ container.body.removeAttribute('color');
+ container.body.removeAttribute('bgcolor');
+
+ try {
+ container.doc.execCommand('styleWithCSS', false, false);
+ } catch (ex) {
+ // Ignore exception if unsupported.
+ }
+}
+
+/**
+ * Initialize the editor document.
+ */
+function initEditorDocs() {
+ for (var c = 0; c < containers.length; ++c) {
+ var container = containers[c];
+
+ container.iframe = document.getElementById('iframe-' + container.id);
+ container.win = container.iframe.contentWindow;
+ container.doc = container.win.document;
+ container.body = container.doc.body;
+ // container.editor is set per test (changes on embedded editor elements).
+
+ // Some browsers require a selection to go with their 'styleWithCSS'.
+ try {
+ container.win.getSelection().selectAllChildren(editor);
+ } catch (ex) {
+ // ignore exception if unsupported
+ }
+ // Default styleWithCSS to false.
+ try {
+ container.doc.execCommand('styleWithCSS', false, false);
+ } catch (ex) {
+ // ignore exception if unsupported
+ }
+ }
+}
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/range-bootstrap.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/range-bootstrap.js
new file mode 100644
index 000000000..24aef7ae9
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/range-bootstrap.js
@@ -0,0 +1,5 @@
+goog.require('goog.dom.Range');
+
+window.createFromWindow = goog.dom.Range.createFromWindow;
+window.createFromNodes = goog.dom.Range.createFromNodes;
+window.createCaret = goog.dom.Range.createCaret;
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/range.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/range.js
new file mode 100644
index 000000000..f323cf9b6
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/range.js
@@ -0,0 +1,6184 @@
+var COMPILED = false;
+var goog = goog || {};
+goog.global = this;
+goog.DEBUG = true;
+goog.LOCALE = "en";
+goog.evalWorksForGlobals_ = null;
+goog.provide = function(name) {
+ if(!COMPILED) {
+ if(goog.getObjectByName(name) && !goog.implicitNamespaces_[name]) {
+ throw Error('Namespace "' + name + '" already declared.');
+ }
+ var namespace = name;
+ while(namespace = namespace.substring(0, namespace.lastIndexOf("."))) {
+ goog.implicitNamespaces_[namespace] = true
+ }
+ }
+ goog.exportPath_(name)
+};
+if(!COMPILED) {
+ goog.implicitNamespaces_ = {}
+}
+goog.exportPath_ = function(name, opt_object, opt_objectToExportTo) {
+ var parts = name.split(".");
+ var cur = opt_objectToExportTo || goog.global;
+ if(!(parts[0] in cur) && cur.execScript) {
+ cur.execScript("var " + parts[0])
+ }
+ for(var part;parts.length && (part = parts.shift());) {
+ if(!parts.length && goog.isDef(opt_object)) {
+ cur[part] = opt_object
+ }else {
+ if(cur[part]) {
+ cur = cur[part]
+ }else {
+ cur = cur[part] = {}
+ }
+ }
+ }
+};
+goog.getObjectByName = function(name, opt_obj) {
+ var parts = name.split(".");
+ var cur = opt_obj || goog.global;
+ for(var part;part = parts.shift();) {
+ if(cur[part]) {
+ cur = cur[part]
+ }else {
+ return null
+ }
+ }
+ return cur
+};
+goog.globalize = function(obj, opt_global) {
+ var global = opt_global || goog.global;
+ for(var x in obj) {
+ global[x] = obj[x]
+ }
+};
+goog.addDependency = function(relPath, provides, requires) {
+ if(!COMPILED) {
+ var provide, require;
+ var path = relPath.replace(/\\/g, "/");
+ var deps = goog.dependencies_;
+ for(var i = 0;provide = provides[i];i++) {
+ deps.nameToPath[provide] = path;
+ if(!(path in deps.pathToNames)) {
+ deps.pathToNames[path] = {}
+ }
+ deps.pathToNames[path][provide] = true
+ }
+ for(var j = 0;require = requires[j];j++) {
+ if(!(path in deps.requires)) {
+ deps.requires[path] = {}
+ }
+ deps.requires[path][require] = true
+ }
+ }
+};
+goog.require = function(rule) {
+ if(!COMPILED) {
+ if(goog.getObjectByName(rule)) {
+ return
+ }
+ var path = goog.getPathFromDeps_(rule);
+ if(path) {
+ goog.included_[path] = true;
+ goog.writeScripts_()
+ }else {
+ var errorMessage = "goog.require could not find: " + rule;
+ if(goog.global.console) {
+ goog.global.console["error"](errorMessage)
+ }
+ throw Error(errorMessage);
+ }
+ }
+};
+goog.basePath = "";
+goog.global.CLOSURE_BASE_PATH;
+goog.nullFunction = function() {
+};
+goog.identityFunction = function(var_args) {
+ return arguments[0]
+};
+goog.abstractMethod = function() {
+ throw Error("unimplemented abstract method");
+};
+goog.addSingletonGetter = function(ctor) {
+ ctor.getInstance = function() {
+ return ctor.instance_ || (ctor.instance_ = new ctor)
+ }
+};
+if(!COMPILED) {
+ goog.included_ = {};
+ goog.dependencies_ = {pathToNames:{}, nameToPath:{}, requires:{}, visited:{}, written:{}};
+ goog.inHtmlDocument_ = function() {
+ var doc = goog.global.document;
+ return typeof doc != "undefined" && "write" in doc
+ };
+ goog.findBasePath_ = function() {
+ if(!goog.inHtmlDocument_()) {
+ return
+ }
+ var doc = goog.global.document;
+ if(goog.global.CLOSURE_BASE_PATH) {
+ goog.basePath = goog.global.CLOSURE_BASE_PATH;
+ return
+ }
+ var scripts = doc.getElementsByTagName("script");
+ for(var i = scripts.length - 1;i >= 0;--i) {
+ var src = scripts[i].src;
+ var l = src.length;
+ if(src.substr(l - 7) == "base.js") {
+ goog.basePath = src.substr(0, l - 7);
+ return
+ }
+ }
+ };
+ goog.writeScriptTag_ = function(src) {
+ if(goog.inHtmlDocument_() && !goog.dependencies_.written[src]) {
+ goog.dependencies_.written[src] = true;
+ var doc = goog.global.document;
+ doc.write('<script type="text/javascript" src="' + src + '"></' + "script>")
+ }
+ };
+ goog.writeScripts_ = function() {
+ var scripts = [];
+ var seenScript = {};
+ var deps = goog.dependencies_;
+ function visitNode(path) {
+ if(path in deps.written) {
+ return
+ }
+ if(path in deps.visited) {
+ if(!(path in seenScript)) {
+ seenScript[path] = true;
+ scripts.push(path)
+ }
+ return
+ }
+ deps.visited[path] = true;
+ if(path in deps.requires) {
+ for(var requireName in deps.requires[path]) {
+ if(requireName in deps.nameToPath) {
+ visitNode(deps.nameToPath[requireName])
+ }else {
+ if(!goog.getObjectByName(requireName)) {
+ throw Error("Undefined nameToPath for " + requireName);
+ }
+ }
+ }
+ }
+ if(!(path in seenScript)) {
+ seenScript[path] = true;
+ scripts.push(path)
+ }
+ }
+ for(var path in goog.included_) {
+ if(!deps.written[path]) {
+ visitNode(path)
+ }
+ }
+ for(var i = 0;i < scripts.length;i++) {
+ if(scripts[i]) {
+ goog.writeScriptTag_(goog.basePath + scripts[i])
+ }else {
+ throw Error("Undefined script input");
+ }
+ }
+ };
+ goog.getPathFromDeps_ = function(rule) {
+ if(rule in goog.dependencies_.nameToPath) {
+ return goog.dependencies_.nameToPath[rule]
+ }else {
+ return null
+ }
+ };
+ goog.findBasePath_();
+}
+goog.typeOf = function(value) {
+ var s = typeof value;
+ if(s == "object") {
+ if(value) {
+ if(value instanceof Array || !(value instanceof Object) && Object.prototype.toString.call(value) == "[object Array]" || typeof value.length == "number" && typeof value.splice != "undefined" && typeof value.propertyIsEnumerable != "undefined" && !value.propertyIsEnumerable("splice")) {
+ return"array"
+ }
+ if(!(value instanceof Object) && (Object.prototype.toString.call(value) == "[object Function]" || typeof value.call != "undefined" && typeof value.propertyIsEnumerable != "undefined" && !value.propertyIsEnumerable("call"))) {
+ return"function"
+ }
+ }else {
+ return"null"
+ }
+ }else {
+ if(s == "function" && typeof value.call == "undefined") {
+ return"object"
+ }
+ }
+ return s
+};
+goog.propertyIsEnumerableCustom_ = function(object, propName) {
+ if(propName in object) {
+ for(var key in object) {
+ if(key == propName && Object.prototype.hasOwnProperty.call(object, propName)) {
+ return true
+ }
+ }
+ }
+ return false
+};
+goog.propertyIsEnumerable_ = function(object, propName) {
+ if(object instanceof Object) {
+ return Object.prototype.propertyIsEnumerable.call(object, propName)
+ }else {
+ return goog.propertyIsEnumerableCustom_(object, propName)
+ }
+};
+goog.isDef = function(val) {
+ return val !== undefined
+};
+goog.isNull = function(val) {
+ return val === null
+};
+goog.isDefAndNotNull = function(val) {
+ return val != null
+};
+goog.isArray = function(val) {
+ return goog.typeOf(val) == "array"
+};
+goog.isArrayLike = function(val) {
+ var type = goog.typeOf(val);
+ return type == "array" || type == "object" && typeof val.length == "number"
+};
+goog.isDateLike = function(val) {
+ return goog.isObject(val) && typeof val.getFullYear == "function"
+};
+goog.isString = function(val) {
+ return typeof val == "string"
+};
+goog.isBoolean = function(val) {
+ return typeof val == "boolean"
+};
+goog.isNumber = function(val) {
+ return typeof val == "number"
+};
+goog.isFunction = function(val) {
+ return goog.typeOf(val) == "function"
+};
+goog.isObject = function(val) {
+ var type = goog.typeOf(val);
+ return type == "object" || type == "array" || type == "function"
+};
+goog.getUid = function(obj) {
+ return obj[goog.UID_PROPERTY_] || (obj[goog.UID_PROPERTY_] = ++goog.uidCounter_)
+};
+goog.removeUid = function(obj) {
+ if("removeAttribute" in obj) {
+ obj.removeAttribute(goog.UID_PROPERTY_)
+ }
+ try {
+ delete obj[goog.UID_PROPERTY_]
+ }catch(ex) {
+ }
+};
+goog.UID_PROPERTY_ = "closure_uid_" + Math.floor(Math.random() * 2147483648).toString(36);
+goog.uidCounter_ = 0;
+goog.getHashCode = goog.getUid;
+goog.removeHashCode = goog.removeUid;
+goog.cloneObject = function(obj) {
+ var type = goog.typeOf(obj);
+ if(type == "object" || type == "array") {
+ if(obj.clone) {
+ return obj.clone()
+ }
+ var clone = type == "array" ? [] : {};
+ for(var key in obj) {
+ clone[key] = goog.cloneObject(obj[key])
+ }
+ return clone
+ }
+ return obj
+};
+Object.prototype.clone;
+goog.bind = function(fn, selfObj, var_args) {
+ var context = selfObj || goog.global;
+ if(arguments.length > 2) {
+ var boundArgs = Array.prototype.slice.call(arguments, 2);
+ return function() {
+ var newArgs = Array.prototype.slice.call(arguments);
+ Array.prototype.unshift.apply(newArgs, boundArgs);
+ return fn.apply(context, newArgs)
+ }
+ }else {
+ return function() {
+ return fn.apply(context, arguments)
+ }
+ }
+};
+goog.partial = function(fn, var_args) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ return function() {
+ var newArgs = Array.prototype.slice.call(arguments);
+ newArgs.unshift.apply(newArgs, args);
+ return fn.apply(this, newArgs)
+ }
+};
+goog.mixin = function(target, source) {
+ for(var x in source) {
+ target[x] = source[x]
+ }
+};
+goog.now = Date.now || function() {
+ return+new Date
+};
+goog.globalEval = function(script) {
+ if(goog.global.execScript) {
+ goog.global.execScript(script, "JavaScript")
+ }else {
+ if(goog.global.eval) {
+ if(goog.evalWorksForGlobals_ == null) {
+ goog.global.eval("var _et_ = 1;");
+ if(typeof goog.global["_et_"] != "undefined") {
+ delete goog.global["_et_"];
+ goog.evalWorksForGlobals_ = true
+ }else {
+ goog.evalWorksForGlobals_ = false
+ }
+ }
+ if(goog.evalWorksForGlobals_) {
+ goog.global.eval(script)
+ }else {
+ var doc = goog.global.document;
+ var scriptElt = doc.createElement("script");
+ scriptElt.type = "text/javascript";
+ scriptElt.defer = false;
+ scriptElt.appendChild(doc.createTextNode(script));
+ doc.body.appendChild(scriptElt);
+ doc.body.removeChild(scriptElt)
+ }
+ }else {
+ throw Error("goog.globalEval not available");
+ }
+ }
+};
+goog.typedef = true;
+goog.cssNameMapping_;
+goog.getCssName = function(className, opt_modifier) {
+ var cssName = className + (opt_modifier ? "-" + opt_modifier : "");
+ return goog.cssNameMapping_ && cssName in goog.cssNameMapping_ ? goog.cssNameMapping_[cssName] : cssName
+};
+goog.setCssNameMapping = function(mapping) {
+ goog.cssNameMapping_ = mapping
+};
+goog.getMsg = function(str, opt_values) {
+ var values = opt_values || {};
+ for(var key in values) {
+ var value = ("" + values[key]).replace(/\$/g, "$$$$");
+ str = str.replace(new RegExp("\\{\\$" + key + "\\}", "gi"), value)
+ }
+ return str
+};
+goog.exportSymbol = function(publicPath, object, opt_objectToExportTo) {
+ goog.exportPath_(publicPath, object, opt_objectToExportTo)
+};
+goog.exportProperty = function(object, publicName, symbol) {
+ object[publicName] = symbol
+};
+goog.inherits = function(childCtor, parentCtor) {
+ function tempCtor() {
+ }
+ tempCtor.prototype = parentCtor.prototype;
+ childCtor.superClass_ = parentCtor.prototype;
+ childCtor.prototype = new tempCtor;
+ childCtor.prototype.constructor = childCtor
+};
+goog.base = function(me, opt_methodName, var_args) {
+ var caller = arguments.callee.caller;
+ if(caller.superClass_) {
+ return caller.superClass_.constructor.apply(me, Array.prototype.slice.call(arguments, 1))
+ }
+ var args = Array.prototype.slice.call(arguments, 2);
+ var foundCaller = false;
+ for(var ctor = me.constructor;ctor;ctor = ctor.superClass_ && ctor.superClass_.constructor) {
+ if(ctor.prototype[opt_methodName] === caller) {
+ foundCaller = true
+ }else {
+ if(foundCaller) {
+ return ctor.prototype[opt_methodName].apply(me, args)
+ }
+ }
+ }
+ if(me[opt_methodName] === caller) {
+ return me.constructor.prototype[opt_methodName].apply(me, args)
+ }else {
+ throw Error("goog.base called from a method of one name " + "to a method of a different name");
+ }
+};
+goog.scope = function(fn) {
+ fn.call(goog.global)
+};
+goog.provide("goog.debug.Error");
+goog.debug.Error = function(opt_msg) {
+ this.stack = (new Error).stack || "";
+ if(opt_msg) {
+ this.message = String(opt_msg)
+ }
+};
+goog.inherits(goog.debug.Error, Error);
+goog.debug.Error.prototype.name = "CustomError";
+goog.provide("goog.string");
+goog.provide("goog.string.Unicode");
+goog.string.Unicode = {NBSP:"\u00a0"};
+goog.string.startsWith = function(str, prefix) {
+ return str.lastIndexOf(prefix, 0) == 0
+};
+goog.string.endsWith = function(str, suffix) {
+ var l = str.length - suffix.length;
+ return l >= 0 && str.indexOf(suffix, l) == l
+};
+goog.string.caseInsensitiveStartsWith = function(str, prefix) {
+ return goog.string.caseInsensitiveCompare(prefix, str.substr(0, prefix.length)) == 0
+};
+goog.string.caseInsensitiveEndsWith = function(str, suffix) {
+ return goog.string.caseInsensitiveCompare(suffix, str.substr(str.length - suffix.length, suffix.length)) == 0
+};
+goog.string.subs = function(str, var_args) {
+ for(var i = 1;i < arguments.length;i++) {
+ var replacement = String(arguments[i]).replace(/\$/g, "$$$$");
+ str = str.replace(/\%s/, replacement)
+ }
+ return str
+};
+goog.string.collapseWhitespace = function(str) {
+ return str.replace(/[\s\xa0]+/g, " ").replace(/^\s+|\s+$/g, "")
+};
+goog.string.isEmpty = function(str) {
+ return/^[\s\xa0]*$/.test(str)
+};
+goog.string.isEmptySafe = function(str) {
+ return goog.string.isEmpty(goog.string.makeSafe(str))
+};
+goog.string.isBreakingWhitespace = function(str) {
+ return!/[^\t\n\r ]/.test(str)
+};
+goog.string.isAlpha = function(str) {
+ return!/[^a-zA-Z]/.test(str)
+};
+goog.string.isNumeric = function(str) {
+ return!/[^0-9]/.test(str)
+};
+goog.string.isAlphaNumeric = function(str) {
+ return!/[^a-zA-Z0-9]/.test(str)
+};
+goog.string.isSpace = function(ch) {
+ return ch == " "
+};
+goog.string.isUnicodeChar = function(ch) {
+ return ch.length == 1 && ch >= " " && ch <= "~" || ch >= "\u0080" && ch <= "\ufffd"
+};
+goog.string.stripNewlines = function(str) {
+ return str.replace(/(\r\n|\r|\n)+/g, " ")
+};
+goog.string.canonicalizeNewlines = function(str) {
+ return str.replace(/(\r\n|\r|\n)/g, "\n")
+};
+goog.string.normalizeWhitespace = function(str) {
+ return str.replace(/\xa0|\s/g, " ")
+};
+goog.string.normalizeSpaces = function(str) {
+ return str.replace(/\xa0|[ \t]+/g, " ")
+};
+goog.string.trim = function(str) {
+ return str.replace(/^[\s\xa0]+|[\s\xa0]+$/g, "")
+};
+goog.string.trimLeft = function(str) {
+ return str.replace(/^[\s\xa0]+/, "")
+};
+goog.string.trimRight = function(str) {
+ return str.replace(/[\s\xa0]+$/, "")
+};
+goog.string.caseInsensitiveCompare = function(str1, str2) {
+ var test1 = String(str1).toLowerCase();
+ var test2 = String(str2).toLowerCase();
+ if(test1 < test2) {
+ return-1
+ }else {
+ if(test1 == test2) {
+ return 0
+ }else {
+ return 1
+ }
+ }
+};
+goog.string.numerateCompareRegExp_ = /(\.\d+)|(\d+)|(\D+)/g;
+goog.string.numerateCompare = function(str1, str2) {
+ if(str1 == str2) {
+ return 0
+ }
+ if(!str1) {
+ return-1
+ }
+ if(!str2) {
+ return 1
+ }
+ var tokens1 = str1.toLowerCase().match(goog.string.numerateCompareRegExp_);
+ var tokens2 = str2.toLowerCase().match(goog.string.numerateCompareRegExp_);
+ var count = Math.min(tokens1.length, tokens2.length);
+ for(var i = 0;i < count;i++) {
+ var a = tokens1[i];
+ var b = tokens2[i];
+ if(a != b) {
+ var num1 = parseInt(a, 10);
+ if(!isNaN(num1)) {
+ var num2 = parseInt(b, 10);
+ if(!isNaN(num2) && num1 - num2) {
+ return num1 - num2
+ }
+ }
+ return a < b ? -1 : 1
+ }
+ }
+ if(tokens1.length != tokens2.length) {
+ return tokens1.length - tokens2.length
+ }
+ return str1 < str2 ? -1 : 1
+};
+goog.string.encodeUriRegExp_ = /^[a-zA-Z0-9\-_.!~*'()]*$/;
+goog.string.urlEncode = function(str) {
+ str = String(str);
+ if(!goog.string.encodeUriRegExp_.test(str)) {
+ return encodeURIComponent(str)
+ }
+ return str
+};
+goog.string.urlDecode = function(str) {
+ return decodeURIComponent(str.replace(/\+/g, " "))
+};
+goog.string.newLineToBr = function(str, opt_xml) {
+ return str.replace(/(\r\n|\r|\n)/g, opt_xml ? "<br />" : "<br>")
+};
+goog.string.htmlEscape = function(str, opt_isLikelyToContainHtmlChars) {
+ if(opt_isLikelyToContainHtmlChars) {
+ return str.replace(goog.string.amperRe_, "&amp;").replace(goog.string.ltRe_, "&lt;").replace(goog.string.gtRe_, "&gt;").replace(goog.string.quotRe_, "&quot;")
+ }else {
+ if(!goog.string.allRe_.test(str)) {
+ return str
+ }
+ if(str.indexOf("&") != -1) {
+ str = str.replace(goog.string.amperRe_, "&amp;")
+ }
+ if(str.indexOf("<") != -1) {
+ str = str.replace(goog.string.ltRe_, "&lt;")
+ }
+ if(str.indexOf(">") != -1) {
+ str = str.replace(goog.string.gtRe_, "&gt;")
+ }
+ if(str.indexOf('"') != -1) {
+ str = str.replace(goog.string.quotRe_, "&quot;")
+ }
+ return str
+ }
+};
+goog.string.amperRe_ = /&/g;
+goog.string.ltRe_ = /</g;
+goog.string.gtRe_ = />/g;
+goog.string.quotRe_ = /\"/g;
+goog.string.allRe_ = /[&<>\"]/;
+goog.string.unescapeEntities = function(str) {
+ if(goog.string.contains(str, "&")) {
+ if("document" in goog.global && !goog.string.contains(str, "<")) {
+ return goog.string.unescapeEntitiesUsingDom_(str)
+ }else {
+ return goog.string.unescapePureXmlEntities_(str)
+ }
+ }
+ return str
+};
+goog.string.unescapeEntitiesUsingDom_ = function(str) {
+ var el = goog.global["document"]["createElement"]("a");
+ el["innerHTML"] = str;
+ if(el[goog.string.NORMALIZE_FN_]) {
+ el[goog.string.NORMALIZE_FN_]()
+ }
+ str = el["firstChild"]["nodeValue"];
+ el["innerHTML"] = "";
+ return str
+};
+goog.string.unescapePureXmlEntities_ = function(str) {
+ return str.replace(/&([^;]+);/g, function(s, entity) {
+ switch(entity) {
+ case "amp":
+ return"&";
+ case "lt":
+ return"<";
+ case "gt":
+ return">";
+ case "quot":
+ return'"';
+ default:
+ if(entity.charAt(0) == "#") {
+ var n = Number("0" + entity.substr(1));
+ if(!isNaN(n)) {
+ return String.fromCharCode(n)
+ }
+ }
+ return s
+ }
+ })
+};
+goog.string.NORMALIZE_FN_ = "normalize";
+goog.string.whitespaceEscape = function(str, opt_xml) {
+ return goog.string.newLineToBr(str.replace(/ /g, " &#160;"), opt_xml)
+};
+goog.string.stripQuotes = function(str, quoteChars) {
+ var length = quoteChars.length;
+ for(var i = 0;i < length;i++) {
+ var quoteChar = length == 1 ? quoteChars : quoteChars.charAt(i);
+ if(str.charAt(0) == quoteChar && str.charAt(str.length - 1) == quoteChar) {
+ return str.substring(1, str.length - 1)
+ }
+ }
+ return str
+};
+goog.string.truncate = function(str, chars, opt_protectEscapedCharacters) {
+ if(opt_protectEscapedCharacters) {
+ str = goog.string.unescapeEntities(str)
+ }
+ if(str.length > chars) {
+ str = str.substring(0, chars - 3) + "..."
+ }
+ if(opt_protectEscapedCharacters) {
+ str = goog.string.htmlEscape(str)
+ }
+ return str
+};
+goog.string.truncateMiddle = function(str, chars, opt_protectEscapedCharacters) {
+ if(opt_protectEscapedCharacters) {
+ str = goog.string.unescapeEntities(str)
+ }
+ if(str.length > chars) {
+ var half = Math.floor(chars / 2);
+ var endPos = str.length - half;
+ half += chars % 2;
+ str = str.substring(0, half) + "..." + str.substring(endPos)
+ }
+ if(opt_protectEscapedCharacters) {
+ str = goog.string.htmlEscape(str)
+ }
+ return str
+};
+goog.string.specialEscapeChars_ = {"\u0000":"\\0", "\u0008":"\\b", "\u000c":"\\f", "\n":"\\n", "\r":"\\r", "\t":"\\t", "\u000b":"\\x0B", '"':'\\"', "\\":"\\\\"};
+goog.string.jsEscapeCache_ = {"'":"\\'"};
+goog.string.quote = function(s) {
+ s = String(s);
+ if(s.quote) {
+ return s.quote()
+ }else {
+ var sb = ['"'];
+ for(var i = 0;i < s.length;i++) {
+ var ch = s.charAt(i);
+ var cc = ch.charCodeAt(0);
+ sb[i + 1] = goog.string.specialEscapeChars_[ch] || (cc > 31 && cc < 127 ? ch : goog.string.escapeChar(ch))
+ }
+ sb.push('"');
+ return sb.join("")
+ }
+};
+goog.string.escapeString = function(str) {
+ var sb = [];
+ for(var i = 0;i < str.length;i++) {
+ sb[i] = goog.string.escapeChar(str.charAt(i))
+ }
+ return sb.join("")
+};
+goog.string.escapeChar = function(c) {
+ if(c in goog.string.jsEscapeCache_) {
+ return goog.string.jsEscapeCache_[c]
+ }
+ if(c in goog.string.specialEscapeChars_) {
+ return goog.string.jsEscapeCache_[c] = goog.string.specialEscapeChars_[c]
+ }
+ var rv = c;
+ var cc = c.charCodeAt(0);
+ if(cc > 31 && cc < 127) {
+ rv = c
+ }else {
+ if(cc < 256) {
+ rv = "\\x";
+ if(cc < 16 || cc > 256) {
+ rv += "0"
+ }
+ }else {
+ rv = "\\u";
+ if(cc < 4096) {
+ rv += "0"
+ }
+ }
+ rv += cc.toString(16).toUpperCase()
+ }
+ return goog.string.jsEscapeCache_[c] = rv
+};
+goog.string.toMap = function(s) {
+ var rv = {};
+ for(var i = 0;i < s.length;i++) {
+ rv[s.charAt(i)] = true
+ }
+ return rv
+};
+goog.string.contains = function(s, ss) {
+ return s.indexOf(ss) != -1
+};
+goog.string.removeAt = function(s, index, stringLength) {
+ var resultStr = s;
+ if(index >= 0 && index < s.length && stringLength > 0) {
+ resultStr = s.substr(0, index) + s.substr(index + stringLength, s.length - index - stringLength)
+ }
+ return resultStr
+};
+goog.string.remove = function(s, ss) {
+ var re = new RegExp(goog.string.regExpEscape(ss), "");
+ return s.replace(re, "")
+};
+goog.string.removeAll = function(s, ss) {
+ var re = new RegExp(goog.string.regExpEscape(ss), "g");
+ return s.replace(re, "")
+};
+goog.string.regExpEscape = function(s) {
+ return String(s).replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, "\\$1").replace(/\x08/g, "\\x08")
+};
+goog.string.repeat = function(string, length) {
+ return(new Array(length + 1)).join(string)
+};
+goog.string.padNumber = function(num, length, opt_precision) {
+ var s = goog.isDef(opt_precision) ? num.toFixed(opt_precision) : String(num);
+ var index = s.indexOf(".");
+ if(index == -1) {
+ index = s.length
+ }
+ return goog.string.repeat("0", Math.max(0, length - index)) + s
+};
+goog.string.makeSafe = function(obj) {
+ return obj == null ? "" : String(obj)
+};
+goog.string.buildString = function(var_args) {
+ return Array.prototype.join.call(arguments, "")
+};
+goog.string.getRandomString = function() {
+ return Math.floor(Math.random() * 2147483648).toString(36) + (Math.floor(Math.random() * 2147483648) ^ goog.now()).toString(36)
+};
+goog.string.compareVersions = function(version1, version2) {
+ var order = 0;
+ var v1Subs = goog.string.trim(String(version1)).split(".");
+ var v2Subs = goog.string.trim(String(version2)).split(".");
+ var subCount = Math.max(v1Subs.length, v2Subs.length);
+ for(var subIdx = 0;order == 0 && subIdx < subCount;subIdx++) {
+ var v1Sub = v1Subs[subIdx] || "";
+ var v2Sub = v2Subs[subIdx] || "";
+ var v1CompParser = new RegExp("(\\d*)(\\D*)", "g");
+ var v2CompParser = new RegExp("(\\d*)(\\D*)", "g");
+ do {
+ var v1Comp = v1CompParser.exec(v1Sub) || ["", "", ""];
+ var v2Comp = v2CompParser.exec(v2Sub) || ["", "", ""];
+ if(v1Comp[0].length == 0 && v2Comp[0].length == 0) {
+ break
+ }
+ var v1CompNum = v1Comp[1].length == 0 ? 0 : parseInt(v1Comp[1], 10);
+ var v2CompNum = v2Comp[1].length == 0 ? 0 : parseInt(v2Comp[1], 10);
+ order = goog.string.compareElements_(v1CompNum, v2CompNum) || goog.string.compareElements_(v1Comp[2].length == 0, v2Comp[2].length == 0) || goog.string.compareElements_(v1Comp[2], v2Comp[2])
+ }while(order == 0)
+ }
+ return order
+};
+goog.string.compareElements_ = function(left, right) {
+ if(left < right) {
+ return-1
+ }else {
+ if(left > right) {
+ return 1
+ }
+ }
+ return 0
+};
+goog.string.HASHCODE_MAX_ = 4294967296;
+goog.string.hashCode = function(str) {
+ var result = 0;
+ for(var i = 0;i < str.length;++i) {
+ result = 31 * result + str.charCodeAt(i);
+ result %= goog.string.HASHCODE_MAX_
+ }
+ return result
+};
+goog.string.uniqueStringCounter_ = Math.random() * 2147483648 | 0;
+goog.string.createUniqueString = function() {
+ return"goog_" + goog.string.uniqueStringCounter_++
+};
+goog.string.toNumber = function(str) {
+ var num = Number(str);
+ if(num == 0 && goog.string.isEmpty(str)) {
+ return NaN
+ }
+ return num
+};
+goog.provide("goog.asserts");
+goog.provide("goog.asserts.AssertionError");
+goog.require("goog.debug.Error");
+goog.require("goog.string");
+goog.asserts.ENABLE_ASSERTS = goog.DEBUG;
+goog.asserts.AssertionError = function(messagePattern, messageArgs) {
+ messageArgs.unshift(messagePattern);
+ goog.debug.Error.call(this, goog.string.subs.apply(null, messageArgs));
+ messageArgs.shift();
+ this.messagePattern = messagePattern
+};
+goog.inherits(goog.asserts.AssertionError, goog.debug.Error);
+goog.asserts.AssertionError.prototype.name = "AssertionError";
+goog.asserts.doAssertFailure_ = function(defaultMessage, defaultArgs, givenMessage, givenArgs) {
+ var message = "Assertion failed";
+ if(givenMessage) {
+ message += ": " + givenMessage;
+ var args = givenArgs
+ }else {
+ if(defaultMessage) {
+ message += ": " + defaultMessage;
+ args = defaultArgs
+ }
+ }
+ throw new goog.asserts.AssertionError("" + message, args || []);
+};
+goog.asserts.assert = function(condition, opt_message, var_args) {
+ if(goog.asserts.ENABLE_ASSERTS && !condition) {
+ goog.asserts.doAssertFailure_("", null, opt_message, Array.prototype.slice.call(arguments, 2))
+ }
+ return condition
+};
+goog.asserts.fail = function(opt_message, var_args) {
+ if(goog.asserts.ENABLE_ASSERTS) {
+ throw new goog.asserts.AssertionError("Failure" + (opt_message ? ": " + opt_message : ""), Array.prototype.slice.call(arguments, 1));
+ }
+};
+goog.asserts.assertNumber = function(value, opt_message, var_args) {
+ if(goog.asserts.ENABLE_ASSERTS && !goog.isNumber(value)) {
+ goog.asserts.doAssertFailure_("Expected number but got %s: %s.", [goog.typeOf(value), value], opt_message, Array.prototype.slice.call(arguments, 2))
+ }
+ return value
+};
+goog.asserts.assertString = function(value, opt_message, var_args) {
+ if(goog.asserts.ENABLE_ASSERTS && !goog.isString(value)) {
+ goog.asserts.doAssertFailure_("Expected string but got %s: %s.", [goog.typeOf(value), value], opt_message, Array.prototype.slice.call(arguments, 2))
+ }
+ return value
+};
+goog.asserts.assertFunction = function(value, opt_message, var_args) {
+ if(goog.asserts.ENABLE_ASSERTS && !goog.isFunction(value)) {
+ goog.asserts.doAssertFailure_("Expected function but got %s: %s.", [goog.typeOf(value), value], opt_message, Array.prototype.slice.call(arguments, 2))
+ }
+ return value
+};
+goog.asserts.assertObject = function(value, opt_message, var_args) {
+ if(goog.asserts.ENABLE_ASSERTS && !goog.isObject(value)) {
+ goog.asserts.doAssertFailure_("Expected object but got %s: %s.", [goog.typeOf(value), value], opt_message, Array.prototype.slice.call(arguments, 2))
+ }
+ return value
+};
+goog.asserts.assertArray = function(value, opt_message, var_args) {
+ if(goog.asserts.ENABLE_ASSERTS && !goog.isArray(value)) {
+ goog.asserts.doAssertFailure_("Expected array but got %s: %s.", [goog.typeOf(value), value], opt_message, Array.prototype.slice.call(arguments, 2))
+ }
+ return value
+};
+goog.asserts.assertBoolean = function(value, opt_message, var_args) {
+ if(goog.asserts.ENABLE_ASSERTS && !goog.isBoolean(value)) {
+ goog.asserts.doAssertFailure_("Expected boolean but got %s: %s.", [goog.typeOf(value), value], opt_message, Array.prototype.slice.call(arguments, 2))
+ }
+ return value
+};
+goog.asserts.assertInstanceof = function(value, type, opt_message, var_args) {
+ if(goog.asserts.ENABLE_ASSERTS && !(value instanceof type)) {
+ goog.asserts.doAssertFailure_("instanceof check failed.", null, opt_message, Array.prototype.slice.call(arguments, 3))
+ }
+};
+goog.provide("goog.array");
+goog.require("goog.asserts");
+goog.array.ArrayLike;
+goog.array.peek = function(array) {
+ return array[array.length - 1]
+};
+goog.array.ARRAY_PROTOTYPE_ = Array.prototype;
+goog.array.indexOf = goog.array.ARRAY_PROTOTYPE_.indexOf ? function(arr, obj, opt_fromIndex) {
+ goog.asserts.assert(arr.length != null);
+ return goog.array.ARRAY_PROTOTYPE_.indexOf.call(arr, obj, opt_fromIndex)
+} : function(arr, obj, opt_fromIndex) {
+ var fromIndex = opt_fromIndex == null ? 0 : opt_fromIndex < 0 ? Math.max(0, arr.length + opt_fromIndex) : opt_fromIndex;
+ if(goog.isString(arr)) {
+ if(!goog.isString(obj) || obj.length != 1) {
+ return-1
+ }
+ return arr.indexOf(obj, fromIndex)
+ }
+ for(var i = fromIndex;i < arr.length;i++) {
+ if(i in arr && arr[i] === obj) {
+ return i
+ }
+ }
+ return-1
+};
+goog.array.lastIndexOf = goog.array.ARRAY_PROTOTYPE_.lastIndexOf ? function(arr, obj, opt_fromIndex) {
+ goog.asserts.assert(arr.length != null);
+ var fromIndex = opt_fromIndex == null ? arr.length - 1 : opt_fromIndex;
+ return goog.array.ARRAY_PROTOTYPE_.lastIndexOf.call(arr, obj, fromIndex)
+} : function(arr, obj, opt_fromIndex) {
+ var fromIndex = opt_fromIndex == null ? arr.length - 1 : opt_fromIndex;
+ if(fromIndex < 0) {
+ fromIndex = Math.max(0, arr.length + fromIndex)
+ }
+ if(goog.isString(arr)) {
+ if(!goog.isString(obj) || obj.length != 1) {
+ return-1
+ }
+ return arr.lastIndexOf(obj, fromIndex)
+ }
+ for(var i = fromIndex;i >= 0;i--) {
+ if(i in arr && arr[i] === obj) {
+ return i
+ }
+ }
+ return-1
+};
+goog.array.forEach = goog.array.ARRAY_PROTOTYPE_.forEach ? function(arr, f, opt_obj) {
+ goog.asserts.assert(arr.length != null);
+ goog.array.ARRAY_PROTOTYPE_.forEach.call(arr, f, opt_obj)
+} : function(arr, f, opt_obj) {
+ var l = arr.length;
+ var arr2 = goog.isString(arr) ? arr.split("") : arr;
+ for(var i = 0;i < l;i++) {
+ if(i in arr2) {
+ f.call(opt_obj, arr2[i], i, arr)
+ }
+ }
+};
+goog.array.forEachRight = function(arr, f, opt_obj) {
+ var l = arr.length;
+ var arr2 = goog.isString(arr) ? arr.split("") : arr;
+ for(var i = l - 1;i >= 0;--i) {
+ if(i in arr2) {
+ f.call(opt_obj, arr2[i], i, arr)
+ }
+ }
+};
+goog.array.filter = goog.array.ARRAY_PROTOTYPE_.filter ? function(arr, f, opt_obj) {
+ goog.asserts.assert(arr.length != null);
+ return goog.array.ARRAY_PROTOTYPE_.filter.call(arr, f, opt_obj)
+} : function(arr, f, opt_obj) {
+ var l = arr.length;
+ var res = [];
+ var resLength = 0;
+ var arr2 = goog.isString(arr) ? arr.split("") : arr;
+ for(var i = 0;i < l;i++) {
+ if(i in arr2) {
+ var val = arr2[i];
+ if(f.call(opt_obj, val, i, arr)) {
+ res[resLength++] = val
+ }
+ }
+ }
+ return res
+};
+goog.array.map = goog.array.ARRAY_PROTOTYPE_.map ? function(arr, f, opt_obj) {
+ goog.asserts.assert(arr.length != null);
+ return goog.array.ARRAY_PROTOTYPE_.map.call(arr, f, opt_obj)
+} : function(arr, f, opt_obj) {
+ var l = arr.length;
+ var res = new Array(l);
+ var arr2 = goog.isString(arr) ? arr.split("") : arr;
+ for(var i = 0;i < l;i++) {
+ if(i in arr2) {
+ res[i] = f.call(opt_obj, arr2[i], i, arr)
+ }
+ }
+ return res
+};
+goog.array.reduce = function(arr, f, val, opt_obj) {
+ if(arr.reduce) {
+ if(opt_obj) {
+ return arr.reduce(goog.bind(f, opt_obj), val)
+ }else {
+ return arr.reduce(f, val)
+ }
+ }
+ var rval = val;
+ goog.array.forEach(arr, function(val, index) {
+ rval = f.call(opt_obj, rval, val, index, arr)
+ });
+ return rval
+};
+goog.array.reduceRight = function(arr, f, val, opt_obj) {
+ if(arr.reduceRight) {
+ if(opt_obj) {
+ return arr.reduceRight(goog.bind(f, opt_obj), val)
+ }else {
+ return arr.reduceRight(f, val)
+ }
+ }
+ var rval = val;
+ goog.array.forEachRight(arr, function(val, index) {
+ rval = f.call(opt_obj, rval, val, index, arr)
+ });
+ return rval
+};
+goog.array.some = goog.array.ARRAY_PROTOTYPE_.some ? function(arr, f, opt_obj) {
+ goog.asserts.assert(arr.length != null);
+ return goog.array.ARRAY_PROTOTYPE_.some.call(arr, f, opt_obj)
+} : function(arr, f, opt_obj) {
+ var l = arr.length;
+ var arr2 = goog.isString(arr) ? arr.split("") : arr;
+ for(var i = 0;i < l;i++) {
+ if(i in arr2 && f.call(opt_obj, arr2[i], i, arr)) {
+ return true
+ }
+ }
+ return false
+};
+goog.array.every = goog.array.ARRAY_PROTOTYPE_.every ? function(arr, f, opt_obj) {
+ goog.asserts.assert(arr.length != null);
+ return goog.array.ARRAY_PROTOTYPE_.every.call(arr, f, opt_obj)
+} : function(arr, f, opt_obj) {
+ var l = arr.length;
+ var arr2 = goog.isString(arr) ? arr.split("") : arr;
+ for(var i = 0;i < l;i++) {
+ if(i in arr2 && !f.call(opt_obj, arr2[i], i, arr)) {
+ return false
+ }
+ }
+ return true
+};
+goog.array.find = function(arr, f, opt_obj) {
+ var i = goog.array.findIndex(arr, f, opt_obj);
+ return i < 0 ? null : goog.isString(arr) ? arr.charAt(i) : arr[i]
+};
+goog.array.findIndex = function(arr, f, opt_obj) {
+ var l = arr.length;
+ var arr2 = goog.isString(arr) ? arr.split("") : arr;
+ for(var i = 0;i < l;i++) {
+ if(i in arr2 && f.call(opt_obj, arr2[i], i, arr)) {
+ return i
+ }
+ }
+ return-1
+};
+goog.array.findRight = function(arr, f, opt_obj) {
+ var i = goog.array.findIndexRight(arr, f, opt_obj);
+ return i < 0 ? null : goog.isString(arr) ? arr.charAt(i) : arr[i]
+};
+goog.array.findIndexRight = function(arr, f, opt_obj) {
+ var l = arr.length;
+ var arr2 = goog.isString(arr) ? arr.split("") : arr;
+ for(var i = l - 1;i >= 0;i--) {
+ if(i in arr2 && f.call(opt_obj, arr2[i], i, arr)) {
+ return i
+ }
+ }
+ return-1
+};
+goog.array.contains = function(arr, obj) {
+ return goog.array.indexOf(arr, obj) >= 0
+};
+goog.array.isEmpty = function(arr) {
+ return arr.length == 0
+};
+goog.array.clear = function(arr) {
+ if(!goog.isArray(arr)) {
+ for(var i = arr.length - 1;i >= 0;i--) {
+ delete arr[i]
+ }
+ }
+ arr.length = 0
+};
+goog.array.insert = function(arr, obj) {
+ if(!goog.array.contains(arr, obj)) {
+ arr.push(obj)
+ }
+};
+goog.array.insertAt = function(arr, obj, opt_i) {
+ goog.array.splice(arr, opt_i, 0, obj)
+};
+goog.array.insertArrayAt = function(arr, elementsToAdd, opt_i) {
+ goog.partial(goog.array.splice, arr, opt_i, 0).apply(null, elementsToAdd)
+};
+goog.array.insertBefore = function(arr, obj, opt_obj2) {
+ var i;
+ if(arguments.length == 2 || (i = goog.array.indexOf(arr, opt_obj2)) < 0) {
+ arr.push(obj)
+ }else {
+ goog.array.insertAt(arr, obj, i)
+ }
+};
+goog.array.remove = function(arr, obj) {
+ var i = goog.array.indexOf(arr, obj);
+ var rv;
+ if(rv = i >= 0) {
+ goog.array.removeAt(arr, i)
+ }
+ return rv
+};
+goog.array.removeAt = function(arr, i) {
+ goog.asserts.assert(arr.length != null);
+ return goog.array.ARRAY_PROTOTYPE_.splice.call(arr, i, 1).length == 1
+};
+goog.array.removeIf = function(arr, f, opt_obj) {
+ var i = goog.array.findIndex(arr, f, opt_obj);
+ if(i >= 0) {
+ goog.array.removeAt(arr, i);
+ return true
+ }
+ return false
+};
+goog.array.concat = function(var_args) {
+ return goog.array.ARRAY_PROTOTYPE_.concat.apply(goog.array.ARRAY_PROTOTYPE_, arguments)
+};
+goog.array.clone = function(arr) {
+ if(goog.isArray(arr)) {
+ return goog.array.concat(arr)
+ }else {
+ var rv = [];
+ for(var i = 0, len = arr.length;i < len;i++) {
+ rv[i] = arr[i]
+ }
+ return rv
+ }
+};
+goog.array.toArray = function(object) {
+ if(goog.isArray(object)) {
+ return goog.array.concat(object)
+ }
+ return goog.array.clone(object)
+};
+goog.array.extend = function(arr1, var_args) {
+ for(var i = 1;i < arguments.length;i++) {
+ var arr2 = arguments[i];
+ var isArrayLike;
+ if(goog.isArray(arr2) || (isArrayLike = goog.isArrayLike(arr2)) && arr2.hasOwnProperty("callee")) {
+ arr1.push.apply(arr1, arr2)
+ }else {
+ if(isArrayLike) {
+ var len1 = arr1.length;
+ var len2 = arr2.length;
+ for(var j = 0;j < len2;j++) {
+ arr1[len1 + j] = arr2[j]
+ }
+ }else {
+ arr1.push(arr2)
+ }
+ }
+ }
+};
+goog.array.splice = function(arr, index, howMany, var_args) {
+ goog.asserts.assert(arr.length != null);
+ return goog.array.ARRAY_PROTOTYPE_.splice.apply(arr, goog.array.slice(arguments, 1))
+};
+goog.array.slice = function(arr, start, opt_end) {
+ goog.asserts.assert(arr.length != null);
+ if(arguments.length <= 2) {
+ return goog.array.ARRAY_PROTOTYPE_.slice.call(arr, start)
+ }else {
+ return goog.array.ARRAY_PROTOTYPE_.slice.call(arr, start, opt_end)
+ }
+};
+goog.array.removeDuplicates = function(arr, opt_rv) {
+ var rv = opt_rv || arr;
+ var seen = {}, cursorInsert = 0, cursorRead = 0;
+ while(cursorRead < arr.length) {
+ var current = arr[cursorRead++];
+ var uid = goog.isObject(current) ? goog.getUid(current) : current;
+ if(!Object.prototype.hasOwnProperty.call(seen, uid)) {
+ seen[uid] = true;
+ rv[cursorInsert++] = current
+ }
+ }
+ rv.length = cursorInsert
+};
+goog.array.binarySearch = function(arr, target, opt_compareFn) {
+ return goog.array.binarySearch_(arr, opt_compareFn || goog.array.defaultCompare, false, target)
+};
+goog.array.binarySelect = function(arr, evaluator, opt_obj) {
+ return goog.array.binarySearch_(arr, evaluator, true, undefined, opt_obj)
+};
+goog.array.binarySearch_ = function(arr, compareFn, isEvaluator, opt_target, opt_selfObj) {
+ var left = 0;
+ var right = arr.length;
+ var found;
+ while(left < right) {
+ var middle = left + right >> 1;
+ var compareResult;
+ if(isEvaluator) {
+ compareResult = compareFn.call(opt_selfObj, arr[middle], middle, arr)
+ }else {
+ compareResult = compareFn(opt_target, arr[middle])
+ }
+ if(compareResult > 0) {
+ left = middle + 1
+ }else {
+ right = middle;
+ found = !compareResult
+ }
+ }
+ return found ? left : ~left
+};
+goog.array.sort = function(arr, opt_compareFn) {
+ goog.asserts.assert(arr.length != null);
+ goog.array.ARRAY_PROTOTYPE_.sort.call(arr, opt_compareFn || goog.array.defaultCompare)
+};
+goog.array.stableSort = function(arr, opt_compareFn) {
+ for(var i = 0;i < arr.length;i++) {
+ arr[i] = {index:i, value:arr[i]}
+ }
+ var valueCompareFn = opt_compareFn || goog.array.defaultCompare;
+ function stableCompareFn(obj1, obj2) {
+ return valueCompareFn(obj1.value, obj2.value) || obj1.index - obj2.index
+ }
+ goog.array.sort(arr, stableCompareFn);
+ for(var i = 0;i < arr.length;i++) {
+ arr[i] = arr[i].value
+ }
+};
+goog.array.sortObjectsByKey = function(arr, key, opt_compareFn) {
+ var compare = opt_compareFn || goog.array.defaultCompare;
+ goog.array.sort(arr, function(a, b) {
+ return compare(a[key], b[key])
+ })
+};
+goog.array.equals = function(arr1, arr2, opt_equalsFn) {
+ if(!goog.isArrayLike(arr1) || !goog.isArrayLike(arr2) || arr1.length != arr2.length) {
+ return false
+ }
+ var l = arr1.length;
+ var equalsFn = opt_equalsFn || goog.array.defaultCompareEquality;
+ for(var i = 0;i < l;i++) {
+ if(!equalsFn(arr1[i], arr2[i])) {
+ return false
+ }
+ }
+ return true
+};
+goog.array.compare = function(arr1, arr2, opt_equalsFn) {
+ return goog.array.equals(arr1, arr2, opt_equalsFn)
+};
+goog.array.defaultCompare = function(a, b) {
+ return a > b ? 1 : a < b ? -1 : 0
+};
+goog.array.defaultCompareEquality = function(a, b) {
+ return a === b
+};
+goog.array.binaryInsert = function(array, value, opt_compareFn) {
+ var index = goog.array.binarySearch(array, value, opt_compareFn);
+ if(index < 0) {
+ goog.array.insertAt(array, value, -(index + 1));
+ return true
+ }
+ return false
+};
+goog.array.binaryRemove = function(array, value, opt_compareFn) {
+ var index = goog.array.binarySearch(array, value, opt_compareFn);
+ return index >= 0 ? goog.array.removeAt(array, index) : false
+};
+goog.array.bucket = function(array, sorter) {
+ var buckets = {};
+ for(var i = 0;i < array.length;i++) {
+ var value = array[i];
+ var key = sorter(value, i, array);
+ if(goog.isDef(key)) {
+ var bucket = buckets[key] || (buckets[key] = []);
+ bucket.push(value)
+ }
+ }
+ return buckets
+};
+goog.array.repeat = function(value, n) {
+ var array = [];
+ for(var i = 0;i < n;i++) {
+ array[i] = value
+ }
+ return array
+};
+goog.array.flatten = function(var_args) {
+ var result = [];
+ for(var i = 0;i < arguments.length;i++) {
+ var element = arguments[i];
+ if(goog.isArray(element)) {
+ result.push.apply(result, goog.array.flatten.apply(null, element))
+ }else {
+ result.push(element)
+ }
+ }
+ return result
+};
+goog.array.rotate = function(array, n) {
+ goog.asserts.assert(array.length != null);
+ if(array.length) {
+ n %= array.length;
+ if(n > 0) {
+ goog.array.ARRAY_PROTOTYPE_.unshift.apply(array, array.splice(-n, n))
+ }else {
+ if(n < 0) {
+ goog.array.ARRAY_PROTOTYPE_.push.apply(array, array.splice(0, -n))
+ }
+ }
+ }
+ return array
+};
+goog.array.zip = function(var_args) {
+ if(!arguments.length) {
+ return[]
+ }
+ var result = [];
+ for(var i = 0;true;i++) {
+ var value = [];
+ for(var j = 0;j < arguments.length;j++) {
+ var arr = arguments[j];
+ if(i >= arr.length) {
+ return result
+ }
+ value.push(arr[i])
+ }
+ result.push(value)
+ }
+};
+goog.provide("goog.userAgent");
+goog.require("goog.string");
+goog.userAgent.ASSUME_IE = false;
+goog.userAgent.ASSUME_GECKO = false;
+goog.userAgent.ASSUME_WEBKIT = false;
+goog.userAgent.ASSUME_MOBILE_WEBKIT = false;
+goog.userAgent.ASSUME_OPERA = false;
+goog.userAgent.BROWSER_KNOWN_ = goog.userAgent.ASSUME_IE || goog.userAgent.ASSUME_GECKO || goog.userAgent.ASSUME_MOBILE_WEBKIT || goog.userAgent.ASSUME_WEBKIT || goog.userAgent.ASSUME_OPERA;
+goog.userAgent.getUserAgentString = function() {
+ return goog.global["navigator"] ? goog.global["navigator"].userAgent : null
+};
+goog.userAgent.getNavigator = function() {
+ return goog.global["navigator"]
+};
+goog.userAgent.init_ = function() {
+ goog.userAgent.detectedOpera_ = false;
+ goog.userAgent.detectedIe_ = false;
+ goog.userAgent.detectedWebkit_ = false;
+ goog.userAgent.detectedMobile_ = false;
+ goog.userAgent.detectedGecko_ = false;
+ var ua;
+ if(!goog.userAgent.BROWSER_KNOWN_ && (ua = goog.userAgent.getUserAgentString())) {
+ var navigator = goog.userAgent.getNavigator();
+ goog.userAgent.detectedOpera_ = ua.indexOf("Opera") == 0;
+ goog.userAgent.detectedIe_ = !goog.userAgent.detectedOpera_ && ua.indexOf("MSIE") != -1;
+ goog.userAgent.detectedWebkit_ = !goog.userAgent.detectedOpera_ && ua.indexOf("WebKit") != -1;
+ goog.userAgent.detectedMobile_ = goog.userAgent.detectedWebkit_ && ua.indexOf("Mobile") != -1;
+ goog.userAgent.detectedGecko_ = !goog.userAgent.detectedOpera_ && !goog.userAgent.detectedWebkit_ && navigator.product == "Gecko"
+ }
+};
+if(!goog.userAgent.BROWSER_KNOWN_) {
+ goog.userAgent.init_()
+}
+goog.userAgent.OPERA = goog.userAgent.BROWSER_KNOWN_ ? goog.userAgent.ASSUME_OPERA : goog.userAgent.detectedOpera_;
+goog.userAgent.IE = goog.userAgent.BROWSER_KNOWN_ ? goog.userAgent.ASSUME_IE : goog.userAgent.detectedIe_;
+goog.userAgent.GECKO = goog.userAgent.BROWSER_KNOWN_ ? goog.userAgent.ASSUME_GECKO : goog.userAgent.detectedGecko_;
+goog.userAgent.WEBKIT = goog.userAgent.BROWSER_KNOWN_ ? goog.userAgent.ASSUME_WEBKIT || goog.userAgent.ASSUME_MOBILE_WEBKIT : goog.userAgent.detectedWebkit_;
+goog.userAgent.MOBILE = goog.userAgent.ASSUME_MOBILE_WEBKIT || goog.userAgent.detectedMobile_;
+goog.userAgent.SAFARI = goog.userAgent.WEBKIT;
+goog.userAgent.determinePlatform_ = function() {
+ var navigator = goog.userAgent.getNavigator();
+ return navigator && navigator.platform || ""
+};
+goog.userAgent.PLATFORM = goog.userAgent.determinePlatform_();
+goog.userAgent.ASSUME_MAC = false;
+goog.userAgent.ASSUME_WINDOWS = false;
+goog.userAgent.ASSUME_LINUX = false;
+goog.userAgent.ASSUME_X11 = false;
+goog.userAgent.PLATFORM_KNOWN_ = goog.userAgent.ASSUME_MAC || goog.userAgent.ASSUME_WINDOWS || goog.userAgent.ASSUME_LINUX || goog.userAgent.ASSUME_X11;
+goog.userAgent.initPlatform_ = function() {
+ goog.userAgent.detectedMac_ = goog.string.contains(goog.userAgent.PLATFORM, "Mac");
+ goog.userAgent.detectedWindows_ = goog.string.contains(goog.userAgent.PLATFORM, "Win");
+ goog.userAgent.detectedLinux_ = goog.string.contains(goog.userAgent.PLATFORM, "Linux");
+ goog.userAgent.detectedX11_ = !!goog.userAgent.getNavigator() && goog.string.contains(goog.userAgent.getNavigator()["appVersion"] || "", "X11")
+};
+if(!goog.userAgent.PLATFORM_KNOWN_) {
+ goog.userAgent.initPlatform_()
+}
+goog.userAgent.MAC = goog.userAgent.PLATFORM_KNOWN_ ? goog.userAgent.ASSUME_MAC : goog.userAgent.detectedMac_;
+goog.userAgent.WINDOWS = goog.userAgent.PLATFORM_KNOWN_ ? goog.userAgent.ASSUME_WINDOWS : goog.userAgent.detectedWindows_;
+goog.userAgent.LINUX = goog.userAgent.PLATFORM_KNOWN_ ? goog.userAgent.ASSUME_LINUX : goog.userAgent.detectedLinux_;
+goog.userAgent.X11 = goog.userAgent.PLATFORM_KNOWN_ ? goog.userAgent.ASSUME_X11 : goog.userAgent.detectedX11_;
+goog.userAgent.determineVersion_ = function() {
+ var version = "", re;
+ if(goog.userAgent.OPERA && goog.global["opera"]) {
+ var operaVersion = goog.global["opera"].version;
+ version = typeof operaVersion == "function" ? operaVersion() : operaVersion
+ }else {
+ if(goog.userAgent.GECKO) {
+ re = /rv\:([^\);]+)(\)|;)/
+ }else {
+ if(goog.userAgent.IE) {
+ re = /MSIE\s+([^\);]+)(\)|;)/
+ }else {
+ if(goog.userAgent.WEBKIT) {
+ re = /WebKit\/(\S+)/
+ }
+ }
+ }
+ if(re) {
+ var arr = re.exec(goog.userAgent.getUserAgentString());
+ version = arr ? arr[1] : ""
+ }
+ }
+ if(goog.userAgent.IE) {
+ var docMode = goog.userAgent.getDocumentMode_();
+ if(docMode > parseFloat(version)) {
+ return String(docMode)
+ }
+ }
+ return version
+};
+goog.userAgent.getDocumentMode_ = function() {
+ var doc = goog.global["document"];
+ return doc ? doc["documentMode"] : undefined
+};
+goog.userAgent.VERSION = goog.userAgent.determineVersion_();
+goog.userAgent.compare = function(v1, v2) {
+ return goog.string.compareVersions(v1, v2)
+};
+goog.userAgent.isVersionCache_ = {};
+goog.userAgent.isVersion = function(version) {
+ return goog.userAgent.isVersionCache_[version] || (goog.userAgent.isVersionCache_[version] = goog.string.compareVersions(goog.userAgent.VERSION, version) >= 0)
+};
+goog.provide("goog.dom.BrowserFeature");
+goog.require("goog.userAgent");
+goog.dom.BrowserFeature = {
+ CAN_ADD_NAME_OR_TYPE_ATTRIBUTES: !goog.userAgent.IE || goog.userAgent.isVersion("9"),
+ CAN_USE_INNER_TEXT: goog.userAgent.IE && !goog.userAgent.isVersion("9"),
+ INNER_HTML_NEEDS_SCOPED_ELEMENT: goog.userAgent.IE
+};
+goog.provide("goog.dom.TagName");
+goog.dom.TagName = {A:"A", ABBR:"ABBR", ACRONYM:"ACRONYM", ADDRESS:"ADDRESS", APPLET:"APPLET", AREA:"AREA", B:"B", BASE:"BASE", BASEFONT:"BASEFONT", BDO:"BDO", BIG:"BIG", BLOCKQUOTE:"BLOCKQUOTE", BODY:"BODY", BR:"BR", BUTTON:"BUTTON", CANVAS:"CANVAS", CAPTION:"CAPTION", CENTER:"CENTER", CITE:"CITE", CODE:"CODE", COL:"COL", COLGROUP:"COLGROUP", DD:"DD", DEL:"DEL", DFN:"DFN", DIR:"DIR", DIV:"DIV", DL:"DL", DT:"DT", EM:"EM", FIELDSET:"FIELDSET", FONT:"FONT", FORM:"FORM", FRAME:"FRAME", FRAMESET:"FRAMESET",
+H1:"H1", H2:"H2", H3:"H3", H4:"H4", H5:"H5", H6:"H6", HEAD:"HEAD", HR:"HR", HTML:"HTML", I:"I", IFRAME:"IFRAME", IMG:"IMG", INPUT:"INPUT", INS:"INS", ISINDEX:"ISINDEX", KBD:"KBD", LABEL:"LABEL", LEGEND:"LEGEND", LI:"LI", LINK:"LINK", MAP:"MAP", MENU:"MENU", META:"META", NOFRAMES:"NOFRAMES", NOSCRIPT:"NOSCRIPT", OBJECT:"OBJECT", OL:"OL", OPTGROUP:"OPTGROUP", OPTION:"OPTION", P:"P", PARAM:"PARAM", PRE:"PRE", Q:"Q", S:"S", SAMP:"SAMP", SCRIPT:"SCRIPT", SELECT:"SELECT", SMALL:"SMALL", SPAN:"SPAN", STRIKE:"STRIKE",
+STRONG:"STRONG", STYLE:"STYLE", SUB:"SUB", SUP:"SUP", TABLE:"TABLE", TBODY:"TBODY", TD:"TD", TEXTAREA:"TEXTAREA", TFOOT:"TFOOT", TH:"TH", THEAD:"THEAD", TITLE:"TITLE", TR:"TR", TT:"TT", U:"U", UL:"UL", VAR:"VAR"};
+goog.provide("goog.dom.classes");
+goog.require("goog.array");
+goog.dom.classes.set = function(element, className) {
+ element.className = className
+};
+goog.dom.classes.get = function(element) {
+ var className = element.className;
+ return className && typeof className.split == "function" ? className.split(/\s+/) : []
+};
+goog.dom.classes.add = function(element, var_args) {
+ var classes = goog.dom.classes.get(element);
+ var args = goog.array.slice(arguments, 1);
+ var b = goog.dom.classes.add_(classes, args);
+ element.className = classes.join(" ");
+ return b
+};
+goog.dom.classes.remove = function(element, var_args) {
+ var classes = goog.dom.classes.get(element);
+ var args = goog.array.slice(arguments, 1);
+ var b = goog.dom.classes.remove_(classes, args);
+ element.className = classes.join(" ");
+ return b
+};
+goog.dom.classes.add_ = function(classes, args) {
+ var rv = 0;
+ for(var i = 0;i < args.length;i++) {
+ if(!goog.array.contains(classes, args[i])) {
+ classes.push(args[i]);
+ rv++
+ }
+ }
+ return rv == args.length
+};
+goog.dom.classes.remove_ = function(classes, args) {
+ var rv = 0;
+ for(var i = 0;i < classes.length;i++) {
+ if(goog.array.contains(args, classes[i])) {
+ goog.array.splice(classes, i--, 1);
+ rv++
+ }
+ }
+ return rv == args.length
+};
+goog.dom.classes.swap = function(element, fromClass, toClass) {
+ var classes = goog.dom.classes.get(element);
+ var removed = false;
+ for(var i = 0;i < classes.length;i++) {
+ if(classes[i] == fromClass) {
+ goog.array.splice(classes, i--, 1);
+ removed = true
+ }
+ }
+ if(removed) {
+ classes.push(toClass);
+ element.className = classes.join(" ")
+ }
+ return removed
+};
+goog.dom.classes.addRemove = function(element, classesToRemove, classesToAdd) {
+ var classes = goog.dom.classes.get(element);
+ if(goog.isString(classesToRemove)) {
+ goog.array.remove(classes, classesToRemove)
+ }else {
+ if(goog.isArray(classesToRemove)) {
+ goog.dom.classes.remove_(classes, classesToRemove)
+ }
+ }
+ if(goog.isString(classesToAdd) && !goog.array.contains(classes, classesToAdd)) {
+ classes.push(classesToAdd)
+ }else {
+ if(goog.isArray(classesToAdd)) {
+ goog.dom.classes.add_(classes, classesToAdd)
+ }
+ }
+ element.className = classes.join(" ")
+};
+goog.dom.classes.has = function(element, className) {
+ return goog.array.contains(goog.dom.classes.get(element), className)
+};
+goog.dom.classes.enable = function(element, className, enabled) {
+ if(enabled) {
+ goog.dom.classes.add(element, className)
+ }else {
+ goog.dom.classes.remove(element, className)
+ }
+};
+goog.dom.classes.toggle = function(element, className) {
+ var add = !goog.dom.classes.has(element, className);
+ goog.dom.classes.enable(element, className, add);
+ return add
+};
+goog.provide("goog.math.Coordinate");
+goog.math.Coordinate = function(opt_x, opt_y) {
+ this.x = goog.isDef(opt_x) ? opt_x : 0;
+ this.y = goog.isDef(opt_y) ? opt_y : 0
+};
+goog.math.Coordinate.prototype.clone = function() {
+ return new goog.math.Coordinate(this.x, this.y)
+};
+if(goog.DEBUG) {
+ goog.math.Coordinate.prototype.toString = function() {
+ return"(" + this.x + ", " + this.y + ")"
+ }
+}
+goog.math.Coordinate.equals = function(a, b) {
+ if(a == b) {
+ return true
+ }
+ if(!a || !b) {
+ return false
+ }
+ return a.x == b.x && a.y == b.y
+};
+goog.math.Coordinate.distance = function(a, b) {
+ var dx = a.x - b.x;
+ var dy = a.y - b.y;
+ return Math.sqrt(dx * dx + dy * dy)
+};
+goog.math.Coordinate.squaredDistance = function(a, b) {
+ var dx = a.x - b.x;
+ var dy = a.y - b.y;
+ return dx * dx + dy * dy
+};
+goog.math.Coordinate.difference = function(a, b) {
+ return new goog.math.Coordinate(a.x - b.x, a.y - b.y)
+};
+goog.math.Coordinate.sum = function(a, b) {
+ return new goog.math.Coordinate(a.x + b.x, a.y + b.y)
+};
+goog.provide("goog.math.Size");
+goog.math.Size = function(width, height) {
+ this.width = width;
+ this.height = height
+};
+goog.math.Size.equals = function(a, b) {
+ if(a == b) {
+ return true
+ }
+ if(!a || !b) {
+ return false
+ }
+ return a.width == b.width && a.height == b.height
+};
+goog.math.Size.prototype.clone = function() {
+ return new goog.math.Size(this.width, this.height)
+};
+if(goog.DEBUG) {
+ goog.math.Size.prototype.toString = function() {
+ return"(" + this.width + " x " + this.height + ")"
+ }
+}
+goog.math.Size.prototype.getLongest = function() {
+ return Math.max(this.width, this.height)
+};
+goog.math.Size.prototype.getShortest = function() {
+ return Math.min(this.width, this.height)
+};
+goog.math.Size.prototype.area = function() {
+ return this.width * this.height
+};
+goog.math.Size.prototype.perimeter = function() {
+ return(this.width + this.height) * 2
+};
+goog.math.Size.prototype.aspectRatio = function() {
+ return this.width / this.height
+};
+goog.math.Size.prototype.isEmpty = function() {
+ return!this.area()
+};
+goog.math.Size.prototype.ceil = function() {
+ this.width = Math.ceil(this.width);
+ this.height = Math.ceil(this.height);
+ return this
+};
+goog.math.Size.prototype.fitsInside = function(target) {
+ return this.width <= target.width && this.height <= target.height
+};
+goog.math.Size.prototype.floor = function() {
+ this.width = Math.floor(this.width);
+ this.height = Math.floor(this.height);
+ return this
+};
+goog.math.Size.prototype.round = function() {
+ this.width = Math.round(this.width);
+ this.height = Math.round(this.height);
+ return this
+};
+goog.math.Size.prototype.scale = function(s) {
+ this.width *= s;
+ this.height *= s;
+ return this
+};
+goog.math.Size.prototype.scaleToFit = function(target) {
+ var s = this.aspectRatio() > target.aspectRatio() ? target.width / this.width : target.height / this.height;
+ return this.scale(s)
+};
+goog.provide("goog.object");
+goog.object.forEach = function(obj, f, opt_obj) {
+ for(var key in obj) {
+ f.call(opt_obj, obj[key], key, obj)
+ }
+};
+goog.object.filter = function(obj, f, opt_obj) {
+ var res = {};
+ for(var key in obj) {
+ if(f.call(opt_obj, obj[key], key, obj)) {
+ res[key] = obj[key]
+ }
+ }
+ return res
+};
+goog.object.map = function(obj, f, opt_obj) {
+ var res = {};
+ for(var key in obj) {
+ res[key] = f.call(opt_obj, obj[key], key, obj)
+ }
+ return res
+};
+goog.object.some = function(obj, f, opt_obj) {
+ for(var key in obj) {
+ if(f.call(opt_obj, obj[key], key, obj)) {
+ return true
+ }
+ }
+ return false
+};
+goog.object.every = function(obj, f, opt_obj) {
+ for(var key in obj) {
+ if(!f.call(opt_obj, obj[key], key, obj)) {
+ return false
+ }
+ }
+ return true
+};
+goog.object.getCount = function(obj) {
+ var rv = 0;
+ for(var key in obj) {
+ rv++
+ }
+ return rv
+};
+goog.object.getAnyKey = function(obj) {
+ for(var key in obj) {
+ return key
+ }
+};
+goog.object.getAnyValue = function(obj) {
+ for(var key in obj) {
+ return obj[key]
+ }
+};
+goog.object.contains = function(obj, val) {
+ return goog.object.containsValue(obj, val)
+};
+goog.object.getValues = function(obj) {
+ var res = [];
+ var i = 0;
+ for(var key in obj) {
+ res[i++] = obj[key]
+ }
+ return res
+};
+goog.object.getKeys = function(obj) {
+ var res = [];
+ var i = 0;
+ for(var key in obj) {
+ res[i++] = key
+ }
+ return res
+};
+goog.object.containsKey = function(obj, key) {
+ return key in obj
+};
+goog.object.containsValue = function(obj, val) {
+ for(var key in obj) {
+ if(obj[key] == val) {
+ return true
+ }
+ }
+ return false
+};
+goog.object.findKey = function(obj, f, opt_this) {
+ for(var key in obj) {
+ if(f.call(opt_this, obj[key], key, obj)) {
+ return key
+ }
+ }
+ return undefined
+};
+goog.object.findValue = function(obj, f, opt_this) {
+ var key = goog.object.findKey(obj, f, opt_this);
+ return key && obj[key]
+};
+goog.object.isEmpty = function(obj) {
+ for(var key in obj) {
+ return false
+ }
+ return true
+};
+goog.object.clear = function(obj) {
+ var keys = goog.object.getKeys(obj);
+ for(var i = keys.length - 1;i >= 0;i--) {
+ goog.object.remove(obj, keys[i])
+ }
+};
+goog.object.remove = function(obj, key) {
+ var rv;
+ if(rv = key in obj) {
+ delete obj[key]
+ }
+ return rv
+};
+goog.object.add = function(obj, key, val) {
+ if(key in obj) {
+ throw Error('The object already contains the key "' + key + '"');
+ }
+ goog.object.set(obj, key, val)
+};
+goog.object.get = function(obj, key, opt_val) {
+ if(key in obj) {
+ return obj[key]
+ }
+ return opt_val
+};
+goog.object.set = function(obj, key, value) {
+ obj[key] = value
+};
+goog.object.setIfUndefined = function(obj, key, value) {
+ return key in obj ? obj[key] : obj[key] = value
+};
+goog.object.clone = function(obj) {
+ var res = {};
+ for(var key in obj) {
+ res[key] = obj[key]
+ }
+ return res
+};
+goog.object.transpose = function(obj) {
+ var transposed = {};
+ for(var key in obj) {
+ transposed[obj[key]] = key
+ }
+ return transposed
+};
+goog.object.PROTOTYPE_FIELDS_ = ["constructor", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "toLocaleString", "toString", "valueOf"];
+goog.object.extend = function(target, var_args) {
+ var key, source;
+ for(var i = 1;i < arguments.length;i++) {
+ source = arguments[i];
+ for(key in source) {
+ target[key] = source[key]
+ }
+ for(var j = 0;j < goog.object.PROTOTYPE_FIELDS_.length;j++) {
+ key = goog.object.PROTOTYPE_FIELDS_[j];
+ if(Object.prototype.hasOwnProperty.call(source, key)) {
+ target[key] = source[key]
+ }
+ }
+ }
+};
+goog.object.create = function(var_args) {
+ var argLength = arguments.length;
+ if(argLength == 1 && goog.isArray(arguments[0])) {
+ return goog.object.create.apply(null, arguments[0])
+ }
+ if(argLength % 2) {
+ throw Error("Uneven number of arguments");
+ }
+ var rv = {};
+ for(var i = 0;i < argLength;i += 2) {
+ rv[arguments[i]] = arguments[i + 1]
+ }
+ return rv
+};
+goog.object.createSet = function(var_args) {
+ var argLength = arguments.length;
+ if(argLength == 1 && goog.isArray(arguments[0])) {
+ return goog.object.createSet.apply(null, arguments[0])
+ }
+ var rv = {};
+ for(var i = 0;i < argLength;i++) {
+ rv[arguments[i]] = true
+ }
+ return rv
+};
+goog.provide("goog.dom");
+goog.provide("goog.dom.DomHelper");
+goog.provide("goog.dom.NodeType");
+goog.require("goog.array");
+goog.require("goog.dom.BrowserFeature");
+goog.require("goog.dom.TagName");
+goog.require("goog.dom.classes");
+goog.require("goog.math.Coordinate");
+goog.require("goog.math.Size");
+goog.require("goog.object");
+goog.require("goog.string");
+goog.require("goog.userAgent");
+goog.dom.ASSUME_QUIRKS_MODE = false;
+goog.dom.ASSUME_STANDARDS_MODE = false;
+goog.dom.COMPAT_MODE_KNOWN_ = goog.dom.ASSUME_QUIRKS_MODE || goog.dom.ASSUME_STANDARDS_MODE;
+goog.dom.NodeType = {ELEMENT:1, ATTRIBUTE:2, TEXT:3, CDATA_SECTION:4, ENTITY_REFERENCE:5, ENTITY:6, PROCESSING_INSTRUCTION:7, COMMENT:8, DOCUMENT:9, DOCUMENT_TYPE:10, DOCUMENT_FRAGMENT:11, NOTATION:12};
+goog.dom.getDomHelper = function(opt_element) {
+ return opt_element ? new goog.dom.DomHelper(goog.dom.getOwnerDocument(opt_element)) : goog.dom.defaultDomHelper_ || (goog.dom.defaultDomHelper_ = new goog.dom.DomHelper)
+};
+goog.dom.defaultDomHelper_;
+goog.dom.getDocument = function() {
+ return document
+};
+goog.dom.getElement = function(element) {
+ return goog.isString(element) ? document.getElementById(element) : element
+};
+goog.dom.$ = goog.dom.getElement;
+goog.dom.getElementsByTagNameAndClass = function(opt_tag, opt_class, opt_el) {
+ return goog.dom.getElementsByTagNameAndClass_(document, opt_tag, opt_class, opt_el)
+};
+goog.dom.getElementsByClass = function(className, opt_el) {
+ var parent = opt_el || document;
+ if(goog.dom.canUseQuerySelector_(parent)) {
+ return parent.querySelectorAll("." + className)
+ }else {
+ if(parent.getElementsByClassName) {
+ return parent.getElementsByClassName(className)
+ }
+ }
+ return goog.dom.getElementsByTagNameAndClass_(document, "*", className, opt_el)
+};
+goog.dom.getElementByClass = function(className, opt_el) {
+ var parent = opt_el || document;
+ var retVal = null;
+ if(goog.dom.canUseQuerySelector_(parent)) {
+ retVal = parent.querySelector("." + className)
+ }else {
+ retVal = goog.dom.getElementsByClass(className, opt_el)[0]
+ }
+ return retVal || null
+};
+goog.dom.canUseQuerySelector_ = function(parent) {
+ return parent.querySelectorAll && parent.querySelector && (!goog.userAgent.WEBKIT || goog.dom.isCss1CompatMode_(document) || goog.userAgent.isVersion("528"))
+};
+goog.dom.getElementsByTagNameAndClass_ = function(doc, opt_tag, opt_class, opt_el) {
+ var parent = opt_el || doc;
+ var tagName = opt_tag && opt_tag != "*" ? opt_tag.toUpperCase() : "";
+ if(goog.dom.canUseQuerySelector_(parent) && (tagName || opt_class)) {
+ var query = tagName + (opt_class ? "." + opt_class : "");
+ return parent.querySelectorAll(query)
+ }
+ if(opt_class && parent.getElementsByClassName) {
+ var els = parent.getElementsByClassName(opt_class);
+ if(tagName) {
+ var arrayLike = {};
+ var len = 0;
+ for(var i = 0, el;el = els[i];i++) {
+ if(tagName == el.nodeName) {
+ arrayLike[len++] = el
+ }
+ }
+ arrayLike.length = len;
+ return arrayLike
+ }else {
+ return els
+ }
+ }
+ var els = parent.getElementsByTagName(tagName || "*");
+ if(opt_class) {
+ var arrayLike = {};
+ var len = 0;
+ for(var i = 0, el;el = els[i];i++) {
+ var className = el.className;
+ if(typeof className.split == "function" && goog.array.contains(className.split(/\s+/), opt_class)) {
+ arrayLike[len++] = el
+ }
+ }
+ arrayLike.length = len;
+ return arrayLike
+ }else {
+ return els
+ }
+};
+goog.dom.$$ = goog.dom.getElementsByTagNameAndClass;
+goog.dom.setProperties = function(element, properties) {
+ goog.object.forEach(properties, function(val, key) {
+ if(key == "style") {
+ element.style.cssText = val
+ }else {
+ if(key == "class") {
+ element.className = val
+ }else {
+ if(key == "for") {
+ element.htmlFor = val
+ }else {
+ if(key in goog.dom.DIRECT_ATTRIBUTE_MAP_) {
+ element.setAttribute(goog.dom.DIRECT_ATTRIBUTE_MAP_[key], val)
+ }else {
+ element[key] = val
+ }
+ }
+ }
+ }
+ })
+};
+goog.dom.DIRECT_ATTRIBUTE_MAP_ = {cellpadding:"cellPadding", cellspacing:"cellSpacing", colspan:"colSpan", rowspan:"rowSpan", valign:"vAlign", height:"height", width:"width", usemap:"useMap", frameborder:"frameBorder", type:"type"};
+goog.dom.getViewportSize = function(opt_window) {
+ return goog.dom.getViewportSize_(opt_window || window)
+};
+goog.dom.getViewportSize_ = function(win) {
+ var doc = win.document;
+ if(goog.userAgent.WEBKIT && !goog.userAgent.isVersion("500") && !goog.userAgent.MOBILE) {
+ if(typeof win.innerHeight == "undefined") {
+ win = window
+ }
+ var innerHeight = win.innerHeight;
+ var scrollHeight = win.document.documentElement.scrollHeight;
+ if(win == win.top) {
+ if(scrollHeight < innerHeight) {
+ innerHeight -= 15
+ }
+ }
+ return new goog.math.Size(win.innerWidth, innerHeight)
+ }
+ var readsFromDocumentElement = goog.dom.isCss1CompatMode_(doc);
+ if(goog.userAgent.OPERA && !goog.userAgent.isVersion("9.50")) {
+ readsFromDocumentElement = false
+ }
+ var el = readsFromDocumentElement ? doc.documentElement : doc.body;
+ return new goog.math.Size(el.clientWidth, el.clientHeight)
+};
+goog.dom.getDocumentHeight = function() {
+ return goog.dom.getDocumentHeight_(window)
+};
+goog.dom.getDocumentHeight_ = function(win) {
+ var doc = win.document;
+ var height = 0;
+ if(doc) {
+ var vh = goog.dom.getViewportSize_(win).height;
+ var body = doc.body;
+ var docEl = doc.documentElement;
+ if(goog.dom.isCss1CompatMode_(doc) && docEl.scrollHeight) {
+ height = docEl.scrollHeight != vh ? docEl.scrollHeight : docEl.offsetHeight
+ }else {
+ var sh = docEl.scrollHeight;
+ var oh = docEl.offsetHeight;
+ if(docEl.clientHeight != oh) {
+ sh = body.scrollHeight;
+ oh = body.offsetHeight
+ }
+ if(sh > vh) {
+ height = sh > oh ? sh : oh
+ }else {
+ height = sh < oh ? sh : oh
+ }
+ }
+ }
+ return height
+};
+goog.dom.getPageScroll = function(opt_window) {
+ var win = opt_window || goog.global || window;
+ return goog.dom.getDomHelper(win.document).getDocumentScroll()
+};
+goog.dom.getDocumentScroll = function() {
+ return goog.dom.getDocumentScroll_(document)
+};
+goog.dom.getDocumentScroll_ = function(doc) {
+ var el = goog.dom.getDocumentScrollElement_(doc);
+ return new goog.math.Coordinate(el.scrollLeft, el.scrollTop)
+};
+goog.dom.getDocumentScrollElement = function() {
+ return goog.dom.getDocumentScrollElement_(document)
+};
+goog.dom.getDocumentScrollElement_ = function(doc) {
+ return!goog.userAgent.WEBKIT && goog.dom.isCss1CompatMode_(doc) ? doc.documentElement : doc.body
+};
+goog.dom.getWindow = function(opt_doc) {
+ return opt_doc ? goog.dom.getWindow_(opt_doc) : window
+};
+goog.dom.getWindow_ = function(doc) {
+ return doc.parentWindow || doc.defaultView
+};
+goog.dom.createDom = function(tagName, opt_attributes, var_args) {
+ return goog.dom.createDom_(document, arguments)
+};
+goog.dom.createDom_ = function(doc, args) {
+ var tagName = args[0];
+ var attributes = args[1];
+ if(!goog.dom.BrowserFeature.CAN_ADD_NAME_OR_TYPE_ATTRIBUTES && attributes && (attributes.name || attributes.type)) {
+ var tagNameArr = ["<", tagName];
+ if(attributes.name) {
+ tagNameArr.push(' name="', goog.string.htmlEscape(attributes.name), '"')
+ }
+ if(attributes.type) {
+ tagNameArr.push(' type="', goog.string.htmlEscape(attributes.type), '"');
+ var clone = {};
+ goog.object.extend(clone, attributes);
+ attributes = clone;
+ delete attributes.type
+ }
+ tagNameArr.push(">");
+ tagName = tagNameArr.join("")
+ }
+ var element = doc.createElement(tagName);
+ if(attributes) {
+ if(goog.isString(attributes)) {
+ element.className = attributes
+ }else {
+ if(goog.isArray(attributes)) {
+ goog.dom.classes.add.apply(null, [element].concat(attributes))
+ }else {
+ goog.dom.setProperties(element, attributes)
+ }
+ }
+ }
+ if(args.length > 2) {
+ goog.dom.append_(doc, element, args, 2)
+ }
+ return element
+};
+goog.dom.append_ = function(doc, parent, args, startIndex) {
+ function childHandler(child) {
+ if(child) {
+ parent.appendChild(goog.isString(child) ? doc.createTextNode(child) : child)
+ }
+ }
+ for(var i = startIndex;i < args.length;i++) {
+ var arg = args[i];
+ if(goog.isArrayLike(arg) && !goog.dom.isNodeLike(arg)) {
+ goog.array.forEach(goog.dom.isNodeList(arg) ? goog.array.clone(arg) : arg, childHandler)
+ }else {
+ childHandler(arg)
+ }
+ }
+};
+goog.dom.$dom = goog.dom.createDom;
+goog.dom.createElement = function(name) {
+ return document.createElement(name)
+};
+goog.dom.createTextNode = function(content) {
+ return document.createTextNode(content)
+};
+goog.dom.createTable = function(rows, columns, opt_fillWithNbsp) {
+ return goog.dom.createTable_(document, rows, columns, !!opt_fillWithNbsp)
+};
+goog.dom.createTable_ = function(doc, rows, columns, fillWithNbsp) {
+ var rowHtml = ["<tr>"];
+ for(var i = 0;i < columns;i++) {
+ rowHtml.push(fillWithNbsp ? "<td>&nbsp;</td>" : "<td></td>")
+ }
+ rowHtml.push("</tr>");
+ rowHtml = rowHtml.join("");
+ var totalHtml = ["<table>"];
+ for(i = 0;i < rows;i++) {
+ totalHtml.push(rowHtml)
+ }
+ totalHtml.push("</table>");
+ var elem = doc.createElement(goog.dom.TagName.DIV);
+ elem.innerHTML = totalHtml.join("");
+ return elem.removeChild(elem.firstChild)
+};
+goog.dom.htmlToDocumentFragment = function(htmlString) {
+ return goog.dom.htmlToDocumentFragment_(document, htmlString)
+};
+goog.dom.htmlToDocumentFragment_ = function(doc, htmlString) {
+ var tempDiv = doc.createElement("div");
+ if(goog.dom.BrowserFeature.INNER_HTML_NEEDS_SCOPED_ELEMENT) {
+ tempDiv.innerHTML = "<br>" + htmlString;
+ tempDiv.removeChild(tempDiv.firstChild)
+ }else {
+ tempDiv.innerHTML = htmlString
+ }
+ if(tempDiv.childNodes.length == 1) {
+ return tempDiv.removeChild(tempDiv.firstChild)
+ }else {
+ var fragment = doc.createDocumentFragment();
+ while(tempDiv.firstChild) {
+ fragment.appendChild(tempDiv.firstChild)
+ }
+ return fragment
+ }
+};
+goog.dom.getCompatMode = function() {
+ return goog.dom.isCss1CompatMode() ? "CSS1Compat" : "BackCompat"
+};
+goog.dom.isCss1CompatMode = function() {
+ return goog.dom.isCss1CompatMode_(document)
+};
+goog.dom.isCss1CompatMode_ = function(doc) {
+ if(goog.dom.COMPAT_MODE_KNOWN_) {
+ return goog.dom.ASSUME_STANDARDS_MODE
+ }
+ return doc.compatMode == "CSS1Compat"
+};
+goog.dom.canHaveChildren = function(node) {
+ if(node.nodeType != goog.dom.NodeType.ELEMENT) {
+ return false
+ }
+ switch(node.tagName) {
+ case goog.dom.TagName.APPLET:
+ ;
+ case goog.dom.TagName.AREA:
+ ;
+ case goog.dom.TagName.BASE:
+ ;
+ case goog.dom.TagName.BR:
+ ;
+ case goog.dom.TagName.COL:
+ ;
+ case goog.dom.TagName.FRAME:
+ ;
+ case goog.dom.TagName.HR:
+ ;
+ case goog.dom.TagName.IMG:
+ ;
+ case goog.dom.TagName.INPUT:
+ ;
+ case goog.dom.TagName.IFRAME:
+ ;
+ case goog.dom.TagName.ISINDEX:
+ ;
+ case goog.dom.TagName.LINK:
+ ;
+ case goog.dom.TagName.NOFRAMES:
+ ;
+ case goog.dom.TagName.NOSCRIPT:
+ ;
+ case goog.dom.TagName.META:
+ ;
+ case goog.dom.TagName.OBJECT:
+ ;
+ case goog.dom.TagName.PARAM:
+ ;
+ case goog.dom.TagName.SCRIPT:
+ ;
+ case goog.dom.TagName.STYLE:
+ return false
+ }
+ return true
+};
+goog.dom.appendChild = function(parent, child) {
+ parent.appendChild(child)
+};
+goog.dom.append = function(parent, var_args) {
+ goog.dom.append_(goog.dom.getOwnerDocument(parent), parent, arguments, 1)
+};
+goog.dom.removeChildren = function(node) {
+ var child;
+ while(child = node.firstChild) {
+ node.removeChild(child)
+ }
+};
+goog.dom.insertSiblingBefore = function(newNode, refNode) {
+ if(refNode.parentNode) {
+ refNode.parentNode.insertBefore(newNode, refNode)
+ }
+};
+goog.dom.insertSiblingAfter = function(newNode, refNode) {
+ if(refNode.parentNode) {
+ refNode.parentNode.insertBefore(newNode, refNode.nextSibling)
+ }
+};
+goog.dom.removeNode = function(node) {
+ return node && node.parentNode ? node.parentNode.removeChild(node) : null
+};
+goog.dom.replaceNode = function(newNode, oldNode) {
+ var parent = oldNode.parentNode;
+ if(parent) {
+ parent.replaceChild(newNode, oldNode)
+ }
+};
+goog.dom.flattenElement = function(element) {
+ var child, parent = element.parentNode;
+ if(parent && parent.nodeType != goog.dom.NodeType.DOCUMENT_FRAGMENT) {
+ if(element.removeNode) {
+ return element.removeNode(false)
+ }else {
+ while(child = element.firstChild) {
+ parent.insertBefore(child, element)
+ }
+ return goog.dom.removeNode(element)
+ }
+ }
+};
+goog.dom.getFirstElementChild = function(node) {
+ return goog.dom.getNextElementNode_(node.firstChild, true)
+};
+goog.dom.getLastElementChild = function(node) {
+ return goog.dom.getNextElementNode_(node.lastChild, false)
+};
+goog.dom.getNextElementSibling = function(node) {
+ return goog.dom.getNextElementNode_(node.nextSibling, true)
+};
+goog.dom.getPreviousElementSibling = function(node) {
+ return goog.dom.getNextElementNode_(node.previousSibling, false)
+};
+goog.dom.getNextElementNode_ = function(node, forward) {
+ while(node && node.nodeType != goog.dom.NodeType.ELEMENT) {
+ node = forward ? node.nextSibling : node.previousSibling
+ }
+ return node
+};
+goog.dom.getNextNode = function(node) {
+ if(!node) {
+ return null
+ }
+ if(node.firstChild) {
+ return node.firstChild
+ }
+ while(node && !node.nextSibling) {
+ node = node.parentNode
+ }
+ return node ? node.nextSibling : null
+};
+goog.dom.getPreviousNode = function(node) {
+ if(!node) {
+ return null
+ }
+ if(!node.previousSibling) {
+ return node.parentNode
+ }
+ node = node.previousSibling;
+ while(node && node.lastChild) {
+ node = node.lastChild
+ }
+ return node
+};
+goog.dom.isNodeLike = function(obj) {
+ return goog.isObject(obj) && obj.nodeType > 0
+};
+goog.dom.contains = function(parent, descendant) {
+ if(parent.contains && descendant.nodeType == goog.dom.NodeType.ELEMENT) {
+ return parent == descendant || parent.contains(descendant)
+ }
+ if(typeof parent.compareDocumentPosition != "undefined") {
+ return parent == descendant || Boolean(parent.compareDocumentPosition(descendant) & 16)
+ }
+ while(descendant && parent != descendant) {
+ descendant = descendant.parentNode
+ }
+ return descendant == parent
+};
+goog.dom.compareNodeOrder = function(node1, node2) {
+ if(node1 == node2) {
+ return 0
+ }
+ if(node1.compareDocumentPosition) {
+ return node1.compareDocumentPosition(node2) & 2 ? 1 : -1
+ }
+ if("sourceIndex" in node1 || node1.parentNode && "sourceIndex" in node1.parentNode) {
+ var isElement1 = node1.nodeType == goog.dom.NodeType.ELEMENT;
+ var isElement2 = node2.nodeType == goog.dom.NodeType.ELEMENT;
+ if(isElement1 && isElement2) {
+ return node1.sourceIndex - node2.sourceIndex
+ }else {
+ var parent1 = node1.parentNode;
+ var parent2 = node2.parentNode;
+ if(parent1 == parent2) {
+ return goog.dom.compareSiblingOrder_(node1, node2)
+ }
+ if(!isElement1 && goog.dom.contains(parent1, node2)) {
+ return-1 * goog.dom.compareParentsDescendantNodeIe_(node1, node2)
+ }
+ if(!isElement2 && goog.dom.contains(parent2, node1)) {
+ return goog.dom.compareParentsDescendantNodeIe_(node2, node1)
+ }
+ return(isElement1 ? node1.sourceIndex : parent1.sourceIndex) - (isElement2 ? node2.sourceIndex : parent2.sourceIndex)
+ }
+ }
+ var doc = goog.dom.getOwnerDocument(node1);
+ var range1, range2;
+ range1 = doc.createRange();
+ range1.selectNode(node1);
+ range1.collapse(true);
+ range2 = doc.createRange();
+ range2.selectNode(node2);
+ range2.collapse(true);
+ return range1.compareBoundaryPoints(goog.global["Range"].START_TO_END, range2)
+};
+goog.dom.compareParentsDescendantNodeIe_ = function(textNode, node) {
+ var parent = textNode.parentNode;
+ if(parent == node) {
+ return-1
+ }
+ var sibling = node;
+ while(sibling.parentNode != parent) {
+ sibling = sibling.parentNode
+ }
+ return goog.dom.compareSiblingOrder_(sibling, textNode)
+};
+goog.dom.compareSiblingOrder_ = function(node1, node2) {
+ var s = node2;
+ while(s = s.previousSibling) {
+ if(s == node1) {
+ return-1
+ }
+ }
+ return 1
+};
+goog.dom.findCommonAncestor = function(var_args) {
+ var i, count = arguments.length;
+ if(!count) {
+ return null
+ }else {
+ if(count == 1) {
+ return arguments[0]
+ }
+ }
+ var paths = [];
+ var minLength = Infinity;
+ for(i = 0;i < count;i++) {
+ var ancestors = [];
+ var node = arguments[i];
+ while(node) {
+ ancestors.unshift(node);
+ node = node.parentNode
+ }
+ paths.push(ancestors);
+ minLength = Math.min(minLength, ancestors.length)
+ }
+ var output = null;
+ for(i = 0;i < minLength;i++) {
+ var first = paths[0][i];
+ for(var j = 1;j < count;j++) {
+ if(first != paths[j][i]) {
+ return output
+ }
+ }
+ output = first
+ }
+ return output
+};
+goog.dom.getOwnerDocument = function(node) {
+ return node.nodeType == goog.dom.NodeType.DOCUMENT ? node : node.ownerDocument || node.document
+};
+goog.dom.getFrameContentDocument = function(frame) {
+ var doc;
+ if(goog.userAgent.WEBKIT) {
+ doc = frame.document || frame.contentWindow.document
+ }else {
+ doc = frame.contentDocument || frame.contentWindow.document
+ }
+ return doc
+};
+goog.dom.getFrameContentWindow = function(frame) {
+ return frame.contentWindow || goog.dom.getWindow_(goog.dom.getFrameContentDocument(frame))
+};
+goog.dom.setTextContent = function(element, text) {
+ if("textContent" in element) {
+ element.textContent = text
+ }else {
+ if(element.firstChild && element.firstChild.nodeType == goog.dom.NodeType.TEXT) {
+ while(element.lastChild != element.firstChild) {
+ element.removeChild(element.lastChild)
+ }
+ element.firstChild.data = text
+ }else {
+ goog.dom.removeChildren(element);
+ var doc = goog.dom.getOwnerDocument(element);
+ element.appendChild(doc.createTextNode(text))
+ }
+ }
+};
+goog.dom.getOuterHtml = function(element) {
+ if("outerHTML" in element) {
+ return element.outerHTML
+ }else {
+ var doc = goog.dom.getOwnerDocument(element);
+ var div = doc.createElement("div");
+ div.appendChild(element.cloneNode(true));
+ return div.innerHTML
+ }
+};
+goog.dom.findNode = function(root, p) {
+ var rv = [];
+ var found = goog.dom.findNodes_(root, p, rv, true);
+ return found ? rv[0] : undefined
+};
+goog.dom.findNodes = function(root, p) {
+ var rv = [];
+ goog.dom.findNodes_(root, p, rv, false);
+ return rv
+};
+goog.dom.findNodes_ = function(root, p, rv, findOne) {
+ if(root != null) {
+ for(var i = 0, child;child = root.childNodes[i];i++) {
+ if(p(child)) {
+ rv.push(child);
+ if(findOne) {
+ return true
+ }
+ }
+ if(goog.dom.findNodes_(child, p, rv, findOne)) {
+ return true
+ }
+ }
+ }
+ return false
+};
+goog.dom.TAGS_TO_IGNORE_ = {SCRIPT:1, STYLE:1, HEAD:1, IFRAME:1, OBJECT:1};
+goog.dom.PREDEFINED_TAG_VALUES_ = {IMG:" ", BR:"\n"};
+goog.dom.isFocusableTabIndex = function(element) {
+ var attrNode = element.getAttributeNode("tabindex");
+ if(attrNode && attrNode.specified) {
+ var index = element.tabIndex;
+ return goog.isNumber(index) && index >= 0
+ }
+ return false
+};
+goog.dom.setFocusableTabIndex = function(element, enable) {
+ if(enable) {
+ element.tabIndex = 0
+ }else {
+ element.removeAttribute("tabIndex")
+ }
+};
+goog.dom.getTextContent = function(node) {
+ var textContent;
+ if(goog.dom.BrowserFeature.CAN_USE_INNER_TEXT && "innerText" in node) {
+ textContent = goog.string.canonicalizeNewlines(node.innerText)
+ }else {
+ var buf = [];
+ goog.dom.getTextContent_(node, buf, true);
+ textContent = buf.join("")
+ }
+ textContent = textContent.replace(/ \xAD /g, " ").replace(/\xAD/g, "");
+ if(!goog.userAgent.IE) {
+ textContent = textContent.replace(/ +/g, " ")
+ }
+ if(textContent != " ") {
+ textContent = textContent.replace(/^\s*/, "")
+ }
+ return textContent
+};
+goog.dom.getRawTextContent = function(node) {
+ var buf = [];
+ goog.dom.getTextContent_(node, buf, false);
+ return buf.join("")
+};
+goog.dom.getTextContent_ = function(node, buf, normalizeWhitespace) {
+ if(node.nodeName in goog.dom.TAGS_TO_IGNORE_) {
+ }else {
+ if(node.nodeType == goog.dom.NodeType.TEXT) {
+ if(normalizeWhitespace) {
+ buf.push(String(node.nodeValue).replace(/(\r\n|\r|\n)/g, ""))
+ }else {
+ buf.push(node.nodeValue)
+ }
+ }else {
+ if(node.nodeName in goog.dom.PREDEFINED_TAG_VALUES_) {
+ buf.push(goog.dom.PREDEFINED_TAG_VALUES_[node.nodeName])
+ }else {
+ var child = node.firstChild;
+ while(child) {
+ goog.dom.getTextContent_(child, buf, normalizeWhitespace);
+ child = child.nextSibling
+ }
+ }
+ }
+ }
+};
+goog.dom.getNodeTextLength = function(node) {
+ return goog.dom.getTextContent(node).length
+};
+goog.dom.getNodeTextOffset = function(node, opt_offsetParent) {
+ var root = opt_offsetParent || goog.dom.getOwnerDocument(node).body;
+ var buf = [];
+ while(node && node != root) {
+ var cur = node;
+ while(cur = cur.previousSibling) {
+ buf.unshift(goog.dom.getTextContent(cur))
+ }
+ node = node.parentNode
+ }
+ return goog.string.trimLeft(buf.join("")).replace(/ +/g, " ").length
+};
+goog.dom.getNodeAtOffset = function(parent, offset, opt_result) {
+ var stack = [parent], pos = 0, cur;
+ while(stack.length > 0 && pos < offset) {
+ cur = stack.pop();
+ if(cur.nodeName in goog.dom.TAGS_TO_IGNORE_) {
+ }else {
+ if(cur.nodeType == goog.dom.NodeType.TEXT) {
+ var text = cur.nodeValue.replace(/(\r\n|\r|\n)/g, "").replace(/ +/g, " ");
+ pos += text.length
+ }else {
+ if(cur.nodeName in goog.dom.PREDEFINED_TAG_VALUES_) {
+ pos += goog.dom.PREDEFINED_TAG_VALUES_[cur.nodeName].length
+ }else {
+ for(var i = cur.childNodes.length - 1;i >= 0;i--) {
+ stack.push(cur.childNodes[i])
+ }
+ }
+ }
+ }
+ }
+ if(goog.isObject(opt_result)) {
+ opt_result.remainder = cur ? cur.nodeValue.length + offset - pos - 1 : 0;
+ opt_result.node = cur
+ }
+ return cur
+};
+goog.dom.isNodeList = function(val) {
+ if(val && typeof val.length == "number") {
+ if(goog.isObject(val)) {
+ return typeof val.item == "function" || typeof val.item == "string"
+ }else {
+ if(goog.isFunction(val)) {
+ return typeof val.item == "function"
+ }
+ }
+ }
+ return false
+};
+goog.dom.getAncestorByTagNameAndClass = function(element, opt_tag, opt_class) {
+ var tagName = opt_tag ? opt_tag.toUpperCase() : null;
+ return goog.dom.getAncestor(element, function(node) {
+ return(!tagName || node.nodeName == tagName) && (!opt_class || goog.dom.classes.has(node, opt_class))
+ }, true)
+};
+goog.dom.getAncestor = function(element, matcher, opt_includeNode, opt_maxSearchSteps) {
+ if(!opt_includeNode) {
+ element = element.parentNode
+ }
+ var ignoreSearchSteps = opt_maxSearchSteps == null;
+ var steps = 0;
+ while(element && (ignoreSearchSteps || steps <= opt_maxSearchSteps)) {
+ if(matcher(element)) {
+ return element
+ }
+ element = element.parentNode;
+ steps++
+ }
+ return null
+};
+goog.dom.DomHelper = function(opt_document) {
+ this.document_ = opt_document || goog.global.document || document
+};
+goog.dom.DomHelper.prototype.getDomHelper = goog.dom.getDomHelper;
+goog.dom.DomHelper.prototype.setDocument = function(document) {
+ this.document_ = document
+};
+goog.dom.DomHelper.prototype.getDocument = function() {
+ return this.document_
+};
+goog.dom.DomHelper.prototype.getElement = function(element) {
+ if(goog.isString(element)) {
+ return this.document_.getElementById(element)
+ }else {
+ return element
+ }
+};
+goog.dom.DomHelper.prototype.$ = goog.dom.DomHelper.prototype.getElement;
+goog.dom.DomHelper.prototype.getElementsByTagNameAndClass = function(opt_tag, opt_class, opt_el) {
+ return goog.dom.getElementsByTagNameAndClass_(this.document_, opt_tag, opt_class, opt_el)
+};
+goog.dom.DomHelper.prototype.getElementsByClass = function(className, opt_el) {
+ var doc = opt_el || this.document_;
+ return goog.dom.getElementsByClass(className, doc)
+};
+goog.dom.DomHelper.prototype.getElementByClass = function(className, opt_el) {
+ var doc = opt_el || this.document_;
+ return goog.dom.getElementByClass(className, doc)
+};
+goog.dom.DomHelper.prototype.$$ = goog.dom.DomHelper.prototype.getElementsByTagNameAndClass;
+goog.dom.DomHelper.prototype.setProperties = goog.dom.setProperties;
+goog.dom.DomHelper.prototype.getViewportSize = function(opt_window) {
+ return goog.dom.getViewportSize(opt_window || this.getWindow())
+};
+goog.dom.DomHelper.prototype.getDocumentHeight = function() {
+ return goog.dom.getDocumentHeight_(this.getWindow())
+};
+goog.dom.Appendable;
+goog.dom.DomHelper.prototype.createDom = function(tagName, opt_attributes, var_args) {
+ return goog.dom.createDom_(this.document_, arguments)
+};
+goog.dom.DomHelper.prototype.$dom = goog.dom.DomHelper.prototype.createDom;
+goog.dom.DomHelper.prototype.createElement = function(name) {
+ return this.document_.createElement(name)
+};
+goog.dom.DomHelper.prototype.createTextNode = function(content) {
+ return this.document_.createTextNode(content)
+};
+goog.dom.DomHelper.prototype.createTable = function(rows, columns, opt_fillWithNbsp) {
+ return goog.dom.createTable_(this.document_, rows, columns, !!opt_fillWithNbsp)
+};
+goog.dom.DomHelper.prototype.htmlToDocumentFragment = function(htmlString) {
+ return goog.dom.htmlToDocumentFragment_(this.document_, htmlString)
+};
+goog.dom.DomHelper.prototype.getCompatMode = function() {
+ return this.isCss1CompatMode() ? "CSS1Compat" : "BackCompat"
+};
+goog.dom.DomHelper.prototype.isCss1CompatMode = function() {
+ return goog.dom.isCss1CompatMode_(this.document_)
+};
+goog.dom.DomHelper.prototype.getWindow = function() {
+ return goog.dom.getWindow_(this.document_)
+};
+goog.dom.DomHelper.prototype.getDocumentScrollElement = function() {
+ return goog.dom.getDocumentScrollElement_(this.document_)
+};
+goog.dom.DomHelper.prototype.getDocumentScroll = function() {
+ return goog.dom.getDocumentScroll_(this.document_)
+};
+goog.dom.DomHelper.prototype.appendChild = goog.dom.appendChild;
+goog.dom.DomHelper.prototype.append = goog.dom.append;
+goog.dom.DomHelper.prototype.removeChildren = goog.dom.removeChildren;
+goog.dom.DomHelper.prototype.insertSiblingBefore = goog.dom.insertSiblingBefore;
+goog.dom.DomHelper.prototype.insertSiblingAfter = goog.dom.insertSiblingAfter;
+goog.dom.DomHelper.prototype.removeNode = goog.dom.removeNode;
+goog.dom.DomHelper.prototype.replaceNode = goog.dom.replaceNode;
+goog.dom.DomHelper.prototype.flattenElement = goog.dom.flattenElement;
+goog.dom.DomHelper.prototype.getFirstElementChild = goog.dom.getFirstElementChild;
+goog.dom.DomHelper.prototype.getLastElementChild = goog.dom.getLastElementChild;
+goog.dom.DomHelper.prototype.getNextElementSibling = goog.dom.getNextElementSibling;
+goog.dom.DomHelper.prototype.getPreviousElementSibling = goog.dom.getPreviousElementSibling;
+goog.dom.DomHelper.prototype.getNextNode = goog.dom.getNextNode;
+goog.dom.DomHelper.prototype.getPreviousNode = goog.dom.getPreviousNode;
+goog.dom.DomHelper.prototype.isNodeLike = goog.dom.isNodeLike;
+goog.dom.DomHelper.prototype.contains = goog.dom.contains;
+goog.dom.DomHelper.prototype.getOwnerDocument = goog.dom.getOwnerDocument;
+goog.dom.DomHelper.prototype.getFrameContentDocument = goog.dom.getFrameContentDocument;
+goog.dom.DomHelper.prototype.getFrameContentWindow = goog.dom.getFrameContentWindow;
+goog.dom.DomHelper.prototype.setTextContent = goog.dom.setTextContent;
+goog.dom.DomHelper.prototype.findNode = goog.dom.findNode;
+goog.dom.DomHelper.prototype.findNodes = goog.dom.findNodes;
+goog.dom.DomHelper.prototype.getTextContent = goog.dom.getTextContent;
+goog.dom.DomHelper.prototype.getNodeTextLength = goog.dom.getNodeTextLength;
+goog.dom.DomHelper.prototype.getNodeTextOffset = goog.dom.getNodeTextOffset;
+goog.dom.DomHelper.prototype.getAncestorByTagNameAndClass = goog.dom.getAncestorByTagNameAndClass;
+goog.dom.DomHelper.prototype.getAncestor = goog.dom.getAncestor;
+goog.provide("goog.Disposable");
+goog.provide("goog.dispose");
+goog.Disposable = function() {
+};
+goog.Disposable.prototype.disposed_ = false;
+goog.Disposable.prototype.isDisposed = function() {
+ return this.disposed_
+};
+goog.Disposable.prototype.getDisposed = goog.Disposable.prototype.isDisposed;
+goog.Disposable.prototype.dispose = function() {
+ if(!this.disposed_) {
+ this.disposed_ = true;
+ this.disposeInternal()
+ }
+};
+goog.Disposable.prototype.disposeInternal = function() {
+};
+goog.dispose = function(obj) {
+ if(obj && typeof obj.dispose == "function") {
+ obj.dispose()
+ }
+};
+goog.provide("goog.structs");
+goog.require("goog.array");
+goog.require("goog.object");
+goog.structs.getCount = function(col) {
+ if(typeof col.getCount == "function") {
+ return col.getCount()
+ }
+ if(goog.isArrayLike(col) || goog.isString(col)) {
+ return col.length
+ }
+ return goog.object.getCount(col)
+};
+goog.structs.getValues = function(col) {
+ if(typeof col.getValues == "function") {
+ return col.getValues()
+ }
+ if(goog.isString(col)) {
+ return col.split("")
+ }
+ if(goog.isArrayLike(col)) {
+ var rv = [];
+ var l = col.length;
+ for(var i = 0;i < l;i++) {
+ rv.push(col[i])
+ }
+ return rv
+ }
+ return goog.object.getValues(col)
+};
+goog.structs.getKeys = function(col) {
+ if(typeof col.getKeys == "function") {
+ return col.getKeys()
+ }
+ if(typeof col.getValues == "function") {
+ return undefined
+ }
+ if(goog.isArrayLike(col) || goog.isString(col)) {
+ var rv = [];
+ var l = col.length;
+ for(var i = 0;i < l;i++) {
+ rv.push(i)
+ }
+ return rv
+ }
+ return goog.object.getKeys(col)
+};
+goog.structs.contains = function(col, val) {
+ if(typeof col.contains == "function") {
+ return col.contains(val)
+ }
+ if(typeof col.containsValue == "function") {
+ return col.containsValue(val)
+ }
+ if(goog.isArrayLike(col) || goog.isString(col)) {
+ return goog.array.contains(col, val)
+ }
+ return goog.object.containsValue(col, val)
+};
+goog.structs.isEmpty = function(col) {
+ if(typeof col.isEmpty == "function") {
+ return col.isEmpty()
+ }
+ if(goog.isArrayLike(col) || goog.isString(col)) {
+ return goog.array.isEmpty(col)
+ }
+ return goog.object.isEmpty(col)
+};
+goog.structs.clear = function(col) {
+ if(typeof col.clear == "function") {
+ col.clear()
+ }else {
+ if(goog.isArrayLike(col)) {
+ goog.array.clear(col)
+ }else {
+ goog.object.clear(col)
+ }
+ }
+};
+goog.structs.forEach = function(col, f, opt_obj) {
+ if(typeof col.forEach == "function") {
+ col.forEach(f, opt_obj)
+ }else {
+ if(goog.isArrayLike(col) || goog.isString(col)) {
+ goog.array.forEach(col, f, opt_obj)
+ }else {
+ var keys = goog.structs.getKeys(col);
+ var values = goog.structs.getValues(col);
+ var l = values.length;
+ for(var i = 0;i < l;i++) {
+ f.call(opt_obj, values[i], keys && keys[i], col)
+ }
+ }
+ }
+};
+goog.structs.filter = function(col, f, opt_obj) {
+ if(typeof col.filter == "function") {
+ return col.filter(f, opt_obj)
+ }
+ if(goog.isArrayLike(col) || goog.isString(col)) {
+ return goog.array.filter(col, f, opt_obj)
+ }
+ var rv;
+ var keys = goog.structs.getKeys(col);
+ var values = goog.structs.getValues(col);
+ var l = values.length;
+ if(keys) {
+ rv = {};
+ for(var i = 0;i < l;i++) {
+ if(f.call(opt_obj, values[i], keys[i], col)) {
+ rv[keys[i]] = values[i]
+ }
+ }
+ }else {
+ rv = [];
+ for(var i = 0;i < l;i++) {
+ if(f.call(opt_obj, values[i], undefined, col)) {
+ rv.push(values[i])
+ }
+ }
+ }
+ return rv
+};
+goog.structs.map = function(col, f, opt_obj) {
+ if(typeof col.map == "function") {
+ return col.map(f, opt_obj)
+ }
+ if(goog.isArrayLike(col) || goog.isString(col)) {
+ return goog.array.map(col, f, opt_obj)
+ }
+ var rv;
+ var keys = goog.structs.getKeys(col);
+ var values = goog.structs.getValues(col);
+ var l = values.length;
+ if(keys) {
+ rv = {};
+ for(var i = 0;i < l;i++) {
+ rv[keys[i]] = f.call(opt_obj, values[i], keys[i], col)
+ }
+ }else {
+ rv = [];
+ for(var i = 0;i < l;i++) {
+ rv[i] = f.call(opt_obj, values[i], undefined, col)
+ }
+ }
+ return rv
+};
+goog.structs.some = function(col, f, opt_obj) {
+ if(typeof col.some == "function") {
+ return col.some(f, opt_obj)
+ }
+ if(goog.isArrayLike(col) || goog.isString(col)) {
+ return goog.array.some(col, f, opt_obj)
+ }
+ var keys = goog.structs.getKeys(col);
+ var values = goog.structs.getValues(col);
+ var l = values.length;
+ for(var i = 0;i < l;i++) {
+ if(f.call(opt_obj, values[i], keys && keys[i], col)) {
+ return true
+ }
+ }
+ return false
+};
+goog.structs.every = function(col, f, opt_obj) {
+ if(typeof col.every == "function") {
+ return col.every(f, opt_obj)
+ }
+ if(goog.isArrayLike(col) || goog.isString(col)) {
+ return goog.array.every(col, f, opt_obj)
+ }
+ var keys = goog.structs.getKeys(col);
+ var values = goog.structs.getValues(col);
+ var l = values.length;
+ for(var i = 0;i < l;i++) {
+ if(!f.call(opt_obj, values[i], keys && keys[i], col)) {
+ return false
+ }
+ }
+ return true
+};
+goog.provide("goog.iter");
+goog.provide("goog.iter.Iterator");
+goog.provide("goog.iter.StopIteration");
+goog.require("goog.array");
+goog.iter.Iterable;
+if("StopIteration" in goog.global) {
+ goog.iter.StopIteration = goog.global["StopIteration"]
+}else {
+ goog.iter.StopIteration = Error("StopIteration")
+}
+goog.iter.Iterator = function() {
+};
+goog.iter.Iterator.prototype.next = function() {
+ throw goog.iter.StopIteration;
+};
+goog.iter.Iterator.prototype.__iterator__ = function(opt_keys) {
+ return this
+};
+goog.iter.toIterator = function(iterable) {
+ if(iterable instanceof goog.iter.Iterator) {
+ return iterable
+ }
+ if(typeof iterable.__iterator__ == "function") {
+ return iterable.__iterator__(false)
+ }
+ if(goog.isArrayLike(iterable)) {
+ var i = 0;
+ var newIter = new goog.iter.Iterator;
+ newIter.next = function() {
+ while(true) {
+ if(i >= iterable.length) {
+ throw goog.iter.StopIteration;
+ }
+ if(!(i in iterable)) {
+ i++;
+ continue
+ }
+ return iterable[i++]
+ }
+ };
+ return newIter
+ }
+ throw Error("Not implemented");
+};
+goog.iter.forEach = function(iterable, f, opt_obj) {
+ if(goog.isArrayLike(iterable)) {
+ try {
+ goog.array.forEach(iterable, f, opt_obj)
+ }catch(ex) {
+ if(ex !== goog.iter.StopIteration) {
+ throw ex;
+ }
+ }
+ }else {
+ iterable = goog.iter.toIterator(iterable);
+ try {
+ while(true) {
+ f.call(opt_obj, iterable.next(), undefined, iterable)
+ }
+ }catch(ex) {
+ if(ex !== goog.iter.StopIteration) {
+ throw ex;
+ }
+ }
+ }
+};
+goog.iter.filter = function(iterable, f, opt_obj) {
+ iterable = goog.iter.toIterator(iterable);
+ var newIter = new goog.iter.Iterator;
+ newIter.next = function() {
+ while(true) {
+ var val = iterable.next();
+ if(f.call(opt_obj, val, undefined, iterable)) {
+ return val
+ }
+ }
+ };
+ return newIter
+};
+goog.iter.range = function(startOrStop, opt_stop, opt_step) {
+ var start = 0;
+ var stop = startOrStop;
+ var step = opt_step || 1;
+ if(arguments.length > 1) {
+ start = startOrStop;
+ stop = opt_stop
+ }
+ if(step == 0) {
+ throw Error("Range step argument must not be zero");
+ }
+ var newIter = new goog.iter.Iterator;
+ newIter.next = function() {
+ if(step > 0 && start >= stop || step < 0 && start <= stop) {
+ throw goog.iter.StopIteration;
+ }
+ var rv = start;
+ start += step;
+ return rv
+ };
+ return newIter
+};
+goog.iter.join = function(iterable, deliminator) {
+ return goog.iter.toArray(iterable).join(deliminator)
+};
+goog.iter.map = function(iterable, f, opt_obj) {
+ iterable = goog.iter.toIterator(iterable);
+ var newIter = new goog.iter.Iterator;
+ newIter.next = function() {
+ while(true) {
+ var val = iterable.next();
+ return f.call(opt_obj, val, undefined, iterable)
+ }
+ };
+ return newIter
+};
+goog.iter.reduce = function(iterable, f, val, opt_obj) {
+ var rval = val;
+ goog.iter.forEach(iterable, function(val) {
+ rval = f.call(opt_obj, rval, val)
+ });
+ return rval
+};
+goog.iter.some = function(iterable, f, opt_obj) {
+ iterable = goog.iter.toIterator(iterable);
+ try {
+ while(true) {
+ if(f.call(opt_obj, iterable.next(), undefined, iterable)) {
+ return true
+ }
+ }
+ }catch(ex) {
+ if(ex !== goog.iter.StopIteration) {
+ throw ex;
+ }
+ }
+ return false
+};
+goog.iter.every = function(iterable, f, opt_obj) {
+ iterable = goog.iter.toIterator(iterable);
+ try {
+ while(true) {
+ if(!f.call(opt_obj, iterable.next(), undefined, iterable)) {
+ return false
+ }
+ }
+ }catch(ex) {
+ if(ex !== goog.iter.StopIteration) {
+ throw ex;
+ }
+ }
+ return true
+};
+goog.iter.chain = function(var_args) {
+ var args = arguments;
+ var length = args.length;
+ var i = 0;
+ var newIter = new goog.iter.Iterator;
+ newIter.next = function() {
+ try {
+ if(i >= length) {
+ throw goog.iter.StopIteration;
+ }
+ var current = goog.iter.toIterator(args[i]);
+ return current.next()
+ }catch(ex) {
+ if(ex !== goog.iter.StopIteration || i >= length) {
+ throw ex;
+ }else {
+ i++;
+ return this.next()
+ }
+ }
+ };
+ return newIter
+};
+goog.iter.dropWhile = function(iterable, f, opt_obj) {
+ iterable = goog.iter.toIterator(iterable);
+ var newIter = new goog.iter.Iterator;
+ var dropping = true;
+ newIter.next = function() {
+ while(true) {
+ var val = iterable.next();
+ if(dropping && f.call(opt_obj, val, undefined, iterable)) {
+ continue
+ }else {
+ dropping = false
+ }
+ return val
+ }
+ };
+ return newIter
+};
+goog.iter.takeWhile = function(iterable, f, opt_obj) {
+ iterable = goog.iter.toIterator(iterable);
+ var newIter = new goog.iter.Iterator;
+ var taking = true;
+ newIter.next = function() {
+ while(true) {
+ if(taking) {
+ var val = iterable.next();
+ if(f.call(opt_obj, val, undefined, iterable)) {
+ return val
+ }else {
+ taking = false
+ }
+ }else {
+ throw goog.iter.StopIteration;
+ }
+ }
+ };
+ return newIter
+};
+goog.iter.toArray = function(iterable) {
+ if(goog.isArrayLike(iterable)) {
+ return goog.array.toArray(iterable)
+ }
+ iterable = goog.iter.toIterator(iterable);
+ var array = [];
+ goog.iter.forEach(iterable, function(val) {
+ array.push(val)
+ });
+ return array
+};
+goog.iter.equals = function(iterable1, iterable2) {
+ iterable1 = goog.iter.toIterator(iterable1);
+ iterable2 = goog.iter.toIterator(iterable2);
+ var b1, b2;
+ try {
+ while(true) {
+ b1 = b2 = false;
+ var val1 = iterable1.next();
+ b1 = true;
+ var val2 = iterable2.next();
+ b2 = true;
+ if(val1 != val2) {
+ return false
+ }
+ }
+ }catch(ex) {
+ if(ex !== goog.iter.StopIteration) {
+ throw ex;
+ }else {
+ if(b1 && !b2) {
+ return false
+ }
+ if(!b2) {
+ try {
+ val2 = iterable2.next();
+ return false
+ }catch(ex1) {
+ if(ex1 !== goog.iter.StopIteration) {
+ throw ex1;
+ }
+ return true
+ }
+ }
+ }
+ }
+ return false
+};
+goog.iter.nextOrValue = function(iterable, defaultValue) {
+ try {
+ return goog.iter.toIterator(iterable).next()
+ }catch(e) {
+ if(e != goog.iter.StopIteration) {
+ throw e;
+ }
+ return defaultValue
+ }
+};
+goog.provide("goog.structs.Map");
+goog.require("goog.iter.Iterator");
+goog.require("goog.iter.StopIteration");
+goog.require("goog.object");
+goog.require("goog.structs");
+goog.structs.Map = function(opt_map, var_args) {
+ this.map_ = {};
+ this.keys_ = [];
+ var argLength = arguments.length;
+ if(argLength > 1) {
+ if(argLength % 2) {
+ throw Error("Uneven number of arguments");
+ }
+ for(var i = 0;i < argLength;i += 2) {
+ this.set(arguments[i], arguments[i + 1])
+ }
+ }else {
+ if(opt_map) {
+ this.addAll(opt_map)
+ }
+ }
+};
+goog.structs.Map.prototype.count_ = 0;
+goog.structs.Map.prototype.version_ = 0;
+goog.structs.Map.prototype.getCount = function() {
+ return this.count_
+};
+goog.structs.Map.prototype.getValues = function() {
+ this.cleanupKeysArray_();
+ var rv = [];
+ for(var i = 0;i < this.keys_.length;i++) {
+ var key = this.keys_[i];
+ rv.push(this.map_[key])
+ }
+ return rv
+};
+goog.structs.Map.prototype.getKeys = function() {
+ this.cleanupKeysArray_();
+ return this.keys_.concat()
+};
+goog.structs.Map.prototype.containsKey = function(key) {
+ return goog.structs.Map.hasKey_(this.map_, key)
+};
+goog.structs.Map.prototype.containsValue = function(val) {
+ for(var i = 0;i < this.keys_.length;i++) {
+ var key = this.keys_[i];
+ if(goog.structs.Map.hasKey_(this.map_, key) && this.map_[key] == val) {
+ return true
+ }
+ }
+ return false
+};
+goog.structs.Map.prototype.equals = function(otherMap, opt_equalityFn) {
+ if(this === otherMap) {
+ return true
+ }
+ if(this.count_ != otherMap.getCount()) {
+ return false
+ }
+ var equalityFn = opt_equalityFn || goog.structs.Map.defaultEquals;
+ this.cleanupKeysArray_();
+ for(var key, i = 0;key = this.keys_[i];i++) {
+ if(!equalityFn(this.get(key), otherMap.get(key))) {
+ return false
+ }
+ }
+ return true
+};
+goog.structs.Map.defaultEquals = function(a, b) {
+ return a === b
+};
+goog.structs.Map.prototype.isEmpty = function() {
+ return this.count_ == 0
+};
+goog.structs.Map.prototype.clear = function() {
+ this.map_ = {};
+ this.keys_.length = 0;
+ this.count_ = 0;
+ this.version_ = 0
+};
+goog.structs.Map.prototype.remove = function(key) {
+ if(goog.structs.Map.hasKey_(this.map_, key)) {
+ delete this.map_[key];
+ this.count_--;
+ this.version_++;
+ if(this.keys_.length > 2 * this.count_) {
+ this.cleanupKeysArray_()
+ }
+ return true
+ }
+ return false
+};
+goog.structs.Map.prototype.cleanupKeysArray_ = function() {
+ if(this.count_ != this.keys_.length) {
+ var srcIndex = 0;
+ var destIndex = 0;
+ while(srcIndex < this.keys_.length) {
+ var key = this.keys_[srcIndex];
+ if(goog.structs.Map.hasKey_(this.map_, key)) {
+ this.keys_[destIndex++] = key
+ }
+ srcIndex++
+ }
+ this.keys_.length = destIndex
+ }
+ if(this.count_ != this.keys_.length) {
+ var seen = {};
+ var srcIndex = 0;
+ var destIndex = 0;
+ while(srcIndex < this.keys_.length) {
+ var key = this.keys_[srcIndex];
+ if(!goog.structs.Map.hasKey_(seen, key)) {
+ this.keys_[destIndex++] = key;
+ seen[key] = 1
+ }
+ srcIndex++
+ }
+ this.keys_.length = destIndex
+ }
+};
+goog.structs.Map.prototype.get = function(key, opt_val) {
+ if(goog.structs.Map.hasKey_(this.map_, key)) {
+ return this.map_[key]
+ }
+ return opt_val
+};
+goog.structs.Map.prototype.set = function(key, value) {
+ if(!goog.structs.Map.hasKey_(this.map_, key)) {
+ this.count_++;
+ this.keys_.push(key);
+ this.version_++
+ }
+ this.map_[key] = value
+};
+goog.structs.Map.prototype.addAll = function(map) {
+ var keys, values;
+ if(map instanceof goog.structs.Map) {
+ keys = map.getKeys();
+ values = map.getValues()
+ }else {
+ keys = goog.object.getKeys(map);
+ values = goog.object.getValues(map)
+ }
+ for(var i = 0;i < keys.length;i++) {
+ this.set(keys[i], values[i])
+ }
+};
+goog.structs.Map.prototype.clone = function() {
+ return new goog.structs.Map(this)
+};
+goog.structs.Map.prototype.transpose = function() {
+ var transposed = new goog.structs.Map;
+ for(var i = 0;i < this.keys_.length;i++) {
+ var key = this.keys_[i];
+ var value = this.map_[key];
+ transposed.set(value, key)
+ }
+ return transposed
+};
+goog.structs.Map.prototype.toObject = function() {
+ this.cleanupKeysArray_();
+ var obj = {};
+ for(var i = 0;i < this.keys_.length;i++) {
+ var key = this.keys_[i];
+ obj[key] = this.map_[key]
+ }
+ return obj
+};
+goog.structs.Map.prototype.getKeyIterator = function() {
+ return this.__iterator__(true)
+};
+goog.structs.Map.prototype.getValueIterator = function() {
+ return this.__iterator__(false)
+};
+goog.structs.Map.prototype.__iterator__ = function(opt_keys) {
+ this.cleanupKeysArray_();
+ var i = 0;
+ var keys = this.keys_;
+ var map = this.map_;
+ var version = this.version_;
+ var selfObj = this;
+ var newIter = new goog.iter.Iterator;
+ newIter.next = function() {
+ while(true) {
+ if(version != selfObj.version_) {
+ throw Error("The map has changed since the iterator was created");
+ }
+ if(i >= keys.length) {
+ throw goog.iter.StopIteration;
+ }
+ var key = keys[i++];
+ return opt_keys ? key : map[key]
+ }
+ };
+ return newIter
+};
+goog.structs.Map.hasKey_ = function(obj, key) {
+ return Object.prototype.hasOwnProperty.call(obj, key)
+};
+goog.provide("goog.structs.Set");
+goog.require("goog.structs");
+goog.require("goog.structs.Map");
+goog.structs.Set = function(opt_values) {
+ this.map_ = new goog.structs.Map;
+ if(opt_values) {
+ this.addAll(opt_values)
+ }
+};
+goog.structs.Set.getKey_ = function(val) {
+ var type = typeof val;
+ if(type == "object" && val || type == "function") {
+ return"o" + goog.getUid(val)
+ }else {
+ return type.substr(0, 1) + val
+ }
+};
+goog.structs.Set.prototype.getCount = function() {
+ return this.map_.getCount()
+};
+goog.structs.Set.prototype.add = function(element) {
+ this.map_.set(goog.structs.Set.getKey_(element), element)
+};
+goog.structs.Set.prototype.addAll = function(col) {
+ var values = goog.structs.getValues(col);
+ var l = values.length;
+ for(var i = 0;i < l;i++) {
+ this.add(values[i])
+ }
+};
+goog.structs.Set.prototype.removeAll = function(col) {
+ var values = goog.structs.getValues(col);
+ var l = values.length;
+ for(var i = 0;i < l;i++) {
+ this.remove(values[i])
+ }
+};
+goog.structs.Set.prototype.remove = function(element) {
+ return this.map_.remove(goog.structs.Set.getKey_(element))
+};
+goog.structs.Set.prototype.clear = function() {
+ this.map_.clear()
+};
+goog.structs.Set.prototype.isEmpty = function() {
+ return this.map_.isEmpty()
+};
+goog.structs.Set.prototype.contains = function(element) {
+ return this.map_.containsKey(goog.structs.Set.getKey_(element))
+};
+goog.structs.Set.prototype.containsAll = function(col) {
+ return goog.structs.every(col, this.contains, this)
+};
+goog.structs.Set.prototype.intersection = function(col) {
+ var result = new goog.structs.Set;
+ var values = goog.structs.getValues(col);
+ for(var i = 0;i < values.length;i++) {
+ var value = values[i];
+ if(this.contains(value)) {
+ result.add(value)
+ }
+ }
+ return result
+};
+goog.structs.Set.prototype.getValues = function() {
+ return this.map_.getValues()
+};
+goog.structs.Set.prototype.clone = function() {
+ return new goog.structs.Set(this)
+};
+goog.structs.Set.prototype.equals = function(col) {
+ return this.getCount() == goog.structs.getCount(col) && this.isSubsetOf(col)
+};
+goog.structs.Set.prototype.isSubsetOf = function(col) {
+ var colCount = goog.structs.getCount(col);
+ if(this.getCount() > colCount) {
+ return false
+ }
+ if(!(col instanceof goog.structs.Set) && colCount > 5) {
+ col = new goog.structs.Set(col)
+ }
+ return goog.structs.every(this, function(value) {
+ return goog.structs.contains(col, value)
+ })
+};
+goog.structs.Set.prototype.__iterator__ = function(opt_keys) {
+ return this.map_.__iterator__(false)
+};
+goog.provide("goog.debug");
+goog.require("goog.array");
+goog.require("goog.string");
+goog.require("goog.structs.Set");
+goog.debug.catchErrors = function(logFunc, opt_cancel, opt_target) {
+ var target = opt_target || goog.global;
+ var oldErrorHandler = target.onerror;
+ target.onerror = function(message, url, line) {
+ if(oldErrorHandler) {
+ oldErrorHandler(message, url, line)
+ }
+ logFunc({message:message, fileName:url, line:line});
+ return Boolean(opt_cancel)
+ }
+};
+goog.debug.expose = function(obj, opt_showFn) {
+ if(typeof obj == "undefined") {
+ return"undefined"
+ }
+ if(obj == null) {
+ return"NULL"
+ }
+ var str = [];
+ for(var x in obj) {
+ if(!opt_showFn && goog.isFunction(obj[x])) {
+ continue
+ }
+ var s = x + " = ";
+ try {
+ s += obj[x]
+ }catch(e) {
+ s += "*** " + e + " ***"
+ }
+ str.push(s)
+ }
+ return str.join("\n")
+};
+goog.debug.deepExpose = function(obj, opt_showFn) {
+ var previous = new goog.structs.Set;
+ var str = [];
+ var helper = function(obj, space) {
+ var nestspace = space + " ";
+ var indentMultiline = function(str) {
+ return str.replace(/\n/g, "\n" + space)
+ };
+ try {
+ if(!goog.isDef(obj)) {
+ str.push("undefined")
+ }else {
+ if(goog.isNull(obj)) {
+ str.push("NULL")
+ }else {
+ if(goog.isString(obj)) {
+ str.push('"' + indentMultiline(obj) + '"')
+ }else {
+ if(goog.isFunction(obj)) {
+ str.push(indentMultiline(String(obj)))
+ }else {
+ if(goog.isObject(obj)) {
+ if(previous.contains(obj)) {
+ str.push("*** reference loop detected ***")
+ }else {
+ previous.add(obj);
+ str.push("{");
+ for(var x in obj) {
+ if(!opt_showFn && goog.isFunction(obj[x])) {
+ continue
+ }
+ str.push("\n");
+ str.push(nestspace);
+ str.push(x + " = ");
+ helper(obj[x], nestspace)
+ }
+ str.push("\n" + space + "}")
+ }
+ }else {
+ str.push(obj)
+ }
+ }
+ }
+ }
+ }
+ }catch(e) {
+ str.push("*** " + e + " ***")
+ }
+ };
+ helper(obj, "");
+ return str.join("")
+};
+goog.debug.exposeArray = function(arr) {
+ var str = [];
+ for(var i = 0;i < arr.length;i++) {
+ if(goog.isArray(arr[i])) {
+ str.push(goog.debug.exposeArray(arr[i]))
+ }else {
+ str.push(arr[i])
+ }
+ }
+ return"[ " + str.join(", ") + " ]"
+};
+goog.debug.exposeException = function(err, opt_fn) {
+ try {
+ var e = goog.debug.normalizeErrorObject(err);
+ var error = "Message: " + goog.string.htmlEscape(e.message) + '\nUrl: <a href="view-source:' + e.fileName + '" target="_new">' + e.fileName + "</a>\nLine: " + e.lineNumber + "\n\nBrowser stack:\n" + goog.string.htmlEscape(e.stack + "-> ") + "[end]\n\nJS stack traversal:\n" + goog.string.htmlEscape(goog.debug.getStacktrace(opt_fn) + "-> ");
+ return error
+ }catch(e2) {
+ return"Exception trying to expose exception! You win, we lose. " + e2
+ }
+};
+goog.debug.normalizeErrorObject = function(err) {
+ var href = goog.getObjectByName("window.location.href");
+ return typeof err == "string" ? {message:err, name:"Unknown error", lineNumber:"Not available", fileName:href, stack:"Not available"} : !err.lineNumber || !err.fileName || !err.stack ? {message:err.message, name:err.name, lineNumber:err.lineNumber || err.line || "Not available", fileName:err.fileName || err.filename || err.sourceURL || href, stack:err.stack || "Not available"} : err
+};
+goog.debug.enhanceError = function(err, opt_message) {
+ var error = typeof err == "string" ? Error(err) : err;
+ if(!error.stack) {
+ error.stack = goog.debug.getStacktrace(arguments.callee.caller)
+ }
+ if(opt_message) {
+ var x = 0;
+ while(error["message" + x]) {
+ ++x
+ }
+ error["message" + x] = String(opt_message)
+ }
+ return error
+};
+goog.debug.getStacktraceSimple = function(opt_depth) {
+ var sb = [];
+ var fn = arguments.callee.caller;
+ var depth = 0;
+ while(fn && (!opt_depth || depth < opt_depth)) {
+ sb.push(goog.debug.getFunctionName(fn));
+ sb.push("()\n");
+ try {
+ fn = fn.caller
+ }catch(e) {
+ sb.push("[exception trying to get caller]\n");
+ break
+ }
+ depth++;
+ if(depth >= goog.debug.MAX_STACK_DEPTH) {
+ sb.push("[...long stack...]");
+ break
+ }
+ }
+ if(opt_depth && depth >= opt_depth) {
+ sb.push("[...reached max depth limit...]")
+ }else {
+ sb.push("[end]")
+ }
+ return sb.join("")
+};
+goog.debug.MAX_STACK_DEPTH = 50;
+goog.debug.getStacktrace = function(opt_fn) {
+ return goog.debug.getStacktraceHelper_(opt_fn || arguments.callee.caller, [])
+};
+goog.debug.getStacktraceHelper_ = function(fn, visited) {
+ var sb = [];
+ if(goog.array.contains(visited, fn)) {
+ sb.push("[...circular reference...]")
+ }else {
+ if(fn && visited.length < goog.debug.MAX_STACK_DEPTH) {
+ sb.push(goog.debug.getFunctionName(fn) + "(");
+ var args = fn.arguments;
+ for(var i = 0;i < args.length;i++) {
+ if(i > 0) {
+ sb.push(", ")
+ }
+ var argDesc;
+ var arg = args[i];
+ switch(typeof arg) {
+ case "object":
+ argDesc = arg ? "object" : "null";
+ break;
+ case "string":
+ argDesc = arg;
+ break;
+ case "number":
+ argDesc = String(arg);
+ break;
+ case "boolean":
+ argDesc = arg ? "true" : "false";
+ break;
+ case "function":
+ argDesc = goog.debug.getFunctionName(arg);
+ argDesc = argDesc ? argDesc : "[fn]";
+ break;
+ case "undefined":
+ ;
+ default:
+ argDesc = typeof arg;
+ break
+ }
+ if(argDesc.length > 40) {
+ argDesc = argDesc.substr(0, 40) + "..."
+ }
+ sb.push(argDesc)
+ }
+ visited.push(fn);
+ sb.push(")\n");
+ try {
+ sb.push(goog.debug.getStacktraceHelper_(fn.caller, visited))
+ }catch(e) {
+ sb.push("[exception trying to get caller]\n")
+ }
+ }else {
+ if(fn) {
+ sb.push("[...long stack...]")
+ }else {
+ sb.push("[end]")
+ }
+ }
+ }
+ return sb.join("")
+};
+goog.debug.getFunctionName = function(fn) {
+ var functionSource = String(fn);
+ if(!goog.debug.fnNameCache_[functionSource]) {
+ var matches = /function ([^\(]+)/.exec(functionSource);
+ if(matches) {
+ var method = matches[1];
+ goog.debug.fnNameCache_[functionSource] = method
+ }else {
+ goog.debug.fnNameCache_[functionSource] = "[Anonymous]"
+ }
+ }
+ return goog.debug.fnNameCache_[functionSource]
+};
+goog.debug.makeWhitespaceVisible = function(string) {
+ return string.replace(/ /g, "[_]").replace(/\f/g, "[f]").replace(/\n/g, "[n]\n").replace(/\r/g, "[r]").replace(/\t/g, "[t]")
+};
+goog.debug.fnNameCache_ = {};
+goog.provide("goog.debug.LogRecord");
+goog.debug.LogRecord = function(level, msg, loggerName, opt_time, opt_sequenceNumber) {
+ this.reset(level, msg, loggerName, opt_time, opt_sequenceNumber)
+};
+goog.debug.LogRecord.prototype.time_;
+goog.debug.LogRecord.prototype.level_;
+goog.debug.LogRecord.prototype.msg_;
+goog.debug.LogRecord.prototype.loggerName_;
+goog.debug.LogRecord.prototype.sequenceNumber_ = 0;
+goog.debug.LogRecord.prototype.exception_ = null;
+goog.debug.LogRecord.prototype.exceptionText_ = null;
+goog.debug.LogRecord.ENABLE_SEQUENCE_NUMBERS = true;
+goog.debug.LogRecord.nextSequenceNumber_ = 0;
+goog.debug.LogRecord.prototype.reset = function(level, msg, loggerName, opt_time, opt_sequenceNumber) {
+ if(goog.debug.LogRecord.ENABLE_SEQUENCE_NUMBERS) {
+ this.sequenceNumber_ = typeof opt_sequenceNumber == "number" ? opt_sequenceNumber : goog.debug.LogRecord.nextSequenceNumber_++
+ }
+ this.time_ = opt_time || goog.now();
+ this.level_ = level;
+ this.msg_ = msg;
+ this.loggerName_ = loggerName;
+ delete this.exception_;
+ delete this.exceptionText_
+};
+goog.debug.LogRecord.prototype.getLoggerName = function() {
+ return this.loggerName_
+};
+goog.debug.LogRecord.prototype.getException = function() {
+ return this.exception_
+};
+goog.debug.LogRecord.prototype.setException = function(exception) {
+ this.exception_ = exception
+};
+goog.debug.LogRecord.prototype.getExceptionText = function() {
+ return this.exceptionText_
+};
+goog.debug.LogRecord.prototype.setExceptionText = function(text) {
+ this.exceptionText_ = text
+};
+goog.debug.LogRecord.prototype.setLoggerName = function(loggerName) {
+ this.loggerName_ = loggerName
+};
+goog.debug.LogRecord.prototype.getLevel = function() {
+ return this.level_
+};
+goog.debug.LogRecord.prototype.setLevel = function(level) {
+ this.level_ = level
+};
+goog.debug.LogRecord.prototype.getMessage = function() {
+ return this.msg_
+};
+goog.debug.LogRecord.prototype.setMessage = function(msg) {
+ this.msg_ = msg
+};
+goog.debug.LogRecord.prototype.getMillis = function() {
+ return this.time_
+};
+goog.debug.LogRecord.prototype.setMillis = function(time) {
+ this.time_ = time
+};
+goog.debug.LogRecord.prototype.getSequenceNumber = function() {
+ return this.sequenceNumber_
+};
+goog.provide("goog.debug.LogBuffer");
+goog.require("goog.asserts");
+goog.require("goog.debug.LogRecord");
+goog.debug.LogBuffer = function() {
+ goog.asserts.assert(goog.debug.LogBuffer.isBufferingEnabled(), "Cannot use goog.debug.LogBuffer without defining " + "goog.debug.LogBuffer.CAPACITY.");
+ this.clear()
+};
+goog.debug.LogBuffer.getInstance = function() {
+ if(!goog.debug.LogBuffer.instance_) {
+ goog.debug.LogBuffer.instance_ = new goog.debug.LogBuffer
+ }
+ return goog.debug.LogBuffer.instance_
+};
+goog.debug.LogBuffer.CAPACITY = 0;
+goog.debug.LogBuffer.prototype.buffer_;
+goog.debug.LogBuffer.prototype.curIndex_;
+goog.debug.LogBuffer.prototype.isFull_;
+goog.debug.LogBuffer.prototype.addRecord = function(level, msg, loggerName) {
+ var curIndex = (this.curIndex_ + 1) % goog.debug.LogBuffer.CAPACITY;
+ this.curIndex_ = curIndex;
+ if(this.isFull_) {
+ var ret = this.buffer_[curIndex];
+ ret.reset(level, msg, loggerName);
+ return ret
+ }
+ this.isFull_ = curIndex == goog.debug.LogBuffer.CAPACITY - 1;
+ return this.buffer_[curIndex] = new goog.debug.LogRecord(level, msg, loggerName)
+};
+goog.debug.LogBuffer.isBufferingEnabled = function() {
+ return goog.debug.LogBuffer.CAPACITY > 0
+};
+goog.debug.LogBuffer.prototype.clear = function() {
+ this.buffer_ = new Array(goog.debug.LogBuffer.CAPACITY);
+ this.curIndex_ = -1;
+ this.isFull_ = false
+};
+goog.debug.LogBuffer.prototype.forEachRecord = function(func) {
+ var buffer = this.buffer_;
+ if(!buffer[0]) {
+ return
+ }
+ var curIndex = this.curIndex_;
+ var i = this.isFull_ ? curIndex : -1;
+ do {
+ i = (i + 1) % goog.debug.LogBuffer.CAPACITY;
+ func(buffer[i])
+ }while(i != curIndex)
+};
+goog.provide("goog.debug.LogManager");
+goog.provide("goog.debug.Logger");
+goog.provide("goog.debug.Logger.Level");
+goog.require("goog.array");
+goog.require("goog.asserts");
+goog.require("goog.debug");
+goog.require("goog.debug.LogBuffer");
+goog.require("goog.debug.LogRecord");
+goog.debug.Logger = function(name) {
+ this.name_ = name
+};
+goog.debug.Logger.prototype.parent_ = null;
+goog.debug.Logger.prototype.level_ = null;
+goog.debug.Logger.prototype.children_ = null;
+goog.debug.Logger.prototype.handlers_ = null;
+goog.debug.Logger.ENABLE_HIERARCHY = true;
+if(!goog.debug.Logger.ENABLE_HIERARCHY) {
+ goog.debug.Logger.rootHandlers_ = [];
+ goog.debug.Logger.rootLevel_
+}
+goog.debug.Logger.Level = function(name, value) {
+ this.name = name;
+ this.value = value
+};
+goog.debug.Logger.Level.prototype.toString = function() {
+ return this.name
+};
+goog.debug.Logger.Level.OFF = new goog.debug.Logger.Level("OFF", Infinity);
+goog.debug.Logger.Level.SHOUT = new goog.debug.Logger.Level("SHOUT", 1200);
+goog.debug.Logger.Level.SEVERE = new goog.debug.Logger.Level("SEVERE", 1E3);
+goog.debug.Logger.Level.WARNING = new goog.debug.Logger.Level("WARNING", 900);
+goog.debug.Logger.Level.INFO = new goog.debug.Logger.Level("INFO", 800);
+goog.debug.Logger.Level.CONFIG = new goog.debug.Logger.Level("CONFIG", 700);
+goog.debug.Logger.Level.FINE = new goog.debug.Logger.Level("FINE", 500);
+goog.debug.Logger.Level.FINER = new goog.debug.Logger.Level("FINER", 400);
+goog.debug.Logger.Level.FINEST = new goog.debug.Logger.Level("FINEST", 300);
+goog.debug.Logger.Level.ALL = new goog.debug.Logger.Level("ALL", 0);
+goog.debug.Logger.Level.PREDEFINED_LEVELS = [goog.debug.Logger.Level.OFF, goog.debug.Logger.Level.SHOUT, goog.debug.Logger.Level.SEVERE, goog.debug.Logger.Level.WARNING, goog.debug.Logger.Level.INFO, goog.debug.Logger.Level.CONFIG, goog.debug.Logger.Level.FINE, goog.debug.Logger.Level.FINER, goog.debug.Logger.Level.FINEST, goog.debug.Logger.Level.ALL];
+goog.debug.Logger.Level.predefinedLevelsCache_ = null;
+goog.debug.Logger.Level.createPredefinedLevelsCache_ = function() {
+ goog.debug.Logger.Level.predefinedLevelsCache_ = {};
+ for(var i = 0, level;level = goog.debug.Logger.Level.PREDEFINED_LEVELS[i];i++) {
+ goog.debug.Logger.Level.predefinedLevelsCache_[level.value] = level;
+ goog.debug.Logger.Level.predefinedLevelsCache_[level.name] = level
+ }
+};
+goog.debug.Logger.Level.getPredefinedLevel = function(name) {
+ if(!goog.debug.Logger.Level.predefinedLevelsCache_) {
+ goog.debug.Logger.Level.createPredefinedLevelsCache_()
+ }
+ return goog.debug.Logger.Level.predefinedLevelsCache_[name] || null
+};
+goog.debug.Logger.Level.getPredefinedLevelByValue = function(value) {
+ if(!goog.debug.Logger.Level.predefinedLevelsCache_) {
+ goog.debug.Logger.Level.createPredefinedLevelsCache_()
+ }
+ if(value in goog.debug.Logger.Level.predefinedLevelsCache_) {
+ return goog.debug.Logger.Level.predefinedLevelsCache_[value]
+ }
+ for(var i = 0;i < goog.debug.Logger.Level.PREDEFINED_LEVELS.length;++i) {
+ var level = goog.debug.Logger.Level.PREDEFINED_LEVELS[i];
+ if(level.value <= value) {
+ return level
+ }
+ }
+ return null
+};
+goog.debug.Logger.getLogger = function(name) {
+ return goog.debug.LogManager.getLogger(name)
+};
+goog.debug.Logger.prototype.getName = function() {
+ return this.name_
+};
+goog.debug.Logger.prototype.addHandler = function(handler) {
+ if(goog.debug.Logger.ENABLE_HIERARCHY) {
+ if(!this.handlers_) {
+ this.handlers_ = []
+ }
+ this.handlers_.push(handler)
+ }else {
+ goog.asserts.assert(!this.name_, "Cannot call addHandler on a non-root logger when " + "goog.debug.Logger.ENABLE_HIERARCHY is false.");
+ goog.debug.Logger.rootHandlers_.push(handler)
+ }
+};
+goog.debug.Logger.prototype.removeHandler = function(handler) {
+ var handlers = goog.debug.Logger.ENABLE_HIERARCHY ? this.handlers_ : goog.debug.Logger.rootHandlers_;
+ return!!handlers && goog.array.remove(handlers, handler)
+};
+goog.debug.Logger.prototype.getParent = function() {
+ return this.parent_
+};
+goog.debug.Logger.prototype.getChildren = function() {
+ if(!this.children_) {
+ this.children_ = {}
+ }
+ return this.children_
+};
+goog.debug.Logger.prototype.setLevel = function(level) {
+ if(goog.debug.Logger.ENABLE_HIERARCHY) {
+ this.level_ = level
+ }else {
+ goog.asserts.assert(!this.name_, "Cannot call setLevel() on a non-root logger when " + "goog.debug.Logger.ENABLE_HIERARCHY is false.");
+ goog.debug.Logger.rootLevel_ = level
+ }
+};
+goog.debug.Logger.prototype.getLevel = function() {
+ return this.level_
+};
+goog.debug.Logger.prototype.getEffectiveLevel = function() {
+ if(!goog.debug.Logger.ENABLE_HIERARCHY) {
+ return goog.debug.Logger.rootLevel_
+ }
+ if(this.level_) {
+ return this.level_
+ }
+ if(this.parent_) {
+ return this.parent_.getEffectiveLevel()
+ }
+ goog.asserts.fail("Root logger has no level set.");
+ return null
+};
+goog.debug.Logger.prototype.isLoggable = function(level) {
+ return level.value >= this.getEffectiveLevel().value
+};
+goog.debug.Logger.prototype.log = function(level, msg, opt_exception) {
+ if(this.isLoggable(level)) {
+ this.doLogRecord_(this.getLogRecord(level, msg, opt_exception))
+ }
+};
+goog.debug.Logger.prototype.getLogRecord = function(level, msg, opt_exception) {
+ if(goog.debug.LogBuffer.isBufferingEnabled()) {
+ var logRecord = goog.debug.LogBuffer.getInstance().addRecord(level, msg, this.name_)
+ }else {
+ logRecord = new goog.debug.LogRecord(level, String(msg), this.name_)
+ }
+ if(opt_exception) {
+ logRecord.setException(opt_exception);
+ logRecord.setExceptionText(goog.debug.exposeException(opt_exception, arguments.callee.caller))
+ }
+ return logRecord
+};
+goog.debug.Logger.prototype.shout = function(msg, opt_exception) {
+ this.log(goog.debug.Logger.Level.SHOUT, msg, opt_exception)
+};
+goog.debug.Logger.prototype.severe = function(msg, opt_exception) {
+ this.log(goog.debug.Logger.Level.SEVERE, msg, opt_exception)
+};
+goog.debug.Logger.prototype.warning = function(msg, opt_exception) {
+ this.log(goog.debug.Logger.Level.WARNING, msg, opt_exception)
+};
+goog.debug.Logger.prototype.info = function(msg, opt_exception) {
+ this.log(goog.debug.Logger.Level.INFO, msg, opt_exception)
+};
+goog.debug.Logger.prototype.config = function(msg, opt_exception) {
+ this.log(goog.debug.Logger.Level.CONFIG, msg, opt_exception)
+};
+goog.debug.Logger.prototype.fine = function(msg, opt_exception) {
+ this.log(goog.debug.Logger.Level.FINE, msg, opt_exception)
+};
+goog.debug.Logger.prototype.finer = function(msg, opt_exception) {
+ this.log(goog.debug.Logger.Level.FINER, msg, opt_exception)
+};
+goog.debug.Logger.prototype.finest = function(msg, opt_exception) {
+ this.log(goog.debug.Logger.Level.FINEST, msg, opt_exception)
+};
+goog.debug.Logger.prototype.logRecord = function(logRecord) {
+ if(this.isLoggable(logRecord.getLevel())) {
+ this.doLogRecord_(logRecord)
+ }
+};
+goog.debug.Logger.prototype.doLogRecord_ = function(logRecord) {
+ if(goog.debug.Logger.ENABLE_HIERARCHY) {
+ var target = this;
+ while(target) {
+ target.callPublish_(logRecord);
+ target = target.getParent()
+ }
+ }else {
+ for(var i = 0, handler;handler = goog.debug.Logger.rootHandlers_[i++];) {
+ handler(logRecord)
+ }
+ }
+};
+goog.debug.Logger.prototype.callPublish_ = function(logRecord) {
+ if(this.handlers_) {
+ for(var i = 0, handler;handler = this.handlers_[i];i++) {
+ handler(logRecord)
+ }
+ }
+};
+goog.debug.Logger.prototype.setParent_ = function(parent) {
+ this.parent_ = parent
+};
+goog.debug.Logger.prototype.addChild_ = function(name, logger) {
+ this.getChildren()[name] = logger
+};
+goog.debug.LogManager = {};
+goog.debug.LogManager.loggers_ = {};
+goog.debug.LogManager.rootLogger_ = null;
+goog.debug.LogManager.initialize = function() {
+ if(!goog.debug.LogManager.rootLogger_) {
+ goog.debug.LogManager.rootLogger_ = new goog.debug.Logger("");
+ goog.debug.LogManager.loggers_[""] = goog.debug.LogManager.rootLogger_;
+ goog.debug.LogManager.rootLogger_.setLevel(goog.debug.Logger.Level.CONFIG)
+ }
+};
+goog.debug.LogManager.getLoggers = function() {
+ return goog.debug.LogManager.loggers_
+};
+goog.debug.LogManager.getRoot = function() {
+ goog.debug.LogManager.initialize();
+ return goog.debug.LogManager.rootLogger_
+};
+goog.debug.LogManager.getLogger = function(name) {
+ goog.debug.LogManager.initialize();
+ var ret = goog.debug.LogManager.loggers_[name];
+ return ret || goog.debug.LogManager.createLogger_(name)
+};
+goog.debug.LogManager.createFunctionForCatchErrors = function(opt_logger) {
+ return function(info) {
+ var logger = opt_logger || goog.debug.LogManager.getRoot();
+ logger.severe("Error: " + info.message + " (" + info.fileName + " @ Line: " + info.line + ")")
+ }
+};
+goog.debug.LogManager.createLogger_ = function(name) {
+ var logger = new goog.debug.Logger(name);
+ if(goog.debug.Logger.ENABLE_HIERARCHY) {
+ var lastDotIndex = name.lastIndexOf(".");
+ var parentName = name.substr(0, lastDotIndex);
+ var leafName = name.substr(lastDotIndex + 1);
+ var parentLogger = goog.debug.LogManager.getLogger(parentName);
+ parentLogger.addChild_(leafName, logger);
+ logger.setParent_(parentLogger)
+ }
+ goog.debug.LogManager.loggers_[name] = logger;
+ return logger
+};
+goog.provide("goog.dom.SavedRange");
+goog.require("goog.Disposable");
+goog.require("goog.debug.Logger");
+goog.dom.SavedRange = function() {
+ goog.Disposable.call(this)
+};
+goog.inherits(goog.dom.SavedRange, goog.Disposable);
+goog.dom.SavedRange.logger_ = goog.debug.Logger.getLogger("goog.dom.SavedRange");
+goog.dom.SavedRange.prototype.restore = function(opt_stayAlive) {
+ if(this.isDisposed()) {
+ goog.dom.SavedRange.logger_.severe("Disposed SavedRange objects cannot be restored.")
+ }
+ var range = this.restoreInternal();
+ if(!opt_stayAlive) {
+ this.dispose()
+ }
+ return range
+};
+goog.dom.SavedRange.prototype.restoreInternal = goog.abstractMethod;
+goog.provide("goog.dom.SavedCaretRange");
+goog.require("goog.array");
+goog.require("goog.dom");
+goog.require("goog.dom.SavedRange");
+goog.require("goog.dom.TagName");
+goog.require("goog.string");
+goog.dom.SavedCaretRange = function(range) {
+ goog.dom.SavedRange.call(this);
+ this.startCaretId_ = goog.string.createUniqueString();
+ this.endCaretId_ = goog.string.createUniqueString();
+ this.dom_ = goog.dom.getDomHelper(range.getDocument());
+ range.surroundWithNodes(this.createCaret_(true), this.createCaret_(false))
+};
+goog.inherits(goog.dom.SavedCaretRange, goog.dom.SavedRange);
+goog.dom.SavedCaretRange.prototype.toAbstractRange = function() {
+ var range = null;
+ var startCaret = this.getCaret(true);
+ var endCaret = this.getCaret(false);
+ if(startCaret && endCaret) {
+ range = goog.dom.Range.createFromNodes(startCaret, 0, endCaret, 0)
+ }
+ return range
+};
+goog.dom.SavedCaretRange.prototype.getCaret = function(start) {
+ return this.dom_.getElement(start ? this.startCaretId_ : this.endCaretId_)
+};
+goog.dom.SavedCaretRange.prototype.removeCarets = function(opt_range) {
+ goog.dom.removeNode(this.getCaret(true));
+ goog.dom.removeNode(this.getCaret(false));
+ return opt_range
+};
+goog.dom.SavedCaretRange.prototype.setRestorationDocument = function(doc) {
+ this.dom_.setDocument(doc)
+};
+goog.dom.SavedCaretRange.prototype.restoreInternal = function() {
+ var range = null;
+ var startCaret = this.getCaret(true);
+ var endCaret = this.getCaret(false);
+ if(startCaret && endCaret) {
+ var startNode = startCaret.parentNode;
+ var startOffset = goog.array.indexOf(startNode.childNodes, startCaret);
+ var endNode = endCaret.parentNode;
+ var endOffset = goog.array.indexOf(endNode.childNodes, endCaret);
+ if(endNode == startNode) {
+ endOffset -= 1
+ }
+ range = goog.dom.Range.createFromNodes(startNode, startOffset, endNode, endOffset);
+ range = this.removeCarets(range);
+ range.select()
+ }else {
+ this.removeCarets()
+ }
+ return range
+};
+goog.dom.SavedCaretRange.prototype.disposeInternal = function() {
+ this.removeCarets();
+ this.dom_ = null
+};
+goog.dom.SavedCaretRange.prototype.createCaret_ = function(start) {
+ return this.dom_.createDom(goog.dom.TagName.SPAN, {id:start ? this.startCaretId_ : this.endCaretId_})
+};
+goog.dom.SavedCaretRange.CARET_REGEX = /<span\s+id="?goog_\d+"?><\/span>/ig;
+goog.dom.SavedCaretRange.htmlEqual = function(str1, str2) {
+ return str1 == str2 || str1.replace(goog.dom.SavedCaretRange.CARET_REGEX, "") == str2.replace(goog.dom.SavedCaretRange.CARET_REGEX, "")
+};
+goog.provide("goog.dom.TagIterator");
+goog.provide("goog.dom.TagWalkType");
+goog.require("goog.dom.NodeType");
+goog.require("goog.iter.Iterator");
+goog.require("goog.iter.StopIteration");
+goog.dom.TagWalkType = {START_TAG:1, OTHER:0, END_TAG:-1};
+goog.dom.TagIterator = function(opt_node, opt_reversed, opt_unconstrained, opt_tagType, opt_depth) {
+ this.reversed = !!opt_reversed;
+ if(opt_node) {
+ this.setPosition(opt_node, opt_tagType)
+ }
+ this.depth = opt_depth != undefined ? opt_depth : this.tagType || 0;
+ if(this.reversed) {
+ this.depth *= -1
+ }
+ this.constrained = !opt_unconstrained
+};
+goog.inherits(goog.dom.TagIterator, goog.iter.Iterator);
+goog.dom.TagIterator.prototype.node = null;
+goog.dom.TagIterator.prototype.tagType = goog.dom.TagWalkType.OTHER;
+goog.dom.TagIterator.prototype.depth;
+goog.dom.TagIterator.prototype.reversed;
+goog.dom.TagIterator.prototype.constrained;
+goog.dom.TagIterator.prototype.started_ = false;
+goog.dom.TagIterator.prototype.setPosition = function(node, opt_tagType, opt_depth) {
+ this.node = node;
+ if(node) {
+ if(goog.isNumber(opt_tagType)) {
+ this.tagType = opt_tagType
+ }else {
+ this.tagType = this.node.nodeType != goog.dom.NodeType.ELEMENT ? goog.dom.TagWalkType.OTHER : this.reversed ? goog.dom.TagWalkType.END_TAG : goog.dom.TagWalkType.START_TAG
+ }
+ }
+ if(goog.isNumber(opt_depth)) {
+ this.depth = opt_depth
+ }
+};
+goog.dom.TagIterator.prototype.copyFrom = function(other) {
+ this.node = other.node;
+ this.tagType = other.tagType;
+ this.depth = other.depth;
+ this.reversed = other.reversed;
+ this.constrained = other.constrained
+};
+goog.dom.TagIterator.prototype.clone = function() {
+ return new goog.dom.TagIterator(this.node, this.reversed, !this.constrained, this.tagType, this.depth)
+};
+goog.dom.TagIterator.prototype.skipTag = function() {
+ var check = this.reversed ? goog.dom.TagWalkType.END_TAG : goog.dom.TagWalkType.START_TAG;
+ if(this.tagType == check) {
+ this.tagType = check * -1;
+ this.depth += this.tagType * (this.reversed ? -1 : 1)
+ }
+};
+goog.dom.TagIterator.prototype.restartTag = function() {
+ var check = this.reversed ? goog.dom.TagWalkType.START_TAG : goog.dom.TagWalkType.END_TAG;
+ if(this.tagType == check) {
+ this.tagType = check * -1;
+ this.depth += this.tagType * (this.reversed ? -1 : 1)
+ }
+};
+goog.dom.TagIterator.prototype.next = function() {
+ var node;
+ if(this.started_) {
+ if(!this.node || this.constrained && this.depth == 0) {
+ throw goog.iter.StopIteration;
+ }
+ node = this.node;
+ var startType = this.reversed ? goog.dom.TagWalkType.END_TAG : goog.dom.TagWalkType.START_TAG;
+ if(this.tagType == startType) {
+ var child = this.reversed ? node.lastChild : node.firstChild;
+ if(child) {
+ this.setPosition(child)
+ }else {
+ this.setPosition(node, startType * -1)
+ }
+ }else {
+ var sibling = this.reversed ? node.previousSibling : node.nextSibling;
+ if(sibling) {
+ this.setPosition(sibling)
+ }else {
+ this.setPosition(node.parentNode, startType * -1)
+ }
+ }
+ this.depth += this.tagType * (this.reversed ? -1 : 1)
+ }else {
+ this.started_ = true
+ }
+ node = this.node;
+ if(!this.node) {
+ throw goog.iter.StopIteration;
+ }
+ return node
+};
+goog.dom.TagIterator.prototype.isStarted = function() {
+ return this.started_
+};
+goog.dom.TagIterator.prototype.isStartTag = function() {
+ return this.tagType == goog.dom.TagWalkType.START_TAG
+};
+goog.dom.TagIterator.prototype.isEndTag = function() {
+ return this.tagType == goog.dom.TagWalkType.END_TAG
+};
+goog.dom.TagIterator.prototype.isNonElement = function() {
+ return this.tagType == goog.dom.TagWalkType.OTHER
+};
+goog.dom.TagIterator.prototype.equals = function(other) {
+ return other.node == this.node && (!this.node || other.tagType == this.tagType)
+};
+goog.dom.TagIterator.prototype.splice = function(var_args) {
+ var node = this.node;
+ this.restartTag();
+ this.reversed = !this.reversed;
+ goog.dom.TagIterator.prototype.next.call(this);
+ this.reversed = !this.reversed;
+ var arr = goog.isArrayLike(arguments[0]) ? arguments[0] : arguments;
+ for(var i = arr.length - 1;i >= 0;i--) {
+ goog.dom.insertSiblingAfter(arr[i], node)
+ }
+ goog.dom.removeNode(node)
+};
+goog.provide("goog.dom.AbstractRange");
+goog.provide("goog.dom.RangeIterator");
+goog.provide("goog.dom.RangeType");
+goog.require("goog.dom");
+goog.require("goog.dom.NodeType");
+goog.require("goog.dom.SavedCaretRange");
+goog.require("goog.dom.TagIterator");
+goog.require("goog.userAgent");
+goog.dom.RangeType = {TEXT:"text", CONTROL:"control", MULTI:"mutli"};
+goog.dom.AbstractRange = function() {
+};
+goog.dom.AbstractRange.getBrowserSelectionForWindow = function(win) {
+ if(win.getSelection) {
+ return win.getSelection()
+ }else {
+ var doc = win.document;
+ var sel = doc.selection;
+ if(sel) {
+ try {
+ var range = sel.createRange();
+ if(range.parentElement) {
+ if(range.parentElement().document != doc) {
+ return null
+ }
+ }else {
+ if(!range.length || range.item(0).document != doc) {
+ return null
+ }
+ }
+ }catch(e) {
+ return null
+ }
+ return sel
+ }
+ return null
+ }
+};
+goog.dom.AbstractRange.isNativeControlRange = function(range) {
+ return!!range && !!range.addElement
+};
+goog.dom.AbstractRange.prototype.clone = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.getType = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.getBrowserRangeObject = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.setBrowserRangeObject = function(nativeRange) {
+ return false
+};
+goog.dom.AbstractRange.prototype.getTextRangeCount = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.getTextRange = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.getTextRanges = function() {
+ var output = [];
+ for(var i = 0, len = this.getTextRangeCount();i < len;i++) {
+ output.push(this.getTextRange(i))
+ }
+ return output
+};
+goog.dom.AbstractRange.prototype.getContainer = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.getContainerElement = function() {
+ var node = this.getContainer();
+ return node.nodeType == goog.dom.NodeType.ELEMENT ? node : node.parentNode
+};
+goog.dom.AbstractRange.prototype.getStartNode = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.getStartOffset = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.getEndNode = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.getEndOffset = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.getAnchorNode = function() {
+ return this.isReversed() ? this.getEndNode() : this.getStartNode()
+};
+goog.dom.AbstractRange.prototype.getAnchorOffset = function() {
+ return this.isReversed() ? this.getEndOffset() : this.getStartOffset()
+};
+goog.dom.AbstractRange.prototype.getFocusNode = function() {
+ return this.isReversed() ? this.getStartNode() : this.getEndNode()
+};
+goog.dom.AbstractRange.prototype.getFocusOffset = function() {
+ return this.isReversed() ? this.getStartOffset() : this.getEndOffset()
+};
+goog.dom.AbstractRange.prototype.isReversed = function() {
+ return false
+};
+goog.dom.AbstractRange.prototype.getDocument = function() {
+ return goog.dom.getOwnerDocument(goog.userAgent.IE ? this.getContainer() : this.getStartNode())
+};
+goog.dom.AbstractRange.prototype.getWindow = function() {
+ return goog.dom.getWindow(this.getDocument())
+};
+goog.dom.AbstractRange.prototype.containsRange = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.containsNode = function(node, opt_allowPartial) {
+ return this.containsRange(goog.dom.Range.createFromNodeContents(node), opt_allowPartial)
+};
+goog.dom.AbstractRange.prototype.isRangeInDocument = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.isCollapsed = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.getText = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.getHtmlFragment = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.getValidHtml = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.getPastableHtml = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.__iterator__ = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.select = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.removeContents = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.insertNode = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.replaceContentsWithNode = function(node) {
+ if(!this.isCollapsed()) {
+ this.removeContents()
+ }
+ return this.insertNode(node, true)
+};
+goog.dom.AbstractRange.prototype.surroundWithNodes = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.saveUsingDom = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.saveUsingCarets = function() {
+ return this.getStartNode() && this.getEndNode() ? new goog.dom.SavedCaretRange(this) : null
+};
+goog.dom.AbstractRange.prototype.collapse = goog.abstractMethod;
+goog.dom.RangeIterator = function(node, opt_reverse) {
+ goog.dom.TagIterator.call(this, node, opt_reverse, true)
+};
+goog.inherits(goog.dom.RangeIterator, goog.dom.TagIterator);
+goog.dom.RangeIterator.prototype.getStartTextOffset = goog.abstractMethod;
+goog.dom.RangeIterator.prototype.getEndTextOffset = goog.abstractMethod;
+goog.dom.RangeIterator.prototype.getStartNode = goog.abstractMethod;
+goog.dom.RangeIterator.prototype.getEndNode = goog.abstractMethod;
+goog.dom.RangeIterator.prototype.isLast = goog.abstractMethod;
+goog.provide("goog.dom.AbstractMultiRange");
+goog.require("goog.array");
+goog.require("goog.dom");
+goog.require("goog.dom.AbstractRange");
+goog.dom.AbstractMultiRange = function() {
+};
+goog.inherits(goog.dom.AbstractMultiRange, goog.dom.AbstractRange);
+goog.dom.AbstractMultiRange.prototype.containsRange = function(otherRange, opt_allowPartial) {
+ var ranges = this.getTextRanges();
+ var otherRanges = otherRange.getTextRanges();
+ var fn = opt_allowPartial ? goog.array.some : goog.array.every;
+ return fn(otherRanges, function(otherRange) {
+ return goog.array.some(ranges, function(range) {
+ return range.containsRange(otherRange, opt_allowPartial)
+ })
+ })
+};
+goog.dom.AbstractMultiRange.prototype.insertNode = function(node, before) {
+ if(before) {
+ goog.dom.insertSiblingBefore(node, this.getStartNode())
+ }else {
+ goog.dom.insertSiblingAfter(node, this.getEndNode())
+ }
+ return node
+};
+goog.dom.AbstractMultiRange.prototype.surroundWithNodes = function(startNode, endNode) {
+ this.insertNode(startNode, true);
+ this.insertNode(endNode, false)
+};
+goog.provide("goog.dom.TextRangeIterator");
+goog.require("goog.array");
+goog.require("goog.dom.NodeType");
+goog.require("goog.dom.RangeIterator");
+goog.require("goog.dom.TagName");
+goog.require("goog.iter.StopIteration");
+goog.dom.TextRangeIterator = function(startNode, startOffset, endNode, endOffset, opt_reverse) {
+ var goNext;
+ if(startNode) {
+ this.startNode_ = startNode;
+ this.startOffset_ = startOffset;
+ this.endNode_ = endNode;
+ this.endOffset_ = endOffset;
+ if(startNode.nodeType == goog.dom.NodeType.ELEMENT && startNode.tagName != goog.dom.TagName.BR) {
+ var startChildren = startNode.childNodes;
+ var candidate = startChildren[startOffset];
+ if(candidate) {
+ this.startNode_ = candidate;
+ this.startOffset_ = 0
+ }else {
+ if(startChildren.length) {
+ this.startNode_ = goog.array.peek(startChildren)
+ }
+ goNext = true
+ }
+ }
+ if(endNode.nodeType == goog.dom.NodeType.ELEMENT) {
+ this.endNode_ = endNode.childNodes[endOffset];
+ if(this.endNode_) {
+ this.endOffset_ = 0
+ }else {
+ this.endNode_ = endNode
+ }
+ }
+ }
+ goog.dom.RangeIterator.call(this, opt_reverse ? this.endNode_ : this.startNode_, opt_reverse);
+ if(goNext) {
+ try {
+ this.next()
+ }catch(e) {
+ if(e != goog.iter.StopIteration) {
+ throw e;
+ }
+ }
+ }
+};
+goog.inherits(goog.dom.TextRangeIterator, goog.dom.RangeIterator);
+goog.dom.TextRangeIterator.prototype.startNode_ = null;
+goog.dom.TextRangeIterator.prototype.endNode_ = null;
+goog.dom.TextRangeIterator.prototype.startOffset_ = 0;
+goog.dom.TextRangeIterator.prototype.endOffset_ = 0;
+goog.dom.TextRangeIterator.prototype.getStartTextOffset = function() {
+ return this.node.nodeType != goog.dom.NodeType.TEXT ? -1 : this.node == this.startNode_ ? this.startOffset_ : 0
+};
+goog.dom.TextRangeIterator.prototype.getEndTextOffset = function() {
+ return this.node.nodeType != goog.dom.NodeType.TEXT ? -1 : this.node == this.endNode_ ? this.endOffset_ : this.node.nodeValue.length
+};
+goog.dom.TextRangeIterator.prototype.getStartNode = function() {
+ return this.startNode_
+};
+goog.dom.TextRangeIterator.prototype.setStartNode = function(node) {
+ if(!this.isStarted()) {
+ this.setPosition(node)
+ }
+ this.startNode_ = node;
+ this.startOffset_ = 0
+};
+goog.dom.TextRangeIterator.prototype.getEndNode = function() {
+ return this.endNode_
+};
+goog.dom.TextRangeIterator.prototype.setEndNode = function(node) {
+ this.endNode_ = node;
+ this.endOffset_ = 0
+};
+goog.dom.TextRangeIterator.prototype.isLast = function() {
+ return this.isStarted() && this.node == this.endNode_ && (!this.endOffset_ || !this.isStartTag())
+};
+goog.dom.TextRangeIterator.prototype.next = function() {
+ if(this.isLast()) {
+ throw goog.iter.StopIteration;
+ }
+ return goog.dom.TextRangeIterator.superClass_.next.call(this)
+};
+goog.dom.TextRangeIterator.prototype.skipTag = function() {
+ goog.dom.TextRangeIterator.superClass_.skipTag.apply(this);
+ if(goog.dom.contains(this.node, this.endNode_)) {
+ throw goog.iter.StopIteration;
+ }
+};
+goog.dom.TextRangeIterator.prototype.copyFrom = function(other) {
+ this.startNode_ = other.startNode_;
+ this.endNode_ = other.endNode_;
+ this.startOffset_ = other.startOffset_;
+ this.endOffset_ = other.endOffset_;
+ this.isReversed_ = other.isReversed_;
+ goog.dom.TextRangeIterator.superClass_.copyFrom.call(this, other)
+};
+goog.dom.TextRangeIterator.prototype.clone = function() {
+ var copy = new goog.dom.TextRangeIterator(this.startNode_, this.startOffset_, this.endNode_, this.endOffset_, this.isReversed_);
+ copy.copyFrom(this);
+ return copy
+};
+goog.provide("goog.dom.RangeEndpoint");
+goog.dom.RangeEndpoint = {START:1, END:0};
+goog.provide("goog.userAgent.jscript");
+goog.require("goog.string");
+goog.userAgent.jscript.ASSUME_NO_JSCRIPT = false;
+goog.userAgent.jscript.init_ = function() {
+ var hasScriptEngine = "ScriptEngine" in goog.global;
+ goog.userAgent.jscript.DETECTED_HAS_JSCRIPT_ = hasScriptEngine && goog.global["ScriptEngine"]() == "JScript";
+ goog.userAgent.jscript.DETECTED_VERSION_ = goog.userAgent.jscript.DETECTED_HAS_JSCRIPT_ ? goog.global["ScriptEngineMajorVersion"]() + "." + goog.global["ScriptEngineMinorVersion"]() + "." + goog.global["ScriptEngineBuildVersion"]() : "0"
+};
+if(!goog.userAgent.jscript.ASSUME_NO_JSCRIPT) {
+ goog.userAgent.jscript.init_()
+}
+goog.userAgent.jscript.HAS_JSCRIPT = goog.userAgent.jscript.ASSUME_NO_JSCRIPT ? false : goog.userAgent.jscript.DETECTED_HAS_JSCRIPT_;
+goog.userAgent.jscript.VERSION = goog.userAgent.jscript.ASSUME_NO_JSCRIPT ? "0" : goog.userAgent.jscript.DETECTED_VERSION_;
+goog.userAgent.jscript.isVersion = function(version) {
+ return goog.string.compareVersions(goog.userAgent.jscript.VERSION, version) >= 0
+};
+goog.provide("goog.string.StringBuffer");
+goog.require("goog.userAgent.jscript");
+goog.string.StringBuffer = function(opt_a1, var_args) {
+ this.buffer_ = goog.userAgent.jscript.HAS_JSCRIPT ? [] : "";
+ if(opt_a1 != null) {
+ this.append.apply(this, arguments)
+ }
+};
+goog.string.StringBuffer.prototype.set = function(s) {
+ this.clear();
+ this.append(s)
+};
+if(goog.userAgent.jscript.HAS_JSCRIPT) {
+ goog.string.StringBuffer.prototype.bufferLength_ = 0;
+ goog.string.StringBuffer.prototype.append = function(a1, opt_a2, var_args) {
+ if(opt_a2 == null) {
+ this.buffer_[this.bufferLength_++] = a1
+ }else {
+ this.buffer_.push.apply(this.buffer_, arguments);
+ this.bufferLength_ = this.buffer_.length
+ }
+ return this
+ }
+}else {
+ goog.string.StringBuffer.prototype.append = function(a1, opt_a2, var_args) {
+ this.buffer_ += a1;
+ if(opt_a2 != null) {
+ for(var i = 1;i < arguments.length;i++) {
+ this.buffer_ += arguments[i]
+ }
+ }
+ return this
+ }
+}
+goog.string.StringBuffer.prototype.clear = function() {
+ if(goog.userAgent.jscript.HAS_JSCRIPT) {
+ this.buffer_.length = 0;
+ this.bufferLength_ = 0
+ }else {
+ this.buffer_ = ""
+ }
+};
+goog.string.StringBuffer.prototype.getLength = function() {
+ return this.toString().length
+};
+goog.string.StringBuffer.prototype.toString = function() {
+ if(goog.userAgent.jscript.HAS_JSCRIPT) {
+ var str = this.buffer_.join("");
+ this.clear();
+ if(str) {
+ this.append(str)
+ }
+ return str
+ }else {
+ return this.buffer_
+ }
+};
+goog.provide("goog.dom.browserrange.AbstractRange");
+goog.require("goog.dom");
+goog.require("goog.dom.NodeType");
+goog.require("goog.dom.RangeEndpoint");
+goog.require("goog.dom.TagName");
+goog.require("goog.dom.TextRangeIterator");
+goog.require("goog.iter");
+goog.require("goog.string");
+goog.require("goog.string.StringBuffer");
+goog.require("goog.userAgent");
+goog.dom.browserrange.AbstractRange = function() {
+};
+goog.dom.browserrange.AbstractRange.prototype.clone = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.getBrowserRange = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.getContainer = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.getStartNode = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.getStartOffset = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.getEndNode = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.getEndOffset = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.compareBrowserRangeEndpoints = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.containsRange = function(abstractRange, opt_allowPartial) {
+ var checkPartial = opt_allowPartial && !abstractRange.isCollapsed();
+ var range = abstractRange.getBrowserRange();
+ var start = goog.dom.RangeEndpoint.START, end = goog.dom.RangeEndpoint.END;
+ try {
+ if(checkPartial) {
+ return this.compareBrowserRangeEndpoints(range, end, start) >= 0 && this.compareBrowserRangeEndpoints(range, start, end) <= 0
+ }else {
+ return this.compareBrowserRangeEndpoints(range, end, end) >= 0 && this.compareBrowserRangeEndpoints(range, start, start) <= 0
+ }
+ }catch(e) {
+ if(!goog.userAgent.IE) {
+ throw e;
+ }
+ return false
+ }
+};
+goog.dom.browserrange.AbstractRange.prototype.containsNode = function(node, opt_allowPartial) {
+ return this.containsRange(goog.dom.browserrange.createRangeFromNodeContents(node), opt_allowPartial)
+};
+goog.dom.browserrange.AbstractRange.prototype.isCollapsed = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.getText = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.getHtmlFragment = function() {
+ var output = new goog.string.StringBuffer;
+ goog.iter.forEach(this, function(node, ignore, it) {
+ if(node.nodeType == goog.dom.NodeType.TEXT) {
+ output.append(goog.string.htmlEscape(node.nodeValue.substring(it.getStartTextOffset(), it.getEndTextOffset())))
+ }else {
+ if(node.nodeType == goog.dom.NodeType.ELEMENT) {
+ if(it.isEndTag()) {
+ if(goog.dom.canHaveChildren(node)) {
+ output.append("</" + node.tagName + ">")
+ }
+ }else {
+ var shallow = node.cloneNode(false);
+ var html = goog.dom.getOuterHtml(shallow);
+ if(goog.userAgent.IE && node.tagName == goog.dom.TagName.LI) {
+ output.append(html)
+ }else {
+ var index = html.lastIndexOf("<");
+ output.append(index ? html.substr(0, index) : html)
+ }
+ }
+ }
+ }
+ }, this);
+ return output.toString()
+};
+goog.dom.browserrange.AbstractRange.prototype.getValidHtml = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.__iterator__ = function(opt_keys) {
+ return new goog.dom.TextRangeIterator(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset())
+};
+goog.dom.browserrange.AbstractRange.prototype.select = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.removeContents = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.surroundContents = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.insertNode = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.surroundWithNodes = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.collapse = goog.abstractMethod;
+goog.provide("goog.dom.browserrange.W3cRange");
+goog.require("goog.dom");
+goog.require("goog.dom.NodeType");
+goog.require("goog.dom.RangeEndpoint");
+goog.require("goog.dom.browserrange.AbstractRange");
+goog.require("goog.string");
+goog.dom.browserrange.W3cRange = function(range) {
+ this.range_ = range
+};
+goog.inherits(goog.dom.browserrange.W3cRange, goog.dom.browserrange.AbstractRange);
+goog.dom.browserrange.W3cRange.getBrowserRangeForNode = function(node) {
+ var nodeRange = goog.dom.getOwnerDocument(node).createRange();
+ if(node.nodeType == goog.dom.NodeType.TEXT) {
+ nodeRange.setStart(node, 0);
+ nodeRange.setEnd(node, node.length)
+ }else {
+ if(!goog.dom.browserrange.canContainRangeEndpoint(node)) {
+ var rangeParent = node.parentNode;
+ var rangeStartOffset = goog.array.indexOf(rangeParent.childNodes, node);
+ nodeRange.setStart(rangeParent, rangeStartOffset);
+ nodeRange.setEnd(rangeParent, rangeStartOffset + 1)
+ }else {
+ var tempNode, leaf = node;
+ while((tempNode = leaf.firstChild) && goog.dom.browserrange.canContainRangeEndpoint(tempNode)) {
+ leaf = tempNode
+ }
+ nodeRange.setStart(leaf, 0);
+ leaf = node;
+ while((tempNode = leaf.lastChild) && goog.dom.browserrange.canContainRangeEndpoint(tempNode)) {
+ leaf = tempNode
+ }
+ nodeRange.setEnd(leaf, leaf.nodeType == goog.dom.NodeType.ELEMENT ? leaf.childNodes.length : leaf.length)
+ }
+ }
+ return nodeRange
+};
+goog.dom.browserrange.W3cRange.getBrowserRangeForNodes = function(startNode, startOffset, endNode, endOffset) {
+ var nodeRange = goog.dom.getOwnerDocument(startNode).createRange();
+ nodeRange.setStart(startNode, startOffset);
+ nodeRange.setEnd(endNode, endOffset);
+ return nodeRange
+};
+goog.dom.browserrange.W3cRange.createFromNodeContents = function(node) {
+ return new goog.dom.browserrange.W3cRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNode(node))
+};
+goog.dom.browserrange.W3cRange.createFromNodes = function(startNode, startOffset, endNode, endOffset) {
+ return new goog.dom.browserrange.W3cRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNodes(startNode, startOffset, endNode, endOffset))
+};
+goog.dom.browserrange.W3cRange.prototype.clone = function() {
+ return new this.constructor(this.range_.cloneRange())
+};
+goog.dom.browserrange.W3cRange.prototype.getBrowserRange = function() {
+ return this.range_
+};
+goog.dom.browserrange.W3cRange.prototype.getContainer = function() {
+ return this.range_.commonAncestorContainer
+};
+goog.dom.browserrange.W3cRange.prototype.getStartNode = function() {
+ return this.range_.startContainer
+};
+goog.dom.browserrange.W3cRange.prototype.getStartOffset = function() {
+ return this.range_.startOffset
+};
+goog.dom.browserrange.W3cRange.prototype.getEndNode = function() {
+ return this.range_.endContainer
+};
+goog.dom.browserrange.W3cRange.prototype.getEndOffset = function() {
+ return this.range_.endOffset
+};
+goog.dom.browserrange.W3cRange.prototype.compareBrowserRangeEndpoints = function(range, thisEndpoint, otherEndpoint) {
+ return this.range_.compareBoundaryPoints(otherEndpoint == goog.dom.RangeEndpoint.START ? thisEndpoint == goog.dom.RangeEndpoint.START ? goog.global["Range"].START_TO_START : goog.global["Range"].START_TO_END : thisEndpoint == goog.dom.RangeEndpoint.START ? goog.global["Range"].END_TO_START : goog.global["Range"].END_TO_END, range)
+};
+goog.dom.browserrange.W3cRange.prototype.isCollapsed = function() {
+ return this.range_.collapsed
+};
+goog.dom.browserrange.W3cRange.prototype.getText = function() {
+ return this.range_.toString()
+};
+goog.dom.browserrange.W3cRange.prototype.getValidHtml = function() {
+ var div = goog.dom.getDomHelper(this.range_.startContainer).createDom("div");
+ div.appendChild(this.range_.cloneContents());
+ var result = div.innerHTML;
+ if(goog.string.startsWith(result, "<") || !this.isCollapsed() && !goog.string.contains(result, "<")) {
+ return result
+ }
+ var container = this.getContainer();
+ container = container.nodeType == goog.dom.NodeType.ELEMENT ? container : container.parentNode;
+ var html = goog.dom.getOuterHtml(container.cloneNode(false));
+ return html.replace(">", ">" + result)
+};
+goog.dom.browserrange.W3cRange.prototype.select = function(reverse) {
+ var win = goog.dom.getWindow(goog.dom.getOwnerDocument(this.getStartNode()));
+ this.selectInternal(win.getSelection(), reverse)
+};
+goog.dom.browserrange.W3cRange.prototype.selectInternal = function(selection, reverse) {
+ selection.removeAllRanges();
+ selection.addRange(this.range_)
+};
+goog.dom.browserrange.W3cRange.prototype.removeContents = function() {
+ var range = this.range_;
+ range.extractContents();
+ if(range.startContainer.hasChildNodes()) {
+ var rangeStartContainer = range.startContainer.childNodes[range.startOffset];
+ if(rangeStartContainer) {
+ var rangePrevious = rangeStartContainer.previousSibling;
+ if(goog.dom.getRawTextContent(rangeStartContainer) == "") {
+ goog.dom.removeNode(rangeStartContainer)
+ }
+ if(rangePrevious && goog.dom.getRawTextContent(rangePrevious) == "") {
+ goog.dom.removeNode(rangePrevious)
+ }
+ }
+ }
+};
+goog.dom.browserrange.W3cRange.prototype.surroundContents = function(element) {
+ this.range_.surroundContents(element);
+ return element
+};
+goog.dom.browserrange.W3cRange.prototype.insertNode = function(node, before) {
+ var range = this.range_.cloneRange();
+ range.collapse(before);
+ range.insertNode(node);
+ range.detach();
+ return node
+};
+goog.dom.browserrange.W3cRange.prototype.surroundWithNodes = function(startNode, endNode) {
+ var win = goog.dom.getWindow(goog.dom.getOwnerDocument(this.getStartNode()));
+ var selectionRange = goog.dom.Range.createFromWindow(win);
+ if(selectionRange) {
+ var sNode = selectionRange.getStartNode();
+ var eNode = selectionRange.getEndNode();
+ var sOffset = selectionRange.getStartOffset();
+ var eOffset = selectionRange.getEndOffset()
+ }
+ var clone1 = this.range_.cloneRange();
+ var clone2 = this.range_.cloneRange();
+ clone1.collapse(false);
+ clone2.collapse(true);
+ clone1.insertNode(endNode);
+ clone2.insertNode(startNode);
+ clone1.detach();
+ clone2.detach();
+ if(selectionRange) {
+ var isInsertedNode = function(n) {
+ return n == startNode || n == endNode
+ };
+ if(sNode.nodeType == goog.dom.NodeType.TEXT) {
+ while(sOffset > sNode.length) {
+ sOffset -= sNode.length;
+ do {
+ sNode = sNode.nextSibling
+ }while(isInsertedNode(sNode))
+ }
+ }
+ if(eNode.nodeType == goog.dom.NodeType.TEXT) {
+ while(eOffset > eNode.length) {
+ eOffset -= eNode.length;
+ do {
+ eNode = eNode.nextSibling
+ }while(isInsertedNode(eNode))
+ }
+ }
+ goog.dom.Range.createFromNodes(sNode, sOffset, eNode, eOffset).select()
+ }
+};
+goog.dom.browserrange.W3cRange.prototype.collapse = function(toStart) {
+ this.range_.collapse(toStart)
+};
+goog.provide("goog.dom.browserrange.GeckoRange");
+goog.require("goog.dom.browserrange.W3cRange");
+goog.dom.browserrange.GeckoRange = function(range) {
+ goog.dom.browserrange.W3cRange.call(this, range)
+};
+goog.inherits(goog.dom.browserrange.GeckoRange, goog.dom.browserrange.W3cRange);
+goog.dom.browserrange.GeckoRange.createFromNodeContents = function(node) {
+ return new goog.dom.browserrange.GeckoRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNode(node))
+};
+goog.dom.browserrange.GeckoRange.createFromNodes = function(startNode, startOffset, endNode, endOffset) {
+ return new goog.dom.browserrange.GeckoRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNodes(startNode, startOffset, endNode, endOffset))
+};
+goog.dom.browserrange.GeckoRange.prototype.selectInternal = function(selection, reversed) {
+ var anchorNode = reversed ? this.getEndNode() : this.getStartNode();
+ var anchorOffset = reversed ? this.getEndOffset() : this.getStartOffset();
+ var focusNode = reversed ? this.getStartNode() : this.getEndNode();
+ var focusOffset = reversed ? this.getStartOffset() : this.getEndOffset();
+ selection.collapse(anchorNode, anchorOffset);
+ if(anchorNode != focusNode || anchorOffset != focusOffset) {
+ selection.extend(focusNode, focusOffset)
+ }
+};
+goog.provide("goog.dom.NodeIterator");
+goog.require("goog.dom.TagIterator");
+goog.dom.NodeIterator = function(opt_node, opt_reversed, opt_unconstrained, opt_depth) {
+ goog.dom.TagIterator.call(this, opt_node, opt_reversed, opt_unconstrained, null, opt_depth)
+};
+goog.inherits(goog.dom.NodeIterator, goog.dom.TagIterator);
+goog.dom.NodeIterator.prototype.next = function() {
+ do {
+ goog.dom.NodeIterator.superClass_.next.call(this)
+ }while(this.isEndTag());
+ return this.node
+};
+goog.provide("goog.dom.browserrange.IeRange");
+goog.require("goog.array");
+goog.require("goog.debug.Logger");
+goog.require("goog.dom");
+goog.require("goog.dom.NodeIterator");
+goog.require("goog.dom.NodeType");
+goog.require("goog.dom.RangeEndpoint");
+goog.require("goog.dom.TagName");
+goog.require("goog.dom.browserrange.AbstractRange");
+goog.require("goog.iter");
+goog.require("goog.iter.StopIteration");
+goog.require("goog.string");
+goog.dom.browserrange.IeRange = function(range, doc) {
+ this.range_ = range;
+ this.doc_ = doc
+};
+goog.inherits(goog.dom.browserrange.IeRange, goog.dom.browserrange.AbstractRange);
+goog.dom.browserrange.IeRange.logger_ = goog.debug.Logger.getLogger("goog.dom.browserrange.IeRange");
+goog.dom.browserrange.IeRange.getBrowserRangeForNode_ = function(node) {
+ var nodeRange = goog.dom.getOwnerDocument(node).body.createTextRange();
+ if(node.nodeType == goog.dom.NodeType.ELEMENT) {
+ nodeRange.moveToElementText(node);
+ if(goog.dom.browserrange.canContainRangeEndpoint(node) && !node.childNodes.length) {
+ nodeRange.collapse(false)
+ }
+ }else {
+ var offset = 0;
+ var sibling = node;
+ while(sibling = sibling.previousSibling) {
+ var nodeType = sibling.nodeType;
+ if(nodeType == goog.dom.NodeType.TEXT) {
+ offset += sibling.length
+ }else {
+ if(nodeType == goog.dom.NodeType.ELEMENT) {
+ nodeRange.moveToElementText(sibling);
+ break
+ }
+ }
+ }
+ if(!sibling) {
+ nodeRange.moveToElementText(node.parentNode)
+ }
+ nodeRange.collapse(!sibling);
+ if(offset) {
+ nodeRange.move("character", offset)
+ }
+ nodeRange.moveEnd("character", node.length)
+ }
+ return nodeRange
+};
+goog.dom.browserrange.IeRange.getBrowserRangeForNodes_ = function(startNode, startOffset, endNode, endOffset) {
+ var child, collapse = false;
+ if(startNode.nodeType == goog.dom.NodeType.ELEMENT) {
+ if(startOffset > startNode.childNodes.length) {
+ goog.dom.browserrange.IeRange.logger_.severe("Cannot have startOffset > startNode child count")
+ }
+ child = startNode.childNodes[startOffset];
+ collapse = !child;
+ startNode = child || startNode.lastChild || startNode;
+ startOffset = 0
+ }
+ var leftRange = goog.dom.browserrange.IeRange.getBrowserRangeForNode_(startNode);
+ if(startOffset) {
+ leftRange.move("character", startOffset)
+ }
+ if(startNode == endNode && startOffset == endOffset) {
+ leftRange.collapse(true);
+ return leftRange
+ }
+ if(collapse) {
+ leftRange.collapse(false)
+ }
+ collapse = false;
+ if(endNode.nodeType == goog.dom.NodeType.ELEMENT) {
+ if(endOffset > endNode.childNodes.length) {
+ goog.dom.browserrange.IeRange.logger_.severe("Cannot have endOffset > endNode child count")
+ }
+ child = endNode.childNodes[endOffset];
+ endNode = child || endNode.lastChild || endNode;
+ endOffset = 0;
+ collapse = !child
+ }
+ var rightRange = goog.dom.browserrange.IeRange.getBrowserRangeForNode_(endNode);
+ rightRange.collapse(!collapse);
+ if(endOffset) {
+ rightRange.moveEnd("character", endOffset)
+ }
+ leftRange.setEndPoint("EndToEnd", rightRange);
+ return leftRange
+};
+goog.dom.browserrange.IeRange.createFromNodeContents = function(node) {
+ var range = new goog.dom.browserrange.IeRange(goog.dom.browserrange.IeRange.getBrowserRangeForNode_(node), goog.dom.getOwnerDocument(node));
+ if(!goog.dom.browserrange.canContainRangeEndpoint(node)) {
+ range.startNode_ = range.endNode_ = range.parentNode_ = node.parentNode;
+ range.startOffset_ = goog.array.indexOf(range.parentNode_.childNodes, node);
+ range.endOffset_ = range.startOffset_ + 1
+ }else {
+ var tempNode, leaf = node;
+ while((tempNode = leaf.firstChild) && goog.dom.browserrange.canContainRangeEndpoint(tempNode)) {
+ leaf = tempNode
+ }
+ range.startNode_ = leaf;
+ range.startOffset_ = 0;
+ leaf = node;
+ while((tempNode = leaf.lastChild) && goog.dom.browserrange.canContainRangeEndpoint(tempNode)) {
+ leaf = tempNode
+ }
+ range.endNode_ = leaf;
+ range.endOffset_ = leaf.nodeType == goog.dom.NodeType.ELEMENT ? leaf.childNodes.length : leaf.length;
+ range.parentNode_ = node
+ }
+ return range
+};
+goog.dom.browserrange.IeRange.createFromNodes = function(startNode, startOffset, endNode, endOffset) {
+ var range = new goog.dom.browserrange.IeRange(goog.dom.browserrange.IeRange.getBrowserRangeForNodes_(startNode, startOffset, endNode, endOffset), goog.dom.getOwnerDocument(startNode));
+ range.startNode_ = startNode;
+ range.startOffset_ = startOffset;
+ range.endNode_ = endNode;
+ range.endOffset_ = endOffset;
+ return range
+};
+goog.dom.browserrange.IeRange.prototype.parentNode_ = null;
+goog.dom.browserrange.IeRange.prototype.startNode_ = null;
+goog.dom.browserrange.IeRange.prototype.endNode_ = null;
+goog.dom.browserrange.IeRange.prototype.startOffset_ = -1;
+goog.dom.browserrange.IeRange.prototype.endOffset_ = -1;
+goog.dom.browserrange.IeRange.prototype.clone = function() {
+ var range = new goog.dom.browserrange.IeRange(this.range_.duplicate(), this.doc_);
+ range.parentNode_ = this.parentNode_;
+ range.startNode_ = this.startNode_;
+ range.endNode_ = this.endNode_;
+ return range
+};
+goog.dom.browserrange.IeRange.prototype.getBrowserRange = function() {
+ return this.range_
+};
+goog.dom.browserrange.IeRange.prototype.clearCachedValues_ = function() {
+ this.parentNode_ = this.startNode_ = this.endNode_ = null;
+ this.startOffset_ = this.endOffset_ = -1
+};
+goog.dom.browserrange.IeRange.prototype.getContainer = function() {
+ if(!this.parentNode_) {
+ var selectText = this.range_.text;
+ var range = this.range_.duplicate();
+ var rightTrimmedSelectText = selectText.replace(/ +$/, "");
+ var numSpacesAtEnd = selectText.length - rightTrimmedSelectText.length;
+ if(numSpacesAtEnd) {
+ range.moveEnd("character", -numSpacesAtEnd)
+ }
+ var parent = range.parentElement();
+ var htmlText = range.htmlText;
+ var htmlTextLen = goog.string.stripNewlines(htmlText).length;
+ if(this.isCollapsed() && htmlTextLen > 0) {
+ return this.parentNode_ = parent
+ }
+ while(htmlTextLen > goog.string.stripNewlines(parent.outerHTML).length) {
+ parent = parent.parentNode
+ }
+ while(parent.childNodes.length == 1 && parent.innerText == goog.dom.browserrange.IeRange.getNodeText_(parent.firstChild)) {
+ if(!goog.dom.browserrange.canContainRangeEndpoint(parent.firstChild)) {
+ break
+ }
+ parent = parent.firstChild
+ }
+ if(selectText.length == 0) {
+ parent = this.findDeepestContainer_(parent)
+ }
+ this.parentNode_ = parent
+ }
+ return this.parentNode_
+};
+goog.dom.browserrange.IeRange.prototype.findDeepestContainer_ = function(node) {
+ var childNodes = node.childNodes;
+ for(var i = 0, len = childNodes.length;i < len;i++) {
+ var child = childNodes[i];
+ if(goog.dom.browserrange.canContainRangeEndpoint(child)) {
+ var childRange = goog.dom.browserrange.IeRange.getBrowserRangeForNode_(child);
+ var start = goog.dom.RangeEndpoint.START;
+ var end = goog.dom.RangeEndpoint.END;
+ var isChildRangeErratic = childRange.htmlText != child.outerHTML;
+ var isNativeInRangeErratic = this.isCollapsed() && isChildRangeErratic;
+ var inChildRange = isNativeInRangeErratic ? this.compareBrowserRangeEndpoints(childRange, start, start) >= 0 && this.compareBrowserRangeEndpoints(childRange, start, end) <= 0 : this.range_.inRange(childRange);
+ if(inChildRange) {
+ return this.findDeepestContainer_(child)
+ }
+ }
+ }
+ return node
+};
+goog.dom.browserrange.IeRange.prototype.getStartNode = function() {
+ if(!this.startNode_) {
+ this.startNode_ = this.getEndpointNode_(goog.dom.RangeEndpoint.START);
+ if(this.isCollapsed()) {
+ this.endNode_ = this.startNode_
+ }
+ }
+ return this.startNode_
+};
+goog.dom.browserrange.IeRange.prototype.getStartOffset = function() {
+ if(this.startOffset_ < 0) {
+ this.startOffset_ = this.getOffset_(goog.dom.RangeEndpoint.START);
+ if(this.isCollapsed()) {
+ this.endOffset_ = this.startOffset_
+ }
+ }
+ return this.startOffset_
+};
+goog.dom.browserrange.IeRange.prototype.getEndNode = function() {
+ if(this.isCollapsed()) {
+ return this.getStartNode()
+ }
+ if(!this.endNode_) {
+ this.endNode_ = this.getEndpointNode_(goog.dom.RangeEndpoint.END)
+ }
+ return this.endNode_
+};
+goog.dom.browserrange.IeRange.prototype.getEndOffset = function() {
+ if(this.isCollapsed()) {
+ return this.getStartOffset()
+ }
+ if(this.endOffset_ < 0) {
+ this.endOffset_ = this.getOffset_(goog.dom.RangeEndpoint.END);
+ if(this.isCollapsed()) {
+ this.startOffset_ = this.endOffset_
+ }
+ }
+ return this.endOffset_
+};
+goog.dom.browserrange.IeRange.prototype.compareBrowserRangeEndpoints = function(range, thisEndpoint, otherEndpoint) {
+ return this.range_.compareEndPoints((thisEndpoint == goog.dom.RangeEndpoint.START ? "Start" : "End") + "To" + (otherEndpoint == goog.dom.RangeEndpoint.START ? "Start" : "End"), range)
+};
+goog.dom.browserrange.IeRange.prototype.getEndpointNode_ = function(endpoint, opt_node) {
+ var node = opt_node || this.getContainer();
+ if(!node || !node.firstChild) {
+ return node
+ }
+ var start = goog.dom.RangeEndpoint.START, end = goog.dom.RangeEndpoint.END;
+ var isStartEndpoint = endpoint == start;
+ for(var j = 0, length = node.childNodes.length;j < length;j++) {
+ var i = isStartEndpoint ? j : length - j - 1;
+ var child = node.childNodes[i];
+ var childRange;
+ try {
+ childRange = goog.dom.browserrange.createRangeFromNodeContents(child)
+ }catch(e) {
+ continue
+ }
+ var ieRange = childRange.getBrowserRange();
+ if(this.isCollapsed()) {
+ if(!goog.dom.browserrange.canContainRangeEndpoint(child)) {
+ if(this.compareBrowserRangeEndpoints(ieRange, start, start) == 0) {
+ this.startOffset_ = this.endOffset_ = i;
+ return node
+ }
+ }else {
+ if(childRange.containsRange(this)) {
+ return this.getEndpointNode_(endpoint, child)
+ }
+ }
+ }else {
+ if(this.containsRange(childRange)) {
+ if(!goog.dom.browserrange.canContainRangeEndpoint(child)) {
+ if(isStartEndpoint) {
+ this.startOffset_ = i
+ }else {
+ this.endOffset_ = i + 1
+ }
+ return node
+ }
+ return this.getEndpointNode_(endpoint, child)
+ }else {
+ if(this.compareBrowserRangeEndpoints(ieRange, start, end) < 0 && this.compareBrowserRangeEndpoints(ieRange, end, start) > 0) {
+ return this.getEndpointNode_(endpoint, child)
+ }
+ }
+ }
+ }
+ return node
+};
+goog.dom.browserrange.IeRange.prototype.compareNodeEndpoints_ = function(node, thisEndpoint, otherEndpoint) {
+ return this.range_.compareEndPoints((thisEndpoint == goog.dom.RangeEndpoint.START ? "Start" : "End") + "To" + (otherEndpoint == goog.dom.RangeEndpoint.START ? "Start" : "End"), goog.dom.browserrange.createRangeFromNodeContents(node).getBrowserRange())
+};
+goog.dom.browserrange.IeRange.prototype.getOffset_ = function(endpoint, opt_container) {
+ var isStartEndpoint = endpoint == goog.dom.RangeEndpoint.START;
+ var container = opt_container || (isStartEndpoint ? this.getStartNode() : this.getEndNode());
+ if(container.nodeType == goog.dom.NodeType.ELEMENT) {
+ var children = container.childNodes;
+ var len = children.length;
+ var edge = isStartEndpoint ? 0 : len - 1;
+ var sign = isStartEndpoint ? 1 : -1;
+ for(var i = edge;i >= 0 && i < len;i += sign) {
+ var child = children[i];
+ if(goog.dom.browserrange.canContainRangeEndpoint(child)) {
+ continue
+ }
+ var endPointCompare = this.compareNodeEndpoints_(child, endpoint, endpoint);
+ if(endPointCompare == 0) {
+ return isStartEndpoint ? i : i + 1
+ }
+ }
+ return i == -1 ? 0 : i
+ }else {
+ var range = this.range_.duplicate();
+ var nodeRange = goog.dom.browserrange.IeRange.getBrowserRangeForNode_(container);
+ range.setEndPoint(isStartEndpoint ? "EndToEnd" : "StartToStart", nodeRange);
+ var rangeLength = range.text.length;
+ return isStartEndpoint ? container.length - rangeLength : rangeLength
+ }
+};
+goog.dom.browserrange.IeRange.getNodeText_ = function(node) {
+ return node.nodeType == goog.dom.NodeType.TEXT ? node.nodeValue : node.innerText
+};
+goog.dom.browserrange.IeRange.prototype.isRangeInDocument = function() {
+ var range = this.doc_.body.createTextRange();
+ range.moveToElementText(this.doc_.body);
+ return this.containsRange(new goog.dom.browserrange.IeRange(range, this.doc_), true)
+};
+goog.dom.browserrange.IeRange.prototype.isCollapsed = function() {
+ return this.range_.compareEndPoints("StartToEnd", this.range_) == 0
+};
+goog.dom.browserrange.IeRange.prototype.getText = function() {
+ return this.range_.text
+};
+goog.dom.browserrange.IeRange.prototype.getValidHtml = function() {
+ return this.range_.htmlText
+};
+goog.dom.browserrange.IeRange.prototype.select = function(opt_reverse) {
+ this.range_.select()
+};
+goog.dom.browserrange.IeRange.prototype.removeContents = function() {
+ if(this.range_.htmlText) {
+ var startNode = this.getStartNode();
+ var endNode = this.getEndNode();
+ var oldText = this.range_.text;
+ var clone = this.range_.duplicate();
+ clone.moveStart("character", 1);
+ clone.moveStart("character", -1);
+ if(clone.text != oldText) {
+ var iter = new goog.dom.NodeIterator(startNode, false, true);
+ var toDelete = [];
+ goog.iter.forEach(iter, function(node) {
+ if(node.nodeType != goog.dom.NodeType.TEXT && this.containsNode(node)) {
+ toDelete.push(node);
+ iter.skipTag()
+ }
+ if(node == endNode) {
+ throw goog.iter.StopIteration;
+ }
+ });
+ this.collapse(true);
+ goog.array.forEach(toDelete, goog.dom.removeNode);
+ this.clearCachedValues_();
+ return
+ }
+ this.range_ = clone;
+ this.range_.text = "";
+ this.clearCachedValues_();
+ var newStartNode = this.getStartNode();
+ var newStartOffset = this.getStartOffset();
+ try {
+ var sibling = startNode.nextSibling;
+ if(startNode == endNode && startNode.parentNode && startNode.nodeType == goog.dom.NodeType.TEXT && sibling && sibling.nodeType == goog.dom.NodeType.TEXT) {
+ startNode.nodeValue += sibling.nodeValue;
+ goog.dom.removeNode(sibling);
+ this.range_ = goog.dom.browserrange.IeRange.getBrowserRangeForNode_(newStartNode);
+ this.range_.move("character", newStartOffset);
+ this.clearCachedValues_()
+ }
+ }catch(e) {
+ }
+ }
+};
+goog.dom.browserrange.IeRange.getDomHelper_ = function(range) {
+ return goog.dom.getDomHelper(range.parentElement())
+};
+goog.dom.browserrange.IeRange.pasteElement_ = function(range, element, opt_domHelper) {
+ opt_domHelper = opt_domHelper || goog.dom.browserrange.IeRange.getDomHelper_(range);
+ var id;
+ var originalId = id = element.id;
+ if(!id) {
+ id = element.id = goog.string.createUniqueString()
+ }
+ range.pasteHTML(element.outerHTML);
+ element = opt_domHelper.getElement(id);
+ if(element) {
+ if(!originalId) {
+ element.removeAttribute("id")
+ }
+ }
+ return element
+};
+goog.dom.browserrange.IeRange.prototype.surroundContents = function(element) {
+ goog.dom.removeNode(element);
+ element.innerHTML = this.range_.htmlText;
+ element = goog.dom.browserrange.IeRange.pasteElement_(this.range_, element);
+ if(element) {
+ this.range_.moveToElementText(element)
+ }
+ this.clearCachedValues_();
+ return element
+};
+goog.dom.browserrange.IeRange.insertNode_ = function(clone, node, before, opt_domHelper) {
+ opt_domHelper = opt_domHelper || goog.dom.browserrange.IeRange.getDomHelper_(clone);
+ var isNonElement;
+ if(node.nodeType != goog.dom.NodeType.ELEMENT) {
+ isNonElement = true;
+ node = opt_domHelper.createDom(goog.dom.TagName.DIV, null, node)
+ }
+ clone.collapse(before);
+ node = goog.dom.browserrange.IeRange.pasteElement_(clone, node, opt_domHelper);
+ if(isNonElement) {
+ var newNonElement = node.firstChild;
+ opt_domHelper.flattenElement(node);
+ node = newNonElement
+ }
+ return node
+};
+goog.dom.browserrange.IeRange.prototype.insertNode = function(node, before) {
+ var output = goog.dom.browserrange.IeRange.insertNode_(this.range_.duplicate(), node, before);
+ this.clearCachedValues_();
+ return output
+};
+goog.dom.browserrange.IeRange.prototype.surroundWithNodes = function(startNode, endNode) {
+ var clone1 = this.range_.duplicate();
+ var clone2 = this.range_.duplicate();
+ goog.dom.browserrange.IeRange.insertNode_(clone1, startNode, true);
+ goog.dom.browserrange.IeRange.insertNode_(clone2, endNode, false);
+ this.clearCachedValues_()
+};
+goog.dom.browserrange.IeRange.prototype.collapse = function(toStart) {
+ this.range_.collapse(toStart);
+ if(toStart) {
+ this.endNode_ = this.startNode_;
+ this.endOffset_ = this.startOffset_
+ }else {
+ this.startNode_ = this.endNode_;
+ this.startOffset_ = this.endOffset_
+ }
+};
+goog.provide("goog.dom.browserrange.OperaRange");
+goog.require("goog.dom.browserrange.W3cRange");
+goog.dom.browserrange.OperaRange = function(range) {
+ goog.dom.browserrange.W3cRange.call(this, range)
+};
+goog.inherits(goog.dom.browserrange.OperaRange, goog.dom.browserrange.W3cRange);
+goog.dom.browserrange.OperaRange.createFromNodeContents = function(node) {
+ return new goog.dom.browserrange.OperaRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNode(node))
+};
+goog.dom.browserrange.OperaRange.createFromNodes = function(startNode, startOffset, endNode, endOffset) {
+ return new goog.dom.browserrange.OperaRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNodes(startNode, startOffset, endNode, endOffset))
+};
+goog.dom.browserrange.OperaRange.prototype.selectInternal = function(selection, reversed) {
+ selection.collapse(this.getStartNode(), this.getStartOffset());
+ if(this.getEndNode() != this.getStartNode() || this.getEndOffset() != this.getStartOffset()) {
+ selection.extend(this.getEndNode(), this.getEndOffset())
+ }
+ if(selection.rangeCount == 0) {
+ selection.addRange(this.range_)
+ }
+};
+goog.provide("goog.dom.browserrange.WebKitRange");
+goog.require("goog.dom.RangeEndpoint");
+goog.require("goog.dom.browserrange.W3cRange");
+goog.require("goog.userAgent");
+goog.dom.browserrange.WebKitRange = function(range) {
+ goog.dom.browserrange.W3cRange.call(this, range)
+};
+goog.inherits(goog.dom.browserrange.WebKitRange, goog.dom.browserrange.W3cRange);
+goog.dom.browserrange.WebKitRange.createFromNodeContents = function(node) {
+ return new goog.dom.browserrange.WebKitRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNode(node))
+};
+goog.dom.browserrange.WebKitRange.createFromNodes = function(startNode, startOffset, endNode, endOffset) {
+ return new goog.dom.browserrange.WebKitRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNodes(startNode, startOffset, endNode, endOffset))
+};
+goog.dom.browserrange.WebKitRange.prototype.compareBrowserRangeEndpoints = function(range, thisEndpoint, otherEndpoint) {
+ if(goog.userAgent.isVersion("528")) {
+ return goog.dom.browserrange.WebKitRange.superClass_.compareBrowserRangeEndpoints.call(this, range, thisEndpoint, otherEndpoint)
+ }
+ return this.range_.compareBoundaryPoints(otherEndpoint == goog.dom.RangeEndpoint.START ? thisEndpoint == goog.dom.RangeEndpoint.START ? goog.global["Range"].START_TO_START : goog.global["Range"].END_TO_START : thisEndpoint == goog.dom.RangeEndpoint.START ? goog.global["Range"].START_TO_END : goog.global["Range"].END_TO_END, range)
+};
+goog.dom.browserrange.WebKitRange.prototype.selectInternal = function(selection, reversed) {
+ selection.removeAllRanges();
+ if(reversed) {
+ selection.setBaseAndExtent(this.getEndNode(), this.getEndOffset(), this.getStartNode(), this.getStartOffset())
+ }else {
+ selection.setBaseAndExtent(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset())
+ }
+};
+goog.provide("goog.dom.browserrange");
+goog.provide("goog.dom.browserrange.Error");
+goog.require("goog.dom");
+goog.require("goog.dom.browserrange.GeckoRange");
+goog.require("goog.dom.browserrange.IeRange");
+goog.require("goog.dom.browserrange.OperaRange");
+goog.require("goog.dom.browserrange.W3cRange");
+goog.require("goog.dom.browserrange.WebKitRange");
+goog.require("goog.userAgent");
+goog.dom.browserrange.Error = {NOT_IMPLEMENTED:"Not Implemented"};
+goog.dom.browserrange.createRange = function(range) {
+ if(goog.userAgent.IE && !goog.userAgent.isVersion("9")) {
+ return new goog.dom.browserrange.IeRange(range, goog.dom.getOwnerDocument(range.parentElement()))
+ }else {
+ if(goog.userAgent.WEBKIT) {
+ return new goog.dom.browserrange.WebKitRange(range)
+ }else {
+ if(goog.userAgent.GECKO) {
+ return new goog.dom.browserrange.GeckoRange(range)
+ }else {
+ if(goog.userAgent.OPERA) {
+ return new goog.dom.browserrange.OperaRange(range)
+ }else {
+ return new goog.dom.browserrange.W3cRange(range)
+ }
+ }
+ }
+ }
+};
+goog.dom.browserrange.createRangeFromNodeContents = function(node) {
+ if(goog.userAgent.IE && !goog.userAgent.isVersion("9")) {
+ return goog.dom.browserrange.IeRange.createFromNodeContents(node)
+ }else {
+ if(goog.userAgent.WEBKIT) {
+ return goog.dom.browserrange.WebKitRange.createFromNodeContents(node)
+ }else {
+ if(goog.userAgent.GECKO) {
+ return goog.dom.browserrange.GeckoRange.createFromNodeContents(node)
+ }else {
+ if(goog.userAgent.OPERA) {
+ return goog.dom.browserrange.OperaRange.createFromNodeContents(node)
+ }else {
+ return goog.dom.browserrange.W3cRange.createFromNodeContents(node)
+ }
+ }
+ }
+ }
+};
+goog.dom.browserrange.createRangeFromNodes = function(startNode, startOffset, endNode, endOffset) {
+ if(goog.userAgent.IE && !goog.userAgent.isVersion("9")) {
+ return goog.dom.browserrange.IeRange.createFromNodes(startNode, startOffset, endNode, endOffset)
+ }else {
+ if(goog.userAgent.WEBKIT) {
+ return goog.dom.browserrange.WebKitRange.createFromNodes(startNode, startOffset, endNode, endOffset)
+ }else {
+ if(goog.userAgent.GECKO) {
+ return goog.dom.browserrange.GeckoRange.createFromNodes(startNode, startOffset, endNode, endOffset)
+ }else {
+ if(goog.userAgent.OPERA) {
+ return goog.dom.browserrange.OperaRange.createFromNodes(startNode, startOffset, endNode, endOffset)
+ }else {
+ return goog.dom.browserrange.W3cRange.createFromNodes(startNode, startOffset, endNode, endOffset)
+ }
+ }
+ }
+ }
+};
+goog.dom.browserrange.canContainRangeEndpoint = function(node) {
+ return goog.dom.canHaveChildren(node) || node.nodeType == goog.dom.NodeType.TEXT
+};
+goog.provide("goog.dom.TextRange");
+goog.require("goog.array");
+goog.require("goog.dom");
+goog.require("goog.dom.AbstractRange");
+goog.require("goog.dom.RangeType");
+goog.require("goog.dom.SavedRange");
+goog.require("goog.dom.TagName");
+goog.require("goog.dom.TextRangeIterator");
+goog.require("goog.dom.browserrange");
+goog.require("goog.string");
+goog.require("goog.userAgent");
+goog.dom.TextRange = function() {
+};
+goog.inherits(goog.dom.TextRange, goog.dom.AbstractRange);
+goog.dom.TextRange.createFromBrowserRange = function(range, opt_isReversed) {
+ return goog.dom.TextRange.createFromBrowserRangeWrapper_(goog.dom.browserrange.createRange(range), opt_isReversed)
+};
+goog.dom.TextRange.createFromBrowserRangeWrapper_ = function(browserRange, opt_isReversed) {
+ var range = new goog.dom.TextRange;
+ range.browserRangeWrapper_ = browserRange;
+ range.isReversed_ = !!opt_isReversed;
+ return range
+};
+goog.dom.TextRange.createFromNodeContents = function(node, opt_isReversed) {
+ return goog.dom.TextRange.createFromBrowserRangeWrapper_(goog.dom.browserrange.createRangeFromNodeContents(node), opt_isReversed)
+};
+goog.dom.TextRange.createFromNodes = function(anchorNode, anchorOffset, focusNode, focusOffset) {
+ var range = new goog.dom.TextRange;
+ range.isReversed_ = goog.dom.Range.isReversed(anchorNode, anchorOffset, focusNode, focusOffset);
+ if(anchorNode.tagName == "BR") {
+ var parent = anchorNode.parentNode;
+ anchorOffset = goog.array.indexOf(parent.childNodes, anchorNode);
+ anchorNode = parent
+ }
+ if(focusNode.tagName == "BR") {
+ var parent = focusNode.parentNode;
+ focusOffset = goog.array.indexOf(parent.childNodes, focusNode);
+ focusNode = parent
+ }
+ if(range.isReversed_) {
+ range.startNode_ = focusNode;
+ range.startOffset_ = focusOffset;
+ range.endNode_ = anchorNode;
+ range.endOffset_ = anchorOffset
+ }else {
+ range.startNode_ = anchorNode;
+ range.startOffset_ = anchorOffset;
+ range.endNode_ = focusNode;
+ range.endOffset_ = focusOffset
+ }
+ return range
+};
+goog.dom.TextRange.prototype.browserRangeWrapper_ = null;
+goog.dom.TextRange.prototype.startNode_ = null;
+goog.dom.TextRange.prototype.startOffset_ = null;
+goog.dom.TextRange.prototype.endNode_ = null;
+goog.dom.TextRange.prototype.endOffset_ = null;
+goog.dom.TextRange.prototype.isReversed_ = false;
+goog.dom.TextRange.prototype.clone = function() {
+ var range = new goog.dom.TextRange;
+ range.browserRangeWrapper_ = this.browserRangeWrapper_;
+ range.startNode_ = this.startNode_;
+ range.startOffset_ = this.startOffset_;
+ range.endNode_ = this.endNode_;
+ range.endOffset_ = this.endOffset_;
+ range.isReversed_ = this.isReversed_;
+ return range
+};
+goog.dom.TextRange.prototype.getType = function() {
+ return goog.dom.RangeType.TEXT
+};
+goog.dom.TextRange.prototype.getBrowserRangeObject = function() {
+ return this.getBrowserRangeWrapper_().getBrowserRange()
+};
+goog.dom.TextRange.prototype.setBrowserRangeObject = function(nativeRange) {
+ if(goog.dom.AbstractRange.isNativeControlRange(nativeRange)) {
+ return false
+ }
+ this.browserRangeWrapper_ = goog.dom.browserrange.createRange(nativeRange);
+ this.clearCachedValues_();
+ return true
+};
+goog.dom.TextRange.prototype.clearCachedValues_ = function() {
+ this.startNode_ = this.startOffset_ = this.endNode_ = this.endOffset_ = null
+};
+goog.dom.TextRange.prototype.getTextRangeCount = function() {
+ return 1
+};
+goog.dom.TextRange.prototype.getTextRange = function(i) {
+ return this
+};
+goog.dom.TextRange.prototype.getBrowserRangeWrapper_ = function() {
+ return this.browserRangeWrapper_ || (this.browserRangeWrapper_ = goog.dom.browserrange.createRangeFromNodes(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset()))
+};
+goog.dom.TextRange.prototype.getContainer = function() {
+ return this.getBrowserRangeWrapper_().getContainer()
+};
+goog.dom.TextRange.prototype.getStartNode = function() {
+ return this.startNode_ || (this.startNode_ = this.getBrowserRangeWrapper_().getStartNode())
+};
+goog.dom.TextRange.prototype.getStartOffset = function() {
+ return this.startOffset_ != null ? this.startOffset_ : this.startOffset_ = this.getBrowserRangeWrapper_().getStartOffset()
+};
+goog.dom.TextRange.prototype.getEndNode = function() {
+ return this.endNode_ || (this.endNode_ = this.getBrowserRangeWrapper_().getEndNode())
+};
+goog.dom.TextRange.prototype.getEndOffset = function() {
+ return this.endOffset_ != null ? this.endOffset_ : this.endOffset_ = this.getBrowserRangeWrapper_().getEndOffset()
+};
+goog.dom.TextRange.prototype.moveToNodes = function(startNode, startOffset, endNode, endOffset, isReversed) {
+ this.startNode_ = startNode;
+ this.startOffset_ = startOffset;
+ this.endNode_ = endNode;
+ this.endOffset_ = endOffset;
+ this.isReversed_ = isReversed;
+ this.browserRangeWrapper_ = null
+};
+goog.dom.TextRange.prototype.isReversed = function() {
+ return this.isReversed_
+};
+goog.dom.TextRange.prototype.containsRange = function(otherRange, opt_allowPartial) {
+ var otherRangeType = otherRange.getType();
+ if(otherRangeType == goog.dom.RangeType.TEXT) {
+ return this.getBrowserRangeWrapper_().containsRange(otherRange.getBrowserRangeWrapper_(), opt_allowPartial)
+ }else {
+ if(otherRangeType == goog.dom.RangeType.CONTROL) {
+ var elements = otherRange.getElements();
+ var fn = opt_allowPartial ? goog.array.some : goog.array.every;
+ return fn(elements, function(el) {
+ return this.containsNode(el, opt_allowPartial)
+ }, this)
+ }
+ }
+ return false
+};
+goog.dom.TextRange.isAttachedNode = function(node) {
+ if(goog.userAgent.IE) {
+ var returnValue = false;
+ try {
+ returnValue = node.parentNode
+ }catch(e) {
+ }
+ return!!returnValue
+ }else {
+ return goog.dom.contains(node.ownerDocument.body, node)
+ }
+};
+goog.dom.TextRange.prototype.isRangeInDocument = function() {
+ return(!this.startNode_ || goog.dom.TextRange.isAttachedNode(this.startNode_)) && (!this.endNode_ || goog.dom.TextRange.isAttachedNode(this.endNode_)) && (!goog.userAgent.IE || this.getBrowserRangeWrapper_().isRangeInDocument())
+};
+goog.dom.TextRange.prototype.isCollapsed = function() {
+ return this.getBrowserRangeWrapper_().isCollapsed()
+};
+goog.dom.TextRange.prototype.getText = function() {
+ return this.getBrowserRangeWrapper_().getText()
+};
+goog.dom.TextRange.prototype.getHtmlFragment = function() {
+ return this.getBrowserRangeWrapper_().getHtmlFragment()
+};
+goog.dom.TextRange.prototype.getValidHtml = function() {
+ return this.getBrowserRangeWrapper_().getValidHtml()
+};
+goog.dom.TextRange.prototype.getPastableHtml = function() {
+ var html = this.getValidHtml();
+ if(html.match(/^\s*<td\b/i)) {
+ html = "<table><tbody><tr>" + html + "</tr></tbody></table>"
+ }else {
+ if(html.match(/^\s*<tr\b/i)) {
+ html = "<table><tbody>" + html + "</tbody></table>"
+ }else {
+ if(html.match(/^\s*<tbody\b/i)) {
+ html = "<table>" + html + "</table>"
+ }else {
+ if(html.match(/^\s*<li\b/i)) {
+ var container = this.getContainer();
+ var tagType = goog.dom.TagName.UL;
+ while(container) {
+ if(container.tagName == goog.dom.TagName.OL) {
+ tagType = goog.dom.TagName.OL;
+ break
+ }else {
+ if(container.tagName == goog.dom.TagName.UL) {
+ break
+ }
+ }
+ container = container.parentNode
+ }
+ html = goog.string.buildString("<", tagType, ">", html, "</", tagType, ">")
+ }
+ }
+ }
+ }
+ return html
+};
+goog.dom.TextRange.prototype.__iterator__ = function(opt_keys) {
+ return new goog.dom.TextRangeIterator(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset())
+};
+goog.dom.TextRange.prototype.select = function() {
+ this.getBrowserRangeWrapper_().select(this.isReversed_)
+};
+goog.dom.TextRange.prototype.removeContents = function() {
+ this.getBrowserRangeWrapper_().removeContents();
+ this.clearCachedValues_()
+};
+goog.dom.TextRange.prototype.surroundContents = function(element) {
+ var output = this.getBrowserRangeWrapper_().surroundContents(element);
+ this.clearCachedValues_();
+ return output
+};
+goog.dom.TextRange.prototype.insertNode = function(node, before) {
+ var output = this.getBrowserRangeWrapper_().insertNode(node, before);
+ this.clearCachedValues_();
+ return output
+};
+goog.dom.TextRange.prototype.surroundWithNodes = function(startNode, endNode) {
+ this.getBrowserRangeWrapper_().surroundWithNodes(startNode, endNode);
+ this.clearCachedValues_()
+};
+goog.dom.TextRange.prototype.saveUsingDom = function() {
+ return new goog.dom.DomSavedTextRange_(this)
+};
+goog.dom.TextRange.prototype.collapse = function(toAnchor) {
+ var toStart = this.isReversed() ? !toAnchor : toAnchor;
+ if(this.browserRangeWrapper_) {
+ this.browserRangeWrapper_.collapse(toStart)
+ }
+ if(toStart) {
+ this.endNode_ = this.startNode_;
+ this.endOffset_ = this.startOffset_
+ }else {
+ this.startNode_ = this.endNode_;
+ this.startOffset_ = this.endOffset_
+ }
+ this.isReversed_ = false
+};
+goog.dom.DomSavedTextRange_ = function(range) {
+ this.anchorNode_ = range.getAnchorNode();
+ this.anchorOffset_ = range.getAnchorOffset();
+ this.focusNode_ = range.getFocusNode();
+ this.focusOffset_ = range.getFocusOffset()
+};
+goog.inherits(goog.dom.DomSavedTextRange_, goog.dom.SavedRange);
+goog.dom.DomSavedTextRange_.prototype.restoreInternal = function() {
+ return goog.dom.Range.createFromNodes(this.anchorNode_, this.anchorOffset_, this.focusNode_, this.focusOffset_)
+};
+goog.dom.DomSavedTextRange_.prototype.disposeInternal = function() {
+ goog.dom.DomSavedTextRange_.superClass_.disposeInternal.call(this);
+ this.anchorNode_ = null;
+ this.focusNode_ = null
+};
+goog.provide("goog.dom.ControlRange");
+goog.provide("goog.dom.ControlRangeIterator");
+goog.require("goog.array");
+goog.require("goog.dom");
+goog.require("goog.dom.AbstractMultiRange");
+goog.require("goog.dom.AbstractRange");
+goog.require("goog.dom.RangeIterator");
+goog.require("goog.dom.RangeType");
+goog.require("goog.dom.SavedRange");
+goog.require("goog.dom.TagWalkType");
+goog.require("goog.dom.TextRange");
+goog.require("goog.iter.StopIteration");
+goog.require("goog.userAgent");
+goog.dom.ControlRange = function() {
+};
+goog.inherits(goog.dom.ControlRange, goog.dom.AbstractMultiRange);
+goog.dom.ControlRange.createFromBrowserRange = function(controlRange) {
+ var range = new goog.dom.ControlRange;
+ range.range_ = controlRange;
+ return range
+};
+goog.dom.ControlRange.createFromElements = function(var_args) {
+ var range = goog.dom.getOwnerDocument(arguments[0]).body.createControlRange();
+ for(var i = 0, len = arguments.length;i < len;i++) {
+ range.addElement(arguments[i])
+ }
+ return goog.dom.ControlRange.createFromBrowserRange(range)
+};
+goog.dom.ControlRange.prototype.range_ = null;
+goog.dom.ControlRange.prototype.elements_ = null;
+goog.dom.ControlRange.prototype.sortedElements_ = null;
+goog.dom.ControlRange.prototype.clearCachedValues_ = function() {
+ this.elements_ = null;
+ this.sortedElements_ = null
+};
+goog.dom.ControlRange.prototype.clone = function() {
+ return goog.dom.ControlRange.createFromElements.apply(this, this.getElements())
+};
+goog.dom.ControlRange.prototype.getType = function() {
+ return goog.dom.RangeType.CONTROL
+};
+goog.dom.ControlRange.prototype.getBrowserRangeObject = function() {
+ return this.range_ || document.body.createControlRange()
+};
+goog.dom.ControlRange.prototype.setBrowserRangeObject = function(nativeRange) {
+ if(!goog.dom.AbstractRange.isNativeControlRange(nativeRange)) {
+ return false
+ }
+ this.range_ = nativeRange;
+ return true
+};
+goog.dom.ControlRange.prototype.getTextRangeCount = function() {
+ return this.range_ ? this.range_.length : 0
+};
+goog.dom.ControlRange.prototype.getTextRange = function(i) {
+ return goog.dom.TextRange.createFromNodeContents(this.range_.item(i))
+};
+goog.dom.ControlRange.prototype.getContainer = function() {
+ return goog.dom.findCommonAncestor.apply(null, this.getElements())
+};
+goog.dom.ControlRange.prototype.getStartNode = function() {
+ return this.getSortedElements()[0]
+};
+goog.dom.ControlRange.prototype.getStartOffset = function() {
+ return 0
+};
+goog.dom.ControlRange.prototype.getEndNode = function() {
+ var sorted = this.getSortedElements();
+ var startsLast = goog.array.peek(sorted);
+ return goog.array.find(sorted, function(el) {
+ return goog.dom.contains(el, startsLast)
+ })
+};
+goog.dom.ControlRange.prototype.getEndOffset = function() {
+ return this.getEndNode().childNodes.length
+};
+goog.dom.ControlRange.prototype.getElements = function() {
+ if(!this.elements_) {
+ this.elements_ = [];
+ if(this.range_) {
+ for(var i = 0;i < this.range_.length;i++) {
+ this.elements_.push(this.range_.item(i))
+ }
+ }
+ }
+ return this.elements_
+};
+goog.dom.ControlRange.prototype.getSortedElements = function() {
+ if(!this.sortedElements_) {
+ this.sortedElements_ = this.getElements().concat();
+ this.sortedElements_.sort(function(a, b) {
+ return a.sourceIndex - b.sourceIndex
+ })
+ }
+ return this.sortedElements_
+};
+goog.dom.ControlRange.prototype.isRangeInDocument = function() {
+ var returnValue = false;
+ try {
+ returnValue = goog.array.every(this.getElements(), function(element) {
+ return goog.userAgent.IE ? element.parentNode : goog.dom.contains(element.ownerDocument.body, element)
+ })
+ }catch(e) {
+ }
+ return returnValue
+};
+goog.dom.ControlRange.prototype.isCollapsed = function() {
+ return!this.range_ || !this.range_.length
+};
+goog.dom.ControlRange.prototype.getText = function() {
+ return""
+};
+goog.dom.ControlRange.prototype.getHtmlFragment = function() {
+ return goog.array.map(this.getSortedElements(), goog.dom.getOuterHtml).join("")
+};
+goog.dom.ControlRange.prototype.getValidHtml = function() {
+ return this.getHtmlFragment()
+};
+goog.dom.ControlRange.prototype.getPastableHtml = goog.dom.ControlRange.prototype.getValidHtml;
+goog.dom.ControlRange.prototype.__iterator__ = function(opt_keys) {
+ return new goog.dom.ControlRangeIterator(this)
+};
+goog.dom.ControlRange.prototype.select = function() {
+ if(this.range_) {
+ this.range_.select()
+ }
+};
+goog.dom.ControlRange.prototype.removeContents = function() {
+ if(this.range_) {
+ var nodes = [];
+ for(var i = 0, len = this.range_.length;i < len;i++) {
+ nodes.push(this.range_.item(i))
+ }
+ goog.array.forEach(nodes, goog.dom.removeNode);
+ this.collapse(false)
+ }
+};
+goog.dom.ControlRange.prototype.replaceContentsWithNode = function(node) {
+ var result = this.insertNode(node, true);
+ if(!this.isCollapsed()) {
+ this.removeContents()
+ }
+ return result
+};
+goog.dom.ControlRange.prototype.saveUsingDom = function() {
+ return new goog.dom.DomSavedControlRange_(this)
+};
+goog.dom.ControlRange.prototype.collapse = function(toAnchor) {
+ this.range_ = null;
+ this.clearCachedValues_()
+};
+goog.dom.DomSavedControlRange_ = function(range) {
+ this.elements_ = range.getElements()
+};
+goog.inherits(goog.dom.DomSavedControlRange_, goog.dom.SavedRange);
+goog.dom.DomSavedControlRange_.prototype.restoreInternal = function() {
+ var doc = this.elements_.length ? goog.dom.getOwnerDocument(this.elements_[0]) : document;
+ var controlRange = doc.body.createControlRange();
+ for(var i = 0, len = this.elements_.length;i < len;i++) {
+ controlRange.addElement(this.elements_[i])
+ }
+ return goog.dom.ControlRange.createFromBrowserRange(controlRange)
+};
+goog.dom.DomSavedControlRange_.prototype.disposeInternal = function() {
+ goog.dom.DomSavedControlRange_.superClass_.disposeInternal.call(this);
+ delete this.elements_
+};
+goog.dom.ControlRangeIterator = function(range) {
+ if(range) {
+ this.elements_ = range.getSortedElements();
+ this.startNode_ = this.elements_.shift();
+ this.endNode_ = goog.array.peek(this.elements_) || this.startNode_
+ }
+ goog.dom.RangeIterator.call(this, this.startNode_, false)
+};
+goog.inherits(goog.dom.ControlRangeIterator, goog.dom.RangeIterator);
+goog.dom.ControlRangeIterator.prototype.startNode_ = null;
+goog.dom.ControlRangeIterator.prototype.endNode_ = null;
+goog.dom.ControlRangeIterator.prototype.elements_ = null;
+goog.dom.ControlRangeIterator.prototype.getStartTextOffset = function() {
+ return 0
+};
+goog.dom.ControlRangeIterator.prototype.getEndTextOffset = function() {
+ return 0
+};
+goog.dom.ControlRangeIterator.prototype.getStartNode = function() {
+ return this.startNode_
+};
+goog.dom.ControlRangeIterator.prototype.getEndNode = function() {
+ return this.endNode_
+};
+goog.dom.ControlRangeIterator.prototype.isLast = function() {
+ return!this.depth && !this.elements_.length
+};
+goog.dom.ControlRangeIterator.prototype.next = function() {
+ if(this.isLast()) {
+ throw goog.iter.StopIteration;
+ }else {
+ if(!this.depth) {
+ var el = this.elements_.shift();
+ this.setPosition(el, goog.dom.TagWalkType.START_TAG, goog.dom.TagWalkType.START_TAG);
+ return el
+ }
+ }
+ return goog.dom.ControlRangeIterator.superClass_.next.call(this)
+};
+goog.dom.ControlRangeIterator.prototype.copyFrom = function(other) {
+ this.elements_ = other.elements_;
+ this.startNode_ = other.startNode_;
+ this.endNode_ = other.endNode_;
+ goog.dom.ControlRangeIterator.superClass_.copyFrom.call(this, other)
+};
+goog.dom.ControlRangeIterator.prototype.clone = function() {
+ var copy = new goog.dom.ControlRangeIterator(null);
+ copy.copyFrom(this);
+ return copy
+};
+goog.provide("goog.dom.MultiRange");
+goog.provide("goog.dom.MultiRangeIterator");
+goog.require("goog.array");
+goog.require("goog.debug.Logger");
+goog.require("goog.dom.AbstractMultiRange");
+goog.require("goog.dom.AbstractRange");
+goog.require("goog.dom.RangeIterator");
+goog.require("goog.dom.RangeType");
+goog.require("goog.dom.SavedRange");
+goog.require("goog.dom.TextRange");
+goog.require("goog.iter.StopIteration");
+goog.dom.MultiRange = function() {
+ this.browserRanges_ = [];
+ this.ranges_ = [];
+ this.sortedRanges_ = null;
+ this.container_ = null
+};
+goog.inherits(goog.dom.MultiRange, goog.dom.AbstractMultiRange);
+goog.dom.MultiRange.createFromBrowserSelection = function(selection) {
+ var range = new goog.dom.MultiRange;
+ for(var i = 0, len = selection.rangeCount;i < len;i++) {
+ range.browserRanges_.push(selection.getRangeAt(i))
+ }
+ return range
+};
+goog.dom.MultiRange.createFromBrowserRanges = function(browserRanges) {
+ var range = new goog.dom.MultiRange;
+ range.browserRanges_ = goog.array.clone(browserRanges);
+ return range
+};
+goog.dom.MultiRange.createFromTextRanges = function(textRanges) {
+ var range = new goog.dom.MultiRange;
+ range.ranges_ = textRanges;
+ range.browserRanges_ = goog.array.map(textRanges, function(range) {
+ return range.getBrowserRangeObject()
+ });
+ return range
+};
+goog.dom.MultiRange.prototype.logger_ = goog.debug.Logger.getLogger("goog.dom.MultiRange");
+goog.dom.MultiRange.prototype.clearCachedValues_ = function() {
+ this.ranges_ = [];
+ this.sortedRanges_ = null;
+ this.container_ = null
+};
+goog.dom.MultiRange.prototype.clone = function() {
+ return goog.dom.MultiRange.createFromBrowserRanges(this.browserRanges_)
+};
+goog.dom.MultiRange.prototype.getType = function() {
+ return goog.dom.RangeType.MULTI
+};
+goog.dom.MultiRange.prototype.getBrowserRangeObject = function() {
+ if(this.browserRanges_.length > 1) {
+ this.logger_.warning("getBrowserRangeObject called on MultiRange with more than 1 range")
+ }
+ return this.browserRanges_[0]
+};
+goog.dom.MultiRange.prototype.setBrowserRangeObject = function(nativeRange) {
+ return false
+};
+goog.dom.MultiRange.prototype.getTextRangeCount = function() {
+ return this.browserRanges_.length
+};
+goog.dom.MultiRange.prototype.getTextRange = function(i) {
+ if(!this.ranges_[i]) {
+ this.ranges_[i] = goog.dom.TextRange.createFromBrowserRange(this.browserRanges_[i])
+ }
+ return this.ranges_[i]
+};
+goog.dom.MultiRange.prototype.getContainer = function() {
+ if(!this.container_) {
+ var nodes = [];
+ for(var i = 0, len = this.getTextRangeCount();i < len;i++) {
+ nodes.push(this.getTextRange(i).getContainer())
+ }
+ this.container_ = goog.dom.findCommonAncestor.apply(null, nodes)
+ }
+ return this.container_
+};
+goog.dom.MultiRange.prototype.getSortedRanges = function() {
+ if(!this.sortedRanges_) {
+ this.sortedRanges_ = this.getTextRanges();
+ this.sortedRanges_.sort(function(a, b) {
+ var aStartNode = a.getStartNode();
+ var aStartOffset = a.getStartOffset();
+ var bStartNode = b.getStartNode();
+ var bStartOffset = b.getStartOffset();
+ if(aStartNode == bStartNode && aStartOffset == bStartOffset) {
+ return 0
+ }
+ return goog.dom.Range.isReversed(aStartNode, aStartOffset, bStartNode, bStartOffset) ? 1 : -1
+ })
+ }
+ return this.sortedRanges_
+};
+goog.dom.MultiRange.prototype.getStartNode = function() {
+ return this.getSortedRanges()[0].getStartNode()
+};
+goog.dom.MultiRange.prototype.getStartOffset = function() {
+ return this.getSortedRanges()[0].getStartOffset()
+};
+goog.dom.MultiRange.prototype.getEndNode = function() {
+ return goog.array.peek(this.getSortedRanges()).getEndNode()
+};
+goog.dom.MultiRange.prototype.getEndOffset = function() {
+ return goog.array.peek(this.getSortedRanges()).getEndOffset()
+};
+/*
+goog.dom.MultiRange.prototype.isRangeInDocument = function() {
+ return goog.array.every(this.getTextRanges(), function(range) {
+ return range.isRangeInDocument()
+ })
+};
+*/
+goog.dom.MultiRange.prototype.isCollapsed = function() {
+ return this.browserRanges_.length == 0 || this.browserRanges_.length == 1 && this.getTextRange(0).isCollapsed()
+};
+goog.dom.MultiRange.prototype.getText = function() {
+ return goog.array.map(this.getTextRanges(), function(range) {
+ return range.getText()
+ }).join("")
+};
+goog.dom.MultiRange.prototype.getHtmlFragment = function() {
+ return this.getValidHtml()
+};
+goog.dom.MultiRange.prototype.getValidHtml = function() {
+ return goog.array.map(this.getTextRanges(), function(range) {
+ return range.getValidHtml()
+ }).join("")
+};
+goog.dom.MultiRange.prototype.getPastableHtml = function() {
+ return this.getValidHtml()
+};
+goog.dom.MultiRange.prototype.__iterator__ = function(opt_keys) {
+ return new goog.dom.MultiRangeIterator(this)
+};
+goog.dom.MultiRange.prototype.select = function() {
+ var selection = goog.dom.AbstractRange.getBrowserSelectionForWindow(this.getWindow());
+ selection.removeAllRanges();
+ for(var i = 0, len = this.getTextRangeCount();i < len;i++) {
+ selection.addRange(this.getTextRange(i).getBrowserRangeObject())
+ }
+};
+goog.dom.MultiRange.prototype.removeContents = function() {
+ goog.array.forEach(this.getTextRanges(), function(range) {
+ range.removeContents()
+ })
+};
+goog.dom.MultiRange.prototype.saveUsingDom = function() {
+ return new goog.dom.DomSavedMultiRange_(this)
+};
+goog.dom.MultiRange.prototype.collapse = function(toAnchor) {
+ if(!this.isCollapsed()) {
+ var range = toAnchor ? this.getTextRange(0) : this.getTextRange(this.getTextRangeCount() - 1);
+ this.clearCachedValues_();
+ range.collapse(toAnchor);
+ this.ranges_ = [range];
+ this.sortedRanges_ = [range];
+ this.browserRanges_ = [range.getBrowserRangeObject()]
+ }
+};
+goog.dom.DomSavedMultiRange_ = function(range) {
+ this.savedRanges_ = goog.array.map(range.getTextRanges(), function(range) {
+ return range.saveUsingDom()
+ })
+};
+goog.inherits(goog.dom.DomSavedMultiRange_, goog.dom.SavedRange);
+goog.dom.DomSavedMultiRange_.prototype.restoreInternal = function() {
+ var ranges = goog.array.map(this.savedRanges_, function(savedRange) {
+ return savedRange.restore()
+ });
+ return goog.dom.MultiRange.createFromTextRanges(ranges)
+};
+goog.dom.DomSavedMultiRange_.prototype.disposeInternal = function() {
+ goog.dom.DomSavedMultiRange_.superClass_.disposeInternal.call(this);
+ goog.array.forEach(this.savedRanges_, function(savedRange) {
+ savedRange.dispose()
+ });
+ delete this.savedRanges_
+};
+goog.dom.MultiRangeIterator = function(range) {
+ if(range) {
+ this.iterators_ = goog.array.map(range.getSortedRanges(), function(r) {
+ return goog.iter.toIterator(r)
+ })
+ }
+ goog.dom.RangeIterator.call(this, range ? this.getStartNode() : null, false)
+};
+goog.inherits(goog.dom.MultiRangeIterator, goog.dom.RangeIterator);
+goog.dom.MultiRangeIterator.prototype.iterators_ = null;
+goog.dom.MultiRangeIterator.prototype.currentIdx_ = 0;
+goog.dom.MultiRangeIterator.prototype.getStartTextOffset = function() {
+ return this.iterators_[this.currentIdx_].getStartTextOffset()
+};
+goog.dom.MultiRangeIterator.prototype.getEndTextOffset = function() {
+ return this.iterators_[this.currentIdx_].getEndTextOffset()
+};
+goog.dom.MultiRangeIterator.prototype.getStartNode = function() {
+ return this.iterators_[0].getStartNode()
+};
+goog.dom.MultiRangeIterator.prototype.getEndNode = function() {
+ return goog.array.peek(this.iterators_).getEndNode()
+};
+goog.dom.MultiRangeIterator.prototype.isLast = function() {
+ return this.iterators_[this.currentIdx_].isLast()
+};
+goog.dom.MultiRangeIterator.prototype.next = function() {
+ try {
+ var it = this.iterators_[this.currentIdx_];
+ var next = it.next();
+ this.setPosition(it.node, it.tagType, it.depth);
+ return next
+ }catch(ex) {
+ if(ex !== goog.iter.StopIteration || this.iterators_.length - 1 == this.currentIdx_) {
+ throw ex;
+ }else {
+ this.currentIdx_++;
+ return this.next()
+ }
+ }
+};
+goog.dom.MultiRangeIterator.prototype.copyFrom = function(other) {
+ this.iterators_ = goog.array.clone(other.iterators_);
+ goog.dom.MultiRangeIterator.superClass_.copyFrom.call(this, other)
+};
+goog.dom.MultiRangeIterator.prototype.clone = function() {
+ var copy = new goog.dom.MultiRangeIterator(null);
+ copy.copyFrom(this);
+ return copy
+};
+goog.provide("goog.dom.Range");
+goog.require("goog.dom");
+goog.require("goog.dom.AbstractRange");
+goog.require("goog.dom.ControlRange");
+goog.require("goog.dom.MultiRange");
+goog.require("goog.dom.NodeType");
+goog.require("goog.dom.TextRange");
+goog.require("goog.userAgent");
+goog.dom.Range.createFromWindow = function(opt_win) {
+ var sel = goog.dom.AbstractRange.getBrowserSelectionForWindow(opt_win || window);
+ return sel && goog.dom.Range.createFromBrowserSelection(sel)
+};
+goog.dom.Range.createFromBrowserSelection = function(selection) {
+ var range;
+ var isReversed = false;
+ if(selection.createRange) {
+ try {
+ range = selection.createRange()
+ }catch(e) {
+ return null
+ }
+ }else {
+ if(selection.rangeCount) {
+ if(selection.rangeCount > 1) {
+ return goog.dom.MultiRange.createFromBrowserSelection(selection)
+ }else {
+ range = selection.getRangeAt(0);
+ isReversed = goog.dom.Range.isReversed(selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset)
+ }
+ }else {
+ return null
+ }
+ }
+ return goog.dom.Range.createFromBrowserRange(range, isReversed)
+};
+goog.dom.Range.createFromBrowserRange = function(range, opt_isReversed) {
+ return goog.dom.AbstractRange.isNativeControlRange(range) ? goog.dom.ControlRange.createFromBrowserRange(range) : goog.dom.TextRange.createFromBrowserRange(range, opt_isReversed)
+};
+goog.dom.Range.createFromNodeContents = function(node, opt_isReversed) {
+ return goog.dom.TextRange.createFromNodeContents(node, opt_isReversed)
+};
+goog.dom.Range.createCaret = function(node, offset) {
+ return goog.dom.TextRange.createFromNodes(node, offset, node, offset)
+};
+goog.dom.Range.createFromNodes = function(startNode, startOffset, endNode, endOffset) {
+ return goog.dom.TextRange.createFromNodes(startNode, startOffset, endNode, endOffset)
+};
+goog.dom.Range.clearSelection = function(opt_win) {
+ var sel = goog.dom.AbstractRange.getBrowserSelectionForWindow(opt_win || window);
+ if(!sel) {
+ return
+ }
+ if(sel.empty) {
+ try {
+ sel.empty()
+ }catch(e) {
+ }
+ }else {
+ sel.removeAllRanges()
+ }
+};
+goog.dom.Range.hasSelection = function(opt_win) {
+ var sel = goog.dom.AbstractRange.getBrowserSelectionForWindow(opt_win || window);
+ return!!sel && (goog.userAgent.IE ? sel.type != "None" : !!sel.rangeCount)
+};
+goog.dom.Range.isReversed = function(anchorNode, anchorOffset, focusNode, focusOffset) {
+ if(anchorNode == focusNode) {
+ return focusOffset < anchorOffset
+ }
+ var child;
+ if(anchorNode.nodeType == goog.dom.NodeType.ELEMENT && anchorOffset) {
+ child = anchorNode.childNodes[anchorOffset];
+ if(child) {
+ anchorNode = child;
+ anchorOffset = 0
+ }else {
+ if(goog.dom.contains(anchorNode, focusNode)) {
+ return true
+ }
+ }
+ }
+ if(focusNode.nodeType == goog.dom.NodeType.ELEMENT && focusOffset) {
+ child = focusNode.childNodes[focusOffset];
+ if(child) {
+ focusNode = child;
+ focusOffset = 0
+ }else {
+ if(goog.dom.contains(focusNode, anchorNode)) {
+ return false
+ }
+ }
+ }
+ return(goog.dom.compareNodeOrder(anchorNode, focusNode) || anchorOffset - focusOffset) > 0
+};
+window.createFromWindow = goog.dom.Range.createFromWindow;
+window.createFromNodes = goog.dom.Range.createFromNodes;
+window.createCaret = goog.dom.Range.createCaret; \ No newline at end of file
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/run.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/run.js
new file mode 100644
index 000000000..6e2acd937
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/run.js
@@ -0,0 +1,383 @@
+/**
+ * @fileoverview
+ * Main functions used in running the RTE test suite.
+ *
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License')
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @version 0.1
+ * @author rolandsteiner@google.com
+ */
+
+/**
+ * Info function: returns true if the suite (mainly) tests the result HTML/Text.
+ *
+ * @param suite {String} the test suite
+ * @return {boolean} Whether the suite main focus is the output HTML/Text
+ */
+function suiteChecksHTMLOrText(suite) {
+ return suite.id[0] != 'S';
+}
+
+/**
+ * Info function: returns true if the suite checks the result selection.
+ *
+ * @param suite {String} the test suite
+ * @return {boolean} Whether the suite checks the selection
+ */
+function suiteChecksSelection(suite) {
+ return suite.id[0] != 'Q';
+}
+
+/**
+ * Helper function returning the effective value of a test parameter.
+ *
+ * @param suite {Object} the test suite
+ * @param group {Object} group of tests within the suite the test belongs to
+ * @param test {Object} the test
+ * @param param {String} the test parameter to be checked
+ * @return {Any} the effective value of the parameter (can be undefined)
+ */
+function getTestParameter(suite, group, test, param) {
+ var val = test[param];
+ if (val === undefined) {
+ val = group[param];
+ }
+ if (val === undefined) {
+ val = suite[param];
+ }
+ return val;
+}
+
+/**
+ * Helper function returning the effective value of a container/test parameter.
+ *
+ * @param suite {Object} the test suite
+ * @param group {Object} group of tests within the suite the test belongs to
+ * @param test {Object} the test
+ * @param container {Object} the container descriptor object
+ * @param param {String} the test parameter to be checked
+ * @return {Any} the effective value of the parameter (can be undefined)
+ */
+function getContainerParameter(suite, group, test, container, param) {
+ var val = undefined;
+ if (test[container.id]) {
+ val = test[container.id][param];
+ }
+ if (val === undefined) {
+ val = test[param];
+ }
+ if (val === undefined) {
+ val = group[param];
+ }
+ if (val === undefined) {
+ val = suite[param];
+ }
+ return val;
+}
+
+/**
+ * Initializes the global variables before any tests are run.
+ */
+function initVariables() {
+ results = {
+ count: 0,
+ valscore: 0,
+ selscore: 0
+ };
+}
+
+/**
+ * Runs a single test - outputs and sets the result variables.
+ *
+ * @param suite {Object} suite that test originates in as object reference
+ * @param group {Object} group of tests within the suite the test belongs to
+ * @param test {Object} test to be run as object reference
+ * @param container {Object} container descriptor as object reference
+ * @see variables.js for RESULT... values
+ */
+function runSingleTest(suite, group, test, container) {
+ var result = {
+ valscore: 0,
+ selscore: 0,
+ valresult: VALRESULT_NOT_RUN,
+ selresult: SELRESULT_NOT_RUN,
+ output: ''
+ };
+
+ // 1.) Populate the editor element with the initial test setup HTML.
+ try {
+ initContainer(suite, group, test, container);
+ } catch(ex) {
+ result.valresult = VALRESULT_SETUP_EXCEPTION;
+ result.selresult = SELRESULT_NA;
+ result.output = SETUP_EXCEPTION + ex.toString();
+ return result;
+ }
+
+ // 2.) Run the test command, general function or query function.
+ var isHTMLTest = false;
+
+ try {
+ var cmd = undefined;
+
+ if (cmd = getTestParameter(suite, group, test, PARAM_EXECCOMMAND)) {
+ isHTMLTest = true;
+ // Note: "getTestParameter(suite, group, test, PARAM_VALUE) || null"
+ // doesn't work, since value might be the empty string, e.g., for 'insertText'!
+ var value = getTestParameter(suite, group, test, PARAM_VALUE);
+ if (value === undefined) {
+ value = null;
+ }
+ container.doc.execCommand(cmd, false, value);
+ } else if (cmd = getTestParameter(suite, group, test, PARAM_FUNCTION)) {
+ isHTMLTest = true;
+ eval(cmd);
+ } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDSUPPORTED)) {
+ result.output = container.doc.queryCommandSupported(cmd);
+ } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDENABLED)) {
+ result.output = container.doc.queryCommandEnabled(cmd);
+ } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDINDETERM)) {
+ result.output = container.doc.queryCommandIndeterm(cmd);
+ } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDSTATE)) {
+ result.output = container.doc.queryCommandState(cmd);
+ } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDVALUE)) {
+ result.output = container.doc.queryCommandValue(cmd);
+ if (result.output === false) {
+ // A return value of boolean 'false' for queryCommandValue means 'not supported'.
+ result.valresult = VALRESULT_UNSUPPORTED;
+ result.selresult = SELRESULT_NA;
+ result.output = UNSUPPORTED;
+ return result;
+ }
+ } else {
+ result.valresult = VALRESULT_SETUP_EXCEPTION;
+ result.selresult = SELRESULT_NA;
+ result.output = SETUP_EXCEPTION + SETUP_NOCOMMAND;
+ return result;
+ }
+ } catch (ex) {
+ result.valresult = VALRESULT_EXECUTION_EXCEPTION;
+ result.selresult = SELRESULT_NA;
+ result.output = EXECUTION_EXCEPTION + ex.toString();
+ return result;
+ }
+
+ // 4.) Verify test result
+ try {
+ if (isHTMLTest) {
+ // First, retrieve HTML from container
+ prepareHTMLTestResult(container, result);
+
+ // Compare result to expectations
+ compareHTMLTestResult(suite, group, test, container, result);
+
+ result.valscore = (result.valresult === VALRESULT_EQUAL) ? 1 : 0;
+ result.selscore = (result.selresult === SELRESULT_EQUAL) ? 1 : 0;
+ } else {
+ compareTextTestResult(suite, group, test, result);
+
+ result.selresult = SELRESULT_NA;
+ result.valscore = (result.valresult === VALRESULT_EQUAL) ? 1 : 0;
+ }
+ } catch (ex) {
+ result.valresult = VALRESULT_VERIFICATION_EXCEPTION;
+ result.selresult = SELRESULT_NA;
+ result.output = VERIFICATION_EXCEPTION + ex.toString();
+ return result;
+ }
+
+ return result;
+}
+
+/**
+ * Initializes the results dictionary for a given test suite.
+ * (for all classes -> tests -> containers)
+ *
+ * @param {Object} suite as object reference
+ */
+function initTestSuiteResults(suite) {
+ var suiteID = suite.id;
+
+ // Initialize results entries for this suite
+ results[suiteID] = {
+ count: 0,
+ valscore: 0,
+ selscore: 0,
+ time: 0
+ };
+ var totalTestCount = 0;
+
+ for (var clsIdx = 0; clsIdx < testClassCount; ++clsIdx) {
+ var clsID = testClassIDs[clsIdx];
+ var cls = suite[clsID];
+ if (!cls)
+ continue;
+
+ results[suiteID][clsID] = {
+ count: 0,
+ valscore: 0,
+ selscore: 0
+ };
+ var clsTestCount = 0;
+
+ var groupCount = cls.length;
+ for (var groupIdx = 0; groupIdx < groupCount; ++groupIdx) {
+ var group = cls[groupIdx];
+ var testCount = group.tests.length;
+
+ clsTestCount += testCount;
+ totalTestCount += testCount;
+
+ for (var testIdx = 0; testIdx < testCount; ++testIdx) {
+ var test = group.tests[testIdx];
+
+ results[suiteID][clsID ][test.id] = {
+ valscore: 0,
+ selscore: 0,
+ valresult: VALRESULT_NOT_RUN,
+ selresult: SELRESULT_NOT_RUN
+ };
+ for (var cntIdx = 0; cntIdx < containers.length; ++cntIdx) {
+ var cntID = containers[cntIdx].id;
+
+ results[suiteID][clsID][test.id][cntID] = {
+ valscore: 0,
+ selscore: 0,
+ valresult: VALRESULT_NOT_RUN,
+ selresult: SELRESULT_NOT_RUN,
+ output: ''
+ }
+ }
+ }
+ }
+ results[suiteID][clsID].count = clsTestCount;
+ }
+ results[suiteID].count = totalTestCount;
+}
+
+/**
+ * Runs a single test suite (such as DELETE tests or INSERT tests).
+ *
+ * @param suite {Object} suite as object reference
+ */
+function runTestSuite(suite) {
+ var suiteID = suite.id;
+ var suiteStartTime = new Date().getTime();
+
+ initTestSuiteResults(suite);
+
+ for (var clsIdx = 0; clsIdx < testClassCount; ++clsIdx) {
+ var clsID = testClassIDs[clsIdx];
+ var cls = suite[clsID];
+ if (!cls)
+ continue;
+
+ var groupCount = cls.length;
+
+ for (var groupIdx = 0; groupIdx < groupCount; ++groupIdx) {
+ var group = cls[groupIdx];
+ var testCount = group.tests.length;
+
+ for (var testIdx = 0; testIdx < testCount; ++testIdx) {
+ var test = group.tests[testIdx];
+
+ var valscore = 1;
+ var selscore = 1;
+ var valresult = VALRESULT_EQUAL;
+ var selresult = SELRESULT_EQUAL;
+
+ for (var cntIdx = 0; cntIdx < containers.length; ++cntIdx) {
+ var container = containers[cntIdx];
+ var cntID = container.id;
+
+ var result = runSingleTest(suite, group, test, container);
+
+ results[suiteID][clsID][test.id][cntID] = result;
+
+ valscore = Math.min(valscore, result.valscore);
+ selscore = Math.min(selscore, result.selscore);
+ valresult = Math.min(valresult, result.valresult);
+ selresult = Math.min(selresult, result.selresult);
+
+ resetContainer(container);
+ }
+
+ results[suiteID][clsID][test.id].valscore = valscore;
+ results[suiteID][clsID][test.id].selscore = selscore;
+ results[suiteID][clsID][test.id].valresult = valresult;
+ results[suiteID][clsID][test.id].selresult = selresult;
+
+ results[suiteID][clsID].valscore += valscore;
+ results[suiteID][clsID].selscore += selscore;
+ results[suiteID].valscore += valscore;
+ results[suiteID].selscore += selscore;
+ results.valscore += valscore;
+ results.selscore += selscore;
+ }
+ }
+ }
+
+ results[suiteID].time = new Date().getTime() - suiteStartTime;
+}
+
+/**
+ * Runs a single test suite (such as DELETE tests or INSERT tests)
+ * and updates the output HTML.
+ *
+ * @param {Object} suite as object reference
+ */
+function runAndOutputTestSuite(suite) {
+ runTestSuite(suite);
+ outputTestSuiteResults(suite);
+}
+
+/**
+ * Fills the beacon with the test results.
+ */
+function fillResults() {
+ // Result totals of the individual categories
+ categoryTotals = [
+ 'selection=' + results['S'].selscore,
+ 'apply=' + results['A'].valscore,
+ 'applyCSS=' + results['AC'].valscore,
+ 'change=' + results['C'].valscore,
+ 'changeCSS=' + results['CC'].valscore,
+ 'unapply=' + results['U'].valscore,
+ 'unapplyCSS=' + results['UC'].valscore,
+ 'delete=' + results['D'].valscore,
+ 'forwarddelete=' + results['FD'].valscore,
+ 'insert=' + results['I'].valscore,
+ 'selectionResult=' + (results['A'].selscore +
+ results['AC'].selscore +
+ results['C'].selscore +
+ results['CC'].selscore +
+ results['U'].selscore +
+ results['UC'].selscore +
+ results['D'].selscore +
+ results['FD'].selscore +
+ results['I'].selscore),
+ 'querySupported=' + results['Q'].valscore,
+ 'queryEnabled=' + results['QE'].valscore,
+ 'queryIndeterm=' + results['QI'].valscore,
+ 'queryState=' + results['QS'].valscore,
+ 'queryStateCSS=' + results['QSC'].valscore,
+ 'queryValue=' + results['QV'].valscore,
+ 'queryValueCSS=' + results['QVC'].valscore
+ ];
+
+ // Beacon copies category results
+ beacon = categoryTotals.slice(0);
+}
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/units.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/units.js
new file mode 100644
index 000000000..f2c23fbe5
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/units.js
@@ -0,0 +1,416 @@
+/**
+ * @fileoverview
+ * Common constants and variables used in the RTE test suite.
+ *
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License')
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @version 0.1
+ * @author rolandsteiner@google.com
+ */
+
+// All colors defined in CSS3.
+var colorChart = {
+ 'aliceblue': {red: 0xF0, green: 0xF8, blue: 0xFF},
+ 'antiquewhite': {red: 0xFA, green: 0xEB, blue: 0xD7},
+ 'aqua': {red: 0x00, green: 0xFF, blue: 0xFF},
+ 'aquamarine': {red: 0x7F, green: 0xFF, blue: 0xD4},
+ 'azure': {red: 0xF0, green: 0xFF, blue: 0xFF},
+ 'beige': {red: 0xF5, green: 0xF5, blue: 0xDC},
+ 'bisque': {red: 0xFF, green: 0xE4, blue: 0xC4},
+ 'black': {red: 0x00, green: 0x00, blue: 0x00},
+ 'blanchedalmond': {red: 0xFF, green: 0xEB, blue: 0xCD},
+ 'blue': {red: 0x00, green: 0x00, blue: 0xFF},
+ 'blueviolet': {red: 0x8A, green: 0x2B, blue: 0xE2},
+ 'brown': {red: 0xA5, green: 0x2A, blue: 0x2A},
+ 'burlywood': {red: 0xDE, green: 0xB8, blue: 0x87},
+ 'cadetblue': {red: 0x5F, green: 0x9E, blue: 0xA0},
+ 'chartreuse': {red: 0x7F, green: 0xFF, blue: 0x00},
+ 'chocolate': {red: 0xD2, green: 0x69, blue: 0x1E},
+ 'coral': {red: 0xFF, green: 0x7F, blue: 0x50},
+ 'cornflowerblue': {red: 0x64, green: 0x95, blue: 0xED},
+ 'cornsilk': {red: 0xFF, green: 0xF8, blue: 0xDC},
+ 'crimson': {red: 0xDC, green: 0x14, blue: 0x3C},
+ 'cyan': {red: 0x00, green: 0xFF, blue: 0xFF},
+ 'darkblue': {red: 0x00, green: 0x00, blue: 0x8B},
+ 'darkcyan': {red: 0x00, green: 0x8B, blue: 0x8B},
+ 'darkgoldenrod': {red: 0xB8, green: 0x86, blue: 0x0B},
+ 'darkgray': {red: 0xA9, green: 0xA9, blue: 0xA9},
+ 'darkgreen': {red: 0x00, green: 0x64, blue: 0x00},
+ 'darkgrey': {red: 0xA9, green: 0xA9, blue: 0xA9},
+ 'darkkhaki': {red: 0xBD, green: 0xB7, blue: 0x6B},
+ 'darkmagenta': {red: 0x8B, green: 0x00, blue: 0x8B},
+ 'darkolivegreen': {red: 0x55, green: 0x6B, blue: 0x2F},
+ 'darkorange': {red: 0xFF, green: 0x8C, blue: 0x00},
+ 'darkorchid': {red: 0x99, green: 0x32, blue: 0xCC},
+ 'darkred': {red: 0x8B, green: 0x00, blue: 0x00},
+ 'darksalmon': {red: 0xE9, green: 0x96, blue: 0x7A},
+ 'darkseagreen': {red: 0x8F, green: 0xBC, blue: 0x8F},
+ 'darkslateblue': {red: 0x48, green: 0x3D, blue: 0x8B},
+ 'darkslategray': {red: 0x2F, green: 0x4F, blue: 0x4F},
+ 'darkslategrey': {red: 0x2F, green: 0x4F, blue: 0x4F},
+ 'darkturquoise': {red: 0x00, green: 0xCE, blue: 0xD1},
+ 'darkviolet': {red: 0x94, green: 0x00, blue: 0xD3},
+ 'deeppink': {red: 0xFF, green: 0x14, blue: 0x93},
+ 'deepskyblue': {red: 0x00, green: 0xBF, blue: 0xFF},
+ 'dimgray': {red: 0x69, green: 0x69, blue: 0x69},
+ 'dimgrey': {red: 0x69, green: 0x69, blue: 0x69},
+ 'dodgerblue': {red: 0x1E, green: 0x90, blue: 0xFF},
+ 'firebrick': {red: 0xB2, green: 0x22, blue: 0x22},
+ 'floralwhite': {red: 0xFF, green: 0xFA, blue: 0xF0},
+ 'forestgreen': {red: 0x22, green: 0x8B, blue: 0x22},
+ 'fuchsia': {red: 0xFF, green: 0x00, blue: 0xFF},
+ 'gainsboro': {red: 0xDC, green: 0xDC, blue: 0xDC},
+ 'ghostwhite': {red: 0xF8, green: 0xF8, blue: 0xFF},
+ 'gold': {red: 0xFF, green: 0xD7, blue: 0x00},
+ 'goldenrod': {red: 0xDA, green: 0xA5, blue: 0x20},
+ 'gray': {red: 0x80, green: 0x80, blue: 0x80},
+ 'green': {red: 0x00, green: 0x80, blue: 0x00},
+ 'greenyellow': {red: 0xAD, green: 0xFF, blue: 0x2F},
+ 'grey': {red: 0x80, green: 0x80, blue: 0x80},
+ 'honeydew': {red: 0xF0, green: 0xFF, blue: 0xF0},
+ 'hotpink': {red: 0xFF, green: 0x69, blue: 0xB4},
+ 'indianred': {red: 0xCD, green: 0x5C, blue: 0x5C},
+ 'indigo': {red: 0x4B, green: 0x00, blue: 0x82},
+ 'ivory': {red: 0xFF, green: 0xFF, blue: 0xF0},
+ 'khaki': {red: 0xF0, green: 0xE6, blue: 0x8C},
+ 'lavender': {red: 0xE6, green: 0xE6, blue: 0xFA},
+ 'lavenderblush': {red: 0xFF, green: 0xF0, blue: 0xF5},
+ 'lawngreen': {red: 0x7C, green: 0xFC, blue: 0x00},
+ 'lemonchiffon': {red: 0xFF, green: 0xFA, blue: 0xCD},
+ 'lightblue': {red: 0xAD, green: 0xD8, blue: 0xE6},
+ 'lightcoral': {red: 0xF0, green: 0x80, blue: 0x80},
+ 'lightcyan': {red: 0xE0, green: 0xFF, blue: 0xFF},
+ 'lightgoldenrodyellow': {red: 0xFA, green: 0xFA, blue: 0xD2},
+ 'lightgray': {red: 0xD3, green: 0xD3, blue: 0xD3},
+ 'lightgreen': {red: 0x90, green: 0xEE, blue: 0x90},
+ 'lightgrey': {red: 0xD3, green: 0xD3, blue: 0xD3},
+ 'lightpink': {red: 0xFF, green: 0xB6, blue: 0xC1},
+ 'lightsalmon': {red: 0xFF, green: 0xA0, blue: 0x7A},
+ 'lightseagreen': {red: 0x20, green: 0xB2, blue: 0xAA},
+ 'lightskyblue': {red: 0x87, green: 0xCE, blue: 0xFA},
+ 'lightslategray': {red: 0x77, green: 0x88, blue: 0x99},
+ 'lightslategrey': {red: 0x77, green: 0x88, blue: 0x99},
+ 'lightsteelblue': {red: 0xB0, green: 0xC4, blue: 0xDE},
+ 'lightyellow': {red: 0xFF, green: 0xFF, blue: 0xE0},
+ 'lime': {red: 0x00, green: 0xFF, blue: 0x00},
+ 'limegreen': {red: 0x32, green: 0xCD, blue: 0x32},
+ 'linen': {red: 0xFA, green: 0xF0, blue: 0xE6},
+ 'magenta': {red: 0xFF, green: 0x00, blue: 0xFF},
+ 'maroon': {red: 0x80, green: 0x00, blue: 0x00},
+ 'mediumaquamarine': {red: 0x66, green: 0xCD, blue: 0xAA},
+ 'mediumblue': {red: 0x00, green: 0x00, blue: 0xCD},
+ 'mediumorchid': {red: 0xBA, green: 0x55, blue: 0xD3},
+ 'mediumpurple': {red: 0x93, green: 0x70, blue: 0xDB},
+ 'mediumseagreen': {red: 0x3C, green: 0xB3, blue: 0x71},
+ 'mediumslateblue': {red: 0x7B, green: 0x68, blue: 0xEE},
+ 'mediumspringgreen': {red: 0x00, green: 0xFA, blue: 0x9A},
+ 'mediumturquoise': {red: 0x48, green: 0xD1, blue: 0xCC},
+ 'mediumvioletred': {red: 0xC7, green: 0x15, blue: 0x85},
+ 'midnightblue': {red: 0x19, green: 0x19, blue: 0x70},
+ 'mintcream': {red: 0xF5, green: 0xFF, blue: 0xFA},
+ 'mistyrose': {red: 0xFF, green: 0xE4, blue: 0xE1},
+ 'moccasin': {red: 0xFF, green: 0xE4, blue: 0xB5},
+ 'navajowhite': {red: 0xFF, green: 0xDE, blue: 0xAD},
+ 'navy': {red: 0x00, green: 0x00, blue: 0x80},
+ 'oldlace': {red: 0xFD, green: 0xF5, blue: 0xE6},
+ 'olive': {red: 0x80, green: 0x80, blue: 0x00},
+ 'olivedrab': {red: 0x6B, green: 0x8E, blue: 0x23},
+ 'orange': {red: 0xFF, green: 0xA5, blue: 0x00},
+ 'orangered': {red: 0xFF, green: 0x45, blue: 0x00},
+ 'orchid': {red: 0xDA, green: 0x70, blue: 0xD6},
+ 'palegoldenrod': {red: 0xEE, green: 0xE8, blue: 0xAA},
+ 'palegreen': {red: 0x98, green: 0xFB, blue: 0x98},
+ 'paleturquoise': {red: 0xAF, green: 0xEE, blue: 0xEE},
+ 'palevioletred': {red: 0xDB, green: 0x70, blue: 0x93},
+ 'papayawhip': {red: 0xFF, green: 0xEF, blue: 0xD5},
+ 'peachpuff': {red: 0xFF, green: 0xDA, blue: 0xB9},
+ 'peru': {red: 0xCD, green: 0x85, blue: 0x3F},
+ 'pink': {red: 0xFF, green: 0xC0, blue: 0xCB},
+ 'plum': {red: 0xDD, green: 0xA0, blue: 0xDD},
+ 'powderblue': {red: 0xB0, green: 0xE0, blue: 0xE6},
+ 'purple': {red: 0x80, green: 0x00, blue: 0x80},
+ 'red': {red: 0xFF, green: 0x00, blue: 0x00},
+ 'rosybrown': {red: 0xBC, green: 0x8F, blue: 0x8F},
+ 'royalblue': {red: 0x41, green: 0x69, blue: 0xE1},
+ 'saddlebrown': {red: 0x8B, green: 0x45, blue: 0x13},
+ 'salmon': {red: 0xFA, green: 0x80, blue: 0x72},
+ 'sandybrown': {red: 0xF4, green: 0xA4, blue: 0x60},
+ 'seagreen': {red: 0x2E, green: 0x8B, blue: 0x57},
+ 'seashell': {red: 0xFF, green: 0xF5, blue: 0xEE},
+ 'sienna': {red: 0xA0, green: 0x52, blue: 0x2D},
+ 'silver': {red: 0xC0, green: 0xC0, blue: 0xC0},
+ 'skyblue': {red: 0x87, green: 0xCE, blue: 0xEB},
+ 'slateblue': {red: 0x6A, green: 0x5A, blue: 0xCD},
+ 'slategray': {red: 0x70, green: 0x80, blue: 0x90},
+ 'slategrey': {red: 0x70, green: 0x80, blue: 0x90},
+ 'snow': {red: 0xFF, green: 0xFA, blue: 0xFA},
+ 'springgreen': {red: 0x00, green: 0xFF, blue: 0x7F},
+ 'steelblue': {red: 0x46, green: 0x82, blue: 0xB4},
+ 'tan': {red: 0xD2, green: 0xB4, blue: 0x8C},
+ 'teal': {red: 0x00, green: 0x80, blue: 0x80},
+ 'thistle': {red: 0xD8, green: 0xBF, blue: 0xD8},
+ 'tomato': {red: 0xFF, green: 0x63, blue: 0x47},
+ 'turquoise': {red: 0x40, green: 0xE0, blue: 0xD0},
+ 'violet': {red: 0xEE, green: 0x82, blue: 0xEE},
+ 'wheat': {red: 0xF5, green: 0xDE, blue: 0xB3},
+ 'white': {red: 0xFF, green: 0xFF, blue: 0xFF},
+ 'whitesmoke': {red: 0xF5, green: 0xF5, blue: 0xF5},
+ 'yellow': {red: 0xFF, green: 0xFF, blue: 0x00},
+ 'yellowgreen': {red: 0x9A, green: 0xCD, blue: 0x32},
+
+ 'transparent': {red: 0x00, green: 0x00, blue: 0x00, alpha: 0.0}
+};
+
+/**
+ * Color class allows cross-browser comparison of values, which can
+ * be returned from queryCommandValue in several formats:
+ * #ff00ff
+ * #f0f
+ * rgb(255, 0, 0)
+ * rgb(100%, 0%, 28%) // disabled for the time being (see below)
+ * rgba(127, 0, 64, 0.25)
+ * rgba(50%, 0%, 10%, 0.65) // disabled for the time being (see below)
+ * palegoldenrod
+ * transparent
+ *
+ * @constructor
+ * @param value {String} original value
+ */
+function Color(value) {
+ this.compare = function(other) {
+ if (!this.valid || !other.valid) {
+ return false;
+ }
+ if (this.alpha != other.alpha) {
+ return false;
+ }
+ if (this.alpha == 0.0) {
+ // both are fully transparent -> ignore the specific color information
+ return true;
+ }
+ // TODO(rolandsteiner): handle hsl/hsla values
+ return this.red == other.red && this.green == other.green && this.blue == other.blue;
+ }
+ this.parse = function(value) {
+ if (!value)
+ return false;
+ value = String(value).toLowerCase();
+ var match;
+ // '#' + 6 hex digits, e.g., #ff3300
+ match = value.match(/#([0-9a-f]{6})/i);
+ if (match) {
+ this.red = parseInt(match[1].substring(0, 2), 16);
+ this.green = parseInt(match[1].substring(2, 4), 16);
+ this.blue = parseInt(match[1].substring(4, 6), 16);
+ this.alpha = 1.0;
+ return true;
+ }
+ // '#' + 3 hex digits, e.g., #f30
+ match = value.match(/#([0-9a-f]{3})/i);
+ if (match) {
+ this.red = parseInt(match[1].substring(0, 1), 16) * 16;
+ this.green = parseInt(match[1].substring(1, 2), 16) * 16;
+ this.blue = parseInt(match[1].substring(2, 3), 16) * 16;
+ this.alpha = 1.0;
+ return true;
+ }
+ // a color name, e.g., springgreen
+ match = colorChart[value];
+ if (match) {
+ this.red = match.red;
+ this.green = match.green;
+ this.blue = match.blue;
+ this.alpha = (match.alpha === undefined) ? 1.0 : match.alpha;
+ return true;
+ }
+ // rgb(r, g, b), e.g., rgb(128, 12, 217)
+ match = value.match(/rgb\(([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/i);
+ if (match) {
+ this.red = Number(match[1]);
+ this.green = Number(match[2]);
+ this.blue = Number(match[3]);
+ this.alpha = 1.0;
+ return true;
+ }
+ // rgb(r%, g%, b%), e.g., rgb(100%, 0%, 50%)
+// Commented out for the time being, since it seems likely that the resulting
+// decimal values will create false negatives when compared with non-% values.
+//
+// => store as separate percent values and do exact matching when compared with % values
+// and fuzzy matching when compared with non-% values?
+//
+// match = value.match(/rgb\(([0-9]{0,3}(?:\.[0-9]+)?)%\s*,\s*([0-9]{0,3}(?:\.[0-9]+)?)%\s*,\s*([0-9]{0,3}(?:\.[0-9]+)?)%\s*\)/i);
+// if (match) {
+// this.red = Number(match[1]) * 255 / 100;
+// this.green = Number(match[2]) * 255 / 100;
+// this.blue = Number(match[3]) * 255 / 100;
+// this.alpha = 1.0;
+// return true;
+// }
+ // rgba(r, g, b, a), e.g., rgb(128, 12, 217, 0.2)
+ match = value.match(/rgba\(([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/i);
+ if (match) {
+ this.red = Number(match[1]);
+ this.green = Number(match[2]);
+ this.blue = Number(match[3]);
+ this.alpha = Number(match[4]);
+ return true;
+ }
+ // rgba(r%, g%, b%, a), e.g., rgb(100%, 0%, 50%, 0.3)
+// Commented out for the time being (cf. rgb() matching above)
+// match = value.match(/rgba\(([0-9]{0,3}(?:\.[0-9]+)?)%\s*,\s*([0-9]{0,3}(?:\.[0-9]+)?)%\s*,\s*([0-9]{0,3}(?:\.[0-9]+)?)%,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/i);
+// if (match) {
+// this.red = Number(match[1]) * 255 / 100;
+// this.green = Number(match[2]) * 255 / 100;
+// this.blue = Number(match[3]) * 255 / 100;
+// this.alpha = Number(match[4]);
+// return true;
+// }
+ // TODO(rolandsteiner): handle "hsl(h, s, l)" and "hsla(h, s, l, a)" notation
+ return false;
+ }
+ this.toString = function() {
+ return this.valid ? this.red + ',' + this.green + ',' + this.blue : '(invalid)';
+ }
+ this.toHexString = function() {
+ if (!this.valid)
+ return '(invalid)';
+ return ((this.red < 16) ? '0' : '') + this.red.toString(16) +
+ ((this.green < 16) ? '0' : '') + this.green.toString(16) +
+ ((this.blue < 16) ? '0' : '') + this.blue.toString(16);
+ }
+ this.valid = this.parse(value);
+}
+
+/**
+ * Utility class for converting font sizes to the size
+ * attribute in a font tag. Currently only converts px because
+ * only the sizes and px ever come from queryCommandValue.
+ *
+ * @constructor
+ * @param value {String} original value
+ */
+function FontSize(value) {
+ this.parse = function(str) {
+ if (!str)
+ this.valid = false;
+ var match;
+ if (match = String(str).match(/([0-9]+)px/)) {
+ var px = Number(match[1]);
+ if (px <= 0 || px > 47)
+ return false;
+ if (px <= 10) {
+ this.size = '1';
+ } else if (px <= 13) {
+ this.size = '2';
+ } else if (px <= 16) {
+ this.size = '3';
+ } else if (px <= 18) {
+ this.size = '4';
+ } else if (px <= 24) {
+ this.size = '5';
+ } else if (px <= 32) {
+ this.size = '6';
+ } else {
+ this.size = '7';
+ }
+ return true;
+ }
+ if (match = String(str).match(/([+-][0-9]+)/)) {
+ this.size = match[1];
+ return this.size >= 1 && this.size <= 7;
+ }
+ if (Number(str)) {
+ this.size = String(Number(str));
+ return this.size >= 1 && this.size <= 7;
+ }
+ switch (str) {
+ case 'x-small':
+ this.size = '1';
+ return true;
+ case 'small':
+ this.size = '2';
+ return true;
+ case 'medium':
+ this.size = '3';
+ return true;
+ case 'large':
+ this.size = '4';
+ return true;
+ case 'x-large':
+ this.size = '5';
+ return true;
+ case 'xx-large':
+ this.size = '6';
+ return true;
+ case 'xxx-large':
+ this.size = '7';
+ return true;
+ case '-webkit-xxx-large':
+ this.size = '7';
+ return true;
+ case 'larger':
+ this.size = '+1';
+ return true;
+ case 'smaller':
+ this.size = '-1';
+ return true;
+ }
+ return false;
+ }
+ this.compare = function(other) {
+ return this.valid && other.valid && this.size === other.size;
+ }
+ this.toString = function() {
+ return this.valid ? this.size : '(invalid)';
+ }
+ this.valid = this.parse(value);
+}
+
+/**
+ * Utility class for converting & canonicalizing font names.
+ *
+ * @constructor
+ * @param value {String} original value
+ */
+function FontName(value) {
+ this.parse = function(str) {
+ if (!str)
+ return false;
+ str = String(str).toLowerCase();
+ switch (str) {
+ case 'arial new':
+ this.fontname = 'arial';
+ return true;
+ case 'courier new':
+ this.fontname = 'courier';
+ return true;
+ case 'times new':
+ case 'times roman':
+ case 'times new roman':
+ this.fontname = 'times';
+ return true;
+ }
+ this.fontname = value;
+ return true;
+ }
+ this.compare = function(other) {
+ return this.valid && other.valid && this.fontname === other.fontname;
+ }
+ this.toString = function() {
+ return this.valid ? this.fontname : '(invalid)';
+ }
+ this.valid = this.parse(value);
+}
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/variables.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/variables.js
new file mode 100644
index 000000000..cdc6f1e92
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/variables.js
@@ -0,0 +1,227 @@
+/**
+ * @fileoverview
+ * Common constants and variables used in the RTE test suite.
+ *
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License')
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @version 0.1
+ * @author rolandsteiner@google.com
+ */
+
+// Constant for indicating a test setup is unsupported or incorrect
+// (threw exception).
+var INTERNAL_ERR = 'INTERNAL ERROR: ';
+var SETUP_EXCEPTION = 'SETUP EXCEPTION: ';
+var EXECUTION_EXCEPTION = 'EXECUTION EXCEPTION: ';
+var VERIFICATION_EXCEPTION = 'VERIFICATION EXCEPTION: ';
+
+var SETUP_CONTAINER = 'WHEN INITIALIZING TEST CONTAINER';
+var SETUP_BAD_SELECTION_SPEC = 'BAD SELECTION SPECIFICATION IN TEST OR EXPECTATION STRING';
+var SETUP_HTML = 'WHEN SETTING TEST HTML';
+var SETUP_SELECTION = 'WHEN SETTING SELECTION';
+var SETUP_NOCOMMAND = 'NO COMMAND, GENERAL FUNCTION OR QUERY FUNCTION GIVEN';
+var HTML_COMPARISON = 'WHEN COMPARING OUTPUT HTML';
+
+// Exceptiona to be thrown on unsupported selection operations
+var SELMODIFY_UNSUPPORTED = 'UNSUPPORTED selection.modify()';
+var SELALLCHILDREN_UNSUPPORTED = 'UNSUPPORTED selection.selectAllChildren()';
+
+// Output string for unsupported functions
+// (returning bool 'false' as opposed to throwing an exception)
+var UNSUPPORTED = '<i>false</i> (UNSUPPORTED)';
+
+// HTML comparison result contants.
+var VALRESULT_NOT_RUN = 0; // test hasn't been run yet
+var VALRESULT_SETUP_EXCEPTION = 1;
+var VALRESULT_EXECUTION_EXCEPTION = 2;
+var VALRESULT_VERIFICATION_EXCEPTION = 3;
+var VALRESULT_UNSUPPORTED = 4;
+var VALRESULT_CANARY = 5; // HTML changes bled into the canary.
+var VALRESULT_DIFF = 6;
+var VALRESULT_ACCEPT = 7; // HTML technically correct, but not ideal.
+var VALRESULT_EQUAL = 8;
+
+var VALOUTPUT = [ // IMPORTANT: this array MUST be coordinated with the values above!!
+ {css: 'grey', output: '???', title: 'The test has not been run yet.'}, // VALRESULT_NOT_RUN
+ {css: 'exception', output: 'EXC.', title: 'Exception was thrown during setup.'}, // VALRESULT_SETUP_EXCEPTION
+ {css: 'exception', output: 'EXC.', title: 'Exception was thrown during execution.'}, // VALRESULT_EXECUTION_EXCEPTION
+ {css: 'exception', output: 'EXC.', title: 'Exception was thrown during result verification.'}, // VALRESULT_VERIFICATION_EXCEPTION
+ {css: 'unsupported', output: 'UNS.', title: 'Unsupported command or value'}, // VALRESULT_UNSUPPORTED
+ {css: 'canary', output: 'CANARY', title: 'The command affected the contentEditable root element, or outside HTML.'}, // VALRESULT_CANARY
+ {css: 'fail', output: 'FAIL', title: 'The result differs from the expectation(s).'}, // VALRESULT_DIFF
+ {css: 'accept', output: 'ACC.', title: 'The result is technically correct, but sub-optimal.'}, // VALRESULT_ACCEPT
+ {css: 'pass', output: 'PASS', title: 'The test result matches the expectation.'} // VALRESULT_EQUAL
+]
+
+// Selection comparison result contants.
+var SELRESULT_NOT_RUN = 0; // test hasn't been run yet
+var SELRESULT_CANARY = 1; // selection escapes the contentEditable element
+var SELRESULT_DIFF = 2;
+var SELRESULT_NA = 3;
+var SELRESULT_ACCEPT = 4; // Selection is acceptable, but not ideal.
+var SELRESULT_EQUAL = 5;
+
+var SELOUTPUT = [ // IMPORTANT: this array MUST be coordinated with the values above!!
+ {css: 'grey', output: 'grey', title: 'The test has not been run yet.'}, // SELRESULT_NOT_RUN
+ {css: 'canary', output: 'CANARY', title: 'The selection escaped the contentEditable boundary!'}, // SELRESULT_CANARY
+ {css: 'fail', output: 'FAIL', title: 'The selection differs from the expectation(s).'}, // SELRESULT_DIFF
+ {css: 'na', output: 'N/A', title: 'The correctness of the selection could not be verified.'}, // SELRESULT_NA
+ {css: 'accept', output: 'ACC.', title: 'The selection is technically correct, but sub-optimal.'}, // SELRESULT_ACCEPT
+ {css: 'pass', output: 'PASS', title: 'The selection matches the expectation.'} // SELRESULT_EQUAL
+];
+
+// RegExp for selection markers
+var SELECTION_MARKERS = /[\[\]\{\}\|\^]/;
+
+// Special attributes used to mark selections within elements that otherwise
+// have no children. Important: attribute name MUST be lower case!
+var ATTRNAME_SEL_START = 'bsselstart';
+var ATTRNAME_SEL_END = 'bsselend';
+
+// DOM node type constants.
+var DOM_NODE_TYPE_ELEMENT = 1;
+var DOM_NODE_TYPE_TEXT = 3;
+var DOM_NODE_TYPE_COMMENT = 8;
+
+// Test parameter names
+var PARAM_DESCRIPTION = 'desc';
+var PARAM_PAD = 'pad';
+var PARAM_EXECCOMMAND = 'command';
+var PARAM_FUNCTION = 'function';
+var PARAM_QUERYCOMMANDSUPPORTED = 'qcsupported';
+var PARAM_QUERYCOMMANDENABLED = 'qcenabled';
+var PARAM_QUERYCOMMANDINDETERM = 'qcindeterm';
+var PARAM_QUERYCOMMANDSTATE = 'qcstate';
+var PARAM_QUERYCOMMANDVALUE = 'qcvalue';
+var PARAM_VALUE = 'value';
+var PARAM_EXPECTED = 'expected';
+var PARAM_EXPECTED_OUTER = 'expOuter';
+var PARAM_ACCEPT = 'accept';
+var PARAM_ACCEPT_OUTER = 'accOuter';
+var PARAM_CHECK_ATTRIBUTES = 'checkAttrs';
+var PARAM_CHECK_STYLE = 'checkStyle';
+var PARAM_CHECK_CLASS = 'checkClass';
+var PARAM_CHECK_ID = 'checkID';
+var PARAM_STYLE_WITH_CSS = 'styleWithCSS';
+
+// ID suffixes for the output columns
+var IDOUT_TR = '_:TR:'; // per container
+var IDOUT_TESTID = '_:tid'; // per test
+var IDOUT_COMMAND = '_:cmd'; // per test
+var IDOUT_VALUE = '_:val'; // per test
+var IDOUT_CHECKATTRS = '_:att'; // per test
+var IDOUT_CHECKSTYLE = '_:sty'; // per test
+var IDOUT_CONTAINER = '_:cnt:'; // per container
+var IDOUT_STATUSVAL = '_:sta:'; // per container
+var IDOUT_STATUSSEL = '_:sel:'; // per container
+var IDOUT_PAD = '_:pad'; // per test
+var IDOUT_EXPECTED = '_:exp'; // per test
+var IDOUT_ACTUAL = '_:act:'; // per container
+
+// Output strings to use for yes/no/NA
+var OUTSTR_YES = '&#x25CF;';
+var OUTSTR_NO = '&#x25CB;';
+var OUTSTR_NA = '-';
+
+// Tags at the start of HTML strings where they were taken from
+var HTMLTAG_BODY = 'B:';
+var HTMLTAG_OUTER = 'O:';
+var HTMLTAG_INNER = 'I:';
+
+// What to use for the canary
+var CANARY = 'CAN<br>ARY';
+
+// Containers for tests, and their associated DOM elements:
+// iframe, win, doc, body, elem
+var containers = [
+ { id: 'dM',
+ iframe: null,
+ win: null,
+ doc: null,
+ body: null,
+ editor: null,
+ tagOpen: '<body>',
+ tagClose: '</body>',
+ editorID: null,
+ canary: '',
+ },
+ { id: 'body',
+ iframe: null,
+ win: null,
+ doc: null,
+ body: null,
+ editor: null,
+ tagOpen: '<body contenteditable="true">',
+ tagClose: '</body>',
+ editorID: null,
+ canary: ''
+ },
+ { id: 'div',
+ iframe: null,
+ win: null,
+ doc: null,
+ body: null,
+ editor: null,
+ tagOpen: '<div contenteditable="true" id="editor-div">',
+ tagClose: '</div>',
+ editorID: 'editor-div',
+ canary: CANARY
+ }
+];
+
+// Helper variables to use in test functions
+var win = null; // window object to use for test functions
+var doc = null; // document object to use for test functions
+var body = null; // The <body> element of the current document
+var editor = null; // The contentEditable element (i.e., the <body> or <div>)
+var sel = null; // The current selection after the pad is set up
+
+// Canonicalization emit flags for various purposes
+var emitFlagsForCanary = {
+ emitAttrs: true,
+ emitStyle: true,
+ emitClass: true,
+ emitID: true,
+ lowercase: true,
+ canonicalizeUnits: true
+};
+var emitFlagsForOutput = {
+ emitAttrs: true,
+ emitStyle: true,
+ emitClass: true,
+ emitID: true,
+ lowercase: false,
+ canonicalizeUnits: false
+};
+
+// Shades of output colors
+var colorShades = ['Lo', 'Hi'];
+
+// Classes of tests
+var testClassIDs = ['Finalized', 'RFC', 'Proposed'];
+var testClassCount = testClassIDs.length;
+
+// Dictionary storing the detailed test results.
+var results = {
+ count: 0,
+ score: 0
+};
+
+// Results - populated by the fillResults() function.
+var beacon = [];
+
+// "compatibility" between Python and JS for test quines
+var True = true;
+var False = false;
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/templates/output.html b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/templates/output.html
new file mode 100644
index 000000000..62d917d69
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/templates/output.html
@@ -0,0 +1,138 @@
+<!-- Legend -->
+<TABLE CLASS="legend framed">
+ <THEAD>
+ <TR><TH COLSPAN=3 CLASS="legendHdr">Result Description</TH></TR>
+ <TR><TH>Status</TH><TH ALIGN="LEFT">Meaning</TH><TH ALIGN="LEFT">Explanation</TH><TH>Scoring</TH></TR>
+ </THEAD>
+ <TBODY>
+ <TR CLASS="lo"><TD CLASS="pass" ALIGN="CENTER">&nbsp;PASS&nbsp;</TD><TD CLASS="legend" ROWSPAN=2>Passed</TD><TD CLASS="legend" ROWSPAN=2>The result matches the expectation.</TD><TD ROWSPAN=2 ALIGN="CENTER" CLASS="pass">PASS (+1)</TD></TR>
+ <TR CLASS="hi"><TD CLASS="pass" ALIGN="CENTER">&nbsp;PASS&nbsp;</TD></TR>
+ <TR CLASS="lo"><TD CLASS="accept" ALIGN="CENTER">&nbsp;ACC.&nbsp;</TD><TD CLASS="legend" ROWSPAN=2>Acceptable</TD><TD CLASS="legend" ROWSPAN=2>The result is technically correct, but not ideal (too verbose, deprecated usage, etc.) - for informative purposes only.</TD><TD ROWSPAN=2 ALIGN="CENTER" CLASS="fail">FAIL (+0)</TD></TR>
+ <TR CLASS="hi"><TD CLASS="accept" ALIGN="CENTER">&nbsp;ACC.&nbsp;</TD></TR>
+ <TR CLASS="lo"><TD CLASS="fail" ALIGN="CENTER">&nbsp;FAIL&nbsp;</TD><TD CLASS="legend" ROWSPAN=2>Failure</TD><TD CLASS="legend" ROWSPAN=2>The result does not match any given expectation.</TD><TD ROWSPAN=2 ALIGN="CENTER" CLASS="fail">FAIL (+0)</TD></TR>
+ <TR CLASS="hi"><TD CLASS="fail" ALIGN="CENTER">&nbsp;FAIL&nbsp;</TD></TR>
+ <TR CLASS="lo"><TD CLASS="canary" ALIGN="CENTER">&nbsp;CANARY&nbsp;</TD><TD CLASS="legend" ROWSPAN=2>Canary</TD><TD CLASS="legend" ROWSPAN=2>The result changes HTML other than children of the contentEditable element.</TD><TD ROWSPAN=2 ALIGN="CENTER" CLASS="fail">FAIL (+0)</TD></TR>
+ <TR CLASS="hi"><TD CLASS="canary" ALIGN="CENTER">&nbsp;CANARY&nbsp;</TD></TR>
+ <TR CLASS="lo"><TD CLASS="unsupported" ALIGN="CENTER">&nbsp;UNS.&nbsp;</TD><TD CLASS="legend" ROWSPAN=2>Unsupported</TD><TD CLASS="legend" ROWSPAN=2>The specific function or value is unsupported (returned boolean 'false').</TD><TD ROWSPAN=2 ALIGN="CENTER" CLASS="fail">FAIL (+0)</TD></TR>
+ <TR CLASS="hi"><TD CLASS="unsupported" ALIGN="CENTER">&nbsp;UNS.&nbsp;</TD></TR>
+ <TR CLASS="lo"><TD CLASS="exception" ALIGN="CENTER">&nbsp;EXC.&nbsp;</TD><TD CLASS="legend" ROWSPAN=2>Exception</TD><TD CLASS="legend" ROWSPAN=2>An unexpected exception was thrown during the execution of the test.</TD><TD ROWSPAN=2 ALIGN="CENTER" CLASS="fail">FAIL (+0)</TD></TR>
+ <TR CLASS="hi"><TD CLASS="exception" ALIGN="CENTER">&nbsp;EXC.&nbsp;</TD></TR>
+ <TR CLASS="lo"><TD CLASS="na" ALIGN="CENTER">&nbsp;N/A&nbsp;</TD><TD CLASS="legend" ROWSPAN=2>Not Applicable</TD><TD CLASS="legend" ROWSPAN=2>The selection could not be tested, because the tested function failed to return a known result.</TD><TD ROWSPAN=2 ALIGN="CENTER" CLASS="fail">FAIL (+0)</TD></TR>
+ <TR CLASS="hi"><TD CLASS="na" ALIGN="CENTER">&nbsp;N/A&nbsp;</TD></TR>
+ </TBODY>
+</TABLE>
+<TABLE CLASS="legend framed">
+ <THEAD>
+ <TR><TH COLSPAN=2 CLASS="legendHdr">Selection and Result Display</TH></TR>
+ <TR><TH>Character</TH><TH ALIGN="LEFT">Explanation</TH></TR>
+ </THEAD>
+ <TBODY>
+ <TR><TD CLASS="sel" ALIGN="CENTER">[</TD><TD>Start of selection - selection point is within a text node.</TD></TR>
+ <TR><TD CLASS="sel" ALIGN="CENTER">]</TD><TD>End of selection - selection point is within a text node.</TD></TR>
+ <TR><TD CLASS="sel" ALIGN="CENTER">^</TD><TD>Collapsed selection - selection point is within a text node.</TD></TR>
+ <TR><TD COLSPAN=2>&nbsp;</TD></TR>
+ <TR><TD CLASS="sel" ALIGN="CENTER">{</TD><TD>Start of selection - selection point is within an element node.</TD></TR>
+ <TR><TD CLASS="sel" ALIGN="CENTER">}</TD><TD>End of selection - selection point is within an element node.</TD></TR>
+ <TR><TD CLASS="sel" ALIGN="CENTER">|</TD><TD>Collapsed selection - selection point is within an element node.</TD></TR>
+ <TR><TD COLSPAN=2>&nbsp;</TD></TR>
+ <TR><TD ALIGN="CENTER"><SPAN CLASS="fade">foo</SPAN></TD><TD>Greyed text indicates parts of the output that are ignored for the purposes of checking the result.</TD></TR>
+ <TR><TD ALIGN="CENTER"><SPAN CLASS="txt">foo</SPAN></TD><TD>Grey border indicates extent of text nodes in the result.</TD></TR>
+ </TBODY>
+</TABLE>
+<!-- progress meter -->
+<HR ID="divider">
+<H1>Running Test Suites: {% for s in suites %}<A HREF="#{{ s.id }}" ID="{{ s.id }}-progress" STYLE="color: #eeeeee">{{ s.id }}</A> {% endfor %}<SPAN ID="done">&nbsp;</SPAN></H1>
+<HR>
+<!-- main output -->
+{% for s in suites %}
+ <H1 ID="{{ s.id }}"><A NAME="{{ s.id }}" HREF="#{{ s.id }}">{{ s.id }}</A> - {{ s.caption }}:
+ <SPAN ID="{{ s.id }}-{% ifequal s.id.0 'S' %}sel{% endifequal %}score">?/?</SPAN>
+ {% ifnotequal s.id.0 "Q" %}{% ifnotequal s.id.0 "S" %}
+ (Selection: <SPAN ID="{{ s.id }}-selscore">?/?</SPAN>)
+ {% endifnotequal %}{% endifnotequal %}
+ (time: <SPAN ID="{{ s.id }}-time">?</SPAN>&nbsp;ms)
+ </H1>
+ {% if s.comment %}
+ <DIV CLASS="comment">{{ s.comment|safe }}</DIV>
+ {% endif %}
+ {% for cls in classes %}{% for pk, pv in s.items %}{% ifequal pk cls %}
+ <H2 ID="{{ s.id }}-{{ cls }}"><A NAME="{{ s.id }}-{{ cls }}" HREF="#{{ s.id }}-{{ cls }}">{{ cls }} Tests</A>:
+ <SPAN ID="{{ s.id }}-{{ cls }}-{% ifequal s.id.0 'S' %}sel{% endifequal %}score">?/?</SPAN>
+ {% ifnotequal s.id.0 "Q" %}{% ifnotequal s.id.0 "S" %}
+ (Selection: <SPAN ID="{{ s.id }}-{{ cls }}-selscore">?/?</SPAN>)
+ {% endifnotequal %}{% endifnotequal %}
+ </H2>
+ <TABLE WIDTH=100%>
+ <THEAD>
+ <TR>
+ <TH TITLE="Unique ID of the test" ALIGN="LEFT">ID</TH>
+ <TH TITLE="Command or function used in the test" ALIGN="LEFT">Command</TH>
+ <TH TITLE="Value field for commands" ALIGN="LEFT">Value</TH>
+ {% ifnotequal s.id.0 "S" %}{% ifnotequal s.id.0 "Q" %}{% comment %} Don't output attribute and style columns for selection and "queryCommand..." tests. {% endcomment %}
+ <TH TITLE="check Atributes?">A</TH>
+ <TH TITLE="check Style">S</TH>
+ {% endifnotequal %}{% endifnotequal %}
+ <TH TITLE="Testing HTML Element">Env.</TH>
+ {% ifnotequal s.id.0 "S" %}{% comment %} Don't output HTML status column for selection tests. {% endcomment %}
+ <TH TITLE="State of the test">Status</TH>
+ {% endifnotequal %}
+ {% ifnotequal s.id.0 "Q" %}{% comment %} Don't output selection result column for "queryCommand..." tests. {% endcomment %}
+ <TH TITLE="State of the test regarding the selection">Selection</TH>
+ {% endifnotequal %}
+ <TH TITLE="Initial HTML and selection" ALIGN="LEFT">Initial</TH>
+ <TH TITLE="Expected HTML and selection" ALIGN="LEFT">Expected</TH>
+ <TH TITLE="Actual result HTML and selection" ALIGN="LEFT">Actual (lower case, canonicalized, selection marks)</TH>
+ <TH TITLE="Short description of the test" ALIGN="LEFT">Description</TH>
+ </TR>
+ </THEAD>
+ <TBODY>
+ {% for g in pv %}{% for t in g.tests %}
+ <TR ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:TR:dM" CLASS="{% cycle 'lo' 'lo' 'lo' 'hi' 'hi' 'hi' as shade %}">
+ <TD ROWSPAN=3 ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:tid"><A CLASS="idLabel" NAME="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}" HREF="#{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}">{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}</A></TD>
+ <TD ROWSPAN=3 ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:cmd">&nbsp;</TD>
+ <TD ROWSPAN=3 ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:val">&nbsp;</TD>
+ {% ifnotequal s.id.0 "S" %}{% ifnotequal s.id.0 "Q" %}{% comment %} Don't output attribute and style columns for selection and "queryCommand..." tests. {% endcomment %}
+ <TD ROWSPAN=3 ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:att" ALIGN="CENTER">&nbsp;</TD>
+ <TD ROWSPAN=3 ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:sty" ALIGN="CENTER">&nbsp;</TD>
+ {% endifnotequal %}{% endifnotequal %}
+ <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:cnt:dM" TITLE="designMode=&quot;on&quot;" ALIGN="CENTER">dM</TD>
+ {% ifnotequal s.id.0 "S" %}
+ <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:sta:dM" ALIGN="CENTER">NONE</TD>
+ {% endifnotequal %}
+ {% ifnotequal s.id.0 "Q" %}
+ <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:sel:dM" ALIGN="CENTER">NONE</TD>
+ {% endifnotequal %}
+ <TD ROWSPAN=3 ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:pad">&nbsp;</TD>
+ <TD ROWSPAN=3 ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:exp">&nbsp;</TD>
+ <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:act:dM"><I>Processing...</I></TD>
+ <TD ROWSPAN=3>{{ t.desc|default:"&nbsp;" }}</TD>
+ </TR>
+ <TR ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:TR:body" CLASS="{% cycle shade %}">
+ <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:cnt:body" TITLE="&lt;body contentEditable=&quot;true&quot;&gt;" ALIGN="CENTER">body</TD>
+ {% ifnotequal s.id.0 "S" %}
+ <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:sta:body" ALIGN="CENTER">NONE</TD>
+ {% endifnotequal %}
+ {% ifnotequal s.id.0 "Q" %}
+ <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:sel:body" ALIGN="CENTER">NONE</TD>
+ {% endifnotequal %}
+ <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:act:body"><I>Processing...</I></TD>
+ </TR>
+ <TR ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:TR:div" CLASS="{% cycle shade %}">
+ <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:cnt:div" TITLE="&lt;div contentEditable=&quot;true&quot;&gt;" ALIGN="CENTER">div</TD>
+ {% ifnotequal s.id.0 "S" %}
+ <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:sta:div" ALIGN="CENTER">NONE</TD>
+ {% endifnotequal %}
+ {% ifnotequal s.id.0 "Q" %}
+ <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:sel:div" ALIGN="CENTER">NONE</TD>
+ {% endifnotequal %}
+ <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:act:div"><I>Processing...</I></TD>
+ </TR>
+ {% endfor %}{% endfor %}
+ </TBODY>
+ </TABLE>
+ {% endifequal %}{% endfor %}{% endfor %}
+{% endfor %}
+
+
+
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/templates/richtext2.html b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/templates/richtext2.html
new file mode 100644
index 000000000..98de8796d
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/templates/richtext2.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+
+ <title>New Rich Text Tests</title>
+
+ <link rel="stylesheet" href="static/common.css" type="text/css">
+ <link rel="stylesheet" href="static/editable.css" type="text/css">
+
+ <!-- utility scripts -->
+ <script src="static/js/variables.js"></script>
+
+ <script src="static/js/canonicalize.js"></script>
+ <script src="static/js/compare.js"></script>
+ <script src="static/js/output.js"></script>
+ <script src="static/js/pad.js"></script>
+ <script src="static/js/range.js"></script>
+ <script src="static/js/units.js"></script>
+
+ <script src="static/js/run.js"></script>
+
+ <!-- new tests -->
+ <script type="text/javascript">
+ {% autoescape off %}
+
+ var commonIDPrefix = '{{ commonIDPrefix }}';
+ {% for s in suites %}
+ var {{ s.id }}_TESTS = {{ s }};
+ {% endfor %}
+
+ /**
+ * Stuff to do after all tests are run:
+ * - write a nice "DONE!" at the end of the progress meter
+ * - beacon the results
+ * - remove the testing <iframe>s
+ */
+ function finish() {
+ var span = document.getElementById('done');
+ if (span)
+ span.innerHTML = ' ... DONE!';
+
+ fillResults();
+ parent.sendScore(beacon, categoryTotals);
+
+ cleanUp();
+ }
+
+ /**
+ * Run every individual suite, with a a brief timeout in between
+ * to allow for screen updates.
+ */
+{% for s in suites %}
+ {% if not forloop.first %}
+ setTimeout("runSuite{{ s.id }}()", 100);
+ }
+ {% endif %}
+
+ function runSuite{{ s.id }}() {
+ runAndOutputTestSuite({{ s.id }}_TESTS);
+{% endfor %}
+ finish();
+ }
+
+ /**
+ * Runs all tests in all suites.
+ */
+ function doRunTests() {
+ initVariables();
+ initEditorDocs();
+
+ // Start with the first test suite
+ runSuite{{ suites.0.id }}();
+ }
+
+ /**
+ * Runs after allowing for some time to have everything loaded
+ * (aka. horrible IE9 kludge)
+ */
+ function runTests() {
+ setTimeout("doRunTests()", 1500);
+ }
+
+ /**
+ * Removes the <iframe>s after all tests are finished
+ */
+ function cleanUp() {
+ var e = document.getElementById('iframe-dM');
+ e.parentNode.removeChild(e);
+ e = document.getElementById('iframe-body');
+ e.parentNode.removeChild(e);
+ e = document.getElementById('iframe-div');
+ e.parentNode.removeChild(e);
+ }
+ {% endautoescape %}
+ </script>
+</head>
+
+<body onload="runTests()">
+ {% include "richtext2/templates/output.html" %}
+ <hr>
+ <iframe name="iframe-dM" id="iframe-dM" src="static/editable-dM.html"></iframe>
+ <iframe name="iframe-body" id="iframe-body" src="static/editable-body.html"></iframe>
+ <iframe name="iframe-div" id="iframe-div" src="static/editable-div.html"></iframe>
+</body>
+</html>
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/__init__.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/__init__.py
new file mode 100644
index 000000000..a1f5279ad
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/__init__.py
@@ -0,0 +1,17 @@
+__all__ = [
+ 'apply',
+ 'applyCSS',
+ 'change',
+ 'changeCSS',
+ 'delete',
+ 'forwarddelete',
+ 'insert',
+ 'queryEnabled',
+ 'queryIndeterm',
+ 'queryState',
+ 'querySupported',
+ 'queryValue',
+ 'selection',
+ 'unapply',
+ 'unapplyCSS'
+] \ No newline at end of file
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/apply.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/apply.py
new file mode 100644
index 000000000..3eb465c84
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/apply.py
@@ -0,0 +1,364 @@
+
+APPLY_TESTS = {
+ 'id': 'A',
+ 'caption': 'Apply Formatting Tests',
+ 'checkAttrs': True,
+ 'checkStyle': True,
+ 'styleWithCSS': False,
+
+ 'Proposed': [
+ { 'desc': '',
+ 'command': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': '[HTML5] bold',
+ 'command': 'bold',
+ 'tests': [
+ { 'id': 'B_TEXT-1_SI',
+ 'rte1-id': 'a-bold-0',
+ 'desc': 'Bold selection',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<b>[bar]</b>baz',
+ 'foo<strong>[bar]</strong>baz' ] },
+
+ { 'id': 'B_TEXT-1_SIR',
+ 'desc': 'Bold reversed selection',
+ 'pad': 'foo]bar[baz',
+ 'expected': [ 'foo<b>[bar]</b>baz',
+ 'foo<strong>[bar]</strong>baz' ] },
+
+ { 'id': 'B_I-1_SL',
+ 'desc': 'Bold selection, partially including italic',
+ 'pad': 'foo[bar<i>baz]qoz</i>quz',
+ 'expected': [ 'foo<b>[bar</b><i><b>baz]</b>qoz</i>quz',
+ 'foo<b>[bar<i>baz]</i></b><i>qoz</i>quz',
+ 'foo<strong>[bar</strong><i><strong>baz]</strong>qoz</i>quz',
+ 'foo<strong>[bar<i>baz]</i></strong><i>qoz</i>quz' ] }
+ ]
+ },
+
+ { 'desc': '[HTML5] italic',
+ 'command': 'italic',
+ 'tests': [
+ { 'id': 'I_TEXT-1_SI',
+ 'rte1-id': 'a-italic-0',
+ 'desc': 'Italicize selection',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<i>[bar]</i>baz',
+ 'foo<em>[bar]</em>baz' ] }
+ ]
+ },
+
+ { 'desc': '[HTML5] underline',
+ 'command': 'underline',
+ 'tests': [
+ { 'id': 'U_TEXT-1_SI',
+ 'rte1-id': 'a-underline-0',
+ 'desc': 'Underline selection',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<u>[bar]</u>baz' }
+ ]
+ },
+
+ { 'desc': '[HTML5] strikethrough',
+ 'command': 'strikethrough',
+ 'tests': [
+ { 'id': 'S_TEXT-1_SI',
+ 'rte1-id': 'a-strikethrough-0',
+ 'desc': 'Strike-through selection',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<s>[bar]</s>baz',
+ 'foo<strike>[bar]</strike>baz',
+ 'foo<del>[bar]</del>baz' ] }
+ ]
+ },
+
+ { 'desc': '[HTML5] subscript',
+ 'command': 'subscript',
+ 'tests': [
+ { 'id': 'SUB_TEXT-1_SI',
+ 'rte1-id': 'a-subscript-0',
+ 'desc': 'Change selection to subscript',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<sub>[bar]</sub>baz' }
+ ]
+ },
+
+ { 'desc': '[HTML5] superscript',
+ 'command': 'superscript',
+ 'tests': [
+ { 'id': 'SUP_TEXT-1_SI',
+ 'rte1-id': 'a-superscript-0',
+ 'desc': 'Change selection to superscript',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<sup>[bar]</sup>baz' }
+ ]
+ },
+
+ { 'desc': '[HTML5] createlink',
+ 'command': 'createlink',
+ 'tests': [
+ { 'id': 'CL:url_TEXT-1_SI',
+ 'rte1-id': 'a-createlink-0',
+ 'desc': 'create a link around the selection',
+ 'value': '#foo',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<a href="#foo">[bar]</a>baz' }
+ ]
+ },
+
+ { 'desc': '[HTML5] formatBlock',
+ 'command': 'formatblock',
+ 'tests': [
+ { 'id': 'FB:H1_TEXT-1_SI',
+ 'rte1-id': 'a-formatblock-0',
+ 'desc': 'format the selection into a block: use <h1>',
+ 'value': 'h1',
+ 'pad': 'foo[bar]baz',
+ 'expected': '<h1>foo[bar]baz</h1>' },
+
+ { 'id': 'FB:P_TEXT-1_SI',
+ 'desc': 'format the selection into a block: use <p>',
+ 'value': 'p',
+ 'pad': 'foo[bar]baz',
+ 'expected': '<p>foo[bar]baz</p>' },
+
+ { 'id': 'FB:PRE_TEXT-1_SI',
+ 'desc': 'format the selection into a block: use <pre>',
+ 'value': 'pre',
+ 'pad': 'foo[bar]baz',
+ 'expected': '<pre>foo[bar]baz</pre>' },
+
+ { 'id': 'FB:ADDRESS_TEXT-1_SI',
+ 'desc': 'format the selection into a block: use <address>',
+ 'value': 'address',
+ 'pad': 'foo[bar]baz',
+ 'expected': '<address>foo[bar]baz</address>' },
+
+ { 'id': 'FB:BQ_TEXT-1_SI',
+ 'desc': 'format the selection into a block: use <blockquote>',
+ 'value': 'blockquote',
+ 'pad': 'foo[bar]baz',
+ 'expected': '<blockquote>foo[bar]baz</blockquote>' },
+
+ { 'id': 'FB:BQ_BR.BR-1_SM',
+ 'desc': 'format a multi-line selection into a block: use <blockquote>',
+ 'command': 'formatblock',
+ 'value': 'blockquote',
+ 'pad': 'fo[o<br>bar<br>b]az',
+ 'expected': '<blockquote>fo[o<br>bar<br>b]az</blockquote>' }
+ ]
+ },
+
+
+ { 'desc': '[MIDAS] backcolor',
+ 'command': 'backcolor',
+ 'tests': [
+ { 'id': 'BC:blue_TEXT-1_SI',
+ 'rte1-id': 'a-backcolor-0',
+ 'desc': 'Change background color (note: no non-CSS variant available)',
+ 'value': 'blue',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<span style="background-color: blue">[bar]</span>baz',
+ 'foo<font style="background-color: blue">[bar]</font>baz' ] }
+ ]
+ },
+
+ { 'desc': '[MIDAS] forecolor',
+ 'command': 'forecolor',
+ 'tests': [
+ { 'id': 'FC:blue_TEXT-1_SI',
+ 'rte1-id': 'a-forecolor-0',
+ 'desc': 'Change the text color',
+ 'value': 'blue',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<font color="blue">[bar]</font>baz' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] hilitecolor',
+ 'command': 'hilitecolor',
+ 'tests': [
+ { 'id': 'HC:blue_TEXT-1_SI',
+ 'rte1-id': 'a-hilitecolor-0',
+ 'desc': 'Change the hilite color',
+ 'value': 'blue',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<span style="background-color: blue">[bar]</span>baz',
+ 'foo<font style="background-color: blue">[bar]</font>baz' ] }
+ ]
+ },
+
+ { 'desc': '[MIDAS] fontname',
+ 'command': 'fontname',
+ 'tests': [
+ { 'id': 'FN:a_TEXT-1_SI',
+ 'rte1-id': 'a-fontname-0',
+ 'desc': 'Change the font name',
+ 'value': 'arial',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<font face="arial">[bar]</font>baz' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] fontsize',
+ 'command': 'fontsize',
+ 'tests': [
+ { 'id': 'FS:2_TEXT-1_SI',
+ 'rte1-id': 'a-fontsize-0',
+ 'desc': 'Change the font size to "2"',
+ 'value': '2',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<font size="2">[bar]</font>baz' },
+
+ { 'id': 'FS:18px_TEXT-1_SI',
+ 'desc': 'Change the font size to "18px"',
+ 'value': '18px',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<font size="18px">[bar]</font>baz' },
+
+ { 'id': 'FS:large_TEXT-1_SI',
+ 'desc': 'Change the font size to "large"',
+ 'value': 'large',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<font size="large">[bar]</font>baz' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] increasefontsize',
+ 'command': 'increasefontsize',
+ 'tests': [
+ { 'id': 'INCFS:2_TEXT-1_SI',
+ 'desc': 'Decrease the font size (to small)',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<font size="4">[bar]</font>baz',
+ 'foo<font size="+1">[bar]</font>baz',
+ 'foo<big>[bar]</big>baz' ] }
+ ]
+ },
+
+ { 'desc': '[MIDAS] decreasefontsize',
+ 'command': 'decreasefontsize',
+ 'tests': [
+ { 'id': 'DECFS:2_TEXT-1_SI',
+ 'rte1-id': 'a-decreasefontsize-0',
+ 'desc': 'Decrease the font size (to small)',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<font size="2">[bar]</font>baz',
+ 'foo<font size="-1">[bar]</font>baz',
+ 'foo<small>[bar]</small>baz' ] }
+ ]
+ },
+
+ { 'desc': '[MIDAS] indent (note: accept the de-facto standard indent of 40px)',
+ 'command': 'indent',
+ 'tests': [
+ { 'id': 'IND_TEXT-1_SI',
+ 'rte1-id': 'a-indent-0',
+ 'desc': 'Indent the text (accept the de-facto standard of 40px indent)',
+ 'pad': 'foo[bar]baz',
+ 'checkAttrs': False,
+ 'expected': [ '<blockquote>foo[bar]baz</blockquote>',
+ '<div style="margin-left: 40px">foo[bar]baz</div>' ],
+ 'div': {
+ 'accOuter': '<div contenteditable="true" style="margin-left: 40px">foo[bar]baz</div>' } }
+ ]
+ },
+
+ { 'desc': '[MIDAS] outdent (-> unapply tests)',
+ 'command': 'outdent',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': '[MIDAS] justifycenter',
+ 'command': 'justifycenter',
+ 'tests': [
+ { 'id': 'JC_TEXT-1_SC',
+ 'rte1-id': 'a-justifycenter-0',
+ 'desc': 'justify the text centrally',
+ 'pad': 'foo^bar',
+ 'expected': [ '<center>foo^bar</center>',
+ '<p align="center">foo^bar</p>',
+ '<p align="middle">foo^bar</p>',
+ '<div align="center">foo^bar</div>',
+ '<div align="middle">foo^bar</div>' ],
+ 'div': {
+ 'accOuter': [ '<div align="center" contenteditable="true">foo^bar</div>',
+ '<div align="middle" contenteditable="true">foo^bar</div>' ] } }
+ ]
+ },
+
+ { 'desc': '[MIDAS] justifyfull',
+ 'command': 'justifyfull',
+ 'tests': [
+ { 'id': 'JF_TEXT-1_SC',
+ 'rte1-id': 'a-justifyfull-0',
+ 'desc': 'justify the text fully',
+ 'pad': 'foo^bar',
+ 'expected': [ '<p align="justify">foo^bar</p>',
+ '<div align="justify">foo^bar</div>' ],
+ 'div': {
+ 'accOuter': '<div align="justify" contenteditable="true">foo^bar</div>' } }
+ ]
+ },
+
+ { 'desc': '[MIDAS] justifyleft',
+ 'command': 'justifyleft',
+ 'tests': [
+ { 'id': 'JL_TEXT-1_SC',
+ 'rte1-id': 'a-justifyleft-0',
+ 'desc': 'justify the text left',
+ 'pad': 'foo^bar',
+ 'expected': [ '<p align="left">foo^bar</p>',
+ '<div align="left">foo^bar</div>' ],
+ 'div': {
+ 'accOuter': '<div align="left" contenteditable="true">foo^bar</div>' } }
+ ]
+ },
+
+ { 'desc': '[MIDAS] justifyright',
+ 'command': 'justifyright',
+ 'tests': [
+ { 'id': 'JR_TEXT-1_SC',
+ 'rte1-id': 'a-justifyright-0',
+ 'desc': 'justify the text right',
+ 'pad': 'foo^bar',
+ 'expected': [ '<p align="right">foo^bar</p>',
+ '<div align="right">foo^bar</div>' ],
+ 'div': {
+ 'accOuter': '<div align="right" contenteditable="true">foo^bar</div>' } }
+ ]
+ },
+
+ { 'desc': '[MIDAS] heading',
+ 'command': 'heading',
+ 'tests': [
+ { 'id': 'H:H1_TEXT-1_SC',
+ 'desc': 'create a heading from the paragraph that contains the selection',
+ 'value': 'h1',
+ 'pad': 'foo[bar]baz',
+ 'expected': '<h1>foo[bar]baz</h1>' }
+ ]
+ },
+
+
+ { 'desc': '[Other] createbookmark',
+ 'command': 'createbookmark',
+ 'tests': [
+ { 'id': 'CB:name_TEXT-1_SI',
+ 'rte1-id': 'a-createbookmark-0',
+ 'desc': 'create a bookmark (named link) around selection',
+ 'value': 'created',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<a name="created">[bar]</a>baz' }
+ ]
+ }
+ ]
+}
+
+
+
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/applyCSS.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/applyCSS.py
new file mode 100644
index 000000000..94cdad83f
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/applyCSS.py
@@ -0,0 +1,244 @@
+
+APPLY_TESTS_CSS = {
+ 'id': 'AC',
+ 'caption': 'Apply Formatting Tests, using styleWithCSS',
+ 'checkAttrs': True,
+ 'checkStyle': True,
+ 'styleWithCSS': True,
+
+ 'Proposed': [
+ { 'desc': '',
+ 'command': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': '[HTML5] bold',
+ 'command': 'bold',
+ 'tests': [
+ { 'id': 'B_TEXT-1_SI',
+ 'rte1-id': 'a-bold-1',
+ 'desc': 'Bold selection',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<span style="font-weight: bold">[bar]</span>baz' }
+ ]
+ },
+
+ { 'desc': '[HTML5] italic',
+ 'command': 'italic',
+ 'tests': [
+ { 'id': 'I_TEXT-1_SI',
+ 'rte1-id': 'a-italic-1',
+ 'desc': 'Italicize selection',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<span style="font-style: italic">[bar]</span>baz' }
+ ]
+ },
+
+ { 'desc': '[HTML5] underline',
+ 'command': 'underline',
+ 'tests': [
+ { 'id': 'U_TEXT-1_SI',
+ 'rte1-id': 'a-underline-1',
+ 'desc': 'Underline selection',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<span style="text-decoration: underline">[bar]</span>baz' }
+ ]
+ },
+
+ { 'desc': '[HTML5] strikethrough',
+ 'command': 'strikethrough',
+ 'tests': [
+ { 'id': 'S_TEXT-1_SI',
+ 'rte1-id': 'a-strikethrough-1',
+ 'desc': 'Strike-through selection',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<span style="text-decoration: line-through">[bar]</span>baz' }
+ ]
+ },
+
+ { 'desc': '[HTML5] subscript',
+ 'command': 'subscript',
+ 'tests': [
+ { 'id': 'SUB_TEXT-1_SI',
+ 'rte1-id': 'a-subscript-1',
+ 'desc': 'Change selection to subscript',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<span style="vertical-align: sub">[bar]</span>baz' }
+ ]
+ },
+
+ { 'desc': '[HTML5] superscript',
+ 'command': 'superscript',
+ 'tests': [
+ { 'id': 'SUP_TEXT-1_SI',
+ 'rte1-id': 'a-superscript-1',
+ 'desc': 'Change selection to superscript',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<span style="vertical-align: super">[bar]</span>baz' }
+ ]
+ },
+
+
+ { 'desc': '[MIDAS] backcolor',
+ 'command': 'backcolor',
+ 'tests': [
+ { 'id': 'BC:blue_TEXT-1_SI',
+ 'rte1-id': 'a-backcolor-1',
+ 'desc': 'Change background color',
+ 'value': 'blue',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<span style="background-color: blue">[bar]</span>baz',
+ 'foo<font style="background-color: blue">[bar]</font>baz' ] }
+ ]
+ },
+
+ { 'desc': '[MIDAS] forecolor',
+ 'command': 'forecolor',
+ 'tests': [
+ { 'id': 'FC:blue_TEXT-1_SI',
+ 'rte1-id': 'a-forecolor-1',
+ 'desc': 'Change the text color',
+ 'value': 'blue',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<span style="color: blue">[bar]</span>baz',
+ 'foo<font style="color: blue">[bar]</font>baz' ] }
+ ]
+ },
+
+ { 'desc': '[MIDAS] hilitecolor',
+ 'command': 'hilitecolor',
+ 'tests': [
+ { 'id': 'HC:blue_TEXT-1_SI',
+ 'rte1-id': 'a-hilitecolor-1',
+ 'desc': 'Change the hilite color',
+ 'value': 'blue',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<span style="background-color: blue">[bar]</span>baz',
+ 'foo<font style="background-color: blue">[bar]</font>baz' ] }
+ ]
+ },
+
+ { 'desc': '[MIDAS] fontname',
+ 'command': 'fontname',
+ 'tests': [
+ { 'id': 'FN:a_TEXT-1_SI',
+ 'rte1-id': 'a-fontname-1',
+ 'desc': 'Change the font name',
+ 'value': 'arial',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<span style="font-family: arial">[bar]</span>baz',
+ 'foo<font style="font-family: blue">[bar]</font>baz' ] }
+ ]
+ },
+
+ { 'desc': '[MIDAS] fontsize',
+ 'command': 'fontsize',
+ 'tests': [
+ { 'id': 'FS:2_TEXT-1_SI',
+ 'rte1-id': 'a-fontsize-1',
+ 'desc': 'Change the font size to "2"',
+ 'value': '2',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<span style="font-size: small">[bar]</span>baz',
+ 'foo<font style="font-size: small">[bar]</font>baz' ] },
+
+ { 'id': 'FS:18px_TEXT-1_SI',
+ 'desc': 'Change the font size to "18px"',
+ 'value': '18px',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<span style="font-size: 18px">[bar]</span>baz',
+ 'foo<font style="font-size: 18px">[bar]</font>baz' ] },
+
+ { 'id': 'FS:large_TEXT-1_SI',
+ 'desc': 'Change the font size to "large"',
+ 'value': 'large',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<span style="font-size: large">[bar]</span>baz',
+ 'foo<font style="font-size: large">[bar]</font>baz' ] }
+ ]
+ },
+
+ { 'desc': '[MIDAS] indent',
+ 'command': 'indent',
+ 'tests': [
+ { 'id': 'IND_TEXT-1_SI',
+ 'rte1-id': 'a-indent-1',
+ 'desc': 'Indent the text (assume "standard" 40px)',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ '<div style="margin-left: 40px">foo[bar]baz</div>',
+ '<div style="margin: 0 0 0 40px">foo[bar]baz</div>',
+ '<blockquote style="margin-left: 40px">foo[bar]baz</blockquote>',
+ '<blockquote style="margin: 0 0 0 40px">foo[bar]baz</blockquote>' ],
+ 'div': {
+ 'accOuter': [ '<div contenteditable="true" style="margin-left: 40px">foo[bar]baz</div>',
+ '<div contenteditable="true" style="margin: 0 0 0 40px">foo[bar]baz</div>' ] } }
+ ]
+ },
+
+ { 'desc': '[MIDAS] outdent (-> unapply tests)',
+ 'command': 'outdent',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': '[MIDAS] justifycenter',
+ 'command': 'justifycenter',
+ 'tests': [
+ { 'id': 'JC_TEXT-1_SC',
+ 'rte1-id': 'a-justifycenter-1',
+ 'desc': 'justify the text centrally',
+ 'pad': 'foo^bar',
+ 'expected': [ '<p style="text-align: center">foo^bar</p>',
+ '<div style="text-align: center">foo^bar</div>' ],
+ 'div': {
+ 'accOuter': '<div contenteditable="true" style="text-align: center">foo^bar</div>' } }
+ ]
+ },
+
+ { 'desc': '[MIDAS] justifyfull',
+ 'command': 'justifyfull',
+ 'tests': [
+ { 'id': 'JF_TEXT-1_SC',
+ 'rte1-id': 'a-justifyfull-1',
+ 'desc': 'justify the text fully',
+ 'pad': 'foo^bar',
+ 'expected': [ '<p style="text-align: justify">foo^bar</p>',
+ '<div style="text-align: justify">foo^bar</div>' ],
+ 'div': {
+ 'accOuter': '<div contenteditable="true" style="text-align: justify">foo^bar</div>' } }
+ ]
+ },
+
+ { 'desc': '[MIDAS] justifyleft',
+ 'command': 'justifyleft',
+ 'tests': [
+ { 'id': 'JL_TEXT-1_SC',
+ 'rte1-id': 'a-justifyleft-1',
+ 'desc': 'justify the text left',
+ 'pad': 'foo^bar',
+ 'expected': [ '<p style="text-align: left">foo^bar</p>',
+ '<div style="text-align: left">foo^bar</div>' ],
+ 'div': {
+ 'accOuter': '<div contenteditable="true" style="text-align: left">foo^bar</div>' } }
+ ]
+ },
+
+ { 'desc': '[MIDAS] justifyright',
+ 'command': 'justifyright',
+ 'tests': [
+ { 'id': 'JR_TEXT-1_SC',
+ 'rte1-id': 'a-justifyright-1',
+ 'desc': 'justify the text right',
+ 'pad': 'foo^bar',
+ 'expected': [ '<p style="text-align: right">foo^bar</p>',
+ '<div style="text-align: right">foo^bar</div>' ],
+ 'div': {
+ 'accOuter': '<div contenteditable="true" style="text-align: right">foo^bar</div>' } }
+ ]
+ }
+ ]
+}
+
+
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/change.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/change.py
new file mode 100644
index 000000000..6a76d3d5f
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/change.py
@@ -0,0 +1,273 @@
+
+CHANGE_TESTS = {
+ 'id': 'C',
+ 'caption': 'Change Existing Format to Different Format Tests',
+ 'checkAttrs': True,
+ 'checkStyle': True,
+ 'styleWithCSS': False,
+
+ 'Proposed': [
+ { 'desc': '',
+ 'command': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': '[HTML5] italic',
+ 'command': 'italic',
+ 'tests': [
+ { 'id': 'I_I-1_SL',
+ 'desc': 'Italicize partially italicized text',
+ 'pad': 'foo[bar<i>baz]</i>qoz',
+ 'expected': 'foo<i>[barbaz]</i>qoz' },
+
+ { 'id': 'I_B-I-1_SO',
+ 'desc': 'Italicize partially italicized text in bold context',
+ 'pad': '<b>foo[bar<i>baz</i>}</b>',
+ 'expected': '<b>foo<i>[barbaz]</i></b>' }
+ ]
+ },
+
+ { 'desc': '[HTML5] underline',
+ 'command': 'underline',
+ 'tests': [
+ { 'id': 'U_U-1_SO',
+ 'desc': 'Underline partially underlined text',
+ 'pad': 'foo[bar<u>baz</u>qoz]quz',
+ 'expected': 'foo<u>[barbazqoz]</u>quz' },
+
+ { 'id': 'U_U-1_SL',
+ 'desc': 'Underline partially underlined text',
+ 'pad': 'foo[bar<u>baz]qoz</u>quz',
+ 'expected': 'foo<u>[barbaz]qoz</u>quz' },
+
+ { 'id': 'U_S-U-1_SO',
+ 'desc': 'Underline partially underlined text in striked context',
+ 'pad': '<s>foo[bar<u>baz</u>}</s>',
+ 'expected': '<s>foo<u>[barbaz]</u></s>' }
+ ]
+ },
+
+
+ { 'desc': '[MIDAS] backcolor',
+ 'command': 'backcolor',
+ 'tests': [
+ { 'id': 'BC:842_FONTs:bc:fca-1_SW',
+ 'rte1-id': 'c-backcolor-0',
+ 'desc': 'Change background color to new color',
+ 'value': '#884422',
+ 'pad': '<font style="background-color: #ffccaa">[foobarbaz]</font>',
+ 'expected': [ '<font style="background-color: #884422">[foobarbaz]</font>',
+ '<span style="background-color: #884422">[foobarbaz]</span>' ] },
+
+ { 'id': 'BC:00f_SPANs:bc:f00-1_SW',
+ 'rte1-id': 'c-backcolor-2',
+ 'desc': 'Change background color to new color',
+ 'value': '#0000ff',
+ 'pad': '<span style="background-color: #ff0000">[foobarbaz]</span>',
+ 'expected': [ '<font style="background-color: #0000ff">[foobarbaz]</font>',
+ '<span style="background-color: #0000ff">[foobarbaz]</span>' ] },
+
+ { 'id': 'BC:ace_FONT.ass.s:bc:rgb-1_SW',
+ 'rte1-id': 'c-backcolor-1',
+ 'desc': 'Change background color in styled span to new color',
+ 'value': '#aaccee',
+ 'pad': '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0)">[foobarbaz]</span>',
+ 'expected': [ '<font style="background-color: #aaccee">[foobarbaz]</font>',
+ '<span style="background-color: #aaccee">[foobarbaz]</span>' ] }
+ ]
+ },
+
+ { 'desc': '[MIDAS] forecolor',
+ 'command': 'forecolor',
+ 'tests': [
+ { 'id': 'FC:g_FONTc:b-1_SW',
+ 'rte1-id': 'c-forecolor-0',
+ 'desc': 'Change the text color (without CSS)',
+ 'value': 'green',
+ 'pad': '<font color="blue">[foobarbaz]</font>',
+ 'expected': '<font color="green">[foobarbaz]</font>' },
+
+ { 'id': 'FC:g_SPANs:c:g-1_SW',
+ 'rte1-id': 'c-forecolor-1',
+ 'desc': 'Change the text color from a styled span (without CSS)',
+ 'value': 'green',
+ 'pad': '<span style="color: blue">[foobarbaz]</span>',
+ 'expected': '<font color="green">[foobarbaz]</font>' },
+
+ { 'id': 'FC:g_FONTc:b.s:c:r-1_SW',
+ 'rte1-id': 'c-forecolor-2',
+ 'desc': 'Change the text color from conflicting color and style (without CSS)',
+ 'value': 'green',
+ 'pad': '<font color="blue" style="color: red">[foobarbaz]</font>',
+ 'expected': '<font color="green">[foobarbaz]</font>' },
+
+ { 'id': 'FC:g_FONTc:b.sz:6-1_SI',
+ 'desc': 'Change the font color in content with a different font size and font color',
+ 'value': 'green',
+ 'pad': '<font color="blue" size="6">foo[bar]baz</font>',
+ 'expected': [ '<font color="blue" size="6">foo<font color="green">[bar]</font>baz</font>',
+ '<font size="6"><font color="blue">foo<font color="green">[bar]</font><font color="blue">baz</font></font>' ] }
+ ]
+ },
+
+ { 'desc': '[MIDAS] hilitecolor',
+ 'command': 'hilitecolor',
+ 'tests': [
+ { 'id': 'HC:g_FONTs:c:b-1_SW',
+ 'rte1-id': 'c-hilitecolor-0',
+ 'desc': 'Change the hilite color (without CSS)',
+ 'value': 'green',
+ 'pad': '<font style="background-color: blue">[foobarbaz]</font>',
+ 'expected': [ '<font style="background-color: green">[foobarbaz]</font>',
+ '<span style="background-color: green">[foobarbaz]</span>' ] },
+
+ { 'id': 'HC:g_SPANs:c:g-1_SW',
+ 'rte1-id': 'c-hilitecolor-2',
+ 'desc': 'Change the hilite color from a styled span (without CSS)',
+ 'value': 'green',
+ 'pad': '<span style="background-color: blue">[foobarbaz]</span>',
+ 'expected': '<span style="background-color: green">[foobarbaz]</span>' },
+
+ { 'id': 'HC:g_SPAN.ass.s:c:rgb-1_SW',
+ 'rte1-id': 'c-hilitecolor-1',
+ 'desc': 'Change the hilite color from a styled span (without CSS)',
+ 'value': 'green',
+ 'pad': '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0);">[foobarbaz]</span>',
+ 'expected': '<span style="background-color: green">[foobarbaz]</span>' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] fontname',
+ 'command': 'fontname',
+ 'tests': [
+ { 'id': 'FN:c_FONTf:a-1_SW',
+ 'rte1-id': 'c-fontname-0',
+ 'desc': 'Change existing font name to new font name (without CSS)',
+ 'value': 'courier',
+ 'pad': '<font face="arial">[foobarbaz]</font>',
+ 'expected': '<font face="courier">[foobarbaz]</font>' },
+
+ { 'id': 'FN:c_SPANs:ff:a-1_SW',
+ 'rte1-id': 'c-fontname-1',
+ 'desc': 'Change existing font name from style to new font name (without CSS)',
+ 'value': 'courier',
+ 'pad': '<span style="font-family: arial">[foobarbaz]</span>',
+ 'expected': '<font face="courier">[foobarbaz]</font>' },
+
+ { 'id': 'FN:c_FONTf:a.s:ff:v-1_SW',
+ 'rte1-id': 'c-fontname-2',
+ 'desc': 'Change existing font name with conflicting face and style to new font name (without CSS)',
+ 'value': 'courier',
+ 'pad': '<font face="arial" style="font-family: verdana">[foobarbaz]</font>',
+ 'expected': '<font face="courier">[foobarbaz]</font>' },
+
+ { 'id': 'FN:c_FONTf:a-1_SI',
+ 'desc': 'Change existing font name to new font name, text partially selected',
+ 'value': 'courier',
+ 'pad': '<font face="arial">foo[bar]baz</font>',
+ 'expected': '<font face="arial">foo</font><font face="courier">[bar]</font><font face="arial">baz</font>',
+ 'accept': '<font face="arial">foo<font face="courier">[bar]</font>baz</font>' },
+
+ { 'id': 'FN:c_FONTf:a-2_SL',
+ 'desc': 'Change existing font name to new font name, using CSS styling',
+ 'value': 'courier',
+ 'pad': 'foo[bar<font face="arial">baz]qoz</font>',
+ 'expected': 'foo<font face="courier">[barbaz]</font><font face="arial">qoz</font>' },
+
+ { 'id': 'FN:c_FONTf:v-FONTf:a-1_SW',
+ 'rte1-id': 'c-fontname-3',
+ 'desc': 'Change existing font name in nested <font> tags to new font name (without CSS)',
+ 'value': 'courier',
+ 'pad': '<font face="verdana"><font face="arial">[foobarbaz]</font></font>',
+ 'expected': '<font face="courier">[foobarbaz]</font>',
+ 'accept': '<font face="verdana"><font face="courier">[foobarbaz]</font></font>' },
+
+ { 'id': 'FN:c_SPANs:ff:v-FONTf:a-1_SW',
+ 'rte1-id': 'c-fontname-4',
+ 'desc': 'Change existing font name in nested mixed tags to new font name (without CSS)',
+ 'value': 'courier',
+ 'pad': '<span style="font-family: verdana"><font face="arial">[foobarbaz]</font></span>',
+ 'expected': '<font face="courier">[foobarbaz]</font>',
+ 'accept': '<span style="font-family: verdana"><font face="courier">[foobarbaz]</font></span>' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] fontsize',
+ 'command': 'fontsize',
+ 'tests': [
+ { 'id': 'FS:1_FONTsz:4-1_SW',
+ 'rte1-id': 'c-fontsize-0',
+ 'desc': 'Change existing font size to new size (without CSS)',
+ 'value': '1',
+ 'pad': '<font size="4">[foobarbaz]</font>',
+ 'expected': '<font size="1">[foobarbaz]</font>' },
+
+ { 'id': 'FS:1_SPAN.ass.s:fs:large-1_SW',
+ 'rte1-id': 'c-fontsize-1',
+ 'desc': 'Change existing font size from styled span to new size (without CSS)',
+ 'value': '1',
+ 'pad': '<span class="Apple-style-span" style="font-size: large">[foobarbaz]</span>',
+ 'expected': '<font size="1">[foobarbaz]</font>' },
+
+ { 'id': 'FS:5_FONTsz:1.s:fs:xs-1_SW',
+ 'rte1-id': 'c-fontsize-2',
+ 'desc': 'Change existing font size from tag with conflicting size and style to new size (without CSS)',
+ 'value': '5',
+ 'pad': '<font size="1" style="font-size:x-small">[foobarbaz]</font>',
+ 'expected': '<font size="5">[foobarbaz]</font>' },
+
+ { 'id': 'FS:2_FONTc:b.sz:6-1_SI',
+ 'desc': 'Change the font size in content with a different font size and font color',
+ 'value': '2',
+ 'pad': '<font color="blue" size="6">foo[bar]baz</font>',
+ 'expected': [ '<font color="blue" size="6">foo<font size="2">[bar]</font>baz</font>',
+ '<font color="blue"><font size="6">foo</font><font size="2">[bar]</font><font size="6">baz</font></font>' ] },
+
+ { 'id': 'FS:larger_FONTsz:4',
+ 'desc': 'Change selection to use next larger font',
+ 'value': 'larger',
+ 'pad': '<font size="4">foo[bar]baz</font>',
+ 'expected': '<font size="4">foo<font size="larger">[bar]</font>baz</font>',
+ 'accept': '<font size="4">foo</font><font size="5">[bar]</font><font size="4">baz</font>' },
+
+ { 'id': 'FS:smaller_FONTsz:4',
+ 'desc': 'Change selection to use next smaller font',
+ 'value': 'smaller',
+ 'pad': '<font size="4">foo[bar]baz</font>',
+ 'expected': '<font size="4">foo<font size="smaller">[bar]</font>baz</font>',
+ 'accept': '<font size="4">foo</font><font size="3">[bar]</font><font size="4">baz</font>' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] formatblock',
+ 'command': 'formatblock',
+ 'tests': [
+ { 'id': 'FB:h1_ADDRESS-1_SW',
+ 'desc': 'change block from <address> to <h1>',
+ 'value': 'h1',
+ 'pad': '<address>foo [bar] baz</address>',
+ 'expected': '<h1>foo [bar] baz</h1>' },
+
+ { 'id': 'FB:h1_ADDRESS-FONTsz:4-1_SO',
+ 'desc': 'change block from <address> with partially formatted content to <h1>',
+ 'value': 'h1',
+ 'pad': '<address>foo [<font size="4">bar</font>] baz</address>',
+ 'expected': '<h1>foo [bar] baz</h1>' },
+
+ { 'id': 'FB:h1_ADDRESS-FONTsz:4-1_SW',
+ 'desc': 'change block from <address> with partially formatted content to <h1>',
+ 'value': 'h1',
+ 'pad': '<address>foo <font size="4">[bar]</font> baz</address>',
+ 'expected': '<h1>foo [bar] baz</h1>' },
+
+ { 'id': 'FB:h1_ADDRESS-FONT.ass.sz:4-1_SW',
+ 'desc': 'change block from <address> with partially formatted content to <h1>',
+ 'value': 'h1',
+ 'pad': '<address>foo <font class="Apple-style-span" size="4">[bar]</font> baz</address>',
+ 'expected': '<h1>foo [bar] baz</h1>' }
+ ]
+ }
+ ]
+}
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/changeCSS.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/changeCSS.py
new file mode 100644
index 000000000..4862b9b73
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/changeCSS.py
@@ -0,0 +1,210 @@
+
+CHANGE_TESTS_CSS = {
+ 'id': 'CC',
+ 'caption': 'Change Existing Format to Different Format Tests, using styleWithCSS',
+ 'checkAttrs': True,
+ 'checkStyle': True,
+ 'styleWithCSS': True,
+
+ 'Proposed': [
+ { 'desc': '',
+ 'command': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': '[HTML5] italic',
+ 'command': 'italic',
+ 'tests': [
+ { 'id': 'I_I-1_SL',
+ 'desc': 'Italicize partially italicized text',
+ 'pad': 'foo[bar<i>baz]</i>qoz',
+ 'expected': 'foo<span style="font-style: italic">[barbaz]</span>qoz' },
+
+ { 'id': 'I_B-1_SL',
+ 'desc': 'Italicize partially bolded text',
+ 'pad': 'foo[bar<b>baz]</b>qoz',
+ 'expected': 'foo<span style="font-style: italic">[bar<b>baz]</b></span>qoz',
+ 'accept': 'foo<span style="font-style: italic">[bar<b>baz</b>}</span>qoz' },
+
+ { 'id': 'I_B-1_SW',
+ 'desc': 'Italicize bold text, ideally combining both',
+ 'pad': 'foobar<b>[baz]</b>qoz',
+ 'expected': 'foobar<span style="font-style: italic; font-weight: bold">[baz]</span>qoz',
+ 'accept': 'foobar<b><span style="font-style: italic">[baz]</span></b>qoz' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] backcolor',
+ 'command': 'backcolor',
+ 'tests': [
+ { 'id': 'BC:gray_SPANs:bc:b-1_SW',
+ 'desc': 'Change background color from blue to gray',
+ 'value': 'gray',
+ 'pad': '<span style="background-color: blue">[foobarbaz]</span>',
+ 'expected': '<span style="background-color: gray">[foobarbaz]</span>' },
+
+ { 'id': 'BC:gray_SPANs:bc:b-1_SO',
+ 'desc': 'Change background color from blue to gray',
+ 'value': 'gray',
+ 'pad': '{<span style="background-color: blue">foobarbaz</span>}',
+ 'expected': [ '{<span style="background-color: gray">foobarbaz</span>}',
+ '<span style="background-color: gray">[foobarbaz]</span>' ] },
+
+ { 'id': 'BC:gray_SPANs:bc:b-1_SI',
+ 'desc': 'Change background color from blue to gray',
+ 'value': 'gray',
+ 'pad': '<span style="background-color: blue">foo[bar]baz</span>',
+ 'expected': '<span style="background-color: blue">foo</span><span style="background-color: gray">[bar]</span><span style="background-color: blue">baz</span>',
+ 'accept': '<span style="background-color: blue">foo<span style="background-color: gray">[bar]</span>baz</span>' },
+
+ { 'id': 'BC:gray_P-SPANs:bc:b-1_SW',
+ 'desc': 'Change background color within a paragraph from blue to gray',
+ 'value': 'gray',
+ 'pad': '<p><span style="background-color: blue">[foobarbaz]</span></p>',
+ 'expected': [ '<p><span style="background-color: gray">[foobarbaz]</span></p>',
+ '<p style="background-color: gray">[foobarbaz]</p>' ] },
+
+ { 'id': 'BC:gray_P-SPANs:bc:b-2_SW',
+ 'desc': 'Change background color within a paragraph from blue to gray',
+ 'value': 'gray',
+ 'pad': '<p>foo<span style="background-color: blue">[bar]</span>baz</p>',
+ 'expected': '<p>foo<span style="background-color: gray">[bar]</span>baz</p>' },
+
+ { 'id': 'BC:gray_P-SPANs:bc:b-3_SO',
+ 'desc': 'Change background color within a paragraph from blue to gray (selection encloses more than previous span)',
+ 'value': 'gray',
+ 'pad': '<p>[foo<span style="background-color: blue">barbaz</span>qoz]quz</p>',
+ 'expected': '<p><span style="background-color: gray">[foobarbazqoz]</span>quz</p>' },
+
+ { 'id': 'BC:gray_P-SPANs:bc:b-3_SL',
+ 'desc': 'Change background color within a paragraph from blue to gray (previous span partially selected)',
+ 'value': 'gray',
+ 'pad': '<p>[foo<span style="background-color: blue">bar]baz</span>qozquz</p>',
+ 'expected': '<p><span style="background-color: gray">[foobar]</span><span style="background-color: blue">baz</span>qozquz</p>' },
+
+ { 'id': 'BC:gray_SPANs:bc:b-2_SL',
+ 'desc': 'Change background color from blue to gray on partially covered span, selection extends left',
+ 'value': 'gray',
+ 'pad': 'foo [bar <span style="background-color: blue">baz] qoz</span> quz sic',
+ 'expected': 'foo <span style="background-color: gray">[bar baz]</span><span style="background-color: blue"> qoz</span> quz sic' },
+
+ { 'id': 'BC:gray_SPANs:bc:b-2_SR',
+ 'desc': 'Change background color from blue to gray on partially covered span, selection extends right',
+ 'value': 'gray',
+ 'pad': 'foo bar <span style="background-color: blue">baz [qoz</span> quz] sic',
+ 'expected': 'foo bar <span style="background-color: blue">baz </span><span style="background-color: gray">[qoz quz]</span> sic' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] fontname',
+ 'command': 'fontname',
+ 'tests': [
+ { 'id': 'FN:c_SPANs:ff:a-1_SW',
+ 'desc': 'Change existing font name to new font name, using CSS styling',
+ 'value': 'courier',
+ 'pad': '<span style="font-family: arial">[foobarbaz]</span>',
+ 'expected': '<span style="font-family: courier">[foobarbaz]</span>' },
+
+ { 'id': 'FN:c_FONTf:a-1_SW',
+ 'desc': 'Change existing font name to new font name, using CSS styling',
+ 'value': 'courier',
+ 'pad': '<font face="arial">[foobarbaz]</font>',
+ 'expected': [ '<font style="font-family: courier">[foobarbaz]</font>',
+ '<span style="font-family: courier">[foobarbaz]</span>' ] },
+
+ { 'id': 'FN:c_FONTf:a-1_SI',
+ 'desc': 'Change existing font name to new font name, using CSS styling',
+ 'value': 'courier',
+ 'pad': '<font face="arial">foo[bar]baz</font>',
+ 'expected': '<font face="arial">foo</font><span style="font-family: courier">[bar]</span><font face="arial">baz</font>' },
+
+ { 'id': 'FN:a_FONTf:a-1_SI',
+ 'desc': 'Change existing font name to same font name, using CSS styling (should be noop)',
+ 'value': 'arial',
+ 'pad': '<font face="arial">foo[bar]baz</font>',
+ 'expected': '<font face="arial">foo[bar]baz</font>' },
+
+ { 'id': 'FN:a_FONTf:a-1_SW',
+ 'desc': 'Change existing font name to same font name, using CSS styling (should be noop or perhaps change tag)',
+ 'value': 'arial',
+ 'pad': '<font face="arial">[foobarbaz]</font>',
+ 'expected': [ '<font face="arial">[foobarbaz]</font>',
+ '<span style="font-family: arial">[foobarbaz]</span>' ] },
+
+ { 'id': 'FN:a_FONTf:a-1_SO',
+ 'desc': 'Change existing font name to same font name, using CSS styling (should be noop or perhaps change tag)',
+ 'value': 'arial',
+ 'pad': '{<font face="arial">foobarbaz</font>}',
+ 'expected': [ '{<font face="arial">foobarbaz</font>}',
+ '<font face="arial">[foobarbaz]</font>',
+ '{<span style="font-family: arial">foobarbaz</span>}',
+ '<span style="font-family: arial">[foobarbaz]</span>' ] },
+
+ { 'id': 'FN:a_SPANs:ff:a-1_SI',
+ 'desc': 'Change existing font name to same font name, using CSS styling (should be noop)',
+ 'value': 'arial',
+ 'pad': '<span style="font-family: arial">[foobarbaz]</span>',
+ 'expected': '<span style="font-family: arial">[foobarbaz]</span>' },
+
+ { 'id': 'FN:c_FONTf:a-2_SL',
+ 'desc': 'Change existing font name to new font name, using CSS styling',
+ 'value': 'courier',
+ 'pad': 'foo[bar<font face="arial">baz]qoz</font>',
+ 'expected': 'foo<span style="font-family: courier">[barbaz]</span><font face="arial">qoz</font>' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] fontsize',
+ 'command': 'fontsize',
+ 'tests': [
+ { 'id': 'FS:1_SPANs:fs:l-1_SW',
+ 'desc': 'Change existing font size to new size, using CSS styling',
+ 'value': '1',
+ 'pad': '<span style="font-size: large">[foobarbaz]</span>',
+ 'expected': '<span style="font-size: x-small">[foobarbaz]</span>' },
+
+ { 'id': 'FS:large_SPANs:fs:l-1_SW',
+ 'desc': 'Change existing font size to same size (should be noop)',
+ 'value': 'large',
+ 'pad': '<span style="font-size: large">[foobarbaz]</span>',
+ 'expected': '<span style="font-size: large">[foobarbaz]</span>' },
+
+ { 'id': 'FS:18px_SPANs:fs:l-1_SW',
+ 'desc': 'Change existing font size to equivalent px size (should be noop, or change unit)',
+ 'value': '18px',
+ 'pad': '<span style="font-size: large">[foobarbaz]</span>',
+ 'expected': [ '<span style="font-size: 18px">[foobarbaz]</span>',
+ '<span style="font-size: large">[foobarbaz]</span>' ] },
+
+ { 'id': 'FS:4_SPANs:fs:l-1_SW',
+ 'desc': 'Change existing font size to equivalent numeric size (should be noop)',
+ 'value': '4',
+ 'pad': '<span style="font-size: large">[foobarbaz]</span>',
+ 'expected': '<span style="font-size: large">[foobarbaz]</span>' },
+
+ { 'id': 'FS:4_SPANs:fs:18px-1_SW',
+ 'desc': 'Change existing font size to equivalent numeric size (should be noop)',
+ 'value': '4',
+ 'pad': '<span style="font-size: 18px">[foobarbaz]</span>',
+ 'expected': '<span style="font-size: 18px">[foobarbaz]</span>' },
+
+ { 'id': 'FS:larger_SPANs:fs:l-1_SI',
+ 'desc': 'Change selection to use next larger font',
+ 'value': 'larger',
+ 'pad': '<span style="font-size: large">foo[bar]baz</span>',
+ 'expected': [ '<span style="font-size: large">foo<span style="font-size: x-large">[bar]</span>baz</span>',
+ '<span style="font-size: large">foo</span><span style="font-size: x-large">[bar]</span><span style="font-size: large">baz</span>' ],
+ 'accept': '<span style="font-size: large">foo<font size="larger">[bar]</font>baz</span>' },
+
+ { 'id': 'FS:smaller_SPANs:fs:l-1_SI',
+ 'desc': 'Change selection to use next smaller font',
+ 'value': 'smaller',
+ 'pad': '<span style="font-size: large">foo[bar]baz</span>',
+ 'expected': [ '<span style="font-size: large">foo<span style="font-size: medium">[bar]</span>baz</span>',
+ '<span style="font-size: large">foo</span><span style="font-size: medium">[bar]</span><span style="font-size: large">baz</span>' ],
+ 'accept': '<span style="font-size: large">foo<font size="smaller">[bar]</font>baz</span>' }
+ ]
+ }
+ ]
+}
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/delete.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/delete.py
new file mode 100644
index 000000000..0cc659225
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/delete.py
@@ -0,0 +1,330 @@
+
+DELETE_TESTS = {
+ 'id': 'D',
+ 'caption': 'Delete Tests',
+ 'command': 'delete',
+ 'checkAttrs': True,
+ 'checkStyle': False,
+
+ 'Proposed': [
+ { 'desc': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': 'delete single characters',
+ 'tests': [
+ { 'id': 'CHAR-1_SC',
+ 'desc': 'Delete 1 character',
+ 'pad': 'foo^barbaz',
+ 'expected': 'fo^barbaz' },
+
+ { 'id': 'CHAR-2_SC',
+ 'desc': 'Delete 1 pre-composed character o with diaeresis',
+ 'pad': 'fo&#xF6;^barbaz',
+ 'expected': 'fo^barbaz' },
+
+ { 'id': 'CHAR-3_SC',
+ 'desc': 'Delete 1 character with combining diaeresis above',
+ 'pad': 'foo&#x0308;^barbaz',
+ 'expected': 'fo^barbaz' },
+
+ { 'id': 'CHAR-4_SC',
+ 'desc': 'Delete 1 character with combining diaeresis below',
+ 'pad': 'foo&#x0324;^barbaz',
+ 'expected': 'fo^barbaz' },
+
+ { 'id': 'CHAR-5_SC',
+ 'desc': 'Delete 1 character with combining diaeresis above and below',
+ 'pad': 'foo&#x0308;&#x0324;^barbaz',
+ 'expected': 'fo^barbaz' },
+
+ { 'id': 'CHAR-5_SI-1',
+ 'desc': 'Delete 1 character with combining diaeresis above and below, selection on diaeresis above',
+ 'pad': 'foo[&#x0308;]&#x0324;barbaz',
+ 'expected': 'fo^barbaz' },
+
+ { 'id': 'CHAR-5_SI-2',
+ 'desc': 'Delete 1 character with combining diaeresis above and below, selection on diaeresis below',
+ 'pad': 'foo&#x0308;[&#x0324;]barbaz',
+ 'expected': 'fo^barbaz' },
+
+ { 'id': 'CHAR-5_SR',
+ 'desc': 'Delete 1 character with combining diaeresis above and below, selection oblique on diaeresis and following text',
+ 'pad': 'foo&#x0308;[&#x0324;bar]baz',
+ 'expected': 'fo^baz' },
+
+ { 'id': 'CHAR-6_SC',
+ 'desc': 'Delete 1 character with enclosing square',
+ 'pad': 'foo&#x20DE;^barbaz',
+ 'expected': 'fo^barbaz' },
+
+ { 'id': 'CHAR-7_SC',
+ 'desc': 'Delete 1 character with combining long solidus overlay',
+ 'pad': 'foo&#x0338;^barbaz',
+ 'expected': 'fo^barbaz' }
+ ]
+ },
+
+ { 'desc': 'delete text selection',
+ 'tests': [
+ { 'id': 'TEXT-1_SI',
+ 'desc': 'Delete text selection',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo^baz' },
+
+ { 'id': 'B-1_SS',
+ 'desc': 'Delete at start of span',
+ 'pad': 'foo<b>^bar</b>baz',
+ 'expected': 'fo^<b>bar</b>baz' },
+
+ { 'id': 'B-1_SA',
+ 'desc': 'Delete from position after span',
+ 'pad': 'foo<b>bar</b>^baz',
+ 'expected': 'foo<b>ba^</b>baz' },
+
+ { 'id': 'B-1_SW',
+ 'desc': 'Delete selection that wraps the whole span content',
+ 'pad': 'foo<b>[bar]</b>baz',
+ 'expected': 'foo^baz' },
+
+ { 'id': 'B-1_SO',
+ 'desc': 'Delete selection that wraps the whole span',
+ 'pad': 'foo[<b>bar</b>]baz',
+ 'expected': 'foo^baz' },
+
+ { 'id': 'B-1_SL',
+ 'desc': 'Delete oblique selection that starts before span',
+ 'pad': 'foo[bar<b>baz]quoz</b>quuz',
+ 'expected': 'foo^<b>quoz</b>quuz' },
+
+ { 'id': 'B-1_SR',
+ 'desc': 'Delete oblique selection that ends after span',
+ 'pad': 'foo<b>bar[baz</b>quoz]quuz',
+ 'expected': 'foo<b>bar^</b>quuz' },
+
+ { 'id': 'B.I-1_SM',
+ 'desc': 'Delete oblique selection that starts and ends in different spans',
+ 'pad': 'foo<b>bar[baz</b><i>qoz]quuz</i>quuuz',
+ 'expected': 'foo<b>bar^</b><i>quuz</i>quuuz' },
+
+ { 'id': 'GEN-1_SS',
+ 'desc': 'Delete at start of span with generated content',
+ 'pad': 'foo<gen>^bar</gen>baz',
+ 'expected': 'fo^<gen>bar</gen>baz' },
+
+ { 'id': 'GEN-1_SA',
+ 'desc': 'Delete from position after span with generated content',
+ 'pad': 'foo<gen>bar</gen>^baz',
+ 'expected': 'foo<gen>ba^</gen>baz' }
+ ]
+ },
+
+ { 'desc': 'delete paragraphs',
+ 'tests': [
+ { 'id': 'P2-1_SS2',
+ 'desc': 'Delete from collapsed selection at start of paragraph - should merge with previous',
+ 'pad': '<p>foobar</p><p>^bazqoz</p>',
+ 'expected': '<p>foobar^bazqoz</p>' },
+
+ { 'id': 'P2-1_SI2',
+ 'desc': 'Delete non-collapsed selection at start of paragraph - should not merge with previous',
+ 'pad': '<p>foobar</p><p>[baz]qoz</p>',
+ 'expected': '<p>foobar</p><p>^qoz</p>' },
+
+ { 'id': 'P2-1_SM',
+ 'desc': 'Delete non-collapsed selection spanning 2 paragraphs - should merge them',
+ 'pad': '<p>foo[bar</p><p>baz]qoz</p>',
+ 'expected': '<p>foo^qoz</p>' }
+ ]
+ },
+
+ { 'desc': 'delete lists and list items',
+ 'tests': [
+ { 'id': 'OL-LI2-1_SO1',
+ 'desc': 'Delete fully wrapped list item',
+ 'pad': 'foo<ol>{<li>bar</li>}<li>baz</li></ol>qoz',
+ 'expected': ['foo<ol>|<li>baz</li></ol>qoz',
+ 'foo<ol><li>^baz</li></ol>qoz'] },
+
+ { 'id': 'OL-LI2-1_SM',
+ 'desc': 'Delete oblique range between list items within same list',
+ 'pad': 'foo<ol><li>ba[r</li><li>b]az</li></ol>qoz',
+ 'expected': 'foo<ol><li>ba^az</li></ol>qoz' },
+
+ { 'id': 'OL-LI-1_SW',
+ 'desc': 'Delete contents of last list item (list should remain)',
+ 'pad': 'foo<ol><li>[foo]</li></ol>qoz',
+ 'expected': ['foo<ol><li>|</li></ol>qoz',
+ 'foo<ol><li>^</li></ol>qoz'] },
+
+ { 'id': 'OL-LI-1_SO',
+ 'desc': 'Delete last list item of list (should remove entire list)',
+ 'pad': 'foo<ol>{<li>foo</li>}</ol>qoz',
+ 'expected': 'foo^qoz' }
+ ]
+ },
+
+ { 'desc': 'delete with strange selections',
+ 'tests': [
+ { 'id': 'HR.BR-1_SM',
+ 'desc': 'Delete selection that starts and ends within nodes that don\'t have children',
+ 'pad': 'foo<hr {>bar<br }>baz',
+ 'expected': 'foo<hr>|<br>baz' }
+ ]
+ },
+
+ { 'desc': 'delete after table',
+ 'tests': [
+ { 'id': 'TABLE-1_SA',
+ 'desc': 'Delete from position immediately after table (should have no effect)',
+ 'pad': 'foo<table><tbody><tr><td>bar</td></tr></tbody></table>^baz',
+ 'expected': 'foo<table><tbody><tr><td>bar</td></tr></tbody></table>^baz' }
+ ]
+ },
+
+ { 'desc': 'delete within table cells',
+ 'tests': [
+ { 'id': 'TD-1_SS',
+ 'desc': 'Delete from start of first cell (should have no effect)',
+ 'pad': 'foo<table><tbody><tr><td>^bar</td></tr></tbody></table>baz',
+ 'expected': 'foo<table><tbody><tr><td>^bar</td></tr></tbody></table>baz' },
+
+ { 'id': 'TD2-1_SS2',
+ 'desc': 'Delete from start of inner cell (should have no effect)',
+ 'pad': 'foo<table><tbody><tr><td>bar</td><td>^baz</td></tr></tbody></table>quoz',
+ 'expected': 'foo<table><tbody><tr><td>bar</td><td>^baz</td></tr></tbody></table>quoz' },
+
+ { 'id': 'TD2-1_SM',
+ 'desc': 'Delete with selection spanning 2 cells',
+ 'pad': 'foo<table><tbody><tr><td>ba[r</td><td>b]az</td></tr></tbody></table>quoz',
+ 'expected': 'foo<table><tbody><tr><td>ba^</td><td>az</td></tr></tbody></table>quoz' }
+ ]
+ },
+
+ { 'desc': 'delete table rows',
+ 'tests': [
+ { 'id': 'TR3-1_SO1',
+ 'desc': 'Delete first table row',
+ 'pad': '<table><tbody>{<tr><td>A</td></tr>}<tr><td>B</td></tr><tr><td>C</td></tr></tbody></table>',
+ 'expected': ['<table><tbody>|<tr><td>B</td></tr><tr><td>C</td></tr></tbody></table>',
+ '<table><tbody><tr><td>^B</td></tr><tr><td>C</td></tr></tbody></table>'] },
+
+ { 'id': 'TR3-1_SO2',
+ 'desc': 'Delete middle table row',
+ 'pad': '<table><tbody><tr><td>A</td></tr>{<tr><td>B</td></tr>}<tr><td>C</td></tr></tbody></table>',
+ 'expected': ['<table><tbody><tr><td>A</td></tr>|<tr><td>C</td></tr></tbody></table>',
+ '<table><tbody><tr><td>A</td></tr><tr><td>^C</td></tr></tbody></table>'] },
+
+ { 'id': 'TR3-1_SO3',
+ 'desc': 'Delete last table row',
+ 'pad': '<table><tbody><tr><td>A</td></tr><tr><td>B</td></tr>{<tr><td>C</td></tr>}</tbody></table>',
+ 'expected': ['<table><tbody><tr><td>A</td></tr><tr><td>B</td></tr>|</tbody></table>',
+ '<table><tbody><tr><td>A</td></tr><tr><td>B^</td></tr></tbody></table>'] },
+
+ { 'id': 'TR2rs:2-1_SO1',
+ 'desc': 'Delete first table row where a cell has rowspan 2',
+ 'pad': '<table><tbody>{<tr><td>A</td><td rowspan=2>R</td></tr>}<tr><td>B</td></tr></tbody></table>',
+ 'expected': ['<table><tbody>|<tr><td>B</td><td>R</td></tr></tbody></table>',
+ '<table><tbody><tr><td>^B</td><td>R</td></tr></tbody></table>'] },
+
+ { 'id': 'TR2rs:2-1_SO2',
+ 'desc': 'Delete second table row where a cell has rowspan 2',
+ 'pad': '<table><tbody><tr><td>A</td><td rowspan=2>R</td></tr>{<tr><td>B</td></tr>}</tbody></table>',
+ 'expected': ['<table><tbody><tr><td>A</td><td>R</td></tr>|</tbody></table>',
+ '<table><tbody><tr><td>A</td><td>R^</td></tr></tbody></table>'] },
+
+ { 'id': 'TR3rs:3-1_SO1',
+ 'desc': 'Delete first table row where a cell has rowspan 3',
+ 'pad': '<table><tbody>{<tr><td>A</td><td rowspan=3>R</td></tr>}<tr><td>B</td></tr><tr><td>C</td></tr></tbody></table>',
+ 'expected': ['<table><tbody>|<tr><td>A</td><td rowspan="2">R</td></tr><tr><td>C</td></tr></tbody></table>',
+ '<table><tbody><tr><td>^A</td><td rowspan="2">R</td></tr><tr><td>C</td></tr></tbody></table>'] },
+
+ { 'id': 'TR3rs:3-1_SO2',
+ 'desc': 'Delete middle table row where a cell has rowspan 3',
+ 'pad': '<table><tbody><tr><td>A</td><td rowspan=3>R</td></tr>{<tr><td>B</td></tr>}<tr><td>C</td></tr></tbody></table>',
+ 'expected': ['<table><tbody><tr><td>B</td><td rowspan="2">R</td></tr>|<tr><td>C</td></tr></tbody></table>',
+ '<table><tbody><tr><td>B</td><td rowspan="2">R</td></tr><tr><td>^C</td></tr></tbody></table>'] },
+
+ { 'id': 'TR3rs:3-1_SO3',
+ 'desc': 'Delete last table row where a cell has rowspan 3',
+ 'pad': '<table><tbody><tr><td>A</td><td rowspan=3>R</td></tr><tr><td>B</td></tr>{<tr><td>C</td></tr>}</tbody></table>',
+ 'expected': ['<table><tbody><tr><td>A</td><td rowspan="2">R</td></tr><tr><td>B</td></tr>|</tbody></table>',
+ '<table><tbody><tr><td>A</td><td rowspan="2">R</td></tr><tr><td>B^</td></tr></tbody></table>'] }
+ ]
+ },
+
+ { 'desc': 'delete with non-editable nested content',
+ 'tests': [
+ { 'id': 'DIV:ce:false-1_SO',
+ 'desc': 'Delete nested non-editable <div>',
+ 'pad': 'foo[bar<div contenteditable="false">NESTED</div>baz]qoz',
+ 'expected': 'foo^qoz' },
+
+ { 'id': 'DIV:ce:false-1_SB',
+ 'desc': 'Delete from immediately after a nested non-editable <div> (should be no-op)',
+ 'pad': 'foobar<div contenteditable="false">NESTED</div>^bazqoz',
+ 'expected': 'foobar<div contenteditable="false">NESTED</div>^bazqoz' },
+
+ { 'id': 'DIV:ce:false-1_SL',
+ 'desc': 'Delete nested non-editable <div> with oblique selection',
+ 'pad': 'foo[bar<div contenteditable="false">NES]TED</div>bazqoz',
+ 'expected': [ 'foo^<div contenteditable="false">NESTED</div>bazqoz',
+ 'foo<div contenteditable="false">[NES]TED</div>bazqoz' ] },
+
+ { 'id': 'DIV:ce:false-1_SR',
+ 'desc': 'Delete nested non-editable <div> with oblique selection',
+ 'pad': 'foobar<div contenteditable="false">NES[TED</div>baz]qoz',
+ 'expected': [ 'foobar<div contenteditable="false">NESTED</div>^qoz',
+ 'foobar<div contenteditable="false">NES[TED]</div>qoz' ] },
+
+ { 'id': 'DIV:ce:false-1_SI',
+ 'desc': 'Delete inside nested non-editable <div> (should be no-op)',
+ 'pad': 'foobar<div contenteditable="false">NE[ST]ED</div>bazqoz',
+ 'expected': 'foobar<div contenteditable="false">NE[ST]ED</div>bazqoz' }
+ ]
+ },
+
+ { 'desc': 'Delete with display:inline-block',
+ 'checkStyle': True,
+ 'tests': [
+ { 'id': 'SPAN:d:ib-1_SC',
+ 'desc': 'Delete inside an inline-block <span>',
+ 'pad': 'foo<span style="display: inline-block">bar^baz</span>qoz',
+ 'expected': 'foo<span style="display: inline-block">ba^baz</span>qoz' },
+
+ { 'id': 'SPAN:d:ib-1_SA',
+ 'desc': 'Delete from immediately after an inline-block <span>',
+ 'pad': 'foo<span style="display: inline-block">barbaz</span>^qoz',
+ 'expected': 'foo<span style="display: inline-block">barba^</span>qoz' },
+
+ { 'id': 'SPAN:d:ib-2_SL',
+ 'desc': 'Delete with nested inline-block <span>, oblique selection',
+ 'pad': 'foo[DEL<span style="display: inline-block">ETE]bar</span>baz',
+ 'expected': 'foo^<span style="display: inline-block">bar</span>baz' },
+
+ { 'id': 'SPAN:d:ib-3_SR',
+ 'desc': 'Delete with nested inline-block <span>, oblique selection',
+ 'pad': 'foo<span style="display: inline-block">bar[DEL</span>ETE]baz',
+ 'expected': 'foo<span style="display: inline-block">bar^</span>baz' },
+
+ { 'id': 'SPAN:d:ib-4i_SI',
+ 'desc': 'Delete with nested inline-block <span>, oblique selection',
+ 'pad': 'foo<span style="display: inline-block">bar[DELETE]baz</span>qoz',
+ 'expected': 'foo<span style="display: inline-block">bar^baz</span>qoz' },
+
+ { 'id': 'SPAN:d:ib-4l_SI',
+ 'desc': 'Delete with nested inline-block <span>, oblique selection',
+ 'pad': 'foo<span style="display: inline-block">[DELETE]barbaz</span>qoz',
+ 'expected': 'foo<span style="display: inline-block">^barbaz</span>qoz' },
+
+ { 'id': 'SPAN:d:ib-4r_SI',
+ 'desc': 'Delete with nested inline-block <span>, oblique selection',
+ 'pad': 'foo<span style="display: inline-block">barbaz[DELETE]</span>qoz',
+ 'expected': 'foo<span style="display: inline-block">barbaz^</span>qoz' }
+ ]
+ }
+ ]
+}
+
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/forwarddelete.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/forwarddelete.py
new file mode 100644
index 000000000..d625a2a7d
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/forwarddelete.py
@@ -0,0 +1,315 @@
+
+FORWARDDELETE_TESTS = {
+ 'id': 'FD',
+ 'caption': 'Forward-Delete Tests',
+ 'command': 'forwardDelete',
+ 'checkAttrs': True,
+ 'checkStyle': False,
+
+ 'Proposed': [
+ { 'desc': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': 'forward-delete single characters',
+ 'tests': [
+ { 'id': 'CHAR-1_SC',
+ 'desc': 'Delete 1 character',
+ 'pad': 'foo^barbaz',
+ 'expected': 'foo^arbaz' },
+
+ { 'id': 'CHAR-2_SC',
+ 'desc': 'Delete 1 pre-composed character o with diaeresis',
+ 'pad': 'fo^&#xF6;barbaz',
+ 'expected': 'fo^barbaz' },
+
+ { 'id': 'CHAR-3_SC',
+ 'desc': 'Delete 1 character with combining diaeresis above',
+ 'pad': 'fo^o&#x0308;barbaz',
+ 'expected': 'fo^barbaz' },
+
+ { 'id': 'CHAR-4_SC',
+ 'desc': 'Delete 1 character with combining diaeresis below',
+ 'pad': 'fo^o&#x0324;barbaz',
+ 'expected': 'fo^barbaz' },
+
+ { 'id': 'CHAR-5_SC',
+ 'desc': 'Delete 1 character with combining diaeresis above and below',
+ 'pad': 'fo^o&#x0308;&#x0324;barbaz',
+ 'expected': 'fo^barbaz' },
+
+ { 'id': 'CHAR-6_SC',
+ 'desc': 'Delete 1 character with enclosing square',
+ 'pad': 'fo^o&#x20DE;barbaz',
+ 'expected': 'fo^barbaz' },
+
+ { 'id': 'CHAR-7_SC',
+ 'desc': 'Delete 1 character with combining long solidus overlay',
+ 'pad': 'fo^o&#x0338;barbaz',
+ 'expected': 'fo^barbaz' }
+ ]
+ },
+
+ { 'desc': 'forward-delete text selections',
+ 'tests': [
+ { 'id': 'TEXT-1_SI',
+ 'desc': 'Delete text selection',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo^baz' },
+
+ { 'id': 'B-1_SE',
+ 'desc': 'Forward-delete at end of span',
+ 'pad': 'foo<b>bar^</b>baz',
+ 'expected': 'foo<b>bar^</b>az' },
+
+ { 'id': 'B-1_SB',
+ 'desc': 'Forward-delete from position before span',
+ 'pad': 'foo^<b>bar</b>baz',
+ 'expected': 'foo^<b>ar</b>baz' },
+
+ { 'id': 'B-1_SW',
+ 'desc': 'Delete selection that wraps the whole span content',
+ 'pad': 'foo<b>[bar]</b>baz',
+ 'expected': 'foo^baz' },
+
+ { 'id': 'B-1_SO',
+ 'desc': 'Delete selection that wraps the whole span',
+ 'pad': 'foo[<b>bar</b>]baz',
+ 'expected': 'foo^baz' },
+
+ { 'id': 'B-1_SL',
+ 'desc': 'Delete oblique selection that starts before span',
+ 'pad': 'foo[bar<b>baz]quoz</b>quuz',
+ 'expected': 'foo^<b>quoz</b>quuz' },
+
+ { 'id': 'B-1_SR',
+ 'desc': 'Delete oblique selection that ends after span',
+ 'pad': 'foo<b>bar[baz</b>quoz]quuz',
+ 'expected': 'foo<b>bar^</b>quuz' },
+
+ { 'id': 'B.I-1_SM',
+ 'desc': 'Delete oblique selection that starts and ends in different spans',
+ 'pad': 'foo<b>bar[baz</b><i>qoz]quuz</i>quuuz',
+ 'expected': 'foo<b>bar^</b><i>quuz</i>quuuz' },
+
+ { 'id': 'GEN-1_SE',
+ 'desc': 'Delete at end of span with generated content',
+ 'pad': 'foo<gen>bar^</gen>baz',
+ 'expected': 'foo<gen>bar^</gen>az' },
+
+ { 'id': 'GEN-1_SB',
+ 'desc': 'Delete from position before span with generated content',
+ 'pad': 'foo^<gen>bar</gen>baz',
+ 'expected': 'foo^<gen>ar</gen>baz' }
+ ]
+ },
+
+ { 'desc': 'forward-delete paragraphs',
+ 'tests': [
+ { 'id': 'P2-1_SE1',
+ 'desc': 'Delete from collapsed selection at end of paragraph - should merge with next',
+ 'pad': '<p>foobar^</p><p>bazqoz</p>',
+ 'expected': '<p>foobar^bazqoz</p>' },
+
+ { 'id': 'P2-1_SI1',
+ 'desc': 'Delete non-collapsed selection at end of paragraph - should not merge with next',
+ 'pad': '<p>foo[bar]</p><p>bazqoz</p>',
+ 'expected': '<p>foo^</p><p>bazqoz</p>' },
+
+ { 'id': 'P2-1_SM',
+ 'desc': 'Delete non-collapsed selection spanning 2 paragraphs - should merge them',
+ 'pad': '<p>foo[bar</p><p>baz]qoz</p>',
+ 'expected': '<p>foo^qoz</p>' }
+ ]
+ },
+
+ { 'desc': 'forward-delete lists and list items',
+ 'tests': [
+ { 'id': 'OL-LI2-1_SO1',
+ 'desc': 'Delete fully wrapped list item',
+ 'pad': 'foo<ol>{<li>bar</li>}<li>baz</li></ol>qoz',
+ 'expected': ['foo<ol>|<li>baz</li></ol>qoz',
+ 'foo<ol><li>^baz</li></ol>qoz'] },
+
+ { 'id': 'OL-LI2-1_SM',
+ 'desc': 'Delete oblique range between list items within same list',
+ 'pad': 'foo<ol><li>ba[r</li><li>b]az</li></ol>qoz',
+ 'expected': 'foo<ol><li>ba^az</li></ol>qoz' },
+
+ { 'id': 'OL-LI-1_SW',
+ 'desc': 'Delete contents of last list item (list should remain)',
+ 'pad': 'foo<ol><li>[foo]</li></ol>qoz',
+ 'expected': ['foo<ol><li>|</li></ol>qoz',
+ 'foo<ol><li>^</li></ol>qoz'] },
+
+ { 'id': 'OL-LI-1_SO',
+ 'desc': 'Delete last list item of list (should remove entire list)',
+ 'pad': 'foo<ol>{<li>foo</li>}</ol>qoz',
+ 'expected': 'foo^qoz' }
+ ]
+ },
+
+ { 'desc': 'forward-delete with strange selections',
+ 'tests': [
+ { 'id': 'HR.BR-1_SM',
+ 'desc': 'Delete selection that starts and ends within nodes that don\'t have children',
+ 'pad': 'foo<hr {>bar<br }>baz',
+ 'expected': 'foo<hr>|<br>baz' }
+ ]
+ },
+
+ { 'desc': 'forward-delete from immediately before a table',
+ 'tests': [
+ { 'id': 'TABLE-1_SB',
+ 'desc': 'Delete from position immediately before table (should have no effect)',
+ 'pad': 'foo^<table><tbody><tr><td>bar</td></tr></tbody></table>baz',
+ 'expected': 'foo^<table><tbody><tr><td>bar</td></tr></tbody></table>baz' }
+ ]
+ },
+
+ { 'desc': 'forward-delete within table cells',
+ 'tests': [
+ { 'id': 'TD-1_SE',
+ 'desc': 'Delete from end of last cell (should have no effect)',
+ 'pad': 'foo<table><tbody><tr><td>bar^</td></tr></tbody></table>baz',
+ 'expected': 'foo<table><tbody><tr><td>bar^</td></tr></tbody></table>baz' },
+
+ { 'id': 'TD2-1_SE1',
+ 'desc': 'Delete from end of inner cell (should have no effect)',
+ 'pad': 'foo<table><tbody><tr><td>bar^</td><td>baz</td></tr></tbody></table>quoz',
+ 'expected': 'foo<table><tbody><tr><td>bar^</td><td>baz</td></tr></tbody></table>quoz' },
+
+ { 'id': 'TD2-1_SM',
+ 'desc': 'Delete with selection spanning 2 cells',
+ 'pad': 'foo<table><tbody><tr><td>ba[r</td><td>b]az</td></tr></tbody></table>quoz',
+ 'expected': 'foo<table><tbody><tr><td>ba^</td><td>az</td></tr></tbody></table>quoz' }
+ ]
+ },
+
+ { 'desc': 'forward-delete table rows',
+ 'tests': [
+ { 'id': 'TR3-1_SO1',
+ 'desc': 'Delete first table row',
+ 'pad': '<table><tbody>{<tr><td>A</td></tr>}<tr><td>B</td></tr><tr><td>C</td></tr></tbody></table>',
+ 'expected': ['<table><tbody>|<tr><td>B</td></tr><tr><td>C</td></tr></tbody></table>',
+ '<table><tbody><tr><td>^B</td></tr><tr><td>C</td></tr></tbody></table>'] },
+
+ { 'id': 'TR3-1_SO2',
+ 'desc': 'Delete middle table row',
+ 'pad': '<table><tbody><tr><td>A</td></tr>{<tr><td>B</td></tr>}<tr><td>C</td></tr></tbody></table>',
+ 'expected': ['<table><tbody><tr><td>A</td></tr>|<tr><td>C</td></tr></tbody></table>',
+ '<table><tbody><tr><td>A</td></tr><tr><td>^C</td></tr></tbody></table>'] },
+
+ { 'id': 'TR3-1_SO3',
+ 'desc': 'Delete last table row',
+ 'pad': '<table><tbody><tr><td>A</td></tr><tr><td>B</td></tr>{<tr><td>C</td></tr>}</tbody></table>',
+ 'expected': ['<table><tbody><tr><td>A</td></tr><tr><td>B</td></tr>|</tbody></table>',
+ '<table><tbody><tr><td>A</td></tr><tr><td>B^</td></tr></tbody></table>'] },
+
+ { 'id': 'TR2rs:2-1_SO1',
+ 'desc': 'Delete first table row where a cell has rowspan 2',
+ 'pad': '<table><tbody>{<tr><td>A</td><td rowspan=2>R</td></tr>}<tr><td>B</td></tr></tbody></table>',
+ 'expected': ['<table><tbody>|<tr><td>B</td><td>R</td></tr></tbody></table>',
+ '<table><tbody><tr><td>^B</td><td>R</td></tr></tbody></table>'] },
+
+ { 'id': 'TR2rs:2-1_SO2',
+ 'desc': 'Delete second table row where a cell has rowspan 2',
+ 'pad': '<table><tbody><tr><td>A</td><td rowspan=2>R</td></tr>{<tr><td>B</td></tr>}</tbody></table>',
+ 'expected': ['<table><tbody><tr><td>A</td><td>R</td></tr>|</tbody></table>',
+ '<table><tbody><tr><td>A</td><td>R^</td></tr></tbody></table>'] },
+
+ { 'id': 'TR3rs:3-1_SO1',
+ 'desc': 'Delete first table row where a cell has rowspan 3',
+ 'pad': '<table><tbody>{<tr><td>A</td><td rowspan=3>R</td></tr>}<tr><td>B</td></tr><tr><td>C</td></tr></tbody></table>',
+ 'expected': ['<table><tbody>|<tr><td>A</td><td rowspan="2">R</td></tr><tr><td>C</td></tr></tbody></table>',
+ '<table><tbody><tr><td>^A</td><td rowspan="2">R</td></tr><tr><td>C</td></tr></tbody></table>'] },
+
+ { 'id': 'TR3rs:3-1_SO2',
+ 'desc': 'Delete middle table row where a cell has rowspan 3',
+ 'pad': '<table><tbody><tr><td>A</td><td rowspan=3>R</td></tr>{<tr><td>B</td></tr>}<tr><td>C</td></tr></tbody></table>',
+ 'expected': ['<table><tbody><tr><td>B</td><td rowspan="2">R</td></tr>|<tr><td>C</td></tr></tbody></table>',
+ '<table><tbody><tr><td>B</td><td rowspan="2">R</td></tr><tr><td>^C</td></tr></tbody></table>'] },
+
+ { 'id': 'TR3rs:3-1_SO3',
+ 'desc': 'Delete last table row where a cell has rowspan 3',
+ 'pad': '<table><tbody><tr><td>A</td><td rowspan=3>R</td></tr><tr><td>B</td></tr>{<tr><td>C</td></tr>}</tbody></table>',
+ 'expected': ['<table><tbody><tr><td>A</td><td rowspan="2">R</td></tr><tr><td>B</td></tr>|</tbody></table>',
+ '<table><tbody><tr><td>A</td><td rowspan="2">R</td></tr><tr><td>B^</td></tr></tbody></table>'] }
+ ]
+ },
+
+ { 'desc': 'delete with non-editable nested content',
+ 'tests': [
+ { 'id': 'DIV:ce:false-1_SO',
+ 'desc': 'Delete nested non-editable <div>',
+ 'pad': 'foo[bar<div contenteditable="false">NESTED</div>baz]qoz',
+ 'expected': 'foo^qoz' },
+
+ { 'id': 'DIV:ce:false-1_SB',
+ 'desc': 'Delete from immediately before a nested non-editable <div> (should be no-op)',
+ 'pad': 'foobar^<div contenteditable="false">NESTED</div>bazqoz',
+ 'expected': 'foobar^<div contenteditable="false">NESTED</div>bazqoz' },
+
+ { 'id': 'DIV:ce:false-1_SL',
+ 'desc': 'Delete nested non-editable <div> with oblique selection',
+ 'pad': 'foo[bar<div contenteditable="false">NES]TED</div>bazqoz',
+ 'expected': [ 'foo^<div contenteditable="false">NESTED</div>bazqoz',
+ 'foo<div contenteditable="false">[NES]TED</div>bazqoz' ] },
+
+ { 'id': 'DIV:ce:false-1_SR',
+ 'desc': 'Delete nested non-editable <div> with oblique selection',
+ 'pad': 'foobar<div contenteditable="false">NES[TED</div>baz]qoz',
+ 'expected': [ 'foobar<div contenteditable="false">NESTED</div>^qoz',
+ 'foobar<div contenteditable="false">NES[TED]</div>qoz' ] },
+
+ { 'id': 'DIV:ce:false-1_SI',
+ 'desc': 'Delete inside nested non-editable <div> (should be no-op)',
+ 'pad': 'foobar<div contenteditable="false">NE[ST]ED</div>bazqoz',
+ 'expected': 'foobar<div contenteditable="false">NE[ST]ED</div>bazqoz' }
+ ]
+ },
+
+ { 'desc': 'Delete with display:inline-block',
+ 'checkStyle': True,
+ 'tests': [
+ { 'id': 'SPAN:d:ib-1_SC',
+ 'desc': 'Delete inside an inline-block <span>',
+ 'pad': 'foo<span style="display: inline-block">bar^baz</span>qoz',
+ 'expected': 'foo<span style="display: inline-block">bar^az</span>qoz' },
+
+ { 'id': 'SPAN:d:ib-1_SA',
+ 'desc': 'Delete from immediately before an inline-block <span>',
+ 'pad': 'foo^<span style="display: inline-block">barbaz</span>qoz',
+ 'expected': 'foo^<span style="display: inline-block">arbaz</span>qoz' },
+
+ { 'id': 'SPAN:d:ib-2_SL',
+ 'desc': 'Delete with nested inline-block <span>, oblique selection',
+ 'pad': 'foo[DEL<span style="display: inline-block">ETE]bar</span>baz',
+ 'expected': 'foo^<span style="display: inline-block">bar</span>baz' },
+
+ { 'id': 'SPAN:d:ib-3_SR',
+ 'desc': 'Delete with nested inline-block <span>, oblique selection',
+ 'pad': 'foo<span style="display: inline-block">bar[DEL</span>ETE]baz',
+ 'expected': 'foo<span style="display: inline-block">bar^</span>baz' },
+
+ { 'id': 'SPAN:d:ib-4i_SI',
+ 'desc': 'Delete with nested inline-block <span>, oblique selection',
+ 'pad': 'foo<span style="display: inline-block">bar[DELETE]baz</span>qoz',
+ 'expected': 'foo<span style="display: inline-block">bar^baz</span>qoz' },
+
+ { 'id': 'SPAN:d:ib-4l_SI',
+ 'desc': 'Delete with nested inline-block <span>, oblique selection',
+ 'pad': 'foo<span style="display: inline-block">[DELETE]barbaz</span>qoz',
+ 'expected': 'foo<span style="display: inline-block">^barbaz</span>qoz' },
+
+ { 'id': 'SPAN:d:ib-4r_SI',
+ 'desc': 'Delete with nested inline-block <span>, oblique selection',
+ 'pad': 'foo<span style="display: inline-block">barbaz[DELETE]</span>qoz',
+ 'expected': 'foo<span style="display: inline-block">barbaz^</span>qoz' }
+ ]
+ }
+ ]
+}
+
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/insert.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/insert.py
new file mode 100644
index 000000000..a2e79c27c
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/insert.py
@@ -0,0 +1,285 @@
+
+INSERT_TESTS = {
+ 'id': 'I',
+ 'caption': 'Insert Tests',
+ 'checkAttrs': False,
+ 'checkStyle': False,
+
+ 'Proposed': [
+ { 'desc': '',
+ 'command': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': 'insert <hr>',
+ 'command': 'inserthorizontalrule',
+ 'tests': [
+ { 'id': 'IHR_TEXT-1_SC',
+ 'rte1-id': 'a-inserthorizontalrule-0',
+ 'desc': 'Insert <hr> into text',
+ 'pad': 'foo^bar',
+ 'expected': 'foo<hr>^bar',
+ 'accept': 'foo<hr>|bar' },
+
+ { 'id': 'IHR_TEXT-1_SI',
+ 'desc': 'Insert <hr>, replacing selected text',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<hr>^baz',
+ 'accept': 'foo<hr>|baz' },
+
+ { 'id': 'IHR_DIV-B-1_SX',
+ 'desc': 'Insert <hr> between elements',
+ 'pad': '<div><b>foo</b>|<b>bar</b></div>',
+ 'expected': '<div><b>foo</b><hr>|<b>bar</b></div>' },
+
+ { 'id': 'IHR_DIV-B-2_SO',
+ 'desc': 'Insert <hr>, replacing a fully wrapped element',
+ 'pad': '<div><b>foo</b>{<b>bar</b>}<b>baz</b></div>',
+ 'expected': '<div><b>foo</b><hr>|<b>baz</b></div>' },
+
+ { 'id': 'IHR_B-1_SC',
+ 'desc': 'Insert <hr> into a span, splitting it',
+ 'pad': '<b>foo^bar</b>',
+ 'expected': '<b>foo</b><hr><b>^bar</b>' },
+
+ { 'id': 'IHR_B-1_SS',
+ 'desc': 'Insert <hr> into a span at the start (should not create an empty span)',
+ 'pad': '<b>^foobar</b>',
+ 'expected': '<hr><b>^foobar</b>' },
+
+ { 'id': 'IHR_B-1_SE',
+ 'desc': 'Insert <hr> into a span at the end',
+ 'pad': '<b>foobar^</b>',
+ 'expected': [ '<b>foobar</b><hr>|',
+ '<b>foobar</b><hr><b>^</b>' ] },
+
+ { 'id': 'IHR_B-2_SL',
+ 'desc': 'Insert <hr> with oblique selection starting outside of span',
+ 'pad': 'foo[bar<b>baz]qoz</b>',
+ 'expected': 'foo<hr>|<b>qoz</b>' },
+
+ { 'id': 'IHR_B-2_SLR',
+ 'desc': 'Insert <hr> with oblique reversed selection starting outside of span',
+ 'pad': 'foo]bar<b>baz[qoz</b>',
+ 'expected': [ 'foo<hr>|<b>qoz</b>',
+ 'foo<hr><b>^qoz</b>' ] },
+
+ { 'id': 'IHR_B-3_SR',
+ 'desc': 'Insert <hr> with oblique selection ending outside of span',
+ 'pad': '<b>foo[bar</b>baz]quoz',
+ 'expected': [ '<b>foo</b><hr>|quoz',
+ '<b>foo</b><hr><b>^</b>quoz' ] },
+
+ { 'id': 'IHR_B-3_SRR',
+ 'desc': 'Insert <hr> with oblique reversed selection starting outside of span',
+ 'pad': '<b>foo]bar</b>baz[quoz',
+ 'expected': '<b>foo</b><hr>|quoz' },
+
+ { 'id': 'IHR_B-I-1_SM',
+ 'desc': 'Insert <hr> with oblique selection between different spans',
+ 'pad': '<b>foo[bar</b><i>baz]quoz</i>',
+ 'expected': [ '<b>foo</b><hr>|<i>quoz</i>',
+ '<b>foo</b><hr><b>^</b><i>quoz</i>' ] },
+
+ { 'id': 'IHR_B-I-1_SMR',
+ 'desc': 'Insert <hr> with reversed oblique selection between different spans',
+ 'pad': '<b>foo]bar</b><i>baz[quoz</i>',
+ 'expected': '<b>foo</b><hr><i>^quoz</i>' },
+
+ { 'id': 'IHR_P-1_SC',
+ 'desc': 'Insert <hr> into a paragraph, splitting it',
+ 'pad': '<p>foo^bar</p>',
+ 'expected': [ '<p>foo</p><hr>|<p>bar</p>',
+ '<p>foo</p><hr><p>^bar</p>' ] },
+
+ { 'id': 'IHR_P-1_SS',
+ 'desc': 'Insert <hr> into a paragraph at the start (should not create an empty span)',
+ 'pad': '<p>^foobar</p>',
+ 'expected': [ '<hr>|<p>foobar</p>',
+ '<hr><p>^foobar</p>' ] },
+
+ { 'id': 'IHR_P-1_SE',
+ 'desc': 'Insert <hr> into a paragraph at the end (should not create an empty span)',
+ 'pad': '<p>foobar^</p>',
+ 'expected': '<p>foobar</p><hr>|' }
+ ]
+ },
+
+ { 'desc': 'insert <p>',
+ 'command': 'insertparagraph',
+ 'tests': [
+ { 'id': 'IP_P-1_SC',
+ 'desc': 'Split paragraph',
+ 'pad': '<p>foo^bar</p>',
+ 'expected': '<p>foo</p><p>^bar</p>' },
+
+ { 'id': 'IP_UL-LI-1_SC',
+ 'desc': 'Split list item',
+ 'pad': '<ul><li>foo^bar</li></ul>',
+ 'expected': '<ul><li>foo</li><li>^bar</li></ul>' }
+ ]
+ },
+
+ { 'desc': 'insert text',
+ 'command': 'inserttext',
+ 'tests': [
+ { 'id': 'ITEXT:text_TEXT-1_SC',
+ 'desc': 'Insert text',
+ 'value': 'text',
+ 'pad': 'foo^bar',
+ 'expected': 'footext^bar' },
+
+ { 'id': 'ITEXT:text_TEXT-1_SI',
+ 'desc': 'Insert text, replacing selected text',
+ 'value': 'text',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'footext^baz' }
+ ]
+ },
+
+ { 'desc': 'insert <br>',
+ 'command': 'insertlinebreak',
+ 'tests': [
+ { 'id': 'IBR_TEXT-1_SC',
+ 'desc': 'Insert <br> into text',
+ 'pad': 'foo^bar',
+ 'expected': [ 'foo<br>|bar',
+ 'foo<br>^bar' ] },
+
+ { 'id': 'IBR_TEXT-1_SI',
+ 'desc': 'Insert <br>, replacing selected text',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<br>|baz',
+ 'foo<br>^baz' ] },
+
+ { 'id': 'IBR_LI-1_SC',
+ 'desc': 'Insert <br> within list item',
+ 'pad': '<ul><li>foo^bar</li></ul>',
+ 'expected': '<ul><li>foo<br>^bar</li></ul>' }
+ ]
+ },
+
+ { 'desc': 'insert <img>',
+ 'command': 'insertimage',
+ 'tests': [
+ { 'id': 'IIMG:url_TEXT-1_SC',
+ 'rte1-id': 'a-insertimage-0',
+ 'desc': 'Insert image with URL "bar.png"',
+ 'value': 'bar.png',
+ 'checkAttrs': True,
+ 'pad': 'foo^bar',
+ 'expected': [ 'foo<img src="bar.png">|bar',
+ 'foo<img src="bar.png">^bar' ] },
+
+ { 'id': 'IIMG:url_IMG-1_SO',
+ 'desc': 'Change existing image to new URL, selection on <img>',
+ 'value': 'quz.png',
+ 'checkAttrs': True,
+ 'pad': '<span>foo{<img src="bar.png">}bar</span>',
+ 'expected': [ '<span>foo<img src="quz.png"/>|bar</span>',
+ '<span>foo<img src="quz.png"/>^bar</span>' ] },
+
+ { 'id': 'IIMG:url_SPAN-IMG-1_SO',
+ 'desc': 'Change existing image to new URL, selection in text surrounding <img>',
+ 'value': 'quz.png',
+ 'checkAttrs': True,
+ 'pad': 'foo[<img src="bar.png">]bar',
+ 'expected': [ 'foo<img src="quz.png"/>|bar',
+ 'foo<img src="quz.png"/>^bar' ] },
+
+ { 'id': 'IIMG:._SPAN-IMG-1_SO',
+ 'desc': 'Remove existing image or URL, selection on <img>',
+ 'value': '',
+ 'checkAttrs': True,
+ 'pad': '<span>foo{<img src="bar.png">}bar</span>',
+ 'expected': [ '<span>foo^bar</span>',
+ '<span>foo<img>|bar</span>',
+ '<span>foo<img>^bar</span>',
+ '<span>foo<img src="">|bar</span>',
+ '<span>foo<img src="">^bar</span>' ] },
+
+ { 'id': 'IIMG:._IMG-1_SO',
+ 'desc': 'Remove existing image or URL, selection in text surrounding <img>',
+ 'value': '',
+ 'checkAttrs': True,
+ 'pad': 'foo[<img src="bar.png">]bar',
+ 'expected': [ 'foo^bar',
+ 'foo<img>|bar',
+ 'foo<img>^bar',
+ 'foo<img src="">|bar',
+ 'foo<img src="">^bar' ] }
+ ]
+ },
+
+ { 'desc': 'insert <ol>',
+ 'command': 'insertorderedlist',
+ 'tests': [
+ { 'id': 'IOL_TEXT-1_SC',
+ 'rte1-id': 'a-insertorderedlist-0',
+ 'desc': 'Insert ordered list on collapsed selection',
+ 'pad': 'foo^bar',
+ 'expected': '<ol><li>foo^bar</li></ol>' },
+
+ { 'id': 'IOL_TEXT-1_SI',
+ 'desc': 'Insert ordered list on selected text',
+ 'pad': 'foo[bar]baz',
+ 'expected': '<ol><li>foo[bar]baz</li></ol>' }
+ ]
+ },
+
+ { 'desc': 'insert <ul>',
+ 'command': 'insertunorderedlist',
+ 'tests': [
+ { 'id': 'IUL_TEXT-1_SC',
+ 'desc': 'Insert unordered list on collapsed selection',
+ 'pad': 'foo^bar',
+ 'expected': '<ul><li>foo^bar</li></ul>' },
+
+ { 'id': 'IUL_TEXT-1_SI',
+ 'rte1-id': 'a-insertunorderedlist-0',
+ 'desc': 'Insert unordered list on selected text',
+ 'pad': 'foo[bar]baz',
+ 'expected': '<ul><li>foo[bar]baz</li></ul>' }
+ ]
+ },
+
+ { 'desc': 'insert arbitrary HTML',
+ 'command': 'inserthtml',
+ 'tests': [
+ { 'id': 'IHTML:BR_TEXT-1_SC',
+ 'rte1-id': 'a-inserthtml-0',
+ 'desc': 'InsertHTML: <br>',
+ 'value': '<br>',
+ 'pad': 'foo^barbaz',
+ 'expected': 'foo<br>^barbaz' },
+
+ { 'id': 'IHTML:text_TEXT-1_SI',
+ 'desc': 'InsertHTML: "NEW"',
+ 'value': 'NEW',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'fooNEW^baz' },
+
+ { 'id': 'IHTML:S_TEXT-1_SI',
+ 'desc': 'InsertHTML: "<span>NEW<span>"',
+ 'value': '<span>NEW</span>',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<span>NEW</span>^baz' },
+
+ { 'id': 'IHTML:H1.H2_TEXT-1_SI',
+ 'desc': 'InsertHTML: "<h1>NEW</h1><h2>HTML</h2>"',
+ 'value': '<h1>NEW</h1><h2>HTML</h2>',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<h1>NEW</h1><h2>HTML</h2>^baz' },
+
+ { 'id': 'IHTML:P-B_TEXT-1_SI',
+ 'desc': 'InsertHTML: "<p>NEW<b>HTML</b>!</p>"',
+ 'value': '<p>NEW<b>HTML</b>!</p>',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<p>NEW<b>HTML</b>!</p>^baz' }
+ ]
+ }
+ ]
+}
+
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryEnabled.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryEnabled.py
new file mode 100644
index 000000000..eb721923b
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryEnabled.py
@@ -0,0 +1,215 @@
+
+QUERYENABLED_TESTS = {
+ 'id': 'QE',
+ 'caption': 'queryCommandEnabled Tests',
+ 'pad': 'foo[bar]baz',
+ 'checkAttrs': False,
+ 'checkStyle': False,
+ 'styleWithCSS': False,
+ 'expected': True,
+
+ 'Proposed': [
+ { 'desc': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': 'HTML5 commands',
+ 'tests': [
+ { 'id': 'SELECTALL_TEXT-1',
+ 'desc': 'check whether the "selectall" command is enabled',
+ 'qcenabled': 'selectall' },
+
+ { 'id': 'UNSELECT_TEXT-1',
+ 'desc': 'check whether the "unselect" command is enabled',
+ 'qcenabled': 'unselect' },
+
+ { 'id': 'UNDO_TEXT-1',
+ 'desc': 'check whether the "undo" command is enabled',
+ 'qcenabled': 'undo' },
+
+ { 'id': 'REDO_TEXT-1',
+ 'desc': 'check whether the "redo" command is enabled',
+ 'qcenabled': 'redo' },
+
+ { 'id': 'BOLD_TEXT-1',
+ 'desc': 'check whether the "bold" command is enabled',
+ 'qcenabled': 'bold' },
+
+ { 'id': 'ITALIC_TEXT-1',
+ 'desc': 'check whether the "italic" command is enabled',
+ 'qcenabled': 'italic' },
+
+ { 'id': 'UNDERLINE_TEXT-1',
+ 'desc': 'check whether the "underline" command is enabled',
+ 'qcenabled': 'underline' },
+
+ { 'id': 'STRIKETHROUGH_TEXT-1',
+ 'desc': 'check whether the "strikethrough" command is enabled',
+ 'qcenabled': 'strikethrough' },
+
+ { 'id': 'SUBSCRIPT_TEXT-1',
+ 'desc': 'check whether the "subscript" command is enabled',
+ 'qcenabled': 'subscript' },
+
+ { 'id': 'SUPERSCRIPT_TEXT-1',
+ 'desc': 'check whether the "superscript" command is enabled',
+ 'qcenabled': 'superscript' },
+
+ { 'id': 'FORMATBLOCK_TEXT-1',
+ 'desc': 'check whether the "formatblock" command is enabled',
+ 'qcenabled': 'formatblock' },
+
+ { 'id': 'CREATELINK_TEXT-1',
+ 'desc': 'check whether the "createlink" command is enabled',
+ 'qcenabled': 'createlink' },
+
+ { 'id': 'UNLINK_TEXT-1',
+ 'desc': 'check whether the "unlink" command is enabled',
+ 'qcenabled': 'unlink' },
+
+ { 'id': 'INSERTHTML_TEXT-1',
+ 'desc': 'check whether the "inserthtml" command is enabled',
+ 'qcenabled': 'inserthtml' },
+
+ { 'id': 'INSERTHORIZONTALRULE_TEXT-1',
+ 'desc': 'check whether the "inserthorizontalrule" command is enabled',
+ 'qcenabled': 'inserthorizontalrule' },
+
+ { 'id': 'INSERTIMAGE_TEXT-1',
+ 'desc': 'check whether the "insertimage" command is enabled',
+ 'qcenabled': 'insertimage' },
+
+ { 'id': 'INSERTLINEBREAK_TEXT-1',
+ 'desc': 'check whether the "insertlinebreak" command is enabled',
+ 'qcenabled': 'insertlinebreak' },
+
+ { 'id': 'INSERTPARAGRAPH_TEXT-1',
+ 'desc': 'check whether the "insertparagraph" command is enabled',
+ 'qcenabled': 'insertparagraph' },
+
+ { 'id': 'INSERTORDEREDLIST_TEXT-1',
+ 'desc': 'check whether the "insertorderedlist" command is enabled',
+ 'qcenabled': 'insertorderedlist' },
+
+ { 'id': 'INSERTUNORDEREDLIST_TEXT-1',
+ 'desc': 'check whether the "insertunorderedlist" command is enabled',
+ 'qcenabled': 'insertunorderedlist' },
+
+ { 'id': 'INSERTTEXT_TEXT-1',
+ 'desc': 'check whether the "inserttext" command is enabled',
+ 'qcenabled': 'inserttext' },
+
+ { 'id': 'DELETE_TEXT-1',
+ 'desc': 'check whether the "delete" command is enabled',
+ 'qcenabled': 'delete' },
+
+ { 'id': 'FORWARDDELETE_TEXT-1',
+ 'desc': 'check whether the "forwarddelete" command is enabled',
+ 'qcenabled': 'forwarddelete' }
+ ]
+ },
+
+ { 'desc': 'MIDAS commands',
+ 'tests': [
+ { 'id': 'STYLEWITHCSS_TEXT-1',
+ 'desc': 'check whether the "styleWithCSS" command is enabled',
+ 'qcenabled': 'styleWithCSS' },
+
+ { 'id': 'CONTENTREADONLY_TEXT-1',
+ 'desc': 'check whether the "contentreadonly" command is enabled',
+ 'qcenabled': 'contentreadonly' },
+
+ { 'id': 'BACKCOLOR_TEXT-1',
+ 'desc': 'check whether the "backcolor" command is enabled',
+ 'qcenabled': 'backcolor' },
+
+ { 'id': 'FORECOLOR_TEXT-1',
+ 'desc': 'check whether the "forecolor" command is enabled',
+ 'qcenabled': 'forecolor' },
+
+ { 'id': 'HILITECOLOR_TEXT-1',
+ 'desc': 'check whether the "hilitecolor" command is enabled',
+ 'qcenabled': 'hilitecolor' },
+
+ { 'id': 'FONTNAME_TEXT-1',
+ 'desc': 'check whether the "fontname" command is enabled',
+ 'qcenabled': 'fontname' },
+
+ { 'id': 'FONTSIZE_TEXT-1',
+ 'desc': 'check whether the "fontsize" command is enabled',
+ 'qcenabled': 'fontsize' },
+
+ { 'id': 'INCREASEFONTSIZE_TEXT-1',
+ 'desc': 'check whether the "increasefontsize" command is enabled',
+ 'qcenabled': 'increasefontsize' },
+
+ { 'id': 'DECREASEFONTSIZE_TEXT-1',
+ 'desc': 'check whether the "decreasefontsize" command is enabled',
+ 'qcenabled': 'decreasefontsize' },
+
+ { 'id': 'HEADING_TEXT-1',
+ 'desc': 'check whether the "heading" command is enabled',
+ 'qcenabled': 'heading' },
+
+ { 'id': 'INDENT_TEXT-1',
+ 'desc': 'check whether the "indent" command is enabled',
+ 'qcenabled': 'indent' },
+
+ { 'id': 'OUTDENT_TEXT-1',
+ 'desc': 'check whether the "outdent" command is enabled',
+ 'qcenabled': 'outdent' },
+
+ { 'id': 'CREATEBOOKMARK_TEXT-1',
+ 'desc': 'check whether the "createbookmark" command is enabled',
+ 'qcenabled': 'createbookmark' },
+
+ { 'id': 'UNBOOKMARK_TEXT-1',
+ 'desc': 'check whether the "unbookmark" command is enabled',
+ 'qcenabled': 'unbookmark' },
+
+ { 'id': 'JUSTIFYCENTER_TEXT-1',
+ 'desc': 'check whether the "justifycenter" command is enabled',
+ 'qcenabled': 'justifycenter' },
+
+ { 'id': 'JUSTIFYFULL_TEXT-1',
+ 'desc': 'check whether the "justifyfull" command is enabled',
+ 'qcenabled': 'justifyfull' },
+
+ { 'id': 'JUSTIFYLEFT_TEXT-1',
+ 'desc': 'check whether the "justifyleft" command is enabled',
+ 'qcenabled': 'justifyleft' },
+
+ { 'id': 'JUSTIFYRIGHT_TEXT-1',
+ 'desc': 'check whether the "justifyright" command is enabled',
+ 'qcenabled': 'justifyright' },
+
+ { 'id': 'REMOVEFORMAT_TEXT-1',
+ 'desc': 'check whether the "removeformat" command is enabled',
+ 'qcenabled': 'removeformat' },
+
+ { 'id': 'COPY_TEXT-1',
+ 'desc': 'check whether the "copy" command is enabled',
+ 'qcenabled': 'copy' },
+
+ { 'id': 'CUT_TEXT-1',
+ 'desc': 'check whether the "cut" command is enabled',
+ 'qcenabled': 'cut' },
+
+ { 'id': 'PASTE_TEXT-1',
+ 'desc': 'check whether the "paste" command is enabled',
+ 'qcenabled': 'paste' }
+ ]
+ },
+
+ { 'desc': 'Other tests',
+ 'tests': [
+ { 'id': 'garbage-1_TEXT-1',
+ 'desc': 'check correct return value with garbage input',
+ 'qcenabled': '#!#@7',
+ 'expected': False }
+ ]
+ }
+ ]
+}
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryIndeterm.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryIndeterm.py
new file mode 100644
index 000000000..d1ad8debd
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryIndeterm.py
@@ -0,0 +1,214 @@
+
+QUERYINDETERM_TESTS = {
+ 'id': 'QI',
+ 'caption': 'queryCommandIndeterm Tests',
+ 'pad': 'foo[bar]baz',
+ 'checkAttrs': False,
+ 'checkStyle': False,
+ 'styleWithCSS': False,
+ 'expected': False,
+
+ 'Proposed': [
+ { 'desc': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': 'HTML5 commands',
+ 'tests': [
+ { 'id': 'SELECTALL_TEXT-1',
+ 'desc': 'check whether the "selectall" command is indeterminate',
+ 'qcindeterm': 'selectall' },
+
+ { 'id': 'UNSELECT_TEXT-1',
+ 'desc': 'check whether the "unselect" command is indeterminate',
+ 'qcindeterm': 'unselect' },
+
+ { 'id': 'UNDO_TEXT-1',
+ 'desc': 'check whether the "undo" command is indeterminate',
+ 'qcindeterm': 'undo' },
+
+ { 'id': 'REDO_TEXT-1',
+ 'desc': 'check whether the "redo" command is indeterminate',
+ 'qcindeterm': 'redo' },
+
+ { 'id': 'BOLD_TEXT-1',
+ 'desc': 'check whether the "bold" command is indeterminate',
+ 'qcindeterm': 'bold' },
+
+ { 'id': 'ITALIC_TEXT-1',
+ 'desc': 'check whether the "italic" command is indeterminate',
+ 'qcindeterm': 'italic' },
+
+ { 'id': 'UNDERLINE_TEXT-1',
+ 'desc': 'check whether the "underline" command is indeterminate',
+ 'qcindeterm': 'underline' },
+
+ { 'id': 'STRIKETHROUGH_TEXT-1',
+ 'desc': 'check whether the "strikethrough" command is indeterminate',
+ 'qcindeterm': 'strikethrough' },
+
+ { 'id': 'SUBSCRIPT_TEXT-1',
+ 'desc': 'check whether the "subscript" command is indeterminate',
+ 'qcindeterm': 'subscript' },
+
+ { 'id': 'SUPERSCRIPT_TEXT-1',
+ 'desc': 'check whether the "superscript" command is indeterminate',
+ 'qcindeterm': 'superscript' },
+
+ { 'id': 'FORMATBLOCK_TEXT-1',
+ 'desc': 'check whether the "formatblock" command is indeterminate',
+ 'qcindeterm': 'formatblock' },
+
+ { 'id': 'CREATELINK_TEXT-1',
+ 'desc': 'check whether the "createlink" command is indeterminate',
+ 'qcindeterm': 'createlink' },
+
+ { 'id': 'UNLINK_TEXT-1',
+ 'desc': 'check whether the "unlink" command is indeterminate',
+ 'qcindeterm': 'unlink' },
+
+ { 'id': 'INSERTHTML_TEXT-1',
+ 'desc': 'check whether the "inserthtml" command is indeterminate',
+ 'qcindeterm': 'inserthtml' },
+
+ { 'id': 'INSERTHORIZONTALRULE_TEXT-1',
+ 'desc': 'check whether the "inserthorizontalrule" command is indeterminate',
+ 'qcindeterm': 'inserthorizontalrule' },
+
+ { 'id': 'INSERTIMAGE_TEXT-1',
+ 'desc': 'check whether the "insertimage" command is indeterminate',
+ 'qcindeterm': 'insertimage' },
+
+ { 'id': 'INSERTLINEBREAK_TEXT-1',
+ 'desc': 'check whether the "insertlinebreak" command is indeterminate',
+ 'qcindeterm': 'insertlinebreak' },
+
+ { 'id': 'INSERTPARAGRAPH_TEXT-1',
+ 'desc': 'check whether the "insertparagraph" command is indeterminate',
+ 'qcindeterm': 'insertparagraph' },
+
+ { 'id': 'INSERTORDEREDLIST_TEXT-1',
+ 'desc': 'check whether the "insertorderedlist" command is indeterminate',
+ 'qcindeterm': 'insertorderedlist' },
+
+ { 'id': 'INSERTUNORDEREDLIST_TEXT-1',
+ 'desc': 'check whether the "insertunorderedlist" command is indeterminate',
+ 'qcindeterm': 'insertunorderedlist' },
+
+ { 'id': 'INSERTTEXT_TEXT-1',
+ 'desc': 'check whether the "inserttext" command is indeterminate',
+ 'qcindeterm': 'inserttext' },
+
+ { 'id': 'DELETE_TEXT-1',
+ 'desc': 'check whether the "delete" command is indeterminate',
+ 'qcindeterm': 'delete' },
+
+ { 'id': 'FORWARDDELETE_TEXT-1',
+ 'desc': 'check whether the "forwarddelete" command is indeterminate',
+ 'qcindeterm': 'forwarddelete' }
+ ]
+ },
+
+ { 'desc': 'MIDAS commands',
+ 'tests': [
+ { 'id': 'STYLEWITHCSS_TEXT-1',
+ 'desc': 'check whether the "styleWithCSS" command is indeterminate',
+ 'qcindeterm': 'styleWithCSS' },
+
+ { 'id': 'CONTENTREADONLY_TEXT-1',
+ 'desc': 'check whether the "contentreadonly" command is indeterminate',
+ 'qcindeterm': 'contentreadonly' },
+
+ { 'id': 'BACKCOLOR_TEXT-1',
+ 'desc': 'check whether the "backcolor" command is indeterminate',
+ 'qcindeterm': 'backcolor' },
+
+ { 'id': 'FORECOLOR_TEXT-1',
+ 'desc': 'check whether the "forecolor" command is indeterminate',
+ 'qcindeterm': 'forecolor' },
+
+ { 'id': 'HILITECOLOR_TEXT-1',
+ 'desc': 'check whether the "hilitecolor" command is indeterminate',
+ 'qcindeterm': 'hilitecolor' },
+
+ { 'id': 'FONTNAME_TEXT-1',
+ 'desc': 'check whether the "fontname" command is indeterminate',
+ 'qcindeterm': 'fontname' },
+
+ { 'id': 'FONTSIZE_TEXT-1',
+ 'desc': 'check whether the "fontsize" command is indeterminate',
+ 'qcindeterm': 'fontsize' },
+
+ { 'id': 'INCREASEFONTSIZE_TEXT-1',
+ 'desc': 'check whether the "increasefontsize" command is indeterminate',
+ 'qcindeterm': 'increasefontsize' },
+
+ { 'id': 'DECREASEFONTSIZE_TEXT-1',
+ 'desc': 'check whether the "decreasefontsize" command is indeterminate',
+ 'qcindeterm': 'decreasefontsize' },
+
+ { 'id': 'HEADING_TEXT-1',
+ 'desc': 'check whether the "heading" command is indeterminate',
+ 'qcindeterm': 'heading' },
+
+ { 'id': 'INDENT_TEXT-1',
+ 'desc': 'check whether the "indent" command is indeterminate',
+ 'qcindeterm': 'indent' },
+
+ { 'id': 'OUTDENT_TEXT-1',
+ 'desc': 'check whether the "outdent" command is indeterminate',
+ 'qcindeterm': 'outdent' },
+
+ { 'id': 'CREATEBOOKMARK_TEXT-1',
+ 'desc': 'check whether the "createbookmark" command is indeterminate',
+ 'qcindeterm': 'createbookmark' },
+
+ { 'id': 'UNBOOKMARK_TEXT-1',
+ 'desc': 'check whether the "unbookmark" command is indeterminate',
+ 'qcindeterm': 'unbookmark' },
+
+ { 'id': 'JUSTIFYCENTER_TEXT-1',
+ 'desc': 'check whether the "justifycenter" command is indeterminate',
+ 'qcindeterm': 'justifycenter' },
+
+ { 'id': 'JUSTIFYFULL_TEXT-1',
+ 'desc': 'check whether the "justifyfull" command is indeterminate',
+ 'qcindeterm': 'justifyfull' },
+
+ { 'id': 'JUSTIFYLEFT_TEXT-1',
+ 'desc': 'check whether the "justifyleft" command is indeterminate',
+ 'qcindeterm': 'justifyleft' },
+
+ { 'id': 'JUSTIFYRIGHT_TEXT-1',
+ 'desc': 'check whether the "justifyright" command is indeterminate',
+ 'qcindeterm': 'justifyright' },
+
+ { 'id': 'REMOVEFORMAT_TEXT-1',
+ 'desc': 'check whether the "removeformat" command is indeterminate',
+ 'qcindeterm': 'removeformat' },
+
+ { 'id': 'COPY_TEXT-1',
+ 'desc': 'check whether the "copy" command is indeterminate',
+ 'qcindeterm': 'copy' },
+
+ { 'id': 'CUT_TEXT-1',
+ 'desc': 'check whether the "cut" command is indeterminate',
+ 'qcindeterm': 'cut' },
+
+ { 'id': 'PASTE_TEXT-1',
+ 'desc': 'check whether the "paste" command is indeterminate',
+ 'qcindeterm': 'paste' }
+ ]
+ },
+
+ { 'desc': 'Other tests',
+ 'tests': [
+ { 'id': 'garbage-1_TEXT-1',
+ 'desc': 'check correct return value with garbage input',
+ 'qcindeterm': '#!#@7' }
+ ]
+ }
+ ]
+}
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryState.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryState.py
new file mode 100644
index 000000000..297559d62
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryState.py
@@ -0,0 +1,575 @@
+
+QUERYSTATE_TESTS = {
+ 'id': 'QS',
+ 'caption': 'queryCommandState Tests',
+ 'checkAttrs': False,
+ 'checkStyle': False,
+ 'styleWithCSS': False,
+
+ 'Proposed': [
+ { 'desc': '',
+ 'qcstate': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': 'query bold state',
+ 'qcstate': 'bold',
+ 'tests': [
+ { 'id': 'B_TEXT_SI',
+ 'rte1-id': 'q-bold-0',
+ 'desc': 'query the "bold" state',
+ 'pad': 'foo[bar]baz',
+ 'expected': False },
+
+ { 'id': 'B_B-1_SI',
+ 'rte1-id': 'q-bold-1',
+ 'desc': 'query the "bold" state',
+ 'pad': '<b>foo[bar]baz</b>',
+ 'expected': True },
+
+ { 'id': 'B_STRONG-1_SI',
+ 'rte1-id': 'q-bold-2',
+ 'desc': 'query the "bold" state',
+ 'pad': '<strong>foo[bar]baz</strong>',
+ 'expected': True },
+
+ { 'id': 'B_SPANs:fw:b-1_SI',
+ 'rte1-id': 'q-bold-3',
+ 'desc': 'query the "bold" state',
+ 'pad': '<span style="font-weight: bold">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'B_SPANs:fw:n-1_SI',
+ 'desc': 'query the "bold" state',
+ 'pad': '<span style="font-weight: normal">foo[bar]baz</span>',
+ 'expected': False },
+
+ { 'id': 'B_Bs:fw:n-1_SI',
+ 'rte1-id': 'q-bold-4',
+ 'desc': 'query the "bold" state',
+ 'pad': '<span style="font-weight: normal">foo[bar]baz</span>',
+ 'expected': False },
+
+ { 'id': 'B_B-SPANs:fw:n-1_SI',
+ 'rte1-id': 'q-bold-5',
+ 'desc': 'query the "bold" state',
+ 'pad': '<b><span style="font-weight: normal">foo[bar]baz</span></b>',
+ 'expected': False },
+
+ { 'id': 'B_SPAN.b-1-SI',
+ 'desc': 'query the "bold" state',
+ 'pad': '<span class="b">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'B_MYB-1-SI',
+ 'desc': 'query the "bold" state',
+ 'pad': '<myb>foo[bar]baz</myb>',
+ 'expected': True },
+
+ { 'id': 'B_B-I-1_SC',
+ 'desc': 'query the "bold" state, bold tag not immediate parent',
+ 'pad': '<b>foo<i>ba^r</i>baz</b>',
+ 'expected': True },
+
+ { 'id': 'B_B-I-1_SL',
+ 'desc': 'query the "bold" state, selection partially in child element',
+ 'pad': '<b>fo[o<i>b]ar</i>baz</b>',
+ 'expected': True },
+
+ { 'id': 'B_B-I-1_SR',
+ 'desc': 'query the "bold" state, selection partially in child element',
+ 'pad': '<b>foo<i>ba[r</i>b]az</b>',
+ 'expected': True },
+
+ { 'id': 'B_STRONG-I-1_SC',
+ 'desc': 'query the "bold" state, bold tag not immediate parent',
+ 'pad': '<strong>foo<i>ba^r</i>baz</strong>',
+ 'expected': True },
+
+ { 'id': 'B_B-I-U-1_SC',
+ 'desc': 'query the "bold" state, bold tag not immediate parent',
+ 'pad': '<b>foo<i>bar<u>b^az</u></i></strong>',
+ 'expected': True },
+
+ { 'id': 'B_B-I-U-1_SM',
+ 'desc': 'query the "bold" state, bold tag not immediate parent',
+ 'pad': '<b>foo<i>ba[r<u>b]az</u></i></strong>',
+ 'expected': True },
+
+ { 'id': 'B_TEXT-B-1_SO-1',
+ 'desc': 'query the "bold" state, selection wrapping the bold tag',
+ 'pad': 'foo[<b>bar</b>]baz',
+ 'expected': True },
+
+ { 'id': 'B_TEXT-B-1_SO-2',
+ 'desc': 'query the "bold" state, selection wrapping the bold tag',
+ 'pad': 'foo{<b>bar</b>}baz',
+ 'expected': True },
+
+ { 'id': 'B_TEXT-B-1_SL',
+ 'desc': 'query the "bold" state, selection containing non-bold text',
+ 'pad': 'fo[o<b>ba]r</b>baz',
+ 'expected': False },
+
+ { 'id': 'B_TEXT-B-1_SR',
+ 'desc': 'query the "bold" state, selection containing non-bold text',
+ 'pad': 'foo<b>b[ar</b>b]az',
+ 'expected': False },
+
+ { 'id': 'B_TEXT-B-1_SO-3',
+ 'desc': 'query the "bold" state, selection containing non-bold text',
+ 'pad': 'fo[o<b>bar</b>b]az',
+ 'expected': False },
+
+ { 'id': 'B_B.TEXT.B-1_SM',
+ 'desc': 'query the "bold" state, selection including non-bold text',
+ 'pad': '<b>fo[o</b>bar<b>b]az</b>',
+ 'expected': False },
+
+ { 'id': 'B_B.B.B-1_SM',
+ 'desc': 'query the "bold" state, selection mixed, but all bold',
+ 'pad': '<b>fo[o</b><b>bar</b><b>b]az</b>',
+ 'expected': True },
+
+ { 'id': 'B_B.STRONG.B-1_SM',
+ 'desc': 'query the "bold" state, selection mixed, but all bold',
+ 'pad': '<b>fo[o</b><strong>bar</strong><b>b]az</b>',
+ 'expected': True },
+
+ { 'id': 'B_SPAN.b.MYB.SPANs:fw:b-1_SM',
+ 'desc': 'query the "bold" state, selection mixed, but all bold',
+ 'pad': '<span class="b">fo[o</span><myb>bar</myb><span style="font-weight: bold">b]az</span>',
+ 'expected': True }
+ ]
+ },
+
+ { 'desc': 'query italic state',
+ 'qcstate': 'italic',
+ 'tests': [
+ { 'id': 'I_TEXT_SI',
+ 'rte1-id': 'q-italic-0',
+ 'desc': 'query the "italic" state',
+ 'pad': 'foo[bar]baz',
+ 'expected': False },
+
+ { 'id': 'I_I-1_SI',
+ 'rte1-id': 'q-italic-1',
+ 'desc': 'query the "italic" state',
+ 'pad': '<i>foo[bar]baz</i>',
+ 'expected': True },
+
+ { 'id': 'I_EM-1_SI',
+ 'rte1-id': 'q-italic-2',
+ 'desc': 'query the "italic" state',
+ 'pad': '<em>foo[bar]baz</em>',
+ 'expected': True },
+
+ { 'id': 'I_SPANs:fs:i-1_SI',
+ 'rte1-id': 'q-italic-3',
+ 'desc': 'query the "italic" state',
+ 'pad': '<span style="font-style: italic">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'I_SPANs:fs:n-1_SI',
+ 'desc': 'query the "italic" state',
+ 'pad': '<span style="font-style: normal">foo[bar]baz</span>',
+ 'expected': False },
+
+ { 'id': 'I_I-SPANs:fs:n-1_SI',
+ 'rte1-id': 'q-italic-4',
+ 'desc': 'query the "italic" state',
+ 'pad': '<i><span style="font-style: normal">foo[bar]baz</span></i>',
+ 'expected': False },
+
+ { 'id': 'I_SPAN.i-1-SI',
+ 'desc': 'query the "italic" state',
+ 'pad': '<span class="i">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'I_MYI-1-SI',
+ 'desc': 'query the "italic" state',
+ 'pad': '<myi>foo[bar]baz</myi>',
+ 'expected': True }
+ ]
+ },
+
+ { 'desc': 'query underline state',
+ 'qcstate': 'underline',
+ 'tests': [
+ { 'id': 'U_TEXT_SI',
+ 'rte1-id': 'q-underline-0',
+ 'desc': 'query the "underline" state',
+ 'pad': 'foo[bar]baz',
+ 'expected': False },
+
+ { 'id': 'U_U-1_SI',
+ 'rte1-id': 'q-underline-1',
+ 'desc': 'query the "underline" state',
+ 'pad': '<u>foo[bar]baz</u>',
+ 'expected': True },
+
+ { 'id': 'U_Us:td:n-1_SI',
+ 'rte1-id': 'q-underline-4',
+ 'desc': 'query the "underline" state',
+ 'pad': '<u style="text-decoration: none">foo[bar]baz</u>',
+ 'expected': False },
+
+ { 'id': 'U_Ah:url-1_SI',
+ 'rte1-id': 'q-underline-2',
+ 'desc': 'query the "underline" state',
+ 'pad': '<a href="http://www.goo.gl">foo[bar]baz</a>',
+ 'expected': True },
+
+ { 'id': 'U_Ah:url.s:td:n-1_SI',
+ 'rte1-id': 'q-underline-5',
+ 'desc': 'query the "underline" state',
+ 'pad': '<a href="http://www.goo.gl" style="text-decoration: none">foo[bar]baz</a>',
+ 'expected': False },
+
+ { 'id': 'U_SPANs:td:u-1_SI',
+ 'rte1-id': 'q-underline-3',
+ 'desc': 'query the "underline" state',
+ 'pad': '<span style="text-decoration: underline">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'U_SPAN.u-1-SI',
+ 'desc': 'query the "underline" state',
+ 'pad': '<span class="u">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'U_MYU-1-SI',
+ 'desc': 'query the "underline" state',
+ 'pad': '<myu>foo[bar]baz</myu>',
+ 'expected': True }
+ ]
+ },
+
+ { 'desc': 'query strike-through state',
+ 'qcstate': 'strikethrough',
+ 'tests': [
+ { 'id': 'S_TEXT_SI',
+ 'rte1-id': 'q-strikethrough-0',
+ 'desc': 'query the "strikethrough" state',
+ 'pad': 'foo[bar]baz',
+ 'expected': False },
+
+ { 'id': 'S_S-1_SI',
+ 'rte1-id': 'q-strikethrough-3',
+ 'desc': 'query the "strikethrough" state',
+ 'pad': '<s>foo[bar]baz</s>',
+ 'expected': True },
+
+ { 'id': 'S_STRIKE-1_SI',
+ 'rte1-id': 'q-strikethrough-1',
+ 'desc': 'query the "strikethrough" state',
+ 'pad': '<strike>foo[bar]baz</strike>',
+ 'expected': True },
+
+ { 'id': 'S_STRIKEs:td:n-1_SI',
+ 'rte1-id': 'q-strikethrough-2',
+ 'desc': 'query the "strikethrough" state',
+ 'pad': '<strike style="text-decoration: none">foo[bar]baz</strike>',
+ 'expected': False },
+
+ { 'id': 'S_DEL-1_SI',
+ 'rte1-id': 'q-strikethrough-4',
+ 'desc': 'query the "strikethrough" state',
+ 'pad': '<del>foo[bar]baz</del>',
+ 'expected': True },
+
+ { 'id': 'S_SPANs:td:lt-1_SI',
+ 'rte1-id': 'q-strikethrough-5',
+ 'desc': 'query the "strikethrough" state',
+ 'pad': '<span style="text-decoration: line-through">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'S_SPAN.s-1-SI',
+ 'desc': 'query the "strikethrough" state',
+ 'pad': '<span class="s">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'S_MYS-1-SI',
+ 'desc': 'query the "strikethrough" state',
+ 'pad': '<mys>foo[bar]baz</mys>',
+ 'expected': True },
+
+ { 'id': 'S_S.STRIKE.DEL-1_SM',
+ 'desc': 'query the "strikethrough" state, selection mixed, but all struck',
+ 'pad': '<s>fo[o</s><strike>bar</strike><del>b]az</del>',
+ 'expected': True }
+ ]
+ },
+
+ { 'desc': 'query subscript state',
+ 'qcstate': 'subscript',
+ 'tests': [
+ { 'id': 'SUB_TEXT_SI',
+ 'rte1-id': 'q-subscript-0',
+ 'desc': 'query the "subscript" state',
+ 'pad': 'foo[bar]baz',
+ 'expected': False },
+
+ { 'id': 'SUB_SUB-1_SI',
+ 'rte1-id': 'q-subscript-1',
+ 'desc': 'query the "subscript" state',
+ 'pad': '<sub>foo[bar]baz</sub>',
+ 'expected': True },
+
+ { 'id': 'SUB_SPAN.sub-1-SI',
+ 'desc': 'query the "subscript" state',
+ 'pad': '<span class="sub">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'SUB_MYSUB-1-SI',
+ 'desc': 'query the "subscript" state',
+ 'pad': '<mysub>foo[bar]baz</mysub>',
+ 'expected': True }
+ ]
+ },
+
+ { 'desc': 'query superscript state',
+ 'qcstate': 'superscript',
+ 'tests': [
+ { 'id': 'SUP_TEXT_SI',
+ 'rte1-id': 'q-superscript-0',
+ 'desc': 'query the "superscript" state',
+ 'pad': 'foo[bar]baz',
+ 'expected': False },
+
+ { 'id': 'SUP_SUP-1_SI',
+ 'rte1-id': 'q-superscript-1',
+ 'desc': 'query the "superscript" state',
+ 'pad': '<sup>foo[bar]baz</sup>',
+ 'expected': True },
+
+ { 'id': 'IOL_TEXT_SI',
+ 'desc': 'query the "insertorderedlist" state',
+ 'pad': 'foo[bar]baz',
+ 'expected': False },
+
+ { 'id': 'SUP_SPAN.sup-1-SI',
+ 'desc': 'query the "superscript" state',
+ 'pad': '<span class="sup">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'SUP_MYSUP-1-SI',
+ 'desc': 'query the "superscript" state',
+ 'pad': '<mysup>foo[bar]baz</mysup>',
+ 'expected': True }
+ ]
+ },
+
+ { 'desc': 'query whether the selection is in an ordered list',
+ 'qcstate': 'insertorderedlist',
+ 'tests': [
+ { 'id': 'IOL_TEXT-1_SI',
+ 'rte1-id': 'q-insertorderedlist-0',
+ 'desc': 'query the "insertorderedlist" state',
+ 'pad': 'foo[bar]baz',
+ 'expected': False },
+
+ { 'id': 'IOL_OL-LI-1_SI',
+ 'rte1-id': 'q-insertorderedlist-1',
+ 'desc': 'query the "insertorderedlist" state',
+ 'pad': '<ol><li>foo[bar]baz</li></ol>',
+ 'expected': True },
+
+ { 'id': 'IOL_UL_LI-1_SI',
+ 'rte1-id': 'q-insertorderedlist-2',
+ 'desc': 'query the "insertorderedlist" state',
+ 'pad': '<ul><li>foo[bar]baz</li></ul>',
+ 'expected': False }
+ ]
+ },
+
+ { 'desc': 'query whether the selection is in an unordered list',
+ 'qcstate': 'insertunorderedlist',
+ 'tests': [
+ { 'id': 'IUL_TEXT_SI',
+ 'rte1-id': 'q-insertunorderedlist-0',
+ 'desc': 'query the "insertunorderedlist" state',
+ 'pad': 'foo[bar]baz',
+ 'expected': False },
+
+ { 'id': 'IUL_OL-LI-1_SI',
+ 'rte1-id': 'q-insertunorderedlist-1',
+ 'desc': 'query the "insertunorderedlist" state',
+ 'pad': '<ol><li>foo[bar]baz</li></ol>',
+ 'expected': False },
+
+ { 'id': 'IUL_UL-LI-1_SI',
+ 'rte1-id': 'q-insertunorderedlist-2',
+ 'desc': 'query the "insertunorderedlist" state',
+ 'pad': '<ul><li>foo[bar]baz</li></ul>',
+ 'expected': True }
+ ]
+ },
+
+ { 'desc': 'query whether the paragraph is centered',
+ 'qcstate': 'justifycenter',
+ 'tests': [
+ { 'id': 'JC_TEXT_SI',
+ 'rte1-id': 'q-justifycenter-0',
+ 'desc': 'query the "justifycenter" state',
+ 'pad': 'foo[bar]baz',
+ 'expected': False },
+
+ { 'id': 'JC_DIVa:c-1_SI',
+ 'rte1-id': 'q-justifycenter-1',
+ 'desc': 'query the "justifycenter" state',
+ 'pad': '<div align="center">foo[bar]baz</div>',
+ 'expected': True },
+
+ { 'id': 'JC_Pa:c-1_SI',
+ 'rte1-id': 'q-justifycenter-2',
+ 'desc': 'query the "justifycenter" state',
+ 'pad': '<p align="center">foo[bar]baz</p>',
+ 'expected': True },
+
+ { 'id': 'JC_SPANs:ta:c-1_SI',
+ 'rte1-id': 'q-justifycenter-3',
+ 'desc': 'query the "justifycenter" state',
+ 'pad': '<div style="text-align: center">foo[bar]baz</div>',
+ 'expected': True },
+
+ { 'id': 'JC_SPAN.jc-1-SI',
+ 'desc': 'query the "justifycenter" state',
+ 'pad': '<span class="jc">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'JC_MYJC-1-SI',
+ 'desc': 'query the "justifycenter" state',
+ 'pad': '<myjc>foo[bar]baz</myjc>',
+ 'expected': True }
+ ]
+ },
+
+ { 'desc': 'query whether the paragraph is justified',
+ 'qcstate': 'justifyfull',
+ 'tests': [
+ { 'id': 'JF_TEXT_SI',
+ 'rte1-id': 'q-justifyfull-0',
+ 'desc': 'query the "justifyfull" state',
+ 'pad': 'foo[bar]baz',
+ 'expected': False },
+
+ { 'id': 'JF_DIVa:j-1_SI',
+ 'rte1-id': 'q-justifyfull-1',
+ 'desc': 'query the "justifyfull" state',
+ 'pad': '<div align="justify">foo[bar]baz</div>',
+ 'expected': True },
+
+ { 'id': 'JF_Pa:j-1_SI',
+ 'rte1-id': 'q-justifyfull-2',
+ 'desc': 'query the "justifyfull" state',
+ 'pad': '<p align="justify">foo[bar]baz</p>',
+ 'expected': True },
+
+ { 'id': 'JF_SPANs:ta:j-1_SI',
+ 'rte1-id': 'q-justifyfull-3',
+ 'desc': 'query the "justifyfull" state',
+ 'pad': '<span style="text-align: justify">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'JF_SPAN.jf-1-SI',
+ 'desc': 'query the "justifyfull" state',
+ 'pad': '<span class="jf">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'JF_MYJF-1-SI',
+ 'desc': 'query the "justifyfull" state',
+ 'pad': '<myjf>foo[bar]baz</myjf>',
+ 'expected': True }
+ ]
+ },
+
+ { 'desc': 'query whether the paragraph is aligned left',
+ 'qcstate': 'justifyleft',
+ 'tests': [
+ { 'id': 'JL_TEXT_SI',
+ 'desc': 'query the "justifyleft" state',
+ 'pad': 'foo[bar]baz',
+ 'expected': False },
+
+ { 'id': 'JL_DIVa:l-1_SI',
+ 'rte1-id': 'q-justifyleft-0',
+ 'desc': 'query the "justifyleft" state',
+ 'pad': '<div align="left">foo[bar]baz</div>',
+ 'expected': True },
+
+ { 'id': 'JL_Pa:l-1_SI',
+ 'rte1-id': 'q-justifyleft-1',
+ 'desc': 'query the "justifyleft" state',
+ 'pad': '<p align="left">foo[bar]baz</p>',
+ 'expected': True },
+
+ { 'id': 'JL_SPANs:ta:l-1_SI',
+ 'rte1-id': 'q-justifyleft-2',
+ 'desc': 'query the "justifyleft" state',
+ 'pad': '<span style="text-align: left">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'JL_SPAN.jl-1-SI',
+ 'desc': 'query the "justifyleft" state',
+ 'pad': '<span class="jl">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'JL_MYJL-1-SI',
+ 'desc': 'query the "justifyleft" state',
+ 'pad': '<myjl>foo[bar]baz</myjl>',
+ 'expected': True }
+ ]
+ },
+
+ { 'desc': 'query whether the paragraph is aligned right',
+ 'qcstate': 'justifyright',
+ 'tests': [
+ { 'id': 'JR_TEXT_SI',
+ 'rte1-id': 'q-justifyright-0',
+ 'desc': 'query the "justifyright" state',
+ 'pad': 'foo[bar]baz',
+ 'expected': False },
+
+ { 'id': 'JR_DIVa:r-1_SI',
+ 'rte1-id': 'q-justifyright-1',
+ 'desc': 'query the "justifyright" state',
+ 'pad': '<div align="right">foo[bar]baz</div>',
+ 'expected': True },
+
+ { 'id': 'JR_Pa:r-1_SI',
+ 'rte1-id': 'q-justifyright-2',
+ 'desc': 'query the "justifyright" state',
+ 'pad': '<p align="right">foo[bar]baz</p>',
+ 'expected': True },
+
+ { 'id': 'JR_SPANs:ta:r-1_SI',
+ 'rte1-id': 'q-justifyright-3',
+ 'desc': 'query the "justifyright" state',
+ 'pad': '<span style="text-align: right">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'JR_SPAN.jr-1-SI',
+ 'desc': 'query the "justifyright" state',
+ 'pad': '<span class="jr">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'JR_MYJR-1-SI',
+ 'desc': 'query the "justifyright" state',
+ 'pad': '<myjr>foo[bar]baz</myjr>',
+ 'expected': True }
+ ]
+ }
+ ]
+}
+
+QUERYSTATE_TESTS_CSS = {
+ 'id': 'QSC',
+ 'caption': 'queryCommandState Tests, using styleWithCSS',
+ 'checkAttrs': False,
+ 'checkStyle': False,
+ 'styleWithCSS': True,
+
+ 'Proposed': QUERYSTATE_TESTS['Proposed']
+}
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/querySupported.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/querySupported.py
new file mode 100644
index 000000000..af23a428c
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/querySupported.py
@@ -0,0 +1,226 @@
+
+QUERYSUPPORTED_TESTS = {
+ 'id': 'Q',
+ 'caption': 'queryCommandSupported Tests',
+ 'pad': 'foo[bar]baz',
+ 'checkAttrs': False,
+ 'checkStyle': False,
+ 'styleWithCSS': False,
+ 'expected': True,
+
+ 'Proposed': [
+ { 'desc': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': 'HTML5 commands',
+ 'tests': [
+ { 'id': 'SELECTALL_TEXT-1',
+ 'desc': 'check whether the "selectall" command is supported',
+ 'qcsupported': 'selectall' },
+
+ { 'id': 'UNSELECT_TEXT-1',
+ 'desc': 'check whether the "unselect" command is supported',
+ 'qcsupported': 'unselect' },
+
+ { 'id': 'UNDO_TEXT-1',
+ 'desc': 'check whether the "undo" command is supported',
+ 'qcsupported': 'undo' },
+
+ { 'id': 'REDO_TEXT-1',
+ 'desc': 'check whether the "redo" command is supported',
+ 'qcsupported': 'redo' },
+
+ { 'id': 'BOLD_TEXT-1',
+ 'desc': 'check whether the "bold" command is supported',
+ 'qcsupported': 'bold' },
+
+ { 'id': 'BOLD_B',
+ 'desc': 'check whether the "bold" command is supported',
+ 'qcsupported': 'bold',
+ 'pad': '<b>foo[bar]baz</b>' },
+
+ { 'id': 'ITALIC_TEXT-1',
+ 'desc': 'check whether the "italic" command is supported',
+ 'qcsupported': 'italic' },
+
+ { 'id': 'ITALIC_I',
+ 'desc': 'check whether the "italic" command is supported',
+ 'qcsupported': 'italic',
+ 'pad': '<i>foo[bar]baz</i>' },
+
+ { 'id': 'UNDERLINE_TEXT-1',
+ 'desc': 'check whether the "underline" command is supported',
+ 'qcsupported': 'underline' },
+
+ { 'id': 'STRIKETHROUGH_TEXT-1',
+ 'desc': 'check whether the "strikethrough" command is supported',
+ 'qcsupported': 'strikethrough' },
+
+ { 'id': 'SUBSCRIPT_TEXT-1',
+ 'desc': 'check whether the "subscript" command is supported',
+ 'qcsupported': 'subscript' },
+
+ { 'id': 'SUPERSCRIPT_TEXT-1',
+ 'desc': 'check whether the "superscript" command is supported',
+ 'qcsupported': 'superscript' },
+
+ { 'id': 'FORMATBLOCK_TEXT-1',
+ 'desc': 'check whether the "formatblock" command is supported',
+ 'qcsupported': 'formatblock' },
+
+ { 'id': 'CREATELINK_TEXT-1',
+ 'desc': 'check whether the "createlink" command is supported',
+ 'qcsupported': 'createlink' },
+
+ { 'id': 'UNLINK_TEXT-1',
+ 'desc': 'check whether the "unlink" command is supported',
+ 'qcsupported': 'unlink' },
+
+ { 'id': 'INSERTHTML_TEXT-1',
+ 'desc': 'check whether the "inserthtml" command is supported',
+ 'qcsupported': 'inserthtml' },
+
+ { 'id': 'INSERTHORIZONTALRULE_TEXT-1',
+ 'desc': 'check whether the "inserthorizontalrule" command is supported',
+ 'qcsupported': 'inserthorizontalrule' },
+
+ { 'id': 'INSERTIMAGE_TEXT-1',
+ 'desc': 'check whether the "insertimage" command is supported',
+ 'qcsupported': 'insertimage' },
+
+ { 'id': 'INSERTLINEBREAK_TEXT-1',
+ 'desc': 'check whether the "insertlinebreak" command is supported',
+ 'qcsupported': 'insertlinebreak' },
+
+ { 'id': 'INSERTPARAGRAPH_TEXT-1',
+ 'desc': 'check whether the "insertparagraph" command is supported',
+ 'qcsupported': 'insertparagraph' },
+
+ { 'id': 'INSERTORDEREDLIST_TEXT-1',
+ 'desc': 'check whether the "insertorderedlist" command is supported',
+ 'qcsupported': 'insertorderedlist' },
+
+ { 'id': 'INSERTUNORDEREDLIST_TEXT-1',
+ 'desc': 'check whether the "insertunorderedlist" command is supported',
+ 'qcsupported': 'insertunorderedlist' },
+
+ { 'id': 'INSERTTEXT_TEXT-1',
+ 'desc': 'check whether the "inserttext" command is supported',
+ 'qcsupported': 'inserttext' },
+
+ { 'id': 'DELETE_TEXT-1',
+ 'desc': 'check whether the "delete" command is supported',
+ 'qcsupported': 'delete' },
+
+ { 'id': 'FORWARDDELETE_TEXT-1',
+ 'desc': 'check whether the "forwarddelete" command is supported',
+ 'qcsupported': 'forwarddelete' }
+ ]
+ },
+
+ { 'desc': 'MIDAS commands',
+ 'tests': [
+ { 'id': 'STYLEWITHCSS_TEXT-1',
+ 'desc': 'check whether the "styleWithCSS" command is supported',
+ 'qcsupported': 'styleWithCSS' },
+
+ { 'id': 'CONTENTREADONLY_TEXT-1',
+ 'desc': 'check whether the "contentreadonly" command is supported',
+ 'qcsupported': 'contentreadonly' },
+
+ { 'id': 'BACKCOLOR_TEXT-1',
+ 'desc': 'check whether the "backcolor" command is supported',
+ 'qcsupported': 'backcolor' },
+
+ { 'id': 'FORECOLOR_TEXT-1',
+ 'desc': 'check whether the "forecolor" command is supported',
+ 'qcsupported': 'forecolor' },
+
+ { 'id': 'HILITECOLOR_TEXT-1',
+ 'desc': 'check whether the "hilitecolor" command is supported',
+ 'qcsupported': 'hilitecolor' },
+
+ { 'id': 'FONTNAME_TEXT-1',
+ 'desc': 'check whether the "fontname" command is supported',
+ 'qcsupported': 'fontname' },
+
+ { 'id': 'FONTSIZE_TEXT-1',
+ 'desc': 'check whether the "fontsize" command is supported',
+ 'qcsupported': 'fontsize' },
+
+ { 'id': 'INCREASEFONTSIZE_TEXT-1',
+ 'desc': 'check whether the "increasefontsize" command is supported',
+ 'qcsupported': 'increasefontsize' },
+
+ { 'id': 'DECREASEFONTSIZE_TEXT-1',
+ 'desc': 'check whether the "decreasefontsize" command is supported',
+ 'qcsupported': 'decreasefontsize' },
+
+ { 'id': 'HEADING_TEXT-1',
+ 'desc': 'check whether the "heading" command is supported',
+ 'qcsupported': 'heading' },
+
+ { 'id': 'INDENT_TEXT-1',
+ 'desc': 'check whether the "indent" command is supported',
+ 'qcsupported': 'indent' },
+
+ { 'id': 'OUTDENT_TEXT-1',
+ 'desc': 'check whether the "outdent" command is supported',
+ 'qcsupported': 'outdent' },
+
+ { 'id': 'CREATEBOOKMARK_TEXT-1',
+ 'desc': 'check whether the "createbookmark" command is supported',
+ 'qcsupported': 'createbookmark' },
+
+ { 'id': 'UNBOOKMARK_TEXT-1',
+ 'desc': 'check whether the "unbookmark" command is supported',
+ 'qcsupported': 'unbookmark' },
+
+ { 'id': 'JUSTIFYCENTER_TEXT-1',
+ 'desc': 'check whether the "justifycenter" command is supported',
+ 'qcsupported': 'justifycenter' },
+
+ { 'id': 'JUSTIFYFULL_TEXT-1',
+ 'desc': 'check whether the "justifyfull" command is supported',
+ 'qcsupported': 'justifyfull' },
+
+ { 'id': 'JUSTIFYLEFT_TEXT-1',
+ 'desc': 'check whether the "justifyleft" command is supported',
+ 'qcsupported': 'justifyleft' },
+
+ { 'id': 'JUSTIFYRIGHT_TEXT-1',
+ 'desc': 'check whether the "justifyright" command is supported',
+ 'qcsupported': 'justifyright' },
+
+ { 'id': 'REMOVEFORMAT_TEXT-1',
+ 'desc': 'check whether the "removeformat" command is supported',
+ 'qcsupported': 'removeformat' },
+
+ { 'id': 'COPY_TEXT-1',
+ 'desc': 'check whether the "copy" command is supported',
+ 'qcsupported': 'copy' },
+
+ { 'id': 'CUT_TEXT-1',
+ 'desc': 'check whether the "cut" command is supported',
+ 'qcsupported': 'cut' },
+
+ { 'id': 'PASTE_TEXT-1',
+ 'desc': 'check whether the "paste" command is supported',
+ 'qcsupported': 'paste' }
+ ]
+ },
+
+ { 'desc': 'Other tests',
+ 'tests': [
+ { 'id': 'garbage-1_TEXT-1',
+ 'desc': 'check correct return value with garbage input',
+ 'qcsupported': '#!#@7',
+ 'expected': False }
+ ]
+ }
+ ]
+}
+
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryValue.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryValue.py
new file mode 100644
index 000000000..793b7cb6c
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryValue.py
@@ -0,0 +1,429 @@
+
+QUERYVALUE_TESTS = {
+ 'id': 'QV',
+ 'caption': 'queryCommandValue Tests',
+ 'checkAttrs': False,
+ 'checkStyle': False,
+ 'styleWithCSS': False,
+
+ 'Proposed': [
+ { 'desc': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': '[HTML5] query bold value',
+ 'qcvalue': 'bold',
+ 'tests': [
+ { 'id': 'B_TEXT_SI',
+ 'desc': 'query the "bold" value',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'false' },
+
+ { 'id': 'B_B-1_SI',
+ 'desc': 'query the "bold" value',
+ 'pad': '<b>foo[bar]baz</b>',
+ 'expected': 'true' },
+
+ { 'id': 'B_STRONG-1_SI',
+ 'desc': 'query the "bold" value',
+ 'pad': '<strong>foo[bar]baz</strong>',
+ 'expected': 'true' },
+
+ { 'id': 'B_SPANs:fw:b-1_SI',
+ 'desc': 'query the "bold" value',
+ 'pad': '<span style="font-weight: bold">foo[bar]baz</span>',
+ 'expected': 'true' },
+
+ { 'id': 'B_SPANs:fw:n-1_SI',
+ 'desc': 'query the "bold" value',
+ 'pad': '<span style="font-weight: normal">foo[bar]baz</span>',
+ 'expected': 'false' },
+
+ { 'id': 'B_Bs:fw:n-1_SI',
+ 'desc': 'query the "bold" value',
+ 'pad': '<b><span style="font-weight: normal">foo[bar]baz</span></b>',
+ 'expected': 'false' },
+
+ { 'id': 'B_SPAN.b-1_SI',
+ 'desc': 'query the "bold" value',
+ 'pad': '<span class="b">foo[bar]baz</span>',
+ 'expected': 'true' },
+
+ { 'id': 'B_MYB-1-SI',
+ 'desc': 'query the "bold" value',
+ 'pad': '<myb>foo[bar]baz</myb>',
+ 'expected': 'true' }
+ ]
+ },
+
+ { 'desc': '[HTML5] query italic value',
+ 'qcvalue': 'italic',
+ 'tests': [
+ { 'id': 'I_TEXT_SI',
+ 'desc': 'query the "bold" value',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'false' },
+
+ { 'id': 'I_I-1_SI',
+ 'desc': 'query the "bold" value',
+ 'pad': '<i>foo[bar]baz</i>',
+ 'expected': 'true' },
+
+ { 'id': 'I_EM-1_SI',
+ 'desc': 'query the "bold" value',
+ 'pad': '<em>foo[bar]baz</em>',
+ 'expected': 'true' },
+
+ { 'id': 'I_SPANs:fs:i-1_SI',
+ 'desc': 'query the "bold" value',
+ 'pad': '<span style="font-style: italic">foo[bar]baz</span>',
+ 'expected': 'true' },
+
+ { 'id': 'I_SPANs:fs:n-1_SI',
+ 'desc': 'query the "bold" value',
+ 'pad': '<span style="font-style: normal">foo[bar]baz</span>',
+ 'expected': 'false' },
+
+ { 'id': 'I_I-SPANs:fs:n-1_SI',
+ 'desc': 'query the "bold" value',
+ 'pad': '<i><span style="font-style: normal">foo[bar]baz</span></i>',
+ 'expected': 'false' },
+
+ { 'id': 'I_SPAN.i-1_SI',
+ 'desc': 'query the "italic" value',
+ 'pad': '<span class="i">foo[bar]baz</span>',
+ 'expected': 'true' },
+
+ { 'id': 'I_MYI-1-SI',
+ 'desc': 'query the "italic" value',
+ 'pad': '<myi>foo[bar]baz</myi>',
+ 'expected': 'true' }
+ ]
+ },
+
+ { 'desc': '[HTML5] query block formatting value',
+ 'qcvalue': 'formatblock',
+ 'tests': [
+ { 'id': 'FB_TEXT-1_SC',
+ 'desc': 'query the "formatBlock" value',
+ 'pad': 'foobar^baz',
+ 'expected': '',
+ 'accept': 'normal' },
+
+ { 'id': 'FB_H1-1_SC',
+ 'desc': 'query the "formatBlock" value',
+ 'pad': '<h1>foobar^baz</h1>',
+ 'expected': 'h1' },
+
+ { 'id': 'FB_PRE-1_SC',
+ 'desc': 'query the "formatBlock" value',
+ 'pad': '<pre>foobar^baz</pre>',
+ 'expected': 'pre' },
+
+ { 'id': 'FB_BQ-1_SC',
+ 'desc': 'query the "formatBlock" value',
+ 'pad': '<blockquote>foobar^baz</blockquote>',
+ 'expected': 'blockquote' },
+
+ { 'id': 'FB_ADDRESS-1_SC',
+ 'desc': 'query the "formatBlock" value',
+ 'pad': '<address>foobar^baz</address>',
+ 'expected': 'address' },
+
+ { 'id': 'FB_H1-H2-1_SC',
+ 'desc': 'query the "formatBlock" value',
+ 'pad': '<h1>foo<h2>ba^r</h2>baz</h1>',
+ 'expected': 'h2' },
+
+ { 'id': 'FB_H1-H2-1_SL',
+ 'desc': 'query the "formatBlock" value on oblique selection (outermost formatting expected)',
+ 'pad': '<h1>fo[o<h2>ba]r</h2>baz</h1>',
+ 'expected': 'h1' },
+
+ { 'id': 'FB_H1-H2-1_SR',
+ 'desc': 'query the "formatBlock" value on oblique selection (outermost formatting expected)',
+ 'pad': '<h1>foo<h2>b[ar</h2>ba]z</h1>',
+ 'expected': 'h1' },
+
+ { 'id': 'FB_TEXT-ADDRESS-1_SL',
+ 'desc': 'query the "formatBlock" value on oblique selection (outermost formatting expected)',
+ 'pad': 'fo[o<ADDRESS>ba]r</ADDRESS>baz',
+ 'expected': '',
+ 'accept': 'normal' },
+
+ { 'id': 'FB_TEXT-ADDRESS-1_SR',
+ 'desc': 'query the "formatBlock" value on oblique selection (outermost formatting expected)',
+ 'pad': 'foo<ADDRESS>b[ar</ADDRESS>ba]z',
+ 'expected': '',
+ 'accept': 'normal' },
+
+ { 'id': 'FB_H1-H2.TEXT.H2-1_SM',
+ 'desc': 'query the "formatBlock" value on oblique selection (outermost formatting expected)',
+ 'pad': '<h1><h2>fo[o</h2>bar<h2>b]az</h2></h1>',
+ 'expected': 'h1' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] query heading type',
+ 'qcvalue': 'heading',
+ 'tests': [
+ { 'id': 'H_H1-1_SC',
+ 'desc': 'query the "heading" type',
+ 'pad': '<h1>foobar^baz</h1>',
+ 'expected': 'h1',
+ 'accept': '<h1>' },
+
+ { 'id': 'H_H3-1_SC',
+ 'desc': 'query the "heading" type',
+ 'pad': '<h3>foobar^baz</h3>',
+ 'expected': 'h3',
+ 'accept': '<h3>' },
+
+ { 'id': 'H_H1-H2-H3-H4-1_SC',
+ 'desc': 'query the "heading" type within nested heading tags',
+ 'pad': '<h1><h2><h3><h4>foobar^baz</h4></h3></h2></h1>',
+ 'expected': 'h4',
+ 'accept': '<h4>' },
+
+ { 'id': 'H_P-1_SC',
+ 'desc': 'query the "heading" type outside of a heading',
+ 'pad': '<p>foobar^baz</p>',
+ 'expected': '' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] query font name',
+ 'qcvalue': 'fontname',
+ 'tests': [
+ { 'id': 'FN_FONTf:a-1_SI',
+ 'rte1-id': 'q-fontname-0',
+ 'desc': 'query the "fontname" value',
+ 'pad': '<font face="arial">foo[bar]baz</font>',
+ 'expected': 'arial' },
+
+ { 'id': 'FN_SPANs:ff:a-1_SI',
+ 'rte1-id': 'q-fontname-1',
+ 'desc': 'query the "fontname" value',
+ 'pad': '<span style="font-family: arial">foo[bar]baz</span>',
+ 'expected': 'arial' },
+
+ { 'id': 'FN_FONTf:a.s:ff:c-1_SI',
+ 'rte1-id': 'q-fontname-2',
+ 'desc': 'query the "fontname" value',
+ 'pad': '<font face="arial" style="font-family: courier">foo[bar]baz</font>',
+ 'expected': 'courier' },
+
+ { 'id': 'FN_FONTf:a-FONTf:c-1_SI',
+ 'rte1-id': 'q-fontname-3',
+ 'desc': 'query the "fontname" value',
+ 'pad': '<font face="arial"><font face="courier">foo[bar]baz</font></font>',
+ 'expected': 'courier' },
+
+ { 'id': 'FN_SPANs:ff:c-FONTf:a-1_SI',
+ 'rte1-id': 'q-fontname-4',
+ 'desc': 'query the "fontname" value',
+ 'pad': '<span style="font-family: courier"><font face="arial">foo[bar]baz</font></span>',
+ 'expected': 'arial' },
+
+ { 'id': 'FN_SPAN.fs18px-1_SI',
+ 'desc': 'query the "fontname" value',
+ 'pad': '<span class="courier">foo[bar]baz</span>',
+ 'expected': 'courier' },
+
+ { 'id': 'FN_MYCOURIER-1-SI',
+ 'desc': 'query the "fontname" value',
+ 'pad': '<mycourier>foo[bar]baz</mycourier>',
+ 'expected': 'courier' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] query font size',
+ 'qcvalue': 'fontsize',
+ 'tests': [
+ { 'id': 'FS_FONTsz:4-1_SI',
+ 'rte1-id': 'q-fontsize-0',
+ 'desc': 'query the "fontsize" value',
+ 'pad': '<font size=4>foo[bar]baz</font>',
+ 'expected': '18px' },
+
+ { 'id': 'FS_FONTs:fs:l-1_SI',
+ 'desc': 'query the "fontsize" value',
+ 'pad': '<font style="font-size: large">foo[bar]baz</font>',
+ 'expected': '18px' },
+
+ { 'id': 'FS_FONT.ass.s:fs:l-1_SI',
+ 'rte1-id': 'q-fontsize-1',
+ 'desc': 'query the "fontsize" value',
+ 'pad': '<font class="Apple-style-span" style="font-size: large">foo[bar]baz</font>',
+ 'expected': '18px' },
+
+ { 'id': 'FS_FONTsz:1.s:fs:xl-1_SI',
+ 'rte1-id': 'q-fontsize-2',
+ 'desc': 'query the "fontsize" value',
+ 'pad': '<font size=1 style="font-size: x-large">foo[bar]baz</font>',
+ 'expected': '24px' },
+
+ { 'id': 'FS_SPAN.large-1_SI',
+ 'desc': 'query the "fontsize" value',
+ 'pad': '<span class="large">foo[bar]baz</span>',
+ 'expected': 'large' },
+
+ { 'id': 'FS_SPAN.fs18px-1_SI',
+ 'desc': 'query the "fontsize" value',
+ 'pad': '<span class="fs18px">foo[bar]baz</span>',
+ 'expected': '18px' },
+
+ { 'id': 'FA_MYLARGE-1-SI',
+ 'desc': 'query the "fontsize" value',
+ 'pad': '<mylarge>foo[bar]baz</mylarge>',
+ 'expected': 'large' },
+
+ { 'id': 'FA_MYFS18PX-1-SI',
+ 'desc': 'query the "fontsize" value',
+ 'pad': '<myfs18px>foo[bar]baz</myfs18px>',
+ 'expected': '18px' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] query background color',
+ 'qcvalue': 'backcolor',
+ 'tests': [
+ { 'id': 'BC_FONTs:bc:fca-1_SI',
+ 'rte1-id': 'q-backcolor-0',
+ 'desc': 'query the "backcolor" value',
+ 'pad': '<font style="background-color: #ffccaa">foo[bar]baz</font>',
+ 'expected': '#ffccaa' },
+
+ { 'id': 'BC_SPANs:bc:abc-1_SI',
+ 'rte1-id': 'q-backcolor-2',
+ 'desc': 'query the "backcolor" value',
+ 'pad': '<span style="background-color: #aabbcc">foo[bar]baz</span>',
+ 'expected': '#aabbcc' },
+
+ { 'id': 'BC_FONTs:bc:084-SPAN-1_SI',
+ 'desc': 'query the "backcolor" value, where the color was set on an ancestor',
+ 'pad': '<font style="background-color: #008844"><span>foo[bar]baz</span></font>',
+ 'expected': '#008844' },
+
+ { 'id': 'BC_SPANs:bc:cde-SPAN-1_SI',
+ 'desc': 'query the "backcolor" value, where the color was set on an ancestor',
+ 'pad': '<span style="background-color: #ccddee"><span>foo[bar]baz</span></span>',
+ 'expected': '#ccddee' },
+
+ { 'id': 'BC_SPAN.ass.s:bc:rgb-1_SI',
+ 'rte1-id': 'q-backcolor-1',
+ 'desc': 'query the "backcolor" value',
+ 'pad': '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0)">foo[bar]baz</span>',
+ 'expected': '#ff0000' },
+
+ { 'id': 'BC_SPAN.bcred-1_SI',
+ 'desc': 'query the "backcolor" value',
+ 'pad': '<span class="bcred">foo[bar]baz</span>',
+ 'expected': 'red' },
+
+ { 'id': 'BC_MYBCRED-1-SI',
+ 'desc': 'query the "backcolor" value',
+ 'pad': '<mybcred>foo[bar]baz</mybcred>',
+ 'expected': 'red' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] query text color',
+ 'qcvalue': 'forecolor',
+ 'tests': [
+ { 'id': 'FC_FONTc:f00-1_SI',
+ 'rte1-id': 'q-forecolor-0',
+ 'desc': 'query the "forecolor" value',
+ 'pad': '<font color="#ff0000">foo[bar]baz</font>',
+ 'expected': '#ff0000' },
+
+ { 'id': 'FC_SPANs:c:0f0-1_SI',
+ 'rte1-id': 'q-forecolor-1',
+ 'desc': 'query the "forecolor" value',
+ 'pad': '<span style="color: #00ff00">foo[bar]baz</span>',
+ 'expected': '#00ff00' },
+
+ { 'id': 'FC_FONTc:333.s:c:999-1_SI',
+ 'rte1-id': 'q-forecolor-2',
+ 'desc': 'query the "forecolor" value',
+ 'pad': '<font color="#333333" style="color: #999999">foo[bar]baz</font>',
+ 'expected': '#999999' },
+
+ { 'id': 'FC_FONTc:641-SPAN-1_SI',
+ 'desc': 'query the "forecolor" value, where the color was set on an ancestor',
+ 'pad': '<font color="#664411"><span>foo[bar]baz</span></font>',
+ 'expected': '#664411' },
+
+ { 'id': 'FC_SPANs:c:d95-SPAN-1_SI',
+ 'desc': 'query the "forecolor" value, where the color was set on an ancestor',
+ 'pad': '<span style="color: #dd9955"><span>foo[bar]baz</span></span>',
+ 'expected': '#dd9955' },
+
+ { 'id': 'FC_SPAN.red-1_SI',
+ 'desc': 'query the "forecolor" value',
+ 'pad': '<span class="red">foo[bar]baz</span>',
+ 'expected': 'red' },
+
+ { 'id': 'FC_MYRED-1-SI',
+ 'desc': 'query the "forecolor" value',
+ 'pad': '<myred>foo[bar]baz</myred>',
+ 'expected': 'red' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] query hilight color (same as background color)',
+ 'qcvalue': 'hilitecolor',
+ 'tests': [
+ { 'id': 'HC_FONTs:bc:fc0-1_SI',
+ 'rte1-id': 'q-hilitecolor-0',
+ 'desc': 'query the "hilitecolor" value',
+ 'pad': '<font style="background-color: #ffcc00">foo[bar]baz</font>',
+ 'expected': '#ffcc00' },
+
+ { 'id': 'HC_SPANs:bc:a0c-1_SI',
+ 'rte1-id': 'q-hilitecolor-2',
+ 'desc': 'query the "hilitecolor" value',
+ 'pad': '<span style="background-color: #aa00cc">foo[bar]baz</span>',
+ 'expected': '#aa00cc' },
+
+ { 'id': 'HC_SPAN.ass.s:bc:rgb-1_SI',
+ 'rte1-id': 'q-hilitecolor-1',
+ 'desc': 'query the "hilitecolor" value',
+ 'pad': '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0)">foo[bar]baz</span>',
+ 'expected': '#ff0000' },
+
+ { 'id': 'HC_FONTs:bc:83e-SPAN-1_SI',
+ 'desc': 'query the "hilitecolor" value, where the color was set on an ancestor',
+ 'pad': '<font style="background-color: #8833ee"><span>foo[bar]baz</span></font>',
+ 'expected': '#8833ee' },
+
+ { 'id': 'HC_SPANs:bc:b12-SPAN-1_SI',
+ 'desc': 'query the "hilitecolor" value, where the color was set on an ancestor',
+ 'pad': '<span style="background-color: #bb1122"><span>foo[bar]baz</span></span>',
+ 'expected': '#bb1122' },
+
+ { 'id': 'HC_SPAN.bcred-1_SI',
+ 'desc': 'query the "hilitecolor" value',
+ 'pad': '<span class="bcred">foo[bar]baz</span>',
+ 'expected': 'red' },
+
+ { 'id': 'HC_MYBCRED-1-SI',
+ 'desc': 'query the "hilitecolor" value',
+ 'pad': '<mybcred>foo[bar]baz</mybcred>',
+ 'expected': 'red' }
+ ]
+ }
+ ]
+}
+
+QUERYVALUE_TESTS_CSS = {
+ 'id': 'QVC',
+ 'caption': 'queryCommandValue Tests, using styleWithCSS',
+ 'checkAttrs': False,
+ 'checkStyle': False,
+ 'styleWithCSS': True,
+
+ 'Proposed': QUERYVALUE_TESTS['Proposed']
+}
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/selection.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/selection.py
new file mode 100644
index 000000000..35891386a
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/selection.py
@@ -0,0 +1,772 @@
+
+SELECTION_TESTS = {
+ 'id': 'S',
+ 'caption': 'Selection Tests',
+ 'checkAttrs': True,
+ 'checkStyle': True,
+ 'styleWithCSS': False,
+
+ 'Proposed': [
+ { 'desc': '',
+ 'command': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': 'selectall',
+ 'command': 'selectall',
+ 'tests': [
+ { 'id': 'SELALL_TEXT-1_SI',
+ 'desc': 'select all, text only',
+ 'pad': 'foo [bar] baz',
+ 'expected': [ '[foo bar baz]',
+ '{foo bar baz}' ] },
+
+ { 'id': 'SELALL_I-1_SI',
+ 'desc': 'select all, with outer tags',
+ 'pad': '<i>foo [bar] baz</i>',
+ 'expected': '{<i>foo bar baz</i>}' }
+ ]
+ },
+
+ { 'desc': 'unselect',
+ 'command': 'unselect',
+ 'tests': [
+ { 'id': 'UNSEL_TEXT-1_SI',
+ 'desc': 'unselect',
+ 'pad': 'foo [bar] baz',
+ 'expected': 'foo bar baz' }
+ ]
+ },
+
+ { 'desc': 'sel.modify (generic)',
+ 'tests': [
+ { 'id': 'SM:m.f.c_TEXT-1_SC-1',
+ 'desc': 'move caret 1 character forward',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'foo b^ar baz',
+ 'expected': 'foo ba^r baz' },
+
+ { 'id': 'SM:m.b.c_TEXT-1_SC-1',
+ 'desc': 'move caret 1 character backward',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'foo b^ar baz',
+ 'expected': 'foo ^bar baz' },
+
+ { 'id': 'SM:m.f.c_TEXT-1_SI-1',
+ 'desc': 'move caret forward (sollapse selection)',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'foo [bar] baz',
+ 'expected': 'foo bar^ baz' },
+
+ { 'id': 'SM:m.b.c_TEXT-1_SI-1',
+ 'desc': 'move caret backward (collapse selection)',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'foo [bar] baz',
+ 'expected': 'foo ^bar baz' },
+
+ { 'id': 'SM:m.f.w_TEXT-1_SC-1',
+ 'desc': 'move caret 1 word forward',
+ 'function': 'sel.modify("move", "forward", "word");',
+ 'pad': 'foo b^ar baz',
+ 'expected': 'foo bar^ baz' },
+
+ { 'id': 'SM:m.f.w_TEXT-1_SC-2',
+ 'desc': 'move caret 1 word forward',
+ 'function': 'sel.modify("move", "forward", "word");',
+ 'pad': 'foo^ bar baz',
+ 'expected': 'foo bar^ baz' },
+
+ { 'id': 'SM:m.f.w_TEXT-1_SI-1',
+ 'desc': 'move caret 1 word forward from non-collapsed selection',
+ 'function': 'sel.modify("move", "forward", "word");',
+ 'pad': 'foo [bar] baz',
+ 'expected': 'foo bar baz^' },
+
+ { 'id': 'SM:m.b.w_TEXT-1_SC-1',
+ 'desc': 'move caret 1 word backward',
+ 'function': 'sel.modify("move", "backward", "word");',
+ 'pad': 'foo b^ar baz',
+ 'expected': 'foo ^bar baz' },
+
+ { 'id': 'SM:m.b.w_TEXT-1_SC-3',
+ 'desc': 'move caret 1 word backward',
+ 'function': 'sel.modify("move", "backward", "word");',
+ 'pad': 'foo bar ^baz',
+ 'expected': 'foo ^bar baz' },
+
+ { 'id': 'SM:m.b.w_TEXT-1_SI-1',
+ 'desc': 'move caret 1 word backward from non-collapsed selection',
+ 'function': 'sel.modify("move", "backward", "word");',
+ 'pad': 'foo [bar] baz',
+ 'expected': '^foo bar baz' }
+ ]
+ },
+
+ { 'desc': 'sel.modify: move forward over combining diacritics, etc.',
+ 'tests': [
+ { 'id': 'SM:m.f.c_CHAR-2_SC-1',
+ 'desc': 'move 1 character forward over combined o with diaeresis',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'fo^&#xF6;barbaz',
+ 'expected': 'fo&#xF6;^barbaz' },
+
+ { 'id': 'SM:m.f.c_CHAR-3_SC-1',
+ 'desc': 'move 1 character forward over character with combining diaeresis above',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'fo^o&#x0308;barbaz',
+ 'expected': 'foo&#x0308;^barbaz' },
+
+ { 'id': 'SM:m.f.c_CHAR-4_SC-1',
+ 'desc': 'move 1 character forward over character with combining diaeresis below',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'fo^o&#x0324;barbaz',
+ 'expected': 'foo&#x0324;^barbaz' },
+
+ { 'id': 'SM:m.f.c_CHAR-5_SC-1',
+ 'desc': 'move 1 character forward over character with combining diaeresis above and below',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'fo^o&#x0308;&#x0324;barbaz',
+ 'expected': 'foo&#x0308;&#x0324;^barbaz' },
+
+ { 'id': 'SM:m.f.c_CHAR-5_SI-1',
+ 'desc': 'move 1 character forward over character with combining diaeresis above and below, selection on diaeresis above',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'foo[&#x0308;]&#x0324;barbaz',
+ 'expected': 'foo&#x0308;&#x0324;^barbaz' },
+
+ { 'id': 'SM:m.f.c_CHAR-5_SI-2',
+ 'desc': 'move 1 character forward over character with combining diaeresis above and below, selection on diaeresis below',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'foo&#x0308;[&#x0324;]barbaz',
+ 'expected': 'foo&#x0308;&#x0324;^barbaz' },
+
+ { 'id': 'SM:m.f.c_CHAR-5_SL',
+ 'desc': 'move 1 character forward over character with combining diaeresis above and below, selection oblique on diaeresis and preceding text',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'fo[o&#x0308;]&#x0324;barbaz',
+ 'expected': 'foo&#x0308;&#x0324;^barbaz' },
+
+ { 'id': 'SM:m.f.c_CHAR-5_SR',
+ 'desc': 'move 1 character forward over character with combining diaeresis above and below, selection oblique on diaeresis and following text',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'foo&#x0308;[&#x0324;bar]baz',
+ 'expected': 'foo&#x0308;&#x0324;bar^baz' },
+
+ { 'id': 'SM:m.f.c_CHAR-6_SC-1',
+ 'desc': 'move 1 character forward over character with enclosing square',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'fo^o&#x20DE;barbaz',
+ 'expected': 'foo&#x20DE;^barbaz' },
+
+ { 'id': 'SM:m.f.c_CHAR-7_SC-1',
+ 'desc': 'move 1 character forward over character with combining long solidus overlay',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'fo^o&#x0338;barbaz',
+ 'expected': 'foo&#x0338;^barbaz' }
+ ]
+ },
+
+ { 'desc': 'sel.modify: move backward over combining diacritics, etc.',
+ 'tests': [
+ { 'id': 'SM:m.b.c_CHAR-2_SC-1',
+ 'desc': 'move 1 character backward over combined o with diaeresis',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'fo&#xF6;^barbaz',
+ 'expected': 'fo^&#xF6;barbaz' },
+
+ { 'id': 'SM:m.b.c_CHAR-3_SC-1',
+ 'desc': 'move 1 character backward over character with combining diaeresis above',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'foo&#x0308;^barbaz',
+ 'expected': 'fo^o&#x0308;barbaz' },
+
+ { 'id': 'SM:m.b.c_CHAR-4_SC-1',
+ 'desc': 'move 1 character backward over character with combining diaeresis below',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'foo&#x0324;^barbaz',
+ 'expected': 'fo^o&#x0324;barbaz' },
+
+ { 'id': 'SM:m.b.c_CHAR-5_SC-1',
+ 'desc': 'move 1 character backward over character with combining diaeresis above and below',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'foo&#x0308;&#x0324;^barbaz',
+ 'expected': 'fo^o&#x0308;&#x0324;barbaz' },
+
+ { 'id': 'SM:m.b.c_CHAR-5_SI-1',
+ 'desc': 'move 1 character backward over character with combining diaeresis above and below, selection on diaeresis above',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'foo[&#x0308;]&#x0324;barbaz',
+ 'expected': 'fo^o&#x0308;&#x0324;barbaz' },
+
+ { 'id': 'SM:m.b.c_CHAR-5_SI-2',
+ 'desc': 'move 1 character backward over character with combining diaeresis above and below, selection on diaeresis below',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'foo&#x0308;[&#x0324;]barbaz',
+ 'expected': 'fo^o&#x0308;&#x0324;barbaz' },
+
+ { 'id': 'SM:m.b.c_CHAR-5_SL',
+ 'desc': 'move 1 character backward over character with combining diaeresis above and below, selection oblique on diaeresis and preceding text',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'fo[o&#x0308;]&#x0324;barbaz',
+ 'expected': 'fo^o&#x0308;&#x0324;barbaz' },
+
+ { 'id': 'SM:m.b.c_CHAR-5_SR',
+ 'desc': 'move 1 character backward over character with combining diaeresis above and below, selection oblique on diaeresis and following text',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'foo&#x0308;[&#x0324;bar]baz',
+ 'expected': 'fo^o&#x0308;&#x0324;barbaz' },
+
+ { 'id': 'SM:m.b.c_CHAR-6_SC-1',
+ 'desc': 'move 1 character backward over character with enclosing square',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'foo&#x20DE;^barbaz',
+ 'expected': 'fo^o&#x20DE;barbaz' },
+
+ { 'id': 'SM:m.b.c_CHAR-7_SC-1',
+ 'desc': 'move 1 character backward over character with combining long solidus overlay',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'foo&#x0338;^barbaz',
+ 'expected': 'fo^o&#x0338;barbaz' }
+ ]
+ },
+
+ { 'desc': 'sel.modify: move forward/backward/left/right in RTL text',
+ 'tests': [
+ { 'id': 'SM:m.f.c_Pdir:rtl-1_SC-1',
+ 'desc': 'move caret forward 1 character in right-to-left text',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': '<p dir="rtl">foo b^ar baz</p>',
+ 'expected': '<p dir="rtl">foo ba^r baz</p>' },
+
+ { 'id': 'SM:m.b.c_Pdir:rtl-1_SC-1',
+ 'desc': 'move caret backward 1 character in right-to-left text',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': '<p dir="rtl">foo ba^r baz</p>',
+ 'expected': '<p dir="rtl">foo b^ar baz</p>' },
+
+ { 'id': 'SM:m.r.c_Pdir:rtl-1_SC-1',
+ 'desc': 'move caret 1 character to the right in LTR text within RTL context',
+ 'function': 'sel.modify("move", "right", "character");',
+ 'pad': '<p dir="rtl">foo b^ar baz</p>',
+ 'expected': '<p dir="rtl">foo ba^r baz</p>' },
+
+ { 'id': 'SM:m.l.c_Pdir:rtl-1_SC-1',
+ 'desc': 'move caret 1 character to the left in LTR text within RTL context',
+ 'function': 'sel.modify("move", "left", "character");',
+ 'pad': '<p dir="rtl">foo ba^r baz</p>',
+ 'expected': '<p dir="rtl">foo b^ar baz</p>' },
+
+
+ { 'id': 'SM:m.f.c_TEXT:ar-1_SC-1',
+ 'desc': 'move caret forward 1 character in Arabic text',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': '&#1605;&#x0631;&#1581;^&#1576;&#x0627; &#x0627;&#1604;&#x0639;&#x0627;&#1604;&#1605;',
+ 'expected': '&#1605;&#x0631;&#1581;&#1576;^&#x0627; &#x0627;&#1604;&#x0639;&#x0627;&#1604;&#1605;' },
+
+ { 'id': 'SM:m.b.c_TEXT:ar-1_SC-1',
+ 'desc': 'move caret backward 1 character in Arabic text',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': '&#1605;&#x0631;&#1581;^&#1576;&#x0627; &#x0627;&#1604;&#x0639;&#x0627;&#1604;&#1605;',
+ 'expected': '&#1605;&#x0631;^&#1581;&#1576;&#x0627; &#x0627;&#1604;&#x0639;&#x0627;&#1604;&#1605;' },
+
+ { 'id': 'SM:m.f.c_TEXT:he-1_SC-1',
+ 'desc': 'move caret forward 1 character in Hebrew text',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': '&#x05E9;&#x05DC;^&#x05D5;&#x05DD; &#x05E2;&#x05D5;&#x05DC;&#x05DD;',
+ 'expected': '&#x05E9;&#x05DC;&#x05D5;^&#x05DD; &#x05E2;&#x05D5;&#x05DC;&#x05DD;' },
+
+ { 'id': 'SM:m.b.c_TEXT:he-1_SC-1',
+ 'desc': 'move caret backward 1 character in Hebrew text',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': '&#x05E9;&#x05DC;^&#x05D5;&#x05DD; &#x05E2;&#x05D5;&#x05DC;&#x05DD;',
+ 'expected': '&#x05E9;^&#x05DC;&#x05D5;&#x05DD; &#x05E2;&#x05D5;&#x05DC;&#x05DD;' },
+
+
+ { 'id': 'SM:m.f.c_BDOdir:rtl-1_SC-1',
+ 'desc': 'move caret forward 1 character inside <bdo>',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'foo <bdo dir="rtl">b^ar</bdo> baz',
+ 'expected': 'foo <bdo dir="rtl">ba^r</bdo> baz' },
+
+ { 'id': 'SM:m.b.c_BDOdir:rtl-1_SC-1',
+ 'desc': 'move caret backward 1 character inside <bdo>',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'foo <bdo dir="rtl">ba^r</bdo> baz',
+ 'expected': 'foo <bdo dir="rtl">b^ar</bdo> baz' },
+
+ { 'id': 'SM:m.r.c_BDOdir:rtl-1_SC-1',
+ 'desc': 'move caret 1 character to the right inside <bdo>',
+ 'function': 'sel.modify("move", "right", "character");',
+ 'pad': 'foo <bdo dir="rtl">ba^r</bdo> baz',
+ 'expected': 'foo <bdo dir="rtl">b^ar</bdo> baz' },
+
+ { 'id': 'SM:m.l.c_BDOdir:rtl-1_SC-1',
+ 'desc': 'move caret 1 character to the left inside <bdo>',
+ 'function': 'sel.modify("move", "left", "character");',
+ 'pad': 'foo <bdo dir="rtl">b^ar</bdo> baz',
+ 'expected': 'foo <bdo dir="rtl">ba^r</bdo> baz' },
+
+
+ { 'id': 'SM:m.f.c_TEXTrle-1_SC-rtl-1',
+ 'desc': 'move caret forward in RTL text within RLE-PDF marks',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'I said, "(RLE)&#x202B;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;^&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLE)&#x202B;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;^&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+ { 'id': 'SM:m.b.c_TEXTrle-1_SC-rtl-1',
+ 'desc': 'move caret backward in RTL text within RLE-PDF marks',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'I said, "(RLE)&#x202B;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;^&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLE)&#x202B;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;^&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+ { 'id': 'SM:m.r.c_TEXTrle-1_SC-rtl-1',
+ 'desc': 'move caret 1 character to the right in RTL text within RLE-PDF marks',
+ 'function': 'sel.modify("move", "right", "character");',
+ 'pad': 'I said, "(RLE)&#x202B;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;^&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLE)&#x202B;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;^&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+ { 'id': 'SM:m.l.c_TEXTrle-1_SC-rtl-1',
+ 'desc': 'move caret 1 character to the left in RTL text within RLE-PDF marks',
+ 'function': 'sel.modify("move", "left", "character");',
+ 'pad': 'I said, "(RLE)&#x202B;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;^&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLE)&#x202B;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;^&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+ { 'id': 'SM:m.f.c_TEXTrle-1_SC-ltr-1',
+ 'desc': 'move caret forward in LTR text within RLE-PDF marks',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'I said, "(RLE)&#x202B;c^ar &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLE)&#x202B;ca^r &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+ { 'id': 'SM:m.b.c_TEXTrle-1_SC-ltr-1',
+ 'desc': 'move caret backward in LTR text within RLE-PDF marks',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'I said, "(RLE)&#x202B;ca^r &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLE)&#x202B;c^ar &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+ { 'id': 'SM:m.r.c_TEXTrle-1_SC-ltr-1',
+ 'desc': 'move caret 1 character to the right in LTR text within RLE-PDF marks',
+ 'function': 'sel.modify("move", "right", "character");',
+ 'pad': 'I said, "(RLE)&#x202B;c^ar &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLE)&#x202B;ca^r &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+ { 'id': 'SM:m.l.c_TEXTrle-1_SC-ltr-1',
+ 'desc': 'move caret 1 character to the left in LTR text within RLE-PDF marks',
+ 'function': 'sel.modify("move", "left", "character");',
+ 'pad': 'I said, "(RLE)&#x202B;ca^r &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLE)&#x202B;c^ar &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+
+ { 'id': 'SM:m.f.c_TEXTrlo-1_SC-rtl-1',
+ 'desc': 'move caret forward in RTL text within RLO-PDF marks',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'I said, "(RLO)&#x202E;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;^&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLO)&#x202E;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;^&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+ { 'id': 'SM:m.b.c_TEXTrlo-1_SC-rtl-1',
+ 'desc': 'move caret backward in RTL text within RLO-PDF marks',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'I said, "(RLO)&#x202E;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;^&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLO)&#x202E;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;^&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+ { 'id': 'SM:m.r.c_TEXTrlo-1_SC-rtl-1',
+ 'desc': 'move caret 1 character to the right in RTL text within RLO-PDF marks',
+ 'function': 'sel.modify("move", "right", "character");',
+ 'pad': 'I said, "(RLO)&#x202E;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;^&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLO)&#x202E;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;^&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+ { 'id': 'SM:m.l.c_TEXTrlo-1_SC-rtl-1',
+ 'desc': 'move caret 1 character to the left in RTL text within RLO-PDF marks',
+ 'function': 'sel.modify("move", "left", "character");',
+ 'pad': 'I said, "(RLO)&#x202E;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;^&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLO)&#x202E;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;^&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+ { 'id': 'SM:m.f.c_TEXTrlo-1_SC-ltr-1',
+ 'desc': 'move caret forward in Latin text within RLO-PDF marks',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'I said, "(RLO)&#x202E;c^ar &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLO)&#x202E;ca^r &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+ { 'id': 'SM:m.b.c_TEXTrlo-1_SC-ltr-1',
+ 'desc': 'move caret backward in Latin text within RLO-PDF marks',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'I said, "(RLO)&#x202E;ca^r &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLO)&#x202E;c^ar &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+ { 'id': 'SM:m.r.c_TEXTrlo-1_SC-ltr-1',
+ 'desc': 'move caret 1 character to the right in Latin text within RLO-PDF marks',
+ 'function': 'sel.modify("move", "right", "character");',
+ 'pad': 'I said, "(RLO)&#x202E;ca^r &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLO)&#x202E;c^ar &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+ { 'id': 'SM:m.l.c_TEXTrlo-1_SC-ltr-1',
+ 'desc': 'move caret 1 character to the left in Latin text within RLO-PDF marks',
+ 'function': 'sel.modify("move", "left", "character");',
+ 'pad': 'I said, "(RLO)&#x202E;c^ar &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLO)&#x202E;ca^r &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+
+ { 'id': 'SM:m.f.c_TEXTrlm-1_SC-1',
+ 'desc': 'move caret forward in RTL text within neutral characters followed by RLM',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'I said, "&#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;!^?!&#x200F;(RLM)".',
+ 'expected': 'I said, "&#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;!?^!&#x200F;(RLM)".' },
+
+ { 'id': 'SM:m.b.c_TEXTrlm-1_SC-1',
+ 'desc': 'move caret backward in RTL text within neutral characters followed by RLM',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'I said, "&#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;!?^!&#x200F;(RLM)".',
+ 'expected': 'I said, "&#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;!^?!&#x200F;(RLM)".' },
+
+ { 'id': 'SM:m.r.c_TEXTrlm-1_SC-1',
+ 'desc': 'move caret 1 character to the right in RTL text within neutral characters followed by RLM',
+ 'function': 'sel.modify("move", "right", "character");',
+ 'pad': 'I said, "&#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;!?^!&#x200F;(RLM)".',
+ 'expected': 'I said, "&#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;!^?!&#x200F;(RLM)".' },
+
+ { 'id': 'SM:m.l.c_TEXTrlm-1_SC-1',
+ 'desc': 'move caret 1 character to the left in RTL text within neutral characters followed by RLM',
+ 'function': 'sel.modify("move", "left", "character");',
+ 'pad': 'I said, "&#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;!^?!&#x200F;(RLM)".',
+ 'expected': 'I said, "&#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;!?^!&#x200F;(RLM)".' }
+ ]
+ },
+
+ { 'desc': 'sel.modify: move forward/backward over words in Japanese text',
+ 'tests': [
+ { 'id': 'SM:m.f.w_TEXT-jp_SC-1',
+ 'desc': 'move caret forward 1 word in Japanese text (adjective)',
+ 'function': 'sel.modify("move", "forward", "word");',
+ 'pad': '^&#x9762;&#x767D;&#x3044;&#x4F8B;&#x6587;&#x3092;&#x30C6;&#x30B9;&#x30C8;&#x3057;&#x307E;&#x3057;&#x3087;&#x3046;&#x3002;',
+ 'expected': '&#x9762;&#x767D;&#x3044;^&#x4F8B;&#x6587;&#x3092;&#x30C6;&#x30B9;&#x30C8;&#x3057;&#x307E;&#x3057;&#x3087;&#x3046;&#x3002;' },
+
+ { 'id': 'SM:m.f.w_TEXT-jp_SC-2',
+ 'desc': 'move caret forward 1 word in Japanese text (in the middle of a word)',
+ 'function': 'sel.modify("move", "forward", "word");',
+ 'pad': '&#x9762;^&#x767D;&#x3044;&#x4F8B;&#x6587;&#x3092;&#x30C6;&#x30B9;&#x30C8;&#x3057;&#x307E;&#x3057;&#x3087;&#x3046;&#x3002;',
+ 'expected': '&#x9762;&#x767D;&#x3044;^&#x4F8B;&#x6587;&#x3092;&#x30C6;&#x30B9;&#x30C8;&#x3057;&#x307E;&#x3057;&#x3087;&#x3046;&#x3002;' },
+
+ { 'id': 'SM:m.f.w_TEXT-jp_SC-3',
+ 'desc': 'move caret forward 1 word in Japanese text (noun)',
+ 'function': 'sel.modify("move", "forward", "word");',
+ 'pad': '&#x9762;&#x767D;&#x3044;^&#x4F8B;&#x6587;&#x3092;&#x30C6;&#x30B9;&#x30C8;&#x3057;&#x307E;&#x3057;&#x3087;&#x3046;&#x3002;',
+ 'expected': [ '&#x9762;&#x767D;&#x3044;&#x4F8B;&#x6587;^&#x3092;&#x30C6;&#x30B9;&#x30C8;&#x3057;&#x307E;&#x3057;&#x3087;&#x3046;&#x3002;',
+ '&#x9762;&#x767D;&#x3044;&#x4F8B;&#x6587;&#x3092;^&#x30C6;&#x30B9;&#x30C8;&#x3057;&#x307E;&#x3057;&#x3087;&#x3046;&#x3002;' ] },
+
+ { 'id': 'SM:m.f.w_TEXT-jp_SC-4',
+ 'desc': 'move caret forward 1 word in Japanese text (Katakana)',
+ 'function': 'sel.modify("move", "forward", "word");',
+ 'pad': '&#x9762;&#x767D;&#x3044;&#x4F8B;&#x6587;&#x3092;^&#x30C6;&#x30B9;&#x30C8;&#x3057;&#x307E;&#x3057;&#x3087;&#x3046;&#x3002;',
+ 'expected': '&#x9762;&#x767D;&#x3044;&#x4F8B;&#x6587;&#x3092;&#x30C6;&#x30B9;&#x30C8;^&#x3057;&#x307E;&#x3057;&#x3087;&#x3046;&#x3002;' },
+
+ { 'id': 'SM:m.f.w_TEXT-jp_SC-5',
+ 'desc': 'move caret forward 1 word in Japanese text (verb)',
+ 'function': 'sel.modify("move", "forward", "word");',
+ 'pad': '&#x9762;&#x767D;&#x3044;&#x4F8B;&#x6587;&#x3092;&#x30C6;&#x30B9;&#x30C8;^&#x3057;&#x307E;&#x3057;&#x3087;&#x3046;&#x3002;',
+ 'expected': '&#x9762;&#x767D;&#x3044;&#x4F8B;&#x6587;&#x3092;&#x30C6;&#x30B9;&#x30C8;&#x3057;&#x307E;&#x3057;&#x3087;&#x3046;^&#x3002;' }
+ ]
+ },
+
+ { 'desc': 'sel.modify: extend selection forward',
+ 'tests': [
+ { 'id': 'SM:e.f.c_TEXT-1_SC-1',
+ 'desc': 'extend selection 1 character forward',
+ 'function': 'sel.modify("extend", "forward", "character");',
+ 'pad': 'foo ^bar baz',
+ 'expected': 'foo [b]ar baz' },
+
+ { 'id': 'SM:e.f.c_TEXT-1_SI-1',
+ 'desc': 'extend selection 1 character forward',
+ 'function': 'sel.modify("extend", "forward", "character");',
+ 'pad': 'foo [b]ar baz',
+ 'expected': 'foo [ba]r baz' },
+
+ { 'id': 'SM:e.f.w_TEXT-1_SC-1',
+ 'desc': 'extend selection 1 word forward',
+ 'function': 'sel.modify("extend", "forward", "word");',
+ 'pad': 'foo ^bar baz',
+ 'expected': 'foo [bar] baz' },
+
+ { 'id': 'SM:e.f.w_TEXT-1_SI-1',
+ 'desc': 'extend selection 1 word forward',
+ 'function': 'sel.modify("extend", "forward", "word");',
+ 'pad': 'foo [b]ar baz',
+ 'expected': 'foo [bar] baz' },
+
+ { 'id': 'SM:e.f.w_TEXT-1_SI-2',
+ 'desc': 'extend selection 1 word forward',
+ 'function': 'sel.modify("extend", "forward", "word");',
+ 'pad': 'foo [bar] baz',
+ 'expected': 'foo [bar baz]' }
+ ]
+ },
+
+ { 'desc': 'sel.modify: extend selection backward, shrinking it',
+ 'tests': [
+ { 'id': 'SM:e.b.c_TEXT-1_SI-2',
+ 'desc': 'extend selection 1 character backward',
+ 'function': 'sel.modify("extend", "backward", "character");',
+ 'pad': 'foo [bar] baz',
+ 'expected': 'foo [ba]r baz' },
+
+ { 'id': 'SM:e.b.c_TEXT-1_SI-1',
+ 'desc': 'extend selection 1 character backward',
+ 'function': 'sel.modify("extend", "backward", "character");',
+ 'pad': 'foo [b]ar baz',
+ 'expected': 'foo ^bar baz' },
+
+ { 'id': 'SM:e.b.w_TEXT-1_SI-3',
+ 'desc': 'extend selection 1 word backward',
+ 'function': 'sel.modify("extend", "backward", "word");',
+ 'pad': 'foo [bar baz]',
+ 'expected': 'foo [bar] baz' },
+
+ { 'id': 'SM:e.b.w_TEXT-1_SI-2',
+ 'desc': 'extend selection 1 word backward',
+ 'function': 'sel.modify("extend", "backward", "word");',
+ 'pad': 'foo [bar] baz',
+ 'expected': 'foo ^bar baz' },
+
+ { 'id': 'SM:e.b.w_TEXT-1_SI-4',
+ 'desc': 'extend selection 1 word backward',
+ 'function': 'sel.modify("extend", "backward", "word");',
+ 'pad': 'foo b[ar baz]',
+ 'expected': 'foo b[ar] baz' },
+
+ { 'id': 'SM:e.b.w_TEXT-1_SI-5',
+ 'desc': 'extend selection 1 word backward',
+ 'function': 'sel.modify("extend", "backward", "word");',
+ 'pad': 'foo b[ar] baz',
+ 'expected': 'foo b^ar baz' }
+ ]
+ },
+
+ { 'desc': 'sel.modify: extend selection backward, creating or extending a reverse selections',
+ 'tests': [
+ { 'id': 'SM:e.b.c_TEXT-1_SC-1',
+ 'desc': 'extend selection 1 character backward',
+ 'function': 'sel.modify("extend", "backward", "character");',
+ 'pad': 'foo b^ar baz',
+ 'expected': 'foo ]b[ar baz' },
+
+ { 'id': 'SM:e.b.c_TEXT-1_SIR-1',
+ 'desc': 'extend selection 1 character backward',
+ 'function': 'sel.modify("extend", "backward", "character");',
+ 'pad': 'foo b]a[r baz',
+ 'expected': 'foo ]ba[r baz' },
+
+ { 'id': 'SM:e.b.w_TEXT-1_SIR-1',
+ 'desc': 'extend selection 1 word backward',
+ 'function': 'sel.modify("extend", "backward", "word");',
+ 'pad': 'foo b]a[r baz',
+ 'expected': 'foo ]ba[r baz' },
+
+ { 'id': 'SM:e.b.w_TEXT-1_SIR-2',
+ 'desc': 'extend selection 1 word backward',
+ 'function': 'sel.modify("extend", "backward", "word");',
+ 'pad': 'foo ]ba[r baz',
+ 'expected': ']foo ba[r baz' }
+ ]
+ },
+
+ { 'desc': 'sel.modify: extend selection forward, shrinking a reverse selections',
+ 'tests': [
+ { 'id': 'SM:e.f.c_TEXT-1_SIR-1',
+ 'desc': 'extend selection 1 character forward',
+ 'function': 'sel.modify("extend", "forward", "character");',
+ 'pad': 'foo b]a[r baz',
+ 'expected': 'foo ba^r baz' },
+
+ { 'id': 'SM:e.f.c_TEXT-1_SIR-2',
+ 'desc': 'extend selection 1 character forward',
+ 'function': 'sel.modify("extend", "forward", "character");',
+ 'pad': 'foo ]ba[r baz',
+ 'expected': 'foo b]a[r baz' },
+
+ { 'id': 'SM:e.f.w_TEXT-1_SIR-1',
+ 'desc': 'extend selection 1 word forward',
+ 'function': 'sel.modify("extend", "forward", "word");',
+ 'pad': 'foo ]ba[r baz',
+ 'expected': 'foo ba^r baz' },
+
+ { 'id': 'SM:e.f.w_TEXT-1_SIR-3',
+ 'desc': 'extend selection 1 word forward',
+ 'function': 'sel.modify("extend", "forward", "word");',
+ 'pad': ']foo ba[r baz',
+ 'expected': 'foo ]ba[r baz' }
+ ]
+ },
+
+ { 'desc': 'sel.modify: extend selection forward to line boundary',
+ 'tests': [
+ { 'id': 'SM:e.f.lb_BR.BR-1_SC-1',
+ 'desc': 'extend selection forward to line boundary',
+ 'function': 'sel.modify("extend", "forward", "lineboundary");',
+ 'pad': 'fo^o<br>bar<br>baz',
+ 'expected': 'fo[o]<br>bar<br>baz' },
+
+ { 'id': 'SM:e.f.lb_BR.BR-1_SI-1',
+ 'desc': 'extend selection forward to next line boundary',
+ 'function': 'sel.modify("extend", "forward", "lineboundary");',
+ 'pad': 'fo[o]<br>bar<br>baz',
+ 'expected': 'fo[o<br>bar]<br>baz' },
+
+ { 'id': 'SM:e.f.lb_BR.BR-1_SM-1',
+ 'desc': 'extend selection forward to line boundary',
+ 'function': 'sel.modify("extend", "forward", "lineboundary");',
+ 'pad': 'fo[o<br>b]ar<br>baz',
+ 'expected': 'fo[o<br>bar]<br>baz' },
+
+ { 'id': 'SM:e.f.lb_P.P.P-1_SC-1',
+ 'desc': 'extend selection forward to line boundary',
+ 'function': 'sel.modify("extend", "forward", "lineboundary");',
+ 'pad': '<p>fo^o</p><p>bar</p><p>baz</p>',
+ 'expected': '<p>fo[o]</p><p>bar</p><p>baz</p>' },
+
+ { 'id': 'SM:e.f.lb_P.P.P-1_SI-1',
+ 'desc': 'extend selection forward to next line boundary',
+ 'function': 'sel.modify("extend", "forward", "lineboundary");',
+ 'pad': '<p>fo[o]</p><p>bar</p><p>baz</p>',
+ 'expected': '<p>fo[o</p><p>bar]</p><p>baz</p>' },
+
+ { 'id': 'SM:e.f.lb_P.P.P-1_SM-1',
+ 'desc': 'extend selection forward to line boundary',
+ 'function': 'sel.modify("extend", "forward", "lineboundary");',
+ 'pad': '<p>fo[o</p><p>b]ar</p><p>baz</p>',
+ 'expected': '<p>fo[o</p><p>bar]</p><p>baz</p>' },
+
+ { 'id': 'SM:e.f.lb_P.P.P-1_SMR-1',
+ 'desc': 'extend selection forward to line boundary',
+ 'function': 'sel.modify("extend", "forward", "lineboundary");',
+ 'pad': '<p>foo</p><p>b]a[r</p><p>baz</p>',
+ 'expected': '<p>foo</p><p>ba[r]</p><p>baz</p>' }
+ ]
+ },
+
+ { 'desc': 'sel.modify: extend selection backward to line boundary',
+ 'tests': [
+ { 'id': 'SM:e.b.lb_BR.BR-1_SC-2',
+ 'desc': 'extend selection backward to line boundary',
+ 'function': 'sel.modify("extend", "backward", "lineboundary");',
+ 'pad': 'foo<br>bar<br>b^az',
+ 'expected': 'foo<br>bar<br>]b[az' },
+
+ { 'id': 'SM:e.b.lb_BR.BR-1_SIR-2',
+ 'desc': 'extend selection backward to previous line boundary',
+ 'function': 'sel.modify("extend", "backward", "lineboundary");',
+ 'pad': 'foo<br>bar<br>]b[az',
+ 'expected': 'foo<br>]bar<br>b[az' },
+
+ { 'id': 'SM:e.b.lb_BR.BR-1_SMR-2',
+ 'desc': 'extend selection backward to line boundary',
+ 'function': 'sel.modify("extend", "backward", "lineboundary");',
+ 'pad': 'foo<br>ba]r<br>b[az',
+ 'expected': 'foo<br>]bar<br>b[az' },
+
+ { 'id': 'SM:e.b.lb_P.P.P-1_SC-2',
+ 'desc': 'extend selection backward to line boundary',
+ 'function': 'sel.modify("extend", "backward", "lineboundary");',
+ 'pad': '<p>foo</p><p>bar</p><p>b^az</p>',
+ 'expected': '<p>foo</p><p>bar</p><p>]b[az</p>' },
+
+ { 'id': 'SM:e.b.lb_P.P.P-1_SIR-2',
+ 'desc': 'extend selection backward to previous line boundary',
+ 'function': 'sel.modify("extend", "backward", "lineboundary");',
+ 'pad': '<p>foo</p><p>bar</p><p>]b[az</p>',
+ 'expected': '<p>foo</p><p>]bar</p><p>b[az</p>' },
+
+ { 'id': 'SM:e.b.lb_P.P.P-1_SMR-2',
+ 'desc': 'extend selection backward to line boundary',
+ 'function': 'sel.modify("extend", "backward", "lineboundary");',
+ 'pad': '<p>foo</p><p>ba]r</p><p>b[az</p>',
+ 'expected': '<p>foo</p><p>]bar</p><p>b[az</p>' },
+
+ { 'id': 'SM:e.b.lb_P.P.P-1_SM-2',
+ 'desc': 'extend selection backward to line boundary',
+ 'function': 'sel.modify("extend", "backward", "lineboundary");',
+ 'pad': '<p>foo</p><p>b[a]r</p><p>baz</p>',
+ 'expected': '<p>foo</p><p>]b[ar</p><p>baz</p>' }
+ ]
+ },
+
+ { 'desc': 'sel.modify: extend selection forward to next line (NOTE: use identical text in every line!)',
+ 'tests': [
+ { 'id': 'SM:e.f.l_BR.BR-2_SC-1',
+ 'desc': 'extend selection forward to next line',
+ 'function': 'sel.modify("extend", "forward", "line");',
+ 'pad': 'fo^o<br>foo<br>foo',
+ 'expected': 'fo[o<br>fo]o<br>foo' },
+
+ { 'id': 'SM:e.f.l_BR.BR-2_SI-1',
+ 'desc': 'extend selection forward to next line',
+ 'function': 'sel.modify("extend", "forward", "line");',
+ 'pad': 'fo[o]<br>foo<br>foo',
+ 'expected': 'fo[o<br>foo]<br>foo' },
+
+ { 'id': 'SM:e.f.l_BR.BR-2_SM-1',
+ 'desc': 'extend selection forward to next line',
+ 'function': 'sel.modify("extend", "forward", "line");',
+ 'pad': 'fo[o<br>f]oo<br>foo',
+ 'expected': 'fo[o<br>foo<br>f]oo' },
+
+ { 'id': 'SM:e.f.l_P.P-1_SC-1',
+ 'desc': 'extend selection forward to next line over paragraph boundaries',
+ 'function': 'sel.modify("extend", "forward", "line");',
+ 'pad': '<p>foo^bar</p><p>foobar</p>',
+ 'expected': '<p>foo[bar</p><p>foo]bar</p>' },
+
+ { 'id': 'SM:e.f.l_P.P-1_SMR-1',
+ 'desc': 'extend selection forward to next line over paragraph boundaries',
+ 'function': 'sel.modify("extend", "forward", "line");',
+ 'pad': '<p>fo]obar</p><p>foob[ar</p>',
+ 'expected': '<p>foobar</p><p>fo]ob[ar</p>' }
+ ]
+ },
+
+ { 'desc': 'sel.modify: extend selection backward to previous line (NOTE: use identical text in every line!)',
+ 'tests': [
+ { 'id': 'SM:e.b.l_BR.BR-2_SC-2',
+ 'desc': 'extend selection backward to previous line',
+ 'function': 'sel.modify("extend", "backward", "line");',
+ 'pad': 'foo<br>foo<br>f^oo',
+ 'expected': 'foo<br>f]oo<br>f[oo' },
+
+ { 'id': 'SM:e.b.l_BR.BR-2_SIR-2',
+ 'desc': 'extend selection backward to previous line',
+ 'function': 'sel.modify("extend", "backward", "line");',
+ 'pad': 'foo<br>foo<br>]f[oo',
+ 'expected': 'foo<br>]foo<br>f[oo' },
+
+ { 'id': 'SM:e.b.l_BR.BR-2_SMR-2',
+ 'desc': 'extend selection backward to previous line',
+ 'function': 'sel.modify("extend", "backward", "line");',
+ 'pad': 'foo<br>fo]o<br>f[oo',
+ 'expected': 'fo]o<br>foo<br>f[oo' },
+
+ { 'id': 'SM:e.b.l_P.P-1_SC-2',
+ 'desc': 'extend selection backward to next line over paragraph boundaries',
+ 'function': 'sel.modify("extend", "backward", "line");',
+ 'pad': '<p>foobar</p><p>foo^bar</p>',
+ 'expected': '<p>foo]bar</p><p>foo[bar</p>' },
+
+ { 'id': 'SM:e.b.l_P.P-1_SM-1',
+ 'desc': 'extend selection backward to next line over paragraph boundaries',
+ 'function': 'sel.modify("extend", "backward", "line");',
+ 'pad': '<p>fo[obar</p><p>foob]ar</p>',
+ 'expected': '<p>fo[ob]ar</p><p>foobar</p>' }
+ ]
+ },
+
+ { 'desc': 'sel.selectAllChildren(<element>)',
+ 'function': 'sel.selectAllChildren(doc.getElementById("div"));',
+ 'tests': [
+ { 'id': 'SAC:div_DIV-1_SC-1',
+ 'desc': 'selectAllChildren(<div>)',
+ 'pad': 'foo<div id="div">bar <span>ba^z</span></div>qoz',
+ 'expected': [ 'foo<div id="div">[bar <span>baz</span>}</div>qoz',
+ 'foo<div id="div">{bar <span>baz</span>}</div>qoz' ] },
+ ]
+ }
+ ]
+}
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/unapply.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/unapply.py
new file mode 100644
index 000000000..adad65617
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/unapply.py
@@ -0,0 +1,462 @@
+
+UNAPPLY_TESTS = {
+ 'id': 'U',
+ 'caption': 'Unapply Existing Formatting Tests',
+ 'checkAttrs': True,
+ 'checkStyle': True,
+ 'styleWithCSS': False,
+ 'expected': 'foo[bar]baz',
+
+ 'RFC': [
+ { 'desc': '',
+ 'command': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': 'remove link',
+ 'command': 'unlink',
+ 'tests': [
+ { 'id': 'UNLINK_A-1_SO',
+ 'desc': 'unlink wrapped <a> element',
+ 'pad': 'foo[<a>bar</a>]baz' },
+
+ { 'id': 'UNLINK_A-1_SW',
+ 'desc': 'unlink <a> element where the selection wraps the full content',
+ 'pad': 'foo<a>[bar]</a>baz' },
+
+ { 'id': 'UNLINK_An:a.h:id-1_SO',
+ 'desc': 'unlink wrapped <a> element that has a name and href attribute',
+ 'pad': 'foo[<a name="A" href="#UNLINK:An:a.h:id-1_SO">bar</a>]baz' },
+
+ { 'id': 'UNLINK_A-2_SO',
+ 'desc': 'unlink contained <a> element',
+ 'pad': 'foo[b<a>a</a>r]baz' },
+
+ { 'id': 'UNLINK_A2-1_SO',
+ 'desc': 'unlink 2 contained <a> elements',
+ 'pad': 'foo[<a>b</a>a<a>r</a>]baz' }
+ ]
+ }
+ ],
+
+ 'Proposed': [
+ { 'desc': '',
+ 'command': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': 'remove bold',
+ 'command': 'bold',
+ 'tests': [
+ { 'id': 'B_B-1_SW',
+ 'rte1-id': 'u-bold-0',
+ 'desc': 'Selection within tags; remove <b> tags',
+ 'pad': 'foo<b>[bar]</b>baz' },
+
+ { 'id': 'B_B-1_SO',
+ 'desc': 'Selection outside of tags; remove <b> tags',
+ 'pad': 'foo[<b>bar</b>]baz' },
+
+ { 'id': 'B_B-1_SL',
+ 'desc': 'Selection oblique left; remove <b> tags',
+ 'pad': 'foo[<b>bar]</b>baz' },
+
+ { 'id': 'B_B-1_SR',
+ 'desc': 'Selection oblique right; remove <b> tags',
+ 'pad': 'foo<b>[bar</b>]baz' },
+
+ { 'id': 'B_STRONG-1_SW',
+ 'rte1-id': 'u-bold-1',
+ 'desc': 'Selection within tags; remove <strong> tags',
+ 'pad': 'foo<strong>[bar]</strong>baz' },
+
+ { 'id': 'B_STRONG-1_SO',
+ 'desc': 'Selection outside of tags; remove <strong> tags',
+ 'pad': 'foo[<strong>bar</strong>]baz' },
+
+ { 'id': 'B_STRONG-1_SL',
+ 'desc': 'Selection oblique left; remove <strong> tags',
+ 'pad': 'foo[<strong>bar]</strong>baz' },
+
+ { 'id': 'B_STRONG-1_SR',
+ 'desc': 'Selection oblique right; remove <strong> tags',
+ 'pad': 'foo<strong>[bar</strong>]baz' },
+
+ { 'id': 'B_SPANs:fw:b-1_SW',
+ 'rte1-id': 'u-bold-2',
+ 'desc': 'Selection within tags; remove "font-weight: bold"',
+ 'pad': 'foo<span style="font-weight: bold">[bar]</span>baz' },
+
+ { 'id': 'B_SPANs:fw:b-1_SO',
+ 'desc': 'Selection outside of tags; remove "font-weight: bold"',
+ 'pad': 'foo[<span style="font-weight: bold">bar</span>]baz' },
+
+ { 'id': 'B_SPANs:fw:b-1_SL',
+ 'desc': 'Selection oblique left; remove "font-weight: bold"',
+ 'pad': 'foo[<span style="font-weight: bold">bar]</span>baz' },
+
+ { 'id': 'B_SPANs:fw:b-1_SR',
+ 'desc': 'Selection oblique right; remove "font-weight: bold"',
+ 'pad': 'foo<span style="font-weight: bold">[bar</span>]baz' },
+
+ { 'id': 'B_B-P3-1_SO12',
+ 'desc': 'Unbolding multiple paragraphs in inside bolded content with content-model violation',
+ 'pad': '<b>{<p>foo</p><p>bar</p>}<p>baz</p></b>',
+ 'expected': [ '<p>[foo</p><p>bar]</p><p><b>baz</b></p>',
+ '<p>[foo</p><p>bar]</p><b><p>baz</p></b>' ] },
+
+ { 'id': 'B_B-P-I..P-1_SO-I',
+ 'desc': 'Unbolding italicized content inside bolded content with content-model violation',
+ 'pad': '<b><p>foo[<i>bar</i>]</p><p>baz</p></b>',
+ 'expected': [ '<p><b>foo</b><i>[bar]</i></p><p><b>baz</b></p>',
+ '<b><p>foo</p></b><p><i>[bar]</i></p><b><p>baz</p></b>' ] },
+
+ { 'id': 'B_B-2_SL',
+ 'desc': 'Remove partially covered bold, selection extends left',
+ 'pad': 'foo [bar <b>baz] qoz</b> quz sic',
+ 'expected': 'foo [bar baz]<b> qoz</b> quz sic' },
+
+ { 'id': 'B_B-2_SR',
+ 'desc': 'Remove partially covered bold, selection extends right',
+ 'pad': 'foo bar <b>baz [qoz</b> quz] sic',
+ 'expected': 'foo bar <b>baz </b>[qoz quz] sic' }
+ ]
+ },
+
+ { 'desc': 'remove italic',
+ 'command': 'italic',
+ 'tests': [
+ { 'id': 'I_I-1_SW',
+ 'rte1-id': 'u-italic-0',
+ 'desc': 'Selection within tags; remove <i> tags',
+ 'pad': 'foo<i>[bar]</i>baz' },
+
+ { 'id': 'I_I-1_SO',
+ 'desc': 'Selection outside of tags; remove <i> tags',
+ 'pad': 'foo[<i>bar</i>]baz' },
+
+ { 'id': 'I_I-1_SL',
+ 'desc': 'Selection oblique left; remove <i> tags',
+ 'pad': 'foo[<i>bar]</i>baz' },
+
+ { 'id': 'I_I-1_SR',
+ 'desc': 'Selection oblique right; remove <i> tags',
+ 'pad': 'foo<i>[bar</i>]baz' },
+
+ { 'id': 'I_EM-1_SW',
+ 'rte1-id': 'u-italic-1',
+ 'desc': 'Selection within tags; remove <em> tags',
+ 'pad': 'foo<em>[bar]</em>baz' },
+
+ { 'id': 'I_EM-1_SO',
+ 'desc': 'Selection outside of tags; remove <em> tags',
+ 'pad': 'foo[<em>bar</em>]baz' },
+
+ { 'id': 'I_EM-1_SL',
+ 'desc': 'Selection oblique left; remove <em> tags',
+ 'pad': 'foo[<em>bar]</em>baz' },
+
+ { 'id': 'I_EM-1_SR',
+ 'desc': 'Selection oblique right; remove <em> tags',
+ 'pad': 'foo<em>[bar</em>]baz' },
+
+ { 'id': 'I_SPANs:fs:i-1_SW',
+ 'rte1-id': 'u-italic-2',
+ 'desc': 'Selection within tags; remove "font-style: italic"',
+ 'pad': 'foo<span style="font-style: italic">[bar]</span>baz' },
+
+ { 'id': 'I_SPANs:fs:i-1_SO',
+ 'desc': 'Selection outside of tags; Italicize "font-style: italic"',
+ 'pad': 'foo[<span style="font-style: italic">bar</span>]baz' },
+
+ { 'id': 'I_SPANs:fs:i-1_SL',
+ 'desc': 'Selection oblique left; Italicize "font-style: italic"',
+ 'pad': 'foo[<span style="font-style: italic">bar]</span>baz' },
+
+ { 'id': 'I_SPANs:fs:i-1_SR',
+ 'desc': 'Selection oblique right; Italicize "font-style: italic"',
+ 'pad': 'foo<span style="font-style: italic">[bar</span>]baz' },
+
+ { 'id': 'I_I-P3-1_SO2',
+ 'desc': 'Unitalicize content with content-model violation',
+ 'pad': '<i><p>foo</p>{<p>bar</p>}<p>baz</p></i>',
+ 'expected': [ '<p><i>foo</i></p><p>[bar]</p><p><i>baz</i></p>',
+ '<i><p>foo</p></i><p>[bar]</p><i><p>baz</p></i>' ] }
+ ]
+ },
+
+ { 'desc': 'remove underline',
+ 'command': 'underline',
+ 'tests': [
+ { 'id': 'U_U-1_SW',
+ 'rte1-id': 'u-underline-0',
+ 'desc': 'Selection within tags; remove <u> tags',
+ 'pad': 'foo<u>[bar]</u>baz' },
+
+ { 'id': 'U_U-1_SO',
+ 'desc': 'Selection outside of tags; remove <u> tags',
+ 'pad': 'foo[<u>bar</u>]baz' },
+
+ { 'id': 'U_U-1_SL',
+ 'desc': 'Selection oblique left; remove <u> tags',
+ 'pad': 'foo[<u>bar]</u>baz' },
+
+ { 'id': 'U_U-1_SR',
+ 'desc': 'Selection oblique right; remove <u> tags',
+ 'pad': 'foo<u>[bar</u>]baz' },
+
+ { 'id': 'U_SPANs:td:u-1_SW',
+ 'rte1-id': 'u-underline-1',
+ 'desc': 'Selection within tags; remove "text-decoration: underline"',
+ 'pad': 'foo<span style="text-decoration: underline">[bar]</span>baz' },
+
+ { 'id': 'U_SPANs:td:u-1_SO',
+ 'desc': 'Selection outside of tags; remove "text-decoration: underline"',
+ 'pad': 'foo[<span style="text-decoration: underline">bar</span>]baz' },
+
+ { 'id': 'U_SPANs:td:u-1_SL',
+ 'desc': 'Selection oblique left; remove "text-decoration: underline"',
+ 'pad': 'foo[<span style="text-decoration: underline">bar]</span>baz' },
+
+ { 'id': 'U_SPANs:td:u-1_SR',
+ 'desc': 'Selection oblique right; remove "text-decoration: underline"',
+ 'pad': 'foo<span style="text-decoration: underline">[bar</span>]baz' },
+
+ { 'id': 'U_U-S-1_SO',
+ 'desc': 'Removing underline from underlined content with striked content',
+ 'pad': '<u>foo[bar<s>baz</s>quoz]</u>',
+ 'expected': '<u>foo</u>[bar<s>baz</s>quoz]' },
+
+ { 'id': 'U_U-S-2_SI',
+ 'desc': 'Removing underline from striked content inside underlined content',
+ 'pad': '<u><s>foo[bar]baz</s>quoz</u>',
+ 'expected': '<s><u>foo</u>[bar]<u>baz</u>quoz</s>' },
+
+ { 'id': 'U_U-P3-1_SO',
+ 'desc': 'Removing underline from underlined content with content-model violation',
+ 'pad': '<u><p>foo</p>{<p>bar</p>}<p>baz</p></u>',
+ 'expected': [ '<p><u>foo</u></p><p>[bar]</p><p><u>baz</u></p>',
+ '<u><p>foo</p></u><p>[bar]</p><u><p>baz</p></u>' ] }
+ ]
+ },
+
+ { 'desc': 'remove strike through',
+ 'command': 'strikethrough',
+ 'tests': [
+ { 'id': 'S_S-1_SW',
+ 'rte1-id': 'u-strikethrough-1',
+ 'desc': 'Selection within tags; remove <s> tags',
+ 'pad': 'foo<s>[bar]</s>baz' },
+
+ { 'id': 'S_S-1_SO',
+ 'desc': 'Selection outside of tags; remove <s> tags',
+ 'pad': 'foo[<s>bar</s>]baz' },
+
+ { 'id': 'S_S-1_SL',
+ 'desc': 'Selection oblique left; remove <s> tags',
+ 'pad': 'foo[<s>bar]</s>baz' },
+
+ { 'id': 'S_S-1_SR',
+ 'desc': 'Selection oblique right; remove <s> tags',
+ 'pad': 'foo<s>[bar</s>]baz' },
+
+ { 'id': 'S_STRIKE-1_SW',
+ 'rte1-id': 'u-strikethrough-0',
+ 'desc': 'Selection within tags; remove <strike> tags',
+ 'pad': 'foo<strike>[bar]</strike>baz' },
+
+ { 'id': 'S_STRIKE-1_SO',
+ 'desc': 'Selection outside of tags; remove <strike> tags',
+ 'pad': 'foo[<strike>bar</strike>]baz' },
+
+ { 'id': 'S_STRIKE-1_SL',
+ 'desc': 'Selection oblique left; remove <strike> tags',
+ 'pad': 'foo[<strike>bar]</strike>baz' },
+
+ { 'id': 'S_STRIKE-2_SR',
+ 'desc': 'Selection oblique right; remove <strike> tags',
+ 'pad': 'foo<strike>[bar</strike>]baz' },
+
+ { 'id': 'S_DEL-1_SW',
+ 'rte1-id': 'u-strikethrough-2',
+ 'desc': 'Selection within tags; remove <del> tags',
+ 'pad': 'foo<del>[bar]</del>baz' },
+
+ { 'id': 'S_SPANs:td:lt-1_SW',
+ 'rte1-id': 'u-strikethrough-3',
+ 'desc': 'Selection within tags; remove "text-decoration:line-through"',
+ 'pad': 'foo<span style="text-decoration:line-through">[bar]</span>baz' },
+
+ { 'id': 'S_SPANs:td:lt-1_SO',
+ 'desc': 'Selection outside of tags; Italicize "text-decoration:line-through"',
+ 'pad': 'foo[<span style="text-decoration:line-through">bar</span>]baz' },
+
+ { 'id': 'S_SPANs:td:lt-1_SL',
+ 'desc': 'Selection oblique left; Italicize "text-decoration:line-through"',
+ 'pad': 'foo[<span style="text-decoration:line-through">bar]</span>baz' },
+
+ { 'id': 'S_SPANs:td:lt-1_SR',
+ 'desc': 'Selection oblique right; Italicize "text-decoration:line-through"',
+ 'pad': 'foo<span style="text-decoration:line-through">[bar</span>]baz' },
+
+ { 'id': 'S_S-U-1_SI',
+ 'desc': 'Removing underline from underlined content inside striked content',
+ 'pad': '<s><u>foo[bar]baz</u>quoz</s>',
+ 'expected': '<s><u>foo</u></s><u>[bar]</u><s><u>baz</u>quoz</s>' },
+
+ { 'id': 'S_U-S-1_SI',
+ 'desc': 'Removing underline from striked content inside underlined content',
+ 'pad': '<u><s>foo[bar]baz</s>quoz</u>',
+ 'expected': '<u><s>foo</s>[bar]<s>baz</s>quoz</u>' }
+ ]
+ },
+
+ { 'desc': 'remove subscript',
+ 'command': 'subscript',
+ 'tests': [
+ { 'id': 'SUB_SUB-1_SW',
+ 'rte1-id': 'u-subscript-0',
+ 'desc': 'remove subscript',
+ 'pad': 'foo<sub>[bar]</sub>baz' },
+
+ { 'id': 'SUB_SPANs:va:sub-1_SW',
+ 'rte1-id': 'u-subscript-1',
+ 'desc': 'remove subscript',
+ 'pad': 'foo<span style="vertical-align: sub">[bar]</span>baz' }
+ ]
+ },
+
+ { 'desc': 'remove superscript',
+ 'command': 'superscript',
+ 'tests': [
+ { 'id': 'SUP_SUP-1_SW',
+ 'rte1-id': 'u-superscript-0',
+ 'desc': 'remove superscript',
+ 'pad': 'foo<sup>[bar]</sup>baz' },
+
+ { 'id': 'SUP_SPANs:va:super-1_SW',
+ 'rte1-id': 'u-superscript-1',
+ 'desc': 'remove superscript',
+ 'pad': 'foo<span style="vertical-align: super">[bar]</span>baz' }
+ ]
+ },
+
+ { 'desc': 'remove links',
+ 'command': 'unlink',
+ 'tests': [
+ { 'id': 'UNLINK_Ahref:url-1_SW',
+ 'rte1-id': 'u-unlink-0',
+ 'desc': 'unlink an <a> element with href attribute where all children are selected',
+ 'pad': 'foo<a href="http://www.goo.gl">[bar]</a>baz' },
+
+ { 'id': 'UNLINK_A-1_SC',
+ 'desc': 'unlink an <a> element that contains the collapsed selection',
+ 'pad': 'foo<a>ba^r</a>baz',
+ 'expected': 'fooba^rbaz' },
+
+ { 'id': 'UNLINK_A-1_SI',
+ 'desc': 'unlink an <a> element that contains the whole selection',
+ 'pad': 'foo<a>b[a]r</a>baz',
+ 'expected': 'foob[a]rbaz' },
+
+ { 'id': 'UNLINK_A-2_SL',
+ 'desc': 'unlink a partially contained <a> element',
+ 'pad': 'foo[ba<a>r]ba</a>z' },
+
+ { 'id': 'UNLINK_A-3_SR',
+ 'desc': 'unlink a partially contained <a> element',
+ 'pad': 'fo<a>o[ba</a>r]baz' },
+
+ { 'id': 'UNLINK_As:d:b.fw:b-1_SW',
+ 'desc': 'unlink, preserving styles',
+ 'pad': 'foo<a href="#" style="display: block; font-weight: bold">[bar]</a>baz',
+ 'expected': 'foo<span style="display: block; font-weight: bold">[bar]</span>baz' },
+
+ { 'id': 'UNLINK_A-IMG-1_SO',
+ 'desc': 'unlink a linked image at the start of the content',
+ 'pad': '{<a href="#"><img src="pic.jpg" align="right" height="140" width="200"></a>abc]',
+ 'expected': '{<img src="pic.jpg" align="right" height="140" width="200">abc]' }
+ ]
+ },
+
+ { 'desc': 'outdent',
+ 'command': 'outdent',
+ 'tests': [
+ { 'id': 'OUTDENT_BQ-1_SW',
+ 'rte1-id': 'u-outdent-0',
+ 'desc': 'outdent (remove) a <blockquote>',
+ 'pad': 'foo<blockquote>[bar]</blockquote>baz',
+ 'expected': [ 'foo<p>[bar]</p>baz',
+ 'foo<div>[bar]</div>baz' ],
+ 'accept': 'foo<br>[bar]<br>baz' },
+
+ { 'id': 'OUTDENT_BQ.wibq.s:m:00040.b:n.p:0-1_SW',
+ 'rte1-id': 'u-outdent-1',
+ 'desc': 'outdent (remove) a styled <blockquote>',
+ 'pad': 'foo<blockquote class="webkit-indent-blockquote" style="margin: 0 0 0 40px; border: none; padding: 0px">[bar]</blockquote>baz',
+ 'expected': [ 'foo<p>[bar]</p>baz',
+ 'foo<div>[bar]</div>baz' ],
+ 'accept': 'foo<br>[bar]<br>baz' },
+
+ { 'id': 'OUTDENT_OL-LI-1_SW',
+ 'rte1-id': 'u-outdent-3',
+ 'desc': 'outdent (remove) an ordered list',
+ 'pad': 'foo<ol><li>[bar]</li></ol>baz',
+ 'expected': [ 'foo<p>[bar]</p>baz',
+ 'foo<div>[bar]</div>baz' ],
+ 'accept': 'foo<br>[bar]<br>baz' },
+
+ { 'id': 'OUTDENT_UL-LI-1_SW',
+ 'rte1-id': 'u-outdent-2',
+ 'desc': 'outdent (remove) an unordered list',
+ 'pad': 'foo<ul><li>[bar]</li></ul>baz',
+ 'expected': [ 'foo<p>[bar]</p>baz',
+ 'foo<div>[bar]</div>baz' ],
+ 'accept': 'foo<br>[bar]<br>baz' },
+
+ { 'id': 'OUTDENT_DIV-1_SW',
+ 'rte1-id': 'u-outdent-4',
+ 'desc': 'outdent (remove) a styled <div> with margin',
+ 'pad': 'foo<div style="margin-left: 40px;">[bar]</div>baz',
+ 'expected': [ 'foo<p>[bar]</p>baz',
+ 'foo<div>[bar]</div>baz' ],
+ 'accept': 'foo<br>[bar]<br>baz' }
+ ]
+ },
+
+ { 'desc': 'remove all formatting',
+ 'command': 'removeformat',
+ 'tests': [
+ { 'id': 'REMOVEFORMAT_B-1_SW',
+ 'rte1-id': 'u-removeformat-0',
+ 'desc': 'remove a <b> tag using "removeformat"',
+ 'pad': 'foo<b>[bar]</b>baz' },
+
+ { 'id': 'REMOVEFORMAT_Ahref:url-1_SW',
+ 'rte1-id': 'u-removeformat-0',
+ 'desc': 'remove a link using "removeformat"',
+ 'pad': 'foo<a href="http://www.goo.gl">[bar]</a>baz' },
+
+ { 'id': 'REMOVEFORMAT_TABLE-TBODY-TR-TD-1_SW',
+ 'rte1-id': 'u-removeformat-2',
+ 'desc': 'remove a table using "removeformat"',
+ 'pad': 'foo<table><tbody><tr><td>[bar]</td></tr></tbody></table>baz',
+ 'expected': [ 'foo<p>[bar]</p>baz',
+ 'foo<div>[bar]</div>baz' ],
+ 'accept': 'foo<br>[bar]<br>baz' }
+ ]
+ },
+
+ { 'desc': 'remove bookmark',
+ 'command': 'unbookmark',
+ 'tests': [
+ { 'id': 'UNBOOKMARK_An:name-1_SW',
+ 'rte1-id': 'u-unbookmark-0',
+ 'desc': 'unlink a bookmark (a named <a> element) where all children are selected',
+ 'pad': 'foo<a name="bookmark">[bar]</a>baz' }
+ ]
+ }
+ ]
+}
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/unapplyCSS.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/unapplyCSS.py
new file mode 100644
index 000000000..6f934a0f0
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/unapplyCSS.py
@@ -0,0 +1,226 @@
+
+UNAPPLY_TESTS_CSS = {
+ 'id': 'UC',
+ 'caption': 'Unapply Existing Formatting Tests, using styleWithCSS',
+ 'checkAttrs': True,
+ 'checkStyle': True,
+ 'styleWithCSS': True,
+ 'expected': 'foo[bar]baz',
+
+ 'Proposed': [
+ { 'desc': '',
+ 'id': '',
+ 'command': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': 'remove bold',
+ 'command': 'bold',
+ 'tests': [
+ { 'id': 'B_B-1_SW',
+ 'desc': 'Selection within tags; remove <b> tags',
+ 'pad': 'foo<b>[bar]</b>baz' },
+
+ { 'id': 'B_B-1_SO',
+ 'desc': 'Selection outside of tags; remove <b> tags',
+ 'pad': 'foo[<b>bar</b>]baz' },
+
+ { 'id': 'B_B-1_SL',
+ 'desc': 'Selection oblique left; remove <b> tags',
+ 'pad': 'foo[<b>bar]</b>baz' },
+
+ { 'id': 'B_B-1_SR',
+ 'desc': 'Selection oblique right; remove <b> tags',
+ 'pad': 'foo<b>[bar</b>]baz' },
+
+ { 'id': 'B_STRONG-1_SW',
+ 'desc': 'Selection within tags; remove <strong> tags',
+ 'pad': 'foo<strong>[bar]</strong>baz' },
+
+ { 'id': 'B_STRONG-1_SO',
+ 'desc': 'Selection outside of tags; remove <strong> tags',
+ 'pad': 'foo[<strong>bar</strong>]baz' },
+
+ { 'id': 'B_STRONG-1_SL',
+ 'desc': 'Selection oblique left; remove <strong> tags',
+ 'pad': 'foo[<strong>bar]</strong>baz' },
+
+ { 'id': 'B_STRONG-1_SR',
+ 'desc': 'Selection oblique right; remove <strong> tags',
+ 'pad': 'foo<strong>[bar</strong>]baz' },
+
+ { 'id': 'B_SPANs:fw:b-1_SW',
+ 'desc': 'Selection within tags; remove "font-weight: bold"',
+ 'pad': 'foo<span style="font-weight: bold">[bar]</span>baz' },
+
+ { 'id': 'B_SPANs:fw:b-1_SO',
+ 'desc': 'Selection outside of tags; remove "font-weight: bold"',
+ 'pad': 'foo[<span style="font-weight: bold">bar</span>]baz' },
+
+ { 'id': 'B_SPANs:fw:b-1_SL',
+ 'desc': 'Selection oblique left; remove "font-weight: bold"',
+ 'pad': 'foo[<span style="font-weight: bold">bar]</span>baz' },
+
+ { 'id': 'B_SPANs:fw:b-1_SR',
+ 'desc': 'Selection oblique right; remove "font-weight: bold"',
+ 'pad': 'foo<span style="font-weight: bold">[bar</span>]baz' }
+ ]
+ },
+
+ { 'desc': 'remove italic',
+ 'command': 'italic',
+ 'tests': [
+ { 'id': 'I_I-1_SW',
+ 'desc': 'Selection within tags; remove <i> tags',
+ 'pad': 'foo<i>[bar]</i>baz' },
+
+ { 'id': 'I_I-1_SO',
+ 'desc': 'Selection outside of tags; remove <i> tags',
+ 'pad': 'foo[<i>bar</i>]baz' },
+
+ { 'id': 'I_I-1_SL',
+ 'desc': 'Selection oblique left; remove <i> tags',
+ 'pad': 'foo[<i>bar]</i>baz' },
+
+ { 'id': 'I_I-1_SR',
+ 'desc': 'Selection oblique right; remove <i> tags',
+ 'pad': 'foo<i>[bar</i>]baz' },
+
+ { 'id': 'I_EM-1_SW',
+ 'desc': 'Selection within tags; remove <em> tags',
+ 'pad': 'foo<em>[bar]</em>baz' },
+
+ { 'id': 'I_EM-1_SO',
+ 'desc': 'Selection outside of tags; remove <em> tags',
+ 'pad': 'foo[<em>bar</em>]baz' },
+
+ { 'id': 'I_EM-1_SL',
+ 'desc': 'Selection oblique left; remove <em> tags',
+ 'pad': 'foo[<em>bar]</em>baz' },
+
+ { 'id': 'I_EM-1_SR',
+ 'desc': 'Selection oblique right; remove <em> tags',
+ 'pad': 'foo<em>[bar</em>]baz' },
+
+ { 'id': 'I_SPANs:fs:i-1_SW',
+ 'desc': 'Selection within tags; remove "font-style: italic"',
+ 'pad': 'foo<span style="font-style: italic">[bar]</span>baz' },
+
+ { 'id': 'I_SPANs:fs:i-1_SO',
+ 'desc': 'Selection outside of tags; Italicize "font-style: italic"',
+ 'pad': 'foo[<span style="font-style: italic">bar</span>]baz' },
+
+ { 'id': 'I_SPANs:fs:i-1_SL',
+ 'desc': 'Selection oblique left; Italicize "font-style: italic"',
+ 'pad': 'foo[<span style="font-style: italic">bar]</span>baz' },
+
+ { 'id': 'I_SPANs:fs:i-1_SR',
+ 'desc': 'Selection oblique right; Italicize "font-style: italic"',
+ 'pad': 'foo<span style="font-style: italic">[bar</span>]baz' }
+ ]
+ },
+
+ { 'desc': 'remove underline',
+ 'command': 'underline',
+ 'tests': [
+ { 'id': 'U_U-1_SW',
+ 'desc': 'Selection within tags; remove <u> tags',
+ 'pad': 'foo<u>[bar]</u>baz' },
+
+ { 'id': 'U_U-1_SO',
+ 'desc': 'Selection outside of tags; remove <u> tags',
+ 'pad': 'foo[<u>bar</u>]baz' },
+
+ { 'id': 'U_U-1_SL',
+ 'desc': 'Selection oblique left; remove <u> tags',
+ 'pad': 'foo[<u>bar]</u>baz' },
+
+ { 'id': 'U_U-1_SR',
+ 'desc': 'Selection oblique right; remove <u> tags',
+ 'pad': 'foo<u>[bar</u>]baz' },
+
+ { 'id': 'U_SPANs:td:u-1_SW',
+ 'desc': 'Selection within tags; remove "text-decoration: underline"',
+ 'pad': 'foo<span style="text-decoration: underline">[bar]</span>baz' },
+
+ { 'id': 'U_SPANs:td:u-1_SO',
+ 'desc': 'Selection outside of tags; remove "text-decoration: underline"',
+ 'pad': 'foo[<span style="text-decoration: underline">bar</span>]baz' },
+
+ { 'id': 'U_SPANs:td:u-1_SL',
+ 'desc': 'Selection oblique left; remove "text-decoration: underline"',
+ 'pad': 'foo[<span style="text-decoration: underline">bar]</span>baz' },
+
+ { 'id': 'U_SPANs:td:u-1_SR',
+ 'desc': 'Selection oblique right; remove "text-decoration: underline"',
+ 'pad': 'foo<span style="text-decoration: underline">[bar</span>]baz' }
+ ]
+ },
+
+ { 'desc': 'remove strike-through',
+ 'command': 'strikethrough',
+ 'tests': [
+ { 'id': 'S_S-1_SW',
+ 'desc': 'Selection within tags; remove <s> tags',
+ 'pad': 'foo<s>[bar]</s>baz' },
+
+ { 'id': 'S_S-1_SO',
+ 'desc': 'Selection outside of tags; remove <s> tags',
+ 'pad': 'foo[<s>bar</s>]baz' },
+
+ { 'id': 'S_S-1_SL',
+ 'desc': 'Selection oblique left; remove <s> tags',
+ 'pad': 'foo[<s>bar]</s>baz' },
+
+ { 'id': 'S_S-1_SR',
+ 'desc': 'Selection oblique right; remove <s> tags',
+ 'pad': 'foo<s>[bar</s>]baz' },
+
+ { 'id': 'S_STRIKE-1_SW',
+ 'desc': 'Selection within tags; remove <strike> tags',
+ 'pad': 'foo<strike>[bar]</strike>baz' },
+
+ { 'id': 'S_STRIKE-1_SO',
+ 'desc': 'Selection outside of tags; remove <strike> tags',
+ 'pad': 'foo[<strike>bar</strike>]baz' },
+
+ { 'id': 'S_STRIKE-1_SL',
+ 'desc': 'Selection oblique left; remove <strike> tags',
+ 'pad': 'foo[<strike>bar]</strike>baz' },
+
+ { 'id': 'S_STRIKE-1_SR',
+ 'desc': 'Selection oblique right; remove <strike> tags',
+ 'pad': 'foo<strike>[bar</strike>]baz' },
+
+ { 'id': 'S_SPANs:td:lt-1_SW',
+ 'desc': 'Selection within tags; remove "text-decoration:line-through"',
+ 'pad': 'foo<span style="text-decoration:line-through">[bar]</span>baz' },
+
+ { 'id': 'S_SPANs:td:lt-1_SO',
+ 'desc': 'Selection outside of tags; Italicize "text-decoration:line-through"',
+ 'pad': 'foo[<span style="text-decoration:line-through">bar</span>]baz' },
+
+ { 'id': 'S_SPANs:td:lt-1_SL',
+ 'desc': 'Selection oblique left; Italicize "text-decoration:line-through"',
+ 'pad': 'foo[<span style="text-decoration:line-through">bar]</span>baz' },
+
+ { 'id': 'S_SPANs:td:lt-1_SR',
+ 'desc': 'Selection oblique right; Italicize "text-decoration:line-through"',
+ 'pad': 'foo<span style="text-decoration:line-through">[bar</span>]baz' },
+
+ { 'id': 'S_SPANc:s-1_SW',
+ 'desc': 'Unapply "strike-through" on interited CSS style',
+ 'checkClass': True,
+ 'pad': 'foo<span class="s">[bar]</span>baz' },
+
+ { 'id': 'S_SPANc:s-2_SI',
+ 'desc': 'Unapply "strike-through" on interited CSS style',
+ 'pad': '<span class="s">foo[bar]baz</span>',
+ 'checkClass': True,
+ 'expected': '<span class="s">foo</span>[bar]<span class="s">baz</span>' }
+ ]
+ }
+ ]
+}
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/unittestexample.html b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/unittestexample.html
new file mode 100644
index 000000000..4e27b0554
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/unittestexample.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+
+ <title>Rich Text 2 Unit Test Example</title>
+
+ <!-- utility scripts -->
+ <script type="text/javascript" src="static/js/variables.js"></script>
+ <script type="text/javascript" src="static/js/canonicalize.js"></script>
+ <script type="text/javascript" src="static/js/compare.js"></script>
+ <script type="text/javascript" src="static/js/pad.js"></script>
+ <script type="text/javascript" src="static/js/range.js"></script>
+ <script type="text/javascript" src="static/js/units.js"></script>
+ <script type="text/javascript" src="static/js/run.js"></script>
+ <!-- you do not need static/js/output.js -->
+
+ <!--
+ Tests - note that those have the extensions .py,
+ but can be used as JS files directly.
+ -->
+ <script type="text/javascript" src="tests/selection.py"></script>
+ <script type="text/javascript" src="tests/apply.py"></script>
+ <script type="text/javascript" src="tests/applyCSS.py"></script>
+ <script type="text/javascript" src="tests/change.py"></script>
+ <script type="text/javascript" src="tests/changeCSS.py"></script>
+ <script type="text/javascript" src="tests/unapply.py"></script>
+ <script type="text/javascript" src="tests/unapplyCSS.py"></script>
+ <script type="text/javascript" src="tests/delete.py"></script>
+ <script type="text/javascript" src="tests/forwarddelete.py"></script>
+ <script type="text/javascript" src="tests/insert.py"></script>
+ <script type="text/javascript" src="tests/querySupported.py"></script>
+ <script type="text/javascript" src="tests/queryEnabled.py"></script>
+ <script type="text/javascript" src="tests/queryIndeterm.py"></script>
+ <script type="text/javascript" src="tests/queryState.py"></script>
+ <script type="text/javascript" src="tests/queryValue.py"></script>
+
+ <!-- Do something -->
+ <script type="text/javascript">
+ function runTest() {
+ initVariables();
+ initEditorDocs();
+
+ runTestSuite(UNAPPLY_TESTS);
+
+ // Below alert is just a simple demonstration on how to access the test results.
+ // Note that we only ran UNAPPLY tests above, so we have only results from that test set.
+ //
+ // The 'results' structure is as follows:
+ //
+ // results structure containing all results
+ // [<suite ID>] structure containing the results for the given suite *)
+ // .count number of tests in the given suite
+ // .valscore sum of all test value results (HTML or query value)
+ // .selscore sum of all selection results (HTML tests only)
+ // [<class ID>] structure containing the results for the given class **)
+ // .count number of tests in the given suite
+ // .valscore sum of all test value results (HTML or query value)
+ // .selscore sum of all selection results (HTML tests only)
+ // [<test ID>] structure containing the reults for a given test ***)
+ // .valscore value score (0 or 1), minimum over all containers
+ // .selscore selection score (0 or 1), minimum over all containers (HTML tests only)
+ // .valresult worst test value result (integer, see variables.js)
+ // .selresult worst selection result (integer, see variables.js)
+ // [<cont. ID>] structure containing the results of the test for a given container ****)
+ // .valscore value score (0 or 1)
+ // .selscore selection score (0 or 1)
+ // .valresult value result (integer, see variables.js)
+ // .selresult selection result (integer, see variables.js)
+ // .output output string (mainly for use by the online version)
+ // .innerHTML inner HTML of the testing container (<div> or <body>) after the test
+ // .outerHTML outer HTML of the testing container (<div> or <body>) after the test
+ // .bodyInnerHTML inner HTML of the <body> after the test
+ // .bodyOuterHTML outer HTML of the <body> after the test
+ //
+ // *) <suite ID>: a 1-3 character ID, e.g. UNAPPLY_TESTS.id, or 'U' (both referring the same suite)
+ // **) <class ID>: one of 'Proposed', 'RFC' or 'Finalized'
+ // ***) <test ID>: the ID of the test, without the leading 'RTE2-<suite ID>_' part
+ // ****) <container ID>: one of 'div' (test within a <div contenteditable="true">)
+ // 'dM' (test with designMode = 'on')
+ // 'body' (test within a <body contenteditable="true">)
+
+ alert("Result of 'Apply' tests:\nOut of " +
+ results[UNAPPLY_TESTS.id].count + " tests\n" +
+ results[UNAPPLY_TESTS.id].valscore + " had correct HTML, and\n" +
+ results[UNAPPLY_TESTS.id].selscore + " had a correct result selection\n(in all testing containers)." +
+ "\n\n" +
+ "Test RTE2-U_B_B-1_SW results with a contenteditable <body>:\n" +
+ results['U']['Proposed']['B_B-1_SW']['body'].valscore + " points for the value result, and\n" +
+ results['U']['Proposed']['B_B-1_SW']['body'].selscore + " points for the selection" +
+ ""
+ );
+ }
+ </script>
+</head>
+
+<body onload="runTest()">
+ <iframe name="iframe-dM" id="iframe-dM" src="static/editable-dM.html"></iframe>
+ <iframe name="iframe-body" id="iframe-body" src="static/editable-body.html"></iframe>
+ <iframe name="iframe-div" id="iframe-div" src="static/editable-div.html"></iframe>
+</body>
+</html>
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/update_from_upstream b/editor/libeditor/tests/browserscope/lib/richtext2/update_from_upstream
new file mode 100644
index 000000000..baeb76745
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/update_from_upstream
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+set -x
+
+if test -d richtext2; then
+ rm -drf richtext2;
+fi
+
+svn checkout http://browserscope.googlecode.com/svn/trunk/categories/richtext2 richtext2 | tail -1 | sed 's/[^0-9]//g' > current_revision
+
+find richtext2 -type d -name .svn -exec rm -drf \{\} \; 2> /dev/null
+
+# Remove test_set.py and other similarly named files because they confuse our mochitest runner
+find richtext2 =type f -name test_\* -exec rm -rf \{\} \; 2> /dev/null
+
+hg add current_revision richtext2
+
+hg stat .
+
diff --git a/editor/libeditor/tests/browserscope/mochitest.ini b/editor/libeditor/tests/browserscope/mochitest.ini
new file mode 100644
index 000000000..e6e2db413
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/mochitest.ini
@@ -0,0 +1,59 @@
+[default]
+support-files =
+ lib/richtext2/current_revision
+ lib/richtext2/richtext2/common.py
+ lib/richtext2/richtext2/unittestexample.html
+ lib/richtext2/richtext2/static/editable-dM.html
+ lib/richtext2/richtext2/static/editable.css
+ lib/richtext2/richtext2/static/editable-body.html
+ lib/richtext2/richtext2/static/editable-div.html
+ lib/richtext2/richtext2/static/js/variables.js
+ lib/richtext2/richtext2/static/js/range-bootstrap.js
+ lib/richtext2/richtext2/static/js/range.js
+ lib/richtext2/richtext2/static/js/output.js
+ lib/richtext2/richtext2/static/js/compare.js
+ lib/richtext2/richtext2/static/js/canonicalize.js
+ lib/richtext2/richtext2/static/js/pad.js
+ lib/richtext2/richtext2/static/js/run.js
+ lib/richtext2/richtext2/static/js/units.js
+ lib/richtext2/richtext2/static/common.css
+ lib/richtext2/richtext2/__init__.py
+ lib/richtext2/richtext2/handlers.py
+ lib/richtext2/richtext2/templates/output.html
+ lib/richtext2/richtext2/templates/richtext2.html
+ lib/richtext2/richtext2/tests/forwarddelete.py
+ lib/richtext2/richtext2/tests/selection.py
+ lib/richtext2/richtext2/tests/queryIndeterm.py
+ lib/richtext2/richtext2/tests/unapplyCSS.py
+ lib/richtext2/richtext2/tests/apply.py
+ lib/richtext2/richtext2/tests/unapply.py
+ lib/richtext2/richtext2/tests/change.py
+ lib/richtext2/richtext2/tests/queryState.py
+ lib/richtext2/richtext2/tests/queryValue.py
+ lib/richtext2/richtext2/tests/__init__.py
+ lib/richtext2/richtext2/tests/insert.py
+ lib/richtext2/richtext2/tests/queryEnabled.py
+ lib/richtext2/richtext2/tests/applyCSS.py
+ lib/richtext2/richtext2/tests/changeCSS.py
+ lib/richtext2/richtext2/tests/delete.py
+ lib/richtext2/richtext2/tests/querySupported.py
+ lib/richtext2/README
+ lib/richtext2/update_from_upstream
+ lib/richtext2/LICENSE
+ lib/richtext2/README.Mozilla
+ lib/richtext2/currentStatus.js
+ lib/richtext/current_revision
+ lib/richtext/README
+ lib/richtext/update_from_upstream
+ lib/richtext/LICENSE
+ lib/richtext/README.Mozilla
+ lib/richtext/richtext/editable.html
+ lib/richtext/richtext/richtext.html
+ lib/richtext/richtext/js/range.js
+ lib/richtext/currentStatus.js
+
+[test_richtext2.html]
+subsuite = clipboard
+skip-if = os == 'android' && debug # Bug 1202045
+[test_richtext.html]
+
diff --git a/editor/libeditor/tests/browserscope/test_richtext.html b/editor/libeditor/tests/browserscope/test_richtext.html
new file mode 100644
index 000000000..45f8bef38
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/test_richtext.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+BrowserScope richtext category tests
+-->
+<head>
+ <title>BrowserScope Richtext Tests</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="lib/richtext/currentStatus.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=550569">Mozilla Bug 550569</a>
+<p id="display"></p>
+<div id="content">
+ <iframe src="lib/richtext/richtext/richtext.html"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+// Running all of the tests can take a long time, try to account for it
+SimpleTest.requestLongerTimeout(5);
+
+function sendScore(results, continueParams) {
+ ok(results.length > 1, "At least one test should have been run");
+ for (var i = 1; i < results.length; ++i) {
+ var result = results[i];
+ [type, command, param, success] = result.split(/[\-=]/);
+ var comp = is;
+ if (isKnownFailure(type, command, param)) {
+ comp = todo_is;
+ }
+ comp(success, "1", "Browserscope richtext category=" + type +
+ " test=" + command +
+ " param=" + param);
+ }
+}
+
+document.getElementsByTagName("iframe")[0].addEventListener("load", function() {
+ SimpleTest.finish();
+}, false);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/browserscope/test_richtext2.html b/editor/libeditor/tests/browserscope/test_richtext2.html
new file mode 100644
index 000000000..c0ce07a8f
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/test_richtext2.html
@@ -0,0 +1,233 @@
+<!DOCTYPE html>
+<html lang="en">
+<!--
+BrowserScope richtext2 category tests
+
+This test is originally based on the unit test example available as part of the
+RichText2 suite:
+http://code.google.com/p/browserscope/source/browse/trunk/categories/richtext2/unittestexample.html
+-->
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+
+ <title>BrowserScope Richtext2 Tests</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+
+ <!-- utility scripts -->
+ <script type="text/javascript" src="lib/richtext2/richtext2/static/js/variables.js"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/static/js/canonicalize.js"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/static/js/compare.js"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/static/js/pad.js"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/static/js/range.js"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/static/js/units.js"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/static/js/run.js"></script>
+ <!-- you do not need static/js/output.js -->
+
+ <!--
+ Tests - note that those have the extensions .py,
+ but can be used as JS files directly.
+ -->
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/selection.py"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/apply.py"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/applyCSS.py"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/change.py"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/changeCSS.py"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/unapply.py"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/unapplyCSS.py"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/delete.py"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/forwarddelete.py"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/insert.py"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/querySupported.py"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/queryEnabled.py"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/queryIndeterm.py"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/queryState.py"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/queryValue.py"></script>
+
+ <script type="text/javascript" src="lib/richtext2/currentStatus.js"></script>
+
+ <!-- Do something -->
+ <script type="text/javascript">
+ // Set this constant to true in order to get the current status of the test suite.
+ // This is useful for updating the currentStatus.js file when an editor bug is fixed.
+ const UPDATE_TEST_RESULTS = false;
+
+ // some tests (at least RTE2-QE_PASTE_TEXT-1) require clipboard data
+ function startTest() {
+ SimpleTest.waitForClipboard("foo",
+ function() {
+ SpecialPowers.clipboardCopyString("foo");
+ },
+ runTest,
+ function() {
+ ok(false, "Failed to copy a string to the clipboard");
+ SimpleTest.finish();
+ }
+ );
+ }
+
+ function runTest() {
+ initVariables();
+ initEditorDocs();
+
+ const tests = [
+ SELECTION_TESTS,
+ APPLY_TESTS,
+ APPLY_TESTS_CSS,
+ CHANGE_TESTS,
+ CHANGE_TESTS_CSS,
+ UNAPPLY_TESTS,
+ UNAPPLY_TESTS_CSS,
+ DELETE_TESTS,
+ FORWARDDELETE_TESTS,
+ INSERT_TESTS,
+ QUERYSUPPORTED_TESTS,
+ QUERYENABLED_TESTS,
+ QUERYINDETERM_TESTS,
+ QUERYSTATE_TESTS,
+ QUERYVALUE_TESTS,
+ ];
+
+ for (var i = 0; i < tests.length; ++i) {
+ runTestSuite(tests[i]);
+ }
+
+ // Below alert is just a simple demonstration on how to access the test results.
+ // Note that we only ran UNAPPLY tests above, so we have only results from that test set.
+ //
+ // The 'results' structure is as follows:
+ //
+ // results structure containing all results
+ // [<suite ID>] structure containing the results for the given suite *)
+ // .count number of tests in the given suite
+ // .valscore sum of all test value results (HTML or query value)
+ // .selscore sum of all selection results (HTML tests only)
+ // [<class ID>] structure containing the results for the given class **)
+ // .count number of tests in the given suite
+ // .valscore sum of all test value results (HTML or query value)
+ // .selscore sum of all selection results (HTML tests only)
+ // [<test ID>] structure containing the reults for a given test ***)
+ // .valscore value score (0 or 1), minimum over all containers
+ // .selscore selection score (0 or 1), minimum over all containers (HTML tests only)
+ // .valresult worst test value result (integer, see variables.js)
+ // .selresult worst selection result (integer, see variables.js)
+ // [<cont. ID>] structure containing the results of the test for a given container ****)
+ // .valscore value score (0 or 1)
+ // .selscore selection score (0 or 1)
+ // .valresult value result (integer, see variables.js)
+ // .selresult selection result (integer, see variables.js)
+ // .output output string (mainly for use by the online version)
+ // .innerHTML inner HTML of the testing container (<div> or <body>) after the test
+ // .outerHTML outer HTML of the testing container (<div> or <body>) after the test
+ // .bodyInnerHTML inner HTML of the <body> after the test
+ // .bodyOuterHTML outer HTML of the <body> after the test
+ //
+ // *) <suite ID>: a 1-3 character ID, e.g. UNAPPLY_TESTS.id, or 'U' (both referring the same suite)
+ // **) <class ID>: one of 'Proposed', 'RFC' or 'Finalized'
+ // ***) <test ID>: the ID of the test, without the leading 'RTE2-<suite ID>_' part
+ // ****) <container ID>: one of 'div' (test within a <div contenteditable="true">)
+ // 'dM' (test with designMode = 'on')
+ // 'body' (test within a <body contenteditable="true">)
+
+ if (UPDATE_TEST_RESULTS) {
+ var newKnownFailures = {value: {}, select: {}};
+ for (var i = 0; i < tests.length; ++i) {
+ var category = tests[i];
+ for (var group in results[category.id]) {
+ switch (group) {
+ // Skip the known properties
+ case "count":
+ case "valscore":
+ case "selscore":
+ case "time":
+ break;
+ default:
+ for (var test_id in results[category.id][group]) {
+ switch (test_id) {
+ // Skip the known properties
+ case "count":
+ case "valscore":
+ case "selscore":
+ break;
+ default:
+ for (var structure in results[category.id][group][test_id]) {
+ switch (structure) {
+ // Only look at each test structure
+ case "dM":
+ case "body":
+ case "div":
+ if (!results[category.id][group][test_id][structure].valscore) {
+ newKnownFailures.value[category.id + "-" + group + "-" + test_id + "-" + structure] = true;
+ }
+ if (!results[category.id][group][test_id][structure].selscore) {
+ newKnownFailures.select[category.id + "-" + group + "-" + test_id + "-" + structure] = true;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ var resultContainer = document.getElementById("results");
+ resultContainer.style.display = "";
+ resultContainer.textContent = JSON.stringify(newKnownFailures);
+ } else {
+ for (var i = 0; i < tests.length; ++i) {
+ var category = tests[i];
+ for (var group in results[category.id]) {
+ switch (group) {
+ // Skip the known properties
+ case "count":
+ case "valscore":
+ case "selscore":
+ case "time":
+ break;
+ default:
+ for (var test_id in results[category.id][group]) {
+ switch (test_id) {
+ // Skip the known properties
+ case "count":
+ case "valscore":
+ case "selscore":
+ break;
+ default:
+ for (var structure in results[category.id][group][test_id]) {
+ switch (structure) {
+ // Only look at each test structure
+ case "dM":
+ case "body":
+ case "div":
+ var row = results[category.id][group][test_id][structure];
+ var testName = [category.id, group, test_id, structure].join("-");
+ (testName in knownFailures.value ? todo_is : is)(
+ row.valscore, 1, "Browserscope richtext2 value: " + testName);
+ (testName in knownFailures.select ? todo_is : is)(
+ row.selscore, 1, "Browserscope richtext2 selection: " + testName);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ // Running all of the tests can take a long time, try to account for it
+ SimpleTest.requestLongerTimeout(5);
+ </script>
+</head>
+
+<body onload="startTest()">
+ <iframe name="iframe-dM" id="iframe-dM" src="lib/richtext2/richtext2/static/editable-dM.html"></iframe>
+ <iframe name="iframe-body" id="iframe-body" src="lib/richtext2/richtext2/static/editable-body.html"></iframe>
+ <iframe name="iframe-div" id="iframe-div" src="lib/richtext2/richtext2/static/editable-div.html"></iframe>
+ <pre id="results" style="display: none"></pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/bug527935.html b/editor/libeditor/tests/bug527935.html
new file mode 100644
index 000000000..4bfa1bac2
--- /dev/null
+++ b/editor/libeditor/tests/bug527935.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+<div id="content">
+ <iframe id="formTarget" name="formTarget"></iframe>
+ <form action="data:text/html," target="formTarget">
+ <input name="test" id="initValue"><input type="submit">
+ </form>
+</div>
+</body>
+</html
diff --git a/editor/libeditor/tests/bug629172.html b/editor/libeditor/tests/bug629172.html
new file mode 100644
index 000000000..e583b2d44
--- /dev/null
+++ b/editor/libeditor/tests/bug629172.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<script type="text/javascript" src="/tests/SimpleTest/WindowSnapshot.js"></script>
+<style>
+textarea { resize: none }
+</style>
+</head>
+<body>
+<div id="content">
+<textarea id="ltr-ref" style="display: none">test.</textarea>
+<textarea id="rtl-ref" style="display: none; direction: rtl">test.</textarea>
+</div
+</body>
+</html>
diff --git a/editor/libeditor/tests/chrome.ini b/editor/libeditor/tests/chrome.ini
new file mode 100644
index 000000000..98db30001
--- /dev/null
+++ b/editor/libeditor/tests/chrome.ini
@@ -0,0 +1,14 @@
+[DEFAULT]
+skip-if = os == 'android'
+support-files = green.png
+
+[test_bug489202.xul]
+[test_bug599983.xul]
+[test_bug607584.xul]
+[test_bug616590.xul]
+[test_bug780908.xul]
+[test_contenteditable_text_input_handling.html]
+[test_htmleditor_keyevent_handling.html]
+[test_set_document_title_transaction.html]
+[test_texteditor_keyevent_handling.html]
+skip-if = (debug && os=='win') || (os == 'linux') # Bug 1116205, leaks on windows debug, fails delete key on linux
diff --git a/editor/libeditor/tests/data/cfhtml-chromium.txt b/editor/libeditor/tests/data/cfhtml-chromium.txt
new file mode 100644
index 000000000..7e0253715
--- /dev/null
+++ b/editor/libeditor/tests/data/cfhtml-chromium.txt
Binary files differ
diff --git a/editor/libeditor/tests/data/cfhtml-firefox.txt b/editor/libeditor/tests/data/cfhtml-firefox.txt
new file mode 100644
index 000000000..cc686d856
--- /dev/null
+++ b/editor/libeditor/tests/data/cfhtml-firefox.txt
Binary files differ
diff --git a/editor/libeditor/tests/data/cfhtml-ie.txt b/editor/libeditor/tests/data/cfhtml-ie.txt
new file mode 100644
index 000000000..a30bc5295
--- /dev/null
+++ b/editor/libeditor/tests/data/cfhtml-ie.txt
Binary files differ
diff --git a/editor/libeditor/tests/data/cfhtml-nocontext.txt b/editor/libeditor/tests/data/cfhtml-nocontext.txt
new file mode 100644
index 000000000..aa4882227
--- /dev/null
+++ b/editor/libeditor/tests/data/cfhtml-nocontext.txt
@@ -0,0 +1,18 @@
+Version:0.9
+StartHTML:-1
+EndHTML:-1
+StartFragment:0000000111
+EndFragment:0000000246
+<!--StartFragment-->
+<html>
+ <head>
+ <title>Test</title>
+
+ </head>
+ <body>
+ <p>
+ 3.<b>1415926535897932</b>
+ </p>
+ </body>
+</html>
+<!--EndFragment-->
diff --git a/editor/libeditor/tests/data/cfhtml-ooo.txt b/editor/libeditor/tests/data/cfhtml-ooo.txt
new file mode 100644
index 000000000..0bcf7616e
--- /dev/null
+++ b/editor/libeditor/tests/data/cfhtml-ooo.txt
Binary files differ
diff --git a/editor/libeditor/tests/file_bug549262.html b/editor/libeditor/tests/file_bug549262.html
new file mode 100644
index 000000000..92a0c76f3
--- /dev/null
+++ b/editor/libeditor/tests/file_bug549262.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <a href="">test</a>
+ <div id="editor" contenteditable="true">abc</div>
+ <div style="height: 20000px;"></div>
+ </body>
+</html>
diff --git a/editor/libeditor/tests/file_bug586662.html b/editor/libeditor/tests/file_bug586662.html
new file mode 100644
index 000000000..298953197
--- /dev/null
+++ b/editor/libeditor/tests/file_bug586662.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <div style="height: 20000px;"></div>
+ <textarea id="editor"></textarea>
+ </body>
+</html>
diff --git a/editor/libeditor/tests/file_bug674770-1.html b/editor/libeditor/tests/file_bug674770-1.html
new file mode 100644
index 000000000..6750bb878
--- /dev/null
+++ b/editor/libeditor/tests/file_bug674770-1.html
@@ -0,0 +1,5 @@
+<!DOCTYPE>
+<script>
+ localStorage["clicked"] = "true";
+ close();
+</script>
diff --git a/editor/libeditor/tests/file_bug915962.html b/editor/libeditor/tests/file_bug915962.html
new file mode 100644
index 000000000..85c5139d3
--- /dev/null
+++ b/editor/libeditor/tests/file_bug915962.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <button>Button</button>
+ <img src="green.png" usemap="#map">
+ <map name="map">
+ <!-- This URL ensures that the link doesn't get clicked, since
+ mochitests cannot access the outside network. -->
+ <area shape="rect" coords="0,0,10,10" href="https://youtube.com/">
+ </map>
+ <div style="height: 20000px;" tabindex="-1"><hr></div>
+ </body>
+</html>
diff --git a/editor/libeditor/tests/file_select_all_without_body.html b/editor/libeditor/tests/file_select_all_without_body.html
new file mode 100644
index 000000000..70050a847
--- /dev/null
+++ b/editor/libeditor/tests/file_select_all_without_body.html
@@ -0,0 +1,41 @@
+<html>
+<head>
+<script type="text/javascript">
+
+function is(aLeft, aRight, aMessage)
+{
+ window.opener.SimpleTest.is(aLeft, aRight, aMessage);
+}
+
+function unload()
+{
+ window.opener.SimpleTest.finish();
+}
+
+function boom()
+{
+ var root = document.documentElement;
+ while(root.firstChild) {
+ root.removeChild(root.firstChild);
+ }
+ root.appendChild(document.createTextNode("Mozilla"));
+ root.focus();
+ cespan = document.createElementNS("http://www.w3.org/1999/xhtml", "span");
+ cespan.setAttributeNS(null, "contenteditable", "true");
+ root.appendChild(cespan);
+ try {
+ document.execCommand("selectAll", false, null);
+ } catch(e) { }
+
+ is(window.getSelection().toString(), "Mozilla",
+ "The nodes are not selected");
+
+ window.close();
+}
+
+window.opener.SimpleTest.waitForFocus(boom, window);
+
+</script></head>
+
+<body onunload="unload();"></body>
+</html>
diff --git a/editor/libeditor/tests/green.png b/editor/libeditor/tests/green.png
new file mode 100644
index 000000000..0aaec2093
--- /dev/null
+++ b/editor/libeditor/tests/green.png
Binary files differ
diff --git a/editor/libeditor/tests/mochitest.ini b/editor/libeditor/tests/mochitest.ini
new file mode 100644
index 000000000..447fb8b65
--- /dev/null
+++ b/editor/libeditor/tests/mochitest.ini
@@ -0,0 +1,245 @@
+[DEFAULT]
+support-files =
+ data/cfhtml-chromium.txt
+ data/cfhtml-firefox.txt
+ data/cfhtml-ie.txt
+ data/cfhtml-ooo.txt
+ data/cfhtml-nocontext.txt
+ file_bug549262.html
+ file_bug586662.html
+ file_bug674770-1.html
+ file_bug915962.html
+ file_select_all_without_body.html
+ green.png
+ spellcheck.js
+
+[test_bug46555.html]
+[test_bug200416.html]
+[test_bug289384.html]
+skip-if = os != "mac"
+[test_bug290026.html]
+[test_bug291780.html]
+[test_bug309731.html]
+[test_bug316447.html]
+[test_bug318065.html]
+[test_bug332636.html]
+support-files = test_bug332636.html^headers^
+[test_bug366682.html]
+skip-if = os == 'android'
+[test_bug372345.html]
+skip-if = toolkit == 'android'
+[test_bug404320.html]
+[test_bug408231.html]
+skip-if = toolkit == 'android'
+[test_bug410986.html]
+subsuite = clipboard
+skip-if = toolkit == 'android'
+[test_bug414526.html]
+[test_bug417418.html]
+skip-if = android_version == '18' # bug 1147989
+[test_bug432225.html]
+skip-if = toolkit == 'android'
+[test_bug439808.html]
+[test_bug442186.html]
+[test_bug449243.html]
+[test_bug455992.html]
+[test_bug456244.html]
+[test_bug460740.html]
+[test_bug468353.html]
+[test_bug471319.html]
+[test_bug471722.html]
+[test_bug478725.html]
+subsuite = clipboard
+skip-if = toolkit == 'android'
+[test_bug480647.html]
+[test_bug480972.html]
+subsuite = clipboard
+skip-if = toolkit == 'android'
+[test_bug483651.html]
+[test_bug484181.html]
+skip-if = toolkit == 'android'
+[test_bug487524.html]
+[test_bug490879.html]
+subsuite = clipboard
+skip-if = toolkit == 'android' # bug 1299578
+[test_bug502673.html]
+[test_bug514156.html]
+[test_bug520189.html]
+subsuite = clipboard
+skip-if = toolkit == 'android'
+[test_bug525389.html]
+subsuite = clipboard
+skip-if = toolkit == 'android'
+[test_bug537046.html]
+[test_bug549262.html]
+skip-if = toolkit == 'android'
+[test_bug550434.html]
+skip-if = android_version == '18' # bug 1147989
+[test_bug551704.html]
+subsuite = clipboard
+[test_bug552782.html]
+[test_bug567213.html]
+[test_bug569988.html]
+skip-if = os == 'android'
+[test_bug570144.html]
+[test_bug578771.html]
+skip-if = android_version == '18' # bug 1147989
+[test_bug586662.html]
+skip-if = toolkit == 'android'
+[test_bug587461.html]
+[test_bug590554.html]
+[test_bug592592.html]
+[test_bug596001.html]
+subsuite = clipboard
+[test_bug596333.html]
+skip-if = toolkit == 'android'
+[test_bug596506.html]
+[test_bug597331.html]
+skip-if = toolkit == 'android' || asan || (os == "win" && os_version != "5.1") # Bug 718316, Bug 1211213
+[test_bug597784.html]
+[test_bug599322.html]
+subsuite = clipboard
+skip-if = toolkit == 'android'
+[test_bug599983.html]
+[test_bug600570.html]
+subsuite = clipboard
+skip-if = toolkit == 'android' || (os == "win" && os_version != "5.1") # Bug 718316
+[test_bug602130.html]
+[test_bug603556.html]
+subsuite = clipboard
+[test_bug604532.html]
+skip-if = toolkit == 'android'
+[test_bug607584.html]
+[test_bug611182.html]
+skip-if = toolkit == 'android'
+[test_bug612128.html]
+[test_bug612447.html]
+[test_bug620906.html]
+skip-if = toolkit == 'android' #TIMED_OUT
+[test_bug622371.html]
+skip-if = toolkit == 'android' #bug 957797
+[test_bug625452.html]
+[test_bug629845.html]
+[test_bug635636.html]
+skip-if = e10s || os == 'android'
+[test_bug636465.html]
+skip-if = os == 'android'
+[test_bug638596.html]
+[test_bug640321.html]
+skip-if = android_version == '18' # bug 1147989
+[test_bug641466.html]
+[test_bug645914.html]
+[test_bug646194.html]
+[test_bug668599.html]
+[test_bug674770-1.html]
+subsuite = clipboard
+skip-if = toolkit == 'android'
+[test_bug674770-2.html]
+subsuite = clipboard
+skip-if = toolkit == 'android'
+[test_bug674861.html]
+[test_bug676401.html]
+[test_bug677752.html]
+[test_bug681229.html]
+subsuite = clipboard
+[test_bug686203.html]
+[test_bug692520.html]
+[test_bug697842.html]
+[test_bug725069.html]
+[test_bug735059.html]
+[test_bug738366.html]
+[test_bug740784.html]
+[test_bug742261.html]
+[test_bug757371.html]
+[test_bug757771.html]
+[test_bug767684.html]
+[test_bug772796.html]
+skip-if = toolkit == 'android' # bug 1309431
+[test_bug773262.html]
+[test_bug780035.html]
+[test_bug787432.html]
+[test_bug790475.html]
+[test_bug795418.html]
+[test_bug795418-2.html]
+[test_bug795418-3.html]
+[test_bug795418-4.html]
+[test_bug795418-5.html]
+[test_bug795418-6.html]
+[test_bug795785.html]
+[test_bug796839.html]
+[test_bug830600.html]
+subsuite = clipboard
+skip-if = e10s
+[test_bug832025.html]
+[test_bug850043.html]
+[test_bug857487.html]
+[test_bug858918.html]
+[test_bug915962.html]
+[test_bug974309.html]
+skip-if = toolkit == 'android'
+[test_bug966155.html]
+skip-if = os != "win"
+[test_bug966552.html]
+skip-if = os != "win"
+[test_bug998188.html]
+[test_bug1026397.html]
+[test_bug1053048.html]
+[test_bug1067255.html]
+[test_bug1068979.html]
+subsuite = clipboard
+[test_bug1094000.html]
+[test_bug1100966.html]
+skip-if = os == 'android'
+[test_bug1102906.html]
+skip-if = os == 'android'
+[test_bug1101392.html]
+subsuite = clipboard
+[test_bug1109465.html]
+[test_bug1140105.html]
+[test_bug1140617.html]
+subsuite = clipboard
+skip-if = toolkit == 'android' # bug 1299578
+[test_bug1153237.html]
+[test_bug1154791.html]
+skip-if = os == 'android'
+[test_bug1162952.html]
+[test_bug1181130-1.html]
+[test_bug1181130-2.html]
+[test_bug1186799.html]
+[test_bug1230473.html]
+[test_bug1247483.html]
+subsuite = clipboard
+skip-if = toolkit == 'android'
+[test_bug1248128.html]
+[test_bug1250010.html]
+[test_bug1257363.html]
+[test_bug1248185.html]
+[test_bug1258085.html]
+[test_bug1268736.html]
+[test_bug1270235.html]
+[test_bug1310912.html]
+skip-if = toolkit == 'android' # bug 1315898
+[test_bug1314790.html]
+[test_bug1315065.html]
+[test_bug1328023.html]
+[test_bug1330796.html]
+[test_bug1332876.html]
+
+[test_CF_HTML_clipboard.html]
+subsuite = clipboard
+[test_composition_event_created_in_chrome.html]
+[test_contenteditable_focus.html]
+[test_dom_input_event_on_htmleditor.html]
+skip-if = toolkit == 'android' # bug 1054087
+[test_dom_input_event_on_texteditor.html]
+[test_dragdrop.html]
+skip-if = os == 'android'
+[test_keypress_untrusted_event.html]
+[test_root_element_replacement.html]
+[test_select_all_without_body.html]
+[test_spellcheck_pref.html]
+skip-if = toolkit == 'android'
+[test_backspace_vs.html]
+[test_css_chrome_load_access.html]
+skip-if = toolkit == 'android' # chrome urls not available due to packaging
+[test_selection_move_commands.html]
diff --git a/editor/libeditor/tests/spellcheck.js b/editor/libeditor/tests/spellcheck.js
new file mode 100644
index 000000000..9d36c3254
--- /dev/null
+++ b/editor/libeditor/tests/spellcheck.js
@@ -0,0 +1,20 @@
+function isSpellingCheckOk(aEditor, aMisspelledWords) {
+ var selcon = aEditor.selectionController;
+ var sel = selcon.getSelection(selcon.SELECTION_SPELLCHECK);
+ var numWords = sel.rangeCount;
+
+ is(numWords, aMisspelledWords.length, "Correct number of misspellings and words.");
+
+ if (numWords !== aMisspelledWords.length) {
+ return false;
+ }
+
+ for (var i = 0; i < numWords; ++i) {
+ var word = String(sel.getRangeAt(i));
+ is(word, aMisspelledWords[i], "Misspelling is what we think it is.");
+ if (word !== aMisspelledWords[i]) {
+ return false;
+ }
+ }
+ return true;
+}
diff --git a/editor/libeditor/tests/test_CF_HTML_clipboard.html b/editor/libeditor/tests/test_CF_HTML_clipboard.html
new file mode 100644
index 000000000..4949f40b3
--- /dev/null
+++ b/editor/libeditor/tests/test_CF_HTML_clipboard.html
@@ -0,0 +1,159 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=572642
+-->
+<head>
+ <title>Test for Bug 572642</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=572642">Mozilla Bug 572642</a>
+<p id="display"></p>
+<div id="content">
+ <div id="editor1" contenteditable="true"></div>
+ <iframe id="editor2"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 572642 **/
+
+function copyCF_HTML(cfhtml, success, failure) {
+ const Cc = SpecialPowers.Cc;
+ const Ci = SpecialPowers.Ci;
+ const CF_HTML = "application/x-moz-nativehtml";
+
+ function getLoadContext() {
+ return SpecialPowers.wrap(window).QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsILoadContext);
+ }
+
+ var cb = Cc["@mozilla.org/widget/clipboard;1"].
+ getService(Ci.nsIClipboard);
+
+ var counter = 0;
+ function copyCF_HTML_worker(success, failure) {
+ if (++counter > 50) {
+ ok(false, "Timed out while polling clipboard for pasted data");
+ failure();
+ return;
+ }
+
+ var flavors = [CF_HTML];
+ if (!cb.hasDataMatchingFlavors(flavors, flavors.length, cb.kGlobalClipboard)) {
+ setTimeout(function() { copyCF_HTML_worker(success, failure); }, 100);
+ return;
+ }
+
+ var trans = Cc["@mozilla.org/widget/transferable;1"].
+ createInstance(Ci.nsITransferable);
+ trans.init(getLoadContext());
+ trans.addDataFlavor(CF_HTML);
+ cb.getData(trans, cb.kGlobalClipboard);
+ var data = SpecialPowers.createBlankObject();
+ try {
+ trans.getTransferData(CF_HTML, data, {});
+ data = SpecialPowers.wrap(data).value.QueryInterface(Ci.nsISupportsCString).data;
+ } catch (e) {
+ setTimeout(function() { copyCF_HTML_worker(success, failure); }, 100);
+ return;
+ }
+ success();
+ }
+
+ var trans = Cc["@mozilla.org/widget/transferable;1"].
+ createInstance(Ci.nsITransferable);
+ trans.init(getLoadContext());
+ trans.addDataFlavor(CF_HTML);
+ var data = Cc["@mozilla.org/supports-cstring;1"].
+ createInstance(Ci.nsISupportsCString);
+ data.data = cfhtml;
+ trans.setTransferData(CF_HTML, data, cfhtml.length);
+ cb.setData(trans, null, cb.kGlobalClipboard);
+ copyCF_HTML_worker(success, failure);
+}
+
+function loadCF_HTMLdata(filename) {
+ var req = new XMLHttpRequest();
+ req.open("GET", filename, false);
+ req.overrideMimeType("text/plain; charset=x-user-defined");
+ req.send(null);
+ ok(req.status, 200, "Could not read the binary file " + filename);
+ return req.responseText;
+}
+
+var gTests = [
+ // Copied from Firefox
+ {fileName: "cfhtml-firefox.txt", expected: "Firefox"},
+ // Copied from OpenOffice.org
+ {fileName: "cfhtml-ooo.txt", expected: "hello"},
+ // Copied from IE
+ {fileName: "cfhtml-ie.txt", expected: "browser"},
+ // Copied from Chromium
+ {fileName: "cfhtml-chromium.txt", expected: "Pacific"},
+ // CF_HTML with no context specified (StartHTML and EndHTML set to -1)
+ {fileName: "cfhtml-nocontext.txt", expected: "3.1415926535897932"},
+];
+var gTestIndex = 0;
+
+SimpleTest.waitForExplicitFinish();
+
+for (var i = 0; i < gTests.length; ++i) {
+ gTests[i].data = loadCF_HTMLdata("data/" + gTests[i].fileName);
+}
+
+function runTest() {
+ var test = gTests[gTestIndex++];
+
+ copyCF_HTML(test.data, function() {
+ // contenteditable
+ var contentEditable = document.getElementById("editor1");
+ contentEditable.innerHTML = "";
+ contentEditable.focus();
+ synthesizeKey("v", {accelKey: true});
+ isnot(contentEditable.textContent.indexOf(test.expected), -1,
+ "Paste operation for " + test.fileName + " should be successful in contenteditable");
+
+ // designMode
+ var iframe = document.getElementById("editor2");
+ iframe.addEventListener("load", function() {
+ iframe.removeEventListener("load", arguments.callee, false);
+ var doc = iframe.contentDocument;
+ var win = doc.defaultView;
+ setTimeout(function() {
+ win.addEventListener("focus", function() {
+ win.removeEventListener("focus", arguments.callee, false);
+ doc.designMode = "on";
+ synthesizeKey("v", {accelKey: true}, win);
+ isnot(doc.body.textContent.indexOf(test.expected), -1,
+ "Paste operation for " + test.fileName + " should be successful in designMode");
+
+ if (gTestIndex == gTests.length)
+ SimpleTest.finish();
+ else
+ runTest();
+ }, false);
+ win.focus();
+ }, 0);
+ }, false);
+ iframe.src = "data:text/html,";
+ }, SimpleTest.finish);
+}
+
+var isMac = ("nsILocalFileMac" in SpecialPowers.Ci);
+if (isMac)
+ SimpleTest.waitForFocus(runTest);
+else {
+ // This test is not yet supported on non-Mac platforms, see bug 574005.
+ todo(false, "Test not supported on this platform");
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_backspace_vs.html b/editor/libeditor/tests/test_backspace_vs.html
new file mode 100644
index 000000000..1ee754c95
--- /dev/null
+++ b/editor/libeditor/tests/test_backspace_vs.html
@@ -0,0 +1,130 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1216427
+-->
+<head>
+ <title>Test for Bug 1216427</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1216427">Mozilla Bug 1216427</a>
+<p id="display"></p>
+<div id="content">
+ <div id="edit1" contenteditable="true">a&#x263a;&#xfe0f;b</div><!-- BMP symbol with VS16 -->
+ <div id="edit2" contenteditable="true">a&#x1f310;&#xfe0e;b</div><!-- plane 1 symbol with VS15 -->
+ <div id="edit3" contenteditable="true">a&#x3402;&#xE0100;b</div><!-- BMP ideograph with VS17 -->
+ <div id="edit4" contenteditable="true">a&#x20000;&#xE0101;b</div><!-- SMP ideograph with VS18 -->
+ <div id="edit5" contenteditable="true">a&#x263a;&#xfe01;&#xfe02;&#xfe03;b</div><!-- BMP symbol with extra VSes -->
+ <div id="edit6" contenteditable="true">a&#x20000;&#xE0100;&#xE0101;&#xE0102;b</div><!-- SMP symbol with extra VSes -->
+ <!-- The Regional Indicator combinations here were supported by Apple Color Emoji
+ even prior to the major extension of coverage in the 10.10.5 timeframe. -->
+ <div id="edit7" contenteditable="true">a&#x1F1E8;&#x1F1F3;b</div><!-- Regional Indicator flag: CN -->
+ <div id="edit8" contenteditable="true">a&#x1F1E8;&#x1F1F3;&#x1F1E9;&#x1F1EA;b</div><!-- two RI flags: CN, DE -->
+ <div id="edit9" contenteditable="true">a&#x1F1E8;&#x1F1F3;&#x1F1E9;&#x1F1EA;&#x1F1EA;&#x1F1F8;b</div><!-- three RI flags: CN, DE, ES -->
+ <div id="edit10" contenteditable="true">a&#x1F1E8;&#x1F1F3;&#x1F1E9;&#x1F1EA;&#x1F1EA;&#x1F1F8;&#x1F1EB;&#x1F1F7;b</div><!-- four RI flags: CN, DE, ES, FR -->
+ <div id="edit11" contenteditable="true">a&#x1F1E8;&#x1F1F3;&#x1F1E9;&#x1F1EA;&#x1F1EA;&#x1F1F8;&#x1F1EB;&#x1F1F7;&#x1F1EC;&#x1F1E7;b</div><!-- five RI flags: CN, DE, ES, FR, GB -->
+
+ <div id="edit1b" contenteditable="true">a&#x263a;&#xfe0f;b</div><!-- BMP symbol with VS16 -->
+ <div id="edit2b" contenteditable="true">a&#x1f310;&#xfe0e;b</div><!-- plane 1 symbol with VS15 -->
+ <div id="edit3b" contenteditable="true">a&#x3402;&#xE0100;b</div><!-- BMP ideograph with VS17 -->
+ <div id="edit4b" contenteditable="true">a&#x20000;&#xE0101;b</div><!-- SMP ideograph with VS18 -->
+ <div id="edit5b" contenteditable="true">a&#x263a;&#xfe01;&#xfe02;&#xfe03;b</div><!-- BMP symbol with extra VSes -->
+ <div id="edit6b" contenteditable="true">a&#x20000;&#xE0100;&#xE0101;&#xE0102;b</div><!-- SMP symbol with extra VSes -->
+ <div id="edit7b" contenteditable="true">a&#x1F1E8;&#x1F1F3;b</div><!-- Regional Indicator flag: CN -->
+ <div id="edit8b" contenteditable="true">a&#x1F1E8;&#x1F1F3;&#x1F1E9;&#x1F1EA;b</div><!-- two RI flags: CN, DE -->
+ <div id="edit9b" contenteditable="true">a&#x1F1E8;&#x1F1F3;&#x1F1E9;&#x1F1EA;&#x1F1EA;&#x1F1F8;b</div><!-- three RI flags: CN, DE, ES -->
+ <div id="edit10b" contenteditable="true">a&#x1F1E8;&#x1F1F3;&#x1F1E9;&#x1F1EA;&#x1F1EA;&#x1F1F8;&#x1F1EB;&#x1F1F7;b</div><!-- four RI flags: CN, DE, ES, FR -->
+ <div id="edit11b" contenteditable="true">a&#x1F1E8;&#x1F1F3;&#x1F1E9;&#x1F1EA;&#x1F1EA;&#x1F1F8;&#x1F1EB;&#x1F1F7;&#x1F1EC;&#x1F1E7;b</div><!-- five RI flags: CN, DE, ES, FR, GB -->
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1216427 **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+
+function test(edit, bsCount) {
+ edit.focus();
+ var sel = window.getSelection();
+ sel.collapse(edit.childNodes[0], edit.textContent.length - 1);
+ for (i = 0; i < bsCount; ++i) {
+ synthesizeKey("VK_BACK_SPACE", {});
+ }
+ is(edit.textContent, "ab", "The backspace key should delete the characters correctly");
+}
+
+function testWithMove(edit, offset, bsCount) {
+ edit.focus();
+ var sel = window.getSelection();
+ sel.collapse(edit.childNodes[0], 0);
+ var i;
+ for (i = 0; i < offset; ++i) {
+ synthesizeKey("VK_RIGHT", {});
+ synthesizeKey("VK_LEFT", {});
+ synthesizeKey("VK_RIGHT", {});
+ }
+ for (i = 0; i < bsCount; ++i) {
+ synthesizeKey("VK_BACK_SPACE", {});
+ }
+ is(edit.textContent, "ab", "The backspace key should delete the characters correctly");
+}
+
+function runTest() {
+ /* test backspace-deletion of the middle character(s) */
+ test(document.getElementById("edit1"), 1);
+ test(document.getElementById("edit2"), 1);
+ test(document.getElementById("edit3"), 1);
+ test(document.getElementById("edit4"), 1);
+ test(document.getElementById("edit5"), 1);
+ test(document.getElementById("edit6"), 1);
+
+ /*
+ * Tests with Regional Indicator flags: these behave differently depending
+ * whether an emoji font is present, as ligated flags are edited as single
+ * characters whereas non-ligated RI characters act individually.
+ *
+ * For now, only rely on such an emoji font on OS X 10.7+. (Note that the
+ * Segoe UI Emoji font on Win8.1 and Win10 does not implement Regional
+ * Indicator flags.)
+ *
+ * Once the Firefox Emoji font is ready, we can load that via @font-face
+ * and expect these tests to work across all platforms.
+ */
+ hasEmojiFont =
+ (navigator.platform.indexOf("Mac") == 0 &&
+ /10\.([7-9]|[1-9][0-9])/.test(navigator.oscpu));
+
+ if (hasEmojiFont) {
+ test(document.getElementById("edit7"), 1);
+ test(document.getElementById("edit8"), 2);
+ test(document.getElementById("edit9"), 3);
+ test(document.getElementById("edit10"), 4);
+ test(document.getElementById("edit11"), 5);
+ }
+
+ /* extra tests with the use of RIGHT and LEFT to get to the right place */
+ testWithMove(document.getElementById("edit1b"), 2, 1);
+ testWithMove(document.getElementById("edit2b"), 2, 1);
+ testWithMove(document.getElementById("edit3b"), 2, 1);
+ testWithMove(document.getElementById("edit4b"), 2, 1);
+ testWithMove(document.getElementById("edit5b"), 2, 1);
+ testWithMove(document.getElementById("edit6b"), 2, 1);
+ if (hasEmojiFont) {
+ testWithMove(document.getElementById("edit7b"), 2, 1);
+ testWithMove(document.getElementById("edit8b"), 3, 2);
+ testWithMove(document.getElementById("edit9b"), 4, 3);
+ testWithMove(document.getElementById("edit10b"), 5, 4);
+ testWithMove(document.getElementById("edit11b"), 6, 5);
+ }
+
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1026397.html b/editor/libeditor/tests/test_bug1026397.html
new file mode 100644
index 000000000..487f3e87f
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1026397.html
@@ -0,0 +1,103 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1026397
+-->
+<head>
+ <title>Test for Bug 1026397</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1026397">Mozilla Bug 1026397</a>
+<p id="display"></p>
+<div id="content">
+<input id="input">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1026397 **/
+SimpleTest.waitForExplicitFinish();
+
+function runTests()
+{
+ var input = document.getElementById("input");
+ input.focus();
+
+ function doTest(aMaxLength, aInitialValue, aCaretOffset,
+ aInsertString, aExpectedValueDuringComposition,
+ aExpectedValueAfterCompositionEnd, aAdditionalExplanation)
+ {
+ input.value = aInitialValue;
+ var maxLengthStr = "";
+ if (aMaxLength >= 0) {
+ input.maxLength = aMaxLength;
+ maxLengthStr = aMaxLength.toString();
+ } else {
+ input.removeAttribute("maxlength");
+ maxLengthStr = "not specified";
+ }
+ input.selectionStart = input.selectionEnd = aCaretOffset;
+ if (aAdditionalExplanation) {
+ aAdditionalExplanation = " " + aAdditionalExplanation;
+ } else {
+ aAdditionalExplanation = "";
+ }
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": aInsertString,
+ "clauses":
+ [
+ { "length": aInsertString.length, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": aInsertString.length, "length": 0 }
+ });
+ is(input.value, aExpectedValueDuringComposition,
+ "The value of input whose maxlength is " + maxLengthStr + " should be "
+ + aExpectedValueDuringComposition + " during composition" + aAdditionalExplanation);
+ synthesizeComposition({ type: "compositioncommitasis" });
+ is(input.value, aExpectedValueAfterCompositionEnd,
+ "The value of input whose maxlength is " + maxLengthStr + " should be "
+ + aExpectedValueAfterCompositionEnd + " after compositionend" + aAdditionalExplanation);
+ }
+
+ // maxlength hasn't been specified yet.
+ doTest(-1, "", 0, "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE\u5BB6");
+
+ // maxlength="1"
+ doTest(1, "", 0, "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE\u5BB6", "");
+
+ // maxlength="2"
+ doTest(2, "", 0, "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7");
+ doTest(2, "X", 1, "\uD842\uDFB7\u91CE\u5BB6", "X\uD842\uDFB7\u91CE\u5BB6", "X");
+ doTest(2, "Y", 0, "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE\u5BB6Y", "Y");
+
+ // maxlength="3"
+ doTest(3, "", 0, "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE");
+ doTest(3, "A", 1, "\uD842\uDFB7\u91CE\u5BB6", "A\uD842\uDFB7\u91CE\u5BB6", "A\uD842\uDFB7");
+ doTest(3, "B", 0, "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE\u5BB6B", "\uD842\uDFB7B");
+ doTest(3, "CD", 1, "\uD842\uDFB7\u91CE\u5BB6", "C\uD842\uDFB7\u91CE\u5BB6D", "CD");
+
+ // maxlength="4"
+ doTest(4, "EF", 1, "\uD842\uDFB7\u91CE\u5BB6", "E\uD842\uDFB7\u91CE\u5BB6F", "E\uD842\uDFB7F");
+ doTest(4, "GHI", 1, "\uD842\uDFB7\u91CE\u5BB6", "G\uD842\uDFB7\u91CE\u5BB6HI", "GHI");
+
+ // maxlength="1", inputting only high surrogate
+ doTest(1, "", 0, "\uD842", "\uD842", "\uD842", "even if input string is only a high surrogate");
+
+ // maxlength="1", inputting only low surrogate
+ doTest(1, "", 0, "\uDFB7", "\uDFB7", "\uDFB7", "even if input string is only a low surrogate");
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForFocus(runTests);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1053048.html b/editor/libeditor/tests/test_bug1053048.html
new file mode 100644
index 000000000..4032d32c2
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1053048.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1053048
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1053048</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+
+ /** Test for Bug 1053048 **/
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(runTests);
+
+ const nsISelectionPrivate = SpecialPowers.Ci.nsISelectionPrivate;
+ const nsISelectionListener = SpecialPowers.Ci.nsISelectionListener;
+ const nsIDOMNSEditableElement = SpecialPowers.Ci.nsIDOMNSEditableElement;
+
+ function runTests()
+ {
+ var textarea = SpecialPowers.wrap(document.getElementById("textarea"));
+ textarea.focus();
+
+ var editor = textarea.QueryInterface(nsIDOMNSEditableElement).editor;
+ var selectionPrivate = editor.selection.QueryInterface(nsISelectionPrivate);
+
+ var selectionListener = {
+ count: 0,
+ notifySelectionChanged: function (aDocument, aSelection, aReason)
+ {
+ ok(true, "selectionStart: " + textarea.selectionStart);
+ ok(true, "selectionEnd: " + textarea.selectionEnd);
+ this.count++;
+ }
+ };
+
+ // Move caret to the end of the textarea
+ synthesizeMouse(textarea, 290, 10, {});
+ is(textarea.selectionStart, 3, "selectionStart should be 3 (after \"foo\")");
+ is(textarea.selectionEnd, 3, "selectionEnd should be 3 (after \"foo\")");
+
+ selectionPrivate.addSelectionListener(selectionListener);
+
+ sendKey("RETURN");
+ is(selectionListener.count, 1, "nsISelectionListener.notifySelectionChanged() should be called");
+ is(textarea.selectionStart, 4, "selectionStart should be 4");
+ is(textarea.selectionEnd, 4, "selectionEnd should be 4");
+ is(textarea.value, "foo\n", "The line break should be appended");
+
+ selectionPrivate.removeSelectionListener(selectionListener);
+ SimpleTest.finish();
+ }
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1053048">Mozilla Bug 1053048</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+
+<textarea id="textarea"
+ style="height: 100px; width: 300px; -moz-appearance: none"
+ spellcheck="false"
+ onkeydown="this.style.display='block'; this.style.height='200px';">foo</textarea>
+
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1067255.html b/editor/libeditor/tests/test_bug1067255.html
new file mode 100644
index 000000000..be2d703c5
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1067255.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1067255
+-->
+
+<head>
+ <title>Test for Bug 1067255</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<body onload="doTest();">
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1067255">Mozilla Bug 1067255</a>
+
+ <pre id="test">
+ <script type="application/javascript">
+ /** Test for Bug 1067255 **/
+ SimpleTest.waitForExplicitFinish();
+
+ function doTest() {
+ var text = $("text-field");
+ var password = $("password-field");
+
+ var editor1 = SpecialPowers.wrap(text).editor;
+ var editor2 = SpecialPowers.wrap(password).editor;
+
+ text.focus();
+ text.select();
+
+ ok(editor1.canCopy(), "can copy, text");
+ ok(editor1.canCut(), "can cut, text");
+ ok(editor1.canDelete(), "can delete, text");
+
+ password.focus();
+ password.select();
+
+ // Copy and cut commands don't do anything on passoword fields by default,
+ // but webpages can hook up event handlers to the event, and thus, we have to
+ // always keep the cut and copy event enabled in HTML/XHTML documents.
+ ok(editor2.canCopy(), "can copy, password");
+ ok(editor2.canCut(), "can cut, password");
+ ok(editor2.canDelete(), "can delete, password");
+
+ SimpleTest.finish();
+ }
+ </script>
+ </pre>
+
+ <input type="text" value="Gonzo says hi" id="text-field" />
+ <input type="password" value="Jan also" id="password-field" />
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1068979.html b/editor/libeditor/tests/test_bug1068979.html
new file mode 100644
index 000000000..126c39d27
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1068979.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1068979
+-->
+<head>
+ <title>Test for Bug 1068979</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1068979">Mozilla Bug 1068979</a>
+<p id="display"></p>
+<div id="content">
+ <div id="editor1" contenteditable="true">&#x1d400;</div>
+ <div id="editor2" contenteditable="true">a<u>&#x1d401;</u>b</div>
+ <div id="editor3" contenteditable="true">a&#x1d402;<u>b</u></div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1068979 **/
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ // Test backspacing over SMP characters pasted-in to a contentEditable
+ getSelection().selectAllChildren(document.getElementById("editor1"));
+ var ed1 = document.getElementById("editor1");
+ var ch1 = ed1.textContent;
+ ed1.focus();
+ synthesizeKey("C", {accelKey: true});
+ synthesizeKey("V", {accelKey: true});
+ synthesizeKey("V", {accelKey: true});
+ synthesizeKey("V", {accelKey: true});
+ synthesizeKey("V", {accelKey: true});
+ is(ed1.textContent, ch1 + ch1 + ch1 + ch1, "Should have four SMP characters");
+ sendKey("back_space");
+ is(ed1.textContent, ch1 + ch1 + ch1, "Three complete characters should remain");
+ sendKey("back_space");
+ is(ed1.textContent, ch1 + ch1, "Two complete characters should remain");
+ sendKey("back_space");
+ is(ed1.textContent, ch1, "Only one complete SMP character should remain");
+ ed1.blur();
+
+ // Test backspacing across an SMP character in a sub-element
+ getSelection().selectAllChildren(document.getElementById("editor2"));
+ var ed2 = document.getElementById("editor2");
+ ed2.focus();
+ sendKey("right");
+ sendKey("back_space");
+ sendKey("back_space");
+ is(ed2.textContent, "a", "Only the 'a' should remain");
+ ed2.blur();
+
+ // Test backspacing across an SMP character from a following sub-element
+ getSelection().selectAllChildren(document.getElementById("editor3"));
+ var ed3 = document.getElementById("editor3");
+ ed3.focus();
+ sendKey("right");
+ sendKey("left");
+ sendKey("back_space");
+ is(ed3.textContent, "ab", "The letters 'ab' should remain");
+ ed3.blur();
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1094000.html b/editor/libeditor/tests/test_bug1094000.html
new file mode 100644
index 000000000..cc27cc675
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1094000.html
@@ -0,0 +1,104 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1094000
+-->
+<head>
+ <title>Test for Bug 1094000</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1094000">Mozilla Bug 1094000</a>
+<p id="display"></p>
+<div id="content">
+ <div id="editor0" contenteditable></div>
+ <div id="editor1" contenteditable><br></div>
+ <div id="editor2" contenteditable>a</div>
+ <div id="editor3" contenteditable>b</div>
+ <div id="editor4" contenteditable>c</div>
+ <div id="editor5" contenteditable>d</div>
+ <div id="editor6" contenteditable>e</div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1094000 **/
+
+SimpleTest.waitForExplicitFinish();
+
+const kIsLinux = navigator.platform.indexOf("Linux") == 0;
+
+function runTests()
+{
+ var editor0 = document.getElementById("editor0");
+ var editor1 = document.getElementById("editor1");
+ var editor2 = document.getElementById("editor2");
+ var editor3 = document.getElementById("editor3");
+ var editor4 = document.getElementById("editor4");
+ var editor5 = document.getElementById("editor5");
+ var editor6 = document.getElementById("editor6");
+
+ ok(editor1.getBoundingClientRect().height - editor0.getBoundingClientRect().height > 1,
+ "an editor having a <br> element and an empty editor shouldn't be same height");
+ ok(Math.abs(editor1.getBoundingClientRect().height - editor2.getBoundingClientRect().height) <= 1,
+ "an editor having only a <br> element and an editor having \"a\" should be same height");
+
+ editor2.focus();
+ synthesizeKey("VK_RIGHT", {});
+ synthesizeKey("VK_BACK_SPACE", {});
+ is(editor2.innerHTML, "<br>",
+ "an editor which had \"a\" should have only <br> element after Backspace keypress");
+ ok(Math.abs(editor2.getBoundingClientRect().height - editor1.getBoundingClientRect().height) <= 1,
+ "an editor whose content was removed by Backspace key should have a place to put a caret");
+
+ editor3.focus();
+ synthesizeKey("VK_LEFT", {});
+ synthesizeKey("VK_DELETE", {});
+ is(editor3.innerHTML, "<br>",
+ "an editor which had \"b\" should have only <br> element after Delete keypress");
+ ok(Math.abs(editor3.getBoundingClientRect().height - editor1.getBoundingClientRect().height) <= 1,
+ "an editor whose content was removed by Delete key should have a place to put a caret");
+
+ editor4.focus();
+ window.getSelection().selectAllChildren(editor4);
+ synthesizeKey("VK_BACK_SPACE", {});
+ is(editor4.innerHTML, "<br>",
+ "an editor which had \"c\" should have only <br> element after removing selected text with Backspace key");
+ ok(Math.abs(editor4.getBoundingClientRect().height - editor1.getBoundingClientRect().height) <= 1,
+ "an editor whose content was selected and removed by Backspace key should have a place to put a caret");
+
+ editor5.focus();
+ window.getSelection().selectAllChildren(editor5);
+ synthesizeKey("VK_DELETE", {});
+ is(editor5.innerHTML, "<br>",
+ "an editor which had \"d\" should have only <br> element after removing selected text with Delete key");
+ ok(Math.abs(editor5.getBoundingClientRect().height - editor1.getBoundingClientRect().height) <= 1,
+ "an editor whose content was selected and removed by Delete key should have a place to put a caret");
+
+ editor6.focus();
+ window.getSelection().selectAllChildren(editor6);
+ synthesizeKey("x", { accelKey: true });
+ is(editor6.innerHTML, "<br>",
+ "an editor which had \"e\" should have only <br> element after removing selected text by \"Cut\"");
+ ok(Math.abs(editor6.getBoundingClientRect().height - editor1.getBoundingClientRect().height) <= 1,
+ "an editor whose content was selected and removed by \"Cut\" should have a place to put a caret");
+
+ editor0.focus();
+ synthesizeKey("VK_BACK_SPACE", {});
+ is(editor0.innerHTML, "",
+ "an empty editor should keep being empty even if Backspace key is pressed");
+ synthesizeKey("VK_DELETE", {});
+ is(editor0.innerHTML, "",
+ "an empty editor should keep being empty even if Delete key is pressed");
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForFocus(runTests);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1100966.html b/editor/libeditor/tests/test_bug1100966.html
new file mode 100644
index 000000000..28c30c849
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1100966.html
@@ -0,0 +1,65 @@
+<!DOCTYPE>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1100966
+-->
+<head>
+ <title>Test for Bug 1100966</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<div id="display">
+</div>
+<div id="content" contenteditable>
+=====<br>
+correct<br>
+fivee sixx<br>
+====
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+/** Test for Bug 1100966 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var div = document.getElementById("content");
+ div.focus();
+ synthesizeMouseAtCenter(div, {});
+
+ synthesizeKey(" ", {});
+ setTimeout(function() {
+ synthesizeKey("a", {});
+ setTimeout(function() {
+ synthesizeKey("VK_BACK_SPACE", {});
+
+ var sel = getSpellCheckSelection();
+ is(sel.rangeCount, 2, "We should have two misspelled words");
+ is(String(sel.getRangeAt(0)), "fivee", "Correct misspelled word");
+ is(String(sel.getRangeAt(1)), "sixx", "Correct misspelled word");
+
+ SimpleTest.finish();
+ },0);
+ },0);
+
+});
+
+function getSpellCheckSelection() {
+ var Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIEditingSession);
+ var editor = editingSession.getEditorForWindow(window);
+ var selcon = editor.selectionController;
+ return selcon.getSelection(selcon.SELECTION_SPELLCHECK);
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_bug1101392.html b/editor/libeditor/tests/test_bug1101392.html
new file mode 100644
index 000000000..76917203b
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1101392.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1101392
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1101392</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 1101392 **/
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(runTests);
+
+ function runCopyCommand(element, compareText, nextTest)
+ {
+ element.focus();
+ var expectedEndpoint, sel;
+ if (element.localName == "textarea") {
+ element.select();
+ expectedEndpoint = element.selectionEnd;
+ } else {
+ sel = getSelection();
+ sel.selectAllChildren(element.parentNode);
+ expectedEndpoint = [sel.getRangeAt(0).endContainer,
+ sel.getRangeAt(0).endOffset];
+ }
+
+ function checkCollapse() {
+ var desc = " after cmd_copyAndCollapseToEnd for " +
+ element.localName;
+ if (element.localName == "textarea") {
+ is(element.selectionStart, expectedEndpoint, "start offset" + desc);
+ is(element.selectionEnd, expectedEndpoint, "end offset" + desc);
+ } else {
+ is(sel.isCollapsed, true, "collapsed" + desc);
+ is(sel.anchorNode, expectedEndpoint[0], "node" + desc);
+ is(sel.anchorOffset, expectedEndpoint[1], "offset" + desc);
+ }
+
+ nextTest();
+ }
+
+ SimpleTest.waitForClipboard(compareText,
+ () => SpecialPowers.doCommand(window, "cmd_copyAndCollapseToEnd"),
+ checkCollapse, checkCollapse);
+ }
+
+ function testDiv()
+ {
+ var content = document.getElementById("content");
+ runCopyCommand(content, 'abc', testTextarea);
+ }
+
+ function testTextarea()
+ {
+ var textarea = document.getElementById("textarea");
+ runCopyCommand(textarea, 'def', SimpleTest.finish);
+ }
+
+ function runTests()
+ {
+ testDiv();
+ }
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1101392">Mozilla Bug 1101392</a>
+<div><div id="content">abc</div></div>
+
+<textarea id="textarea">def</textarea>
+
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1102906.html b/editor/libeditor/tests/test_bug1102906.html
new file mode 100644
index 000000000..36bfc8600
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1102906.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1102906
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1102906</title>
+
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+
+ <script>
+ "use strict";
+
+ /* Test for Bug 1102906 */
+ /* The caret should be movable by using keyboard after drag-and-drop. */
+
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus( () => {
+ let content = document.getElementById("content");
+ let drag = document.getElementById("drag")
+ let selection = window.getSelection();
+
+ /* Perform drag-and-drop for an arbitrary content. The caret should be at
+ the end of the contenteditable. */
+ selection.selectAllChildren(drag);
+ synthesizeDrop(drag, content, {}, "copy");
+
+ let textContentAfterDrop = content.textContent;
+
+ /* Move the caret to the front of the contenteditable by using keyboard. */
+ for (let i = 0; i < content.textContent.length; ++i) {
+ sendKey("LEFT");
+ }
+ sendChar("!");
+
+ is(content.textContent, "!" + textContentAfterDrop,
+ "The exclamation mark should be inserted at the front.");
+
+ SimpleTest.finish();
+ });
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1102906">Mozilla Bug 1102906</a>
+<div id="content" contenteditable="true"><span id="drag">Drag</span></div>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1109465.html b/editor/libeditor/tests/test_bug1109465.html
new file mode 100644
index 000000000..dc31e9bf0
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1109465.html
@@ -0,0 +1,69 @@
+<!DOCTYPE>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1109465
+-->
+<head>
+ <title>Test for Bug 1109465</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="display">
+ <textarea></textarea>
+</div>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+/** Test for Bug 1109465 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var t = document.querySelector("textarea");
+ t.focus();
+
+ // Type foo\nbar and place the caret at the end of the last line
+ synthesizeKey("f", {});
+ synthesizeKey("o", {});
+ synthesizeKey("o", {});
+ synthesizeKey("VK_RETURN", {});
+ synthesizeKey("b", {});
+ synthesizeKey("a", {});
+ synthesizeKey("r", {});
+ synthesizeKey("VK_UP", {});
+ is(t.selectionStart, 3, "Correct start of selection");
+ is(t.selectionEnd, 3, "Correct end of selection");
+
+ // Compose an IME string
+ synthesizeComposition({ type: "compositionstart" });
+ var composingString = "\u306B";
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": composingString,
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+ synthesizeComposition({ type: "compositioncommitasis" });
+ is(t.value, "foo\u306B\nbar", "Correct value after composition");
+
+ // Now undo to test that the transaction merger has correctly detected the
+ // IMETextTxn.
+ synthesizeKey("Z", {accelKey: true});
+ is(t.value, "foo\nbar", "Correct value after undo");
+
+ SimpleTest.finish();
+});
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_bug1140105.html b/editor/libeditor/tests/test_bug1140105.html
new file mode 100644
index 000000000..21d003054
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1140105.html
@@ -0,0 +1,71 @@
+<!DOCTYPE>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1140105
+-->
+<head>
+ <title>Test for Bug 1140105</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<div id="display">
+</div>
+
+<div id="content" contenteditable><font face="Arial">1234567890</font></div>
+
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+/** Test for Bug 1140105 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var div = document.getElementById("content");
+ div.focus();
+ synthesizeMouseAtCenter(div, {});
+ synthesizeKey("VK_LEFT", {});
+
+ var sel = window.getSelection();
+ var selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "#text", "selection should be in text node");
+ is(selRange.endOffset, 9, "offset should be 9");
+
+ var firstHas = {};
+ var anyHas = {};
+ var allHas = {};
+ var editor = getEditor();
+
+ var atomService = SpecialPowers.Cc["@mozilla.org/atom-service;1"].getService(SpecialPowers.Ci.nsIAtomService);
+ var propAtom = atomService.getAtom("font");
+ editor.getInlineProperty(propAtom, "face", "Arial", firstHas, anyHas, allHas);
+ is(firstHas.value, true, "Test for Arial: firstHas: true expected");
+ is(anyHas.value, true, "Test for Arial: anyHas: true expected");
+ is(allHas.value, true, "Test for Arial: allHas: true expected");
+ editor.getInlineProperty(propAtom, "face", "Courier", firstHas, anyHas, allHas);
+ is(firstHas.value, false, "Test for Courier: firstHas: false expected");
+ is(anyHas.value, false, "Test for Courier: anyHas: false expected");
+ is(allHas.value, false, "Test for Courier: allHas: false expected");
+
+ SimpleTest.finish();
+
+});
+
+function getEditor() {
+ var Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIEditingSession);
+ var editor = editingSession.getEditorForWindow(window);
+ editor.QueryInterface(Ci.nsIHTMLEditor);
+ return editor;
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_bug1140617.html b/editor/libeditor/tests/test_bug1140617.html
new file mode 100644
index 000000000..078458a3a
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1140617.html
@@ -0,0 +1,37 @@
+<!doctype html>
+<title>Mozilla Bug 1140617</title>
+<link rel=stylesheet href="/tests/SimpleTest/test.css">
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1140617"
+ target="_blank">Mozilla Bug 1140617</a>
+<iframe id="i1" width="200" height="100" src="about:blank"></iframe>
+<img id="i" src="green.png">
+<script>
+function runTest() {
+ SpecialPowers.setCommandNode(window, document.getElementById("i"));
+ SpecialPowers.doCommand(window, "cmd_copyImageContents");
+
+ var e = document.getElementById('i1');
+ var doc = e.contentDocument;
+ doc.designMode = "on";
+ doc.defaultView.focus();
+ var selection = doc.defaultView.getSelection();
+ selection.removeAllRanges();
+ selection.selectAllChildren(doc.body);
+ selection.collapseToEnd();
+
+ doc.execCommand("fontname", false, "Arial");
+ doc.execCommand("bold", false, null);
+ doc.execCommand("insertText", false, "12345");
+ doc.execCommand("paste", false, null);
+ doc.execCommand("insertText", false, "a");
+
+ is(doc.queryCommandValue("fontname"), "Arial", "Arial expected");
+ is(doc.queryCommandState("bold"), true, "Bold expected");
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+</script>
diff --git a/editor/libeditor/tests/test_bug1153237.html b/editor/libeditor/tests/test_bug1153237.html
new file mode 100644
index 000000000..38d631326
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1153237.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1153237
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1153237</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ SimpleTest.waitForExplicitFinish();
+
+ // Avoid platform selection differences
+ SimpleTest.waitForFocus(function() {
+ SpecialPowers.pushPrefEnv({
+ "set": [["layout.word_select.eat_space_to_next_word", true]]
+ }, runTests);
+ });
+
+ function runTests()
+ {
+ var element = document.getElementById("editor");
+ var sel = window.getSelection();
+
+ element.focus();
+ is(sel.getRangeAt(0).startOffset, 0, "offset is zero");
+
+ SpecialPowers.doCommand(window, "cmd_selectRight2");
+ is(sel.toString(), "Some ",
+ "first word + space is selected: got '" + sel.toString() + "'");
+
+ SpecialPowers.doCommand(window, "cmd_selectRight2");
+ is(sel.toString(), "Some text",
+ "both words are selected: got '" + sel.toString() + "'");
+
+ SimpleTest.finish();
+ }
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1153237">Mozilla Bug 1153237</a>
+<div id="editor" contenteditable>Some text</div><span></span>
+
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1154791.html b/editor/libeditor/tests/test_bug1154791.html
new file mode 100644
index 000000000..03b605e20
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1154791.html
@@ -0,0 +1,67 @@
+<!DOCTYPE>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1154791
+-->
+<head>
+ <title>Test for Bug 1154791</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<div id="display">
+</div>
+
+<div id="content" contenteditable>
+<tt>thiss onee is stilll a</tt>
+</div>
+
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+/** Test for Bug 1154791 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var div = document.getElementById("content");
+ div.focus();
+ synthesizeMouseAtCenter(div, {});
+ synthesizeKey("VK_LEFT", {});
+ synthesizeKey("VK_LEFT", {});
+
+ setTimeout(function() {
+ synthesizeKey("VK_BACK_SPACE", {});
+ setTimeout(function() {
+ synthesizeKey(" ", {});
+
+ setTimeout(function() {
+ var sel = getSpellCheckSelection();
+ is(sel.rangeCount, 2, "We should have two misspelled words");
+ is(String(sel.getRangeAt(0)), "thiss", "Correct misspelled word");
+ is(String(sel.getRangeAt(1)), "onee", "Correct misspelled word");
+
+ SimpleTest.finish();
+ },0);
+ },0);
+ },0);
+
+});
+
+function getSpellCheckSelection() {
+ var Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIEditingSession);
+ var editor = editingSession.getEditorForWindow(window);
+ var selcon = editor.selectionController;
+ return selcon.getSelection(selcon.SELECTION_SPELLCHECK);
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_bug1162952.html b/editor/libeditor/tests/test_bug1162952.html
new file mode 100644
index 000000000..ad119de87
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1162952.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1162952
+-->
+<head>
+ <title>Test for Bug 1162952</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1162952">Mozilla Bug 1162952</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 1162952 **/
+var userCallbackRun = false;
+
+document.addEventListener('keydown', function() {
+ // During a user callback, the commands should be enabled
+ userCallbackRun = true;
+ is(true, document.queryCommandEnabled('cut'));
+ is(true, document.queryCommandEnabled('copy'));
+});
+
+// Otherwise, they should be disabled
+is(false, document.queryCommandEnabled('cut'));
+is(false, document.queryCommandEnabled('copy'));
+
+// Fire a user callback
+synthesizeKey('A', {});
+
+ok(userCallbackRun, "User callback should've been run");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1181130-1.html b/editor/libeditor/tests/test_bug1181130-1.html
new file mode 100644
index 000000000..eb27526a3
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1181130-1.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1181130
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1181130</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1181130">Mozilla Bug 1181130</a>
+<p id="display"></p>
+<div id="container" contenteditable="true">
+ editable div
+ <div id="noneditable" contenteditable="false">
+ non-editable div
+ <div id="editable" contenteditable="true">nested editable div</div>
+ </div>
+</div>
+<script type="application/javascript">
+/** Test for Bug 1181130 **/
+var container = document.getElementById("container");
+var noneditable = document.getElementById("noneditable");
+var editable = document.getElementById("editable");
+
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(function() {
+ synthesizeMouseAtCenter(noneditable, {});
+ ok(!document.getSelection().toString().includes("nested editable div"),
+ "Selection should not include non-editable content");
+
+ synthesizeMouseAtCenter(container, {});
+ ok(!document.getSelection().toString().includes("nested editable div"),
+ "Selection should not include non-editable content");
+
+ synthesizeMouseAtCenter(editable, {});
+ ok(!document.getSelection().toString().includes("nested editable div"),
+ "Selection should not include non-editable content");
+
+ SimpleTest.finish();
+});
+</script>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1181130-2.html b/editor/libeditor/tests/test_bug1181130-2.html
new file mode 100644
index 000000000..edb380e98
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1181130-2.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1181130
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1181130</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1181130">Mozilla Bug 1181130</a>
+<p id="display"></p>
+<div id="container" contenteditable="true">
+ editable div
+ <div id="noneditable" contenteditable="false">
+ non-editable div
+ </div>
+</div>
+<script type="application/javascript">
+/** Test for Bug 1181130 **/
+var container = document.getElementById("container");
+var noneditable = document.getElementById("noneditable");
+
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(function() {
+ var nonHTMLElement = document.createElementNS("http://www.example.com", "element");
+ nonHTMLElement.innerHTML = '<div contenteditable="true">nested editable div</div>';
+ noneditable.appendChild(nonHTMLElement);
+
+ synthesizeMouseAtCenter(noneditable, {});
+ ok(!document.getSelection().toString().includes("nested editable div"),
+ "Selection should not include non-editable content");
+
+ SimpleTest.finish();
+});
+</script>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1186799.html b/editor/libeditor/tests/test_bug1186799.html
new file mode 100644
index 000000000..b0b583a7e
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1186799.html
@@ -0,0 +1,81 @@
+<!DOCTYPE>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1186799
+-->
+<head>
+ <title>Test for Bug 1186799</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="content">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1186799">Mozilla Bug 1186799</a>
+<p id="display"></p>
+<div id="content">
+ <span id="span">span</span>
+ <div id="editor" contenteditable></div>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 1186799 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var span = document.getElementById("span");
+ var editor = document.getElementById("editor");
+ editor.focus();
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3042",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+
+ ok(isThereIMESelection(), "There should be IME selection");
+
+ var compositionEnd = false;
+ editor.addEventListener("compositionend", function () { compositionEnd = true; }, true);
+
+ synthesizeMouseAtCenter(span, {});
+
+ ok(compositionEnd, "composition end should be fired at clicking outside of the editor");
+ ok(!isThereIMESelection(), "There should be no IME selection");
+
+ SimpleTest.finish();
+});
+
+function isThereIMESelection()
+{
+ var selCon = SpecialPowers.wrap(window).
+ QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor).
+ getInterface(SpecialPowers.Ci.nsIWebNavigation).
+ QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor).
+ getInterface(SpecialPowers.Ci.nsIEditingSession).
+ getEditorForWindow(window).
+ selectionController;
+ const kIMESelections = [
+ SpecialPowers.Ci.nsISelectionController.SELECTION_IME_RAWINPUT,
+ SpecialPowers.Ci.nsISelectionController.SELECTION_IME_SELECTEDRAWTEXT,
+ SpecialPowers.Ci.nsISelectionController.SELECTION_IME_CONVERTEDTEXT,
+ SpecialPowers.Ci.nsISelectionController.SELECTION_IME_SELECTEDCONVERTEDTEXT
+ ];
+ for (var i = 0; i < kIMESelections.length; i++) {
+ var sel = selCon.getSelection(kIMESelections[i]);
+ if (sel && sel.rangeCount) {
+ return true;
+ }
+ }
+ return false;
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1230473.html b/editor/libeditor/tests/test_bug1230473.html
new file mode 100644
index 000000000..bff7826d1
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1230473.html
@@ -0,0 +1,124 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1230473
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1230473</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1230473">Mozilla Bug 1230473</a>
+<input id="input">
+<textarea id="textarea"></textarea>
+<div id="div" contenteditable></div>
+<script type="application/javascript">
+/** Test for Bug 1230473 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(()=>{
+ function runTest(aEditor) {
+ function committer() {
+ aEditor.blur();
+ aEditor.focus();
+ }
+ function isNSEditableElement() {
+ return aEditor.tagName.toLowerCase() == "input" || aEditor.tagName.toLowerCase() == "textarea";
+ }
+ function value() {
+ return isNSEditableElement() ? aEditor.value : aEditor.textContent;
+ }
+ function isComposing() {
+ return isNSEditableElement() ? SpecialPowers.wrap(aEditor)
+ .QueryInterface(SpecialPowers.Ci.nsIDOMNSEditableElement)
+ .editor
+ .QueryInterface(SpecialPowers.Ci.nsIEditorIMESupport)
+ .composing :
+ SpecialPowers.wrap(window)
+ .QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
+ .getInterface(SpecialPowers.Ci.nsIWebNavigation)
+ .QueryInterface(SpecialPowers.Ci.nsIDocShell)
+ .editor
+ .QueryInterface(SpecialPowers.Ci.nsIEditorIMESupport)
+ .composing;
+ }
+ function clear() {
+ if (isNSEditableElement()) {
+ aEditor.value = "";
+ } else {
+ aEditor.textContent = "";
+ }
+ }
+
+ clear();
+
+ // Committing at compositionstart
+ aEditor.focus();
+ aEditor.addEventListener("compositionstart", committer, true);
+ synthesizeCompositionChange({ composition: { string: "a", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }] },
+ caret: { start: 1, length: 0 }});
+ aEditor.removeEventListener("compositionstart", committer, true);
+ ok(!isComposing(), "composition in " + aEditor.id + " should be committed by compositionstart event handler");
+ is(value(), "", "composition in " + aEditor.id + " shouldn't insert any text since it's committed at compositionstart");
+ clear();
+
+ // Committing at first compositionupdate
+ aEditor.focus();
+ aEditor.addEventListener("compositionupdate", committer, true);
+ synthesizeCompositionChange({ composition: { string: "a", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }] },
+ caret: { start: 1, length: 0 }});
+ aEditor.removeEventListener("compositionupdate", committer, true);
+ ok(!isComposing(), "composition in " + aEditor.id + " should be committed by compositionupdate event handler");
+ is(value(), "", "composition in " + aEditor.id + " shouldn't have inserted any text since it's committed at first compositionupdate");
+ clear();
+
+ // Committing at first text (eCompositionChange)
+ aEditor.focus();
+ aEditor.addEventListener("text", committer, true);
+ synthesizeCompositionChange({ composition: { string: "a", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }] },
+ caret: { start: 1, length: 0 }});
+ aEditor.removeEventListener("text", committer, true);
+ ok(!isComposing(), "composition in " + aEditor.id + " should be committed by text event handler");
+ is(value(), "", "composition in " + aEditor.id + " should have inserted any text since it's committed at first text");
+ clear();
+
+ // Committing at second compositionupdate
+ aEditor.focus();
+ synthesizeComposition({ type: "compositionstart" });
+ synthesizeCompositionChange({ composition: { string: "a", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }] },
+ caret: { start: 1, length: 0 }});
+ ok(isComposing(), "composition should be in " + aEditor.id + " before dispatching second compositionupdate");
+ is(value(), "a", "composition in " + aEditor.id + " should be 'a' before dispatching second compositionupdate");
+ aEditor.addEventListener("compositionupdate", committer, true);
+ synthesizeCompositionChange({ composition: { string: "ab", clauses: [{length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }] },
+ caret: { start: 2, length: 0 }});
+ aEditor.removeEventListener("compositionupdate", committer, true);
+ ok(!isComposing(), "composition in " + aEditor.id + " should be committed by compositionupdate event handler");
+ todo_is(value(), "a", "composition in " + aEditor.id + " shouldn't have been modified since it's committed at second compositionupdate");
+ clear();
+
+ // Committing at second text (eCompositionChange)
+ aEditor.focus();
+ synthesizeComposition({ type: "compositionstart" });
+ synthesizeCompositionChange({ composition: { string: "a", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }] },
+ caret: { start: 1, length: 0 }});
+ ok(isComposing(), "composition should be in " + aEditor.id + " before dispatching second text");
+ is(value(), "a", "composition in " + aEditor.id + " should be 'a' before dispatching second text");
+ aEditor.addEventListener("text", committer, true);
+ synthesizeCompositionChange({ composition: { string: "ab", clauses: [{length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }] },
+ caret: { start: 2, length: 0 }});
+ aEditor.removeEventListener("text", committer, true);
+ ok(!isComposing(), "composition in " + aEditor.id + " should be committed by text event handler");
+ todo_is(value(), "a", "composition in " + aEditor.id + " shouldn't have been modified since it's committed at second text");
+ clear();
+ }
+ runTest(document.getElementById("input"));
+ runTest(document.getElementById("textarea"));
+ runTest(document.getElementById("div"));
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1247483.html b/editor/libeditor/tests/test_bug1247483.html
new file mode 100644
index 000000000..40dbc36ce
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1247483.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html><head>
+<title>Test for bug 1247483</title>
+<style src="/tests/SimpleTest/test.css" type="text/css"></style>
+<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+<script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+
+<script class="testbody" type="application/javascript">
+
+function runTest() {
+ // Copy content from table.
+ var selection = getSelection();
+ var startRange = document.createRange();
+ startRange.setStart(document.getElementById("start"), 0);
+ startRange.setEnd(document.getElementById("end"), 2);
+ selection.removeAllRanges();
+ selection.addRange(startRange);
+ SpecialPowers.wrap(document).execCommand("copy", false, null);
+
+ // Paste content into "pastecontainer"
+ var pasteContainer = document.getElementById("pastecontainer");
+ var pasteRange = document.createRange();
+ pasteRange.selectNodeContents(pasteContainer);
+ pasteRange.collapse(false);
+ selection.removeAllRanges();
+ selection.addRange(pasteRange);
+ SpecialPowers.wrap(document).execCommand("paste", false, null);
+
+ is(pasteContainer.querySelectorAll("td").length, 4, "4 <td> should be pasted.");
+
+ document.execCommand("undo", false, null);
+
+ is(pasteContainer.querySelectorAll("td").length, 0, "Undo should have remove the 4 pasted <td>.");
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(runTest);
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1247483">Mozilla Bug 1247483</a>
+<p id="display"></p>
+
+<pre id="test">
+</pre>
+
+<div id="container" contenteditable="true">
+<table>
+ <tr id="start"><td>1 1</td><td>1 2</td></tr>
+ <tr id="end"><td>2 1</td><td>2 2</td></tr>
+</table>
+</div>
+
+<div id="pastecontainer" contenteditable="true">
+</div>
+
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1248128.html b/editor/libeditor/tests/test_bug1248128.html
new file mode 100644
index 000000000..08b0139c9
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1248128.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1248128
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1248128</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ SimpleTest.waitForExplicitFinish();
+
+ SimpleTest.waitForFocus(function() {
+ var outer = document.querySelector("html");
+ ok(outer.scrollTop == 0, "scrollTop is zero: got " + outer.scrollTop);
+
+ var input = document.getElementById("testInput");
+ input.focus();
+
+ var scroll = outer.scrollTop;
+ ok(scroll > 0, "element has scrolled: new value " + scroll);
+
+ try {
+ SpecialPowers.doCommand(window, "cmd_moveLeft");
+ ok(false, "should not be able to do kMoveLeft");
+ } catch (e) {
+ ok(true, "unable to perform kMoveLeft");
+ }
+
+ ok(outer.scrollTop == scroll,
+ "scroll is unchanged: got " + outer.scrollTop + ", expected " + scroll);
+
+ // Make sure cmd_moveLeft isn't failing for some unrelated reason
+ synthesizeKey("a", {});
+ is(input.selectionStart, 1, "selectionStart after typing");
+ SpecialPowers.doCommand(window, "cmd_moveLeft");
+ is(input.selectionStart, 0, "selectionStart after move left");
+
+ SimpleTest.finish();
+ });
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1248128">Mozilla Bug 1248128</a>
+<div style="height: 2000px;"></div>
+<input type="text" id="testInput"></input>
+<div style="height: 200px;"></div>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1248185.html b/editor/libeditor/tests/test_bug1248185.html
new file mode 100644
index 000000000..d545cfc53
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1248185.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1248185
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1248185</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ SimpleTest.waitForExplicitFinish();
+
+ // Avoid platform selection differences
+ SimpleTest.waitForFocus(function() {
+ SpecialPowers.pushPrefEnv({
+ "set": [["layout.word_select.eat_space_to_next_word", true]]
+ }, runTests);
+ });
+
+ function runTests()
+ {
+ var editor = document.querySelector("#test");
+ editor.focus();
+
+ var sel = window.getSelection();
+
+ SpecialPowers.doCommand(window, "cmd_moveRight2");
+ SpecialPowers.doCommand(window, "cmd_moveRight2");
+ SpecialPowers.doCommand(window, "cmd_moveRight2");
+ SpecialPowers.doCommand(window, "cmd_selectRight2");
+ ok(sel.toString() == "three ", "expected 'three ' to be selected");
+
+ SpecialPowers.doCommand(window, "cmd_moveRight2");
+ SpecialPowers.doCommand(window, "cmd_moveRight2");
+ SpecialPowers.doCommand(window, "cmd_moveRight2");
+ ok(sel.toString() == "", "expected empty selection");
+
+ SpecialPowers.doCommand(window, "cmd_selectLeft2");
+ ok(sel.toString() == "five", "expected 'five' to be selected");
+
+ SimpleTest.finish();
+ }
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1248185">Mozilla Bug 1248185</a>
+<body>
+<div style="font: 12px monospace; width: 45ch;">
+<span contenteditable="" id="test">blablablablablablablablablablablablablabla one two three four five</span>
+<div>
+<span>foo</span>
+</div>
+</div>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1250010.html b/editor/libeditor/tests/test_bug1250010.html
new file mode 100644
index 000000000..d1e0154dc
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1250010.html
@@ -0,0 +1,93 @@
+<!DOCTYPE>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1250010
+-->
+<head>
+ <title>Test for Bug 1250010</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<div id="display">
+</div>
+
+<div id="test1" contenteditable><p><b><font color="red">1234567890</font></b></p></div>
+<div id="test2" contenteditable><p><tt>xyz</tt></p><p><tt><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAIAAABvrngfAAAAFklEQVQImWMwjWhCQwxECoW3oCHihAB0LyYv5/oAHwAAAABJRU5ErkJggg=="></tt></p></div>
+
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+function getImageDataURI()
+{
+ return document.getElementsByTagName("img")[0].getAttribute("src");
+}
+
+/** Test for Bug 1250010 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+
+ // First test: Empty paragraph is split correctly.
+ var div = document.getElementById("test1");
+ div.focus();
+ synthesizeMouseAtCenter(div, {});
+
+ var sel = window.getSelection();
+ var selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node");
+ is(selRange.endOffset, 10, "offset should be 10");
+
+ synthesizeKey("VK_RETURN", {});
+ synthesizeKey("VK_RETURN", {});
+ synthesizeKey("b", {});
+ synthesizeKey("VK_UP", {});
+ synthesizeKey("a", {});
+
+ is(div.innerHTML, "<p><b><font color=\"red\">1234567890</font></b></p>" +
+ "<p><b><font color=\"red\">a<br></font></b></p>" +
+ "<p><b><font color=\"red\">b<br></font></b></p>",
+ "unexpected HTML");
+
+ // Second test: Since we modified the code path that splits non-text nodes,
+ // test that this works, if the split node is not empty.
+ div = document.getElementById("test2");
+ div.focus();
+ synthesizeMouseAtCenter(div, {});
+
+ selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node");
+ is(selRange.endOffset, 3, "offset should be 3");
+
+ // Move behind the image and press enter, insert an "A".
+ // That should insert a new empty paragraph with the "A" after what we have.
+ synthesizeKey("VK_RIGHT", {});
+ synthesizeKey("VK_RIGHT", {});
+ synthesizeKey("VK_RETURN", {});
+ synthesizeKey("A", {});
+
+ // The resulting HTML is sadly less than optimal:
+ // A <br> gets inserted after the image and the "A" is followed by an empty <tt></tt>.
+ var newHTML = div.innerHTML;
+ var expectedHTML;
+ // Existing part with additional <br> inserted.
+ expectedHTML = "<p><tt>xyz</tt></p><p><tt><img src=\"" + getImageDataURI() + "\"><br></tt></p>" +
+ // New part caused by pressing enter after the image and typing an "A".
+ "<p><tt>A</tt><br><tt></tt></p>";
+ is(newHTML, expectedHTML, "unexpected HTML");
+
+ // In case the empty tag gets deleted some day, let them know that something improved.
+ expectedHTML = "<p><tt>xyz</tt></p><p><tt><img src=\"" + getImageDataURI() + "\"><br></tt></p>" +
+ "<p><tt>A</tt><br></p>";
+ todo_is(newHTML, expectedHTML, "unexpected HTML");
+
+ SimpleTest.finish();
+
+});
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_bug1257363.html b/editor/libeditor/tests/test_bug1257363.html
new file mode 100644
index 000000000..c1610494f
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1257363.html
@@ -0,0 +1,182 @@
+<!DOCTYPE>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1257363
+-->
+<head>
+ <title>Test for Bug 1257363</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<div id="display">
+</div>
+
+<div id="backspaceCSS" contenteditable><p style="color:red;">12345</p>67</div>
+<div id="backspace" contenteditable><p><font color="red">12345</font></p>67</div>
+<div id="deleteCSS" contenteditable><p style="color:red;">x</p></div>
+<div id="delete" contenteditable><p><font color="red">y</font></p></div>
+
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+/** Test for Bug 1257363 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+
+ // ***** Backspace test *****
+ var div = document.getElementById("backspaceCSS");
+ div.focus();
+ synthesizeMouse(div, 100, 2, {}); /* click behind and down */
+
+ var sel = window.getSelection();
+ var selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node");
+ is(selRange.endOffset, 5, "offset should be 5");
+
+ // Return and backspace should take us to where we started.
+ synthesizeKey("VK_RETURN", {});
+ synthesizeKey("VK_BACK_SPACE", {});
+
+ selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node");
+ is(selRange.endOffset, 5, "offset should be 5");
+
+ // Add an "a" to the end of the paragraph.
+ synthesizeKey("a", {});
+
+ // Return and forward delete should take us to the following line.
+ synthesizeKey("VK_RETURN", {});
+ synthesizeKey("VK_DELETE", {});
+
+ // Add a "b" to the start.
+ synthesizeKey("b", {});
+
+ is(div.innerHTML, "<p style=\"color:red;\">12345a</p>b67",
+ "unexpected HTML");
+
+ // Let's repeat the whole thing, but a font tag instead of CSS.
+ // The behaviour is different since the font is carried over.
+ div = document.getElementById("backspace");
+ div.focus();
+ synthesizeMouse(div, 100, 2, {}); /* click behind and down */
+
+ sel = window.getSelection();
+ selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node");
+ is(selRange.endOffset, 5, "offset should be 5");
+
+ // Return and backspace should take us to where we started.
+ synthesizeKey("VK_RETURN", {});
+ synthesizeKey("VK_BACK_SPACE", {});
+
+ selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node");
+ is(selRange.endOffset, 5, "offset should be 5");
+
+ // Add an "a" to the end of the paragraph.
+ synthesizeKey("a", {});
+
+ // Return and forward delete should take us to the following line.
+ synthesizeKey("VK_RETURN", {});
+ synthesizeKey("VK_DELETE", {});
+
+ // Add a "b" to the start.
+ synthesizeKey("b", {});
+
+ // Here we get a somewhat ugly result since the red sticks.
+ is(div.innerHTML, "<p><font color=\"red\">12345a</font></p><font color=\"red\">b</font>67",
+ "unexpected HTML");
+
+ // ***** Delete test *****
+ div = document.getElementById("deleteCSS");
+ div.focus();
+ synthesizeMouse(div, 100, 2, {}); /* click behind and down */
+
+ var sel = window.getSelection();
+ var selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node");
+ is(selRange.endOffset, 1, "offset should be 1");
+
+ // left, enter should create a new empty paragraph before
+ // but leave the selection at the start of the existing paragraph.
+ synthesizeKey("VK_LEFT", {});
+ synthesizeKey("VK_RETURN", {});
+
+ selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "#text", "selection should be at the start of text node");
+ is(selRange.endOffset, 0, "offset should be 0");
+ is(selRange.endContainer.nodeValue, "x", "we should be in the text node with the x");
+
+ // Now moving up into the new empty paragraph.
+ synthesizeKey("VK_UP", {});
+
+ selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "P", "selection should be the new empty paragraph");
+ is(selRange.endOffset, 0, "offset should be 0");
+
+ // Forward delete should now take us to where we started.
+ synthesizeKey("VK_DELETE", {});
+
+ selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "#text", "selection should be at the start of text node");
+ is(selRange.endOffset, 0, "offset should be 0");
+
+ // Add an "a" to the start of the paragraph.
+ synthesizeKey("a", {});
+
+ is(div.innerHTML, "<p style=\"color:red;\">ax</p>",
+ "unexpected HTML");
+
+ // Let's repeat the whole thing, but a font tag instead of CSS.
+ div = document.getElementById("delete");
+ div.focus();
+ synthesizeMouse(div, 100, 2, {}); /* click behind and down */
+
+ var sel = window.getSelection();
+ var selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node");
+ is(selRange.endOffset, 1, "offset should be 1");
+
+ // left, enter should create a new empty paragraph before
+ // but leave the selection at the start of the existing paragraph.
+ synthesizeKey("VK_LEFT", {});
+ synthesizeKey("VK_RETURN", {});
+
+ selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "#text", "selection should be at the start of text node");
+ is(selRange.endOffset, 0, "offset should be 0");
+ is(selRange.endContainer.nodeValue, "y", "we should be in the text node with the y");
+
+ // Now moving up into the new empty paragraph.
+ synthesizeKey("VK_UP", {});
+
+ selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "FONT", "selection should be the font tag");
+ is(selRange.endOffset, 0, "offset should be 0");
+ is(selRange.endContainer.parentNode.nodeName, "P", "the parent of the font should be a paragraph");
+
+ // Forward delete should now take us to where we started.
+ synthesizeKey("VK_DELETE", {});
+
+ selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "#text", "selection should be at the start of text node");
+ is(selRange.endOffset, 0, "offset should be 0");
+
+ // Add an "a" to the start of the paragraph.
+ synthesizeKey("a", {});
+
+ is(div.innerHTML, "<p><font color=\"red\">ay</font></p>",
+ "unexpected HTML");
+
+ SimpleTest.finish();
+
+});
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_bug1258085.html b/editor/libeditor/tests/test_bug1258085.html
new file mode 100644
index 000000000..342f068ee
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1258085.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<title>Test for Bug 1186799</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<div contenteditable></div>
+<script>
+var div = document.querySelector("div");
+
+function reset() {
+ div.innerHTML = "x<br> y";
+ div.focus();
+ synthesizeKey("VK_DOWN", {});
+}
+
+function checks(msg) {
+ is(div.innerHTML, "x<br><br>",
+ msg + ": Should add a second <br> to prevent collapse of first");
+ is(div.childNodes.length, 3, msg + ": No empty text nodes allowed");
+ ok(getSelection().isCollapsed, msg + ": Selection must be collapsed");
+ is(getSelection().focusNode, div, msg + ": Focus must be in div");
+ is(getSelection().focusOffset, 2,
+ msg + ": Focus must be between the two <br>s");
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ // Put selection after the "y" and backspace
+ reset();
+ synthesizeKey("VK_RIGHT", {});
+ synthesizeKey("VK_BACK_SPACE", {});
+ checks("Collapsed backspace");
+
+ // Now do the same with delete
+ reset();
+ synthesizeKey("VK_DELETE", {});
+ checks("Collapsed delete");
+
+ // Forward selection
+ reset();
+ synthesizeKey("VK_RIGHT", {shiftKey: true});
+ synthesizeKey("VK_BACK_SPACE", {});
+ checks("Forward-selected backspace");
+
+ // Backward selection
+ reset();
+ synthesizeKey("VK_RIGHT", {});
+ synthesizeKey("VK_LEFT", {shiftKey: true});
+ synthesizeKey("VK_BACK_SPACE", {});
+ checks("Backward-selected backspace");
+
+ // Make sure we're not deleting if the whitespace isn't actually collapsed
+ div.style.whiteSpace = "pre-wrap";
+ reset();
+ synthesizeKey("VK_RIGHT", {});
+ synthesizeKey("VK_RIGHT", {});
+ synthesizeKey("VK_BACK_SPACE", {});
+ if (div.innerHTML, "x<br> ", "pre-wrap: Don't delete uncollapsed space");
+ ok(getSelection().isCollapsed, "pre-wrap: Selection must be collapsed");
+ is(getSelection().focusNode, div.lastChild,
+ "pre-wrap: Focus must be in final text node");
+ is(getSelection().focusOffset, 1, "pre-wrap: Focus must be at end of node");
+
+ SimpleTest.finish();
+});
+</script>
diff --git a/editor/libeditor/tests/test_bug1268736.html b/editor/libeditor/tests/test_bug1268736.html
new file mode 100644
index 000000000..fb1f341b5
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1268736.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1268736
+-->
+<head>
+ <title>Test for Bug 1268736</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1268736">Mozilla Bug 1268736</a>
+<table id="table" border="1" width="100%">
+ <tbody>
+ <tr>
+ <td>a</td>
+ <td>b</td>
+ <td>c</td>
+ </tr>
+ <tr>
+ <td>d</td>
+ <td id="cell_readonly">e</td>
+ <td contenteditable="true" id="cell_writable">f</td>
+ </tr>
+ </tbody>
+</table>
+
+<script type="application/javascript">
+
+/**
+ * Test for Bug 1268736
+ *
+ * Tests for editing a table cell's contents when the table cell is or isn't a child of a contenteditable node.
+ *
+ */
+
+function getEditor() {
+ const Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIEditingSession);
+ return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsITableEditor);
+}
+
+var table = document.getElementById("table");
+var tableHTML = table.innerHTML;
+var editor = getEditor();
+
+var cell = document.getElementById("cell_readonly");
+cell.focus();
+editor.deleteTableCellContents();
+is(table.innerHTML == tableHTML, true, "editor should not modify non-editable table cell" );
+
+cell = document.getElementById("cell_writable");
+cell.focus();
+editor.deleteTableCellContents();
+is(cell.innerHTML == "<br>", true, "editor can modify editable table cells" );
+
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1270235.html b/editor/libeditor/tests/test_bug1270235.html
new file mode 100644
index 000000000..da7fd4e7a
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1270235.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1270235
+-->
+<head>
+ <title>Test for Bug 1270235</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1270235">Mozilla Bug 1270235</a>
+<p id="display"></p>
+<div id="content" style="display: none;"></div>
+
+<div id="edit1" contenteditable="true"><p>AB</p></div>
+<script type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(()=>{
+ let element = document.getElementById('edit1');
+ element.focus();
+ let textNode = element.firstChild.firstChild;
+ let node = textNode.splitText(0);
+ node.parentNode.removeChild(node);
+
+ ok(!node.parentNode, 'parent must be null');
+
+ let newRange = document.createRange();
+ newRange.setStart(node, 0);
+ newRange.setEnd(node, 0);
+ let selection = document.getSelection();
+ selection.removeAllRanges();
+ selection.addRange(newRange);
+
+ ok(selection.isCollapsed, 'isCollapsed must be true');
+
+ // Don't crash by user input
+ synthesizeKey("X", {});
+
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1310912.html b/editor/libeditor/tests/test_bug1310912.html
new file mode 100644
index 000000000..d73366a63
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1310912.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1310912
+-->
+<html>
+<head>
+ <title>Test for Bug 1310912</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1310912">Mozilla Bug 1310912</a>
+<p id="display"></p>
+<div id="content" style="display: none;">
+
+</div>
+
+<div id="editable1" contenteditable="true">ABC</div>
+<pre id="test">
+
+<script class="testbody" type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ let elm = document.getElementById("editable1");
+
+ elm.focus();
+ let sel = window.getSelection();
+ sel.collapse(elm.childNodes[0], elm.textContent.length);
+
+ synthesizeCompositionChange({
+ composition: {
+ string: "DEF",
+ clauses: [
+ { length: 3, attr: COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ caret: { start: 3, length: 0 }
+ });
+ ok(elm.textContent == "ABCDEF", "composing text should be set");
+
+ window.getSelection().getRangeAt(0).insertNode(document.createTextNode(""));
+ synthesizeCompositionChange({
+ composition: {
+ string: "GHI",
+ clauses: [
+ { length: 3, attr: COMPOSITION_ATTR_CONVERTED_CLAUSE }
+ ]
+ },
+ caret: { start: 0, length: 0 }
+ });
+ ok(elm.textContent == "ABCGHI", "composing text should be replaced");
+
+ window.getSelection().getRangeAt(0).insertNode(document.createTextNode(""));
+ synthesizeCompositionChange({
+ composition: {
+ string: "JKL",
+ clauses: [
+ { length: 3, attr: COMPOSITION_ATTR_CONVERTED_CLAUSE }
+ ]
+ },
+ caret: { start: 0, length: 0 }
+ });
+ ok(elm.textContent == "ABCJKL", "composing text should be replaced");
+
+ window.getSelection().getRangeAt(0).insertNode(document.createTextNode(""));
+ synthesizeCompositionChange({
+ composition: {
+ string: "MNO",
+ clauses: [
+ { length: 3, attr: COMPOSITION_ATTR_CONVERTED_CLAUSE }
+ ]
+ },
+ caret: { start: 1, length: 0 }
+ });
+ ok(elm.textContent == "ABCMNO", "composing text should be replaced");
+
+ window.getSelection().getRangeAt(0).insertNode(document.createTextNode(""));
+ synthesizeComposition({ type: "compositioncommitasis" });
+ ok(elm.textContent == "ABCMNO", "composing text should be committed");
+
+ synthesizeKey("Z", { accelKey: true });
+ ok(elm.textContent == "ABC", "text should be undoed");
+
+ synthesizeKey("Z", { accelKey: true, shiftKey: true });
+ ok(elm.textContent == "ABCMNO", "text should be redoed");
+
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1314790.html b/editor/libeditor/tests/test_bug1314790.html
new file mode 100644
index 000000000..ff1487244
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1314790.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1314790
+-->
+<html>
+<head>
+ <title>Test for Bug 1314790</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1314790">Mozilla Bug 1314790</a>
+<p id="display"></p>
+<div id="content" style="display: none;">
+
+</div>
+
+<div contenteditable="true" id="contenteditable1"><p>pen pineapple</p></div>
+<pre id="test">
+
+<script class="testbody" type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ let elm = document.getElementById("contenteditable1");
+ elm.focus();
+ window.getSelection().collapse(elm.childNodes[0], 0);
+
+ SpecialPowers.doCommand(window, "cmd_wordNext");
+ SpecialPowers.doCommand(window, "cmd_wordNext");
+
+ synthesizeKey("VK_RETURN", {});
+ synthesizeKey("a", {});
+ synthesizeKey("p", {});
+ synthesizeKey("p", {});
+ synthesizeKey("l", {});
+ synthesizeKey("e", {});
+ synthesizeKey(" ", {});
+ synthesizeKey("p", {});
+ synthesizeKey("e", {});
+ synthesizeKey("n", {});
+
+ is(elm.childNodes[0].textContent, "pen pineapple",
+ "'pen pineapple' is first elment");
+ is(elm.childNodes[1].textContent, "apple pen",
+ "'apple pen' is second elment");
+
+ SpecialPowers.doCommand(window, "cmd_deleteWordBackward");
+ SpecialPowers.doCommand(window, "cmd_deleteWordBackward");
+ is(elm.childNodes[0].textContent, "pen pineapple",
+ "'pen pineapple' is first elment");
+
+ SpecialPowers.doCommand(window, "cmd_deleteWordBackward");
+ is(elm.childNodes[0].textContent, "pen pineapple",
+ "'pen pineapple' is first elment");
+
+ SpecialPowers.doCommand(window, "cmd_deleteWordBackward");
+ is(elm.childNodes[0].textContent, "pen ", "'pen ' is first elment");
+
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1315065.html b/editor/libeditor/tests/test_bug1315065.html
new file mode 100644
index 000000000..16f0de4e3
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1315065.html
@@ -0,0 +1,145 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1315065
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1315065</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1315065">Mozilla Bug 1315065</a>
+<div contenteditable><p>abc<br></p></div>
+<script type="application/javascript">
+/** Test for Bug 1315065 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(()=>{
+ var editor = document.getElementsByTagName("div")[0];
+ function initForBackspace(aSelectionCollapsedTo /* = 0 ~ 3 */) {
+ editor.innerHTML = "<p id='p'>abc<br></p>";
+ var p = document.getElementById("p");
+ // FYI: We cannot inserting empty text nodes as expected with
+ // Node.appendChild() nor Node.insertBefore(). Therefore, let's use
+ // Range.insertNode() like actual web apps.
+ var selection = window.getSelection();
+ selection.collapse(p, 1);
+ var range = selection.getRangeAt(0);
+ var emptyTextNode3 = document.createTextNode("");
+ range.insertNode(emptyTextNode3);
+ var emptyTextNode2 = document.createTextNode("");
+ range.insertNode(emptyTextNode2);
+ var emptyTextNode1 = document.createTextNode("");
+ range.insertNode(emptyTextNode1);
+ is(p.childNodes.length, 5, "Failed to initialize the editor");
+ is(p.childNodes.item(1), emptyTextNode1, "1st text node should be emptyTextNode1");
+ is(p.childNodes.item(2), emptyTextNode2, "2nd text node should be emptyTextNode2");
+ is(p.childNodes.item(3), emptyTextNode3, "3rd text node should be emptyTextNode3");
+ switch (aSelectionCollapsedTo) {
+ case 0:
+ selection.collapse(p.firstChild, 3); // next to 'c'
+ break;
+ case 1:
+ selection.collapse(emptyTextNode1, 0);
+ break;
+ case 2:
+ selection.collapse(emptyTextNode2, 0);
+ break;
+ case 3:
+ selection.collapse(emptyTextNode3, 0);
+ break;
+ default:
+ ok(false, "aSelectionCollapsedTo is illegal value");
+ }
+ }
+
+ for (var i = 0; i < 4; i++) {
+ const kDescription = i == 0 ? "Backspace from immediately after the last character" :
+ "Backspace from " + i + "th empty text node";
+ editor.focus();
+ initForBackspace(i);
+ synthesizeKey("KEY_Backspace", { code: "Backspace" });
+ var p = document.getElementById("p");
+ ok(p, kDescription + ": <p> element shouldn't be removed by Backspace key press");
+ is(p.tagName.toLowerCase(), "p", kDescription + ": <p> element shouldn't be removed by Backspace key press");
+ // When Backspace key is pressed even in empty text nodes, Gecko should not remove empty text nodes for now
+ // because we should keep our traditional behavior (same as Edge) for backward compatibility as far as possible.
+ // In this case, Chromium removes all empty text nodes, but Edge doesn't remove any empty text nodes.
+ is(p.childNodes.length, 5, kDescription + ": <p> should have 5 children after pressing Backspace key");
+ is(p.childNodes.item(0).textContent, "ab", kDescription + ": 'c' should be removed by pressing Backspace key");
+ is(p.childNodes.item(1).textContent, "", kDescription + ": 1st empty text node should not be removed by pressing Backspace key");
+ is(p.childNodes.item(2).textContent, "", kDescription + ": 2nd empty text node should not be removed by pressing Backspace key");
+ is(p.childNodes.item(3).textContent, "", kDescription + ": 3rd empty text node should not be removed by pressing Backspace key");
+ editor.blur();
+ }
+
+ function initForDelete(aSelectionCollapsedTo /* = 0 ~ 3 */) {
+ editor.innerHTML = "<p id='p'>abc<br></p>";
+ var p = document.getElementById("p");
+ // FYI: We cannot inserting empty text nodes as expected with
+ // Node.appendChild() nor Node.insertBefore(). Therefore, let's use
+ // Range.insertNode() like actual web apps.
+ var selection = window.getSelection();
+ selection.collapse(p, 0);
+ var range = selection.getRangeAt(0);
+ var emptyTextNode1 = document.createTextNode("");
+ range.insertNode(emptyTextNode1);
+ var emptyTextNode2 = document.createTextNode("");
+ range.insertNode(emptyTextNode2);
+ var emptyTextNode3 = document.createTextNode("");
+ range.insertNode(emptyTextNode3);
+ is(p.childNodes.length, 5, "Failed to initialize the editor");
+ is(p.childNodes.item(0), emptyTextNode3, "1st text node should be emptyTextNode3");
+ is(p.childNodes.item(1), emptyTextNode2, "2nd text node should be emptyTextNode2");
+ is(p.childNodes.item(2), emptyTextNode1, "3rd text node should be emptyTextNode1");
+ switch (aSelectionCollapsedTo) {
+ case 0:
+ selection.collapse(p.childNodes.item(3), 0); // next to 'a'
+ break;
+ case 1:
+ selection.collapse(emptyTextNode1, 0);
+ break;
+ case 2:
+ selection.collapse(emptyTextNode2, 0);
+ break;
+ case 3:
+ selection.collapse(emptyTextNode3, 0);
+ break;
+ default:
+ ok(false, "aSelectionCollapsedTo is illegal value");
+ }
+ }
+
+ for (var i = 0; i < 4; i++) {
+ const kDescription = i == 0 ? "Delete from immediately before the first character" :
+ "Delete from " + i + "th empty text node";
+ editor.focus();
+ initForDelete(i);
+ synthesizeKey("KEY_Delete", { code: "Delete" });
+ var p = document.getElementById("p");
+ ok(p, kDescription + ": <p> element shouldn't be removed by Delete key press");
+ is(p.tagName.toLowerCase(), "p", kDescription + ": <p> element shouldn't be removed by Delete key press");
+ if (i == 0) {
+ // If Delete key is pressed in non-empty text node, only the text node should be modified.
+ // This is same behavior as Chromium, but different from Edge. Edge removes all empty text nodes in this case.
+ is(p.childNodes.length, 5, kDescription + ": <p> should have only 2 children after pressing Delete key (empty text nodes should be removed");
+ is(p.childNodes.item(0).textContent, "", kDescription + ": 1st empty text node should not be removed by pressing Delete key");
+ is(p.childNodes.item(1).textContent, "", kDescription + ": 2nd empty text node should not be removed by pressing Delete key");
+ is(p.childNodes.item(2).textContent, "", kDescription + ": 3rd empty text node should not be removed by pressing Delete key");
+ is(p.childNodes.item(3).textContent, "bc", kDescription + ": 'a' should be removed by pressing Delete key");
+ } else {
+ // If Delete key is pressed in an empty text node, it and following empty text nodes should be removed and the non-empty text node should be modified.
+ // This is same behavior as Chromium, but different from Edge. Edge removes all empty text nodes in this case.
+ var expectedEmptyTextNodes = 3 - i;
+ is(p.childNodes.length, expectedEmptyTextNodes + 2, kDescription + ": <p> should have only " + i + " children after pressing Delete key (" + i + " empty text nodes should be removed");
+ is(p.childNodes.item(expectedEmptyTextNodes).textContent, "bc", kDescription + ": empty text nodes and 'a' should be removed by pressing Delete key");
+ }
+ editor.blur();
+ }
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1328023.html b/editor/libeditor/tests/test_bug1328023.html
new file mode 100644
index 000000000..1b7fb7bf5
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1328023.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1328023
+-->
+<html>
+<head>
+ <title>Test for Bug 1328023</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1328023">Mozilla Bug 1328023</a>
+<p id="display"></p>
+<div id="content" style="display: none;">
+
+</div>
+
+<input type="text" id="input1"/>
+<pre id="test">
+
+<script class="testbody" type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ let elm = document.getElementById("input1");
+
+ elm.focus();
+ synthesizeKey("A", {});
+ synthesizeKey("B", {});
+ is(elm.value, "AB", "AB is input.value now");
+
+ synthesizeKey("VK_BACK_SPACE", {});
+ is(elm.value, "A", "A is input.value now");
+
+ synthesizeKey("Z", { accelKey: true });
+ is(elm.value, "AB", "AB is input.value now");
+
+ synthesizeKey("C", {});
+ is(elm.value, "ABC", "ABC is input.value now");
+
+ synthesizeKey("VK_BACK_SPACE", {});
+ synthesizeKey("VK_BACK_SPACE", {});
+ synthesizeKey("VK_BACK_SPACE", {});
+
+ synthesizeKey("A", {});
+ synthesizeKey("B", {});
+ synthesizeKey("C", {});
+ is(elm.value, "ABC", "ABC is input.value now");
+
+ synthesizeKey("Z", { accelKey: true });
+ is(elm.value, "", "'' is input.value now");
+
+ synthesizeKey("Z", { accelKey: true, shiftKey: true });
+ is(elm.value, "ABC", "ABC is input.value now");
+
+ synthesizeKey("D", {});
+ is(elm.value, "ABCD", "ABCD is input.value now");
+
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1330796.html b/editor/libeditor/tests/test_bug1330796.html
new file mode 100644
index 000000000..f8af02087
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1330796.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1330796
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 772796</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style> .pre { white-space: pre } </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=772796">Mozilla Bug 1330796</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<div id="editable" contenteditable></div>
+
+<pre id="test">
+
+<script type="application/javascript">
+// We want to test what happens when the user splits a mail cite by clicking
+// at the start, the middle and the end of the cite and hitting the enter key.
+// Mail cites are spans, and since bug 1288911 they are displayed as blocks.
+// The _moz_quote attribute is used to give the cite a blue color via CSS.
+// As an internal attribute, it's not returned from the innerHTML.
+// To the user the tests look like:
+// > mailcite
+// This text is 10 characters long, so we position at 0, 5 and 10.
+// Althought since bug 1288911 those cites are displayed as block,
+// the tests are repeated also for inline display.
+// Each entry of the 'tests' array has the original HTML, the offset to click
+// at and the expected result HTML.
+var tests = [
+ // With style="display: block;".
+ [ "<span _moz_quote=true style=\"display: block;\">&gt; mailcite<br></span>", 0,
+ "x<br><span style=\"display: block;\">&gt; mailcite<br></span>" ],
+ [ "<span _moz_quote=true style=\"display: block;\">&gt; mailcite<br></span>", 5,
+ "<span style=\"display: block;\">&gt; mai<br></span>x<br><span style=\"display: block;\">lcite<br></span>"],
+ [ "<span _moz_quote=true style=\"display: block;\">&gt; mailcite<br></span>", 10,
+ "<span style=\"display: block;\">&gt; mailcite<br></span>x<br>" ],
+ // No <br> at the end to simulate prior deletion to the end of the quote.
+ [ "<span _moz_quote=true style=\"display: block;\">&gt; mailcite</span>", 10,
+ "<span style=\"display: block;\">&gt; mailcite<br></span>x<br>" ],
+
+ // Without style="display: block;".
+ [ "<span _moz_quote=true>&gt; mailcite<br></span>", 0,
+ "x<br><span>&gt; mailcite<br></span>" ],
+ [ "<span _moz_quote=true>&gt; mailcite<br></span>", 5,
+ "<span>&gt; mai</span><br>x<br><span>lcite<br></span>" ],
+ [ "<span _moz_quote=true>&gt; mailcite<br></span>", 10,
+ "<span>&gt; mailcite<br></span>x<br>" ],
+ // No <br> at the end to simulate prior deletion to the end of the quote.
+ [ "<span _moz_quote=true>&gt; mailcite</span>", 10,
+ "<span>&gt; mailcite</span><br>x<br>" ]
+];
+
+/** Test for Bug 1330796 **/
+
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(function() {
+
+ var sel = window.getSelection();
+ var theEdit = document.getElementById("editable");
+ makeMailEditor();
+
+ for (i = 0; i < tests.length; i++) {
+ theEdit.innerHTML = tests[i][0];
+ theEdit.focus();
+ var theText = theEdit.firstChild.firstChild;
+ // Position set at the beginning , middle and end of the text.
+ sel.collapse(theText, tests[i][1]);
+
+ synthesizeKey("KEY_Enter", { code: "Enter" });
+ synthesizeKey("x", { code: "KeyX" });
+ is(theEdit.innerHTML, tests[i][2], "unexpected HTML for test " + i.toString());
+ }
+
+ SimpleTest.finish();
+
+});
+
+function makeMailEditor() {
+ var Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIEditingSession);
+ var editor = editingSession.getEditorForWindow(window);
+ editor.flags |= Ci.nsIPlaintextEditor.eEditorMailMask;
+}
+</script>
+
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1332876.html b/editor/libeditor/tests/test_bug1332876.html
new file mode 100644
index 000000000..76dfa0fc7
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1332876.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=795418
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1332876</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1332876">Mozilla Bug 1332876</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<iframe src="data:text/html,<html><body><span>Edit me!</span>"></iframe>
+
+<pre id="test">
+
+<script type="application/javascript">
+
+/** Test for Bug 1332876 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var iframe = document.querySelector("iframe");
+ iframe.contentDocument.designMode='on';
+
+ iframe.contentWindow.addEventListener('keypress', function() {
+ iframe.style.display='none';
+ document.body.offsetHeight;
+ ok(true, "did not crash");
+ SimpleTest.finish();
+ });
+
+ iframe.contentWindow.addEventListener('click', function() {
+ synthesizeKey('a', {}, iframe.contentWindow);
+ });
+
+ synthesizeMouse(iframe,20,20,{})
+});
+
+</script>
+
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug200416.html b/editor/libeditor/tests/test_bug200416.html
new file mode 100644
index 000000000..9fb656425
--- /dev/null
+++ b/editor/libeditor/tests/test_bug200416.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=200416
+-->
+<title>Test for Bug 200416</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=200416">Mozilla Bug 200416</a>
+<div contenteditable><span>foo<p>bar</p></span></div>
+<script>
+getSelection().collapse(document.querySelector("p").firstChild, 0);
+document.execCommand("delete");
+var innerHTML = document.querySelector("div").innerHTML;
+ok(/foo.*bar/.test(innerHTML), "foo needs to still come before bar");
+</script>
diff --git a/editor/libeditor/tests/test_bug289384.html b/editor/libeditor/tests/test_bug289384.html
new file mode 100644
index 000000000..1d55e0c3f
--- /dev/null
+++ b/editor/libeditor/tests/test_bug289384.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=289384
+-->
+<head>
+ <title>Test for Bug 289384</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=289384">Mozilla Bug 289384</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+ var win = window.open("data:text/html,<a href=\"data:text/html,<body contenteditable onload='opener.continueTest(window);'>foo bar</body>\">link</a>", "", "test-289384");
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad);
+ win.document.querySelector("a").click();
+ }, false);
+});
+
+function continueTest(win) {
+ SimpleTest.waitForFocus(function() {
+ var doc = win.document;
+ var sel = win.getSelection();
+ doc.body.focus();
+ sel.collapse(doc.body.firstChild, 3);
+ SimpleTest.executeSoon(function() {
+ synthesizeKey("VK_LEFT", {accelKey: true}, win);
+ ok(sel.isCollapsed, "The selection must be collapsed");
+ is(sel.anchorNode, doc.body.firstChild, "The anchor node should be the body element's text node");
+ is(sel.anchorOffset, 0, "The anchor offset should be 0");
+ win.close();
+ SimpleTest.finish();
+ });
+ }, win);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug290026.html b/editor/libeditor/tests/test_bug290026.html
new file mode 100644
index 000000000..9e7686e72
--- /dev/null
+++ b/editor/libeditor/tests/test_bug290026.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=290026
+-->
+<head>
+ <title>Test for Bug 290026</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=290026">Mozilla Bug 290026</a>
+<p id="display"></p>
+<div id="editor" contenteditable></div>
+
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 290026 **/
+SimpleTest.waitForExplicitFinish();
+
+var editor = document.getElementById("editor");
+editor.innerHTML = '<p></p><ul><li>Item 1</li><li>Item 2</li></ul><p></p>';
+editor.focus();
+
+addLoadEvent(function() {
+ document.execCommand("stylewithcss", false, "true");
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+ var lis = document.getElementsByTagName("li");
+ var range = document.createRange();
+ range.setStart(lis[0], 0);
+ range.setEnd(lis[1], lis[1].childNodes.length);
+ sel.addRange(range);
+ document.execCommand("indent", false, false);
+ var oneindent = '<p></p><ul style="margin-left: 40px;"><li>Item 1</li><li>Item 2</li></ul><p></p>';
+ is(editor.innerHTML, oneindent, "a once indented bulleted list");
+ document.execCommand("indent", false, false);
+ var twoindent = '<p></p><ul style="margin-left: 80px;"><li>Item 1</li><li>Item 2</li></ul><p></p>';
+ is(editor.innerHTML, twoindent, "a twice indented bulleted list");
+ document.execCommand("outdent", false, false);
+ is(editor.innerHTML, oneindent, "outdenting a twice indented bulleted list");
+
+ // done
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug291780.html b/editor/libeditor/tests/test_bug291780.html
new file mode 100644
index 000000000..93f63af61
--- /dev/null
+++ b/editor/libeditor/tests/test_bug291780.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=291780
+-->
+<head>
+ <title>Test for Bug 291780</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=291780">Mozilla Bug 291780</a>
+<p id="display"></p>
+<div id="editor" contenteditable></div>
+
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 291780 **/
+SimpleTest.waitForExplicitFinish();
+
+var original = '<ul style="margin-left: 40px;"><li>Item 1</li><li>Item 2</li><li>Item 3</li><li>Item 4</li></ul>';
+var editor = document.getElementById("editor");
+editor.innerHTML = original;
+editor.focus();
+
+addLoadEvent(function() {
+
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+ var lis = document.getElementsByTagName("li");
+ var range = document.createRange();
+ range.setStart(lis[1], 0);
+ range.setEnd(lis[2], lis[2].childNodes.length);
+ sel.addRange(range);
+ document.execCommand("indent", false, false);
+ var expected = '<ul style="margin-left: 40px;"><li>Item 1</li><ul><li>Item 2</li><li>Item 3</li></ul><li>Item 4</li></ul>';
+ is(editor.innerHTML, expected, "indenting part of an already indented bulleted list");
+ document.execCommand("outdent", false, false);
+ is(editor.innerHTML, original, "outdenting the partially indented part of an already indented bulleted list");
+
+ // done
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug309731.html b/editor/libeditor/tests/test_bug309731.html
new file mode 100644
index 000000000..85406905c
--- /dev/null
+++ b/editor/libeditor/tests/test_bug309731.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=309731
+-->
+<head>
+ <title>Test for Bug 309731</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=309731">Mozilla Bug 309731</a>
+<p id="display"></p>
+
+<div id="content">
+ <div id="input" contentEditable="true"></div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+/** Test for Bug 309731 **/
+
+function selectNode(node) {
+ getSelection().selectAllChildren(node);
+}
+
+function selectInNode(node) {
+ getSelection().collapse(node, 0);
+}
+
+function doTest() {
+ var input = document.getElementById("input");
+
+ is(input.textContent, "", "Input node starts empty");
+
+ selectInNode(input);
+ ok(document.execCommand("inserthtml", false, ""), "execCommand should return true");
+ is(input.textContent, "", "empty inserthtml with empty selection shouldn't change contents");
+
+ selectInNode(input);
+ ok(document.execCommand("inserthtml", false, "foo"), "execCommand should return true");
+ is(input.textContent, "foo", "'foo'inserthtml with empty selection should add foo to contents");
+
+ selectNode(input);
+ ok(document.execCommand("inserthtml", false, "bar"), "execCommand should return true");
+ is(input.textContent, "bar", "'bar' inserthtml with complete selection should replace contents with bar");
+
+ selectNode(input);
+ ok(document.execCommand("inserthtml", false, ""), "execCommand should return true");
+ is(input.textContent, "", "empty inserthtml with complete selection should delete everything");
+}
+
+doTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug316447.html b/editor/libeditor/tests/test_bug316447.html
new file mode 100644
index 000000000..76d123815
--- /dev/null
+++ b/editor/libeditor/tests/test_bug316447.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=316447
+-->
+<title>Test for Bug 316447</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=316447">Mozilla Bug 316447</a>
+<div contenteditable><br></div>
+<script>
+/** Test for Bug 316447 **/
+
+getSelection().selectAllChildren(document.querySelector("div"));
+document.execCommand("inserthorizontalrule");
+is(document.querySelector("div").innerHTML, "<hr>", "Wrong innerHTML");
+</script>
diff --git a/editor/libeditor/tests/test_bug318065.html b/editor/libeditor/tests/test_bug318065.html
new file mode 100644
index 000000000..541653ab1
--- /dev/null
+++ b/editor/libeditor/tests/test_bug318065.html
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=318065
+-->
+
+<head>
+ <title>Test for Bug 318065</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=318065">Mozilla Bug 318065</a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+
+ <pre id="test">
+ <script type="application/javascript">
+
+ /** Test for Bug 318065 **/
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(function() {
+ var expectedValues = ["A", "", "A", "", "A", "", "A"];
+ var messages = ["Initial text inserted",
+ "Initial text deleted",
+ "Undo of deletion",
+ "Redo of deletion",
+ "Initial text typed",
+ "Undo of typing",
+ "Redo of typing"];
+ var step = 0;
+
+ function onInput() {
+ is(this.value, expectedValues[step], messages[step]);
+ step++;
+ if (step == expectedValues.length) {
+ this.removeEventListener("input", onInput, false);
+ SimpleTest.finish();
+ }
+ }
+
+ var input = document.getElementById("t1");
+ input.addEventListener("input", onInput, false);
+ var input2 = document.getElementById("t2");
+ input2.addEventListener("input", onInput, false);
+
+ input.focus();
+
+ // Tests 0 + 1: Input letter and delete it again
+ synthesizeKey("A", {});
+ synthesizeKey("VK_BACK_SPACE", {});
+
+ // Test 2: Undo deletion. Value of input should be "A"
+ synthesizeKey("Z", {accelKey: true});
+
+ // Test 3: Redo deletion. Value of input should be ""
+ synthesizeKey("Z", {accelKey: true, shiftKey: true});
+
+ input2.focus();
+
+ // Test 4: Input letter
+ synthesizeKey("A", {});
+
+ // Test 5: Undo typing. Value of input should be ""
+ synthesizeKey("Z", {accelKey: true});
+
+ // Test 6: Redo typing. Value of input should be "A"
+ synthesizeKey("Z", {accelKey: true, shiftKey: true});
+ });
+ </script>
+ </pre>
+
+ <input type="text" value="" id="t1" />
+ <input type="text" value="" id="t2" />
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug332636.html b/editor/libeditor/tests/test_bug332636.html
new file mode 100644
index 000000000..5df386ac4
--- /dev/null
+++ b/editor/libeditor/tests/test_bug332636.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=332636
+-->
+<head>
+ <title>Test for Bug 332636</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=332636">Mozilla Bug 332636</a>
+<p id="display"></p>
+<div id="content">
+ <div id="edit0" contenteditable="true">axb</div><!-- reference: plane 0 base character -->
+ <div id="edit1" contenteditable="true">a&#x0308;b</div><!-- reference: plane 0 diacritic -->
+ <div id="edit2" contenteditable="true">a&#x10400;b</div><!-- plane 1 base character -->
+ <div id="edit3" contenteditable="true">a&#x10a0f;b</div><!-- plane 1 diacritic -->
+
+ <div id="edit0b" contenteditable="true">axb</div><!-- reference: plane 0 base character -->
+ <div id="edit1b" contenteditable="true">a&#x0308;b</div><!-- reference: plane 0 diacritic -->
+ <div id="edit2b" contenteditable="true">a&#x10400;b</div><!-- plane 1 base character -->
+ <div id="edit3b" contenteditable="true">a&#x10a0f;b</div><!-- plane 1 diacritic -->
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 332636 **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+
+function test(edit) {
+ edit.focus();
+ var sel = window.getSelection();
+ sel.collapse(edit.childNodes[0], edit.textContent.length - 1);
+ synthesizeKey("VK_BACK_SPACE", {});
+ is(edit.textContent, "ab", "The backspace key should delete the UTF-16 surrogate pair correctly");
+}
+
+function testWithMove(edit, offset) {
+ edit.focus();
+ var sel = window.getSelection();
+ sel.collapse(edit.childNodes[0], 0);
+ var i;
+ for (i = 0; i < offset; ++i) {
+ synthesizeKey("VK_RIGHT", {});
+ synthesizeKey("VK_LEFT", {});
+ synthesizeKey("VK_RIGHT", {});
+ }
+ synthesizeKey("VK_BACK_SPACE", {});
+ is(edit.textContent, "ab", "The backspace key should delete the UTF-16 surrogate pair correctly");
+}
+
+function runTest() {
+ /* test backspace-deletion of the middle character */
+ test(document.getElementById("edit0"));
+ test(document.getElementById("edit1"));
+ test(document.getElementById("edit2"));
+ test(document.getElementById("edit3"));
+
+ /* extra tests with the use of RIGHT and LEFT to get to the right place */
+ testWithMove(document.getElementById("edit0b"), 2);
+ testWithMove(document.getElementById("edit1b"), 1);
+ testWithMove(document.getElementById("edit2b"), 2);
+ testWithMove(document.getElementById("edit3b"), 1);
+
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug332636.html^headers^ b/editor/libeditor/tests/test_bug332636.html^headers^
new file mode 100644
index 000000000..e853d6cee
--- /dev/null
+++ b/editor/libeditor/tests/test_bug332636.html^headers^
@@ -0,0 +1 @@
+Content-Type: text/html; charset=UTF-8
diff --git a/editor/libeditor/tests/test_bug366682.html b/editor/libeditor/tests/test_bug366682.html
new file mode 100644
index 000000000..bac618941
--- /dev/null
+++ b/editor/libeditor/tests/test_bug366682.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=366682
+-->
+<head>
+ <title>Test for Bug 366682</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="spellcheck.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=366682">Mozilla Bug 366682</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 366682 **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+
+var gMisspeltWords;
+
+function getEdit() {
+ return document.getElementById('edit');
+}
+
+function editDoc() {
+ return getEdit().contentDocument;
+}
+
+function getEditor() {
+ var Ci = SpecialPowers.Ci;
+ var win = editDoc().defaultView;
+ var editingSession = SpecialPowers.wrap(win)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIEditingSession);
+ return editingSession.getEditorForWindow(win);
+}
+
+function runTest() {
+ editDoc().body.innerHTML = "<div>errror and an other errror</div>";
+ gMisspeltWords = ["errror", "errror"];
+ editDoc().designMode = "on";
+
+ SpecialPowers.Cu.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm")
+ .onSpellCheck(editDoc().documentElement, evalTest);
+}
+
+function evalTest() {
+ ok(isSpellingCheckOk(getEditor(), gMisspeltWords),
+ "All misspellings accounted for.");
+ SimpleTest.finish();
+}
+</script>
+</pre>
+
+<iframe id="edit" width="200" height="100" src="about:blank"></iframe>
+
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug372345.html b/editor/libeditor/tests/test_bug372345.html
new file mode 100644
index 000000000..e9b1ac7a9
--- /dev/null
+++ b/editor/libeditor/tests/test_bug372345.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=372345
+-->
+<head>
+ <title>Test for Bug 372345</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=372345">Mozilla Bug 372345</a>
+<p id="display"></p>
+<div id="content">
+ <iframe src="data:text/html,<body>"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 372345 **/
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var iframe = document.querySelector("iframe");
+ var doc = iframe.contentDocument;
+ var content = doc.body;
+ var link = content.querySelector("a");
+ function testCursor(post) {
+ setTimeout(function() {
+ var link = document.createElement("a");
+ link.href = "http://mozilla.org/";
+ link.textContent = "link";
+ link.style.cursor = "pointer";
+ content.appendChild(link);
+ is(iframe.contentWindow.getComputedStyle(link, null).cursor, "pointer", "Make sure that the cursor is set to pointer");
+ setTimeout(post, 0);
+ }, 0);
+ }
+ testCursor(function() {
+ doc.designMode = "on";
+ testCursor(function() {
+ doc.designMode = "off";
+ testCursor(function() {
+ content.setAttribute("contenteditable", "true");
+ testCursor(function() {
+ content.removeAttribute("contenteditable");
+ testCursor(function() {
+ SimpleTest.finish();
+ });
+ });
+ });
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug404320.html b/editor/libeditor/tests/test_bug404320.html
new file mode 100644
index 000000000..b8249a557
--- /dev/null
+++ b/editor/libeditor/tests/test_bug404320.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=404320
+-->
+<head>
+ <title>Test for Bug 404320</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=404320">Mozilla Bug 404320</a>
+<p id="display"></p>
+<div id="content">
+ <iframe id="testIframe"></iframe>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 404320 **/
+
+SimpleTest.waitForExplicitFinish();
+
+function runTests() {
+ var win = document.getElementById("testIframe").contentWindow;
+ var doc = document.getElementById("testIframe").contentDocument;
+
+ function testFormatBlock(tag, withAngleBrackets, shouldSucceed)
+ {
+ win.getSelection().selectAllChildren(doc.body.firstChild);
+ doc.execCommand("FormatBlock", false,
+ withAngleBrackets ? tag : "<" + tag + ">");
+ var resultNode;
+ if (shouldSucceed && (tag == "dd" || tag == "dt")) {
+ is(doc.body.firstChild.tagName, "DL", "tag was changed");
+ resultNode = doc.body.firstChild.firstChild;
+ }
+ else {
+ resultNode = doc.body.firstChild;
+ }
+
+ is(resultNode.tagName, shouldSucceed ? tag.toUpperCase() : "P", "tag was changed");
+ }
+
+ function formatBlockTests(tags, shouldSucceed)
+ {
+ var html = "<p>Content</p>";
+ for (var i = 0; i < tags.length; ++i) {
+ var tag = tags[i];
+ var resultTag = tag.toUpperCase();
+
+ doc.body.innerHTML = html;
+ testFormatBlock(tag, false, shouldSucceed);
+
+ doc.body.innerHTML = html;
+ testFormatBlock(tag, true, shouldSucceed);
+ }
+ }
+
+ doc.designMode = "on";
+
+ var goodTags = [ "address",
+ "blockquote",
+ "dd",
+ "div",
+ "dl",
+ "dt",
+ "h1",
+ "h2",
+ "h3",
+ "h4",
+ "h5",
+ "h6",
+ "p",
+ "pre" ];
+ var badTags = [ "b",
+ "i",
+ "span",
+ "foo" ];
+
+ formatBlockTests(goodTags, true);
+ formatBlockTests(badTags, false);
+ SimpleTest.finish();
+}
+
+addLoadEvent(runTests);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug408231.html b/editor/libeditor/tests/test_bug408231.html
new file mode 100644
index 000000000..d365bfa09
--- /dev/null
+++ b/editor/libeditor/tests/test_bug408231.html
@@ -0,0 +1,250 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=408231
+-->
+<head>
+ <title>Test for Bug 408231</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=408231">Mozilla Bug 408231</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 408231 **/
+
+ var commandEnabledResults = [
+ ["contentReadOnly", "true"],
+ ["copy", "false"],
+ ["createlink", "true"],
+ ["cut", "false"],
+ ["decreasefontsize", "true"],
+ ["delete", "true"],
+ ["fontname", "true"],
+ ["fontsize", "true"],
+ ["formatblock", "true"],
+ ["heading", "true"],
+ ["hilitecolor", "true"],
+ ["increasefontsize", "true"],
+ ["indent", "true"],
+ ["inserthorizontalrule", "true"],
+ ["inserthtml", "true"],
+ ["insertimage", "true"],
+ ["insertorderedlist", "true"],
+ ["insertunorderedlist", "true"],
+ ["insertparagraph", "true"],
+ ["italic", "true"],
+ ["justifycenter", "true"],
+ ["justifyfull", "true"],
+ ["justifyleft", "true"],
+ ["justifyright", "true"],
+ ["outdent", "true"],
+ ["paste", "false"],
+ ["redo", "false"],
+ ["removeformat", "true"],
+ ["selectall", "true"],
+ ["strikethrough", "true"],
+ ["styleWithCSS", "true"],
+ ["subscript", "true"],
+ ["superscript", "true"],
+ ["underline", "true"],
+ ["undo", "false"],
+ ["unlink", "true"],
+ ["not-a-command", "false"]
+ ];
+
+ var commandIndetermResults = [
+ ["contentReadOnly", "false"],
+ ["copy", "false"],
+ ["createlink", "false"],
+ ["cut", "false"],
+ ["decreasefontsize", "false"],
+ ["delete", "false"],
+ ["fontname", "false"],
+ ["fontsize", "false"],
+ ["formatblock", "false"],
+ ["heading", "false"],
+ ["hilitecolor", "false"],
+ ["increasefontsize", "false"],
+ ["indent", "false"],
+ ["inserthorizontalrule", "false"],
+ ["inserthtml", "false"],
+ ["insertimage", "false"],
+ ["insertorderedlist", "false"],
+ ["insertunorderedlist", "false"],
+ ["insertparagraph", "false"],
+ ["italic", "false"],
+ ["justifycenter", "false"],
+ ["justifyfull", "false"],
+ ["justifyleft", "false"],
+ ["justifyright", "false"],
+ ["outdent", "false"],
+ //["paste", "false"],
+ ["redo", "false"],
+ ["removeformat", "false"],
+ ["selectall", "false"],
+ ["strikethrough", "false"],
+ ["styleWithCSS", "false"],
+ ["subscript", "false"],
+ ["superscript", "false"],
+ ["underline", "false"],
+ ["undo", "false"],
+ ["unlink", "false"],
+ ["not-a-command", "false"]
+ ];
+
+ var commandStateResults = [
+ ["contentReadOnly", "false"],
+ ["copy", "false"],
+ ["createlink", "false"],
+ ["cut", "false"],
+ ["decreasefontsize", "false"],
+ ["delete", "false"],
+ ["fontname", "false"],
+ ["fontsize", "false"],
+ ["formatblock", "false"],
+ ["heading", "false"],
+ ["hilitecolor", "false"],
+ ["increasefontsize", "false"],
+ ["indent", "false"],
+ ["inserthorizontalrule", "false"],
+ ["inserthtml", "false"],
+ ["insertimage", "false"],
+ ["insertorderedlist", "false"],
+ ["insertunorderedlist", "false"],
+ ["insertparagraph", "false"],
+ ["italic", "false"],
+ ["justifycenter", "false"],
+ ["justifyfull", "false"],
+ ["justifyleft", "true"],
+ ["justifyright", "false"],
+ ["outdent", "false"],
+ //["paste", "false"],
+ ["redo", "false"],
+ ["removeformat", "false"],
+ ["selectall", "false"],
+ ["strikethrough", "false"],
+ ["styleWithCSS", "false"],
+ ["subscript", "false"],
+ ["superscript", "false"],
+ ["underline", "false"],
+ ["undo", "false"],
+ ["unlink", "false"],
+ ["not-a-command", "false"]
+ ];
+
+ var commandValueResults = [
+ ["contentReadOnly", ""],
+ ["copy", ""],
+ ["createlink", ""],
+ ["cut", ""],
+ ["decreasefontsize", ""],
+ ["delete", ""],
+ ["fontname", "serif"],
+ ["fontsize", ""],
+ ["formatblock", ""],
+ ["heading", ""],
+ ["hilitecolor", "transparent"],
+ ["increasefontsize", ""],
+ ["indent", ""],
+ ["inserthorizontalrule", ""],
+ ["inserthtml", ""],
+ ["insertimage", ""],
+ ["insertorderedlist", ""],
+ ["insertunorderedlist", ""],
+ ["insertparagraph", ""],
+ ["italic", ""],
+ ["justifycenter", "left"],
+ ["justifyfull", "left"],
+ ["justifyleft", "left"],
+ ["justifyright", "left"],
+ ["outdent", ""],
+ //["paste", ""],
+ ["redo", ""],
+ ["removeformat", ""],
+ ["selectall", ""],
+ ["strikethrough", ""],
+ ["styleWithCSS", ""],
+ ["subscript", ""],
+ ["superscript", ""],
+ ["underline", ""],
+ ["undo", ""],
+ ["unlink", ""],
+ ["not-a-command", ""],
+ ];
+
+
+ function callQueryCommandEnabled(cmdName) {
+ var result;
+ try {
+ result = '' + document.queryCommandEnabled( cmdName );
+ } catch( error ) {
+ result = 'name' in error ? error.name : 'exception';
+ }
+ return result;
+ }
+
+ function callQueryCommandIndeterm(cmdName) {
+ var result;
+ try {
+ result = '' + document.queryCommandIndeterm( cmdName );
+ } catch( error ) {
+ result = 'name' in error ? error.name : 'exception';
+ }
+ return result;
+ }
+
+ function callQueryCommandState(cmdName) {
+ var result;
+ try {
+ result = '' + document.queryCommandState( cmdName );
+ } catch( error ) {
+ result = 'name' in error ? error.name : 'exception';
+ }
+ return result;
+ }
+
+ function callQueryCommandValue(cmdName) {
+ var result;
+ try {
+ result = '' + document.queryCommandValue( cmdName );
+ } catch( error ) {
+ result = 'name' in error ? error.name : 'exception';
+ }
+ return result;
+ }
+
+ function testQueryCommand(expectedResults, fun, funName) {
+ for (i=0; i<expectedResults.length; i++) {
+ var commandName = expectedResults[i][0];
+ var expectedResult = expectedResults[i][1];
+ var result = fun(commandName);
+ ok(result == expectedResult, funName + '('+commandName+') result=' +result+ ' expected=' + expectedResult);
+ }
+ }
+
+ function runTests() {
+ document.designMode='on';
+ window.getSelection().collapse(document.body, 0);
+ testQueryCommand(commandEnabledResults, callQueryCommandEnabled, "queryCommandEnabled");
+ testQueryCommand(commandIndetermResults, callQueryCommandIndeterm, "queryCommandIndeterm");
+ testQueryCommand(commandStateResults, callQueryCommandState, "queryCommandState");
+ testQueryCommand(commandValueResults, callQueryCommandValue, "queryCommandValue");
+ document.designMode='off';
+ SimpleTest.finish();
+ }
+
+ window.onload = runTests;
+ SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/editor/libeditor/tests/test_bug410986.html b/editor/libeditor/tests/test_bug410986.html
new file mode 100644
index 000000000..a3f3a5602
--- /dev/null
+++ b/editor/libeditor/tests/test_bug410986.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=410986
+-->
+<head>
+ <title>Test for Bug 410986</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=410986">Mozilla Bug 410986</a>
+<p id="display"></p>
+<div id="content">
+ <div id="contents"><span style="color: green;">green text</span></div>
+ <div id="editor" contenteditable="true"></div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 410986 **/
+
+var gPasteEvents = 0;
+document.getElementById("editor").addEventListener("paste", function() {
+ ++gPasteEvents;
+}, false);
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ getSelection().selectAllChildren(document.getElementById("contents"));
+ SimpleTest.waitForClipboard("green text",
+ function() {
+ synthesizeKey("C", {accelKey: true});
+ },
+ function() {
+ var ed = document.getElementById("editor");
+ ed.focus();
+ if (navigator.platform.indexOf("Mac") >= 0) {
+ synthesizeKey("V", {accelKey: true, shiftKey: true, altKey: true});
+ } else {
+ synthesizeKey("V", {accelKey: true, shiftKey: true});
+ }
+ is(ed.innerHTML, "green text", "Content should be pasted in plaintext format");
+ is(gPasteEvents, 1, "One paste event must be fired");
+
+ ed.innerHTML = "";
+ ed.blur();
+ getSelection().selectAllChildren(document.getElementById("contents"));
+ SimpleTest.waitForClipboard("green text",
+ function() {
+ synthesizeKey("C", {accelKey: true});
+ },
+ function() {
+ var ed = document.getElementById("editor");
+ ed.focus();
+ synthesizeKey("V", {accelKey: true});
+ isnot(ed.innerHTML.indexOf("<span style=\"color: green;\">green text</span>"), -1,
+ "Content should be pasted in HTML format");
+ is(gPasteEvents, 2, "Two paste events must be fired");
+
+ SimpleTest.finish();
+ },
+ function() {
+ ok(false, "Failed to copy the second item to the clipboard");
+ SimpleTest.finish();
+ }
+ );
+ },
+ function() {
+ ok(false, "Failed to copy the first item to the clipboard");
+ SimpleTest.finish();
+ }
+ );
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug414526.html b/editor/libeditor/tests/test_bug414526.html
new file mode 100644
index 000000000..0975b6a5a
--- /dev/null
+++ b/editor/libeditor/tests/test_bug414526.html
@@ -0,0 +1,247 @@
+<html>
+<head>
+ <title>Test for backspace key and delete key shouldn't remove another editing host's text</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<div id="display"></div>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+function runTests()
+{
+
+ var container = document.getElementById("display");
+
+ function reset()
+ {
+ document.execCommand("Undo", false, null);
+ }
+
+ var selection = window.getSelection();
+ function moveCaretToStartOf(aEditor)
+ {
+ selection.selectAllChildren(aEditor);
+ selection.collapseToStart();
+ }
+
+ function moveCaretToEndOf(aEditor)
+ {
+ selection.selectAllChildren(aEditor);
+ selection.collapseToEnd();
+ }
+
+ /* TestCase #1
+ */
+ const kTestCase1 =
+ "<p id=\"editor1\" contenteditable=\"true\">editor1</p>" +
+ "<p id=\"editor2\" contenteditable=\"true\">editor2</p>" +
+ "<div id=\"editor3\" contenteditable=\"true\"><div>editor3</div></div>" +
+ "<p id=\"editor4\" contenteditable=\"true\">editor4</p>" +
+ "non-editable text" +
+ "<p id=\"editor5\" contenteditable=\"true\">editor5</p>";
+
+ const kTestCase1_editor3_deleteAtStart =
+ "<p id=\"editor1\" contenteditable=\"true\">editor1</p>" +
+ "<p id=\"editor2\" contenteditable=\"true\">editor2</p>" +
+ "<div id=\"editor3\" contenteditable=\"true\"><div>ditor3</div></div>" +
+ "<p id=\"editor4\" contenteditable=\"true\">editor4</p>" +
+ "non-editable text" +
+ "<p id=\"editor5\" contenteditable=\"true\">editor5</p>";
+
+ const kTestCase1_editor3_backspaceAtEnd =
+ "<p id=\"editor1\" contenteditable=\"true\">editor1</p>" +
+ "<p id=\"editor2\" contenteditable=\"true\">editor2</p>" +
+ "<div id=\"editor3\" contenteditable=\"true\"><div>editor</div></div>" +
+ "<p id=\"editor4\" contenteditable=\"true\">editor4</p>" +
+ "non-editable text" +
+ "<p id=\"editor5\" contenteditable=\"true\">editor5</p>";
+
+ container.innerHTML = kTestCase1;
+
+ var editor1 = document.getElementById("editor1");
+ var editor2 = document.getElementById("editor2");
+ var editor3 = document.getElementById("editor3");
+ var editor4 = document.getElementById("editor4");
+ var editor5 = document.getElementById("editor5");
+
+ /* TestCase #1:
+ * pressing backspace key at start should not change the content.
+ */
+ editor2.focus();
+ moveCaretToStartOf(editor2);
+ synthesizeKey("VK_BACK_SPACE", { });
+ is(container.innerHTML, kTestCase1,
+ "Pressing backspace key at start of editor2 changes the content");
+ reset();
+
+ editor3.focus();
+ moveCaretToStartOf(editor3);
+ synthesizeKey("VK_BACK_SPACE", { });
+ is(container.innerHTML, kTestCase1,
+ "Pressing backspace key at start of editor3 changes the content");
+ reset();
+
+ editor4.focus();
+ moveCaretToStartOf(editor4);
+ synthesizeKey("VK_BACK_SPACE", { });
+ is(container.innerHTML, kTestCase1,
+ "Pressing backspace key at start of editor4 changes the content");
+ reset();
+
+ editor5.focus();
+ moveCaretToStartOf(editor5);
+ synthesizeKey("VK_BACK_SPACE", { });
+ is(container.innerHTML, kTestCase1,
+ "Pressing backspace key at start of editor5 changes the content");
+ reset();
+
+ /* TestCase #1:
+ * pressing delete key at end should not change the content.
+ */
+ editor1.focus();
+ moveCaretToEndOf(editor1);
+ synthesizeKey("VK_DELETE", { });
+ is(container.innerHTML, kTestCase1,
+ "Pressing delete key at end of editor1 changes the content");
+ reset();
+
+ editor2.focus();
+ moveCaretToEndOf(editor2);
+ synthesizeKey("VK_DELETE", { });
+ is(container.innerHTML, kTestCase1,
+ "Pressing delete key at end of editor2 changes the content");
+ reset();
+
+ editor3.focus();
+ moveCaretToEndOf(editor3);
+ synthesizeKey("VK_DELETE", { });
+ is(container.innerHTML, kTestCase1,
+ "Pressing delete key at end of editor3 changes the content");
+ reset();
+
+ editor4.focus();
+ moveCaretToEndOf(editor4);
+ synthesizeKey("VK_DELETE", { });
+ is(container.innerHTML, kTestCase1,
+ "Pressing delete key at end of editor4 changes the content");
+ reset();
+
+ /* TestCase #1: cases when the caret is not on text node.
+ * - pressing delete key at start should remove the first character
+ * - pressing backspace key at end should remove the first character
+ * and the adjacent blocks should not be changed.
+ */
+ editor3.focus();
+ moveCaretToStartOf(editor3);
+ synthesizeKey("VK_DELETE", { });
+ is(container.innerHTML, kTestCase1_editor3_deleteAtStart,
+ "Pressing delete key at start of editor3 changes adjacent elements"
+ + " and/or does not remove the first character.");
+ reset();
+
+ // Backspace doesn't work here yet.
+ editor3.focus();
+ moveCaretToEndOf(editor3);
+ synthesizeKey("VK_BACK_SPACE", { });
+ todo_is(container.innerHTML, kTestCase1_editor3_backspaceAtEnd,
+ "Pressing backspace key at end of editor3 changes adjacent elements"
+ + " and/or does not remove the last character.");
+ reset();
+ // We can still check that adjacent elements are not affected.
+ editor3.focus();
+ moveCaretToEndOf(editor3);
+ synthesizeKey("VK_BACK_SPACE", { });
+ is(container.innerHTML, kTestCase1,
+ "Pressing backspace key at end of editor3 changes the content");
+ reset();
+
+ /* TestCase #2:
+ * two adjacent editable <span> in a table cell.
+ */
+ const kTestCase2 = "<table><tbody><tr><td><span id=\"editor1\" contenteditable=\"true\">test</span>" +
+ "<span id=\"editor2\" contenteditable=\"true\">test</span></td></tr></tbody></table>";
+
+ container.innerHTML = kTestCase2;
+ editor1 = document.getElementById("editor1");
+ editor2 = document.getElementById("editor2");
+
+ editor2.focus();
+ moveCaretToStartOf(editor2);
+ synthesizeKey("VK_BACK_SPACE", { });
+ is(container.innerHTML, kTestCase2,
+ "Pressing backspace key at the start of editor2 changes the content for kTestCase2");
+ reset();
+
+ editor1.focus();
+ moveCaretToEndOf(editor1);
+ synthesizeKey("VK_DELETE", { });
+ is(container.innerHTML, kTestCase2,
+ "Pressing delete key at the end of editor1 changes the content for kTestCase2");
+ reset();
+
+ /* TestCase #3:
+ * editable <span> in two adjacent table cells.
+ */
+ const kTestCase3 = "<table><tbody><tr><td><span id=\"editor1\" contenteditable=\"true\">test</span></td>" +
+ "<td><span id=\"editor2\" contenteditable=\"true\">test</span></td></tr></tbody></table>";
+
+ container.innerHTML = kTestCase3;
+ editor1 = document.getElementById("editor1");
+ editor2 = document.getElementById("editor2");
+
+ editor2.focus();
+ moveCaretToStartOf(editor2);
+ synthesizeKey("VK_BACK_SPACE", { });
+ is(container.innerHTML, kTestCase3,
+ "Pressing backspace key at the start of editor2 changes the content for kTestCase3");
+ reset();
+
+ editor1.focus();
+ moveCaretToEndOf(editor1);
+ synthesizeKey("VK_DELETE", { });
+ is(container.innerHTML, kTestCase3,
+ "Pressing delete key at the end of editor1 changes the content for kTestCase3");
+ reset();
+
+ /* TestCase #4:
+ * editable <div> in two adjacent table cells.
+ */
+ const kTestCase4 = "<table><tbody><tr><td><div id=\"editor1\" contenteditable=\"true\">test</div></td>" +
+ "<td><div id=\"editor2\" contenteditable=\"true\">test</div></td></tr></tbody></table>";
+
+ container.innerHTML = kTestCase4;
+ editor1 = document.getElementById("editor1");
+ editor2 = document.getElementById("editor2");
+
+ editor2.focus();
+ moveCaretToStartOf(editor2);
+ synthesizeKey("VK_BACK_SPACE", { });
+ is(container.innerHTML, kTestCase4,
+ "Pressing backspace key at the start of editor2 changes the content for kTestCase4");
+ reset();
+
+ editor1.focus();
+ moveCaretToEndOf(editor1);
+ synthesizeKey("VK_DELETE", { });
+ is(container.innerHTML, kTestCase4,
+ "Pressing delete key at the end of editor1 changes the content for kTestCase4");
+ reset();
+
+ SimpleTest.finish();
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_bug417418.html b/editor/libeditor/tests/test_bug417418.html
new file mode 100644
index 000000000..146de0920
--- /dev/null
+++ b/editor/libeditor/tests/test_bug417418.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=417418
+-->
+<head>
+ <title>Test for Bug 417418</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=417418">Mozilla Bug 417418</a>
+<div id="display" contenteditable="true">
+<p id="coin">first paragraph</p>
+<p>second paragraph. <img id="img" src="green.png"></p>
+</div>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 417418 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTest);
+
+function resetSelection() {
+ window.getSelection().collapse(document.getElementById("coin"), 0);
+}
+
+function runTest() {
+ var rightClickDown = {type: 'mousedown', button: 2},
+ rightClickUp = {type: 'mouseup', button: 2},
+ singleClickDown = {type: 'mousedown', button: 0},
+ singleClickUp = {type: 'mouseup', button: 0};
+ var selection = window.getSelection();
+
+ var div = document.getElementById('display');
+ var img = document.getElementById('img');
+ var divRect = div.getBoundingClientRect();
+ var imgselected;
+
+ resetSelection();
+ synthesizeMouse(div, divRect.width - 1, divRect.height - 1, rightClickDown);
+ synthesizeMouse(div, divRect.width - 1, divRect.height - 1, rightClickUp);
+ ok(selection.isCollapsed, "selection is not collapsed");
+
+ resetSelection();
+ synthesizeMouse(div, divRect.width - 1, divRect.height - 1, singleClickDown);
+ synthesizeMouse(div, divRect.width - 1, divRect.height - 1, singleClickUp);
+ ok(selection.isCollapsed, "selection is not collapsed");
+
+ resetSelection();
+ synthesizeMouseAtCenter(img, rightClickDown);
+ synthesizeMouseAtCenter(img, rightClickUp);
+ imgselected = selection.anchorNode == img.parentNode &&
+ selection.anchorOffset === 1 &&
+ selection.rangeCount === 1;
+ ok(imgselected, "image is not selected");
+
+ resetSelection();
+ synthesizeMouseAtCenter(img, singleClickDown);
+ synthesizeMouseAtCenter(img, singleClickUp);
+ imgselected = selection.anchorNode == img.parentNode &&
+ selection.anchorOffset === 1 &&
+ selection.rangeCount === 1;
+ ok(imgselected, "image is not selected");
+
+ SimpleTest.finish();
+}
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug432225.html b/editor/libeditor/tests/test_bug432225.html
new file mode 100644
index 000000000..58d158722
--- /dev/null
+++ b/editor/libeditor/tests/test_bug432225.html
@@ -0,0 +1,71 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=432225
+-->
+<head>
+ <title>Test for Bug 432225</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="spellcheck.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=432225">Mozilla Bug 432225</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 432225 **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+
+var gMisspeltWords = [];
+
+function getEdit() {
+ return document.getElementById('edit');
+}
+
+function editDoc() {
+ return getEdit().contentDocument;
+}
+
+function getEditor() {
+ var Ci = SpecialPowers.Ci;
+ var win = editDoc().defaultView;
+ var editingSession = SpecialPowers.wrap(win)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIEditingSession);
+ return editingSession.getEditorForWindow(win);
+}
+
+function runTest() {
+ editDoc().designMode = "on";
+ setTimeout(function() { addWords(100); }, 0);
+}
+
+function addWords(aLimit) {
+ if (aLimit == 0) {
+ ok(isSpellingCheckOk(getEditor(), gMisspeltWords),
+ "All misspellings accounted for.");
+ SimpleTest.finish();
+ return;
+ }
+ getEdit().focus();
+ sendString('aa OK ');
+ gMisspeltWords.push("aa");
+ setTimeout(function() { addWords(aLimit-1); }, 0);
+}
+</script>
+</pre>
+
+<iframe id="edit" width="200" height="100" src="about:blank"></iframe>
+
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug439808.html b/editor/libeditor/tests/test_bug439808.html
new file mode 100644
index 000000000..a04d1d4d4
--- /dev/null
+++ b/editor/libeditor/tests/test_bug439808.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=439808
+-->
+<head>
+ <title>Test for Bug 439808</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=439808">Mozilla Bug 439808</a>
+<p id="display"></p>
+<div id="content">
+<span><span contenteditable id="e">twest</span></span>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 439808 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var e = document.getElementById("e");
+ e.focus();
+ getSelection().collapse(e.firstChild, 1);
+ synthesizeKey("VK_DELETE", {});
+ is(e.textContent, "test", "Delete key worked");
+ synthesizeKey("VK_BACK_SPACE", {});
+ is(e.textContent, "est", "Backspace key worked");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug442186.html b/editor/libeditor/tests/test_bug442186.html
new file mode 100644
index 000000000..eab81e055
--- /dev/null
+++ b/editor/libeditor/tests/test_bug442186.html
@@ -0,0 +1,103 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=442186
+-->
+<head>
+ <title>Test for Bug 442186</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=442186">Mozilla Bug 442186</a>
+<p id="display"></p>
+<div id="content">
+ <h2> two &lt;div&gt; containers </h2>
+ <section contenteditable id="test1">
+ <div> First paragraph with some text. </div>
+ <div> Second paragraph with some text. </div>
+ </section>
+
+ <h2> two paragraphs </h2>
+ <section contenteditable id="test2">
+ <p> First paragraph with some text. </p>
+ <p> Second paragraph with some text. </p>
+ </section>
+
+ <h2> one text node, one paragraph </h2>
+ <section contenteditable id="test3">
+ First paragraph with some text.
+ <p> Second paragraph with some text. </p>
+ </section>
+</div>
+
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 442186 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+function justify(textNode, pos) {
+ if (!pos) pos = 10;
+
+ // put the caret on the requested character
+ var range = document.createRange();
+ var sel = window.getSelection();
+ range.setStart(textNode, pos);
+ range.setEnd(textNode, pos);
+ sel.addRange(range);
+
+ // align
+ document.execCommand("justifyright", false, null);
+}
+
+function runTests() {
+ document.execCommand("stylewithcss", false, "true");
+
+ const test1 = document.getElementById("test1");
+ const test2 = document.getElementById("test2");
+ const test3 = document.getElementById("test3");
+
+ // #test1: two <div> containers
+ const line1 = test1.querySelector("div").firstChild;
+ test1.focus();
+ justify(line1);
+ is(test1.querySelectorAll("*").length, 2,
+ "Aligning the first child should not create nor remove any element.");
+ is(line1.parentNode.nodeName.toLowerCase(), "div",
+ "Aligning the first <div> should not modify its node type.");
+ is(line1.parentNode.style.textAlign, "right",
+ "Aligning the first <div> should set a 'text-align: right' style rule.");
+
+ // #test2: two paragraphs
+ const line2 = test2.querySelector("p").firstChild;
+ test2.focus();
+ justify(line2);
+ is(test2.querySelectorAll("*").length, 2,
+ "Aligning the first child should not create nor remove any element.");
+ is(line2.parentNode.nodeName.toLowerCase(), "p",
+ "Aligning the first paragraph should not modify its node type.");
+ is(line2.parentNode.style.textAlign, "right",
+ "Aligning the first paragraph should set a 'text-align: right' style rule.");
+
+ // #test3: one text node, two paragraphs
+ const line3 = test3.firstChild;
+ test3.focus();
+ justify(line3);
+ is(test3.querySelectorAll("*").length, 2,
+ "Aligning the first child should create a block element.");
+ is(line3.parentNode.nodeName.toLowerCase(), "div",
+ "Aligning the first child should create a block element.");
+ is(line3.parentNode.style.textAlign, "right",
+ "Aligning the first line should set a 'text-align: right' style rule.");
+
+ // done
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug449243.html b/editor/libeditor/tests/test_bug449243.html
new file mode 100644
index 000000000..77a7c6a7d
--- /dev/null
+++ b/editor/libeditor/tests/test_bug449243.html
@@ -0,0 +1,136 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=449243
+-->
+<head>
+ <title>Test for Bug 449243</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=449243">Mozilla Bug 449243</a>
+<p id="display"></p>
+<div id="content" contenteditable>
+ <h2>This is a title</h2>
+ <ul>
+ <li>this is a</li>
+ <li>bullet list</li>
+ </ul>
+ <ol>
+ <li>this is a</li>
+ <li>numbered list</li>
+ </ol>
+</div>
+
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 449243 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+const CARET_BEGIN = 0;
+const CARET_MIDDLE = 1;
+const CARET_END = 2;
+
+function split(element, caretPos, nbKeyPresses) {
+ // put the caret on the requested position
+ var sel = window.getSelection();
+ var len = element.textContent.length;
+ var pos = -1;
+ switch (caretPos) {
+ case CARET_BEGIN:
+ pos = 0;
+ break;
+ case CARET_MIDDLE:
+ pos = Math.floor(len/2);
+ break;
+ case CARET_END:
+ pos = len;
+ break;
+ }
+ sel.collapse(element.firstChild, pos);
+
+ // simulates a [Return] keypress
+ for (var i = 0; i < nbKeyPresses; i++)
+ synthesizeKey("VK_RETURN", {});
+}
+
+function undo(nbKeyPresses) {
+ for (var i = 0; i < nbKeyPresses; i++)
+ document.execCommand("Undo", false, null);
+}
+
+function SameTypeAsPreviousSibling(element) {
+ var sibling = element.previousSibling;
+ while (sibling && sibling.nodeType != 1)
+ sibling = element.previousSibling;
+ return (element.nodeName == sibling.nodeName);
+}
+
+function isParagraph(element) {
+ return element.nodeName.toLowerCase() == "p";
+}
+
+function runTests() {
+ const content = document.querySelector("[contenteditable]");
+ const header = content.querySelector("h2");
+ const ulItem = content.querySelector("ul > li:last-child");
+ const olItem = content.querySelector("ol > li:last-child");
+ content.focus();
+
+ // beginning of selection: split current node
+ split(header, CARET_BEGIN, 1);
+ ok(SameTypeAsPreviousSibling(header),
+ "Pressing [Return] at the beginning of a header " +
+ "should create another header.");
+ split(ulItem, CARET_BEGIN, 2);
+ ok(SameTypeAsPreviousSibling(ulItem),
+ "Pressing [Return] at the beginning of an unordered list item " +
+ "should create another list item.");
+ split(olItem, CARET_BEGIN, 2);
+ ok(SameTypeAsPreviousSibling(olItem),
+ "Pressing [Return] at the beginning of an ordered list item " +
+ "should create another list item.");
+ undo(3);
+
+ // middle of selection: split current node
+ split(header, CARET_MIDDLE, 1);
+ ok(SameTypeAsPreviousSibling(header),
+ "Pressing [Return] at the middle of a header " +
+ "should create another header.");
+ split(ulItem, CARET_MIDDLE, 2);
+ ok(SameTypeAsPreviousSibling(ulItem),
+ "Pressing [Return] at the middle of an unordered list item " +
+ "should create another list item.");
+ split(olItem, CARET_MIDDLE, 2);
+ ok(SameTypeAsPreviousSibling(olItem),
+ "Pressing [Return] at the middle of an ordered list item " +
+ "should create another list item.");
+ undo(3);
+
+ // end of selection: create a new paragraph
+ split(header, CARET_END, 1);
+ ok(isParagraph(content.querySelector("h2+*")),
+ "Pressing [Return] at the end of a header " +
+ "should create a new paragraph.");
+ split(ulItem, CARET_END, 2);
+ ok(isParagraph(content.querySelector("ul+*")),
+ "Pressing [Return] twice at the end of an unordered list item " +
+ "should create a new paragraph.");
+ split(olItem, CARET_END, 2);
+ ok(isParagraph(content.querySelector("ol+*")),
+ "Pressing [Return] twice at the end of an ordered list item " +
+ "should create a new paragraph.");
+ undo(3);
+
+ // done
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug455992.html b/editor/libeditor/tests/test_bug455992.html
new file mode 100644
index 000000000..daf362acf
--- /dev/null
+++ b/editor/libeditor/tests/test_bug455992.html
@@ -0,0 +1,97 @@
+<!DOCTYPE HTML>
+<html><head>
+<title>Test for bug 455992</title>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+<script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+
+<script class="testbody" type="application/javascript">
+function runTest() {
+
+ function select(id) {
+ var e = document.getElementById(id);
+ e.focus();
+ return e;
+ }
+
+ function setupIframe(id) {
+ var e = document.getElementById(id);
+ var doc = e.contentDocument;
+ doc.body.innerHTML = String.fromCharCode(10)+'<span id="' + id + '_span" style="border:1px solid blue" contenteditable="true">X</span>'+String.fromCharCode(10);
+ e = doc.getElementById(id + "_span");
+ e.focus();
+ return e;
+ }
+
+ function test_begin_bs(e) {
+ const msg = "BACKSPACE at beginning of contenteditable inline element";
+ var before = e.parentNode.childNodes[0].nodeValue;
+ sendKey("back_space");
+ is(e.parentNode.childNodes[0].nodeValue, before, msg + " with id=" + e.id);
+ is(e.innerHTML, "X", msg + " with id=" + e.id);
+ }
+
+ function test_begin_space(e) {
+ const msg = "SPACE at beginning of contenteditable inline element";
+ var before = e.parentNode.childNodes[0].nodeValue;
+ sendChar(" ");
+ is(e.parentNode.childNodes[0].nodeValue, before, msg + " with id=" + e.id);
+ is(e.innerHTML, "&nbsp;X", msg + " with id=" + e.id);
+ }
+
+ function test_end_delete(e) {
+ const msg = "DEL at end of contenteditable inline element";
+ var before = e.parentNode.childNodes[2].nodeValue;
+ sendKey("right");
+ sendKey("delete");
+ is(e.parentNode.childNodes[2].nodeValue, before, msg + " with id=" + e.id);
+ is(e.innerHTML, "X", msg + " with id=" + e.id);
+ }
+
+ function test_end_space(e) {
+ const msg = "SPACE at end of contenteditable inline element";
+ var before = e.parentNode.childNodes[2].nodeValue;
+ sendKey("right");
+ sendChar(" ");
+ is(e.parentNode.childNodes[2].nodeValue, before, msg + " with id=" + e.id);
+ is(e.innerHTML, "X" + (e.tagName=="SPAN" ? "&nbsp;" : " <br>"), msg + " with id=" + e.id);
+ }
+
+ test_begin_bs(select("t1"));
+ test_begin_space(select("t2"));
+ test_end_delete(select("t3"));
+ test_end_space(select("t4"));
+ test_end_space(select("t5"));
+
+ test_begin_bs(setupIframe('i1'));
+ test_begin_space(setupIframe('i2'));
+ test_end_delete(setupIframe('i3'));
+ test_end_space(setupIframe('i4'));
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=455992">Mozilla Bug 455992</a>
+<p id="display"></p>
+
+<pre id="test">
+</pre>
+
+<div> <span id="t1" style="border:1px solid blue" contenteditable="true">X</span> Y</div>
+<div> <span id="t2" style="border:1px solid blue" contenteditable="true">X</span> Y</div>
+<div> <span id="t3" style="border:1px solid blue" contenteditable="true">X</span> Y</div>
+<div> <span id="t4" style="border:1px solid blue" contenteditable="true">X</span> Y</div>
+<div> <div id="t5" style="border:1px solid blue" contenteditable="true">X</div> Y</div>
+
+<iframe id="i1" width="200" height="100" src="about:blank"></iframe><br>
+<iframe id="i2" width="200" height="100" src="about:blank"></iframe><br>
+<iframe id="i3" width="200" height="100" src="about:blank"></iframe><br>
+<iframe id="i4" width="200" height="100" src="about:blank"></iframe><br>
+
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug456244.html b/editor/libeditor/tests/test_bug456244.html
new file mode 100644
index 000000000..03cc2c9e3
--- /dev/null
+++ b/editor/libeditor/tests/test_bug456244.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html><head>
+<title>Test for bug 456244</title>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+<script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+
+<script class="testbody" type="application/javascript">
+function runTest() {
+
+ function select(id) {
+ var e = document.getElementById(id);
+ e.focus();
+ return e;
+ }
+
+ function setupIframe(id) {
+ var e = document.getElementById(id);
+ var doc = e.contentDocument;
+ doc.body.innerHTML = String.fromCharCode(10)+'<span id="' + id + '_span" style="border:1px solid blue" contenteditable="true">X</span>'+String.fromCharCode(10);
+ e = doc.getElementById(id + "_span");
+ e.focus();
+ return e;
+ }
+
+ function test_end_bs(e) {
+ const msg = "Deleting all text in contenteditable inline element";
+ var before = e.parentNode.childNodes[0].nodeValue;
+ sendKey("right");
+ sendKey("back_space");
+ sendKey("back_space");
+ is(e.parentNode.childNodes[0].nodeValue, before, msg + " with id=" + e.id);
+ is(e.innerHTML, "", msg + " with id=" + e.id);
+ }
+
+ test_end_bs(select("t1"));
+ test_end_bs(setupIframe('i1',0));
+
+ {
+ const msg = "Deleting all text in contenteditable body element";
+ var e = document.getElementById('i2');
+ var doc = e.contentDocument;
+ doc.body.setAttribute("contenteditable", "true");
+ doc.body.focus();
+ sendKey("right");
+ sendKey("back_space");
+ is(doc.body.innerHTML, "<br>", msg + " with id=" + e.id);
+ }
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=456244">Mozilla Bug 456244</a>
+<p id="display"></p>
+
+<pre id="test">
+</pre>
+
+<div> <span id="t1" style="border:1px solid blue" contenteditable="true">X</span> Y</div>
+
+<iframe id="i1" width="200" height="100" src="about:blank"></iframe><br>
+<iframe id="i2" width="200" height="100" src="about:blank">X</iframe><br>
+
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug460740.html b/editor/libeditor/tests/test_bug460740.html
new file mode 100644
index 000000000..b9e79c1e0
--- /dev/null
+++ b/editor/libeditor/tests/test_bug460740.html
@@ -0,0 +1,124 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=460740
+-->
+<head>
+ <title>Test for Bug 460740</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=460740">Mozilla Bug 460740</a>
+<p id="display"></p>
+<div id="content">
+ <ul>
+ <li contenteditable>
+ Editable LI
+ </li>
+ <li>
+ <div contenteditable>
+ Editable DIV inside LI
+ </div>
+ </li>
+ <li>
+ <div>
+ <div contenteditable>
+ Editable DIV inside DIV inside LI
+ </div>
+ </div>
+ </li>
+ <li>
+ <h3>
+ <div contenteditable>
+ Editable DIV inside H3 inside LI
+ </div>
+ </h3>
+ </li>
+ </ul>
+ <div contenteditable>
+ Editable DIV
+ </div>
+ <h3 contenteditable>
+ Editable H3
+ </h3>
+ <p contenteditable>
+ Editable P
+ </p>
+ <div>
+ <p contenteditable>
+ Editable P in a DIV
+ </p>
+ </div>
+ <p><span contenteditable>Editable SPAN in a P</span></p>
+</div>
+
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 460740 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+const CARET_BEGIN = 0;
+const CARET_MIDDLE = 1;
+const CARET_END = 2;
+
+function split(element, caretPos) {
+ // compute the requested position
+ var len = element.textContent.length;
+ var pos = -1;
+ switch (caretPos) {
+ case CARET_BEGIN:
+ pos = 0;
+ break;
+ case CARET_MIDDLE:
+ pos = Math.floor(len/2);
+ break;
+ case CARET_END:
+ pos = len;
+ break;
+ }
+
+ // put the caret on the requested position
+ var range = document.createRange();
+ var sel = window.getSelection();
+ range.setStart(element.firstChild, pos);
+ range.setEnd(element.firstChild, pos);
+ sel.addRange(range);
+
+ // simulates a [Return] keypress
+ synthesizeKey("VK_RETURN", {});
+}
+
+// count the number of non-BR elements in #content
+function getBlockCount() {
+ return document.querySelectorAll("#content *:not(br)").length;
+}
+
+// count the number of BRs in element
+function checkBR(element) {
+ return element.querySelectorAll("br").length;
+}
+
+function runTests() {
+ var count = getBlockCount();
+ var nodes = document.querySelectorAll("#content [contenteditable]");
+ for (var i = 0; i < nodes.length; i++) {
+ var node = nodes[i];
+ node.focus();
+ is(checkBR(node), 0, node.textContent.trim() + ": This node should not have any <br> element yet.");
+ for (var j = 0; j < 3; j++) { // CARET_BEGIN|MIDDLE|END
+ split(node, j);
+ ok(checkBR(node) > 0, node.textContent.trim() + " " + j + ": Pressing [Return] should add (at least) one <br> element.");
+ is(getBlockCount(), count, node.textContent.trim() + " " + j + ": Pressing [Return] should not change the number of non-<br> elements.");
+ document.execCommand("Undo", false, null);
+ }
+ }
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug46555.html b/editor/libeditor/tests/test_bug46555.html
new file mode 100644
index 000000000..3838bdb3b
--- /dev/null
+++ b/editor/libeditor/tests/test_bug46555.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=46555
+-->
+
+<head>
+ <title>Test for Bug 46555</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=46555">Mozilla Bug 46555</a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+
+ <input type="text" value="" id="t1" />
+
+ <pre id="test">
+ <script type="application/javascript">
+
+ /** Test for Bug 46555 **/
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(function() {
+ const kCmd = "cmd_selectAll";
+
+ var input = document.getElementById("t1");
+ input.focus();
+ var controller =
+ SpecialPowers.wrap(input).controllers.getControllerForCommand(kCmd);
+
+ // Test 1: Select All should be disabled if editor is empty
+ is(controller.isCommandEnabled(kCmd), false,
+ "Select All command disabled when editor is empty");
+
+ SimpleTest.finish();
+ });
+ </script>
+ </pre>
+
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug468353.html b/editor/libeditor/tests/test_bug468353.html
new file mode 100644
index 000000000..179c41cdc
--- /dev/null
+++ b/editor/libeditor/tests/test_bug468353.html
@@ -0,0 +1,117 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=468353
+-->
+<head>
+ <title>Test for Bug 468353</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=468353">Mozilla Bug 468353</a>
+<p id="display"></p>
+<div id="content">
+ <iframe></iframe>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var styleSheets = null;
+
+function checkStylesheets() {
+ // Evidently RemoveStyleSheet is the only method in nsIEditorStyleSheets
+ // that would throw. RemoveOverrideStyleSheet returns NS_OK even if the
+ // sheet is not there
+ var removed = 0;
+ try
+ {
+ styleSheets.removeStyleSheet("resource://gre/res/designmode.css");
+ removed++;
+ }
+ catch (ex) { }
+
+ try {
+ styleSheets.removeStyleSheet("resource://gre/res/contenteditable.css");
+ removed++;
+ }
+ catch (ex) { }
+
+ is(removed, 0, "Should have thrown if stylesheet was not there");
+}
+
+function runTest() {
+ const Ci = SpecialPowers.Ci;
+
+ /** Found while fixing bug 440614 **/
+ var editframe = window.frames[0];
+ var editdoc = editframe.document;
+ var editor = null;
+ editdoc.write('');
+ editdoc.close();
+
+ editdoc.designMode='on';
+
+ // Hold the reference to the editor
+ editor = SpecialPowers.wrap(editframe)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIEditingSession)
+ .getEditorForWindow(editframe);
+
+ styleSheets = editor.QueryInterface(Ci.nsIEditorStyleSheets);
+
+ editdoc.designMode='off';
+
+ checkStylesheets();
+
+ // Let go
+ editor = null;
+ styleSheets = null;
+
+ editdoc.body.contentEditable = true;
+
+ // Hold the reference to the editor
+ editor = SpecialPowers.wrap(editframe)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIEditingSession)
+ .getEditorForWindow(editframe);
+
+ styleSheets = editor.QueryInterface(Ci.nsIEditorStyleSheets);
+
+ editdoc.body.contentEditable = false;
+
+ checkStylesheets();
+
+ editdoc.designMode = "on";
+ editdoc.body.contentEditable = true;
+ editdoc.designMode = "off";
+
+ // Hold the reference to the editor
+ editor = SpecialPowers.wrap(editframe)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIEditingSession)
+ .getEditorForWindow(editframe);
+
+ styleSheets = editor.QueryInterface(Ci.nsIEditorStyleSheets);
+
+ editdoc.body.contentEditable = false;
+
+ checkStylesheets();
+
+ SimpleTest.finish();
+}
+
+//XXX I don't know if this is necessary, but we're dealing with iframes...
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug471319.html b/editor/libeditor/tests/test_bug471319.html
new file mode 100644
index 000000000..399ba4611
--- /dev/null
+++ b/editor/libeditor/tests/test_bug471319.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=471319
+-->
+
+<head>
+ <title>Test for Bug 471319</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+
+<body onload="doTest();">
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=471319">Mozilla Bug 471319</a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+
+ <pre id="test">
+ <script type="application/javascript;version=1.7">
+
+ /** Test for Bug 471319 **/
+
+ SimpleTest.waitForExplicitFinish();
+
+ function doTest() {
+ let t1 = SpecialPowers.wrap($("t1"));
+ let editor = null;
+
+ // Test 1: Undo on an empty editor - the editor should not forget about
+ // the bogus node
+ t1.QueryInterface(SpecialPowers.Ci.nsIDOMNSEditableElement);
+ t1Editor = t1.editor;
+
+ // Did the editor recognize the new bogus node?
+ t1Editor.undo(1);
+ ok(!t1.value, "<br> still recognized as bogus node on undo");
+
+
+ // Test 2: Redo on an empty editor - the editor should not forget about
+ // the bogus node
+ let t2 = SpecialPowers.wrap($("t2"));
+ t2.QueryInterface(SpecialPowers.Ci.nsIDOMNSEditableElement);
+ t2Editor = t2.editor;
+
+ // Did the editor recognize the new bogus node?
+ t2Editor.redo(1);
+ ok(!t2.value, "<br> still recognized as bogus node on redo");
+
+
+ // Test 3: Undoing a batched transaction where both end points of the
+ // transaction are the bogus node - the bogus node should still be
+ // recognized as bogus
+ t1Editor.transactionManager.beginBatch(null);
+ t1.value = "mozilla";
+ t1.value = "";
+ t1Editor.transactionManager.endBatch(false);
+ t1Editor.undo(1);
+ ok(!t1.value,
+ "recreated <br> from undo transaction recognized as bogus");
+
+
+ // Test 4: Redoing a batched transaction where both end points of the
+ // transaction are the bogus node - the bogus node should still be
+ // recognized as bogus
+ t1Editor.redo(1);
+ ok(!t1.value,
+ "recreated <br> from redo transaction recognized as bogus");
+ SimpleTest.finish();
+ }
+ </script>
+ </pre>
+
+ <input type="text" id="t1" />
+ <input type="text" id="t2" />
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug471722.html b/editor/libeditor/tests/test_bug471722.html
new file mode 100644
index 000000000..74ff55307
--- /dev/null
+++ b/editor/libeditor/tests/test_bug471722.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=471722
+-->
+
+<head>
+ <title>Test for Bug 471722</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<body onload="doTest();">
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=471722">Mozilla Bug 471722</a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+
+ <pre id="test">
+ <script type="application/javascript">
+
+ /** Test for Bug 471722 **/
+
+ SimpleTest.waitForExplicitFinish();
+
+ function doTest() {
+ var t1 = $("t1");
+ var editor = null;
+
+ if (t1 instanceof SpecialPowers.Ci.nsIDOMNSEditableElement)
+ editor = SpecialPowers.wrap(t1).editor;
+ ok(editor, "able to get editor for the element");
+ t1.focus();
+ t1.select();
+
+ try {
+
+ // Cut the initial text in the textbox
+ ok(editor.canCut(), "can cut text");
+ editor.cut();
+ is(t1.value, "", "initial text was removed");
+
+ // So now we will have emptied the textfield
+ // and the editor will have created a bogus node
+ // Check the transaction is in the undo stack...
+ var t1Enabled = {};
+ var t1CanUndo = {};
+ editor.canUndo(t1Enabled, t1CanUndo);
+ ok(t1CanUndo.value, "undo is enabled");
+
+ // Undo the cut
+ editor.undo(1);
+ is(t1.value, "minefield", "text reinserted");
+
+ // So now, the cut should be in the redo stack,
+ // so executing the redo will clear the text once again
+ // and reinsert the bogus node that was removed after undo.
+ // This will require the editor to figure out that we have a
+ // bogus node again...
+ var t1CanRedo = {};
+ editor.canRedo(t1Enabled, t1CanRedo);
+ ok(t1CanRedo.value, "redo is enabled");
+ editor.redo(1);
+
+ // Did the editor notice a bogus node reappeared?
+ is(t1.value, "", "editor found bogus node");
+ } catch (e) {
+ ok(false, "test failed with error "+e);
+ }
+ SimpleTest.finish();
+ }
+ </script>
+ </pre>
+
+ <input type="text" value="minefield" id="t1" />
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug478725.html b/editor/libeditor/tests/test_bug478725.html
new file mode 100644
index 000000000..8df85dfff
--- /dev/null
+++ b/editor/libeditor/tests/test_bug478725.html
@@ -0,0 +1,131 @@
+<!DOCTYPE HTML>
+<html><head>
+<title>Test for bug 478725</title>
+<style src="/tests/SimpleTest/test.css" type="text/css"></style>
+<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+<script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+
+<script class="testbody" type="application/javascript">
+
+function runTest() {
+ function verifyContent(s) {
+ var e = document.getElementById('i1');
+ var doc = e.contentDocument;
+ is(doc.body.innerHTML, s, "");
+ }
+
+ function pasteInto(html,target_id) {
+ var e = document.getElementById('i1');
+ var doc = e.contentDocument;
+ doc.designMode = "on";
+ doc.body.innerHTML = html;
+ e = doc.getElementById(target_id);
+ doc.defaultView.focus();
+ var selection = doc.defaultView.getSelection();
+ selection.removeAllRanges();
+ selection.selectAllChildren(e);
+ selection.collapseToEnd();
+ SpecialPowers.wrap(doc).execCommand("paste", false, null);
+ return e;
+ }
+
+ function copyToClipBoard(s,asHTML,target_id) {
+ var e = document.getElementById('i2');
+ var doc = e.contentDocument;
+ if (asHTML) {
+ doc.body.innerHTML = s;
+ } else {
+ var text = doc.createTextNode(s);
+ doc.body.appendChild(text);
+ }
+ doc.designMode = "on";
+ doc.defaultView.focus();
+ var selection = doc.defaultView.getSelection();
+ selection.removeAllRanges();
+ if (!target_id) {
+ selection.selectAllChildren(doc.body);
+ } else {
+ var range = document.createRange();
+ range.selectNode(doc.getElementById(target_id));
+ selection.addRange(range);
+ }
+ SpecialPowers.wrap(doc).execCommand("copy", false, null);
+ return e;
+ }
+
+ copyToClipBoard("<dl><dd>Hello Kitty</dd></dl>", true);
+ pasteInto('<ol><li id="paste_here">X</li></ol>',"paste_here");
+ verifyContent('<ol><li id="paste_here">X<dl><dd>Hello Kitty</dd></dl></li></ol>');
+
+ copyToClipBoard("<li>Hello Kitty</li>", true);
+ pasteInto('<ol><li id="paste_here">X</li></ol>',"paste_here");
+ verifyContent('<ol><li id="paste_here">X</li><li>Hello Kitty</li></ol>');
+
+ copyToClipBoard("<ol><li>Hello Kitty</li></ol>", true);
+ pasteInto('<ol><li id="paste_here">X</li></ol>',"paste_here");
+ verifyContent('<ol><li id="paste_here">X</li><li>Hello Kitty</li></ol>');
+
+ copyToClipBoard("<ul><li>Hello Kitty</li></ul>", true);
+ pasteInto('<ol><li id="paste_here">X</li></ol>',"paste_here");
+ verifyContent('<ol><li id="paste_here">X</li><li>Hello Kitty</li></ol>');
+
+ copyToClipBoard("<ul><li>Hello</li><ul><li>Kitty</li></ul></ul>", true);
+ pasteInto('<ol><li id="paste_here">X</li></ol>',"paste_here");
+ verifyContent('<ol><li id="paste_here">X</li><li>Hello</li><ul><li>Kitty</li></ul></ol>');
+
+ copyToClipBoard("<dl><dd>Hello</dd><dd>Kitty</dd></dl>", true);
+ pasteInto('<dl><dd id="paste_here">X</dd></dl>',"paste_here");
+ verifyContent('<dl><dd id="paste_here">X</dd><dd>Hello</dd><dd>Kitty</dd></dl>');
+
+ copyToClipBoard("<dl><dd>Hello</dd><dd>Kitty</dd></dl>", true);
+ pasteInto('<dl><dt id="paste_here">X</dt></dl>',"paste_here");
+ verifyContent('<dl><dt id="paste_here">X</dt><dd>Hello</dd><dd>Kitty</dd></dl>');
+
+ copyToClipBoard("<dl><dt>Hello</dt><dd>Kitty</dd></dl>", true);
+ pasteInto('<dl><dd id="paste_here">X</dd></dl>',"paste_here");
+ verifyContent('<dl><dd id="paste_here">X</dd><dt>Hello</dt><dd>Kitty</dd></dl>');
+
+ copyToClipBoard("<pre>Kitty</pre>", true);
+ pasteInto('<pre id="paste_here">Hello </pre>',"paste_here");
+ verifyContent('<pre id="paste_here">Hello Kitty</pre>');
+
+// I was expecting these to trigger the special TABLE/TR rules in nsHTMLEditor::InsertHTMLWithContext
+// but they don't for some reason...
+// copyToClipBoard('<table><tr id="copy_here"><td>Kitty</td></tr></table>', true, "copy_here");
+// pasteInto('<table><tr id="paste_here"><td>Hello</td></tr></table>',"paste_here");
+// verifyContent('');
+//
+// copyToClipBoard('<table id="copy_here"><tr><td>Kitty</td></tr></table>', true, "copy_here");
+// pasteInto('<table><tr id="paste_here"><td>Hello</td></tr></table>',"paste_here");
+// verifyContent('');
+//
+// copyToClipBoard('<table id="copy_here"><tr><td>Kitty</td></tr></table>', true, "copy_here");
+// pasteInto('<table id="paste_here"><tr><td>Hello</td></tr></table>',"paste_here");
+// verifyContent('');
+//
+// copyToClipBoard('<table><tr id="copy_here"><td>Kitty</td></tr></table>', true, "copy_here");
+// pasteInto('<table id="paste_here"><tr><td>Hello</td></tr></table>',"paste_here");
+// verifyContent('');
+
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=478725">Mozilla Bug 478725</a>
+<p id="display"></p>
+
+<pre id="test">
+</pre>
+
+
+<iframe id="i1" width="200" height="100" src="about:blank"></iframe><br>
+<iframe id="i2" width="200" height="100" src="about:blank"></iframe><br>
+
+</body>
+</html>
+
diff --git a/editor/libeditor/tests/test_bug480647.html b/editor/libeditor/tests/test_bug480647.html
new file mode 100644
index 000000000..33f088a1b
--- /dev/null
+++ b/editor/libeditor/tests/test_bug480647.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=480647
+-->
+<title>Test for Bug 480647</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=480647">Mozilla Bug 480647</a>
+<div contenteditable></div>
+<script>
+/** Test for Bug 480647 **/
+
+var div = document.querySelector("div");
+
+function parseFontSize(input, expected) {
+ parseFontSizeInner(input, expected, is);
+}
+
+function parseFontSizeTodo(input, expected) {
+ parseFontSizeInner(input, expected, todo_is);
+}
+
+function parseFontSizeInner(input, expected, fn) {
+ div.innerHTML = "foo";
+ getSelection().selectAllChildren(div);
+ document.execCommand("fontSize", false, input);
+ if (expected === null) {
+ fn(div.innerHTML, "foo",
+ 'execCommand("fontSize", false, "' + input + '") should be no-op');
+ } else {
+ fn(div.innerHTML, '<font size="' + expected + '">foo</font>',
+ 'execCommand("fontSize", false, "' + input + '") should parse to ' +
+ expected);
+ }
+}
+
+// Parse errors
+parseFontSize("", null);
+parseFontSize("abc", null);
+parseFontSize("larger", null);
+parseFontSize("smaller", null);
+parseFontSize("xx-small", null);
+parseFontSize("x-small", null);
+parseFontSize("small", null);
+parseFontSize("medium", null);
+parseFontSize("large", null);
+parseFontSize("x-large", null);
+parseFontSize("xx-large", null);
+parseFontSize("xxx-large", null);
+// Bug 747879
+parseFontSizeTodo("1.2em", null);
+parseFontSizeTodo("8px", null);
+parseFontSizeTodo("-1.2em", null);
+parseFontSizeTodo("-8px", null);
+parseFontSizeTodo("+1.2em", null);
+parseFontSizeTodo("+8px", null);
+
+// Numbers
+parseFontSize("0", 1);
+parseFontSize("1", 1);
+parseFontSize("2", 2);
+parseFontSize("3", 3);
+parseFontSize("4", 4);
+parseFontSize("5", 5);
+parseFontSize("6", 6);
+parseFontSize("7", 7);
+parseFontSize("8", 7);
+parseFontSize("9", 7);
+parseFontSize("10", 7);
+parseFontSize("1000000000000000000000", 7);
+parseFontSize("2.72", 2);
+parseFontSize("2.72e9", 2);
+
+// Minus sign
+parseFontSize("-0", 3);
+parseFontSize("-1", 2);
+parseFontSize("-2", 1);
+parseFontSize("-3", 1);
+parseFontSize("-4", 1);
+parseFontSize("-5", 1);
+parseFontSize("-6", 1);
+parseFontSize("-7", 1);
+parseFontSize("-8", 1);
+parseFontSize("-9", 1);
+parseFontSize("-10", 1);
+parseFontSize("-1000000000000000000000", 1);
+parseFontSize("-1.72", 2);
+parseFontSize("-1.72e9", 2);
+
+// Plus sign
+parseFontSize("+0", 3);
+parseFontSize("+1", 4);
+parseFontSize("+2", 5);
+parseFontSize("+3", 6);
+parseFontSize("+4", 7);
+parseFontSize("+5", 7);
+parseFontSize("+6", 7);
+parseFontSize("+7", 7);
+parseFontSize("+8", 7);
+parseFontSize("+9", 7);
+parseFontSize("+10", 7);
+parseFontSize("+1000000000000000000000", 7);
+parseFontSize("+1.72", 4);
+parseFontSize("+1.72e9", 4);
+
+// Whitespace
+parseFontSize(" \t\n\r\f5 \t\n\r\f", 5);
+parseFontSize("\u00a05", null);
+parseFontSize("\b5", null);
+</script>
diff --git a/editor/libeditor/tests/test_bug480972.html b/editor/libeditor/tests/test_bug480972.html
new file mode 100644
index 000000000..3eed97100
--- /dev/null
+++ b/editor/libeditor/tests/test_bug480972.html
@@ -0,0 +1,98 @@
+<!DOCTYPE HTML>
+<html><head>
+<title>Test for bug 480972</title>
+<style src="/tests/SimpleTest/test.css" type="text/css"></style>
+<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+<script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+
+<script class="testbody" type="application/javascript">
+
+function runTest() {
+ function verifyContent(s) {
+ var e = document.getElementById('i1');
+ var doc = e.contentDocument;
+ is(doc.body.innerHTML, s, "");
+ }
+
+ function pasteInto(html,target_id) {
+ var e = document.getElementById('i1');
+ var doc = e.contentDocument;
+ doc.designMode = "on";
+ doc.body.innerHTML = html;
+ doc.defaultView.focus();
+ if (target_id)
+ e = doc.getElementById(target_id);
+ else
+ e = doc.body;
+ var selection = doc.defaultView.getSelection();
+ selection.removeAllRanges();
+ selection.selectAllChildren(e);
+ selection.collapseToEnd();
+ SpecialPowers.wrap(doc).execCommand("paste", false, null);
+ return e;
+ }
+
+ function copyToClipBoard(s,asHTML,target_id) {
+ var e = document.getElementById('i2');
+ var doc = e.contentDocument;
+ if (asHTML) {
+ doc.body.innerHTML = s;
+ } else {
+ var text = doc.createTextNode(s);
+ doc.body.appendChild(text);
+ }
+ doc.designMode = "on";
+ doc.defaultView.focus();
+ var selection = doc.defaultView.getSelection();
+ selection.removeAllRanges();
+ if (!target_id) {
+ selection.selectAllChildren(doc.body);
+ } else {
+ var range = document.createRange();
+ range.selectNode(doc.getElementById(target_id));
+ selection.addRange(range);
+ }
+ SpecialPowers.wrap(doc).execCommand("copy", false, null);
+ return e;
+ }
+
+ copyToClipBoard('<span>Hello</span><span>Kitty</span>', true);
+ pasteInto('');
+ verifyContent('<span>Hello</span><span>Kitty</span>');
+
+ copyToClipBoard("<dl><dd>Hello Kitty</dd></dl><span>Hello</span><span>Kitty</span>", true);
+ pasteInto('<ol><li id="paste_here">X</li></ol>',"paste_here");
+ verifyContent('<ol><li id="paste_here">X<dl><dd>Hello Kitty</dd></dl><span>Hello</span><span>Kitty</span></li></ol>');
+
+// The following test doesn't do what I expected, because the special handling
+// of IsList nodes in nsHTMLEditor::InsertHTMLWithContext simply removes
+// non-list/item children. See bug 481177.
+// copyToClipBoard("<ol><li>Hello Kitty</li><span>Hello</span></ol>", true);
+// pasteInto('<ol><li id="paste_here">X</li></ol>',"paste_here");
+// verifyContent('<ol><li id="paste_here">X</li><li>Hello Kitty</li><span>Hello</span></ol>');
+
+ copyToClipBoard("<pre>Kitty</pre><span>Hello</span>", true);
+ pasteInto('<pre id="paste_here">Hello </pre>',"paste_here");
+ verifyContent('<pre id="paste_here">Hello Kitty<span>Hello</span></pre>');
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=480972">Mozilla Bug 480972</a>
+<p id="display"></p>
+
+<pre id="test">
+</pre>
+
+
+<iframe id="i1" width="200" height="100" src="about:blank"></iframe><br>
+<iframe id="i2" width="200" height="100" src="about:blank"></iframe><br>
+
+</body>
+</html>
+
diff --git a/editor/libeditor/tests/test_bug483651.html b/editor/libeditor/tests/test_bug483651.html
new file mode 100644
index 000000000..ee256b807
--- /dev/null
+++ b/editor/libeditor/tests/test_bug483651.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=483651
+-->
+
+<head>
+ <title>Test for Bug 483651</title>
+ <script src="/MochiKit/packed.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+
+<body onload="doTest();">
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=483651">Mozilla Bug 483651</a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+
+ <pre id="test">
+ <script type="application/javascript">
+
+ /** Test for Bug 483651 **/
+
+ SimpleTest.waitForExplicitFinish();
+
+ function doTest() {
+ var t1 = $("t1");
+ var editor = SpecialPowers.wrap(t1).editor;
+
+ ok(editor, "able to get editor for the element");
+ t1.focus();
+ synthesizeKey("A", {});
+ synthesizeKey("VK_BACK_SPACE", {});
+
+ try {
+ // Was the trailing br removed?
+ is(editor.documentIsEmpty, true, "trailing <br> correctly removed");
+ } catch (e) {
+ ok(false, "test failed with error "+e);
+ }
+ SimpleTest.finish();
+ }
+ </script>
+ </pre>
+
+ <textarea id="t1" rows="2" columns="80"></textarea>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug484181.html b/editor/libeditor/tests/test_bug484181.html
new file mode 100644
index 000000000..55cd8e806
--- /dev/null
+++ b/editor/libeditor/tests/test_bug484181.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=484181
+-->
+<head>
+ <title>Test for Bug 484181</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="spellcheck.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=484181">Mozilla Bug 484181</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 484181 **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+
+var gMisspeltWords;
+
+function getEditor() {
+ var Ci = SpecialPowers.Ci;
+ var win = window;
+ var editingSession = SpecialPowers.wrap(win).QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIEditingSession);
+ return editingSession.getEditorForWindow(win);
+}
+
+function append(str) {
+ var edit = document.getElementById("edit");
+ var editor = getEditor();
+ var sel = editor.selection;
+ sel.selectAllChildren(edit);
+ sel.collapseToEnd();
+
+ for (var i = 0; i < str.length; ++i) {
+ synthesizeKey(str[i], {});
+ }
+}
+
+function runTest() {
+ gMisspeltWords = ["haz", "cheezburger"];
+ var edit = document.getElementById("edit");
+ edit.focus();
+
+ SpecialPowers.Cu.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm", window);
+ onSpellCheck(edit, function () {
+ ok(isSpellingCheckOk(getEditor(), gMisspeltWords),
+ "All misspellings before editing are accounted for.");
+
+ append(" becaz I'm a lulcat!");
+ onSpellCheck(edit, function () {
+ gMisspeltWords.push("becaz");
+ gMisspeltWords.push("lulcat");
+ ok(isSpellingCheckOk(getEditor(), gMisspeltWords),
+ "All misspellings after typing are accounted for.");
+
+ SimpleTest.finish();
+ });
+ });
+}
+</script>
+</pre>
+
+<div><div></div><div id="edit" contenteditable="true">I can haz cheezburger</div></div>
+
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug487524.html b/editor/libeditor/tests/test_bug487524.html
new file mode 100644
index 000000000..d4972ba91
--- /dev/null
+++ b/editor/libeditor/tests/test_bug487524.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html><head>
+<title>Test for bug 487524</title>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+<script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+
+<script class="testbody" type="application/javascript">
+function runTest() {
+
+ function setupIframe(e,html,focus_id) {
+ var doc = e.contentDocument;
+ doc.body.innerHTML = html;
+ doc.designMode = "on";
+ e = doc.getElementById(focus_id);
+ doc.defaultView.focus();
+ if (e) e.focus();
+ return e;
+ }
+
+ var i1 = document.getElementById('i1')
+ var li1 = setupIframe(i1,'<ul><li id="li1">one</li><li>two</li><ul><li>a</li></ul></ul>','li1')
+ var doc = li1.ownerDocument;
+
+ var selection = doc.defaultView.getSelection();
+ selection.removeAllRanges();
+
+ var range = doc.createRange();
+ range.setStart(li1,0);
+ range.setEnd(li1.nextSibling,0);
+ selection.addRange(range);
+
+ sendKey('delete');
+ is(doc.body.innerHTML,'<ul><li>two</li><ul><li>a</li></ul></ul>','delete 1st LI');
+
+ var li2 = setupIframe(i1,'<ul><li id="li2">two</li><ul><li>a</li></ul></ul>','li2')
+ selection = doc.defaultView.getSelection();
+ selection.removeAllRanges();
+
+ range = doc.createRange();
+ range.setStart(li2,0);
+ range.setEnd(li2.nextSibling.firstChild,0);
+ selection.addRange(range);
+
+ sendKey('delete');
+ is(doc.body.innerHTML,'<ul><ul><li>a</li></ul></ul>','delete 2nd LI');
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=487524">Mozilla Bug 487524</a>
+<p id="display"></p>
+
+<pre id="test">
+</pre>
+
+<iframe id="i1" width="200" height="100" src="about:blank"></iframe><br>
+
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug489202.xul b/editor/libeditor/tests/test_bug489202.xul
new file mode 100644
index 000000000..30e2a730d
--- /dev/null
+++ b/editor/libeditor/tests/test_bug489202.xul
@@ -0,0 +1,81 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin"
+ type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=489202
+-->
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Mozilla Bug 489202" onload="runTest();">
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=489202"
+ target="_blank">Mozilla Bug 489202</a>
+ <p/>
+ <editor xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="i1"
+ type="content"
+ editortype="htmlmail"
+ style="width: 400px; height: 100px;"/>
+ <p/>
+ <pre id="test">
+ </pre>
+ </body>
+ <script class="testbody" type="application/javascript">
+ <![CDATA[
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ var Cc = Components.classes;
+ var Ci = Components.interfaces;
+
+function getLoadContext() {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsILoadContext);
+}
+
+function runTest() {
+ var trans = Cc["@mozilla.org/widget/transferable;1"]
+ .createInstance(Ci.nsITransferable);
+ trans.init(getLoadContext());
+ trans.addDataFlavor("text/html");
+ var test_data = '<meta/><a href="http://mozilla.org/">mozilla.org</a>';
+ var cstr = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ cstr.data = test_data;
+ trans.setTransferData("text/html", cstr, test_data.length*2);
+
+ window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIWebNavigation)
+ .QueryInterface(Components.interfaces.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Components.interfaces.nsIDocShell)
+ .appType = Components.interfaces.nsIDocShell.APP_TYPE_EDITOR;
+ var e = document.getElementById('i1');
+ var doc = e.contentDocument;
+ doc.designMode = "on";
+ doc.body.innerHTML = "";
+ doc.defaultView.focus();
+ var selection = doc.defaultView.getSelection();
+ selection.removeAllRanges();
+ selection.selectAllChildren(doc.body);
+ selection.collapseToEnd();
+
+ var point = doc.defaultView.getSelection().getRangeAt(0).startOffset;
+ ok(point==0, "Cursor should be at editor start before paste");
+
+ utils.sendContentCommandEvent("pasteTransferable", trans);
+
+ point = doc.defaultView.getSelection().getRangeAt(0).startOffset;
+ ok(point>0, "Cursor should not be at editor start after paste");
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+]]>
+</script>
+</window>
diff --git a/editor/libeditor/tests/test_bug490879.html b/editor/libeditor/tests/test_bug490879.html
new file mode 100644
index 000000000..1e412a7d6
--- /dev/null
+++ b/editor/libeditor/tests/test_bug490879.html
@@ -0,0 +1,45 @@
+<!doctype html>
+<title>Mozilla Bug 490879</title>
+<link rel=stylesheet href="/tests/SimpleTest/test.css">
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=490879"
+ target="_blank">Mozilla Bug 490879</a>
+<iframe id="i1" width="200" height="100" src="about:blank"></iframe>
+<img id="i" src="green.png">
+<script>
+function runTest() {
+ function verifyContent() {
+ const kExpectedImgSpec = "data:image/png;base64,";
+ var e = document.getElementById('i1');
+ var doc = e.contentDocument;
+ is(doc.getElementsByTagName("img")[0].src.substring(0, kExpectedImgSpec.length),
+ kExpectedImgSpec, "The pasted image is a base64-encoded data: URI");
+ }
+
+ function pasteInto() {
+ var e = document.getElementById('i1');
+ var doc = e.contentDocument;
+ doc.designMode = "on";
+ doc.defaultView.focus();
+ var selection = doc.defaultView.getSelection();
+ selection.removeAllRanges();
+ selection.selectAllChildren(doc.body);
+ selection.collapseToEnd();
+ SpecialPowers.doCommand(window, "cmd_paste");
+ }
+
+ function copyToClipBoard() {
+ SpecialPowers.setCommandNode(window, document.getElementById("i"));
+ SpecialPowers.doCommand(window, "cmd_copyImageContents");
+ }
+
+ copyToClipBoard();
+ pasteInto();
+ verifyContent();
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+</script>
diff --git a/editor/libeditor/tests/test_bug502673.html b/editor/libeditor/tests/test_bug502673.html
new file mode 100644
index 000000000..3bee4554a
--- /dev/null
+++ b/editor/libeditor/tests/test_bug502673.html
@@ -0,0 +1,108 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=502673
+-->
+
+<head>
+ <title>Test for Bug 502673</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<body onload="doTest();">
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=502673">Mozilla Bug 502673</a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+
+ <pre id="test">
+ <script type="application/javascript">
+
+ /** Test for Bug 502673 **/
+
+ SimpleTest.waitForExplicitFinish();
+
+ function listener() {
+ }
+
+ listener.prototype =
+ {
+ NotifyDocumentWillBeDestroyed: function () {
+ if (this.input instanceof SpecialPowers.Ci.nsIDOMNSEditableElement) {
+ var editor = SpecialPowers.wrap(this.input).editor;
+ editor.removeDocumentStateListener(this);
+ }
+ },
+
+ NotifyDocumentCreated: function () {
+ },
+
+ NotifyDocumentStateChanged: function (aNowDirty) {
+ if (this.input instanceof SpecialPowers.Ci.nsIDOMNSEditableElement) {
+ var editor = SpecialPowers.wrap(this.input).editor;
+ editor.removeDocumentStateListener(this);
+ }
+ },
+
+ QueryInterface: SpecialPowers.wrapCallback(function(iid) {
+ if (iid.equals(SpecialPowers.Ci.nsIDocumentStateListener) ||
+ iid.equals(SpecialPowers.Ci.nsISupports))
+ return this;
+ throw SpecialPowers.Cr.NS_ERROR_NO_INTERFACE;
+ }),
+ };
+
+ function doTest() {
+ var input = document.getElementById("ip");
+ if (input instanceof SpecialPowers.Ci.nsIDOMNSEditableElement) {
+ // Add multiple listeners to the same editor
+ var editor = SpecialPowers.wrap(input).editor;
+ var listener1 = new listener();
+ listener1.input = input;
+ var listener2 = new listener();
+ listener2.input = input;
+ var listener3 = new listener();
+ listener3.input = input;
+ editor.addDocumentStateListener(listener1);
+ editor.addDocumentStateListener(listener2);
+ editor.addDocumentStateListener(listener3);
+
+ // Test 1. Fire NotifyDocumentStateChanged notifications where the
+ // listeners remove themselves
+ input.value = "mozilla";
+ editor.undo(1);
+
+ // Report success if we get here - clearly we didn't crash
+ ok(true, "Multiple listeners removed themselves after " +
+ "NotifyDocumentStateChanged notifications - didn't crash");
+
+ // Add the listeners again for the next test
+ editor.addDocumentStateListener(listener1);
+ editor.addDocumentStateListener(listener2);
+ editor.addDocumentStateListener(listener3);
+
+ }
+
+ // Test 2. Fire NotifyDocumentWillBeDestroyed notifications where the
+ // listeners remove themselves (though in the real world, listeners
+ // shouldn't do this as nsEditor::PreDestroy removes them as
+ // listeners anyway)
+ document.body.removeChild(input);
+ ok(true, "Multiple listeners removed themselves after " +
+ "NotifyDocumentWillBeDestroyed notifications - didn't crash");
+
+ // TODO: Test for NotifyDocumentCreated
+
+ SimpleTest.finish();
+ }
+ </script>
+ </pre>
+
+ <input type="text" id="ip" />
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug514156.html b/editor/libeditor/tests/test_bug514156.html
new file mode 100644
index 000000000..3594d1c8d
--- /dev/null
+++ b/editor/libeditor/tests/test_bug514156.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=514156
+-->
+<head>
+ <title>Test for Bug 514156</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/WindowSnapshot.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="test()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=514156">Mozilla Bug 514156</a>
+<p id="display"></p>
+<div id="content">
+<input type="text" id="input1">
+<input type="text" id="input2">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 514156 **/
+
+SimpleTest.waitForExplicitFinish();
+
+function test() {
+ var input1 = $("input1");
+ input1.focus();
+ synthesizeKey("\u200e", { });
+ synthesizeKey("\u05d0", { });
+ synthesizeKey("\u05d1", { });
+ is(escape(input1.value), escape("\u200e\u05d0\u05d1"), "non-spacing character and direction change shouldn't change content");
+
+ var input2 = $("input2");
+ input2.focus();
+ synthesizeKey("\u05b6", { });
+ synthesizeKey("a", { });
+ synthesizeKey("b", { });
+ synthesizeKey("c", { });
+ is(escape(input2.value), escape("\u05b6abc"), "non-spacing character and direction change shouldn't change content");
+
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/editor/libeditor/tests/test_bug520189.html b/editor/libeditor/tests/test_bug520189.html
new file mode 100644
index 000000000..d1b429000
--- /dev/null
+++ b/editor/libeditor/tests/test_bug520189.html
@@ -0,0 +1,621 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=520182
+-->
+<head>
+ <title>Test for Bug 520182</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=520182">Mozilla Bug 520182</a>
+<p id="display"></p>
+<div id="content">
+ <iframe id="a" src="about:blank"></iframe>
+ <iframe id="b" src="about:blank"></iframe>
+ <iframe id="c" src="about:blank"></iframe>
+ <div id="d" contenteditable="true"></div>
+ <div id="e" contenteditable="true"></div>
+ <div id="f" contenteditable="true"></div>
+ <iframe id="g" src="about:blank"></iframe>
+ <iframe id="h" src="about:blank"></iframe>
+ <div id="i" contenteditable="true"></div>
+ <div id="j" contenteditable="true"></div>
+ <iframe id="k" src="about:blank"></iframe>
+ <div id="l" contenteditable="true"></div>
+ <iframe id="m" src="about:blank"></iframe>
+ <div id="n" contenteditable="true"></div>
+ <iframe id="o" src="about:blank"></iframe>
+ <div id="p" contenteditable="true"></div>
+ <iframe id="q" src="about:blank"></iframe>
+ <div id="r" contenteditable="true"></div>
+ <iframe id="s" src="about:blank"></iframe>
+ <div id="t" contenteditable="true"></div>
+ <iframe id="u" src="about:blank"></iframe>
+ <div id="v" contenteditable="true"></div>
+ <iframe id="w" src="about:blank"></iframe>
+ <div id="x" contenteditable="true"></div>
+ <iframe id="y" src="about:blank"></iframe>
+ <div id="z" contenteditable="true"></div>
+ <iframe id="aa" src="about:blank"></iframe>
+ <div id="bb" contenteditable="true"></div>
+ <iframe id="cc" src="about:blank"></iframe>
+ <div id="dd" contenteditable="true"></div>
+ <iframe id="ee" src="about:blank"></iframe>
+ <div id="ff" contenteditable="true"></div>
+ <iframe id="gg" src="about:blank"></iframe>
+ <div id="hh" contenteditable="true"></div>
+ <iframe id="ii" src="about:blank"></iframe>
+ <div id="jj" contenteditable="true"></div>
+ <iframe id="kk" src="about:blank"></iframe>
+ <div id="ll" contenteditable="true"></div>
+ <iframe id="mm" src="about:blank"></iframe>
+ <div id="nn" contenteditable="true"></div>
+ <iframe id="oo" src="about:blank"></iframe>
+ <div id="pp" contenteditable="true"></div>
+ <iframe id="qq" src="about:blank"></iframe>
+ <div id="rr" contenteditable="true"></div>
+ <iframe id="ss" src="about:blank"></iframe>
+ <div id="tt" contenteditable="true"></div>
+ <iframe id="uu" src="about:blank"></iframe>
+ <div id="vv" contenteditable="true"></div>
+ <div id="sss" contenteditable="true"></div>
+ <iframe id="ssss" src="about:blank"></iframe>
+ <div id="ttt" contenteditable="true"></div>
+ <iframe id="tttt" src="about:blank"></iframe>
+ <div id="uuu" contenteditable="true"></div>
+ <iframe id="uuuu" src="about:blank"></iframe>
+ <div id="vvv" contenteditable="true"></div>
+ <iframe id="vvvv" src="about:blank"></iframe>
+ <div id="www" contenteditable="true"></div>
+ <iframe id="wwww" src="about:blank"></iframe>
+ <div id="xxx" contenteditable="true"></div>
+ <iframe id="xxxx" src="about:blank"></iframe>
+ <div id="yyy" contenteditable="true"></div>
+ <iframe id="yyyy" src="about:blank"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 520182 **/
+
+const dataPayload = "foo<iframe src=\"data:text/html,bar\"></iframe>baz";
+const jsPayload = "foo<iframe src=\"javascript:void('bar');\"></iframe>baz";
+const httpPayload = "foo<iframe src=\"http://mochi.test:8888/\"></iframe>baz";
+const scriptPayload ="foo<script>document.write(\"<iframe></iframe>\");</sc" + "ript>baz";
+const scriptExternalPayload = "foo<script src=\"data:text/javascript,document.write('<iframe></iframe>');\"></sc" + "ript>baz";
+const validStyle1Payload = "foo<style>#bar{color:red;}</style>baz";
+const validStyle2Payload = "foo<span style=\"color:red\">bar</span>baz";
+const validStyle3Payload = "foo<style>@font-face{font-family:xxx;src:'xxx.ttf';}</style>baz";
+const validStyle4Payload = "foo<style>@namespace xxx url(http://example.com/);</style>baz";
+const invalidStyle1Payload = "foo<style>#bar{-moz-binding:url('data:text/xml,<?xml version=\"1.0\"><binding xmlns=\"http://www.mozilla.org/xbl\"/>');}</style>baz";
+const invalidStyle2Payload = "foo<span style=\"-moz-binding:url('data:text/xml,<?xml version=&quot;1.0&quot;><binding xmlns=&quot;http://www.mozilla.org/xbl&quot;/>');\">bar</span>baz";
+const invalidStyle3Payload = "foo<style>@import 'xxx.css';</style>baz";
+const invalidStyle4Payload = "foo<span style=\"@import 'xxx.css';\">bar</span>baz";
+const invalidStyle5Payload = "foo<span style=\"@font-face{font-family:xxx;src:'xxx.ttf';}\">bar</span>baz";
+const invalidStyle6Payload = "foo<span style=\"@namespace xxx url(http://example.com/);\">bar</span>baz";
+const invalidStyle7Payload = "<html><head><title>xxx</title></head><body>foo</body></html>";
+const invalidStyle8Payload = "foo<style>@-moz-document url(http://example.com/) {};</style>baz";
+const invalidStyle9Payload = "foo<style>@-moz-keyframes bar {};</style>baz";
+const nestedStylePayload = "foo<style>#bar1{-moz-binding:url('data:text/xml,<?xml version=&quot;1.0&quot;><binding xmlns=&quot;http://www.mozilla.org/xbl&quot; id=&quot;binding-1&quot;/>');<style></style>#bar2{-moz-binding:url('data:text/xml,<?xml version=&quot;1.0&quot;><binding xmlns=&quot;http://www.mozilla.org/xbl&quot; id=&quot;binding-2&quot;/>');</style>baz";
+const validImgSrc1Payload = "foo<img src=\"data:image/png,bar\">baz";
+const validImgSrc2Payload = "foo<img src=\"javascript:void('bar');\">baz";
+const validImgSrc3Payload = "foo<img src=\"file:///bar.png\">baz";
+const validDataFooPayload = "foo<span data-bar=\"value\">baz</span>";
+const validDataFoo2Payload = "foo<span _bar=\"value\">baz</span>";
+const svgPayload = "foo<svg><title>svgtitle</title></svg>bar";
+const svg2Payload = "foo<svg><bogussvg/></svg>bar";
+const mathPayload = "foo<math><bogusmath/></math>bar";
+const math2Payload = "foo<math><style>@import \"yyy.css\";</style</math>bar";
+const math3Payload = "foo<math><mi></mi></math>bar";
+const videoPayload = "foo<video></video>bar";
+const microdataPayload = "<head><meta name=foo content=bar><link rel=stylesheet href=url></head><body><meta itemprop=foo content=bar><link itemprop=bar href=url></body>";
+
+var tests = [
+ {
+ id: "a",
+ isIFrame: true,
+ payload: dataPayload,
+ iframeCount: 0,
+ rootElement() { return document.getElementById("a").contentDocument.documentElement; },
+ },
+ {
+ id: "b",
+ isIFrame: true,
+ payload: jsPayload,
+ iframeCount: 0,
+ rootElement() { return document.getElementById("b").contentDocument.documentElement; },
+ },
+ {
+ id: "c",
+ isIFrame: true,
+ payload: httpPayload,
+ iframeCount: 0,
+ rootElement() { return document.getElementById("c").contentDocument.documentElement; },
+ },
+ {
+ id: "g",
+ isIFrame: true,
+ payload: scriptPayload,
+ rootElement() { return document.getElementById("g").contentDocument.documentElement; },
+ iframeCount: 0
+ },
+ {
+ id: "h",
+ isIFrame: true,
+ payload: scriptExternalPayload,
+ rootElement() { return document.getElementById("h").contentDocument.documentElement; },
+ iframeCount: 0
+ },
+ {
+ id: "d",
+ payload: dataPayload,
+ iframeCount: 0,
+ rootElement() { return document.getElementById("d"); },
+ },
+ {
+ id: "e",
+ payload: jsPayload,
+ iframeCount: 0,
+ rootElement() { return document.getElementById("e"); },
+ },
+ {
+ id: "f",
+ payload: httpPayload,
+ iframeCount: 0,
+ rootElement() { return document.getElementById("f"); },
+ },
+ {
+ id: "i",
+ payload: scriptPayload,
+ rootElement() { return document.getElementById("i"); },
+ iframeCount: 0
+ },
+ {
+ id: "j",
+ payload: scriptExternalPayload,
+ rootElement() { return document.getElementById("j"); },
+ iframeCount: 0
+ },
+ {
+ id: "k",
+ isIFrame: true,
+ payload: validStyle1Payload,
+ rootElement() { return document.getElementById("k").contentDocument.documentElement; },
+ checkResult(html) { isnot(html.indexOf("style"), -1, "Should have retained style"); },
+ },
+ {
+ id: "l",
+ payload: validStyle1Payload,
+ rootElement() { return document.getElementById("l"); },
+ checkResult(html) { isnot(html.indexOf("style"), -1, "Should have retained style"); },
+ },
+ {
+ id: "m",
+ isIFrame: true,
+ payload: validStyle2Payload,
+ rootElement() { return document.getElementById("m").contentDocument.documentElement; },
+ checkResult(html) { isnot(html.indexOf("style"), -1, "Should have retained style"); },
+ },
+ {
+ id: "n",
+ payload: validStyle2Payload,
+ rootElement() { return document.getElementById("n"); },
+ checkResult(html) { isnot(html.indexOf("style"), -1, "Should have retained style"); },
+ },
+ {
+ id: "o",
+ isIFrame: true,
+ payload: invalidStyle1Payload,
+ rootElement() { return document.getElementById("o").contentDocument.documentElement; },
+ checkResult(html) { is(html.indexOf("binding"), -1, "Should not have retained the binding style"); },
+ },
+ {
+ id: "p",
+ payload: invalidStyle1Payload,
+ rootElement() { return document.getElementById("p"); },
+ checkResult(html) { is(html.indexOf("binding"), -1, "Should not have retained the binding style"); },
+ },
+ {
+ id: "q",
+ isIFrame: true,
+ payload: invalidStyle2Payload,
+ rootElement() { return document.getElementById("q").contentDocument.documentElement; },
+ checkResult(html) { is(html.indexOf("binding"), -1, "Should not have retained the binding style"); },
+ },
+ {
+ id: "r",
+ payload: invalidStyle2Payload,
+ rootElement() { return document.getElementById("r"); },
+ checkResult(html) { is(html.indexOf("binding"), -1, "Should not have retained the binding style"); },
+ },
+ {
+ id: "s",
+ isIFrame: true,
+ payload: invalidStyle1Payload,
+ rootElement() { return document.getElementById("s").contentDocument.documentElement; },
+ checkResult(html) { is(html.indexOf("xxx"), -1, "Should not have retained the import style"); },
+ },
+ {
+ id: "t",
+ payload: invalidStyle1Payload,
+ rootElement() { return document.getElementById("t"); },
+ checkResult(html) { is(html.indexOf("xxx"), -1, "Should not have retained the import style"); },
+ },
+ {
+ id: "u",
+ isIFrame: true,
+ payload: invalidStyle2Payload,
+ rootElement() { return document.getElementById("u").contentDocument.documentElement; },
+ checkResult(html) { is(html.indexOf("xxx"), -1, "Should not have retained the import style"); },
+ },
+ {
+ id: "v",
+ payload: invalidStyle2Payload,
+ rootElement() { return document.getElementById("v"); },
+ checkResult(html) { is(html.indexOf("xxx"), -1, "Should not have retained the import style"); },
+ },
+ {
+ id: "w",
+ isIFrame: true,
+ payload: validStyle3Payload,
+ rootElement() { return document.getElementById("w").contentDocument.documentElement; },
+ checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should have retained the font-face style"); },
+ },
+ {
+ id: "x",
+ payload: validStyle3Payload,
+ rootElement() { return document.getElementById("x"); },
+ checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should have retained the font-face style"); },
+ },
+ {
+ id: "y",
+ isIFrame: true,
+ payload: invalidStyle5Payload,
+ rootElement() { return document.getElementById("y").contentDocument.documentElement; },
+ checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should not have retained the font-face style"); },
+ },
+ {
+ id: "z",
+ payload: invalidStyle5Payload,
+ rootElement() { return document.getElementById("z"); },
+ checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should not have retained the font-face style"); },
+ },
+ {
+ id: "aa",
+ isIFrame: true,
+ payload: nestedStylePayload,
+ rootElement() { return document.getElementById("aa").contentDocument.documentElement; },
+ checkResult: function(html, text) {
+ is(html.indexOf("binding-1"), -1, "Should not have retained the binding-1 style");
+ isnot(text.indexOf("#bar2"), -1, "Should have retained binding-2 as text content");
+ is(text.indexOf("binding-2"), -1, "Should not have retained binding-2 as a tag");
+ }
+ },
+ {
+ id: "bb",
+ payload: nestedStylePayload,
+ rootElement() { return document.getElementById("bb"); },
+ checkResult: function(html, text) {
+ is(html.indexOf("binding-1"), -1, "Should not have retained the binding-1 style");
+ isnot(text.indexOf("#bar2"), -1, "Should have retained binding-2 as text content");
+ is(text.indexOf("binding-2"), -1, "Should not have retained binding-2 as a tag");
+ }
+ },
+ {
+ id: "cc",
+ isIFrame: true,
+ payload: validStyle4Payload,
+ rootElement() { return document.getElementById("cc").contentDocument.documentElement; },
+ checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should have retained the namespace style"); },
+ },
+ {
+ id: "dd",
+ payload: validStyle4Payload,
+ rootElement() { return document.getElementById("dd"); },
+ checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should have retained the namespace style"); },
+ },
+ {
+ id: "ee",
+ isIFrame: true,
+ payload: invalidStyle6Payload,
+ rootElement() { return document.getElementById("ee").contentDocument.documentElement; },
+ checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should not have retained the namespace style"); },
+ },
+ {
+ id: "ff",
+ payload: invalidStyle6Payload,
+ rootElement() { return document.getElementById("ff"); },
+ checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should not have retained the namespace style"); },
+ },
+ {
+ id: "gg",
+ isIFrame: true,
+ payload: invalidStyle6Payload,
+ rootElement() { return document.getElementById("gg").contentDocument.documentElement; },
+ checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the src attribute for the image"); },
+ },
+ {
+ id: "hh",
+ payload: invalidStyle6Payload,
+ rootElement() { return document.getElementById("hh"); },
+ checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the src attribute for the image"); },
+ },
+ {
+ id: "ii",
+ isIFrame: true,
+ payload: invalidStyle6Payload,
+ rootElement() { return document.getElementById("ii").contentDocument.documentElement; },
+ checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the src attribute for the image"); },
+ },
+ {
+ id: "jj",
+ payload: invalidStyle6Payload,
+ rootElement() { return document.getElementById("jj"); },
+ checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the src attribute for the image"); },
+ },
+ {
+ id: "kk",
+ isIFrame: true,
+ payload: invalidStyle6Payload,
+ rootElement() { return document.getElementById("kk").contentDocument.documentElement; },
+ checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the src attribute for the image"); },
+ },
+ {
+ id: "ll",
+ payload: invalidStyle6Payload,
+ rootElement() { return document.getElementById("ll"); },
+ checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the src attribute for the image"); },
+ },
+ {
+ id: "mm",
+ isIFrame: true,
+ indirectPaste: true,
+ payload: invalidStyle7Payload,
+ rootElement() { return document.getElementById("mm").contentDocument.documentElement; },
+ checkResult: function(html) {
+ is(html.indexOf("xxx"), -1, "Should not have retained the title text");
+ isnot(html.indexOf("foo"), -1, "Should have retained the body text");
+ }
+ },
+ {
+ id: "nn",
+ indirectPaste: true,
+ payload: invalidStyle7Payload,
+ rootElement() { return document.getElementById("nn"); },
+ checkResult: function(html) {
+ is(html.indexOf("xxx"), -1, "Should not have retained the title text");
+ isnot(html.indexOf("foo"), -1, "Should have retained the body text");
+ }
+ },
+ {
+ id: "oo",
+ isIFrame: true,
+ payload: validDataFooPayload,
+ rootElement() { return document.getElementById("oo").contentDocument.documentElement; },
+ checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the data-bar attribute"); },
+ },
+ {
+ id: "pp",
+ payload: validDataFooPayload,
+ rootElement() { return document.getElementById("pp"); },
+ checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the data-bar attribute"); },
+ },
+ {
+ id: "qq",
+ isIFrame: true,
+ payload: validDataFoo2Payload,
+ rootElement() { return document.getElementById("qq").contentDocument.documentElement; },
+ checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the _bar attribute"); },
+ },
+ {
+ id: "rr",
+ payload: validDataFoo2Payload,
+ rootElement() { return document.getElementById("rr"); },
+ checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the _bar attribute"); },
+ },
+ {
+ id: "ss",
+ isIFrame: true,
+ payload: invalidStyle8Payload,
+ rootElement() { return document.getElementById("ss").contentDocument.documentElement; },
+ checkResult(html) { is(html.indexOf("@-moz-document"), -1, "Should not have retained the @-moz-document rule"); },
+ },
+ {
+ id: "tt",
+ payload: invalidStyle8Payload,
+ rootElement() { return document.getElementById("tt"); },
+ checkResult(html) { is(html.indexOf("@-moz-document"), -1, "Should not have retained the @-moz-document rule"); },
+ },
+ {
+ id: "uu",
+ isIFrame: true,
+ payload: invalidStyle9Payload,
+ rootElement() { return document.getElementById("uu").contentDocument.documentElement; },
+ checkResult(html) { is(html.indexOf("@-moz-keyframes"), -1, "Should not have retained the @-moz-keyframes rule"); },
+ },
+ {
+ id: "vv",
+ payload: invalidStyle9Payload,
+ rootElement() { return document.getElementById("vv"); },
+ checkResult(html) { is(html.indexOf("@-moz-keyframes"), -1, "Should not have retained the @-moz-keyframes rule"); },
+ },
+ {
+ id: "sss",
+ payload: svgPayload,
+ rootElement() { return document.getElementById("sss"); },
+ checkResult(html) { isnot(html.indexOf("svgtitle"), -1, "Should have retained SVG title"); },
+ },
+ {
+ id: "ssss",
+ isIFrame: true,
+ payload: svgPayload,
+ rootElement() { return document.getElementById("ssss").contentDocument.documentElement; },
+ checkResult(html) { isnot(html.indexOf("svgtitle"), -1, "Should have retained SVG title"); },
+ },
+ {
+ id: "ttt",
+ payload: svg2Payload,
+ rootElement() { return document.getElementById("ttt"); },
+ checkResult(html) { is(html.indexOf("bogussvg"), -1, "Should have dropped bogussvg element"); },
+ },
+ {
+ id: "tttt",
+ isIFrame: true,
+ payload: svg2Payload,
+ rootElement() { return document.getElementById("tttt").contentDocument.documentElement; },
+ checkResult(html) { is(html.indexOf("bogussvg"), -1, "Should have dropped bogussvg element"); },
+ },
+ {
+ id: "uuu",
+ payload: mathPayload,
+ rootElement() { return document.getElementById("uuu"); },
+ checkResult(html) { is(html.indexOf("bogusmath"), -1, "Should have dropped bogusmath element"); },
+ },
+ {
+ id: "uuuu",
+ isIFrame: true,
+ payload: mathPayload,
+ rootElement() { return document.getElementById("uuuu").contentDocument.documentElement; },
+ checkResult(html) { is(html.indexOf("bogusmath"), -1, "Should have dropped bogusmath element"); },
+ },
+ {
+ id: "vvv",
+ payload: math2Payload,
+ rootElement() { return document.getElementById("vvv"); },
+ checkResult(html) { is(html.indexOf("yyy.css"), -1, "Should have dropped MathML style element"); },
+ },
+ {
+ id: "vvvv",
+ isIFrame: true,
+ payload: math2Payload,
+ rootElement() { return document.getElementById("vvvv").contentDocument.documentElement; },
+ checkResult(html) { is(html.indexOf("yyy.css"), -1, "Should have dropped MathML style element"); },
+ },
+ {
+ id: "www",
+ payload: math3Payload,
+ rootElement() { return document.getElementById("www"); },
+ checkResult(html) { isnot(html.indexOf("<mi"), -1, "Should not have dropped MathML mi element"); },
+ },
+ {
+ id: "wwww",
+ isIFrame: true,
+ payload: math3Payload,
+ rootElement() { return document.getElementById("wwww").contentDocument.documentElement; },
+ checkResult(html) { isnot(html.indexOf("<mi"), -1, "Should not have dropped MathML mi element"); },
+ },
+ {
+ id: "xxx",
+ payload: videoPayload,
+ rootElement() { return document.getElementById("xxx"); },
+ checkResult(html) { isnot(html.indexOf("controls="), -1, "Should have added the controls attribute"); },
+ },
+ {
+ id: "xxxx",
+ isIFrame: true,
+ payload: videoPayload,
+ rootElement() { return document.getElementById("xxxx").contentDocument.documentElement; },
+ checkResult(html) { isnot(html.indexOf("controls="), -1, "Should have added the controls attribute"); },
+ },
+ {
+ id: "yyy",
+ payload: microdataPayload,
+ rootElement() { return document.getElementById("yyy"); },
+ checkResult: function(html) { is(html.indexOf("name"), -1, "Should have dropped name."); is(html.indexOf("rel"), -1, "Should have dropped rel."); isnot(html.indexOf("itemprop"), -1, "Should not have dropped itemprop."); }
+ },
+ {
+ id: "yyyy",
+ isIFrame: true,
+ payload: microdataPayload,
+ rootElement() { return document.getElementById("yyyy").contentDocument.documentElement; },
+ checkResult: function(html) { is(html.indexOf("name"), -1, "Should have dropped name."); is(html.indexOf("rel"), -1, "Should have dropped rel."); isnot(html.indexOf("itemprop"), -1, "Should not have dropped itemprop."); }
+ }
+];
+
+function doNextTest() {
+ if (typeof testCounter == "undefined")
+ testCounter = 0;
+ else if (++testCounter == tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ runTest(tests[testCounter]);
+
+ doNextTest();
+}
+
+function getLoadContext() {
+ const Ci = SpecialPowers.Ci;
+ return SpecialPowers.wrap(window)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsILoadContext);
+}
+
+function runTest(test) {
+ var elem = document.getElementById(test.id);
+ if ("isIFrame" in test) {
+ elem.contentDocument.designMode = "on";
+ elem.contentWindow.focus();
+ } else
+ elem.focus();
+
+ var trans = SpecialPowers.Cc["@mozilla.org/widget/transferable;1"]
+ .createInstance(SpecialPowers.Ci.nsITransferable);
+ trans.init(getLoadContext());
+ var data = SpecialPowers.Cc["@mozilla.org/supports-string;1"]
+ .createInstance(SpecialPowers.Ci.nsISupportsString);
+ data.data = test.payload;
+ trans.addDataFlavor("text/html");
+ trans.setTransferData("text/html", data, data.data.length * 2);
+
+ if ("indirectPaste" in test) {
+ var editor, win;
+ if ("isIFrame" in test) {
+ win = elem.contentDocument.defaultView;
+ } else {
+ getSelection().collapse(elem, 0);
+ win = window;
+ }
+ editor = SpecialPowers.wrap(win).QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
+ .getInterface(SpecialPowers.Ci.nsIWebNavigation)
+ .QueryInterface(SpecialPowers.Ci.nsIDocShell)
+ .editor;
+ editor.pasteTransferable(trans);
+ } else {
+ var clipboard = SpecialPowers.Cc["@mozilla.org/widget/clipboard;1"]
+ .getService(SpecialPowers.Ci.nsIClipboard);
+
+ clipboard.setData(trans, null, SpecialPowers.Ci.nsIClipboard.kGlobalClipboard);
+
+ synthesizeKey("V", {accelKey: true});
+ }
+
+ if ("checkResult" in test) {
+ if ("isIFrame" in test) {
+ test.checkResult(elem.contentDocument.documentElement.innerHTML,
+ elem.contentDocument.documentElement.textContent);
+ } else {
+ test.checkResult(elem.innerHTML, elem.textContent);
+ }
+ } else {
+ var iframes = test.rootElement().querySelectorAll("iframe");
+ var expectedIFrameCount = ("iframeCount" in test) ? test.iframeCount : 1;
+ is(iframes.length, expectedIFrameCount, "Only " + expectedIFrameCount + " iframe should be pasted");
+ if (expectedIFrameCount > 0) {
+ ok(!iframes[0].hasAttribute("src"), "iframe should not have a src attrib");
+ }
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(doNextTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug525389.html b/editor/libeditor/tests/test_bug525389.html
new file mode 100644
index 000000000..43916eb51
--- /dev/null
+++ b/editor/libeditor/tests/test_bug525389.html
@@ -0,0 +1,198 @@
+<!DOCTYPE HTML>
+<html><head>
+<title>Test for bug 525389</title>
+<style src="/tests/SimpleTest/test.css" type="text/css"></style>
+<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+<script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+
+<script class="testbody" type="application/javascript">
+
+ var utils = SpecialPowers.wrap(window)
+ .QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor).getInterface(SpecialPowers.Ci.nsIDOMWindowUtils);
+ var Cc = SpecialPowers.Cc;
+ var Ci = SpecialPowers.Ci;
+
+function getLoadContext() {
+ return SpecialPowers.wrap(window)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsILoadContext);
+}
+
+function runTest() {
+ var pasteCount = 0;
+ var pasteFunc = function (event) { pasteCount++; };
+
+ function verifyContent(s) {
+ var e = document.getElementById('i1');
+ var doc = e.contentDocument;
+ if (navigator.platform.indexOf("Win") >= 0) {
+ // On Windows ignore \n which got left over from the removal of the fragment tags
+ // <html><body>\n<!--StartFragment--> and <!--EndFragment-->\n</body>\n</html>.
+ is(doc.body.innerHTML.replace(/\n/g, ""), s, "");
+ } else {
+ is(doc.body.innerHTML, s, "");
+ }
+ }
+
+ function pasteInto(trans, html, target_id) {
+ var e = document.getElementById('i1');
+ var doc = e.contentDocument;
+ doc.designMode = "on";
+ doc.body.innerHTML = html;
+ doc.defaultView.focus();
+ if (target_id)
+ e = doc.getElementById(target_id);
+ else
+ e = doc.body;
+ var selection = doc.defaultView.getSelection();
+ selection.removeAllRanges();
+ selection.selectAllChildren(e);
+ selection.collapseToEnd();
+
+ pasteCount = 0;
+ e.addEventListener("paste", pasteFunc, false);
+ utils.sendContentCommandEvent("pasteTransferable", trans);
+ e.removeEventListener("paste", pasteFunc, false);
+
+ return e;
+ }
+
+ function getTransferableFromClipboard(asHTML) {
+ var trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable);
+ trans.init(getLoadContext());
+ if (asHTML) {
+ trans.addDataFlavor("text/html");
+ } else {
+ trans.addDataFlavor("text/unicode");
+ }
+ var clip = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
+ clip.getData(trans, Ci.nsIClipboard.kGlobalClipboard);
+ return trans;
+ }
+
+ function makeTransferable(s,asHTML,target_id) {
+ var e = document.getElementById('i2');
+ var doc = e.contentDocument;
+ if (asHTML) {
+ doc.body.innerHTML = s;
+ } else {
+ var text = doc.createTextNode(s);
+ doc.body.appendChild(text);
+ }
+ doc.designMode = "on";
+ doc.defaultView.focus();
+ var selection = doc.defaultView.getSelection();
+ selection.removeAllRanges();
+ if (!target_id) {
+ selection.selectAllChildren(doc.body);
+ } else {
+ var range = document.createRange();
+ range.selectNode(doc.getElementById(target_id));
+ selection.addRange(range);
+ }
+
+ // We cannot use plain strings, we have to use nsSupportsString.
+ var supportsStringClass = SpecialPowers.Components.classes["@mozilla.org/supports-string;1"];
+ var ssData = supportsStringClass.createInstance(Ci.nsISupportsString);
+
+ // Create the transferable.
+ var trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable);
+ trans.init(getLoadContext());
+
+ // Add the data to the transferable.
+ if (asHTML) {
+ trans.addDataFlavor("text/html");
+ ssData.data = doc.body.innerHTML;
+ trans.setTransferData("text/html", ssData, ssData.length * 2);
+ } else {
+ trans.addDataFlavor("text/unicode");
+ ssData.data = doc.body.innerHTML;
+ trans.setTransferData("text/unicode", ssData, ssData.length * 2);
+ }
+
+ return trans;
+ }
+
+ function copyToClipBoard(s,asHTML,target_id) {
+ var e = document.getElementById('i2');
+ var doc = e.contentDocument;
+ if (asHTML) {
+ doc.body.innerHTML = s;
+ } else {
+ var text = doc.createTextNode(s);
+ doc.body.appendChild(text);
+ }
+ doc.designMode = "on";
+ doc.defaultView.focus();
+ var selection = doc.defaultView.getSelection();
+ selection.removeAllRanges();
+ if (!target_id) {
+ selection.selectAllChildren(doc.body);
+ } else {
+ var range = document.createRange();
+ range.selectNode(doc.getElementById(target_id));
+ selection.addRange(range);
+ }
+ SpecialPowers.wrap(doc).execCommand("copy", false, null);
+ return e;
+ }
+
+ copyToClipBoard('<span>Hello</span><span>Kitty</span>', true);
+ var trans = getTransferableFromClipboard(true);
+ pasteInto(trans, '');
+ verifyContent('<span>Hello</span><span>Kitty</span>');
+ is(pasteCount, 1, "paste event was not triggered");
+
+ // this test is not working out exactly like the clipboard test
+ // has to do with generating the nsITransferable above
+ //trans = makeTransferable('<span>Hello</span><span>Kitty</span>', true);
+ //pasteInto(trans, '');
+ //verifyContent('<span>Hello</span><span>Kitty</span>');
+
+ copyToClipBoard("<dl><dd>Hello Kitty</dd></dl><span>Hello</span><span>Kitty</span>", true);
+ trans = getTransferableFromClipboard(true);
+ pasteInto(trans, '<ol><li id="paste_here">X</li></ol>',"paste_here");
+ verifyContent('<ol><li id="paste_here">X<dl><dd>Hello Kitty</dd></dl><span>Hello</span><span>Kitty</span></li></ol>');
+ is(pasteCount, 1, "paste event was not triggered");
+
+// The following test doesn't do what I expected, because the special handling
+// of IsList nodes in nsHTMLEditor::InsertHTMLWithContext simply removes
+// non-list/item children. See bug 481177.
+// copyToClipBoard("<ol><li>Hello Kitty</li><span>Hello</span></ol>", true);
+// pasteInto('<ol><li id="paste_here">X</li></ol>',"paste_here");
+// verifyContent('<ol><li id="paste_here">X</li><li>Hello Kitty</li><span>Hello</span></ol>');
+
+ copyToClipBoard("<pre>Kitty</pre><span>Hello</span>", true);
+ trans = getTransferableFromClipboard(true);
+ pasteInto(trans, '<pre id="paste_here">Hello </pre>',"paste_here");
+ verifyContent('<pre id="paste_here">Hello Kitty<span>Hello</span></pre>');
+ is(pasteCount, 1, "paste event was not triggered");
+
+ // test that we can preventDefault pastes
+ pasteFunc = function (event) { event.preventDefault(); return false; };
+ copyToClipBoard("<pre>Kitty</pre><span>Hello</span>", true);
+ trans = getTransferableFromClipboard(true);
+ pasteInto(trans, '<pre id="paste_here">Hello </pre>',"paste_here");
+ verifyContent('<pre id="paste_here">Hello </pre>');
+ is(pasteCount, 0, "paste event was triggered");
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=525389">Mozilla Bug 525389</a>
+<p id="display"></p>
+
+<pre id="test">
+</pre>
+
+<iframe id="i1" width="200" height="100" src="about:blank"></iframe><br>
+<iframe id="i2" width="200" height="100" src="about:blank"></iframe><br>
+
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug537046.html b/editor/libeditor/tests/test_bug537046.html
new file mode 100644
index 000000000..6c3c07b29
--- /dev/null
+++ b/editor/libeditor/tests/test_bug537046.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=537046
+-->
+<head>
+ <title>Test for Bug 537046</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=537046">Mozilla Bug 537046</a>
+<p id="display"></p>
+<div id="content">
+ <div id="editor" contenteditable="true">
+ Some editable content
+ </div>
+ <div id="source" contenteditable="true">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 537046 **/
+
+SimpleTest.expectAssertions(1);
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var ed = document.getElementById("editor");
+ var src = document.getElementById("source");
+ ed.addEventListener("DOMSubtreeModified", function() {
+ src.textContent = ed.innerHTML;
+ }, false);
+ src.addEventListener("DOMSubtreeModified", function() {
+ ed.innerHTML = ed.textContent;
+ }, false);
+
+ // Simulate pressing Enter twice
+ ed.focus();
+ synthesizeKey("VK_RETURN", {});
+ synthesizeKey("VK_RETURN", {});
+
+ ok(true, "Didn't crash!");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug549262.html b/editor/libeditor/tests/test_bug549262.html
new file mode 100644
index 000000000..fa1cbabc4
--- /dev/null
+++ b/editor/libeditor/tests/test_bug549262.html
@@ -0,0 +1,132 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=549262
+-->
+<head>
+ <title>Test for Bug 549262</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=549262">Mozilla Bug 549262</a>
+<p id="display"></p>
+<div id="content">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 549262 **/
+
+var smoothScrollPref = "general.smoothScroll";
+SimpleTest.waitForExplicitFinish();
+var win = window.open("file_bug549262.html", "_blank",
+ "width=600,height=600,scrollbars=yes");
+
+// grab the timer right at the start
+var cwu = SpecialPowers.getDOMWindowUtils(win);
+function step() {
+ cwu.advanceTimeAndRefresh(100);
+}
+SimpleTest.waitForFocus(function() {
+ SpecialPowers.pushPrefEnv({"set":[[smoothScrollPref, false]]}, startTest);
+}, win);
+function startTest() {
+ // Make sure that pressing Space when a contenteditable element is not focused
+ // will scroll the page.
+ var ed = win.document.getElementById("editor");
+ var sc = win.document.querySelector("a");
+ sc.focus();
+ is(win.scrollY, 0, "Sanity check");
+ synthesizeKey(" ", {}, win);
+
+ step();
+
+ isnot(win.scrollY, 0, "Page is scrolled down");
+ is(ed.textContent, "abc", "The content of the editable element has not changed");
+ var oldY = win.scrollY;
+ synthesizeKey(" ", {shiftKey: true}, win);
+
+ step();
+
+ ok(win.scrollY < oldY, "Page is scrolled up");
+ is(ed.textContent, "abc", "The content of the editable element has not changed");
+
+ // Make sure that pressing Space when a contenteditable element is focused
+ // will not scroll the page, and will edit the element.
+ ed.focus();
+ win.getSelection().collapse(ed.firstChild, 1);
+ oldY = win.scrollY;
+ synthesizeKey(" ", {}, win);
+
+ step();
+
+ ok(win.scrollY <= oldY, "Page is not scrolled down");
+ is(ed.textContent, "a bc", "The content of the editable element has changed");
+ sc.focus();
+ synthesizeKey(" ", {}, win);
+
+ step();
+
+ isnot(win.scrollY, 0, "Page is scrolled down");
+ is(ed.textContent, "a bc", "The content of the editable element has not changed");
+ ed.focus();
+ win.getSelection().collapse(ed.firstChild, 3);
+ synthesizeKey(" ", {shiftKey: true}, win);
+
+ step();
+
+ isnot(win.scrollY, 0, "Page is not scrolled up");
+ is(ed.textContent, "a b c", "The content of the editable element has changed");
+
+ // Now let's test the down/up keys
+ sc = document.body;
+
+ step();
+
+ ed.blur();
+ sc.focus();
+ oldY = win.scrollY;
+ synthesizeKey("VK_UP", {}, win);
+
+ step();
+
+ ok(win.scrollY < oldY, "Page is scrolled up");
+ oldY = win.scrollY;
+ ed.focus();
+ win.getSelection().collapse(ed.firstChild, 3);
+ synthesizeKey("VK_UP", {}, win);
+
+ step();
+
+ is(win.scrollY, oldY, "Page is not scrolled up");
+ is(win.getSelection().focusNode, ed.firstChild, "Correct element selected");
+ is(win.getSelection().focusOffset, 0, "Selection should be moved to the beginning");
+ win.getSelection().removeAllRanges();
+ synthesizeMouse(sc, 300, 300, {}, win);
+ synthesizeKey("VK_DOWN", {}, win);
+
+ step();
+
+ ok(win.scrollY > oldY, "Page is scrolled down");
+ ed.focus();
+ win.getSelection().collapse(ed.firstChild, 3);
+ oldY = win.scrollY;
+ synthesizeKey("VK_DOWN", {}, win);
+
+ step();
+
+ is(win.scrollY, oldY, "Page is not scrolled down");
+ is(win.getSelection().focusNode, ed.firstChild, "Correct element selected");
+ is(win.getSelection().focusOffset, ed.textContent.length, "Selection should be moved to the end");
+
+ win.close();
+ cwu.restoreNormalRefresh();
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug550434.html b/editor/libeditor/tests/test_bug550434.html
new file mode 100644
index 000000000..0fa3ad159
--- /dev/null
+++ b/editor/libeditor/tests/test_bug550434.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=550434
+-->
+<head>
+ <title>Test for Bug 550434</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=550434">Mozilla Bug 550434</a>
+<p id="display"></p>
+<div id="content">
+ <div id="editor" contenteditable="true"
+ style="height: 250px; height: 200px; border: 4px solid red; outline: none;"></div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 550434 **/
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var ed = document.getElementById("editor");
+
+ // Simulate click twice
+ synthesizeMouse(ed, 10, 10, {});
+ synthesizeMouse(ed, 50, 50, {});
+ setTimeout(function() {
+ synthesizeKey("x", {});
+
+ is(ed.innerHTML, "x", "Editor should work after being clicked twice");
+ SimpleTest.finish();
+ }, 0);
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug551704.html b/editor/libeditor/tests/test_bug551704.html
new file mode 100644
index 000000000..8f335276f
--- /dev/null
+++ b/editor/libeditor/tests/test_bug551704.html
@@ -0,0 +1,125 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=551704
+-->
+<head>
+ <title>Test for Bug 551704</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=551704">Mozilla Bug 551704</a>
+<p id="display"></p>
+<div id="content">
+ <div id="preformatted" style="white-space: pre" contenteditable>a&#10;b</div>
+ <div id="test1" contenteditable><br></div>
+ <div id="test2" contenteditable>a<br></div>
+ <div id="test3" contenteditable style="white-space: pre"><br></div>
+ <div id="test4" contenteditable style="white-space: pre">a<br></div>
+ <div id="test5" contenteditable></div>
+ <div id="test6" contenteditable>a</div>
+ <div id="test7" contenteditable style="white-space: pre"></div>
+ <div id="test8" contenteditable style="white-space: pre">a</div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+function testLineBreak(div, type, expectedText, expectedHTML, callback)
+{
+ div.focus();
+ getSelection().collapse(div, 0);
+ type();
+ is(div.innerHTML, expectedHTML, "The expected HTML after editing should be correct");
+ SimpleTest.waitForClipboard(expectedText,
+ function() {
+ getSelection().selectAllChildren(div);
+ synthesizeKey("C", {accelKey: true});
+ },
+ function() {
+ var t = document.createElement("textarea");
+ document.body.appendChild(t);
+ t.focus();
+ synthesizeKey("V", {accelKey: true});
+ is(t.value, expectedText, "The expected text should be copied to the clipboard");
+ callback();
+ },
+ function() {
+ SimpleTest.finish();
+ }
+ );
+}
+
+function typeABCDEF() {
+ synthesizeKey("a", {});
+ typeBCDEF_chars();
+}
+
+function typeBCDEF() {
+ synthesizeKey("VK_RIGHT", {});
+ typeBCDEF_chars();
+}
+
+function typeBCDEF_chars() {
+ synthesizeKey("b", {});
+ synthesizeKey("c", {});
+ synthesizeKey("VK_RETURN", {});
+ synthesizeKey("d", {});
+ synthesizeKey("e", {});
+ synthesizeKey("f", {});
+}
+
+/** Test for Bug 551704 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var preformatted = document.getElementById("preformatted");
+ is(preformatted.innerHTML, "a\nb", "No BR node should be injected for preformatted editable fields");
+
+ var iframe = document.createElement("iframe");
+ iframe.addEventListener("load", function() {
+ var sel = iframe.contentWindow.getSelection();
+ is(sel.rangeCount, 0, "There should be no range in the selection initially");
+ iframe.contentDocument.designMode = "on";
+ sel = iframe.contentWindow.getSelection();
+ is(sel.rangeCount, 1, "There should be a single range in the selection after setting designMode");
+ var range = sel.getRangeAt(0);
+ ok(range.collapsed, "The range should be collapsed");
+ is(range.startContainer, iframe.contentDocument.body.firstChild, "The range should start on the text");
+ is(range.startOffset, 0, "The start offset should be zero");
+
+ continueTest();
+ }, false);
+ iframe.src = "data:text/html,foo";
+ document.getElementById("content").appendChild(iframe);
+});
+
+function continueTest() {
+ var divs = [];
+ for (var i = 0; i < 8; ++i) {
+ divs[i] = document.getElementById("test" + (i+1));
+ }
+ var current = 0;
+ function doNextTest() {
+ if (current == divs.length) {
+ SimpleTest.finish();
+ return;
+ }
+ var div = divs[current++];
+ if (div.textContent == "a") {
+ var type = typeBCDEF;
+ } else {
+ var type = typeABCDEF;
+ }
+ var expectedHTML = "abc<br>def<br>";
+ var expectedText = "abc\ndef";
+ testLineBreak(div, type, expectedText, expectedHTML, doNextTest);
+ }
+
+ doNextTest();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug552782.html b/editor/libeditor/tests/test_bug552782.html
new file mode 100644
index 000000000..5c53e92c1
--- /dev/null
+++ b/editor/libeditor/tests/test_bug552782.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=552782
+-->
+<head>
+ <title>Test for Bug 552782</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=290026">Mozilla Bug 552782</a>
+<p id="display"></p>
+<div id="editor" contenteditable></div>
+
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 552782 **/
+SimpleTest.waitForExplicitFinish();
+
+var original = '<ol><li>Item 1</li><ol><li>Item 2</li><li>Item 3</li><li>Item 4</li></ol></ol>';
+var editor = document.getElementById("editor");
+editor.innerHTML = original;
+editor.focus();
+
+addLoadEvent(function() {
+
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+ var lis = document.getElementsByTagName("li");
+ sel.selectAllChildren(lis[2]);
+ document.execCommand("outdent", false, false);
+ var expected = '<ol><li>Item 1</li><ol><li>Item 2</li></ol><li>Item 3</li><ol><li>Item 4</li></ol></ol>';
+ is(editor.innerHTML, expected, "outdenting third item in a partially indented numbered list");
+ document.execCommand("indent", false, false);
+ todo_is(editor.innerHTML, original, "re-indenting third item in a partially indented numbered list");
+
+ // done
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug567213.html b/editor/libeditor/tests/test_bug567213.html
new file mode 100644
index 000000000..22418f9e2
--- /dev/null
+++ b/editor/libeditor/tests/test_bug567213.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=567213
+-->
+
+<head>
+ <title>Test for Bug 567213</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=567213">Mozilla Bug 567213</a>
+ <p id="display"></p>
+ <div id="content">
+ <div id="target" contenteditable="true">test</div>
+ <button id="thief">theif</button>
+ </div>
+
+ <pre id="test">
+ <script type="application/javascript">
+
+ /** Test for Bug 567213 **/
+
+ SimpleTest.waitForExplicitFinish();
+
+ addLoadEvent(function() {
+ var target = document.getElementById("target");
+ var thief = document.getElementById("thief");
+ var sel = window.getSelection();
+
+ // select the contents of the editable area
+ sel.removeAllRanges();
+ sel.selectAllChildren(target);
+ target.focus();
+
+ // press some key
+ synthesizeKey("X", {});
+ is(target.textContent, "X", "Text input should work (sanity check)");
+
+ // select the contents of the editable area again
+ sel.removeAllRanges();
+ sel.selectAllChildren(target);
+ thief.focus();
+
+ // press some key with the thief having focus
+ synthesizeKey("Y", {});
+ is(target.textContent, "X", "Text entry should not work with another element focused");
+
+ SimpleTest.finish();
+ });
+
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug569988.html b/editor/libeditor/tests/test_bug569988.html
new file mode 100644
index 000000000..e42bbb985
--- /dev/null
+++ b/editor/libeditor/tests/test_bug569988.html
@@ -0,0 +1,99 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=569988
+-->
+<head>
+ <title>Test for Bug 569988</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=569988">Mozilla Bug 569988</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 569988 **/
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTest);
+
+
+function runTest()
+{
+ var script = SpecialPowers.loadChromeScript(function() {
+ var gPromptInput = null;
+ var os = Components.classes["@mozilla.org/observer-service;1"]
+ .getService(Components.interfaces.nsIObserverService);
+
+ os.addObserver(onPromptLoad, "common-dialog-loaded", false);
+ os.addObserver(onPromptLoad, "tabmodal-dialog-loaded", false);
+
+ function onPromptLoad(subject, topic, data) {
+ sendAsyncMessage("ok", [true, "onPromptLoad is called"]);
+ gPromptInput = subject.Dialog.ui.loginTextbox;
+ gPromptInput.addEventListener("focus", onPromptFocus, false);
+ // shift focus to ensure it fires.
+ subject.Dialog.ui.button0.focus();
+ gPromptInput.focus();
+ }
+
+ function onPromptFocus() {
+ sendAsyncMessage("ok", [true, "onPromptFocus is called"]);
+ gPromptInput.removeEventListener("focus", onPromptFocus, false);
+
+ var listenerService =
+ Components.classes["@mozilla.org/eventlistenerservice;1"]
+ .getService(Components.interfaces.nsIEventListenerService);
+
+ var listener = {
+ handleEvent: function _hv(aEvent) {
+ var isPrevented = aEvent.defaultPrevented;
+ sendAsyncMessage("ok", [!isPrevented,
+ "ESC key event is prevented by editor"]);
+ listenerService.removeSystemEventListener(gPromptInput, "keypress",
+ listener, false);
+ }
+ };
+ listenerService.addSystemEventListener(gPromptInput, "keypress",
+ listener, false);
+
+ sendAsyncMessage("info", "sending key");
+ var EventUtils = {};
+ EventUtils.window = {};
+ EventUtils._EU_Ci = Components.interfaces;
+ EventUtils._EU_Cc = Components.classes;
+ Components.classes['@mozilla.org/moz/jssubscript-loader;1']
+ .getService(Components.interfaces.mozIJSSubScriptLoader)
+ .loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ EventUtils);
+ EventUtils.synthesizeKey("VK_ESCAPE", {},
+ gPromptInput.ownerDocument.defaultView);
+ }
+
+ addMessageListener("destroy", function() {
+ os.removeObserver(onPromptLoad, "tabmodal-dialog-loaded");
+ os.removeObserver(onPromptLoad, "common-dialog-loaded");
+ });
+ });
+ script.addMessageListener("ok", ([val, msg]) => ok(val, msg));
+ script.addMessageListener("info", msg => info(msg));
+
+ info("opening prompt...");
+ prompt("summary", "text");
+ info("prompt is closed");
+
+ script.sendSyncMessage("destroy");
+
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug570144.html b/editor/libeditor/tests/test_bug570144.html
new file mode 100644
index 000000000..e3b98b8d6
--- /dev/null
+++ b/editor/libeditor/tests/test_bug570144.html
@@ -0,0 +1,123 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=570144
+-->
+<head>
+ <title>Test for Bug 570144</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=570144">Mozilla Bug 570144</a>
+<p id="display"></p>
+<div id="content">
+ <!-- editable paragraphs in list item -->
+ <section id="test1">
+ <ol>
+ <li><p contenteditable>foo</p></li>
+ </ol>
+ <ul>
+ <li><p contenteditable>foo</p></li>
+ </ul>
+ <dl>
+ <dt>foo</dt>
+ <dd><p contenteditable>bar</p></dd>
+ </dl>
+ </section>
+ <!-- paragraphs in editable list item -->
+ <section id="test2">
+ <ol>
+ <li contenteditable><p>foo</p></li>
+ </ol>
+ <ul>
+ <li contenteditable><p>foo</p></li>
+ </ul>
+ <dl>
+ <dt>foo</dt>
+ <dd contenteditable><p>bar</p></dd>
+ </dl>
+ </section>
+ <!-- paragraphs in editable list -->
+ <section id="test3">
+ <ol contenteditable>
+ <li><p>foo</p></li>
+ </ol>
+ <ul contenteditable>
+ <li><p>foo</p></li>
+ </ul>
+ <dl contenteditable>
+ <dt>foo</dt>
+ <dd><p>bar</p></dd>
+ </dl>
+ </section>
+</div>
+
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 570144 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+function try2split(list) {
+ var editor = list.hasAttribute("contenteditable")
+ ? list : list.querySelector("*[contenteditable]");
+ editor.focus();
+ // put the caret at the end of the paragraph
+ var selection = window.getSelection();
+ if (editor.nodeName.toLowerCase() == "p")
+ selection.selectAllChildren(editor);
+ else
+ selection.selectAllChildren(editor.querySelector("p"));
+ selection.collapseToEnd();
+ // simulate a [Return] keypress
+ synthesizeKey("VK_RETURN", {});
+}
+
+function testSection(element, context, shouldCreateLI, shouldCreateP) {
+ var nbLI = shouldCreateLI ? 2 : 1; // number of expected list items
+ var nbP = shouldCreateP ? 2 : 1; // number of expected paragraphs
+
+ function message(nodeName, dup) {
+ return context + ":[Return] should " + (dup ? "" : "not ")
+ + "create another <" + nodeName + ">."
+ }
+ var msgP = message("p", shouldCreateP);
+ var msgLI = message("li", shouldCreateLI);
+ var msgDT = message("dt", shouldCreateLI);
+ var msgDD = message("dd", false);
+
+ const ol = element.querySelector("ol");
+ try2split(ol);
+ is(ol.querySelectorAll("li").length, nbLI, msgLI);
+ is(ol.querySelectorAll("p").length, nbP, msgP);
+
+ const ul = element.querySelector("ul");
+ try2split(ul);
+ is(ul.querySelectorAll("li").length, nbLI, msgLI);
+ is(ul.querySelectorAll("p").length, nbP, msgP);
+
+ const dl = element.querySelector("dl");
+ try2split(dl);
+ is(dl.querySelectorAll("dt").length, nbLI, msgDT);
+ is(dl.querySelectorAll("dd").length, 1, msgDD);
+ is(dl.querySelectorAll("p").length, nbP, msgP);
+}
+
+function runTests() {
+ testSection(document.getElementById("test1"), "editable paragraph in list item", false, false);
+ testSection(document.getElementById("test2"), "paragraph in editable list item", false, true);
+ testSection(document.getElementById("test3"), "paragraph in editable list", true, false);
+ /* Note: concerning #test3, it would be preferrable that [Return] creates
+ * another paragraph in another list item (i.e. last argument = 'true').
+ * Currently it just creates an empty list item, which is acceptable.
+ */
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug578771.html b/editor/libeditor/tests/test_bug578771.html
new file mode 100644
index 000000000..09f163c51
--- /dev/null
+++ b/editor/libeditor/tests/test_bug578771.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ - You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=578771
+-->
+
+<head>
+ <title>Test for Bug 578771</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=578771">Mozilla Bug 578771</a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+
+ <pre id="test">
+ <script type="application/javascript">
+
+ /** Test for Bug 578771 **/
+ SimpleTest.waitForExplicitFinish();
+
+ function testElem(elem, elemTag) {
+ var ce = document.getElementById("ce");
+ ce.focus();
+
+ synthesizeMouse(elem, 5, 5, {clickCount: 2 });
+ ok(elem.selectionStart == 0 && elem.selectionEnd == 7,
+ " Double-clicking on another " + elemTag + " works correctly");
+
+ ce.focus();
+ synthesizeMouse(elem, 5, 5, {clickCount: 3 });
+ ok(elem.selectionStart == 0 && elem.selectionEnd == 14,
+ "Triple-clicking on another " + elemTag + " works correctly");
+ }
+ // Avoid platform selection differences
+ SimpleTest.waitForFocus(function() {
+ SpecialPowers.pushPrefEnv({"set":[["layout.word_select.eat_space_to_next_word", false]]}, startTest);
+ });
+
+ function startTest() {
+ var input = document.getElementById("ip");
+ testElem(input, "input");
+
+ var textarea = document.getElementById("ta");
+ testElem(textarea, "textarea");
+
+ SimpleTest.finish();
+ }
+ </script>
+ </pre>
+
+ <input id="ip" type="text" value="Mozilla editor" />
+ <textarea id="ta">Mozilla editor</textarea>
+ <div id="ce" contenteditable="true">Contenteditable div that could interfere with focus</div>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug586662.html b/editor/libeditor/tests/test_bug586662.html
new file mode 100644
index 000000000..36c56d759
--- /dev/null
+++ b/editor/libeditor/tests/test_bug586662.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=586662
+-->
+
+<head>
+ <title>Test for Bug 586662</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=586662">Mozilla Bug 586662</a>
+ <p id="display"><textarea onkeypress="this.style.overflow = 'hidden'"></textarea></p>
+ <div id="content" style="display: none">
+ </div>
+
+ <pre id="test">
+ <script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var t = document.querySelector("textarea");
+ t.focus();
+ synthesizeKey("a", {});
+ is(getComputedStyle(t, null).overflow, "hidden", "The event handler should be executed");
+ is(t.value, "a", "The key entry should result in a character being added to the field");
+
+ var win = window.open("file_bug586662.html", "_blank",
+ "width=600,height=600,scrollbars=yes");
+ SimpleTest.waitForFocus(function() {
+ // Make sure that focusing the textarea will cause the page to scroll
+ var ed = win.document.getElementById("editor");
+ ed.focus();
+ setTimeout(function() {
+ isnot(win.scrollY, 0, "Page is scrolled down");
+ // Scroll back up
+ win.scrollTo(0, 0);
+ setTimeout(function() {
+ is(win.scrollY, 0, "Page is scrolled back up");
+ // Make sure that typing something into the textarea will cause the
+ // page to scroll down
+ synthesizeKey("a", {}, win);
+ setTimeout(function() {
+ isnot(win.scrollY, 0, "Page is scrolled down again");
+
+ win.close();
+ SimpleTest.finish();
+ }, 0);
+ }, 0);
+ }, 0);
+ }, win);
+});
+
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug587461.html b/editor/libeditor/tests/test_bug587461.html
new file mode 100644
index 000000000..2cf9f29fc
--- /dev/null
+++ b/editor/libeditor/tests/test_bug587461.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=587461
+-->
+<title>Test for Bug 587461</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=587461">Mozilla Bug 587461</a>
+<div contenteditable><b>foobar</b></div>
+<script>
+var div = document.querySelector("div");
+getSelection().collapse(div.firstChild.firstChild, 3);
+document.execCommand("inserthtml", false, "a");
+is(div.innerHTML, "<b>fooabar</b>", "innerHTML");
+</script>
diff --git a/editor/libeditor/tests/test_bug590554.html b/editor/libeditor/tests/test_bug590554.html
new file mode 100644
index 000000000..bc98503ed
--- /dev/null
+++ b/editor/libeditor/tests/test_bug590554.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=590554
+-->
+
+<head>
+ <title>Test for Bug 590554</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<body>
+
+ <script type="application/javascript">
+
+ /** Test for Bug 590554 **/
+
+ SimpleTest.waitForExplicitFinish();
+
+ SimpleTest.waitForFocus(function() {
+ var t = document.querySelector("textarea");
+ t.focus();
+ synthesizeKey("VK_RETURN", {});
+ is(t.value, "\n", "Pressing enter should work the first time");
+ synthesizeKey("VK_RETURN", {});
+ is(t.value, "\n", "Pressing enter should not work the second time");
+ SimpleTest.finish();
+ });
+
+ </script>
+
+ <textarea maxlength="1"></textarea>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug592592.html b/editor/libeditor/tests/test_bug592592.html
new file mode 100644
index 000000000..834ecbe1d
--- /dev/null
+++ b/editor/libeditor/tests/test_bug592592.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=592592
+-->
+<head>
+ <title>Test for Bug 592592</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=592592">Mozilla Bug 592592</a>
+<p id="display"></p>
+<div id="content">
+ <div id="editor" contenteditable="true" style="white-space:pre-wrap">a b</div>
+ <div id="editor2" contenteditable="true" style="white-space:pre-wrap">a b</div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 592592 **/
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var ed = document.getElementById("editor");
+
+ // Put the selection right after "a"
+ ed.focus();
+ window.getSelection().collapse(ed.firstChild, 1);
+
+ // Press space
+ synthesizeKey(" ", {});
+
+ // Make sure we haven't added an nbsp
+ is(ed.innerHTML, "a b", "We should not be adding an &nbsp; for preformatted text");
+
+ // Remove the preformatted style
+ ed.removeAttribute("style");
+
+ // Reset the DOM
+ ed.innerHTML = "a b";
+
+ // Reset the selection
+ ed.focus();
+ window.getSelection().collapse(ed.firstChild, 1);
+
+ // Press space
+ synthesizeKey(" ", {});
+
+ // Make sure that we have added an nbsp
+ is(ed.innerHTML, "a&nbsp; b", "We should add an &nbsp; for non-preformatted text");
+
+ ed = document.getElementById("editor2");
+
+ // Put the selection after the second space in the second editable field
+ ed.focus();
+ window.getSelection().collapse(ed.firstChild, 3);
+
+ // Press the back-space key
+ synthesizeKey("VK_BACK_SPACE", {});
+
+ // Make sure that we've only deleted a single space
+ is(ed.innerHTML, "a b", "We should only be deleting a single space");
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug596001.html b/editor/libeditor/tests/test_bug596001.html
new file mode 100644
index 000000000..c677df359
--- /dev/null
+++ b/editor/libeditor/tests/test_bug596001.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=596001
+-->
+<head>
+ <title>Test for Bug 596001</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=596001">Mozilla Bug 596001</a>
+<p id="display"></p>
+<div id="content">
+<textarea id="src">a&#9;b</textarea>
+<textarea id="dst"></textarea>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 596001 **/
+
+function testTab(prefix, callback) {
+ var src = document.getElementById("src");
+ var dst = document.getElementById("dst");
+ dst.value = prefix;
+ src.focus();
+ src.select();
+ SimpleTest.waitForClipboard("a\tb",
+ function() {
+ synthesizeKey("c", {accelKey: true});
+ },
+ function() {
+ dst.focus();
+ var inputReceived = false;
+ dst.addEventListener("input", function() { inputReceived = true; }, false);
+ synthesizeKey("v", {accelKey: true});
+ ok(inputReceived, "An input event should be raised");
+ is(dst.value, prefix + src.value, "The value should be pasted verbatim");
+ callback();
+ },
+ callback
+ );
+}
+
+testTab("", function() {
+ testTab("foo", function() {
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug596333.html b/editor/libeditor/tests/test_bug596333.html
new file mode 100644
index 000000000..a94726325
--- /dev/null
+++ b/editor/libeditor/tests/test_bug596333.html
@@ -0,0 +1,124 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=596333
+-->
+<head>
+ <title>Test for Bug 596333</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="spellcheck.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=596333">Mozilla Bug 596333</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 596333 **/
+const Ci = SpecialPowers.Ci;
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+
+var gMisspeltWords;
+
+function getEditor() {
+ return SpecialPowers.wrap(document.getElementById("edit")).editor;
+}
+
+function append(str) {
+ var edit = document.getElementById("edit");
+ edit.focus();
+ edit.selectionStart = edit.selectionEnd = edit.value.length;
+ var editor = getEditor();
+
+ for (var i = 0; i < str.length; ++i) {
+ synthesizeKey(str[i], {});
+ }
+}
+
+function getLoadContext() {
+ return SpecialPowers.wrap(window)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsILoadContext);
+}
+
+function paste(str) {
+ var edit = document.getElementById("edit");
+ var Cc = SpecialPowers.Cc, Ci = SpecialPowers.Ci;
+ var trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable);
+ trans.init(getLoadContext());
+ var s = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
+ s.data = str;
+ trans.setTransferData("text/unicode", s, str.length * 2);
+
+ getEditor().pasteTransferable(trans);
+}
+
+function runOnFocus() {
+ var edit = document.getElementById("edit");
+
+ gMisspeltWords = ["haz", "cheezburger"];
+ ok(isSpellingCheckOk(getEditor(), gMisspeltWords),
+ "All misspellings before editing are accounted for.");
+ append(" becaz I'm a lulcat!");
+ onSpellCheck(edit, function () {
+ gMisspeltWords.push("becaz");
+ gMisspeltWords.push("lulcat");
+ ok(isSpellingCheckOk(getEditor(), gMisspeltWords),
+ "All misspellings after typing are accounted for.");
+
+ // Now, type an invalid word, and instead of hitting "space" at the end, just blur
+ // the textarea and see if the spell check after the blur event catches it.
+ append(" workd");
+ edit.blur();
+ onSpellCheck(edit, function () {
+ gMisspeltWords.push("workd");
+ ok(isSpellingCheckOk(getEditor(), gMisspeltWords),
+ "All misspellings after blur are accounted for.");
+
+ // Also, test the case when we're entering the first word in a textarea
+ gMisspeltWords = ["workd"];
+ edit.value = "";
+ append("workd ");
+ onSpellCheck(edit, function () {
+ ok(isSpellingCheckOk(getEditor(), gMisspeltWords),
+ "Misspelling in the first entered word is accounted for.");
+
+ // Make sure that pasting would also trigger spell checking for the previous word
+ gMisspeltWords = ["workd"];
+ edit.value = "";
+ append("workd");
+ paste(" x");
+ onSpellCheck(edit, function () {
+ ok(isSpellingCheckOk(getEditor(), gMisspeltWords),
+ "Misspelling is accounted for after pasting.");
+
+ SimpleTest.finish();
+ });
+ });
+ });
+ });
+}
+
+function runTest()
+{
+ var edit = document.getElementById("edit");
+ edit.focus();
+
+ SpecialPowers.Cu.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm", window);
+ onSpellCheck(edit, runOnFocus);
+}
+</script>
+</pre>
+
+<textarea id="edit">I can haz cheezburger</textarea>
+
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug596506.html b/editor/libeditor/tests/test_bug596506.html
new file mode 100644
index 000000000..0fc1adabf
--- /dev/null
+++ b/editor/libeditor/tests/test_bug596506.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=596506
+-->
+<head>
+ <title>Test for Bug 596506</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=596506">Mozilla Bug 596506</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 596506 **/
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTest);
+
+const kIsMac = navigator.platform.indexOf("Mac") == 0;
+
+function append(str) {
+ for (var i = 0; i < str.length; ++i) {
+ synthesizeKey(str[i], {});
+ }
+}
+
+function runTest() {
+ var edit = document.getElementById("edit");
+ edit.focus();
+
+ append("First");
+ synthesizeKey("VK_RETURN", {});
+ append("Second");
+ synthesizeKey("VK_UP", {});
+ synthesizeKey("VK_UP", {});
+ if (kIsMac) {
+ synthesizeKey("VK_RIGHT", {accelKey: true});
+ } else {
+ synthesizeKey("VK_END", {});
+ }
+ append("ly");
+ is(edit.value, "Firstly\nSecond",
+ "Pressing end should position the cursor before the terminating newline");
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+
+<textarea id="edit"></textarea>
+
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug597331.html b/editor/libeditor/tests/test_bug597331.html
new file mode 100644
index 000000000..f35413cb6
--- /dev/null
+++ b/editor/libeditor/tests/test_bug597331.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=597331
+-->
+<head>
+ <title>Test for Bug 597331</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/WindowSnapshot.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=597331">Mozilla Bug 597331</a>
+<p id="display"></p>
+<div id="content">
+<textarea>line1
+line2
+line3
+</textarea>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 597331 **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ SimpleTest.executeSoon(function() {
+ var t = document.querySelector("textarea");
+ t.focus();
+ t.selectionStart = 4;
+ t.selectionEnd = 4;
+ SimpleTest.executeSoon(function() {
+ t.getBoundingClientRect(); // flush layout
+ var before = snapshotWindow(window, true);
+ t.selectionStart = 5;
+ t.selectionEnd = 5;
+ t.addEventListener("keydown", function() {
+ t.removeEventListener("keydown", arguments.callee, false);
+
+ SimpleTest.executeSoon(function() {
+ t.style.display = 'block';
+ document.body.offsetWidth;
+ t.style.display = '';
+ document.body.offsetWidth;
+
+ is(t.selectionStart, 4, "Cursor should be moved correctly");
+ is(t.selectionEnd, 4, "Cursor should be moved correctly");
+
+ var after = snapshotWindow(window, true);
+
+ var result = compareSnapshots(before, after, true);
+ var msg = "The caret should be displayed correctly after reframing";
+ if (!result[0]) {
+ msg += "\nRESULT:\n" + result[2];
+ msg += "\nREFERENCE:\n" + result[1];
+ }
+ ok(result[0], msg);
+
+ SimpleTest.finish();
+ });
+ }, false);
+ synthesizeKey("VK_LEFT", {});
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug597784.html b/editor/libeditor/tests/test_bug597784.html
new file mode 100644
index 000000000..321f2ad1c
--- /dev/null
+++ b/editor/libeditor/tests/test_bug597784.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=597784
+-->
+<head>
+ <title>Test for Bug 597784</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=597784">Mozilla Bug 597784</a>
+<p id="display"></p>
+<div id="content"></div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 597784 **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ document.designMode = "on";
+ var content = document.getElementById("content");
+ getSelection().collapse(content, 0);
+ var html = "<test:tag>test:tag</test:tag>" +
+ "<a href=\"http://mozilla.org/\" test:attr=\"test:attr\" custom=\"value\">link</a>";
+ document.execCommand("insertHTML", false, html);
+ is(content.innerHTML, html,
+ "The custom tags and attributes should be inserted into the document using the insertHTML command");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug599322.html b/editor/libeditor/tests/test_bug599322.html
new file mode 100644
index 000000000..578bcb11a
--- /dev/null
+++ b/editor/libeditor/tests/test_bug599322.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=599322.patch
+-->
+<head>
+ <title>Test for Bug 599322.patch</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=599322.patch">Mozilla Bug 599322.patch</a>
+<p id="display"></p>
+<div id="content">
+<div id="src">src<img src="/tests/editor/libeditor/tests/green.png"></div>
+<iframe id="dst" src="javascript:;"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 599322.patch **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var src = document.getElementById("src");
+ var dst = document.getElementById("dst");
+ var doc = dst.contentDocument;
+ doc.open();
+ doc.write("<html><head><base href='http://mochi.test:8888/'></head><body></body></html>");
+ doc.close();
+ SimpleTest.waitForFocus(function() {
+ getSelection().selectAllChildren(src);
+ SimpleTest.waitForClipboard("src",
+ function() {
+ synthesizeKey("c", {accelKey: true});
+ },
+ function() {
+ dst.contentDocument.designMode = "on";
+ dst.focus();
+ dst.contentDocument.body.focus();
+ synthesizeKey("v", {accelKey: true});
+ is(dst.contentDocument.querySelector("img").src,
+ document.querySelector("img").src,
+ "The source should be correctly set based on the base URI");
+ SimpleTest.finish();
+ },
+ function() {
+ SimpleTest.finish();
+ }
+ );
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug599983.html b/editor/libeditor/tests/test_bug599983.html
new file mode 100644
index 000000000..08fc9a228
--- /dev/null
+++ b/editor/libeditor/tests/test_bug599983.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=599983
+-->
+<title>Test for Bug 599983</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=599983">Mozilla Bug 599983</a>
+<div contenteditable>foo</div>
+<script>
+getSelection().selectAllChildren(document.querySelector("div"));
+document.execCommand("bold");
+is(document.querySelector("[_moz_dirty]"), null,
+ "No _moz_dirty allowed in webpages");
+</script>
diff --git a/editor/libeditor/tests/test_bug599983.xul b/editor/libeditor/tests/test_bug599983.xul
new file mode 100644
index 000000000..8b5d52a8c
--- /dev/null
+++ b/editor/libeditor/tests/test_bug599983.xul
@@ -0,0 +1,70 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin"
+ type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=599983
+-->
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Mozilla Bug 599983" onload="runTest()">
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=599983"
+ target="_blank">Mozilla Bug 599983</a>
+ <editor xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="editor"
+ editortype="html"
+ src="about:blank" />
+ </body>
+ <script type="application/javascript">
+ <![CDATA[
+
+ SimpleTest.waitForExplicitFinish();
+
+ const kAllowInteraction = Components.interfaces.nsIPlaintextEditor
+ .eEditorAllowInteraction;
+ const kMailMask = Components.interfaces.nsIPlaintextEditor.eEditorMailMask;
+
+ function runTest() {
+ testEditor(false, false);
+ testEditor(false, true);
+ testEditor(true, false);
+ testEditor(true, true);
+
+ SimpleTest.finish();
+ }
+
+ function testEditor(setAllowInteraction, setMailMask) {
+ var desc = " with " + (setAllowInteraction ? "" : "no ") +
+ "eEditorAllowInteraction and " +
+ (setMailMask ? "" : "no ") + "eEditorMailMask";
+
+ var editorElem = document.getElementById("editor");
+
+ var editorObj = editorElem.getEditor(editorElem.contentWindow);
+ editorObj.flags = (setAllowInteraction ? kAllowInteraction : 0) |
+ (setMailMask ? kMailMask : 0);
+
+ var editorDoc = editorElem.contentDocument;
+ editorDoc.body.innerHTML = "<p>foo<p>bar";
+ editorDoc.getSelection().selectAllChildren(editorDoc.body.firstChild);
+ editorDoc.execCommand("bold");
+
+ var createsDirty = !setAllowInteraction || setMailMask;
+
+ (createsDirty ? isnot : is)(editorDoc.querySelector("[_moz_dirty]"), null,
+ "Elements with _moz_dirty" + desc);
+
+ // Even if we do create _moz_dirty, we should strip it for innerHTML.
+ is(editorDoc.body.innerHTML, "<p><b>foo</b></p><p>bar</p>",
+ "innerHTML" + desc);
+ }
+
+ ]]>
+ </script>
+</window>
diff --git a/editor/libeditor/tests/test_bug600570.html b/editor/libeditor/tests/test_bug600570.html
new file mode 100644
index 000000000..0a5a814f8
--- /dev/null
+++ b/editor/libeditor/tests/test_bug600570.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=600570
+-->
+<head>
+ <title>Test for Bug 600570</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/WindowSnapshot.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=600570">Mozilla Bug 600570</a>
+<p id="display"></p>
+<div id="content">
+<textarea spellcheck="false">
+aaa
+[bbb]</textarea>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 600570 **/
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var t = document.querySelector("textarea");
+ t.value = "[aaa\nbbb]";
+ t.focus();
+ synthesizeKey("A", {accelKey: true});
+
+ SimpleTest.executeSoon(function() {
+ t.getBoundingClientRect(); // flush layout
+ var afterSetValue = snapshotWindow(window);
+
+ t.value = t.defaultValue;
+
+ t.selectionStart = 0;
+ t.selectionEnd = 4;
+ SimpleTest.waitForClipboard("aaa\n",
+ function() {
+ synthesizeKey("X", {accelKey: true});
+ },
+ function() {
+ t.addEventListener("input", function() {
+ t.removeEventListener("input", arguments.callee, false);
+
+ setTimeout(function() { // Avoid the assertion in bug 649797
+ is(t.value, "[aaa\nbbb]", "The value of the textarea should be correct");
+ synthesizeKey("A", {accelKey: true});
+ is(t.selectionStart, 0, "Select all should set the selection start to the beginning of textarea");
+ is(t.selectionEnd, 9, "Select all should set the selection end to the end of textarea");
+
+ var afterPaste = snapshotWindow(window);
+
+ var res = compareSnapshots(afterSetValue, afterPaste, true);
+ var msg = "Pasting and setting the value directly should result in the same rendering";
+ if (!res[0]) {
+ msg += "\nRESULT:\n" + res[2] + "\nREFERENCE:\n" + res[1];
+ }
+ ok(res[0], msg);
+
+ SimpleTest.finish();
+ }, 0);
+ }, false);
+ synthesizeKey("VK_RIGHT", {});
+ synthesizeKey("V", {accelKey: true});
+ },
+ function() {
+ SimpleTest.finish();
+ }
+ );
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug602130.html b/editor/libeditor/tests/test_bug602130.html
new file mode 100644
index 000000000..a61e5c9c3
--- /dev/null
+++ b/editor/libeditor/tests/test_bug602130.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=602130
+-->
+<head>
+ <title>Test for Bug 602130</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=602130">Mozilla Bug 602130</a>
+<p id="display"></p>
+<div id="content">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 602130 **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var i = document.createElement("input");
+ document.body.appendChild(i);
+ SpecialPowers.wrap(i).QueryInterface(SpecialPowers.Ci.nsIDOMNSEditableElement);
+ i.select();
+ i.focus();
+ is(SpecialPowers.wrap(i).editor.transactionManager.numberOfUndoItems, 0,
+ "The number of undo items should be 0 after initing the editor");
+ i.style.display = "none";
+ document.offsetWidth;
+ i.style.display = "";
+ document.offsetWidth;
+ i.select();
+ i.focus();
+ is(SpecialPowers.wrap(i).editor.transactionManager.numberOfUndoItems, 0,
+ "The number of undo items should be 0 after re-initing the editor");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug603556.html b/editor/libeditor/tests/test_bug603556.html
new file mode 100644
index 000000000..0e0a70464
--- /dev/null
+++ b/editor/libeditor/tests/test_bug603556.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=603556
+-->
+<head>
+ <title>Test for Bug 603556</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=603556">Mozilla Bug 603556</a>
+<p id="display"></p>
+<div id="content">
+ <div id="src">testing</div>
+ <input maxlength="4">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 603556 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var i = document.querySelector("input");
+ var src = document.getElementById("src");
+ SimpleTest.waitForClipboard(src.textContent,
+ function() {
+ getSelection().selectAllChildren(src);
+ synthesizeKey("C", {accelKey: true});
+ },
+ function() {
+ i.focus();
+ synthesizeKey("V", {accelKey: true});
+ is(i.value, src.textContent.substr(0, i.maxLength),
+ "Pasting should paste maxlength chars worth of the clipboard contents");
+ SimpleTest.finish();
+ },
+ function() {
+ SimpleTest.finish();
+ }
+ );
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug604532.html b/editor/libeditor/tests/test_bug604532.html
new file mode 100644
index 000000000..519a179b1
--- /dev/null
+++ b/editor/libeditor/tests/test_bug604532.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=604532
+-->
+<head>
+ <title>Test for Bug 604532</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=604532">Mozilla Bug 604532</a>
+<p id="display"></p>
+<div id="content">
+<input>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 604532 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var i = document.querySelector("input");
+ i.focus();
+ i.value = "foo";
+ synthesizeKey("A", {accelKey: true});
+ is(i.selectionStart, 0, "Selection should start at 0 before appending");
+ is(i.selectionEnd, 3, "Selection should end at 3 before appending");
+ synthesizeKey("VK_RIGHT", {});
+ synthesizeKey("x", {});
+ is(i.value, "foox", "The text should be appended correctly");
+ synthesizeKey("A", {accelKey: true});
+ is(i.selectionStart, 0, "Selection should start at 0 after appending");
+ is(i.selectionEnd, 4, "Selection should end at 4 after appending");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug607584.html b/editor/libeditor/tests/test_bug607584.html
new file mode 100644
index 000000000..aa22b6f30
--- /dev/null
+++ b/editor/libeditor/tests/test_bug607584.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=607584
+-->
+<head>
+ <title>Test for Bug 607584</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=607584">Mozilla Bug 607584</a>
+<p id="display"></p>
+<div id="content" contenteditable>
+<p id="foo">Hello world</p>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 607584 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var content = document.getElementById("content");
+ content.focus();
+ var sel = getSelection();
+ sel.collapse(document.getElementById("foo").firstChild, 5);
+ synthesizeKey("VK_RETURN", {});
+ var paragraphs = content.querySelectorAll("p");
+ is(paragraphs.length, 2, "The paragraph should be split in two");
+ is(paragraphs[0].textContent, "Hello", "The first paragraph should have the correct content");
+ is(paragraphs[1].textContent, " world", "The second paragraph should have the correct content");
+ is(paragraphs[0].getAttribute("id"), "foo", "The id of the first paragraph should be retained");
+ is(paragraphs[1].hasAttribute("id"), false, "The second paragraph shouldn't have an ID");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug607584.xul b/editor/libeditor/tests/test_bug607584.xul
new file mode 100644
index 000000000..fb16cee83
--- /dev/null
+++ b/editor/libeditor/tests/test_bug607584.xul
@@ -0,0 +1,115 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin"
+ type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=607584
+-->
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Mozilla Bug 607584" onload="runTest();">
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=607584"
+ target="_blank">Mozilla Bug 607584</a>
+ <p/>
+ <editor xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="editor"
+ type="content-primary"
+ editortype="html"
+ style="width: 400px; height: 100px; border: thin solid black"/>
+ <p/>
+ <pre id="test">
+ </pre>
+ </body>
+ <script class="testbody" type="application/javascript">
+ <![CDATA[
+
+ SimpleTest.waitForExplicitFinish();
+
+ function EditorContentListener(aEditor)
+ {
+ this.init(aEditor);
+ }
+
+ EditorContentListener.prototype = {
+ init : function(aEditor)
+ {
+ this.mEditor = aEditor;
+ },
+
+ QueryInterface : function(aIID)
+ {
+ if (aIID.equals(Components.interfaces.nsIWebProgressListener) ||
+ aIID.equals(Components.interfaces.nsISupportsWeakReference) ||
+ aIID.equals(Components.interfaces.nsISupports))
+ return this;
+ throw Components.results.NS_NOINTERFACE;
+ },
+
+ onStateChange : function(aWebProgress, aRequest, aStateFlags, aStatus)
+ {
+ if (aStateFlags & Components.interfaces.nsIWebProgressListener.STATE_STOP)
+ {
+ var editor = this.mEditor.getEditor(this.mEditor.contentWindow);
+ if (editor) {
+ this.mEditor.focus();
+ editor instanceof Components.interfaces.nsIHTMLEditor;
+ editor.returnInParagraphCreatesNewParagraph = true;
+ editor.insertHTML("<p id='foo'>this is a paragraph carrying id 'foo'</p>");
+ var p = editor.document.getElementById('foo')
+ editor.beginningOfDocument();
+ sendKey("return");
+ var firstP = p.parentNode.firstElementChild;
+ var lastP = p.parentNode.lastElementChild;
+ var isOk = firstP.nodeName.toLowerCase() == "p" &&
+ firstP.id == "foo" &&
+ lastP.id == "";
+ ok(isOk, "CR in a paragraph with an ID should not create two paragraphs of same ID");
+ progress.removeProgressListener(this);
+ SimpleTest.finish();
+ }
+ }
+
+ },
+
+
+ onProgressChange : function(aWebProgress, aRequest,
+ aCurSelfProgress, aMaxSelfProgress,
+ aCurTotalProgress, aMaxTotalProgress)
+ {
+ },
+
+ onLocationChange : function(aWebProgress, aRequest, aLocation, aFlags)
+ {
+ },
+
+ onStatusChange : function(aWebProgress, aRequest, aStatus, aMessage)
+ {
+ },
+
+ onSecurityChange : function(aWebProgress, aRequest, aState)
+ {
+ },
+
+ mEditor: null
+ };
+
+ var progress, progressListener;
+
+ function runTest() {
+ var newEditorElement = document.getElementById("editor");
+ newEditorElement.makeEditable("html", true);
+ var docShell = newEditorElement.boxObject.docShell;
+ progress = docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIWebProgress);
+ progressListener = new EditorContentListener(newEditorElement);
+ progress.addProgressListener(progressListener, Components.interfaces.nsIWebProgress.NOTIFY_ALL);
+ newEditorElement.setAttribute("src", "data:text/html,");
+ }
+]]>
+</script>
+</window>
diff --git a/editor/libeditor/tests/test_bug611182.html b/editor/libeditor/tests/test_bug611182.html
new file mode 100644
index 000000000..e6ecc6716
--- /dev/null
+++ b/editor/libeditor/tests/test_bug611182.html
@@ -0,0 +1,239 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=611182
+-->
+<head>
+ <title>Test for Bug 611182</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/WindowSnapshot.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=611182">Mozilla Bug 611182</a>
+<p id="display"></p>
+<div id="content">
+ <iframe></iframe>
+ <iframe id="ref" src="data:text/html,foo bar"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 611182 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var iframe = document.querySelector("iframe");
+ var refElem = document.querySelector("#ref");
+ var ref = snapshotWindow(refElem.contentWindow, false);
+
+ function findTextNode(doc) {
+ var body = doc.documentElement;
+ var result = findTextNodeWorker(body);
+ ok(result, "Failed to find the text node");
+ return result;
+ }
+
+ function findTextNodeWorker(root) {
+ if (root.isContentEditable) {
+ root.focus();
+ }
+ for (var i = 0; i < root.childNodes.length; ++i) {
+ var node = root.childNodes[i];
+ if (node.nodeType == node.TEXT_NODE &&
+ node.nodeValue == "fooz bar") {
+ return node;
+ }
+ if (node.nodeType == node.ELEMENT_NODE) {
+ node = findTextNodeWorker(node);
+ if (node) {
+ return node;
+ }
+ }
+ }
+ return null;
+ }
+
+ function testBackspace(src, callback) {
+ ok(true, "Testing " + src);
+ iframe.addEventListener("load", function() {
+ iframe.removeEventListener("load", arguments.callee, false);
+
+ var doc = iframe.contentDocument;
+ var win = iframe.contentWindow;
+ doc.body.setAttribute("spellcheck", "false");
+
+ iframe.focus();
+ var textNode = findTextNode(doc);
+ var sel = win.getSelection();
+ sel.collapse(textNode, 4);
+ synthesizeKey("VK_BACK_SPACE", {});
+ is(textNode.textContent, "foo bar", "Backspace should work correctly");
+
+ var snapshot = snapshotWindow(win, false);
+ ok(compareSnapshots(snapshot, ref, true)[0], "No bogus node should exist in the document");
+
+ callback();
+ }, false);
+ iframe.src = src;
+ }
+
+ const TEST_URIS = [
+ "data:text/html,<html contenteditable>fooz bar</html>",
+ "data:text/html,<html contenteditable><body>fooz bar</body></html>",
+ "data:text/html,<body contenteditable>fooz bar</body>",
+ "data:text/html,<body contenteditable><p>fooz bar</p></body>",
+ "data:text/html,<body contenteditable><div>fooz bar</div></body>",
+ "data:text/html,<body contenteditable><span>fooz bar</span></body>",
+ "data:text/html,<p contenteditable style='outline:none'>fooz bar</p>",
+ "data:text/html,<!DOCTYPE html><html><body contenteditable>fooz bar</body></html>",
+ "data:text/html,<!DOCTYPE html><html contenteditable><body>fooz bar</body></html>",
+ 'data:application/xhtml+xml,<html xmlns="http://www.w3.org/1999/xhtml"><body contenteditable="true">fooz bar</body></html>',
+ 'data:application/xhtml+xml,<html xmlns="http://www.w3.org/1999/xhtml" contenteditable="true"><body>fooz bar</body></html>',
+ "data:text/html,<body onload=\"document.designMode='on'\">fooz bar</body>",
+ 'data:text/html,<html><script>' +
+ 'onload = function() {' +
+ 'var old = document.body;' +
+ 'old.parentNode.removeChild(old);' +
+ 'var r = document.documentElement;' +
+ 'var b = document.createElement("body");' +
+ 'r.appendChild(b);' +
+ 'b.appendChild(document.createTextNode("fooz bar"));' +
+ 'b.contentEditable = "true";' +
+ '};' +
+ '<\/script><body></body></html>',
+ 'data:text/html,<html><script>' +
+ 'onload = function() {' +
+ 'var old = document.body;' +
+ 'old.parentNode.removeChild(old);' +
+ 'var r = document.documentElement;' +
+ 'var b = document.createElement("body");' +
+ 'b.appendChild(document.createTextNode("fooz bar"));' +
+ 'b.contentEditable = "true";' +
+ 'r.appendChild(b);' +
+ '};' +
+ '<\/script><body></body></html>',
+ 'data:text/html,<html><script>' +
+ 'onload = function() {' +
+ 'var old = document.body;' +
+ 'old.parentNode.removeChild(old);' +
+ 'var r = document.documentElement;' +
+ 'var b = document.createElement("body");' +
+ 'r.appendChild(b);' +
+ 'b.appendChild(document.createTextNode("fooz bar"));' +
+ 'b.setAttribute("contenteditable", "true");' +
+ '};' +
+ '<\/script><body></body></html>',
+ 'data:text/html,<html><script>' +
+ 'onload = function() {' +
+ 'var old = document.body;' +
+ 'old.parentNode.removeChild(old);' +
+ 'var r = document.documentElement;' +
+ 'var b = document.createElement("body");' +
+ 'b.appendChild(document.createTextNode("fooz bar"));' +
+ 'b.setAttribute("contenteditable", "true");' +
+ 'r.appendChild(b);' +
+ '};' +
+ '<\/script><body></body></html>',
+ 'data:text/html,<html><script>' +
+ 'onload = function() {' +
+ 'var old = document.body;' +
+ 'old.parentNode.removeChild(old);' +
+ 'var r = document.documentElement;' +
+ 'var b = document.createElement("body");' +
+ 'r.appendChild(b);' +
+ 'b.contentEditable = "true";' +
+ 'b.appendChild(document.createTextNode("fooz bar"));' +
+ '};' +
+ '<\/script><body></body></html>',
+ 'data:text/html,<html><script>' +
+ 'onload = function() {' +
+ 'var old = document.body;' +
+ 'old.parentNode.removeChild(old);' +
+ 'var r = document.documentElement;' +
+ 'var b = document.createElement("body");' +
+ 'b.contentEditable = "true";' +
+ 'r.appendChild(b);' +
+ 'b.appendChild(document.createTextNode("fooz bar"));' +
+ '};' +
+ '<\/script><body></body></html>',
+ 'data:text/html,<html><script>' +
+ 'onload = function() {' +
+ 'var old = document.body;' +
+ 'old.parentNode.removeChild(old);' +
+ 'var r = document.documentElement;' +
+ 'var b = document.createElement("body");' +
+ 'r.appendChild(b);' +
+ 'b.setAttribute("contenteditable", "true");' +
+ 'b.appendChild(document.createTextNode("fooz bar"));' +
+ '};' +
+ '<\/script><body></body></html>',
+ 'data:text/html,<html><script>' +
+ 'onload = function() {' +
+ 'var old = document.body;' +
+ 'old.parentNode.removeChild(old);' +
+ 'var r = document.documentElement;' +
+ 'var b = document.createElement("body");' +
+ 'b.setAttribute("contenteditable", "true");' +
+ 'r.appendChild(b);' +
+ 'b.appendChild(document.createTextNode("fooz bar"));' +
+ '};' +
+ '<\/script><body></body></html>',
+ 'data:text/html,<html><script>' +
+ 'onload = function() {' +
+ 'document.open();' +
+ 'document.write("<body contenteditable>fooz bar</body>");' +
+ 'document.close();' +
+ '};' +
+ '<\/script><body></body></html>',
+ 'data:text/html,<html><script>' +
+ 'onload = function() {' +
+ 'document.open();' +
+ 'document.write("<body contenteditable><div>fooz bar</div></body>");' +
+ 'document.close();' +
+ '};' +
+ '<\/script><body></body></html>',
+ 'data:text/html,<html><script>' +
+ 'onload = function() {' +
+ 'document.open();' +
+ 'document.write("<body contenteditable><span>fooz bar</span></body>");' +
+ 'document.close();' +
+ '};' +
+ '<\/script><body></body></html>',
+ 'data:text/html,<html><script>' +
+ 'onload = function() {' +
+ 'document.open();' +
+ 'document.write("<p contenteditable style=\\"outline: none\\">fooz bar</p>");' +
+ 'document.close();' +
+ '};' +
+ '<\/script><body></body></html>',
+ 'data:text/html,<html><script>' +
+ 'onload = function() {' +
+ 'document.open();' +
+ 'document.write("<html contenteditable>fooz bar</html>");' +
+ 'document.close();' +
+ '};' +
+ '<\/script><body></body></html>',
+ 'data:text/html,<html><script>' +
+ 'onload = function() {' +
+ 'document.open();' +
+ 'document.write("<html contenteditable><body>fooz bar</body></html>");' +
+ 'document.close();' +
+ '};' +
+ '<\/script><body></body></html>',
+ ];
+ var currentTest = 0;
+ function runAllTests() {
+ if (currentTest == TEST_URIS.length) {
+ SimpleTest.finish();
+ return;
+ }
+ testBackspace(TEST_URIS[currentTest++], runAllTests);
+ }
+ runAllTests();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug612128.html b/editor/libeditor/tests/test_bug612128.html
new file mode 100644
index 000000000..b23d6f12a
--- /dev/null
+++ b/editor/libeditor/tests/test_bug612128.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=612128
+-->
+<head>
+ <title>Test for Bug 612128</title>
+ <script type="application/javascript" src="/MochiKit/packed.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=612128">Mozilla Bug 612128</a>
+<p id="display"></p>
+<div id="content">
+<input>
+<div contenteditable></div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 612128 **/
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ document.querySelector("input").focus();
+ var threw = false;
+ try {
+ is(document.execCommand("inserthtml", null, "<span>f" + "oo</span>"),
+ false, "The insertHTML command should return false");
+ } catch (e) {
+ ok(false, "insertHTML should not throw here");
+ }
+ is(document.querySelectorAll("span").length, 0, "No span element should be injected inside the page");
+ is(document.body.innerHTML.indexOf("f" + "oo"), -1, "No text should be injected inside the page");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug612447.html b/editor/libeditor/tests/test_bug612447.html
new file mode 100644
index 000000000..b06739288
--- /dev/null
+++ b/editor/libeditor/tests/test_bug612447.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=612447
+-->
+<head>
+ <title>Test for Bug 612447</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/WindowSnapshot.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=612447">Mozilla Bug 612447</a>
+<p id="display"></p>
+<div id="content">
+<iframe></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 612447 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ function editorCommandsEnabled() {
+ var caught = false;
+ try {
+ doc.execCommand("justifyfull", false, null);
+ } catch (e) {
+ caught = true;
+ }
+ return !caught;
+ }
+
+ var i = document.querySelector("iframe");
+ var doc = i.contentDocument;
+ var win = i.contentWindow;
+ var b = doc.body;
+ doc.designMode = "on";
+ i.focus();
+ b.focus();
+ var beforeA = snapshotWindow(win, true);
+ synthesizeKey("X", {});
+ var beforeB = snapshotWindow(win, true);
+ is(b.textContent, "X", "Typing should work");
+ while (b.firstChild) {
+ b.removeChild(b.firstChild);
+ }
+ ok(editorCommandsEnabled(), "The editor commands should work");
+
+ i.style.display = "block";
+ document.clientWidth;
+
+ i.focus();
+ b.focus();
+ var afterA = snapshotWindow(win, true);
+ synthesizeKey("X", {});
+ var afterB = snapshotWindow(win, true);
+ is(b.textContent, "X", "Typing should work");
+ while (b.firstChild) {
+ b.removeChild(b.firstChild);
+ }
+ ok(editorCommandsEnabled(), "The editor commands should work");
+
+ ok(compareSnapshots(beforeA, afterA, true)[0], "The iframes should look the same before typing");
+ ok(compareSnapshots(beforeB, afterB, true)[0], "The iframes should look the same after typing");
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug616590.xul b/editor/libeditor/tests/test_bug616590.xul
new file mode 100644
index 000000000..57c29a028
--- /dev/null
+++ b/editor/libeditor/tests/test_bug616590.xul
@@ -0,0 +1,105 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin"
+ type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=616590
+-->
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Mozilla Bug 616590" onload="runTest();">
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=616590"
+ target="_blank">Mozilla Bug 616590</a>
+ <p/>
+ <editor xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="editor"
+ type="content"
+ editortype="htmlmail"
+ style="width: 400px; height: 100px;"/>
+ <p/>
+ <pre id="test">
+ </pre>
+ </body>
+ <script class="testbody" type="application/javascript">
+ <![CDATA[
+
+ SimpleTest.waitForExplicitFinish();
+
+ function EditorContentListener(aEditor)
+ {
+ this.init(aEditor);
+ }
+
+ EditorContentListener.prototype = {
+ init : function(aEditor)
+ {
+ this.mEditor = aEditor;
+ },
+
+ QueryInterface : function(aIID)
+ {
+ if (aIID.equals(Components.interfaces.nsIWebProgressListener) ||
+ aIID.equals(Components.interfaces.nsISupportsWeakReference) ||
+ aIID.equals(Components.interfaces.nsISupports))
+ return this;
+ throw Components.results.NS_NOINTERFACE;
+ },
+
+ onStateChange : function(aWebProgress, aRequest, aStateFlags, aStatus)
+ {
+ if (aStateFlags & Components.interfaces.nsIWebProgressListener.STATE_STOP)
+ {
+ var editor = this.mEditor.getEditor(this.mEditor.contentWindow);
+ if (editor) {
+ editor.QueryInterface(Components.interfaces.nsIEditorMailSupport);
+ editor.insertAsCitedQuotation("<html><body><div contenteditable>foo</div></body></html>", "", true);
+ document.documentElement.clientWidth;
+ progress.removeProgressListener(this);
+ ok(true, "Test complete");
+ SimpleTest.finish();
+ }
+ }
+ },
+
+
+ onProgressChange : function(aWebProgress, aRequest,
+ aCurSelfProgress, aMaxSelfProgress,
+ aCurTotalProgress, aMaxTotalProgress)
+ {
+ },
+
+ onLocationChange : function(aWebProgress, aRequest, aLocation, aFlags)
+ {
+ },
+
+ onStatusChange : function(aWebProgress, aRequest, aStatus, aMessage)
+ {
+ },
+
+ onSecurityChange : function(aWebProgress, aRequest, aState)
+ {
+ },
+
+ mEditor: null
+ };
+
+ var progress, progressListener;
+
+ function runTest() {
+ var editorElement = document.getElementById("editor");
+ editorElement.makeEditable("htmlmail", true);
+ var docShell = editorElement.boxObject.docShell;
+ progress = docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIWebProgress);
+ progressListener = new EditorContentListener(editorElement);
+ progress.addProgressListener(progressListener, Components.interfaces.nsIWebProgress.NOTIFY_ALL);
+ editorElement.setAttribute("src", "data:text/html,");
+ }
+]]>
+</script>
+</window>
diff --git a/editor/libeditor/tests/test_bug620906.html b/editor/libeditor/tests/test_bug620906.html
new file mode 100644
index 000000000..208bdfd28
--- /dev/null
+++ b/editor/libeditor/tests/test_bug620906.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=620906
+-->
+<head>
+ <title>Test for Bug 620906</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=620906">Mozilla Bug 620906</a>
+<p id="display"></p>
+<div id="content">
+ <iframe src="data:text/html,
+ <body contenteditable
+ onmousedown='
+ document.designMode=&quot;on&quot;;
+ document.designMode=&quot;off&quot;;
+ '
+ >
+ <div style='height: 1000px;'></div>
+ </body>">
+ </iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 620906 **/
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var iframe = document.querySelector("iframe");
+ is(iframe.contentWindow.scrollY, 0, "Sanity check");
+ var rect = iframe.getBoundingClientRect();
+ setTimeout(function() {
+ var onscroll = function () {
+ iframe.contentWindow.removeEventListener("scroll", onscroll, false);
+ isnot(iframe.contentWindow.scrollY, 0, "The scrollbar should work");
+ SimpleTest.finish();
+ }
+ iframe.contentWindow.addEventListener("scroll", onscroll, false);
+ synthesizeMouse(iframe, rect.width - 5, rect.height / 2, {});
+ }, 0);
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug622371.html b/editor/libeditor/tests/test_bug622371.html
new file mode 100644
index 000000000..d08ba8214
--- /dev/null
+++ b/editor/libeditor/tests/test_bug622371.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=622371
+-->
+<head>
+ <title>Test for Bug 622371</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=622371">Mozilla Bug 622371</a>
+<p id="display"></p>
+<div id="content">
+ <iframe src="data:text/html,<body contenteditable>abc</body>"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 622371 **/
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var i = document.querySelector("iframe");
+ var sel = i.contentWindow.getSelection();
+ var doc = i.contentDocument;
+ var body = doc.body;
+ i.focus();
+ sel.collapse(body, 1);
+ doc.designMode = "on";
+ doc.designMode = "off";
+ is(sel.getRangeAt(0).startOffset, 1, "The start offset of the selection shouldn't change");
+ is(sel.getRangeAt(0).endOffset, 1, "The end offset of the selection shouldn't change");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug625452.html b/editor/libeditor/tests/test_bug625452.html
new file mode 100644
index 000000000..e2292d753
--- /dev/null
+++ b/editor/libeditor/tests/test_bug625452.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=625452
+-->
+<head>
+ <title>Test for Bug 625452</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=625452">Mozilla Bug 625452</a>
+<p id="display"></p>
+<div id="content">
+<input>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 625452 **/
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var i = document.querySelector("input");
+ var inputCount = 0;
+ i.addEventListener("input", function() { inputCount++; }, false);
+
+ // test cut
+ i.focus();
+ i.value = "foo bar";
+ i.selectionStart = 0;
+ i.selectionEnd = 4;
+ synthesizeKey("X", {accelKey: true});
+ is(i.value, "bar", "Cut should work correctly");
+ is(inputCount, 1, "input event should be raised correctly");
+
+ // test undo
+ synthesizeKey("Z", {accelKey: true});
+ is(i.value, "foo bar", "Undo should work correctly");
+ is(inputCount, 2, "input event should be raised correctly");
+
+ // test redo
+ synthesizeKey("Z", {accelKey: true, shiftKey: true});
+ is(i.value, "bar", "Redo should work correctly");
+ is(inputCount, 3, "input event should be raised correctly");
+
+ // test delete
+ i.selectionStart = 0;
+ i.selectionEnd = 2;
+ synthesizeKey("VK_DELETE", {});
+ is(i.value, "r", "Delete should work correctly");
+ is(inputCount, 4, "input event should be raised correctly");
+
+ // test DeleteSelection(eNone)
+ i.value = "retest"; // the "r" common prefix is crucial here
+ is(inputCount, 4, "input event should not have been raised");
+
+ // paste is tested in test_bug596001.html
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug629845.html b/editor/libeditor/tests/test_bug629845.html
new file mode 100644
index 000000000..9eb24f904
--- /dev/null
+++ b/editor/libeditor/tests/test_bug629845.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=629845
+-->
+<head>
+ <title>Test for Bug 629845</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=629845">Mozilla Bug 629845</a>
+<p id="display"></p>
+
+<script>
+function initFrame(frame)
+{
+ frame.contentWindow.document.designMode="on";
+ frame.contentWindow.document.writeln("<body></body>");
+
+ document.getElementsByTagName('button')[0].click();
+}
+
+function command(aName)
+{
+ var frame = document.getElementsByTagName('iframe')[0];
+
+ is(frame.contentDocument.designMode, "on", "design mode should be on!");
+ var caught = false;
+ try {
+ frame.contentDocument.execCommand(aName, false, null);
+ } catch (e) {
+ ok(false, "exception " + e + " was thrown");
+ caught = true;
+ }
+
+ ok(!caught, "No exception should have been thrown.");
+
+ // Stop the document load before finishing, just to be clean.
+ document.getElementsByTagName('iframe')[0].contentWindow.document.close();
+ SimpleTest.finish();
+}
+</script>
+
+<div id="content">
+ <button type="button" onclick="command('bold');">Bold</button>
+ <iframe onload="initFrame(this);"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 629845 **/
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug635636.html b/editor/libeditor/tests/test_bug635636.html
new file mode 100644
index 000000000..e5bbb5322
--- /dev/null
+++ b/editor/libeditor/tests/test_bug635636.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=635636
+-->
+<head>
+ <title>Test for Bug 635636</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=635636">Mozilla Bug 635636</a>
+<p id="display"></p>
+<div id="content">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 635636 **/
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var w, d;
+
+ function b1()
+ {
+ w = window.open('data:application/xhtml+xml,<html xmlns="http://www.w3.org/1999/xhtml"><div>1</div></html>');
+ SimpleTest.waitForFocus(b2, w);
+ }
+
+ function b2()
+ {
+ w.document.designMode = 'on';
+ w.location = "data:text/plain,2";
+ d = w.document.getElementsByTagName("div")[0];
+ const Ci = SpecialPowers.Ci;
+ var mainWindow = SpecialPowers.wrap(w)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ var browser = mainWindow.gBrowser.selectedBrowser;
+ browser.addEventListener("pageshow", function() {
+ setTimeout(b3, 0);
+ }, false);
+ }
+
+ function b3()
+ {
+ d.parentNode.removeChild(d);
+ ok(true, "Should not crash");
+ // Not needed for the crash
+ w.close();
+ SimpleTest.finish();
+ }
+
+ b1();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug636465.html b/editor/libeditor/tests/test_bug636465.html
new file mode 100644
index 000000000..37ceebe5a
--- /dev/null
+++ b/editor/libeditor/tests/test_bug636465.html
@@ -0,0 +1,54 @@
+<!doctype html>
+<title>Mozilla bug 636465</title>
+<link rel=stylesheet href="/tests/SimpleTest/test.css">
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/WindowSnapshot.js"></script>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=636465"
+ target="_blank">Mozilla Bug 636465</a>
+<input id="x" value="foobarbaz" spellcheck="true" style="background-color: transparent; border: transparent;">
+<script>
+SimpleTest.waitForExplicitFinish();
+
+function runTest() {
+ SpecialPowers.Cu.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm",
+ window);
+ var x = document.getElementById("x");
+ x.focus();
+ onSpellCheck(x, function () {
+ x.blur();
+ var spellCheckTrue = snapshotWindow(window);
+ x.setAttribute("spellcheck", "false");
+ var spellCheckFalse = snapshotWindow(window);
+ x.setAttribute("spellcheck", "true");
+ x.focus();
+ onSpellCheck(x, function () {
+ x.blur();
+ var spellCheckTrueAgain = snapshotWindow(window);
+ x.removeAttribute("spellcheck");
+ var spellCheckNone = snapshotWindow(window);
+ var after = snapshotWindow(window);
+ var ret = compareSnapshots(spellCheckTrue, spellCheckFalse, false)[0];
+ ok(ret,
+ "Setting the spellcheck attribute to false should work");
+ if (!ret) {
+ ok(false, "\nspellCheckTrue: " + spellCheckTrue.toDataURL() + "\nspellCheckFalse: " + spellCheckFalse.toDataURL());
+ }
+ ret = compareSnapshots(spellCheckTrue, spellCheckTrueAgain, true)[0];
+ ok(ret,
+ "Setting the spellcheck attribute back to true should work");
+ if (!ret) {
+ ok(false, "\nspellCheckTrue: " + spellCheckTrue.toDataURL() + "\nspellCheckTrueAgain: " + spellCheckTrueAgain.toDataURL());
+ }
+ ret = compareSnapshots(spellCheckNone, spellCheckFalse, true)[0];
+ ok(ret,
+ "Unsetting the spellcheck attribute should work");
+ if (!ret) {
+ ok(false, "\spellCheckNone: " + spellCheckNone.toDataURL() + "\nspellCheckFalse: " + spellCheckFalse.toDataURL());
+ }
+ SimpleTest.finish();
+ });
+ });
+}
+addLoadEvent(runTest);
+</script>
diff --git a/editor/libeditor/tests/test_bug638596.html b/editor/libeditor/tests/test_bug638596.html
new file mode 100644
index 000000000..62ef103f0
--- /dev/null
+++ b/editor/libeditor/tests/test_bug638596.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=638596
+-->
+<head>
+ <title>Test for Bug 638596</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=638596">Mozilla Bug 638596</a>
+<p id="display"></p>
+<div id="content">
+ <input type="password" style="font-size: 0">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 638596 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var i = document.querySelector("input");
+ i.focus();
+ synthesizeKey("t", {});
+ synthesizeKey("e", {});
+ synthesizeKey("s", {});
+ synthesizeKey("t", {});
+ is(i.value, "test", "The correct value should be stored in the field");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug640321.html b/editor/libeditor/tests/test_bug640321.html
new file mode 100644
index 000000000..984ea295a
--- /dev/null
+++ b/editor/libeditor/tests/test_bug640321.html
@@ -0,0 +1,190 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=640321
+-->
+<head>
+ <title>Test for Bug 640321</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=640321">Mozilla Bug 640321</a>
+<p id="display"></p>
+<div id="content" contenteditable style="text-align: center">
+ <img src="green.png">
+</div>
+<div id="clickaway" style="width: 10px; height: 10px"></div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 640321 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var img = document.querySelector("img");
+
+ function cancel(e) { e.stopPropagation(); }
+ var content = document.getElementById("content");
+ content.addEventListener("mousedown", cancel, false);
+ content.addEventListener("mousemove", cancel, false);
+ content.addEventListener("mouseup", cancel, false);
+
+ /**
+ * This function is a generic resizer test.
+ * We have 8 resizers that we'd like to test, and each can be moved in 8 different directions.
+ * In specifying baseX, W can be considered to be the width of the image, and for baseY, H
+ * can be considered to be the height of the image. deltaX and deltaY are regular pixel values
+ * which can be positive or negative.
+ */
+ const W = 1;
+ const H = 1;
+ function testResizer(baseX, baseY, deltaX, deltaY, expectedDeltaX, expectedDeltaY) {
+ ok(true, "testResizer(" + [baseX, baseY, deltaX, deltaY, expectedDeltaX, expectedDeltaY].join(", ") + ")");
+
+ // Reset the dimensions of the image
+ img.style.width = "100px";
+ img.style.height = "100px";
+ var rect = img.getBoundingClientRect();
+ is(rect.width, 100, "Sanity check the length");
+ is(rect.height, 100, "Sanity check the height");
+
+ // Click on the image to show the resizers
+ synthesizeMouseAtCenter(img, {});
+
+ // Determine which resizer we're dealing with
+ var basePosX = rect.width * baseX;
+ var basePosY = rect.height * baseY;
+
+ // Click on the correct resizer
+ synthesizeMouse(img, basePosX, basePosY, {type: "mousedown"});
+ // Drag it delta pixels to the right and bottom (or maybe left and top!)
+ synthesizeMouse(img, basePosX + deltaX, basePosY + deltaY, {type: "mousemove"});
+ // Release the mouse button
+ synthesizeMouse(img, basePosX + deltaX, basePosY + deltaY, {type: "mouseup"});
+ // Move the mouse delta more pixels to the same direction to make sure that the
+ // resize operation has stopped.
+ synthesizeMouse(img, basePosX + deltaX * 2, basePosY + deltaY * 2, {type: "mousemove"});
+ // Click outside of the image to hide the resizers
+ synthesizeMouseAtCenter(document.getElementById("clickaway"), {});
+
+ // Get the new dimensions for the image
+ var newRect = img.getBoundingClientRect();
+ is(newRect.width, rect.width + expectedDeltaX, "The width should be increased by " + expectedDeltaX + " pixels");
+ is(newRect.height, rect.height + expectedDeltaY, "The height should be increased by " + expectedDeltaY + "pixels");
+ }
+
+ function runTests(preserveRatio) {
+ // Account for changes in the resizing behavior when we're trying to preserve
+ // the aspect ration.
+ // ignoredGrowth means we don't change the size of a dimension because otherwise
+ // the aspect ratio would change undesirably.
+ // needlessGrowth means that we change the size of a dimension perpendecular to
+ // the mouse movement axis in order to preserve the aspect ratio.
+ // reversedGrowth means that we change the size of a dimension in the opposite
+ // direction to the mouse movement in order to maintain the aspect ratio.
+ const ignoredGrowth = preserveRatio ? 0 : 1;
+ const needlessGrowth = preserveRatio ? 1 : 0;
+ const reversedGrowth = preserveRatio ? -1 : 1;
+
+ // top resizer
+ testResizer(W/2, 0, -10, -10, 0, 10);
+ testResizer(W/2, 0, -10, 0, 0, 0);
+ testResizer(W/2, 0, -10, 10, 0, -10);
+ testResizer(W/2, 0, 0, -10, 0, 10);
+ testResizer(W/2, 0, 0, 0, 0, 0);
+ testResizer(W/2, 0, 0, 10, 0, -10);
+ testResizer(W/2, 0, 10, -10, 0, 10);
+ testResizer(W/2, 0, 10, 0, 0, 0);
+ testResizer(W/2, 0, 10, 10, 0, -10);
+
+ // top right resizer
+ testResizer( W, 0, -10, -10, -10 * reversedGrowth, 10);
+ testResizer( W, 0, -10, 0, -10 * ignoredGrowth, 0);
+ testResizer( W, 0, -10, 10, -10, -10);
+ testResizer( W, 0, 0, -10, 10 * needlessGrowth, 10);
+ testResizer( W, 0, 0, 0, 0, 0);
+ testResizer( W, 0, 0, 10, 0, -10 * ignoredGrowth);
+ testResizer( W, 0, 10, -10, 10, 10);
+ testResizer( W, 0, 10, 0, 10, 10 * needlessGrowth);
+ testResizer( W, 0, 10, 10, 10, -10 * reversedGrowth);
+
+ // right resizer
+ testResizer( W, H/2, -10, -10, -10, 0);
+ testResizer( W, H/2, -10, 0, -10, 0);
+ testResizer( W, H/2, -10, 10, -10, 0);
+ testResizer( W, H/2, 0, -10, 0, 0);
+ testResizer( W, H/2, 0, 0, 0, 0);
+ testResizer( W, H/2, 0, 10, 0, 0);
+ testResizer( W, H/2, 10, -10, 10, 0);
+ testResizer( W, H/2, 10, 0, 10, 0);
+ testResizer( W, H/2, 10, 10, 10, 0);
+
+ // bottom right resizer
+ testResizer( W, H, -10, -10, -10, -10);
+ testResizer( W, H, -10, 0, -10 * ignoredGrowth, 0);
+ testResizer( W, H, -10, 10, -10 * reversedGrowth, 10);
+ testResizer( W, H, 0, -10, 0, -10 * ignoredGrowth);
+ testResizer( W, H, 0, 0, 0, 0);
+ testResizer( W, H, 0, 10, 10 * needlessGrowth, 10);
+ testResizer( W, H, 10, -10, 10, -10 * reversedGrowth);
+ testResizer( W, H, 10, 0, 10, 10 * needlessGrowth);
+ testResizer( W, H, 10, 10, 10, 10);
+
+ // bottom resizer
+ testResizer(W/2, H, -10, -10, 0, -10);
+ testResizer(W/2, H, -10, 0, 0, 0);
+ testResizer(W/2, H, -10, 10, 0, 10);
+ testResizer(W/2, H, 0, -10, 0, -10);
+ testResizer(W/2, H, 0, 0, 0, 0);
+ testResizer(W/2, H, 0, 10, 0, 10);
+ testResizer(W/2, H, 10, -10, 0, -10);
+ testResizer(W/2, H, 10, 0, 0, 0);
+ testResizer(W/2, H, 10, 10, 0, 10);
+
+ // bottom left resizer
+ testResizer( 0, H, -10, -10, 10, -10 * reversedGrowth);
+ testResizer( 0, H, -10, 0, 10, 10 * needlessGrowth);
+ testResizer( 0, H, -10, 10, 10, 10);
+ testResizer( 0, H, 0, -10, 0, -10 * ignoredGrowth);
+ testResizer( 0, H, 0, 0, 0, 0);
+ testResizer( 0, H, 0, 10, 10 * needlessGrowth, 10);
+ testResizer( 0, H, 10, -10, -10, -10);
+ testResizer( 0, H, 10, 0, -10 * ignoredGrowth, 0);
+ testResizer( 0, H, 10, 10, -10 * reversedGrowth, 10);
+
+ // left resizer
+ testResizer( 0, H/2, -10, -10, 10, 0);
+ testResizer( 0, H/2, -10, 0, 10, 0);
+ testResizer( 0, H/2, -10, 10, 10, 0);
+ testResizer( 0, H/2, 0, -10, 0, 0);
+ testResizer( 0, H/2, 0, 0, 0, 0);
+ testResizer( 0, H/2, 0, 10, 0, 0);
+ testResizer( 0, H/2, 10, -10, -10, 0);
+ testResizer( 0, H/2, 10, 0, -10, 0);
+ testResizer( 0, H/2, 10, 10, -10, 0);
+
+ // top left resizer
+ testResizer( 0, 0, -10, -10, 10, 10);
+ testResizer( 0, 0, -10, 0, 10, 10 * needlessGrowth);
+ testResizer( 0, 0, -10, 10, 10, -10 * reversedGrowth);
+ testResizer( 0, 0, 0, -10, 10 * needlessGrowth, 10);
+ testResizer( 0, 0, 0, 0, 0, 0);
+ testResizer( 0, 0, 0, 10, 0, -10 * ignoredGrowth);
+ testResizer( 0, 0, 10, -10, -10 * reversedGrowth, 10);
+ testResizer( 0, 0, 10, 0, -10 * ignoredGrowth, 0);
+ testResizer( 0, 0, 10, 10, -10, -10);
+ }
+ SpecialPowers.pushPrefEnv({"set": [["editor.resizing.preserve_ratio", false]]}, function() {
+ runTests(false);
+ SpecialPowers.pushPrefEnv({"set": [["editor.resizing.preserve_ratio", true]]}, function() {
+ runTests(true);
+ SimpleTest.finish();
+ });
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug641466.html b/editor/libeditor/tests/test_bug641466.html
new file mode 100644
index 000000000..4a77b0b89
--- /dev/null
+++ b/editor/libeditor/tests/test_bug641466.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=641466
+-->
+<head>
+ <title>Test for Bug 641466</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=641466">Mozilla Bug 641466</a>
+<p id="display"></p>
+<div id="content">
+<input value="&#x10451;&#x10467;&#x10455;&#x10451;">
+<textarea>&#x10451;&#x10467;&#x10455;&#x10451;</textarea>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 641466 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ function doTest(element) {
+ element.focus();
+ element.selectionStart = 4;
+ element.selectionEnd = 4;
+ synthesizeKey("VK_BACK_SPACE", {});
+ synthesizeKey("VK_BACK_SPACE", {});
+ synthesizeKey("VK_BACK_SPACE", {});
+ synthesizeKey("VK_BACK_SPACE", {});
+
+ ok(element.value, "", "4 backspaces should delete all of the characters in the " + element.localName);
+ }
+
+ doTest(document.querySelector("input"));
+ doTest(document.querySelector("textarea"));
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug645914.html b/editor/libeditor/tests/test_bug645914.html
new file mode 100644
index 000000000..cdf799e56
--- /dev/null
+++ b/editor/libeditor/tests/test_bug645914.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=645914
+-->
+<head>
+ <title>Test for Bug 645914</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=645914">Mozilla Bug 645914</a>
+<p id="display"></p>
+<div id="content">
+<textarea>foo
+bar</textarea>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 645914 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ SpecialPowers.pushPrefEnv({"set":[["layout.word_select.eat_space_to_next_word", true],
+ ["browser.triple_click_selects_paragraph", false]]}, startTest);
+});
+function startTest() {
+ var textarea = document.querySelector("textarea");
+ textarea.selectionStart = textarea.selectionEnd = 0;
+
+ // Simulate a double click on foo
+ synthesizeMouse(textarea, 5, 5, {clickCount: 2});
+
+ ok(true, "Testing word selection");
+ is(textarea.selectionStart, 0, "The start of the selection should be at the beginning of the text");
+ is(textarea.selectionEnd, 3, "The end of the selection should not include a newline character");
+
+ textarea.selectionStart = textarea.selectionEnd = 0;
+
+ // Simulate a triple click on foo
+ synthesizeMouse(textarea, 5, 5, {clickCount: 3});
+
+ ok(true, "Testing line selection");
+ is(textarea.selectionStart, 0, "The start of the selection should be at the beginning of the text");
+ is(textarea.selectionEnd, 3, "The end of the selection should not include a newline character");
+
+ textarea.selectionStart = textarea.selectionEnd = 0;
+ textarea.value = "Very very long value which would eventually overflow the visible section of the textarea";
+
+ // Simulate a quadruple click on Very
+ synthesizeMouse(textarea, 5, 5, {clickCount: 4});
+
+ ok(true, "Testing paragraph selection");
+ is(textarea.selectionStart, 0, "The start of the selection should be at the beginning of the text");
+ is(textarea.selectionEnd, textarea.value.length, "The end of the selection should be the end of the paragraph");
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug646194.html b/editor/libeditor/tests/test_bug646194.html
new file mode 100644
index 000000000..8a0e4a829
--- /dev/null
+++ b/editor/libeditor/tests/test_bug646194.html
@@ -0,0 +1,38 @@
+<!doctype html>
+<title>Mozilla Bug 646194</title>
+<link rel=stylesheet href="/tests/SimpleTest/test.css">
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=646194"
+ target="_blank">Mozilla Bug 646194</a>
+<iframe id="i" src="data:text/html,&lt;div contenteditable=true id=t&gt;test me now&lt;/div&gt;"></iframe>
+<script>
+SimpleTest.expectAssertions(1);
+
+function runTest() {
+ var i = document.getElementById("i");
+ i.focus();
+ var win = i.contentWindow;
+ var doc = i.contentDocument;
+ var t = doc.getElementById("t");
+ t.focus();
+ // put the caret at the end
+ win.getSelection().collapse(t.firstChild, 11);
+
+ // Simulate pression Option+Delete on Mac
+ // We do things this way because not every platform can invoke this
+ // command using the available key bindings.
+ SpecialPowers.doCommand(window, "cmd_wordPrevious");
+ SpecialPowers.doCommand(window, "cmd_wordPrevious");
+ SpecialPowers.doCommand(window, "cmd_deleteWordBackward");
+ SpecialPowers.doCommand(window, "cmd_deleteWordBackward");
+
+ // If we reach here, we haven't crashed. Phew!
+ // But let's check the value too, now that we're here.
+ is(t.textContent, "me now", "The command has worked correctly");
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+</script>
diff --git a/editor/libeditor/tests/test_bug668599.html b/editor/libeditor/tests/test_bug668599.html
new file mode 100644
index 000000000..8405d08ab
--- /dev/null
+++ b/editor/libeditor/tests/test_bug668599.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=668599
+-->
+<head>
+ <title>Test for Bug 668599</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=668599">Mozilla Bug 668599</a>
+<p id="display"></p>
+<div id="content">
+ <div id="test1">
+ block <span contenteditable>type here</span> block
+ </div>
+ <div id="test2">
+ <p contenteditable>
+ block <span>type here</span> block
+ </p>
+ </div>
+</div>
+
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 668599 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+function select(element) {
+ // select the element text content
+ var userSelection = window.getSelection();
+ window.getSelection().removeAllRanges();
+ var range = document.createRange();
+ range.setStart(element.firstChild, 0);
+ range.setEnd(element.firstChild, element.textContent.length);
+ userSelection.addRange(range);
+};
+
+function runTests() {
+ var span = document.querySelector("#test1 span");
+
+ // editable <span> => the <span> *content* should be deleted
+ select(span);
+ span.focus();
+ synthesizeKey("x", {});
+ is(span.textContent, "x", "The <span> content should have been replaced by 'x'.");
+
+ // same thing, but using [Del] instead of typing some text
+ document.execCommand("Undo", false, null);
+ select(span);
+ span.focus();
+ synthesizeKey("VK_DELETE", {});
+ is(span.textContent, "", "The <span> content should have been deleted.");
+
+ // <span> in editable block => the <span> *element* should be deleted
+ select(document.querySelector("#test2 span"));
+ document.querySelector("#test2 [contenteditable]").focus();
+ synthesizeKey("VK_DELETE", {});
+ is(document.querySelector("#test2 span"), null,
+ "The <span> element should have been deleted.");
+
+ // done
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug674770-1.html b/editor/libeditor/tests/test_bug674770-1.html
new file mode 100644
index 000000000..4ba65f507
--- /dev/null
+++ b/editor/libeditor/tests/test_bug674770-1.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=674770
+-->
+<head>
+ <title>Test for Bug 674770</title>
+ <script type="application/javascript" src="/MochiKit/packed.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=674770">Mozilla Bug 674770</a>
+<p id="display"></p>
+<div id="content">
+<a href="file_bug674770-1.html" id="link1">test</a>
+<div contenteditable>
+<a href="file_bug674770-1.html" id="link2">test</a>
+</div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ SpecialPowers.pushPrefEnv({"set":[["middlemouse.paste", true], ["dom.ipc.processCount", 1]]}, startTests);
+});
+
+function startTests() {
+ var tests = [
+ { description: "Testing link in <div>: ",
+ target: function () { return document.querySelector("#link1"); },
+ linkShouldWork: true },
+ { description: "Testing link in <div contenteditable>: ",
+ target: function () { return document.querySelector("#link2"); },
+ linkShouldWork: false },
+ ];
+ var currentTest;
+ function runNextTest() {
+ localStorage.removeItem("clicked");
+ currentTest = tests.shift();
+ if (!currentTest) {
+ SimpleTest.finish();
+ return;
+ }
+ ok(true, currentTest.description + "Starting to test...");
+ synthesizeMouseAtCenter(currentTest.target(), { button: 1 });
+ }
+
+
+ addEventListener("storage", function(e) {
+ is(e.key, "clicked", currentTest.description + "Key should always be 'clicked'");
+ is(e.newValue, "true", currentTest.description + "Value should always be 'true'");
+ ok(currentTest.linkShouldWork, currentTest.description + "The click operation on the link " + (currentTest.linkShouldWork ? "should work" : "shouldn't work"));
+ SimpleTest.executeSoon(runNextTest);
+ }, false);
+
+ SpecialPowers.addSystemEventListener(window, "click", function (aEvent) {
+ // When the click event should cause default action, e.g., opening the link,
+ // the event shouldn't have been consumed except the link handler.
+ // However, in e10s mode, it's not consumed during propagating the event but
+ // in non-e10s mode, it's consumed during the propagation. Therefore,
+ // we cannot check defaultPrevented value when the link should work as is
+ // if there is no way to detect if it's running in e10s mode or not.
+ // So, let's skip checking Event.defaultPrevented value when the link should
+ // work. In such case, we should receive "storage" event later.
+ if (currentTest.linkShouldWork) {
+ return;
+ }
+
+ ok(SpecialPowers.defaultPreventedInAnyGroup(aEvent),
+ currentTest.description + "The default action should be consumed because the link should work as is");
+ if (SpecialPowers.defaultPreventedInAnyGroup(aEvent)) {
+ // In this case, "storage" event won't be fired.
+ SimpleTest.executeSoon(runNextTest);
+ }
+ }, false);
+
+ SimpleTest.executeSoon(runNextTest);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug674770-2.html b/editor/libeditor/tests/test_bug674770-2.html
new file mode 100644
index 000000000..c69311e95
--- /dev/null
+++ b/editor/libeditor/tests/test_bug674770-2.html
@@ -0,0 +1,395 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=674770
+-->
+<head>
+ <title>Test for Bug 674770</title>
+ <script type="application/javascript" src="/MochiKit/packed.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=674770">Mozilla Bug 674770</a>
+<p id="display"></p>
+<div id="content">
+<iframe id="iframe" style="display: block; height: 14em;"
+ src="data:text/html,<input id='text' value='pasted'><input id='input' style='display: block;'><p id='editor1' contenteditable>editor1:</p><p id='editor2' contenteditable>editor2:</p>"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 674770 **/
+SimpleTest.waitForExplicitFinish();
+
+var iframe = document.getElementById("iframe");
+var frameWindow, frameDocument;
+
+var gClicked = false;
+var gClicking = null;
+var gDoPreventDefault1 = null;
+var gDoPreventDefault2 = null;
+
+function clickEventHandler(aEvent)
+{
+ if (aEvent.button == 1 && aEvent.target == gClicking) {
+ gClicked = true;
+ }
+ if (gDoPreventDefault1 == aEvent.target) {
+ aEvent.preventDefault();
+ }
+ if (gDoPreventDefault2 == aEvent.target) {
+ aEvent.preventDefault();
+ }
+}
+
+// NOTE: tests need to check the result *after* the content is actually
+// modified. Sometimes, the modification is delayed. Therefore, there
+// are a lot of functions and SimpleTest.executeSoon()s.
+SimpleTest.waitForFocus(function() {
+ SpecialPowers.pushPrefEnv({"set":[["middlemouse.contentLoadURL", false],
+ ["middlemouse.paste", true]]}, startTest);
+});
+function startTest() {
+ frameWindow = iframe.contentWindow;
+ frameDocument = iframe.contentDocument;
+
+ frameDocument.getElementById("input").addEventListener("click", clickEventHandler, false);
+ frameDocument.getElementById("editor1").addEventListener("click", clickEventHandler, false);
+ frameDocument.getElementById("editor2").addEventListener("click", clickEventHandler, false);
+
+ var text = frameDocument.getElementById("text");
+
+ text.focus();
+ if (navigator.platform.indexOf("Linux") == 0) {
+ synthesizeKey("a", { altKey: true }, frameWindow);
+ } else {
+ synthesizeKey("a", { accelKey: true }, frameWindow);
+ }
+ // Windows and Mac don't have primary selection, we should copy the text to
+ // the global clipboard.
+ if (!SpecialPowers.supportsSelectionClipboard()) {
+ SimpleTest.waitForClipboard("pasted",
+ function() { synthesizeKey("c", { accelKey: true }, frameWindow); },
+ function() { SimpleTest.executeSoon(runInputTests1) },
+ cleanup);
+ } else {
+ // Otherwise, don't call waitForClipboard since it breaks primary
+ // selection.
+ runInputTests1();
+ }
+}
+
+function runInputTests1()
+{
+ var input = frameDocument.getElementById("input");
+
+ // first, copy text.
+
+ // when middle clicked in focused input element, text should be pasted.
+ input.value = "";
+ input.focus();
+
+ SimpleTest.executeSoon(function() {
+ gClicked = false;
+ gClicking = input;
+ gDoPreventDefault1 = null;
+ gDoPreventDefault2 = null;
+
+ synthesizeMouseAtCenter(input, {button: 1}, frameWindow);
+
+ SimpleTest.executeSoon(function() {
+ todo(gClicked, "click event hasn't been fired for runInputTests1");
+ is(input.value, "pasted", "failed to paste in input element");
+
+ SimpleTest.executeSoon(runInputTests2);
+ });
+ });
+}
+
+function runInputTests2()
+{
+ var input = frameDocument.getElementById("input");
+
+ // even if the input element hasn't had focus, middle click should set focus
+ // to it and paste the text.
+ input.value = "";
+ input.blur();
+
+ SimpleTest.executeSoon(function() {
+ gClicked = false;
+ gClicking = input;
+ gDoPreventDefault1 = null;
+ gDoPreventDefault2 = null;
+
+ synthesizeMouseAtCenter(input, {button: 1}, frameWindow);
+
+ SimpleTest.executeSoon(function() {
+ todo(gClicked, "click event hasn't been fired for runInputTests2");
+ is(input.value, "pasted",
+ "failed to paste in input element when it hasn't had focus yet");
+
+ SimpleTest.executeSoon(runInputTests3);
+ });
+ });
+}
+
+function runInputTests3()
+{
+ var input = frameDocument.getElementById("input");
+ var editor1 = frameDocument.getElementById("editor1");
+
+ // preventDefault() of HTML editor's click event handler shouldn't prevent
+ // middle click pasting in input element.
+ input.value = "";
+ input.focus();
+
+ SimpleTest.executeSoon(function() {
+ gClicked = false;
+ gClicking = input;
+ gDoPreventDefault1 = editor1;
+ gDoPreventDefault2 = null;
+
+ synthesizeMouseAtCenter(input, {button: 1}, frameWindow);
+
+ SimpleTest.executeSoon(function() {
+ todo(gClicked, "click event hasn't been fired for runInputTests3");
+ is(input.value, "pasted",
+ "failed to paste in input element when editor1 does preventDefault()");
+
+ SimpleTest.executeSoon(runInputTests4);
+ });
+ });
+}
+
+function runInputTests4()
+{
+ var input = frameDocument.getElementById("input");
+ var editor1 = frameDocument.getElementById("editor1");
+
+ // preventDefault() of input element's click event handler should prevent
+ // middle click pasting in it.
+ input.value = "";
+ input.focus();
+
+ SimpleTest.executeSoon(function() {
+ gClicked = false;
+ gClicking = input;
+ gDoPreventDefault1 = input;
+ gDoPreventDefault2 = null;
+
+ synthesizeMouseAtCenter(input, {button: 1}, frameWindow);
+
+ SimpleTest.executeSoon(function() {
+ todo(gClicked, "click event hasn't been fired for runInputTests4");
+ todo_is(input.value, "",
+ "pasted in input element when it does preventDefault()");
+
+ SimpleTest.executeSoon(runContentEditableTests1);
+ });
+ });
+}
+
+function runContentEditableTests1()
+{
+ var editor1 = frameDocument.getElementById("editor1");
+
+ // when middle clicked in focused contentediable editor, text should be
+ // pasted.
+ editor1.innerHTML = "editor1:";
+ editor1.focus();
+
+ SimpleTest.executeSoon(function() {
+ gClicked = false;
+ gClicking = editor1;
+ gDoPreventDefault1 = null;
+ gDoPreventDefault2 = null;
+
+ synthesizeMouseAtCenter(editor1, {button: 1}, frameWindow);
+
+ SimpleTest.executeSoon(function() {
+ todo(gClicked, "click event hasn't been fired for runContentEditableTests1");
+ is(editor1.innerHTML, "editor1:pasted",
+ "failed to paste text in contenteditable editor");
+ SimpleTest.executeSoon(runContentEditableTests2);
+ });
+ });
+}
+
+function runContentEditableTests2()
+{
+ var editor1 = frameDocument.getElementById("editor1");
+
+ // even if the contenteditable editor hasn't had focus, middle click should
+ // set focus to it and paste the text.
+ editor1.innerHTML = "editor1:";
+ editor1.blur();
+
+ SimpleTest.executeSoon(function() {
+ gClicked = false;
+ gClicking = editor1;
+ gDoPreventDefault1 = null;
+ gDoPreventDefault2 = null;
+
+ synthesizeMouseAtCenter(editor1, {button: 1}, frameWindow);
+
+ SimpleTest.executeSoon(function() {
+ todo(gClicked, "click event hasn't been fired for runContentEditableTests2");
+ is(editor1.innerHTML, "editor1:pasted",
+ "failed to paste in contenteditable editor #1 when it hasn't had focus yet");
+ SimpleTest.executeSoon(runContentEditableTests3);
+ });
+ });
+}
+
+function runContentEditableTests3()
+{
+ var editor1 = frameDocument.getElementById("editor1");
+ var editor2 = frameDocument.getElementById("editor2");
+
+ // When editor1 has focus but editor2 is middle clicked, should be pasted
+ // in the editor2.
+ editor1.innerHTML = "editor1:";
+ editor2.innerHTML = "editor2:";
+ editor1.focus();
+
+ SimpleTest.executeSoon(function() {
+ gClicked = false;
+ gClicking = editor2;
+ gDoPreventDefault1 = null;
+ gDoPreventDefault2 = null;
+
+ synthesizeMouseAtCenter(editor2, {button: 1}, frameWindow);
+
+ SimpleTest.executeSoon(function() {
+ todo(gClicked, "click event hasn't been fired for runContentEditableTests3");
+ is(editor1.innerHTML, "editor1:",
+ "pasted in contenteditable editor #1 when editor2 is clicked");
+ is(editor2.innerHTML, "editor2:pasted",
+ "failed to paste in contenteditable editor #2 when editor2 is clicked");
+ SimpleTest.executeSoon(runContentEditableTests4);
+ });
+ });
+}
+
+function runContentEditableTests4()
+{
+ var editor1 = frameDocument.getElementById("editor1");
+
+ // preventDefault() of editor1's click event handler should prevent
+ // middle click pasting in it.
+ editor1.innerHTML = "editor1:";
+ editor1.focus();
+
+ SimpleTest.executeSoon(function() {
+ gClicked = false;
+ gClicking = editor1;
+ gDoPreventDefault1 = editor1;
+ gDoPreventDefault2 = null;
+
+ synthesizeMouseAtCenter(editor1, {button: 1}, frameWindow);
+
+ SimpleTest.executeSoon(function() {
+ todo(gClicked, "click event hasn't been fired for runContentEditableTests4");
+ todo_is(editor1.innerHTML, "editor1:",
+ "pasted in contenteditable editor #1 when it does preventDefault()");
+ SimpleTest.executeSoon(runContentEditableTests5);
+ });
+ });
+}
+
+function runContentEditableTests5()
+{
+ var editor1 = frameDocument.getElementById("editor1");
+ var editor2 = frameDocument.getElementById("editor2");
+
+ // preventDefault() of editor1's click event handler shouldn't prevent
+ // middle click pasting in editor2.
+ editor1.innerHTML = "editor1:";
+ editor2.innerHTML = "editor2:";
+ editor2.focus();
+
+ SimpleTest.executeSoon(function() {
+ gClicked = false;
+ gClicking = editor2;
+ gDoPreventDefault1 = editor1;
+ gDoPreventDefault2 = null;
+
+ synthesizeMouseAtCenter(editor2, {button: 1}, frameWindow);
+
+ SimpleTest.executeSoon(function() {
+ todo(gClicked, "click event hasn't been fired for runContentEditableTests5");
+ is(editor1.innerHTML, "editor1:",
+ "pasted in contenteditable editor #1?");
+ is(editor2.innerHTML, "editor2:pasted",
+ "failed to paste in contenteditable editor #2");
+
+ SimpleTest.executeSoon(initForBodyEditableDocumentTests);
+ });
+ });
+}
+
+function initForBodyEditableDocumentTests()
+{
+ frameDocument.getElementById("input").removeEventListener("click", clickEventHandler, false);
+ frameDocument.getElementById("editor1").removeEventListener("click", clickEventHandler, false);
+ frameDocument.getElementById("editor2").removeEventListener("click", clickEventHandler, false);
+
+ iframe.onload =
+ function (aEvent) { SimpleTest.executeSoon(runBodyEditableDocumentTests1); };
+ iframe.src =
+ "data:text/html,<body contenteditable>body:</body>";
+}
+
+function runBodyEditableDocumentTests1()
+{
+ frameWindow = iframe.contentWindow;
+ frameDocument = iframe.contentDocument;
+
+ var body = frameDocument.body;
+
+ is(body.innerHTML, "body:",
+ "failed to initialize at runBodyEditableDocumentTests1");
+
+ // middle click on html element should cause pasting text in its body.
+ synthesizeMouseAtCenter(frameDocument.documentElement, {button: 1}, frameWindow);
+
+ SimpleTest.executeSoon(function() {
+ is(body.innerHTML,
+ "body:pasted",
+ "failed to paste in editable body element when clicked on html element");
+
+ SimpleTest.executeSoon(runBodyEditableDocumentTests2);
+ });
+}
+
+function runBodyEditableDocumentTests2()
+{
+ frameDocument.body.innerHTML = "body:<span id='span' contenteditable='false'>non-editable</span>";
+
+ var body = frameDocument.body;
+
+ is(body.innerHTML, "body:<span id=\"span\" contenteditable=\"false\">non-editable</span>",
+ "failed to initialize at runBodyEditableDocumentTests2");
+
+ synthesizeMouseAtCenter(frameDocument.getElementById("span"), {button: 1}, frameWindow);
+
+ SimpleTest.executeSoon(function() {
+ is(body.innerHTML,
+ "body:<span id=\"span\" contenteditable=\"false\">non-editable</span>",
+ "pasted when middle clicked in non-editable element");
+
+ SimpleTest.executeSoon(cleanup);
+ });
+}
+
+function cleanup()
+{
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug674861.html b/editor/libeditor/tests/test_bug674861.html
new file mode 100644
index 000000000..5974b4aed
--- /dev/null
+++ b/editor/libeditor/tests/test_bug674861.html
@@ -0,0 +1,185 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=674861
+-->
+<head>
+ <title>Test for Bug 674861</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=674861">Mozilla Bug 674861</a>
+<p id="display"></p>
+<div id="content">
+ <section id="test1">
+ <h2> Editable Bullet List </h2>
+ <ul contenteditable>
+ <li> item A </li>
+ <li> item B </li>
+ <li> item C </li>
+ </ul>
+
+ <h2> Editable Ordered List </h2>
+ <ol contenteditable>
+ <li> item A </li>
+ <li> item B </li>
+ <li> item C </li>
+ </ol>
+
+ <h2> Editable Definition List </h2>
+ <dl contenteditable>
+ <dt> term A </dt>
+ <dd> definition A </dd>
+ <dt> term B </dt>
+ <dd> definition B </dd>
+ <dt> term C </dt>
+ <dd> definition C </dd>
+ </dl>
+ </section>
+
+ <section id="test2" contenteditable>
+ <h2> Bullet List In Editable Section </h2>
+ <ul>
+ <li> item A </li>
+ <li> item B </li>
+ <li> item C </li>
+ </ul>
+
+ <h2> Ordered List In Editable Section </h2>
+ <ol>
+ <li> item A </li>
+ <li> item B </li>
+ <li> item C </li>
+ </ol>
+
+ <h2> Definition List In Editable Section </h2>
+ <dl>
+ <dt> term A </dt>
+ <dd> definition A </dd>
+ <dt> term B </dt>
+ <dd> definition B </dd>
+ <dt> term C </dt>
+ <dd> definition C </dd>
+ </dl>
+ </section>
+</div>
+
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 674861 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+const CARET_BEGIN = 0;
+const CARET_MIDDLE = 1;
+const CARET_END = 2;
+
+function try2split(element, caretPos) {
+ // compute the requested position
+ var len = element.textContent.length;
+ var pos = -1;
+ switch (caretPos) {
+ case CARET_BEGIN:
+ pos = 0;
+ break;
+ case CARET_MIDDLE:
+ pos = Math.floor(len/2);
+ break;
+ case CARET_END:
+ pos = len;
+ break;
+ }
+
+ // put the caret on the requested position
+ var sel = window.getSelection();
+ for (var i = 0; i < sel.rangeCount; i++) {
+ var range = sel.getRangeAt(i);
+ sel.removeRange(range);
+ }
+ range = document.createRange();
+ range.setStart(element.firstChild, pos);
+ range.setEnd(element.firstChild, pos);
+ sel.addRange(range);
+
+ // simulates two [Return] keypresses
+ synthesizeKey("VK_RETURN", {});
+ synthesizeKey("VK_RETURN", {});
+}
+
+function runTests() {
+ const test1 = document.getElementById("test1");
+ const test2 = document.getElementById("test2");
+
+ // -----------------------------------------------------------------------
+ // #test1: editable lists should NOT be splittable
+ // -----------------------------------------------------------------------
+ const ul = test1.querySelector("ul");
+ const ol = test1.querySelector("ol");
+ const dl = test1.querySelector("dl");
+
+ // bullet list
+ ul.focus();
+ try2split(ul.querySelector("li"), CARET_END);
+ is(test1.querySelectorAll("ul").length, 1,
+ "The <ul contenteditable> list should not be splittable.");
+ is(ul.querySelectorAll("li").length, 5,
+ "Two new <li> elements should have been created.");
+
+ // ordered list
+ ol.focus();
+ try2split(ol.querySelector("li"), CARET_END);
+ is(test1.querySelectorAll("ol").length, 1,
+ "The <ol contenteditable> list should not be splittable.");
+ is(ol.querySelectorAll("li").length, 5,
+ "Two new <li> elements should have been created.");
+
+ // definition list
+ dl.focus();
+ try2split(dl.querySelector("dd"), CARET_END);
+ is(test1.querySelectorAll("dl").length, 1,
+ "The <dl contenteditable> list should not be splittable.");
+ is(dl.querySelectorAll("dt").length, 5,
+ "Two new <dt> elements should have been created.");
+
+ // -----------------------------------------------------------------------
+ // #test2: lists in editable blocks should be splittable
+ // -----------------------------------------------------------------------
+ test2.focus();
+
+ // bullet list
+ try2split(test2.querySelector("ul li"), CARET_END);
+ is(test2.querySelectorAll("ul").length, 2,
+ "The <ul> list should have been splitted.");
+ is(test2.querySelectorAll("ul li").length, 3,
+ "No new <li> element should have been created.");
+ is(test2.querySelectorAll("ul+p").length, 1,
+ "A new paragraph should have been created.");
+
+ // ordered list
+ try2split(test2.querySelector("ol li"), CARET_END);
+ is(test2.querySelectorAll("ol").length, 2,
+ "The <ol> list should have been splitted.");
+ is(test2.querySelectorAll("ol li").length, 3,
+ "No new <li> element should have been created.");
+ is(test2.querySelectorAll("ol+p").length, 1,
+ "A new paragraph should have been created.");
+
+ // definition list
+ try2split(test2.querySelector("dl dd"), CARET_END);
+ is(test2.querySelectorAll("dl").length, 2,
+ "The <dl> list should have been splitted.");
+ is(test2.querySelectorAll("dt").length, 3,
+ "No new <dt> element should have been created.");
+ is(test2.querySelectorAll("dl+p").length, 1,
+ "A new paragraph should have been created.");
+
+ // done
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug676401.html b/editor/libeditor/tests/test_bug676401.html
new file mode 100644
index 000000000..aa468fdc6
--- /dev/null
+++ b/editor/libeditor/tests/test_bug676401.html
@@ -0,0 +1,119 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=676401
+-->
+<head>
+ <title>Test for Bug 676401</title>
+ <script type="application/javascript" src="/MochiKit/packed.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=676401">Mozilla Bug 676401</a>
+<p id="display"></p>
+<div id="content">
+ <!-- we need a blockquote to test the "outdent" command -->
+ <section>
+ <blockquote> not editable </blockquote>
+ </section>
+ <section contenteditable>
+ <blockquote> editable </blockquote>
+ </section>
+</div>
+
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 676401 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+var gBlock1, gBlock2;
+
+var alwaysEnabledCommands = [
+ "contentReadOnly",
+ "copy",
+ "cut",
+ "enableInlineTableEditing",
+ "enableObjectResizing",
+ "insertBrOnReturn",
+ "selectAll",
+ "styleWithCSS",
+];
+
+function IsCommandEnabled(command) {
+ var enabled;
+
+ // non-editable div: should return false unless alwaysEnabled
+ window.getSelection().selectAllChildren(gBlock1);
+ enabled = document.queryCommandEnabled(command);
+ is(enabled, alwaysEnabledCommands.indexOf(command) != -1,
+ "'" + command + "' should not be enabled on a non-editable block.");
+
+ // editable div: should return true
+ window.getSelection().selectAllChildren(gBlock2);
+ enabled = document.queryCommandEnabled(command);
+ is(enabled, true, "'" + command + "' should be enabled on an editable block.");
+}
+
+function runTests() {
+ var i, commands;
+ gBlock1 = document.querySelector("#content section blockquote");
+ gBlock2 = document.querySelector("#content [contenteditable] blockquote");
+
+ // common commands: test with and without "styleWithCSS"
+ commands = [
+ "bold", "italic", "underline", "strikeThrough",
+ "subscript", "superscript", "foreColor", "backColor", "hiliteColor",
+ "fontName", "fontSize",
+ "justifyLeft", "justifyCenter", "justifyRight", "justifyFull",
+ "indent", "outdent",
+ "insertOrderedList", "insertUnorderedList", "insertParagraph",
+ "heading", "formatBlock",
+ "contentReadOnly", "createLink",
+ "decreaseFontSize", "increaseFontSize",
+ "insertHTML", "insertHorizontalRule", "insertImage",
+ "removeFormat", "selectAll", "styleWithCSS"
+ ];
+ document.execCommand("styleWithCSS", false, false);
+ for (i = 0; i < commands.length; i++)
+ IsCommandEnabled(commands[i]);
+ document.execCommand("styleWithCSS", false, true);
+ for (i = 0; i < commands.length; i++)
+ IsCommandEnabled(commands[i]);
+
+ // Mozilla-specific stuff
+ commands = ["enableInlineTableEditing", "enableObjectResizing", "insertBrOnReturn"];
+ for (i = 0; i < commands.length; i++)
+ IsCommandEnabled(commands[i]);
+
+ // These are privileged, and available only to chrome.
+ commands = ["paste"];
+ for (i = 0; i < commands.length; i++) {
+ is(document.queryCommandEnabled(commands[i]), false,
+ "Command should not be enabled for non-privileged code");
+ is(SpecialPowers.wrap(document).queryCommandEnabled(commands[i]), true,
+ "Command should be enabled for privileged code");
+ is(document.execCommand(commands[i], false, false), false, "Should return false: " + commands[i]);
+ is(SpecialPowers.wrap(document).execCommand(commands[i], false, false), true, "Should return true: " + commands[i]);
+ }
+
+ // delete/undo/redo -- we have to execute this commands because:
+ // * there's nothing to undo if we haven't modified the selection first
+ // * there's nothing to redo if we haven't undone something first
+ commands = ["delete", "undo", "redo"];
+ for (i = 0; i < commands.length; i++) {
+ IsCommandEnabled(commands[i]);
+ document.execCommand(commands[i], false, false);
+ }
+
+ // done
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug677752.html b/editor/libeditor/tests/test_bug677752.html
new file mode 100644
index 000000000..8809c1ead
--- /dev/null
+++ b/editor/libeditor/tests/test_bug677752.html
@@ -0,0 +1,107 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=677752
+-->
+<head>
+ <title>Test for Bug 677752</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=677752">Mozilla Bug 677752</a>
+<p id="display"></p>
+<div id="content">
+ <section contenteditable> foo bar </section>
+ <div contenteditable> foo bar </div>
+ <p contenteditable> foo bar </p>
+</div>
+
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 677752 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+function selectEditor(aEditor) {
+ aEditor.focus();
+ var selection = window.getSelection();
+ selection.selectAllChildren(aEditor);
+ selection.collapseToStart();
+}
+
+function runTests() {
+ var editor, node, initialHTML;
+ document.execCommand('styleWithCSS', false, true);
+
+ // editable <section>
+ editor = document.querySelector("section[contenteditable]");
+ initialHTML = editor.innerHTML;
+ selectEditor(editor);
+ // editable <section>: justify
+ document.execCommand("justifyright", false, null);
+ node = editor.querySelector("*");
+ is(node.nodeName.toLowerCase(), "div", "'justifyright' should create a <div> in the editable <section>.");
+ is(node.style.textAlign, "right", "'justifyright' should create a 'text-align: right' CSS rule.");
+ document.execCommand("undo", false, null);
+ // editable <section>: indent
+ document.execCommand("indent", false, null);
+ node = editor.querySelector("*");
+ is(node.nodeName.toLowerCase(), "div", "'indent' should create a <div> in the editable <section>.");
+ is(node.style.marginLeft, "40px", "'indent' should create a 'margin-left: 40px' CSS rule.");
+ // editable <section>: undo with outdent
+ // this should remove the whole <div> but only removing the CSS rule would be acceptable, too
+ document.execCommand("outdent", false, null);
+ is(editor.innerHTML, initialHTML, "'outdent' should undo the 'indent' action.");
+ // editable <section>: outdent again
+ document.execCommand("outdent", false, null);
+ is(editor.innerHTML, initialHTML, "another 'outdent' should not modify the <section> element.");
+
+ // editable <div>
+ editor = document.querySelector("div[contenteditable]");
+ initialHTML = editor.innerHTML;
+ selectEditor(editor);
+ // editable <div>: justify
+ document.execCommand("justifyright", false, null);
+ node = editor.querySelector("*");
+ is(node.nodeName.toLowerCase(), "div", "'justifyright' should create a <div> in the editable <div>.");
+ is(node.style.textAlign, "right", "'justifyright' should create a 'text-align: right' CSS rule.");
+ document.execCommand("undo", false, null);
+ // editable <div>: indent
+ document.execCommand("indent", false, null);
+ node = editor.querySelector("*");
+ is(node.nodeName.toLowerCase(), "div", "'indent' should create a <div> in the editable <div>.");
+ is(node.style.marginLeft, "40px", "'indent' should create a 'margin-left: 40px' CSS rule.");
+ // editable <div>: undo with outdent
+ // this should remove the whole <div> but only removing the CSS rule would be acceptable, too
+ document.execCommand("outdent", false, null);
+ is(editor.innerHTML, initialHTML, "'outdent' should undo the 'indent' action.");
+ // editable <div>: outdent again
+ document.execCommand("outdent", false, null);
+ is(editor.innerHTML, initialHTML, "another 'outdent' should not modify the <div> element.");
+
+ // editable <p>
+ // all block-level commands should be ignored (<p><div/></p> is not valid)
+ editor = document.querySelector("p[contenteditable]");
+ initialHTML = editor.innerHTML;
+ selectEditor(editor);
+ // editable <p>: justify
+ document.execCommand("justifyright", false, null);
+ is(editor.innerHTML, initialHTML, "'justifyright' should have no effect on <p>.");
+ // editable <p>: indent
+ document.execCommand("indent", false, null);
+ is(editor.innerHTML, initialHTML, "'indent' should have no effect on <p>.");
+ // editable <p>: outdent
+ document.execCommand("outdent", false, null);
+ is(editor.innerHTML, initialHTML, "'outdent' should have no effect on <p>.");
+
+ // done
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug681229.html b/editor/libeditor/tests/test_bug681229.html
new file mode 100644
index 000000000..6debcfde7
--- /dev/null
+++ b/editor/libeditor/tests/test_bug681229.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=681229
+-->
+<head>
+ <title>Test for Bug 681229</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/WindowSnapshot.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=681229">Mozilla Bug 681229</a>
+<p id="display"></p>
+<div id="content">
+<textarea spellcheck="false"></textarea>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 681229 **/
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var t = document.querySelector("textarea");
+ t.focus();
+
+ const kValue = "a\r\nb";
+ const kExpectedValue = (navigator.platform.indexOf("Win") == 0) ?
+ "a\nb" : kValue;
+
+ SimpleTest.waitForClipboard(kExpectedValue,
+ function() {
+ SpecialPowers.clipboardCopyString(kValue);
+ },
+ function() {
+ synthesizeKey("V", {accelKey: true});
+ is(t.value, "a\nb", "The carriage return has been correctly sanitized");
+ SimpleTest.finish();
+ },
+ function() {
+ SimpleTest.finish();
+ }
+ );
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug686203.html b/editor/libeditor/tests/test_bug686203.html
new file mode 100644
index 000000000..c1a856aae
--- /dev/null
+++ b/editor/libeditor/tests/test_bug686203.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ - You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=686203
+-->
+
+<head>
+ <title>Test for Bug 686203</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=686203">Mozilla Bug 686203</a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+
+ <pre id="test">
+ <script type="application/javascript">
+
+ /** Test for Bug 686203 **/
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(function() {
+ var ce = document.getElementById("ce");
+ var input = document.getElementById("input");
+ ce.focus();
+
+ var eventDetails = { button : 2 };
+ synthesizeMouseAtCenter(input, eventDetails);
+
+ synthesizeKey("Z", {});
+
+ /* check values */
+ is(input.value, "Z", "input correctly focused after right-click");
+ is(ce.textContent, "abc", "contenteditable correctly blurred after right-click on input");
+
+ SimpleTest.finish();
+ });
+ </script>
+ </pre>
+
+ <input type="text" value="" id="input" />
+ <div id="ce" contenteditable="true">abc</div>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug692520.html b/editor/libeditor/tests/test_bug692520.html
new file mode 100644
index 000000000..6dfefd8db
--- /dev/null
+++ b/editor/libeditor/tests/test_bug692520.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=692520
+-->
+<head>
+ <title>Test for Bug 692520</title>
+ <script type="application/javascript" src="/MochiKit/packed.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=692520">Mozilla Bug 692520</a>
+<p id="display"></p>
+<div id="content">
+<textarea></textarea>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 692520 **/
+function test(prop, value) {
+ var t = document.querySelector("textarea");
+ t.value = "testing";
+ t.selectionStart = 1;
+ t.selectionEnd = 3;
+ t.selectionDirection = "backward";
+ t.style.display = "";
+ document.body.clientWidth;
+ t.style.display = "none";
+ is(t[prop], value, "Correct value for the " + prop + " property");
+}
+
+test("selectionStart", 1);
+test("selectionEnd", 3);
+test("selectionDirection", "backward");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug697842.html b/editor/libeditor/tests/test_bug697842.html
new file mode 100644
index 000000000..463ff76dc
--- /dev/null
+++ b/editor/libeditor/tests/test_bug697842.html
@@ -0,0 +1,117 @@
+<!DOCTYPE>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=697842
+-->
+<head>
+ <title>Test for Bug 697842</title>
+ <script type="application/javascript" src="/MochiKit/packed.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="display">
+ <p id="editor" contenteditable style="min-height: 1.5em;"></p>
+</div>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+/** Test for Bug 697842 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+function runTests()
+{
+ var editor = document.getElementById("editor");
+ editor.focus();
+
+ SimpleTest.executeSoon(function() {
+ var composingString = "";
+
+ function handler(aEvent) {
+ switch (aEvent.type) {
+ case "compositionstart":
+ // Selected string at starting composition must be empty in this test.
+ is(aEvent.data, "", "mismatch selected string");
+ break;
+ case "compositionupdate":
+ case "compositionend":
+ is(aEvent.data, composingString, "mismatch composition string");
+ break;
+ default:
+ break;
+ }
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ }
+
+ editor.addEventListener("compositionstart", handler, true);
+ editor.addEventListener("compositionend", handler, true);
+ editor.addEventListener("compositionupdate", handler, true);
+ editor.addEventListener("text", handler, true);
+
+ // input first character
+ composingString = "\u306B";
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": composingString,
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+
+ // input second character
+ composingString = "\u306B\u3085";
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": composingString,
+ "clauses":
+ [
+ { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 2, "length": 0 }
+ });
+
+ // convert them
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": composingString,
+ "clauses":
+ [
+ { "length": 2,
+ "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }
+ ]
+ },
+ "caret": { "start": 2, "length": 0 }
+ });
+
+ synthesizeComposition({ type: "compositioncommitasis" });
+
+ is(editor.innerHTML, composingString,
+ "editor has unexpected result");
+
+ editor.removeEventListener("compositionstart", handler, true);
+ editor.removeEventListener("compositionend", handler, true);
+ editor.removeEventListener("compositionupdate", handler, true);
+ editor.removeEventListener("text", handler, true);
+
+ SimpleTest.finish();
+ });
+}
+
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_bug725069.html b/editor/libeditor/tests/test_bug725069.html
new file mode 100644
index 000000000..5096ede3c
--- /dev/null
+++ b/editor/libeditor/tests/test_bug725069.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=725069
+-->
+<head>
+ <title>Test for Bug 725069</title>
+ <script type="application/javascript" src="/MochiKit/packed.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body contenteditable>abc<!-- XXX -->def<span></span>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=725069">Mozilla Bug 725069</a>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 725069 **/
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var body = document.querySelector("body");
+ is(body.firstChild.nodeType, body.TEXT_NODE, "The first node is a text node");
+ is(body.firstChild.nodeValue, "abc", "The first text node is there");
+ is(body.firstChild.nextSibling.nodeType, body.COMMENT_NODE, "The second node is a comment node");
+ is(body.firstChild.nextSibling.nodeValue, " XXX ", "The value of the comment node is not changed");
+ is(body.firstChild.nextSibling.nextSibling.nodeType, body.TEXT_NODE, "The last text node is a text node");
+ is(body.firstChild.nextSibling.nextSibling.nodeValue, "def", "The last next node is there");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug735059.html b/editor/libeditor/tests/test_bug735059.html
new file mode 100644
index 000000000..3b81ce48b
--- /dev/null
+++ b/editor/libeditor/tests/test_bug735059.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=735059
+-->
+<title>Test for Bug 735059</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=735059">Mozilla Bug 735059</a>
+<div id="display" contenteditable>foo</div>
+<pre id="test">
+<script>
+/** Test for Bug 735059 **/
+
+// Value defaults to the empty string, which evaluates to true, so this
+// disables CSS styling
+document.execCommand("usecss");
+getSelection().selectAllChildren(document.getElementById("display"));
+document.execCommand("bold");
+is(document.getElementById("display").innerHTML, "<b>foo</b>",
+ "execCommand() needs to work with only one parameter");
+</script>
+</pre>
diff --git a/editor/libeditor/tests/test_bug738366.html b/editor/libeditor/tests/test_bug738366.html
new file mode 100644
index 000000000..a54aec7a2
--- /dev/null
+++ b/editor/libeditor/tests/test_bug738366.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=738366
+-->
+<title>Test for Bug 738366</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=738366">Mozilla Bug 738366</a>
+<div id="display" contenteditable>foobarbaz</div>
+<script>
+/** Test for Bug 738366 **/
+
+getSelection().collapse(document.getElementById("display").firstChild, 3);
+getSelection().extend(document.getElementById("display").firstChild, 6);
+document.execCommand("bold");
+is(document.getElementById("display").innerHTML, "foo<b>bar</b>baz",
+ "styleWithCSS must default to false");
+document.execCommand("stylewithcss", false, "true");
+document.execCommand("bold");
+document.execCommand("bold");
+is(document.getElementById("display").innerHTML,
+ 'foo<span style="font-weight: bold;">bar</span>baz',
+ "styleWithCSS must be settable to true");
+</script>
diff --git a/editor/libeditor/tests/test_bug740784.html b/editor/libeditor/tests/test_bug740784.html
new file mode 100644
index 000000000..26c918241
--- /dev/null
+++ b/editor/libeditor/tests/test_bug740784.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ - You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=740784
+-->
+
+<head>
+ <title>Test for Bug 740784</title>
+ <script type="application/javascript" src="/MochiKit/packed.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=740784">Mozilla Bug 740784</a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+
+ <pre id="test">
+ <script type="application/javascript">
+
+ /** Test for Bug 740784 **/
+
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(function() {
+ var t1 = $("t1");
+
+ t1.focus();
+ synthesizeKey("VK_END", {});
+ synthesizeKey("VK_BACK_SPACE", {});
+ synthesizeKey("Z", {accelKey: true});
+
+ // Was the former bogus node changed to a mozBR?
+ is(t1.value, "a", "trailing <br> correctly ignored");
+
+ SimpleTest.finish();
+ });
+ </script>
+ </pre>
+
+ <textarea id="t1" rows="2" columns="80">a</textarea>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug742261.html b/editor/libeditor/tests/test_bug742261.html
new file mode 100644
index 000000000..9ad41dd52
--- /dev/null
+++ b/editor/libeditor/tests/test_bug742261.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=742261
+-->
+<title>Test for Bug 742261</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<body>
+<script>
+is(document.execCommandShowHelp, undefined,
+ "execCommandShowHelp shouldn't exist");
+is(document.queryCommandText, undefined,
+ "queryCommandText shouldn't exist");
+</script>
diff --git a/editor/libeditor/tests/test_bug757371.html b/editor/libeditor/tests/test_bug757371.html
new file mode 100644
index 000000000..5ca41a595
--- /dev/null
+++ b/editor/libeditor/tests/test_bug757371.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=757371
+-->
+<title>Test for Bug 757371</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=757371">Mozilla Bug 757371</a>
+<div contenteditable></div>
+<script>
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var div = document.querySelector("div");
+ div.focus();
+ getSelection().collapse(div, 0);
+ document.execCommand("bold");
+ sendString("ab");
+ sendKey("BACK_SPACE");
+ sendChar("b");
+
+ is(div.innerHTML, "<b>ab</b>");
+
+ SimpleTest.finish();
+});
+</script>
diff --git a/editor/libeditor/tests/test_bug757771.html b/editor/libeditor/tests/test_bug757771.html
new file mode 100644
index 000000000..9ef980b66
--- /dev/null
+++ b/editor/libeditor/tests/test_bug757771.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=757771
+-->
+<title>Test for Bug 757771</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=757771">Mozilla Bug 757771</a>
+<input value=foo maxlength=4>
+<input type=password value=password>
+<script>
+/** Test for Bug 757771 **/
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var textInput = document.querySelector("input");
+ textInput.focus();
+ textInput.select();
+ sendString("abcde");
+
+ var passwordInput = document.querySelector("input + input");
+ passwordInput.focus();
+ passwordInput.select();
+ sendString("hunter2");
+
+ ok(true, "No real tests, just crashes/asserts");
+
+ SimpleTest.finish();
+});
+</script>
diff --git a/editor/libeditor/tests/test_bug767684.html b/editor/libeditor/tests/test_bug767684.html
new file mode 100644
index 000000000..0e65a88a7
--- /dev/null
+++ b/editor/libeditor/tests/test_bug767684.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=767684
+-->
+<title>Test for Bug 767684</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=767684">Mozilla Bug 767684</a>
+<div contenteditable>foo<b>bar</b>baz</div>
+<script>
+getSelection().selectAllChildren(document.querySelector("div"));
+document.execCommand("increaseFontSize");
+is(document.querySelector("div").innerHTML, "<big>foo<b>bar</b>baz</big>",
+ "All selected text must be embiggened");
+</script>
diff --git a/editor/libeditor/tests/test_bug772796.html b/editor/libeditor/tests/test_bug772796.html
new file mode 100644
index 000000000..9a15dccd2
--- /dev/null
+++ b/editor/libeditor/tests/test_bug772796.html
@@ -0,0 +1,223 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=772796
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 772796</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style> .pre { white-space: pre } </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=772796">Mozilla Bug 772796</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<div id="editable" contenteditable></div>
+
+<pre id="test">
+
+ <script type="application/javascript">
+ var tests = [
+/*00*/[ "<div>test</div><pre>foobar\nbaz</pre>", "<div>testfoobar\n</div><pre>baz</pre>" ],
+/*01*/[ "<div>test</div><pre><b>foobar\nbaz</b></pre>", "<div>test<b>foobar\n</b></div><pre><b>baz</b></pre>" ],
+/*02*/[ "<div>test</div><pre><b>foo</b>bar\nbaz</pre>", "<div>test<b>foo</b>bar\n</div><pre>baz</pre>" ],
+/*03*/[ "<div>test</div><pre><b>foo</b>\nbar</pre>", "<div>test<b>foo</b>\n</div><pre>bar</pre>" ],
+/*04*/[ "<div>test</div><pre><b>foo\n</b>bar\nbaz</pre>", "<div>test<b>foo\n</b></div><pre>bar\nbaz</pre>" ],
+ /* The <br> after the foobar is unfortunate but is behaviour that hasn't changed in bug 772796. */
+/*05*/[ "<div>test</div><pre>foobar<br>baz</pre>", "<div>testfoobar<br></div><pre>baz</pre>" ],
+/*06*/[ "<div>test</div><pre><b>foobar<br>baz</b></pre>", "<div>test<b>foobar</b><br></div><pre><b>baz</b></pre>" ],
+
+ /*
+ * Some tests with block elements.
+ * Tests 07, 09 and 11 don't use "MoveBlock", they use "JoinNodesSmart".
+ * Test 11 is a pain: <div>foo\bar</div> is be joined to "test", losing the visible line break.
+ */
+/*07*/[ "<div>test</div><pre><div>foobar</div>baz</pre>", "<div>testfoobar</div><pre>baz</pre>" ],
+/*08*/[ "<div>test</div><pre>foobar<div>baz</div></pre>", "<div>testfoobar</div><pre><div>baz</div></pre>" ],
+/*09*/[ "<div>test</div><pre><div>foobar</div>baz\nfred</pre>", "<div>testfoobar</div><pre>baz\nfred</pre>" ],
+/*10*/[ "<div>test</div><pre>foobar<div>baz</div>\nfred</pre>", "<div>testfoobar</div><pre><div>baz</div>\nfred</pre>" ],
+/*11*/[ "<div>test</div><pre><div>foo\nbar</div>baz\nfred</pre>", "<div>testfoo\nbar</div><pre>baz\nfred</pre>" ], // BAD
+/*12*/[ "<div>test</div><pre>foo<div>bar</div>baz\nfred</pre>", "<div>testfoo</div><pre><div>bar</div>baz\nfred</pre>" ],
+
+ /*
+ * Repeating all tests above with the <pre> on a new line.
+ * We know that backspace doesn't work (bug 1190161). Third argument shows the current outcome.
+ */
+/*13-00*/[ "<div>test</div>\n<pre>foobar\nbaz</pre>", "<div>testfoobar\n</div><pre>baz</pre>",
+ "<div>test</div>foobar\n<pre>baz</pre>" ],
+/*14-01*/[ "<div>test</div>\n<pre><b>foobar\nbaz</b></pre>", "<div>test<b>foobar\n</b></div><pre><b>baz</b></pre>",
+ "<div>test</div><b>foobar\n</b><pre><b>baz</b></pre>" ],
+/*15-02*/[ "<div>test</div>\n<pre><b>foo</b>bar\nbaz</pre>", "<div>test<b>foo</b>bar\n</div><pre>baz</pre>",
+ "<div>test</div><b>foo</b>bar\n<pre>baz</pre>" ],
+/*16-03*/[ "<div>test</div>\n<pre><b>foo</b>\nbar</pre>", "<div>test<b>foo</b>\n</div><pre>bar</pre>",
+ "<div>test</div><b>foo</b>\n<pre>bar</pre>" ],
+/*17-04*/[ "<div>test</div>\n<pre><b>foo\n</b>bar\nbaz</pre>", "<div>test<b>foo\n</b></div><pre>bar\nbaz</pre>",
+ "<div>test</div><b>foo\n</b><pre>bar\nbaz</pre>" ],
+/*18-05*/[ "<div>test</div>\n<pre>foobar<br>baz</pre>", "<div>testfoobar<br></div><pre>baz</pre>",
+ "<div>test</div>foobar<br><pre>baz</pre>" ],
+/*19-06*/[ "<div>test</div>\n<pre><b>foobar<br>baz</b></pre>", "<div>test<b>foobar</b><br></div><pre><b>baz</b></pre>",
+ "<div>test</div><b>foobar</b><br><pre><b>baz</b></pre>" ],
+/*20-07*/[ "<div>test</div>\n<pre><div>foobar</div>baz</pre>", "<div>testfoobar</div><pre>baz</pre>",
+ "<div>test</div>foobar<pre>baz</pre>" ],
+/*21-08*/[ "<div>test</div>\n<pre>foobar<div>baz</div></pre>", "<div>testfoobar</div><pre><div>baz</div></pre>",
+ "<div>test</div>foobar<pre><div>baz</div></pre>" ],
+/*22-09*/[ "<div>test</div>\n<pre><div>foobar</div>baz\nfred</pre>", "<div>testfoobar</div><pre>baz\nfred</pre>",
+ "<div>test</div>foobar<pre>baz\nfred</pre>" ],
+/*23-10*/[ "<div>test</div>\n<pre>foobar<div>baz</div>\nfred</pre>", "<div>testfoobar</div><pre><div>baz</div>\nfred</pre>",
+ "<div>test</div>foobar<pre><div>baz</div>\nfred</pre>" ],
+/*24-11*/[ "<div>test</div>\n<pre><div>foo\nbar</div>baz\nfred</pre>", "<div>testfoo\nbar</div><pre>baz\nfred</pre>", // BAD
+ "<div>test</div>foo\n<pre><div>bar</div>baz\nfred</pre>" ],
+/*25-12*/[ "<div>test</div>\n<pre>foo<div>bar</div>baz\nfred</pre>", "<div>testfoo</div><pre><div>bar</div>baz\nfred</pre>",
+ "<div>test</div>foo<pre><div>bar</div>baz\nfred</pre>" ],
+
+ /* Some tests without <div>. These exercise the MoveBlock "right in left" */
+/*26-00*/[ "test<pre>foobar\nbaz</pre>", "testfoobar\n<pre>baz</pre>" ],
+/*27-01*/[ "test<pre><b>foobar\nbaz</b></pre>", "test<b>foobar\n</b><pre><b>baz</b></pre>" ],
+/*28-02*/[ "test<pre><b>foo</b>bar\nbaz</pre>", "test<b>foo</b>bar\n<pre>baz</pre>" ],
+/*29-03*/[ "test<pre><b>foo</b>\nbar</pre>", "test<b>foo</b>\n<pre>bar</pre>" ],
+/*30-04*/[ "test<pre><b>foo\n</b>bar\nbaz</pre>", "test<b>foo\n</b><pre>bar\nbaz</pre>" ],
+/*31-05*/[ "test<pre>foobar<br>baz</pre>", "testfoobar<br><pre>baz</pre>" ],
+/*32-06*/[ "test<pre><b>foobar<br>baz</b></pre>", "test<b>foobar</b><br><pre><b>baz</b></pre>" ],
+/*33-07*/[ "test<pre><div>foobar</div>baz</pre>", "testfoobar<pre>baz</pre>" ],
+/*34-08*/[ "test<pre>foobar<div>baz</div></pre>", "testfoobar<pre><div>baz</div></pre>" ],
+/*35-09*/[ "test<pre><div>foobar</div>baz\nfred</pre>", "testfoobar<pre>baz\nfred</pre>" ],
+/*36-10*/[ "test<pre>foobar<div>baz</div>\nfred</pre>", "testfoobar<pre><div>baz</div>\nfred</pre>" ],
+/*37-11*/[ "test<pre><div>foo\nbar</div>baz\nfred</pre>", "testfoo\n<pre><div>bar</div>baz\nfred</pre>" ],
+/*38-12*/[ "test<pre>foo<div>bar</div>baz\nfred</pre>", "testfoo<pre><div>bar</div>baz\nfred</pre>" ],
+
+ /*
+ * Some tests with <span class="pre">. Again 07, 09 and 11 use "JoinNodesSmart".
+ * All these exercise MoveBlock "left in right". The "right" is the surrounding "contenteditable" div.
+ */
+/*39-00*/[ "<div>test</div><span class=\"pre\">foobar\nbaz</span>", "<div>test<span class=\"pre\">foobar\n</span></div><span class=\"pre\">baz</span>" ],
+/*40-01*/[ "<div>test</div><span class=\"pre\"><b>foobar\nbaz</b></span>", "<div>test<span class=\"pre\"><b>foobar\n</b></span></div><span class=\"pre\"><b>baz</b></span>" ],
+/*41-02*/[ "<div>test</div><span class=\"pre\"><b>foo</b>bar\nbaz</span>", "<div>test<span class=\"pre\"><b>foo</b>bar\n</span></div><span class=\"pre\">baz</span>" ],
+/*42-03*/[ "<div>test</div><span class=\"pre\"><b>foo</b>\nbar</span>", "<div>test<span class=\"pre\"><b>foo</b>\n</span></div><span class=\"pre\">bar</span>" ],
+/*43-04*/[ "<div>test</div><span class=\"pre\"><b>foo\n</b>bar\nbaz</span>", "<div>test<span class=\"pre\"><b>foo\n</b></span></div><span class=\"pre\">bar\nbaz</span>" ],
+/*44-05*/[ "<div>test</div><span class=\"pre\">foobar<br>baz</span>", "<div>test<span class=\"pre\">foobar</span><br><span class=\"pre\"></span></div><span class=\"pre\">baz</span>" ],
+/*45-06*/[ "<div>test</div><span class=\"pre\"><b>foobar<br>baz</b></span>", "<div>test<span class=\"pre\"><b>foobar</b></span><br><span class=\"pre\"></span></div><span class=\"pre\"><b>baz</b></span>" ],
+/*46-07*/[ "<div>test</div><span class=\"pre\"><div>foobar</div>baz</span>", "<div>testfoobar</div><span class=\"pre\">baz</span>" ],
+/*47-08*/[ "<div>test</div><span class=\"pre\">foobar<div>baz</div></span>", "<div>test<span class=\"pre\">foobar</span></div><span class=\"pre\"><div>baz</div></span>" ],
+/*48-09*/[ "<div>test</div><span class=\"pre\"><div>foobar</div>baz\nfred</span>", "<div>testfoobar</div><span class=\"pre\">baz\nfred</span>" ],
+/*49-10*/[ "<div>test</div><span class=\"pre\">foobar<div>baz</div>\nfred</span>", "<div>test<span class=\"pre\">foobar</span></div><span class=\"pre\"><div>baz</div>\nfred</span>" ],
+/*50-11*/[ "<div>test</div><span class=\"pre\"><div>foo\nbar</div>baz\nfred</span>", "<div>testfoo\nbar</div><span class=\"pre\">baz\nfred</span>" ], // BAD
+/*51-12*/[ "<div>test</div><span class=\"pre\">foo<div>bar</div>baz\nfred</span>", "<div>test<span class=\"pre\">foo</span></div><span class=\"pre\"><div>bar</div>baz\nfred</span>" ],
+
+ /* Some tests with <div class="pre">. */
+ /*
+ * The results are pretty ugly, since joining two <divs> sadly carrys the properties of the right to the left,
+ * but not in all cases: 07, 09, 11 are actually right. All cases use "JoinNodesSmart".
+ * Here we merely document the ugly behaviour. See bug 1191875 for more information.
+ */
+/*52-00*/[ "<div>test</div><div class=\"pre\">foobar\nbaz</div>", "<div class=\"pre\">testfoobar\nbaz</div>" ],
+/*53-01*/[ "<div>test</div><div class=\"pre\"><b>foobar\nbaz</b></div>", "<div class=\"pre\">test<b>foobar\nbaz</b></div>" ],
+/*54-02*/[ "<div>test</div><div class=\"pre\"><b>foo</b>bar\nbaz</div>", "<div class=\"pre\">test<b>foo</b>bar\nbaz</div>" ],
+/*55-03*/[ "<div>test</div><div class=\"pre\"><b>foo</b>\nbar</div>", "<div class=\"pre\">test<b>foo</b>\nbar</div>" ],
+/*56-04*/[ "<div>test</div><div class=\"pre\"><b>foo\n</b>bar\nbaz</div>", "<div class=\"pre\">test<b>foo\n</b>bar\nbaz</div>" ],
+/*57-05*/[ "<div>test</div><div class=\"pre\">foobar<br>baz</div>", "<div class=\"pre\">testfoobar<br>baz</div>" ],
+/*58-06*/[ "<div>test</div><div class=\"pre\"><b>foobar<br>baz</b></div>", "<div class=\"pre\">test<b>foobar<br>baz</b></div>" ],
+/*59-07*/[ "<div>test</div><div class=\"pre\"><div>foobar</div>baz</div>", "<div>testfoobar</div><div class=\"pre\">baz</div>" ],
+/*60-08*/[ "<div>test</div><div class=\"pre\">foobar<div>baz</div></div>", "<div class=\"pre\">testfoobar<div>baz</div></div>" ],
+/*61-09*/[ "<div>test</div><div class=\"pre\"><div>foobar</div>baz\nfred</div>", "<div>testfoobar</div><div class=\"pre\">baz\nfred</div>" ],
+/*62-10*/[ "<div>test</div><div class=\"pre\">foobar<div>baz</div>\nfred</div>", "<div class=\"pre\">testfoobar<div>baz</div>\nfred</div>" ],
+/*63-11*/[ "<div>test</div><div class=\"pre\"><div>foo\nbar</div>baz\nfred</div>", "<div>testfoo\nbar</div><div class=\"pre\">baz\nfred</div>" ], // BAD
+/*64-12*/[ "<div>test</div><div class=\"pre\">foo<div>bar</div>baz\nfred</div>", "<div class=\"pre\">testfoo<div>bar</div>baz\nfred</div>" ],
+
+ /* Some tests with lists. These exercise the MoveBlock "left in right". */
+/*65*/[ "<ul><pre><li>test</li>foobar\nbaz</pre></ul>", "<ul><pre><li>testfoobar\n</li>baz</pre></ul>" ],
+/*66*/[ "<ul><pre><li>test</li><b>foobar\nbaz</b></pre></ul>", "<ul><pre><li>test<b>foobar\n</b></li><b>baz</b></pre></ul>" ],
+/*67*/[ "<ul><pre><li>test</li><b>foo</b>bar\nbaz</pre></ul>", "<ul><pre><li>test<b>foo</b>bar\n</li>baz</pre></ul>" ],
+/*68*/[ "<ul><pre><li>test</li><b>foo</b>\nbar</pre></ul>", "<ul><pre><li>test<b>foo</b>\n</li>bar</pre></ul>" ],
+/*69*/[ "<ul><pre><li>test</li><b>foo\n</b>bar\nbaz</pre></ul>", "<ul><pre><li>test<b>foo\n</b></li>bar\nbaz</pre></ul>" ],
+
+ /* Last not least, some simple edge case tests. */
+/*70*/[ "<div>test</div><pre>foobar\n</pre>baz", "<div>testfoobar\n</div>baz" ],
+/*71*/[ "<div>test</div><pre>\nfoo\nbar</pre>", "<div>testfoo\n</div><pre>bar</pre>" ],
+/*72*/[ "<div>test</div><pre>\n\nfoo\nbar</pre>", "<div>test</div><pre>foo\nbar</pre>", "<div>test\n</div><pre>foo\nbar</pre>" ],
+ ];
+
+ /** Test for Bug 772796 **/
+
+ SimpleTest.waitForExplicitFinish();
+
+ SimpleTest.waitForFocus(function() {
+
+ var sel = window.getSelection();
+ var theEdit = document.getElementById("editable");
+ var testName;
+ var theDiv;
+
+ for (i = 0; i < tests.length; i++) {
+ testName = "test" + i.toString();
+ dump (testName+"\n");
+ dump (tests[i][0]+"\n");
+
+ /* Set up the selection. */
+ theEdit.innerHTML = "<div id=\"" + testName + "\">" + tests[i][0] + "</div>";
+ theDiv = document.getElementById(testName);
+ theDiv.focus();
+ sel.collapse(theDiv, 0);
+ synthesizeMouse(theDiv, 100, 2, {}); /* click behind and down */
+
+ /** First round: Forward delete. **/
+ synthesizeKey("VK_DELETE", {});
+ is(theDiv.innerHTML, tests[i][1], "delete(collapsed): inner HTML for " + testName);
+
+ /* Set up the selection. */
+ theEdit.innerHTML = "<div id=\"" + testName + "\">" + tests[i][0] + "</div>";
+ theDiv = document.getElementById(testName);
+ theDiv.focus();
+ sel.collapse(theDiv, 0);
+ synthesizeMouse(theDiv, 100, 2, {}); /* click behind and down */
+
+ /** Second round: Backspace. **/
+ synthesizeKey("VK_RIGHT", {});
+ synthesizeKey("VK_BACK_SPACE", {});
+ if (tests[i].length == 2) {
+ is(theDiv.innerHTML, tests[i][1], "backspace: inner HTML for " + testName);
+ } else {
+ todo_is(theDiv.innerHTML, tests[i][1], "backspace(should be): inner HTML for " + testName);
+ is(theDiv.innerHTML, tests[i][2], "backspace(currently is): inner HTML for " + testName);
+ }
+
+ /* Set up the selection. */
+ theEdit.innerHTML = "<div id=\"" + testName + "\">" + tests[i][0] + "</div>";
+ theDiv = document.getElementById(testName);
+ theDiv.focus();
+ sel.collapse(theDiv, 0);
+ synthesizeMouse(theDiv, 100, 2, {}); /* click behind and down */
+
+ /** Third round: Delete with non-collapsed selection. **/
+ if (i == 72) {
+ // This test doesn't work, since we can't select only one newline using the right arrow key.
+ continue;
+ }
+ synthesizeKey("VK_LEFT", {});
+ /* Strangely enough we need to hit "right arrow" three times to select two characters. */
+ synthesizeKey("VK_RIGHT", {shiftKey:true});
+ synthesizeKey("VK_RIGHT", {shiftKey:true});
+ synthesizeKey("VK_RIGHT", {shiftKey:true});
+ synthesizeKey("VK_DELETE", {});
+
+ /* We always expect to the delete the "tf" in "testfoo". */
+ var expected = tests[i][1].replace("testfoo", "tesoo")
+ .replace("test<b>foo", "tes<b>oo")
+ .replace("test<span class=\"pre\">foo", "tes<span class=\"pre\">oo")
+ .replace("test<span class=\"pre\"><b>foo", "tes<span class=\"pre\"><b>oo");
+ is(theDiv.innerHTML, expected, "delete(non-collapsed): inner HTML for " + testName);
+ }
+
+ SimpleTest.finish();
+
+ });
+
+ </script>
+
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug773262.html b/editor/libeditor/tests/test_bug773262.html
new file mode 100644
index 000000000..b0dc82755
--- /dev/null
+++ b/editor/libeditor/tests/test_bug773262.html
@@ -0,0 +1,63 @@
+<!doctype html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=773262
+-->
+<title>Test for Bug 773262</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<p><a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=773262">Mozilla Bug 773262</a></p>
+<iframe></iframe>
+<script>
+function runTest(doc, desc) {
+ is(doc.queryCommandEnabled("undo"), false,
+ desc + ": Undo shouldn't be enabled yet");
+ is(doc.queryCommandEnabled("redo"), false,
+ desc + ": Redo shouldn't be enabled yet");
+ is(doc.body.innerHTML, "<p>Hello</p>", desc + ": Wrong initial innerHTML");
+
+ doc.getSelection().selectAllChildren(doc.body.firstChild);
+ doc.execCommand("bold");
+ is(doc.queryCommandEnabled("undo"), true,
+ desc + ": Undo should be enabled after bold");
+ is(doc.queryCommandEnabled("redo"), false,
+ desc + ": Redo still shouldn't be enabled");
+ is(doc.body.innerHTML, "<p><b>Hello</b></p>",
+ desc + ": Wrong innerHTML after bold");
+
+ doc.execCommand("undo");
+ is(doc.queryCommandEnabled("undo"), false,
+ desc + ": Undo should be disabled again");
+ is(doc.queryCommandEnabled("redo"), true,
+ desc + ": Redo should be enabled now");
+ is(doc.body.innerHTML, "<p>Hello</p>",
+ desc + ": Wrong innerHTML after undo");
+
+ doc.execCommand("redo");
+ is(doc.queryCommandEnabled("undo"), true,
+ desc + ": Undo should be enabled after redo");
+ is(doc.queryCommandEnabled("redo"), false,
+ desc + ": Redo should be disabled again");
+ is(doc.body.innerHTML, "<p><b>Hello</b></p>",
+ desc + ": Wrong innerHTML after redo");
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var doc = document.querySelector("iframe").contentDocument;
+
+ // First turn on designMode and run the test like that, as a sanity check.
+ doc.body.innerHTML = "<p>Hello</p>";
+ doc.designMode = "on";
+ runTest(doc, "1");
+
+ // Now to test the actual bug: repeat all the above, but with designMode
+ // toggled. This should clear the undo history, so everything should be
+ // exactly as before.
+ doc.designMode = "off";
+ doc.body.innerHTML = "<p>Hello</p>";
+ doc.designMode = "on";
+ runTest(doc, "2");
+
+ SimpleTest.finish();
+});
+</script>
diff --git a/editor/libeditor/tests/test_bug780035.html b/editor/libeditor/tests/test_bug780035.html
new file mode 100644
index 000000000..7c99b9ff5
--- /dev/null
+++ b/editor/libeditor/tests/test_bug780035.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=780035
+-->
+<title>Test for Bug 780035</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=780035">Mozilla Bug 780035</a>
+<div contenteditable style="font-size: 13.3333px"></div>
+<script>
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ document.querySelector("div").focus();
+ document.execCommand("stylewithcss", false, true);
+ sendKey("RETURN");
+ sendChar("x");
+ is(document.querySelector("div").innerHTML, "x<br>",
+ "No <font> tag should be generated");
+ SimpleTest.finish();
+});
+</script>
diff --git a/editor/libeditor/tests/test_bug780908.xul b/editor/libeditor/tests/test_bug780908.xul
new file mode 100644
index 000000000..312f02787
--- /dev/null
+++ b/editor/libeditor/tests/test_bug780908.xul
@@ -0,0 +1,113 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin"
+ type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=780908
+
+adapted from test_bug607584.xul by Kent James <kent@caspia.com>
+-->
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Mozilla Bug 780908" onload="runTest();">
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=780908"
+ target="_blank">Mozilla Bug 780908</a>
+ <p/>
+ <editor xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="editor"
+ type="content-primary"
+ editortype="html"
+ style="width: 400px; height: 100px; border: thin solid black"/>
+ <p/>
+ <pre id="test">
+ </pre>
+ </body>
+ <script class="testbody" type="application/javascript">
+ <![CDATA[
+
+ SimpleTest.waitForExplicitFinish();
+
+ function EditorContentListener(aEditor)
+ {
+ this.init(aEditor);
+ }
+
+ EditorContentListener.prototype = {
+ init : function(aEditor)
+ {
+ this.mEditor = aEditor;
+ },
+
+ QueryInterface : function(aIID)
+ {
+ if (aIID.equals(Components.interfaces.nsIWebProgressListener) ||
+ aIID.equals(Components.interfaces.nsISupportsWeakReference) ||
+ aIID.equals(Components.interfaces.nsISupports))
+ return this;
+ throw Components.results.NS_NOINTERFACE;
+ },
+
+ onStateChange : function(aWebProgress, aRequest, aStateFlags, aStatus)
+ {
+ if (aStateFlags & Components.interfaces.nsIWebProgressListener.STATE_STOP)
+ {
+ var editor = this.mEditor.getEditor(this.mEditor.contentWindow);
+ if (editor) {
+ this.mEditor.focus();
+ editor instanceof Components.interfaces.nsIHTMLEditor;
+ editor.returnInParagraphCreatesNewParagraph = true;
+ source = "<html><body><table><head></table></body></html>";
+ editor.rebuildDocumentFromSource(source);
+ ok(true, "Don't crash when head appears after body");
+ source = "<html></head><head><body></body></html>";
+ editor.rebuildDocumentFromSource(source);
+ ok(true, "Don't crash when /head appears before head");
+ SimpleTest.finish();
+ progress.removeProgressListener(this);
+ }
+ }
+
+ },
+
+
+ onProgressChange : function(aWebProgress, aRequest,
+ aCurSelfProgress, aMaxSelfProgress,
+ aCurTotalProgress, aMaxTotalProgress)
+ {
+ },
+
+ onLocationChange : function(aWebProgress, aRequest, aLocation, aFlags)
+ {
+ },
+
+ onStatusChange : function(aWebProgress, aRequest, aStatus, aMessage)
+ {
+ },
+
+ onSecurityChange : function(aWebProgress, aRequest, aState)
+ {
+ },
+
+ mEditor: null
+ };
+
+ var progress, progressListener;
+
+ function runTest() {
+ var newEditorElement = document.getElementById("editor");
+ newEditorElement.makeEditable("html", true);
+ var docShell = newEditorElement.boxObject.docShell;
+ progress = docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIWebProgress);
+ progressListener = new EditorContentListener(newEditorElement);
+ progress.addProgressListener(progressListener, Components.interfaces.nsIWebProgress.NOTIFY_ALL);
+ newEditorElement.setAttribute("src", "data:text/html,");
+ }
+]]>
+</script>
+</window>
diff --git a/editor/libeditor/tests/test_bug787432.html b/editor/libeditor/tests/test_bug787432.html
new file mode 100644
index 000000000..c73bb3c7e
--- /dev/null
+++ b/editor/libeditor/tests/test_bug787432.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=787432
+-->
+<title>Test for Bug 787432</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=787432">Mozilla Bug 787432</a>
+<div id="test" contenteditable><span class="insert">%</span><br></div>
+<script>
+var div = document.getElementById("test");
+getSelection().collapse(div.firstChild, 0);
+getSelection().extend(div.firstChild, 1);
+document.execCommand("inserttext", false, "x");
+is(div.innerHTML, '<span class="insert">x</span><br>',
+ "Empty <span> needs to not be removed");
+</script>
diff --git a/editor/libeditor/tests/test_bug790475.html b/editor/libeditor/tests/test_bug790475.html
new file mode 100644
index 000000000..d7685458b
--- /dev/null
+++ b/editor/libeditor/tests/test_bug790475.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=790475
+-->
+<head>
+ <title>Test for Bug 790475</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=790475">Mozilla Bug 790475</a>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test">
+<script type="application/javascript">
+
+/**
+ * Test for Bug 790475
+ *
+ * Tests that inline spell checking works properly through adjacent text nodes.
+ */
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+
+var gMisspeltWords;
+
+function getEditor() {
+ const Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIEditingSession);
+ return editingSession.getEditorForWindow(window);
+}
+
+function getSpellCheckSelection() {
+ var editor = getEditor();
+ var selcon = editor.selectionController;
+ return selcon.getSelection(selcon.SELECTION_SPELLCHECK);
+}
+
+function runTest() {
+ gMisspeltWords = [];
+ var edit = document.getElementById("edit");
+ edit.focus();
+
+ SimpleTest.executeSoon(function() {
+ gMisspeltWords = [];
+ is(isSpellingCheckOk(), true, "Should not find any misspellings yet.");
+
+ var newTextNode = document.createTextNode("ing string");
+ edit.appendChild(newTextNode);
+ var editor = getEditor();
+ var sel = editor.selection;
+ sel.collapse(newTextNode, newTextNode.textContent.length);
+ synthesizeKey("!", {});
+
+ edit.blur();
+
+ SimpleTest.executeSoon(function() {
+ is(isSpellingCheckOk(), true, "Should not have found any misspellings. ");
+ SimpleTest.finish();
+ });
+ });
+}
+
+function isSpellingCheckOk() {
+ var sel = getSpellCheckSelection();
+ var numWords = sel.rangeCount;
+
+ is(numWords, gMisspeltWords.length, "Correct number of misspellings and words.");
+
+ if (numWords != gMisspeltWords.length)
+ return false;
+
+ for (var i = 0; i < numWords; i++) {
+ var word = sel.getRangeAt(i);
+ is (word, gMisspeltWords[i], "Misspelling is what we think it is.");
+ if (word != gMisspeltWords[i])
+ return false;
+ }
+ return true;
+}
+
+</script>
+</pre>
+
+<div id="edit" contenteditable="true">This is a test</div>
+
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug795418-2.html b/editor/libeditor/tests/test_bug795418-2.html
new file mode 100644
index 000000000..3f44900ee
--- /dev/null
+++ b/editor/libeditor/tests/test_bug795418-2.html
@@ -0,0 +1,88 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=795418
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test #2 for Bug 772796</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=772796">Mozilla Bug 795418</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<div id="copySource">Copy this</div>
+<iframe src="data:application/xhtml+xml,<html contenteditable='' xmlns='http://www.w3.org/1999/xhtml'><span>AB</span></html>"></iframe>
+
+<pre id="test">
+
+<script type="application/javascript">
+
+/** Test for Bug 795418 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var div = document.getElementById("copySource");
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+
+ // Select the text from the text node in div.
+ var r = document.createRange();
+ r.setStart(div.firstChild, 0);
+ r.setEnd(div.firstChild, 9);
+ sel.addRange(r);
+
+ function checkResult() {
+ var iframe = document.querySelector("iframe");
+ var iframeWindow = iframe.contentWindow;
+ var theEdit = iframe.contentDocument.firstChild;
+ theEdit.offsetHeight;
+ is(theEdit.innerHTML,
+ "<blockquote xmlns=\"http://www.w3.org/1999/xhtml\" type=\"cite\">Copy this</blockquote><span xmlns=\"http://www.w3.org/1999/xhtml\">AB</span>",
+ "unexpected HTML for test");
+ SimpleTest.finish();
+ }
+
+ function pasteQuote() {
+ var iframe = document.querySelector("iframe");
+ var iframeWindow = iframe.contentWindow;
+ var theEdit = iframe.contentDocument.firstChild;
+ theEdit.offsetHeight;
+ iframeWindow.focus();
+ SimpleTest.waitForFocus(function() {
+ var iframeSel = iframeWindow.getSelection();
+ iframeSel.removeAllRanges();
+ let span = iframe.contentDocument.querySelector('span');
+ iframeSel.collapse(span, 1);
+
+ SpecialPowers.doCommand(iframeWindow, "cmd_pasteQuote");
+ setTimeout(checkResult, 0);
+ }, iframeWindow);
+ }
+
+ SimpleTest.waitForClipboard(
+ function compare(value) {
+ return true;
+ },
+ function setup() {
+ synthesizeKey("C", {accelKey: true});
+ },
+ function onSuccess() {
+ setTimeout(pasteQuote, 0);
+ },
+ function onFailure() {
+ SimpleTest.finish();
+ },
+ "text/html"
+ );
+});
+
+</script>
+
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug795418-3.html b/editor/libeditor/tests/test_bug795418-3.html
new file mode 100644
index 000000000..bbe1a58b3
--- /dev/null
+++ b/editor/libeditor/tests/test_bug795418-3.html
@@ -0,0 +1,88 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=795418
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test #3 for Bug 772796</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=772796">Mozilla Bug 795418</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<div id="copySource">Copy this</div>
+<iframe src="data:text/html,<html><body><span>AB</span>"></iframe>
+
+<pre id="test">
+
+<script type="application/javascript">
+
+/** Test for Bug 795418 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var div = document.getElementById("copySource");
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+
+ // Select the text from the text node in div.
+ var r = document.createRange();
+ r.setStart(div.firstChild, 0);
+ r.setEnd(div.firstChild, 9);
+ sel.addRange(r);
+
+ function checkResult() {
+ var iframe = document.querySelector("iframe");
+ var iframeWindow = iframe.contentWindow;
+ var theEdit = iframe.contentDocument.body;
+ theEdit.offsetHeight;
+ is(theEdit.innerHTML,
+ "<span>AB<blockquote type=\"cite\">Copy this</blockquote></span>",
+ "unexpected HTML for test");
+ SimpleTest.finish();
+ }
+
+ function pasteQuote() {
+ var iframe = document.querySelector("iframe");
+ var iframeWindow = iframe.contentWindow;
+ var theEdit = iframe.contentDocument.body;
+ iframe.contentDocument.designMode='on';
+ iframe.contentDocument.body.offsetHeight;
+ iframeWindow.focus();
+ SimpleTest.waitForFocus(function() {
+ var iframeSel = iframeWindow.getSelection();
+ iframeSel.removeAllRanges();
+ iframeSel.collapse(theEdit.firstChild, 1);
+
+ SpecialPowers.doCommand(iframeWindow, "cmd_pasteQuote");
+ setTimeout(checkResult, 0);
+ }, iframeWindow);
+ }
+
+ SimpleTest.waitForClipboard(
+ function compare(value) {
+ return true;
+ },
+ function setup() {
+ synthesizeKey("C", {accelKey: true});
+ },
+ function onSuccess() {
+ setTimeout(pasteQuote, 0);
+ },
+ function onFailure() {
+ SimpleTest.finish();
+ },
+ "text/html"
+ );
+});
+
+</script>
+
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug795418-4.html b/editor/libeditor/tests/test_bug795418-4.html
new file mode 100644
index 000000000..6c1ae05d1
--- /dev/null
+++ b/editor/libeditor/tests/test_bug795418-4.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=795418
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test #4 for Bug 795418</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=795418">Mozilla Bug 795418</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<div id="copySource">Copy this</div>
+<div id="editable" contenteditable style="display:grid">AB</div>
+
+<pre id="test">
+
+<script type="application/javascript">
+
+/** Test for Bug 795418 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var div = document.getElementById("copySource");
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+
+ // Select the text from the text node in div.
+ var r = document.createRange();
+ r.setStart(div.firstChild, 0);
+ r.setEnd(div.firstChild, 9);
+ sel.addRange(r);
+
+ SimpleTest.waitForClipboard(
+ function compare(value) {
+ var theEdit = document.getElementById("editable");
+ sel.collapse(theEdit.firstChild, 2);
+
+ SpecialPowers.doCommand(window, "cmd_paste");
+ is(theEdit.innerHTML,
+ "ABCopy this",
+ "unexpected HTML for test");
+ return true;
+ },
+ function setup() {
+ synthesizeKey("C", {accelKey: true});
+ },
+ function onSuccess() {
+ SimpleTest.finish();
+ },
+ function onFailure() {
+ SimpleTest.finish();
+ },
+ "text/html"
+ );
+});
+
+</script>
+
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug795418-5.html b/editor/libeditor/tests/test_bug795418-5.html
new file mode 100644
index 000000000..5ff90b15a
--- /dev/null
+++ b/editor/libeditor/tests/test_bug795418-5.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=795418
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test #5 for Bug 795418</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=795418">Mozilla Bug 795418</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<div id="copySource">Copy this</div>
+<div id="editable" contenteditable style="display:ruby">AB</div>
+
+<pre id="test">
+
+<script type="application/javascript">
+
+/** Test for Bug 795418 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var div = document.getElementById("copySource");
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+
+ // Select the text from the text node in div.
+ var r = document.createRange();
+ r.setStart(div.firstChild, 0);
+ r.setEnd(div.firstChild, 9);
+ sel.addRange(r);
+
+ SimpleTest.waitForClipboard(
+ function compare(value) {
+ var theEdit = document.getElementById("editable");
+ sel.collapse(theEdit.firstChild, 2);
+
+ SpecialPowers.doCommand(window, "cmd_paste");
+ is(theEdit.innerHTML,
+ "ABCopy this",
+ "unexpected HTML for test");
+ return true;
+ },
+ function setup() {
+ synthesizeKey("C", {accelKey: true});
+ },
+ function onSuccess() {
+ SimpleTest.finish();
+ },
+ function onFailure() {
+ SimpleTest.finish();
+ },
+ "text/html"
+ );
+});
+
+</script>
+
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug795418-6.html b/editor/libeditor/tests/test_bug795418-6.html
new file mode 100644
index 000000000..798a6534b
--- /dev/null
+++ b/editor/libeditor/tests/test_bug795418-6.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=795418
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test #5 for Bug 795418</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=795418">Mozilla Bug 795418</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<div id="copySource">Copy this</div>
+<div id="editable" contenteditable style="display:table">AB</div>
+
+<pre id="test">
+
+<script type="application/javascript">
+
+/** Test for Bug 795418 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var div = document.getElementById("copySource");
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+
+ // Select the text from the text node in div.
+ var r = document.createRange();
+ r.setStart(div.firstChild, 0);
+ r.setEnd(div.firstChild, 9);
+ sel.addRange(r);
+
+ SimpleTest.waitForClipboard(
+ function compare(value) {
+ var theEdit = document.getElementById("editable");
+ sel.collapse(theEdit.firstChild, 2);
+
+ SpecialPowers.doCommand(window, "cmd_pasteQuote");
+ is(theEdit.innerHTML,
+ "AB<blockquote type=\"cite\">Copy this</blockquote>",
+ "unexpected HTML for test");
+ return true;
+ },
+ function setup() {
+ synthesizeKey("C", {accelKey: true});
+ },
+ function onSuccess() {
+ SimpleTest.finish();
+ },
+ function onFailure() {
+ SimpleTest.finish();
+ },
+ "text/html"
+ );
+});
+
+</script>
+
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug795418.html b/editor/libeditor/tests/test_bug795418.html
new file mode 100644
index 000000000..1db8cf026
--- /dev/null
+++ b/editor/libeditor/tests/test_bug795418.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=795418
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 795418</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=795418">Mozilla Bug 795418</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<div id="copySource">Copy this</div>
+<div id="editable" contenteditable><span>AB</span></div>
+
+<pre id="test">
+
+<script type="application/javascript">
+
+/** Test for Bug 795418 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var div = document.getElementById("copySource");
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+
+ // Select the text from the text node in div.
+ var r = document.createRange();
+ r.setStart(div.firstChild, 0);
+ r.setEnd(div.firstChild, 9);
+ sel.addRange(r);
+
+ SimpleTest.waitForClipboard(
+ function compare(value) {
+ var theEdit = document.getElementById("editable");
+ sel.collapse(theEdit.firstChild, 1);
+
+ SpecialPowers.doCommand(window, "cmd_pasteQuote");
+ is(theEdit.innerHTML,
+ "<span>AB<blockquote type=\"cite\">Copy this</blockquote></span>",
+ "unexpected HTML for test");
+ return true;
+ },
+ function setup() {
+ synthesizeKey("C", {accelKey: true});
+ },
+ function onSuccess() {
+ SimpleTest.finish();
+ },
+ function onFailure() {
+ SimpleTest.finish();
+ },
+ "text/html"
+ );
+});
+
+</script>
+
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug795785.html b/editor/libeditor/tests/test_bug795785.html
new file mode 100644
index 000000000..5f93d5142
--- /dev/null
+++ b/editor/libeditor/tests/test_bug795785.html
@@ -0,0 +1,168 @@
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=795785
+-->
+<head>
+ <title>Test for Bug 795785</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=795785">Mozilla Bug 795785</a>
+<div id="display">
+ <textarea id="textarea" style="overflow: hidden; height: 3em; width: 5em; word-wrap: normal;"></textarea>
+ <div id="div" contenteditable style="overflow: hidden; height: 3em; width: 5em;"></div>
+</div>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("This test uses setTimeouts in order to fix an intermittent failure.");
+
+// Turn off spatial navigation because it hijacks arrow key events and VK_RETURN
+// events.
+SimpleTest.waitForFocus(function() {
+ SpecialPowers.pushPrefEnv({"set":[["snav.enabled", false]]}, runTests);
+});
+var textarea = document.getElementById("textarea");
+var div = document.getElementById("div");
+
+function hitEventLoop(aFunc, aTimes)
+{
+ if (--aTimes) {
+ setTimeout(hitEventLoop, 0, aFunc, aTimes);
+ } else {
+ setTimeout(aFunc, 100);
+ }
+}
+
+function doKeyEventTest(aElement, aElementDescription, aCallback)
+{
+ aElement.focus();
+ aElement.scrollTop = 0;
+ hitEventLoop(function () {
+ is(aElement.scrollTop, 0,
+ aElementDescription + "'s scrollTop isn't 0");
+ synthesizeKey("VK_RETURN", { });
+ synthesizeKey("VK_RETURN", { });
+ synthesizeKey("VK_RETURN", { });
+ synthesizeKey("VK_RETURN", { });
+ synthesizeKey("VK_RETURN", { });
+ synthesizeKey("VK_RETURN", { });
+ hitEventLoop(function () {
+ isnot(aElement.scrollTop, 0,
+ aElementDescription + " was not scrolled by inserting line breaks");
+ var scrollTop = aElement.scrollTop;
+ synthesizeKey("VK_UP", { });
+ synthesizeKey("VK_UP", { });
+ synthesizeKey("VK_UP", { });
+ synthesizeKey("VK_UP", { });
+ synthesizeKey("VK_UP", { });
+ hitEventLoop(function () {
+ isnot(aElement.scrollTop, scrollTop,
+ aElementDescription + " was not scrolled by up key events");
+ synthesizeKey("VK_DOWN", { });
+ synthesizeKey("VK_DOWN", { });
+ synthesizeKey("VK_DOWN", { });
+ synthesizeKey("VK_DOWN", { });
+ synthesizeKey("VK_DOWN", { });
+ hitEventLoop(function () {
+ is(aElement.scrollTop, scrollTop,
+ aElementDescription + " was not scrolled by down key events");
+ var longWord = "aaaaaaaaaaaaaaaaaaaa";
+ sendString(longWord);
+ hitEventLoop(function () {
+ isnot(aElement.scrollLeft, 0,
+ aElementDescription + " was not scrolled by typing long word");
+ var scrollLeft = aElement.scrollLeft;
+ var i;
+ for (i = 0; i < longWord.length; i++) {
+ synthesizeKey("VK_LEFT", { });
+ }
+ hitEventLoop(function () {
+ isnot(aElement.scrollLeft, scrollLeft,
+ aElementDescription + " was not scrolled by left key events");
+ for (i = 0; i < longWord.length; i++) {
+ synthesizeKey("VK_RIGHT", { });
+ }
+ hitEventLoop(function () {
+ is(aElement.scrollLeft, scrollLeft,
+ aElementDescription + " was not scrolled by right key events");
+ aCallback();
+ }, 20);
+ }, 20);
+ }, 20);
+ }, 20);
+ }, 20);
+ }, 20);
+ }, 20);
+}
+
+function doCompositionTest(aElement, aElementDescription, aCallback)
+{
+ aElement.focus();
+ aElement.scrollTop = 0;
+ hitEventLoop(function () {
+ is(aElement.scrollTop, 0,
+ aElementDescription + "'s scrollTop isn't 0");
+ var str = "Web \u958b\u767a\u8005\u306e\u7686\u3055\u3093\u306f\u3001" +
+ "Firefox \u306b\u5b9f\u88c5\u3055\u308c\u3066\u3044\u308b HTML5" +
+ " \u3084 CSS \u306e\u65b0\u6a5f\u80fd\u3092\u6d3b\u7528\u3059" +
+ "\u308b\u3053\u3068\u3067\u3001\u9b45\u529b\u3042\u308b Web " +
+ "\u30b5\u30a4\u30c8\u3084\u9769\u65b0\u7684\u306a Web \u30a2" +
+ "\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u3088\u308a" +
+ "\u77ed\u6642\u9593\u3067\u7c21\u5358\u306b\u4f5c\u6210\u3067" +
+ "\u304d\u307e\u3059\u3002";
+ synthesizeCompositionChange({
+ composition: {
+ string: str,
+ clauses: [
+ { length: str.length, attr: COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ caret: { start: str.length, length: 0 }
+ });
+ hitEventLoop(function () {
+ isnot(aElement.scrollTop, 0,
+ aElementDescription + " was not scrolled by composition");
+ synthesizeComposition({ type: "compositioncommit", data: "" });
+ hitEventLoop(function () {
+ is(aElement.scrollTop, 0,
+ aElementDescription + " was not scrolled back to the top by canceling composition");
+ aCallback();
+ }, 20);
+ }, 20);
+ }, 20);
+}
+
+function runTests()
+{
+ doKeyEventTest(textarea, "textarea",
+ function () {
+ textarea.value = "";
+ doKeyEventTest(div, "div (contenteditable)",
+ function () {
+ div.innerHTML = "";
+ doCompositionTest(textarea, "textarea",
+ function () {
+ doCompositionTest(div, "div (contenteditable)",
+ function () {
+ SimpleTest.finish();
+ });
+ });
+ });
+ });
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_bug796839.html b/editor/libeditor/tests/test_bug796839.html
new file mode 100644
index 000000000..be4be316c
--- /dev/null
+++ b/editor/libeditor/tests/test_bug796839.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=796839
+-->
+<title>Test for Bug 796839</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=796839">Mozilla Bug 796839</a>
+<div id="test" contenteditable><br></div>
+<script>
+var div = document.getElementById("test");
+var text = document.createTextNode("");
+div.insertBefore(text, div.firstChild);
+getSelection().collapse(text, 0);
+document.execCommand("inserthtml", false, "x");
+is(div.textContent, 'x', "Empty textnodes should be editable");
+</script>
diff --git a/editor/libeditor/tests/test_bug830600.html b/editor/libeditor/tests/test_bug830600.html
new file mode 100644
index 000000000..39ced297a
--- /dev/null
+++ b/editor/libeditor/tests/test_bug830600.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=830600
+-->
+<head>
+ <title>Test for Bug 830600</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=830600">Mozilla Bug 830600</a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <input type="text" id="t1" />
+ <pre id="test">
+ <script type="application/javascript;version=1.7">
+
+ /** Test for Bug 830600 **/
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(function() {
+ const Ci = SpecialPowers.Ci;
+ function test(str, expected, callback) {
+ var t = document.getElementById("t1");
+ SpecialPowers.wrap(t).QueryInterface(Ci.nsIDOMNSEditableElement);
+ t.focus();
+ t.value = "";
+ var editor = SpecialPowers.wrap(t).editor;
+ editor.QueryInterface(Ci.nsIPlaintextEditor);
+ var origNewlineHandling = editor.newlineHandling;
+ editor.newlineHandling = Ci.nsIPlaintextEditor.eNewlinesStripSurroundingWhitespace
+ SimpleTest.waitForClipboard(str,
+ function() {
+ SpecialPowers.Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper)
+ .copyString(str);
+ },
+ function() {
+ synthesizeKey("V", {accelKey: true});
+ is(t.value, expected, "New line handling works correctly");
+ t.value = "";
+ callback();
+ },
+ function() {
+ ok(false, "Failed to copy the string");
+ SimpleTest.finish();
+ }
+ );
+ }
+
+ function runNextTest() {
+ if (tests.length) {
+ var currentTest = tests.shift();
+ test(currentTest[0], currentTest[1], runNextTest);
+ } else {
+ SimpleTest.finish();
+ }
+ }
+
+ var tests = [
+ ["abc", "abc"],
+ ["\n", ""],
+ [" \n", ""],
+ ["\n ", ""],
+ [" \n ", ""],
+ [" a", " a"],
+ ["a ", "a "],
+ [" a ", " a "],
+ [" \nabc", "abc"],
+ ["\n abc", "abc"],
+ [" \n abc", "abc"],
+ [" \nabc ", "abc "],
+ ["\n abc ", "abc "],
+ [" \n abc ", "abc "],
+ ["abc\n ", "abc"],
+ ["abc \n", "abc"],
+ ["abc \n ", "abc"],
+ [" abc\n ", " abc"],
+ [" abc \n", " abc"],
+ [" abc \n ", " abc"],
+ [" abc \n def \n ", " abcdef"],
+ ["\n abc \n def \n ", "abcdef"],
+ [" \n abc \n def ", "abcdef "],
+ [" abc\n\ndef ", " abcdef "],
+ [" abc \n\n def ", " abcdef "],
+ ];
+
+ runNextTest();
+ });
+
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug832025.html b/editor/libeditor/tests/test_bug832025.html
new file mode 100644
index 000000000..40f4f4734
--- /dev/null
+++ b/editor/libeditor/tests/test_bug832025.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=832025
+-->
+<head>
+ <title>Test for Bug 832025</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=832025">Mozilla Bug 832025</a>
+<div id="test" contenteditable="true">header1</div>
+<script type="application/javascript">
+
+/**
+ * Test for Bug 832025
+ *
+ */
+
+document.execCommand("stylewithcss", false, "true");
+var test = document.getElementById("test");
+test.focus();
+
+// place caret at end of editable area
+var sel = getSelection();
+sel.collapse(test, test.childNodes.length);
+
+// make it a H1
+document.execCommand("heading", false, "H1");
+// simulate a CR key
+sendKey("return");
+// insert some text
+document.execCommand("insertText", false, "abc");
+
+is(test.innerHTML == '<h1>header1</h1><p>abc<br></p>',
+ true, "A paragraph automatically created after a CR at the end of an H1 should not be bold");
+
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug850043.html b/editor/libeditor/tests/test_bug850043.html
new file mode 100644
index 000000000..b811c86a6
--- /dev/null
+++ b/editor/libeditor/tests/test_bug850043.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=850043
+-->
+<head>
+ <title>Test for Bug 850043</title>
+ <script type="application/javascript" src="/MochiKit/packed.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=850043">Mozilla Bug 850043</a>
+<div id="display">
+<textarea id="textarea">b&#x9080;&#xe010f;&#x8fba;&#xe0101;</textarea>
+<div contenteditable id="edit">b&#x9080;&#xe010f;&#x8fba;&#xe0101;</div>
+</div>
+<div id="content" style="display: none">
+</div>
+
+<pre id="test">
+</pre>
+<script>
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ let fm = SpecialPowers.Cc["@mozilla.org/focus-manager;1"].
+ getService(SpecialPowers.Ci.nsIFocusManager);
+
+ let element = document.getElementById("textarea");
+ element.setSelectionRange(element.value.length, element.value.length);
+ element.focus();
+ is(SpecialPowers.unwrap(fm.focusedElement), element, "failed to move focus");
+
+ synthesizeKey("VK_END", { });
+ synthesizeKey("a", { });
+ is(element.value, "b\u{9080}\u{e010f}\u{8fba}\u{e0101}a", "a isn't last character");
+
+ synthesizeKey("VK_BACK_SPACE", { });
+ synthesizeKey("VK_BACK_SPACE", { });
+ synthesizeKey("VK_BACK_SPACE", { });
+ is(element.value, 'b', "cannot remove all IVS characters");
+
+ element = document.getElementById("edit");
+ element.focus();
+ is(SpecialPowers.unwrap(fm.focusedElement), element, "failed to move focus");
+
+ let sel = window.getSelection();
+ sel.collapse(element.childNodes[0], element.textContent.length);
+
+ synthesizeKey("a", { });
+ is(element.textContent, "b\u{9080}\u{e010f}\u{8fba}\u{e0101}a", "a isn't last character");
+
+ synthesizeKey("VK_BACK_SPACE", { });
+ synthesizeKey("VK_BACK_SPACE", { });
+ synthesizeKey("VK_BACK_SPACE", { });
+ is(element.textContent, 'b', "cannot remove all IVS characters");
+
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug857487.html b/editor/libeditor/tests/test_bug857487.html
new file mode 100644
index 000000000..a3746d44c
--- /dev/null
+++ b/editor/libeditor/tests/test_bug857487.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=857487
+-->
+<head>
+ <title>Test for Bug 857487</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=857487">Mozilla Bug 857487</a>
+<div id="edit" contenteditable="true">
+ <table id="table" border="1" width="100%">
+ <tbody>
+ <tr>
+ <td>a</td>
+ <td>b</td>
+ <td>c</td>
+ </tr>
+ <tr>
+ <td>d</td>
+ <td id="cell">e</td>
+ <td>f</td>
+ </tr>
+ <tr>
+ <td>g</td>
+ <td>h</td>
+ <td>i</td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+<script type="application/javascript">
+
+/**
+ * Test for Bug 857487
+ *
+ * Tests that removing a table row through nsIHTMLEditor works
+ */
+
+function getEditor() {
+ const Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIEditingSession);
+ return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsITableEditor);
+}
+
+var cell = document.getElementById("cell");
+cell.focus();
+
+// place caret at end of center cell
+var sel = getSelection();
+sel.collapse(cell, cell.childNodes.length);
+
+var editor = getEditor();
+editor.deleteTableRow(1);
+
+var table = document.getElementById("table");
+
+is(table.innerHTML == "\n <tbody>\n <tr>\n <td>a</td>\n <td>b</td>\n <td>c</td>\n </tr>\n \n <tr>\n <td>g</td>\n <td>h</td>\n <td>i</td>\n </tr>\n </tbody>\n ",
+ true, "editor.deleteTableRow(1) should delete the row containing the selection");
+
+</script>
+
+
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug858918.html b/editor/libeditor/tests/test_bug858918.html
new file mode 100644
index 000000000..46f841bbc
--- /dev/null
+++ b/editor/libeditor/tests/test_bug858918.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=858918
+-->
+<title>Test for Bug 858918</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=858918">Mozilla Bug 858918</a>
+<span contenteditable style="display:block;min-height:1em"></span>
+<script>
+var span = document.querySelector("span");
+getSelection().collapse(span, 0);
+document.execCommand("inserthtml", false, "<div>doesn't go in span</div>");
+is(span.innerHTML, "<div>doesn't go in span</div>");
+</script>
diff --git a/editor/libeditor/tests/test_bug915962.html b/editor/libeditor/tests/test_bug915962.html
new file mode 100644
index 000000000..32968b310
--- /dev/null
+++ b/editor/libeditor/tests/test_bug915962.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=915962
+-->
+<head>
+ <title>Test for Bug 915962</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=915962">Mozilla Bug 915962</a>
+<p id="display"></p>
+<div id="content">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 915962 **/
+
+var smoothScrollPref = "general.smoothScroll";
+SimpleTest.waitForExplicitFinish();
+var win = window.open("file_bug915962.html", "_blank",
+ "width=600,height=600,scrollbars=yes");
+
+// grab the timer right at the start
+var cwu = SpecialPowers.getDOMWindowUtils(win);
+function step() {
+ cwu.advanceTimeAndRefresh(100);
+}
+SimpleTest.waitForFocus(function() {
+ SpecialPowers.pushPrefEnv({"set":[[smoothScrollPref, false]]}, startTest);
+}, win);
+function startTest() {
+ // Make sure that pressing Space when a tabindex=-1 element is focused
+ // will scroll the page.
+ var button = win.document.querySelector("button");
+ var sc = win.document.querySelector("div");
+ sc.focus();
+ is(win.scrollY, 0, "Sanity check");
+ synthesizeKey(" ", {}, win);
+
+ step();
+
+ isnot(win.scrollY, 0, "Page is scrolled down");
+ var oldY = win.scrollY;
+ synthesizeKey(" ", {shiftKey: true}, win);
+
+ step();
+
+ ok(win.scrollY < oldY, "Page is scrolled up");
+
+ // Make sure that pressing Space when a tabindex=-1 element is focused
+ // will not scroll the page, and will activate the element.
+ button.focus();
+ var clicked = false;
+ button.onclick = () => clicked = true;
+ oldY = win.scrollY;
+ synthesizeKey(" ", {}, win);
+
+ step();
+
+ ok(win.scrollY <= oldY, "Page is not scrolled down");
+ ok(clicked, "The button should be clicked");
+ synthesizeKey("VK_TAB", {}, win);
+
+ step();
+
+ oldY = win.scrollY;
+ synthesizeKey(" ", {}, win);
+
+ step()
+
+ ok(win.scrollY >= oldY, "Page is scrolled down");
+
+ win.close();
+ cwu.restoreNormalRefresh();
+
+ win = window.open("file_bug915962.html", "_blank",
+ "width=600,height=600,scrollbars=yes");
+ cwu = SpecialPowers.getDOMWindowUtils(win);
+ SimpleTest.waitForFocus(function() {
+ is(win.scrollY, 0, "Sanity check");
+ synthesizeKey(" ", {}, win);
+
+ step();
+
+ isnot(win.scrollY, 0, "Page is scrolled down without crashing");
+
+ win.close();
+ cwu.restoreNormalRefresh();
+
+ SimpleTest.finish();
+ }, win);
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug966155.html b/editor/libeditor/tests/test_bug966155.html
new file mode 100644
index 000000000..524b15d69
--- /dev/null
+++ b/editor/libeditor/tests/test_bug966155.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=966155
+-->
+<head>
+ <title>Test for Bug 966155</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=966155">Mozilla Bug 966155</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+ var win = window.open("data:text/html,<input><iframe onload=\"contentDocument.designMode = 'on';\">", "", "test-966155");
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad);
+ runTest(win);
+ }, false);
+});
+
+function runTest(win) {
+ SimpleTest.waitForFocus(function() {
+ var doc = win.document;
+ var iframe = doc.querySelector("iframe");
+ var iframeDoc = iframe.contentDocument;
+ var input = doc.querySelector("input");
+ iframe.focus();
+ iframeDoc.body.focus();
+ // Type some text
+ "test".split("").forEach(function(letter) {
+ synthesizeKey(letter, {}, win);
+ });
+ is(iframeDoc.body.textContent, "test", "entered the text");
+ // focus the input box
+ input.focus();
+ // press tab
+ synthesizeKey("VK_TAB", {}, win);
+ // Now press Ctrl+Backspace
+ synthesizeKey("VK_BACK_SPACE", {ctrlKey: true}, win);
+ is(iframeDoc.body.textContent, "", "deleted the text");
+ win.close();
+ SimpleTest.finish();
+ }, win);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug966552.html b/editor/libeditor/tests/test_bug966552.html
new file mode 100644
index 000000000..3d0ec5fe3
--- /dev/null
+++ b/editor/libeditor/tests/test_bug966552.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=966552
+-->
+<head>
+ <title>Test for Bug 966552</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=966552">Mozilla Bug 966552</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+ var win = window.open("data:text/html,<body onload=\"document.designMode='on'\">test</body>", "", "test-966552");
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad);
+ runTest(win);
+ }, false);
+});
+
+function runTest(win) {
+ SimpleTest.waitForFocus(function() {
+ var doc = win.document;
+ var sel = win.getSelection();
+ doc.body.focus();
+ sel.collapse(doc.body.firstChild, 2);
+ synthesizeKey("VK_BACK_SPACE", {ctrlKey: true}, win);
+ is(doc.body.textContent, "st");
+ win.close();
+ SimpleTest.finish();
+ }, win);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug974309.html b/editor/libeditor/tests/test_bug974309.html
new file mode 100644
index 000000000..e3caa87fb
--- /dev/null
+++ b/editor/libeditor/tests/test_bug974309.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=974309
+-->
+<head>
+ <title>Test for Bug 974309</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=974309">Mozilla Bug 974309</a>
+<div id="edit_not_table_parent" contenteditable="true"></div>
+<div>
+ <table id="table" border="1" width="100%">
+ <tbody>
+ <tr>
+ <td>a</td>
+ <td>b</td>
+ <td>c</td>
+ </tr>
+ <tr>
+ <td>d</td>
+ <td id="cell">e</td>
+ <td>f</td>
+ </tr>
+ <tr>
+ <td>g</td>
+ <td>h</td>
+ <td>i</td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+<script type="application/javascript">
+
+/**
+ * Test for Bug 974309
+ *
+ * Tests that editing a table row fails when the table or row is _not_ a child of a contenteditable node.
+ * See bug 857487 for tests that cover when the table or row _is_ a child of a contenteditable node.
+ */
+
+function getEditor() {
+ const Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIEditingSession);
+ return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsITableEditor);
+}
+
+var cell = document.getElementById("cell");
+cell.focus();
+
+// place caret at end of center cell
+var sel = getSelection();
+sel.collapse(cell, cell.childNodes.length);
+
+var table = document.getElementById("table");
+
+var tableHTML = table.innerHTML;
+
+var editor = getEditor();
+editor.deleteTableRow(1);
+
+is(table.innerHTML == tableHTML, true, "editor should not modify non-editable table" );
+
+isnot(table.innerHTML == "\n <tbody>\n <tr>\n <td>a</td>\n <td>b</td>\n <td>c</td>\n </tr>\n \n <tr>\n <td>g</td>\n <td>h</td>\n <td>i</td>\n </tr>\n </tbody>\n ",
+ true, "editor.deleteTableRow(1) should not delete a non-editable row containing the selection");
+
+</script>
+
+
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug998188.html b/editor/libeditor/tests/test_bug998188.html
new file mode 100644
index 000000000..2d167f0bd
--- /dev/null
+++ b/editor/libeditor/tests/test_bug998188.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=565392
+-->
+<head>
+ <title>Test for Bug 998188</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=998188">Mozilla Bug 998188</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<div id="editor" contenteditable>abc</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 998188 **/
+
+SimpleTest.waitForExplicitFinish();
+
+function runTests()
+{
+ var editor = document.getElementById("editor");
+ editor.focus();
+
+ var textNode1 = document.createTextNode("def");
+ var textNode2 = document.createTextNode("ghi");
+
+ editor.appendChild(textNode1);
+ editor.appendChild(textNode2);
+
+ window.getSelection().collapse(textNode2, 3);
+
+ for (var i = 0; i < 9; i++) {
+ var caretRect = synthesizeQueryCaretRect(i);
+ ok(caretRect.succeeded, "QueryCaretRect should succeeded (" + i + ")");
+ }
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForFocus(runTests);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_composition_event_created_in_chrome.html b/editor/libeditor/tests/test_composition_event_created_in_chrome.html
new file mode 100644
index 000000000..18b72ccd4
--- /dev/null
+++ b/editor/libeditor/tests/test_composition_event_created_in_chrome.html
@@ -0,0 +1,82 @@
+<!doctype html>
+<html>
+
+<head>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<body>
+
+<input id="input">
+
+<script type="application/javascript">
+
+// In nsEditorEventListener, when listening event is not created with proper
+// event interface, it asserts the fact.
+SimpleTest.waitForExplicitFinish();
+
+var gInputElement = document.getElementById("input");
+
+function getEditorIMESupport(aInputElement)
+{
+ var editableElement = SpecialPowers.wrap(aInputElement).QueryInterface(SpecialPowers.Ci.nsIDOMNSEditableElement);
+ ok(editableElement, "The input element doesn't have nsIDOMNSEditableElement interface");
+ ok(editableElement.editor, "There is no editor for the input element");
+ var editorIMESupport = SpecialPowers.wrap(editableElement).editor.QueryInterface(SpecialPowers.Ci.nsIEditorIMESupport);
+ ok(editorIMESupport, "The input element doesn't have nsIEditorIMESupport interface");
+ return editorIMESupport;
+}
+
+var gEditorIMESupport;
+
+function testNotGenerateCompositionByCreatedEvents(aEventInterface)
+{
+ var compositionEvent = document.createEvent(aEventInterface);
+ if (compositionEvent.initCompositionEvent) {
+ compositionEvent.initCompositionEvent("compositionstart", true, true, window, "", "");
+ } else if (compositionEvent.initMouseEvent) {
+ compositionEvent.initMouseEvent("compositionstart", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
+ }
+ gInputElement.dispatchEvent(compositionEvent);
+ ok(!gEditorIMESupport.composing, "Composition shouldn't be started with a created compositionstart event (" + aEventInterface + ")");
+
+ compositionEvent = document.createEvent(aEventInterface);
+ if (compositionEvent.initCompositionEvent) {
+ compositionEvent.initCompositionEvent("compositionupdate", true, false, window, "abc", "");
+ } else if (compositionEvent.initMouseEvent) {
+ compositionEvent.initMouseEvent("compositionupdate", true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
+ }
+ gInputElement.dispatchEvent(compositionEvent);
+ ok(!gEditorIMESupport.composing, "Composition shouldn't be started with a created compositionupdate event (" + aEventInterface + ")");
+ is(gInputElement.value, "", "Input element shouldn't be modified with a created compositionupdate event (" + aEventInterface + ")");
+
+ compositionEvent = document.createEvent(aEventInterface);
+ if (compositionEvent.initCompositionEvent) {
+ compositionEvent.initCompositionEvent("compositionend", true, false, window, "abc", "");
+ } else if (compositionEvent.initMouseEvent) {
+ compositionEvent.initMouseEvent("compositionend", true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
+ }
+ gInputElement.dispatchEvent(compositionEvent);
+ ok(!gEditorIMESupport.composing, "Composition shouldn't be committed with a created compositionend event (" + aEventInterface + ")");
+ is(gInputElement.value, "", "Input element shouldn't be committed with a created compositionend event (" + aEventInterface + ")");
+}
+
+function doTests()
+{
+ gInputElement.focus();
+ gEditorIMESupport = getEditorIMESupport(gInputElement);
+
+ testNotGenerateCompositionByCreatedEvents("CompositionEvent");
+ testNotGenerateCompositionByCreatedEvents("MouseEvent");
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForFocus(doTests);
+
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_contenteditable_focus.html b/editor/libeditor/tests/test_contenteditable_focus.html
new file mode 100644
index 000000000..051ac7b2f
--- /dev/null
+++ b/editor/libeditor/tests/test_contenteditable_focus.html
@@ -0,0 +1,209 @@
+<html>
+<head>
+ <title>Test for contenteditable focus</title>
+ <script type="text/javascript"
+ src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="display">
+ First text in this document.<br>
+ <input id="inputText" type="text"><br>
+ <input id="inputTextReadonly" type="text" readonly><br>
+ <input id="inputButton" type="button" value="input[type=button]"><br>
+ <button id="button">button</button><br>
+ <div id="editor" contenteditable="true">
+ editable contents.<br>
+ <input id="inputTextInEditor" type="text"><br>
+ <input id="inputTextReadonlyInEditor" type="text" readonly><br>
+ <input id="inputButtonInEditor" type="button" value="input[type=button]"><br>
+ <button id="buttonInEditor">button</button><br>
+ <div id="noeditableInEditor" contenteditable="false">
+ <span id="spanInNoneditableInEditor">span element in noneditable in editor</span><br>
+ <input id="inputTextInNoneditableInEditor" type="text"><br>
+ <input id="inputTextReadonlyInNoneditableInEditor" type="text" readonly><br>
+ <input id="inputButtonInNoneditableInEditor" type="button" value="input[type=button]"><br>
+ <button id="buttonInNoneditableInEditor">button</button><br>
+ </div>
+ <span id="spanInEditor">span element in editor</span><br>
+ </div>
+ <div id="otherEditor" contenteditable="true">
+ other editor.
+ </div>
+</div>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests, window);
+
+function runTests()
+{
+ runTestsInternal();
+ SimpleTest.finish();
+}
+
+function runTestsInternal()
+{
+ var fm = SpecialPowers.Cc["@mozilla.org/focus-manager;1"].
+ getService(SpecialPowers.Ci.nsIFocusManager);
+ // XXX using selCon for checking the visibility of the caret, however,
+ // selCon is shared in document, cannot get the element of owner of the
+ // caret from javascript?
+ var selCon = SpecialPowers.wrap(window).
+ QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor).
+ getInterface(SpecialPowers.Ci.nsIWebNavigation).
+ QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor).
+ getInterface(SpecialPowers.Ci.nsISelectionDisplay).
+ QueryInterface(SpecialPowers.Ci.nsISelectionController);
+ var selection = window.getSelection();
+
+ var inputText = document.getElementById("inputText");
+ var inputTextReadonly = document.getElementById("inputTextReadonly");
+ var inputButton = document.getElementById("inputButton");
+ var button = document.getElementById("button");
+ var editor = document.getElementById("editor");
+ var inputTextInEditor = document.getElementById("inputTextInEditor");
+ var inputTextReadonlyInEditor = document.getElementById("inputTextReadonlyInEditor");
+ var inputButtonInEditor = document.getElementById("inputButtonInEditor");
+ var noeditableInEditor = document.getElementById("noeditableInEditor");
+ var spanInNoneditableInEditor = document.getElementById("spanInNoneditableInEditor");
+ var inputTextInNoneditableInEditor = document.getElementById("inputTextInNoneditableInEditor");
+ var inputTextReadonlyInNoneditableInEditor = document.getElementById("inputTextReadonlyInNoneditableInEditor");
+ var inputButtonInNoneditableInEditor = document.getElementById("inputButtonInNoneditableInEditor");
+ var buttonInNoneditableInEditor = document.getElementById("buttonInNoneditableInEditor");
+ var spanInEditor = document.getElementById("spanInEditor");
+ var otherEditor = document.getElementById("otherEditor");
+
+ // XXX if there is a contenteditable element, HTML editor sets dom selection
+ // to first editable node, but this makes inconsistency with normal document
+ // behavior.
+ todo_is(selection.rangeCount, 0, "unexpected selection range is there");
+ ok(!selCon.caretVisible, "caret is visible in the document");
+ // Move focus to inputTextInEditor
+ inputTextInEditor.focus();
+ is(SpecialPowers.unwrap(fm.focusedElement), inputTextInEditor,
+ "inputTextInEditor didn't get focus");
+ todo_is(selection.rangeCount, 0, "unexpected selection range is there");
+ ok(selCon.caretVisible, "caret isn't visible in the inputTextInEditor");
+ // Move focus to the editor
+ editor.focus();
+ is(SpecialPowers.unwrap(fm.focusedElement), editor,
+ "editor didn't get focus");
+ is(selection.rangeCount, 1,
+ "there is no selection range when editor has focus");
+ 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");
+ ok(selCon.caretVisible, "caret isn't visible in the editor");
+ // Move focus to other editor
+ otherEditor.focus();
+ is(SpecialPowers.unwrap(fm.focusedElement), otherEditor,
+ "the other editor didn't get focus");
+ is(selection.rangeCount, 1,
+ "there is no selection range when the other editor has focus");
+ 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");
+ ok(selCon.caretVisible, "caret isn't visible in the other editor");
+ // Move focus to inputTextInEditor
+ inputTextInEditor.focus();
+ is(SpecialPowers.unwrap(fm.focusedElement), inputTextInEditor,
+ "inputTextInEditor didn't get focus #2");
+ is(selection.rangeCount, 1, "selection range is lost from the document");
+ 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");
+ // XXX maybe, the caret can stay on the other editor if it's better.
+ is(startNode, editor,
+ "the caret should stay on the other editor");
+ ok(selCon.caretVisible,
+ "caret isn't visible in the inputTextInEditor");
+ // Move focus to the other editor again
+ otherEditor.focus();
+ is(SpecialPowers.unwrap(fm.focusedElement), otherEditor,
+ "the other editor didn't get focus #2");
+ // Set selection to the span element in the editor (unfocused)
+ range = document.createRange();
+ range.setStart(spanInEditor.firstChild, 5);
+ selection.removeAllRanges();
+ selection.addRange(range);
+ is(selection.rangeCount, 1, "selection range is lost from the document");
+ is(SpecialPowers.unwrap(fm.focusedElement), otherEditor,
+ "the other editor shouldn't lose focus by selection range change");
+ ok(selCon.caretVisible, "caret isn't visible in inputTextInEditor");
+ // Move focus to the editor
+ editor.focus();
+ is(SpecialPowers.unwrap(fm.focusedElement), editor,
+ "the editor didn't get focus #2");
+ is(selection.rangeCount, 1, "selection range is lost from the document");
+ range = selection.getRangeAt(0);
+ ok(range.collapsed, "the selection range isn't collapsed");
+ is(range.startOffset, 5,
+ "the caret is moved when the editor was focused (offset)");
+ var startNode = range.startContainer;
+ is(startNode.nodeType, 3, "the caret isn't in text node");
+ is(startNode.parentNode, spanInEditor,
+ "the caret is moved when the editor was focused (node)");
+ ok(selCon.caretVisible, "caret isn't visible in the editor (spanInEditor)");
+
+ // Move focus to each focusable element in the editor.
+ function testFocusMove(aSetFocusElementID, aFocusable, aCaretVisible)
+ {
+ editor.focus();
+ is(SpecialPowers.unwrap(fm.focusedElement), editor,
+ "testFocusMove: the editor didn't get focus at initializing (" +
+ aSetFocusElementID + ")");
+ var setFocusElement = document.getElementById(aSetFocusElementID);
+ setFocusElement.focus();
+ if (aFocusable) {
+ is(SpecialPowers.unwrap(fm.focusedElement), setFocusElement,
+ "testFocusMove: the " + aSetFocusElementID +
+ " didn't get focus");
+ } else {
+ is(SpecialPowers.unwrap(fm.focusedElement), editor,
+ "testFocusMove: the editor lost focus by focus() of the " +
+ aSetFocusElementID);
+ }
+ if (aCaretVisible) {
+ ok(selCon.caretVisible,
+ "testFocusMove: caret isn't visible when the " +
+ aSetFocusElementID + " has focus");
+ } else {
+ ok(!selCon.caretVisible,
+ "testFocusMove: caret is visible when the " +
+ aSetFocusElementID + " has focus");
+ }
+ }
+ testFocusMove("inputTextInEditor", true, true);
+ testFocusMove("inputTextReadonlyInEditor", true, true);
+ // XXX shouldn't the caret become invisible?
+ testFocusMove("inputButtonInEditor", true, true);
+ testFocusMove("noeditableInEditor", false, true);
+ testFocusMove("spanInNoneditableInEditor", false, true);
+ testFocusMove("inputTextInNoneditableInEditor", true, true);
+ testFocusMove("inputTextReadonlyInNoneditableInEditor", true, true);
+ testFocusMove("inputButtonInNoneditableInEditor", true, false);
+ testFocusMove("buttonInNoneditableInEditor", true, false);
+ testFocusMove("spanInEditor", false, true);
+ testFocusMove("inputText", true, true);
+ testFocusMove("inputTextReadonly", true, true);
+ testFocusMove("inputButton", true, false);
+ testFocusMove("button", true, false);
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_contenteditable_text_input_handling.html b/editor/libeditor/tests/test_contenteditable_text_input_handling.html
new file mode 100644
index 000000000..06b95fbb8
--- /dev/null
+++ b/editor/libeditor/tests/test_contenteditable_text_input_handling.html
@@ -0,0 +1,329 @@
+<html>
+<head>
+ <title>Test for text input event handling on contenteditable editor</title>
+ <script type="text/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="display">
+ <p id="static">static content<input id="inputInStatic"><textarea id="textareaInStatic"></textarea></p>
+ <p id="editor"contenteditable="true">content editable<input id="inputInEditor"><textarea id="textareaInEditor"></textarea></p>
+</div>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+const kLF = !navigator.platform.indexOf("Win") ? "\r\n" : "\n";
+
+function runTests()
+{
+ var fm = Components.classes["@mozilla.org/focus-manager;1"].
+ getService(Components.interfaces.nsIFocusManager);
+
+ var listener = {
+ handleEvent: function _hv(aEvent)
+ {
+ aEvent.preventDefault(); // prevent the browser default behavior
+ }
+ };
+ var els = Components.classes["@mozilla.org/eventlistenerservice;1"].
+ getService(Components.interfaces.nsIEventListenerService);
+ els.addSystemEventListener(window, "keypress", listener, false);
+
+ var staticContent = document.getElementById("static");
+ staticContent._defaultValue = getTextValue(staticContent);
+ staticContent._isFocusable = false;
+ staticContent._isEditable = false;
+ staticContent._isContentEditable = false;
+ staticContent._description = "non-editable p element";
+ var inputInStatic = document.getElementById("inputInStatic");
+ inputInStatic._defaultValue = getTextValue(inputInStatic);
+ inputInStatic._isFocusable = true;
+ inputInStatic._isEditable = true;
+ inputInStatic._isContentEditable = false;
+ inputInStatic._description = "input element in static content";
+ var textareaInStatic = document.getElementById("textareaInStatic");
+ textareaInStatic._defaultValue = getTextValue(textareaInStatic);
+ textareaInStatic._isFocusable = true;
+ textareaInStatic._isEditable = true;
+ textareaInStatic._isContentEditable = false;
+ textareaInStatic._description = "textarea element in static content";
+ var editor = document.getElementById("editor");
+ editor._defaultValue = getTextValue(editor);
+ editor._isFocusable = true;
+ editor._isEditable = true;
+ editor._isContentEditable = true;
+ editor._description = "contenteditable editor";
+ var inputInEditor = document.getElementById("inputInEditor");
+ inputInEditor._defaultValue = getTextValue(inputInEditor);
+ inputInEditor._isFocusable = true;
+ inputInEditor._isEditable = true;
+ inputInEditor._isContentEditable = false;
+ inputInEditor._description = "input element in contenteditable editor";
+ var textareaInEditor = document.getElementById("textareaInEditor");
+ textareaInEditor._defaultValue = getTextValue(textareaInEditor);
+ textareaInEditor._isFocusable = true;
+ textareaInEditor._isEditable = true;
+ textareaInEditor._isContentEditable = false;
+ textareaInEditor._description = "textarea element in contenteditable editor";
+
+ function getTextValue(aElement)
+ {
+ if (aElement == editor) {
+ var value = "";
+ for (var node = aElement.firstChild; node; node = node.nextSibling) {
+ if (node.nodeType == Node.TEXT_NODE) {
+ value += node.data;
+ } else if (node.nodeType == Node.ELEMENT_NODE) {
+ var tagName = node.tagName.toLowerCase();
+ switch (tagName) {
+ case "input":
+ case "textarea":
+ value += kLF;
+ break;
+ default:
+ ok(false, "Undefined tag is used in the editor: " + tagName);
+ break;
+ }
+ }
+ }
+ return value;
+ }
+ return aElement.value;
+ }
+
+ function testTextInput(aFocus)
+ {
+ var when = " when " +
+ ((aFocus && aFocus._isFocusable) ? aFocus._description + " has focus" :
+ "nobody has focus");
+
+ function checkValue(aElement, aInsertedText)
+ {
+ if (aElement == aFocus && aElement._isEditable) {
+ is(getTextValue(aElement), aInsertedText + aElement._defaultValue,
+ aElement._description +
+ " wasn't edited by synthesized key events" + when);
+ return;
+ }
+ is(getTextValue(aElement), aElement._defaultValue,
+ aElement._description +
+ " was edited by synthesized key events" + when);
+ }
+
+ if (aFocus && aFocus._isFocusable) {
+ aFocus.focus();
+ is(fm.focusedElement, aFocus,
+ aFocus._description + " didn't get focus at preparing tests" + when);
+ } else {
+ var focusedElement = fm.focusedElement;
+ if (focusedElement) {
+ focusedElement.blur();
+ }
+ ok(!fm.focusedElement,
+ "Failed to blur at preparing tests" + when);
+ }
+
+ if (aFocus && aFocus._isFocusable) {
+ synthesizeKey("A", { });
+ synthesizeKey("B", { });
+ synthesizeKey("C", { });
+ checkValue(staticContent, "ABC");
+ checkValue(inputInStatic, "ABC");
+ checkValue(textareaInStatic, "ABC");
+ checkValue(editor, "ABC");
+ checkValue(inputInEditor, "ABC");
+ checkValue(textareaInEditor, "ABC");
+
+ if (aFocus._isEditable) {
+ synthesizeKey("VK_BACK_SPACE", { });
+ synthesizeKey("VK_BACK_SPACE", { });
+ synthesizeKey("VK_BACK_SPACE", { });
+ checkValue(staticContent, "");
+ checkValue(inputInStatic, "");
+ checkValue(textareaInStatic, "");
+ checkValue(editor, "");
+ checkValue(inputInEditor, "");
+ checkValue(textareaInEditor, "");
+ }
+ }
+
+ // When key events are fired on unfocused editor.
+ function testDispatchedKeyEvent(aTarget)
+ {
+ var targetDescription = " (dispatched to " + aTarget._description + ")";
+ function dispatchKeyEvent(aKeyCode, aChar, aTarget)
+ {
+ var keyEvent = document.createEvent("KeyboardEvent");
+ keyEvent.initKeyEvent("keypress", true, true, null, false, false,
+ false, false, aKeyCode,
+ aChar ? aChar.charCodeAt(0) : 0);
+ aTarget.dispatchEvent(keyEvent);
+ }
+
+ function checkValueForDispatchedKeyEvent(aElement, aInsertedText)
+ {
+ if (aElement == aTarget && aElement._isEditable &&
+ (!aElement._isContentEditable || aElement == aFocus)) {
+ is(getTextValue(aElement), aInsertedText + aElement._defaultValue,
+ aElement._description +
+ " wasn't edited by dispatched key events" +
+ when + targetDescription);
+ return;
+ }
+ if (aElement == aTarget) {
+ is(getTextValue(aElement), aElement._defaultValue,
+ aElement._description +
+ " was edited by dispatched key events" +
+ when + targetDescription);
+ return;
+ }
+ is(getTextValue(aElement), aElement._defaultValue,
+ aElement._description +
+ " was edited by key events unexpectedly" +
+ when + targetDescription);
+ }
+
+ dispatchKeyEvent(0, "A", aTarget);
+ dispatchKeyEvent(0, "B", aTarget);
+ dispatchKeyEvent(0, "C", aTarget);
+
+ checkValueForDispatchedKeyEvent(staticContent, "ABC");
+ checkValueForDispatchedKeyEvent(inputInStatic, "ABC");
+ checkValueForDispatchedKeyEvent(textareaInStatic, "ABC");
+ checkValueForDispatchedKeyEvent(editor, "ABC");
+ checkValueForDispatchedKeyEvent(inputInEditor, "ABC");
+ checkValueForDispatchedKeyEvent(textareaInEditor, "ABC");
+
+ const nsIDOMKeyEvent = Components.interfaces.nsIDOMKeyEvent;
+ dispatchKeyEvent(nsIDOMKeyEvent.DOM_VK_BACK_SPACE, 0, aTarget);
+ dispatchKeyEvent(nsIDOMKeyEvent.DOM_VK_BACK_SPACE, 0, aTarget);
+ dispatchKeyEvent(nsIDOMKeyEvent.DOM_VK_BACK_SPACE, 0, aTarget);
+
+ checkValueForDispatchedKeyEvent(staticContent, "");
+ checkValueForDispatchedKeyEvent(inputInStatic, "");
+ checkValueForDispatchedKeyEvent(textareaInStatic, "");
+ checkValueForDispatchedKeyEvent(editor, "");
+ checkValueForDispatchedKeyEvent(inputInEditor, "");
+ checkValueForDispatchedKeyEvent(textareaInEditor, "");
+ }
+
+ testDispatchedKeyEvent(staticContent);
+ testDispatchedKeyEvent(inputInStatic);
+ testDispatchedKeyEvent(textareaInStatic);
+ testDispatchedKeyEvent(editor);
+ testDispatchedKeyEvent(inputInEditor);
+ testDispatchedKeyEvent(textareaInEditor);
+
+ if (!aFocus._isEditable) {
+ return;
+ }
+
+ // IME
+ // input first character
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3089",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+ var queryText = synthesizeQueryTextContent(0, 100);
+ ok(queryText, "query text event result is null" + when);
+ if (!queryText) {
+ return;
+ }
+ ok(queryText.succeeded, "query text event failed" + when);
+ if (!queryText.succeeded) {
+ return;
+ }
+ is(queryText.text, "\u3089" + aFocus._defaultValue,
+ "composing text is incorrect" + when);
+ var querySelectedText = synthesizeQuerySelectedText();
+ ok(querySelectedText, "query selected text event result is null" + when);
+ if (!querySelectedText) {
+ return;
+ }
+ ok(querySelectedText.succeeded, "query selected text event failed" + when);
+ if (!querySelectedText.succeeded) {
+ return;
+ }
+ is(querySelectedText.offset, 1,
+ "query selected text event returns wrong offset" + when);
+ is(querySelectedText.text, "",
+ "query selected text event returns wrong selected text" + when);
+ // commit composition
+ synthesizeComposition({ type: "compositioncommitasis" });
+ queryText = synthesizeQueryTextContent(0, 100);
+ ok(queryText, "query text event result is null after commit" + when);
+ if (!queryText) {
+ return;
+ }
+ ok(queryText.succeeded, "query text event failed after commit" + when);
+ if (!queryText.succeeded) {
+ return;
+ }
+ is(queryText.text, "\u3089" + aFocus._defaultValue,
+ "composing text is incorrect after commit" + when);
+ querySelectedText = synthesizeQuerySelectedText();
+ ok(querySelectedText,
+ "query selected text event result is null after commit" + when);
+ if (!querySelectedText) {
+ return;
+ }
+ ok(querySelectedText.succeeded,
+ "query selected text event failed after commit" + when);
+ if (!querySelectedText.succeeded) {
+ return;
+ }
+ is(querySelectedText.offset, 1,
+ "query selected text event returns wrong offset after commit" + when);
+ is(querySelectedText.text, "",
+ "query selected text event returns wrong selected text after commit" +
+ when);
+
+ checkValue(staticContent, "\u3089");
+ checkValue(inputInStatic, "\u3089");
+ checkValue(textareaInStatic, "\u3089");
+ checkValue(editor, "\u3089");
+ checkValue(inputInEditor, "\u3089");
+ checkValue(textareaInEditor, "\u3089");
+
+ synthesizeKey("VK_BACK_SPACE", { });
+ checkValue(staticContent, "");
+ checkValue(inputInStatic, "");
+ checkValue(textareaInStatic, "");
+ checkValue(editor, "");
+ checkValue(inputInEditor, "");
+ checkValue(textareaInEditor, "");
+ }
+
+ testTextInput(inputInStatic);
+ testTextInput(textareaInStatic);
+ testTextInput(editor);
+ testTextInput(inputInEditor);
+ testTextInput(textareaInEditor);
+
+ els.removeSystemEventListener(window, "keypress", listener, false);
+
+ SimpleTest.finish();
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_css_chrome_load_access.html b/editor/libeditor/tests/test_css_chrome_load_access.html
new file mode 100644
index 000000000..b6bb3fb46
--- /dev/null
+++ b/editor/libeditor/tests/test_css_chrome_load_access.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1245681
+-->
+<head>
+ <title>Test for Bug 1245681</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1245681">Mozilla Bug 1245681</a>
+<p id="display"></p>
+<div id="content">
+ <iframe></iframe>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+const Ci = SpecialPowers.Ci;
+var styleSheets = null;
+
+function runTest() {
+
+ var editframe = window.frames[0];
+ var editdoc = editframe.document;
+ editdoc.designMode = 'on';
+ var editor = SpecialPowers.wrap(editframe)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIEditingSession)
+ .getEditorForWindow(editframe);
+
+ styleSheets = editor.QueryInterface(Ci.nsIEditorStyleSheets);
+
+ // test 1: try to access chrome:// url that is accessible to content
+ try
+ {
+ styleSheets.addOverrideStyleSheet("chrome://browser/content/pageinfo/pageInfo.css");
+ ok(true, "should be allowed to access chrome://*.css if contentaccessible");
+ }
+ catch (ex) {
+ ok(false, "should be allowed to access chrome://*.css if contentaccessible");
+ }
+
+ // test 2: try to access chrome:// url that is *not* accessible to content
+ // please note that addOverrideStyleSheet() is triggered by the system,
+ // so the load should also *always* succeed.
+ try
+ {
+ styleSheets.addOverrideStyleSheet("chrome://mozapps/skin/aboutNetworking.css");
+ ok(true, "should be allowed to access chrome://*.css even if *not* contentaccessible");
+ }
+ catch (ex) {
+ ok(false, "should be allowed to access chrome://*.css even if *not* contentaccessible");
+ }
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_dom_input_event_on_htmleditor.html b/editor/libeditor/tests/test_dom_input_event_on_htmleditor.html
new file mode 100644
index 000000000..d1716a228
--- /dev/null
+++ b/editor/libeditor/tests/test_dom_input_event_on_htmleditor.html
@@ -0,0 +1,182 @@
+<html>
+<head>
+ <title>Test for input event of text editor</title>
+ <script type="text/javascript"
+ src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript"
+ src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="display">
+ <iframe id="editor1" src="data:text/html,<html><body contenteditable id='eventTarget'></body></html>"></iframe>
+ <iframe id="editor2" src="data:text/html,<html contenteditable id='eventTarget'><body></body></html>"></iframe>
+ <iframe id="editor3" src="data:text/html,<html><body><div contenteditable id='eventTarget'></div></body></html>"></iframe>
+ <iframe id="editor4" src="data:text/html,<html contenteditable id='eventTarget'><body><div contenteditable id='editTarget'></div></body></html>"></iframe>
+ <iframe id="editor5" src="data:text/html,<html><body id='eventTarget'></body><script>document.designMode='on';</script></html>"></iframe>
+</div>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests, window);
+
+const kIsMac = navigator.platform.indexOf("Mac") == 0;
+
+function runTests()
+{
+ function doTests(aDocument, aWindow, aDescription)
+ {
+ aDescription += ": ";
+ aWindow.focus();
+
+ var body = aDocument.body;
+
+ var eventTarget = aDocument.getElementById("eventTarget");
+ // The event target must be focusable because it's the editing host.
+ eventTarget.focus();
+
+ var editTarget = aDocument.getElementById("editTarget");
+ if (!editTarget) {
+ editTarget = eventTarget;
+ }
+
+ // Root element never can be edit target. If the editTarget is the root
+ // element, replace with its body.
+ if (editTarget == aDocument.documentElement) {
+ editTarget = body;
+ }
+
+ editTarget.innerHTML = "";
+
+ // If the editTarget isn't its editing host, move caret to the start of it.
+ if (eventTarget != editTarget) {
+ aDocument.getSelection().collapse(editTarget, 0);
+ }
+
+ var inputEvent = null;
+
+ var handler = function (aEvent) {
+ is(aEvent.target, eventTarget,
+ "input event is fired on unexpected element: " + aEvent.target.tagName);
+ ok(!aEvent.cancelable, "input event must not be cancelable");
+ ok(aEvent.bubbles, "input event must be bubbles");
+ if (SpecialPowers.getBoolPref("dom.event.highrestimestamp.enabled")) {
+ var duration = Math.abs(window.performance.now() - aEvent.timeStamp);
+ ok(duration < 30 * 1000,
+ "perhaps, timestamp wasn't set correctly :" + aEvent.timeStamp +
+ " (expected it to be within 30s of the current time but it " +
+ "differed by " + duration + "ms)");
+ } else {
+ var eventTime = new Date(aEvent.timeStamp);
+ var duration = Math.abs(Date.now() - aEvent.timeStamp);
+ ok(duration < 30 * 1000,
+ "perhaps, timestamp wasn't set correctly :" +
+ eventTime.toLocaleString() +
+ " (expected it to be within 30s of the current time but it " +
+ "differed by " + duration + "ms)");
+ }
+ inputEvent = aEvent;
+ };
+
+ aWindow.addEventListener("input", handler, true);
+
+ inputEvent = null;
+ synthesizeKey("a", { }, aWindow);
+ is(editTarget.innerHTML, "a", aDescription + "wrong element was edited");
+ ok(inputEvent, aDescription + "input event wasn't fired by 'a' key");
+ ok(inputEvent.isTrusted, aDescription + "input event by 'a' key wasn't trusted event");
+
+ inputEvent = null;
+ synthesizeKey("VK_BACK_SPACE", { }, aWindow);
+ ok(inputEvent, aDescription + "input event wasn't fired by BackSpace key");
+ ok(inputEvent.isTrusted, aDescription + "input event by BackSpace key wasn't trusted event");
+
+ inputEvent = null;
+ synthesizeKey("B", { shiftKey: true }, aWindow);
+ ok(inputEvent, aDescription + "input event wasn't fired by 'B' key");
+ ok(inputEvent.isTrusted, aDescription + "input event by 'B' key wasn't trusted event");
+
+ inputEvent = null;
+ synthesizeKey("VK_RETURN", { }, aWindow);
+ ok(inputEvent, aDescription + "input event wasn't fired by Enter key");
+ ok(inputEvent.isTrusted, aDescription + "input event by Enter key wasn't trusted event");
+
+ inputEvent = null;
+ synthesizeKey("C", { shiftKey: true }, aWindow);
+ ok(inputEvent, aDescription + "input event wasn't fired by 'C' key");
+ ok(inputEvent.isTrusted, aDescription + "input event by 'C' key wasn't trusted event");
+
+ inputEvent = null;
+ synthesizeKey("VK_RETURN", { }, aWindow);
+ ok(inputEvent, aDescription + "input event wasn't fired by Enter key (again)");
+ ok(inputEvent.isTrusted, aDescription + "input event by Enter key (again) wasn't trusted event");
+
+ inputEvent = null;
+ editTarget.innerHTML = "foo-bar";
+ ok(!inputEvent, aDescription + "input event was fired by setting value");
+
+ inputEvent = null;
+ editTarget.innerHTML = "";
+ ok(!inputEvent, aDescription + "input event was fired by setting empty value");
+
+ inputEvent = null;
+ synthesizeKey(" ", { }, aWindow);
+ ok(inputEvent, aDescription + "input event wasn't fired by Space key");
+ ok(inputEvent.isTrusted, aDescription + "input event by Space key wasn't trusted event");
+
+ inputEvent = null;
+ synthesizeKey("VK_DELETE", { }, aWindow);
+ ok(!inputEvent, aDescription + "input event was fired by Delete key at the end");
+
+ inputEvent = null;
+ synthesizeKey("VK_LEFT", { }, aWindow);
+ ok(!inputEvent, aDescription + "input event was fired by Left key");
+
+ inputEvent = null;
+ synthesizeKey("VK_DELETE", { }, aWindow);
+ ok(inputEvent, aDescription + "input event wasn't fired by Delete key at the start");
+ ok(inputEvent.isTrusted, aDescription + "input event by Delete key wasn't trusted event");
+
+ inputEvent = null;
+ synthesizeKey("z", { accelKey: true }, aWindow);
+ ok(inputEvent, aDescription + "input event wasn't fired by Undo");
+ ok(inputEvent.isTrusted, aDescription + "input event by Undo wasn't trusted event");
+
+ inputEvent = null;
+ synthesizeKey("z", { accelKey: true, shiftKey: true }, aWindow);
+ ok(inputEvent, aDescription + "input event wasn't fired by Redo");
+ ok(inputEvent.isTrusted, aDescription + "input event by Redo wasn't trusted event");
+
+ aWindow.removeEventListener("input", handler, true);
+ }
+
+ doTests(document.getElementById("editor1").contentDocument,
+ document.getElementById("editor1").contentWindow,
+ "Editor1, body has contenteditable attribute");
+ doTests(document.getElementById("editor2").contentDocument,
+ document.getElementById("editor2").contentWindow,
+ "Editor2, html has contenteditable attribute");
+ doTests(document.getElementById("editor3").contentDocument,
+ document.getElementById("editor3").contentWindow,
+ "Editor3, div has contenteditable attribute");
+ doTests(document.getElementById("editor4").contentDocument,
+ document.getElementById("editor4").contentWindow,
+ "Editor4, html and div has contenteditable attribute");
+ doTests(document.getElementById("editor5").contentDocument,
+ document.getElementById("editor5").contentWindow,
+ "Editor5, html and div has contenteditable attribute");
+
+ SimpleTest.finish();
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_dom_input_event_on_texteditor.html b/editor/libeditor/tests/test_dom_input_event_on_texteditor.html
new file mode 100644
index 000000000..b1395e99c
--- /dev/null
+++ b/editor/libeditor/tests/test_dom_input_event_on_texteditor.html
@@ -0,0 +1,140 @@
+<html>
+<head>
+ <title>Test for input event of text editor</title>
+ <script type="text/javascript"
+ src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript"
+ src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="display">
+ <input type="text" id="input">
+ <textarea id="textarea"></textarea>
+</div>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests, window);
+
+const kIsMac = navigator.platform.indexOf("Mac") == 0;
+
+function runTests()
+{
+ function doTests(aElement, aDescription, aIsTextarea)
+ {
+ aDescription += ": ";
+ aElement.focus();
+ aElement.value = "";
+
+ var inputEvent = null;
+
+ var handler = function (aEvent) {
+ is(aEvent.target, aElement,
+ "input event is fired on unexpected element: " + aEvent.target.tagName);
+ ok(!aEvent.cancelable, "input event must not be cancelable");
+ ok(aEvent.bubbles, "input event must be bubbles");
+ if (SpecialPowers.getBoolPref("dom.event.highrestimestamp.enabled")) {
+ var duration = Math.abs(window.performance.now() - aEvent.timeStamp);
+ ok(duration < 30 * 1000,
+ "perhaps, timestamp wasn't set correctly :" + aEvent.timeStamp +
+ " (expected it to be within 30s of the current time but it " +
+ "differed by " + duration + "ms)");
+ } else {
+ var eventTime = new Date(aEvent.timeStamp);
+ var duration = Math.abs(Date.now() - aEvent.timeStamp);
+ ok(duration < 30 * 1000,
+ "perhaps, timestamp wasn't set correctly :" +
+ eventTime.toLocaleString() +
+ " (expected it to be within 30s of the current time but it " +
+ "differed by " + duration + "ms)");
+ }
+ inputEvent = aEvent;
+ };
+
+ aElement.addEventListener("input", handler, true);
+
+ inputEvent = null;
+ synthesizeKey("a", { });
+ is(aElement.value, "a", aDescription + "'a' key didn't change the value");
+ ok(inputEvent, aDescription + "input event wasn't fired by 'a' key");
+ ok(inputEvent.isTrusted, aDescription + "input event by 'a' key wasn't trusted event");
+
+ inputEvent = null;
+ synthesizeKey("VK_BACK_SPACE", { });
+ is(aElement.value, "", aDescription + "BackSpace key didn't remove the value");
+ ok(inputEvent, aDescription + "input event wasn't fired by BackSpace key");
+ ok(inputEvent.isTrusted, aDescription + "input event by BackSpace key wasn't trusted event");
+
+ if (aIsTextarea) {
+ inputEvent = null;
+ synthesizeKey("VK_RETURN", { });
+ is(aElement.value, "\n", aDescription + "Enter key didn't change the value");
+ ok(inputEvent, aDescription + "input event wasn't fired by Enter key");
+ ok(inputEvent.isTrusted, aDescription + "input event by Enter key wasn't trusted event");
+ }
+
+ inputEvent = null;
+ aElement.value = "foo-bar";
+ is(aElement.value, "foo-bar", aDescription + "value wasn't set");
+ ok(!inputEvent, aDescription + "input event was fired by setting value");
+
+ inputEvent = null;
+ aElement.value = "";
+ is(aElement.value, "", aDescription + "value wasn't set (empty)");
+ ok(!inputEvent, aDescription + "input event was fired by setting empty value");
+
+ inputEvent = null;
+ synthesizeKey(" ", { });
+ is(aElement.value, " ", aDescription + "Space key didn't change the value");
+ ok(inputEvent, aDescription + "input event wasn't fired by Space key");
+ ok(inputEvent.isTrusted, aDescription + "input event by Space key wasn't trusted event");
+
+ inputEvent = null;
+ synthesizeKey("VK_DELETE", { });
+ is(aElement.value, " ", aDescription + "Delete key removed the value");
+ ok(!inputEvent, aDescription + "input event was fired by Delete key at the end");
+
+ inputEvent = null;
+ synthesizeKey("VK_LEFT", { });
+ is(aElement.value, " ", aDescription + "Left key removed the value");
+ ok(!inputEvent, aDescription + "input event was fired by Left key");
+
+ inputEvent = null;
+ synthesizeKey("VK_DELETE", { });
+ is(aElement.value, "", aDescription + "Delete key didn't remove the value");
+ ok(inputEvent, aDescription + "input event wasn't fired by Delete key at the start");
+ ok(inputEvent.isTrusted, aDescription + "input event by Delete key wasn't trusted event");
+
+ inputEvent = null;
+ synthesizeKey("z", { accelKey: true });
+ is(aElement.value, " ", aDescription + "Accel+Z key didn't undo the value");
+ ok(inputEvent, aDescription + "input event wasn't fired by Undo");
+ ok(inputEvent.isTrusted, aDescription + "input event by Undo wasn't trusted event");
+
+ inputEvent = null;
+ synthesizeKey("z", { accelKey: true, shiftKey: true });
+ is(aElement.value, "", aDescription + "Accel+Y key didn't redo the value");
+ ok(inputEvent, aDescription + "input event wasn't fired by Redo");
+ ok(inputEvent.isTrusted, aDescription + "input event by Redo wasn't trusted event");
+
+ aElement.removeEventListener("input", handler, true);
+ }
+
+ doTests(document.getElementById("input"), "<input type=\"text\">", false);
+ doTests(document.getElementById("textarea"), "<textarea>", true);
+
+ SimpleTest.finish();
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_dragdrop.html b/editor/libeditor/tests/test_dragdrop.html
new file mode 100644
index 000000000..c992b7142
--- /dev/null
+++ b/editor/libeditor/tests/test_dragdrop.html
@@ -0,0 +1,178 @@
+<!doctype html>
+<html>
+
+<head>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<body>
+ <span id="text" style="font-size: 40px;">Some Text</span>
+
+ <input id="input" value="Drag Me">
+ <textarea id="textarea">Some Text To Drag</textarea>
+ <p id="contenteditable" contenteditable="true">This is some <b id="bold">editable</b> text.</p>
+ <p id="nestedce" contenteditable="true"><span id="first"> </span>First letter <span id="noneditable" contenteditable="false">Middle</span> Last part</p>
+
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// This listener allows us to clear the default data for the selection added for the drag.
+var shouldClear = false;
+window.addEventListener("dragstart", function (event) { if (shouldClear) event.dataTransfer.clearData() }, true);
+
+function doTest()
+{
+ const htmlContextData = { type: 'text/_moz_htmlcontext',
+ data: '<html><body></body></html>' };
+ const htmlInfoData = { type: 'text/_moz_htmlinfo', data: '0,0' };
+ const htmlData = { type: 'text/html', data: '<span id="text" style="font-size: 40px;">Some Text</span>' };
+
+ const htmlContextDataEditable = { type: 'text/_moz_htmlcontext',
+ data: '<html><body><p id="contenteditable" contenteditable="true"></p></body></html>' };
+
+ var text = document.getElementById("text");
+ var input = document.getElementById("input");
+ var contenteditable = document.getElementById("contenteditable");
+
+ var selection = window.getSelection();
+
+ // -------- Test dragging regular text
+ selection.selectAllChildren(text);
+ var result = synthesizeDragStart(text, [[htmlContextData, htmlInfoData, htmlData,
+ {type: "text/plain", data: "Some Text"}]], window, 40, 10);
+ is(result, null, "Test dragging regular text");
+
+ // -------- Test dragging text from an <input>
+ input.setSelectionRange(1, 4);
+ result = synthesizeDragStart(input, [[{type: "text/plain", data: "rag"}]], window, 25, 6);
+ is(result, null, "Test dragging input");
+
+ // -------- Test dragging text from a <textarea>
+ textarea.setSelectionRange(1, 7);
+ result = synthesizeDragStart(textarea, [[{type: "text/plain", data: "ome Te"}]], window, 25, 6);
+ is(result, null, "Test dragging textarea");
+ textarea.blur();
+
+ // -------- Test dragging text from a contenteditable
+ selection.selectAllChildren(contenteditable.childNodes[1]);
+ result = synthesizeDragStart(contenteditable.childNodes[1],
+ [[htmlContextDataEditable, htmlInfoData,
+ {type: 'text/html', data: '<b id="bold">editable</b>' },
+ {type: "text/plain", data: "editable"}]], window, 5, 6);
+ is(result, null, "Test dragging contenteditable");
+ contenteditable.blur();
+
+ // -------- Test dragging regular text of text/html to <input>
+
+ selection.selectAllChildren(text);
+ input.value = "";
+ synthesizeDrop(text, input, [], "copy");
+ is(input.value, "Some Text", "Drag text/html onto input");
+
+ // -------- Test dragging regular text of text/html to disabled <input>
+
+ selection.selectAllChildren(text);
+ input.value = "";
+ input.disabled = true;
+ synthesizeDrop(text, input, [], "copy");
+ is(input.value, "", "Drag text/html onto disabled input");
+ input.disabled = false;
+
+ // -------- Test dragging regular text of text/html to readonly <input>
+
+ selection.selectAllChildren(text);
+ input.readOnly = true;
+ synthesizeDrop(text, input, [], "copy");
+ is(input.value, "", "Drag text/html onto readonly input");
+ input.readOnly = false;
+
+ // -------- Test dragging regular text of text/html to <input>. This sets
+ // shouldClear to true so that the default drag data is not present
+ // and we can use the data passed to synthesizeDrop. This allows
+ // testing of a drop with just text/html.
+ shouldClear = true;
+ selection.selectAllChildren(text);
+ input.value = "";
+ synthesizeDrop(text, input, [[{type: "text/html", data: "Some <b>Bold<b> Text"}]], "copy");
+ is(input.value, "", "Drag text/html onto input");
+
+ // -------- Test dragging regular text of text/plain and text/html to <input>
+
+ selection.selectAllChildren(text);
+ input.value = "";
+ synthesizeDrop(text, input, [[{type: "text/html", data: "Some <b>Bold<b> Text"},
+ {type: "text/plain", data: "Some Plain Text"}]], "copy");
+ is(input.value, "Some Plain Text", "Drag text/html and text/plain onto input");
+
+ // -------- Test dragging regular text of text/plain to <textarea>
+
+// XXXndeakin Can't test textareas due to some event handling issue
+// selection.selectAllChildren(text);
+// synthesizeDrop(text, textarea, [[{type: "text/plain", data: "Somewhat Longer Text"}]], "copy");
+// is(textarea.value, "Somewhat Longer Text", "Drag text/plain onto textarea");
+
+ // -------- Test dragging special text type of text/plain to contenteditable
+
+ selection.selectAllChildren(text);
+ synthesizeDrop(text, input, [[{type: "text/x-moz-text-internal", data: "Some Special Text"}]], "copy");
+ is(input.value, "Some Plain Text", "Drag text/x-moz-text-internal onto input");
+
+ // -------- Test dragging regular text of text/plain to contenteditable
+
+ selection.selectAllChildren(text);
+ synthesizeDrop(text, contenteditable, [[{type: "text/plain", data: "Sample Text"}]], "copy");
+ is(contenteditable.childNodes.length, 3, "Drag text/plain onto contenteditable child nodes");
+ is(contenteditable.textContent, "This is some editable text.Sample Text",
+ "Drag text/plain onto contenteditable text");
+
+ // -------- Test dragging regular text of text/html to contenteditable
+
+ selection.selectAllChildren(text);
+ synthesizeDrop(text, contenteditable, [[{type: "text/html", data: "Sample <i>Italic</i> Text"}]], "copy");
+ is(contenteditable.childNodes.length, 6, "Drag text/html onto contenteditable child nodes");
+ is(contenteditable.childNodes[4].tagName, "I", "Drag text/html onto contenteditable italic");
+ is(contenteditable.childNodes[4].textContent, "Italic", "Drag text/html onto contenteditable italic text");
+
+ // -------- Test dragging contenteditable to <input>
+
+ selection.selectAllChildren(document.getElementById("bold"));
+ synthesizeDrop(bold, input, [[{type: "text/html", data: "<b>editable</b>"},
+ {type: "text/plain", data: "editable"}]], "copy");
+ is(input.value, "Some Plain Texteditable", "Move text/html and text/plain from contenteditable onto input");
+
+ // -------- Test dragging contenteditable to contenteditable
+
+ shouldClear = false;
+
+ selection.selectAllChildren(contenteditable.childNodes[4]);
+ synthesizeDrop(contenteditable.childNodes[4], contenteditable, [], "copy");
+ is(contenteditable.childNodes.length, 7, "Move text/html and text/plain from contenteditable onto itself child nodes");
+ is(contenteditable.childNodes[6].tagName, "I", "Move text/html and text/plain from contenteditable onto itself italic");
+ is(contenteditable.childNodes[6].textContent, "Italic", "Move text/html and text/plain from contenteditable onto itself text");
+
+ // We'd test 'move' here as well as 'copy', but that requires knowledge of
+ // the source of the drag which drag simulation doesn't provide.
+
+ // -------- Test dragging non-editable nested inside contenteditable to contenteditable
+
+ input.focus(); // this resets some state in the selection otherwise an inexplicable error occurs calling selectAllChildren.
+ input.blur();
+
+ var nonEditable = document.getElementById("noneditable");
+ selection.selectAllChildren(nonEditable);
+ synthesizeDrop(nonEditable, document.getElementById("first"), [], "copy");
+ is(document.getElementById("nestedce").textContent, " MiddleFirst letter Middle Last part",
+ "Drag non-editable text/html onto contenteditable text");
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForFocus(doTest);
+
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_htmleditor_keyevent_handling.html b/editor/libeditor/tests/test_htmleditor_keyevent_handling.html
new file mode 100644
index 000000000..bfec290a5
--- /dev/null
+++ b/editor/libeditor/tests/test_htmleditor_keyevent_handling.html
@@ -0,0 +1,664 @@
+<html>
+<head>
+ <title>Test for key event handler of HTML editor</title>
+ <script type="text/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="display">
+ <div id="htmlEditor" contenteditable="true"><br></div>
+</div>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests, window);
+
+var htmlEditor = document.getElementById("htmlEditor");
+
+const kIsMac = navigator.platform.indexOf("Mac") == 0;
+const kIsWin = navigator.platform.indexOf("Win") == 0;
+const kIsLinux = navigator.platform.indexOf("Linux") == 0 || navigator.platform.indexOf("SunOS") == 0 ;
+
+function runTests()
+{
+ document.execCommand("stylewithcss", false, "true");
+
+ var fm = SpecialPowers.Cc["@mozilla.org/focus-manager;1"].
+ getService(SpecialPowers.Ci.nsIFocusManager);
+
+ var capturingPhase = { fired: false, prevented: false };
+ var bubblingPhase = { fired: false, prevented: false };
+
+ var listener = {
+ handleEvent: function _hv(aEvent)
+ {
+ is(aEvent.type, "keypress", "unexpected event is handled");
+ switch (aEvent.eventPhase) {
+ case aEvent.CAPTURING_PHASE:
+ capturingPhase.fired = true;
+ capturingPhase.prevented = aEvent.defaultPrevented;
+ break;
+ case aEvent.BUBBLING_PHASE:
+ bubblingPhase.fired = true;
+ bubblingPhase.prevented = aEvent.defaultPrevented;
+ aEvent.preventDefault(); // prevent the browser default behavior
+ break;
+ default:
+ ok(false, "event is handled in unexpected phase");
+ }
+ }
+ };
+
+ function check(aDescription,
+ aFiredOnCapture, aFiredOnBubbling, aPreventedOnBubbling)
+ {
+ function getDesciption(aExpected)
+ {
+ return aDescription + (aExpected ? " wasn't " : " was ");
+ }
+ is(capturingPhase.fired, aFiredOnCapture,
+ getDesciption(aFiredOnCapture) + "fired on capture phase");
+ is(bubblingPhase.fired, aFiredOnBubbling,
+ getDesciption(aFiredOnBubbling) + "fired on bubbling phase");
+
+ // If the event is fired on bubbling phase and it was already prevented
+ // on capture phase, it must be prevented on bubbling phase too.
+ if (capturingPhase.prevented) {
+ todo(false, aDescription +
+ " was consumed already, so, we cannot test the editor behavior actually");
+ aPreventedOnBubbling = true;
+ }
+
+ is(bubblingPhase.prevented, aPreventedOnBubbling,
+ getDesciption(aPreventedOnBubbling) + "prevented on bubbling phase");
+ }
+
+ SpecialPowers.addSystemEventListener(window, "keypress", listener, true);
+ SpecialPowers.addSystemEventListener(window, "keypress", listener, false);
+
+ function doTest(aElement, aDescription,
+ aIsReadonly, aIsTabbable, aIsPlaintext)
+ {
+ function reset(aText)
+ {
+ capturingPhase.fired = false;
+ capturingPhase.prevented = false;
+ bubblingPhase.fired = false;
+ bubblingPhase.prevented = false;
+ aElement.innerHTML = aText;
+ var sel = window.getSelection();
+ var range = document.createRange();
+ range.setStart(aElement, aElement.childNodes.length);
+ sel.removeAllRanges();
+ sel.addRange(range);
+ }
+
+ function resetForIndent(aText)
+ {
+ capturingPhase.fired = false;
+ capturingPhase.prevented = false;
+ bubblingPhase.fired = false;
+ bubblingPhase.prevented = false;
+ aElement.innerHTML = aText;
+ var sel = window.getSelection();
+ var range = document.createRange();
+ var target = document.getElementById("target").firstChild;
+ range.setStart(target, target.length);
+ sel.removeAllRanges();
+ sel.addRange(range);
+ }
+
+ if (document.activeElement) {
+ document.activeElement.blur();
+ }
+
+ aDescription += ": "
+
+ aElement.focus();
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement, aDescription + "failed to move focus");
+
+ // Backspace key:
+ // If editor is readonly, it doesn't consume.
+ // If editor is editable, it consumes backspace and shift+backspace.
+ // Otherwise, editor doesn't consume the event.
+ reset("");
+ synthesizeKey("VK_BACK_SPACE", { });
+ check(aDescription + "Backspace", true, true, true);
+
+ reset("");
+ synthesizeKey("VK_BACK_SPACE", { shiftKey: true });
+ check(aDescription + "Shift+Backspace", true, true, true);
+
+ reset("");
+ synthesizeKey("VK_BACK_SPACE", { ctrlKey: true });
+ check(aDescription + "Ctrl+Backspace", true, true, aIsReadonly);
+
+ reset("");
+ synthesizeKey("VK_BACK_SPACE", { altKey: true });
+ check(aDescription + "Alt+Backspace", true, true, aIsReadonly || kIsMac);
+
+ reset("");
+ synthesizeKey("VK_BACK_SPACE", { metaKey: true });
+ check(aDescription + "Meta+Backspace", true, true, aIsReadonly);
+
+ reset("");
+ synthesizeKey("VK_BACK_SPACE", { osKey: true });
+ check(aDescription + "OS+Backspace", true, true, aIsReadonly);
+
+ // Delete key:
+ // If editor is readonly, it doesn't consume.
+ // If editor is editable, delete is consumed.
+ // Otherwise, editor doesn't consume the event.
+ reset("");
+ synthesizeKey("VK_DELETE", { });
+ check(aDescription + "Delete", true, true, !aIsReadonly || kIsMac);
+
+ reset("");
+ synthesizeKey("VK_DELETE", { shiftKey: true });
+ check(aDescription + "Shift+Delete", true, true, kIsMac);
+
+ reset("");
+ synthesizeKey("VK_DELETE", { ctrlKey: true });
+ check(aDescription + "Ctrl+Delete", true, true, false);
+
+ reset("");
+ synthesizeKey("VK_DELETE", { altKey: true });
+ check(aDescription + "Alt+Delete", true, true, kIsMac);
+
+ reset("");
+ synthesizeKey("VK_DELETE", { metaKey: true });
+ check(aDescription + "Meta+Delete", true, true, false);
+
+ reset("");
+ synthesizeKey("VK_DELETE", { osKey: true });
+ check(aDescription + "OS+Delete", true, true, false);
+
+ // Return key:
+ // If editor is readonly, it doesn't consume.
+ // If editor is editable and not single line editor, it consumes Return
+ // and Shift+Return.
+ // Otherwise, editor doesn't consume the event.
+ reset("a");
+ synthesizeKey("VK_RETURN", { });
+ check(aDescription + "Return",
+ true, true, !aIsReadonly);
+ is(aElement.innerHTML, aIsReadonly ? "a" : "a<br><br>",
+ aDescription + "Return");
+
+ reset("a");
+ synthesizeKey("VK_RETURN", { shiftKey: true });
+ check(aDescription + "Shift+Return",
+ true, true, !aIsReadonly);
+ is(aElement.innerHTML, aIsReadonly ? "a" : "a<br><br>",
+ aDescription + "Shift+Return");
+
+ reset("a");
+ synthesizeKey("VK_RETURN", { ctrlKey: true });
+ check(aDescription + "Ctrl+Return", true, true, false);
+ is(aElement.innerHTML, "a", aDescription + "Ctrl+Return");
+
+ reset("a");
+ synthesizeKey("VK_RETURN", { altKey: true });
+ check(aDescription + "Alt+Return", true, true, false);
+ is(aElement.innerHTML, "a", aDescription + "Alt+Return");
+
+ reset("a");
+ synthesizeKey("VK_RETURN", { metaKey: true });
+ check(aDescription + "Meta+Return", true, true, false);
+ is(aElement.innerHTML, "a", aDescription + "Meta+Return");
+
+ reset("a");
+ synthesizeKey("VK_RETURN", { osKey: true });
+ check(aDescription + "OS+Return", true, true, false);
+ is(aElement.innerHTML, "a", aDescription + "OS+Return");
+
+ // Tab key:
+ // If editor is tabbable, editor doesn't consume all tab key events.
+ // Otherwise, editor consumes tab key event without any modifier keys.
+ reset("a");
+ synthesizeKey("VK_TAB", { });
+ check(aDescription + "Tab",
+ true, true, !aIsTabbable && !aIsReadonly);
+ is(aElement.innerHTML,
+ aIsTabbable || aIsReadonly ? "a" :
+ aIsPlaintext ? "a\t" : "a&nbsp;&nbsp;&nbsp; <br>",
+ aDescription + "Tab");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Tab)");
+
+ reset("a");
+ synthesizeKey("VK_TAB", { shiftKey: true });
+ check(aDescription + "Shift+Tab", true, true, false);
+ is(aElement.innerHTML, "a", aDescription + "Shift+Tab");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Shift+Tab)");
+
+ // Ctrl+Tab should be consumed by tabbrowser at keydown, so, keypress
+ // event should never be fired.
+ reset("a");
+ synthesizeKey("VK_TAB", { ctrlKey: true });
+ check(aDescription + "Ctrl+Tab", false, false, false);
+ is(aElement.innerHTML, "a", aDescription + "Ctrl+Tab");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Ctrl+Tab)");
+
+ reset("a");
+ synthesizeKey("VK_TAB", { altKey: true });
+ check(aDescription + "Alt+Tab", true, true, false);
+ is(aElement.innerHTML, "a", aDescription + "Alt+Tab");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Alt+Tab)");
+
+ reset("a");
+ synthesizeKey("VK_TAB", { metaKey: true });
+ check(aDescription + "Meta+Tab", true, true, false);
+ is(aElement.innerHTML, "a", aDescription + "Meta+Tab");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Meta+Tab)");
+
+ reset("a");
+ synthesizeKey("VK_TAB", { osKey: true });
+ check(aDescription + "OS+Tab", true, true, false);
+ is(aElement.innerHTML, "a", aDescription + "OS+Tab");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (OS+Tab)");
+
+ // Indent/Outdent tests:
+ // UL
+ resetForIndent("<ul><li id=\"target\">ul list item</li></ul>");
+ synthesizeKey("VK_TAB", { });
+ check(aDescription + "Tab on UL",
+ true, true, !aIsTabbable && !aIsReadonly);
+ is(aElement.innerHTML,
+ aIsReadonly || aIsTabbable ?
+ "<ul><li id=\"target\">ul list item</li></ul>" :
+ aIsPlaintext ? "<ul><li id=\"target\">ul list item\t</li></ul>" :
+ "<ul><ul><li id=\"target\">ul list item</li></ul></ul>",
+ aDescription + "Tab on UL");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Tab on UL)");
+ synthesizeKey("VK_TAB", { shiftKey: true });
+ check(aDescription + "Shift+Tab after Tab on UL",
+ true, true, !aIsTabbable && !aIsReadonly && !aIsPlaintext);
+ is(aElement.innerHTML,
+ aIsReadonly || aIsTabbable || (!aIsPlaintext) ?
+ "<ul><li id=\"target\">ul list item</li></ul>" :
+ "<ul><li id=\"target\">ul list item\t</li></ul>",
+ aDescription + "Shift+Tab after Tab on UL");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Shift+Tab after Tab on UL)");
+
+ resetForIndent("<ul><li id=\"target\">ul list item</li></ul>");
+ synthesizeKey("VK_TAB", { shiftKey: true });
+ check(aDescription + "Shift+Tab on UL",
+ true, true, !aIsTabbable && !aIsReadonly && !aIsPlaintext);
+ is(aElement.innerHTML,
+ aIsReadonly || aIsTabbable || aIsPlaintext ?
+ "<ul><li id=\"target\">ul list item</li></ul>" : "ul list item",
+ aDescription + "Shift+Tab on UL");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Shift+Tab on UL)");
+
+ // Ctrl+Tab should be consumed by tabbrowser at keydown, so, keypress
+ // event should never be fired.
+ resetForIndent("<ul><li id=\"target\">ul list item</li></ul>");
+ synthesizeKey("VK_TAB", { ctrlKey: true });
+ check(aDescription + "Ctrl+Tab on UL", false, false, false);
+ is(aElement.innerHTML, "<ul><li id=\"target\">ul list item</li></ul>",
+ aDescription + "Ctrl+Tab on UL");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Ctrl+Tab on UL)");
+
+ resetForIndent("<ul><li id=\"target\">ul list item</li></ul>");
+ synthesizeKey("VK_TAB", { altKey: true });
+ check(aDescription + "Alt+Tab on UL", true, true, false);
+ is(aElement.innerHTML, "<ul><li id=\"target\">ul list item</li></ul>",
+ aDescription + "Alt+Tab on UL");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Alt+Tab on UL)");
+
+ resetForIndent("<ul><li id=\"target\">ul list item</li></ul>");
+ synthesizeKey("VK_TAB", { metaKey: true });
+ check(aDescription + "Meta+Tab on UL", true, true, false);
+ is(aElement.innerHTML, "<ul><li id=\"target\">ul list item</li></ul>",
+ aDescription + "Meta+Tab on UL");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Meta+Tab on UL)");
+
+ resetForIndent("<ul><li id=\"target\">ul list item</li></ul>");
+ synthesizeKey("VK_TAB", { osKey: true });
+ check(aDescription + "OS+Tab on UL", true, true, false);
+ is(aElement.innerHTML, "<ul><li id=\"target\">ul list item</li></ul>",
+ aDescription + "OS+Tab on UL");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (OS+Tab on UL)");
+
+ // OL
+ resetForIndent("<ol><li id=\"target\">ol list item</li></ol>");
+ synthesizeKey("VK_TAB", { });
+ check(aDescription + "Tab on OL",
+ true, true, !aIsTabbable && !aIsReadonly);
+ is(aElement.innerHTML,
+ aIsReadonly || aIsTabbable ?
+ "<ol><li id=\"target\">ol list item</li></ol>" :
+ aIsPlaintext ? "<ol><li id=\"target\">ol list item\t</li></ol>" :
+ "<ol><ol><li id=\"target\">ol list item</li></ol></ol>",
+ aDescription + "Tab on OL");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Tab on OL)");
+ synthesizeKey("VK_TAB", { shiftKey: true });
+ check(aDescription + "Shift+Tab after Tab on OL",
+ true, true, !aIsTabbable && !aIsReadonly && !aIsPlaintext);
+ is(aElement.innerHTML,
+ aIsReadonly || aIsTabbable || (!aIsPlaintext) ?
+ "<ol><li id=\"target\">ol list item</li></ol>" :
+ "<ol><li id=\"target\">ol list item\t</li></ol>",
+ aDescription + "Shift+Tab after Tab on OL");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Shift+Tab after Tab on OL)");
+
+ resetForIndent("<ol><li id=\"target\">ol list item</li></ol>");
+ synthesizeKey("VK_TAB", { shiftKey: true });
+ check(aDescription + "Shift+Tab on OL",
+ true, true, !aIsTabbable && !aIsReadonly && !aIsPlaintext);
+ is(aElement.innerHTML,
+ aIsReadonly || aIsTabbable || aIsPlaintext ?
+ "<ol><li id=\"target\">ol list item</li></ol>" : "ol list item",
+ aDescription + "Shfit+Tab on OL");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Shift+Tab on OL)");
+
+ // Ctrl+Tab should be consumed by tabbrowser at keydown, so, keypress
+ // event should never be fired.
+ resetForIndent("<ol><li id=\"target\">ol list item</li></ol>");
+ synthesizeKey("VK_TAB", { ctrlKey: true });
+ check(aDescription + "Ctrl+Tab on OL", false, false, false);
+ is(aElement.innerHTML, "<ol><li id=\"target\">ol list item</li></ol>",
+ aDescription + "Ctrl+Tab on OL");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Ctrl+Tab on OL)");
+
+ resetForIndent("<ol><li id=\"target\">ol list item</li></ol>");
+ synthesizeKey("VK_TAB", { altKey: true });
+ check(aDescription + "Alt+Tab on OL", true, true, false);
+ is(aElement.innerHTML, "<ol><li id=\"target\">ol list item</li></ol>",
+ aDescription + "Alt+Tab on OL");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Alt+Tab on OL)");
+
+ resetForIndent("<ol><li id=\"target\">ol list item</li></ol>");
+ synthesizeKey("VK_TAB", { metaKey: true });
+ check(aDescription + "Meta+Tab on OL", true, true, false);
+ is(aElement.innerHTML, "<ol><li id=\"target\">ol list item</li></ol>",
+ aDescription + "Meta+Tab on OL");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Meta+Tab on OL)");
+
+ resetForIndent("<ol><li id=\"target\">ol list item</li></ol>");
+ synthesizeKey("VK_TAB", { osKey: true });
+ check(aDescription + "OS+Tab on OL", true, true, false);
+ is(aElement.innerHTML, "<ol><li id=\"target\">ol list item</li></ol>",
+ aDescription + "OS+Tab on OL");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (OS+Tab on OL)");
+
+ // TD
+ resetForIndent("<table><tr><td id=\"target\">td</td></tr></table>");
+ synthesizeKey("VK_TAB", { });
+ check(aDescription + "Tab on TD",
+ true, true, !aIsTabbable && !aIsReadonly);
+ is(aElement.innerHTML,
+ aIsTabbable || aIsReadonly ?
+ "<table><tbody><tr><td id=\"target\">td</td></tr></tbody></table>" :
+ aIsPlaintext ? "<table><tbody><tr><td id=\"target\">td\t</td></tr></tbody></table>" :
+ "<table><tbody><tr><td id=\"target\">td</td></tr><tr><td style=\"vertical-align: top;\"><br></td></tr></tbody></table>",
+ aDescription + "Tab on TD");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Tab on TD)");
+ synthesizeKey("VK_TAB", { shiftKey: true });
+ check(aDescription + "Shift+Tab after Tab on TD",
+ true, true, !aIsTabbable && !aIsReadonly && !aIsPlaintext);
+ is(aElement.innerHTML,
+ aIsTabbable || aIsReadonly ?
+ "<table><tbody><tr><td id=\"target\">td</td></tr></tbody></table>" :
+ aIsPlaintext ? "<table><tbody><tr><td id=\"target\">td\t</td></tr></tbody></table>" :
+ "<table><tbody><tr><td id=\"target\">td</td></tr><tr><td style=\"vertical-align: top;\"><br></td></tr></tbody></table>",
+ aDescription + "Shift+Tab after Tab on TD");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Shift+Tab after Tab on TD)");
+
+ resetForIndent("<table><tr><td id=\"target\">td</td></tr></table>");
+ synthesizeKey("VK_TAB", { shiftKey: true });
+ check(aDescription + "Shift+Tab on TD", true, true, false);
+ is(aElement.innerHTML,
+ "<table><tbody><tr><td id=\"target\">td</td></tr></tbody></table>",
+ aDescription + "Shift+Tab on TD");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Shift+Tab on TD)");
+
+ // Ctrl+Tab should be consumed by tabbrowser at keydown, so, keypress
+ // event should never be fired.
+ resetForIndent("<table><tr><td id=\"target\">td</td></tr></table>");
+ synthesizeKey("VK_TAB", { ctrlKey: true });
+ check(aDescription + "Ctrl+Tab on TD", false, false, false);
+ is(aElement.innerHTML,
+ "<table><tbody><tr><td id=\"target\">td</td></tr></tbody></table>",
+ aDescription + "Ctrl+Tab on TD");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Ctrl+Tab on TD)");
+
+ resetForIndent("<table><tr><td id=\"target\">td</td></tr></table>");
+ synthesizeKey("VK_TAB", { altKey: true });
+ check(aDescription + "Alt+Tab on TD", true, true, false);
+ is(aElement.innerHTML,
+ "<table><tbody><tr><td id=\"target\">td</td></tr></tbody></table>",
+ aDescription + "Alt+Tab on TD");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Alt+Tab on TD)");
+
+ resetForIndent("<table><tr><td id=\"target\">td</td></tr></table>");
+ synthesizeKey("VK_TAB", { metaKey: true });
+ check(aDescription + "Meta+Tab on TD", true, true, false);
+ is(aElement.innerHTML,
+ "<table><tbody><tr><td id=\"target\">td</td></tr></tbody></table>",
+ aDescription + "Meta+Tab on TD");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Meta+Tab on TD)");
+
+ resetForIndent("<table><tr><td id=\"target\">td</td></tr></table>");
+ synthesizeKey("VK_TAB", { osKey: true });
+ check(aDescription + "OS+Tab on TD", true, true, false);
+ is(aElement.innerHTML,
+ "<table><tbody><tr><td id=\"target\">td</td></tr></tbody></table>",
+ aDescription + "OS+Tab on TD");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (OS+Tab on TD)");
+
+ // TH
+ resetForIndent("<table><tr><th id=\"target\">th</th></tr></table>");
+ synthesizeKey("VK_TAB", { });
+ check(aDescription + "Tab on TH",
+ true, true, !aIsTabbable && !aIsReadonly);
+ is(aElement.innerHTML,
+ aIsTabbable || aIsReadonly ?
+ "<table><tbody><tr><th id=\"target\">th</th></tr></tbody></table>" :
+ aIsPlaintext ? "<table><tbody><tr><th id=\"target\">th\t</th></tr></tbody></table>" :
+ "<table><tbody><tr><th id=\"target\">th</th></tr><tr><td style=\"vertical-align: top;\"><br></td></tr></tbody></table>",
+ aDescription + "Tab on TH");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Tab on TH)");
+ synthesizeKey("VK_TAB", { shiftKey: true });
+ check(aDescription + "Shift+Tab after Tab on TH",
+ true, true, !aIsTabbable && !aIsReadonly && !aIsPlaintext);
+ is(aElement.innerHTML,
+ aIsTabbable || aIsReadonly ?
+ "<table><tbody><tr><th id=\"target\">th</th></tr></tbody></table>" :
+ aIsPlaintext ? "<table><tbody><tr><th id=\"target\">th\t</th></tr></tbody></table>" :
+ "<table><tbody><tr><th id=\"target\">th</th></tr><tr><td style=\"vertical-align: top;\"><br></td></tr></tbody></table>",
+ aDescription + "Shift+Tab after Tab on TH");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Shift+Tab after Tab on TH)");
+
+ resetForIndent("<table><tr><th id=\"target\">th</th></tr></table>");
+ synthesizeKey("VK_TAB", { shiftKey: true });
+ check(aDescription + "Shift+Tab on TH", true, true, false);
+ is(aElement.innerHTML,
+ "<table><tbody><tr><th id=\"target\">th</th></tr></tbody></table>",
+ aDescription + "Shift+Tab on TH");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Shift+Tab on TH)");
+
+ // Ctrl+Tab should be consumed by tabbrowser at keydown, so, keypress
+ // event should never be fired.
+ resetForIndent("<table><tr><th id=\"target\">th</th></tr></table>");
+ synthesizeKey("VK_TAB", { ctrlKey: true });
+ check(aDescription + "Ctrl+Tab on TH", false, false, false);
+ is(aElement.innerHTML,
+ "<table><tbody><tr><th id=\"target\">th</th></tr></tbody></table>",
+ aDescription + "Ctrl+Tab on TH");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Ctrl+Tab on TH)");
+
+ resetForIndent("<table><tr><th id=\"target\">th</th></tr></table>");
+ synthesizeKey("VK_TAB", { altKey: true });
+ check(aDescription + "Alt+Tab on TH", true, true, false);
+ is(aElement.innerHTML,
+ "<table><tbody><tr><th id=\"target\">th</th></tr></tbody></table>",
+ aDescription + "Alt+Tab on TH");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Alt+Tab on TH)");
+
+ resetForIndent("<table><tr><th id=\"target\">th</th></tr></table>");
+ synthesizeKey("VK_TAB", { metaKey: true });
+ check(aDescription + "Meta+Tab on TH", true, true, false);
+ is(aElement.innerHTML,
+ "<table><tbody><tr><th id=\"target\">th</th></tr></tbody></table>",
+ aDescription + "Meta+Tab on TH");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Meta+Tab on TH)");
+
+ resetForIndent("<table><tr><th id=\"target\">th</th></tr></table>");
+ synthesizeKey("VK_TAB", { osKey: true });
+ check(aDescription + "OS+Tab on TH", true, true, false);
+ is(aElement.innerHTML,
+ "<table><tbody><tr><th id=\"target\">th</th></tr></tbody></table>",
+ aDescription + "OS+Tab on TH");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (OS+Tab on TH)");
+
+ // Esc key:
+ // In all cases, esc key events are not consumed
+ reset("abc");
+ synthesizeKey("VK_ESCAPE", { });
+ check(aDescription + "Esc", true, true, false);
+
+ reset("abc");
+ synthesizeKey("VK_ESCAPE", { shiftKey: true });
+ check(aDescription + "Shift+Esc", true, true, false);
+
+ reset("abc");
+ synthesizeKey("VK_ESCAPE", { ctrlKey: true });
+ check(aDescription + "Ctrl+Esc", true, true, false);
+
+ reset("abc");
+ synthesizeKey("VK_ESCAPE", { altKey: true });
+ check(aDescription + "Alt+Esc", true, true, false);
+
+ reset("abc");
+ synthesizeKey("VK_ESCAPE", { metaKey: true });
+ check(aDescription + "Meta+Esc", true, true, false);
+
+ reset("abc");
+ synthesizeKey("VK_ESCAPE", { osKey: true });
+ check(aDescription + "OS+Esc", true, true, false);
+
+ // typical typing tests:
+ reset("");
+ synthesizeKey("M", { shiftKey: true });
+ check(aDescription + "M", true, true, !aIsReadonly);
+ synthesizeKey("o", { });
+ check(aDescription + "o", true, true, !aIsReadonly);
+ synthesizeKey("z", { });
+ check(aDescription + "z", true, true, !aIsReadonly);
+ synthesizeKey("i", { });
+ check(aDescription + "i", true, true, !aIsReadonly);
+ synthesizeKey("l", { });
+ check(aDescription + "l", true, true, !aIsReadonly);
+ synthesizeKey("l", { });
+ check(aDescription + "l", true, true, !aIsReadonly);
+ synthesizeKey("a", { });
+ check(aDescription + "a", true, true, !aIsReadonly);
+ synthesizeKey(" ", { });
+ check(aDescription + "' '", true, true, !aIsReadonly);
+ is(aElement.innerHTML,
+ aIsReadonly ? "" : aIsPlaintext ? "Mozilla " : "Mozilla <br>",
+ aDescription + "typed \"Mozilla \"");
+ }
+
+ doTest(htmlEditor, "contenteditable=\"true\"", false, true, false);
+
+ const nsIPlaintextEditor = SpecialPowers.Ci.nsIPlaintextEditor;
+ var editor = SpecialPowers.wrap(window).
+ QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor).
+ getInterface(SpecialPowers.Ci.nsIWebNavigation).
+ QueryInterface(SpecialPowers.Ci.nsIDocShell).editor;
+ var flags = editor.flags;
+ // readonly
+ editor.flags = flags | nsIPlaintextEditor.eEditorReadonlyMask;
+ doTest(htmlEditor, "readonly HTML editor", true, true, false);
+
+ // non-tabbable
+ editor.flags = flags & ~(nsIPlaintextEditor.eEditorAllowInteraction);
+ doTest(htmlEditor, "non-tabbable HTML editor", false, false, false);
+
+ // readonly and non-tabbable
+ editor.flags =
+ (flags | nsIPlaintextEditor.eEditorReadonlyMask) &
+ ~(nsIPlaintextEditor.eEditorAllowInteraction);
+ doTest(htmlEditor, "readonly and non-tabbable HTML editor",
+ true, false, false);
+
+ // plaintext
+ editor.flags = flags | nsIPlaintextEditor.eEditorPlaintextMask;
+ doTest(htmlEditor, "HTML editor but plaintext mode", false, true, true);
+
+ // plaintext and non-tabbable
+ editor.flags = (flags | nsIPlaintextEditor.eEditorPlaintextMask) &
+ ~(nsIPlaintextEditor.eEditorAllowInteraction);
+ doTest(htmlEditor, "non-tabbable HTML editor but plaintext mode",
+ false, false, true);
+
+
+ // readonly and plaintext
+ editor.flags = flags | nsIPlaintextEditor.eEditorPlaintextMask |
+ nsIPlaintextEditor.eEditorReadonlyMask;
+ doTest(htmlEditor, "readonly HTML editor but plaintext mode",
+ true, true, true);
+
+ // readonly, plaintext and non-tabbable
+ editor.flags = (flags | nsIPlaintextEditor.eEditorPlaintextMask |
+ nsIPlaintextEditor.eEditorReadonlyMask) &
+ ~(nsIPlaintextEditor.eEditorAllowInteraction);
+ doTest(htmlEditor, "readonly and non-tabbable HTML editor but plaintext mode",
+ true, false, true);
+
+ SpecialPowers.removeSystemEventListener(window, "keypress", listener, true);
+ SpecialPowers.removeSystemEventListener(window, "keypress", listener, false);
+
+ SimpleTest.finish();
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_keypress_untrusted_event.html b/editor/libeditor/tests/test_keypress_untrusted_event.html
new file mode 100644
index 000000000..6875c5a33
--- /dev/null
+++ b/editor/libeditor/tests/test_keypress_untrusted_event.html
@@ -0,0 +1,99 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=622245
+-->
+<head>
+ <title>Test for untrusted keypress events</title>
+ <script type="application/javascript" src="/MochiKit/packed.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=622245">Mozilla Bug 622245</a>
+<p id="display"></p>
+<div id="content">
+<input id="i"><br>
+<textarea id="t"></textarea><br>
+<div id="d" contenteditable style="min-height: 1em;"></div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 674770 **/
+SimpleTest.waitForExplicitFinish();
+
+var input = document.getElementById("i");
+var textarea = document.getElementById("t");
+var div = document.getElementById("d");
+
+addLoadEvent(function() {
+ input.focus();
+
+ SimpleTest.executeSoon(function() {
+ input.addEventListener("keypress",
+ function(aEvent) {
+ input.removeEventListener("keypress", arguments.callee, false);
+ is(aEvent.target, input,
+ "The keypress event target isn't the input element");
+
+ SimpleTest.executeSoon(function() {
+ is(input.value, "",
+ "Did keypress event cause modifying the input element?");
+ textarea.focus();
+ SimpleTest.executeSoon(runTextareaTest);
+ });
+ }, false);
+ var keypress = document.createEvent("KeyboardEvent");
+ keypress.initKeyEvent("keypress", true, true, document.defaultView,
+ false, false, false, false, 0, "a".charCodeAt(0));
+ input.dispatchEvent(keypress);
+ });
+});
+
+function runTextareaTest()
+{
+ textarea.addEventListener("keypress",
+ function(aEvent) {
+ textarea.removeEventListener("keypress", arguments.callee, false);
+ is(aEvent.target, textarea,
+ "The keypress event target isn't the textarea element");
+
+ SimpleTest.executeSoon(function() {
+ is(textarea.value, "",
+ "Did keypress event cause modifying the textarea element?");
+ div.focus();
+ SimpleTest.executeSoon(runContentediableTest);
+ });
+ }, false);
+ var keypress = document.createEvent("KeyboardEvent");
+ keypress.initKeyEvent("keypress", true, true, document.defaultView,
+ false, false, false, false, 0, "b".charCodeAt(0));
+ textarea.dispatchEvent(keypress);
+}
+
+function runContentediableTest()
+{
+ div.addEventListener("keypress",
+ function(aEvent) {
+ div.removeEventListener("keypress", arguments.callee, false);
+ is(aEvent.target, div,
+ "The keypress event target isn't the div element");
+
+ SimpleTest.executeSoon(function() {
+ is(div.innerHTML, "",
+ "Did keypress event cause modifying the div element?");
+
+ SimpleTest.finish();
+ });
+ }, false);
+ var keypress = document.createEvent("KeyboardEvent");
+ keypress.initKeyEvent("keypress", true, true, document.defaultView,
+ false, false, false, false, 0, "c".charCodeAt(0));
+ div.dispatchEvent(keypress);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_root_element_replacement.html b/editor/libeditor/tests/test_root_element_replacement.html
new file mode 100644
index 000000000..f8b6f4336
--- /dev/null
+++ b/editor/libeditor/tests/test_root_element_replacement.html
@@ -0,0 +1,148 @@
+<html>
+<head>
+ <title>Test for root element replacement</title>
+ <script type="text/javascript"
+ src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript"
+ src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display">
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTest);
+
+function runDesignModeTest(aDoc, aFocus, aNewSource)
+{
+ aDoc.designMode = "on";
+
+ if (aFocus) {
+ aDoc.documentElement.focus();
+ }
+
+ aDoc.open();
+ aDoc.write(aNewSource);
+ aDoc.close();
+ aDoc.documentElement.focus();
+}
+
+function runContentEditableTest(aDoc, aFocus, aNewSource)
+{
+ if (aFocus) {
+ aDoc.body.setAttribute("contenteditable", "true");
+ aDoc.body.focus();
+ }
+
+ aDoc.open();
+ aDoc.write(aNewSource);
+ aDoc.close();
+ aDoc.getElementById("focus").focus();
+}
+
+var gTestIndex = 0;
+
+const kTests = [
+ { description: "Replace to '<body></body>', designMode",
+ initializer: runDesignModeTest,
+ args: [ "<body></body>" ] },
+ { description: "Replace to '<html><body></body></html>', designMode",
+ initializer: runDesignModeTest,
+ args: [ "<html><body></body></html>" ] },
+ { description: "Replace to '<html>&nbsp;<body></body></html>', designMode",
+ initializer: runDesignModeTest,
+ args: [ "<html> <body></body></html>" ] },
+ { description: "Replace to '&nbsp;<html>&nbsp;<body></body></html>', designMode",
+ initializer: runDesignModeTest,
+ args: [ " <html> <body></body></html>" ] },
+
+ { description: "Replace to '<html contenteditable='true'><body></body></html>",
+ initializer: runContentEditableTest,
+ args: [ "<html contenteditable='true' id='focus'><body></body></html>" ] },
+ { description: "Replace to '<html><body contenteditable='true'></body></html>",
+ initializer: runContentEditableTest,
+ args: [ "<html><body contenteditable='true' id='focus'></body></html>" ] },
+ { description: "Replace to '<body contenteditable='true'></body>",
+ initializer: runContentEditableTest,
+ args: [ "<body contenteditable='true' id='focus'></body>" ] },
+];
+
+var gIFrame;
+var gSetFocusToIFrame = false;
+
+function onLoadIFrame()
+{
+ var frameDoc = gIFrame.contentWindow.document;
+
+ var selCon = SpecialPowers.wrap(gIFrame).contentWindow.
+ QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor).
+ getInterface(SpecialPowers.Ci.nsIWebNavigation).
+ QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor).
+ getInterface(SpecialPowers.Ci.nsISelectionDisplay).
+ QueryInterface(SpecialPowers.Ci.nsISelectionController);
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ const nsIDOMNode = SpecialPowers.Ci.nsIDOMNode;
+
+ // move focus to the HTML editor
+ const kTest = kTests[gTestIndex];
+ ok(true, "Running " + kTest.description);
+ if (kTest.args.length == 1) {
+ kTest.initializer(frameDoc, gSetFocusToIFrame, kTest.args[0]);
+ ok(selCon.caretVisible, "caret isn't visible -- " + kTest.description);
+ } else {
+ ok(false, "kTests is broken at index=" + gTestIndex);
+ }
+
+ is(utils.IMEStatus, utils.IME_STATUS_ENABLED,
+ "IME isn't enabled -- " + kTest.description);
+ synthesizeKey("A", { }, gIFrame.contentWindow);
+ synthesizeKey("B", { }, gIFrame.contentWindow);
+ synthesizeKey("C", { }, gIFrame.contentWindow);
+ var content = frameDoc.body.firstChild;
+ ok(content, "body doesn't have contents -- " + kTest.description);
+ if (content) {
+ is(content.nodeType, nsIDOMNode.TEXT_NODE,
+ "the content of body isn't text node -- " + kTest.description);
+ if (content.nodeType == nsIDOMNode.TEXT_NODE) {
+ is(content.data, "ABC",
+ "the content of body text isn't 'ABC' -- " + kTest.description);
+ is(frameDoc.body.innerHTML, "ABC",
+ "the innerHTML of body isn't 'ABC' -- " + kTest.description);
+ }
+ }
+
+ document.getElementById("display").removeChild(gIFrame);
+
+ // Do next test or finish the tests.
+ if (++gTestIndex < kTests.length) {
+ setTimeout(runTest, 0);
+ } else if (!gSetFocusToIFrame) {
+ gSetFocusToIFrame = true;
+ gTestIndex = 0;
+ setTimeout(runTest, 0);
+ } else {
+ SimpleTest.finish();
+ }
+}
+
+function runTest()
+{
+ gIFrame = document.createElement("iframe");
+ document.getElementById("display").appendChild(gIFrame);
+ gIFrame.src = "about:blank";
+ gIFrame.onload = onLoadIFrame;
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_select_all_without_body.html b/editor/libeditor/tests/test_select_all_without_body.html
new file mode 100644
index 000000000..d947400c4
--- /dev/null
+++ b/editor/libeditor/tests/test_select_all_without_body.html
@@ -0,0 +1,27 @@
+<html>
+<head>
+ <title>Test select all in HTML editor without body element</title>
+ <script type="text/javascript"
+ src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display">
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+window.open("file_select_all_without_body.html", "_blank",
+ "width=600,height=600");
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_selection_move_commands.html b/editor/libeditor/tests/test_selection_move_commands.html
new file mode 100644
index 000000000..e217f8fdf
--- /dev/null
+++ b/editor/libeditor/tests/test_selection_move_commands.html
@@ -0,0 +1,219 @@
+<!doctype html>
+<title>Test for nsSelectionMoveCommands</title>
+<link rel=stylesheet href="/tests/SimpleTest/test.css">
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/SpawnTask.js"></script>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=454004">Mozilla Bug 454004</a>
+
+<iframe id="edit" width="200" height="100" src="about:blank"></iframe>
+
+<script>
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("Legacy test, possibly no good reason");
+
+var winUtils = SpecialPowers.getDOMWindowUtils(window);
+
+function* setup() {
+ yield SpecialPowers.pushPrefEnv({set: [["general.smoothScroll", false]]});
+ winUtils.advanceTimeAndRefresh(100);
+}
+
+function* runTests() {
+ var e = document.getElementById("edit");
+ var doc = e.contentDocument;
+ var win = e.contentWindow;
+ var root = doc.documentElement;
+ var body = doc.body;
+
+ body.style.fontSize='16px';
+ body.style.lineHeight='16px';
+ body.style.height='400px';
+ body.style.padding='0px';
+ body.style.margin='0px';
+ body.style.borderWidth='0px';
+
+ var sel = win.getSelection();
+ doc.designMode='on';
+ body.innerHTML = "1<br>2<br>3<br>4<br>5<br>6<br>7<br>8<br>9<br>10<br>11<br>12<br>";
+ win.focus();
+ // Flush out layout to make sure that the subdocument will be the size we
+ // expect by the time we try to scroll it.
+ is(body.getBoundingClientRect().height, 400,
+ "Body height should be what we set it to");
+ yield;
+
+ function testScrollCommand(cmd, expectTop) {
+ // http://dev.w3.org/csswg/cssom-view/#dom-element-getboundingclientrect
+ // doesn't explicitly rule out -0 here, but for now assume that only
+ // positive zeroes are permitted.
+ if (navigator.appVersion.indexOf("Android") != -1 && expectTop != 0) {
+ // Android doesn't get the values exactly correct for some reason
+ todo_is(root.getBoundingClientRect().top, -expectTop + 0, cmd);
+ ok(Math.abs(root.getBoundingClientRect().top + expectTop) < 0.2,
+ cmd + " (approximately)");
+ } else {
+ is(root.getBoundingClientRect().top, -expectTop + 0, cmd);
+ }
+ }
+
+ function testMoveCommand(cmd, expectNode, expectOffset) {
+ SpecialPowers.doCommand(window, cmd);
+ is(sel.isCollapsed, true, "collapsed after " + cmd);
+ is(sel.anchorNode, expectNode, "node after " + cmd);
+ is(sel.anchorOffset, expectOffset, "offset after " + cmd);
+ }
+
+ function findChildNum(e, child) {
+ var i = 0;
+ var n = e.firstChild;
+ while (n && n != child) {
+ n = n.nextSibling;
+ ++i;
+ }
+ if (!n)
+ return -1;
+ return i;
+ }
+
+ function testPageMoveCommand(cmd, expectOffset) {
+ SpecialPowers.doCommand(window, cmd);
+ is(sel.isCollapsed, true, "collapsed after " + cmd);
+ is(sel.anchorOffset, expectOffset, "offset after " + cmd);
+ return findChildNum(body, sel.anchorNode);
+ }
+
+ function testSelectCommand(cmd, expectNode, expectOffset) {
+ var anchorNode = sel.anchorNode;
+ var anchorOffset = sel.anchorOffset;
+ SpecialPowers.doCommand(window, cmd);
+ is(sel.isCollapsed, false, "not collapsed after " + cmd);
+ is(sel.anchorNode, anchorNode, "anchor not moved after " + cmd);
+ is(sel.anchorOffset, anchorOffset, "anchor not moved after " + cmd);
+ is(sel.focusNode, expectNode, "node after " + cmd);
+ is(sel.focusOffset, expectOffset, "offset after " + cmd);
+ }
+
+ function testPageSelectCommand(cmd, expectOffset) {
+ var anchorNode = sel.anchorNode;
+ var anchorOffset = sel.anchorOffset;
+ SpecialPowers.doCommand(window, cmd);
+ is(sel.isCollapsed, false, "not collapsed after " + cmd);
+ is(sel.anchorNode, anchorNode, "anchor not moved after " + cmd);
+ is(sel.anchorOffset, anchorOffset, "anchor not moved after " + cmd);
+ is(sel.focusOffset, expectOffset, "offset after " + cmd);
+ return findChildNum(body, sel.focusNode);
+ }
+
+ function node(i) {
+ var n = body.firstChild;
+ while (i > 0) {
+ n = n.nextSibling;
+ --i;
+ }
+ return n;
+ }
+
+ SpecialPowers.doCommand(window, "cmd_scrollBottom");
+ yield;
+ testScrollCommand("cmd_scrollBottom", root.scrollHeight - 100);
+ SpecialPowers.doCommand(window, "cmd_scrollTop");
+ yield;
+ testScrollCommand("cmd_scrollTop", 0);
+
+ SpecialPowers.doCommand(window, "cmd_scrollPageDown");
+ yield;
+ var pageHeight = -root.getBoundingClientRect().top;
+ ok(pageHeight > 0, "cmd_scrollPageDown works");
+ ok(pageHeight <= 100, "cmd_scrollPageDown doesn't scroll too much");
+ SpecialPowers.doCommand(window, "cmd_scrollBottom");
+ SpecialPowers.doCommand(window, "cmd_scrollPageUp");
+ yield;
+ testScrollCommand("cmd_scrollPageUp", root.scrollHeight - 100 - pageHeight);
+
+ SpecialPowers.doCommand(window, "cmd_scrollTop");
+ SpecialPowers.doCommand(window, "cmd_scrollLineDown");
+ yield;
+ var lineHeight = -root.getBoundingClientRect().top;
+ ok(lineHeight > 0, "Can scroll by lines");
+ SpecialPowers.doCommand(window, "cmd_scrollBottom");
+ SpecialPowers.doCommand(window, "cmd_scrollLineUp");
+ yield;
+ testScrollCommand("cmd_scrollLineUp", root.scrollHeight - 100 - lineHeight);
+
+ var runSelectionTests = function(selectWordNextNode, selectWordNextOffset) {
+ testMoveCommand("cmd_moveBottom", body, 23);
+ testMoveCommand("cmd_moveTop", node(0), 0);
+ testSelectCommand("cmd_selectBottom", body, 23);
+ SpecialPowers.doCommand(window, "cmd_moveBottom");
+ testSelectCommand("cmd_selectTop", node(0), 0);
+
+ SpecialPowers.doCommand(window, "cmd_moveTop");
+ testMoveCommand("cmd_lineNext", node(2), 0);
+ testMoveCommand("cmd_linePrevious", node(0), 0);
+ testSelectCommand("cmd_selectLineNext", node(2), 0);
+ SpecialPowers.doCommand(window, "cmd_moveBottom");
+ testSelectCommand("cmd_selectLinePrevious", node(20), 2);
+
+ SpecialPowers.doCommand(window, "cmd_moveBottom");
+ testMoveCommand("cmd_charPrevious", node(22), 1);
+ testMoveCommand("cmd_charNext", node(22), 2);
+ testSelectCommand("cmd_selectCharPrevious", node(22), 1);
+ SpecialPowers.doCommand(window, "cmd_moveTop");
+ testSelectCommand("cmd_selectCharNext", node(0), 1);
+
+ SpecialPowers.doCommand(window, "cmd_moveTop");
+ testMoveCommand("cmd_endLine", node(0), 1);
+ testMoveCommand("cmd_beginLine", node(0), 0);
+ testSelectCommand("cmd_selectEndLine", node(0), 1);
+ SpecialPowers.doCommand(window, "cmd_moveBottom");
+ testSelectCommand("cmd_selectBeginLine", node(22), 0);
+
+ SpecialPowers.doCommand(window, "cmd_moveBottom");
+ testMoveCommand("cmd_wordPrevious", node(22), 0);
+ testMoveCommand("cmd_wordNext", body, 23);
+ testSelectCommand("cmd_selectWordPrevious", node(22), 0);
+ SpecialPowers.doCommand(window, "cmd_moveTop");
+ testSelectCommand("cmd_selectWordNext", selectWordNextNode, selectWordNextOffset);
+
+ SpecialPowers.doCommand(window, "cmd_moveTop");
+ var lineNum = testPageMoveCommand("cmd_movePageDown", 0);
+ ok(lineNum > 0, "cmd_movePageDown works");
+ SpecialPowers.doCommand(window, "cmd_moveBottom");
+ SpecialPowers.doCommand(window, "cmd_beginLine");
+ is(testPageMoveCommand("cmd_movePageUp", 0), 22 - lineNum, "cmd_movePageUp");
+
+ SpecialPowers.doCommand(window, "cmd_moveTop");
+ is(testPageSelectCommand("cmd_selectPageDown", 0), lineNum, "cmd_selectPageDown");
+ SpecialPowers.doCommand(window, "cmd_moveBottom");
+ SpecialPowers.doCommand(window, "cmd_beginLine");
+ is(testPageSelectCommand("cmd_selectPageUp", 0), 22 - lineNum, "cmd_selectPageUp");
+ }
+
+ yield SpecialPowers.pushPrefEnv({set: [["layout.word_select.eat_space_to_next_word", false]]});
+ runSelectionTests(body, 1);
+ yield SpecialPowers.pushPrefEnv({set: [["layout.word_select.eat_space_to_next_word", true]]});
+ runSelectionTests(node(2), 0);
+}
+
+function cleanup() {
+ winUtils.restoreNormalRefresh();
+ SimpleTest.finish();
+}
+
+function* testRunner() {
+ let curTest = runTests();
+ while (true) {
+ winUtils.advanceTimeAndRefresh(100);
+ if (curTest.next().done) {
+ break;
+ }
+ winUtils.advanceTimeAndRefresh(100);
+ yield new Promise(resolve => setTimeout(resolve, 20));
+ }
+}
+
+spawn_task(setup)
+ .then(() => spawn_task(testRunner))
+ .then(() => spawn_task(cleanup))
+ .catch(err => ok(false, err));
+</script>
diff --git a/editor/libeditor/tests/test_set_document_title_transaction.html b/editor/libeditor/tests/test_set_document_title_transaction.html
new file mode 100644
index 000000000..d745d4f13
--- /dev/null
+++ b/editor/libeditor/tests/test_set_document_title_transaction.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for SetDocumentTitleTransaction</title>
+ <script type="text/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body onload="runTests()">
+<div id="display">
+ <iframe src="data:text/html,<!DOCTYPE html><html><head><title>first title</title></head><body></body></html>"></iframe>
+</div>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+function runTests() {
+ var iframe = document.getElementsByTagName("iframe")[0];
+ function isDocumentTitleEquals(aDescription, aExpectedTitle) {
+ is(iframe.contentDocument.title, aExpectedTitle, aDescription + ": document.title should be " + aExpectedTitle);
+ is(iframe.contentDocument.getElementsByTagName("title")[0].textContent, aExpectedTitle, aDescription + ": The text in the title element should be " + aExpectedTitle);
+ }
+
+ isDocumentTitleEquals("Checking isDocumentTitleEquals()", "first title");
+
+ const kTests = [
+ { description: "designMode=\"on\"",
+ init: function () {
+ iframe.contentDocument.designMode = "on";
+ },
+ cleanUp: function () {
+ iframe.contentDocument.designMode = "off";
+ }
+ },
+ { description: "html element has contenteditable attribute",
+ init: function () {
+ iframe.contentDocument.documentElement.setAttribute("contenteditable", "true");
+ },
+ cleanUp: function () {
+ iframe.contentDocument.documentElement.removeAttribute("contenteditable");
+ }
+ },
+ ];
+
+ for (var i = 0; i < kTests.length; i++) {
+ const kTest = kTests[i];
+ kTest.init();
+
+ var editor = SpecialPowers.wrap(iframe.contentWindow).
+ QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor).
+ getInterface(SpecialPowers.Ci.nsIWebNavigation).
+ QueryInterface(SpecialPowers.Ci.nsIDocShell).editor;
+ ok(editor, kTest.description + ": The docshell should have editor");
+ var htmlEditor = editor.QueryInterface(SpecialPowers.Ci.nsIHTMLEditor);
+ ok(htmlEditor, kTest.description + ": The editor should have nsIHTMLEditor interface");
+
+ // Replace existing title.
+ htmlEditor.setDocumentTitle("Modified title");
+ isDocumentTitleEquals(kTest.description, "Modified title");
+
+ // When the document doesn't have <title> element, title element should be created automatically.
+ iframe.contentDocument.head.removeChild(iframe.contentDocument.getElementsByTagName("title")[0]);
+ is(iframe.contentDocument.getElementsByTagName("title").length, 0, kTest.description + ": There should be no title element");
+ htmlEditor.setDocumentTitle("new title");
+ is(iframe.contentDocument.getElementsByTagName("title").length, 1, kTest.description + ": There should be a title element");
+ isDocumentTitleEquals(kTest.description, "new title");
+
+ kTest.cleanUp();
+ }
+
+ SimpleTest.finish();
+}
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_spellcheck_pref.html b/editor/libeditor/tests/test_spellcheck_pref.html
new file mode 100644
index 000000000..9faff45f3
--- /dev/null
+++ b/editor/libeditor/tests/test_spellcheck_pref.html
@@ -0,0 +1,23 @@
+<html>
+<head>
+ <title>Test if spellcheck is turned on</title>
+ <script type="text/javascript"
+ src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+ is(SpecialPowers.getIntPref("layout.spellcheckDefault"), 1, "Check if the layout.spellcheckDefault pref is turned on");
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_texteditor_keyevent_handling.html b/editor/libeditor/tests/test_texteditor_keyevent_handling.html
new file mode 100644
index 000000000..5c4a8d1c2
--- /dev/null
+++ b/editor/libeditor/tests/test_texteditor_keyevent_handling.html
@@ -0,0 +1,386 @@
+<html>
+<head>
+ <title>Test for key event handler of text editor</title>
+ <script type="text/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="display">
+ <input type="text" id="inputField">
+ <input type="password" id="passwordField">
+ <textarea id="textarea"></textarea>
+</div>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests, window);
+
+var inputField = document.getElementById("inputField");
+var passwordField = document.getElementById("passwordField");
+var textarea = document.getElementById("textarea");
+
+const kIsMac = navigator.platform.indexOf("Mac") == 0;
+const kIsWin = navigator.platform.indexOf("Win") == 0;
+const kIsLinux = navigator.platform.indexOf("Linux") == 0;
+
+function runTests()
+{
+ var fm = SpecialPowers.Cc["@mozilla.org/focus-manager;1"].
+ getService(SpecialPowers.Ci.nsIFocusManager);
+
+ var capturingPhase = { fired: false, prevented: false };
+ var bubblingPhase = { fired: false, prevented: false };
+
+ var listener = {
+ handleEvent: function _hv(aEvent)
+ {
+ is(aEvent.type, "keypress", "unexpected event is handled");
+ switch (aEvent.eventPhase) {
+ case aEvent.CAPTURING_PHASE:
+ capturingPhase.fired = true;
+ capturingPhase.prevented = aEvent.defaultPrevented;
+ break;
+ case aEvent.BUBBLING_PHASE:
+ bubblingPhase.fired = true;
+ bubblingPhase.prevented = aEvent.defaultPrevented;
+ aEvent.preventDefault(); // prevent the browser default behavior
+ break;
+ default:
+ ok(false, "event is handled in unexpected phase");
+ }
+ }
+ };
+
+ function check(aDescription,
+ aFiredOnCapture, aFiredOnBubbling, aPreventedOnBubbling)
+ {
+ function getDesciption(aExpected)
+ {
+ return aDescription + (aExpected ? " wasn't " : " was ");
+ }
+
+ is(capturingPhase.fired, aFiredOnCapture,
+ getDesciption(aFiredOnCapture) + "fired on capture phase");
+ is(bubblingPhase.fired, aFiredOnBubbling,
+ getDesciption(aFiredOnBubbling) + "fired on bubbling phase");
+
+ // If the event is fired on bubbling phase and it was already prevented
+ // on capture phase, it must be prevented on bubbling phase too.
+ if (capturingPhase.prevented) {
+ todo(false, aDescription +
+ " was consumed already, so, we cannot test the editor behavior actually");
+ aPreventedOnBubbling = true;
+ }
+
+ is(bubblingPhase.prevented, aPreventedOnBubbling,
+ getDesciption(aPreventedOnBubbling) + "prevented on bubbling phase");
+ }
+
+ var parentElement = document.getElementById("display");
+ SpecialPowers.addSystemEventListener(parentElement, "keypress", listener,
+ true);
+ SpecialPowers.addSystemEventListener(parentElement, "keypress", listener,
+ false);
+
+ function doTest(aElement, aDescription, aIsSingleLine, aIsReadonly,
+ aIsTabbable)
+ {
+ function reset(aText)
+ {
+ capturingPhase.fired = false;
+ capturingPhase.prevented = false;
+ bubblingPhase.fired = false;
+ bubblingPhase.prevented = false;
+ aElement.value = aText;
+ }
+
+ if (document.activeElement) {
+ document.activeElement.blur();
+ }
+
+ aDescription += ": "
+
+ aElement.focus();
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement, aDescription + "failed to move focus");
+
+ // Backspace key:
+ // If editor is readonly, it doesn't consume.
+ // If editor is editable, it consumes backspace and shift+backspace.
+ // Otherwise, editor doesn't consume the event but the native key
+ // bindings on nsTextControlFrame may consume it.
+ reset("");
+ synthesizeKey("VK_BACK_SPACE", { });
+ check(aDescription + "Backspace", true, true, true);
+
+ reset("");
+ synthesizeKey("VK_BACK_SPACE", { shiftKey: true });
+ check(aDescription + "Shift+Backspace", true, true, true);
+
+ reset("");
+ synthesizeKey("VK_BACK_SPACE", { ctrlKey: true });
+ // Win: cmd_deleteWordBackward
+ check(aDescription + "Ctrl+Backspace",
+ true, true, aIsReadonly || kIsWin);
+
+ reset("");
+ synthesizeKey("VK_BACK_SPACE", { altKey: true });
+ // Win: cmd_undo
+ // Mac: cmd_deleteWordBackward
+ check(aDescription + "Alt+Backspace",
+ true, true, aIsReadonly || kIsWin || kIsMac);
+
+ reset("");
+ synthesizeKey("VK_BACK_SPACE", { metaKey: true });
+ check(aDescription + "Meta+Backspace", true, true, aIsReadonly);
+
+ reset("");
+ synthesizeKey("VK_BACK_SPACE", { osKey: true });
+ check(aDescription + "OS+Backspace", true, true, aIsReadonly);
+
+ // Delete key:
+ // If editor is readonly, it doesn't consume.
+ // If editor is editable, delete is consumed.
+ // Otherwise, editor doesn't consume the event but the native key
+ // bindings on nsTextControlFrame may consume it.
+ reset("");
+ synthesizeKey("VK_DELETE", { });
+ // Linux: native handler
+ // Mac: cmd_deleteCharForward
+ check(aDescription + "Delete",
+ true, true, !aIsReadonly || kIsLinux || kIsMac);
+
+ reset("");
+ // Win: cmd_cutOrDelete
+ // Linux: cmd_cut
+ // Mac: cmd_deleteCharForward
+ synthesizeKey("VK_DELETE", { shiftKey: true });
+ check(aDescription + "Shift+Delete",
+ true, true, true);
+
+ reset("");
+ synthesizeKey("VK_DELETE", { ctrlKey: true });
+ // Win: cmd_deleteWordForward
+ // Linux: cmd_copy
+ check(aDescription + "Ctrl+Delete",
+ true, true, kIsWin || kIsLinux);
+
+ reset("");
+ synthesizeKey("VK_DELETE", { altKey: true });
+ // Mac: cmd_deleteWordForward
+ check(aDescription + "Alt+Delete",
+ true, true, kIsMac);
+
+ reset("");
+ synthesizeKey("VK_DELETE", { metaKey: true });
+ // Linux: native handler consumed.
+ check(aDescription + "Meta+Delete",
+ true, true, kIsLinux);
+
+ reset("");
+ synthesizeKey("VK_DELETE", { osKey: true });
+ check(aDescription + "OS+Delete",
+ true, true, false);
+
+ // XXX input.value returns "\n" when it's empty, so, we should use dummy
+ // value ("a") for the following tests.
+
+ // Return key:
+ // If editor is readonly, it doesn't consume.
+ // If editor is editable and not single line editor, it consumes Return
+ // and Shift+Return.
+ // Otherwise, editor doesn't consume the event.
+ reset("a");
+ synthesizeKey("VK_RETURN", { });
+ check(aDescription + "Return",
+ true, true, !aIsSingleLine && !aIsReadonly);
+ is(aElement.value, !aIsSingleLine && !aIsReadonly ? "a\n" : "a",
+ aDescription + "Return");
+
+ reset("a");
+ synthesizeKey("VK_RETURN", { shiftKey: true });
+ check(aDescription + "Shift+Return",
+ true, true, !aIsSingleLine && !aIsReadonly);
+ is(aElement.value, !aIsSingleLine && !aIsReadonly ? "a\n" : "a",
+ aDescription + "Shift+Return");
+
+ reset("a");
+ synthesizeKey("VK_RETURN", { ctrlKey: true });
+ check(aDescription + "Ctrl+Return", true, true, false);
+ is(aElement.value, "a", aDescription + "Ctrl+Return");
+
+ reset("a");
+ synthesizeKey("VK_RETURN", { altKey: true });
+ check(aDescription + "Alt+Return", true, true, false);
+ is(aElement.value, "a", aDescription + "Alt+Return");
+
+ reset("a");
+ synthesizeKey("VK_RETURN", { metaKey: true });
+ check(aDescription + "Meta+Return", true, true, false);
+ is(aElement.value, "a", aDescription + "Meta+Return");
+
+ reset("a");
+ synthesizeKey("VK_RETURN", { osKey: true });
+ check(aDescription + "OS+Return", true, true, false);
+ is(aElement.value, "a", aDescription + "OS+Return");
+
+ // Tab key:
+ // If editor is tabbable, editor doesn't consume all tab key events.
+ // Otherwise, editor consumes tab key event without any modifier keys.
+ reset("a");
+ synthesizeKey("VK_TAB", { });
+ check(aDescription + "Tab",
+ true, true, !aIsTabbable && !aIsReadonly);
+ is(aElement.value, !aIsTabbable && !aIsReadonly ? "a\t" : "a",
+ aDescription + "Tab");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Tab)");
+
+ // If the editor is not tabbable, make sure that it accepts tab characters
+ // even if it's empty.
+ if (!aIsTabbable && !aIsReadonly) {
+ reset("");
+ synthesizeKey("VK_TAB", {});
+ check(aDescription + "Tab on empty textarea",
+ true, true, !aIsReadonly);
+ is(aElement.value, "\t", aDescription + "Tab on empty textarea");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Tab on empty textarea");
+ }
+
+ reset("a");
+ synthesizeKey("VK_TAB", { shiftKey: true });
+ check(aDescription + "Shift+Tab", true, true, false);
+ is(aElement.value, "a", aDescription + "Shift+Tab");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Shift+Tab)");
+
+ // Ctrl+Tab should be consumed by tabbrowser at keydown, so, keypress
+ // event should never be fired.
+ reset("a");
+ synthesizeKey("VK_TAB", { ctrlKey: true });
+ check(aDescription + "Ctrl+Tab", false, false, false);
+ is(aElement.value, "a", aDescription + "Ctrl+Tab");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Ctrl+Tab)");
+
+ reset("a");
+ synthesizeKey("VK_TAB", { altKey: true });
+ check(aDescription + "Alt+Tab", true, true, false);
+ is(aElement.value, "a", aDescription + "Alt+Tab");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Alt+Tab)");
+
+ reset("a");
+ synthesizeKey("VK_TAB", { metaKey: true });
+ check(aDescription + "Meta+Tab", true, true, false);
+ is(aElement.value, "a", aDescription + "Meta+Tab");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Meta+Tab)");
+
+ reset("a");
+ synthesizeKey("VK_TAB", { osKey: true });
+ check(aDescription + "OS+Tab", true, true, false);
+ is(aElement.value, "a", aDescription + "OS+Tab");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (OS+Tab)");
+
+ // Esc key:
+ // In all cases, esc key events are not consumed
+ reset("abc");
+ synthesizeKey("VK_ESCAPE", { });
+ check(aDescription + "Esc", true, true, false);
+
+ reset("abc");
+ synthesizeKey("VK_ESCAPE", { shiftKey: true });
+ check(aDescription + "Shift+Esc", true, true, false);
+
+ reset("abc");
+ synthesizeKey("VK_ESCAPE", { ctrlKey: true });
+ check(aDescription + "Ctrl+Esc", true, true, false);
+
+ reset("abc");
+ synthesizeKey("VK_ESCAPE", { altKey: true });
+ check(aDescription + "Alt+Esc", true, true, false);
+
+ reset("abc");
+ synthesizeKey("VK_ESCAPE", { metaKey: true });
+ check(aDescription + "Meta+Esc", true, true, false);
+
+ reset("abc");
+ synthesizeKey("VK_ESCAPE", { osKey: true });
+ check(aDescription + "OS+Esc", true, true, false);
+
+ // typical typing tests:
+ reset("");
+ synthesizeKey("M", { shiftKey: true });
+ check(aDescription + "M", true, true, !aIsReadonly);
+ synthesizeKey("o", { });
+ check(aDescription + "o", true, true, !aIsReadonly);
+ synthesizeKey("z", { });
+ check(aDescription + "z", true, true, !aIsReadonly);
+ synthesizeKey("i", { });
+ check(aDescription + "i", true, true, !aIsReadonly);
+ synthesizeKey("l", { });
+ check(aDescription + "l", true, true, !aIsReadonly);
+ synthesizeKey("l", { });
+ check(aDescription + "l", true, true, !aIsReadonly);
+ synthesizeKey("a", { });
+ check(aDescription + "a", true, true, !aIsReadonly);
+ synthesizeKey(" ", { });
+ check(aDescription + "' '", true, true, !aIsReadonly);
+ is(aElement.value, !aIsReadonly ? "Mozilla " : "",
+ aDescription + "typed \"Mozilla \"");
+ }
+
+ doTest(inputField, "<input type=\"text\">", true, false, true);
+
+ inputField.setAttribute("readonly", "readonly");
+ doTest(inputField, "<input type=\"text\" readonly>", true, true, true);
+
+ doTest(passwordField, "<input type=\"password\">", true, false, true);
+
+ passwordField.setAttribute("readonly", "readonly");
+ doTest(passwordField, "<input type=\"password\" readonly>", true, true, true);
+
+ doTest(textarea, "<textarea>", false, false, true);
+
+ textarea.setAttribute("readonly", "readonly");
+ doTest(textarea, "<textarea readonly>", false, true, true);
+
+ // make non-tabbable plaintext editor
+ textarea.removeAttribute("readonly");
+ const nsIPlaintextEditor = SpecialPowers.Ci.nsIPlaintextEditor;
+ const nsIDOMNSEditableElement = SpecialPowers.Ci.nsIDOMNSEditableElement;
+ var editor = SpecialPowers.wrap(textarea).editor;
+ var flags = editor.flags;
+ editor.flags = flags & ~(nsIPlaintextEditor.eEditorWidgetMask |
+ nsIPlaintextEditor.eEditorAllowInteraction);
+ doTest(textarea, "non-tabbable <textarea>", false, false, false);
+
+ textarea.setAttribute("readonly", "readonly");
+ doTest(textarea, "non-tabbable <textarea readonly>", false, true, false);
+
+ editor.flags = flags;
+
+ SpecialPowers.removeSystemEventListener(parentElement, "keypress", listener,
+ true);
+ SpecialPowers.removeSystemEventListener(parentElement, "keypress", listener,
+ false);
+
+ SimpleTest.finish();
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/moz.build b/editor/moz.build
new file mode 100644
index 000000000..f673e2486
--- /dev/null
+++ b/editor/moz.build
@@ -0,0 +1,44 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += [
+ 'txtsvc',
+ 'libeditor',
+ 'txmgr',
+ 'composer',
+]
+
+XPIDL_SOURCES += [
+ 'nsIContentFilter.idl',
+ 'nsIDocumentStateListener.idl',
+ 'nsIEditActionListener.idl',
+ 'nsIEditor.idl',
+ 'nsIEditorIMESupport.idl',
+ 'nsIEditorMailSupport.idl',
+ 'nsIEditorObserver.idl',
+ 'nsIEditorSpellCheck.idl',
+ 'nsIEditorStyleSheets.idl',
+ 'nsIEditorUtils.idl',
+ 'nsIHTMLAbsPosEditor.idl',
+ 'nsIHTMLEditor.idl',
+ 'nsIHTMLInlineTableEditor.idl',
+ 'nsIHTMLObjectResizeListener.idl',
+ 'nsIHTMLObjectResizer.idl',
+ 'nsIPlaintextEditor.idl',
+ 'nsITableEditor.idl',
+ 'nsIURIRefObject.idl',
+ 'nsPIEditorTransaction.idl',
+]
+
+XPIDL_MODULE = 'editor'
+
+EXPORTS += [
+ 'nsEditorCID.h',
+]
+
+EXTRA_JS_MODULES += [
+ 'AsyncSpellCheckTestHelper.jsm',
+]
diff --git a/editor/nsEditorCID.h b/editor/nsEditorCID.h
new file mode 100644
index 000000000..4b288771e
--- /dev/null
+++ b/editor/nsEditorCID.h
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef NSEDITORCID_H___
+
+#define NS_EDITOR_CID \
+{/* {D3DE3431-8A75-11d2-918C-0080C8E44DB5}*/ \
+0xd3de3431, 0x8a75, 0x11d2, \
+{ 0x91, 0x8c, 0x0, 0x80, 0xc8, 0xe4, 0x4d, 0xb5 } }
+
+#define NS_TEXTEDITOR_CID \
+{/* {e197cc01-cfe1-11d4-8eb0-87ae406dfd3f}*/ \
+0xe197cc01, 0xcfe1, 0x11d4, \
+{ 0x8e, 0xb0, 0x87, 0xae, 0x40, 0x6d, 0xfd, 0x3f } }
+
+#define NS_HTMLEDITOR_CID \
+{/* {ed0244e0-c144-11d2-8f4c-006008159b0c}*/ \
+0xed0244e0, 0xc144, 0x11d2, \
+{ 0x8f, 0x4c, 0x0, 0x60, 0x08, 0x15, 0x9b, 0x0c } }
+
+#endif //NSEDITORCID_H___
+
+
+
diff --git a/editor/nsIContentFilter.idl b/editor/nsIContentFilter.idl
new file mode 100644
index 000000000..585901ec1
--- /dev/null
+++ b/editor/nsIContentFilter.idl
@@ -0,0 +1,85 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "domstubs.idl"
+
+interface nsIURL;
+
+[scriptable, uuid(c18c49a8-62f0-4045-9884-4aa91e388f14)]
+interface nsIContentFilter : nsISupports
+{
+ /**
+ * This notification occurs in an editor during these events:
+ * * open of document (once rendered in window but before editable)
+ * * paste from clipboard
+ * * drop from mouse
+ * * insertion of html (such as with "cmd_insertHTML")
+ * It provides a hook so the above actions can be canceled or the data
+ * can be modified (using standard DOM APIs) or left untouched. The data
+ * that results (if any) from all filter callbacks is what will be used
+ * for transaction purposes (undo/redo) except for the open event.
+ *
+ * The willDeleteSelection parameter is offered for filters who want to
+ * handle the insertion themselves and need to handle drag/drop correctly.
+ * The flag is true when the editor intends to delete the selection.
+ *
+ * Callers who want to cancel all insertion can simply set
+ * continueWithInsertion to PR_FALSE and return.
+ * Note: If cancellation occurs during the "open" event, the editor will
+ * still be available but will be empty.
+ *
+ * Callers who want to allow insertion of the data with no changes
+ * can simply set continueWithInsertion to PR_TRUE and return.
+ *
+ * Callers who want to modify the content (docFragment) being inserted are
+ * responsible for updating contentStartNode, contentStartOffset,
+ * contentEndNode, and contentEndOffset (if necessary).
+ * Callers are responsible for freeing and addref'ing if they want to
+ * completely replace any of the DOM nodes passed in.
+ *
+ * The location where insertion will occur should be considered an
+ * approximation since the editor may need to adjust it if it deletes
+ * the selection as part of the event and later determines that insertion
+ * point is an empty container which should also be removed (or in other
+ * scenarios such as -moz-user-select:none).
+ *
+ * In some scenarios the selection will be deleted. If callers choose
+ * to adjust the insertion point, they should be careful that the insertion
+ * point is not in the current selection.
+ *
+ * The contentStartNode and contentEndNode are not necessarily
+ * immediate children of the docFragment. Any nodes outside of the range
+ * set by contentStartNode and contentEndNode are for context from the
+ * source document.
+ *
+ * @param mimeType the mimetype used for retrieving data
+ * @param contentSourceURL location where docFragment came from
+ * @param sourceDocument document where content came from (can be null)
+ * @param willDeleteSelection tells hook if selection will/should be deleted
+ * @param docFragment fragment of node to be inserted
+ * @param contentStartNode node under which content to be inserted begins
+ * @param contentStartOffset start offset within contentStartNode
+ * @param contentEndNode node under which content to be inserted ends
+ * @param contentEndOffset ending offset withing contentEndNode
+ * @param insertionPointNode location where insertion will occur
+ * @param insertionPointOffset offset within node where insertion occurs
+ * @param continueWithInsertion flag to cancel insertion (if desired)
+ */
+
+ void notifyOfInsertion(in AString mimeType,
+ in nsIURL contentSourceURL,
+ in nsIDOMDocument sourceDocument,
+ in boolean willDeleteSelection,
+ inout nsIDOMNode docFragment,
+ inout nsIDOMNode contentStartNode,
+ inout long contentStartOffset,
+ inout nsIDOMNode contentEndNode,
+ inout long contentEndOffset,
+ inout nsIDOMNode insertionPointNode,
+ inout long insertionPointOffset,
+ out boolean continueWithInsertion);
+
+};
diff --git a/editor/nsIDocumentStateListener.idl b/editor/nsIDocumentStateListener.idl
new file mode 100644
index 000000000..bf8dce051
--- /dev/null
+++ b/editor/nsIDocumentStateListener.idl
@@ -0,0 +1,16 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(050cdc00-3b8e-11d3-9ce4-a458f454fcbc)]
+interface nsIDocumentStateListener : nsISupports
+{
+
+ void NotifyDocumentCreated();
+ void NotifyDocumentWillBeDestroyed();
+ void NotifyDocumentStateChanged(in boolean nowDirty);
+
+};
diff --git a/editor/nsIEditActionListener.idl b/editor/nsIEditActionListener.idl
new file mode 100644
index 000000000..bd2085405
--- /dev/null
+++ b/editor/nsIEditActionListener.idl
@@ -0,0 +1,199 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "domstubs.idl"
+
+interface nsISelection;
+
+
+/*
+Editor Action Listener interface to outside world
+*/
+
+
+/**
+ * A generic editor action listener interface.
+ * <P>
+ * nsIEditActionListener is the interface used by applications wishing to be notified
+ * when the editor modifies the DOM tree.
+ *
+ * Note: this is the wrong class to implement if you are interested in generic
+ * change notifications. For generic notifications, you should implement
+ * nsIDocumentObserver.
+ */
+[scriptable, uuid(b22907b1-ee93-11d2-8d50-000064657374)]
+
+interface nsIEditActionListener : nsISupports{
+
+ /**
+ * Called before the editor creates a node.
+ * @param aTag The tag name of the DOM Node to create.
+ * @param aParent The node to insert the new object into
+ * @param aPosition The place in aParent to insert the new node
+ * 0=first child, 1=second child, etc.
+ * any number > number of current children = last child
+ */
+ void WillCreateNode(in DOMString aTag,
+ in nsIDOMNode aParent,
+ in long aPosition);
+
+ /**
+ * Called after the editor creates a node.
+ * @param aTag The tag name of the DOM Node to create.
+ * @param aNode The DOM Node that was created.
+ * @param aParent The node to insert the new object into
+ * @param aPosition The place in aParent to insert the new node
+ * 0=first child, 1=second child, etc.
+ * any number > number of current children = last child
+ * @param aResult The result of the create node operation.
+ */
+ void DidCreateNode(in DOMString aTag,
+ in nsIDOMNode aNode,
+ in nsIDOMNode aParent,
+ in long aPosition,
+ in nsresult aResult);
+
+ /**
+ * Called before the editor inserts a node.
+ * @param aNode The DOM Node to insert.
+ * @param aParent The node to insert the new object into
+ * @param aPosition The place in aParent to insert the new node
+ * 0=first child, 1=second child, etc.
+ * any number > number of current children = last child
+ */
+ void WillInsertNode(in nsIDOMNode aNode,
+ in nsIDOMNode aParent,
+ in long aPosition);
+
+ /**
+ * Called after the editor inserts a node.
+ * @param aNode The DOM Node to insert.
+ * @param aParent The node to insert the new object into
+ * @param aPosition The place in aParent to insert the new node
+ * 0=first child, 1=second child, etc.
+ * any number > number of current children = last child
+ * @param aResult The result of the insert node operation.
+ */
+ void DidInsertNode(in nsIDOMNode aNode,
+ in nsIDOMNode aParent,
+ in long aPosition,
+ in nsresult aResult);
+
+ /**
+ * Called before the editor deletes a node.
+ * @param aChild The node to delete
+ */
+ void WillDeleteNode(in nsIDOMNode aChild);
+
+ /**
+ * Called after the editor deletes a node.
+ * @param aChild The node to delete
+ * @param aResult The result of the delete node operation.
+ */
+ void DidDeleteNode(in nsIDOMNode aChild, in nsresult aResult);
+
+ /**
+ * Called before the editor splits a node.
+ * @param aExistingRightNode the node to split. It will become the new node's next sibling.
+ * @param aOffset the offset of aExistingRightNode's content|children to do the split at
+ * @param aNewLeftNode [OUT] the new node resulting from the split, becomes aExistingRightNode's previous sibling.
+ */
+ void WillSplitNode(in nsIDOMNode aExistingRightNode,
+ in long aOffset);
+
+ /**
+ * Called after the editor splits a node.
+ * @param aExistingRightNode the node to split. It will become the new node's next sibling.
+ * @param aOffset the offset of aExistingRightNode's content|children to do the split at
+ * @param aNewLeftNode [OUT] the new node resulting from the split, becomes aExistingRightNode's previous sibling.
+ */
+ void DidSplitNode(in nsIDOMNode aExistingRightNode,
+ in long aOffset,
+ in nsIDOMNode aNewLeftNode,
+ in nsresult aResult);
+
+ /**
+ * Called before the editor joins 2 nodes.
+ * @param aLeftNode This node will be merged into the right node
+ * @param aRightNode The node that will be merged into.
+ * There is no requirement that the two nodes be of
+ * the same type.
+ * @param aParent The parent of aRightNode
+ */
+ void WillJoinNodes(in nsIDOMNode aLeftNode,
+ in nsIDOMNode aRightNode,
+ in nsIDOMNode aParent);
+
+ /**
+ * Called after the editor joins 2 nodes.
+ * @param aLeftNode This node will be merged into the right node
+ * @param aRightNode The node that will be merged into.
+ * There is no requirement that the two nodes be of
+ * the same type.
+ * @param aParent The parent of aRightNode
+ * @param aResult The result of the join operation.
+ */
+ void DidJoinNodes(in nsIDOMNode aLeftNode,
+ in nsIDOMNode aRightNode,
+ in nsIDOMNode aParent,
+ in nsresult aResult);
+
+ /**
+ * Called before the editor inserts text.
+ * @param aTextNode This node getting inserted text
+ * @param aOffset The offset in aTextNode to insert at.
+ * @param aString The string that gets inserted.
+ */
+ void WillInsertText(in nsIDOMCharacterData aTextNode,
+ in long aOffset,
+ in DOMString aString);
+
+ /**
+ * Called after the editor inserts text.
+ * @param aTextNode This node getting inserted text
+ * @param aOffset The offset in aTextNode to insert at.
+ * @param aString The string that gets inserted.
+ * @param aResult The result of the insert text operation.
+ */
+ void DidInsertText(in nsIDOMCharacterData aTextNode,
+ in long aOffset,
+ in DOMString aString,
+ in nsresult aResult);
+
+ /**
+ * Called before the editor deletes text.
+ * @param aTextNode This node getting text deleted
+ * @param aOffset The offset in aTextNode to delete at.
+ * @param aLength The amount of text to delete.
+ */
+ void WillDeleteText(in nsIDOMCharacterData aTextNode,
+ in long aOffset,
+ in long aLength);
+
+ /**
+ * Called before the editor deletes text.
+ * @param aTextNode This node getting text deleted
+ * @param aOffset The offset in aTextNode to delete at.
+ * @param aLength The amount of text to delete.
+ * @param aResult The result of the delete text operation.
+ */
+ void DidDeleteText(in nsIDOMCharacterData aTextNode,
+ in long aOffset,
+ in long aLength,
+ in nsresult aResult);
+
+ /**
+ * Called before the editor deletes the selection.
+ * @param aSelection The selection to be deleted
+ */
+ void WillDeleteSelection(in nsISelection aSelection);
+
+ /**
+ * Called after the editor deletes the selection.
+ * @param aSelection The selection, after deletion
+ */
+ void DidDeleteSelection(in nsISelection aSelection);
+};
diff --git a/editor/nsIEditor.idl b/editor/nsIEditor.idl
new file mode 100644
index 000000000..bb9026d0e
--- /dev/null
+++ b/editor/nsIEditor.idl
@@ -0,0 +1,567 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "domstubs.idl"
+
+interface nsIURI;
+interface nsIAtom;
+interface nsIContent;
+interface nsISelection;
+interface nsISelectionController;
+interface nsIDocumentStateListener;
+interface nsIOutputStream;
+interface nsITransactionManager;
+interface nsITransaction;
+interface nsIEditorObserver;
+interface nsIEditActionListener;
+interface nsIInlineSpellChecker;
+interface nsITransferable;
+
+[scriptable, uuid(094be624-f0bf-400f-89e2-6a84baab9474)]
+interface nsIEditor : nsISupports
+{
+%{C++
+ typedef short EDirection;
+ typedef short EStripWrappers;
+%}
+ const short eNone = 0;
+ const short eNext = 1;
+ const short ePrevious = 2;
+ const short eNextWord = 3;
+ const short ePreviousWord = 4;
+ const short eToBeginningOfLine = 5;
+ const short eToEndOfLine = 6;
+
+ const short eStrip = 0;
+ const short eNoStrip = 1;
+
+ readonly attribute nsISelection selection;
+
+ /**
+ * Finalizes selection and caret for the editor.
+ */
+ [noscript] void finalizeSelection();
+
+ /**
+ * Init is to tell the implementation of nsIEditor to begin its services
+ * @param aDoc The dom document interface being observed
+ * @param aRoot This is the root of the editable section of this
+ * document. If it is null then we get root
+ * from document body.
+ * @param aSelCon this should be used to get the selection location
+ * (will be null for HTML editors)
+ * @param aFlags A bitmask of flags for specifying the behavior
+ * of the editor.
+ */
+ [noscript] void init(in nsIDOMDocument doc,
+ in nsIContent aRoot,
+ in nsISelectionController aSelCon,
+ in unsigned long aFlags,
+ in AString initialValue);
+
+ void setAttributeOrEquivalent(in nsIDOMElement element,
+ in AString sourceAttrName,
+ in AString sourceAttrValue,
+ in boolean aSuppressTransaction);
+ void removeAttributeOrEquivalent(in nsIDOMElement element,
+ in DOMString sourceAttrName,
+ in boolean aSuppressTransaction);
+
+ /**
+ * postCreate should be called after Init, and is the time that the editor
+ * tells its documentStateObservers that the document has been created.
+ */
+ void postCreate();
+
+ /**
+ * preDestroy is called before the editor goes away, and gives the editor a
+ * chance to tell its documentStateObservers that the document is going away.
+ * @param aDestroyingFrames set to true when the frames being edited
+ * are being destroyed (so there is no need to modify any nsISelections,
+ * nor is it safe to do so)
+ */
+ void preDestroy(in boolean aDestroyingFrames);
+
+ /** edit flags for this editor. May be set at any time. */
+ attribute unsigned long flags;
+
+ /**
+ * the MimeType of the document
+ */
+ attribute string contentsMIMEType;
+
+ /** Returns true if we have a document that is not marked read-only */
+ readonly attribute boolean isDocumentEditable;
+
+ /** Returns true if the current selection anchor is editable */
+ readonly attribute boolean isSelectionEditable;
+
+ /**
+ * the DOM Document this editor is associated with, refcounted.
+ */
+ readonly attribute nsIDOMDocument document;
+
+ /** the body element, i.e. the root of the editable document.
+ */
+ readonly attribute nsIDOMElement rootElement;
+
+ /**
+ * the selection controller for the current presentation, refcounted.
+ */
+ readonly attribute nsISelectionController selectionController;
+
+
+ /* ------------ Selected content removal -------------- */
+
+ /**
+ * DeleteSelection removes all nodes in the current selection.
+ * @param aDir if eNext, delete to the right (for example, the DEL key)
+ * if ePrevious, delete to the left (for example, the BACKSPACE key)
+ * @param stripWrappers If eStrip, strip any empty inline elements left
+ * behind after the deletion; if eNoStrip, don't. If in
+ * doubt, pass eStrip -- eNoStrip is only for if you're
+ * about to insert text or similar right after.
+ */
+ void deleteSelection(in short action, in short stripWrappers);
+
+
+ /* ------------ Document info and file methods -------------- */
+
+ /** Returns true if the document has no *meaningful* content */
+ readonly attribute boolean documentIsEmpty;
+
+ /** Returns true if the document is modifed and needs saving */
+ readonly attribute boolean documentModified;
+
+ /** Sets the current 'Save' document character set */
+ attribute ACString documentCharacterSet;
+
+ /** to be used ONLY when we need to override the doc's modification
+ * state (such as when it's saved).
+ */
+ void resetModificationCount();
+
+ /** Gets the modification count of the document we are editing.
+ * @return the modification count of the document being edited.
+ * Zero means unchanged.
+ */
+ long getModificationCount();
+
+ /** called each time we modify the document.
+ * Increments the modification count of the document.
+ * @param aModCount the number of modifications by which
+ * to increase or decrease the count
+ */
+ void incrementModificationCount(in long aModCount);
+
+ /* ------------ Transaction methods -------------- */
+
+ /** transactionManager Get the transaction manager the editor is using.
+ */
+ attribute nsITransactionManager transactionManager;
+
+ /** doTransaction() fires a transaction.
+ * It is provided here so clients can create their own transactions.
+ * If a transaction manager is present, it is used.
+ * Otherwise, the transaction is just executed directly.
+ *
+ * @param aTxn the transaction to execute
+ */
+ void doTransaction(in nsITransaction txn);
+
+
+ /** turn the undo system on or off
+ * @param aEnable if PR_TRUE, the undo system is turned on if available
+ * if PR_FALSE the undo system is turned off if it
+ * was previously on
+ * @return if aEnable is PR_TRUE, returns NS_OK if
+ * the undo system could be initialized properly
+ * if aEnable is PR_FALSE, returns NS_OK.
+ */
+ void enableUndo(in boolean enable);
+
+ /**
+ * The number of items on the undo stack.
+ */
+ readonly attribute long numberOfUndoItems;
+
+ /**
+ * The number of items on the redo stack.
+ */
+ readonly attribute long numberOfRedoItems;
+
+ /** undo reverses the effects of the last Do operation,
+ * if Undo is enabled in the editor.
+ * It is provided here so clients need no knowledge of whether
+ * the editor has a transaction manager or not.
+ * If a transaction manager is present, it is told to undo,
+ * and the result of that undo is returned.
+ * Otherwise, the Undo request is ignored and an
+ * error NS_ERROR_NOT_AVAILABLE is returned.
+ *
+ */
+ void undo(in unsigned long count);
+
+ /** returns state information about the undo system.
+ * @param aIsEnabled [OUT] PR_TRUE if undo is enabled
+ * @param aCanUndo [OUT] PR_TRUE if at least one transaction is
+ * currently ready to be undone.
+ */
+ void canUndo(out boolean isEnabled, out boolean canUndo);
+
+ /** redo reverses the effects of the last Undo operation
+ * It is provided here so clients need no knowledge of whether
+ * the editor has a transaction manager or not.
+ * If a transaction manager is present, it is told to redo and the
+ * result of the previously undone transaction is reapplied to the document.
+ * If no transaction is available for Redo, or if the document
+ * has no transaction manager, the Redo request is ignored and an
+ * error NS_ERROR_NOT_AVAILABLE is returned.
+ *
+ */
+ void redo(in unsigned long count);
+
+ /** returns state information about the redo system.
+ * @param aIsEnabled [OUT] PR_TRUE if redo is enabled
+ * @param aCanRedo [OUT] PR_TRUE if at least one transaction is
+ currently ready to be redone.
+ */
+ void canRedo(out boolean isEnabled, out boolean canRedo);
+
+ /** beginTransaction is a signal from the caller to the editor that
+ * the caller will execute multiple updates to the content tree
+ * that should be treated as a single logical operation,
+ * in the most efficient way possible.<br>
+ * All transactions executed between a call to beginTransaction and
+ * endTransaction will be undoable as an atomic action.<br>
+ * endTransaction must be called after beginTransaction.<br>
+ * Calls to beginTransaction can be nested, as long as endTransaction
+ * is called once per beginUpdate.
+ */
+ void beginTransaction();
+
+ /** endTransaction is a signal to the editor that the caller is
+ * finished updating the content model.<br>
+ * beginUpdate must be called before endTransaction is called.<br>
+ * Calls to beginTransaction can be nested, as long as endTransaction
+ * is called once per beginTransaction.
+ */
+ void endTransaction();
+
+ void beginPlaceHolderTransaction(in nsIAtom name);
+ void endPlaceHolderTransaction();
+ boolean shouldTxnSetSelection();
+
+ /** Set the flag that prevents insertElementTxn from changing the selection
+ * @param should Set false to suppress changing the selection;
+ * i.e., before using InsertElement() to insert
+ * under <head> element
+ * WARNING: You must be very careful to reset back to PR_TRUE after
+ * setting PR_FALSE, else selection/caret is trashed
+ * for further editing.
+ */
+ void setShouldTxnSetSelection(in boolean should);
+
+ /* ------------ Inline Spell Checking methods -------------- */
+
+ /** Returns the inline spell checker associated with this object. The spell
+ * checker is lazily created, so this function may create the object for
+ * you during this call.
+ * @param autoCreate If true, this will create a spell checker object
+ * if one does not exist yet for this editor. If false
+ * and the object has not been created, this function
+ * WILL RETURN NULL.
+ */
+ nsIInlineSpellChecker getInlineSpellChecker(in boolean autoCreate);
+
+ /** Resyncs spellchecking state (enabled/disabled). This should be called
+ * when anything that affects spellchecking state changes, such as the
+ * spellcheck attribute value.
+ */
+ void syncRealTimeSpell();
+
+ /** Called when the user manually overrides the spellchecking state for this
+ * editor.
+ * @param enable The new state of spellchecking in this editor, as
+ * requested by the user.
+ */
+ void setSpellcheckUserOverride(in boolean enable);
+
+ /* ------------ Clipboard methods -------------- */
+
+ /** cut the currently selected text, putting it into the OS clipboard
+ * What if no text is selected?
+ * What about mixed selections?
+ * What are the clipboard formats?
+ */
+ void cut();
+
+ /** Can we cut? True if the doc is modifiable, and we have a non-
+ * collapsed selection.
+ */
+ boolean canCut();
+
+ /** copy the currently selected text, putting it into the OS clipboard
+ * What if no text is selected?
+ * What about mixed selections?
+ * What are the clipboard formats?
+ */
+ void copy();
+
+ /** Can we copy? True if we have a non-collapsed selection.
+ */
+ boolean canCopy();
+
+ /** Can we delete? True if we have a non-collapsed selection.
+ */
+ boolean canDelete();
+
+ /** paste the text in the OS clipboard at the cursor position, replacing
+ * the selected text (if any)
+ */
+ void paste(in long aSelectionType);
+
+ /** Paste the text in |aTransferable| at the cursor position, replacing the
+ * selected text (if any).
+ */
+ void pasteTransferable(in nsITransferable aTransferable);
+
+ /** Can we paste? True if the doc is modifiable, and we have
+ * pasteable data in the clipboard.
+ */
+ boolean canPaste(in long aSelectionType);
+
+ /** Can we paste |aTransferable| or, if |aTransferable| is null, will a call
+ * to pasteTransferable later possibly succeed if given an instance of
+ * nsITransferable then? True if the doc is modifiable, and, if
+ * |aTransfeable| is non-null, we have pasteable data in |aTransfeable|.
+ */
+ boolean canPasteTransferable([optional] in nsITransferable aTransferable);
+
+ /* ------------ Selection methods -------------- */
+
+ /** sets the document selection to the entire contents of the document */
+ void selectAll();
+
+ /** sets the document selection to the beginning of the document */
+ void beginningOfDocument();
+
+ /** sets the document selection to the end of the document */
+ void endOfDocument();
+
+ /* ------------ Node manipulation methods -------------- */
+
+ /**
+ * setAttribute() sets the attribute of aElement.
+ * No checking is done to see if aAttribute is a legal attribute of the node,
+ * or if aValue is a legal value of aAttribute.
+ *
+ * @param aElement the content element to operate on
+ * @param aAttribute the string representation of the attribute to set
+ * @param aValue the value to set aAttribute to
+ */
+ void setAttribute(in nsIDOMElement aElement, in AString attributestr,
+ in AString attvalue);
+
+ /**
+ * getAttributeValue() retrieves the attribute's value for aElement.
+ *
+ * @param aElement the content element to operate on
+ * @param aAttribute the string representation of the attribute to get
+ * @param aResultValue [OUT] the value of aAttribute.
+ * Only valid if aResultIsSet is PR_TRUE
+ * @return PR_TRUE if aAttribute is set on the current node,
+ * PR_FALSE if it is not.
+ */
+ boolean getAttributeValue(in nsIDOMElement aElement,
+ in AString attributestr,
+ out AString resultValue);
+
+ /**
+ * removeAttribute() deletes aAttribute from the attribute list of aElement.
+ * If aAttribute is not an attribute of aElement, nothing is done.
+ *
+ * @param aElement the content element to operate on
+ * @param aAttribute the string representation of the attribute to get
+ */
+ void removeAttribute(in nsIDOMElement aElement,
+ in AString aAttribute);
+
+ /**
+ * cloneAttribute() copies the attribute from the source node to
+ * the destination node and delete those not in the source.
+ *
+ * The supplied nodes MUST BE ELEMENTS (most callers are working with nodes)
+ * @param aAttribute the name of the attribute to copy
+ * @param aDestNode the destination element to operate on
+ * @param aSourceNode the source element to copy attributes from
+ * @exception NS_ERROR_NULL_POINTER at least one of the nodes is null
+ * @exception NS_ERROR_NO_INTERFACE at least one of the nodes is not an
+ * element
+ */
+ void cloneAttribute(in AString aAttribute,
+ in nsIDOMNode aDestNode, in nsIDOMNode aSourceNode);
+
+ /**
+ * cloneAttributes() is similar to nsIDOMNode::cloneNode(),
+ * it assures the attribute nodes of the destination are identical
+ * with the source node by copying all existing attributes from the
+ * source and deleting those not in the source.
+ * This is used when the destination node (element) already exists
+ *
+ * The supplied nodes MUST BE ELEMENTS (most callers are working with nodes)
+ * @param aDestNode the destination element to operate on
+ * @param aSourceNode the source element to copy attributes from
+ */
+ void cloneAttributes(in nsIDOMNode destNode, in nsIDOMNode sourceNode);
+
+ /**
+ * createNode instantiates a new element of type aTag and inserts it
+ * into aParent at aPosition.
+ * @param aTag The type of object to create
+ * @param aParent The node to insert the new object into
+ * @param aPosition The place in aParent to insert the new node
+ * @return The node created. Caller must release aNewNode.
+ */
+ nsIDOMNode createNode(in AString tag,
+ in nsIDOMNode parent,
+ in long position);
+
+ /**
+ * insertNode inserts aNode into aParent at aPosition.
+ * No checking is done to verify the legality of the insertion.
+ * That is the responsibility of the caller.
+ * @param aNode The DOM Node to insert.
+ * @param aParent The node to insert the new object into
+ * @param aPosition The place in aParent to insert the new node
+ * 0=first child, 1=second child, etc.
+ * any number > number of current children = last child
+ */
+ void insertNode(in nsIDOMNode node,
+ in nsIDOMNode parent,
+ in long aPosition);
+
+
+ /**
+ * splitNode() creates a new node identical to an existing node,
+ * and split the contents between the two nodes
+ * @param aExistingRightNode the node to split.
+ * It will become the new node's next sibling.
+ * @param aOffset the offset of aExistingRightNode's
+ * content|children to do the split at
+ * @param aNewLeftNode [OUT] the new node resulting from the split,
+ * becomes aExistingRightNode's previous sibling.
+ */
+ void splitNode(in nsIDOMNode existingRightNode,
+ in long offset,
+ out nsIDOMNode newLeftNode);
+
+ /**
+ * joinNodes() takes 2 nodes and merge their content|children.
+ * @param aLeftNode The left node. It will be deleted.
+ * @param aRightNode The right node. It will remain after the join.
+ * @param aParent The parent of aExistingRightNode
+ *
+ * There is no requirement that the two nodes be
+ * of the same type. However, a text node can be
+ * merged only with another text node.
+ */
+ void joinNodes(in nsIDOMNode leftNode,
+ in nsIDOMNode rightNode,
+ in nsIDOMNode parent);
+
+ /**
+ * deleteNode removes aChild from aParent.
+ * @param aChild The node to delete
+ */
+ void deleteNode(in nsIDOMNode child);
+
+ /**
+ * Returns true if markNodeDirty() has any effect. Returns false if
+ * markNodeDirty() is a no-op.
+ */
+ [notxpcom] boolean outputsMozDirty();
+
+ /**
+ * markNodeDirty() sets a special dirty attribute on the node.
+ * Usually this will be called immediately after creating a new node.
+ * @param aNode The node for which to insert formatting.
+ */
+ void markNodeDirty(in nsIDOMNode node);
+
+/* ---------- direction controller ---------- */
+
+ /**
+ * Switches the editor element direction; from "Left-to-Right" to
+ * "Right-to-Left", and vice versa.
+ */
+ void switchTextDirection();
+
+/* ------------ Output methods -------------- */
+
+ /**
+ * Output methods:
+ * aFormatType is a mime type, like text/plain.
+ */
+ AString outputToString(in AString formatType,
+ in unsigned long flags);
+ void outputToStream(in nsIOutputStream aStream,
+ in AString formatType,
+ in ACString charsetOverride,
+ in unsigned long flags);
+
+
+ /* ------------ Various listeners methods --------------
+ * nsIEditor holds strong references to the editor observers, action listeners
+ * and document state listeners.
+ */
+
+ /** add an EditorObserver to the editors list of observers. */
+ void addEditorObserver(in nsIEditorObserver observer);
+
+ /** Remove an EditorObserver from the editor's list of observers. */
+ void removeEditorObserver(in nsIEditorObserver observer);
+
+ /** add an EditActionListener to the editors list of listeners. */
+ void addEditActionListener(in nsIEditActionListener listener);
+
+ /** Remove an EditActionListener from the editor's list of listeners. */
+ void removeEditActionListener(in nsIEditActionListener listener);
+
+ /** Add a DocumentStateListener to the editors list of doc state listeners. */
+ void addDocumentStateListener(in nsIDocumentStateListener listener);
+
+ /** Remove a DocumentStateListener to the editors list of doc state listeners. */
+ void removeDocumentStateListener(in nsIDocumentStateListener listener);
+
+
+ /* ------------ Debug methods -------------- */
+
+ /**
+ * And a debug method -- show us what the tree looks like right now
+ */
+ void dumpContentTree();
+
+ /** Dumps a text representation of the content tree to standard out */
+ void debugDumpContent() ;
+
+ /* Run unit tests. Noop in optimized builds */
+ void debugUnitTests(out long outNumTests, out long outNumTestsFailed);
+
+ /* checks if a node is read-only or not */
+ [notxpcom] boolean isModifiableNode(in nsIDOMNode aNode);
+
+ /* Set true if you want to suppress dispatching input event. */
+ attribute boolean suppressDispatchingInputEvent;
+
+ /**
+ * True if an edit action is being handled (in other words, between calls of
+ * nsIEditorObserver::BeforeEditAction() and nsIEditorObserver::EditAction()
+ * or nsIEditorObserver::CancelEditAction(). Otherwise, false.
+ */
+ [noscript] readonly attribute boolean isInEditAction;
+};
diff --git a/editor/nsIEditorIMESupport.idl b/editor/nsIEditorIMESupport.idl
new file mode 100644
index 000000000..4e4e4a50a
--- /dev/null
+++ b/editor/nsIEditorIMESupport.idl
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "domstubs.idl"
+
+%{C++
+namespace mozilla {
+namespace widget {
+struct IMEState;
+} // namespace widget
+} // namespace mozilla
+%}
+
+native IMEState(mozilla::widget::IMEState);
+
+[scriptable, uuid(0ba7f490-afb8-46dd-87fc-bc6137fbc899)]
+
+interface nsIEditorIMESupport : nsISupports
+{
+ /**
+ * forceCompositionEnd() force the composition end
+ */
+
+ void forceCompositionEnd();
+
+ /**
+ * Get preferred IME status of current widget.
+ */
+
+ [noscript] IMEState getPreferredIMEState();
+
+ /**
+ * whether this editor has active IME transaction
+ */
+ readonly attribute boolean composing;
+};
+
diff --git a/editor/nsIEditorMailSupport.idl b/editor/nsIEditorMailSupport.idl
new file mode 100644
index 000000000..3616bceee
--- /dev/null
+++ b/editor/nsIEditorMailSupport.idl
@@ -0,0 +1,81 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIArray;
+interface nsIDOMNode;
+
+[scriptable, uuid(fdf23301-4a94-11d3-9ce4-9960496c41bc)]
+
+interface nsIEditorMailSupport : nsISupports
+{
+ /** Paste the text in the OS clipboard at the cursor position,
+ * as a quotation (whose representation is dependant on the editor type),
+ * replacing the selected text (if any).
+ * @param aSelectionType Text or html?
+ */
+ void pasteAsQuotation(in long aSelectionType);
+
+ /** Insert a string as quoted text
+ * (whose representation is dependant on the editor type),
+ * replacing the selected text (if any).
+ * @param aQuotedText The actual text to be quoted
+ * @return The node which was inserted
+ */
+ nsIDOMNode insertAsQuotation(in AString aQuotedText);
+
+ /**
+ * Inserts a plaintext string at the current location,
+ * with special processing for lines beginning with ">",
+ * which will be treated as mail quotes and inserted
+ * as plaintext quoted blocks.
+ * If the selection is not collapsed, the selection is deleted
+ * and the insertion takes place at the resulting collapsed selection.
+ *
+ * @param aString the string to be inserted
+ */
+ void insertTextWithQuotations(in DOMString aStringToInsert);
+
+ /** Paste a string as quoted text,
+ * whose representation is dependant on the editor type,
+ * replacing the selected text (if any)
+ * @param aCitation The "mid" URL of the source message
+ * @param aSelectionType Text or html?
+ */
+ void pasteAsCitedQuotation(in AString aCitation,
+ in long aSelectionType);
+
+ /** Insert a string as quoted text
+ * (whose representation is dependant on the editor type),
+ * replacing the selected text (if any),
+ * including, if possible, a "cite" attribute.
+ * @param aQuotedText The actual text to be quoted
+ * @param aCitation The "mid" URL of the source message
+ * @param aInsertHTML Insert as html? (vs plaintext)
+ * @return The node which was inserted
+ */
+ nsIDOMNode insertAsCitedQuotation(in AString aQuotedText,
+ in AString aCitation,
+ in boolean aInsertHTML);
+
+ /**
+ * Rewrap the selected part of the document, re-quoting if necessary.
+ * @param aRespectNewlines Try to maintain newlines in the original?
+ */
+ void rewrap(in boolean aRespectNewlines);
+
+ /**
+ * Strip any citations in the selected part of the document.
+ */
+ void stripCites();
+
+
+ /**
+ * Get a list of IMG and OBJECT tags in the current document.
+ */
+ nsIArray getEmbeddedObjects();
+};
+
diff --git a/editor/nsIEditorObserver.idl b/editor/nsIEditorObserver.idl
new file mode 100644
index 000000000..c916b4c13
--- /dev/null
+++ b/editor/nsIEditorObserver.idl
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+/*
+Editor Observer interface to outside world
+*/
+
+[scriptable, uuid(f3ee57a6-890c-4ce0-a584-8a84bba0292e)]
+
+/**
+ * A generic editor observer interface.
+ * <P>
+ * nsIEditorObserver is the interface used by applications wishing to be notified
+ * when the editor has completed a user action.
+ *
+ */
+interface nsIEditorObserver : nsISupports {
+ /**
+ * Called after the editor completes a user action.
+ */
+ void EditAction();
+ /**
+ * Called when editor starts to handle a user action. I.e., This must be
+ * called before the first DOM change.
+ */
+ void BeforeEditAction();
+ /**
+ * Called after BeforeEditAction() is called but EditorAction() won't be
+ * called.
+ */
+ void CancelEditAction();
+};
diff --git a/editor/nsIEditorSpellCheck.idl b/editor/nsIEditorSpellCheck.idl
new file mode 100644
index 000000000..adf4a0a03
--- /dev/null
+++ b/editor/nsIEditorSpellCheck.idl
@@ -0,0 +1,166 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIEditor;
+interface nsITextServicesFilter;
+interface nsIEditorSpellCheckCallback;
+
+[scriptable, uuid(a171c25f-e4a8-4d08-adef-b797e6377bdc)]
+interface nsIEditorSpellCheck : nsISupports
+{
+
+ /**
+ * Returns true if we can enable spellchecking. If there are no available
+ * dictionaries, this will return false.
+ */
+ boolean canSpellCheck();
+
+ /**
+ * Turns on the spell checker for the given editor. enableSelectionChecking
+ * set means that we only want to check the current selection in the editor,
+ * (this controls the behavior of GetNextMisspelledWord). For spellchecking
+ * clients with no modal UI (such as inline spellcheckers), this flag doesn't
+ * matter. Initialization is asynchronous and is not complete until the given
+ * callback is called.
+ */
+ void InitSpellChecker(in nsIEditor editor, in boolean enableSelectionChecking,
+ [optional] in nsIEditorSpellCheckCallback callback);
+
+ /**
+ * When interactively spell checking the document, this will return the
+ * value of the next word that is misspelled. This also computes the
+ * suggestions which you can get by calling GetSuggestedWord.
+ *
+ * @see nsISpellChecker::GetNextMisspelledWord
+ */
+ wstring GetNextMisspelledWord();
+
+ /**
+ * Used to get suggestions for the last word that was checked and found to
+ * be misspelled. The first call will give you the first (best) suggestion.
+ * Subsequent calls will iterate through all the suggestions, allowing you
+ * to build a list. When there are no more suggestions, an empty string
+ * (not a null pointer) will be returned.
+ *
+ * @see nsISpellChecker::GetSuggestedWord
+ */
+ wstring GetSuggestedWord();
+
+ /**
+ * Check a given word. In spite of the name, this function checks the word
+ * you give it, returning true if the word is misspelled. If the word is
+ * misspelled, it will compute the suggestions which you can get from
+ * GetSuggestedWord().
+ *
+ * @see nsISpellChecker::CheckCurrentWord
+ */
+ boolean CheckCurrentWord(in wstring suggestedWord);
+
+ /**
+ * Use when modally checking the document to replace a word.
+ *
+ * @see nsISpellChecker::CheckCurrentWord
+ */
+ void ReplaceWord(in wstring misspelledWord, in wstring replaceWord, in boolean allOccurrences);
+
+ /**
+ * @see nsISpellChecker::IgnoreAll
+ */
+ void IgnoreWordAllOccurrences(in wstring word);
+
+ /**
+ * Fills an internal list of words added to the personal dictionary. These
+ * words can be retrieved using GetPersonalDictionaryWord()
+ *
+ * @see nsISpellChecker::GetPersonalDictionary
+ * @see GetPersonalDictionaryWord
+ */
+ void GetPersonalDictionary();
+
+ /**
+ * Used after you call GetPersonalDictionary() to iterate through all the
+ * words added to the personal dictionary. Will return the empty string when
+ * there are no more words.
+ */
+ wstring GetPersonalDictionaryWord();
+
+ /**
+ * Adds a word to the current personal dictionary.
+ *
+ * @see nsISpellChecker::AddWordToDictionary
+ */
+ void AddWordToDictionary(in wstring word);
+
+ /**
+ * Removes a word from the current personal dictionary.
+ *
+ * @see nsISpellChecker::RemoveWordFromPersonalDictionary
+ */
+ void RemoveWordFromDictionary(in wstring word);
+
+ /**
+ * Retrieves a list of the currently available dictionaries. The strings will
+ * typically be language IDs, like "en-US".
+ *
+ * @see mozISpellCheckingEngine::GetDictionaryList
+ */
+ void GetDictionaryList([array, size_is(count)] out wstring dictionaryList, out uint32_t count);
+
+ /**
+ * @see nsISpellChecker::GetCurrentDictionary
+ */
+ AString GetCurrentDictionary();
+
+ /**
+ * @see nsISpellChecker::SetCurrentDictionary
+ */
+ void SetCurrentDictionary(in AString dictionary);
+
+ /**
+ * Call this to free up the spell checking object. It will also save the
+ * current selected language as the default for future use.
+ *
+ * If you have called CanSpellCheck but not InitSpellChecker, you can still
+ * call this function to clear the cached spell check object, and no
+ * preference saving will happen.
+ */
+ void UninitSpellChecker();
+
+ /**
+ * Used to filter the content (for example, to skip blockquotes in email from
+ * spellchecking. Call this before calling InitSpellChecker; calling it
+ * after initialization will have no effect.
+ *
+ * @see nsITextServicesDocument::setFilter
+ */
+ void setFilter(in nsITextServicesFilter filter);
+
+ /**
+ * Like CheckCurrentWord, checks the word you give it, returning true if it's
+ * misspelled. This is faster than CheckCurrentWord because it does not
+ * compute any suggestions.
+ *
+ * Watch out: this does not clear any suggestions left over from previous
+ * calls to CheckCurrentWord, so there may be suggestions, but they will be
+ * invalid.
+ */
+ boolean CheckCurrentWordNoSuggest(in wstring suggestedWord);
+
+ /**
+ * Update the dictionary in use to be sure it corresponds to what the editor
+ * needs. The update is asynchronous and is not complete until the given
+ * callback is called.
+ */
+ void UpdateCurrentDictionary([optional] in nsIEditorSpellCheckCallback callback);
+
+};
+
+[scriptable, function, uuid(5f0a4bab-8538-4074-89d3-2f0e866a1c0b)]
+interface nsIEditorSpellCheckCallback : nsISupports
+{
+ void editorSpellCheckDone();
+};
diff --git a/editor/nsIEditorStyleSheets.idl b/editor/nsIEditorStyleSheets.idl
new file mode 100644
index 000000000..7f8c365e6
--- /dev/null
+++ b/editor/nsIEditorStyleSheets.idl
@@ -0,0 +1,71 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(4805e682-49b9-11d3-9ce4-ed60bd6cb5bc)]
+
+interface nsIEditorStyleSheets : nsISupports
+{
+ /** Load and apply the style sheet, specified by aURL, to the
+ * editor's document, replacing the last style sheet added (if any).
+ * This is always asynchronous, and may cause network I/O.
+ *
+ * @param aURL The style sheet to be loaded and applied.
+ */
+ void replaceStyleSheet(in AString aURL);
+
+ /** Add the given style sheet to the editor's document,
+ * on top of any that are already there.
+ * This is always asynchronous, and may cause network I/O.
+ *
+ * @param aURL The style sheet to be loaded and applied.
+ */
+ void addStyleSheet(in AString aURL);
+
+ /** Load and apply the override style sheet, specified by aURL, to the
+ * editor's document, replacing the last override style sheet added (if any).
+ * This is always synchronous, so aURL should be a local file with only
+ * local @imports. This action is not undoable. It is not intended for
+ * "user" style sheets, only for editor developers to add sheets to change
+ * display behavior for editing (like showing special cursors) that will
+ * not be affected by loading "document" style sheets with addStyleSheet or
+ * especially replaceStyleSheet.
+ *
+ * @param aURL The style sheet to be loaded and applied.
+ */
+ void replaceOverrideStyleSheet(in AString aURL);
+
+ /** Load and apply an override style sheet, specified by aURL, to
+ * the editor's document, on top of any that are already there.
+ * This is always synchronous, so the same caveats about local files and no
+ * non-local @import as replaceOverrideStyleSheet apply here, too.
+ *
+ * @param aURL The style sheet to be loaded and applied.
+ */
+ void addOverrideStyleSheet(in AString aURL);
+
+ /** Remove the given style sheet from the editor's document
+ * This is always synchronous
+ *
+ * @param aURL The style sheet to be removed
+ */
+ void removeStyleSheet(in AString aURL);
+
+ /** Remove the given override style sheet from the editor's document
+ * This is always synchronous
+ *
+ * @param aURL The style sheet to be removed.
+ */
+ void removeOverrideStyleSheet(in AString aURL);
+
+ /** Enable or disable the given style sheet from the editor's document
+ * This is always synchronous
+ *
+ * @param aURL The style sheet to be enabled or disabled
+ * @param aEnable true to enable, or false to disable the style sheet
+ */
+ void enableStyleSheet(in AString aURL, in boolean aEnable);
+};
diff --git a/editor/nsIEditorUtils.idl b/editor/nsIEditorUtils.idl
new file mode 100644
index 000000000..95637d281
--- /dev/null
+++ b/editor/nsIEditorUtils.idl
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "domstubs.idl"
+
+interface nsIDOMBlob;
+interface mozIDOMWindowProxy;
+
+[scriptable, uuid(eb8b8ad9-5d8f-43bd-8ce5-5b943c180d56)]
+interface nsIEditorBlobListener : nsISupports
+{
+ void onResult(in ACString aResult);
+ void onError(in AString aErrorName);
+};
+
+/**
+ * A collection of utility functions that editors can use that are more easily
+ * done in JavaScript.
+ */
+[scriptable, uuid(4bf94928-575e-4bd1-8321-a2c4b3d0119e)]
+interface nsIEditorUtils : nsISupports
+{
+ /**
+ * Given a blob, returns the data from that blob, asynchronously.
+ */
+ void slurpBlob(in nsIDOMBlob aBlob, in mozIDOMWindowProxy aScope,
+ in nsIEditorBlobListener aListener);
+};
diff --git a/editor/nsIHTMLAbsPosEditor.idl b/editor/nsIHTMLAbsPosEditor.idl
new file mode 100644
index 000000000..7ecffac60
--- /dev/null
+++ b/editor/nsIHTMLAbsPosEditor.idl
@@ -0,0 +1,132 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "domstubs.idl"
+
+[scriptable, uuid(91375f52-20e6-4757-9835-eb04fabe5498)]
+
+interface nsIHTMLAbsPosEditor : nsISupports
+{
+ /**
+ * true if the selection container is absolutely positioned
+ */
+ readonly attribute boolean selectionContainerAbsolutelyPositioned;
+
+ /**
+ * this contains the absolutely positioned element currently edited
+ * or null
+ */
+ readonly attribute nsIDOMElement positionedElement;
+
+ /**
+ * true if Absolute Positioning handling is enabled in the editor
+ */
+ attribute boolean absolutePositioningEnabled;
+
+
+ /* Utility methods */
+
+ /**
+ * true if Snap To Grid is enabled in the editor.
+ */
+ attribute boolean snapToGridEnabled;
+
+ /**
+ * sets the grid size in pixels.
+ * @param aSizeInPixels [IN] the size of the grid in pixels
+ */
+ attribute unsigned long gridSize;
+
+ /* Selection-based methods */
+
+ /**
+ * returns the deepest absolutely positioned container of the selection
+ * if it exists or null.
+ */
+ readonly attribute nsIDOMElement absolutelyPositionedSelectionContainer;
+
+ /**
+ * extracts the selection from the normal flow of the document and
+ * positions it.
+ * @param aEnabled [IN] true to absolutely position the selection,
+ * false to put it back in the normal flow
+ */
+ void absolutePositionSelection(in boolean aEnabled);
+
+ /**
+ * adds aChange to the z-index of the currently positioned element.
+ * @param aChange [IN] relative change to apply to current z-index
+ */
+ void relativeChangeZIndex(in long aChange);
+
+ /* Element-based methods */
+
+ /**
+ * extracts an element from the normal flow of the document and
+ * positions it, and puts it back in the normal flow.
+ * @param aElement [IN] the element
+ * @param aEnabled [IN] true to absolutely position the element,
+ * false to put it back in the normal flow
+ */
+ void absolutelyPositionElement(in nsIDOMElement aElement,
+ in boolean aEnabled);
+
+ /**
+ * sets the position of an element; warning it does NOT check if the
+ * element is already positioned or not and that's on purpose.
+ * @param aElement [IN] the element
+ * @param aX [IN] the x position in pixels.
+ * @param aY [IN] the y position in pixels.
+ */
+ void setElementPosition(in nsIDOMElement aElement, in long aX, in long aY);
+
+ /**
+ * returns the absolute z-index of a positioned element. Never returns 'auto'.
+ * @return the z-index of the element
+ * @param aElement [IN] the element.
+ */
+ long getElementZIndex(in nsIDOMElement aElement);
+
+ /**
+ * sets the z-index of an element.
+ * @param aElement [IN] the element
+ * @param aZorder [IN] the z-index
+ */
+ void setElementZIndex(in nsIDOMElement aElement, in long aZorder);
+
+ /**
+ * adds aChange to the z-index of an arbitrary element.
+ * @return the new z-index of the element
+ * @param aElement [IN] the element
+ * @param aChange [IN] relative change to apply to current z-index of
+ * the element
+ */
+ long relativeChangeElementZIndex(in nsIDOMElement aElement, in long aChange);
+
+ /* Other */
+
+ /**
+ * shows a grabber attached to an arbitrary element. The grabber is an image
+ * positioned on the left hand side of the top border of the element. Dragging
+ * and dropping it allows to change the element's absolute position in the
+ * document. See chrome://editor/content/images/grabber.gif
+ * @param aElement [IN] the element
+ */
+ void showGrabberOnElement(in nsIDOMElement aElement);
+
+ /**
+ * hide the grabber if it shown.
+ */
+ void hideGrabber();
+
+ /**
+ * refreshes the grabber if it shown, possibly updating its position or
+ * even hiding it.
+ */
+ void refreshGrabber();
+
+};
+
diff --git a/editor/nsIHTMLEditor.idl b/editor/nsIHTMLEditor.idl
new file mode 100644
index 000000000..27bca9dbe
--- /dev/null
+++ b/editor/nsIHTMLEditor.idl
@@ -0,0 +1,559 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "domstubs.idl"
+
+interface nsIAtom;
+interface nsIContent;
+interface nsIArray;
+interface nsISelection;
+interface nsIContentFilter;
+
+%{C++
+namespace mozilla {
+namespace dom {
+class Element;
+}
+}
+%}
+
+[ptr] native Element (mozilla::dom::Element);
+
+[scriptable, uuid(87ee993e-985f-4a43-a974-0d9512da2fb0)]
+interface nsIHTMLEditor : nsISupports
+{
+%{C++
+ typedef short EAlignment;
+%}
+
+ // used by GetAlignment()
+ const short eLeft = 0;
+ const short eCenter = 1;
+ const short eRight = 2;
+ const short eJustify = 3;
+
+
+ /* ------------ Inline property methods -------------- */
+
+
+ /**
+ * AddDefaultProperty() registers a default style property with the editor
+ *
+ * @param aProperty the property to set by default
+ * @param aAttribute the attribute of the property, if applicable.
+ * May be null.
+ * Example: aProperty="font", aAttribute="color"
+ * @param aValue if aAttribute is not null, the value of the attribute.
+ * Example: aProperty="font", aAttribute="color",
+ * aValue="0x00FFFF"
+ */
+ void addDefaultProperty(in nsIAtom aProperty,
+ in AString aAttribute,
+ in AString aValue);
+
+ /**
+ * RemoveDefaultProperty() unregisters a default style property with the editor
+ *
+ * @param aProperty the property to remove from defaults
+ * @param aAttribute the attribute of the property, if applicable.
+ * May be null.
+ * Example: aProperty="font", aAttribute="color"
+ * @param aValue if aAttribute is not null, the value of the attribute.
+ * Example: aProperty="font", aAttribute="color",
+ * aValue="0x00FFFF"
+ */
+ void removeDefaultProperty(in nsIAtom aProperty,
+ in AString aAttribute,
+ in AString aValue);
+
+ /**
+ * RemoveAllDefaultProperties() unregisters all default style properties with the editor
+ *
+ */
+ void removeAllDefaultProperties();
+
+ /**
+ * SetInlineProperty() sets the aggregate properties on the current selection
+ *
+ * @param aProperty the property to set on the selection
+ * @param aAttribute the attribute of the property, if applicable.
+ * May be null.
+ * Example: aProperty="font", aAttribute="color"
+ * @param aValue if aAttribute is not null, the value of the attribute.
+ * May be null.
+ * Example: aProperty="font", aAttribute="color",
+ * aValue="0x00FFFF"
+ */
+ void setInlineProperty(in nsIAtom aProperty,
+ in AString aAttribute,
+ in AString aValue);
+
+ /**
+ * getInlineProperty() gets aggregate properties of the current selection.
+ * All object in the current selection are scanned and their attributes are
+ * represented in a list of Property object.
+ *
+ * @param aProperty the property to get on the selection
+ * @param aAttribute the attribute of the property, if applicable.
+ * May be null.
+ * Example: aProperty="font", aAttribute="color"
+ * @param aValue if aAttribute is not null, the value of the attribute.
+ * May be null.
+ * Example: aProperty="font", aAttribute="color",
+ * aValue="0x00FFFF"
+ * @param aFirst [OUT] PR_TRUE if the first text node in the
+ * selection has the property
+ * @param aAny [OUT] PR_TRUE if any of the text nodes in the
+ * selection have the property
+ * @param aAll [OUT] PR_TRUE if all of the text nodes in the
+ * selection have the property
+ */
+ void getInlineProperty(in nsIAtom aProperty,
+ in AString aAttribute,
+ in AString aValue,
+ out boolean aFirst,
+ out boolean aAny,
+ out boolean aAll);
+
+ AString getInlinePropertyWithAttrValue(in nsIAtom aProperty,
+ in AString aAttribute,
+ in AString aValue,
+ out boolean aFirst,
+ out boolean aAny,
+ out boolean aAll);
+
+ /**
+ * removeAllInlineProperties() deletes all the inline properties from all
+ * text in the current selection.
+ */
+ void removeAllInlineProperties();
+
+
+ /**
+ * removeInlineProperty() deletes the properties from all text in the current
+ * selection. If aProperty is not set on the selection, nothing is done.
+ *
+ * @param aProperty the property to remove from the selection
+ * All atoms are for normal HTML tags (e.g.:
+ * nsIEditorProperty::font) except when you want to
+ * remove just links and not named anchors.
+ * For that, use nsIEditorProperty::href
+ * @param aAttribute the attribute of the property, if applicable.
+ * May be null.
+ * Example: aProperty=nsIEditorProptery::font,
+ * aAttribute="color"
+ * nsIEditProperty::allAttributes is special.
+ * It indicates that all content-based text properties
+ * are to be removed from the selection.
+ */
+ void removeInlineProperty(in nsIAtom aProperty, in AString aAttribute);
+
+ /**
+ * Increase font size for text in selection by 1 HTML unit
+ * All existing text is scanned for existing <FONT SIZE> attributes
+ * so they will be incremented instead of inserting new <FONT> tag
+ */
+ void increaseFontSize();
+
+ /**
+ * Decrease font size for text in selection by 1 HTML unit
+ * All existing text is scanned for existing <FONT SIZE> attributes
+ * so they will be decreased instead of inserting new <FONT> tag
+ */
+ void decreaseFontSize();
+
+ /* ------------ HTML content methods -------------- */
+
+ /**
+ * Tests if a node is a BLOCK element according the the HTML 4.0 DTD.
+ * This does NOT consider CSS effect on display type
+ *
+ * @param aNode the node to test
+ */
+ boolean nodeIsBlock(in nsIDOMNode node);
+
+ /**
+ * Insert some HTML source at the current location
+ *
+ * @param aInputString the string to be inserted
+ */
+ void insertHTML(in AString aInputString);
+
+
+ /**
+ * Paste the text in the OS clipboard at the cursor position, replacing
+ * the selected text (if any), but strip out any HTML styles and formatting
+ */
+ void pasteNoFormatting(in long aSelectionType);
+
+ /**
+ * Rebuild the entire document from source HTML
+ * Needed to be able to edit HEAD and other outside-of-BODY content
+ *
+ * @param aSourceString HTML source string of the entire new document
+ */
+ void rebuildDocumentFromSource(in AString aSourceString);
+
+ /**
+ * Insert some HTML source, interpreting
+ * the string argument according to the given context.
+ *
+ * @param aInputString the string to be inserted
+ * @param aContextStr Context of insertion
+ * @param aInfoStr Related info to aInputString
+ * @param aFlavor Transferable flavor, can be ""
+ * @param aSourceDoc document where input was dragged from (may be null)
+ * @param aDestinationNode location for insertion (such as when dropped)
+ * @param aDestinationOffset used with aDestNode to determine insert location
+ * @param aDeleteSelection used with aDestNode during drag&drop
+ * @param aCollapseSelection used with aDestNode during drag&drop
+ */
+ void insertHTMLWithContext(in AString aInputString,
+ in AString aContextStr,
+ in AString aInfoStr,
+ in AString aFlavor,
+ in nsIDOMDocument aSourceDoc,
+ in nsIDOMNode aDestinationNode,
+ in long aDestinationOffset,
+ in boolean aDeleteSelection);
+
+
+ /**
+ * Insert an element, which may have child nodes, at the selection
+ * Used primarily to insert a new element for various insert element dialogs,
+ * but it enforces the HTML 4.0 DTD "CanContain" rules, so it should
+ * be useful for other elements.
+ *
+ * @param aElement The element to insert
+ * @param aDeleteSelection Delete the selection before inserting
+ * If aDeleteSelection is PR_FALSE, then the element is inserted
+ * after the end of the selection for all element except
+ * Named Anchors, which insert before the selection
+ */
+ void insertElementAtSelection(in nsIDOMElement aElement,
+ in boolean aDeleteSelection);
+
+ /**
+ * Set the documents title.
+ */
+ void setDocumentTitle(in AString aTitle);
+
+ /**
+ * Set the BaseURL for the document to the current URL
+ * but only if the page doesn't have a <base> tag
+ * This should be done after the document URL has changed,
+ * such as after saving a file
+ * This is used as base for relativizing link and image urls
+ */
+ void updateBaseURL();
+
+
+ /* ------------ Selection manipulation -------------- */
+ /* Should these be moved to nsISelection? */
+
+ /**
+ * Set the selection at the suppled element
+ *
+ * @param aElement An element in the document
+ */
+ void selectElement(in nsIDOMElement aElement);
+
+ /**
+ * Create a collapsed selection just after aElement
+ *
+ * XXX could we parameterize SelectElement(before/select/after>?
+ *
+ * The selection is set to parent-of-aElement with an
+ * offset 1 greater than aElement's offset
+ * but it enforces the HTML 4.0 DTD "CanContain" rules, so it should
+ * be useful for other elements.
+ *
+ * @param aElement An element in the document
+ */
+ void setCaretAfterElement(in nsIDOMElement aElement);
+
+ /**
+ * SetParagraphFormat Insert a block paragraph tag around selection
+ * @param aParagraphFormat "p", "h1" to "h6", "address", "pre", or "blockquote"
+ */
+ void setParagraphFormat(in AString aParagraphFormat);
+
+ /**
+ * getParagraphState returns what block tag paragraph format is in
+ * the selection.
+ * @param aMixed True if there is more than one format
+ * @return Name of block tag. "" is returned for none.
+ */
+ AString getParagraphState(out boolean aMixed);
+
+ /**
+ * getFontFaceState returns what font face is in the selection.
+ * @param aMixed True if there is more than one font face
+ * @return Name of face. Note: "tt" is returned for
+ * tt tag. "" is returned for none.
+ */
+ AString getFontFaceState(out boolean aMixed);
+
+ /**
+ * getFontColorState returns what font face is in the selection.
+ * @param aMixed True if there is more than one font color
+ * @return Color string. "" is returned for none.
+ */
+ AString getFontColorState(out boolean aMixed);
+
+ /**
+ * getFontColorState returns what font face is in the selection.
+ * @param aMixed True if there is more than one font color
+ * @return Color string. "" is returned for none.
+ */
+ AString getBackgroundColorState(out boolean aMixed);
+
+ /**
+ * getHighlightColorState returns what the highlight color of the selection.
+ * @param aMixed True if there is more than one font color
+ * @return Color string. "" is returned for none.
+ */
+ AString getHighlightColorState(out boolean aMixed);
+
+ /**
+ * getListState returns what list type is in the selection.
+ * @param aMixed True if there is more than one type of list, or
+ * if there is some list and non-list
+ * @param aOL The company that employs me. No, really, it's
+ * true if an "ol" list is selected.
+ * @param aUL true if an "ul" list is selected.
+ * @param aDL true if a "dl" list is selected.
+ */
+ void getListState(out boolean aMixed, out boolean aOL, out boolean aUL,
+ out boolean aDL);
+
+ /**
+ * getListItemState returns what list item type is in the selection.
+ * @param aMixed True if there is more than one type of list item, or
+ * if there is some list and non-list
+ * @param aLI true if "li" list items are selected.
+ * @param aDT true if "dt" list items are selected.
+ * @param aDD true if "dd" list items are selected.
+ */
+ void getListItemState(out boolean aMixed, out boolean aLI,
+ out boolean aDT, out boolean aDD);
+
+ /**
+ * getAlignment returns what alignment is in the selection.
+ * @param aMixed True if there is more than one type of list item, or
+ * if there is some list and non-list
+ * @param aAlign enum value for first encountered alignment
+ * (left/center/right)
+ */
+ void getAlignment(out boolean aMixed, out short aAlign);
+
+ /**
+ * Document me!
+ *
+ */
+ void getIndentState(out boolean aCanIndent, out boolean aCanOutdent);
+
+ /**
+ * Document me!
+ *
+ */
+ void makeOrChangeList(in AString aListType, in boolean entireList,
+ in AString aBulletType);
+
+ /**
+ * Document me!
+ *
+ */
+ void removeList(in AString aListType);
+
+ /**
+ * Document me!
+ *
+ */
+ void indent(in AString aIndent);
+
+ /**
+ * Document me!
+ *
+ */
+ void align(in AString aAlign);
+
+ /**
+ * Return the input node or a parent matching the given aTagName,
+ * starting the search at the supplied node.
+ * An example of use is for testing if a node is in a table cell
+ * given a selection anchor node.
+ *
+ * @param aTagName The HTML tagname
+ * Special input values:
+ * Use "href" to get a link node
+ * (an "A" tag with the "href" attribute set)
+ * Use "anchor" or "namedanchor" to get a named anchor node
+ * (an "A" tag with the "name" attribute set)
+ * Use "list" to get an OL, UL, or DL list node
+ * Use "td" to get either a TD or TH cell node
+ *
+ * @param aNode The node in the document to start the search.
+ * If it is null, the anchor node of the current selection is used.
+ * @return NS_EDITOR_ELEMENT_NOT_FOUND if an element is not found
+ * (passes NS_SUCCEEDED macro)
+ */
+ nsIDOMElement getElementOrParentByTagName(in AString aTagName,
+ in nsIDOMNode aNode);
+
+ /**
+ * Return an element only if it is the only node selected,
+ * such as an image, horizontal rule, etc.
+ * The exception is a link, which is more like a text attribute:
+ * The Anchor tag is returned if the selection is within the textnode(s)
+ * that are children of the "A" node.
+ * This could be a collapsed selection, i.e., a caret
+ * within the link text.
+ *
+ * @param aTagName The HTML tagname or and empty string
+ * to get any element (but only if it is the only element selected)
+ * Special input values for Links and Named anchors:
+ * Use "href" to get a link node
+ * (an "A" tag with the "href" attribute set)
+ * Use "anchor" or "namedanchor" to get a named anchor node
+ * (an "A" tag with the "name" attribute set)
+ * @return NS_EDITOR_ELEMENT_NOT_FOUND if an element is not found
+ * (passes NS_SUCCEEDED macro)
+ */
+ nsIDOMElement getSelectedElement(in AString aTagName);
+
+ /**
+ * Output the contents of the <HEAD> section as text/HTML format
+ */
+ AString getHeadContentsAsHTML();
+
+ /**
+ * Replace all children of <HEAD> with string of HTML source
+ */
+ void replaceHeadContentsWithHTML(in AString aSourceToInsert);
+
+ /**
+ * Return a new element with default attribute values
+ *
+ * This does not rely on the selection, and is not sensitive to context.
+ *
+ * Used primarily to supply new element for various insert element dialogs
+ * (Image, Link, NamedAnchor, Table, and HorizontalRule
+ * are the only returned elements as of 7/25/99)
+ *
+ * @param aTagName The HTML tagname
+ * Special input values for Links and Named anchors:
+ * Use "href" to get a link node
+ * (an "A" tag with the "href" attribute set)
+ * Use "anchor" or "namedanchor" to get a named anchor node
+ * (an "A" tag with the "name" attribute set)
+ * @return The new element created.
+ */
+ nsIDOMElement createElementWithDefaults(in AString aTagName);
+
+ /**
+ * Insert an link element as the parent of the current selection
+ *
+ * @param aElement An "A" element with a non-empty "href" attribute
+ */
+ void insertLinkAroundSelection(in nsIDOMElement aAnchorElement);
+
+ /**
+ * Set the value of the "bgcolor" attribute on the document's <body> element
+ *
+ * @param aColor The HTML color string, such as "#ffccff" or "yellow"
+ */
+ void setBackgroundColor(in AString aColor);
+
+
+ /**
+ * Set an attribute on the document's <body> element
+ * such as text, link, background colors
+ *
+ * 8/31/00 THIS ISN'T BEING USED? SHOULD WE DROP IT?
+ *
+ * @param aAttr The attribute to be set
+ * @param aValue The value of the attribute
+ */
+ void setBodyAttribute(in AString aAttr, in AString aValue);
+
+ /**
+ * Find all the nodes in the document which contain references
+ * to outside URIs (e.g. a href, img src, script src, etc.)
+ * The objects in the array will be type nsIURIRefObject.
+ *
+ * @return aNodeList the linked nodes found
+ */
+ nsIArray getLinkedObjects();
+
+ /**
+ * A boolean which is true is the HTMLEditor has been instantiated
+ * with CSS knowledge and if the CSS pref is currently checked
+ *
+ * @return true if CSS handled and enabled
+ */
+ attribute boolean isCSSEnabled;
+
+ /**
+ * Add listener for insertion override
+ * @param inFilter function which callers want called during insertion
+ */
+ void addInsertionListener(in nsIContentFilter inFilter);
+
+ /**
+ * Remove listener for insertion override
+ * @param inFilter function which callers do not want called during insertion
+ */
+ void removeInsertionListener(in nsIContentFilter inFilter);
+
+ /**
+ * Returns an anonymous nsDOMElement of type aTag,
+ * child of aParentNode. If aIsCreatedHidden is true, the class
+ * "hidden" is added to the created element. If aAnonClass is not
+ * the empty string, it becomes the value of the attribute "_moz_anonclass"
+ * @return a DOM Element
+ * @param aTag [IN] a string representing the desired type of
+ * the element to create
+ * @param aParentNode [IN] the parent node of the created anonymous
+ * element
+ * @param aAnonClass [IN] contents of the _moz_anonclass attribute
+ * @param aIsCreatedHidden [IN] a boolean specifying if the class "hidden"
+ * is to be added to the created anonymous
+ * element
+ */
+ nsIDOMElement createAnonymousElement(in AString aTag, in nsIDOMNode aParentNode,
+ in AString aAnonClass, in boolean aIsCreatedHidden);
+
+ /**
+ * returns the deepest container of the selection
+ * @return a DOM Element
+ */
+ nsIDOMElement getSelectionContainer();
+
+ /**
+ * Checks if the anonymous nodes created by the HTML editor have to be
+ * refreshed or hidden depending on a possible new state of the selection
+ * @param aSelection [IN] a selection
+ */
+ void checkSelectionStateForAnonymousButtons(in nsISelection aSelection);
+
+ boolean isAnonymousElement(in nsIDOMElement aElement);
+
+ /**
+ * A boolean indicating if a return key pressed in a paragraph creates
+ * another paragraph or just inserts a <br> at the caret
+ *
+ * @return true if CR in a paragraph creates a new paragraph
+ */
+ attribute boolean returnInParagraphCreatesNewParagraph;
+
+ /**
+ * Get an active editor's editing host in DOM window. If this editor isn't
+ * active in the DOM window, this returns NULL.
+ */
+ [noscript, notxpcom] Element GetActiveEditingHost();
+};
+
diff --git a/editor/nsIHTMLInlineTableEditor.idl b/editor/nsIHTMLInlineTableEditor.idl
new file mode 100644
index 000000000..f7b83b8d8
--- /dev/null
+++ b/editor/nsIHTMLInlineTableEditor.idl
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "domstubs.idl"
+
+[scriptable, uuid(eda2e65c-a758-451f-9b05-77cb8de74ed2)]
+
+interface nsIHTMLInlineTableEditor : nsISupports
+{
+ /**
+ * boolean indicating if inline table editing is enabled in the editor.
+ * When inline table editing is enabled, and when the selection is
+ * contained in a table cell, special buttons allowing to add/remove
+ * a line/column are available on the cell's border.
+ */
+ attribute boolean inlineTableEditingEnabled;
+
+ /**
+ * Shows inline table editing UI around a table cell
+ * @param aCell [IN] a DOM Element being a table cell, td or th
+ */
+ void showInlineTableEditingUI(in nsIDOMElement aCell);
+
+ /**
+ * Hide all inline table editing UI
+ */
+ void hideInlineTableEditingUI();
+
+ /**
+ * Modifies the table containing the selection according to the
+ * activation of an inline table editing UI element
+ * @param aUIAnonymousElement [IN] the inline table editing UI element
+ */
+ void doInlineTableEditingAction(in nsIDOMElement aUIAnonymousElement);
+
+ /**
+ * Refresh already visible inline table editing UI
+ */
+ void refreshInlineTableEditingUI();
+};
+
diff --git a/editor/nsIHTMLObjectResizeListener.idl b/editor/nsIHTMLObjectResizeListener.idl
new file mode 100644
index 000000000..42e1c15f1
--- /dev/null
+++ b/editor/nsIHTMLObjectResizeListener.idl
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "domstubs.idl"
+
+[scriptable, uuid(27b00295-349c-429f-ad0c-87b859e77130)]
+
+interface nsIHTMLObjectResizeListener : nsISupports
+{
+ /**
+ * Listener's callback called by the editor when the user
+ * starts resizing an element
+ * @param aElement [IN] the element
+ */
+ void onStartResizing(in nsIDOMElement aElement);
+
+ /**
+ * Listener's callback called by the editor when the user
+ * has finalized the resizing of an element
+ * @param aElement [IN] the element that was resized
+ * @param aOldWidth [IN] the width of the element before resizing
+ * @param aOldHeight [IN] the height of the element before resizing
+ * @param aNewWidth [IN] the width of the element after resizing
+ * @param aNewHeight [IN] the height of the element after resizing
+ */
+ void onEndResizing(in nsIDOMElement aElement,
+ in long aOldWidth, in long aOldHeight,
+ in long aNewWidth, in long aNewHeight);
+
+};
diff --git a/editor/nsIHTMLObjectResizer.idl b/editor/nsIHTMLObjectResizer.idl
new file mode 100644
index 000000000..50201b34e
--- /dev/null
+++ b/editor/nsIHTMLObjectResizer.idl
@@ -0,0 +1,93 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "domstubs.idl"
+
+interface nsIHTMLObjectResizeListener;
+
+[scriptable, uuid(8b396020-69d3-451f-80c1-1a96a7da25a9)]
+
+interface nsIHTMLObjectResizer : nsISupports
+{
+%{C++
+ typedef short EResizerLocation;
+%}
+ const short eTopLeft = 0;
+ const short eTop = 1;
+ const short eTopRight = 2;
+ const short eLeft = 3;
+ const short eRight = 4;
+ const short eBottomLeft = 5;
+ const short eBottom = 6;
+ const short eBottomRight = 7;
+
+ /**
+ * the element currently displaying resizers
+ */
+ readonly attribute nsIDOMElement resizedObject;
+
+ /**
+ * a boolean indicating if object resizing is enabled in the editor
+ */
+ attribute boolean objectResizingEnabled;
+
+ /**
+ * Shows active resizers around an element's frame
+ * @param aResizedElement [IN] a DOM Element
+ */
+ void showResizers(in nsIDOMElement aResizedElement);
+
+ /**
+ * Hide resizers if they are visible
+ */
+ void hideResizers();
+
+ /**
+ * Refresh visible resizers
+ */
+ void refreshResizers();
+
+ /**
+ * event callback when a mouse button is pressed
+ * @param aX [IN] horizontal position of the pointer
+ * @param aY [IN] vertical position of the pointer
+ * @param aTarget [IN] the element triggering the event
+ * @param aMouseEvent [IN] the event
+ */
+ void mouseDown(in long aX, in long aY,
+ in nsIDOMElement aTarget, in nsIDOMEvent aMouseEvent);
+
+ /**
+ * event callback when a mouse button is released
+ * @param aX [IN] horizontal position of the pointer
+ * @param aY [IN] vertical position of the pointer
+ * @param aTarget [IN] the element triggering the event
+ */
+ void mouseUp(in long aX, in long aY,
+ in nsIDOMElement aTarget);
+
+ /**
+ * event callback when the mouse pointer is moved
+ * @param aMouseEvent [IN] the event
+ */
+ void mouseMove(in nsIDOMEvent aMouseEvent);
+
+ /* Event Listeners */
+
+ /**
+ * Creates a resize listener that can be used to get notifications
+ * that the user started to resize an object or finalized such an operation
+ * @param aListener [IN] an instance of nsIHTMLObjectResizeListener
+ */
+ void addObjectResizeEventListener(in nsIHTMLObjectResizeListener aListener);
+
+ /**
+ * Deletes a resize listener
+ * @param aListener [IN] an instance of nsIHTMLObjectResizeListener
+ */
+ void removeObjectResizeEventListener(in nsIHTMLObjectResizeListener aListener);
+};
+
diff --git a/editor/nsIPlaintextEditor.idl b/editor/nsIPlaintextEditor.idl
new file mode 100644
index 000000000..d823a4632
--- /dev/null
+++ b/editor/nsIPlaintextEditor.idl
@@ -0,0 +1,112 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(b74fb158-1265-4102-91eb-edd0136b49f8)]
+interface nsIPlaintextEditor : nsISupports
+{
+
+ // XXX Why aren't these in nsIEditor?
+ // only plain text entry is allowed via events
+ const long eEditorPlaintextMask = 0x0001;
+ // enter key and CR-LF handled specially
+ const long eEditorSingleLineMask = 0x0002;
+ // text is not entered into content, only a representative character
+ const long eEditorPasswordMask = 0x0004;
+ // editing events are disabled. Editor may still accept focus.
+ const long eEditorReadonlyMask = 0x0008;
+ // all events are disabled (like scrolling). Editor will not accept focus.
+ const long eEditorDisabledMask = 0x0010;
+ // text input is limited to certain character types, use mFilter
+ const long eEditorFilterInputMask = 0x0020;
+ // use mail-compose editing rules
+ const long eEditorMailMask = 0x0040;
+ // allow the editor to set font: monospace on the root node
+ const long eEditorEnableWrapHackMask = 0x0080;
+ // bit for widgets (form elements)
+ const long eEditorWidgetMask = 0x0100;
+ // this HTML editor should not create css styles
+ const long eEditorNoCSSMask = 0x0200;
+ // whether HTML document specific actions are executed or not.
+ // e.g., if this flag is set, the editor doesn't handle Tab key.
+ // besides, anchors of HTML are not clickable.
+ const long eEditorAllowInteraction = 0x0400;
+ // when this is set, the characters in password editor are always masked.
+ // see bug 530367 for the detail.
+ const long eEditorDontEchoPassword = 0x0800;
+ // when this flag is set, the internal direction of the editor is RTL.
+ // if neither of the direction flags are set, the direction is determined
+ // from the text control's content node.
+ const long eEditorRightToLeft = 0x1000;
+ // when this flag is set, the internal direction of the editor is LTR.
+ const long eEditorLeftToRight = 0x2000;
+ // when this flag is set, the editor's text content is not spell checked.
+ const long eEditorSkipSpellCheck = 0x4000;
+
+ /*
+ * The valid values for newlines handling.
+ * Can't change the values unless we remove
+ * use of the pref.
+ */
+ const long eNewlinesPasteIntact = 0;
+ const long eNewlinesPasteToFirst = 1;
+ const long eNewlinesReplaceWithSpaces = 2;
+ const long eNewlinesStrip = 3;
+ const long eNewlinesReplaceWithCommas = 4;
+ const long eNewlinesStripSurroundingWhitespace = 5;
+
+ /**
+ * The length of the contents in characters.
+ * XXX change this type to 'unsigned long'
+ */
+ readonly attribute long textLength;
+
+ /**
+ * The maximum number of characters allowed.
+ * default: -1 (unlimited).
+ */
+ attribute long maxTextLength;
+
+ /** Get and set the body wrap width.
+ *
+ * Special values:
+ * 0 = wrap to window width
+ * -1 = no wrap at all
+ */
+ attribute long wrapWidth;
+
+ /**
+ * Similar to the setter for wrapWidth, but just sets the editor
+ * internal state without actually changing the content being edited
+ * to wrap at that column. This should only be used by callers who
+ * are sure that their content is already set up correctly.
+ */
+ void setWrapColumn(in long aWrapColumn);
+
+ /** Get and set newline handling.
+ *
+ * Values are the constants defined above.
+ */
+ attribute long newlineHandling;
+
+ /**
+ * Inserts a string at the current location,
+ * given by the selection.
+ * If the selection is not collapsed, the selection is deleted
+ * and the insertion takes place at the resulting collapsed selection.
+ *
+ * @param aString the string to be inserted
+ */
+ void insertText(in DOMString aStringToInsert);
+
+ /**
+ * Insert a line break into the content model.
+ * The interpretation of a break is up to the implementation:
+ * it may enter a character, split a node in the tree, etc.
+ * This may be more efficient than calling InsertText with a newline.
+ */
+ void insertLineBreak();
+};
diff --git a/editor/nsITableEditor.idl b/editor/nsITableEditor.idl
new file mode 100644
index 000000000..e48062af2
--- /dev/null
+++ b/editor/nsITableEditor.idl
@@ -0,0 +1,337 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+#include "nsISupports.idl"
+
+interface nsIDOMNode;
+interface nsIDOMElement;
+interface nsIDOMRange;
+
+[scriptable, uuid(4805e684-49b9-11d3-9ce4-ed60bd6cb5bc)]
+
+interface nsITableEditor : nsISupports
+{
+ const short eNoSearch = 0;
+ const short ePreviousColumn = 1;
+ const short ePreviousRow = 2;
+
+ /* ------------ Table editing Methods -------------- */
+
+ /** Insert table methods
+ * Insert relative to the selected cell or the
+ * cell enclosing the selection anchor
+ * The selection is collapsed and is left in the new cell
+ * at the same row,col location as the original anchor cell
+ *
+ * @param aNumber Number of items to insert
+ * @param aAfter If TRUE, insert after the current cell,
+ * else insert before current cell
+ */
+ void insertTableCell(in long aNumber, in boolean aAfter);
+ void insertTableColumn(in long aNumber, in boolean aAfter);
+ void insertTableRow(in long aNumber, in boolean aAfter);
+
+ /** Delete table methods
+ * Delete starting at the selected cell or the
+ * cell (or table) enclosing the selection anchor
+ * The selection is collapsed and is left in the
+ * cell at the same row,col location as
+ * the previous selection anchor, if possible,
+ * else in the closest neigboring cell
+ *
+ * @param aNumber Number of items to insert/delete
+ */
+ void deleteTable();
+
+ /** Delete just the cell contents
+ * This is what should happen when Delete key is used
+ * for selected cells, to minimize upsetting the table layout
+ */
+ void deleteTableCellContents();
+
+ /** Delete cell elements as well as contents
+ * @param aNumber Number of contiguous cells, rows, or columns
+ *
+ * When there are more than 1 selected cells, aNumber is ignored.
+ * For Delete Rows or Columns, the complete columns or rows are
+ * determined by the selected cells. E.g., to delete 2 complete rows,
+ * user simply selects a cell in each, and they don't
+ * have to be contiguous.
+ */
+ void deleteTableCell(in long aNumber);
+ void deleteTableColumn(in long aNumber);
+ void deleteTableRow(in long aNumber);
+
+ /** Table Selection methods
+ * Selecting a row or column actually
+ * selects all cells (not TR in the case of rows)
+ */
+ void selectTableCell();
+
+ /** Select a rectangular block of cells:
+ * all cells falling within the row/column index of aStartCell
+ * to through the row/column index of the aEndCell
+ * aStartCell can be any location relative to aEndCell,
+ * as long as they are in the same table
+ * @param aStartCell starting cell in block
+ * @param aEndCell ending cell in block
+ */
+ void selectBlockOfCells(in nsIDOMElement aStartCell,
+ in nsIDOMElement aEndCell);
+
+ void selectTableRow();
+ void selectTableColumn();
+ void selectTable();
+ void selectAllTableCells();
+
+ /** Create a new TD or TH element, the opposite type of the supplied aSourceCell
+ * 1. Copy all attributes from aSourceCell to the new cell
+ * 2. Move all contents of aSourceCell to the new cell
+ * 3. Replace aSourceCell in the table with the new cell
+ *
+ * @param aSourceCell The cell to be replaced
+ * @return The new cell that replaces aSourceCell
+ */
+ nsIDOMElement switchTableCellHeaderType(in nsIDOMElement aSourceCell);
+
+ /** Merges contents of all selected cells
+ * for selected cells that are adjacent,
+ * this will result in a larger cell with appropriate
+ * rowspan and colspan, and original cells are deleted
+ * The resulting cell is in the location of the
+ * cell at the upper-left corner of the adjacent
+ * block of selected cells
+ *
+ * @param aMergeNonContiguousContents:
+ * If true:
+ * Non-contiguous cells are not deleted,
+ * but their contents are still moved
+ * to the upper-left cell
+ * If false: contiguous cells are ignored
+ *
+ * If there are no selected cells,
+ * and selection or caret is in a cell,
+ * that cell and the one to the right
+ * are merged
+ */
+ void joinTableCells(in boolean aMergeNonContiguousContents);
+
+ /** Split a cell that has rowspan and/or colspan > 0
+ * into cells such that all new cells have
+ * rowspan = 1 and colspan = 1
+ * All of the contents are not touched --
+ * they will appear to be in the upper-left cell
+ */
+ void splitTableCell();
+
+ /** Scan through all rows and add cells as needed so
+ * all locations in the cellmap are occupied.
+ * Used after inserting single cells or pasting
+ * a collection of cells that extend past the
+ * previous size of the table
+ * If aTable is null, it uses table enclosing the selection anchor
+ * This doesn't doesn't change the selection,
+ * thus it can be used to fixup all tables
+ * in a page independent of the selection
+ */
+ void normalizeTable(in nsIDOMElement aTable);
+
+ /** Get the row an column index from the layout's cellmap
+ * If aCell is null, it will try to find enclosing table of selection anchor
+ *
+ */
+ void getCellIndexes(in nsIDOMElement aCell,
+ out long aRowIndex, out long aColIndex);
+
+ /** Get the number of rows and columns in a table from the layout's cellmap
+ * If aTable is null, it will try to find enclosing table of selection ancho
+ * Note that all rows in table will not have this many because of
+ * ROWSPAN effects or if table is not "rectangular" (has short rows)
+ */
+ void getTableSize(in nsIDOMElement aTable,
+ out long aRowCount, out long aColCount);
+
+ /** Get a cell element at cellmap grid coordinates
+ * A cell that spans across multiple cellmap locations will
+ * be returned multiple times, once for each location it occupies
+ *
+ * @param aTable A table in the document
+ * @param aRowIndex, aColIndex The 0-based cellmap indexes
+ *
+ * (in C++ returns: NS_EDITOR_ELEMENT_NOT_FOUND if an element is not found
+ * passes NS_SUCCEEDED macro)
+ *
+ * You can scan for all cells in a row or column
+ * by iterating through the appropriate indexes
+ * until the returned aCell is null
+ */
+ nsIDOMElement getCellAt(in nsIDOMElement aTable,
+ in long aRowIndex, in long aColIndex);
+
+ /** Get a cell at cellmap grid coordinates and associated data
+ * A cell that spans across multiple cellmap locations will
+ * be returned multiple times, once for each location it occupies
+ * Examine the returned aStartRowIndex and aStartColIndex to see
+ * if it is in the same layout column or layout row:
+ * A "layout row" is all cells sharing the same top edge
+ * A "layout column" is all cells sharing the same left edge
+ * This is important to determine what to do when inserting or deleting a column or row
+ *
+ * @param aTable A table in the document
+ * @param aRowIndex, aColIndex The 0-based cellmap indexes
+ * returns values:
+ * @param aCell The cell at this cellmap location
+ * @param aStartRowIndex The row index where cell starts
+ * @param aStartColIndex The col index where cell starts
+ * @param aRowSpan May be 0 (to span down entire table) or number of cells spanned
+ * @param aColSpan May be 0 (to span across entire table) or number of cells spanned
+ * @param aActualRowSpan The actual number of cellmap locations (rows) spanned by the cell
+ * @param aActualColSpan The actual number of cellmap locations (columns) spanned by the cell
+ * @param aIsSelected
+ * @param
+ *
+ * (in C++ returns: NS_EDITOR_ELEMENT_NOT_FOUND if an element is not found
+ * passes NS_SUCCEEDED macro)
+ */
+ void getCellDataAt(in nsIDOMElement aTable,
+ in long aRowIndex, in long aColIndex,
+ out nsIDOMElement aCell,
+ out long aStartRowIndex, out long aStartColIndex,
+ out long aRowSpan, out long aColSpan,
+ out long aActualRowSpan, out long aActualColSpan,
+ out boolean aIsSelected);
+
+ /** Get the first row element in a table
+ *
+ * @return The row at the requested index
+ * Returns null if there are no rows in table
+ * (in C++ returns: NS_EDITOR_ELEMENT_NOT_FOUND if an element is not found
+ * passes NS_SUCCEEDED macro)
+ */
+ nsIDOMNode getFirstRow(in nsIDOMElement aTableElement);
+
+ /** Get the next row element starting the search from aTableElement
+ *
+ * @param aTableElement Any TR or child-of-TR element in the document
+ *
+ * @return The row to start search from
+ * and the row returned from the search
+ * Returns null if there isn't another row
+ * (in C++ returns: NS_EDITOR_ELEMENT_NOT_FOUND if an element is not found
+ * passes NS_SUCCEEDED macro)
+ */
+ nsIDOMNode getNextRow(in nsIDOMNode aTableElement);
+
+ /** Preferred direction to search for neighboring cell
+ * when trying to locate a cell to place caret in after
+ * a table editing action.
+ * Used for aDirection param in SetSelectionAfterTableEdit
+ */
+
+ /** Reset a selected cell or collapsed selection (the caret) after table editing
+ *
+ * @param aTable A table in the document
+ * @param aRow The row ...
+ * @param aCol ... and column defining the cell
+ * where we will try to place the caret
+ * @param aSelected If true, we select the whole cell instead of setting caret
+ * @param aDirection If cell at (aCol, aRow) is not found,
+ * search for previous cell in the same
+ * column (aPreviousColumn) or row (ePreviousRow)
+ * or don't search for another cell (aNoSearch)
+ * If no cell is found, caret is place just before table;
+ * and if that fails, at beginning of document.
+ * Thus we generally don't worry about the return value
+ * and can use the nsSetSelectionAfterTableEdit stack-based
+ * object to insure we reset the caret in a table-editing method.
+ */
+ void setSelectionAfterTableEdit(in nsIDOMElement aTable,
+ in long aRow, in long aCol,
+ in long aDirection, in boolean aSelected);
+
+ /** Examine the current selection and find
+ * a selected TABLE, TD or TH, or TR element.
+ * or return the parent TD or TH if selection is inside a table cell
+ * Returns null if no table element is found.
+ *
+ * @param aTagName The tagname of returned element
+ * Note that "td" will be returned if name
+ * is actually "th"
+ * @param aCount How many table elements were selected
+ * This tells us if we have multiple cells selected
+ * (0 if element is a parent cell of selection)
+ * @return The table element (table, row, or first selected cell)
+ *
+ */
+ nsIDOMElement getSelectedOrParentTableElement(out AString aTagName, out long aCount);
+
+ /** Generally used after GetSelectedOrParentTableElement
+ * to test if selected cells are complete rows or columns
+ *
+ * @param aElement Any table or cell element or any element
+ * inside a table
+ * Used to get enclosing table.
+ * If null, selection's anchorNode is used
+ *
+ * @return
+ * 0 aCellElement was not a cell
+ * (returned result = NS_ERROR_FAILURE)
+ * TABLESELECTION_CELL There are 1 or more cells selected but
+ * complete rows or columns are not selected
+ * TABLESELECTION_ROW All cells are in 1 or more rows
+ * and in each row, all cells selected
+ * Note: This is the value if all rows
+ * (thus all cells) are selected
+ * TABLESELECTION_COLUMN All cells are in 1 or more columns
+ * and in each column, all cells are selected
+ */
+ uint32_t getSelectedCellsType(in nsIDOMElement aElement);
+
+ /** Get first selected element from first selection range.
+ * (If multiple cells were selected this is the first in the order they were selected)
+ * Assumes cell-selection model where each cell
+ * is in a separate range (selection parent node is table row)
+ * @param aCell [OUT] Selected cell or null if ranges don't contain
+ * cell selections
+ * @param aRange [OUT] Optional: if not null, return the selection range
+ * associated with the cell
+ * Returns the DOM cell element
+ * (in C++: returns NS_EDITOR_ELEMENT_NOT_FOUND if an element is not found
+ * passes NS_SUCCEEDED macro)
+ */
+ nsIDOMElement getFirstSelectedCell(out nsIDOMRange aRange);
+
+ /** Get first selected element in the table
+ * This is the upper-left-most selected cell in table,
+ * ignoring the order that the user selected them (order in the selection ranges)
+ * Assumes cell-selection model where each cell
+ * is in a separate range (selection parent node is table row)
+ * @param aCell Selected cell or null if ranges don't contain
+ * cell selections
+ * @param aRowIndex Optional: if not null, return row index of 1st cell
+ * @param aColIndex Optional: if not null, return column index of 1st cell
+ *
+ * Returns the DOM cell element
+ * (in C++: returns NS_EDITOR_ELEMENT_NOT_FOUND if an element is not found
+ * passes NS_SUCCEEDED macro)
+ */
+ nsIDOMElement getFirstSelectedCellInTable(out long aRowIndex, out long aColIndex);
+
+ /** Get next selected cell element from first selection range.
+ * Assumes cell-selection model where each cell
+ * is in a separate range (selection parent node is table row)
+ * Always call GetFirstSelectedCell() to initialize stored index of "next" cell
+ * @param aCell Selected cell or null if no more selected cells
+ * or ranges don't contain cell selections
+ * @param aRange Optional: if not null, return the selection range
+ * associated with the cell
+ *
+ * Returns the DOM cell element
+ * (in C++: returns NS_EDITOR_ELEMENT_NOT_FOUND if an element is not found
+ * passes NS_SUCCEEDED macro)
+ */
+ nsIDOMElement getNextSelectedCell(out nsIDOMRange aRange);
+};
diff --git a/editor/nsIURIRefObject.idl b/editor/nsIURIRefObject.idl
new file mode 100644
index 000000000..da53dd74d
--- /dev/null
+++ b/editor/nsIURIRefObject.idl
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "domstubs.idl"
+
+interface nsIDOMNode;
+
+/** A class which can represent any node which points to an
+ * external URI, e.g. <a>, <img>, <script> etc,
+ * and has the capability to rewrite URLs to be
+ * relative or absolute.
+ * Used by the editor but not dependant on it.
+ */
+
+[scriptable, uuid(2226927e-1dd2-11b2-b57f-faab47288563)]
+
+interface nsIURIRefObject : nsISupports
+{
+ attribute nsIDOMNode node;
+
+ /**
+ * Go back to the beginning of the attribute list.
+ */
+ void Reset();
+
+ /**
+ * Return the next rewritable URI.
+ */
+ DOMString GetNextURI();
+
+ /**
+ * Go back to the beginning of the attribute list
+ *
+ * @param aOldPat Old pattern to be replaced, e.g. file:///a/b/
+ * @param aNewPat New pattern to be replaced, e.g. http://mypage.aol.com/
+ * @param aMakeRel Rewrite links as relative vs. absolute
+ */
+ void RewriteAllURIs(in DOMString aOldPat, in DOMString aNewPat,
+ in boolean aMakeRel);
+};
diff --git a/editor/nsPIEditorTransaction.idl b/editor/nsPIEditorTransaction.idl
new file mode 100644
index 000000000..99ecdb891
--- /dev/null
+++ b/editor/nsPIEditorTransaction.idl
@@ -0,0 +1,20 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+/*
+ * The nsPIEditorTransaction interface.
+ * <P>
+ * This interface is implemented by every transaction created by this
+ * editor implementation. It is used to help us distinguish our internal
+ * transactions from those generated by 3rd party extensions.
+ */
+[scriptable, uuid(4f18ada2-0ddc-11d5-9d3a-0060b0f8baff)]
+interface nsPIEditorTransaction : nsISupports
+{
+ readonly attribute DOMString txnDescription;
+};
+
diff --git a/editor/reftests/1088158-ref.html b/editor/reftests/1088158-ref.html
new file mode 100644
index 000000000..be4592e69
--- /dev/null
+++ b/editor/reftests/1088158-ref.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<textarea placeholder="placeholder"></textarea>
diff --git a/editor/reftests/1088158.html b/editor/reftests/1088158.html
new file mode 100644
index 000000000..435aa3f63
--- /dev/null
+++ b/editor/reftests/1088158.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<script>
+ onload = function() {
+ var t = document.createElement('textarea');
+ t.placeholder = "placeholder";
+ document.body.appendChild(t.cloneNode(true));
+ }
+</script>
diff --git a/editor/reftests/338427-1-ref.html b/editor/reftests/338427-1-ref.html
new file mode 100644
index 000000000..d645ad9fb
--- /dev/null
+++ b/editor/reftests/338427-1-ref.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <textarea spellcheck="false" lang="testing-XX">strangeimpossibleword</textarea>
+</body>
+</html>
+
diff --git a/editor/reftests/338427-1.html b/editor/reftests/338427-1.html
new file mode 100644
index 000000000..7a645a224
--- /dev/null
+++ b/editor/reftests/338427-1.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <!-- invalid language will default to en-US, but no spell check since element is not focussed -->
+ <textarea lang="testing-XX">strangeimpossibleword</textarea>
+</body>
+</html>
diff --git a/editor/reftests/338427-2-ref.html b/editor/reftests/338427-2-ref.html
new file mode 100644
index 000000000..30a468c3e
--- /dev/null
+++ b/editor/reftests/338427-2-ref.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+function init() {
+ var editor = document.getElementById('editor');
+ editor.addEventListener("focus", function() {
+ window.setTimeout(function() {
+ document.documentElement.className = '';
+ }, 0);
+ }, false);
+ editor.focus();
+}
+</script>
+<body onload="init()">
+ <!-- invalid language will default to en-US -->
+ <div id="editor" lang="testing-XX" contenteditable="true" spellcheck="false">good possible word</div>
+</body>
+</html>
+
diff --git a/editor/reftests/338427-2.html b/editor/reftests/338427-2.html
new file mode 100644
index 000000000..7f29e91fb
--- /dev/null
+++ b/editor/reftests/338427-2.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+function init() {
+ var editor = document.getElementById('editor');
+ editor.addEventListener("focus", function() {
+ window.setTimeout(function() {
+ document.documentElement.className = '';
+ }, 0);
+ }, false);
+ editor.focus();
+}
+</script>
+<body onload="init()">
+ <!-- invalid language will default to en-US -->
+ <div id="editor" lang="testing-XX" contenteditable="true">good possible word</div>
+</body>
+</html>
diff --git a/editor/reftests/338427-3-ref.html b/editor/reftests/338427-3-ref.html
new file mode 100644
index 000000000..b72ccc997
--- /dev/null
+++ b/editor/reftests/338427-3-ref.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+function init() {
+ var editor = document.getElementById('editor');
+ // invalid language will default to en-US
+ editor.setAttribute('lang', 'testing-XX');
+ editor.addEventListener("focus", function() {
+ window.setTimeout(function() {
+ document.documentElement.className = '';
+ }, 0);
+ }, false);
+ editor.focus();
+}
+</script>
+<body onload="init()">
+ <textarea id="editor" spellcheck="false" lang="en-US">good possible word</textarea>
+</body>
+</html>
diff --git a/editor/reftests/338427-3.html b/editor/reftests/338427-3.html
new file mode 100644
index 000000000..6aead81da
--- /dev/null
+++ b/editor/reftests/338427-3.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+function init() {
+ var editor = document.getElementById('editor');
+ // invalid language will default to en-US
+ editor.setAttribute('lang', 'testing-XX');
+ editor.addEventListener("focus", function() {
+ window.setTimeout(function() {
+ document.documentElement.className = '';
+ }, 0);
+ }, false);
+ editor.focus();
+}
+</script>
+<body onload="init()">
+ <textarea id="editor" lang="en-US">good possible word</textarea>
+</body>
+</html>
diff --git a/editor/reftests/388980-1-ref.html b/editor/reftests/388980-1-ref.html
new file mode 100644
index 000000000..8b14d7e18
--- /dev/null
+++ b/editor/reftests/388980-1-ref.html
@@ -0,0 +1,25 @@
+<html>
+<head>
+<title>Reftest for bug 388980</title></html>
+<script type="text/javascript">
+
+var text = '<html><head></head><body style="font-size:16px;">'
+ + '<p><span style="background-color:red;">This paragraph should be red</span></p>'
+ + '<p><span style="background-color:blue;">This paragraph should be blue</span></p>'
+ + '<p>This paragraph should not be colored</p>'
+ + '</body></html>';
+
+function initIFrame() {
+ var doc = document.getElementById('theIFrame').contentDocument;
+ doc.designMode = 'on';
+ doc.open('text/html');
+ doc.write(text);
+ doc.close();
+}
+</script>
+</head>
+<body onload="initIFrame()" >
+<iframe id="theIFrame">
+</iframe>
+</body>
+</html>
diff --git a/editor/reftests/388980-1.html b/editor/reftests/388980-1.html
new file mode 100644
index 000000000..f2e7d0de0
--- /dev/null
+++ b/editor/reftests/388980-1.html
@@ -0,0 +1,43 @@
+<html>
+<head>
+<title>Reftest for bug 388980</title></html>
+<script type="text/javascript">
+
+var text = '<html><head></head><body style="font-size:16px;">'
+ + '<p id="redpar">This paragraph should be red</p>'
+ + '<p id="bluepar">This paragraph should be blue</p>'
+ + '<p>This paragraph should not be colored</p>'
+ +'</body></html>';
+
+
+function colorPar(par, color) {
+ var doc = document.getElementById('theIFrame').contentDocument;
+ var win = document.getElementById('theIFrame').contentWindow;
+ win.getSelection().selectAllChildren(doc.getElementById(par));
+ doc.execCommand("hilitecolor", false, color);
+ win.getSelection().removeAllRanges();
+}
+
+function initIFrame() {
+ var doc = document.getElementById('theIFrame').contentDocument;
+ doc.designMode = 'on';
+ doc.open('text/html');
+ doc.write(text);
+ doc.close();
+
+ // Test hilighting with styleWithCSS, should hilight the text...
+ doc.execCommand("styleWithCSS", false, true);
+ colorPar("redpar", "red");
+
+ // Test highlighting without styleWithCSS, should also work.
+ doc.execCommand("styleWithCSS", false, false);
+ colorPar("bluepar", "blue");
+
+}
+</script>
+</head>
+<body>
+<iframe id="theIFrame" onload="initIFrame()">
+</iframe>
+</body>
+</html>
diff --git a/editor/reftests/462758-grabbers-resizers-ref.html b/editor/reftests/462758-grabbers-resizers-ref.html
new file mode 100644
index 000000000..5627cde74
--- /dev/null
+++ b/editor/reftests/462758-grabbers-resizers-ref.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <script type="text/javascript">
+ function init() {
+ var editor = document.querySelector("div[contenteditable]");
+ editor.addEventListener("focus", function() {
+ setTimeout(function() {
+ document.documentElement.className = "";
+ }, 0);
+ }, false);
+ editor.focus();
+ }
+ </script>
+ <style type="text/css">
+ html, body, div {
+ margin: 0;
+ padding: 0;
+ }
+ div {
+ border: 1px solid black;
+ margin: 50px;
+ height: 200px;
+ width: 200px;
+ }
+ </style>
+</head>
+<body onload="init()">
+ <div contenteditable>
+ this editable container should be neither draggable nor resizable.
+ </div>
+</body>
+</html>
+
diff --git a/editor/reftests/462758-grabbers-resizers.html b/editor/reftests/462758-grabbers-resizers.html
new file mode 100644
index 000000000..66a8d8482
--- /dev/null
+++ b/editor/reftests/462758-grabbers-resizers.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <script type="text/javascript">
+ function init() {
+ var editor = document.querySelector("div[contenteditable]");
+ editor.addEventListener("focus", function() {
+ setTimeout(function() {
+ document.documentElement.className = "";
+ }, 0);
+ }, false);
+ editor.focus();
+ }
+ </script>
+ <style type="text/css">
+ html, body, div {
+ margin: 0;
+ padding: 0;
+ }
+ div {
+ border: 1px solid black;
+ margin: 50px;
+ height: 200px;
+ width: 200px;
+ }
+ </style>
+</head>
+<body onload="init()">
+ <div contenteditable style="position: absolute">
+ this editable container should be neither draggable nor resizable.
+ </div>
+</body>
+</html>
diff --git a/editor/reftests/642800-iframe.html b/editor/reftests/642800-iframe.html
new file mode 100644
index 000000000..bb1ab6397
--- /dev/null
+++ b/editor/reftests/642800-iframe.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <style>
+ @media only screen and (max-width: 480px) {
+ .overflow-hidden
+ {
+ overflow: hidden;
+ }
+
+ .float-left
+ {
+ float: left;
+ background: #f0f;
+ }
+ }
+ </style>
+</head>
+<body>
+ <h1>Iframe content</h1>
+ <div class="float-left">
+ <textarea>This text should be visible when window is resized </textarea>
+
+ </div>
+ <div class="overflow-hidden">
+ <textarea>This text should be visible when window is resized </textarea>
+ </div>
+</body>
+</html>
diff --git a/editor/reftests/642800-ref.html b/editor/reftests/642800-ref.html
new file mode 100644
index 000000000..f062b145b
--- /dev/null
+++ b/editor/reftests/642800-ref.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<body>
+ <iframe onload="document.documentElement.className=''" src="642800-iframe.html" id="iframe" style="width: 500px; height: 200px"></iframe>
+</body>
+</html>
+
diff --git a/editor/reftests/642800.html b/editor/reftests/642800.html
new file mode 100644
index 000000000..f2af58923
--- /dev/null
+++ b/editor/reftests/642800.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <script type="text/javascript">
+ function reframe(node) {
+ node.style.display = "none";
+ document.body.offsetWidth;
+ node.style.display = "block";
+ document.documentElement.className='';
+ }
+ </script>
+</head>
+<body>
+ <iframe onload="reframe(this)" src="642800-iframe.html" id="iframe" style="width: 500px; height: 200px"></iframe>
+
+</body>
+</html>
+
diff --git a/editor/reftests/672709-ref.html b/editor/reftests/672709-ref.html
new file mode 100644
index 000000000..b9fc369b0
--- /dev/null
+++ b/editor/reftests/672709-ref.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <body>
+ <style>
+ :-moz-read-only { color: red; }
+ :-moz-read-write { color: green; }
+ </style>
+ <script>
+ onload = function() {
+ document.designMode = "on";
+ var p = document.createElement("p");
+ p.textContent = "test";
+ document.getElementById("x").appendChild(p);
+ getSelection().removeAllRanges(); // don't need a caret
+ document.documentElement.removeAttribute("class");
+ };
+ </script>
+ <div contenteditable id="x">
+ </div>
+ more test
+ </body>
+</html>
diff --git a/editor/reftests/672709.html b/editor/reftests/672709.html
new file mode 100644
index 000000000..d42c54b0c
--- /dev/null
+++ b/editor/reftests/672709.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <style>
+ body { color: green; }
+ </style>
+ <div>
+ <p>test</p>
+ </div>
+ more test
+ </body>
+</html>
diff --git a/editor/reftests/674212-spellcheck-ref.html b/editor/reftests/674212-spellcheck-ref.html
new file mode 100644
index 000000000..2b2a6dfc6
--- /dev/null
+++ b/editor/reftests/674212-spellcheck-ref.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="en-US" class="reftest-wait">
+<head>
+ <script type="text/javascript">
+ function init() {
+ var editor = document.querySelector("div[contenteditable]");
+ editor.addEventListener("focus", function() {
+ setTimeout(function() {
+ document.documentElement.className = "";
+ }, 0);
+ }, false);
+ editor.focus();
+ }
+ </script>
+</head>
+<body onload="init()">
+ <div contenteditable spellcheck>This is another misspellored word.</div>
+</body>
+</html>
+
diff --git a/editor/reftests/674212-spellcheck.html b/editor/reftests/674212-spellcheck.html
new file mode 100644
index 000000000..f22f5c453
--- /dev/null
+++ b/editor/reftests/674212-spellcheck.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="en-US" class="reftest-wait">
+<head>
+ <script type="text/javascript">
+ function init() {
+ var editor = document.querySelector("div[contenteditable]");
+ editor.addEventListener("focus", function() {
+ editor.textContent = "This is another misspellored word.";
+ setTimeout(function() {
+ document.documentElement.className = "";
+ }, 0);
+ }, false);
+ editor.focus();
+ }
+ </script>
+</head>
+<body onload="init()">
+ <div contenteditable spellcheck>This is a misspellored word.</div>
+</body>
+</html>
diff --git a/editor/reftests/694880-1.html b/editor/reftests/694880-1.html
new file mode 100644
index 000000000..373c3070a
--- /dev/null
+++ b/editor/reftests/694880-1.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+ <style>
+ :-moz-read-only { color: green; }
+ :-moz-read-write { color: red; }
+ </style>
+ <body onload="document.designMode='on';document.designMode='off'">
+ <div>test</div>
+ </body>
+</html>
diff --git a/editor/reftests/694880-2.html b/editor/reftests/694880-2.html
new file mode 100644
index 000000000..9f2617883
--- /dev/null
+++ b/editor/reftests/694880-2.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+ <style>
+ :-moz-read-only { color: green; }
+ :-moz-read-write { color: red; }
+ </style>
+ <body onload="document.designMode='on';document.designMode='off'">
+ <div>test</div>
+ <div contenteditable></div>
+ </body>
+</html>
diff --git a/editor/reftests/694880-3.html b/editor/reftests/694880-3.html
new file mode 100644
index 000000000..c6d7837f7
--- /dev/null
+++ b/editor/reftests/694880-3.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+ <style>
+ :-moz-read-only { color: red; }
+ :-moz-read-write { color: green; }
+ </style>
+ <body onload="document.designMode='on';document.designMode='off'">
+ <div contenteditable>test</div>
+ </body>
+</html>
diff --git a/editor/reftests/694880-ref.html b/editor/reftests/694880-ref.html
new file mode 100644
index 000000000..d5c40547e
--- /dev/null
+++ b/editor/reftests/694880-ref.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <style>
+ div { color: green; }
+ </style>
+ <body>
+ <div>test</div>
+ </body>
+</html>
diff --git a/editor/reftests/824080-1-ref.html b/editor/reftests/824080-1-ref.html
new file mode 100644
index 000000000..8a8786472
--- /dev/null
+++ b/editor/reftests/824080-1-ref.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="text/javascript">
+ function doTest()
+ {
+ var editor = document.getElementById("editor");
+ document.getSelection().selectAllChildren(document.body);
+ }
+ </script>
+</head>
+<body onload="doTest();">
+<p>normal text</p>
+<div id="editor">editable text</div>
+</body>
+</html>
+
diff --git a/editor/reftests/824080-1.html b/editor/reftests/824080-1.html
new file mode 100644
index 000000000..2dfe7e2c6
--- /dev/null
+++ b/editor/reftests/824080-1.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="text/javascript">
+ function doTest()
+ {
+ var editor = document.getElementById("editor");
+ editor.focus();
+ editor.blur();
+ document.getSelection().selectAllChildren(document.body);
+ }
+ </script>
+</head>
+<body onload="doTest();">
+<p>normal text</p>
+<div id="editor" contenteditable>editable text</div>
+</body>
+</html>
+
diff --git a/editor/reftests/824080-2-ref.html b/editor/reftests/824080-2-ref.html
new file mode 100644
index 000000000..597e09fdc
--- /dev/null
+++ b/editor/reftests/824080-2-ref.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="text/javascript">
+ function doTest()
+ {
+ document.getSelection().selectAllChildren(document.getElementById("text"));
+ var editor = document.getElementById("editor");
+ var editorBody = editor.contentDocument.body;
+ editor.contentDocument.getSelection().selectAllChildren(editorBody);
+ editor.focus();
+ editor.blur();
+ }
+ </script>
+</head>
+<body>
+<p id="text">normal text</p>
+<iframe id="editor" onload="doTest();"
+ src="data:text/html,<body>editable text</body>"></iframe>
+</body>
+</html>
+
diff --git a/editor/reftests/824080-2.html b/editor/reftests/824080-2.html
new file mode 100644
index 000000000..c4dc9afc6
--- /dev/null
+++ b/editor/reftests/824080-2.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="text/javascript">
+ function doTest()
+ {
+ document.getSelection().selectAllChildren(document.getElementById("text"));
+ var editor = document.getElementById("editor");
+ var editorBody = editor.contentDocument.body;
+ editor.contentDocument.getSelection().selectAllChildren(editorBody);
+ editor.focus();
+ editor.blur();
+ }
+ </script>
+</head>
+<body>
+<p id="text">normal text</p>
+<iframe id="editor" onload="doTest();"
+ src="data:text/html,<script>document.designMode='on';</script><body>editable text</body>"></iframe>
+</body>
+</html>
+
diff --git a/editor/reftests/824080-3-ref.html b/editor/reftests/824080-3-ref.html
new file mode 100644
index 000000000..8849fc383
--- /dev/null
+++ b/editor/reftests/824080-3-ref.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="text/javascript">
+ function doTest()
+ {
+ document.getSelection().selectAllChildren(document.getElementById("text"));
+ var editor = document.getElementById("editor");
+ var editorBody = editor.contentDocument.body;
+ editor.contentDocument.getSelection().selectAllChildren(editorBody);
+ editor.focus();
+ }
+ </script>
+</head>
+<body>
+<p id="text">normal text</p>
+<iframe id="editor" onload="doTest();"
+ src="data:text/html,<body>editable text</body>"></iframe>
+</body>
+</html>
+
diff --git a/editor/reftests/824080-3.html b/editor/reftests/824080-3.html
new file mode 100644
index 000000000..b9882e5bb
--- /dev/null
+++ b/editor/reftests/824080-3.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="text/javascript">
+ function doTest()
+ {
+ document.getSelection().selectAllChildren(document.getElementById("text"));
+ var editor = document.getElementById("editor");
+ var editorBody = editor.contentDocument.body;
+ editor.contentDocument.getSelection().selectAllChildren(editorBody);
+ editor.focus();
+ }
+ </script>
+</head>
+<body>
+<p id="text">normal text</p>
+<iframe id="editor" onload="doTest();"
+ src="data:text/html,<script>document.designMode='on';</script><body>editable text</body>"></iframe>
+</body>
+</html>
+
diff --git a/editor/reftests/824080-4-ref.html b/editor/reftests/824080-4-ref.html
new file mode 100644
index 000000000..3bee66458
--- /dev/null
+++ b/editor/reftests/824080-4-ref.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="text/javascript">
+ function doTest()
+ {
+ document.getSelection().selectAllChildren(document.body);
+ var editor = document.getElementById("editor");
+ var editorBody = editor.contentDocument.body;
+ editor.contentDocument.getSelection().selectAllChildren(editorBody);
+ }
+ </script>
+</head>
+<body>
+<p id="text">normal text</p>
+<div>content editable</div>
+<iframe id="editor" onload="doTest();"
+ src="data:text/html,<body>editable text</body>"></iframe>
+</body>
+</html>
+
diff --git a/editor/reftests/824080-4.html b/editor/reftests/824080-4.html
new file mode 100644
index 000000000..14cf4eedf
--- /dev/null
+++ b/editor/reftests/824080-4.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="text/javascript">
+ function doTest()
+ {
+ var editor1 = document.getElementById("editor1");
+ editor1.focus();
+ editor1.blur();
+ document.getSelection().selectAllChildren(document.body);
+ var editor2 = document.getElementById("editor2");
+ var editorBody = editor2.contentDocument.body;
+ editor2.contentDocument.getSelection().selectAllChildren(editorBody);
+ editor2.focus();
+ editor2.blur();
+ }
+ </script>
+</head>
+<body>
+<p>normal text</p>
+<div id="editor1" contenteditable>content editable</div>
+<iframe id="editor2" onload="doTest();"
+ src="data:text/html,<script>document.designMode='on';</script><body>editable text</body>"></iframe>
+</body>
+</html>
+
diff --git a/editor/reftests/824080-5-ref.html b/editor/reftests/824080-5-ref.html
new file mode 100644
index 000000000..237fea213
--- /dev/null
+++ b/editor/reftests/824080-5-ref.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="text/javascript">
+ function doTest()
+ {
+ document.getSelection().selectAllChildren(document.body);
+ var editor = document.getElementById("editor");
+ var editorBody = editor.contentDocument.body;
+ editor.contentDocument.getSelection().selectAllChildren(editorBody);
+ editor.focus();
+ }
+ </script>
+</head>
+<body>
+<p id="text">normal text</p>
+<div>content editable</div>
+<iframe id="editor" onload="doTest();"
+ src="data:text/html,<body>editable text</body>"></iframe>
+</body>
+</html>
+
diff --git a/editor/reftests/824080-5.html b/editor/reftests/824080-5.html
new file mode 100644
index 000000000..30771d63f
--- /dev/null
+++ b/editor/reftests/824080-5.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="text/javascript">
+ function doTest()
+ {
+ var editor1 = document.getElementById("editor1");
+ editor1.focus();
+ editor1.blur();
+ document.getSelection().selectAllChildren(document.body);
+ var editor2 = document.getElementById("editor2");
+ var editorBody = editor2.contentDocument.body;
+ editor2.contentDocument.getSelection().selectAllChildren(editorBody);
+ editor2.focus();
+ }
+ </script>
+</head>
+<body>
+<p>normal text</p>
+<div id="editor1" contenteditable>content editable</div>
+<iframe id="editor2" onload="doTest();"
+ src="data:text/html,<script>document.designMode='on';</script><body>editable text</body>"></iframe>
+</body>
+</html>
+
diff --git a/editor/reftests/824080-6-ref.html b/editor/reftests/824080-6-ref.html
new file mode 100644
index 000000000..7813c74f1
--- /dev/null
+++ b/editor/reftests/824080-6-ref.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="text/javascript">
+ function doTest()
+ {
+ var editor = document.getElementById("editor");
+ editor.focus();
+ editor.blur();
+ }
+ </script>
+</head>
+<body onload="doTest()">
+<p>normal text</p>
+<textarea id="editor">textarea</textarea>
+</body>
+</html>
+
diff --git a/editor/reftests/824080-6.html b/editor/reftests/824080-6.html
new file mode 100644
index 000000000..54bafd680
--- /dev/null
+++ b/editor/reftests/824080-6.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="text/javascript">
+ function doTest()
+ {
+ document.getSelection().selectAllChildren(document.body);
+ var editor = document.getElementById("editor");
+ editor.focus();
+ editor.select();
+ editor.blur();
+ }
+ </script>
+</head>
+<body onload="doTest()">
+<p>normal text</p>
+<textarea id="editor">textarea</textarea>
+</body>
+</html>
+
diff --git a/editor/reftests/824080-7-ref.html b/editor/reftests/824080-7-ref.html
new file mode 100644
index 000000000..10162cb1f
--- /dev/null
+++ b/editor/reftests/824080-7-ref.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="text/javascript">
+ function doTest()
+ {
+ var editor = document.getElementById("editor");
+ editor.focus();
+ editor.selectionStart = 2;
+ editor.selectionEnd = 4;
+ }
+ </script>
+</head>
+<body onload="doTest()">
+<p>normal text</p>
+<textarea id="editor">textarea</textarea>
+</body>
+</html>
+
diff --git a/editor/reftests/824080-7.html b/editor/reftests/824080-7.html
new file mode 100644
index 000000000..d09e1b5ba
--- /dev/null
+++ b/editor/reftests/824080-7.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="text/javascript">
+ function doTest()
+ {
+ document.getSelection().selectAllChildren(document.body);
+ var editor = document.getElementById("editor");
+ editor.focus();
+ editor.selectionStart = 2;
+ editor.selectionEnd = 4;
+ editor.blur();
+ editor.focus();
+ }
+ </script>
+</head>
+<body onload="doTest()">
+<p>normal text</p>
+<textarea id="editor">textarea</textarea>
+</body>
+</html>
+
diff --git a/editor/reftests/911201-ref.html b/editor/reftests/911201-ref.html
new file mode 100644
index 000000000..323613ca2
--- /dev/null
+++ b/editor/reftests/911201-ref.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<body contenteditable><div contenteditable=false>foo</div></body>
diff --git a/editor/reftests/911201.html b/editor/reftests/911201.html
new file mode 100644
index 000000000..6b78e1cd2
--- /dev/null
+++ b/editor/reftests/911201.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<body contenteditable onload="document.body.innerHTML='<div contenteditable=false>foo</div>';"></body>
diff --git a/editor/reftests/969773-ref.html b/editor/reftests/969773-ref.html
new file mode 100644
index 000000000..888ed5186
--- /dev/null
+++ b/editor/reftests/969773-ref.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <meta charset="utf-8">
+ <title>Contenteditable Selection Test Case</title>
+ <script>
+ function runTests() {
+ var text = document.getElementById("text");
+
+ text.focus();
+
+ setTimeout(function () {
+ document.body.offsetHeight;
+ document.documentElement.removeAttribute('class');
+ }, 0);
+ }
+ document.addEventListener('MozReftestInvalidate', runTests, false);
+ </script>
+</head>
+<body>
+ <div>This is a contenteditable.</div>
+ <div id="text" tabindex="0">This is focusable text</div>
+</body>
+</html>
diff --git a/editor/reftests/969773.html b/editor/reftests/969773.html
new file mode 100644
index 000000000..1202c1555
--- /dev/null
+++ b/editor/reftests/969773.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <meta charset="utf-8">
+ <title>Contenteditable Selection Test Case</title>
+ <script>
+ function runTests() {
+ var editable = document.getElementById("editable");
+ var text = document.getElementById("text");
+
+ editable.focus();
+
+ setTimeout(function () {
+ editable.setAttribute("contenteditable", "false");
+ text.focus();
+ setTimeout(function () {
+ document.body.offsetHeight;
+ document.documentElement.removeAttribute('class');
+ }, 0);
+ }, 0);
+ }
+ document.addEventListener('MozReftestInvalidate', runTests, false);
+ </script>
+</head>
+<body>
+ <div id="editable" contenteditable="true" tabindex="0" spellcheck="false">This is a contenteditable.</div>
+ <div id="text" tabindex="0">This is focusable text</div>
+</body>
+</html>
diff --git a/editor/reftests/997805-ref.html b/editor/reftests/997805-ref.html
new file mode 100644
index 000000000..be4592e69
--- /dev/null
+++ b/editor/reftests/997805-ref.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<textarea placeholder="placeholder"></textarea>
diff --git a/editor/reftests/997805.html b/editor/reftests/997805.html
new file mode 100644
index 000000000..91750138b
--- /dev/null
+++ b/editor/reftests/997805.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<textarea placeholder="placeholder"></textarea>
+<script>
+onload = function() {
+ var t = document.querySelector("textarea");
+ t.style.display = "none";
+ t.value = "test";
+ setTimeout(function() {
+ t.style.display = "";
+ t.value = "";
+ document.documentElement.className = "";
+ }, 0);
+};
+</script>
+</html>
diff --git a/editor/reftests/caret_after_reframe-ref.html b/editor/reftests/caret_after_reframe-ref.html
new file mode 100644
index 000000000..63c49f66c
--- /dev/null
+++ b/editor/reftests/caret_after_reframe-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <input autofocus style="display:block">
+ </body>
+</html>
diff --git a/editor/reftests/caret_after_reframe.html b/editor/reftests/caret_after_reframe.html
new file mode 100644
index 000000000..5e9c0f133
--- /dev/null
+++ b/editor/reftests/caret_after_reframe.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <body>
+ <input onfocus="focused()" autofocus>
+ <script>
+ function focused() {
+ var i = document.querySelector("input");
+ i.style.display = "block";
+ document.offsetWidth;
+ document.documentElement.removeAttribute("class");
+ }
+ </script>
+ </body>
+</html>
diff --git a/editor/reftests/caret_on_focus-ref.html b/editor/reftests/caret_on_focus-ref.html
new file mode 100644
index 000000000..4282ac7f5
--- /dev/null
+++ b/editor/reftests/caret_on_focus-ref.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <style>
+ div { min-height: 20px; }
+ </style>
+ </head>
+ <body onload="document.querySelector('div').focus();">
+ <div contenteditable="true"></div>
+ </body>
+</html>
diff --git a/editor/reftests/caret_on_focus.html b/editor/reftests/caret_on_focus.html
new file mode 100644
index 000000000..6dcedb4a4
--- /dev/null
+++ b/editor/reftests/caret_on_focus.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <style>
+ div { min-height: 20px; }
+ </style>
+ </head>
+ <body onload="document.querySelector('div').focus();">
+ <div contenteditable="true"> </div>
+ </body>
+</html>
diff --git a/editor/reftests/caret_on_positioned-ref.html b/editor/reftests/caret_on_positioned-ref.html
new file mode 100644
index 000000000..04773979d
--- /dev/null
+++ b/editor/reftests/caret_on_positioned-ref.html
@@ -0,0 +1,8 @@
+<html><head>
+<title>caret should be visible on stack context contents</title>
+</head><body>
+<div id="d" style="width: 100px; height: 100px; background: none repeat scroll 0% 0% cyan;" contenteditable=""></div>
+<script>
+document.getElementById("d").focus();
+</script>
+</body></html> \ No newline at end of file
diff --git a/editor/reftests/caret_on_positioned.html b/editor/reftests/caret_on_positioned.html
new file mode 100644
index 000000000..8a4a3c2f3
--- /dev/null
+++ b/editor/reftests/caret_on_positioned.html
@@ -0,0 +1,8 @@
+<html><head>
+<title>caret should be visible on stack context contents</title>
+</head><body>
+<div id="d" style="position: absolute; width: 100px; height: 100px; background: none repeat scroll 0% 0% cyan;" contenteditable=""></div>
+<script>
+document.getElementById("d").focus();
+</script>
+</body></html> \ No newline at end of file
diff --git a/editor/reftests/caret_on_presshell_reinit-2.html b/editor/reftests/caret_on_presshell_reinit-2.html
new file mode 100644
index 000000000..5acf3c97b
--- /dev/null
+++ b/editor/reftests/caret_on_presshell_reinit-2.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <body>
+ <iframe src="data:text/html,<body><div></div></body>"></iframe>
+ <script type="text/javascript">
+ onload = function() {
+ var i = document.querySelector("iframe");
+ var win = i.contentWindow;
+ var doc = win.document;
+ var div = doc.querySelector("div");
+ win.getSelection().collapse(div, 0);
+ i.focus();
+ div.contentEditable = true;
+ div.focus();
+ setTimeout(function() {
+ var span = doc.createElement("span");
+ span.appendChild(doc.createTextNode("foo"));
+ div.appendChild(span);
+ div.style.outlineWidth = 0; // remove the focus outline
+ i.style.position = "absolute";
+ document.body.clientWidth;
+ document.documentElement.removeAttribute("class");
+ }, 0);
+ };
+ </script>
+ </body>
+</html>
diff --git a/editor/reftests/caret_on_presshell_reinit-ref.html b/editor/reftests/caret_on_presshell_reinit-ref.html
new file mode 100644
index 000000000..db5b8d620
--- /dev/null
+++ b/editor/reftests/caret_on_presshell_reinit-ref.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <div style="position: absolute">
+ <iframe src="data:text/html,<body contenteditable>foo</body>"></iframe>
+ </div>
+ <script type="text/javascript">
+ onload = function() {
+ var iframe = document.querySelector("iframe");
+ var win = iframe.contentWindow;
+ var body = win.document.body;
+ iframe.focus();
+ body.focus();
+ var sel = win.getSelection();
+ sel.collapse(body.firstChild, 0);
+ };
+ </script>
+ </body>
+</html>
diff --git a/editor/reftests/caret_on_presshell_reinit.html b/editor/reftests/caret_on_presshell_reinit.html
new file mode 100644
index 000000000..2d872383a
--- /dev/null
+++ b/editor/reftests/caret_on_presshell_reinit.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <div>
+ <iframe src="data:text/html,<body contenteditable>foo</body>"></iframe>
+ </div>
+ <script type="text/javascript">
+ onload = function() {
+ var div = document.querySelector("div");
+ div.style.position = "absolute";
+ document.body.clientWidth;
+ var iframe = document.querySelector("iframe");
+ var win = iframe.contentWindow;
+ var body = win.document.body;
+ iframe.focus();
+ body.focus();
+ var sel = win.getSelection();
+ sel.collapse(body.firstChild, 0);
+ };
+ </script>
+ </body>
+</html>
diff --git a/editor/reftests/caret_on_textarea_lastline-ref.html b/editor/reftests/caret_on_textarea_lastline-ref.html
new file mode 100644
index 000000000..9c1040255
--- /dev/null
+++ b/editor/reftests/caret_on_textarea_lastline-ref.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<body onload="loaded()">
+<script>
+ function loaded() {
+ var t = document.querySelector('textarea');
+ t.selectionStart = t.selectionEnd = t.value.length;
+ t.focus();
+ }
+</script>
+<textarea>foo</textarea>
+</body>
+</html>
diff --git a/editor/reftests/caret_on_textarea_lastline.html b/editor/reftests/caret_on_textarea_lastline.html
new file mode 100644
index 000000000..24a53bd7d
--- /dev/null
+++ b/editor/reftests/caret_on_textarea_lastline.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<body onload="loaded()">
+<script>
+ function loaded() {
+ var t = document.querySelector('textarea');
+ t.selectionStart = t.selectionEnd = t.value.length;
+ t.focus();
+ }
+</script>
+<textarea>foo
+</textarea>
+</body>
+</html>
diff --git a/editor/reftests/dynamic-1.html b/editor/reftests/dynamic-1.html
new file mode 100644
index 000000000..5f2b8b7dc
--- /dev/null
+++ b/editor/reftests/dynamic-1.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="text">
+ <script>
+ document.getElementsByTagName("input")[0].value = "abcdef";
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/dynamic-overflow-change-ref.html b/editor/reftests/dynamic-overflow-change-ref.html
new file mode 100644
index 000000000..52e5f5bb0
--- /dev/null
+++ b/editor/reftests/dynamic-overflow-change-ref.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea rows="2" style="overflow: hidden;">
+ this
+ is
+ a
+ textarea
+ with
+ overflow
+ </textarea>
+ </body>
+</html>
diff --git a/editor/reftests/dynamic-overflow-change.html b/editor/reftests/dynamic-overflow-change.html
new file mode 100644
index 000000000..57a1b8f74
--- /dev/null
+++ b/editor/reftests/dynamic-overflow-change.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+ <body onload="document.querySelector('textarea').style.overflow='hidden'">
+ <textarea rows="2">
+ this
+ is
+ a
+ textarea
+ with
+ overflow
+ </textarea>
+ </body>
+</html>
diff --git a/editor/reftests/dynamic-ref.html b/editor/reftests/dynamic-ref.html
new file mode 100644
index 000000000..07882ee7a
--- /dev/null
+++ b/editor/reftests/dynamic-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="text" value="abcdef">
+</body>
+</html>
diff --git a/editor/reftests/dynamic-type-1.html b/editor/reftests/dynamic-type-1.html
new file mode 100644
index 000000000..fb0c3ec68
--- /dev/null
+++ b/editor/reftests/dynamic-type-1.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="checkbox">
+ <script>
+ var i = document.getElementsByTagName("input")[0];
+ i.type = "text";
+ i.value = "abcdef";
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/dynamic-type-2.html b/editor/reftests/dynamic-type-2.html
new file mode 100644
index 000000000..4d99ac06e
--- /dev/null
+++ b/editor/reftests/dynamic-type-2.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="checkbox">
+ <script>
+ var i = document.getElementsByTagName("input")[0];
+ i.value = "abcdef";
+ i.type = "text";
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/dynamic-type-3.html b/editor/reftests/dynamic-type-3.html
new file mode 100644
index 000000000..7cf5be6ab
--- /dev/null
+++ b/editor/reftests/dynamic-type-3.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="checkbox" value="foo">
+ <script>
+ var i = document.getElementsByTagName("input")[0];
+ i.type = "text";
+ i.value = "abcdef";
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/dynamic-type-4.html b/editor/reftests/dynamic-type-4.html
new file mode 100644
index 000000000..7cf5be6ab
--- /dev/null
+++ b/editor/reftests/dynamic-type-4.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="checkbox" value="foo">
+ <script>
+ var i = document.getElementsByTagName("input")[0];
+ i.type = "text";
+ i.value = "abcdef";
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/emptypasswd-1.html b/editor/reftests/emptypasswd-1.html
new file mode 100644
index 000000000..86775633b
--- /dev/null
+++ b/editor/reftests/emptypasswd-1.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="password">
+</body>
+</html>
diff --git a/editor/reftests/emptypasswd-2.html b/editor/reftests/emptypasswd-2.html
new file mode 100644
index 000000000..6e33f46b1
--- /dev/null
+++ b/editor/reftests/emptypasswd-2.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="password" value="abcdef">
+ <script>
+ document.getElementsByTagName("input")[0].value = "";
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/emptypasswd-ref.html b/editor/reftests/emptypasswd-ref.html
new file mode 100644
index 000000000..7f09f6e8b
--- /dev/null
+++ b/editor/reftests/emptypasswd-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="text">
+</body>
+</html>
diff --git a/editor/reftests/input-text-notheme-onfocus-reframe-ref.html b/editor/reftests/input-text-notheme-onfocus-reframe-ref.html
new file mode 100644
index 000000000..e97f55f16
--- /dev/null
+++ b/editor/reftests/input-text-notheme-onfocus-reframe-ref.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+ <title>bug 536421</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+<style>
+input { border:1px solid blue; }
+</style>
+</head>
+<body onload="doTest()">
+ <input value="test" id="textbox" onfocus="triggerBug();" type="text">
+ <script type="text/javascript">
+ function finishTest()
+ {
+ document.documentElement.removeAttribute("class");
+ }
+ function triggerBug()
+ {
+ finishTest();
+ }
+ function doTest()
+ {
+ var t = document.getElementById("textbox");
+ t.focus();
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/input-text-notheme-onfocus-reframe.html b/editor/reftests/input-text-notheme-onfocus-reframe.html
new file mode 100644
index 000000000..19bc273d1
--- /dev/null
+++ b/editor/reftests/input-text-notheme-onfocus-reframe.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+ <title>bug 536421</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+<style>
+input { border:1px solid blue; }
+</style>
+</head>
+<body onload="doTest()">
+ <input value="test" id="textbox" onfocus="triggerBug();" type="text">
+ <script type="text/javascript">
+ function finishTest()
+ {
+ document.documentElement.removeAttribute("class");
+ }
+ function triggerBug()
+ {
+ var t = document.getElementById("textbox");
+ t.style.display = "none";
+ document.body.offsetWidth;
+ t.style.display = "";
+ finishTest();
+ }
+ function doTest()
+ {
+ var t = document.getElementById("textbox");
+ t.focus();
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/input-text-onfocus-reframe-ref.html b/editor/reftests/input-text-onfocus-reframe-ref.html
new file mode 100644
index 000000000..e17757897
--- /dev/null
+++ b/editor/reftests/input-text-onfocus-reframe-ref.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+ <title>bug 536421</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+</head>
+<body onload="doTest()">
+ <input value="test" id="textbox" onfocus="triggerBug();" type="text">
+ <script type="text/javascript">
+ function finishTest()
+ {
+ document.documentElement.removeAttribute("class");
+ }
+ function triggerBug()
+ {
+ finishTest();
+ }
+ function doTest()
+ {
+ var t = document.getElementById("textbox");
+ t.focus();
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/input-text-onfocus-reframe.html b/editor/reftests/input-text-onfocus-reframe.html
new file mode 100644
index 000000000..339ef95c6
--- /dev/null
+++ b/editor/reftests/input-text-onfocus-reframe.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+ <title>bug 536421</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+</head>
+<body onload="doTest()">
+ <input value="test" id="textbox" onfocus="triggerBug();" type="text">
+ <script type="text/javascript">
+ function finishTest()
+ {
+ document.documentElement.removeAttribute("class");
+ }
+ function triggerBug()
+ {
+ var t = document.getElementById("textbox");
+ t.style.display = "none";
+ document.body.offsetWidth;
+ t.style.display = "";
+ finishTest();
+ }
+ function doTest()
+ {
+ var t = document.getElementById("textbox");
+ t.focus();
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/newline-1.html b/editor/reftests/newline-1.html
new file mode 100644
index 000000000..5a7ce8c19
--- /dev/null
+++ b/editor/reftests/newline-1.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="text" value="aaa&#10;bbb">
+</body>
+</html>
diff --git a/editor/reftests/newline-2.html b/editor/reftests/newline-2.html
new file mode 100644
index 000000000..7965bc860
--- /dev/null
+++ b/editor/reftests/newline-2.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="text" value="&#10;aaa bbb">
+</body>
+</html>
diff --git a/editor/reftests/newline-3.html b/editor/reftests/newline-3.html
new file mode 100644
index 000000000..18760df4c
--- /dev/null
+++ b/editor/reftests/newline-3.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="text" value="aaa bbb&#10;">
+</body>
+</html>
diff --git a/editor/reftests/newline-4.html b/editor/reftests/newline-4.html
new file mode 100644
index 000000000..2f51eaa20
--- /dev/null
+++ b/editor/reftests/newline-4.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="text" value="a&#10;a&#10;a&#10; &#10;b&#10;b&#10;b">
+</body>
+</html>
diff --git a/editor/reftests/newline-ref.html b/editor/reftests/newline-ref.html
new file mode 100644
index 000000000..3630626dd
--- /dev/null
+++ b/editor/reftests/newline-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="text" value="aaa bbb">
+</body>
+</html>
diff --git a/editor/reftests/nobogusnode-1.html b/editor/reftests/nobogusnode-1.html
new file mode 100644
index 000000000..450d6b1e5
--- /dev/null
+++ b/editor/reftests/nobogusnode-1.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body contenteditable>
+ This is a test.
+</body>
+</html>
diff --git a/editor/reftests/nobogusnode-2.html b/editor/reftests/nobogusnode-2.html
new file mode 100644
index 000000000..532e59740
--- /dev/null
+++ b/editor/reftests/nobogusnode-2.html
@@ -0,0 +1,5 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<body contenteditable="true">
+ This is a test.
+</body>
+</html>
diff --git a/editor/reftests/nobogusnode-ref.html b/editor/reftests/nobogusnode-ref.html
new file mode 100644
index 000000000..052a53b51
--- /dev/null
+++ b/editor/reftests/nobogusnode-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ This is a test.
+</body>
+</html>
diff --git a/editor/reftests/passwd-1.html b/editor/reftests/passwd-1.html
new file mode 100644
index 000000000..f6f21d84f
--- /dev/null
+++ b/editor/reftests/passwd-1.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="password" value="123456">
+</body>
+</html>
diff --git a/editor/reftests/passwd-2.html b/editor/reftests/passwd-2.html
new file mode 100644
index 000000000..07882ee7a
--- /dev/null
+++ b/editor/reftests/passwd-2.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="text" value="abcdef">
+</body>
+</html>
diff --git a/editor/reftests/passwd-3.html b/editor/reftests/passwd-3.html
new file mode 100644
index 000000000..3e1e715eb
--- /dev/null
+++ b/editor/reftests/passwd-3.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="password">
+ <script>
+ document.getElementsByTagName("input")[0].value = "abcdef";
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/passwd-4.html b/editor/reftests/passwd-4.html
new file mode 100644
index 000000000..607a22ae4
--- /dev/null
+++ b/editor/reftests/passwd-4.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<!--
+Make sure that focusing a password text element does not
+cause a non-breaking space character to show up.
+-->
+<html class="reftest-wait">
+<body onload="loaded()">
+ <input type="password">
+ <script>
+ function loaded() {
+ var i = document.getElementsByTagName("input")[0];
+ i.focus();
+ i.value += "abcdef";
+ i.blur();
+ document.documentElement.className = "";
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/passwd-ref.html b/editor/reftests/passwd-ref.html
new file mode 100644
index 000000000..b203fa7d5
--- /dev/null
+++ b/editor/reftests/passwd-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="password" value="abcdef">
+</body>
+</html>
diff --git a/editor/reftests/readonly-editable-ref.html b/editor/reftests/readonly-editable-ref.html
new file mode 100644
index 000000000..99f1e5101
--- /dev/null
+++ b/editor/reftests/readonly-editable-ref.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <input>
+ <input readonly>
+ <input type=password>
+ <input type=password readonly>
+ <input type=email>
+ <input type=email readonly>
+ <textarea></textarea>
+ <textarea readonly></textarea>
+ </body>
+</html>
diff --git a/editor/reftests/readonly-editable.html b/editor/reftests/readonly-editable.html
new file mode 100644
index 000000000..49210e581
--- /dev/null
+++ b/editor/reftests/readonly-editable.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <style>
+ :-moz-read-write + span {
+ display: none;
+ }
+ span {
+ color: transparent; /* workaround for bug 617524 */
+ outline: 1px solid green;
+ }
+ </style>
+ </head>
+ <body contenteditable>
+ <input><span>hide me</span>
+ <input readonly><span>hide me</span>
+ <input type=password><span>hide me</span>
+ <input type=password readonly><span>hide me</span>
+ <input type=email><span>hide me</span>
+ <input type=email readonly><span>hide me</span>
+ <textarea></textarea><span>hide me</span>
+ <textarea readonly></textarea><span>hide me</span>
+ </body>
+</html>
diff --git a/editor/reftests/readonly-non-editable-ref.html b/editor/reftests/readonly-non-editable-ref.html
new file mode 100644
index 000000000..a91071e42
--- /dev/null
+++ b/editor/reftests/readonly-non-editable-ref.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <style>
+ span {
+ color: transparent; /* workaround for bug 617524 */
+ outline: 1px solid green;
+ }
+ </style>
+ </head>
+ <body>
+ <input><span>hide me</span>
+ <input readonly>
+ <input type=password><span>hide me</span>
+ <input type=password readonly>
+ <input type=email><span>hide me</span>
+ <input type=email readonly>
+ <textarea></textarea><span>hide me</span>
+ <textarea readonly></textarea>
+ </body>
+</html>
diff --git a/editor/reftests/readonly-non-editable.html b/editor/reftests/readonly-non-editable.html
new file mode 100644
index 000000000..9766045ed
--- /dev/null
+++ b/editor/reftests/readonly-non-editable.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <style>
+ :-moz-read-only + span {
+ display: none;
+ }
+ span {
+ color: transparent; /* workaround for bug 617524 */
+ outline: 1px solid green;
+ }
+ </style>
+ </head>
+ <body>
+ <input><span>hide me</span>
+ <input readonly><span>hide me</span>
+ <input type=password><span>hide me</span>
+ <input type=password readonly><span>hide me</span>
+ <input type=email><span>hide me</span>
+ <input type=email readonly><span>hide me</span>
+ <textarea></textarea><span>hide me</span>
+ <textarea readonly></textarea><span>hide me</span>
+ </body>
+</html>
diff --git a/editor/reftests/readwrite-editable-ref.html b/editor/reftests/readwrite-editable-ref.html
new file mode 100644
index 000000000..99f1e5101
--- /dev/null
+++ b/editor/reftests/readwrite-editable-ref.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <input>
+ <input readonly>
+ <input type=password>
+ <input type=password readonly>
+ <input type=email>
+ <input type=email readonly>
+ <textarea></textarea>
+ <textarea readonly></textarea>
+ </body>
+</html>
diff --git a/editor/reftests/readwrite-editable.html b/editor/reftests/readwrite-editable.html
new file mode 100644
index 000000000..49210e581
--- /dev/null
+++ b/editor/reftests/readwrite-editable.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <style>
+ :-moz-read-write + span {
+ display: none;
+ }
+ span {
+ color: transparent; /* workaround for bug 617524 */
+ outline: 1px solid green;
+ }
+ </style>
+ </head>
+ <body contenteditable>
+ <input><span>hide me</span>
+ <input readonly><span>hide me</span>
+ <input type=password><span>hide me</span>
+ <input type=password readonly><span>hide me</span>
+ <input type=email><span>hide me</span>
+ <input type=email readonly><span>hide me</span>
+ <textarea></textarea><span>hide me</span>
+ <textarea readonly></textarea><span>hide me</span>
+ </body>
+</html>
diff --git a/editor/reftests/readwrite-non-editable-ref.html b/editor/reftests/readwrite-non-editable-ref.html
new file mode 100644
index 000000000..12e1c46c0
--- /dev/null
+++ b/editor/reftests/readwrite-non-editable-ref.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <style>
+ span {
+ color: transparent; /* workaround for bug 617524 */
+ outline: 1px solid green;
+ }
+ </style>
+ </head>
+ <body>
+ <input>
+ <input readonly><span>hide me</span>
+ <input type=password>
+ <input type=password readonly><span>hide me</span>
+ <input type=email>
+ <input type=email readonly><span>hide me</span>
+ <textarea></textarea>
+ <textarea readonly></textarea><span>hide me</span>
+ </body>
+</html>
diff --git a/editor/reftests/readwrite-non-editable.html b/editor/reftests/readwrite-non-editable.html
new file mode 100644
index 000000000..535f21f1a
--- /dev/null
+++ b/editor/reftests/readwrite-non-editable.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <style>
+ :-moz-read-write + span {
+ display: none;
+ }
+ span {
+ color: transparent; /* workaround for bug 617524 */
+ outline: 1px solid green;
+ }
+ </style>
+ </head>
+ <body>
+ <input><span>hide me</span>
+ <input readonly><span>hide me</span>
+ <input type=password><span>hide me</span>
+ <input type=password readonly><span>hide me</span>
+ <input type=email><span>hide me</span>
+ <input type=email readonly><span>hide me</span>
+ <textarea></textarea><span>hide me</span>
+ <textarea readonly></textarea><span>hide me</span>
+ </body>
+</html>
diff --git a/editor/reftests/reftest-stylo.list b/editor/reftests/reftest-stylo.list
new file mode 100644
index 000000000..ce42a4d40
--- /dev/null
+++ b/editor/reftests/reftest-stylo.list
@@ -0,0 +1,177 @@
+# DO NOT EDIT! This is a auto-generated temporary list for Stylo testing
+# include the XUL reftests
+include xul/reftest-stylo.list
+
+== newline-1.html newline-1.html
+== newline-2.html newline-2.html
+== newline-3.html newline-3.html
+== newline-4.html newline-4.html
+== dynamic-1.html dynamic-1.html
+== dynamic-type-1.html dynamic-type-1.html
+== dynamic-type-2.html dynamic-type-2.html
+== dynamic-type-3.html dynamic-type-3.html
+== dynamic-type-4.html dynamic-type-4.html
+== passwd-1.html passwd-1.html
+== passwd-2.html passwd-2.html
+== passwd-3.html passwd-3.html
+needs-focus == passwd-4.html passwd-4.html
+== emptypasswd-1.html emptypasswd-1.html
+== emptypasswd-2.html emptypasswd-2.html
+== caret_on_positioned.html caret_on_positioned.html
+skip-if(B2G||Mulet) fails-if(Android) needs-focus == spellcheck-input-disabled.html spellcheck-input-disabled.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+== spellcheck-input-attr-before.html spellcheck-input-attr-before.html
+skip-if(B2G||Mulet) fails-if(Android) needs-focus == spellcheck-input-attr-before.html spellcheck-input-attr-before.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+== spellcheck-input-attr-after.html spellcheck-input-attr-after.html
+skip-if(B2G||Mulet) fails-if(Android) needs-focus == spellcheck-input-attr-after.html spellcheck-input-attr-after.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+== spellcheck-input-attr-inherit.html spellcheck-input-attr-inherit.html
+skip-if(B2G||Mulet) fails-if(Android) needs-focus == spellcheck-input-attr-inherit.html spellcheck-input-attr-inherit.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+== spellcheck-input-attr-dynamic.html spellcheck-input-attr-dynamic.html
+skip-if(B2G||Mulet) fails-if(Android) needs-focus == spellcheck-input-attr-dynamic.html spellcheck-input-attr-dynamic.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+== spellcheck-input-attr-dynamic-inherit.html spellcheck-input-attr-dynamic-inherit.html
+skip-if(B2G||Mulet) fails-if(Android) needs-focus == spellcheck-input-attr-dynamic-inherit.html spellcheck-input-attr-dynamic-inherit.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+== spellcheck-input-property-dynamic.html spellcheck-input-property-dynamic.html
+skip-if(B2G||Mulet) fails-if(Android) needs-focus == spellcheck-input-property-dynamic.html spellcheck-input-property-dynamic.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+== spellcheck-input-property-dynamic-inherit.html spellcheck-input-property-dynamic-inherit.html
+skip-if(B2G||Mulet) fails-if(Android) needs-focus == spellcheck-input-property-dynamic-inherit.html spellcheck-input-property-dynamic-inherit.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+== spellcheck-input-attr-dynamic-override.html spellcheck-input-attr-dynamic-override.html
+skip-if(B2G||Mulet) fails-if(Android) needs-focus == spellcheck-input-attr-dynamic-override.html spellcheck-input-attr-dynamic-override.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+== spellcheck-input-attr-dynamic-override-inherit.html spellcheck-input-attr-dynamic-override-inherit.html
+skip-if(B2G||Mulet) fails-if(Android) needs-focus == spellcheck-input-attr-dynamic-override-inherit.html spellcheck-input-attr-dynamic-override-inherit.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+== spellcheck-input-property-dynamic-override.html spellcheck-input-property-dynamic-override.html
+skip-if(B2G||Mulet) fails-if(Android) needs-focus == spellcheck-input-property-dynamic-override.html spellcheck-input-property-dynamic-override.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+== spellcheck-input-property-dynamic-override-inherit.html spellcheck-input-property-dynamic-override-inherit.html
+skip-if(B2G||Mulet) fails-if(Android) needs-focus == spellcheck-input-property-dynamic-override-inherit.html spellcheck-input-property-dynamic-override-inherit.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+== spellcheck-textarea-attr.html spellcheck-textarea-attr.html
+#the random-if(Android) tests pass on android native, but fail on android-xul, see bug 728942
+skip-if(B2G||Mulet) random-if(Android) needs-focus == spellcheck-textarea-attr.html spellcheck-textarea-attr.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+needs-focus == spellcheck-textarea-focused.html spellcheck-textarea-focused.html
+needs-focus == spellcheck-textarea-focused-reframe.html spellcheck-textarea-focused-reframe.html
+needs-focus == spellcheck-textarea-focused-notreadonly.html spellcheck-textarea-focused-notreadonly.html
+skip-if(B2G||Mulet) random-if(Android) needs-focus == spellcheck-textarea-nofocus.html spellcheck-textarea-nofocus.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+skip-if(B2G||Mulet) random-if(Android) needs-focus == spellcheck-textarea-disabled.html spellcheck-textarea-disabled.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+skip-if(B2G||Mulet) random-if(Android) needs-focus == spellcheck-textarea-attr-inherit.html spellcheck-textarea-attr-inherit.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+skip-if(B2G||Mulet) random-if(Android) needs-focus == spellcheck-textarea-attr-dynamic.html spellcheck-textarea-attr-dynamic.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+skip-if(B2G||Mulet) random-if(Android) needs-focus == spellcheck-textarea-attr-dynamic-inherit.html spellcheck-textarea-attr-dynamic-inherit.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+skip-if(B2G||Mulet) random-if(Android) needs-focus == spellcheck-textarea-property-dynamic.html spellcheck-textarea-property-dynamic.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+skip-if(B2G||Mulet) random-if(Android) needs-focus == spellcheck-textarea-property-dynamic-inherit.html spellcheck-textarea-property-dynamic-inherit.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+skip-if(B2G||Mulet) random-if(Android) needs-focus == spellcheck-textarea-attr-dynamic-override.html spellcheck-textarea-attr-dynamic-override.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+skip-if(B2G||Mulet) random-if(Android) needs-focus == spellcheck-textarea-attr-dynamic-override-inherit.html spellcheck-textarea-attr-dynamic-override-inherit.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+skip-if(B2G||Mulet) random-if(Android) needs-focus == spellcheck-textarea-property-dynamic-override.html spellcheck-textarea-property-dynamic-override.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+skip-if(B2G||Mulet) random-if(Android) needs-focus == spellcheck-textarea-property-dynamic-override-inherit.html spellcheck-textarea-property-dynamic-override-inherit.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+needs-focus == caret_on_focus.html caret_on_focus.html
+needs-focus == caret_on_textarea_lastline.html caret_on_textarea_lastline.html
+needs-focus == input-text-onfocus-reframe.html input-text-onfocus-reframe.html
+needs-focus == input-text-notheme-onfocus-reframe.html input-text-notheme-onfocus-reframe.html
+skip-if(B2G||Mulet) needs-focus == caret_after_reframe.html caret_after_reframe.html
+# B2G timed out waiting for reftest-wait to be removed
+# Initial mulet triage: parity with B2G/B2G Desktop
+== nobogusnode-1.html nobogusnode-1.html
+== nobogusnode-2.html nobogusnode-2.html
+== spellcheck-hyphen-valid.html spellcheck-hyphen-valid.html
+skip-if(B2G||Mulet) fails-if(Android) needs-focus == spellcheck-hyphen-invalid.html spellcheck-hyphen-invalid.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+== spellcheck-slash-valid.html spellcheck-slash-valid.html
+== spellcheck-period-valid.html spellcheck-period-valid.html
+== spellcheck-space-valid.html spellcheck-space-valid.html
+== spellcheck-comma-valid.html spellcheck-comma-valid.html
+== spellcheck-hyphen-multiple-valid.html spellcheck-hyphen-multiple-valid.html
+skip-if(B2G||Mulet) fails-if(Android) needs-focus == spellcheck-hyphen-multiple-invalid.html spellcheck-hyphen-multiple-invalid.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+== spellcheck-dotafterquote-valid.html spellcheck-dotafterquote-valid.html
+== spellcheck-url-valid.html spellcheck-url-valid.html
+needs-focus == spellcheck-non-latin-arabic.html spellcheck-non-latin-arabic.html
+needs-focus == spellcheck-non-latin-chinese-simplified.html spellcheck-non-latin-chinese-simplified.html
+needs-focus == spellcheck-non-latin-chinese-traditional.html spellcheck-non-latin-chinese-traditional.html
+needs-focus == spellcheck-non-latin-hebrew.html spellcheck-non-latin-hebrew.html
+needs-focus == spellcheck-non-latin-japanese.html spellcheck-non-latin-japanese.html
+needs-focus == spellcheck-non-latin-korean.html spellcheck-non-latin-korean.html
+== unneeded_scroll.html unneeded_scroll.html
+skip-if(B2G||Mulet) == caret_on_presshell_reinit.html caret_on_presshell_reinit.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+fuzzy-if(browserIsRemote,255,3) asserts-if(browserIsRemote,0-1) skip-if(B2G||Mulet) fuzzy-if(skiaContent,1,5) == caret_on_presshell_reinit-2.html caret_on_presshell_reinit-2.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+skip-if(B2G||Mulet) fuzzy-if(asyncPan&&!layersGPUAccelerated,102,2824) == 642800.html 642800.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+== selection_visibility_after_reframe.html selection_visibility_after_reframe.html
+== selection_visibility_after_reframe-2.html selection_visibility_after_reframe-2.html
+== selection_visibility_after_reframe-3.html selection_visibility_after_reframe-3.html
+== 672709.html 672709.html
+== 338427-1.html 338427-1.html
+skip-if(Android||B2G||Mulet) needs-focus == 674212-spellcheck.html 674212-spellcheck.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+skip-if(Android||B2G||Mulet) needs-focus == 338427-2.html 338427-2.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+skip-if(Android||B2G||Mulet) needs-focus == 338427-3.html 338427-3.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+skip-if(Android||B2G||Mulet) needs-focus == 462758-grabbers-resizers.html 462758-grabbers-resizers.html
+# Initial mulet triage: parity with B2G/B2G Desktop
+== readwrite-non-editable.html readwrite-non-editable.html
+== readwrite-editable.html readwrite-editable.html
+== readonly-non-editable.html readonly-non-editable.html
+== readonly-editable.html readonly-editable.html
+== dynamic-overflow-change.html dynamic-overflow-change.html
+== 694880-1.html 694880-1.html
+== 694880-2.html 694880-2.html
+== 694880-3.html 694880-3.html
+skip == 388980-1.html 388980-1.html
+needs-focus == spellcheck-superscript-1.html spellcheck-superscript-1.html
+skip-if(B2G||Mulet) fails-if(Android) needs-focus == spellcheck-superscript-2.html spellcheck-superscript-2.html
+# bug 783658
+# Initial mulet triage: parity with B2G/B2G Desktop
+fuzzy-if(skiaContent,1,3400) needs-focus pref(layout.accessiblecaret.enabled,false) == 824080-1.html 824080-1.html
+needs-focus pref(layout.accessiblecaret.enabled,false) == 824080-2.html 824080-2.html
+needs-focus pref(layout.accessiblecaret.enabled,false) == 824080-3.html 824080-3.html
+needs-focus == 824080-2.html 824080-2.html
+fuzzy-if(skiaContent,1,3200) needs-focus pref(layout.accessiblecaret.enabled,false) == 824080-4.html 824080-4.html
+fails fuzzy-if(skiaContent,2,1800) needs-focus pref(layout.accessiblecaret.enabled,false) == 824080-5.html 824080-5.html
+needs-focus == 824080-4.html 824080-4.html
+needs-focus == 824080-6.html 824080-6.html
+needs-focus pref(layout.accessiblecaret.enabled,false) == 824080-7.html 824080-7.html
+needs-focus == 824080-6.html 824080-6.html
+# Bug 674927: copy spellcheck-textarea tests to contenteditable
+== spellcheck-contenteditable-attr.html spellcheck-contenteditable-attr.html
+fails-if(Android||B2G||Mulet) needs-focus == spellcheck-contenteditable-attr.html spellcheck-contenteditable-attr.html
+# B2G no spellcheck underline
+# Initial mulet triage: parity with B2G/B2G Desktop
+needs-focus == spellcheck-contenteditable-focused.html spellcheck-contenteditable-focused.html
+needs-focus == spellcheck-contenteditable-focused-reframe.html spellcheck-contenteditable-focused-reframe.html
+== spellcheck-contenteditable-nofocus.html spellcheck-contenteditable-nofocus.html
+== spellcheck-contenteditable-disabled.html spellcheck-contenteditable-disabled.html
+== spellcheck-contenteditable-disabled-partial.html spellcheck-contenteditable-disabled-partial.html
+== spellcheck-contenteditable-attr-inherit.html spellcheck-contenteditable-attr-inherit.html
+== spellcheck-contenteditable-attr-dynamic.html spellcheck-contenteditable-attr-dynamic.html
+== spellcheck-contenteditable-attr-dynamic-inherit.html spellcheck-contenteditable-attr-dynamic-inherit.html
+== spellcheck-contenteditable-property-dynamic.html spellcheck-contenteditable-property-dynamic.html
+== spellcheck-contenteditable-property-dynamic-inherit.html spellcheck-contenteditable-property-dynamic-inherit.html
+== spellcheck-contenteditable-attr-dynamic-override.html spellcheck-contenteditable-attr-dynamic-override.html
+== spellcheck-contenteditable-attr-dynamic-override-inherit.html spellcheck-contenteditable-attr-dynamic-override-inherit.html
+== spellcheck-contenteditable-property-dynamic-override.html spellcheck-contenteditable-property-dynamic-override.html
+== spellcheck-contenteditable-property-dynamic-override-inherit.html spellcheck-contenteditable-property-dynamic-override-inherit.html
+== 911201.html 911201.html
+needs-focus == 969773.html 969773.html
+fails fuzzy-if(skiaContent,1,220) == 997805.html 997805.html
+fails fuzzy-if(skiaContent,1,220) == 1088158.html 1088158.html
diff --git a/editor/reftests/reftest.list b/editor/reftests/reftest.list
new file mode 100644
index 000000000..5664f6dc4
--- /dev/null
+++ b/editor/reftests/reftest.list
@@ -0,0 +1,137 @@
+# include the XUL reftests
+include xul/reftest.list
+
+!= newline-1.html newline-ref.html
+== newline-2.html newline-ref.html
+== newline-3.html newline-ref.html
+== newline-4.html newline-ref.html
+== dynamic-1.html dynamic-ref.html
+== dynamic-type-1.html dynamic-ref.html
+== dynamic-type-2.html dynamic-ref.html
+== dynamic-type-3.html dynamic-ref.html
+== dynamic-type-4.html dynamic-ref.html
+== passwd-1.html passwd-ref.html
+!= passwd-2.html passwd-ref.html
+== passwd-3.html passwd-ref.html
+needs-focus == passwd-4.html passwd-ref.html
+== emptypasswd-1.html emptypasswd-ref.html
+== emptypasswd-2.html emptypasswd-ref.html
+== caret_on_positioned.html caret_on_positioned-ref.html
+fails-if(Android) needs-focus != spellcheck-input-disabled.html spellcheck-input-ref.html
+== spellcheck-input-attr-before.html spellcheck-input-nofocus-ref.html
+fails-if(Android) needs-focus != spellcheck-input-attr-before.html spellcheck-input-ref.html
+== spellcheck-input-attr-after.html spellcheck-input-nofocus-ref.html
+fails-if(Android) needs-focus != spellcheck-input-attr-after.html spellcheck-input-ref.html
+== spellcheck-input-attr-inherit.html spellcheck-input-nofocus-ref.html
+fails-if(Android) needs-focus != spellcheck-input-attr-inherit.html spellcheck-input-ref.html
+== spellcheck-input-attr-dynamic.html spellcheck-input-nofocus-ref.html
+fails-if(Android) needs-focus != spellcheck-input-attr-dynamic.html spellcheck-input-ref.html
+== spellcheck-input-attr-dynamic-inherit.html spellcheck-input-nofocus-ref.html
+fails-if(Android) needs-focus != spellcheck-input-attr-dynamic-inherit.html spellcheck-input-ref.html
+== spellcheck-input-property-dynamic.html spellcheck-input-nofocus-ref.html
+fails-if(Android) needs-focus != spellcheck-input-property-dynamic.html spellcheck-input-ref.html
+== spellcheck-input-property-dynamic-inherit.html spellcheck-input-nofocus-ref.html
+fails-if(Android) needs-focus != spellcheck-input-property-dynamic-inherit.html spellcheck-input-ref.html
+== spellcheck-input-attr-dynamic-override.html spellcheck-input-nofocus-ref.html
+fails-if(Android) needs-focus != spellcheck-input-attr-dynamic-override.html spellcheck-input-ref.html
+== spellcheck-input-attr-dynamic-override-inherit.html spellcheck-input-nofocus-ref.html
+fails-if(Android) needs-focus != spellcheck-input-attr-dynamic-override-inherit.html spellcheck-input-ref.html
+== spellcheck-input-property-dynamic-override.html spellcheck-input-nofocus-ref.html
+fails-if(Android) needs-focus != spellcheck-input-property-dynamic-override.html spellcheck-input-ref.html
+== spellcheck-input-property-dynamic-override-inherit.html spellcheck-input-nofocus-ref.html
+fails-if(Android) needs-focus != spellcheck-input-property-dynamic-override-inherit.html spellcheck-input-ref.html
+== spellcheck-textarea-attr.html spellcheck-textarea-nofocus-ref.html
+#the random-if(Android) tests pass on android native, but fail on android-xul, see bug 728942
+random-if(Android) needs-focus != spellcheck-textarea-attr.html spellcheck-textarea-ref.html
+needs-focus == spellcheck-textarea-focused.html spellcheck-textarea-ref.html
+needs-focus == spellcheck-textarea-focused-reframe.html spellcheck-textarea-ref.html
+needs-focus == spellcheck-textarea-focused-notreadonly.html spellcheck-textarea-ref2.html
+random-if(Android) needs-focus != spellcheck-textarea-nofocus.html spellcheck-textarea-ref.html
+random-if(Android) needs-focus != spellcheck-textarea-disabled.html spellcheck-textarea-ref.html
+random-if(Android) needs-focus != spellcheck-textarea-attr-inherit.html spellcheck-textarea-ref.html
+random-if(Android) needs-focus != spellcheck-textarea-attr-dynamic.html spellcheck-textarea-ref.html
+random-if(Android) needs-focus != spellcheck-textarea-attr-dynamic-inherit.html spellcheck-textarea-ref.html
+random-if(Android) needs-focus != spellcheck-textarea-property-dynamic.html spellcheck-textarea-ref.html
+random-if(Android) needs-focus != spellcheck-textarea-property-dynamic-inherit.html spellcheck-textarea-ref.html
+random-if(Android) needs-focus != spellcheck-textarea-attr-dynamic-override.html spellcheck-textarea-ref.html
+random-if(Android) needs-focus != spellcheck-textarea-attr-dynamic-override-inherit.html spellcheck-textarea-ref.html
+random-if(Android) needs-focus != spellcheck-textarea-property-dynamic-override.html spellcheck-textarea-ref.html
+random-if(Android) needs-focus != spellcheck-textarea-property-dynamic-override-inherit.html spellcheck-textarea-ref.html
+needs-focus == caret_on_focus.html caret_on_focus-ref.html
+needs-focus != caret_on_textarea_lastline.html caret_on_textarea_lastline-ref.html
+needs-focus == input-text-onfocus-reframe.html input-text-onfocus-reframe-ref.html
+needs-focus == input-text-notheme-onfocus-reframe.html input-text-notheme-onfocus-reframe-ref.html
+needs-focus == caret_after_reframe.html caret_after_reframe-ref.html
+== nobogusnode-1.html nobogusnode-ref.html
+== nobogusnode-2.html nobogusnode-ref.html
+== spellcheck-hyphen-valid.html spellcheck-hyphen-valid-ref.html
+fails-if(Android) needs-focus != spellcheck-hyphen-invalid.html spellcheck-hyphen-invalid-ref.html
+== spellcheck-slash-valid.html spellcheck-slash-valid-ref.html
+== spellcheck-period-valid.html spellcheck-period-valid-ref.html
+== spellcheck-space-valid.html spellcheck-space-valid-ref.html
+== spellcheck-comma-valid.html spellcheck-comma-valid-ref.html
+== spellcheck-hyphen-multiple-valid.html spellcheck-hyphen-multiple-valid-ref.html
+fails-if(Android) needs-focus != spellcheck-hyphen-multiple-invalid.html spellcheck-hyphen-multiple-invalid-ref.html
+== spellcheck-dotafterquote-valid.html spellcheck-dotafterquote-valid-ref.html
+== spellcheck-url-valid.html spellcheck-url-valid-ref.html
+needs-focus == spellcheck-non-latin-arabic.html spellcheck-non-latin-arabic-ref.html
+needs-focus == spellcheck-non-latin-chinese-simplified.html spellcheck-non-latin-chinese-simplified-ref.html
+needs-focus == spellcheck-non-latin-chinese-traditional.html spellcheck-non-latin-chinese-traditional-ref.html
+needs-focus == spellcheck-non-latin-hebrew.html spellcheck-non-latin-hebrew-ref.html
+needs-focus == spellcheck-non-latin-japanese.html spellcheck-non-latin-japanese-ref.html
+needs-focus == spellcheck-non-latin-korean.html spellcheck-non-latin-korean-ref.html
+== unneeded_scroll.html unneeded_scroll-ref.html
+== caret_on_presshell_reinit.html caret_on_presshell_reinit-ref.html
+fuzzy-if(browserIsRemote,255,3) asserts-if(browserIsRemote,0-1) == caret_on_presshell_reinit-2.html caret_on_presshell_reinit-ref.html
+fuzzy-if(asyncPan&&!layersGPUAccelerated,102,2824) == 642800.html 642800-ref.html
+== selection_visibility_after_reframe.html selection_visibility_after_reframe-ref.html
+!= selection_visibility_after_reframe-2.html selection_visibility_after_reframe-ref.html
+!= selection_visibility_after_reframe-3.html selection_visibility_after_reframe-ref.html
+== 672709.html 672709-ref.html
+== 338427-1.html 338427-1-ref.html
+skip-if(Android) needs-focus == 674212-spellcheck.html 674212-spellcheck-ref.html
+skip-if(Android) needs-focus == 338427-2.html 338427-2-ref.html
+skip-if(Android) needs-focus == 338427-3.html 338427-3-ref.html
+skip-if(Android) needs-focus == 462758-grabbers-resizers.html 462758-grabbers-resizers-ref.html
+== readwrite-non-editable.html readwrite-non-editable-ref.html
+== readwrite-editable.html readwrite-editable-ref.html
+== readonly-non-editable.html readonly-non-editable-ref.html
+== readonly-editable.html readonly-editable-ref.html
+== dynamic-overflow-change.html dynamic-overflow-change-ref.html
+== 694880-1.html 694880-ref.html
+== 694880-2.html 694880-ref.html
+== 694880-3.html 694880-ref.html
+== 388980-1.html 388980-1-ref.html
+needs-focus == spellcheck-superscript-1.html spellcheck-superscript-1-ref.html
+fails-if(Android) needs-focus != spellcheck-superscript-2.html spellcheck-superscript-2-ref.html # bug 783658
+fuzzy-if(skiaContent,1,3400) needs-focus pref(layout.accessiblecaret.enabled,false) pref(layout.accessiblecaret.enabled_on_touch,false) == 824080-1.html 824080-1-ref.html
+fuzzy-if(OSX,1,1) needs-focus pref(layout.accessiblecaret.enabled,false) pref(layout.accessiblecaret.enabled_on_touch,false) == 824080-2.html 824080-2-ref.html #Bug 1313253
+fuzzy-if(OSX,1,1) needs-focus pref(layout.accessiblecaret.enabled,false) pref(layout.accessiblecaret.enabled_on_touch,false) == 824080-3.html 824080-3-ref.html #Bug 1312951
+needs-focus != 824080-2.html 824080-3.html
+fuzzy-if(skiaContent,1,3200) needs-focus pref(layout.accessiblecaret.enabled,false) pref(layout.accessiblecaret.enabled_on_touch,false) == 824080-4.html 824080-4-ref.html
+fuzzy-if(skiaContent,2,1800) needs-focus pref(layout.accessiblecaret.enabled,false) pref(layout.accessiblecaret.enabled_on_touch,false) == 824080-5.html 824080-5-ref.html
+needs-focus != 824080-4.html 824080-5.html
+needs-focus == 824080-6.html 824080-6-ref.html
+needs-focus pref(layout.accessiblecaret.enabled,false) pref(layout.accessiblecaret.enabled_on_touch,false) == 824080-7.html 824080-7-ref.html
+needs-focus != 824080-6.html 824080-7.html
+# Bug 674927: copy spellcheck-textarea tests to contenteditable
+== spellcheck-contenteditable-attr.html spellcheck-contenteditable-nofocus-ref.html
+fails-if(Android) needs-focus != spellcheck-contenteditable-attr.html spellcheck-contenteditable-ref.html
+needs-focus == spellcheck-contenteditable-focused.html spellcheck-contenteditable-ref.html
+needs-focus == spellcheck-contenteditable-focused-reframe.html spellcheck-contenteditable-ref.html
+== spellcheck-contenteditable-nofocus.html spellcheck-contenteditable-disabled-ref.html
+== spellcheck-contenteditable-disabled.html spellcheck-contenteditable-disabled-ref.html
+== spellcheck-contenteditable-disabled-partial.html spellcheck-contenteditable-disabled-partial-ref.html
+== spellcheck-contenteditable-attr-inherit.html spellcheck-contenteditable-disabled-ref.html
+== spellcheck-contenteditable-attr-dynamic.html spellcheck-contenteditable-disabled-ref.html
+== spellcheck-contenteditable-attr-dynamic-inherit.html spellcheck-contenteditable-disabled-ref.html
+== spellcheck-contenteditable-property-dynamic.html spellcheck-contenteditable-disabled-ref.html
+== spellcheck-contenteditable-property-dynamic-inherit.html spellcheck-contenteditable-disabled-ref.html
+== spellcheck-contenteditable-attr-dynamic-override.html spellcheck-contenteditable-disabled-ref.html
+== spellcheck-contenteditable-attr-dynamic-override-inherit.html spellcheck-contenteditable-disabled-ref.html
+== spellcheck-contenteditable-property-dynamic-override.html spellcheck-contenteditable-disabled-ref.html
+== spellcheck-contenteditable-property-dynamic-override-inherit.html spellcheck-contenteditable-disabled-ref.html
+== 911201.html 911201-ref.html
+needs-focus == 969773.html 969773-ref.html
+fuzzy-if(skiaContent,1,220) == 997805.html 997805-ref.html
+fuzzy-if(skiaContent,1,220) == 1088158.html 1088158-ref.html
diff --git a/editor/reftests/selection_visibility_after_reframe-2.html b/editor/reftests/selection_visibility_after_reframe-2.html
new file mode 100644
index 000000000..fb705cc36
--- /dev/null
+++ b/editor/reftests/selection_visibility_after_reframe-2.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <input value="foo">
+ <script>
+ var i = document.querySelector("input");
+ i.selectionStart = 1;
+ i.selectionEnd = 2;
+ </script>
+ </body>
+</html>
diff --git a/editor/reftests/selection_visibility_after_reframe-3.html b/editor/reftests/selection_visibility_after_reframe-3.html
new file mode 100644
index 000000000..b05f130a1
--- /dev/null
+++ b/editor/reftests/selection_visibility_after_reframe-3.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <input value="foo">
+ <script>
+ var i = document.querySelector("input");
+ i.selectionStart = 1;
+ i.selectionEnd = 2;
+ i.style.display = "none";
+ document.body.clientHeight;
+ i.style.display = "";
+ </script>
+ </body>
+</html>
diff --git a/editor/reftests/selection_visibility_after_reframe-ref.html b/editor/reftests/selection_visibility_after_reframe-ref.html
new file mode 100644
index 000000000..c227b39c8
--- /dev/null
+++ b/editor/reftests/selection_visibility_after_reframe-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <input value="foo">
+ </body>
+</html>
diff --git a/editor/reftests/selection_visibility_after_reframe.html b/editor/reftests/selection_visibility_after_reframe.html
new file mode 100644
index 000000000..b72cec829
--- /dev/null
+++ b/editor/reftests/selection_visibility_after_reframe.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <input value="foo">
+ <script>
+ var i = document.querySelector("input");
+ i.selectionStart = 1;
+ i.selectionEnd = 2;
+ document.body.clientHeight;
+ i.style.display = "none";
+ document.body.clientHeight;
+ i.style.display = "";
+ </script>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-comma-valid-ref.html b/editor/reftests/spellcheck-comma-valid-ref.html
new file mode 100644
index 000000000..d5856e06f
--- /dev/null
+++ b/editor/reftests/spellcheck-comma-valid-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus spellcheck="false">good,nice</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-comma-valid.html b/editor/reftests/spellcheck-comma-valid.html
new file mode 100644
index 000000000..768cdbcf2
--- /dev/null
+++ b/editor/reftests/spellcheck-comma-valid.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus>good,nice</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-attr-dynamic-inherit.html b/editor/reftests/spellcheck-contenteditable-attr-dynamic-inherit.html
new file mode 100644
index 000000000..aa4e47c2c
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-attr-dynamic-inherit.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <div contenteditable>blahblahblah</div>
+ <script>
+ function init() {
+ document.body.setAttribute("spellcheck", "false");
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-attr-dynamic-override-inherit.html b/editor/reftests/spellcheck-contenteditable-attr-dynamic-override-inherit.html
new file mode 100644
index 000000000..1b4a0ab3b
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-attr-dynamic-override-inherit.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()" spellcheck="true">
+ <div contenteditable>blahblahblah</div>
+ <script>
+ function init() {
+ document.body.setAttribute("spellcheck", "false");
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-attr-dynamic-override.html b/editor/reftests/spellcheck-contenteditable-attr-dynamic-override.html
new file mode 100644
index 000000000..e3a4d9077
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-attr-dynamic-override.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <div contenteditable spellcheck="true">blahblahblah</div>
+ <script>
+ function init() {
+ document.querySelector("div").setAttribute("spellcheck", "false");
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-attr-dynamic.html b/editor/reftests/spellcheck-contenteditable-attr-dynamic.html
new file mode 100644
index 000000000..37ba9f651
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-attr-dynamic.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <div contenteditable>blahblahblah</div>
+ <script>
+ function init() {
+ document.querySelector("div").setAttribute("spellcheck", "false");
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-attr-inherit.html b/editor/reftests/spellcheck-contenteditable-attr-inherit.html
new file mode 100644
index 000000000..6cbfcb3da
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-attr-inherit.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <span spellcheck="false"><div contenteditable>blahblahblah</div></span>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-attr.html b/editor/reftests/spellcheck-contenteditable-attr.html
new file mode 100644
index 000000000..df119a997
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-attr.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <div contenteditable>blahblahblah</div>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-disabled-partial-ref.html b/editor/reftests/spellcheck-contenteditable-disabled-partial-ref.html
new file mode 100644
index 000000000..30fe7a6bf
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-disabled-partial-ref.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <span contenteditable>sakde</span> kreid <span contenteditable>slodv</span>
+ <script>
+ // Adding focus to the textbox should trigger a spellcheck
+ document.querySelector("span").focus();
+ document.querySelector("span + span").focus();
+ document.querySelector("span + span").blur();
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-disabled-partial.html b/editor/reftests/spellcheck-contenteditable-disabled-partial.html
new file mode 100644
index 000000000..c7b6c427c
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-disabled-partial.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <div contenteditable>sakde <span spellcheck=false>kreid</span> slodv</div>
+ <script>
+ // Adding focus to the textbox should trigger a spellcheck
+ document.querySelector("div").focus();
+ document.querySelector("div").blur();
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-disabled-ref.html b/editor/reftests/spellcheck-contenteditable-disabled-ref.html
new file mode 100644
index 000000000..23571fa5e
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-disabled-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <div>blahblahblah</div>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-disabled.html b/editor/reftests/spellcheck-contenteditable-disabled.html
new file mode 100644
index 000000000..3794f5767
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-disabled.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <div contenteditable spellcheck="false">blahblahblah</div>
+ <script>
+ // Adding focus to the textbox should trigger a spellcheck
+ document.querySelector("div").focus();
+ document.querySelector("div").blur();
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-focused-reframe.html b/editor/reftests/spellcheck-contenteditable-focused-reframe.html
new file mode 100644
index 000000000..733ee05bb
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-focused-reframe.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<body>
+
+ <div contenteditable id="testBox" onfocus="reframe(this);">blahblahblah</div>
+ <script type="text/javascript">
+ function reframe(textbox) {
+ textbox.style.display = "none";
+ textbox.style.display = "";
+ textbox.clientWidth;
+ }
+ //Adding focus to the textbox should trigger a spellcheck
+ document.getElementById("testBox").focus();
+ document.getElementById("testBox").blur();
+ </script>
+
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-focused.html b/editor/reftests/spellcheck-contenteditable-focused.html
new file mode 100644
index 000000000..808667399
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-focused.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<body>
+
+ <div contenteditable id="testBox">blahblahblah</div>
+ <script type="text/javascript">
+ //Adding focus to the textbox should trigger a spellcheck
+ document.getElementById("testBox").focus();
+ document.getElementById("testBox").blur();
+ </script>
+
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-nofocus-ref.html b/editor/reftests/spellcheck-contenteditable-nofocus-ref.html
new file mode 100644
index 000000000..67241fb7f
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-nofocus-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <div contenteditable spellcheck="true">blahblahblah</div>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-nofocus.html b/editor/reftests/spellcheck-contenteditable-nofocus.html
new file mode 100644
index 000000000..7e88dc3e1
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-nofocus.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <div contenteditable>blahblahblah</div>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-property-dynamic-inherit.html b/editor/reftests/spellcheck-contenteditable-property-dynamic-inherit.html
new file mode 100644
index 000000000..feb623dbb
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-property-dynamic-inherit.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <div contenteditable>blahblahblah</div>
+ <script>
+ function init() {
+ document.body.spellcheck = false;
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-property-dynamic-override-inherit.html b/editor/reftests/spellcheck-contenteditable-property-dynamic-override-inherit.html
new file mode 100644
index 000000000..26c5a4223
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-property-dynamic-override-inherit.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()" spellcheck="true">
+ <div contenteditable>blahblahblah</div>
+ <script>
+ function init() {
+ document.body.spellcheck = false;
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-property-dynamic-override.html b/editor/reftests/spellcheck-contenteditable-property-dynamic-override.html
new file mode 100644
index 000000000..dd16894b8
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-property-dynamic-override.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <div contenteditable spellcheck="true">blahblahblah</div>
+ <script>
+ function init() {
+ document.querySelector("div").spellcheck = false;
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-property-dynamic.html b/editor/reftests/spellcheck-contenteditable-property-dynamic.html
new file mode 100644
index 000000000..eaf2db29a
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-property-dynamic.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <div contenteditable>blahblahblah</div>
+ <script>
+ function init() {
+ document.querySelector("div").spellcheck = false;
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-ref.html b/editor/reftests/spellcheck-contenteditable-ref.html
new file mode 100644
index 000000000..d28dbcf96
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-ref.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <div contenteditable spellcheck="true">blahblahblah</div>
+ <script type="text/javascript">
+ var box = document.getElementsByTagName("div")[0];
+ box.focus(); //Bring the textbox into focus, triggering a spellcheck
+ box.blur(); //Blur in order to make things similar to other tests otherwise
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-dotafterquote-valid-ref.html b/editor/reftests/spellcheck-dotafterquote-valid-ref.html
new file mode 100644
index 000000000..b61904400
--- /dev/null
+++ b/editor/reftests/spellcheck-dotafterquote-valid-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus spellcheck="false">'Apple'.</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-dotafterquote-valid.html b/editor/reftests/spellcheck-dotafterquote-valid.html
new file mode 100644
index 000000000..1d3a605bb
--- /dev/null
+++ b/editor/reftests/spellcheck-dotafterquote-valid.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus>'Apple'.</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-hyphen-invalid-ref.html b/editor/reftests/spellcheck-hyphen-invalid-ref.html
new file mode 100644
index 000000000..856fd840e
--- /dev/null
+++ b/editor/reftests/spellcheck-hyphen-invalid-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus spellcheck="false">dddf-gggy</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-hyphen-invalid.html b/editor/reftests/spellcheck-hyphen-invalid.html
new file mode 100644
index 000000000..bc4e4e240
--- /dev/null
+++ b/editor/reftests/spellcheck-hyphen-invalid.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus>dddf-gggy</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-hyphen-multiple-invalid-ref.html b/editor/reftests/spellcheck-hyphen-multiple-invalid-ref.html
new file mode 100644
index 000000000..ab4cbd05a
--- /dev/null
+++ b/editor/reftests/spellcheck-hyphen-multiple-invalid-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea style="width: 400px; height: 200px;" autofocus spellcheck="false">-hlloe hlloe- --hlloe --hlloe ---hlloe hlloe--- ---hlloe----</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-hyphen-multiple-invalid.html b/editor/reftests/spellcheck-hyphen-multiple-invalid.html
new file mode 100644
index 000000000..bcc3f7113
--- /dev/null
+++ b/editor/reftests/spellcheck-hyphen-multiple-invalid.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea style="width: 400px; height: 200px;" autofocus>-hlloe hlloe- --hlloe --hlloe ---hlloe hlloe--- ---hlloe----</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-hyphen-multiple-valid-ref.html b/editor/reftests/spellcheck-hyphen-multiple-valid-ref.html
new file mode 100644
index 000000000..324a566c4
--- /dev/null
+++ b/editor/reftests/spellcheck-hyphen-multiple-valid-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea style="width: 400px; height: 200px;" autofocus spellcheck="false">- -- --- -hello hello- --hello --hello ---hello hello--- ---hello----</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-hyphen-multiple-valid.html b/editor/reftests/spellcheck-hyphen-multiple-valid.html
new file mode 100644
index 000000000..7f0ce681c
--- /dev/null
+++ b/editor/reftests/spellcheck-hyphen-multiple-valid.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea style="width: 400px; height: 200px;" autofocus>- -- --- -hello hello- --hello --hello ---hello hello--- ---hello----</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-hyphen-valid-ref.html b/editor/reftests/spellcheck-hyphen-valid-ref.html
new file mode 100644
index 000000000..73b507a3d
--- /dev/null
+++ b/editor/reftests/spellcheck-hyphen-valid-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus spellcheck="false">scot-free</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-hyphen-valid.html b/editor/reftests/spellcheck-hyphen-valid.html
new file mode 100644
index 000000000..2b56a6e24
--- /dev/null
+++ b/editor/reftests/spellcheck-hyphen-valid.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus>scot-free</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-input-attr-after.html b/editor/reftests/spellcheck-input-attr-after.html
new file mode 100644
index 000000000..1e878b5d1
--- /dev/null
+++ b/editor/reftests/spellcheck-input-attr-after.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="text" value="blahblahblah" spellcheck="true">
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-input-attr-before.html b/editor/reftests/spellcheck-input-attr-before.html
new file mode 100644
index 000000000..8456e6c8c
--- /dev/null
+++ b/editor/reftests/spellcheck-input-attr-before.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="text" spellcheck="true" value="blahblahblah">
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-input-attr-dynamic-inherit.html b/editor/reftests/spellcheck-input-attr-dynamic-inherit.html
new file mode 100644
index 000000000..c87be7c3e
--- /dev/null
+++ b/editor/reftests/spellcheck-input-attr-dynamic-inherit.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <input class="spell-checked" type="text" value="blahblahblah">
+ <script>
+ function init() {
+ document.body.setAttribute("spellcheck", "true");
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-input-attr-dynamic-override-inherit.html b/editor/reftests/spellcheck-input-attr-dynamic-override-inherit.html
new file mode 100644
index 000000000..d7d12b78d
--- /dev/null
+++ b/editor/reftests/spellcheck-input-attr-dynamic-override-inherit.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()" spellcheck="false">
+ <input class="spell-checked" type="text" value="blahblahblah">
+ <script>
+ function init() {
+ document.body.setAttribute("spellcheck", "true");
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-input-attr-dynamic-override.html b/editor/reftests/spellcheck-input-attr-dynamic-override.html
new file mode 100644
index 000000000..0f6095bd0
--- /dev/null
+++ b/editor/reftests/spellcheck-input-attr-dynamic-override.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <input class="spell-checked" type="text" spellcheck="false" value="blahblahblah">
+ <script>
+ function init() {
+ document.querySelector("input").setAttribute("spellcheck", "true");
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-input-attr-dynamic.html b/editor/reftests/spellcheck-input-attr-dynamic.html
new file mode 100644
index 000000000..27c8281fa
--- /dev/null
+++ b/editor/reftests/spellcheck-input-attr-dynamic.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <input class="spell-checked" type="text" value="blahblahblah">
+ <script>
+ function init() {
+ document.querySelector("input").setAttribute("spellcheck", "true");
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-input-attr-inherit.html b/editor/reftests/spellcheck-input-attr-inherit.html
new file mode 100644
index 000000000..c851bd189
--- /dev/null
+++ b/editor/reftests/spellcheck-input-attr-inherit.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <span spellcheck="true"><input class="spell-checked" type="text" value="blahblahblah"></span>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-input-disabled.html b/editor/reftests/spellcheck-input-disabled.html
new file mode 100644
index 000000000..f3b2f2ba9
--- /dev/null
+++ b/editor/reftests/spellcheck-input-disabled.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="text" value="blahblahblah">
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-input-nofocus-ref.html b/editor/reftests/spellcheck-input-nofocus-ref.html
new file mode 100644
index 000000000..1e878b5d1
--- /dev/null
+++ b/editor/reftests/spellcheck-input-nofocus-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="text" value="blahblahblah" spellcheck="true">
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-input-property-dynamic-inherit.html b/editor/reftests/spellcheck-input-property-dynamic-inherit.html
new file mode 100644
index 000000000..1cf839bae
--- /dev/null
+++ b/editor/reftests/spellcheck-input-property-dynamic-inherit.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <input class="spell-checked" type="text" value="blahblahblah">
+ <script>
+ function init() {
+ document.body.spellcheck = true;
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-input-property-dynamic-override-inherit.html b/editor/reftests/spellcheck-input-property-dynamic-override-inherit.html
new file mode 100644
index 000000000..eb380dc96
--- /dev/null
+++ b/editor/reftests/spellcheck-input-property-dynamic-override-inherit.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()" spellcheck="false">
+ <input class="spell-checked" type="text" value="blahblahblah">
+ <script>
+ function init() {
+ document.body.spellcheck = true;
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-input-property-dynamic-override.html b/editor/reftests/spellcheck-input-property-dynamic-override.html
new file mode 100644
index 000000000..fad2fb3ed
--- /dev/null
+++ b/editor/reftests/spellcheck-input-property-dynamic-override.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <input class="spell-checked" type="text" spellcheck="false" value="blahblahblah">
+ <script>
+ function init() {
+ document.querySelector("input").spellcheck = true;
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-input-property-dynamic.html b/editor/reftests/spellcheck-input-property-dynamic.html
new file mode 100644
index 000000000..dd59ec6ea
--- /dev/null
+++ b/editor/reftests/spellcheck-input-property-dynamic.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <input class="spell-checked" type="text" value="blahblahblah">
+ <script>
+ function init() {
+ document.querySelector("input").spellcheck = true;
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-input-ref.html b/editor/reftests/spellcheck-input-ref.html
new file mode 100644
index 000000000..8dde5e6f1
--- /dev/null
+++ b/editor/reftests/spellcheck-input-ref.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="text" value="blahblahblah" spellcheck="true">
+ <script>
+ var i = document.getElementsByTagName("input")[0];
+ i.focus(); // init the editor
+ i.blur(); // we actually don't need the focus
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-non-latin-arabic-ref.html b/editor/reftests/spellcheck-non-latin-arabic-ref.html
new file mode 100644
index 000000000..67850b46c
--- /dev/null
+++ b/editor/reftests/spellcheck-non-latin-arabic-ref.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <textarea autofocus spellcheck="false">سلام</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-non-latin-arabic.html b/editor/reftests/spellcheck-non-latin-arabic.html
new file mode 100644
index 000000000..fbbe19388
--- /dev/null
+++ b/editor/reftests/spellcheck-non-latin-arabic.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <textarea autofocus>سلام</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-non-latin-chinese-simplified-ref.html b/editor/reftests/spellcheck-non-latin-chinese-simplified-ref.html
new file mode 100644
index 000000000..83ad79c26
--- /dev/null
+++ b/editor/reftests/spellcheck-non-latin-chinese-simplified-ref.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <textarea autofocus spellcheck="false">你好</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-non-latin-chinese-simplified.html b/editor/reftests/spellcheck-non-latin-chinese-simplified.html
new file mode 100644
index 000000000..8db16489a
--- /dev/null
+++ b/editor/reftests/spellcheck-non-latin-chinese-simplified.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <textarea autofocus>你好</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-non-latin-chinese-traditional-ref.html b/editor/reftests/spellcheck-non-latin-chinese-traditional-ref.html
new file mode 100644
index 000000000..83ad79c26
--- /dev/null
+++ b/editor/reftests/spellcheck-non-latin-chinese-traditional-ref.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <textarea autofocus spellcheck="false">你好</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-non-latin-chinese-traditional.html b/editor/reftests/spellcheck-non-latin-chinese-traditional.html
new file mode 100644
index 000000000..8db16489a
--- /dev/null
+++ b/editor/reftests/spellcheck-non-latin-chinese-traditional.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <textarea autofocus>你好</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-non-latin-hebrew-ref.html b/editor/reftests/spellcheck-non-latin-hebrew-ref.html
new file mode 100644
index 000000000..e2bd7c6d2
--- /dev/null
+++ b/editor/reftests/spellcheck-non-latin-hebrew-ref.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <textarea autofocus spellcheck="false">שלום</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-non-latin-hebrew.html b/editor/reftests/spellcheck-non-latin-hebrew.html
new file mode 100644
index 000000000..9372c4e9a
--- /dev/null
+++ b/editor/reftests/spellcheck-non-latin-hebrew.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <textarea autofocus>שלום</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-non-latin-japanese-ref.html b/editor/reftests/spellcheck-non-latin-japanese-ref.html
new file mode 100644
index 000000000..a978cd3ce
--- /dev/null
+++ b/editor/reftests/spellcheck-non-latin-japanese-ref.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <textarea autofocus spellcheck="false">こんにちは</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-non-latin-japanese.html b/editor/reftests/spellcheck-non-latin-japanese.html
new file mode 100644
index 000000000..d79bb0e5e
--- /dev/null
+++ b/editor/reftests/spellcheck-non-latin-japanese.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <textarea autofocus>こんにちは</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-non-latin-korean-ref.html b/editor/reftests/spellcheck-non-latin-korean-ref.html
new file mode 100644
index 000000000..53d1909f3
--- /dev/null
+++ b/editor/reftests/spellcheck-non-latin-korean-ref.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <textarea autofocus spellcheck="false">안녕하세요</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-non-latin-korean.html b/editor/reftests/spellcheck-non-latin-korean.html
new file mode 100644
index 000000000..f0f65e82e
--- /dev/null
+++ b/editor/reftests/spellcheck-non-latin-korean.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <textarea autofocus>안녕하세요</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-period-valid-ref.html b/editor/reftests/spellcheck-period-valid-ref.html
new file mode 100644
index 000000000..5ee87992b
--- /dev/null
+++ b/editor/reftests/spellcheck-period-valid-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus spellcheck="false">good.nice</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-period-valid.html b/editor/reftests/spellcheck-period-valid.html
new file mode 100644
index 000000000..aaa5aa468
--- /dev/null
+++ b/editor/reftests/spellcheck-period-valid.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus>good.nice</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-slash-valid-ref.html b/editor/reftests/spellcheck-slash-valid-ref.html
new file mode 100644
index 000000000..fddc03252
--- /dev/null
+++ b/editor/reftests/spellcheck-slash-valid-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus spellcheck="false">good/nice</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-slash-valid.html b/editor/reftests/spellcheck-slash-valid.html
new file mode 100644
index 000000000..37e8d1bf4
--- /dev/null
+++ b/editor/reftests/spellcheck-slash-valid.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus>good/nice</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-space-valid-ref.html b/editor/reftests/spellcheck-space-valid-ref.html
new file mode 100644
index 000000000..9fea33e96
--- /dev/null
+++ b/editor/reftests/spellcheck-space-valid-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus spellcheck="false">good nice</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-space-valid.html b/editor/reftests/spellcheck-space-valid.html
new file mode 100644
index 000000000..575426d47
--- /dev/null
+++ b/editor/reftests/spellcheck-space-valid.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus>good nice</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-superscript-1-ref.html b/editor/reftests/spellcheck-superscript-1-ref.html
new file mode 100644
index 000000000..35df20d70
--- /dev/null
+++ b/editor/reftests/spellcheck-superscript-1-ref.html
@@ -0,0 +1,3 @@
+<!doctype html>
+<textarea spellcheck=false>&sup1; &sup2; &sup3;</textarea>
+<script>document.body.firstChild.focus()</script>
diff --git a/editor/reftests/spellcheck-superscript-1.html b/editor/reftests/spellcheck-superscript-1.html
new file mode 100644
index 000000000..b7b317295
--- /dev/null
+++ b/editor/reftests/spellcheck-superscript-1.html
@@ -0,0 +1,3 @@
+<!doctype html>
+<textarea>&sup1; &sup2; &sup3;</textarea>
+<script>document.body.firstChild.focus()</script>
diff --git a/editor/reftests/spellcheck-superscript-2-ref.html b/editor/reftests/spellcheck-superscript-2-ref.html
new file mode 100644
index 000000000..19276bd71
--- /dev/null
+++ b/editor/reftests/spellcheck-superscript-2-ref.html
@@ -0,0 +1,3 @@
+<!doctype html>
+<textarea>&sup1; &sup2; &sup3; mispeled</textarea>
+<script>document.body.firstChild.focus()</script>
diff --git a/editor/reftests/spellcheck-superscript-2.html b/editor/reftests/spellcheck-superscript-2.html
new file mode 100644
index 000000000..350d2bc8c
--- /dev/null
+++ b/editor/reftests/spellcheck-superscript-2.html
@@ -0,0 +1,3 @@
+<!doctype html>
+<textarea spellcheck=false>&sup1; &sup2; &sup3; mispeled</textarea>
+<script>document.body.firstChild.focus()</script>
diff --git a/editor/reftests/spellcheck-textarea-attr-dynamic-inherit.html b/editor/reftests/spellcheck-textarea-attr-dynamic-inherit.html
new file mode 100644
index 000000000..a4a938494
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-attr-dynamic-inherit.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <textarea>blahblahblah</textarea>
+ <script>
+ function init() {
+ document.body.setAttribute("spellcheck", "false");
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-attr-dynamic-override-inherit.html b/editor/reftests/spellcheck-textarea-attr-dynamic-override-inherit.html
new file mode 100644
index 000000000..a6ae716b5
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-attr-dynamic-override-inherit.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()" spellcheck="true">
+ <textarea>blahblahblah</textarea>
+ <script>
+ function init() {
+ document.body.setAttribute("spellcheck", "false");
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-attr-dynamic-override.html b/editor/reftests/spellcheck-textarea-attr-dynamic-override.html
new file mode 100644
index 000000000..96e956608
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-attr-dynamic-override.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <textarea spellcheck="true">blahblahblah</textarea>
+ <script>
+ function init() {
+ document.querySelector("textarea").setAttribute("spellcheck", "false");
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-attr-dynamic.html b/editor/reftests/spellcheck-textarea-attr-dynamic.html
new file mode 100644
index 000000000..745e66c7c
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-attr-dynamic.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <textarea>blahblahblah</textarea>
+ <script>
+ function init() {
+ document.querySelector("textarea").setAttribute("spellcheck", "false");
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-attr-inherit.html b/editor/reftests/spellcheck-textarea-attr-inherit.html
new file mode 100644
index 000000000..c261fec5a
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-attr-inherit.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <span spellcheck="false"><textarea>blahblahblah</textarea></span>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-attr.html b/editor/reftests/spellcheck-textarea-attr.html
new file mode 100644
index 000000000..223f948dc
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-attr.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <textarea>blahblahblah</textarea>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-disabled.html b/editor/reftests/spellcheck-textarea-disabled.html
new file mode 100644
index 000000000..cfaf3ed53
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-disabled.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <textarea spellcheck="false">blahblahblah</textarea>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-focused-notreadonly.html b/editor/reftests/spellcheck-textarea-focused-notreadonly.html
new file mode 100644
index 000000000..475ae6002
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-focused-notreadonly.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<body>
+
+ <textarea id="testBox" style="padding:2px;" readonly></textarea>
+ <script type="text/javascript">
+ //Adding focus to the textbox should trigger a spellcheck
+ var textbox = document.getElementById("testBox");
+ addEventListener("load", function() {
+ textbox.readOnly = false;
+ textbox.focus();
+ textbox.value = "blahblahblah";
+ textbox.selectionStart = textbox.selectionEnd = 0;
+ textbox.blur();
+ }, false);
+ </script>
+
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-focused-reframe.html b/editor/reftests/spellcheck-textarea-focused-reframe.html
new file mode 100644
index 000000000..6e6f871dd
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-focused-reframe.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<body>
+
+ <textarea id="testBox" onfocus="reframe(this);">blahblahblah</textarea>
+ <script type="text/javascript">
+ function reframe(textbox) {
+ textbox.style.display = "none";
+ textbox.style.display = "";
+ textbox.clientWidth;
+ }
+ //Adding focus to the textbox should trigger a spellcheck
+ document.getElementById("testBox").focus();
+ document.getElementById("testBox").blur();
+ </script>
+
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-focused.html b/editor/reftests/spellcheck-textarea-focused.html
new file mode 100644
index 000000000..04d689cc1
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-focused.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<body>
+
+ <textarea id="testBox">blahblahblah</textarea>
+ <script type="text/javascript">
+ //Adding focus to the textbox should trigger a spellcheck
+ document.getElementById("testBox").focus();
+ document.getElementById("testBox").blur();
+ </script>
+
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-nofocus-ref.html b/editor/reftests/spellcheck-textarea-nofocus-ref.html
new file mode 100644
index 000000000..8d993983e
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-nofocus-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <textarea spellcheck="true">blahblahblah</textarea>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-nofocus.html b/editor/reftests/spellcheck-textarea-nofocus.html
new file mode 100644
index 000000000..a1ce1a0a9
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-nofocus.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <textarea>blahblahblah</textarea>
+</body>
+</html> \ No newline at end of file
diff --git a/editor/reftests/spellcheck-textarea-property-dynamic-inherit.html b/editor/reftests/spellcheck-textarea-property-dynamic-inherit.html
new file mode 100644
index 000000000..125f578bf
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-property-dynamic-inherit.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <textarea>blahblahblah</textarea>
+ <script>
+ function init() {
+ document.body.spellcheck = false;
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-property-dynamic-override-inherit.html b/editor/reftests/spellcheck-textarea-property-dynamic-override-inherit.html
new file mode 100644
index 000000000..0e773646e
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-property-dynamic-override-inherit.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()" spellcheck="true">
+ <textarea>blahblahblah</textarea>
+ <script>
+ function init() {
+ document.body.spellcheck = false;
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-property-dynamic-override.html b/editor/reftests/spellcheck-textarea-property-dynamic-override.html
new file mode 100644
index 000000000..f929d3bec
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-property-dynamic-override.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <textarea spellcheck="true">blahblahblah</textarea>
+ <script>
+ function init() {
+ document.querySelector("textarea").spellcheck = false;
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-property-dynamic.html b/editor/reftests/spellcheck-textarea-property-dynamic.html
new file mode 100644
index 000000000..d0c94f68e
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-property-dynamic.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <textarea>blahblahblah</textarea>
+ <script>
+ function init() {
+ document.querySelector("textarea").spellcheck = false;
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-ref.html b/editor/reftests/spellcheck-textarea-ref.html
new file mode 100644
index 000000000..91ecd1d8e
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-ref.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <textarea spellcheck="true">blahblahblah</textarea>
+ <script type="text/javascript">
+ var box = document.getElementsByTagName("textarea")[0];
+ box.focus(); //Bring the textbox into focus, triggering a spellcheck
+ box.blur(); //Blur in order to make things similar to other tests otherwise
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-ref2.html b/editor/reftests/spellcheck-textarea-ref2.html
new file mode 100644
index 000000000..6bd588a23
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-ref2.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <textarea spellcheck="true" style="padding:2px;">blahblahblah</textarea>
+ <script type="text/javascript">
+ var box = document.getElementsByTagName("textarea")[0];
+ box.focus(); //Bring the textbox into focus, triggering a spellcheck
+ box.blur(); //Blur in order to make things similar to other tests otherwise
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-url-valid-ref.html b/editor/reftests/spellcheck-url-valid-ref.html
new file mode 100644
index 000000000..7f9f7530d
--- /dev/null
+++ b/editor/reftests/spellcheck-url-valid-ref.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus rows=10 cols=60 spellcheck=false>
+http://fooi.barj/bazk
+https://fooi.barj/bazk
+news://fooi.barj/bazk
+ftp://fooi.barj/bazk
+data:fooi/barj,bazk
+javascript:fooi.barj.bazk();
+fooi@barj.bazk
+</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-url-valid.html b/editor/reftests/spellcheck-url-valid.html
new file mode 100644
index 000000000..f492560a2
--- /dev/null
+++ b/editor/reftests/spellcheck-url-valid.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus rows=10 cols=60>
+http://fooi.barj/bazk
+https://fooi.barj/bazk
+news://fooi.barj/bazk
+ftp://fooi.barj/bazk
+data:fooi/barj,bazk
+javascript:fooi.barj.bazk();
+fooi@barj.bazk
+</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/unneeded_scroll-ref.html b/editor/reftests/unneeded_scroll-ref.html
new file mode 100644
index 000000000..9d6ec25bb
--- /dev/null
+++ b/editor/reftests/unneeded_scroll-ref.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <div>
+ <textarea>I
+ am
+ a
+ long
+ long
+ long
+ long
+ textarea
+ </textarea>
+ </div>
+ </body>
+</html>
diff --git a/editor/reftests/unneeded_scroll.html b/editor/reftests/unneeded_scroll.html
new file mode 100644
index 000000000..6d9eba355
--- /dev/null
+++ b/editor/reftests/unneeded_scroll.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <script>
+ document.addEventListener("DOMContentLoaded", function() {
+ var t = document.querySelector("textarea");
+ t.focus();
+ document.querySelector("#dst").appendChild(t);
+ }, false);
+ </script>
+ <div>
+ <textarea>I
+ am
+ a
+ long
+ long
+ long
+ long
+ textarea
+ </textarea>
+ </div>
+ <div id="dst"></div>
+ </body>
+</html>
diff --git a/editor/reftests/xul/autocomplete-1.xul b/editor/reftests/xul/autocomplete-1.xul
new file mode 100644
index 000000000..4ba8edb8c
--- /dev/null
+++ b/editor/reftests/xul/autocomplete-1.xul
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <!-- leading space in the value to ensure no pixels of t get clipped
+ in one rendering but not the other -->
+ <textbox type="autocomplete" value=" test"/>
+
+</window>
diff --git a/editor/reftests/xul/autocomplete-ref.xul b/editor/reftests/xul/autocomplete-ref.xul
new file mode 100644
index 000000000..829948964
--- /dev/null
+++ b/editor/reftests/xul/autocomplete-ref.xul
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="input.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <html:input class="ac" value=" test"/>
+
+</window>
diff --git a/editor/reftests/xul/empty-1.xul b/editor/reftests/xul/empty-1.xul
new file mode 100644
index 000000000..cb0bd576b
--- /dev/null
+++ b/editor/reftests/xul/empty-1.xul
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="placeholder-reset.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <textbox placeholder=" test"/>
+
+</window>
diff --git a/editor/reftests/xul/empty-2.xul b/editor/reftests/xul/empty-2.xul
new file mode 100644
index 000000000..e7f59fa6e
--- /dev/null
+++ b/editor/reftests/xul/empty-2.xul
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <textbox placeholder=" test" value="value"/>
+
+</window>
diff --git a/editor/reftests/xul/empty-ref.xul b/editor/reftests/xul/empty-ref.xul
new file mode 100644
index 000000000..6caf215ed
--- /dev/null
+++ b/editor/reftests/xul/empty-ref.xul
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="input.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <html:input class="empty" value=" test"/>
+
+</window>
diff --git a/editor/reftests/xul/emptyautocomplete-1.xul b/editor/reftests/xul/emptyautocomplete-1.xul
new file mode 100644
index 000000000..ddbd50512
--- /dev/null
+++ b/editor/reftests/xul/emptyautocomplete-1.xul
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <textbox type="autocomplete"/>
+
+</window>
diff --git a/editor/reftests/xul/emptyautocomplete-ref.xul b/editor/reftests/xul/emptyautocomplete-ref.xul
new file mode 100644
index 000000000..abe557fdb
--- /dev/null
+++ b/editor/reftests/xul/emptyautocomplete-ref.xul
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="input.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <html:input class="ac"/>
+
+</window>
diff --git a/editor/reftests/xul/emptymultiline-1.xul b/editor/reftests/xul/emptymultiline-1.xul
new file mode 100644
index 000000000..dbe72be3d
--- /dev/null
+++ b/editor/reftests/xul/emptymultiline-1.xul
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="input.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <textbox multiline="true"/>
+
+</window>
diff --git a/editor/reftests/xul/emptymultiline-2.xul b/editor/reftests/xul/emptymultiline-2.xul
new file mode 100644
index 000000000..c43d432ad
--- /dev/null
+++ b/editor/reftests/xul/emptymultiline-2.xul
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="input.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <textbox multiline="true" rows="10"/>
+
+</window>
diff --git a/editor/reftests/xul/emptymultiline-ref.xul b/editor/reftests/xul/emptymultiline-ref.xul
new file mode 100644
index 000000000..ef79718dd
--- /dev/null
+++ b/editor/reftests/xul/emptymultiline-ref.xul
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="input.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <html:textarea rows="10" style="resize: none;"/>
+
+</window>
diff --git a/editor/reftests/xul/emptytextbox-1.xul b/editor/reftests/xul/emptytextbox-1.xul
new file mode 100644
index 000000000..aba191a94
--- /dev/null
+++ b/editor/reftests/xul/emptytextbox-1.xul
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <textbox/>
+
+</window>
diff --git a/editor/reftests/xul/emptytextbox-2.xul b/editor/reftests/xul/emptytextbox-2.xul
new file mode 100644
index 000000000..973e89882
--- /dev/null
+++ b/editor/reftests/xul/emptytextbox-2.xul
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <textbox type="password"/>
+
+</window>
diff --git a/editor/reftests/xul/emptytextbox-3.xul b/editor/reftests/xul/emptytextbox-3.xul
new file mode 100644
index 000000000..a37ce6429
--- /dev/null
+++ b/editor/reftests/xul/emptytextbox-3.xul
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <textbox type="number"/>
+
+</window>
diff --git a/editor/reftests/xul/emptytextbox-4.xul b/editor/reftests/xul/emptytextbox-4.xul
new file mode 100644
index 000000000..979cd118e
--- /dev/null
+++ b/editor/reftests/xul/emptytextbox-4.xul
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <textbox type="search"/>
+
+</window>
diff --git a/editor/reftests/xul/emptytextbox-5.xul b/editor/reftests/xul/emptytextbox-5.xul
new file mode 100644
index 000000000..6c8c27971
--- /dev/null
+++ b/editor/reftests/xul/emptytextbox-5.xul
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <textbox type="timed"/>
+
+</window>
diff --git a/editor/reftests/xul/emptytextbox-ref.xul b/editor/reftests/xul/emptytextbox-ref.xul
new file mode 100644
index 000000000..58089ca6d
--- /dev/null
+++ b/editor/reftests/xul/emptytextbox-ref.xul
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="input.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <html:input/>
+
+</window>
diff --git a/editor/reftests/xul/input.css b/editor/reftests/xul/input.css
new file mode 100644
index 000000000..2104b16d2
--- /dev/null
+++ b/editor/reftests/xul/input.css
@@ -0,0 +1,70 @@
+@namespace url('http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul');
+@namespace html url('http://www.w3.org/1999/xhtml');
+
+#mac html|input, #mac html|textarea {
+ margin: 4px;
+ padding: 0 1px;
+}
+
+#win html|input, #win html|textarea {
+ margin: 2px 4px;
+ padding: 2px 3px 3px;
+ padding-inline-start: 5px;
+}
+
+#win html|input:-moz-system-metric(windows-default-theme) {
+ padding: 1px 2px 2px;
+ padding-inline-start: 4px;
+}
+
+#linux html|input, #linux html|textarea {
+ margin: 2px 4px;
+ padding: 2px 5px 3px;
+}
+
+textbox[multiline="true"], html|textarea {
+ border: none !important;
+ -moz-appearance: none !important;
+ background-color: white !important;
+ border-top-right-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+html|input, html|textarea {
+ font: inherit;
+}
+
+html|input.ac {
+ padding: 0 4px !important;
+}
+
+html|input.empty {
+ color: graytext;
+}
+
+:root:not(.winxp) html|input.empty:-moz-system-metric(windows-default-theme) {
+ font-style: italic;
+}
+
+html|input.num {
+ text-align: end;
+}
+
+#mac html|input.num {
+ margin-inline-end: 8px;
+}
+
+#win html|input.num {
+ padding: 0 !important;
+}
+
+#linux html|input.num {
+ margin-inline-end: 3px;
+ padding: 3px 4px;
+}
+
+html|div.plainfield {
+ color: -moz-fieldtext;
+ white-space: pre;
+}
+
diff --git a/editor/reftests/xul/number-1.xul b/editor/reftests/xul/number-1.xul
new file mode 100644
index 000000000..a37ce6429
--- /dev/null
+++ b/editor/reftests/xul/number-1.xul
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <textbox type="number"/>
+
+</window>
diff --git a/editor/reftests/xul/number-2.xul b/editor/reftests/xul/number-2.xul
new file mode 100644
index 000000000..73d2e84a0
--- /dev/null
+++ b/editor/reftests/xul/number-2.xul
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <textbox type="number" hidespinbuttons="false"/>
+
+</window>
diff --git a/editor/reftests/xul/number-3.xul b/editor/reftests/xul/number-3.xul
new file mode 100644
index 000000000..8ddadcf63
--- /dev/null
+++ b/editor/reftests/xul/number-3.xul
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <textbox type="number" hidespinbuttons="true"/>
+
+</window>
diff --git a/editor/reftests/xul/number-4.xul b/editor/reftests/xul/number-4.xul
new file mode 100644
index 000000000..40650e8e4
--- /dev/null
+++ b/editor/reftests/xul/number-4.xul
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <textbox type="number" value="1" hidespinbuttons="true"/>
+
+</window>
diff --git a/editor/reftests/xul/number-5.xul b/editor/reftests/xul/number-5.xul
new file mode 100644
index 000000000..bd6346fe5
--- /dev/null
+++ b/editor/reftests/xul/number-5.xul
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <textbox type="number" value="test" hidespinbuttons="true"/>
+
+</window>
diff --git a/editor/reftests/xul/number-ref.xul b/editor/reftests/xul/number-ref.xul
new file mode 100644
index 000000000..abff29d97
--- /dev/null
+++ b/editor/reftests/xul/number-ref.xul
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="input.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <html:input value="0" class="num"/>
+
+</window>
diff --git a/editor/reftests/xul/numberwithvalue-1.xul b/editor/reftests/xul/numberwithvalue-1.xul
new file mode 100644
index 000000000..3aaebcfdf
--- /dev/null
+++ b/editor/reftests/xul/numberwithvalue-1.xul
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <textbox type="number" value="123" hidespinbuttons="true"/>
+
+</window>
diff --git a/editor/reftests/xul/numberwithvalue-ref.xul b/editor/reftests/xul/numberwithvalue-ref.xul
new file mode 100644
index 000000000..8ddc7a144
--- /dev/null
+++ b/editor/reftests/xul/numberwithvalue-ref.xul
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="input.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <html:input value="123" class="num"/>
+
+</window>
diff --git a/editor/reftests/xul/passwd-1.xul b/editor/reftests/xul/passwd-1.xul
new file mode 100644
index 000000000..1f9619a16
--- /dev/null
+++ b/editor/reftests/xul/passwd-1.xul
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <textbox type="password" value="test"/>
+
+</window>
diff --git a/editor/reftests/xul/passwd-2.xul b/editor/reftests/xul/passwd-2.xul
new file mode 100644
index 000000000..54fa5937b
--- /dev/null
+++ b/editor/reftests/xul/passwd-2.xul
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <textbox type="password" value="abcd"/>
+
+</window>
diff --git a/editor/reftests/xul/passwd-3.xul b/editor/reftests/xul/passwd-3.xul
new file mode 100644
index 000000000..b1849931b
--- /dev/null
+++ b/editor/reftests/xul/passwd-3.xul
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <textbox value="test"/>
+
+</window>
diff --git a/editor/reftests/xul/passwd-ref.xul b/editor/reftests/xul/passwd-ref.xul
new file mode 100644
index 000000000..1f93bc339
--- /dev/null
+++ b/editor/reftests/xul/passwd-ref.xul
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="input.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <html:input type="password" value="test"/>
+
+</window>
diff --git a/editor/reftests/xul/placeholder-reset.css b/editor/reftests/xul/placeholder-reset.css
new file mode 100644
index 000000000..a2c41e69b
--- /dev/null
+++ b/editor/reftests/xul/placeholder-reset.css
@@ -0,0 +1,8 @@
+@namespace url('http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul');
+@namespace html url('http://www.w3.org/1999/xhtml');
+
+/* We need to have a non-transparent placeholder so we can test it. */
+html|input::placeholder {
+ opacity: 1.0;
+ color: graytext;
+}
diff --git a/editor/reftests/xul/plain-1.xul b/editor/reftests/xul/plain-1.xul
new file mode 100644
index 000000000..ab573d0fc
--- /dev/null
+++ b/editor/reftests/xul/plain-1.xul
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <textbox value=" test" class="plain"/>
+
+</window>
diff --git a/editor/reftests/xul/plain-ref.xul b/editor/reftests/xul/plain-ref.xul
new file mode 100644
index 000000000..f345e675e
--- /dev/null
+++ b/editor/reftests/xul/plain-ref.xul
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="input.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <html:div class="plainfield"> test</html:div>
+
+</window>
diff --git a/editor/reftests/xul/platform.js b/editor/reftests/xul/platform.js
new file mode 100644
index 000000000..f45e6d1f5
--- /dev/null
+++ b/editor/reftests/xul/platform.js
@@ -0,0 +1,28 @@
+// The appearance of XUL elements is platform-specific, so we set the
+// style of the root element according to the platform, so that the
+// CSS code inside input.css can select the correct styles for each
+// platform.
+
+var id;
+var ua = navigator.userAgent;
+
+if (/Windows/.test(ua)) {
+ id = "win";
+ if (/NT 5\.1/.test(ua) || /NT 5\.2; Win64/.test(ua))
+ var classname = "winxp";
+}
+else if (/Linux/.test(ua))
+ id = "linux";
+else if (/SunOS/.test(ua))
+ id = "linux";
+else if (/Mac OS X/.test(ua))
+ id = "mac";
+
+if (id)
+ document.documentElement.setAttribute("id", id);
+else
+ document.documentElement.appendChild(
+ document.createTextNode("Unrecognized platform")
+ );
+if (classname)
+ document.documentElement.setAttribute("class", classname);
diff --git a/editor/reftests/xul/reftest-stylo.list b/editor/reftests/xul/reftest-stylo.list
new file mode 100644
index 000000000..cfaa7a058
--- /dev/null
+++ b/editor/reftests/xul/reftest-stylo.list
@@ -0,0 +1,67 @@
+# DO NOT EDIT! This is a auto-generated temporary list for Stylo testing
+fails-if(Android||B2G) skip-if((B2G&&browserIsRemote)||Mulet||((browserIsRemote&&winWidget))) == empty-1.xul empty-1.xul
+# bug 783658
+# Initial mulet triage: parity with B2G/B2G Desktop, Windows: bug 1239170
+skip-if((B2G&&browserIsRemote)||Mulet) == empty-2.xul empty-2.xul
+# Initial mulet triage: parity with B2G/B2G Desktop
+# There is no way to simulate an autocomplete textbox in windows XP/Vista/7/8/10 default theme using CSS.
+# Therefore, the equlity tests below should be marked as failing.
+fails-if(Android||B2G) fails-if(windowsDefaultTheme&&/^Windows\x20NT\x20(5\.[12]|6\.[012]|10\.0)/.test(http.oscpu)) skip-if((B2G&&browserIsRemote)||Mulet) == autocomplete-1.xul autocomplete-1.xul
+# bug 783658
+# Initial mulet triage: parity with B2G/B2G Desktop
+fails-if(Android||B2G) fails-if(windowsDefaultTheme&&/^Windows\x20NT\x20(5\.[12]|6\.[012]|10\.0)/.test(http.oscpu)) skip-if((B2G&&browserIsRemote)||Mulet) == emptyautocomplete-1.xul emptyautocomplete-1.xul
+# bug 783658
+# Initial mulet triage: parity with B2G/B2G Desktop
+skip-if((B2G&&browserIsRemote)||Mulet) == emptymultiline-1.xul emptymultiline-1.xul
+# Initial mulet triage: parity with B2G/B2G Desktop
+fails-if(Android||B2G) skip-if((B2G&&browserIsRemote)||Mulet) == emptymultiline-2.xul emptymultiline-2.xul
+# bug 783658
+# Initial mulet triage: parity with B2G/B2G Desktop
+fails-if(Android||B2G) skip-if((B2G&&browserIsRemote)||Mulet||((browserIsRemote&&winWidget))) == emptytextbox-1.xul emptytextbox-1.xul
+# bug 783658
+# Initial mulet triage: parity with B2G/B2G Desktop, Windows: bug 1239170
+fails-if(Android||B2G) skip-if((B2G&&browserIsRemote)||Mulet||((browserIsRemote&&winWidget))) == emptytextbox-2.xul emptytextbox-2.xul
+# bug 783658
+# Initial mulet triage: parity with B2G/B2G Desktop, Windows: bug 1239170
+fails skip-if((B2G&&browserIsRemote)||Mulet) == emptytextbox-3.xul emptytextbox-3.xul
+# Initial mulet triage: parity with B2G/B2G Desktop
+skip-if((B2G&&browserIsRemote)||Mulet) == emptytextbox-4.xul emptytextbox-4.xul
+# Initial mulet triage: parity with B2G/B2G Desktop
+fails-if(Android||B2G) skip-if((B2G&&browserIsRemote)||Mulet||((browserIsRemote&&winWidget))) == emptytextbox-5.xul emptytextbox-5.xul
+# bug 783658
+# Initial mulet triage: parity with B2G/B2G Desktop, Windows: bug 1239170
+# There is no way to simulate a number textbox in windows XP/Vista/7 default theme using CSS.
+# Therefore, the equlity tests below should be marked as failing.
+skip-if((B2G&&browserIsRemote)||Mulet) == number-1.xul number-1.xul
+# Initial mulet triage: parity with B2G/B2G Desktop
+skip-if((B2G&&browserIsRemote)||Mulet) == number-2.xul number-2.xul
+# Initial mulet triage: parity with B2G/B2G Desktop
+fails-if(Android||B2G) fails-if(windowsDefaultTheme&&/^Windows\x20NT\x20(5\.[12]|6\.[012]|10\.0)/.test(http.oscpu)) skip-if((B2G&&browserIsRemote)||Mulet) == number-3.xul number-3.xul
+# bug 783658
+# Initial mulet triage: parity with B2G/B2G Desktop
+skip-if((B2G&&browserIsRemote)||Mulet) == number-4.xul number-4.xul
+# Initial mulet triage: parity with B2G/B2G Desktop
+fails-if(Android||B2G) fails-if(windowsDefaultTheme&&/^Windows\x20NT\x20(5\.[12]|6\.[012]|10\.0)/.test(http.oscpu)) skip-if((B2G&&browserIsRemote)||Mulet) == number-5.xul number-5.xul
+# bug 783658
+# Initial mulet triage: parity with B2G/B2G Desktop
+fails-if(Android||B2G) fails-if(windowsDefaultTheme&&/^Windows\x20NT\x20(5\.[12]|6\.[012]|10\.0)/.test(http.oscpu)) skip-if((B2G&&browserIsRemote)||Mulet) == numberwithvalue-1.xul numberwithvalue-1.xul
+# bug 783658
+# Initial mulet triage: parity with B2G/B2G Desktop
+fails-if(Android||B2G) skip-if((B2G&&browserIsRemote)||Mulet||((browserIsRemote&&winWidget))) == passwd-1.xul passwd-1.xul
+# bug 783658
+# Initial mulet triage: parity with B2G/B2G Desktop, Windows: bug 1239170
+fails-if(Android||B2G) skip-if((B2G&&browserIsRemote)||Mulet||((browserIsRemote&&winWidget))) == passwd-2.xul passwd-2.xul
+# bug 783658
+# Initial mulet triage: parity with B2G/B2G Desktop, Windows: bug 1239170
+skip-if((B2G&&browserIsRemote)||Mulet) == passwd-3.xul passwd-3.xul
+# Initial mulet triage: parity with B2G/B2G Desktop
+fails-if(Android||B2G) skip-if((B2G&&browserIsRemote)||Mulet) == plain-1.xul plain-1.xul
+# bug 783658
+# Initial mulet triage: parity with B2G/B2G Desktop
+fails-if(Android||B2G) skip-if((B2G&&browserIsRemote)||Mulet||(browserIsRemote&&winWidget)) == textbox-1.xul textbox-1.xul
+# Initial mulet triage: parity with B2G/B2G Desktop, Windows: bug 1239170
+skip-if((B2G&&browserIsRemote)||Mulet) == textbox-disabled.xul textbox-disabled.xul
+# Initial mulet triage: parity with B2G/B2G Desktop
+# Read-only textboxes look like normal textboxes in windows Vista/7 default theme
+fails-if(windowsDefaultTheme&&/^Windows\x20NT\x20(6\.[012]|10\.0)/.test(http.oscpu)) skip-if((B2G&&browserIsRemote)||Mulet||(browserIsRemote&&winWidget)) == textbox-readonly.xul textbox-readonly.xul
+# Initial mulet triage: parity with B2G/B2G Desktop, Windows: bug 1239170
diff --git a/editor/reftests/xul/reftest.list b/editor/reftests/xul/reftest.list
new file mode 100644
index 000000000..76bd9174f
--- /dev/null
+++ b/editor/reftests/xul/reftest.list
@@ -0,0 +1,29 @@
+fails-if(Android) skip-if(browserIsRemote&&winWidget) == empty-1.xul empty-ref.xul # Windows: bug 1239170
+!= empty-2.xul empty-ref.xul
+# There is no way to simulate an autocomplete textbox in windows XP/Vista/7/8/10 default theme using CSS.
+# Therefore, the equlity tests below should be marked as failing.
+fails-if(Android) fails-if(windowsDefaultTheme&&/^Windows\x20NT\x20(5\.[12]|6\.[012]|10\.0)/.test(http.oscpu)) == autocomplete-1.xul autocomplete-ref.xul # bug 783658
+fails-if(Android) fails-if(windowsDefaultTheme&&/^Windows\x20NT\x20(5\.[12]|6\.[012]|10\.0)/.test(http.oscpu)) == emptyautocomplete-1.xul emptyautocomplete-ref.xul # bug 783658
+!= emptymultiline-1.xul emptymultiline-ref.xul
+fails-if(Android) == emptymultiline-2.xul emptymultiline-ref.xul # bug 783658
+fails-if(Android) skip-if(browserIsRemote&&winWidget) == emptytextbox-1.xul emptytextbox-ref.xul # Windows: bug 1239170
+fails-if(Android) skip-if(browserIsRemote&&winWidget) == emptytextbox-2.xul emptytextbox-ref.xul # Windows: bug 1239170
+!= emptytextbox-3.xul emptytextbox-ref.xul
+!= emptytextbox-4.xul emptytextbox-ref.xul
+fails-if(Android) skip-if(browserIsRemote&&winWidget) == emptytextbox-5.xul emptytextbox-ref.xul # Windows: bug 1239170
+# There is no way to simulate a number textbox in windows XP/Vista/7 default theme using CSS.
+# Therefore, the equlity tests below should be marked as failing.
+!= number-1.xul number-ref.xul
+!= number-2.xul number-ref.xul
+fails-if(Android) fails-if(windowsDefaultTheme&&/^Windows\x20NT\x20(5\.[12]|6\.[012]|10\.0)/.test(http.oscpu)) == number-3.xul number-ref.xul # bug 783658
+!= number-4.xul number-ref.xul
+fails-if(Android) fails-if(windowsDefaultTheme&&/^Windows\x20NT\x20(5\.[12]|6\.[012]|10\.0)/.test(http.oscpu)) == number-5.xul number-ref.xul # bug 783658
+fails-if(Android) fails-if(windowsDefaultTheme&&/^Windows\x20NT\x20(5\.[12]|6\.[012]|10\.0)/.test(http.oscpu)) == numberwithvalue-1.xul numberwithvalue-ref.xul # bug 783658
+fails-if(Android) skip-if(browserIsRemote&&winWidget) == passwd-1.xul passwd-ref.xul # Windows: bug 1239170
+fails-if(Android) skip-if(browserIsRemote&&winWidget) == passwd-2.xul passwd-ref.xul # Windows: bug 1239170
+!= passwd-3.xul passwd-ref.xul
+fails-if(Android) == plain-1.xul plain-ref.xul # bug 783658
+fails-if(Android) skip-if(browserIsRemote&&winWidget) == textbox-1.xul textbox-ref.xul # Windows: bug 1239170
+!= textbox-disabled.xul textbox-ref.xul
+# Read-only textboxes look like normal textboxes in windows Vista/7 default theme
+fails-if(windowsDefaultTheme&&/^Windows\x20NT\x20(6\.[012]|10\.0)/.test(http.oscpu)) skip-if(browserIsRemote&&winWidget) != textbox-readonly.xul textbox-ref.xul # Windows: bug 1239170
diff --git a/editor/reftests/xul/textbox-1.xul b/editor/reftests/xul/textbox-1.xul
new file mode 100644
index 000000000..aae7e89ea
--- /dev/null
+++ b/editor/reftests/xul/textbox-1.xul
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <textbox value=" test"/>
+
+</window>
diff --git a/editor/reftests/xul/textbox-disabled.xul b/editor/reftests/xul/textbox-disabled.xul
new file mode 100644
index 000000000..c1ae83a3c
--- /dev/null
+++ b/editor/reftests/xul/textbox-disabled.xul
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <textbox value=" test" disabled="true"/>
+
+</window>
diff --git a/editor/reftests/xul/textbox-readonly.xul b/editor/reftests/xul/textbox-readonly.xul
new file mode 100644
index 000000000..f9d8b5f57
--- /dev/null
+++ b/editor/reftests/xul/textbox-readonly.xul
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <textbox value=" test" readonly="true"/>
+
+</window>
diff --git a/editor/reftests/xul/textbox-ref.xul b/editor/reftests/xul/textbox-ref.xul
new file mode 100644
index 000000000..8be9fc8e4
--- /dev/null
+++ b/editor/reftests/xul/textbox-ref.xul
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="input.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <html:input value=" test"/>
+
+</window>
diff --git a/editor/txmgr/moz.build b/editor/txmgr/moz.build
new file mode 100644
index 000000000..3c7f3b55a
--- /dev/null
+++ b/editor/txmgr/moz.build
@@ -0,0 +1,30 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+TEST_DIRS += ['tests']
+
+XPIDL_SOURCES += [
+ 'nsITransaction.idl',
+ 'nsITransactionList.idl',
+ 'nsITransactionListener.idl',
+ 'nsITransactionManager.idl',
+]
+
+XPIDL_MODULE = 'txmgr'
+
+EXPORTS += [
+ 'nsTransactionManagerCID.h',
+]
+
+UNIFIED_SOURCES += [
+ 'nsTransactionItem.cpp',
+ 'nsTransactionList.cpp',
+ 'nsTransactionManager.cpp',
+ 'nsTransactionManagerFactory.cpp',
+ 'nsTransactionStack.cpp',
+]
+
+FINAL_LIBRARY = 'xul'
diff --git a/editor/txmgr/nsITransaction.idl b/editor/txmgr/nsITransaction.idl
new file mode 100644
index 000000000..7e2e2aee4
--- /dev/null
+++ b/editor/txmgr/nsITransaction.idl
@@ -0,0 +1,63 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+/*
+ * The nsITransaction interface.
+ * <P>
+ * This interface is implemented by an object that needs to
+ * execute some behavior that must be tracked by the transaction manager.
+ */
+[scriptable, uuid(58e330c1-7b48-11d2-98b9-00805f297d89)]
+interface nsITransaction : nsISupports
+{
+ /**
+ * Executes the transaction.
+ */
+ void doTransaction();
+
+ /**
+ * Restores the state to what it was before the transaction was executed.
+ */
+ void undoTransaction();
+
+ /**
+ * Executes the transaction again. Can only be called on a transaction that
+ * was previously undone.
+ * <P>
+ * In most cases, the redoTransaction() method will actually call the
+ * doTransaction() method to execute the transaction again.
+ */
+ void redoTransaction();
+
+ /**
+ * The transaction's transient state. This attribute is checked by
+ * the transaction manager after the transaction's Execute() method is called.
+ * If the transient state is false, a reference to the transaction is
+ * held by the transaction manager so that the transactions' undoTransaction()
+ * and redoTransaction() methods can be called. If the transient state is
+ * true, the transaction manager returns immediately after the transaction's
+ * doTransaction() method is called, no references to the transaction are
+ * maintained. Transient transactions cannot be undone or redone by the
+ * transaction manager.
+ */
+ readonly attribute boolean isTransient;
+
+ /**
+ * Attempts to merge a transaction into "this" transaction. Both transactions
+ * must be in their undo state, doTransaction() methods already called. The
+ * transaction manager calls this method to coalesce a new transaction with
+ * the transaction on the top of the undo stack.
+ * This method returns a boolean value that indicates the merge result.
+ * A true value indicates that the transactions were merged successfully,
+ * a false value if the merge was not possible or failed. If true,
+ * the transaction manager will Release() the new transacton instead of
+ * pushing it on the undo stack.
+ * @param aTransaction the previously executed transaction to merge.
+ */
+ boolean merge(in nsITransaction aTransaction);
+};
+
diff --git a/editor/txmgr/nsITransactionList.idl b/editor/txmgr/nsITransactionList.idl
new file mode 100644
index 000000000..de6d191de
--- /dev/null
+++ b/editor/txmgr/nsITransactionList.idl
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsITransaction;
+
+/*
+ * The nsITransactionList interface.
+ * <P>
+ * The implementation for this interface is provided by the Transaction Manager.
+ * This interface provides a mechanism for accessing the transactions on the
+ * Undo or Redo stacks as well as any auto-aggregated children that a
+ * transaction may have.
+ */
+[scriptable, uuid(d007ceff-c978-486a-b697-384ca01997be)]
+
+interface nsITransactionList : nsISupports
+{
+ /**
+ * The number of transactions contained in this list.
+ */
+ readonly attribute long numItems;
+
+ /**
+ * itemIsBatch() returns true if the item at aIndex is a batch. Note that
+ * currently there is no requirement for a TransactionManager implementation
+ * to associate a toplevel nsITransaction with a batch so it is possible for
+ * itemIsBatch() to return true and getItem() to return null. However, you
+ * can still access the transactions contained in the batch with a call to
+ * getChildListForItem().
+ * @param aIndex The index of the item in the list.
+ */
+ boolean itemIsBatch(in long aIndex);
+
+ /**
+ * getItem() returns the transaction at the given index in the list. Note that
+ * a null can be returned here if the item is a batch. The transaction
+ * returned is AddRef'd so it is up to the caller to Release the transaction
+ * when it is done.
+ * @param aIndex The index of the item in the list.
+ */
+ nsITransaction getItem(in long aIndex);
+
+ /**
+ * getData() returns the data (of type nsISupports array) associated with
+ * the transaction list.
+ */
+ void getData(in long aIndex, [optional] out unsigned long aLength,
+ [array, size_is(aLength), retval] out nsISupports aData);
+
+ /**
+ * getNumChildrenForItem() returns the number of child (auto-aggreated)
+ * transactions the item at aIndex has.
+ * @param aIndex The index of the item in the list.
+ */
+ long getNumChildrenForItem(in long aIndex);
+
+ /**
+ * getChildListForItem() returns the list of children associated with the
+ * item at aIndex. Implementations may return null if there are no children,
+ * or an empty list. The list returned is AddRef'd so it is up to the caller
+ * to Release the transaction when it is done.
+ * @param aIndex The index of the item in the list.
+ */
+ nsITransactionList getChildListForItem(in long aIndex);
+};
+
diff --git a/editor/txmgr/nsITransactionListener.idl b/editor/txmgr/nsITransactionListener.idl
new file mode 100644
index 000000000..c63f9e3b8
--- /dev/null
+++ b/editor/txmgr/nsITransactionListener.idl
@@ -0,0 +1,170 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsITransaction;
+interface nsITransactionManager;
+
+/**
+ * The nsITransactionListener interface.
+ * <P>
+ * This interface is implemented by an object that tracks transactions.
+ */
+[scriptable, uuid(58e330c4-7b48-11d2-98b9-00805f297d89)]
+interface nsITransactionListener : nsISupports
+{
+ /**
+ * Called before a transaction manager calls a transaction's
+ * doTransaction() method.
+ * @param aManager the transaction manager doing the transaction.
+ * @param aTransaction the transaction being executed.
+ * @result boolean value returned by listener which indicates
+ * its desire to interrupt normal control flow. Listeners should
+ * return true if they want to interrupt normal control flow, without
+ * throwing an error.
+ */
+ boolean willDo(in nsITransactionManager aManager,
+ in nsITransaction aTransaction);
+
+ /**
+ * Called after a transaction manager calls the doTransaction() method of
+ * a transaction.
+ * @param aManager the transaction manager that did the transaction.
+ * @param aTransaction the transaction that was executed.
+ * @param aDoResult the nsresult returned after executing
+ * the transaction.
+ */
+ void didDo(in nsITransactionManager aManager,
+ in nsITransaction aTransaction,
+ in nsresult aDoResult);
+
+ /**
+ * Called before a transaction manager calls the Undo() method of
+ * a transaction.
+ * @param aManager the transaction manager undoing the transaction.
+ * @param aTransaction the transaction being undone.
+ * @result boolean value returned by listener which indicates
+ * its desire to interrupt normal control flow. Listeners should
+ * return true if they want to interrupt normal control flow, without
+ * throwing an error. Note that listeners can also interrupt normal
+ * control flow by throwing an nsresult that indicates an error.
+ */
+ boolean willUndo(in nsITransactionManager aManager,
+ in nsITransaction aTransaction);
+
+ /**
+ * Called after a transaction manager calls the Undo() method of
+ * a transaction.
+ * @param aManager the transaction manager undoing the transaction.
+ * @param aTransaction the transaction being undone.
+ * @param aUndoResult the nsresult returned after undoing the transaction.
+ */
+ void didUndo(in nsITransactionManager aManager,
+ in nsITransaction aTransaction,
+ in nsresult aUndoResult);
+
+ /**
+ * Called before a transaction manager calls the Redo() method of
+ * a transaction.
+ * @param aManager the transaction manager redoing the transaction.
+ * @param aTransaction the transaction being redone.
+ * @result boolean value returned by listener which indicates
+ * its desire to interrupt normal control flow. Listeners should
+ * return true if they want to interrupt normal control flow, without
+ * throwing an error. Note that listeners can also interrupt normal
+ * control flow by throwing an nsresult that indicates an error.
+ */
+ boolean willRedo(in nsITransactionManager aManager,
+ in nsITransaction aTransaction);
+
+ /**
+ * Called after a transaction manager calls the Redo() method of
+ * a transaction.
+ * @param aManager the transaction manager redoing the transaction.
+ * @param aTransaction the transaction being redone.
+ * @param aRedoResult the nsresult returned after redoing the transaction.
+ */
+ void didRedo(in nsITransactionManager aManager,
+ in nsITransaction aTransaction,
+ in nsresult aRedoResult);
+
+ /**
+ * Called before a transaction manager begins a batch.
+ * @param aManager the transaction manager beginning a batch.
+ * @result boolean value returned by listener which indicates
+ * its desire to interrupt normal control flow. Listeners should
+ * return true if they want to interrupt normal control flow, without
+ * throwing an error. Note that listeners can also interrupt normal
+ * control flow by throwing an nsresult that indicates an error.
+ */
+ boolean willBeginBatch(in nsITransactionManager aManager);
+
+ /**
+ * Called after a transaction manager begins a batch.
+ * @param aManager the transaction manager that began a batch.
+ * @param aResult the nsresult returned after beginning a batch.
+ */
+ void didBeginBatch(in nsITransactionManager aManager,
+ in nsresult aResult);
+
+ /**
+ * Called before a transaction manager ends a batch.
+ * @param aManager the transaction manager ending a batch.
+ * @result boolean value returned by listener which indicates
+ * its desire to interrupt normal control flow. Listeners should
+ * return true if they want to interrupt normal control flow, without
+ * throwing an error. Note that listeners can also interrupt normal
+ * control flow by throwing an nsresult that indicates an error.
+ */
+ boolean willEndBatch(in nsITransactionManager aManager);
+
+ /**
+ * Called after a transaction manager ends a batch.
+ * @param aManager the transaction manager ending a batch.
+ * @param aResult the nsresult returned after ending a batch.
+ */
+ void didEndBatch(in nsITransactionManager aManager,
+ in nsresult aResult);
+
+ /**
+ * Called before a transaction manager tries to merge
+ * a transaction, that was just executed, with the
+ * transaction at the top of the undo stack.
+ * @param aManager the transaction manager ending a batch.
+ * @param aTopTransaction the transaction at the top of the undo stack.
+ * @param aTransactionToMerge the transaction to merge.
+ * @result boolean value returned by listener which indicates
+ * its desire to interrupt normal control flow. Listeners should
+ * return true if they want to interrupt normal control flow, without
+ * throwing an error. Note that listeners can also interrupt normal
+ * control flow by throwing an nsresult that indicates an error.
+ */
+ boolean willMerge(in nsITransactionManager aManager,
+ in nsITransaction aTopTransaction,
+ in nsITransaction aTransactionToMerge);
+
+ /**
+ * Called after a transaction manager tries to merge
+ * a transaction, that was just executed, with the
+ * transaction at the top of the undo stack.
+ * @param aManager the transaction manager ending a batch.
+ * @param aTopTransaction the transaction at the top of the undo stack.
+ * @param aTransactionToMerge the transaction to merge.
+ * @param aDidMerge true if transaction was merged, else false.
+ * @param aMergeResult the nsresult returned after the merge attempt.
+ * @param aInterrupt listeners should set this to PR_TRUE if they
+ * want to interrupt normal control flow, without throwing an error.
+ */
+ void didMerge(in nsITransactionManager aManager,
+ in nsITransaction aTopTransaction,
+ in nsITransaction aTransactionToMerge,
+ in boolean aDidMerge,
+ in nsresult aMergeResult);
+
+
+ /* XXX: We should probably add pruning notification methods. */
+};
+
diff --git a/editor/txmgr/nsITransactionManager.idl b/editor/txmgr/nsITransactionManager.idl
new file mode 100644
index 000000000..0587d5ca1
--- /dev/null
+++ b/editor/txmgr/nsITransactionManager.idl
@@ -0,0 +1,168 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "nsITransaction.idl"
+#include "nsITransactionList.idl"
+#include "nsITransactionListener.idl"
+
+%{ C++
+
+#define NS_TRANSACTIONMANAGER_CONTRACTID "@mozilla.org/transactionmanager;1"
+
+%} C++
+
+/**
+ * The nsITransactionManager interface.
+ * <P>
+ * This interface is implemented by an object that wants to
+ * manage/track transactions.
+ */
+[scriptable, builtinclass, uuid(c77763df-0fb9-41a8-8074-8e882f605755)]
+interface nsITransactionManager : nsISupports
+{
+ /**
+ * Calls a transaction's doTransaction() method, then pushes it on the
+ * undo stack.
+ * <P>
+ * This method calls the transaction's AddRef() method.
+ * The transaction's Release() method will be called when the undo or redo
+ * stack is pruned or when the transaction manager is destroyed.
+ * @param aTransaction the transaction to do.
+ */
+ void doTransaction(in nsITransaction aTransaction);
+
+ /**
+ * Pops the topmost transaction on the undo stack, calls its
+ * undoTransaction() method, then pushes it on the redo stack.
+ */
+ void undoTransaction();
+
+ /**
+ * Pops the topmost transaction on the redo stack, calls its
+ * redoTransaction() method, then pushes it on the undo stack.
+ */
+ void redoTransaction();
+
+ /**
+ * Clears the undo and redo stacks.
+ */
+ void clear();
+
+ /**
+ * Clears the undo stack only.
+ */
+ void clearUndoStack();
+
+ /**
+ * Clears the redo stack only.
+ */
+ void clearRedoStack();
+
+ /**
+ * Turns on the transaction manager's batch mode, forcing all transactions
+ * executed by the transaction manager's doTransaction() method to be
+ * aggregated together until EndBatch() is called. This mode allows an
+ * application to execute and group together several independent transactions
+ * so they can be undone with a single call to undoTransaction().
+ * @param aData An arbitrary nsISupports object that is associated with the
+ * batch. Can be retrieved from nsITransactionList.
+ */
+ void beginBatch(in nsISupports aData);
+
+ /**
+ * Turns off the transaction manager's batch mode.
+ * @param aAllowEmpty If true, a batch containing no children will be
+ * pushed onto the undo stack. Otherwise, ending a batch with no
+ * children will result in no transactions being pushed on the undo stack.
+ */
+ void endBatch(in boolean aAllowEmpty);
+
+ /**
+ * The number of items on the undo stack.
+ */
+ readonly attribute long numberOfUndoItems;
+
+ /**
+ * The number of items on the redo stack.
+ */
+ readonly attribute long numberOfRedoItems;
+
+ /**
+ * Sets the maximum number of transaction items the transaction manager will
+ * maintain at any time. This is commonly referred to as the number of levels
+ * of undo.
+ * @param aMaxCount A value of -1 means no limit. A value of zero means the
+ * transaction manager will execute each transaction, then immediately release
+ * all references it has to the transaction without pushing it on the undo
+ * stack. A value greater than zero indicates the max number of transactions
+ * that can exist at any time on both the undo and redo stacks. This method
+ * will prune the necessary number of transactions on the undo and redo
+ * stacks if the value specified is less than the number of items that exist
+ * on both the undo and redo stacks.
+ */
+ attribute long maxTransactionCount;
+
+ /**
+ * Combines the transaction at the top of the undo stack (if any) with the
+ * preceding undo transaction (if any) into a batch transaction. Thus,
+ * a call to undoTransaction() will undo both transactions.
+ */
+ void batchTopUndo();
+
+ /**
+ * Removes the transaction at the top of the undo stack (if any) without
+ * transacting.
+ */
+ void removeTopUndo();
+
+ /**
+ * Returns an AddRef'd pointer to the transaction at the top of the
+ * undo stack. Callers should be aware that this method could return
+ * return a null in some implementations if there is a batch at the top
+ * of the undo stack.
+ */
+ nsITransaction peekUndoStack();
+
+ /**
+ * Returns an AddRef'd pointer to the transaction at the top of the
+ * redo stack. Callers should be aware that this method could return
+ * return a null in some implementations if there is a batch at the top
+ * of the redo stack.
+ */
+ nsITransaction peekRedoStack();
+
+ /**
+ * Returns the list of transactions on the undo stack. Note that the
+ * transaction at the top of the undo stack will actually be at the
+ * index 'n-1' in the list, where 'n' is the number of items in the list.
+ */
+ nsITransactionList getUndoList();
+
+ /**
+ * Returns the list of transactions on the redo stack. Note that the
+ * transaction at the top of the redo stack will actually be at the
+ * index 'n-1' in the list, where 'n' is the number of items in the list.
+ */
+ nsITransactionList getRedoList();
+
+ /**
+ * Adds a listener to the transaction manager's notification list. Listeners
+ * are notified whenever a transaction is done, undone, or redone.
+ * <P>
+ * The listener's AddRef() method is called.
+ * @param aListener the lister to add.
+ */
+ void AddListener(in nsITransactionListener aListener);
+
+ /**
+ * Removes a listener from the transaction manager's notification list.
+ * <P>
+ * The listener's Release() method is called.
+ * @param aListener the lister to remove.
+ */
+ void RemoveListener(in nsITransactionListener aListener);
+};
+
diff --git a/editor/txmgr/nsTransactionItem.cpp b/editor/txmgr/nsTransactionItem.cpp
new file mode 100644
index 000000000..afea20a0b
--- /dev/null
+++ b/editor/txmgr/nsTransactionItem.cpp
@@ -0,0 +1,349 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/mozalloc.h"
+#include "nsCOMPtr.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsISupportsImpl.h"
+#include "nsITransaction.h"
+#include "nsTransactionItem.h"
+#include "nsTransactionManager.h"
+#include "nsTransactionStack.h"
+
+nsTransactionItem::nsTransactionItem(nsITransaction *aTransaction)
+ : mTransaction(aTransaction), mUndoStack(0), mRedoStack(0)
+{
+}
+
+nsTransactionItem::~nsTransactionItem()
+{
+ delete mRedoStack;
+ delete mUndoStack;
+}
+
+void
+nsTransactionItem::CleanUp()
+{
+ mData.Clear();
+ mTransaction = nullptr;
+ if (mRedoStack) {
+ mRedoStack->DoUnlink();
+ }
+ if (mUndoStack) {
+ mUndoStack->DoUnlink();
+ }
+}
+
+NS_IMPL_CYCLE_COLLECTING_NATIVE_ADDREF(nsTransactionItem)
+NS_IMPL_CYCLE_COLLECTING_NATIVE_RELEASE_WITH_LAST_RELEASE(nsTransactionItem,
+ CleanUp())
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(nsTransactionItem)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsTransactionItem)
+ tmp->CleanUp();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsTransactionItem)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mData)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTransaction)
+ if (tmp->mRedoStack) {
+ tmp->mRedoStack->DoTraverse(cb);
+ }
+ if (tmp->mUndoStack) {
+ tmp->mUndoStack->DoTraverse(cb);
+ }
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(nsTransactionItem, AddRef)
+NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(nsTransactionItem, Release)
+
+nsresult
+nsTransactionItem::AddChild(nsTransactionItem *aTransactionItem)
+{
+ NS_ENSURE_TRUE(aTransactionItem, NS_ERROR_NULL_POINTER);
+
+ if (!mUndoStack) {
+ mUndoStack = new nsTransactionStack(nsTransactionStack::FOR_UNDO);
+ }
+
+ mUndoStack->Push(aTransactionItem);
+ return NS_OK;
+}
+
+already_AddRefed<nsITransaction>
+nsTransactionItem::GetTransaction()
+{
+ nsCOMPtr<nsITransaction> txn = mTransaction;
+ return txn.forget();
+}
+
+nsresult
+nsTransactionItem::GetIsBatch(bool *aIsBatch)
+{
+ NS_ENSURE_TRUE(aIsBatch, NS_ERROR_NULL_POINTER);
+ *aIsBatch = !mTransaction;
+ return NS_OK;
+}
+
+nsresult
+nsTransactionItem::GetNumberOfChildren(int32_t *aNumChildren)
+{
+ NS_ENSURE_TRUE(aNumChildren, NS_ERROR_NULL_POINTER);
+
+ *aNumChildren = 0;
+
+ int32_t ui = 0;
+ nsresult rv = GetNumberOfUndoItems(&ui);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t ri = 0;
+ rv = GetNumberOfRedoItems(&ri);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aNumChildren = ui + ri;
+ return NS_OK;
+}
+
+nsresult
+nsTransactionItem::GetChild(int32_t aIndex, nsTransactionItem **aChild)
+{
+ NS_ENSURE_TRUE(aChild, NS_ERROR_NULL_POINTER);
+
+ *aChild = 0;
+
+ int32_t numItems = 0;
+ nsresult rv = GetNumberOfChildren(&numItems);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (aIndex < 0 || aIndex >= numItems) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Children are expected to be in the order they were added,
+ // so the child first added would be at the bottom of the undo
+ // stack, or if there are no items on the undo stack, it would
+ // be at the top of the redo stack.
+ rv = GetNumberOfUndoItems(&numItems);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (numItems > 0 && aIndex < numItems) {
+ NS_ENSURE_TRUE(mUndoStack, NS_ERROR_FAILURE);
+
+ RefPtr<nsTransactionItem> child = mUndoStack->GetItem(aIndex);
+ child.forget(aChild);
+ return *aChild ? NS_OK : NS_ERROR_FAILURE;
+ }
+
+ // Adjust the index for the redo stack:
+ aIndex -= numItems;
+
+ rv = GetNumberOfRedoItems(&numItems);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(mRedoStack && numItems != 0 && aIndex < numItems, NS_ERROR_FAILURE);
+
+ RefPtr<nsTransactionItem> child = mRedoStack->GetItem(aIndex);
+ child.forget(aChild);
+ return *aChild ? NS_OK : NS_ERROR_FAILURE;
+}
+
+nsresult
+nsTransactionItem::DoTransaction()
+{
+ if (mTransaction) {
+ return mTransaction->DoTransaction();
+ }
+ return NS_OK;
+}
+
+nsresult
+nsTransactionItem::UndoTransaction(nsTransactionManager *aTxMgr)
+{
+ nsresult rv = UndoChildren(aTxMgr);
+ if (NS_FAILED(rv)) {
+ RecoverFromUndoError(aTxMgr);
+ return rv;
+ }
+
+ if (!mTransaction) {
+ return NS_OK;
+ }
+
+ rv = mTransaction->UndoTransaction();
+ if (NS_FAILED(rv)) {
+ RecoverFromUndoError(aTxMgr);
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsTransactionItem::UndoChildren(nsTransactionManager *aTxMgr)
+{
+ if (mUndoStack) {
+ if (!mRedoStack && mUndoStack) {
+ mRedoStack = new nsTransactionStack(nsTransactionStack::FOR_REDO);
+ }
+
+ /* Undo all of the transaction items children! */
+ int32_t sz = mUndoStack->GetSize();
+
+ nsresult rv = NS_OK;
+ while (sz-- > 0) {
+ RefPtr<nsTransactionItem> item = mUndoStack->Peek();
+ if (!item) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsITransaction> t = item->GetTransaction();
+ bool doInterrupt = false;
+ rv = aTxMgr->WillUndoNotify(t, &doInterrupt);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (doInterrupt) {
+ return NS_OK;
+ }
+
+ rv = item->UndoTransaction(aTxMgr);
+ if (NS_SUCCEEDED(rv)) {
+ item = mUndoStack->Pop();
+ mRedoStack->Push(item.forget());
+ }
+
+ nsresult rv2 = aTxMgr->DidUndoNotify(t, rv);
+ if (NS_SUCCEEDED(rv)) {
+ rv = rv2;
+ }
+ }
+ // XXX NS_OK if there is no Undo items or all methods work fine, otherwise,
+ // the result of the last item's UndoTransaction() or
+ // DidUndoNotify() if UndoTransaction() succeeded.
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsTransactionItem::RedoTransaction(nsTransactionManager *aTxMgr)
+{
+ nsCOMPtr<nsITransaction> transaction(mTransaction);
+ if (transaction) {
+ nsresult rv = transaction->RedoTransaction();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsresult rv = RedoChildren(aTxMgr);
+ if (NS_FAILED(rv)) {
+ RecoverFromRedoError(aTxMgr);
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsTransactionItem::RedoChildren(nsTransactionManager *aTxMgr)
+{
+ if (!mRedoStack) {
+ return NS_OK;
+ }
+
+ /* Redo all of the transaction items children! */
+ int32_t sz = mRedoStack->GetSize();
+
+ nsresult rv = NS_OK;
+ while (sz-- > 0) {
+ RefPtr<nsTransactionItem> item = mRedoStack->Peek();
+ if (!item) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsITransaction> t = item->GetTransaction();
+ bool doInterrupt = false;
+ rv = aTxMgr->WillRedoNotify(t, &doInterrupt);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (doInterrupt) {
+ return NS_OK;
+ }
+
+ rv = item->RedoTransaction(aTxMgr);
+ if (NS_SUCCEEDED(rv)) {
+ item = mRedoStack->Pop();
+ mUndoStack->Push(item.forget());
+ }
+
+ // XXX Shouldn't this DidRedoNotify()? (bug 1311626)
+ nsresult rv2 = aTxMgr->DidUndoNotify(t, rv);
+ if (NS_SUCCEEDED(rv)) {
+ rv = rv2;
+ }
+ }
+ // XXX NS_OK if there is no Redo items or all methods work fine, otherwise,
+ // the result of the last item's RedoTransaction() or
+ // DidUndoNotify() if UndoTransaction() succeeded.
+ return rv;
+}
+
+nsresult
+nsTransactionItem::GetNumberOfUndoItems(int32_t *aNumItems)
+{
+ NS_ENSURE_TRUE(aNumItems, NS_ERROR_NULL_POINTER);
+
+ if (!mUndoStack) {
+ *aNumItems = 0;
+ return NS_OK;
+ }
+
+ *aNumItems = mUndoStack->GetSize();
+ return *aNumItems ? NS_OK : NS_ERROR_FAILURE;
+}
+
+nsresult
+nsTransactionItem::GetNumberOfRedoItems(int32_t *aNumItems)
+{
+ NS_ENSURE_TRUE(aNumItems, NS_ERROR_NULL_POINTER);
+
+ if (!mRedoStack) {
+ *aNumItems = 0;
+ return NS_OK;
+ }
+
+ *aNumItems = mRedoStack->GetSize();
+ return *aNumItems ? NS_OK : NS_ERROR_FAILURE;
+}
+
+nsresult
+nsTransactionItem::RecoverFromUndoError(nsTransactionManager *aTxMgr)
+{
+ // If this method gets called, we never got to the point where we
+ // successfully called UndoTransaction() for the transaction item itself.
+ // Just redo any children that successfully called undo!
+ return RedoChildren(aTxMgr);
+}
+
+nsresult
+nsTransactionItem::RecoverFromRedoError(nsTransactionManager *aTxMgr)
+{
+ // If this method gets called, we already successfully called
+ // RedoTransaction() for the transaction item itself. Undo all
+ // the children that successfully called RedoTransaction(),
+ // then undo the transaction item itself.
+ nsresult rv = UndoChildren(aTxMgr);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (!mTransaction) {
+ return NS_OK;
+ }
+
+ return mTransaction->UndoTransaction();
+}
+
diff --git a/editor/txmgr/nsTransactionItem.h b/editor/txmgr/nsTransactionItem.h
new file mode 100644
index 000000000..dc868203b
--- /dev/null
+++ b/editor/txmgr/nsTransactionItem.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsTransactionItem_h__
+#define nsTransactionItem_h__
+
+#include "nsCOMPtr.h"
+#include "nsCOMArray.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupportsImpl.h"
+#include "nscore.h"
+
+class nsITransaction;
+class nsTransactionManager;
+class nsTransactionStack;
+
+class nsTransactionItem final
+{
+ nsCOMArray<nsISupports> mData;
+ nsCOMPtr<nsITransaction> mTransaction;
+ nsTransactionStack *mUndoStack;
+ nsTransactionStack *mRedoStack;
+
+public:
+
+ explicit nsTransactionItem(nsITransaction *aTransaction);
+ NS_METHOD_(MozExternalRefCountType) AddRef();
+ NS_METHOD_(MozExternalRefCountType) Release();
+
+ NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(nsTransactionItem)
+
+ virtual nsresult AddChild(nsTransactionItem *aTransactionItem);
+ already_AddRefed<nsITransaction> GetTransaction();
+ virtual nsresult GetIsBatch(bool *aIsBatch);
+ virtual nsresult GetNumberOfChildren(int32_t *aNumChildren);
+ virtual nsresult GetChild(int32_t aIndex, nsTransactionItem **aChild);
+
+ virtual nsresult DoTransaction(void);
+ virtual nsresult UndoTransaction(nsTransactionManager *aTxMgr);
+ virtual nsresult RedoTransaction(nsTransactionManager *aTxMgr);
+
+ nsCOMArray<nsISupports>& GetData()
+ {
+ return mData;
+ }
+
+private:
+
+ virtual nsresult UndoChildren(nsTransactionManager *aTxMgr);
+ virtual nsresult RedoChildren(nsTransactionManager *aTxMgr);
+
+ virtual nsresult RecoverFromUndoError(nsTransactionManager *aTxMgr);
+ virtual nsresult RecoverFromRedoError(nsTransactionManager *aTxMgr);
+
+ virtual nsresult GetNumberOfUndoItems(int32_t *aNumItems);
+ virtual nsresult GetNumberOfRedoItems(int32_t *aNumItems);
+
+ void CleanUp();
+protected:
+ virtual ~nsTransactionItem();
+
+ nsCycleCollectingAutoRefCnt mRefCnt;
+ NS_DECL_OWNINGTHREAD
+};
+
+#endif // nsTransactionItem_h__
diff --git a/editor/txmgr/nsTransactionList.cpp b/editor/txmgr/nsTransactionList.cpp
new file mode 100644
index 000000000..756edfd49
--- /dev/null
+++ b/editor/txmgr/nsTransactionList.cpp
@@ -0,0 +1,174 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/mozalloc.h"
+#include "nsCOMPtr.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsID.h"
+#include "nsISupportsUtils.h"
+#include "nsITransactionManager.h"
+#include "nsTransactionItem.h"
+#include "nsTransactionList.h"
+#include "nsTransactionStack.h"
+#include "nscore.h"
+
+NS_IMPL_ISUPPORTS(nsTransactionList, nsITransactionList)
+
+nsTransactionList::nsTransactionList(nsITransactionManager *aTxnMgr, nsTransactionStack *aTxnStack)
+ : mTxnStack(aTxnStack)
+ , mTxnItem(nullptr)
+{
+ if (aTxnMgr)
+ mTxnMgr = do_GetWeakReference(aTxnMgr);
+}
+
+nsTransactionList::nsTransactionList(nsITransactionManager *aTxnMgr, nsTransactionItem *aTxnItem)
+ : mTxnStack(0)
+ , mTxnItem(aTxnItem)
+{
+ if (aTxnMgr)
+ mTxnMgr = do_GetWeakReference(aTxnMgr);
+}
+
+nsTransactionList::~nsTransactionList()
+{
+ mTxnStack = 0;
+ mTxnItem = nullptr;
+}
+
+NS_IMETHODIMP nsTransactionList::GetNumItems(int32_t *aNumItems)
+{
+ NS_ENSURE_TRUE(aNumItems, NS_ERROR_NULL_POINTER);
+
+ *aNumItems = 0;
+
+ nsCOMPtr<nsITransactionManager> txMgr = do_QueryReferent(mTxnMgr);
+ NS_ENSURE_TRUE(txMgr, NS_ERROR_FAILURE);
+
+ if (mTxnStack) {
+ *aNumItems = mTxnStack->GetSize();
+ } else if (mTxnItem) {
+ return mTxnItem->GetNumberOfChildren(aNumItems);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTransactionList::ItemIsBatch(int32_t aIndex, bool *aIsBatch)
+{
+ NS_ENSURE_TRUE(aIsBatch, NS_ERROR_NULL_POINTER);
+
+ *aIsBatch = false;
+
+ nsCOMPtr<nsITransactionManager> txMgr = do_QueryReferent(mTxnMgr);
+ NS_ENSURE_TRUE(txMgr, NS_ERROR_FAILURE);
+
+ RefPtr<nsTransactionItem> item;
+ if (mTxnStack) {
+ item = mTxnStack->GetItem(aIndex);
+ } else if (mTxnItem) {
+ nsresult rv = mTxnItem->GetChild(aIndex, getter_AddRefs(item));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ NS_ENSURE_TRUE(item, NS_ERROR_FAILURE);
+
+ return item->GetIsBatch(aIsBatch);
+}
+
+NS_IMETHODIMP nsTransactionList::GetData(int32_t aIndex,
+ uint32_t *aLength,
+ nsISupports ***aData)
+{
+ nsCOMPtr<nsITransactionManager> txMgr = do_QueryReferent(mTxnMgr);
+ NS_ENSURE_TRUE(txMgr, NS_ERROR_FAILURE);
+
+ RefPtr<nsTransactionItem> item;
+ if (mTxnStack) {
+ item = mTxnStack->GetItem(aIndex);
+ } else if (mTxnItem) {
+ nsresult rv = mTxnItem->GetChild(aIndex, getter_AddRefs(item));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMArray<nsISupports>& data = item->GetData();
+ nsISupports** ret = static_cast<nsISupports**>(moz_xmalloc(data.Count() *
+ sizeof(nsISupports*)));
+
+ for (int32_t i = 0; i < data.Count(); i++) {
+ NS_ADDREF(ret[i] = data[i]);
+ }
+ *aLength = data.Count();
+ *aData = ret;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTransactionList::GetItem(int32_t aIndex, nsITransaction **aItem)
+{
+ NS_ENSURE_TRUE(aItem, NS_ERROR_NULL_POINTER);
+
+ *aItem = 0;
+
+ nsCOMPtr<nsITransactionManager> txMgr = do_QueryReferent(mTxnMgr);
+ NS_ENSURE_TRUE(txMgr, NS_ERROR_FAILURE);
+
+ RefPtr<nsTransactionItem> item;
+ if (mTxnStack) {
+ item = mTxnStack->GetItem(aIndex);
+ } else if (mTxnItem) {
+ nsresult rv = mTxnItem->GetChild(aIndex, getter_AddRefs(item));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ NS_ENSURE_TRUE(item, NS_ERROR_FAILURE);
+
+ *aItem = item->GetTransaction().take();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTransactionList::GetNumChildrenForItem(int32_t aIndex, int32_t *aNumChildren)
+{
+ NS_ENSURE_TRUE(aNumChildren, NS_ERROR_NULL_POINTER);
+
+ *aNumChildren = 0;
+
+ nsCOMPtr<nsITransactionManager> txMgr = do_QueryReferent(mTxnMgr);
+ NS_ENSURE_TRUE(txMgr, NS_ERROR_FAILURE);
+
+ RefPtr<nsTransactionItem> item;
+ if (mTxnStack) {
+ item = mTxnStack->GetItem(aIndex);
+ } else if (mTxnItem) {
+ nsresult rv = mTxnItem->GetChild(aIndex, getter_AddRefs(item));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ NS_ENSURE_TRUE(item, NS_ERROR_FAILURE);
+
+ return item->GetNumberOfChildren(aNumChildren);
+}
+
+NS_IMETHODIMP nsTransactionList::GetChildListForItem(int32_t aIndex, nsITransactionList **aTxnList)
+{
+ NS_ENSURE_TRUE(aTxnList, NS_ERROR_NULL_POINTER);
+
+ *aTxnList = 0;
+
+ nsCOMPtr<nsITransactionManager> txMgr = do_QueryReferent(mTxnMgr);
+ NS_ENSURE_TRUE(txMgr, NS_ERROR_FAILURE);
+
+ RefPtr<nsTransactionItem> item;
+ if (mTxnStack) {
+ item = mTxnStack->GetItem(aIndex);
+ } else if (mTxnItem) {
+ nsresult rv = mTxnItem->GetChild(aIndex, getter_AddRefs(item));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ NS_ENSURE_TRUE(item, NS_ERROR_FAILURE);
+
+ *aTxnList = (nsITransactionList *)new nsTransactionList(txMgr, item);
+ NS_ENSURE_TRUE(*aTxnList, NS_ERROR_OUT_OF_MEMORY);
+
+ NS_ADDREF(*aTxnList);
+ return NS_OK;
+}
diff --git a/editor/txmgr/nsTransactionList.h b/editor/txmgr/nsTransactionList.h
new file mode 100644
index 000000000..e3dde1255
--- /dev/null
+++ b/editor/txmgr/nsTransactionList.h
@@ -0,0 +1,46 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsTransactionList_h__
+#define nsTransactionList_h__
+
+#include "nsISupportsImpl.h"
+#include "nsITransactionList.h"
+#include "nsIWeakReferenceUtils.h"
+
+class nsITransaction;
+class nsITransactionManager;
+class nsTransactionItem;
+class nsTransactionStack;
+
+/** implementation of a transaction list object.
+ *
+ */
+class nsTransactionList : public nsITransactionList
+{
+private:
+
+ nsWeakPtr mTxnMgr;
+ nsTransactionStack *mTxnStack;
+ RefPtr<nsTransactionItem> mTxnItem;
+
+protected:
+ virtual ~nsTransactionList();
+
+public:
+
+ nsTransactionList(nsITransactionManager *aTxnMgr, nsTransactionStack *aTxnStack);
+ nsTransactionList(nsITransactionManager *aTxnMgr, nsTransactionItem *aTxnItem);
+
+ /* Macro for AddRef(), Release(), and QueryInterface() */
+ NS_DECL_ISUPPORTS
+
+ /* nsITransactionManager method implementations. */
+ NS_DECL_NSITRANSACTIONLIST
+
+ /* nsTransactionList specific methods. */
+};
+
+#endif // nsTransactionList_h__
diff --git a/editor/txmgr/nsTransactionManager.cpp b/editor/txmgr/nsTransactionManager.cpp
new file mode 100644
index 000000000..10f9c3d06
--- /dev/null
+++ b/editor/txmgr/nsTransactionManager.cpp
@@ -0,0 +1,752 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/Assertions.h"
+#include "mozilla/mozalloc.h"
+#include "nsCOMPtr.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsISupportsBase.h"
+#include "nsISupportsUtils.h"
+#include "nsITransaction.h"
+#include "nsITransactionList.h"
+#include "nsITransactionListener.h"
+#include "nsIWeakReference.h"
+#include "nsTransactionItem.h"
+#include "nsTransactionList.h"
+#include "nsTransactionManager.h"
+#include "nsTransactionStack.h"
+
+nsTransactionManager::nsTransactionManager(int32_t aMaxTransactionCount)
+ : mMaxTransactionCount(aMaxTransactionCount)
+ , mDoStack(nsTransactionStack::FOR_UNDO)
+ , mUndoStack(nsTransactionStack::FOR_UNDO)
+ , mRedoStack(nsTransactionStack::FOR_REDO)
+{
+}
+
+nsTransactionManager::~nsTransactionManager()
+{
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(nsTransactionManager)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsTransactionManager)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mListeners)
+ tmp->mDoStack.DoUnlink();
+ tmp->mUndoStack.DoUnlink();
+ tmp->mRedoStack.DoUnlink();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsTransactionManager)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mListeners)
+ tmp->mDoStack.DoTraverse(cb);
+ tmp->mUndoStack.DoTraverse(cb);
+ tmp->mRedoStack.DoTraverse(cb);
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsTransactionManager)
+ NS_INTERFACE_MAP_ENTRY(nsITransactionManager)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsITransactionManager)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(nsTransactionManager)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(nsTransactionManager)
+
+NS_IMETHODIMP
+nsTransactionManager::DoTransaction(nsITransaction *aTransaction)
+{
+ NS_ENSURE_TRUE(aTransaction, NS_ERROR_NULL_POINTER);
+
+ bool doInterrupt = false;
+
+ nsresult rv = WillDoNotify(aTransaction, &doInterrupt);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (doInterrupt) {
+ return NS_OK;
+ }
+
+ rv = BeginTransaction(aTransaction, nullptr);
+ if (NS_FAILED(rv)) {
+ DidDoNotify(aTransaction, rv);
+ return rv;
+ }
+
+ rv = EndTransaction(false);
+
+ nsresult rv2 = DidDoNotify(aTransaction, rv);
+ if (NS_SUCCEEDED(rv)) {
+ rv = rv2;
+ }
+
+ // XXX The result of EndTransaction() or DidDoNotify() if EndTransaction()
+ // succeeded.
+ return rv;
+}
+
+NS_IMETHODIMP
+nsTransactionManager::UndoTransaction()
+{
+ // It is illegal to call UndoTransaction() while the transaction manager is
+ // executing a transaction's DoTransaction() method! If this happens,
+ // the UndoTransaction() request is ignored, and we return NS_ERROR_FAILURE.
+ if (!mDoStack.IsEmpty()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Peek at the top of the undo stack. Don't remove the transaction
+ // until it has successfully completed.
+ RefPtr<nsTransactionItem> tx = mUndoStack.Peek();
+ if (!tx) {
+ // Bail if there's nothing on the stack.
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsITransaction> t = tx->GetTransaction();
+ bool doInterrupt = false;
+ nsresult rv = WillUndoNotify(t, &doInterrupt);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (doInterrupt) {
+ return NS_OK;
+ }
+
+ rv = tx->UndoTransaction(this);
+ if (NS_SUCCEEDED(rv)) {
+ tx = mUndoStack.Pop();
+ mRedoStack.Push(tx.forget());
+ }
+
+ nsresult rv2 = DidUndoNotify(t, rv);
+ if (NS_SUCCEEDED(rv)) {
+ rv = rv2;
+ }
+
+ // XXX The result of UndoTransaction() or DidUndoNotify() if UndoTransaction()
+ // succeeded.
+ return rv;
+}
+
+NS_IMETHODIMP
+nsTransactionManager::RedoTransaction()
+{
+ // It is illegal to call RedoTransaction() while the transaction manager is
+ // executing a transaction's DoTransaction() method! If this happens,
+ // the RedoTransaction() request is ignored, and we return NS_ERROR_FAILURE.
+ if (!mDoStack.IsEmpty()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Peek at the top of the redo stack. Don't remove the transaction
+ // until it has successfully completed.
+ RefPtr<nsTransactionItem> tx = mRedoStack.Peek();
+ if (!tx) {
+ // Bail if there's nothing on the stack.
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsITransaction> t = tx->GetTransaction();
+ bool doInterrupt = false;
+ nsresult rv = WillRedoNotify(t, &doInterrupt);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (doInterrupt) {
+ return NS_OK;
+ }
+
+ rv = tx->RedoTransaction(this);
+ if (NS_SUCCEEDED(rv)) {
+ tx = mRedoStack.Pop();
+ mUndoStack.Push(tx.forget());
+ }
+
+ nsresult rv2 = DidRedoNotify(t, rv);
+ if (NS_SUCCEEDED(rv)) {
+ rv = rv2;
+ }
+
+ // XXX The result of RedoTransaction() or DidRedoNotify() if RedoTransaction()
+ // succeeded.
+ return rv;
+}
+
+NS_IMETHODIMP
+nsTransactionManager::Clear()
+{
+ nsresult rv = ClearRedoStack();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ return ClearUndoStack();
+}
+
+NS_IMETHODIMP
+nsTransactionManager::BeginBatch(nsISupports* aData)
+{
+ // We can batch independent transactions together by simply pushing
+ // a dummy transaction item on the do stack. This dummy transaction item
+ // will be popped off the do stack, and then pushed on the undo stack
+ // in EndBatch().
+ bool doInterrupt = false;
+ nsresult rv = WillBeginBatchNotify(&doInterrupt);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (doInterrupt) {
+ return NS_OK;
+ }
+
+ rv = BeginTransaction(0, aData);
+
+ nsresult rv2 = DidBeginBatchNotify(rv);
+ if (NS_SUCCEEDED(rv)) {
+ rv = rv2;
+ }
+
+ // XXX The result of BeginTransaction() or DidBeginBatchNotify() if
+ // BeginTransaction() succeeded.
+ return rv;
+}
+
+NS_IMETHODIMP
+nsTransactionManager::EndBatch(bool aAllowEmpty)
+{
+ // XXX: Need to add some mechanism to detect the case where the transaction
+ // at the top of the do stack isn't the dummy transaction, so we can
+ // throw an error!! This can happen if someone calls EndBatch() within
+ // the DoTransaction() method of a transaction.
+ //
+ // For now, we can detect this case by checking the value of the
+ // dummy transaction's mTransaction field. If it is our dummy
+ // transaction, it should be nullptr. This may not be true in the
+ // future when we allow users to execute a transaction when beginning
+ // a batch!!!!
+ RefPtr<nsTransactionItem> tx = mDoStack.Peek();
+ nsCOMPtr<nsITransaction> ti;
+ if (tx) {
+ ti = tx->GetTransaction();
+ }
+ if (!tx || ti) {
+ return NS_ERROR_FAILURE;
+ }
+
+ bool doInterrupt = false;
+ nsresult rv = WillEndBatchNotify(&doInterrupt);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (doInterrupt) {
+ return NS_OK;
+ }
+
+ rv = EndTransaction(aAllowEmpty);
+ nsresult rv2 = DidEndBatchNotify(rv);
+ if (NS_SUCCEEDED(rv)) {
+ rv = rv2;
+ }
+
+ // XXX The result of EndTransaction() or DidEndBatchNotify() if
+ // EndTransaction() succeeded.
+ return rv;
+}
+
+NS_IMETHODIMP
+nsTransactionManager::GetNumberOfUndoItems(int32_t *aNumItems)
+{
+ *aNumItems = mUndoStack.GetSize();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTransactionManager::GetNumberOfRedoItems(int32_t *aNumItems)
+{
+ *aNumItems = mRedoStack.GetSize();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTransactionManager::GetMaxTransactionCount(int32_t *aMaxCount)
+{
+ NS_ENSURE_TRUE(aMaxCount, NS_ERROR_NULL_POINTER);
+ *aMaxCount = mMaxTransactionCount;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTransactionManager::SetMaxTransactionCount(int32_t aMaxCount)
+{
+ // It is illegal to call SetMaxTransactionCount() while the transaction
+ // manager is executing a transaction's DoTransaction() method because
+ // the undo and redo stacks might get pruned! If this happens, the
+ // SetMaxTransactionCount() request is ignored, and we return
+ // NS_ERROR_FAILURE.
+ if (!mDoStack.IsEmpty()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // If aMaxCount is less than zero, the user wants unlimited
+ // levels of undo! No need to prune the undo or redo stacks!
+ if (aMaxCount < 0) {
+ mMaxTransactionCount = -1;
+ return NS_OK;
+ }
+
+ // If aMaxCount is greater than the number of transactions that currently
+ // exist on the undo and redo stack, there is no need to prune the
+ // undo or redo stacks!
+ int32_t numUndoItems = mUndoStack.GetSize();
+ int32_t numRedoItems = mRedoStack.GetSize();
+ int32_t total = numUndoItems + numRedoItems;
+ if (aMaxCount > total) {
+ mMaxTransactionCount = aMaxCount;
+ return NS_OK;
+ }
+
+ // Try getting rid of some transactions on the undo stack! Start at
+ // the bottom of the stack and pop towards the top.
+ while (numUndoItems > 0 && (numRedoItems + numUndoItems) > aMaxCount) {
+ RefPtr<nsTransactionItem> tx = mUndoStack.PopBottom();
+ if (!tx) {
+ return NS_ERROR_FAILURE;
+ }
+ --numUndoItems;
+ }
+
+ // If necessary, get rid of some transactions on the redo stack! Start at
+ // the bottom of the stack and pop towards the top.
+ while (numRedoItems > 0 && (numRedoItems + numUndoItems) > aMaxCount) {
+ RefPtr<nsTransactionItem> tx = mRedoStack.PopBottom();
+ if (!tx) {
+ return NS_ERROR_FAILURE;
+ }
+ --numRedoItems;
+ }
+
+ mMaxTransactionCount = aMaxCount;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTransactionManager::PeekUndoStack(nsITransaction **aTransaction)
+{
+ MOZ_ASSERT(aTransaction);
+ *aTransaction = PeekUndoStack().take();
+ return NS_OK;
+}
+
+already_AddRefed<nsITransaction>
+nsTransactionManager::PeekUndoStack()
+{
+ RefPtr<nsTransactionItem> tx = mUndoStack.Peek();
+ if (!tx) {
+ return nullptr;
+ }
+ return tx->GetTransaction();
+}
+
+NS_IMETHODIMP
+nsTransactionManager::PeekRedoStack(nsITransaction** aTransaction)
+{
+ MOZ_ASSERT(aTransaction);
+ *aTransaction = PeekRedoStack().take();
+ return NS_OK;
+}
+
+already_AddRefed<nsITransaction>
+nsTransactionManager::PeekRedoStack()
+{
+ RefPtr<nsTransactionItem> tx = mRedoStack.Peek();
+ if (!tx) {
+ return nullptr;
+ }
+ return tx->GetTransaction();
+}
+
+NS_IMETHODIMP
+nsTransactionManager::GetUndoList(nsITransactionList **aTransactionList)
+{
+ NS_ENSURE_TRUE(aTransactionList, NS_ERROR_NULL_POINTER);
+
+ *aTransactionList = (nsITransactionList *)new nsTransactionList(this, &mUndoStack);
+ NS_IF_ADDREF(*aTransactionList);
+ return (! *aTransactionList) ? NS_ERROR_OUT_OF_MEMORY : NS_OK;
+}
+
+NS_IMETHODIMP
+nsTransactionManager::GetRedoList(nsITransactionList **aTransactionList)
+{
+ NS_ENSURE_TRUE(aTransactionList, NS_ERROR_NULL_POINTER);
+
+ *aTransactionList = (nsITransactionList *)new nsTransactionList(this, &mRedoStack);
+ NS_IF_ADDREF(*aTransactionList);
+ return (! *aTransactionList) ? NS_ERROR_OUT_OF_MEMORY : NS_OK;
+}
+
+nsresult
+nsTransactionManager::BatchTopUndo()
+{
+ if (mUndoStack.GetSize() < 2) {
+ // Not enough transactions to merge into one batch.
+ return NS_OK;
+ }
+
+ RefPtr<nsTransactionItem> lastUndo;
+ RefPtr<nsTransactionItem> previousUndo;
+
+ lastUndo = mUndoStack.Pop();
+ MOZ_ASSERT(lastUndo, "There should be at least two transactions.");
+
+ previousUndo = mUndoStack.Peek();
+ MOZ_ASSERT(previousUndo, "There should be at least two transactions.");
+
+ nsresult rv = previousUndo->AddChild(lastUndo);
+
+ // Transfer data from the transactions that is going to be
+ // merged to the transaction that it is being merged with.
+ nsCOMArray<nsISupports>& lastData = lastUndo->GetData();
+ nsCOMArray<nsISupports>& previousData = previousUndo->GetData();
+ NS_ENSURE_TRUE(previousData.AppendObjects(lastData), NS_ERROR_UNEXPECTED);
+ lastData.Clear();
+ return rv;
+}
+
+nsresult
+nsTransactionManager::RemoveTopUndo()
+{
+ if (mUndoStack.IsEmpty()) {
+ return NS_OK;
+ }
+
+ RefPtr<nsTransactionItem> lastUndo = mUndoStack.Pop();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTransactionManager::AddListener(nsITransactionListener *aListener)
+{
+ NS_ENSURE_TRUE(aListener, NS_ERROR_NULL_POINTER);
+ return mListeners.AppendObject(aListener) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsTransactionManager::RemoveListener(nsITransactionListener *aListener)
+{
+ NS_ENSURE_TRUE(aListener, NS_ERROR_NULL_POINTER);
+ return mListeners.RemoveObject(aListener) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsTransactionManager::ClearUndoStack()
+{
+ mUndoStack.Clear();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTransactionManager::ClearRedoStack()
+{
+ mRedoStack.Clear();
+ return NS_OK;
+}
+
+nsresult
+nsTransactionManager::WillDoNotify(nsITransaction *aTransaction, bool *aInterrupt)
+{
+ for (int32_t i = 0, lcount = mListeners.Count(); i < lcount; i++) {
+ nsITransactionListener* listener = mListeners[i];
+ NS_ENSURE_TRUE(listener, NS_ERROR_FAILURE);
+
+ nsresult rv = listener->WillDo(this, aTransaction, aInterrupt);
+ if (NS_FAILED(rv) || *aInterrupt) {
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+nsTransactionManager::DidDoNotify(nsITransaction *aTransaction, nsresult aDoResult)
+{
+ for (int32_t i = 0, lcount = mListeners.Count(); i < lcount; i++) {
+ nsITransactionListener* listener = mListeners[i];
+ NS_ENSURE_TRUE(listener, NS_ERROR_FAILURE);
+
+ nsresult rv = listener->DidDo(this, aTransaction, aDoResult);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+nsTransactionManager::WillUndoNotify(nsITransaction *aTransaction, bool *aInterrupt)
+{
+ for (int32_t i = 0, lcount = mListeners.Count(); i < lcount; i++) {
+ nsITransactionListener* listener = mListeners[i];
+ NS_ENSURE_TRUE(listener, NS_ERROR_FAILURE);
+
+ nsresult rv = listener->WillUndo(this, aTransaction, aInterrupt);
+ if (NS_FAILED(rv) || *aInterrupt) {
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+nsTransactionManager::DidUndoNotify(nsITransaction *aTransaction, nsresult aUndoResult)
+{
+ for (int32_t i = 0, lcount = mListeners.Count(); i < lcount; i++) {
+ nsITransactionListener* listener = mListeners[i];
+ NS_ENSURE_TRUE(listener, NS_ERROR_FAILURE);
+
+ nsresult rv = listener->DidUndo(this, aTransaction, aUndoResult);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+nsTransactionManager::WillRedoNotify(nsITransaction *aTransaction, bool *aInterrupt)
+{
+ for (int32_t i = 0, lcount = mListeners.Count(); i < lcount; i++) {
+ nsITransactionListener* listener = mListeners[i];
+ NS_ENSURE_TRUE(listener, NS_ERROR_FAILURE);
+
+ nsresult rv = listener->WillRedo(this, aTransaction, aInterrupt);
+ if (NS_FAILED(rv) || *aInterrupt) {
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+nsTransactionManager::DidRedoNotify(nsITransaction *aTransaction, nsresult aRedoResult)
+{
+ for (int32_t i = 0, lcount = mListeners.Count(); i < lcount; i++) {
+ nsITransactionListener* listener = mListeners[i];
+ NS_ENSURE_TRUE(listener, NS_ERROR_FAILURE);
+
+ nsresult rv = listener->DidRedo(this, aTransaction, aRedoResult);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+nsTransactionManager::WillBeginBatchNotify(bool *aInterrupt)
+{
+ for (int32_t i = 0, lcount = mListeners.Count(); i < lcount; i++) {
+ nsITransactionListener* listener = mListeners[i];
+ NS_ENSURE_TRUE(listener, NS_ERROR_FAILURE);
+
+ nsresult rv = listener->WillBeginBatch(this, aInterrupt);
+ if (NS_FAILED(rv) || *aInterrupt) {
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+nsTransactionManager::DidBeginBatchNotify(nsresult aResult)
+{
+ for (int32_t i = 0, lcount = mListeners.Count(); i < lcount; i++) {
+ nsITransactionListener* listener = mListeners[i];
+ NS_ENSURE_TRUE(listener, NS_ERROR_FAILURE);
+
+ nsresult rv = listener->DidBeginBatch(this, aResult);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+nsTransactionManager::WillEndBatchNotify(bool *aInterrupt)
+{
+ for (int32_t i = 0, lcount = mListeners.Count(); i < lcount; i++) {
+ nsITransactionListener* listener = mListeners[i];
+ NS_ENSURE_TRUE(listener, NS_ERROR_FAILURE);
+
+ nsresult rv = listener->WillEndBatch(this, aInterrupt);
+ if (NS_FAILED(rv) || *aInterrupt) {
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+nsTransactionManager::DidEndBatchNotify(nsresult aResult)
+{
+ for (int32_t i = 0, lcount = mListeners.Count(); i < lcount; i++) {
+ nsITransactionListener* listener = mListeners[i];
+ NS_ENSURE_TRUE(listener, NS_ERROR_FAILURE);
+
+ nsresult rv = listener->DidEndBatch(this, aResult);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+nsTransactionManager::WillMergeNotify(nsITransaction *aTop, nsITransaction *aTransaction, bool *aInterrupt)
+{
+ for (int32_t i = 0, lcount = mListeners.Count(); i < lcount; i++) {
+ nsITransactionListener* listener = mListeners[i];
+ NS_ENSURE_TRUE(listener, NS_ERROR_FAILURE);
+
+ nsresult rv = listener->WillMerge(this, aTop, aTransaction, aInterrupt);
+ if (NS_FAILED(rv) || *aInterrupt) {
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+nsTransactionManager::DidMergeNotify(nsITransaction *aTop,
+ nsITransaction *aTransaction,
+ bool aDidMerge,
+ nsresult aMergeResult)
+{
+ for (int32_t i = 0, lcount = mListeners.Count(); i < lcount; i++) {
+ nsITransactionListener* listener = mListeners[i];
+ NS_ENSURE_TRUE(listener, NS_ERROR_FAILURE);
+
+ nsresult rv =
+ listener->DidMerge(this, aTop, aTransaction, aDidMerge, aMergeResult);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+nsTransactionManager::BeginTransaction(nsITransaction *aTransaction,
+ nsISupports *aData)
+{
+ // XXX: POSSIBLE OPTIMIZATION
+ // We could use a factory that pre-allocates/recycles transaction items.
+ RefPtr<nsTransactionItem> tx = new nsTransactionItem(aTransaction);
+ if (!tx) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ if (aData) {
+ nsCOMArray<nsISupports>& data = tx->GetData();
+ data.AppendObject(aData);
+ }
+
+ mDoStack.Push(tx);
+
+ nsresult rv = tx->DoTransaction();
+ if (NS_FAILED(rv)) {
+ tx = mDoStack.Pop();
+ return rv;
+ }
+ return NS_OK;
+}
+
+nsresult
+nsTransactionManager::EndTransaction(bool aAllowEmpty)
+{
+ RefPtr<nsTransactionItem> tx = mDoStack.Pop();
+ if (!tx) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsITransaction> tint = tx->GetTransaction();
+ if (!tint && !aAllowEmpty) {
+ // If we get here, the transaction must be a dummy batch transaction
+ // created by BeginBatch(). If it contains no children, get rid of it!
+ int32_t nc = 0;
+ tx->GetNumberOfChildren(&nc);
+ if (!nc) {
+ return NS_OK;
+ }
+ }
+
+ // Check if the transaction is transient. If it is, there's nothing
+ // more to do, just return.
+ bool isTransient = false;
+ nsresult rv = NS_OK;
+ if (tint) {
+ rv = tint->GetIsTransient(&isTransient);
+ }
+ if (NS_FAILED(rv) || isTransient || !mMaxTransactionCount) {
+ // XXX: Should we be clearing the redo stack if the transaction
+ // is transient and there is nothing on the do stack?
+ return rv;
+ }
+
+ // Check if there is a transaction on the do stack. If there is,
+ // the current transaction is a "sub" transaction, and should
+ // be added to the transaction at the top of the do stack.
+ RefPtr<nsTransactionItem> top = mDoStack.Peek();
+ if (top) {
+ return top->AddChild(tx); // XXX: What do we do if this fails?
+ }
+
+ // The transaction succeeded, so clear the redo stack.
+ rv = ClearRedoStack();
+ if (NS_FAILED(rv)) {
+ // XXX: What do we do if this fails?
+ }
+
+ // Check if we can coalesce this transaction with the one at the top
+ // of the undo stack.
+ top = mUndoStack.Peek();
+ if (tint && top) {
+ bool didMerge = false;
+ nsCOMPtr<nsITransaction> topTransaction = top->GetTransaction();
+ if (topTransaction) {
+ bool doInterrupt = false;
+ rv = WillMergeNotify(topTransaction, tint, &doInterrupt);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!doInterrupt) {
+ rv = topTransaction->Merge(tint, &didMerge);
+ nsresult rv2 = DidMergeNotify(topTransaction, tint, didMerge, rv);
+ if (NS_SUCCEEDED(rv)) {
+ rv = rv2;
+ }
+ if (NS_FAILED(rv)) {
+ // XXX: What do we do if this fails?
+ }
+ if (didMerge) {
+ return rv;
+ }
+ }
+ }
+ }
+
+ // Check to see if we've hit the max level of undo. If so,
+ // pop the bottom transaction off the undo stack and release it!
+ int32_t sz = mUndoStack.GetSize();
+ if (mMaxTransactionCount > 0 && sz >= mMaxTransactionCount) {
+ RefPtr<nsTransactionItem> overflow = mUndoStack.PopBottom();
+ }
+
+ // Push the transaction on the undo stack:
+ mUndoStack.Push(tx.forget());
+ return NS_OK;
+}
diff --git a/editor/txmgr/nsTransactionManager.h b/editor/txmgr/nsTransactionManager.h
new file mode 100644
index 000000000..17d21819f
--- /dev/null
+++ b/editor/txmgr/nsTransactionManager.h
@@ -0,0 +1,83 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsTransactionManager_h__
+#define nsTransactionManager_h__
+
+#include "nsCOMArray.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsTransactionStack.h"
+#include "nsISupportsImpl.h"
+#include "nsITransactionManager.h"
+#include "nsTransactionStack.h"
+#include "nsWeakReference.h"
+#include "nscore.h"
+
+class nsITransaction;
+class nsITransactionListener;
+
+/** implementation of a transaction manager object.
+ *
+ */
+class nsTransactionManager final : public nsITransactionManager
+ , public nsSupportsWeakReference
+{
+private:
+
+ int32_t mMaxTransactionCount;
+ nsTransactionStack mDoStack;
+ nsTransactionStack mUndoStack;
+ nsTransactionStack mRedoStack;
+ nsCOMArray<nsITransactionListener> mListeners;
+
+ /** The default destructor.
+ */
+ virtual ~nsTransactionManager();
+
+public:
+
+ /** The default constructor.
+ */
+ explicit nsTransactionManager(int32_t aMaxTransactionCount=-1);
+
+ /* Macro for AddRef(), Release(), and QueryInterface() */
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsTransactionManager,
+ nsITransactionManager)
+
+ /* nsITransactionManager method implementations. */
+ NS_DECL_NSITRANSACTIONMANAGER
+
+ already_AddRefed<nsITransaction> PeekUndoStack();
+ already_AddRefed<nsITransaction> PeekRedoStack();
+
+ virtual nsresult WillDoNotify(nsITransaction *aTransaction, bool *aInterrupt);
+ virtual nsresult DidDoNotify(nsITransaction *aTransaction, nsresult aExecuteResult);
+ virtual nsresult WillUndoNotify(nsITransaction *aTransaction, bool *aInterrupt);
+ virtual nsresult DidUndoNotify(nsITransaction *aTransaction, nsresult aUndoResult);
+ virtual nsresult WillRedoNotify(nsITransaction *aTransaction, bool *aInterrupt);
+ virtual nsresult DidRedoNotify(nsITransaction *aTransaction, nsresult aRedoResult);
+ virtual nsresult WillBeginBatchNotify(bool *aInterrupt);
+ virtual nsresult DidBeginBatchNotify(nsresult aResult);
+ virtual nsresult WillEndBatchNotify(bool *aInterrupt);
+ virtual nsresult DidEndBatchNotify(nsresult aResult);
+ virtual nsresult WillMergeNotify(nsITransaction *aTop,
+ nsITransaction *aTransaction,
+ bool *aInterrupt);
+ virtual nsresult DidMergeNotify(nsITransaction *aTop,
+ nsITransaction *aTransaction,
+ bool aDidMerge,
+ nsresult aMergeResult);
+
+private:
+
+ /* nsTransactionManager specific private methods. */
+ virtual nsresult BeginTransaction(nsITransaction *aTransaction,
+ nsISupports *aData);
+ virtual nsresult EndTransaction(bool aAllowEmpty);
+};
+
+#endif // nsTransactionManager_h__
diff --git a/editor/txmgr/nsTransactionManagerCID.h b/editor/txmgr/nsTransactionManagerCID.h
new file mode 100644
index 000000000..f65e98579
--- /dev/null
+++ b/editor/txmgr/nsTransactionManagerCID.h
@@ -0,0 +1,14 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsTransactionManagerCID_h__
+#define nsTransactionManagerCID_h__
+
+#define NS_TRANSACTIONMANAGER_CID \
+{ /* 9C8F9601-801A-11d2-98BA-00805F297D89 */ \
+0x9c8f9601, 0x801a, 0x11d2, \
+{ 0x98, 0xba, 0x0, 0x80, 0x5f, 0x29, 0x7d, 0x89 } }
+
+#endif /* nsTransactionManagerCID_h__ */
diff --git a/editor/txmgr/nsTransactionManagerFactory.cpp b/editor/txmgr/nsTransactionManagerFactory.cpp
new file mode 100644
index 000000000..a4cbb5950
--- /dev/null
+++ b/editor/txmgr/nsTransactionManagerFactory.cpp
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <stddef.h>
+
+#include "mozilla/Module.h"
+#include "mozilla/ModuleUtils.h"
+#include "nsID.h"
+#include "nsITransactionManager.h"
+#include "nsTransactionManager.h"
+#include "nsTransactionManagerCID.h"
+
+////////////////////////////////////////////////////////////////////////
+// Define the contructor function for the objects
+//
+// NOTE: This creates an instance of objects by using the default constructor
+//
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsTransactionManager)
+NS_DEFINE_NAMED_CID(NS_TRANSACTIONMANAGER_CID);
+
+static const mozilla::Module::CIDEntry kTxMgrCIDs[] = {
+ { &kNS_TRANSACTIONMANAGER_CID, false, nullptr, nsTransactionManagerConstructor },
+ { nullptr }
+};
+
+static const mozilla::Module::ContractIDEntry kTxMgrContracts[] = {
+ { NS_TRANSACTIONMANAGER_CONTRACTID, &kNS_TRANSACTIONMANAGER_CID },
+ { nullptr }
+};
+
+static const mozilla::Module kTxMgrModule = {
+ mozilla::Module::kVersion,
+ kTxMgrCIDs,
+ kTxMgrContracts
+};
+NSMODULE_DEFN(nsTransactionManagerModule) = &kTxMgrModule;
diff --git a/editor/txmgr/nsTransactionStack.cpp b/editor/txmgr/nsTransactionStack.cpp
new file mode 100644
index 000000000..febfa3110
--- /dev/null
+++ b/editor/txmgr/nsTransactionStack.cpp
@@ -0,0 +1,108 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupportsUtils.h"
+#include "nsTransactionItem.h"
+#include "nsTransactionStack.h"
+#include "nscore.h"
+
+class nsTransactionStackDeallocator : public nsDequeFunctor {
+ virtual void* operator() (void* aObject) {
+ RefPtr<nsTransactionItem> releaseMe = dont_AddRef(static_cast<nsTransactionItem*>(aObject));
+ return nullptr;
+ }
+};
+
+nsTransactionStack::nsTransactionStack(Type aType)
+ : nsDeque(new nsTransactionStackDeallocator())
+ , mType(aType)
+{
+}
+
+nsTransactionStack::~nsTransactionStack()
+{
+ Clear();
+}
+
+void
+nsTransactionStack::Push(nsTransactionItem* aTransactionItem)
+{
+ if (!aTransactionItem) {
+ return;
+ }
+
+ RefPtr<nsTransactionItem> item(aTransactionItem);
+ Push(item.forget());
+}
+
+void
+nsTransactionStack::Push(already_AddRefed<nsTransactionItem> aTransactionItem)
+{
+ RefPtr<nsTransactionItem> item(aTransactionItem);
+ if (!item) {
+ return;
+ }
+
+ nsDeque::Push(item.forget().take());
+}
+
+already_AddRefed<nsTransactionItem>
+nsTransactionStack::Pop()
+{
+ RefPtr<nsTransactionItem> item =
+ dont_AddRef(static_cast<nsTransactionItem*>(nsDeque::Pop()));
+ return item.forget();
+}
+
+already_AddRefed<nsTransactionItem>
+nsTransactionStack::PopBottom()
+{
+ RefPtr<nsTransactionItem> item =
+ dont_AddRef(static_cast<nsTransactionItem*>(nsDeque::PopFront()));
+ return item.forget();
+}
+
+already_AddRefed<nsTransactionItem>
+nsTransactionStack::Peek()
+{
+ RefPtr<nsTransactionItem> item =
+ static_cast<nsTransactionItem*>(nsDeque::Peek());
+ return item.forget();
+}
+
+already_AddRefed<nsTransactionItem>
+nsTransactionStack::GetItem(int32_t aIndex)
+{
+ if (aIndex < 0 || aIndex >= static_cast<int32_t>(nsDeque::GetSize())) {
+ return nullptr;
+ }
+ RefPtr<nsTransactionItem> item =
+ static_cast<nsTransactionItem*>(nsDeque::ObjectAt(aIndex));
+ return item.forget();
+}
+
+void
+nsTransactionStack::Clear()
+{
+ while (GetSize() != 0) {
+ RefPtr<nsTransactionItem> item =
+ mType == FOR_UNDO ? Pop() : PopBottom();
+ }
+}
+
+void
+nsTransactionStack::DoTraverse(nsCycleCollectionTraversalCallback &cb)
+{
+ int32_t size = GetSize();
+ for (int32_t i = 0; i < size; ++i) {
+ nsTransactionItem* item = static_cast<nsTransactionItem*>(nsDeque::ObjectAt(i));
+ if (item) {
+ NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "transaction stack mDeque[i]");
+ cb.NoteNativeChild(item, NS_CYCLE_COLLECTION_PARTICIPANT(nsTransactionItem));
+ }
+ }
+}
diff --git a/editor/txmgr/nsTransactionStack.h b/editor/txmgr/nsTransactionStack.h
new file mode 100644
index 000000000..f6f6c3a2e
--- /dev/null
+++ b/editor/txmgr/nsTransactionStack.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsTransactionStack_h__
+#define nsTransactionStack_h__
+
+#include "nsDeque.h"
+
+class nsCycleCollectionTraversalCallback;
+class nsTransactionItem;
+
+class nsTransactionStack : private nsDeque
+{
+public:
+ enum Type { FOR_UNDO, FOR_REDO };
+
+ explicit nsTransactionStack(Type aType);
+ ~nsTransactionStack();
+
+ void Push(nsTransactionItem *aTransactionItem);
+ void Push(already_AddRefed<nsTransactionItem> aTransactionItem);
+ already_AddRefed<nsTransactionItem> Pop();
+ already_AddRefed<nsTransactionItem> PopBottom();
+ already_AddRefed<nsTransactionItem> Peek();
+ already_AddRefed<nsTransactionItem> GetItem(int32_t aIndex);
+ void Clear();
+ int32_t GetSize() const { return static_cast<int32_t>(nsDeque::GetSize()); }
+ bool IsEmpty() const { return GetSize() == 0; }
+
+ void DoUnlink() { Clear(); }
+ void DoTraverse(nsCycleCollectionTraversalCallback &cb);
+
+private:
+ const Type mType;
+};
+
+#endif // nsTransactionStack_h__
diff --git a/editor/txmgr/tests/TestTXMgr.cpp b/editor/txmgr/tests/TestTXMgr.cpp
new file mode 100644
index 000000000..4e77aae7f
--- /dev/null
+++ b/editor/txmgr/tests/TestTXMgr.cpp
@@ -0,0 +1,4478 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TestHarness.h"
+
+#include "nsITransactionManager.h"
+#include "nsComponentManagerUtils.h"
+#include "mozilla/Likely.h"
+
+static int32_t sConstructorCount = 0;
+static int32_t sDestructorCount = 0;
+static int32_t *sDestructorOrderArr = 0;
+static int32_t sDoCount = 0;
+static int32_t *sDoOrderArr = 0;
+static int32_t sUndoCount = 0;
+static int32_t *sUndoOrderArr = 0;
+static int32_t sRedoCount = 0;
+static int32_t *sRedoOrderArr = 0;
+
+// #define ENABLE_DEBUG_PRINTFS 1
+
+int32_t sSimpleTestDestructorOrderArr[] = {
+ 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
+ 16, 17, 18, 19, 20, 21, 1, 22, 23, 24, 25, 26, 27, 28,
+ 29, 30, 31, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52,
+ 53, 54, 55, 56, 57, 58, 59, 60, 61, 41, 40, 62, 39, 38,
+ 37, 36, 35, 34, 33, 32, 68, 63, 64, 65, 66, 67, 69, 71,
+ 70, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84,
+ 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98,
+ 99, 100, 101, 111, 110, 109, 108, 107, 106, 105, 104, 103, 102, 131,
+ 130, 129, 128, 127, 126, 125, 124, 123, 122, 121, 120, 119, 118, 117,
+ 116, 115, 114, 113, 112 };
+
+int32_t sSimpleTestDoOrderArr[] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28,
+ 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
+ 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56,
+ 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70,
+ 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84,
+ 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98,
+ 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112,
+ 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126,
+ 127, 128, 129, 130, 131 };
+
+int32_t sSimpleTestUndoOrderArr[] = {
+ 41, 40, 39, 38, 62, 39, 38, 37, 69, 71, 70, 111, 110, 109,
+ 108, 107, 106, 105, 104, 103, 102, 131, 130, 129, 128, 127, 126, 125,
+ 124, 123, 122 };
+
+static int32_t sSimpleTestRedoOrderArr[] = {
+ 38, 39, 70 };
+
+int32_t sAggregateTestDestructorOrderArr[] = {
+ 14, 13, 12, 11, 10, 9, 8, 21, 20, 19, 18, 17, 16, 15,
+ 28, 27, 26, 25, 24, 23, 22, 35, 34, 33, 32, 31, 30, 29,
+ 42, 41, 40, 39, 38, 37, 36, 49, 48, 47, 46, 45, 44, 43,
+ 56, 55, 54, 53, 52, 51, 50, 63, 62, 61, 60, 59, 58, 57,
+ 70, 69, 68, 67, 66, 65, 64, 77, 76, 75, 74, 73, 72, 71,
+ 84, 83, 82, 81, 80, 79, 78, 91, 90, 89, 88, 87, 86, 85,
+ 98, 97, 96, 95, 94, 93, 92, 105, 104, 103, 102, 101, 100, 99,
+ 112, 111, 110, 109, 108, 107, 106, 119, 118, 117, 116, 115, 114, 113,
+ 126, 125, 124, 123, 122, 121, 120, 133, 132, 131, 130, 129, 128, 127,
+ 140, 139, 138, 137, 136, 135, 134, 147, 146, 145, 144, 143, 142, 141,
+ 7, 6, 5, 4, 3, 2, 1, 154, 153, 152, 151, 150, 149, 148,
+ 161, 160, 159, 158, 157, 156, 155, 168, 167, 166, 165, 164, 163, 162,
+ 175, 174, 173, 172, 171, 170, 169, 182, 181, 180, 179, 178, 177, 176,
+ 189, 188, 187, 186, 185, 184, 183, 196, 195, 194, 193, 192, 191, 190,
+ 203, 202, 201, 200, 199, 198, 197, 210, 209, 208, 207, 206, 205, 204,
+ 217, 216, 215, 214, 213, 212, 211, 294, 293, 292, 291, 290, 289, 288,
+ 301, 300, 299, 298, 297, 296, 295, 308, 307, 306, 305, 304, 303, 302,
+ 315, 314, 313, 312, 311, 310, 309, 322, 321, 320, 319, 318, 317, 316,
+ 329, 328, 327, 326, 325, 324, 323, 336, 335, 334, 333, 332, 331, 330,
+ 343, 342, 341, 340, 339, 338, 337, 350, 349, 348, 347, 346, 345, 344,
+ 357, 356, 355, 354, 353, 352, 351, 364, 363, 362, 361, 360, 359, 358,
+ 371, 370, 369, 368, 367, 366, 365, 378, 377, 376, 375, 374, 373, 372,
+ 385, 384, 383, 382, 381, 380, 379, 392, 391, 390, 389, 388, 387, 386,
+ 399, 398, 397, 396, 395, 394, 393, 406, 405, 404, 403, 402, 401, 400,
+ 413, 412, 411, 410, 409, 408, 407, 420, 419, 418, 417, 416, 415, 414,
+ 427, 426, 425, 424, 423, 422, 421, 287, 286, 285, 284, 283, 282, 281,
+ 280, 279, 278, 277, 276, 275, 274, 434, 433, 432, 431, 430, 429, 428,
+ 273, 272, 271, 270, 269, 268, 267, 266, 265, 264, 263, 262, 261, 260,
+ 259, 258, 257, 256, 255, 254, 253, 252, 251, 250, 249, 248, 247, 246,
+ 245, 244, 243, 242, 241, 240, 239, 238, 237, 236, 235, 234, 233, 232,
+ 231, 230, 229, 228, 227, 226, 225, 224, 223, 222, 221, 220, 219, 218,
+ 472, 471, 470, 441, 440, 439, 438, 437, 436, 435, 448, 447, 446, 445,
+ 444, 443, 442, 455, 454, 453, 452, 451, 450, 449, 462, 461, 460, 459,
+ 458, 457, 456, 469, 468, 467, 466, 465, 464, 463, 479, 478, 477, 476,
+ 475, 474, 473, 493, 492, 491, 490, 489, 488, 487, 486, 485, 484, 483,
+ 482, 481, 480, 496, 497, 495, 499, 500, 498, 494, 503, 504, 502, 506,
+ 507, 505, 501, 510, 511, 509, 513, 514, 512, 508, 517, 518, 516, 520,
+ 521, 519, 515, 524, 525, 523, 527, 528, 526, 522, 531, 532, 530, 534,
+ 535, 533, 529, 538, 539, 537, 541, 542, 540, 536, 545, 546, 544, 548,
+ 549, 547, 543, 552, 553, 551, 555, 556, 554, 550, 559, 560, 558, 562,
+ 563, 561, 557, 566, 567, 565, 569, 570, 568, 564, 573, 574, 572, 576,
+ 577, 575, 571, 580, 581, 579, 583, 584, 582, 578, 587, 588, 586, 590,
+ 591, 589, 585, 594, 595, 593, 597, 598, 596, 592, 601, 602, 600, 604,
+ 605, 603, 599, 608, 609, 607, 611, 612, 610, 606, 615, 616, 614, 618,
+ 619, 617, 613, 622, 623, 621, 625, 626, 624, 620, 629, 630, 628, 632,
+ 633, 631, 627, 640, 639, 638, 637, 636, 635, 634, 647, 646, 645, 644,
+ 643, 642, 641, 654, 653, 652, 651, 650, 649, 648, 661, 660, 659, 658,
+ 657, 656, 655, 668, 667, 666, 665, 664, 663, 662, 675, 674, 673, 672,
+ 671, 670, 669, 682, 681, 680, 679, 678, 677, 676, 689, 688, 687, 686,
+ 685, 684, 683, 696, 695, 694, 693, 692, 691, 690, 703, 702, 701, 700,
+ 699, 698, 697, 773, 772, 771, 770, 769, 768, 767, 766, 765, 764, 763,
+ 762, 761, 760, 759, 758, 757, 756, 755, 754, 753, 752, 751, 750, 749,
+ 748, 747, 746, 745, 744, 743, 742, 741, 740, 739, 738, 737, 736, 735,
+ 734, 733, 732, 731, 730, 729, 728, 727, 726, 725, 724, 723, 722, 721,
+ 720, 719, 718, 717, 716, 715, 714, 713, 712, 711, 710, 709, 708, 707,
+ 706, 705, 704, 913, 912, 911, 910, 909, 908, 907, 906, 905, 904, 903,
+ 902, 901, 900, 899, 898, 897, 896, 895, 894, 893, 892, 891, 890, 889,
+ 888, 887, 886, 885, 884, 883, 882, 881, 880, 879, 878, 877, 876, 875,
+ 874, 873, 872, 871, 870, 869, 868, 867, 866, 865, 864, 863, 862, 861,
+ 860, 859, 858, 857, 856, 855, 854, 853, 852, 851, 850, 849, 848, 847,
+ 846, 845, 844, 843, 842, 841, 840, 839, 838, 837, 836, 835, 834, 833,
+ 832, 831, 830, 829, 828, 827, 826, 825, 824, 823, 822, 821, 820, 819,
+ 818, 817, 816, 815, 814, 813, 812, 811, 810, 809, 808, 807, 806, 805,
+ 804, 803, 802, 801, 800, 799, 798, 797, 796, 795, 794, 793, 792, 791,
+ 790, 789, 788, 787, 786, 785, 784, 783, 782, 781, 780, 779, 778, 777,
+ 776, 775, 774 };
+
+int32_t sAggregateTestDoOrderArr[] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28,
+ 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
+ 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56,
+ 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70,
+ 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84,
+ 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98,
+ 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112,
+ 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126,
+ 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140,
+ 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154,
+ 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168,
+ 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182,
+ 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196,
+ 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210,
+ 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224,
+ 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238,
+ 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252,
+ 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266,
+ 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280,
+ 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294,
+ 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308,
+ 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322,
+ 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336,
+ 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350,
+ 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364,
+ 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378,
+ 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392,
+ 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406,
+ 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420,
+ 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434,
+ 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448,
+ 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462,
+ 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476,
+ 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490,
+ 491, 492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 503, 504,
+ 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518,
+ 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532,
+ 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546,
+ 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560,
+ 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574,
+ 575, 576, 577, 578, 579, 580, 581, 582, 583, 584, 585, 586, 587, 588,
+ 589, 590, 591, 592, 593, 594, 595, 596, 597, 598, 599, 600, 601, 602,
+ 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615, 616,
+ 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630,
+ 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, 644,
+ 645, 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658,
+ 659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672,
+ 673, 674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686,
+ 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700,
+ 701, 702, 703, 704, 705, 706, 707, 708, 709, 710, 711, 712, 713, 714,
+ 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 727, 728,
+ 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739, 740, 741, 742,
+ 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755, 756,
+ 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770,
+ 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784,
+ 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, 797, 798,
+ 799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 811, 812,
+ 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 824, 825, 826,
+ 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 839, 840,
+ 841, 842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854,
+ 855, 856, 857, 858, 859, 860, 861, 862, 863, 864, 865, 866, 867, 868,
+ 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879, 880, 881, 882,
+ 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896,
+ 897, 898, 899, 900, 901, 902, 903, 904, 905, 906, 907, 908, 909, 910,
+ 911, 912, 913 };
+
+int32_t sAggregateTestUndoOrderArr[] = {
+ 287, 286, 285, 284, 283, 282, 281, 280, 279, 278, 277, 276, 275, 274,
+ 273, 272, 271, 270, 269, 268, 267, 266, 265, 264, 263, 262, 261, 260,
+ 434, 433, 432, 431, 430, 429, 428, 273, 272, 271, 270, 269, 268, 267,
+ 266, 265, 264, 263, 262, 261, 260, 259, 258, 257, 256, 255, 254, 253,
+ 479, 478, 477, 476, 475, 493, 492, 491, 490, 489, 488, 487, 486, 485,
+ 484, 483, 482, 481, 480, 485, 484, 483, 482, 481, 480, 773, 772, 771,
+ 770, 769, 768, 767, 766, 765, 764, 763, 762, 761, 760, 759, 758, 757,
+ 756, 755, 754, 753, 752, 751, 750, 749, 748, 747, 746, 745, 744, 743,
+ 742, 741, 740, 739, 738, 737, 736, 735, 734, 733, 732, 731, 730, 729,
+ 728, 727, 726, 725, 724, 723, 722, 721, 720, 719, 718, 717, 716, 715,
+ 714, 713, 712, 711, 710, 709, 708, 707, 706, 705, 704, 913, 912, 911,
+ 910, 909, 908, 907, 906, 905, 904, 903, 902, 901, 900, 899, 898, 897,
+ 896, 895, 894, 893, 892, 891, 890, 889, 888, 887, 886, 885, 884, 883,
+ 882, 881, 880, 879, 878, 877, 876, 875, 874, 873, 872, 871, 870, 869,
+ 868, 867, 866, 865, 864, 863, 862, 861, 860, 859, 858, 857, 856, 855,
+ 854, 853, 852, 851, 850, 849, 848, 847, 846, 845, 844 };
+
+int32_t sAggregateTestRedoOrderArr[] = {
+ 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273,
+ 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486 };
+
+int32_t sSimpleBatchTestDestructorOrderArr[] = {
+ 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34,
+ 35, 36, 37, 38, 39, 40, 43, 42, 41, 64, 63, 62, 61, 60,
+ 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46,
+ 45, 44, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9,
+ 8, 7, 6, 5, 4, 3, 2, 1, 65, 67, 66, 68, 69, 70,
+ 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84,
+ 85, 86, 87, 107, 106, 105, 104, 103, 102, 101, 100, 99, 98, 97,
+ 96, 95, 94, 93, 92, 91, 90, 89, 88 };
+
+int32_t sSimpleBatchTestDoOrderArr[] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28,
+ 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
+ 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56,
+ 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70,
+ 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84,
+ 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98,
+ 99, 100, 101, 102, 103, 104, 105, 106, 107 };
+
+int32_t sSimpleBatchTestUndoOrderArr[] = {
+ 43, 42, 41, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10,
+ 9, 8, 7, 6, 5, 4, 3, 2, 1, 43, 42, 41, 63, 62,
+ 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48,
+ 47, 46, 45, 44, 65, 67, 66, 107, 106, 105, 104, 103, 102, 101,
+ 100, 99, 98 };
+
+int32_t sSimpleBatchTestRedoOrderArr[] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+ 15, 16, 17, 18, 19, 20, 41, 42, 43, 66 };
+
+int32_t sAggregateBatchTestDestructorOrderArr[] = {
+ 147, 146, 145, 144, 143, 142, 141, 154, 153, 152, 151, 150, 149, 148,
+ 161, 160, 159, 158, 157, 156, 155, 168, 167, 166, 165, 164, 163, 162,
+ 175, 174, 173, 172, 171, 170, 169, 182, 181, 180, 179, 178, 177, 176,
+ 189, 188, 187, 186, 185, 184, 183, 196, 195, 194, 193, 192, 191, 190,
+ 203, 202, 201, 200, 199, 198, 197, 210, 209, 208, 207, 206, 205, 204,
+ 217, 216, 215, 214, 213, 212, 211, 224, 223, 222, 221, 220, 219, 218,
+ 231, 230, 229, 228, 227, 226, 225, 238, 237, 236, 235, 234, 233, 232,
+ 245, 244, 243, 242, 241, 240, 239, 252, 251, 250, 249, 248, 247, 246,
+ 259, 258, 257, 256, 255, 254, 253, 266, 265, 264, 263, 262, 261, 260,
+ 273, 272, 271, 270, 269, 268, 267, 280, 279, 278, 277, 276, 275, 274,
+ 301, 300, 299, 298, 297, 296, 295, 294, 293, 292, 291, 290, 289, 288,
+ 287, 286, 285, 284, 283, 282, 281, 444, 443, 442, 441, 440, 439, 438,
+ 437, 436, 435, 434, 433, 432, 431, 430, 429, 428, 427, 426, 425, 424,
+ 423, 422, 421, 420, 419, 418, 417, 416, 415, 414, 413, 412, 411, 410,
+ 409, 408, 407, 406, 405, 404, 403, 402, 401, 400, 399, 398, 397, 396,
+ 395, 394, 393, 392, 391, 390, 389, 388, 387, 386, 385, 384, 383, 382,
+ 381, 380, 379, 378, 377, 376, 375, 374, 373, 372, 371, 370, 369, 368,
+ 367, 366, 365, 364, 363, 362, 361, 360, 359, 358, 357, 356, 355, 354,
+ 353, 352, 351, 350, 349, 348, 347, 346, 345, 344, 343, 342, 341, 340,
+ 339, 338, 337, 336, 335, 334, 333, 332, 331, 330, 329, 328, 327, 326,
+ 325, 324, 323, 322, 321, 320, 319, 318, 317, 316, 315, 314, 313, 312,
+ 311, 310, 309, 308, 307, 306, 305, 304, 303, 302, 140, 139, 138, 137,
+ 136, 135, 134, 133, 132, 131, 130, 129, 128, 127, 126, 125, 124, 123,
+ 122, 121, 120, 119, 118, 117, 116, 115, 114, 113, 112, 111, 110, 109,
+ 108, 107, 106, 105, 104, 103, 102, 101, 100, 99, 98, 97, 96, 95,
+ 94, 93, 92, 91, 90, 89, 88, 87, 86, 85, 84, 83, 82, 81,
+ 80, 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67,
+ 66, 65, 64, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53,
+ 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39,
+ 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25,
+ 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11,
+ 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 451, 450, 449, 448,
+ 447, 446, 445, 465, 464, 463, 462, 461, 460, 459, 458, 457, 456, 455,
+ 454, 453, 452, 468, 469, 467, 471, 472, 470, 466, 475, 476, 474, 478,
+ 479, 477, 473, 482, 483, 481, 485, 486, 484, 480, 489, 490, 488, 492,
+ 493, 491, 487, 496, 497, 495, 499, 500, 498, 494, 503, 504, 502, 506,
+ 507, 505, 501, 510, 511, 509, 513, 514, 512, 508, 517, 518, 516, 520,
+ 521, 519, 515, 524, 525, 523, 527, 528, 526, 522, 531, 532, 530, 534,
+ 535, 533, 529, 538, 539, 537, 541, 542, 540, 536, 545, 546, 544, 548,
+ 549, 547, 543, 552, 553, 551, 555, 556, 554, 550, 559, 560, 558, 562,
+ 563, 561, 557, 566, 567, 565, 569, 570, 568, 564, 573, 574, 572, 576,
+ 577, 575, 571, 580, 581, 579, 583, 584, 582, 578, 587, 588, 586, 590,
+ 591, 589, 585, 594, 595, 593, 597, 598, 596, 592, 601, 602, 600, 604,
+ 605, 603, 599, 745, 744, 743, 742, 741, 740, 739, 738, 737, 736, 735,
+ 734, 733, 732, 731, 730, 729, 728, 727, 726, 725, 724, 723, 722, 721,
+ 720, 719, 718, 717, 716, 715, 714, 713, 712, 711, 710, 709, 708, 707,
+ 706, 705, 704, 703, 702, 701, 700, 699, 698, 697, 696, 695, 694, 693,
+ 692, 691, 690, 689, 688, 687, 686, 685, 684, 683, 682, 681, 680, 679,
+ 678, 677, 676, 675, 674, 673, 672, 671, 670, 669, 668, 667, 666, 665,
+ 664, 663, 662, 661, 660, 659, 658, 657, 656, 655, 654, 653, 652, 651,
+ 650, 649, 648, 647, 646, 645, 644, 643, 642, 641, 640, 639, 638, 637,
+ 636, 635, 634, 633, 632, 631, 630, 629, 628, 627, 626, 625, 624, 623,
+ 622, 621, 620, 619, 618, 617, 616, 615, 614, 613, 612, 611, 610, 609,
+ 608, 607, 606 };
+
+int32_t sAggregateBatchTestDoOrderArr[] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28,
+ 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
+ 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56,
+ 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70,
+ 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84,
+ 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98,
+ 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112,
+ 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126,
+ 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140,
+ 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154,
+ 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168,
+ 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182,
+ 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196,
+ 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210,
+ 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224,
+ 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238,
+ 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252,
+ 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266,
+ 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280,
+ 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294,
+ 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308,
+ 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322,
+ 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336,
+ 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350,
+ 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364,
+ 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378,
+ 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392,
+ 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406,
+ 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420,
+ 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434,
+ 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448,
+ 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462,
+ 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476,
+ 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490,
+ 491, 492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 503, 504,
+ 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518,
+ 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532,
+ 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546,
+ 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560,
+ 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574,
+ 575, 576, 577, 578, 579, 580, 581, 582, 583, 584, 585, 586, 587, 588,
+ 589, 590, 591, 592, 593, 594, 595, 596, 597, 598, 599, 600, 601, 602,
+ 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615, 616,
+ 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630,
+ 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, 644,
+ 645, 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658,
+ 659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672,
+ 673, 674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686,
+ 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700,
+ 701, 702, 703, 704, 705, 706, 707, 708, 709, 710, 711, 712, 713, 714,
+ 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 727, 728,
+ 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739, 740, 741, 742,
+ 743, 744, 745 };
+
+int32_t sAggregateBatchTestUndoOrderArr[] = {
+ 301, 300, 299, 298, 297, 296, 295, 294, 293, 292, 291, 290, 289, 288,
+ 287, 286, 285, 284, 283, 282, 281, 140, 139, 138, 137, 136, 135, 134,
+ 133, 132, 131, 130, 129, 128, 127, 126, 125, 124, 123, 122, 121, 120,
+ 119, 118, 117, 116, 115, 114, 113, 112, 111, 110, 109, 108, 107, 106,
+ 105, 104, 103, 102, 101, 100, 99, 98, 97, 96, 95, 94, 93, 92,
+ 91, 90, 89, 88, 87, 86, 85, 84, 83, 82, 81, 80, 79, 78,
+ 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64,
+ 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50,
+ 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36,
+ 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22,
+ 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8,
+ 7, 6, 5, 4, 3, 2, 1, 301, 300, 299, 298, 297, 296, 295,
+ 294, 293, 292, 291, 290, 289, 288, 287, 286, 285, 284, 283, 282, 281,
+ 441, 440, 439, 438, 437, 436, 435, 434, 433, 432, 431, 430, 429, 428,
+ 427, 426, 425, 424, 423, 422, 421, 420, 419, 418, 417, 416, 415, 414,
+ 413, 412, 411, 410, 409, 408, 407, 406, 405, 404, 403, 402, 401, 400,
+ 399, 398, 397, 396, 395, 394, 393, 392, 391, 390, 389, 388, 387, 386,
+ 385, 384, 383, 382, 381, 380, 379, 378, 377, 376, 375, 374, 373, 372,
+ 371, 370, 369, 368, 367, 366, 365, 364, 363, 362, 361, 360, 359, 358,
+ 357, 356, 355, 354, 353, 352, 351, 350, 349, 348, 347, 346, 345, 344,
+ 343, 342, 341, 340, 339, 338, 337, 336, 335, 334, 333, 332, 331, 330,
+ 329, 328, 327, 326, 325, 324, 323, 322, 321, 320, 319, 318, 317, 316,
+ 315, 314, 313, 312, 311, 310, 309, 308, 307, 306, 305, 304, 303, 302,
+ 451, 450, 449, 448, 447, 465, 464, 463, 462, 461, 460, 459, 458, 457,
+ 456, 455, 454, 453, 452, 457, 456, 455, 454, 453, 452, 745, 744, 743,
+ 742, 741, 740, 739, 738, 737, 736, 735, 734, 733, 732, 731, 730, 729,
+ 728, 727, 726, 725, 724, 723, 722, 721, 720, 719, 718, 717, 716, 715,
+ 714, 713, 712, 711, 710, 709, 708, 707, 706, 705, 704, 703, 702, 701,
+ 700, 699, 698, 697, 696, 695, 694, 693, 692, 691, 690, 689, 688, 687,
+ 686, 685, 684, 683, 682, 681, 680, 679, 678, 677, 676 };
+
+int32_t sAggregateBatchTestRedoOrderArr[] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28,
+ 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
+ 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56,
+ 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70,
+ 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84,
+ 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98,
+ 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112,
+ 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126,
+ 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140,
+ 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294,
+ 295, 296, 297, 298, 299, 300, 301, 448, 449, 450, 451, 452, 453, 454,
+ 455, 456, 457, 458 };
+
+#define TEST_TXMGR_IF_RELEASE(tx) if (tx) tx->Release(); // Release but don't clear pointer!
+
+class TestTransaction : public nsITransaction
+{
+protected:
+ virtual ~TestTransaction() {}
+
+public:
+
+ TestTransaction() {}
+
+ NS_DECL_ISUPPORTS
+};
+
+NS_IMPL_ISUPPORTS(TestTransaction, nsITransaction)
+
+class SimpleTransaction : public TestTransaction
+{
+protected:
+
+#define NONE_FLAG 0
+#define THROWS_DO_ERROR_FLAG 1
+#define THROWS_UNDO_ERROR_FLAG 2
+#define THROWS_REDO_ERROR_FLAG 4
+#define MERGE_FLAG 8
+#define TRANSIENT_FLAG 16
+#define BATCH_FLAG 32
+#define ALL_ERROR_FLAGS (THROWS_DO_ERROR_FLAG|THROWS_UNDO_ERROR_FLAG|THROWS_REDO_ERROR_FLAG)
+
+ int32_t mVal;
+ int32_t mFlags;
+
+public:
+
+ explicit SimpleTransaction(int32_t aFlags=NONE_FLAG)
+ : mVal(++sConstructorCount), mFlags(aFlags)
+ {}
+
+ virtual ~SimpleTransaction()
+ {
+ //
+ // Make sure transactions are being destroyed in the order we expect!
+ // Notice that we don't check to see if we go past the end of the array.
+ // This is done on purpose since we want to crash if the order array is out
+ // of date.
+ //
+ /* Disabled because the current cycle collector doesn't delete
+ cycle collectable objects synchronously, nor doesn't guarantee any order.
+ if (sDestructorOrderArr && mVal != sDestructorOrderArr[sDestructorCount]) {
+ fail("~SimpleTransaction expected %d got %d.\n",
+ mVal, sDestructorOrderArr[sDestructorCount]);
+ exit(-1);
+ }
+ */
+
+ ++sDestructorCount;
+
+#ifdef ENABLE_DEBUG_PRINTFS
+ printf("\n~SimpleTransaction: %d - 0x%.8x\n", mVal, (int32_t)this);
+#endif // ENABLE_DEBUG_PRINTFS
+
+ mVal = -1;
+ }
+
+ NS_IMETHOD DoTransaction()
+ {
+ //
+ // Make sure DoTransaction() is called in the order we expect!
+ // Notice that we don't check to see if we go past the end of the array.
+ // This is done on purpose since we want to crash if the order array is out
+ // of date.
+ //
+ if (sDoOrderArr && mVal != sDoOrderArr[sDoCount]) {
+ fail("DoTransaction expected %d got %d.\n",
+ mVal, sDoOrderArr[sDoCount]);
+ exit(-1);
+ }
+
+ ++sDoCount;
+
+#ifdef ENABLE_DEBUG_PRINTFS
+ printf("\nSimpleTransaction.DoTransaction: %d - 0x%.8x\n", mVal, (int32_t)this);
+#endif // ENABLE_DEBUG_PRINTFS
+
+ return (mFlags & THROWS_DO_ERROR_FLAG) ? NS_ERROR_FAILURE : NS_OK;
+ }
+
+ NS_IMETHOD UndoTransaction()
+ {
+ //
+ // Make sure UndoTransaction() is called in the order we expect!
+ // Notice that we don't check to see if we go past the end of the array.
+ // This is done on purpose since we want to crash if the order array is out
+ // of date.
+ //
+ if (sUndoOrderArr && mVal != sUndoOrderArr[sUndoCount]) {
+ fail("UndoTransaction expected %d got %d.\n",
+ mVal, sUndoOrderArr[sUndoCount]);
+ exit(-1);
+ }
+
+ ++sUndoCount;
+
+#ifdef ENABLE_DEBUG_PRINTFS
+ printf("\nSimpleTransaction.Undo: %d - 0x%.8x\n", mVal, (int32_t)this);
+#endif // ENABLE_DEBUG_PRINTFS
+
+ return (mFlags & THROWS_UNDO_ERROR_FLAG) ? NS_ERROR_FAILURE : NS_OK;
+ }
+
+ NS_IMETHOD RedoTransaction()
+ {
+ //
+ // Make sure RedoTransaction() is called in the order we expect!
+ // Notice that we don't check to see if we go past the end of the array.
+ // This is done on purpose since we want to crash if the order array is out
+ // of date.
+ //
+ if (sRedoOrderArr && mVal != sRedoOrderArr[sRedoCount]) {
+ fail("RedoTransaction expected %d got %d.\n",
+ mVal, sRedoOrderArr[sRedoCount]);
+ exit(-1);
+ }
+
+ ++sRedoCount;
+
+#ifdef ENABLE_DEBUG_PRINTFS
+ printf("\nSimpleTransaction.Redo: %d - 0x%.8x\n", mVal, (int32_t)this);
+#endif // ENABLE_DEBUG_PRINTFS
+
+ return (mFlags & THROWS_REDO_ERROR_FLAG) ? NS_ERROR_FAILURE : NS_OK;
+ }
+
+ NS_IMETHOD GetIsTransient(bool *aIsTransient)
+ {
+ if (aIsTransient) {
+ *aIsTransient = (mFlags & TRANSIENT_FLAG) ? true : false;
+ }
+ return NS_OK;
+ }
+
+ NS_IMETHOD Merge(nsITransaction *aTransaction, bool *aDidMerge)
+ {
+ if (aDidMerge) {
+ *aDidMerge = (mFlags & MERGE_FLAG) ? true : false;
+ }
+ return NS_OK;
+ }
+};
+
+class AggregateTransaction : public SimpleTransaction
+{
+private:
+
+ AggregateTransaction(nsITransactionManager *aTXMgr, int32_t aLevel,
+ int32_t aNumber, int32_t aMaxLevel,
+ int32_t aNumChildrenPerNode,
+ int32_t aFlags)
+ {
+ mLevel = aLevel;
+ mNumber = aNumber;
+ mTXMgr = aTXMgr;
+ mFlags = aFlags & (~ALL_ERROR_FLAGS);
+ mErrorFlags = aFlags & ALL_ERROR_FLAGS;
+ mTXMgr = aTXMgr;
+ mMaxLevel = aMaxLevel;
+ mNumChildrenPerNode = aNumChildrenPerNode;
+ }
+
+ nsITransactionManager *mTXMgr;
+
+ int32_t mLevel;
+ int32_t mNumber;
+ int32_t mErrorFlags;
+
+ int32_t mMaxLevel;
+ int32_t mNumChildrenPerNode;
+
+public:
+
+ AggregateTransaction(nsITransactionManager *aTXMgr,
+ int32_t aMaxLevel, int32_t aNumChildrenPerNode,
+ int32_t aFlags=NONE_FLAG)
+ {
+ mLevel = 1;
+ mNumber = 1;
+ mFlags = aFlags & (~ALL_ERROR_FLAGS);
+ mErrorFlags = aFlags & ALL_ERROR_FLAGS;
+ mTXMgr = aTXMgr;
+ mMaxLevel = aMaxLevel;
+ mNumChildrenPerNode = aNumChildrenPerNode;
+ }
+
+ virtual ~AggregateTransaction()
+ {
+ // printf("~AggregateTransaction(0x%.8x) - %3d (%3d)\n", this, mLevel, mVal);
+ }
+
+ NS_IMETHOD DoTransaction()
+ {
+ if (mLevel >= mMaxLevel) {
+ // Only leaf nodes can throw errors!
+ mFlags |= mErrorFlags;
+ }
+
+ nsresult rv = SimpleTransaction::DoTransaction();
+ if (NS_FAILED(rv)) {
+ // fail("QueryInterface() failed for transaction level %d. (%d)\n",
+ // mLevel, rv);
+ return rv;
+ }
+
+ if (mLevel >= mMaxLevel) {
+ return NS_OK;
+ }
+
+ if (mFlags & BATCH_FLAG) {
+ rv = mTXMgr->BeginBatch(nullptr);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ int32_t cLevel = mLevel + 1;
+
+ for (int i = 1; i <= mNumChildrenPerNode; i++) {
+ int32_t flags = mErrorFlags & THROWS_DO_ERROR_FLAG;
+
+ if ((mErrorFlags & THROWS_REDO_ERROR_FLAG) && i == mNumChildrenPerNode) {
+ // Make the rightmost leaf transaction throw the error!
+ flags = THROWS_REDO_ERROR_FLAG;
+ mErrorFlags = mErrorFlags & (~THROWS_REDO_ERROR_FLAG);
+ } else if ((mErrorFlags & THROWS_UNDO_ERROR_FLAG) && i == 1) {
+ // Make the leftmost leaf transaction throw the error!
+ flags = THROWS_UNDO_ERROR_FLAG;
+ mErrorFlags = mErrorFlags & (~THROWS_UNDO_ERROR_FLAG);
+ }
+
+ flags |= mFlags & BATCH_FLAG;
+
+ AggregateTransaction *tximpl =
+ new AggregateTransaction(mTXMgr, cLevel, i, mMaxLevel,
+ mNumChildrenPerNode, flags);
+
+ if (!tximpl) {
+ fail("Failed to allocate AggregateTransaction %d, level %d. (%d)\n",
+ i, mLevel, rv);
+
+ if (mFlags & BATCH_FLAG) {
+ mTXMgr->EndBatch(false);
+ }
+
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ nsITransaction *tx = 0;
+ rv = tximpl->QueryInterface(NS_GET_IID(nsITransaction), (void **)&tx);
+ if (NS_FAILED(rv)) {
+ fail("QueryInterface() failed for transaction %d, level %d. (%d)\n",
+ i, mLevel, rv);
+
+ if (mFlags & BATCH_FLAG) {
+ mTXMgr->EndBatch(false);
+ }
+ return rv;
+ }
+
+ rv = mTXMgr->DoTransaction(tx);
+ if (NS_FAILED(rv)) {
+ // fail("Failed to execute transaction %d, level %d. (%d)\n",
+ // i, mLevel, rv);
+ tx->Release();
+ if (mFlags & BATCH_FLAG) {
+ mTXMgr->EndBatch(false);
+ }
+ return rv;
+ }
+ tx->Release();
+ }
+
+ if (mFlags & BATCH_FLAG) {
+ mTXMgr->EndBatch(false);
+ }
+ return rv;
+ }
+};
+
+class TestTransactionFactory
+{
+public:
+ virtual TestTransaction *create(nsITransactionManager *txmgr, int32_t flags) = 0;
+};
+
+class SimpleTransactionFactory : public TestTransactionFactory
+{
+public:
+
+ TestTransaction *create(nsITransactionManager *txmgr, int32_t flags)
+ {
+ return (TestTransaction *)new SimpleTransaction(flags);
+ }
+};
+
+class AggregateTransactionFactory : public TestTransactionFactory
+{
+private:
+
+ int32_t mMaxLevel;
+ int32_t mNumChildrenPerNode;
+ int32_t mFixedFlags;
+
+public:
+
+ AggregateTransactionFactory(int32_t aMaxLevel, int32_t aNumChildrenPerNode,
+ int32_t aFixedFlags=NONE_FLAG)
+ : mMaxLevel(aMaxLevel), mNumChildrenPerNode(aNumChildrenPerNode),
+ mFixedFlags(aFixedFlags)
+ {
+ }
+
+ virtual TestTransaction *create(nsITransactionManager *txmgr, int32_t flags)
+ {
+ return (TestTransaction *)new AggregateTransaction(txmgr, mMaxLevel,
+ mNumChildrenPerNode,
+ flags | mFixedFlags);
+ }
+};
+
+void
+reset_globals()
+{
+ sConstructorCount = 0;
+
+ sDestructorCount = 0;
+ sDestructorOrderArr = 0;
+
+ sDoCount = 0;
+ sDoOrderArr = 0;
+
+ sUndoCount = 0;
+ sUndoOrderArr = 0;
+
+ sRedoCount = 0;
+ sRedoOrderArr = 0;
+}
+
+/**
+ * Test behaviors in non-batch mode.
+ **/
+nsresult
+quick_test(TestTransactionFactory *factory)
+{
+ /*******************************************************************
+ *
+ * Create a transaction manager implementation:
+ *
+ *******************************************************************/
+
+ nsresult rv;
+ nsCOMPtr<nsITransactionManager> mgr =
+ do_CreateInstance(NS_TRANSACTIONMANAGER_CONTRACTID, &rv);
+ if (NS_FAILED(rv) || !mgr) {
+ fail("Failed to create Transaction Manager instance.\n");
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ passed("Create transaction manager instance");
+
+ /*******************************************************************
+ *
+ * Call DoTransaction() with a null transaction:
+ *
+ *******************************************************************/
+
+ rv = mgr->DoTransaction(0);
+ if (rv != NS_ERROR_NULL_POINTER) {
+ fail("DoTransaction() returned unexpected error. (%d)\n", rv);
+ return rv;
+ }
+
+ passed("Call DoTransaction() with null transaction");
+
+ /*******************************************************************
+ *
+ * Call UndoTransaction() with an empty undo stack:
+ *
+ *******************************************************************/
+
+ rv = mgr->UndoTransaction();
+ if (NS_FAILED(rv)) {
+ fail("Undo on empty undo stack failed. (%d)\n", rv);
+ return rv;
+ }
+
+ passed("Call UndoTransaction() with empty undo stack");
+
+ /*******************************************************************
+ *
+ * Call RedoTransaction() with an empty redo stack:
+ *
+ *******************************************************************/
+
+ rv = mgr->RedoTransaction();
+ if (NS_FAILED(rv)) {
+ fail("Redo on empty redo stack failed. (%d)\n", rv);
+ return rv;
+ }
+
+ passed("Call RedoTransaction() with empty redo stack");
+
+ /*******************************************************************
+ *
+ * Call SetMaxTransactionCount(-1) with empty undo and redo stacks:
+ *
+ *******************************************************************/
+
+ rv = mgr->SetMaxTransactionCount(-1);
+ if (NS_FAILED(rv)) {
+ fail("SetMaxTransactionCount(-1) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ passed("Call SetMaxTransactionCount(-1) with empty undo and redo stacks");
+
+ /*******************************************************************
+ *
+ * Call SetMaxTransactionCount(0) with empty undo and redo stacks:
+ *
+ *******************************************************************/
+
+ rv = mgr->SetMaxTransactionCount(0);
+ if (NS_FAILED(rv)) {
+ fail("SetMaxTransactionCount(0) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ passed("Call SetMaxTransactionCount(0) with empty undo and redo stacks");
+
+ /*******************************************************************
+ *
+ * Call SetMaxTransactionCount(10) with empty undo and redo stacks:
+ *
+ *******************************************************************/
+
+ rv = mgr->SetMaxTransactionCount(10);
+ if (NS_FAILED(rv)) {
+ fail("SetMaxTransactionCount(10) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ passed("Call SetMaxTransactionCount(10) with empty undo and redo stacks");
+
+ /*******************************************************************
+ *
+ * Call Clear() with empty undo and redo stacks:
+ *
+ *******************************************************************/
+
+ rv = mgr->Clear();
+ if (NS_FAILED(rv)) {
+ fail("Clear on empty undo and redo stack failed. (%d)\n", rv);
+ return rv;
+ }
+
+ passed("Call Clear() with empty undo and redo stack");
+
+ int32_t numitems;
+
+ /*******************************************************************
+ *
+ * Call GetNumberOfUndoItems() with an empty undo stack:
+ *
+ *******************************************************************/
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on empty undo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfUndoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Call GetNumberOfUndoItems() with empty undo stack");
+
+ /*******************************************************************
+ *
+ * Call GetNumberOfRedoItems() with an empty redo stack:
+ *
+ *******************************************************************/
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on empty redo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfRedoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Call GetNumberOfRedoItems() with empty redo stack");
+
+ nsITransaction *tx;
+
+ /*******************************************************************
+ *
+ * Call PeekUndoStack() with an empty undo stack:
+ *
+ *******************************************************************/
+
+ tx = 0;
+ rv = mgr->PeekUndoStack(&tx);
+
+ TEST_TXMGR_IF_RELEASE(tx); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("PeekUndoStack() on empty undo stack failed. (%d)\n", rv);
+ return rv;
+ }
+
+ if (tx) {
+ fail("PeekUndoStack() on empty undo stack failed. (%d)\n", rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Call PeekUndoStack() with empty undo stack");
+
+ /*******************************************************************
+ *
+ * Call PeekRedoStack() with an empty undo stack:
+ *
+ *******************************************************************/
+
+ tx = 0;
+ rv = mgr->PeekRedoStack(&tx);
+
+ TEST_TXMGR_IF_RELEASE(tx); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("PeekRedoStack() on empty redo stack failed. (%d)\n", rv);
+ return rv;
+ }
+
+ if (tx) {
+ fail("PeekRedoStack() on empty redo stack failed. (%d)\n", rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Call PeekRedoStack() with empty undo stack");
+
+ /*******************************************************************
+ *
+ * Call AddListener() with a null listener pointer:
+ *
+ *******************************************************************/
+
+ rv = mgr->AddListener(0);
+ if (rv != NS_ERROR_NULL_POINTER) {
+ fail("AddListener() returned unexpected error. (%d)\n", rv);
+ return rv;
+ }
+
+ passed("Call AddListener() with null listener");
+
+ /*******************************************************************
+ *
+ * Call RemoveListener() with a null listener pointer:
+ *
+ *******************************************************************/
+
+ rv = mgr->RemoveListener(0);
+ if (rv != NS_ERROR_NULL_POINTER) {
+ fail("RemoveListener() returned unexpected error. (%d)\n", rv);
+ return rv;
+ }
+
+ passed("Call RemoveListener() with null listener");
+
+ int32_t i;
+ TestTransaction *tximpl;
+ nsITransaction *u1, *u2;
+ nsITransaction *r1, *r2;
+
+ /*******************************************************************
+ *
+ * Test coalescing by executing a transaction that can merge any
+ * command into itself. Then execute 20 transaction. Afterwards,
+ * we should still have the first transaction sitting on the undo
+ * stack. Then clear the undo and redo stacks.
+ *
+ *******************************************************************/
+
+ rv = mgr->SetMaxTransactionCount(10);
+ if (NS_FAILED(rv)) {
+ fail("SetMaxTransactionCount(10) failed. (%d)\n", rv);
+ return rv;
+ }
+
+
+ tximpl = factory->create(mgr, MERGE_FLAG);
+
+ if (!tximpl) {
+ fail("Failed to allocate initial transaction.\n");
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ tx = 0;
+
+ rv = tximpl->QueryInterface(NS_GET_IID(nsITransaction), (void **)&tx);
+ if (NS_FAILED(rv)) {
+ fail("QueryInterface() failed for initial transaction. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ rv = mgr->DoTransaction(tx);
+ if (NS_FAILED(rv)) {
+ fail("Failed to execute initial transaction. (%d)\n", rv);
+ return rv;
+ }
+
+ tx->Release();
+
+ u1 = u2 = r1 = r2 = 0;
+
+ rv = mgr->PeekUndoStack(&u1);
+
+ TEST_TXMGR_IF_RELEASE(u1); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Initial PeekUndoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ if (u1 != tx) {
+ fail("Top of undo stack is different!. (%d)\n", rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->PeekRedoStack(&r1);
+
+ TEST_TXMGR_IF_RELEASE(r1); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Initial PeekRedoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ for (i = 1; i <= 20; i++) {
+ tximpl = factory->create(mgr, NONE_FLAG);
+
+ if (!tximpl) {
+ fail("Failed to allocate transaction %d.\n", i);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ tx = 0;
+ rv = tximpl->QueryInterface(NS_GET_IID(nsITransaction), (void **)&tx);
+ if (NS_FAILED(rv)) {
+ fail("QueryInterface() failed for transaction %d. (%d)\n",
+ i, rv);
+ return rv;
+ }
+
+ rv = mgr->DoTransaction(tx);
+ if (NS_FAILED(rv)) {
+ fail("Failed to execute transaction %d. (%d)\n", i, rv);
+ return rv;
+ }
+
+ tx->Release();
+ }
+
+ rv = mgr->PeekUndoStack(&u2);
+
+ TEST_TXMGR_IF_RELEASE(u2); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Second PeekUndoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ if (u1 != u2) {
+ fail("Top of undo stack changed. (%d)\n", rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->PeekRedoStack(&r2);
+
+ TEST_TXMGR_IF_RELEASE(r2); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Second PeekRedoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ if (r1 != r2) {
+ fail("Top of redo stack changed. (%d)\n", rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 1 item failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 1) {
+ fail("GetNumberOfUndoItems() expected 1 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on empty redo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfRedoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->Clear();
+ if (NS_FAILED(rv)) {
+ fail("Clear() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ passed("Test coalescing of transactions");
+
+ /*******************************************************************
+ *
+ * Execute 20 transactions. Afterwards, we should have 10
+ * transactions on the undo stack:
+ *
+ *******************************************************************/
+
+ for (i = 1; i <= 20; i++) {
+ tximpl = factory->create(mgr, NONE_FLAG);
+
+ if (!tximpl) {
+ fail("Failed to allocate transaction %d.\n", i);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ tx = 0;
+ rv = tximpl->QueryInterface(NS_GET_IID(nsITransaction), (void **)&tx);
+ if (NS_FAILED(rv)) {
+ fail("QueryInterface() failed for transaction %d. (%d)\n",
+ i, rv);
+ return rv;
+ }
+
+ rv = mgr->DoTransaction(tx);
+ if (NS_FAILED(rv)) {
+ fail("Failed to execute transaction %d. (%d)\n", i, rv);
+ return rv;
+ }
+
+ tx->Release();
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 10 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 10) {
+ fail("GetNumberOfUndoItems() expected 10 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on empty redo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfRedoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Execute 20 transactions");
+
+ /*******************************************************************
+ *
+ * Execute 20 transient transactions. Afterwards, we should still
+ * have the same 10 transactions on the undo stack:
+ *
+ *******************************************************************/
+
+ u1 = u2 = r1 = r2 = 0;
+
+ rv = mgr->PeekUndoStack(&u1);
+
+ TEST_TXMGR_IF_RELEASE(u1); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Initial PeekUndoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->PeekRedoStack(&r1);
+
+ TEST_TXMGR_IF_RELEASE(r1); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Initial PeekRedoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ for (i = 1; i <= 20; i++) {
+ tximpl = factory->create(mgr, TRANSIENT_FLAG);
+
+ if (!tximpl) {
+ fail("Failed to allocate transaction %d.\n", i);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ tx = 0;
+ rv = tximpl->QueryInterface(NS_GET_IID(nsITransaction), (void **)&tx);
+ if (NS_FAILED(rv)) {
+ fail("QueryInterface() failed for transaction %d. (%d)\n",
+ i, rv);
+ return rv;
+ }
+
+ rv = mgr->DoTransaction(tx);
+ if (NS_FAILED(rv)) {
+ fail("Failed to execute transaction %d. (%d)\n", i, rv);
+ return rv;
+ }
+
+ tx->Release();
+ }
+
+ rv = mgr->PeekUndoStack(&u2);
+
+ TEST_TXMGR_IF_RELEASE(u2); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Second PeekUndoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ if (u1 != u2) {
+ fail("Top of undo stack changed. (%d)\n", rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->PeekRedoStack(&r2);
+
+ TEST_TXMGR_IF_RELEASE(r2); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Second PeekRedoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ if (r1 != r2) {
+ fail("Top of redo stack changed. (%d)\n", rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 10 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 10) {
+ fail("GetNumberOfUndoItems() expected 10 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on empty redo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfRedoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Execute 20 transient transactions");
+
+ /*******************************************************************
+ *
+ * Undo 4 transactions. Afterwards, we should have 6 transactions
+ * on the undo stack, and 4 on the redo stack:
+ *
+ *******************************************************************/
+
+ for (i = 1; i <= 4; i++) {
+ rv = mgr->UndoTransaction();
+ if (NS_FAILED(rv)) {
+ fail("Failed to undo transaction %d. (%d)\n", i, rv);
+ return rv;
+ }
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 6 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 6) {
+ fail("GetNumberOfUndoItems() expected 6 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on redo stack with 4 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 4) {
+ fail("GetNumberOfRedoItems() expected 4 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Undo 4 transactions");
+
+ /*******************************************************************
+ *
+ * Redo 2 transactions. Afterwards, we should have 8 transactions
+ * on the undo stack, and 2 on the redo stack:
+ *
+ *******************************************************************/
+
+ for (i = 1; i <= 2; ++i) {
+ rv = mgr->RedoTransaction();
+ if (NS_FAILED(rv)) {
+ fail("Failed to redo transaction %d. (%d)\n", i, rv);
+ return rv;
+ }
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 8 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 8) {
+ fail("GetNumberOfUndoItems() expected 8 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on redo stack with 2 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 2) {
+ fail("GetNumberOfRedoItems() expected 2 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Redo 2 transactions");
+
+ /*******************************************************************
+ *
+ * Execute a new transaction. The redo stack should get pruned!
+ *
+ *******************************************************************/
+
+ tximpl = factory->create(mgr, NONE_FLAG);
+
+ if (!tximpl) {
+ fail("Failed to allocate transaction.\n");
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ tx = 0;
+
+ rv = tximpl->QueryInterface(NS_GET_IID(nsITransaction), (void **)&tx);
+ if (NS_FAILED(rv)) {
+ fail("QueryInterface() failed for transaction. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->DoTransaction(tx);
+ if (NS_FAILED(rv)) {
+ fail("Failed to execute transaction. (%d)\n", rv);
+ return rv;
+ }
+
+ tx->Release();
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 9 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 9) {
+ fail("GetNumberOfUndoItems() expected 9 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on empty redo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfRedoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Check if new transactions prune the redo stack");
+
+ /*******************************************************************
+ *
+ * Undo 4 transactions then clear the undo and redo stacks.
+ *
+ *******************************************************************/
+
+ for (i = 1; i <= 4; ++i) {
+ rv = mgr->UndoTransaction();
+ if (NS_FAILED(rv)) {
+ fail("Failed to undo transaction %d. (%d)\n", i, rv);
+ return rv;
+ }
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 5 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 5) {
+ fail("GetNumberOfUndoItems() expected 5 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on redo stack with 4 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 4) {
+ fail("GetNumberOfRedoItems() expected 4 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->Clear();
+ if (NS_FAILED(rv)) {
+ fail("Clear() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on cleared undo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfUndoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on cleared redo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfRedoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Undo 4 transactions then clear the undo and redo stacks");
+
+ /*******************************************************************
+ *
+ * Execute 5 transactions.
+ *
+ *******************************************************************/
+
+ for (i = 1; i <= 5; i++) {
+ tximpl = factory->create(mgr, NONE_FLAG);
+
+ if (!tximpl) {
+ fail("Failed to allocate transaction %d.\n", i);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ tx = 0;
+ rv = tximpl->QueryInterface(NS_GET_IID(nsITransaction), (void **)&tx);
+ if (NS_FAILED(rv)) {
+ fail("QueryInterface() failed for transaction %d. (%d)\n",
+ i, rv);
+ return rv;
+ }
+
+ rv = mgr->DoTransaction(tx);
+ if (NS_FAILED(rv)) {
+ fail("Failed to execute transaction %d. (%d)\n", i, rv);
+ return rv;
+ }
+
+ tx->Release();
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 5 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 5) {
+ fail("GetNumberOfUndoItems() expected 5 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on empty redo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfRedoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Execute 5 transactions");
+
+ /*******************************************************************
+ *
+ * Test transaction DoTransaction() error:
+ *
+ *******************************************************************/
+
+ tximpl = factory->create(mgr, THROWS_DO_ERROR_FLAG);
+
+ if (!tximpl) {
+ fail("Failed to allocate transaction.\n");
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ tx = 0;
+
+ rv = tximpl->QueryInterface(NS_GET_IID(nsITransaction), (void **)&tx);
+ if (NS_FAILED(rv)) {
+ fail("QueryInterface() failed for transaction. (%d)\n", rv);
+ return rv;
+ }
+
+ u1 = u2 = r1 = r2 = 0;
+
+ rv = mgr->PeekUndoStack(&u1);
+
+ TEST_TXMGR_IF_RELEASE(u1); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Initial PeekUndoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->PeekRedoStack(&r1);
+
+ TEST_TXMGR_IF_RELEASE(r1); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Initial PeekRedoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->DoTransaction(tx);
+ if (rv != NS_ERROR_FAILURE) {
+ fail("DoTransaction() returned unexpected error. (%d)\n", rv);
+ return rv;
+ }
+
+ tx->Release();
+
+ rv = mgr->PeekUndoStack(&u2);
+
+ TEST_TXMGR_IF_RELEASE(u2); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Second PeekUndoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ if (u1 != u2) {
+ fail("Top of undo stack changed. (%d)\n", rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->PeekRedoStack(&r2);
+
+ TEST_TXMGR_IF_RELEASE(r2); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Second PeekRedoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ if (r1 != r2) {
+ fail("Top of redo stack changed. (%d)\n", rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 5 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 5) {
+ fail("GetNumberOfUndoItems() expected 5 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on empty redo stack. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfRedoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Test transaction DoTransaction() error");
+
+ /*******************************************************************
+ *
+ * Test transaction UndoTransaction() error:
+ *
+ *******************************************************************/
+
+ tximpl = factory->create(mgr, THROWS_UNDO_ERROR_FLAG);
+
+ if (!tximpl) {
+ fail("Failed to allocate transaction.\n");
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ tx = 0;
+
+ rv = tximpl->QueryInterface(NS_GET_IID(nsITransaction), (void **)&tx);
+ if (NS_FAILED(rv)) {
+ fail("QueryInterface() failed for transaction. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->DoTransaction(tx);
+ if (NS_FAILED(rv)) {
+ fail("DoTransaction() returned unexpected error. (%d)\n", rv);
+ return rv;
+ }
+
+ tx->Release();
+
+ u1 = u2 = r1 = r2 = 0;
+
+ rv = mgr->PeekUndoStack(&u1);
+
+ TEST_TXMGR_IF_RELEASE(u1); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Initial PeekUndoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->PeekRedoStack(&r1);
+
+ TEST_TXMGR_IF_RELEASE(r1); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Initial PeekRedoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->UndoTransaction();
+ if (rv != NS_ERROR_FAILURE) {
+ fail("UndoTransaction() returned unexpected error. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->PeekUndoStack(&u2);
+
+ TEST_TXMGR_IF_RELEASE(u2); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Second PeekUndoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ if (u1 != u2) {
+ fail("Top of undo stack changed. (%d)\n", rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->PeekRedoStack(&r2);
+
+ TEST_TXMGR_IF_RELEASE(r2); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Second PeekRedoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ if (r1 != r2) {
+ fail("Top of redo stack changed. (%d)\n", rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 6 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 6) {
+ fail("GetNumberOfUndoItems() expected 6 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on empty redo stack. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfRedoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Test transaction UndoTransaction() error");
+
+ /*******************************************************************
+ *
+ * Test transaction RedoTransaction() error:
+ *
+ *******************************************************************/
+
+ tximpl = factory->create(mgr, THROWS_REDO_ERROR_FLAG);
+
+ if (!tximpl) {
+ fail("Failed to allocate transaction.\n");
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ tx = 0;
+
+ rv = tximpl->QueryInterface(NS_GET_IID(nsITransaction), (void **)&tx);
+ if (NS_FAILED(rv)) {
+ fail("QueryInterface() failed for RedoErrorTransaction. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ rv = mgr->DoTransaction(tx);
+
+ if (NS_FAILED(rv)) {
+ fail("DoTransaction() returned unexpected error. (%d)\n", rv);
+ return rv;
+ }
+
+ tx->Release();
+
+ //
+ // Execute a normal transaction to be used in a later test:
+ //
+
+ tximpl = factory->create(mgr, NONE_FLAG);
+
+ if (!tximpl) {
+ fail("Failed to allocate transaction.\n");
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ tx = 0;
+
+ rv = tximpl->QueryInterface(NS_GET_IID(nsITransaction), (void **)&tx);
+ if (NS_FAILED(rv)) {
+ fail("QueryInterface() failed for transaction. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->DoTransaction(tx);
+ if (NS_FAILED(rv)) {
+ fail("DoTransaction() returned unexpected error. (%d)\n", rv);
+ return rv;
+ }
+
+ tx->Release();
+
+ //
+ // Undo the 2 transactions just executed.
+ //
+
+ for (i = 1; i <= 2; ++i) {
+ rv = mgr->UndoTransaction();
+ if (NS_FAILED(rv)) {
+ fail("Failed to undo transaction %d. (%d)\n", i, rv);
+ return rv;
+ }
+ }
+
+ //
+ // The RedoErrorTransaction should now be at the top of the redo stack!
+ //
+
+ u1 = u2 = r1 = r2 = 0;
+
+ rv = mgr->PeekUndoStack(&u1);
+
+ TEST_TXMGR_IF_RELEASE(u1); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Initial PeekUndoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->PeekRedoStack(&r1);
+
+ TEST_TXMGR_IF_RELEASE(r1); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Initial PeekRedoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->RedoTransaction();
+ if (rv != NS_ERROR_FAILURE) {
+ fail("RedoTransaction() returned unexpected error. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->PeekUndoStack(&u2);
+
+ TEST_TXMGR_IF_RELEASE(u2); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Second PeekUndoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ if (u1 != u2) {
+ fail("Top of undo stack changed. (%d)\n", rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->PeekRedoStack(&r2);
+
+ TEST_TXMGR_IF_RELEASE(r2); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Second PeekRedoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ if (r1 != r2) {
+ fail("Top of redo stack changed. (%d)\n", rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 6 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 6) {
+ fail("GetNumberOfUndoItems() expected 6 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on redo stack with 2 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 2) {
+ fail("GetNumberOfRedoItems() expected 2 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Test transaction RedoTransaction() error");
+
+ /*******************************************************************
+ *
+ * Make sure that setting the transaction manager's max transaction
+ * count to zero, clears both the undo and redo stacks, and executes
+ * all new commands without pushing them on the undo stack!
+ *
+ *******************************************************************/
+
+ rv = mgr->SetMaxTransactionCount(0);
+ if (NS_FAILED(rv)) {
+ fail("SetMaxTransactionCount(0) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on empty undo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfUndoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on empty redo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfRedoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ for (i = 1; i <= 20; i++) {
+ tximpl = factory->create(mgr, NONE_FLAG);
+
+ if (!tximpl) {
+ fail("Failed to allocate transaction %d.\n", i);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ tx = 0;
+ rv = tximpl->QueryInterface(NS_GET_IID(nsITransaction), (void **)&tx);
+ if (NS_FAILED(rv)) {
+ fail("QueryInterface() failed for transaction %d. (%d)\n",
+ i, rv);
+ return rv;
+ }
+
+ rv = mgr->DoTransaction(tx);
+ if (NS_FAILED(rv)) {
+ fail("Failed to execute transaction %d. (%d)\n", i, rv);
+ return rv;
+ }
+
+ tx->Release();
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on empty undo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfUndoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on empty redo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfRedoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ passed("Test max transaction count of zero");
+
+ /*******************************************************************
+ *
+ * Make sure that setting the transaction manager's max transaction
+ * count to something greater than the number of transactions on
+ * both the undo and redo stacks causes no pruning of the stacks:
+ *
+ *******************************************************************/
+
+ rv = mgr->SetMaxTransactionCount(-1);
+ if (NS_FAILED(rv)) {
+ fail("SetMaxTransactionCount(-1) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ // Push 20 transactions on the undo stack:
+
+ for (i = 1; i <= 20; i++) {
+ tximpl = factory->create(mgr, NONE_FLAG);
+
+ if (!tximpl) {
+ fail("Failed to allocate transaction %d.\n", i);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ tx = 0;
+ rv = tximpl->QueryInterface(NS_GET_IID(nsITransaction), (void **)&tx);
+ if (NS_FAILED(rv)) {
+ fail("QueryInterface() failed for transaction %d. (%d)\n",
+ i, rv);
+ return rv;
+ }
+
+ rv = mgr->DoTransaction(tx);
+ if (NS_FAILED(rv)) {
+ fail("Failed to execute transaction %d. (%d)\n", i, rv);
+ return rv;
+ }
+
+ tx->Release();
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with %d items failed. (%d)\n",
+ i, rv);
+ return rv;
+ }
+
+ if (numitems != i) {
+ fail("GetNumberOfUndoItems() expected %d got %d. (%d)\n",
+ i, numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on empty redo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfRedoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ for (i = 1; i <= 10; i++) {
+ rv = mgr->UndoTransaction();
+ if (NS_FAILED(rv)) {
+ fail("Failed to undo transaction %d. (%d)\n", i, rv);
+ return rv;
+ }
+ }
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on empty undo stack with 10 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 10) {
+ fail("GetNumberOfUndoItems() expected 10 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on redo stack with 10 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 10) {
+ fail("GetNumberOfRedoItems() expected 10 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ u1 = u2 = r1 = r2 = 0;
+
+ rv = mgr->PeekUndoStack(&u1);
+
+ TEST_TXMGR_IF_RELEASE(u1); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Initial PeekUndoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->PeekRedoStack(&r1);
+
+ TEST_TXMGR_IF_RELEASE(r1); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Initial PeekRedoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->SetMaxTransactionCount(25);
+ if (NS_FAILED(rv)) {
+ fail("SetMaxTransactionCount(25) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->PeekUndoStack(&u2);
+
+ TEST_TXMGR_IF_RELEASE(u2); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Second PeekUndoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ if (u1 != u2) {
+ fail("Top of undo stack changed. (%d)\n", rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->PeekRedoStack(&r2);
+
+ TEST_TXMGR_IF_RELEASE(r2); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Second PeekRedoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ if (r1 != r2) {
+ fail("Top of redo stack changed. (%d)\n", rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 10 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 10) {
+ fail("GetNumberOfUndoItems() expected 10 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on redo stack with 10 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 10) {
+ fail("GetNumberOfRedoItems() expected 10 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Test SetMaxTransactionCount() greater than num stack items");
+
+ /*******************************************************************
+ *
+ * Test undo stack pruning by setting the transaction
+ * manager's max transaction count to a number lower than the
+ * number of transactions on both the undo and redo stacks:
+ *
+ *******************************************************************/
+
+ u1 = u2 = r1 = r2 = 0;
+
+ rv = mgr->PeekUndoStack(&u1);
+
+ TEST_TXMGR_IF_RELEASE(u1); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Initial PeekUndoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->PeekRedoStack(&r1);
+
+ TEST_TXMGR_IF_RELEASE(r1); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Initial PeekRedoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->SetMaxTransactionCount(15);
+ if (NS_FAILED(rv)) {
+ fail("SetMaxTransactionCount(15) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->PeekUndoStack(&u2);
+
+ TEST_TXMGR_IF_RELEASE(u2); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Second PeekUndoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ if (u1 != u2) {
+ fail("Top of undo stack changed. (%d)\n", rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->PeekRedoStack(&r2);
+
+ TEST_TXMGR_IF_RELEASE(r2); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Second PeekRedoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ if (r1 != r2) {
+ fail("Top of redo stack changed. (%d)\n", rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 5 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 5) {
+ fail("GetNumberOfUndoItems() expected 5 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on redo stack with 10 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 10) {
+ fail("GetNumberOfRedoItems() expected 10 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Test SetMaxTransactionCount() pruning undo stack");
+
+ /*******************************************************************
+ *
+ * Test redo stack pruning by setting the transaction
+ * manager's max transaction count to a number lower than the
+ * number of transactions on both the undo and redo stacks:
+ *
+ *******************************************************************/
+
+ u1 = u2 = r1 = r2 = 0;
+
+ rv = mgr->PeekUndoStack(&u1);
+
+ TEST_TXMGR_IF_RELEASE(u1); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Initial PeekUndoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->PeekRedoStack(&r1);
+
+ TEST_TXMGR_IF_RELEASE(r1); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Initial PeekRedoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->SetMaxTransactionCount(5);
+ if (NS_FAILED(rv)) {
+ fail("SetMaxTransactionCount(5) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->PeekUndoStack(&u2);
+
+ TEST_TXMGR_IF_RELEASE(u2); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Second PeekUndoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ if (u2) {
+ fail("Unexpected item at top of undo stack. (%d)\n", rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->PeekRedoStack(&r2);
+
+ TEST_TXMGR_IF_RELEASE(r2); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Second PeekRedoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ if (r1 != r2) {
+ fail("Top of redo stack changed. (%d)\n", rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on empty undo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 0) {
+ fail("GetNumberOfUndoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on redo stack with 5 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 5) {
+ fail("GetNumberOfRedoItems() expected 5 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Test SetMaxTransactionCount() pruning redo stack");
+
+ /*******************************************************************
+ *
+ * Release the transaction manager. Any transactions on the undo
+ * and redo stack should automatically be released:
+ *
+ *******************************************************************/
+
+ rv = mgr->SetMaxTransactionCount(-1);
+ if (NS_FAILED(rv)) {
+ fail("SetMaxTransactionCount(-1) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ // Push 20 transactions on the undo stack:
+
+ for (i = 1; i <= 20; i++) {
+ tximpl = factory->create(mgr, NONE_FLAG);
+
+ if (!tximpl) {
+ fail("Failed to allocate transaction %d.\n", i);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ tx = 0;
+ rv = tximpl->QueryInterface(NS_GET_IID(nsITransaction), (void **)&tx);
+ if (NS_FAILED(rv)) {
+ fail("QueryInterface() failed for transaction %d. (%d)\n",
+ i, rv);
+ return rv;
+ }
+
+ rv = mgr->DoTransaction(tx);
+ if (NS_FAILED(rv)) {
+ fail("Failed to execute transaction %d. (%d)\n", i, rv);
+ return rv;
+ }
+
+ tx->Release();
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with %d items failed. (%d)\n",
+ i, rv);
+ return rv;
+ }
+
+ if (numitems != i) {
+ fail("GetNumberOfUndoItems() expected %d got %d. (%d)\n",
+ i, numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on empty redo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfRedoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ for (i = 1; i <= 10; i++) {
+ rv = mgr->UndoTransaction();
+ if (NS_FAILED(rv)) {
+ fail("Failed to undo transaction %d. (%d)\n", i, rv);
+ return rv;
+ }
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 10 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 10) {
+ fail("GetNumberOfUndoItems() expected 10 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on redo stack with 10 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 10) {
+ fail("GetNumberOfRedoItems() expected 10 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->Clear();
+ if (NS_FAILED(rv)) {
+ fail("Clear() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ passed("Release the transaction manager");
+
+ /*******************************************************************
+ *
+ * Make sure number of transactions created matches number of
+ * transactions destroyed!
+ *
+ *******************************************************************/
+
+ /* Disabled because the current cycle collector doesn't delete
+ cycle collectable objects synchronously.
+ if (sConstructorCount != sDestructorCount) {
+ fail("Transaction constructor count (%d) != destructor count (%d).\n",
+ sConstructorCount, sDestructorCount);
+ return NS_ERROR_FAILURE;
+ }*/
+
+ passed("Number of transactions created and destroyed match");
+ passed("%d transactions processed during quick test", sConstructorCount);
+
+ return NS_OK;
+}
+
+nsresult
+simple_test()
+{
+ /*******************************************************************
+ *
+ * Initialize globals for test.
+ *
+ *******************************************************************/
+ reset_globals();
+ sDestructorOrderArr = sSimpleTestDestructorOrderArr;
+ sDoOrderArr = sSimpleTestDoOrderArr;
+ sUndoOrderArr = sSimpleTestUndoOrderArr;
+ sRedoOrderArr = sSimpleTestRedoOrderArr;
+
+ /*******************************************************************
+ *
+ * Run the quick test.
+ *
+ *******************************************************************/
+
+ printf("\n-----------------------------------------------------\n");
+ printf("- Begin Simple Transaction Test:\n");
+ printf("-----------------------------------------------------\n");
+
+ SimpleTransactionFactory factory;
+
+ return quick_test(&factory);
+}
+
+nsresult
+aggregation_test()
+{
+ /*******************************************************************
+ *
+ * Initialize globals for test.
+ *
+ *******************************************************************/
+
+ reset_globals();
+ sDestructorOrderArr = sAggregateTestDestructorOrderArr;
+ sDoOrderArr = sAggregateTestDoOrderArr;
+ sUndoOrderArr = sAggregateTestUndoOrderArr;
+ sRedoOrderArr = sAggregateTestRedoOrderArr;
+
+ /*******************************************************************
+ *
+ * Run the quick test.
+ *
+ *******************************************************************/
+
+ printf("\n-----------------------------------------------------\n");
+ printf("- Begin Aggregate Transaction Test:\n");
+ printf("-----------------------------------------------------\n");
+
+ AggregateTransactionFactory factory(3, 2);
+
+ return quick_test(&factory);
+}
+
+/**
+ * Test behaviors in batch mode.
+ **/
+nsresult
+quick_batch_test(TestTransactionFactory *factory)
+{
+ /*******************************************************************
+ *
+ * Create a transaction manager implementation:
+ *
+ *******************************************************************/
+
+ nsresult rv;
+ nsCOMPtr<nsITransactionManager> mgr =
+ do_CreateInstance(NS_TRANSACTIONMANAGER_CONTRACTID, &rv);
+ if (NS_FAILED(rv) || !mgr) {
+ fail("Failed to create Transaction Manager instance.\n");
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ passed("Create transaction manager instance");
+
+ int32_t numitems;
+
+ /*******************************************************************
+ *
+ * Make sure an unbalanced call to EndBatch(false) with empty undo stack
+ * throws an error!
+ *
+ *******************************************************************/
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on empty undo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfUndoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->EndBatch(false);
+ if (rv != NS_ERROR_FAILURE) {
+ fail("EndBatch(false) returned unexpected status. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on empty undo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfUndoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Test unbalanced EndBatch(false) with empty undo stack");
+
+ /*******************************************************************
+ *
+ * Make sure that an empty batch is not added to the undo stack
+ * when it is closed.
+ *
+ *******************************************************************/
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on empty undo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfUndoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->BeginBatch(nullptr);
+ if (NS_FAILED(rv)) {
+ fail("BeginBatch(nullptr) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on empty undo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfUndoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->EndBatch(false);
+ if (NS_FAILED(rv)) {
+ fail("EndBatch(false) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on empty undo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfUndoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Test empty batch");
+
+ int32_t i;
+ TestTransaction *tximpl;
+ nsITransaction *tx;
+
+ /*******************************************************************
+ *
+ * Execute 20 transactions. Afterwards, we should have 1
+ * transaction on the undo stack:
+ *
+ *******************************************************************/
+
+ rv = mgr->BeginBatch(nullptr);
+ if (NS_FAILED(rv)) {
+ fail("BeginBatch(nullptr) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ for (i = 1; i <= 20; i++) {
+ tximpl = factory->create(mgr, NONE_FLAG);
+
+ if (!tximpl) {
+ fail("Failed to allocate transaction %d.\n", i);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ tx = 0;
+ rv = tximpl->QueryInterface(NS_GET_IID(nsITransaction), (void **)&tx);
+ if (NS_FAILED(rv)) {
+ fail("QueryInterface() failed for transaction %d. (%d)\n",
+ i, rv);
+ return rv;
+ }
+
+ rv = mgr->DoTransaction(tx);
+ if (NS_FAILED(rv)) {
+ fail("Failed to execute transaction %d. (%d)\n", i, rv);
+ return rv;
+ }
+
+ tx->Release();
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on empty undo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 0) {
+ fail("GetNumberOfUndoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->EndBatch(false);
+ if (NS_FAILED(rv)) {
+ fail("EndBatch(false) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 1 item failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 1) {
+ fail("GetNumberOfUndoItems() expected 1 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Execute 20 batched transactions");
+
+ nsITransaction *u1, *u2;
+ nsITransaction *r1, *r2;
+
+ /*******************************************************************
+ *
+ * Execute 20 transient transactions. Afterwards, we should still
+ * have the same transaction on the undo stack:
+ *
+ *******************************************************************/
+
+ u1 = u2 = r1 = r2 = 0;
+
+ rv = mgr->PeekUndoStack(&u1);
+
+ TEST_TXMGR_IF_RELEASE(u1); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Initial PeekUndoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->PeekRedoStack(&r1);
+
+ TEST_TXMGR_IF_RELEASE(r1); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Initial PeekRedoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->BeginBatch(nullptr);
+ if (NS_FAILED(rv)) {
+ fail("BeginBatch(nullptr) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ for (i = 1; i <= 20; i++) {
+ tximpl = factory->create(mgr, TRANSIENT_FLAG);
+
+ if (!tximpl) {
+ fail("Failed to allocate transaction %d.\n", i);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ tx = 0;
+ rv = tximpl->QueryInterface(NS_GET_IID(nsITransaction), (void **)&tx);
+ if (NS_FAILED(rv)) {
+ fail("QueryInterface() failed for transaction %d. (%d)\n",
+ i, rv);
+ return rv;
+ }
+
+ rv = mgr->DoTransaction(tx);
+ if (NS_FAILED(rv)) {
+ fail("Failed to execute transaction %d. (%d)\n", i, rv);
+ return rv;
+ }
+
+ tx->Release();
+ }
+
+ rv = mgr->EndBatch(false);
+ if (NS_FAILED(rv)) {
+ fail("EndBatch(false) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->PeekUndoStack(&u2);
+
+ TEST_TXMGR_IF_RELEASE(u2); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Second PeekUndoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ if (u1 != u2) {
+ fail("Top of undo stack changed. (%d)\n", rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->PeekRedoStack(&r2);
+
+ TEST_TXMGR_IF_RELEASE(r2); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Second PeekRedoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ if (r1 != r2) {
+ fail("Top of redo stack changed. (%d)\n", rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 1 item failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 1) {
+ fail("GetNumberOfUndoItems() expected 1 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on empty redo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfRedoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Execute 20 batched transient transactions");
+
+ /*******************************************************************
+ *
+ * Test nested batching. Afterwards, we should have 2 transactions
+ * on the undo stack:
+ *
+ *******************************************************************/
+
+ rv = mgr->BeginBatch(nullptr);
+ if (NS_FAILED(rv)) {
+ fail("BeginBatch(nullptr) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ tximpl = factory->create(mgr, NONE_FLAG);
+
+ if (!tximpl) {
+ fail("Failed to allocate transaction.\n");
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ tx = 0;
+ rv = tximpl->QueryInterface(NS_GET_IID(nsITransaction), (void **)&tx);
+ if (NS_FAILED(rv)) {
+ fail("QueryInterface() failed for transaction. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->DoTransaction(tx);
+ if (NS_FAILED(rv)) {
+ fail("Failed to execute transaction. (%d)\n", rv);
+ return rv;
+ }
+
+ tx->Release();
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 1 item failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 1) {
+ fail("GetNumberOfUndoItems() expected 1 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->BeginBatch(nullptr);
+ if (NS_FAILED(rv)) {
+ fail("BeginBatch(nullptr) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ tximpl = factory->create(mgr, NONE_FLAG);
+
+ if (!tximpl) {
+ fail("Failed to allocate transaction.\n");
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ tx = 0;
+ rv = tximpl->QueryInterface(NS_GET_IID(nsITransaction), (void **)&tx);
+ if (NS_FAILED(rv)) {
+ fail("QueryInterface() failed for transaction. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->DoTransaction(tx);
+ if (NS_FAILED(rv)) {
+ fail("Failed to execute transaction. (%d)\n", rv);
+ return rv;
+ }
+
+ tx->Release();
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 1 item failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 1) {
+ fail("GetNumberOfUndoItems() expected 1 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->BeginBatch(nullptr);
+ if (NS_FAILED(rv)) {
+ fail("BeginBatch(nullptr) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ tximpl = factory->create(mgr, NONE_FLAG);
+
+ if (!tximpl) {
+ fail("Failed to allocate transaction.\n");
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ tx = 0;
+ rv = tximpl->QueryInterface(NS_GET_IID(nsITransaction), (void **)&tx);
+ if (NS_FAILED(rv)) {
+ fail("QueryInterface() failed for transaction. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->DoTransaction(tx);
+ if (NS_FAILED(rv)) {
+ fail("Failed to execute transaction. (%d)\n", rv);
+ return rv;
+ }
+
+ tx->Release();
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 1 item failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 1) {
+ fail("GetNumberOfUndoItems() expected 1 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->EndBatch(false);
+ if (NS_FAILED(rv)) {
+ fail("EndBatch(false) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->EndBatch(false);
+ if (NS_FAILED(rv)) {
+ fail("EndBatch(false) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->EndBatch(false);
+ if (NS_FAILED(rv)) {
+ fail("EndBatch(false) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 2 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 2) {
+ fail("GetNumberOfUndoItems() expected 2 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Test nested batched transactions");
+
+ /*******************************************************************
+ *
+ * Undo 2 batch transactions. Afterwards, we should have 0
+ * transactions on the undo stack and 2 on the redo stack.
+ *
+ *******************************************************************/
+
+ for (i = 1; i <= 2; ++i) {
+ rv = mgr->UndoTransaction();
+ if (NS_FAILED(rv)) {
+ fail("Failed to undo transaction %d. (%d)\n", i, rv);
+ return rv;
+ }
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on empty undo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfUndoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on redo stack with 2 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 2) {
+ fail("GetNumberOfRedoItems() expected 2 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Undo 2 batch transactions");
+
+ /*******************************************************************
+ *
+ * Redo 2 batch transactions. Afterwards, we should have 2
+ * transactions on the undo stack and 0 on the redo stack.
+ *
+ *******************************************************************/
+
+ for (i = 1; i <= 2; ++i) {
+ rv = mgr->RedoTransaction();
+ if (NS_FAILED(rv)) {
+ fail("Failed to undo transaction %d. (%d)\n", i, rv);
+ return rv;
+ }
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 2 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 2) {
+ fail("GetNumberOfUndoItems() expected 2 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on empty redo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfRedoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Redo 2 batch transactions");
+
+ /*******************************************************************
+ *
+ * Call undo. Afterwards, we should have 1 transaction
+ * on the undo stack, and 1 on the redo stack:
+ *
+ *******************************************************************/
+
+ rv = mgr->UndoTransaction();
+ if (NS_FAILED(rv)) {
+ fail("Failed to undo transaction. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 1 item failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 1) {
+ fail("GetNumberOfUndoItems() expected 1 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on stack with 1 item failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 1) {
+ fail("GetNumberOfRedoItems() expected 1 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Undo a batched transaction that was redone");
+
+ /*******************************************************************
+ *
+ * Make sure an unbalanced call to EndBatch(false) throws an error and
+ * doesn't affect the undo and redo stacks!
+ *
+ *******************************************************************/
+
+ rv = mgr->EndBatch(false);
+ if (rv != NS_ERROR_FAILURE) {
+ fail("EndBatch(false) returned unexpected status. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 1 item failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 1) {
+ fail("GetNumberOfUndoItems() expected 1 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on stack with 1 item failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 1) {
+ fail("GetNumberOfRedoItems() expected 1 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Test effect of unbalanced EndBatch(false) on undo and redo stacks");
+
+ /*******************************************************************
+ *
+ * Make sure that an empty batch is not added to the undo stack
+ * when it is closed, and that it does not affect the undo and redo
+ * stacks.
+ *
+ *******************************************************************/
+
+ rv = mgr->BeginBatch(nullptr);
+ if (NS_FAILED(rv)) {
+ fail("BeginBatch(nullptr) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 1 item failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 1) {
+ fail("GetNumberOfUndoItems() expected 1 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on stack with 1 item failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 1) {
+ fail("GetNumberOfRedoItems() expected 1 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->EndBatch(false);
+ if (NS_FAILED(rv)) {
+ fail("EndBatch(false) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 1 item failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 1) {
+ fail("GetNumberOfUndoItems() expected 1 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on stack with 1 item failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 1) {
+ fail("GetNumberOfRedoItems() expected 1 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Test effect of empty batch on undo and redo stacks");
+
+ /*******************************************************************
+ *
+ * Execute a new transaction. The redo stack should get pruned!
+ *
+ *******************************************************************/
+
+ rv = mgr->BeginBatch(nullptr);
+ if (NS_FAILED(rv)) {
+ fail("BeginBatch(nullptr) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ for (i = 1; i <= 20; i++) {
+ tximpl = factory->create(mgr, NONE_FLAG);
+
+ if (!tximpl) {
+ fail("Failed to allocate transaction %d.\n", i);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ tx = 0;
+ rv = tximpl->QueryInterface(NS_GET_IID(nsITransaction), (void **)&tx);
+ if (NS_FAILED(rv)) {
+ fail("QueryInterface() failed for transaction %d. (%d)\n",
+ i, rv);
+ return rv;
+ }
+
+ rv = mgr->DoTransaction(tx);
+ if (NS_FAILED(rv)) {
+ fail("Failed to execute transaction %d. (%d)\n", i, rv);
+ return rv;
+ }
+
+ tx->Release();
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 1 item failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 1) {
+ fail("GetNumberOfUndoItems() expected 1 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on redo stack with 1 item failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 1) {
+ fail("GetNumberOfRedoItems() expected 1 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->EndBatch(false);
+ if (NS_FAILED(rv)) {
+ fail("EndBatch(false) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 2 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 2) {
+ fail("GetNumberOfUndoItems() expected 2 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on empty redo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfRedoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Check if new batched transactions prune the redo stack");
+
+ /*******************************************************************
+ *
+ * Call undo.
+ *
+ *******************************************************************/
+
+ // Move a transaction over to the redo stack, so that we have one
+ // transaction on the undo stack, and one on the redo stack!
+
+ rv = mgr->UndoTransaction();
+ if (NS_FAILED(rv)) {
+ fail("Failed to undo transaction. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 1 item failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 1) {
+ fail("GetNumberOfUndoItems() expected 1 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on redo stack with 1 item failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 1) {
+ fail("GetNumberOfRedoItems() expected 1 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Call undo");
+
+ /*******************************************************************
+ *
+ * Test transaction DoTransaction() error:
+ *
+ *******************************************************************/
+
+ tximpl = factory->create(mgr, THROWS_DO_ERROR_FLAG);
+
+ if (!tximpl) {
+ fail("Failed to allocate transaction.\n");
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ tx = 0;
+
+ rv = tximpl->QueryInterface(NS_GET_IID(nsITransaction), (void **)&tx);
+ if (NS_FAILED(rv)) {
+ fail("QueryInterface() failed for transaction. (%d)\n", rv);
+ return rv;
+ }
+
+ u1 = u2 = r1 = r2 = 0;
+
+ rv = mgr->PeekUndoStack(&u1);
+
+ TEST_TXMGR_IF_RELEASE(u1); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Initial PeekUndoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->PeekRedoStack(&r1);
+
+ TEST_TXMGR_IF_RELEASE(r1); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Initial PeekRedoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->BeginBatch(nullptr);
+ if (NS_FAILED(rv)) {
+ fail("BeginBatch(nullptr) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->DoTransaction(tx);
+ if (rv != NS_ERROR_FAILURE) {
+ fail("DoTransaction() returned unexpected error. (%d)\n", rv);
+ return rv;
+ }
+
+ tx->Release();
+
+ rv = mgr->EndBatch(false);
+ if (NS_FAILED(rv)) {
+ fail("EndBatch(false) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->PeekUndoStack(&u2);
+
+ TEST_TXMGR_IF_RELEASE(u2); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Second PeekUndoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ if (u1 != u2) {
+ fail("Top of undo stack changed. (%d)\n", rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->PeekRedoStack(&r2);
+
+ TEST_TXMGR_IF_RELEASE(r2); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Second PeekRedoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ if (r1 != r2) {
+ fail("Top of redo stack changed. (%d)\n", rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 1 item failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 1) {
+ fail("GetNumberOfUndoItems() expected 1 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on redo stack with 1 item failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 1) {
+ fail("GetNumberOfRedoItems() expected 1 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Test transaction DoTransaction() error");
+
+ /*******************************************************************
+ *
+ * Test transaction UndoTransaction() error:
+ *
+ *******************************************************************/
+
+ tximpl = factory->create(mgr, THROWS_UNDO_ERROR_FLAG);
+
+ if (!tximpl) {
+ fail("Failed to allocate transaction.\n");
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ tx = 0;
+
+ rv = tximpl->QueryInterface(NS_GET_IID(nsITransaction), (void **)&tx);
+ if (NS_FAILED(rv)) {
+ fail("QueryInterface() failed for transaction. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->BeginBatch(nullptr);
+ if (NS_FAILED(rv)) {
+ fail("BeginBatch(nullptr) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->DoTransaction(tx);
+ if (NS_FAILED(rv)) {
+ fail("DoTransaction() returned unexpected error. (%d)\n", rv);
+ return rv;
+ }
+
+ tx->Release();
+
+ rv = mgr->EndBatch(false);
+ if (NS_FAILED(rv)) {
+ fail("EndBatch(false) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ u1 = u2 = r1 = r2 = 0;
+
+ rv = mgr->PeekUndoStack(&u1);
+
+ TEST_TXMGR_IF_RELEASE(u1); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Initial PeekUndoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->PeekRedoStack(&r1);
+
+ TEST_TXMGR_IF_RELEASE(r1); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Initial PeekRedoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->UndoTransaction();
+ if (rv != NS_ERROR_FAILURE) {
+ fail("UndoTransaction() returned unexpected error. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->PeekUndoStack(&u2);
+
+ TEST_TXMGR_IF_RELEASE(u2); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Second PeekUndoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ if (u1 != u2) {
+ fail("Top of undo stack changed. (%d)\n", rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->PeekRedoStack(&r2);
+
+ TEST_TXMGR_IF_RELEASE(r2); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Second PeekRedoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ if (r1 != r2) {
+ fail("Top of redo stack changed. (%d)\n", rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 2 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 2) {
+ fail("GetNumberOfUndoItems() expected 2 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on empty redo stack. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfRedoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Test transaction UndoTransaction() error");
+
+ /*******************************************************************
+ *
+ * Test transaction RedoTransaction() error:
+ *
+ *******************************************************************/
+
+ tximpl = factory->create(mgr, THROWS_REDO_ERROR_FLAG);
+
+ if (!tximpl) {
+ fail("Failed to allocate transaction.\n");
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ tx = 0;
+
+ rv = tximpl->QueryInterface(NS_GET_IID(nsITransaction), (void **)&tx);
+ if (NS_FAILED(rv)) {
+ fail("QueryInterface() failed for RedoErrorTransaction. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ rv = mgr->BeginBatch(nullptr);
+ if (NS_FAILED(rv)) {
+ fail("BeginBatch(nullptr) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->DoTransaction(tx);
+ if (NS_FAILED(rv)) {
+ fail("DoTransaction() returned unexpected error. (%d)\n", rv);
+ return rv;
+ }
+
+ tx->Release();
+
+ rv = mgr->EndBatch(false);
+ if (NS_FAILED(rv)) {
+ fail("EndBatch(false) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ //
+ // Execute a normal transaction to be used in a later test:
+ //
+
+ tximpl = factory->create(mgr, NONE_FLAG);
+
+ if (!tximpl) {
+ fail("Failed to allocate transaction.\n");
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ tx = 0;
+
+ rv = tximpl->QueryInterface(NS_GET_IID(nsITransaction), (void **)&tx);
+ if (NS_FAILED(rv)) {
+ fail("QueryInterface() failed for transaction. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->DoTransaction(tx);
+ if (NS_FAILED(rv)) {
+ fail("DoTransaction() returned unexpected error. (%d)\n", rv);
+ return rv;
+ }
+
+ tx->Release();
+
+ //
+ // Undo the 2 transactions just executed.
+ //
+
+ for (i = 1; i <= 2; ++i) {
+ rv = mgr->UndoTransaction();
+ if (NS_FAILED(rv)) {
+ fail("Failed to undo transaction %d. (%d)\n", i, rv);
+ return rv;
+ }
+ }
+
+ //
+ // The RedoErrorTransaction should now be at the top of the redo stack!
+ //
+
+ u1 = u2 = r1 = r2 = 0;
+
+ rv = mgr->PeekUndoStack(&u1);
+
+ TEST_TXMGR_IF_RELEASE(u1); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Initial PeekUndoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->PeekRedoStack(&r1);
+
+ TEST_TXMGR_IF_RELEASE(r1); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Initial PeekRedoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->RedoTransaction();
+ if (rv != NS_ERROR_FAILURE) {
+ fail("RedoTransaction() returned unexpected error. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->PeekUndoStack(&u2);
+
+ TEST_TXMGR_IF_RELEASE(u2); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Second PeekUndoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ if (u1 != u2) {
+ fail("Top of undo stack changed. (%d)\n", rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->PeekRedoStack(&r2);
+
+ TEST_TXMGR_IF_RELEASE(r2); // Don't hold onto any references!
+
+ if (NS_FAILED(rv)) {
+ fail("Second PeekRedoStack() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ if (r1 != r2) {
+ fail("Top of redo stack changed. (%d)\n", rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with 2 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 2) {
+ fail("GetNumberOfUndoItems() expected 2 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on redo stack with 2 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 2) {
+ fail("GetNumberOfRedoItems() expected 2 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ passed("Test transaction RedoTransaction() error");
+
+ /*******************************************************************
+ *
+ * Make sure that setting the transaction manager's max transaction
+ * count to zero, clears both the undo and redo stacks, and executes
+ * all new commands without pushing them on the undo stack!
+ *
+ *******************************************************************/
+
+ rv = mgr->SetMaxTransactionCount(0);
+ if (NS_FAILED(rv)) {
+ fail("SetMaxTransactionCount(0) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on empty undo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfUndoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on empty redo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfRedoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ for (i = 1; i <= 20; i++) {
+ tximpl = factory->create(mgr, NONE_FLAG);
+
+ if (!tximpl) {
+ fail("Failed to allocate transaction %d.\n", i);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ tx = 0;
+ rv = tximpl->QueryInterface(NS_GET_IID(nsITransaction), (void **)&tx);
+ if (NS_FAILED(rv)) {
+ fail("QueryInterface() failed for transaction %d. (%d)\n",
+ i, rv);
+ return rv;
+ }
+
+ rv = mgr->BeginBatch(nullptr);
+ if (NS_FAILED(rv)) {
+ fail("BeginBatch(nullptr) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->DoTransaction(tx);
+ if (NS_FAILED(rv)) {
+ fail("Failed to execute transaction %d. (%d)\n", i, rv);
+ return rv;
+ }
+
+ tx->Release();
+
+ rv = mgr->EndBatch(false);
+ if (NS_FAILED(rv)) {
+ fail("EndBatch(false) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on empty undo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfUndoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on empty redo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfRedoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ passed("Test max transaction count of zero");
+
+ /*******************************************************************
+ *
+ * Release the transaction manager. Any transactions on the undo
+ * and redo stack should automatically be released:
+ *
+ *******************************************************************/
+
+ rv = mgr->SetMaxTransactionCount(-1);
+ if (NS_FAILED(rv)) {
+ fail("SetMaxTransactionCount(0) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ // Push 20 transactions on the undo stack:
+
+ for (i = 1; i <= 20; i++) {
+ tximpl = factory->create(mgr, NONE_FLAG);
+
+ if (!tximpl) {
+ fail("Failed to allocate transaction %d.\n", i);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ tx = 0;
+ rv = tximpl->QueryInterface(NS_GET_IID(nsITransaction), (void **)&tx);
+ if (NS_FAILED(rv)) {
+ fail("QueryInterface() failed for transaction %d. (%d)\n",
+ i, rv);
+ return rv;
+ }
+
+ rv = mgr->BeginBatch(nullptr);
+ if (NS_FAILED(rv)) {
+ fail("BeginBatch(nullptr) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->DoTransaction(tx);
+ if (NS_FAILED(rv)) {
+ fail("Failed to execute transaction %d. (%d)\n", i, rv);
+ return rv;
+ }
+
+ tx->Release();
+
+ rv = mgr->EndBatch(false);
+ if (NS_FAILED(rv)) {
+ fail("EndBatch(false) failed. (%d)\n", rv);
+ return rv;
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on undo stack with %d items failed. (%d)\n",
+ i, rv);
+ return rv;
+ }
+
+ if (numitems != i) {
+ fail("GetNumberOfUndoItems() expected %d got %d. (%d)\n",
+ i, numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on empty redo stack failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems) {
+ fail("GetNumberOfRedoItems() expected 0 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ for (i = 1; i <= 10; i++) {
+ rv = mgr->UndoTransaction();
+ if (NS_FAILED(rv)) {
+ fail("Failed to undo transaction %d. (%d)\n", i, rv);
+ return rv;
+ }
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfUndoItems() on empty undo stack with 10 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 10) {
+ fail("GetNumberOfUndoItems() expected 10 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ if (NS_FAILED(rv)) {
+ fail("GetNumberOfRedoItems() on redo stack with 10 items failed. (%d)\n",
+ rv);
+ return rv;
+ }
+
+ if (numitems != 10) {
+ fail("GetNumberOfRedoItems() expected 10 got %d. (%d)\n",
+ numitems, rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mgr->Clear();
+ if (NS_FAILED(rv)) {
+ fail("Clear() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ passed("Release the transaction manager");
+
+ /*******************************************************************
+ *
+ * Make sure number of transactions created matches number of
+ * transactions destroyed!
+ *
+ *******************************************************************/
+
+ /* Disabled because the current cycle collector doesn't delete
+ cycle collectable objects synchronously.
+ if (sConstructorCount != sDestructorCount) {
+ fail("Transaction constructor count (%d) != destructor count (%d).\n",
+ sConstructorCount, sDestructorCount);
+ return NS_ERROR_FAILURE;
+ }*/
+
+ passed("Number of transactions created and destroyed match");
+ passed("%d transactions processed during quick batch test",
+ sConstructorCount);
+
+ return NS_OK;
+}
+
+nsresult
+simple_batch_test()
+{
+ /*******************************************************************
+ *
+ * Initialize globals for test.
+ *
+ *******************************************************************/
+ reset_globals();
+ sDestructorOrderArr = sSimpleBatchTestDestructorOrderArr;
+ sDoOrderArr = sSimpleBatchTestDoOrderArr;
+ sUndoOrderArr = sSimpleBatchTestUndoOrderArr;
+ sRedoOrderArr = sSimpleBatchTestRedoOrderArr;
+
+ /*******************************************************************
+ *
+ * Run the quick batch test.
+ *
+ *******************************************************************/
+
+ printf("\n-----------------------------------------------------\n");
+ printf("- Begin Batch Transaction Test:\n");
+ printf("-----------------------------------------------------\n");
+
+ SimpleTransactionFactory factory;
+
+ return quick_batch_test(&factory);
+}
+
+nsresult
+aggregation_batch_test()
+{
+ /*******************************************************************
+ *
+ * Initialize globals for test.
+ *
+ *******************************************************************/
+
+ reset_globals();
+ sDestructorOrderArr = sAggregateBatchTestDestructorOrderArr;
+ sDoOrderArr = sAggregateBatchTestDoOrderArr;
+ sUndoOrderArr = sAggregateBatchTestUndoOrderArr;
+ sRedoOrderArr = sAggregateBatchTestRedoOrderArr;
+
+ /*******************************************************************
+ *
+ * Run the quick batch test.
+ *
+ *******************************************************************/
+
+ printf("\n-----------------------------------------------------\n");
+ printf("- Begin Batch Aggregate Transaction Test:\n");
+ printf("-----------------------------------------------------\n");
+
+ AggregateTransactionFactory factory(3, 2, BATCH_FLAG);
+
+ return quick_batch_test(&factory);
+}
+
+/**
+ * Create 'iterations * (iterations + 1) / 2' transactions;
+ * do/undo/redo/undo them.
+ **/
+nsresult
+stress_test(TestTransactionFactory *factory, int32_t iterations)
+{
+ printf("Stress test of %i iterations (may take a while) ... ", iterations);
+ fflush(stdout);
+
+ /*******************************************************************
+ *
+ * Create a transaction manager:
+ *
+ *******************************************************************/
+
+ nsresult rv;
+ nsCOMPtr<nsITransactionManager> mgr =
+ do_CreateInstance(NS_TRANSACTIONMANAGER_CONTRACTID, &rv);
+ if (NS_FAILED(rv) || !mgr) {
+ fail("Failed to create Transaction Manager instance.\n");
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ int32_t i, j;
+ nsITransaction *tx;
+
+ for (i = 1; i <= iterations; i++) {
+ /*******************************************************************
+ *
+ * Create and execute a bunch of transactions:
+ *
+ *******************************************************************/
+
+ for (j = 1; j <= i; j++) {
+ TestTransaction *tximpl = factory->create(mgr, NONE_FLAG);
+
+ if (!tximpl) {
+ fail("Failed to allocate transaction %d-%d.\n", i, j);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ tx = 0;
+ rv = tximpl->QueryInterface(NS_GET_IID(nsITransaction), (void **)&tx);
+ if (NS_FAILED(rv)) {
+ fail("QueryInterface() failed for transaction %d-%d. (%d)\n",
+ i, j, rv);
+ return rv;
+ }
+
+ rv = mgr->DoTransaction(tx);
+ if (NS_FAILED(rv)) {
+ fail("Failed to execute transaction %d-%d. (%d)\n",
+ i, j, rv);
+ return rv;
+ }
+
+ tx->Release();
+ }
+
+ /*******************************************************************
+ *
+ * Undo all the transactions:
+ *
+ *******************************************************************/
+
+ for (j = 1; j <= i; j++) {
+ rv = mgr->UndoTransaction();
+ if (NS_FAILED(rv)) {
+ fail("Failed to undo transaction %d-%d. (%d)\n", i, j, rv);
+ return rv;
+ }
+ }
+
+ /*******************************************************************
+ *
+ * Redo all the transactions:
+ *
+ *******************************************************************/
+
+ for (j = 1; j <= i; j++) {
+ rv = mgr->RedoTransaction();
+ if (NS_FAILED(rv)) {
+ fail("Failed to redo transaction %d-%d. (%d)\n", i, j, rv);
+ return rv;
+ }
+ }
+
+ /*******************************************************************
+ *
+ * Undo all the transactions again so that they all end up on
+ * the redo stack for pruning the next time we execute a new
+ * transaction
+ *
+ *******************************************************************/
+
+ for (j = 1; j <= i; j++) {
+ rv = mgr->UndoTransaction();
+ if (NS_FAILED(rv)) {
+ fail("Failed to undo transaction %d-%d. (%d)\n", i, j, rv);
+ return rv;
+ }
+ }
+
+ // Trivial feedback not to let the user think the test is stuck.
+ if (MOZ_UNLIKELY(j % 100 == 0)) {
+ printf("%i ", j);
+ }
+ } // for, iterations.
+
+ printf("passed\n");
+
+ rv = mgr->Clear();
+ if (NS_FAILED(rv)) {
+ fail("Clear() failed. (%d)\n", rv);
+ return rv;
+ }
+
+ /* Disabled because the current cycle collector doesn't delete
+ cycle collectable objects synchronously.
+ if (sConstructorCount != sDestructorCount) {
+ fail("Transaction constructor count (%d) != destructor count (%d).\n",
+ sConstructorCount, sDestructorCount);
+ return NS_ERROR_FAILURE;
+ }*/
+
+ passed("%d transactions processed during stress test", sConstructorCount);
+
+ return NS_OK;
+}
+
+nsresult
+simple_stress_test()
+{
+ /*******************************************************************
+ *
+ * Initialize globals for test.
+ *
+ *******************************************************************/
+
+ reset_globals();
+
+ /*******************************************************************
+ *
+ * Do the stress test:
+ *
+ *******************************************************************/
+
+ printf("\n-----------------------------------------------------\n");
+ printf("- Simple Transaction Stress Test:\n");
+ printf("-----------------------------------------------------\n");
+
+ SimpleTransactionFactory factory;
+
+ int32_t iterations =
+#ifdef DEBUG
+ 10
+#else
+ //
+ // 1500 iterations sends 1,125,750 transactions through the system!!
+ //
+ 1500
+#endif
+ ;
+ return stress_test(&factory, iterations);
+}
+
+nsresult
+aggregation_stress_test()
+{
+ /*******************************************************************
+ *
+ * Initialize globals for test.
+ *
+ *******************************************************************/
+
+ reset_globals();
+
+ /*******************************************************************
+ *
+ * Do the stress test:
+ *
+ *******************************************************************/
+
+ printf("\n-----------------------------------------------------\n");
+ printf("- Aggregate Transaction Stress Test:\n");
+ printf("-----------------------------------------------------\n");
+
+ AggregateTransactionFactory factory(3, 4);
+
+ int32_t iterations =
+#ifdef DEBUG
+ 10
+#else
+ //
+ // 500 iterations sends 2,630,250 transactions through the system!!
+ //
+ 500
+#endif
+ ;
+ return stress_test(&factory, iterations);
+}
+
+nsresult
+aggregation_batch_stress_test()
+{
+ /*******************************************************************
+ *
+ * Initialize globals for test.
+ *
+ *******************************************************************/
+
+ reset_globals();
+
+ /*******************************************************************
+ *
+ * Do the stress test:
+ *
+ *******************************************************************/
+
+ printf("\n-----------------------------------------------------\n");
+ printf("- Aggregate Batch Transaction Stress Test:\n");
+ printf("-----------------------------------------------------\n");
+
+ AggregateTransactionFactory factory(3, 4, BATCH_FLAG);
+
+ int32_t iterations =
+#ifdef DEBUG
+ 10
+#else
+#if defined(MOZ_ASAN) || defined(MOZ_WIDGET_ANDROID)
+ // See Bug 929985: 500 is too many for ASAN and Android, 100 is safe.
+ 100
+#else
+ //
+ // 500 iterations sends 2,630,250 transactions through the system!!
+ //
+ 500
+#endif
+#endif
+ ;
+ return stress_test(&factory, iterations);
+}
+
+int
+main (int argc, char *argv[])
+{
+ ScopedXPCOM xpcom("nsITransactionManager");
+ if (xpcom.failed()) {
+ return 1;
+ }
+
+ //
+ // quick_test() part:
+ //
+
+ nsresult rv = simple_test();
+ NS_ENSURE_SUCCESS(rv, 1);
+
+ rv = aggregation_test();
+ NS_ENSURE_SUCCESS(rv, 1);
+
+ //
+ // quick_batch_test() part:
+ //
+
+ rv = simple_batch_test();
+ NS_ENSURE_SUCCESS(rv, 1);
+
+ rv = aggregation_batch_test();
+ NS_ENSURE_SUCCESS(rv, 1);
+
+ //
+ // stress_test() part:
+ //
+
+ rv = simple_stress_test();
+ NS_ENSURE_SUCCESS(rv, 1);
+
+ rv = aggregation_stress_test();
+ NS_ENSURE_SUCCESS(rv, 1);
+
+ rv = aggregation_batch_stress_test();
+ NS_ENSURE_SUCCESS(rv, 1);
+
+ return 0;
+}
diff --git a/editor/txmgr/tests/crashtests/407072-1.html b/editor/txmgr/tests/crashtests/407072-1.html
new file mode 100644
index 000000000..e3aa3c216
--- /dev/null
+++ b/editor/txmgr/tests/crashtests/407072-1.html
@@ -0,0 +1,22 @@
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var br = document.getElementById("br");
+ br.contentEditable = "true";
+ br.focus();
+
+ try { document.execCommand("justifyfull", false, null); } catch(e) { }
+ try { document.execCommand("justifyfull", false, null); } catch(e) { }
+ document.execCommand("underline", false, null);
+ document.execCommand("insertimage", false, "foo");
+ try { document.execCommand("outdent", false, null); } catch(e) { }
+}
+
+</script>
+</head>
+
+<body onload="boom();"><br id="br"></body>
+</html>
diff --git a/editor/txmgr/tests/crashtests/449006-1.html b/editor/txmgr/tests/crashtests/449006-1.html
new file mode 100644
index 000000000..65e6ed981
--- /dev/null
+++ b/editor/txmgr/tests/crashtests/449006-1.html
@@ -0,0 +1,28 @@
+<html>
+<head>
+<script type="text/javascript">
+
+function boom()
+{
+ document.getElementById("d").focus();
+ document.execCommand("inserthtml", false, "AB");
+ document.execCommand("delete", false, null);
+ document.execCommand("undo", false, null);
+
+ document.addEventListener("DOMCharacterDataModified", f, false);
+ document.execCommand("redo", false, null);
+ document.removeEventListener("DOMCharacterDataModified", f, false);
+
+ function f()
+ {
+ document.removeEventListener("DOMCharacterDataModified", f, false);
+ document.execCommand("formatBlock", false, "<h3>");
+ }
+}
+
+</script>
+</head>
+
+<body onload="boom();"><div id="d" contenteditable="true"></div></body>
+
+</html>
diff --git a/editor/txmgr/tests/crashtests/crashtests.list b/editor/txmgr/tests/crashtests/crashtests.list
new file mode 100644
index 000000000..d39187592
--- /dev/null
+++ b/editor/txmgr/tests/crashtests/crashtests.list
@@ -0,0 +1,2 @@
+needs-focus load 407072-1.html
+load 449006-1.html
diff --git a/editor/txmgr/tests/moz.build b/editor/txmgr/tests/moz.build
new file mode 100644
index 000000000..d04d4f15a
--- /dev/null
+++ b/editor/txmgr/tests/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+GeckoCppUnitTests([
+ 'TestTXMgr',
+])
diff --git a/editor/txtsvc/moz.build b/editor/txtsvc/moz.build
new file mode 100644
index 000000000..4c0b93419
--- /dev/null
+++ b/editor/txtsvc/moz.build
@@ -0,0 +1,26 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPIDL_SOURCES += [
+ 'nsIInlineSpellChecker.idl',
+ 'nsITextServicesFilter.idl',
+]
+
+XPIDL_MODULE = 'txtsvc'
+
+EXPORTS += [
+ 'nsISpellChecker.h',
+ 'nsITextService.h',
+ 'nsITextServicesDocument.h',
+ 'nsTextServicesCID.h',
+]
+
+UNIFIED_SOURCES += [
+ 'nsFilteredContentIterator.cpp',
+ 'nsTextServicesDocument.cpp',
+]
+
+FINAL_LIBRARY = 'xul'
diff --git a/editor/txtsvc/nsFilteredContentIterator.cpp b/editor/txtsvc/nsFilteredContentIterator.cpp
new file mode 100644
index 000000000..c8ea734c4
--- /dev/null
+++ b/editor/txtsvc/nsFilteredContentIterator.cpp
@@ -0,0 +1,422 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/mozalloc.h"
+#include "nsComponentManagerUtils.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsFilteredContentIterator.h"
+#include "nsIAtom.h"
+#include "nsIContent.h"
+#include "nsIContentIterator.h"
+#include "nsIDOMNode.h"
+#include "nsINode.h"
+#include "nsISupportsBase.h"
+#include "nsISupportsUtils.h"
+#include "nsITextServicesFilter.h"
+#include "nsRange.h"
+
+//------------------------------------------------------------
+nsFilteredContentIterator::nsFilteredContentIterator(nsITextServicesFilter* aFilter) :
+ mFilter(aFilter),
+ mDidSkip(false),
+ mIsOutOfRange(false),
+ mDirection(eDirNotSet)
+{
+ mIterator = do_CreateInstance("@mozilla.org/content/post-content-iterator;1");
+ mPreIterator = do_CreateInstance("@mozilla.org/content/pre-content-iterator;1");
+}
+
+//------------------------------------------------------------
+nsFilteredContentIterator::~nsFilteredContentIterator()
+{
+}
+
+//------------------------------------------------------------
+NS_IMPL_CYCLE_COLLECTING_ADDREF(nsFilteredContentIterator)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(nsFilteredContentIterator)
+
+NS_INTERFACE_MAP_BEGIN(nsFilteredContentIterator)
+ NS_INTERFACE_MAP_ENTRY(nsIContentIterator)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIContentIterator)
+ NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(nsFilteredContentIterator)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION(nsFilteredContentIterator,
+ mCurrentIterator,
+ mIterator,
+ mPreIterator,
+ mFilter,
+ mRange)
+
+//------------------------------------------------------------
+nsresult
+nsFilteredContentIterator::Init(nsINode* aRoot)
+{
+ NS_ENSURE_ARG_POINTER(aRoot);
+ NS_ENSURE_TRUE(mPreIterator, NS_ERROR_FAILURE);
+ NS_ENSURE_TRUE(mIterator, NS_ERROR_FAILURE);
+ mIsOutOfRange = false;
+ mDirection = eForward;
+ mCurrentIterator = mPreIterator;
+
+ mRange = new nsRange(aRoot);
+ nsCOMPtr<nsIDOMNode> domNode(do_QueryInterface(aRoot));
+ if (domNode) {
+ mRange->SelectNode(domNode);
+ }
+
+ nsresult rv = mPreIterator->Init(mRange);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return mIterator->Init(mRange);
+}
+
+//------------------------------------------------------------
+nsresult
+nsFilteredContentIterator::Init(nsIDOMRange* aRange)
+{
+ NS_ENSURE_TRUE(mPreIterator, NS_ERROR_FAILURE);
+ NS_ENSURE_TRUE(mIterator, NS_ERROR_FAILURE);
+ NS_ENSURE_ARG_POINTER(aRange);
+ mIsOutOfRange = false;
+ mDirection = eForward;
+ mCurrentIterator = mPreIterator;
+
+ mRange = static_cast<nsRange*>(aRange)->CloneRange();
+
+ nsresult rv = mPreIterator->Init(mRange);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return mIterator->Init(mRange);
+}
+
+//------------------------------------------------------------
+nsresult
+nsFilteredContentIterator::SwitchDirections(bool aChangeToForward)
+{
+ nsINode *node = mCurrentIterator->GetCurrentNode();
+
+ if (aChangeToForward) {
+ mCurrentIterator = mPreIterator;
+ mDirection = eForward;
+ } else {
+ mCurrentIterator = mIterator;
+ mDirection = eBackward;
+ }
+
+ if (node) {
+ nsresult rv = mCurrentIterator->PositionAt(node);
+ if (NS_FAILED(rv)) {
+ mIsOutOfRange = true;
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+//------------------------------------------------------------
+void
+nsFilteredContentIterator::First()
+{
+ if (!mCurrentIterator) {
+ NS_ERROR("Missing iterator!");
+
+ return;
+ }
+
+ // If we are switching directions then
+ // we need to switch how we process the nodes
+ if (mDirection != eForward) {
+ mCurrentIterator = mPreIterator;
+ mDirection = eForward;
+ mIsOutOfRange = false;
+ }
+
+ mCurrentIterator->First();
+
+ if (mCurrentIterator->IsDone()) {
+ return;
+ }
+
+ nsINode *currentNode = mCurrentIterator->GetCurrentNode();
+ nsCOMPtr<nsIDOMNode> node(do_QueryInterface(currentNode));
+
+ bool didCross;
+ CheckAdvNode(node, didCross, eForward);
+}
+
+//------------------------------------------------------------
+void
+nsFilteredContentIterator::Last()
+{
+ if (!mCurrentIterator) {
+ NS_ERROR("Missing iterator!");
+
+ return;
+ }
+
+ // If we are switching directions then
+ // we need to switch how we process the nodes
+ if (mDirection != eBackward) {
+ mCurrentIterator = mIterator;
+ mDirection = eBackward;
+ mIsOutOfRange = false;
+ }
+
+ mCurrentIterator->Last();
+
+ if (mCurrentIterator->IsDone()) {
+ return;
+ }
+
+ nsINode *currentNode = mCurrentIterator->GetCurrentNode();
+ nsCOMPtr<nsIDOMNode> node(do_QueryInterface(currentNode));
+
+ bool didCross;
+ CheckAdvNode(node, didCross, eBackward);
+}
+
+///////////////////////////////////////////////////////////////////////////
+// ContentToParentOffset: returns the content node's parent and offset.
+//
+static void
+ContentToParentOffset(nsIContent *aContent, nsIDOMNode **aParent,
+ int32_t *aOffset)
+{
+ if (!aParent || !aOffset)
+ return;
+
+ *aParent = nullptr;
+ *aOffset = 0;
+
+ if (!aContent)
+ return;
+
+ nsIContent* parent = aContent->GetParent();
+
+ if (!parent)
+ return;
+
+ *aOffset = parent->IndexOf(aContent);
+
+ CallQueryInterface(parent, aParent);
+}
+
+///////////////////////////////////////////////////////////////////////////
+// ContentIsInTraversalRange: returns true if content is visited during
+// the traversal of the range in the specified mode.
+//
+static bool
+ContentIsInTraversalRange(nsIContent *aContent, bool aIsPreMode,
+ nsIDOMNode *aStartNode, int32_t aStartOffset,
+ nsIDOMNode *aEndNode, int32_t aEndOffset)
+{
+ NS_ENSURE_TRUE(aStartNode && aEndNode && aContent, false);
+
+ nsCOMPtr<nsIDOMNode> parentNode;
+ int32_t indx = 0;
+
+ ContentToParentOffset(aContent, getter_AddRefs(parentNode), &indx);
+
+ NS_ENSURE_TRUE(parentNode, false);
+
+ if (!aIsPreMode)
+ ++indx;
+
+ int32_t startRes = nsContentUtils::ComparePoints(aStartNode, aStartOffset,
+ parentNode, indx);
+ int32_t endRes = nsContentUtils::ComparePoints(aEndNode, aEndOffset,
+ parentNode, indx);
+ return (startRes <= 0) && (endRes >= 0);
+}
+
+static bool
+ContentIsInTraversalRange(nsRange* aRange, nsIDOMNode* aNextNode, bool aIsPreMode)
+{
+ nsCOMPtr<nsIContent> content(do_QueryInterface(aNextNode));
+ NS_ENSURE_TRUE(content && aRange, false);
+
+ nsCOMPtr<nsIDOMNode> sNode;
+ nsCOMPtr<nsIDOMNode> eNode;
+ int32_t sOffset;
+ int32_t eOffset;
+ aRange->GetStartContainer(getter_AddRefs(sNode));
+ aRange->GetStartOffset(&sOffset);
+ aRange->GetEndContainer(getter_AddRefs(eNode));
+ aRange->GetEndOffset(&eOffset);
+ return ContentIsInTraversalRange(content, aIsPreMode, sNode, sOffset, eNode, eOffset);
+}
+
+//------------------------------------------------------------
+// Helper function to advance to the next or previous node
+nsresult
+nsFilteredContentIterator::AdvanceNode(nsIDOMNode* aNode, nsIDOMNode*& aNewNode, eDirectionType aDir)
+{
+ nsCOMPtr<nsIDOMNode> nextNode;
+ if (aDir == eForward) {
+ aNode->GetNextSibling(getter_AddRefs(nextNode));
+ } else {
+ aNode->GetPreviousSibling(getter_AddRefs(nextNode));
+ }
+
+ if (nextNode) {
+ // If we got here, that means we found the nxt/prv node
+ // make sure it is in our DOMRange
+ bool intersects = ContentIsInTraversalRange(mRange, nextNode, aDir == eForward);
+ if (intersects) {
+ aNewNode = nextNode;
+ NS_ADDREF(aNewNode);
+ return NS_OK;
+ }
+ } else {
+ // The next node was null so we need to walk up the parent(s)
+ nsCOMPtr<nsIDOMNode> parent;
+ aNode->GetParentNode(getter_AddRefs(parent));
+ NS_ASSERTION(parent, "parent can't be nullptr");
+
+ // Make sure the parent is in the DOMRange before going further
+ bool intersects = ContentIsInTraversalRange(mRange, nextNode, aDir == eForward);
+ if (intersects) {
+ // Now find the nxt/prv node after/before this node
+ nsresult rv = AdvanceNode(parent, aNewNode, aDir);
+ if (NS_SUCCEEDED(rv) && aNewNode) {
+ return NS_OK;
+ }
+ }
+ }
+
+ // if we get here it pretty much means
+ // we went out of the DOM Range
+ mIsOutOfRange = true;
+
+ return NS_ERROR_FAILURE;
+}
+
+//------------------------------------------------------------
+// Helper function to see if the next/prev node should be skipped
+void
+nsFilteredContentIterator::CheckAdvNode(nsIDOMNode* aNode, bool& aDidSkip, eDirectionType aDir)
+{
+ aDidSkip = false;
+ mIsOutOfRange = false;
+
+ if (aNode && mFilter) {
+ nsCOMPtr<nsIDOMNode> currentNode = aNode;
+ bool skipIt;
+ while (1) {
+ nsresult rv = mFilter->Skip(aNode, &skipIt);
+ if (NS_SUCCEEDED(rv) && skipIt) {
+ aDidSkip = true;
+ // Get the next/prev node and then
+ // see if we should skip that
+ nsCOMPtr<nsIDOMNode> advNode;
+ rv = AdvanceNode(aNode, *getter_AddRefs(advNode), aDir);
+ if (NS_SUCCEEDED(rv) && advNode) {
+ aNode = advNode;
+ } else {
+ return; // fell out of range
+ }
+ } else {
+ if (aNode != currentNode) {
+ nsCOMPtr<nsIContent> content(do_QueryInterface(aNode));
+ mCurrentIterator->PositionAt(content);
+ }
+ return; // found something
+ }
+ }
+ }
+}
+
+void
+nsFilteredContentIterator::Next()
+{
+ if (mIsOutOfRange || !mCurrentIterator) {
+ NS_ASSERTION(mCurrentIterator, "Missing iterator!");
+
+ return;
+ }
+
+ // If we are switching directions then
+ // we need to switch how we process the nodes
+ if (mDirection != eForward) {
+ nsresult rv = SwitchDirections(true);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+ }
+
+ mCurrentIterator->Next();
+
+ if (mCurrentIterator->IsDone()) {
+ return;
+ }
+
+ // If we can't get the current node then
+ // don't check to see if we can skip it
+ nsINode *currentNode = mCurrentIterator->GetCurrentNode();
+
+ nsCOMPtr<nsIDOMNode> node(do_QueryInterface(currentNode));
+ CheckAdvNode(node, mDidSkip, eForward);
+}
+
+void
+nsFilteredContentIterator::Prev()
+{
+ if (mIsOutOfRange || !mCurrentIterator) {
+ NS_ASSERTION(mCurrentIterator, "Missing iterator!");
+
+ return;
+ }
+
+ // If we are switching directions then
+ // we need to switch how we process the nodes
+ if (mDirection != eBackward) {
+ nsresult rv = SwitchDirections(false);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+ }
+
+ mCurrentIterator->Prev();
+
+ if (mCurrentIterator->IsDone()) {
+ return;
+ }
+
+ // If we can't get the current node then
+ // don't check to see if we can skip it
+ nsINode *currentNode = mCurrentIterator->GetCurrentNode();
+
+ nsCOMPtr<nsIDOMNode> node(do_QueryInterface(currentNode));
+ CheckAdvNode(node, mDidSkip, eBackward);
+}
+
+nsINode *
+nsFilteredContentIterator::GetCurrentNode()
+{
+ if (mIsOutOfRange || !mCurrentIterator) {
+ return nullptr;
+ }
+
+ return mCurrentIterator->GetCurrentNode();
+}
+
+bool
+nsFilteredContentIterator::IsDone()
+{
+ if (mIsOutOfRange || !mCurrentIterator) {
+ return true;
+ }
+
+ return mCurrentIterator->IsDone();
+}
+
+nsresult
+nsFilteredContentIterator::PositionAt(nsINode* aCurNode)
+{
+ NS_ENSURE_TRUE(mCurrentIterator, NS_ERROR_FAILURE);
+ mIsOutOfRange = false;
+ return mCurrentIterator->PositionAt(aCurNode);
+}
diff --git a/editor/txtsvc/nsFilteredContentIterator.h b/editor/txtsvc/nsFilteredContentIterator.h
new file mode 100644
index 000000000..9dde8132f
--- /dev/null
+++ b/editor/txtsvc/nsFilteredContentIterator.h
@@ -0,0 +1,75 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsFilteredContentIterator_h__
+#define nsFilteredContentIterator_h__
+
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIContentIterator.h"
+#include "nsISupportsImpl.h"
+#include "nscore.h"
+
+class nsIAtom;
+class nsIDOMNode;
+class nsIDOMRange;
+class nsINode;
+class nsITextServicesFilter;
+class nsRange;
+
+class nsFilteredContentIterator final : public nsIContentIterator
+{
+public:
+
+ // nsISupports interface...
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(nsFilteredContentIterator)
+
+ explicit nsFilteredContentIterator(nsITextServicesFilter* aFilter);
+
+ /* nsIContentIterator */
+ virtual nsresult Init(nsINode* aRoot) override;
+ virtual nsresult Init(nsIDOMRange* aRange) override;
+ virtual void First() override;
+ virtual void Last() override;
+ virtual void Next() override;
+ virtual void Prev() override;
+ virtual nsINode *GetCurrentNode() override;
+ virtual bool IsDone() override;
+ virtual nsresult PositionAt(nsINode* aCurNode) override;
+
+ /* Helpers */
+ bool DidSkip() { return mDidSkip; }
+ void ClearDidSkip() { mDidSkip = false; }
+
+protected:
+ nsFilteredContentIterator() : mDidSkip(false), mIsOutOfRange(false) { }
+
+ virtual ~nsFilteredContentIterator();
+
+ // enum to give us the direction
+ typedef enum {eDirNotSet, eForward, eBackward} eDirectionType;
+ nsresult AdvanceNode(nsIDOMNode* aNode, nsIDOMNode*& aNewNode, eDirectionType aDir);
+ void CheckAdvNode(nsIDOMNode* aNode, bool& aDidSkip, eDirectionType aDir);
+ nsresult SwitchDirections(bool aChangeToForward);
+
+ nsCOMPtr<nsIContentIterator> mCurrentIterator;
+ nsCOMPtr<nsIContentIterator> mIterator;
+ nsCOMPtr<nsIContentIterator> mPreIterator;
+
+ nsCOMPtr<nsIAtom> mBlockQuoteAtom;
+ nsCOMPtr<nsIAtom> mScriptAtom;
+ nsCOMPtr<nsIAtom> mTextAreaAtom;
+ nsCOMPtr<nsIAtom> mSelectAreaAtom;
+ nsCOMPtr<nsIAtom> mMapAtom;
+
+ nsCOMPtr<nsITextServicesFilter> mFilter;
+ RefPtr<nsRange> mRange;
+ bool mDidSkip;
+ bool mIsOutOfRange;
+ eDirectionType mDirection;
+};
+
+#endif
diff --git a/editor/txtsvc/nsIInlineSpellChecker.idl b/editor/txtsvc/nsIInlineSpellChecker.idl
new file mode 100644
index 000000000..62ea06bfc
--- /dev/null
+++ b/editor/txtsvc/nsIInlineSpellChecker.idl
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "domstubs.idl"
+
+interface nsISelection;
+interface nsIEditor;
+interface nsIEditorSpellCheck;
+
+[scriptable, uuid(b7b7a77c-40c4-4196-b0b7-b0338243b3fe)]
+interface nsIInlineSpellChecker : nsISupports
+{
+ readonly attribute nsIEditorSpellCheck spellChecker;
+
+ void init(in nsIEditor aEditor);
+ void cleanup(in boolean aDestroyingFrames);
+
+ attribute boolean enableRealTimeSpell;
+
+ void spellCheckAfterEditorChange(in long aAction,
+ in nsISelection aSelection,
+ in nsIDOMNode aPreviousSelectedNode,
+ in long aPreviousSelectedOffset,
+ in nsIDOMNode aStartNode,
+ in long aStartOffset,
+ in nsIDOMNode aEndNode,
+ in long aEndOffset);
+
+ void spellCheckRange(in nsIDOMRange aSelection);
+
+ nsIDOMRange getMisspelledWord(in nsIDOMNode aNode, in long aOffset);
+ void replaceWord(in nsIDOMNode aNode, in long aOffset, in AString aNewword);
+ void addWordToDictionary(in AString aWord);
+ void removeWordFromDictionary(in AString aWord);
+
+ void ignoreWord(in AString aWord);
+ void ignoreWords([array, size_is(aCount)] in wstring aWordsToIgnore, in unsigned long aCount);
+ void updateCurrentDictionary();
+
+ readonly attribute boolean spellCheckPending;
+};
+
+%{C++
+
+#define MOZ_INLINESPELLCHECKER_CONTRACTID "@mozilla.org/spellchecker-inline;1"
+
+%}
diff --git a/editor/txtsvc/nsISpellChecker.h b/editor/txtsvc/nsISpellChecker.h
new file mode 100644
index 000000000..cafc725be
--- /dev/null
+++ b/editor/txtsvc/nsISpellChecker.h
@@ -0,0 +1,122 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsISpellChecker_h__
+#define nsISpellChecker_h__
+
+#include "nsISupports.h"
+#include "nsTArray.h"
+
+#define NS_SPELLCHECKER_CONTRACTID "@mozilla.org/spellchecker;1"
+
+#define NS_ISPELLCHECKER_IID \
+{ /* 27bff957-b486-40ae-9f5d-af0cdd211868 */ \
+0x27bff957, 0xb486, 0x40ae, \
+ { 0x9f, 0x5d, 0xaf, 0x0c, 0xdd, 0x21, 0x18, 0x68 } }
+
+class nsITextServicesDocument;
+class nsString;
+
+/**
+ * A generic interface for a spelling checker.
+ */
+class nsISpellChecker : public nsISupports{
+public:
+
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_ISPELLCHECKER_IID)
+
+ /**
+ * Tells the spellchecker what document to check.
+ * @param aDoc is the document to check.
+ * @param aFromStartOfDoc If true, start check from beginning of document,
+ * if false, start check from current cursor position.
+ */
+ NS_IMETHOD SetDocument(nsITextServicesDocument *aDoc, bool aFromStartofDoc) = 0;
+
+ /**
+ * Selects (hilites) the next misspelled word in the document.
+ * @param aWord will contain the misspelled word.
+ * @param aSuggestions is an array of nsStrings, that represent the
+ * suggested replacements for the misspelled word.
+ */
+ NS_IMETHOD NextMisspelledWord(nsAString &aWord, nsTArray<nsString> *aSuggestions) = 0;
+
+ /**
+ * Checks if a word is misspelled. No document is required to use this method.
+ * @param aWord is the word to check.
+ * @param aIsMisspelled will be set to true if the word is misspelled.
+ * @param aSuggestions is an array of nsStrings which represent the
+ * suggested replacements for the misspelled word. The array will be empty
+ * if there aren't any suggestions.
+ */
+ NS_IMETHOD CheckWord(const nsAString &aWord, bool *aIsMisspelled, nsTArray<nsString> *aSuggestions) = 0;
+
+ /**
+ * Replaces the old word with the specified new word.
+ * @param aOldWord is the word to be replaced.
+ * @param aNewWord is the word that is to replace old word.
+ * @param aAllOccurrences will replace all occurrences of old
+ * word, in the document, with new word when it is true. If
+ * false, it will replace the 1st occurrence only!
+ */
+ NS_IMETHOD Replace(const nsAString &aOldWord, const nsAString &aNewWord, bool aAllOccurrences) = 0;
+
+ /**
+ * Ignores all occurrences of the specified word in the document.
+ * @param aWord is the word to ignore.
+ */
+ NS_IMETHOD IgnoreAll(const nsAString &aWord) = 0;
+
+ /**
+ * Add a word to the user's personal dictionary.
+ * @param aWord is the word to add.
+ */
+ NS_IMETHOD AddWordToPersonalDictionary(const nsAString &aWord) = 0;
+
+ /**
+ * Remove a word from the user's personal dictionary.
+ * @param aWord is the word to remove.
+ */
+ NS_IMETHOD RemoveWordFromPersonalDictionary(const nsAString &aWord) = 0;
+
+ /**
+ * Returns the list of words in the user's personal dictionary.
+ * @param aWordList is an array of nsStrings that represent the
+ * list of words in the user's personal dictionary.
+ */
+ NS_IMETHOD GetPersonalDictionary(nsTArray<nsString> *aWordList) = 0;
+
+ /**
+ * Returns the list of strings representing the dictionaries
+ * the spellchecker supports. It was suggested that the strings
+ * returned be in the RFC 1766 format. This format looks something
+ * like <ISO 639 language code>-<ISO 3166 country code>.
+ * For example: en-US
+ * @param aDictionaryList is an array of nsStrings that represent the
+ * dictionaries supported by the spellchecker.
+ */
+ NS_IMETHOD GetDictionaryList(nsTArray<nsString> *aDictionaryList) = 0;
+
+ /**
+ * Returns a string representing the current dictionary.
+ * @param aDictionary will contain the name of the dictionary.
+ * This name is the same string that is in the list returned
+ * by GetDictionaryList().
+ */
+ NS_IMETHOD GetCurrentDictionary(nsAString &aDictionary) = 0;
+
+ /**
+ * Tells the spellchecker to use a specific dictionary.
+ * @param aDictionary a string that is in the list returned
+ * by GetDictionaryList() or an empty string. If aDictionary is
+ * empty string, spellchecker will be disabled.
+ */
+ NS_IMETHOD SetCurrentDictionary(const nsAString &aDictionary) = 0;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsISpellChecker, NS_ISPELLCHECKER_IID)
+
+#endif // nsISpellChecker_h__
+
diff --git a/editor/txtsvc/nsITextService.h b/editor/txtsvc/nsITextService.h
new file mode 100644
index 000000000..13ffe3657
--- /dev/null
+++ b/editor/txtsvc/nsITextService.h
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsITextService_h__
+#define nsITextService_h__
+
+#include "nsISupports.h"
+
+class nsITextServicesDocument;
+
+/*
+TextService interface to outside world
+*/
+
+#define NS_ITEXTSERVICE_IID \
+{ /* 019718E0-CDB5-11d2-8D3C-000000000000 */ \
+0x019718e0, 0xcdb5, 0x11d2, \
+{ 0x8d, 0x3c, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 } }
+
+
+/**
+ *
+ */
+class nsITextService : public nsISupports{
+public:
+
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_ITEXTSERVICE_IID)
+
+ /**
+ *
+ */
+ NS_IMETHOD Init(nsITextServicesDocument *aDoc) = 0;
+ NS_IMETHOD Execute() = 0;
+ NS_IMETHOD GetMenuString(nsString &aString) = 0;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsITextService, NS_ITEXTSERVICE_IID)
+
+#endif // nsITextService_h__
+
diff --git a/editor/txtsvc/nsITextServicesDocument.h b/editor/txtsvc/nsITextServicesDocument.h
new file mode 100644
index 000000000..8b9d9e506
--- /dev/null
+++ b/editor/txtsvc/nsITextServicesDocument.h
@@ -0,0 +1,185 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsITextServicesDocument_h__
+#define nsITextServicesDocument_h__
+
+#include "nsISupports.h"
+
+class nsIDOMDocument;
+class nsIDOMRange;
+class nsIEditor;
+class nsString;
+class nsITextServicesFilter;
+
+/*
+TextServicesDocument interface to outside world
+*/
+
+#define NS_ITEXTSERVICESDOCUMENT_IID \
+{ /* 019718E1-CDB5-11d2-8D3C-000000000000 */ \
+0x019718e1, 0xcdb5, 0x11d2, \
+{ 0x8d, 0x3c, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 } }
+
+
+/**
+ * The nsITextServicesDocument presents the document in as a
+ * bunch of flattened text blocks. Each text block can be retrieved
+ * as an nsString (array of characters).
+ */
+class nsITextServicesDocument : public nsISupports{
+public:
+
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_ITEXTSERVICESDOCUMENT_IID)
+
+ typedef enum { eDSNormal=0, eDSUndlerline } TSDDisplayStyle;
+
+ typedef enum { eBlockNotFound=0, // There is no text block (TB) in or before the selection (S).
+ eBlockOutside, // No TB in S, but found one before/after S.
+ eBlockInside, // S extends beyond the start and end of TB.
+ eBlockContains, // TB contains entire S.
+ eBlockPartial // S begins or ends in TB but extends outside of TB.
+ } TSDBlockSelectionStatus;
+
+ /**
+ * Get the DOM document for the document in use.
+ * @return aDocument the dom document [OUT]
+ */
+ NS_IMETHOD GetDocument(nsIDOMDocument **aDocument) = 0;
+
+ /**
+ * Initializes the text services document to use a particular
+ * editor. The text services document will use the DOM document
+ * and presentation shell used by the editor.
+ * @param aEditor is the editor to use. The editor is AddRef'd
+ * by this method.
+ */
+ NS_IMETHOD InitWithEditor(nsIEditor *aEditor) = 0;
+
+ /**
+ * Sets the range/extent over which the text services document
+ * will iterate. Note that InitWithEditor() should have been called prior to
+ * calling this method. If this method is never called, the text services
+ * defaults to iterating over the entire document.
+ *
+ * @param aDOMRange is the range to use. aDOMRange must point to a
+ * valid range object.
+ */
+ NS_IMETHOD SetExtent(nsIDOMRange* aDOMRange) = 0;
+
+ /**
+ * Expands the end points of the range so that it spans complete words.
+ * This call does not change any internal state of the text services document.
+ *
+ * @param aDOMRange the range to be expanded/adjusted.
+ */
+ NS_IMETHOD ExpandRangeToWordBoundaries(nsIDOMRange *aRange) = 0;
+
+ /**
+ * Sets the filter to be used while iterating over content.
+ * @param aFilter filter to be used while iterating over content.
+ */
+ NS_IMETHOD SetFilter(nsITextServicesFilter *aFilter) = 0;
+
+ /**
+ * Returns the text in the current text block.
+ * @param aStr will contain the text.
+ */
+
+ NS_IMETHOD GetCurrentTextBlock(nsString *aStr) = 0;
+
+ /**
+ * Tells the document to point to the first text block
+ * in the document. This method does not adjust the current
+ * cursor position or selection.
+ */
+
+ NS_IMETHOD FirstBlock() = 0;
+
+ /**
+ * Tells the document to point to the last text block that
+ * contains the current selection or caret.
+ * @param aSelectionStatus will contain the text block selection status
+ * @param aSelectionOffset will contain the offset into the
+ * string returned by GetCurrentTextBlock() where the selection
+ * begins.
+ * @param aLength will contain the number of characters that are
+ * selected in the string.
+ */
+
+ NS_IMETHOD LastSelectedBlock(TSDBlockSelectionStatus *aSelectionStatus, int32_t *aSelectionOffset, int32_t *aSelectionLength) = 0;
+
+ /**
+ * Tells the document to point to the text block before
+ * the current one. This method will return NS_OK, even
+ * if there is no previous block. Callers should call IsDone()
+ * to check if we have gone beyond the first text block in
+ * the document.
+ */
+
+ NS_IMETHOD PrevBlock() = 0;
+
+ /**
+ * Tells the document to point to the text block after
+ * the current one. This method will return NS_OK, even
+ * if there is no next block. Callers should call IsDone()
+ * to check if we have gone beyond the last text block
+ * in the document.
+ */
+
+ NS_IMETHOD NextBlock() = 0;
+
+ /**
+ * IsDone() will always set aIsDone == false unless
+ * the document contains no text, PrevBlock() was called
+ * while the document was already pointing to the first
+ * text block in the document, or NextBlock() was called
+ * while the document was already pointing to the last
+ * text block in the document.
+ * @param aIsDone will contain the result.
+ */
+
+ NS_IMETHOD IsDone(bool *aIsDone) = 0;
+
+ /**
+ * SetSelection() allows the caller to set the selection
+ * based on an offset into the string returned by
+ * GetCurrentTextBlock(). A length of zero places the cursor
+ * at that offset. A positive non-zero length "n" selects
+ * n characters in the string.
+ * @param aOffset offset into string returned by GetCurrentTextBlock().
+ * @param aLength number characters selected.
+ */
+
+ NS_IMETHOD SetSelection(int32_t aOffset, int32_t aLength) = 0;
+
+ /**
+ * Scrolls the document so that the current selection is visible.
+ */
+
+ NS_IMETHOD ScrollSelectionIntoView() = 0;
+
+ /**
+ * Deletes the text selected by SetSelection(). Calling
+ * DeleteSelection with nothing selected, or with a collapsed
+ * selection (cursor) does nothing and returns NS_OK.
+ */
+
+ NS_IMETHOD DeleteSelection() = 0;
+
+ /**
+ * Inserts the given text at the current cursor position.
+ * If there is a selection, it will be deleted before the
+ * text is inserted.
+ */
+
+ NS_IMETHOD InsertText(const nsString *aText) = 0;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsITextServicesDocument,
+ NS_ITEXTSERVICESDOCUMENT_IID)
+
+#endif // nsITextServicesDocument_h__
+
diff --git a/editor/txtsvc/nsITextServicesFilter.idl b/editor/txtsvc/nsITextServicesFilter.idl
new file mode 100644
index 000000000..b2dba0e4a
--- /dev/null
+++ b/editor/txtsvc/nsITextServicesFilter.idl
@@ -0,0 +1,21 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIDOMNode;
+
+[scriptable, uuid(5BEC321F-59AC-413a-A4AD-8A8D7C50A0D0)]
+interface nsITextServicesFilter : nsISupports
+{
+
+ /**
+ * Indicates whether the content node should be skipped by the iterator
+ * @param aNode - node to skip
+ */
+ boolean skip(in nsIDOMNode aNode);
+
+};
+
diff --git a/editor/txtsvc/nsTSAtomList.h b/editor/txtsvc/nsTSAtomList.h
new file mode 100644
index 000000000..13feb9f12
--- /dev/null
+++ b/editor/txtsvc/nsTSAtomList.h
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/******
+
+ This file contains the list of all text services nsIAtoms and their values
+
+ It is designed to be used as inline input to nsTextServicesDocument.cpp *only*
+ through the magic of C preprocessing.
+
+ All entries must be enclosed in the macro TS_ATOM which will have cruel
+ and unusual things done to it
+
+ It is recommended (but not strictly necessary) to keep all entries
+ in alphabetical order
+
+ The first argument to TS_ATOM is the C++ identifier of the atom
+ The second argument is the string value of the atom
+
+ ******/
+
+// OUTPUT_CLASS=nsTextServicesDocument
+// MACRO_NAME=TS_ATOM
+
+TS_ATOM(sAAtom, "a")
+TS_ATOM(sAddressAtom, "address")
+TS_ATOM(sBigAtom, "big")
+TS_ATOM(sBAtom, "b")
+TS_ATOM(sCiteAtom, "cite")
+TS_ATOM(sCodeAtom, "code")
+TS_ATOM(sDfnAtom, "dfn")
+TS_ATOM(sEmAtom, "em")
+TS_ATOM(sFontAtom, "font")
+TS_ATOM(sIAtom, "i")
+TS_ATOM(sKbdAtom, "kbd")
+TS_ATOM(sKeygenAtom, "keygen")
+TS_ATOM(sNobrAtom, "nobr")
+TS_ATOM(sSAtom, "s")
+TS_ATOM(sSampAtom, "samp")
+TS_ATOM(sSmallAtom, "small")
+TS_ATOM(sSpacerAtom, "spacer")
+TS_ATOM(sSpanAtom, "span")
+TS_ATOM(sStrikeAtom, "strike")
+TS_ATOM(sStrongAtom, "strong")
+TS_ATOM(sSubAtom, "sub")
+TS_ATOM(sSupAtom, "sup")
+TS_ATOM(sTtAtom, "tt")
+TS_ATOM(sUAtom, "u")
+TS_ATOM(sVarAtom, "var")
+TS_ATOM(sWbrAtom, "wbr")
diff --git a/editor/txtsvc/nsTextServicesCID.h b/editor/txtsvc/nsTextServicesCID.h
new file mode 100644
index 000000000..62b282134
--- /dev/null
+++ b/editor/txtsvc/nsTextServicesCID.h
@@ -0,0 +1,19 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsTextServicesCID_h__
+#define nsTextServicesCID_h__
+
+#define NS_TEXTSERVICESDOCUMENTINTERNAL_CID \
+{ /* 019718E2-CDB5-11d2-8D3C-000000000000 */ \
+0x019718e2, 0xcdb5, 0x11d2, \
+{ 0x8d, 0x3c, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 } }
+
+#define NS_TEXTSERVICESDOCUMENT_CID \
+{ /* 019718E3-CDB5-11d2-8D3C-000000000000 */ \
+0x019718e3, 0xcdb5, 0x11d2, \
+{ 0x8d, 0x3c, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 } }
+
+#endif /* nsTextServicesCID_h__ */
diff --git a/editor/txtsvc/nsTextServicesDocument.cpp b/editor/txtsvc/nsTextServicesDocument.cpp
new file mode 100644
index 000000000..e0c779683
--- /dev/null
+++ b/editor/txtsvc/nsTextServicesDocument.cpp
@@ -0,0 +1,3499 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <stddef.h> // for nullptr
+
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
+#include "mozilla/dom/Selection.h"
+#include "mozilla/mozalloc.h" // for operator new, etc
+#include "nsAString.h" // for nsAString_internal::Length, etc
+#include "nsContentUtils.h" // for nsContentUtils
+#include "nsDebug.h" // for NS_ENSURE_TRUE, etc
+#include "nsDependentSubstring.h" // for Substring
+#include "nsError.h" // for NS_OK, NS_ERROR_FAILURE, etc
+#include "nsFilteredContentIterator.h" // for nsFilteredContentIterator
+#include "nsIContent.h" // for nsIContent, etc
+#include "nsIContentIterator.h" // for nsIContentIterator
+#include "nsID.h" // for NS_GET_IID
+#include "nsIDOMDocument.h" // for nsIDOMDocument
+#include "nsIDOMElement.h" // for nsIDOMElement
+#include "nsIDOMHTMLDocument.h" // for nsIDOMHTMLDocument
+#include "nsIDOMHTMLElement.h" // for nsIDOMHTMLElement
+#include "nsIDOMNode.h" // for nsIDOMNode, etc
+#include "nsIDOMRange.h" // for nsIDOMRange, etc
+#include "nsIEditor.h" // for nsIEditor, etc
+#include "nsINode.h" // for nsINode
+#include "nsIPlaintextEditor.h" // for nsIPlaintextEditor
+#include "nsISelection.h" // for nsISelection
+#include "nsISelectionController.h" // for nsISelectionController, etc
+#include "nsISupportsBase.h" // for nsISupports
+#include "nsISupportsUtils.h" // for NS_IF_ADDREF, NS_ADDREF, etc
+#include "nsITextServicesFilter.h" // for nsITextServicesFilter
+#include "nsIWordBreaker.h" // for nsWordRange, nsIWordBreaker
+#include "nsRange.h" // for nsRange
+#include "nsStaticAtom.h" // for NS_STATIC_ATOM, etc
+#include "nsString.h" // for nsString, nsAutoString
+#include "nsTextServicesDocument.h"
+#include "nscore.h" // for nsresult, NS_IMETHODIMP, etc
+
+#define LOCK_DOC(doc)
+#define UNLOCK_DOC(doc)
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+class OffsetEntry
+{
+public:
+ OffsetEntry(nsIDOMNode *aNode, int32_t aOffset, int32_t aLength)
+ : mNode(aNode), mNodeOffset(0), mStrOffset(aOffset), mLength(aLength),
+ mIsInsertedText(false), mIsValid(true)
+ {
+ if (mStrOffset < 1) {
+ mStrOffset = 0;
+ }
+ if (mLength < 1) {
+ mLength = 0;
+ }
+ }
+
+ virtual ~OffsetEntry()
+ {
+ mNode = 0;
+ mNodeOffset = 0;
+ mStrOffset = 0;
+ mLength = 0;
+ mIsValid = false;
+ }
+
+ nsIDOMNode *mNode;
+ int32_t mNodeOffset;
+ int32_t mStrOffset;
+ int32_t mLength;
+ bool mIsInsertedText;
+ bool mIsValid;
+};
+
+#define TS_ATOM(name_, value_) nsIAtom* nsTextServicesDocument::name_ = 0;
+#include "nsTSAtomList.h" // IWYU pragma: keep
+#undef TS_ATOM
+
+nsTextServicesDocument::nsTextServicesDocument()
+{
+ mSelStartIndex = -1;
+ mSelStartOffset = -1;
+ mSelEndIndex = -1;
+ mSelEndOffset = -1;
+
+ mIteratorStatus = eIsDone;
+}
+
+nsTextServicesDocument::~nsTextServicesDocument()
+{
+ ClearOffsetTable(&mOffsetTable);
+}
+
+#define TS_ATOM(name_, value_) NS_STATIC_ATOM_BUFFER(name_##_buffer, value_)
+#include "nsTSAtomList.h" // IWYU pragma: keep
+#undef TS_ATOM
+
+/* static */
+void
+nsTextServicesDocument::RegisterAtoms()
+{
+ static const nsStaticAtom ts_atoms[] = {
+#define TS_ATOM(name_, value_) NS_STATIC_ATOM(name_##_buffer, &name_),
+#include "nsTSAtomList.h" // IWYU pragma: keep
+#undef TS_ATOM
+ };
+
+ NS_RegisterStaticAtoms(ts_atoms);
+}
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(nsTextServicesDocument)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(nsTextServicesDocument)
+
+NS_INTERFACE_MAP_BEGIN(nsTextServicesDocument)
+ NS_INTERFACE_MAP_ENTRY(nsITextServicesDocument)
+ NS_INTERFACE_MAP_ENTRY(nsIEditActionListener)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsITextServicesDocument)
+ NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(nsTextServicesDocument)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION(nsTextServicesDocument,
+ mDOMDocument,
+ mSelCon,
+ mIterator,
+ mPrevTextBlock,
+ mNextTextBlock,
+ mExtent,
+ mTxtSvcFilter)
+
+NS_IMETHODIMP
+nsTextServicesDocument::InitWithEditor(nsIEditor *aEditor)
+{
+ nsCOMPtr<nsISelectionController> selCon;
+ nsCOMPtr<nsIDOMDocument> doc;
+
+ NS_ENSURE_TRUE(aEditor, NS_ERROR_NULL_POINTER);
+
+ LOCK_DOC(this);
+
+ // Check to see if we already have an mSelCon. If we do, it
+ // better be the same one the editor uses!
+
+ nsresult rv = aEditor->GetSelectionController(getter_AddRefs(selCon));
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ if (!selCon || (mSelCon && selCon != mSelCon)) {
+ UNLOCK_DOC(this);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mSelCon) {
+ mSelCon = selCon;
+ }
+
+ // Check to see if we already have an mDOMDocument. If we do, it
+ // better be the same one the editor uses!
+
+ rv = aEditor->GetDocument(getter_AddRefs(doc));
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ if (!doc || (mDOMDocument && doc != mDOMDocument)) {
+ UNLOCK_DOC(this);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mDOMDocument) {
+ mDOMDocument = doc;
+
+ rv = CreateDocumentContentIterator(getter_AddRefs(mIterator));
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ mIteratorStatus = nsTextServicesDocument::eIsDone;
+
+ rv = FirstBlock();
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+ }
+
+ mEditor = do_GetWeakReference(aEditor);
+
+ rv = aEditor->AddEditActionListener(this);
+
+ UNLOCK_DOC(this);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsTextServicesDocument::GetDocument(nsIDOMDocument **aDoc)
+{
+ NS_ENSURE_TRUE(aDoc, NS_ERROR_NULL_POINTER);
+
+ *aDoc = nullptr; // init out param
+ NS_ENSURE_TRUE(mDOMDocument, NS_ERROR_NOT_INITIALIZED);
+
+ *aDoc = mDOMDocument;
+ NS_ADDREF(*aDoc);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTextServicesDocument::SetExtent(nsIDOMRange* aDOMRange)
+{
+ NS_ENSURE_ARG_POINTER(aDOMRange);
+ NS_ENSURE_TRUE(mDOMDocument, NS_ERROR_FAILURE);
+
+ LOCK_DOC(this);
+
+ // We need to store a copy of aDOMRange since we don't
+ // know where it came from.
+
+ mExtent = static_cast<nsRange*>(aDOMRange)->CloneRange();
+
+ // Create a new iterator based on our new extent range.
+
+ nsresult rv = CreateContentIterator(mExtent, getter_AddRefs(mIterator));
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ // Now position the iterator at the start of the first block
+ // in the range.
+
+ mIteratorStatus = nsTextServicesDocument::eIsDone;
+
+ rv = FirstBlock();
+
+ UNLOCK_DOC(this);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsTextServicesDocument::ExpandRangeToWordBoundaries(nsIDOMRange *aRange)
+{
+ NS_ENSURE_ARG_POINTER(aRange);
+ RefPtr<nsRange> range = static_cast<nsRange*>(aRange);
+
+ // Get the end points of the range.
+
+ nsCOMPtr<nsIDOMNode> rngStartNode, rngEndNode;
+ int32_t rngStartOffset, rngEndOffset;
+
+ nsresult rv = GetRangeEndPoints(range, getter_AddRefs(rngStartNode),
+ &rngStartOffset,
+ getter_AddRefs(rngEndNode),
+ &rngEndOffset);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Create a content iterator based on the range.
+
+ nsCOMPtr<nsIContentIterator> iter;
+ rv = CreateContentIterator(range, getter_AddRefs(iter));
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Find the first text node in the range.
+
+ TSDIteratorStatus iterStatus;
+
+ rv = FirstTextNode(iter, &iterStatus);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (iterStatus == nsTextServicesDocument::eIsDone) {
+ // No text was found so there's no adjustment necessary!
+ return NS_OK;
+ }
+
+ nsINode *firstText = iter->GetCurrentNode();
+ NS_ENSURE_TRUE(firstText, NS_ERROR_FAILURE);
+
+ // Find the last text node in the range.
+
+ rv = LastTextNode(iter, &iterStatus);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (iterStatus == nsTextServicesDocument::eIsDone) {
+ // We should never get here because a first text block
+ // was found above.
+ NS_ASSERTION(false, "Found a first without a last!");
+ return NS_ERROR_FAILURE;
+ }
+
+ nsINode *lastText = iter->GetCurrentNode();
+ NS_ENSURE_TRUE(lastText, NS_ERROR_FAILURE);
+
+ // Now make sure our end points are in terms of text nodes in the range!
+
+ nsCOMPtr<nsIDOMNode> firstTextNode = do_QueryInterface(firstText);
+ NS_ENSURE_TRUE(firstTextNode, NS_ERROR_FAILURE);
+
+ if (rngStartNode != firstTextNode) {
+ // The range includes the start of the first text node!
+ rngStartNode = firstTextNode;
+ rngStartOffset = 0;
+ }
+
+ nsCOMPtr<nsIDOMNode> lastTextNode = do_QueryInterface(lastText);
+ NS_ENSURE_TRUE(lastTextNode, NS_ERROR_FAILURE);
+
+ if (rngEndNode != lastTextNode) {
+ // The range includes the end of the last text node!
+ rngEndNode = lastTextNode;
+ nsAutoString str;
+ lastTextNode->GetNodeValue(str);
+ rngEndOffset = str.Length();
+ }
+
+ // Create a doc iterator so that we can scan beyond
+ // the bounds of the extent range.
+
+ nsCOMPtr<nsIContentIterator> docIter;
+ rv = CreateDocumentContentIterator(getter_AddRefs(docIter));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Grab all the text in the block containing our
+ // first text node.
+
+ rv = docIter->PositionAt(firstText);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ iterStatus = nsTextServicesDocument::eValid;
+
+ nsTArray<OffsetEntry*> offsetTable;
+ nsAutoString blockStr;
+
+ rv = CreateOffsetTable(&offsetTable, docIter, &iterStatus,
+ nullptr, &blockStr);
+ if (NS_FAILED(rv)) {
+ ClearOffsetTable(&offsetTable);
+ return rv;
+ }
+
+ nsCOMPtr<nsIDOMNode> wordStartNode, wordEndNode;
+ int32_t wordStartOffset, wordEndOffset;
+
+ rv = FindWordBounds(&offsetTable, &blockStr,
+ rngStartNode, rngStartOffset,
+ getter_AddRefs(wordStartNode), &wordStartOffset,
+ getter_AddRefs(wordEndNode), &wordEndOffset);
+
+ ClearOffsetTable(&offsetTable);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rngStartNode = wordStartNode;
+ rngStartOffset = wordStartOffset;
+
+ // Grab all the text in the block containing our
+ // last text node.
+
+ rv = docIter->PositionAt(lastText);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ iterStatus = nsTextServicesDocument::eValid;
+
+ rv = CreateOffsetTable(&offsetTable, docIter, &iterStatus,
+ nullptr, &blockStr);
+ if (NS_FAILED(rv)) {
+ ClearOffsetTable(&offsetTable);
+ return rv;
+ }
+
+ rv = FindWordBounds(&offsetTable, &blockStr,
+ rngEndNode, rngEndOffset,
+ getter_AddRefs(wordStartNode), &wordStartOffset,
+ getter_AddRefs(wordEndNode), &wordEndOffset);
+
+ ClearOffsetTable(&offsetTable);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // To prevent expanding the range too much, we only change
+ // rngEndNode and rngEndOffset if it isn't already at the start of the
+ // word and isn't equivalent to rngStartNode and rngStartOffset.
+
+ if (rngEndNode != wordStartNode ||
+ rngEndOffset != wordStartOffset ||
+ (rngEndNode == rngStartNode && rngEndOffset == rngStartOffset)) {
+ rngEndNode = wordEndNode;
+ rngEndOffset = wordEndOffset;
+ }
+
+ // Now adjust the range so that it uses our new
+ // end points.
+
+ rv = range->SetEnd(rngEndNode, rngEndOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return range->SetStart(rngStartNode, rngStartOffset);
+}
+
+NS_IMETHODIMP
+nsTextServicesDocument::SetFilter(nsITextServicesFilter *aFilter)
+{
+ // Hang on to the filter so we can set it into the filtered iterator.
+ mTxtSvcFilter = aFilter;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTextServicesDocument::GetCurrentTextBlock(nsString *aStr)
+{
+ NS_ENSURE_TRUE(aStr, NS_ERROR_NULL_POINTER);
+
+ aStr->Truncate();
+
+ NS_ENSURE_TRUE(mIterator, NS_ERROR_FAILURE);
+
+ LOCK_DOC(this);
+
+ nsresult rv = CreateOffsetTable(&mOffsetTable, mIterator, &mIteratorStatus,
+ mExtent, aStr);
+
+ UNLOCK_DOC(this);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsTextServicesDocument::FirstBlock()
+{
+ NS_ENSURE_TRUE(mIterator, NS_ERROR_FAILURE);
+
+ LOCK_DOC(this);
+
+ nsresult rv = FirstTextNode(mIterator, &mIteratorStatus);
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ // Keep track of prev and next blocks, just in case
+ // the text service blows away the current block.
+
+ if (mIteratorStatus == nsTextServicesDocument::eValid) {
+ mPrevTextBlock = nullptr;
+ rv = GetFirstTextNodeInNextBlock(getter_AddRefs(mNextTextBlock));
+ } else {
+ // There's no text block in the document!
+
+ mPrevTextBlock = nullptr;
+ mNextTextBlock = nullptr;
+ }
+
+ UNLOCK_DOC(this);
+
+ // XXX Result of FirstTextNode() or GetFirstTextNodeInNextBlock().
+ return rv;
+}
+
+NS_IMETHODIMP
+nsTextServicesDocument::LastSelectedBlock(TSDBlockSelectionStatus *aSelStatus,
+ int32_t *aSelOffset,
+ int32_t *aSelLength)
+{
+ NS_ENSURE_TRUE(aSelStatus && aSelOffset && aSelLength, NS_ERROR_NULL_POINTER);
+
+ LOCK_DOC(this);
+
+ mIteratorStatus = nsTextServicesDocument::eIsDone;
+
+ *aSelStatus = nsITextServicesDocument::eBlockNotFound;
+ *aSelOffset = *aSelLength = -1;
+
+ if (!mSelCon || !mIterator) {
+ UNLOCK_DOC(this);
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsISelection> domSelection;
+ nsresult rv = mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL,
+ getter_AddRefs(domSelection));
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ RefPtr<Selection> selection = domSelection->AsSelection();
+
+ bool isCollapsed = selection->IsCollapsed();
+
+ nsCOMPtr<nsIContentIterator> iter;
+ RefPtr<nsRange> range;
+ nsCOMPtr<nsIDOMNode> parent;
+ int32_t rangeCount, offset;
+
+ if (isCollapsed) {
+ // We have a caret. Check if the caret is in a text node.
+ // If it is, make the text node's block the current block.
+ // If the caret isn't in a text node, search forwards in
+ // the document, till we find a text node.
+
+ range = selection->GetRangeAt(0);
+
+ if (!range) {
+ UNLOCK_DOC(this);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = range->GetStartContainer(getter_AddRefs(parent));
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ if (!parent) {
+ UNLOCK_DOC(this);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = range->GetStartOffset(&offset);
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ if (IsTextNode(parent)) {
+ // The caret is in a text node. Find the beginning
+ // of the text block containing this text node and
+ // return.
+
+ nsCOMPtr<nsIContent> content(do_QueryInterface(parent));
+
+ if (!content) {
+ UNLOCK_DOC(this);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mIterator->PositionAt(content);
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ rv = FirstTextNodeInCurrentBlock(mIterator);
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ mIteratorStatus = nsTextServicesDocument::eValid;
+
+ rv = CreateOffsetTable(&mOffsetTable, mIterator, &mIteratorStatus,
+ mExtent, nullptr);
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ rv = GetSelection(aSelStatus, aSelOffset, aSelLength);
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ if (*aSelStatus == nsITextServicesDocument::eBlockContains) {
+ rv = SetSelectionInternal(*aSelOffset, *aSelLength, false);
+ }
+ } else {
+ // The caret isn't in a text node. Create an iterator
+ // based on a range that extends from the current caret
+ // position to the end of the document, then walk forwards
+ // till you find a text node, then find the beginning of it's block.
+
+ rv = CreateDocumentContentRootToNodeOffsetRange(parent, offset, false,
+ getter_AddRefs(range));
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ rv = range->GetCollapsed(&isCollapsed);
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ if (isCollapsed) {
+ // If we get here, the range is collapsed because there is nothing after
+ // the caret! Just return NS_OK;
+
+ UNLOCK_DOC(this);
+ return NS_OK;
+ }
+
+ rv = CreateContentIterator(range, getter_AddRefs(iter));
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ iter->First();
+
+ nsCOMPtr<nsIContent> content;
+ while (!iter->IsDone()) {
+ content = do_QueryInterface(iter->GetCurrentNode());
+
+ if (IsTextNode(content)) {
+ break;
+ }
+
+ content = nullptr;
+
+ iter->Next();
+ }
+
+ if (!content) {
+ UNLOCK_DOC(this);
+ return NS_OK;
+ }
+
+ rv = mIterator->PositionAt(content);
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ rv = FirstTextNodeInCurrentBlock(mIterator);
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ mIteratorStatus = nsTextServicesDocument::eValid;
+
+ rv = CreateOffsetTable(&mOffsetTable, mIterator, &mIteratorStatus,
+ mExtent, nullptr);
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ rv = GetSelection(aSelStatus, aSelOffset, aSelLength);
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+ }
+
+ UNLOCK_DOC(this);
+
+ // Result of SetSelectionInternal() in the |if| block or NS_OK.
+ return rv;
+ }
+
+ // If we get here, we have an uncollapsed selection!
+ // Look backwards through each range in the selection till you
+ // find the first text node. If you find one, find the
+ // beginning of its text block, and make it the current
+ // block.
+
+ rv = selection->GetRangeCount(&rangeCount);
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ NS_ASSERTION(rangeCount > 0, "Unexpected range count!");
+
+ if (rangeCount <= 0) {
+ UNLOCK_DOC(this);
+ return NS_OK;
+ }
+
+ // XXX: We may need to add some code here to make sure
+ // the ranges are sorted in document appearance order!
+
+ for (int32_t i = rangeCount - 1; i >= 0; i--) {
+ // Get the i'th range from the selection.
+
+ range = selection->GetRangeAt(i);
+
+ if (!range) {
+ UNLOCK_DOC(this);
+ return NS_OK; // XXX Really?
+ }
+
+ // Create an iterator for the range.
+
+ rv = CreateContentIterator(range, getter_AddRefs(iter));
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ iter->Last();
+
+ // Now walk through the range till we find a text node.
+
+ while (!iter->IsDone()) {
+ if (iter->GetCurrentNode()->NodeType() == nsIDOMNode::TEXT_NODE) {
+ // We found a text node, so position the document's
+ // iterator at the beginning of the block, then get
+ // the selection in terms of the string offset.
+ nsCOMPtr<nsIContent> content = iter->GetCurrentNode()->AsContent();
+
+ rv = mIterator->PositionAt(content);
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ rv = FirstTextNodeInCurrentBlock(mIterator);
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ mIteratorStatus = nsTextServicesDocument::eValid;
+
+ rv = CreateOffsetTable(&mOffsetTable, mIterator, &mIteratorStatus,
+ mExtent, nullptr);
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ rv = GetSelection(aSelStatus, aSelOffset, aSelLength);
+
+ UNLOCK_DOC(this);
+
+ return rv;
+
+ }
+
+ iter->Prev();
+ }
+ }
+
+ // If we get here, we didn't find any text node in the selection!
+ // Create a range that extends from the end of the selection,
+ // to the end of the document, then iterate forwards through
+ // it till you find a text node!
+
+ range = selection->GetRangeAt(rangeCount - 1);
+
+ if (!range) {
+ UNLOCK_DOC(this);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = range->GetEndContainer(getter_AddRefs(parent));
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ if (!parent) {
+ UNLOCK_DOC(this);
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = range->GetEndOffset(&offset);
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ rv = CreateDocumentContentRootToNodeOffsetRange(parent, offset, false,
+ getter_AddRefs(range));
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ rv = range->GetCollapsed(&isCollapsed);
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ if (isCollapsed) {
+ // If we get here, the range is collapsed because there is nothing after
+ // the current selection! Just return NS_OK;
+
+ UNLOCK_DOC(this);
+ return NS_OK;
+ }
+
+ rv = CreateContentIterator(range, getter_AddRefs(iter));
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ iter->First();
+
+ while (!iter->IsDone()) {
+ if (iter->GetCurrentNode()->NodeType() == nsIDOMNode::TEXT_NODE) {
+ // We found a text node! Adjust the document's iterator to point
+ // to the beginning of its text block, then get the current selection.
+ nsCOMPtr<nsIContent> content = iter->GetCurrentNode()->AsContent();
+
+ rv = mIterator->PositionAt(content);
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ rv = FirstTextNodeInCurrentBlock(mIterator);
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+
+ mIteratorStatus = nsTextServicesDocument::eValid;
+
+ rv = CreateOffsetTable(&mOffsetTable, mIterator, &mIteratorStatus,
+ mExtent, nullptr);
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ rv = GetSelection(aSelStatus, aSelOffset, aSelLength);
+
+ UNLOCK_DOC(this);
+
+ return rv;
+ }
+
+ iter->Next();
+ }
+
+ // If we get here, we didn't find any block before or inside
+ // the selection! Just return OK.
+
+ UNLOCK_DOC(this);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTextServicesDocument::PrevBlock()
+{
+ NS_ENSURE_TRUE(mIterator, NS_ERROR_FAILURE);
+
+ LOCK_DOC(this);
+
+ if (mIteratorStatus == nsTextServicesDocument::eIsDone) {
+ return NS_OK;
+ }
+
+ switch (mIteratorStatus) {
+ case nsTextServicesDocument::eValid:
+ case nsTextServicesDocument::eNext: {
+
+ nsresult rv = FirstTextNodeInPrevBlock(mIterator);
+
+ if (NS_FAILED(rv)) {
+ mIteratorStatus = nsTextServicesDocument::eIsDone;
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ if (mIterator->IsDone()) {
+ mIteratorStatus = nsTextServicesDocument::eIsDone;
+ UNLOCK_DOC(this);
+ return NS_OK;
+ }
+
+ mIteratorStatus = nsTextServicesDocument::eValid;
+ break;
+ }
+ case nsTextServicesDocument::ePrev:
+
+ // The iterator already points to the previous
+ // block, so don't do anything.
+
+ mIteratorStatus = nsTextServicesDocument::eValid;
+ break;
+
+ default:
+
+ mIteratorStatus = nsTextServicesDocument::eIsDone;
+ break;
+ }
+
+ // Keep track of prev and next blocks, just in case
+ // the text service blows away the current block.
+ nsresult rv = NS_OK;
+ if (mIteratorStatus == nsTextServicesDocument::eValid) {
+ GetFirstTextNodeInPrevBlock(getter_AddRefs(mPrevTextBlock));
+ rv = GetFirstTextNodeInNextBlock(getter_AddRefs(mNextTextBlock));
+ } else {
+ // We must be done!
+ mPrevTextBlock = nullptr;
+ mNextTextBlock = nullptr;
+ }
+
+ UNLOCK_DOC(this);
+
+ // XXX The result of GetFirstTextNodeInNextBlock() or NS_OK.
+ return rv;
+}
+
+NS_IMETHODIMP
+nsTextServicesDocument::NextBlock()
+{
+ NS_ENSURE_TRUE(mIterator, NS_ERROR_FAILURE);
+
+ LOCK_DOC(this);
+
+ if (mIteratorStatus == nsTextServicesDocument::eIsDone) {
+ return NS_OK;
+ }
+
+ switch (mIteratorStatus) {
+ case nsTextServicesDocument::eValid: {
+
+ // Advance the iterator to the next text block.
+
+ nsresult rv = FirstTextNodeInNextBlock(mIterator);
+
+ if (NS_FAILED(rv)) {
+ mIteratorStatus = nsTextServicesDocument::eIsDone;
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ if (mIterator->IsDone()) {
+ mIteratorStatus = nsTextServicesDocument::eIsDone;
+ UNLOCK_DOC(this);
+ return NS_OK;
+ }
+
+ mIteratorStatus = nsTextServicesDocument::eValid;
+ break;
+ }
+ case nsTextServicesDocument::eNext:
+
+ // The iterator already points to the next block,
+ // so don't do anything to it!
+
+ mIteratorStatus = nsTextServicesDocument::eValid;
+ break;
+
+ case nsTextServicesDocument::ePrev:
+
+ // If the iterator is pointing to the previous block,
+ // we know that there is no next text block! Just
+ // fall through to the default case!
+
+ default:
+
+ mIteratorStatus = nsTextServicesDocument::eIsDone;
+ break;
+ }
+
+ // Keep track of prev and next blocks, just in case
+ // the text service blows away the current block.
+ nsresult rv = NS_OK;
+ if (mIteratorStatus == nsTextServicesDocument::eValid) {
+ GetFirstTextNodeInPrevBlock(getter_AddRefs(mPrevTextBlock));
+ rv = GetFirstTextNodeInNextBlock(getter_AddRefs(mNextTextBlock));
+ } else {
+ // We must be done.
+ mPrevTextBlock = nullptr;
+ mNextTextBlock = nullptr;
+ }
+
+ UNLOCK_DOC(this);
+
+ // The result of GetFirstTextNodeInNextBlock() or NS_OK.
+ return rv;
+}
+
+NS_IMETHODIMP
+nsTextServicesDocument::IsDone(bool *aIsDone)
+{
+ NS_ENSURE_TRUE(aIsDone, NS_ERROR_NULL_POINTER);
+
+ *aIsDone = false;
+
+ NS_ENSURE_TRUE(mIterator, NS_ERROR_FAILURE);
+
+ LOCK_DOC(this);
+
+ *aIsDone = (mIteratorStatus == nsTextServicesDocument::eIsDone) ? true : false;
+
+ UNLOCK_DOC(this);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTextServicesDocument::SetSelection(int32_t aOffset, int32_t aLength)
+{
+ NS_ENSURE_TRUE(mSelCon && aOffset >= 0 && aLength >= 0, NS_ERROR_FAILURE);
+
+ LOCK_DOC(this);
+
+ nsresult rv = SetSelectionInternal(aOffset, aLength, true);
+
+ UNLOCK_DOC(this);
+
+ //**** KDEBUG ****
+ // printf("\n * Sel: (%2d, %4d) (%2d, %4d)\n", mSelStartIndex, mSelStartOffset, mSelEndIndex, mSelEndOffset);
+ //**** KDEBUG ****
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsTextServicesDocument::ScrollSelectionIntoView()
+{
+ NS_ENSURE_TRUE(mSelCon, NS_ERROR_FAILURE);
+
+ LOCK_DOC(this);
+
+ // After ScrollSelectionIntoView(), the pending notifications might be flushed
+ // and PresShell/PresContext/Frames may be dead. See bug 418470.
+ nsresult rv =
+ mSelCon->ScrollSelectionIntoView(
+ nsISelectionController::SELECTION_NORMAL,
+ nsISelectionController::SELECTION_FOCUS_REGION,
+ nsISelectionController::SCROLL_SYNCHRONOUS);
+
+ UNLOCK_DOC(this);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsTextServicesDocument::DeleteSelection()
+{
+ // We don't allow deletion during a collapsed selection!
+ nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
+ NS_ASSERTION(editor, "DeleteSelection called without an editor present!");
+ NS_ASSERTION(SelectionIsValid(), "DeleteSelection called without a valid selection!");
+
+ if (!editor || !SelectionIsValid()) {
+ return NS_ERROR_FAILURE;
+ }
+ if (SelectionIsCollapsed()) {
+ return NS_OK;
+ }
+
+ LOCK_DOC(this);
+
+ //**** KDEBUG ****
+ // printf("\n---- Before Delete\n");
+ // printf("Sel: (%2d, %4d) (%2d, %4d)\n", mSelStartIndex, mSelStartOffset, mSelEndIndex, mSelEndOffset);
+ // PrintOffsetTable();
+ //**** KDEBUG ****
+
+ // If we have an mExtent, save off its current set of
+ // end points so we can compare them against mExtent's
+ // set after the deletion of the content.
+
+ nsCOMPtr<nsIDOMNode> origStartNode, origEndNode;
+ int32_t origStartOffset = 0, origEndOffset = 0;
+
+ if (mExtent) {
+ nsresult rv =
+ GetRangeEndPoints(mExtent,
+ getter_AddRefs(origStartNode), &origStartOffset,
+ getter_AddRefs(origEndNode), &origEndOffset);
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+ }
+
+ int32_t selLength;
+ OffsetEntry *entry, *newEntry;
+
+ for (int32_t i = mSelStartIndex; i <= mSelEndIndex; i++) {
+ entry = mOffsetTable[i];
+
+ if (i == mSelStartIndex) {
+ // Calculate the length of the selection. Note that the
+ // selection length can be zero if the start of the selection
+ // is at the very end of a text node entry.
+
+ if (entry->mIsInsertedText) {
+ // Inserted text offset entries have no width when
+ // talking in terms of string offsets! If the beginning
+ // of the selection is in an inserted text offset entry,
+ // the caret is always at the end of the entry!
+
+ selLength = 0;
+ } else {
+ selLength = entry->mLength - (mSelStartOffset - entry->mStrOffset);
+ }
+
+ if (selLength > 0 && mSelStartOffset > entry->mStrOffset) {
+ // Selection doesn't start at the beginning of the
+ // text node entry. We need to split this entry into
+ // two pieces, the piece before the selection, and
+ // the piece inside the selection.
+
+ nsresult rv = SplitOffsetEntry(i, selLength);
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ // Adjust selection indexes to account for new entry:
+
+ ++mSelStartIndex;
+ ++mSelEndIndex;
+ ++i;
+
+ entry = mOffsetTable[i];
+ }
+
+
+ if (selLength > 0 && mSelStartIndex < mSelEndIndex) {
+ // The entire entry is contained in the selection. Mark the
+ // entry invalid.
+ entry->mIsValid = false;
+ }
+ }
+
+ //**** KDEBUG ****
+ // printf("\n---- Middle Delete\n");
+ // printf("Sel: (%2d, %4d) (%2d, %4d)\n", mSelStartIndex, mSelStartOffset, mSelEndIndex, mSelEndOffset);
+ // PrintOffsetTable();
+ //**** KDEBUG ****
+
+ if (i == mSelEndIndex) {
+ if (entry->mIsInsertedText) {
+ // Inserted text offset entries have no width when
+ // talking in terms of string offsets! If the end
+ // of the selection is in an inserted text offset entry,
+ // the selection includes the entire entry!
+
+ entry->mIsValid = false;
+ } else {
+ // Calculate the length of the selection. Note that the
+ // selection length can be zero if the end of the selection
+ // is at the very beginning of a text node entry.
+
+ selLength = mSelEndOffset - entry->mStrOffset;
+
+ if (selLength > 0 &&
+ mSelEndOffset < entry->mStrOffset + entry->mLength) {
+ // mStrOffset is guaranteed to be inside the selection, even
+ // when mSelStartIndex == mSelEndIndex.
+
+ nsresult rv = SplitOffsetEntry(i, entry->mLength - selLength);
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ // Update the entry fields:
+
+ newEntry = mOffsetTable[i+1];
+ newEntry->mNodeOffset = entry->mNodeOffset;
+ }
+
+
+ if (selLength > 0 &&
+ mSelEndOffset == entry->mStrOffset + entry->mLength) {
+ // The entire entry is contained in the selection. Mark the
+ // entry invalid.
+ entry->mIsValid = false;
+ }
+ }
+ }
+
+ if (i != mSelStartIndex && i != mSelEndIndex) {
+ // The entire entry is contained in the selection. Mark the
+ // entry invalid.
+ entry->mIsValid = false;
+ }
+ }
+
+ // Make sure mIterator always points to something valid!
+
+ AdjustContentIterator();
+
+ // Now delete the actual content!
+
+ nsresult rv =
+ editor->DeleteSelection(nsIEditor::ePrevious, nsIEditor::eStrip);
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ // Now that we've actually deleted the selected content,
+ // check to see if our mExtent has changed, if so, then
+ // we have to create a new content iterator!
+
+ if (origStartNode && origEndNode) {
+ nsCOMPtr<nsIDOMNode> curStartNode, curEndNode;
+ int32_t curStartOffset = 0, curEndOffset = 0;
+
+ rv = GetRangeEndPoints(mExtent,
+ getter_AddRefs(curStartNode), &curStartOffset,
+ getter_AddRefs(curEndNode), &curEndOffset);
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ if (origStartNode != curStartNode || origEndNode != curEndNode) {
+ // The range has changed, so we need to create a new content
+ // iterator based on the new range.
+
+ nsCOMPtr<nsIContent> curContent;
+
+ if (mIteratorStatus != nsTextServicesDocument::eIsDone) {
+ // The old iterator is still pointing to something valid,
+ // so get its current node so we can restore it after we
+ // create the new iterator!
+
+ curContent = mIterator->GetCurrentNode()
+ ? mIterator->GetCurrentNode()->AsContent()
+ : nullptr;
+ }
+
+ // Create the new iterator.
+
+ rv = CreateContentIterator(mExtent, getter_AddRefs(mIterator));
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ // Now make the new iterator point to the content node
+ // the old one was pointing at.
+
+ if (curContent) {
+ rv = mIterator->PositionAt(curContent);
+
+ if (NS_FAILED(rv)) {
+ mIteratorStatus = eIsDone;
+ } else {
+ mIteratorStatus = eValid;
+ }
+ }
+ }
+ }
+
+ entry = 0;
+
+ // Move the caret to the end of the first valid entry.
+ // Start with mSelStartIndex since it may still be valid.
+
+ for (int32_t i = mSelStartIndex; !entry && i >= 0; i--) {
+ entry = mOffsetTable[i];
+
+ if (!entry->mIsValid) {
+ entry = 0;
+ } else {
+ mSelStartIndex = mSelEndIndex = i;
+ mSelStartOffset = mSelEndOffset = entry->mStrOffset + entry->mLength;
+ }
+ }
+
+ // If we still don't have a valid entry, move the caret
+ // to the next valid entry after the selection:
+
+ for (int32_t i = mSelEndIndex;
+ !entry && i < static_cast<int32_t>(mOffsetTable.Length()); i++) {
+ entry = mOffsetTable[i];
+
+ if (!entry->mIsValid) {
+ entry = 0;
+ } else {
+ mSelStartIndex = mSelEndIndex = i;
+ mSelStartOffset = mSelEndOffset = entry->mStrOffset;
+ }
+ }
+
+ if (entry) {
+ SetSelection(mSelStartOffset, 0);
+ } else {
+ // Uuughh we have no valid offset entry to place our
+ // caret ... just mark the selection invalid.
+ mSelStartIndex = mSelEndIndex = -1;
+ mSelStartOffset = mSelEndOffset = -1;
+ }
+
+ // Now remove any invalid entries from the offset table.
+
+ rv = RemoveInvalidOffsetEntries();
+
+ //**** KDEBUG ****
+ // printf("\n---- After Delete\n");
+ // printf("Sel: (%2d, %4d) (%2d, %4d)\n", mSelStartIndex, mSelStartOffset, mSelEndIndex, mSelEndOffset);
+ // PrintOffsetTable();
+ //**** KDEBUG ****
+
+ UNLOCK_DOC(this);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsTextServicesDocument::InsertText(const nsString *aText)
+{
+ nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
+ NS_ASSERTION(editor, "InsertText called without an editor present!");
+
+ if (!editor || !SelectionIsValid()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NS_ENSURE_TRUE(aText, NS_ERROR_NULL_POINTER);
+
+ // If the selection is not collapsed, we need to save
+ // off the selection offsets so we can restore the
+ // selection and delete the selected content after we've
+ // inserted the new text. This is necessary to try and
+ // retain as much of the original style of the content
+ // being deleted.
+
+ bool collapsedSelection = SelectionIsCollapsed();
+ int32_t savedSelOffset = mSelStartOffset;
+ int32_t savedSelLength = mSelEndOffset - mSelStartOffset;
+
+ if (!collapsedSelection) {
+ // Collapse to the start of the current selection
+ // for the insert!
+
+ nsresult rv = SetSelection(mSelStartOffset, 0);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+
+ LOCK_DOC(this);
+
+ nsresult rv = editor->BeginTransaction();
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ nsCOMPtr<nsIPlaintextEditor> textEditor (do_QueryInterface(editor, &rv));
+ if (textEditor) {
+ rv = textEditor->InsertText(*aText);
+ }
+
+ if (NS_FAILED(rv)) {
+ editor->EndTransaction();
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ //**** KDEBUG ****
+ // printf("\n---- Before Insert\n");
+ // printf("Sel: (%2d, %4d) (%2d, %4d)\n", mSelStartIndex, mSelStartOffset, mSelEndIndex, mSelEndOffset);
+ // PrintOffsetTable();
+ //**** KDEBUG ****
+
+ int32_t strLength = aText->Length();
+
+ nsCOMPtr<nsISelection> selection;
+ OffsetEntry *itEntry;
+ OffsetEntry *entry = mOffsetTable[mSelStartIndex];
+ void *node = entry->mNode;
+
+ NS_ASSERTION((entry->mIsValid), "Invalid insertion point!");
+
+ if (entry->mStrOffset == mSelStartOffset) {
+ if (entry->mIsInsertedText) {
+ // If the caret is in an inserted text offset entry,
+ // we simply insert the text at the end of the entry.
+
+ entry->mLength += strLength;
+ } else {
+ // Insert an inserted text offset entry before the current
+ // entry!
+
+ itEntry = new OffsetEntry(entry->mNode, entry->mStrOffset, strLength);
+
+ if (!itEntry) {
+ editor->EndTransaction();
+ UNLOCK_DOC(this);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ itEntry->mIsInsertedText = true;
+ itEntry->mNodeOffset = entry->mNodeOffset;
+
+ if (!mOffsetTable.InsertElementAt(mSelStartIndex, itEntry)) {
+ editor->EndTransaction();
+ UNLOCK_DOC(this);
+ return NS_ERROR_FAILURE;
+ }
+ }
+ } else if (entry->mStrOffset + entry->mLength == mSelStartOffset) {
+ // We are inserting text at the end of the current offset entry.
+ // Look at the next valid entry in the table. If it's an inserted
+ // text entry, add to its length and adjust its node offset. If
+ // it isn't, add a new inserted text entry.
+
+ // XXX Rename this!
+ uint32_t i = mSelStartIndex + 1;
+ itEntry = 0;
+
+ if (mOffsetTable.Length() > i) {
+ itEntry = mOffsetTable[i];
+
+ if (!itEntry) {
+ editor->EndTransaction();
+ UNLOCK_DOC(this);
+ return NS_ERROR_FAILURE;
+ }
+
+ // Check if the entry is a match. If it isn't, set
+ // iEntry to zero.
+
+ if (!itEntry->mIsInsertedText || itEntry->mStrOffset != mSelStartOffset) {
+ itEntry = 0;
+ }
+ }
+
+ if (!itEntry) {
+ // We didn't find an inserted text offset entry, so
+ // create one.
+
+ itEntry = new OffsetEntry(entry->mNode, mSelStartOffset, 0);
+
+ if (!itEntry) {
+ editor->EndTransaction();
+ UNLOCK_DOC(this);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ itEntry->mNodeOffset = entry->mNodeOffset + entry->mLength;
+ itEntry->mIsInsertedText = true;
+
+ if (!mOffsetTable.InsertElementAt(i, itEntry)) {
+ delete itEntry;
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // We have a valid inserted text offset entry. Update its
+ // length, adjust the selection indexes, and make sure the
+ // caret is properly placed!
+
+ itEntry->mLength += strLength;
+
+ mSelStartIndex = mSelEndIndex = i;
+
+ rv = mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL,
+ getter_AddRefs(selection));
+
+ if (NS_FAILED(rv)) {
+ editor->EndTransaction();
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ rv = selection->Collapse(itEntry->mNode,
+ itEntry->mNodeOffset + itEntry->mLength);
+
+ if (NS_FAILED(rv)) {
+ editor->EndTransaction();
+ UNLOCK_DOC(this);
+ return rv;
+ }
+ } else if (entry->mStrOffset + entry->mLength > mSelStartOffset) {
+ // We are inserting text into the middle of the current offset entry.
+ // split the current entry into two parts, then insert an inserted text
+ // entry between them!
+
+ // XXX Rename this!
+ uint32_t i = entry->mLength - (mSelStartOffset - entry->mStrOffset);
+
+ rv = SplitOffsetEntry(mSelStartIndex, i);
+
+ if (NS_FAILED(rv)) {
+ editor->EndTransaction();
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ itEntry = new OffsetEntry(entry->mNode, mSelStartOffset, strLength);
+
+ if (!itEntry) {
+ editor->EndTransaction();
+ UNLOCK_DOC(this);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ itEntry->mIsInsertedText = true;
+ itEntry->mNodeOffset = entry->mNodeOffset + entry->mLength;
+
+ if (!mOffsetTable.InsertElementAt(mSelStartIndex + 1, itEntry)) {
+ editor->EndTransaction();
+ UNLOCK_DOC(this);
+ return NS_ERROR_FAILURE;
+ }
+
+ mSelEndIndex = ++mSelStartIndex;
+ }
+
+ // We've just finished inserting an inserted text offset entry.
+ // update all entries with the same mNode pointer that follow
+ // it in the table!
+
+ for (size_t i = mSelStartIndex + 1; i < mOffsetTable.Length(); i++) {
+ entry = mOffsetTable[i];
+ if (entry->mNode != node) {
+ break;
+ }
+ if (entry->mIsValid) {
+ entry->mNodeOffset += strLength;
+ }
+ }
+
+ //**** KDEBUG ****
+ // printf("\n---- After Insert\n");
+ // printf("Sel: (%2d, %4d) (%2d, %4d)\n", mSelStartIndex, mSelStartOffset, mSelEndIndex, mSelEndOffset);
+ // PrintOffsetTable();
+ //**** KDEBUG ****
+
+ if (!collapsedSelection) {
+ rv = SetSelection(savedSelOffset, savedSelLength);
+
+ if (NS_FAILED(rv)) {
+ editor->EndTransaction();
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ rv = DeleteSelection();
+
+ if (NS_FAILED(rv)) {
+ editor->EndTransaction();
+ UNLOCK_DOC(this);
+ return rv;
+ }
+ }
+
+ rv = editor->EndTransaction();
+
+ UNLOCK_DOC(this);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsTextServicesDocument::DidInsertNode(nsIDOMNode *aNode,
+ nsIDOMNode *aParent,
+ int32_t aPosition,
+ nsresult aResult)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTextServicesDocument::DidDeleteNode(nsIDOMNode *aChild, nsresult aResult)
+{
+ NS_ENSURE_SUCCESS(aResult, NS_OK);
+
+ NS_ENSURE_TRUE(mIterator, NS_ERROR_FAILURE);
+
+ //**** KDEBUG ****
+ // printf("** DeleteNode: 0x%.8x\n", aChild);
+ // fflush(stdout);
+ //**** KDEBUG ****
+
+ LOCK_DOC(this);
+
+ int32_t nodeIndex = 0;
+ bool hasEntry = false;
+ OffsetEntry *entry;
+
+ nsresult rv =
+ NodeHasOffsetEntry(&mOffsetTable, aChild, &hasEntry, &nodeIndex);
+
+ if (NS_FAILED(rv)) {
+ UNLOCK_DOC(this);
+ return rv;
+ }
+
+ if (!hasEntry) {
+ // It's okay if the node isn't in the offset table, the
+ // editor could be cleaning house.
+ UNLOCK_DOC(this);
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIDOMNode> node = do_QueryInterface(mIterator->GetCurrentNode());
+
+ if (node && node == aChild &&
+ mIteratorStatus != nsTextServicesDocument::eIsDone) {
+ // XXX: This should never really happen because
+ // AdjustContentIterator() should have been called prior
+ // to the delete to try and position the iterator on the
+ // next valid text node in the offset table, and if there
+ // wasn't a next, it would've set mIteratorStatus to eIsDone.
+
+ NS_ERROR("DeleteNode called for current iterator node.");
+ }
+
+ int32_t tcount = mOffsetTable.Length();
+
+ while (nodeIndex < tcount) {
+ entry = mOffsetTable[nodeIndex];
+
+ if (!entry) {
+ UNLOCK_DOC(this);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (entry->mNode == aChild) {
+ entry->mIsValid = false;
+ }
+
+ nodeIndex++;
+ }
+
+ UNLOCK_DOC(this);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTextServicesDocument::DidSplitNode(nsIDOMNode *aExistingRightNode,
+ int32_t aOffset,
+ nsIDOMNode *aNewLeftNode,
+ nsresult aResult)
+{
+ //**** KDEBUG ****
+ // printf("** SplitNode: 0x%.8x %d 0x%.8x\n", aExistingRightNode, aOffset, aNewLeftNode);
+ // fflush(stdout);
+ //**** KDEBUG ****
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTextServicesDocument::DidJoinNodes(nsIDOMNode *aLeftNode,
+ nsIDOMNode *aRightNode,
+ nsIDOMNode *aParent,
+ nsresult aResult)
+{
+ NS_ENSURE_SUCCESS(aResult, NS_OK);
+
+ //**** KDEBUG ****
+ // printf("** JoinNodes: 0x%.8x 0x%.8x 0x%.8x\n", aLeftNode, aRightNode, aParent);
+ // fflush(stdout);
+ //**** KDEBUG ****
+
+ // Make sure that both nodes are text nodes -- otherwise we don't care.
+
+ uint16_t type;
+ nsresult rv = aLeftNode->GetNodeType(&type);
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+ if (nsIDOMNode::TEXT_NODE != type) {
+ return NS_OK;
+ }
+
+ rv = aRightNode->GetNodeType(&type);
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+ if (nsIDOMNode::TEXT_NODE != type) {
+ return NS_OK;
+ }
+
+ // Note: The editor merges the contents of the left node into the
+ // contents of the right.
+
+ int32_t leftIndex = 0;
+ int32_t rightIndex = 0;
+ bool leftHasEntry = false;
+ bool rightHasEntry = false;
+
+ rv = NodeHasOffsetEntry(&mOffsetTable, aLeftNode, &leftHasEntry, &leftIndex);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!leftHasEntry) {
+ // It's okay if the node isn't in the offset table, the
+ // editor could be cleaning house.
+ return NS_OK;
+ }
+
+ rv = NodeHasOffsetEntry(&mOffsetTable, aRightNode,
+ &rightHasEntry, &rightIndex);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!rightHasEntry) {
+ // It's okay if the node isn't in the offset table, the
+ // editor could be cleaning house.
+ return NS_OK;
+ }
+
+ NS_ASSERTION(leftIndex < rightIndex, "Indexes out of order.");
+
+ if (leftIndex > rightIndex) {
+ // Don't know how to handle this situation.
+ return NS_ERROR_FAILURE;
+ }
+
+ LOCK_DOC(this);
+
+ OffsetEntry *entry = mOffsetTable[rightIndex];
+ NS_ASSERTION(entry->mNodeOffset == 0, "Unexpected offset value for rightIndex.");
+
+ // Run through the table and change all entries referring to
+ // the left node so that they now refer to the right node:
+
+ nsAutoString str;
+ aLeftNode->GetNodeValue(str);
+ int32_t nodeLength = str.Length();
+
+ for (int32_t i = leftIndex; i < rightIndex; i++) {
+ entry = mOffsetTable[i];
+ if (entry->mNode != aLeftNode) {
+ break;
+ }
+ if (entry->mIsValid) {
+ entry->mNode = aRightNode;
+ }
+ }
+
+ // Run through the table and adjust the node offsets
+ // for all entries referring to the right node.
+
+ for (int32_t i = rightIndex;
+ i < static_cast<int32_t>(mOffsetTable.Length()); i++) {
+ entry = mOffsetTable[i];
+ if (entry->mNode != aRightNode) {
+ break;
+ }
+ if (entry->mIsValid) {
+ entry->mNodeOffset += nodeLength;
+ }
+ }
+
+ // Now check to see if the iterator is pointing to the
+ // left node. If it is, make it point to the right node!
+
+ nsCOMPtr<nsIContent> leftContent = do_QueryInterface(aLeftNode);
+ nsCOMPtr<nsIContent> rightContent = do_QueryInterface(aRightNode);
+
+ if (!leftContent || !rightContent) {
+ UNLOCK_DOC(this);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (mIterator->GetCurrentNode() == leftContent) {
+ mIterator->PositionAt(rightContent);
+ }
+
+ UNLOCK_DOC(this);
+
+ return NS_OK;
+}
+
+nsresult
+nsTextServicesDocument::CreateContentIterator(nsRange* aRange,
+ nsIContentIterator** aIterator)
+{
+ NS_ENSURE_TRUE(aRange && aIterator, NS_ERROR_NULL_POINTER);
+
+ *aIterator = nullptr;
+
+ // Create a nsFilteredContentIterator
+ // This class wraps the ContentIterator in order to give itself a chance
+ // to filter out certain content nodes
+ RefPtr<nsFilteredContentIterator> filter = new nsFilteredContentIterator(mTxtSvcFilter);
+
+ nsresult rv = filter->Init(aRange);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ filter.forget(aIterator);
+ return NS_OK;
+}
+
+nsresult
+nsTextServicesDocument::GetDocumentContentRootNode(nsIDOMNode **aNode)
+{
+ NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER);
+
+ *aNode = 0;
+
+ NS_ENSURE_TRUE(mDOMDocument, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIDOMHTMLDocument> htmlDoc = do_QueryInterface(mDOMDocument);
+
+ if (htmlDoc) {
+ // For HTML documents, the content root node is the body.
+
+ nsCOMPtr<nsIDOMHTMLElement> bodyElement;
+
+ nsresult rv = htmlDoc->GetBody(getter_AddRefs(bodyElement));
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ENSURE_TRUE(bodyElement, NS_ERROR_FAILURE);
+
+ bodyElement.forget(aNode);
+ } else {
+ // For non-HTML documents, the content root node will be the document element.
+
+ nsCOMPtr<nsIDOMElement> docElement;
+
+ nsresult rv = mDOMDocument->GetDocumentElement(getter_AddRefs(docElement));
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ENSURE_TRUE(docElement, NS_ERROR_FAILURE);
+
+ docElement.forget(aNode);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsTextServicesDocument::CreateDocumentContentRange(nsRange** aRange)
+{
+ *aRange = nullptr;
+
+ nsCOMPtr<nsIDOMNode> node;
+ nsresult rv = GetDocumentContentRootNode(getter_AddRefs(node));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsINode> nativeNode = do_QueryInterface(node);
+ NS_ENSURE_STATE(nativeNode);
+
+ RefPtr<nsRange> range = new nsRange(nativeNode);
+
+ rv = range->SelectNodeContents(node);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ range.forget(aRange);
+ return NS_OK;
+}
+
+nsresult
+nsTextServicesDocument::CreateDocumentContentRootToNodeOffsetRange(
+ nsIDOMNode* aParent, int32_t aOffset, bool aToStart, nsRange** aRange)
+{
+ NS_ENSURE_TRUE(aParent && aRange, NS_ERROR_NULL_POINTER);
+
+ *aRange = 0;
+
+ NS_ASSERTION(aOffset >= 0, "Invalid offset!");
+
+ if (aOffset < 0) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIDOMNode> bodyNode;
+ nsresult rv = GetDocumentContentRootNode(getter_AddRefs(bodyNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(bodyNode, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsIDOMNode> startNode;
+ nsCOMPtr<nsIDOMNode> endNode;
+ int32_t startOffset, endOffset;
+
+ if (aToStart) {
+ // The range should begin at the start of the document
+ // and extend up until (aParent, aOffset).
+
+ startNode = bodyNode;
+ startOffset = 0;
+ endNode = aParent;
+ endOffset = aOffset;
+ } else {
+ // The range should begin at (aParent, aOffset) and
+ // extend to the end of the document.
+
+ startNode = aParent;
+ startOffset = aOffset;
+ endNode = bodyNode;
+
+ nsCOMPtr<nsINode> body = do_QueryInterface(bodyNode);
+ endOffset = body ? int32_t(body->GetChildCount()) : 0;
+ }
+
+ return nsRange::CreateRange(startNode, startOffset, endNode, endOffset,
+ aRange);
+}
+
+nsresult
+nsTextServicesDocument::CreateDocumentContentIterator(nsIContentIterator **aIterator)
+{
+ NS_ENSURE_TRUE(aIterator, NS_ERROR_NULL_POINTER);
+
+ RefPtr<nsRange> range;
+
+ nsresult rv = CreateDocumentContentRange(getter_AddRefs(range));
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return CreateContentIterator(range, aIterator);
+}
+
+nsresult
+nsTextServicesDocument::AdjustContentIterator()
+{
+ NS_ENSURE_TRUE(mIterator, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIDOMNode> node(do_QueryInterface(mIterator->GetCurrentNode()));
+
+ NS_ENSURE_TRUE(node, NS_ERROR_FAILURE);
+
+ nsIDOMNode *nodePtr = node.get();
+ int32_t tcount = mOffsetTable.Length();
+
+ nsIDOMNode *prevValidNode = 0;
+ nsIDOMNode *nextValidNode = 0;
+ bool foundEntry = false;
+ OffsetEntry *entry;
+
+ for (int32_t i = 0; i < tcount && !nextValidNode; i++) {
+ entry = mOffsetTable[i];
+
+ NS_ENSURE_TRUE(entry, NS_ERROR_FAILURE);
+
+ if (entry->mNode == nodePtr) {
+ if (entry->mIsValid) {
+ // The iterator is still pointing to something valid!
+ // Do nothing!
+ return NS_OK;
+ }
+ // We found an invalid entry that points to
+ // the current iterator node. Stop looking for
+ // a previous valid node!
+ foundEntry = true;
+ }
+
+ if (entry->mIsValid) {
+ if (!foundEntry) {
+ prevValidNode = entry->mNode;
+ } else {
+ nextValidNode = entry->mNode;
+ }
+ }
+ }
+
+ nsCOMPtr<nsIContent> content;
+
+ if (prevValidNode) {
+ content = do_QueryInterface(prevValidNode);
+ } else if (nextValidNode) {
+ content = do_QueryInterface(nextValidNode);
+ }
+
+ if (content) {
+ nsresult rv = mIterator->PositionAt(content);
+
+ if (NS_FAILED(rv)) {
+ mIteratorStatus = eIsDone;
+ } else {
+ mIteratorStatus = eValid;
+ }
+ return rv;
+ }
+
+ // If we get here, there aren't any valid entries
+ // in the offset table! Try to position the iterator
+ // on the next text block first, then previous if
+ // one doesn't exist!
+
+ if (mNextTextBlock) {
+ nsresult rv = mIterator->PositionAt(mNextTextBlock);
+
+ if (NS_FAILED(rv)) {
+ mIteratorStatus = eIsDone;
+ return rv;
+ }
+
+ mIteratorStatus = eNext;
+ } else if (mPrevTextBlock) {
+ nsresult rv = mIterator->PositionAt(mPrevTextBlock);
+
+ if (NS_FAILED(rv)) {
+ mIteratorStatus = eIsDone;
+ return rv;
+ }
+
+ mIteratorStatus = ePrev;
+ } else {
+ mIteratorStatus = eIsDone;
+ }
+ return NS_OK;
+}
+
+bool
+nsTextServicesDocument::DidSkip(nsIContentIterator* aFilteredIter)
+{
+ // We can assume here that the Iterator is a nsFilteredContentIterator because
+ // all the iterator are created in CreateContentIterator which create a
+ // nsFilteredContentIterator
+ // So if the iterator bailed on one of the "filtered" content nodes then we
+ // consider that to be a block and bail with true
+ if (aFilteredIter) {
+ nsFilteredContentIterator* filter = static_cast<nsFilteredContentIterator *>(aFilteredIter);
+ if (filter && filter->DidSkip()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void
+nsTextServicesDocument::ClearDidSkip(nsIContentIterator* aFilteredIter)
+{
+ // Clear filter's skip flag
+ if (aFilteredIter) {
+ nsFilteredContentIterator* filter = static_cast<nsFilteredContentIterator *>(aFilteredIter);
+ filter->ClearDidSkip();
+ }
+}
+
+bool
+nsTextServicesDocument::IsBlockNode(nsIContent *aContent)
+{
+ if (!aContent) {
+ NS_ERROR("How did a null pointer get passed to IsBlockNode?");
+ return false;
+ }
+
+ nsIAtom *atom = aContent->NodeInfo()->NameAtom();
+
+ return (sAAtom != atom &&
+ sAddressAtom != atom &&
+ sBigAtom != atom &&
+ sBAtom != atom &&
+ sCiteAtom != atom &&
+ sCodeAtom != atom &&
+ sDfnAtom != atom &&
+ sEmAtom != atom &&
+ sFontAtom != atom &&
+ sIAtom != atom &&
+ sKbdAtom != atom &&
+ sKeygenAtom != atom &&
+ sNobrAtom != atom &&
+ sSAtom != atom &&
+ sSampAtom != atom &&
+ sSmallAtom != atom &&
+ sSpacerAtom != atom &&
+ sSpanAtom != atom &&
+ sStrikeAtom != atom &&
+ sStrongAtom != atom &&
+ sSubAtom != atom &&
+ sSupAtom != atom &&
+ sTtAtom != atom &&
+ sUAtom != atom &&
+ sVarAtom != atom &&
+ sWbrAtom != atom);
+}
+
+bool
+nsTextServicesDocument::HasSameBlockNodeParent(nsIContent *aContent1, nsIContent *aContent2)
+{
+ nsIContent* p1 = aContent1->GetParent();
+ nsIContent* p2 = aContent2->GetParent();
+
+ // Quick test:
+
+ if (p1 == p2) {
+ return true;
+ }
+
+ // Walk up the parent hierarchy looking for closest block boundary node:
+
+ while (p1 && !IsBlockNode(p1)) {
+ p1 = p1->GetParent();
+ }
+
+ while (p2 && !IsBlockNode(p2)) {
+ p2 = p2->GetParent();
+ }
+
+ return p1 == p2;
+}
+
+bool
+nsTextServicesDocument::IsTextNode(nsIContent *aContent)
+{
+ NS_ENSURE_TRUE(aContent, false);
+ return nsIDOMNode::TEXT_NODE == aContent->NodeType();
+}
+
+bool
+nsTextServicesDocument::IsTextNode(nsIDOMNode *aNode)
+{
+ NS_ENSURE_TRUE(aNode, false);
+
+ nsCOMPtr<nsIContent> content = do_QueryInterface(aNode);
+ return IsTextNode(content);
+}
+
+nsresult
+nsTextServicesDocument::SetSelectionInternal(int32_t aOffset, int32_t aLength, bool aDoUpdate)
+{
+ NS_ENSURE_TRUE(mSelCon && aOffset >= 0 && aLength >= 0, NS_ERROR_FAILURE);
+
+ nsIDOMNode *sNode = 0, *eNode = 0;
+ int32_t sOffset = 0, eOffset = 0;
+ OffsetEntry *entry;
+
+ // Find start of selection in node offset terms:
+
+ for (size_t i = 0; !sNode && i < mOffsetTable.Length(); i++) {
+ entry = mOffsetTable[i];
+ if (entry->mIsValid) {
+ if (entry->mIsInsertedText) {
+ // Caret can only be placed at the end of an
+ // inserted text offset entry, if the offsets
+ // match exactly!
+
+ if (entry->mStrOffset == aOffset) {
+ sNode = entry->mNode;
+ sOffset = entry->mNodeOffset + entry->mLength;
+ }
+ } else if (aOffset >= entry->mStrOffset) {
+ bool foundEntry = false;
+ int32_t strEndOffset = entry->mStrOffset + entry->mLength;
+
+ if (aOffset < strEndOffset) {
+ foundEntry = true;
+ } else if (aOffset == strEndOffset) {
+ // Peek after this entry to see if we have any
+ // inserted text entries belonging to the same
+ // entry->mNode. If so, we have to place the selection
+ // after it!
+
+ if (i + 1 < mOffsetTable.Length()) {
+ OffsetEntry *nextEntry = mOffsetTable[i+1];
+
+ if (!nextEntry->mIsValid || nextEntry->mStrOffset != aOffset) {
+ // Next offset entry isn't an exact match, so we'll
+ // just use the current entry.
+ foundEntry = true;
+ }
+ }
+ }
+
+ if (foundEntry) {
+ sNode = entry->mNode;
+ sOffset = entry->mNodeOffset + aOffset - entry->mStrOffset;
+ }
+ }
+
+ if (sNode) {
+ mSelStartIndex = static_cast<int32_t>(i);
+ mSelStartOffset = aOffset;
+ }
+ }
+ }
+
+ NS_ENSURE_TRUE(sNode, NS_ERROR_FAILURE);
+
+ // XXX: If we ever get a SetSelection() method in nsIEditor, we should
+ // use it.
+
+ nsCOMPtr<nsISelection> selection;
+
+ if (aDoUpdate) {
+ nsresult rv =
+ mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL,
+ getter_AddRefs(selection));
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = selection->Collapse(sNode, sOffset);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (aLength <= 0) {
+ // We have a collapsed selection. (Caret)
+
+ mSelEndIndex = mSelStartIndex;
+ mSelEndOffset = mSelStartOffset;
+
+ //**** KDEBUG ****
+ // printf("\n* Sel: (%2d, %4d) (%2d, %4d)\n", mSelStartIndex, mSelStartOffset, mSelEndIndex, mSelEndOffset);
+ //**** KDEBUG ****
+
+ return NS_OK;
+ }
+
+ // Find the end of the selection in node offset terms:
+ int32_t endOffset = aOffset + aLength;
+ for (int32_t i = mOffsetTable.Length() - 1; !eNode && i >= 0; i--) {
+ entry = mOffsetTable[i];
+
+ if (entry->mIsValid) {
+ if (entry->mIsInsertedText) {
+ if (entry->mStrOffset == eOffset) {
+ // If the selection ends on an inserted text offset entry,
+ // the selection includes the entire entry!
+
+ eNode = entry->mNode;
+ eOffset = entry->mNodeOffset + entry->mLength;
+ }
+ } else if (endOffset >= entry->mStrOffset &&
+ endOffset <= entry->mStrOffset + entry->mLength) {
+ eNode = entry->mNode;
+ eOffset = entry->mNodeOffset + endOffset - entry->mStrOffset;
+ }
+
+ if (eNode) {
+ mSelEndIndex = i;
+ mSelEndOffset = endOffset;
+ }
+ }
+ }
+
+ if (aDoUpdate && eNode) {
+ nsresult rv = selection->Extend(eNode, eOffset);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ //**** KDEBUG ****
+ // printf("\n * Sel: (%2d, %4d) (%2d, %4d)\n", mSelStartIndex, mSelStartOffset, mSelEndIndex, mSelEndOffset);
+ //**** KDEBUG ****
+
+ return NS_OK;
+}
+
+nsresult
+nsTextServicesDocument::GetSelection(nsITextServicesDocument::TSDBlockSelectionStatus *aSelStatus, int32_t *aSelOffset, int32_t *aSelLength)
+{
+ NS_ENSURE_TRUE(aSelStatus && aSelOffset && aSelLength, NS_ERROR_NULL_POINTER);
+
+ *aSelStatus = nsITextServicesDocument::eBlockNotFound;
+ *aSelOffset = -1;
+ *aSelLength = -1;
+
+ NS_ENSURE_TRUE(mDOMDocument && mSelCon, NS_ERROR_FAILURE);
+
+ if (mIteratorStatus == nsTextServicesDocument::eIsDone) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsISelection> selection;
+ bool isCollapsed;
+
+ nsresult rv = mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL,
+ getter_AddRefs(selection));
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE);
+
+ rv = selection->GetIsCollapsed(&isCollapsed);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // XXX: If we expose this method publicly, we need to
+ // add LOCK_DOC/UNLOCK_DOC calls!
+
+ // LOCK_DOC(this);
+
+ if (isCollapsed) {
+ rv = GetCollapsedSelection(aSelStatus, aSelOffset, aSelLength);
+ } else {
+ rv = GetUncollapsedSelection(aSelStatus, aSelOffset, aSelLength);
+ }
+
+ // UNLOCK_DOC(this);
+
+ // XXX The result of GetCollapsedSelection() or GetUncollapsedSelection().
+ return rv;
+}
+
+nsresult
+nsTextServicesDocument::GetCollapsedSelection(nsITextServicesDocument::TSDBlockSelectionStatus *aSelStatus, int32_t *aSelOffset, int32_t *aSelLength)
+{
+ nsCOMPtr<nsISelection> domSelection;
+ nsresult rv =
+ mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL,
+ getter_AddRefs(domSelection));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(domSelection, NS_ERROR_FAILURE);
+
+ RefPtr<Selection> selection = domSelection->AsSelection();
+
+ // The calling function should have done the GetIsCollapsed()
+ // check already. Just assume it's collapsed!
+ *aSelStatus = nsITextServicesDocument::eBlockOutside;
+ *aSelOffset = *aSelLength = -1;
+
+ int32_t tableCount = mOffsetTable.Length();
+
+ if (!tableCount) {
+ return NS_OK;
+ }
+
+ // Get pointers to the first and last offset entries
+ // in the table.
+
+ OffsetEntry* eStart = mOffsetTable[0];
+ OffsetEntry* eEnd;
+ if (tableCount > 1) {
+ eEnd = mOffsetTable[tableCount - 1];
+ } else {
+ eEnd = eStart;
+ }
+
+ int32_t eStartOffset = eStart->mNodeOffset;
+ int32_t eEndOffset = eEnd->mNodeOffset + eEnd->mLength;
+
+ RefPtr<nsRange> range = selection->GetRangeAt(0);
+ NS_ENSURE_STATE(range);
+
+ nsCOMPtr<nsIDOMNode> domParent;
+ rv = range->GetStartContainer(getter_AddRefs(domParent));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsINode> parent = do_QueryInterface(domParent);
+ MOZ_ASSERT(parent);
+
+ int32_t offset;
+ rv = range->GetStartOffset(&offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t e1s1 = nsContentUtils::ComparePoints(eStart->mNode, eStartOffset,
+ domParent, offset);
+ int32_t e2s1 = nsContentUtils::ComparePoints(eEnd->mNode, eEndOffset,
+ domParent, offset);
+
+ if (e1s1 > 0 || e2s1 < 0) {
+ // We're done if the caret is outside the current text block.
+ return NS_OK;
+ }
+
+ if (parent->NodeType() == nsIDOMNode::TEXT_NODE) {
+ // Good news, the caret is in a text node. Look
+ // through the offset table for the entry that
+ // matches its parent and offset.
+
+ for (int32_t i = 0; i < tableCount; i++) {
+ OffsetEntry* entry = mOffsetTable[i];
+ NS_ENSURE_TRUE(entry, NS_ERROR_FAILURE);
+
+ if (entry->mNode == domParent.get() &&
+ entry->mNodeOffset <= offset &&
+ offset <= entry->mNodeOffset + entry->mLength) {
+ *aSelStatus = nsITextServicesDocument::eBlockContains;
+ *aSelOffset = entry->mStrOffset + (offset - entry->mNodeOffset);
+ *aSelLength = 0;
+
+ return NS_OK;
+ }
+ }
+
+ // If we get here, we didn't find a text node entry
+ // in our offset table that matched.
+
+ return NS_ERROR_FAILURE;
+ }
+
+ // The caret is in our text block, but it's positioned in some
+ // non-text node (ex. <b>). Create a range based on the start
+ // and end of the text block, then create an iterator based on
+ // this range, with its initial position set to the closest
+ // child of this non-text node. Then look for the closest text
+ // node.
+
+ rv = CreateRange(eStart->mNode, eStartOffset, eEnd->mNode, eEndOffset,
+ getter_AddRefs(range));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIContentIterator> iter;
+ rv = CreateContentIterator(range, getter_AddRefs(iter));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsIContent* saveNode;
+ if (parent->HasChildren()) {
+ // XXX: We need to make sure that all of parent's
+ // children are in the text block.
+
+ // If the parent has children, position the iterator
+ // on the child that is to the left of the offset.
+
+ uint32_t childIndex = (uint32_t)offset;
+
+ if (childIndex > 0) {
+ uint32_t numChildren = parent->GetChildCount();
+ NS_ASSERTION(childIndex <= numChildren, "Invalid selection offset!");
+
+ if (childIndex > numChildren) {
+ childIndex = numChildren;
+ }
+
+ childIndex -= 1;
+ }
+
+ nsIContent* content = parent->GetChildAt(childIndex);
+ NS_ENSURE_TRUE(content, NS_ERROR_FAILURE);
+
+ rv = iter->PositionAt(content);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ saveNode = content;
+ } else {
+ // The parent has no children, so position the iterator
+ // on the parent.
+ NS_ENSURE_TRUE(parent->IsContent(), NS_ERROR_FAILURE);
+ nsCOMPtr<nsIContent> content = parent->AsContent();
+
+ rv = iter->PositionAt(content);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ saveNode = content;
+ }
+
+ // Now iterate to the left, towards the beginning of
+ // the text block, to find the first text node you
+ // come across.
+
+ nsIContent* node = nullptr;
+ while (!iter->IsDone()) {
+ nsINode* current = iter->GetCurrentNode();
+ if (current->NodeType() == nsIDOMNode::TEXT_NODE) {
+ node = static_cast<nsIContent*>(current);
+ break;
+ }
+
+ iter->Prev();
+ }
+
+ if (node) {
+ // We found a node, now set the offset to the end
+ // of the text node.
+ offset = node->TextLength();
+ } else {
+ // We should never really get here, but I'm paranoid.
+
+ // We didn't find a text node above, so iterate to
+ // the right, towards the end of the text block, looking
+ // for a text node.
+
+ rv = iter->PositionAt(saveNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ node = nullptr;
+ while (!iter->IsDone()) {
+ nsINode* current = iter->GetCurrentNode();
+
+ if (current->NodeType() == nsIDOMNode::TEXT_NODE) {
+ node = static_cast<nsIContent*>(current);
+ break;
+ }
+
+ iter->Next();
+ }
+
+ NS_ENSURE_TRUE(node, NS_ERROR_FAILURE);
+
+ // We found a text node, so set the offset to
+ // the beginning of the node.
+
+ offset = 0;
+ }
+
+ for (int32_t i = 0; i < tableCount; i++) {
+ OffsetEntry* entry = mOffsetTable[i];
+ NS_ENSURE_TRUE(entry, NS_ERROR_FAILURE);
+
+ if (entry->mNode == node->AsDOMNode() &&
+ entry->mNodeOffset <= offset &&
+ offset <= entry->mNodeOffset + entry->mLength) {
+ *aSelStatus = nsITextServicesDocument::eBlockContains;
+ *aSelOffset = entry->mStrOffset + (offset - entry->mNodeOffset);
+ *aSelLength = 0;
+
+ // Now move the caret so that it is actually in the text node.
+ // We do this to keep things in sync.
+ //
+ // In most cases, the user shouldn't see any movement in the caret
+ // on screen.
+
+ return SetSelectionInternal(*aSelOffset, *aSelLength, true);
+ }
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+nsresult
+nsTextServicesDocument::GetUncollapsedSelection(nsITextServicesDocument::TSDBlockSelectionStatus *aSelStatus, int32_t *aSelOffset, int32_t *aSelLength)
+{
+ RefPtr<nsRange> range;
+ OffsetEntry *entry;
+
+ nsCOMPtr<nsISelection> domSelection;
+ nsresult rv = mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL,
+ getter_AddRefs(domSelection));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(domSelection, NS_ERROR_FAILURE);
+
+ RefPtr<Selection> selection = domSelection->AsSelection();
+
+ // It is assumed that the calling function has made sure that the
+ // selection is not collapsed, and that the input params to this
+ // method are initialized to some defaults.
+
+ nsCOMPtr<nsIDOMNode> startParent, endParent;
+ int32_t startOffset, endOffset;
+ int32_t rangeCount, tableCount;
+ int32_t e1s1 = 0, e1s2 = 0, e2s1 = 0, e2s2 = 0;
+
+ OffsetEntry *eStart, *eEnd;
+ int32_t eStartOffset, eEndOffset;
+
+ tableCount = mOffsetTable.Length();
+
+ // Get pointers to the first and last offset entries
+ // in the table.
+
+ eStart = mOffsetTable[0];
+
+ if (tableCount > 1) {
+ eEnd = mOffsetTable[tableCount - 1];
+ } else {
+ eEnd = eStart;
+ }
+
+ eStartOffset = eStart->mNodeOffset;
+ eEndOffset = eEnd->mNodeOffset + eEnd->mLength;
+
+ rv = selection->GetRangeCount(&rangeCount);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Find the first range in the selection that intersects
+ // the current text block.
+
+ for (int32_t i = 0; i < rangeCount; i++) {
+ range = selection->GetRangeAt(i);
+ NS_ENSURE_STATE(range);
+
+ rv = GetRangeEndPoints(range,
+ getter_AddRefs(startParent), &startOffset,
+ getter_AddRefs(endParent), &endOffset);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ e1s2 = nsContentUtils::ComparePoints(eStart->mNode, eStartOffset,
+ endParent, endOffset);
+ e2s1 = nsContentUtils::ComparePoints(eEnd->mNode, eEndOffset,
+ startParent, startOffset);
+
+ // Break out of the loop if the text block intersects the current range.
+
+ if (e1s2 <= 0 && e2s1 >= 0) {
+ break;
+ }
+ }
+
+ // We're done if we didn't find an intersecting range.
+
+ if (rangeCount < 1 || e1s2 > 0 || e2s1 < 0) {
+ *aSelStatus = nsITextServicesDocument::eBlockOutside;
+ *aSelOffset = *aSelLength = -1;
+ return NS_OK;
+ }
+
+ // Now that we have an intersecting range, find out more info:
+
+ e1s1 = nsContentUtils::ComparePoints(eStart->mNode, eStartOffset,
+ startParent, startOffset);
+ e2s2 = nsContentUtils::ComparePoints(eEnd->mNode, eEndOffset,
+ endParent, endOffset);
+
+ if (rangeCount > 1) {
+ // There are multiple selection ranges, we only deal
+ // with the first one that intersects the current,
+ // text block, so mark this a as a partial.
+ *aSelStatus = nsITextServicesDocument::eBlockPartial;
+ } else if (e1s1 > 0 && e2s2 < 0) {
+ // The range extends beyond the start and
+ // end of the current text block.
+ *aSelStatus = nsITextServicesDocument::eBlockInside;
+ } else if (e1s1 <= 0 && e2s2 >= 0) {
+ // The current text block contains the entire
+ // range.
+ *aSelStatus = nsITextServicesDocument::eBlockContains;
+ } else {
+ // The range partially intersects the block.
+ *aSelStatus = nsITextServicesDocument::eBlockPartial;
+ }
+
+ // Now create a range based on the intersection of the
+ // text block and range:
+
+ nsCOMPtr<nsIDOMNode> p1, p2;
+ int32_t o1, o2;
+
+ // The start of the range will be the rightmost
+ // start node.
+
+ if (e1s1 >= 0) {
+ p1 = do_QueryInterface(eStart->mNode);
+ o1 = eStartOffset;
+ } else {
+ p1 = startParent;
+ o1 = startOffset;
+ }
+
+ // The end of the range will be the leftmost
+ // end node.
+
+ if (e2s2 <= 0) {
+ p2 = do_QueryInterface(eEnd->mNode);
+ o2 = eEndOffset;
+ } else {
+ p2 = endParent;
+ o2 = endOffset;
+ }
+
+ rv = CreateRange(p1, o1, p2, o2, getter_AddRefs(range));
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Now iterate over this range to figure out the selection's
+ // block offset and length.
+
+ nsCOMPtr<nsIContentIterator> iter;
+
+ rv = CreateContentIterator(range, getter_AddRefs(iter));
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Find the first text node in the range.
+
+ bool found;
+ nsCOMPtr<nsIContent> content;
+
+ iter->First();
+
+ if (!IsTextNode(p1)) {
+ found = false;
+
+ while (!iter->IsDone()) {
+ content = do_QueryInterface(iter->GetCurrentNode());
+
+ if (IsTextNode(content)) {
+ p1 = do_QueryInterface(content);
+
+ NS_ENSURE_TRUE(p1, NS_ERROR_FAILURE);
+
+ o1 = 0;
+ found = true;
+
+ break;
+ }
+
+ iter->Next();
+ }
+
+ NS_ENSURE_TRUE(found, NS_ERROR_FAILURE);
+ }
+
+ // Find the last text node in the range.
+
+ iter->Last();
+
+ if (!IsTextNode(p2)) {
+ found = false;
+ while (!iter->IsDone()) {
+ content = do_QueryInterface(iter->GetCurrentNode());
+ if (IsTextNode(content)) {
+ p2 = do_QueryInterface(content);
+
+ NS_ENSURE_TRUE(p2, NS_ERROR_FAILURE);
+
+ nsString str;
+
+ rv = p2->GetNodeValue(str);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ o2 = str.Length();
+ found = true;
+
+ break;
+ }
+
+ iter->Prev();
+ }
+
+ NS_ENSURE_TRUE(found, NS_ERROR_FAILURE);
+ }
+
+ found = false;
+ *aSelLength = 0;
+
+ for (int32_t i = 0; i < tableCount; i++) {
+ entry = mOffsetTable[i];
+ NS_ENSURE_TRUE(entry, NS_ERROR_FAILURE);
+ if (!found) {
+ if (entry->mNode == p1.get() &&
+ entry->mNodeOffset <= o1 &&
+ o1 <= entry->mNodeOffset + entry->mLength) {
+ *aSelOffset = entry->mStrOffset + (o1 - entry->mNodeOffset);
+ if (p1 == p2 &&
+ entry->mNodeOffset <= o2 &&
+ o2 <= entry->mNodeOffset + entry->mLength) {
+ // The start and end of the range are in the same offset
+ // entry. Calculate the length of the range then we're done.
+ *aSelLength = o2 - o1;
+ break;
+ }
+ // Add the length of the sub string in this offset entry
+ // that follows the start of the range.
+ *aSelLength = entry->mLength - (o1 - entry->mNodeOffset);
+ found = true;
+ }
+ } else { // Found.
+ if (entry->mNode == p2.get() &&
+ entry->mNodeOffset <= o2 &&
+ o2 <= entry->mNodeOffset + entry->mLength) {
+ // We found the end of the range. Calculate the length of the
+ // sub string that is before the end of the range, then we're done.
+ *aSelLength += o2 - entry->mNodeOffset;
+ break;
+ }
+ // The entire entry must be in the range.
+ *aSelLength += entry->mLength;
+ }
+ }
+
+ return NS_OK;
+}
+
+bool
+nsTextServicesDocument::SelectionIsCollapsed()
+{
+ return(mSelStartIndex == mSelEndIndex && mSelStartOffset == mSelEndOffset);
+}
+
+bool
+nsTextServicesDocument::SelectionIsValid()
+{
+ return(mSelStartIndex >= 0);
+}
+
+nsresult
+nsTextServicesDocument::GetRangeEndPoints(nsRange* aRange,
+ nsIDOMNode **aStartParent, int32_t *aStartOffset,
+ nsIDOMNode **aEndParent, int32_t *aEndOffset)
+{
+ NS_ENSURE_TRUE(aRange && aStartParent && aStartOffset && aEndParent && aEndOffset, NS_ERROR_NULL_POINTER);
+
+ nsresult rv = aRange->GetStartContainer(aStartParent);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ENSURE_TRUE(aStartParent, NS_ERROR_FAILURE);
+
+ rv = aRange->GetStartOffset(aStartOffset);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = aRange->GetEndContainer(aEndParent);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ENSURE_TRUE(aEndParent, NS_ERROR_FAILURE);
+
+ return aRange->GetEndOffset(aEndOffset);
+}
+
+nsresult
+nsTextServicesDocument::CreateRange(nsIDOMNode *aStartParent, int32_t aStartOffset,
+ nsIDOMNode *aEndParent, int32_t aEndOffset,
+ nsRange** aRange)
+{
+ return nsRange::CreateRange(aStartParent, aStartOffset, aEndParent,
+ aEndOffset, aRange);
+}
+
+nsresult
+nsTextServicesDocument::FirstTextNode(nsIContentIterator *aIterator,
+ TSDIteratorStatus *aIteratorStatus)
+{
+ if (aIteratorStatus) {
+ *aIteratorStatus = nsTextServicesDocument::eIsDone;
+ }
+
+ aIterator->First();
+
+ while (!aIterator->IsDone()) {
+ if (aIterator->GetCurrentNode()->NodeType() == nsIDOMNode::TEXT_NODE) {
+ if (aIteratorStatus) {
+ *aIteratorStatus = nsTextServicesDocument::eValid;
+ }
+ break;
+ }
+ aIterator->Next();
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsTextServicesDocument::LastTextNode(nsIContentIterator *aIterator,
+ TSDIteratorStatus *aIteratorStatus)
+{
+ if (aIteratorStatus) {
+ *aIteratorStatus = nsTextServicesDocument::eIsDone;
+ }
+
+ aIterator->Last();
+
+ while (!aIterator->IsDone()) {
+ if (aIterator->GetCurrentNode()->NodeType() == nsIDOMNode::TEXT_NODE) {
+ if (aIteratorStatus) {
+ *aIteratorStatus = nsTextServicesDocument::eValid;
+ }
+ break;
+ }
+ aIterator->Prev();
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsTextServicesDocument::FirstTextNodeInCurrentBlock(nsIContentIterator *iter)
+{
+ NS_ENSURE_TRUE(iter, NS_ERROR_NULL_POINTER);
+
+ ClearDidSkip(iter);
+
+ nsCOMPtr<nsIContent> last;
+
+ // Walk backwards over adjacent text nodes until
+ // we hit a block boundary:
+
+ while (!iter->IsDone()) {
+ nsCOMPtr<nsIContent> content = iter->GetCurrentNode()->IsContent()
+ ? iter->GetCurrentNode()->AsContent()
+ : nullptr;
+ if (last && IsBlockNode(content)) {
+ break;
+ }
+ if (IsTextNode(content)) {
+ if (last && !HasSameBlockNodeParent(content, last)) {
+ // We're done, the current text node is in a
+ // different block.
+ break;
+ }
+ last = content;
+ }
+
+ iter->Prev();
+
+ if (DidSkip(iter)) {
+ break;
+ }
+ }
+
+ if (last) {
+ iter->PositionAt(last);
+ }
+
+ // XXX: What should we return if last is null?
+
+ return NS_OK;
+}
+
+nsresult
+nsTextServicesDocument::FirstTextNodeInPrevBlock(nsIContentIterator *aIterator)
+{
+ NS_ENSURE_TRUE(aIterator, NS_ERROR_NULL_POINTER);
+
+ // XXX: What if mIterator is not currently on a text node?
+
+ // Make sure mIterator is pointing to the first text node in the
+ // current block:
+
+ nsresult rv = FirstTextNodeInCurrentBlock(aIterator);
+
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE);
+
+ // Point mIterator to the first node before the first text node:
+
+ aIterator->Prev();
+
+ if (aIterator->IsDone()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Now find the first text node of the next block:
+
+ return FirstTextNodeInCurrentBlock(aIterator);
+}
+
+nsresult
+nsTextServicesDocument::FirstTextNodeInNextBlock(nsIContentIterator *aIterator)
+{
+ nsCOMPtr<nsIContent> prev;
+ bool crossedBlockBoundary = false;
+
+ NS_ENSURE_TRUE(aIterator, NS_ERROR_NULL_POINTER);
+
+ ClearDidSkip(aIterator);
+
+ while (!aIterator->IsDone()) {
+ nsCOMPtr<nsIContent> content = aIterator->GetCurrentNode()->IsContent()
+ ? aIterator->GetCurrentNode()->AsContent()
+ : nullptr;
+
+ if (IsTextNode(content)) {
+ if (crossedBlockBoundary ||
+ (prev && !HasSameBlockNodeParent(prev, content))) {
+ break;
+ }
+ prev = content;
+ } else if (!crossedBlockBoundary && IsBlockNode(content)) {
+ crossedBlockBoundary = true;
+ }
+
+ aIterator->Next();
+
+ if (!crossedBlockBoundary && DidSkip(aIterator)) {
+ crossedBlockBoundary = true;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsTextServicesDocument::GetFirstTextNodeInPrevBlock(nsIContent **aContent)
+{
+ NS_ENSURE_TRUE(aContent, NS_ERROR_NULL_POINTER);
+
+ *aContent = 0;
+
+ // Save the iterator's current content node so we can restore
+ // it when we are done:
+
+ nsINode* node = mIterator->GetCurrentNode();
+
+ nsresult rv = FirstTextNodeInPrevBlock(mIterator);
+
+ if (NS_FAILED(rv)) {
+ // Try to restore the iterator before returning.
+ mIterator->PositionAt(node);
+ return rv;
+ }
+
+ if (!mIterator->IsDone()) {
+ nsCOMPtr<nsIContent> current = mIterator->GetCurrentNode()->IsContent()
+ ? mIterator->GetCurrentNode()->AsContent()
+ : nullptr;
+ current.forget(aContent);
+ }
+
+ // Restore the iterator:
+
+ return mIterator->PositionAt(node);
+}
+
+nsresult
+nsTextServicesDocument::GetFirstTextNodeInNextBlock(nsIContent **aContent)
+{
+ NS_ENSURE_TRUE(aContent, NS_ERROR_NULL_POINTER);
+
+ *aContent = 0;
+
+ // Save the iterator's current content node so we can restore
+ // it when we are done:
+
+ nsINode* node = mIterator->GetCurrentNode();
+
+ nsresult rv = FirstTextNodeInNextBlock(mIterator);
+
+ if (NS_FAILED(rv)) {
+ // Try to restore the iterator before returning.
+ mIterator->PositionAt(node);
+ return rv;
+ }
+
+ if (!mIterator->IsDone()) {
+ nsCOMPtr<nsIContent> current = mIterator->GetCurrentNode()->IsContent()
+ ? mIterator->GetCurrentNode()->AsContent()
+ : nullptr;
+ current.forget(aContent);
+ }
+
+ // Restore the iterator:
+ return mIterator->PositionAt(node);
+}
+
+nsresult
+nsTextServicesDocument::CreateOffsetTable(nsTArray<OffsetEntry*> *aOffsetTable,
+ nsIContentIterator *aIterator,
+ TSDIteratorStatus *aIteratorStatus,
+ nsRange* aIterRange, nsString* aStr)
+{
+ nsCOMPtr<nsIContent> first;
+ nsCOMPtr<nsIContent> prev;
+
+ NS_ENSURE_TRUE(aIterator, NS_ERROR_NULL_POINTER);
+
+ ClearOffsetTable(aOffsetTable);
+
+ if (aStr) {
+ aStr->Truncate();
+ }
+
+ if (*aIteratorStatus == nsTextServicesDocument::eIsDone) {
+ return NS_OK;
+ }
+
+ // If we have an aIterRange, retrieve the endpoints so
+ // they can be used in the while loop below to trim entries
+ // for text nodes that are partially selected by aIterRange.
+
+ nsCOMPtr<nsIDOMNode> rngStartNode, rngEndNode;
+ int32_t rngStartOffset = 0, rngEndOffset = 0;
+
+ if (aIterRange) {
+ nsresult rv =
+ GetRangeEndPoints(aIterRange,
+ getter_AddRefs(rngStartNode), &rngStartOffset,
+ getter_AddRefs(rngEndNode), &rngEndOffset);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // The text service could have added text nodes to the beginning
+ // of the current block and called this method again. Make sure
+ // we really are at the beginning of the current block:
+
+ nsresult rv = FirstTextNodeInCurrentBlock(aIterator);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t offset = 0;
+
+ ClearDidSkip(aIterator);
+
+ while (!aIterator->IsDone()) {
+ nsCOMPtr<nsIContent> content = aIterator->GetCurrentNode()->IsContent()
+ ? aIterator->GetCurrentNode()->AsContent()
+ : nullptr;
+ if (IsTextNode(content)) {
+ if (prev && !HasSameBlockNodeParent(prev, content)) {
+ break;
+ }
+
+ nsCOMPtr<nsIDOMNode> node = do_QueryInterface(content);
+ if (node) {
+ nsString str;
+
+ rv = node->GetNodeValue(str);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Add an entry for this text node into the offset table:
+
+ OffsetEntry *entry = new OffsetEntry(node, offset, str.Length());
+ aOffsetTable->AppendElement(entry);
+
+ // If one or both of the endpoints of the iteration range
+ // are in the text node for this entry, make sure the entry
+ // only accounts for the portion of the text node that is
+ // in the range.
+
+ int32_t startOffset = 0;
+ int32_t endOffset = str.Length();
+ bool adjustStr = false;
+
+ if (entry->mNode == rngStartNode) {
+ entry->mNodeOffset = startOffset = rngStartOffset;
+ adjustStr = true;
+ }
+
+ if (entry->mNode == rngEndNode) {
+ endOffset = rngEndOffset;
+ adjustStr = true;
+ }
+
+ if (adjustStr) {
+ entry->mLength = endOffset - startOffset;
+ str = Substring(str, startOffset, entry->mLength);
+ }
+
+ offset += str.Length();
+
+ if (aStr) {
+ // Append the text node's string to the output string:
+ if (!first) {
+ *aStr = str;
+ } else {
+ *aStr += str;
+ }
+ }
+ }
+
+ prev = content;
+
+ if (!first) {
+ first = content;
+ }
+ }
+ // XXX This should be checked before IsTextNode(), but IsBlockNode() returns
+ // true even if content is a text node. See bug 1311934.
+ else if (IsBlockNode(content)) {
+ break;
+ }
+
+ aIterator->Next();
+
+ if (DidSkip(aIterator)) {
+ break;
+ }
+ }
+
+ if (first) {
+ // Always leave the iterator pointing at the first
+ // text node of the current block!
+ aIterator->PositionAt(first);
+ } else {
+ // If we never ran across a text node, the iterator
+ // might have been pointing to something invalid to
+ // begin with.
+ *aIteratorStatus = nsTextServicesDocument::eIsDone;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsTextServicesDocument::RemoveInvalidOffsetEntries()
+{
+ for (size_t i = 0; i < mOffsetTable.Length(); ) {
+ OffsetEntry* entry = mOffsetTable[i];
+ if (!entry->mIsValid) {
+ mOffsetTable.RemoveElementAt(i);
+ if (mSelStartIndex >= 0 && static_cast<size_t>(mSelStartIndex) >= i) {
+ // We are deleting an entry that comes before
+ // mSelStartIndex, decrement mSelStartIndex so
+ // that it points to the correct entry!
+
+ NS_ASSERTION(i != static_cast<size_t>(mSelStartIndex),
+ "Invalid selection index.");
+
+ --mSelStartIndex;
+ --mSelEndIndex;
+ }
+ } else {
+ i++;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsTextServicesDocument::ClearOffsetTable(nsTArray<OffsetEntry*> *aOffsetTable)
+{
+ for (size_t i = 0; i < aOffsetTable->Length(); i++) {
+ delete aOffsetTable->ElementAt(i);
+ }
+
+ aOffsetTable->Clear();
+
+ return NS_OK;
+}
+
+nsresult
+nsTextServicesDocument::SplitOffsetEntry(int32_t aTableIndex, int32_t aNewEntryLength)
+{
+ OffsetEntry *entry = mOffsetTable[aTableIndex];
+
+ NS_ASSERTION((aNewEntryLength > 0), "aNewEntryLength <= 0");
+ NS_ASSERTION((aNewEntryLength < entry->mLength), "aNewEntryLength >= mLength");
+
+ if (aNewEntryLength < 1 || aNewEntryLength >= entry->mLength) {
+ return NS_ERROR_FAILURE;
+ }
+
+ int32_t oldLength = entry->mLength - aNewEntryLength;
+
+ OffsetEntry *newEntry = new OffsetEntry(entry->mNode,
+ entry->mStrOffset + oldLength,
+ aNewEntryLength);
+
+ if (!mOffsetTable.InsertElementAt(aTableIndex + 1, newEntry)) {
+ delete newEntry;
+ return NS_ERROR_FAILURE;
+ }
+
+ // Adjust entry fields:
+
+ entry->mLength = oldLength;
+ newEntry->mNodeOffset = entry->mNodeOffset + oldLength;
+
+ return NS_OK;
+}
+
+nsresult
+nsTextServicesDocument::NodeHasOffsetEntry(nsTArray<OffsetEntry*> *aOffsetTable, nsIDOMNode *aNode, bool *aHasEntry, int32_t *aEntryIndex)
+{
+ NS_ENSURE_TRUE(aNode && aHasEntry && aEntryIndex, NS_ERROR_NULL_POINTER);
+
+ for (size_t i = 0; i < aOffsetTable->Length(); i++) {
+ OffsetEntry* entry = (*aOffsetTable)[i];
+
+ NS_ENSURE_TRUE(entry, NS_ERROR_FAILURE);
+
+ if (entry->mNode == aNode) {
+ *aHasEntry = true;
+ *aEntryIndex = i;
+ return NS_OK;
+ }
+ }
+
+ *aHasEntry = false;
+ *aEntryIndex = -1;
+ return NS_OK;
+}
+
+// Spellchecker code has this. See bug 211343
+#define IS_NBSP_CHAR(c) (((unsigned char)0xa0)==(c))
+
+nsresult
+nsTextServicesDocument::FindWordBounds(nsTArray<OffsetEntry*> *aOffsetTable,
+ nsString *aBlockStr,
+ nsIDOMNode *aNode,
+ int32_t aNodeOffset,
+ nsIDOMNode **aWordStartNode,
+ int32_t *aWordStartOffset,
+ nsIDOMNode **aWordEndNode,
+ int32_t *aWordEndOffset)
+{
+ // Initialize return values.
+
+ if (aWordStartNode) {
+ *aWordStartNode = nullptr;
+ }
+ if (aWordStartOffset) {
+ *aWordStartOffset = 0;
+ }
+ if (aWordEndNode) {
+ *aWordEndNode = nullptr;
+ }
+ if (aWordEndOffset) {
+ *aWordEndOffset = 0;
+ }
+
+ int32_t entryIndex = 0;
+ bool hasEntry = false;
+
+ // It's assumed that aNode is a text node. The first thing
+ // we do is get its index in the offset table so we can
+ // calculate the dom point's string offset.
+
+ nsresult rv = NodeHasOffsetEntry(aOffsetTable, aNode, &hasEntry, &entryIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(hasEntry, NS_ERROR_FAILURE);
+
+ // Next we map aNodeOffset into a string offset.
+
+ OffsetEntry *entry = (*aOffsetTable)[entryIndex];
+ uint32_t strOffset = entry->mStrOffset + aNodeOffset - entry->mNodeOffset;
+
+ // Now we use the word breaker to find the beginning and end
+ // of the word from our calculated string offset.
+
+ const char16_t *str = aBlockStr->get();
+ uint32_t strLen = aBlockStr->Length();
+
+ nsIWordBreaker* wordBreaker = nsContentUtils::WordBreaker();
+ nsWordRange res = wordBreaker->FindWord(str, strLen, strOffset);
+ if (res.mBegin > strLen) {
+ return str ? NS_ERROR_ILLEGAL_VALUE : NS_ERROR_NULL_POINTER;
+ }
+
+ // Strip out the NBSPs at the ends
+ while (res.mBegin <= res.mEnd && IS_NBSP_CHAR(str[res.mBegin])) {
+ res.mBegin++;
+ }
+ if (str[res.mEnd] == (unsigned char)0x20) {
+ uint32_t realEndWord = res.mEnd - 1;
+ while (realEndWord > res.mBegin && IS_NBSP_CHAR(str[realEndWord])) {
+ realEndWord--;
+ }
+ if (realEndWord < res.mEnd - 1) {
+ res.mEnd = realEndWord + 1;
+ }
+ }
+
+ // Now that we have the string offsets for the beginning
+ // and end of the word, run through the offset table and
+ // convert them back into dom points.
+
+ size_t lastIndex = aOffsetTable->Length() - 1;
+ for (size_t i = 0; i <= lastIndex; i++) {
+ entry = (*aOffsetTable)[i];
+
+ int32_t strEndOffset = entry->mStrOffset + entry->mLength;
+
+ // Check to see if res.mBegin is within the range covered
+ // by this entry. Note that if res.mBegin is after the last
+ // character covered by this entry, we will use the next
+ // entry if there is one.
+
+ if (uint32_t(entry->mStrOffset) <= res.mBegin &&
+ (res.mBegin < static_cast<uint32_t>(strEndOffset) ||
+ (res.mBegin == static_cast<uint32_t>(strEndOffset) &&
+ i == lastIndex))) {
+ if (aWordStartNode) {
+ *aWordStartNode = entry->mNode;
+ NS_IF_ADDREF(*aWordStartNode);
+ }
+
+ if (aWordStartOffset) {
+ *aWordStartOffset = entry->mNodeOffset + res.mBegin - entry->mStrOffset;
+ }
+
+ if (!aWordEndNode && !aWordEndOffset) {
+ // We've found our start entry, but if we're not looking
+ // for end entries, we're done.
+ break;
+ }
+ }
+
+ // Check to see if res.mEnd is within the range covered
+ // by this entry.
+
+ if (static_cast<uint32_t>(entry->mStrOffset) <= res.mEnd &&
+ res.mEnd <= static_cast<uint32_t>(strEndOffset)) {
+ if (res.mBegin == res.mEnd &&
+ res.mEnd == static_cast<uint32_t>(strEndOffset) &&
+ i != lastIndex) {
+ // Wait for the next round so that we use the same entry
+ // we did for aWordStartNode.
+ continue;
+ }
+
+ if (aWordEndNode) {
+ *aWordEndNode = entry->mNode;
+ NS_IF_ADDREF(*aWordEndNode);
+ }
+
+ if (aWordEndOffset) {
+ *aWordEndOffset = entry->mNodeOffset + res.mEnd - entry->mStrOffset;
+ }
+ break;
+ }
+ }
+
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTextServicesDocument::WillInsertNode(nsIDOMNode *aNode,
+ nsIDOMNode *aParent,
+ int32_t aPosition)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTextServicesDocument::WillDeleteNode(nsIDOMNode *aChild)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTextServicesDocument::WillSplitNode(nsIDOMNode *aExistingRightNode,
+ int32_t aOffset)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTextServicesDocument::WillJoinNodes(nsIDOMNode *aLeftNode,
+ nsIDOMNode *aRightNode,
+ nsIDOMNode *aParent)
+{
+ return NS_OK;
+}
+
+// -------------------------------
+// stubs for unused listen methods
+// -------------------------------
+
+NS_IMETHODIMP
+nsTextServicesDocument::WillCreateNode(const nsAString& aTag, nsIDOMNode *aParent, int32_t aPosition)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTextServicesDocument::DidCreateNode(const nsAString& aTag, nsIDOMNode *aNode, nsIDOMNode *aParent, int32_t aPosition, nsresult aResult)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTextServicesDocument::WillInsertText(nsIDOMCharacterData *aTextNode, int32_t aOffset, const nsAString &aString)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTextServicesDocument::DidInsertText(nsIDOMCharacterData *aTextNode, int32_t aOffset, const nsAString &aString, nsresult aResult)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTextServicesDocument::WillDeleteText(nsIDOMCharacterData *aTextNode, int32_t aOffset, int32_t aLength)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTextServicesDocument::DidDeleteText(nsIDOMCharacterData *aTextNode, int32_t aOffset, int32_t aLength, nsresult aResult)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTextServicesDocument::WillDeleteSelection(nsISelection *aSelection)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTextServicesDocument::DidDeleteSelection(nsISelection *aSelection)
+{
+ return NS_OK;
+}
diff --git a/editor/txtsvc/nsTextServicesDocument.h b/editor/txtsvc/nsTextServicesDocument.h
new file mode 100644
index 000000000..d4e034546
--- /dev/null
+++ b/editor/txtsvc/nsTextServicesDocument.h
@@ -0,0 +1,236 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsTextServicesDocument_h__
+#define nsTextServicesDocument_h__
+
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIEditActionListener.h"
+#include "nsISupportsImpl.h"
+#include "nsITextServicesDocument.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsTArray.h"
+#include "nscore.h"
+
+class OffsetEntry;
+class nsIAtom;
+class nsIContent;
+class nsIContentIterator;
+class nsIDOMCharacterData;
+class nsIDOMDocument;
+class nsIDOMNode;
+class nsIDOMRange;
+class nsIEditor;
+class nsISelection;
+class nsISelectionController;
+class nsITextServicesFilter;
+class nsString;
+
+/** implementation of a text services object.
+ *
+ */
+class nsTextServicesDocument final : public nsITextServicesDocument,
+ public nsIEditActionListener
+{
+private:
+ static nsIAtom *sAAtom;
+ static nsIAtom *sAddressAtom;
+ static nsIAtom *sBigAtom;
+ static nsIAtom *sBAtom;
+ static nsIAtom *sCiteAtom;
+ static nsIAtom *sCodeAtom;
+ static nsIAtom *sDfnAtom;
+ static nsIAtom *sEmAtom;
+ static nsIAtom *sFontAtom;
+ static nsIAtom *sIAtom;
+ static nsIAtom *sKbdAtom;
+ static nsIAtom *sKeygenAtom;
+ static nsIAtom *sNobrAtom;
+ static nsIAtom *sSAtom;
+ static nsIAtom *sSampAtom;
+ static nsIAtom *sSmallAtom;
+ static nsIAtom *sSpacerAtom;
+ static nsIAtom *sSpanAtom;
+ static nsIAtom *sStrikeAtom;
+ static nsIAtom *sStrongAtom;
+ static nsIAtom *sSubAtom;
+ static nsIAtom *sSupAtom;
+ static nsIAtom *sTtAtom;
+ static nsIAtom *sUAtom;
+ static nsIAtom *sVarAtom;
+ static nsIAtom *sWbrAtom;
+
+ typedef enum { eIsDone=0, // No iterator (I), or iterator doesn't point to anything valid.
+ eValid, // I points to first text node (TN) in current block (CB).
+ ePrev, // No TN in CB, I points to first TN in prev block.
+ eNext // No TN in CB, I points to first TN in next block.
+ } TSDIteratorStatus;
+
+ nsCOMPtr<nsIDOMDocument> mDOMDocument;
+ nsCOMPtr<nsISelectionController>mSelCon;
+ nsWeakPtr mEditor; // avoid a cycle with the spell checker and editor
+ nsCOMPtr<nsIContentIterator> mIterator;
+ TSDIteratorStatus mIteratorStatus;
+ nsCOMPtr<nsIContent> mPrevTextBlock;
+ nsCOMPtr<nsIContent> mNextTextBlock;
+ nsTArray<OffsetEntry*> mOffsetTable;
+
+ int32_t mSelStartIndex;
+ int32_t mSelStartOffset;
+ int32_t mSelEndIndex;
+ int32_t mSelEndOffset;
+
+ RefPtr<nsRange> mExtent;
+
+ nsCOMPtr<nsITextServicesFilter> mTxtSvcFilter;
+
+protected:
+ /** The default destructor.
+ */
+ virtual ~nsTextServicesDocument();
+
+public:
+
+ /** The default constructor.
+ */
+ nsTextServicesDocument();
+
+ /** To be called at module init
+ */
+ static void RegisterAtoms();
+
+ /* Macro for AddRef(), Release(), and QueryInterface() */
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsTextServicesDocument, nsITextServicesDocument)
+
+ /* nsITextServicesDocument method implementations. */
+ NS_IMETHOD InitWithEditor(nsIEditor *aEditor) override;
+ NS_IMETHOD GetDocument(nsIDOMDocument **aDoc) override;
+ NS_IMETHOD SetExtent(nsIDOMRange* aDOMRange) override;
+ NS_IMETHOD ExpandRangeToWordBoundaries(nsIDOMRange *aRange) override;
+ NS_IMETHOD SetFilter(nsITextServicesFilter *aFilter) override;
+ NS_IMETHOD GetCurrentTextBlock(nsString *aStr) override;
+ NS_IMETHOD FirstBlock() override;
+ NS_IMETHOD LastSelectedBlock(TSDBlockSelectionStatus *aSelStatus, int32_t *aSelOffset, int32_t *aSelLength) override;
+ NS_IMETHOD PrevBlock() override;
+ NS_IMETHOD NextBlock() override;
+ NS_IMETHOD IsDone(bool *aIsDone) override;
+ NS_IMETHOD SetSelection(int32_t aOffset, int32_t aLength) override;
+ NS_IMETHOD ScrollSelectionIntoView() override;
+ NS_IMETHOD DeleteSelection() override;
+ NS_IMETHOD InsertText(const nsString *aText) override;
+
+ /* nsIEditActionListener method implementations. */
+ NS_IMETHOD WillInsertNode(nsIDOMNode *aNode,
+ nsIDOMNode *aParent,
+ int32_t aPosition) override;
+ NS_IMETHOD DidInsertNode(nsIDOMNode *aNode,
+ nsIDOMNode *aParent,
+ int32_t aPosition,
+ nsresult aResult) override;
+
+ NS_IMETHOD WillDeleteNode(nsIDOMNode *aChild) override;
+ NS_IMETHOD DidDeleteNode(nsIDOMNode *aChild, nsresult aResult) override;
+
+ NS_IMETHOD WillSplitNode(nsIDOMNode * aExistingRightNode,
+ int32_t aOffset) override;
+ NS_IMETHOD DidSplitNode(nsIDOMNode *aExistingRightNode,
+ int32_t aOffset,
+ nsIDOMNode *aNewLeftNode,
+ nsresult aResult) override;
+
+ NS_IMETHOD WillJoinNodes(nsIDOMNode *aLeftNode,
+ nsIDOMNode *aRightNode,
+ nsIDOMNode *aParent) override;
+ NS_IMETHOD DidJoinNodes(nsIDOMNode *aLeftNode,
+ nsIDOMNode *aRightNode,
+ nsIDOMNode *aParent,
+ nsresult aResult) override;
+ // these listen methods are unused:
+ NS_IMETHOD WillCreateNode(const nsAString& aTag, nsIDOMNode *aParent, int32_t aPosition) override;
+ NS_IMETHOD DidCreateNode(const nsAString& aTag, nsIDOMNode *aNode, nsIDOMNode *aParent, int32_t aPosition, nsresult aResult) override;
+ NS_IMETHOD WillInsertText(nsIDOMCharacterData *aTextNode, int32_t aOffset, const nsAString &aString) override;
+ NS_IMETHOD DidInsertText(nsIDOMCharacterData *aTextNode, int32_t aOffset, const nsAString &aString, nsresult aResult) override;
+ NS_IMETHOD WillDeleteText(nsIDOMCharacterData *aTextNode, int32_t aOffset, int32_t aLength) override;
+ NS_IMETHOD DidDeleteText(nsIDOMCharacterData *aTextNode, int32_t aOffset, int32_t aLength, nsresult aResult) override;
+ NS_IMETHOD WillDeleteSelection(nsISelection *aSelection) override;
+ NS_IMETHOD DidDeleteSelection(nsISelection *aSelection) override;
+
+ /* Helper functions */
+ static nsresult GetRangeEndPoints(nsRange* aRange, nsIDOMNode** aParent1,
+ int32_t* aOffset1, nsIDOMNode** aParent2,
+ int32_t* aOffset2);
+ static nsresult CreateRange(nsIDOMNode* aStartParent, int32_t aStartOffset,
+ nsIDOMNode* aEndParent, int32_t aEndOffset,
+ nsRange** aRange);
+
+private:
+ /* nsTextServicesDocument private methods. */
+
+ nsresult CreateContentIterator(nsRange* aRange,
+ nsIContentIterator** aIterator);
+
+ nsresult GetDocumentContentRootNode(nsIDOMNode **aNode);
+ nsresult CreateDocumentContentRange(nsRange** aRange);
+ nsresult CreateDocumentContentRootToNodeOffsetRange(nsIDOMNode* aParent,
+ int32_t aOffset,
+ bool aToStart,
+ nsRange** aRange);
+ nsresult CreateDocumentContentIterator(nsIContentIterator **aIterator);
+
+ nsresult AdjustContentIterator();
+
+ static nsresult FirstTextNode(nsIContentIterator *aIterator, TSDIteratorStatus *IteratorStatus);
+ static nsresult LastTextNode(nsIContentIterator *aIterator, TSDIteratorStatus *IteratorStatus);
+
+ static nsresult FirstTextNodeInCurrentBlock(nsIContentIterator *aIterator);
+ static nsresult FirstTextNodeInPrevBlock(nsIContentIterator *aIterator);
+ static nsresult FirstTextNodeInNextBlock(nsIContentIterator *aIterator);
+
+ nsresult GetFirstTextNodeInPrevBlock(nsIContent **aContent);
+ nsresult GetFirstTextNodeInNextBlock(nsIContent **aContent);
+
+ static bool IsBlockNode(nsIContent *aContent);
+ static bool IsTextNode(nsIContent *aContent);
+ static bool IsTextNode(nsIDOMNode *aNode);
+
+ static bool DidSkip(nsIContentIterator* aFilteredIter);
+ static void ClearDidSkip(nsIContentIterator* aFilteredIter);
+
+ static bool HasSameBlockNodeParent(nsIContent *aContent1, nsIContent *aContent2);
+
+ nsresult SetSelectionInternal(int32_t aOffset, int32_t aLength, bool aDoUpdate);
+ nsresult GetSelection(TSDBlockSelectionStatus *aSelStatus, int32_t *aSelOffset, int32_t *aSelLength);
+ nsresult GetCollapsedSelection(TSDBlockSelectionStatus *aSelStatus, int32_t *aSelOffset, int32_t *aSelLength);
+ nsresult GetUncollapsedSelection(TSDBlockSelectionStatus *aSelStatus, int32_t *aSelOffset, int32_t *aSelLength);
+
+ bool SelectionIsCollapsed();
+ bool SelectionIsValid();
+
+ static nsresult CreateOffsetTable(nsTArray<OffsetEntry*> *aOffsetTable,
+ nsIContentIterator *aIterator,
+ TSDIteratorStatus *aIteratorStatus,
+ nsRange* aIterRange, nsString* aStr);
+ static nsresult ClearOffsetTable(nsTArray<OffsetEntry*> *aOffsetTable);
+
+ static nsresult NodeHasOffsetEntry(nsTArray<OffsetEntry*> *aOffsetTable,
+ nsIDOMNode *aNode,
+ bool *aHasEntry,
+ int32_t *aEntryIndex);
+
+ nsresult RemoveInvalidOffsetEntries();
+ nsresult SplitOffsetEntry(int32_t aTableIndex, int32_t aOffsetIntoEntry);
+
+ static nsresult FindWordBounds(nsTArray<OffsetEntry*> *offsetTable,
+ nsString *blockStr,
+ nsIDOMNode *aNode, int32_t aNodeOffset,
+ nsIDOMNode **aWordStartNode,
+ int32_t *aWordStartOffset,
+ nsIDOMNode **aWordEndNode,
+ int32_t *aWordEndOffset);
+};
+
+#endif // nsTextServicesDocument_h__
diff --git a/editor/txtsvc/nsTextServicesFactory.cpp b/editor/txtsvc/nsTextServicesFactory.cpp
new file mode 100644
index 000000000..845fb344d
--- /dev/null
+++ b/editor/txtsvc/nsTextServicesFactory.cpp
@@ -0,0 +1,31 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsIGenericFactory.h"
+
+#include "nsTextServicesDocument.h"
+#include "nsTextServicesCID.h"
+
+////////////////////////////////////////////////////////////////////////
+// Define the contructor function for the objects
+//
+// NOTE: This creates an instance of objects by using the default constructor
+//
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsTextServicesDocument)
+
+////////////////////////////////////////////////////////////////////////
+// Define a table of CIDs implemented by this module along with other
+// information like the function to create an instance, contractid, and
+// class name.
+//
+static const nsModuleComponentInfo components[] = {
+ { nullptr, NS_TEXTSERVICESDOCUMENT_CID, "@mozilla.org/textservices/textservicesdocument;1", nsTextServicesDocumentConstructor },
+};
+
+////////////////////////////////////////////////////////////////////////
+// Implement the NSGetModule() exported function for your module
+// and the entire implementation of the module object.
+//
+NS_IMPL_NSGETMODULE(nsTextServicesModule, components)