зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1176280 - make links in Hello chat clickable, r=mikedeboer, r=gerv for license.html changes
This commit is contained in:
Родитель
557d9030a3
Коммит
d55fa0f2d7
|
@ -22,6 +22,7 @@ content/js/panel.js
|
|||
content/js/roomViews.js
|
||||
content/js/feedbackViews.js
|
||||
content/shared/js/textChatView.js
|
||||
content/shared/js/linkifiedTextView.js
|
||||
content/shared/js/views.js
|
||||
standalone/content/js/fxOSMarketplace.js
|
||||
standalone/content/js/standaloneRoomViews.js
|
||||
|
|
|
@ -39,6 +39,8 @@
|
|||
<script type="text/javascript" src="loop/js/feedbackViews.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/textChatStore.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/textChatView.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/linkifiedTextView.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/urlRegExps.js"></script>
|
||||
<script type="text/javascript" src="loop/js/conversationViews.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/websocket.js"></script>
|
||||
<script type="text/javascript" src="loop/js/conversationAppStore.js"></script>
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
/* 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 loop = loop || {};
|
||||
loop.shared = loop.shared || {};
|
||||
loop.shared.views = loop.shared.views || {};
|
||||
loop.shared.views.LinkifiedTextView = (function(mozL10n) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Given a rawText property, renderer a version of that text with any
|
||||
* links starting with http://, https://, or ftp:// as actual clickable
|
||||
* links inside a <p> container.
|
||||
*/
|
||||
var LinkifiedTextView = React.createClass({displayName: "LinkifiedTextView",
|
||||
propTypes: {
|
||||
// Call this instead of allowing the default <a> click semantics, if
|
||||
// given. Also causes sendReferrer and suppressTarget attributes to be
|
||||
// ignored.
|
||||
linkClickHandler: React.PropTypes.func,
|
||||
// The text to be linkified.
|
||||
rawText: React.PropTypes.string.isRequired,
|
||||
// Should the links send a referrer? Defaults to false.
|
||||
sendReferrer: React.PropTypes.bool,
|
||||
// Should we suppress target="_blank" on the link? Defaults to false.
|
||||
// Mostly for testing use.
|
||||
suppressTarget: React.PropTypes.bool
|
||||
},
|
||||
|
||||
mixins: [
|
||||
React.addons.PureRenderMixin
|
||||
],
|
||||
|
||||
_handleClickEvent: function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.props.linkClickHandler(e.currentTarget.href);
|
||||
},
|
||||
|
||||
_generateLinkAttributes: function(href) {
|
||||
var linkAttributes = {
|
||||
href: href
|
||||
};
|
||||
|
||||
if (this.props.linkClickHandler) {
|
||||
linkAttributes.onClick = this._handleClickEvent;
|
||||
|
||||
// if this is specified, we short-circuit return to avoid unnecessarily
|
||||
// creating target and rel attributes.
|
||||
return linkAttributes;
|
||||
}
|
||||
|
||||
if (!this.props.suppressTarget) {
|
||||
linkAttributes.target = "_blank";
|
||||
}
|
||||
|
||||
if (!this.props.sendReferrer) {
|
||||
linkAttributes.rel = "noreferrer";
|
||||
}
|
||||
|
||||
return linkAttributes;
|
||||
},
|
||||
|
||||
/** a
|
||||
* Parse the given string into an array of strings and React <a> elements
|
||||
* in the order in which they should be rendered (i.e. FIFO).
|
||||
*
|
||||
* @param {String} s the raw string to be parsed
|
||||
*
|
||||
* @returns {Array} of strings and React <a> elements in order.
|
||||
*/
|
||||
parseStringToElements: function(s) {
|
||||
var elements = [];
|
||||
var result = loop.shared.urlRegExps.fullUrlMatch.exec(s);
|
||||
var reactElementsCounter = 0; // For giving keys to each ReactElement.
|
||||
|
||||
while (result) {
|
||||
// If there's text preceding the first link, push it onto the array
|
||||
// and update the string pointer.
|
||||
if (result.index) {
|
||||
elements.push(s.substr(0, result.index));
|
||||
s = s.substr(result.index);
|
||||
}
|
||||
|
||||
// Push the first link itself, and advance the string pointer again.
|
||||
elements.push(
|
||||
React.createElement("a", React.__spread({}, this._generateLinkAttributes(result[0]) ,
|
||||
{key: reactElementsCounter++}),
|
||||
result[0]
|
||||
)
|
||||
);
|
||||
s = s.substr(result[0].length);
|
||||
|
||||
// Check for another link, and perhaps continue...
|
||||
result = loop.shared.urlRegExps.fullUrlMatch.exec(s);
|
||||
}
|
||||
|
||||
if (s) {
|
||||
elements.push(s);
|
||||
}
|
||||
|
||||
return elements;
|
||||
},
|
||||
|
||||
render: function () {
|
||||
return ( React.createElement("p", null, this.parseStringToElements(this.props.rawText) ) );
|
||||
}
|
||||
});
|
||||
|
||||
return LinkifiedTextView;
|
||||
|
||||
})(navigator.mozL10n || document.mozL10n);
|
|
@ -0,0 +1,114 @@
|
|||
/* 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 loop = loop || {};
|
||||
loop.shared = loop.shared || {};
|
||||
loop.shared.views = loop.shared.views || {};
|
||||
loop.shared.views.LinkifiedTextView = (function(mozL10n) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Given a rawText property, renderer a version of that text with any
|
||||
* links starting with http://, https://, or ftp:// as actual clickable
|
||||
* links inside a <p> container.
|
||||
*/
|
||||
var LinkifiedTextView = React.createClass({
|
||||
propTypes: {
|
||||
// Call this instead of allowing the default <a> click semantics, if
|
||||
// given. Also causes sendReferrer and suppressTarget attributes to be
|
||||
// ignored.
|
||||
linkClickHandler: React.PropTypes.func,
|
||||
// The text to be linkified.
|
||||
rawText: React.PropTypes.string.isRequired,
|
||||
// Should the links send a referrer? Defaults to false.
|
||||
sendReferrer: React.PropTypes.bool,
|
||||
// Should we suppress target="_blank" on the link? Defaults to false.
|
||||
// Mostly for testing use.
|
||||
suppressTarget: React.PropTypes.bool
|
||||
},
|
||||
|
||||
mixins: [
|
||||
React.addons.PureRenderMixin
|
||||
],
|
||||
|
||||
_handleClickEvent: function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.props.linkClickHandler(e.currentTarget.href);
|
||||
},
|
||||
|
||||
_generateLinkAttributes: function(href) {
|
||||
var linkAttributes = {
|
||||
href: href
|
||||
};
|
||||
|
||||
if (this.props.linkClickHandler) {
|
||||
linkAttributes.onClick = this._handleClickEvent;
|
||||
|
||||
// if this is specified, we short-circuit return to avoid unnecessarily
|
||||
// creating target and rel attributes.
|
||||
return linkAttributes;
|
||||
}
|
||||
|
||||
if (!this.props.suppressTarget) {
|
||||
linkAttributes.target = "_blank";
|
||||
}
|
||||
|
||||
if (!this.props.sendReferrer) {
|
||||
linkAttributes.rel = "noreferrer";
|
||||
}
|
||||
|
||||
return linkAttributes;
|
||||
},
|
||||
|
||||
/** a
|
||||
* Parse the given string into an array of strings and React <a> elements
|
||||
* in the order in which they should be rendered (i.e. FIFO).
|
||||
*
|
||||
* @param {String} s the raw string to be parsed
|
||||
*
|
||||
* @returns {Array} of strings and React <a> elements in order.
|
||||
*/
|
||||
parseStringToElements: function(s) {
|
||||
var elements = [];
|
||||
var result = loop.shared.urlRegExps.fullUrlMatch.exec(s);
|
||||
var reactElementsCounter = 0; // For giving keys to each ReactElement.
|
||||
|
||||
while (result) {
|
||||
// If there's text preceding the first link, push it onto the array
|
||||
// and update the string pointer.
|
||||
if (result.index) {
|
||||
elements.push(s.substr(0, result.index));
|
||||
s = s.substr(result.index);
|
||||
}
|
||||
|
||||
// Push the first link itself, and advance the string pointer again.
|
||||
elements.push(
|
||||
<a { ...this._generateLinkAttributes(result[0]) }
|
||||
key={reactElementsCounter++}>
|
||||
{result[0]}
|
||||
</a>
|
||||
);
|
||||
s = s.substr(result[0].length);
|
||||
|
||||
// Check for another link, and perhaps continue...
|
||||
result = loop.shared.urlRegExps.fullUrlMatch.exec(s);
|
||||
}
|
||||
|
||||
if (s) {
|
||||
elements.push(s);
|
||||
}
|
||||
|
||||
return elements;
|
||||
},
|
||||
|
||||
render: function () {
|
||||
return ( <p>{ this.parseStringToElements(this.props.rawText) }</p> );
|
||||
}
|
||||
});
|
||||
|
||||
return LinkifiedTextView;
|
||||
|
||||
})(navigator.mozL10n || document.mozL10n);
|
|
@ -56,9 +56,15 @@ loop.shared.views.chat = (function(mozL10n) {
|
|||
"room-name": this.props.contentType === CHAT_CONTENT_TYPES.ROOM_NAME
|
||||
});
|
||||
|
||||
var optionalProps = {};
|
||||
if (navigator.mozLoop) {
|
||||
optionalProps.linkClickHandler = navigator.mozLoop.openURL;
|
||||
}
|
||||
|
||||
return (
|
||||
React.createElement("div", {className: classes},
|
||||
React.createElement("p", null, this.props.message),
|
||||
React.createElement(sharedViews.LinkifiedTextView, React.__spread({}, optionalProps,
|
||||
{rawText: this.props.message})),
|
||||
React.createElement("span", {className: "text-chat-arrow"}),
|
||||
this.props.showTimestamp ? this._renderTimestamp() : null
|
||||
)
|
||||
|
|
|
@ -56,9 +56,15 @@ loop.shared.views.chat = (function(mozL10n) {
|
|||
"room-name": this.props.contentType === CHAT_CONTENT_TYPES.ROOM_NAME
|
||||
});
|
||||
|
||||
var optionalProps = {};
|
||||
if (navigator.mozLoop) {
|
||||
optionalProps.linkClickHandler = navigator.mozLoop.openURL;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<p>{this.props.message}</p>
|
||||
<sharedViews.LinkifiedTextView {...optionalProps}
|
||||
rawText={this.props.message} />
|
||||
<span className="text-chat-arrow" />
|
||||
{this.props.showTimestamp ? this._renderTimestamp() : null}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
// This is derived from Diego Perini's code,
|
||||
// currently available at https://gist.github.com/dperini/729294
|
||||
|
||||
// Regular Expression for URL validation
|
||||
//
|
||||
// Original Author: Diego Perini
|
||||
// License: MIT
|
||||
//
|
||||
// Copyright (c) 2010-2013 Diego Perini (http://www.iport.it)
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person
|
||||
// obtaining a copy of this software and associated documentation
|
||||
// files (the "Software"), to deal in the Software without
|
||||
// restriction, including without limitation the rights to use,
|
||||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
var loop = loop || {};
|
||||
loop.shared = loop.shared || {};
|
||||
loop.shared.urlRegExps = (function() {
|
||||
|
||||
"use strict";
|
||||
|
||||
// Some https://wiki.mozilla.org/Loop/Development/RegExpDebugging for tools
|
||||
// if you need to debug changes to this:
|
||||
|
||||
var fullUrlMatch = new RegExp(
|
||||
// Protocol identifier.
|
||||
"(?:(?:https?|ftp)://)" +
|
||||
// User:pass authentication.
|
||||
"((?:\\S+(?::\\S*)?@)?" +
|
||||
"(?:" +
|
||||
// IP address dotted notation octets:
|
||||
// excludes loopback network 0.0.0.0,
|
||||
// excludes reserved space >= 224.0.0.0,
|
||||
// excludes network & broadcast addresses,
|
||||
// (first & last IP address of each class).
|
||||
"(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" +
|
||||
"(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" +
|
||||
"(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +
|
||||
"|" +
|
||||
// Host name.
|
||||
"(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)" +
|
||||
// Domain name.
|
||||
"(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*" +
|
||||
// TLD identifier.
|
||||
"(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))" +
|
||||
// Port number.
|
||||
"(?::\\d{2,5})?" +
|
||||
// Resource path.
|
||||
"(?:[/?#]\\S*)?)", "i");
|
||||
|
||||
return {
|
||||
fullUrlMatch: fullUrlMatch
|
||||
};
|
||||
|
||||
})();
|
|
@ -86,8 +86,10 @@ browser.jar:
|
|||
content/browser/loop/shared/js/mixins.js (content/shared/js/mixins.js)
|
||||
content/browser/loop/shared/js/otSdkDriver.js (content/shared/js/otSdkDriver.js)
|
||||
content/browser/loop/shared/js/views.js (content/shared/js/views.js)
|
||||
content/browser/loop/shared/js/linkifiedTextView.js (content/shared/js/linkifiedTextView.js)
|
||||
content/browser/loop/shared/js/textChatStore.js (content/shared/js/textChatStore.js)
|
||||
content/browser/loop/shared/js/textChatView.js (content/shared/js/textChatView.js)
|
||||
content/browser/loop/shared/js/urlRegExps.js (content/shared/js/urlRegExps.js)
|
||||
content/browser/loop/shared/js/utils.js (content/shared/js/utils.js)
|
||||
content/browser/loop/shared/js/validate.js (content/shared/js/validate.js)
|
||||
content/browser/loop/shared/js/websocket.js (content/shared/js/websocket.js)
|
||||
|
|
|
@ -144,9 +144,10 @@
|
|||
<script type="text/javascript" src="shared/js/fxOSActiveRoomStore.js"></script>
|
||||
<script type="text/javascript" src="shared/js/activeRoomStore.js"></script>
|
||||
<script type="text/javascript" src="shared/js/views.js"></script>
|
||||
<script type="text/javascript" src="shared/js/feedbackViews.js"></script>
|
||||
<script type="text/javascript" src="shared/js/textChatStore.js"></script>
|
||||
<script type="text/javascript" src="shared/js/textChatView.js"></script>
|
||||
<script type="text/javascript" src="shared/js/urlRegExps.js"></script>
|
||||
<script type="text/javascript" src="shared/js/linkifiedTextView.js"></script>
|
||||
<script type="text/javascript" src="js/standaloneAppStore.js"></script>
|
||||
<script type="text/javascript" src="js/standaloneClient.js"></script>
|
||||
<script type="text/javascript" src="js/standaloneMozLoop.js"></script>
|
||||
|
|
|
@ -34,6 +34,8 @@ module.exports = function(config) {
|
|||
"content/shared/js/views.js",
|
||||
"content/shared/js/textChatStore.js",
|
||||
"content/shared/js/textChatView.js",
|
||||
"content/shared/js/urlRegExps.js",
|
||||
"content/shared/js/linkifiedTextView.js",
|
||||
"standalone/content/js/multiplexGum.js",
|
||||
"standalone/content/js/standaloneAppStore.js",
|
||||
"standalone/content/js/standaloneClient.js",
|
||||
|
|
|
@ -63,6 +63,8 @@
|
|||
<script src="../../content/shared/js/views.js"></script>
|
||||
<script src="../../content/shared/js/textChatStore.js"></script>
|
||||
<script src="../../content/shared/js/textChatView.js"></script>
|
||||
<script src="../../content/shared/js/urlRegExps.js"></script>
|
||||
<script src="../../content/shared/js/linkifiedTextView.js"></script>
|
||||
|
||||
<!-- Test scripts -->
|
||||
<script src="models_test.js"></script>
|
||||
|
@ -80,6 +82,8 @@
|
|||
<script src="store_test.js"></script>
|
||||
<script src="textChatStore_test.js"></script>
|
||||
<script src="textChatView_test.js"></script>
|
||||
<script src="linkifiedTextView_test.js"></script>
|
||||
|
||||
<script>
|
||||
describe("Uncaught Error Check", function() {
|
||||
it("should load the tests without errors", function() {
|
||||
|
|
|
@ -0,0 +1,378 @@
|
|||
/*
|
||||
* Many of these tests are ported from Autolinker.js:
|
||||
*
|
||||
* https://github.com/gregjacobs/Autolinker.js/blob/master/tests/AutolinkerSpec.js
|
||||
*
|
||||
* which is released under the MIT license. Thanks to Greg Jacobs for his hard
|
||||
* work there.
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
* Original Work Copyright (c) 2014 Gregory Jacobs (http://greg-jacobs.com)
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to the
|
||||
* following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included
|
||||
* in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
describe("loop.shared.views.LinkifiedTextView", function () {
|
||||
"use strict";
|
||||
|
||||
var expect = chai.expect;
|
||||
var LinkifiedTextView = loop.shared.views.LinkifiedTextView;
|
||||
var TestUtils = React.addons.TestUtils;
|
||||
|
||||
describe("LinkifiedTextView", function () {
|
||||
function renderToMarkup(string, extraProps) {
|
||||
return React.renderToStaticMarkup(
|
||||
React.createElement(
|
||||
LinkifiedTextView,
|
||||
_.extend({rawText: string}, extraProps)));
|
||||
}
|
||||
|
||||
describe("#render", function() {
|
||||
function testRender(testData) {
|
||||
it(testData.desc, function() {
|
||||
var markup = renderToMarkup(testData.rawText,
|
||||
{suppressTarget: true, sendReferrer: true});
|
||||
|
||||
expect(markup).to.equal(testData.markup);
|
||||
});
|
||||
}
|
||||
|
||||
function testSkip(testData) {
|
||||
it.skip(testData.desc, function() {
|
||||
var markup = renderToMarkup(testData.rawText,
|
||||
{suppressTarget: true, sendReferrer: true});
|
||||
|
||||
expect(markup).to.equal(testData.markup);
|
||||
});
|
||||
}
|
||||
|
||||
describe("this.props.suppressTarget", function() {
|
||||
it("should make links w/o a target attr if suppressTarget is true",
|
||||
function() {
|
||||
var markup = renderToMarkup("http://example.com", {suppressTarget: true});
|
||||
|
||||
expect(markup).to.equal(
|
||||
'<p><a href="http://example.com" rel="noreferrer">http://example.com</a></p>');
|
||||
});
|
||||
|
||||
it("should make links with target=_blank if suppressTarget is not given",
|
||||
function() {
|
||||
var markup = renderToMarkup("http://example.com", {});
|
||||
|
||||
expect(markup).to.equal(
|
||||
'<p><a href="http://example.com" target="_blank" rel="noreferrer">http://example.com</a></p>');
|
||||
});
|
||||
});
|
||||
|
||||
describe("this.props.sendReferrer", function() {
|
||||
it("should make links w/o rel=noreferrer if sendReferrer is true",
|
||||
function() {
|
||||
var markup = renderToMarkup("http://example.com", {sendReferrer: true});
|
||||
|
||||
expect(markup).to.equal(
|
||||
'<p><a href="http://example.com" target="_blank">http://example.com</a></p>');
|
||||
});
|
||||
|
||||
it("should make links with rel=noreferrer if sendReferrer is not given",
|
||||
function() {
|
||||
var markup = renderToMarkup("http://example.com", {});
|
||||
|
||||
expect(markup).to.equal(
|
||||
'<p><a href="http://example.com" target="_blank" rel="noreferrer">http://example.com</a></p>');
|
||||
});
|
||||
});
|
||||
|
||||
describe("this.props.linkClickHandler", function () {
|
||||
function mountTestComponent(string, extraProps) {
|
||||
return TestUtils.renderIntoDocument(
|
||||
React.createElement(
|
||||
LinkifiedTextView,
|
||||
_.extend({rawText: string}, extraProps)));
|
||||
}
|
||||
|
||||
it("should be called when a generated link is clicked", function () {
|
||||
var fakeUrl = "http://example.com";
|
||||
var linkClickHandler = sinon.stub();
|
||||
var comp = mountTestComponent(fakeUrl, {linkClickHandler: linkClickHandler});
|
||||
|
||||
TestUtils.Simulate.click(comp.getDOMNode().querySelector("a"));
|
||||
|
||||
sinon.assert.calledOnce(linkClickHandler);
|
||||
});
|
||||
|
||||
it("should cause sendReferrer and suppressTarget props to be ignored",
|
||||
function() {
|
||||
var fakeUrl = "http://example.com";
|
||||
var linkClickHandler = function() {};
|
||||
|
||||
var markup = renderToMarkup("http://example.com", {
|
||||
linkClickHandler: linkClickHandler,
|
||||
sendReferrer: false,
|
||||
suppressTarget: false
|
||||
});
|
||||
|
||||
expect(markup).to.equal(
|
||||
'<p><a href="http://example.com">http://example.com</a></p>');
|
||||
});
|
||||
|
||||
describe("#_handleClickEvent", function () {
|
||||
var fakeEvent;
|
||||
var fakeUrl = "http://example.com";
|
||||
|
||||
beforeEach(function() {
|
||||
fakeEvent = {
|
||||
currentTarget: { href: fakeUrl },
|
||||
preventDefault: sinon.stub(),
|
||||
stopPropagation: sinon.stub()
|
||||
};
|
||||
});
|
||||
|
||||
it("should call preventDefault on the given event", function () {
|
||||
function linkClickHandler() {}
|
||||
var comp = mountTestComponent(
|
||||
fakeUrl, {linkClickHandler: linkClickHandler});
|
||||
|
||||
comp._handleClickEvent(fakeEvent);
|
||||
|
||||
sinon.assert.calledOnce(fakeEvent.preventDefault);
|
||||
sinon.assert.calledWithExactly(fakeEvent.stopPropagation);
|
||||
});
|
||||
|
||||
it("should call stopPropagation on the given event", function () {
|
||||
function linkClickHandler() {}
|
||||
var comp = mountTestComponent(
|
||||
fakeUrl, {linkClickHandler: linkClickHandler});
|
||||
|
||||
comp._handleClickEvent(fakeEvent);
|
||||
|
||||
sinon.assert.calledOnce(fakeEvent.stopPropagation);
|
||||
sinon.assert.calledWithExactly(fakeEvent.stopPropagation);
|
||||
});
|
||||
|
||||
it("should call this.props.linkClickHandler with event.currentTarget.href", function () {
|
||||
var linkClickHandler = sinon.stub();
|
||||
var comp = mountTestComponent(
|
||||
fakeUrl, {linkClickHandler: linkClickHandler});
|
||||
|
||||
comp._handleClickEvent(fakeEvent);
|
||||
|
||||
sinon.assert.calledOnce(linkClickHandler);
|
||||
sinon.assert.calledWithExactly(linkClickHandler, fakeUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Note that these are really integration tests with the parser and React.
|
||||
// Since we're depending on that integration to provide us with security
|
||||
// against various injection problems, it feels fairly important. That
|
||||
// said, these tests are not terribly robust in the face of markup changes
|
||||
// in the code, and over time, some of them may want to be pushed down
|
||||
// to only be unit tests against the parser or against
|
||||
// parseStringToElements. We may also want both unit and integration
|
||||
// testing for some subset of these.
|
||||
var tests = [
|
||||
{
|
||||
desc: "should only add a container to a string with no URLs",
|
||||
rawText: "This is a test.",
|
||||
markup: "<p>This is a test.</p>"
|
||||
},
|
||||
{
|
||||
desc: "should linkify a string containing only a URL",
|
||||
rawText: "http://example.com/",
|
||||
markup: '<p><a href="http://example.com/">http://example.com/</a></p>'
|
||||
},
|
||||
{
|
||||
desc: "should linkify a URL with text preceding it",
|
||||
rawText: "This is a link to http://example.com",
|
||||
markup: '<p>This is a link to <a href="http://example.com">http://example.com</a></p>'
|
||||
},
|
||||
{
|
||||
desc: "should linkify a URL with text before and after",
|
||||
rawText: "Look at http://example.com near the bottom",
|
||||
markup: '<p>Look at <a href="http://example.com">http://example.com</a> near the bottom</p>'
|
||||
},
|
||||
{
|
||||
desc: "should linkify an http URL",
|
||||
rawText: "This is an http://example.com test",
|
||||
markup: '<p>This is an <a href="http://example.com">http://example.com</a> test</p>'
|
||||
},
|
||||
{
|
||||
desc: "should linkify an https URL",
|
||||
rawText: "This is an https://example.com test",
|
||||
markup: '<p>This is an <a href="https://example.com">https://example.com</a> test</p>'
|
||||
},
|
||||
{
|
||||
desc: "should not linkify a data URL",
|
||||
rawText: "This is an data:image/png;base64,iVBORw0KGgoAAA test",
|
||||
markup: "<p>This is an data:image/png;base64,iVBORw0KGgoAAA test</p>"
|
||||
},
|
||||
{
|
||||
desc: "should linkify URLs with a port number",
|
||||
rawText: "Joe went to http://example.com:8000 today.",
|
||||
markup: '<p>Joe went to <a href="http://example.com:8000">http://example.com:8000</a> today.</p>'
|
||||
},
|
||||
{
|
||||
desc: "should linkify URLs with a port number and a trailing slash",
|
||||
rawText: "Joe went to http://example.com:8000/ today.",
|
||||
markup: '<p>Joe went to <a href="http://example.com:8000/">http://example.com:8000/</a> today.</p>'
|
||||
},
|
||||
{
|
||||
desc: "should linkify URLs with a port number and a path",
|
||||
rawText: "Joe went to http://example.com:8000/mysite/page today.",
|
||||
markup: '<p>Joe went to <a href="http://example.com:8000/mysite/page">http://example.com:8000/mysite/page</a> today.</p>'
|
||||
},
|
||||
{
|
||||
desc: "should linkify URLs with a port number and a query string",
|
||||
rawText: "Joe went to http://example.com:8000?page=index today.",
|
||||
markup: '<p>Joe went to <a href="http://example.com:8000?page=index">http://example.com:8000?page=index</a> today.</p>'
|
||||
},
|
||||
{
|
||||
desc: "should linkify a URL with a port number and a hash string",
|
||||
rawText: "Joe went to http://example.com:8000#page=index today.",
|
||||
markup: '<p>Joe went to <a href="http://example.com:8000#page=index">http://example.com:8000#page=index</a> today.</p>'
|
||||
},
|
||||
{
|
||||
desc: "should NOT include preceding ':' intros without a space",
|
||||
rawText: "the link:http://example.com/",
|
||||
markup: '<p>the link:<a href="http://example.com/">http://example.com/</a></p>'
|
||||
},
|
||||
{
|
||||
desc: "should NOT autolink URLs with 'javascript:' URI scheme",
|
||||
rawText: "do not link javascript:window.alert('hi') please",
|
||||
markup: "<p>do not link javascript:window.alert('hi') please</p>"
|
||||
},
|
||||
{
|
||||
desc: "should NOT autolink URLs with the 'JavAscriPt:' scheme",
|
||||
rawText: "do not link JavAscriPt:window.alert('hi') please",
|
||||
markup: "<p>do not link JavAscriPt:window.alert('hi') please</p>"
|
||||
},
|
||||
{
|
||||
desc: "should NOT autolink possible URLs with the 'vbscript:' URI scheme",
|
||||
rawText: "do not link vbscript:window.alert('hi') please",
|
||||
markup: "<p>do not link vbscript:window.alert('hi') please</p>"
|
||||
},
|
||||
{
|
||||
desc: "should NOT autolink URLs with the 'vBsCriPt:' URI scheme",
|
||||
rawText: "do not link vBsCriPt:window.alert('hi') please",
|
||||
markup: "<p>do not link vBsCriPt:window.alert('hi') please</p>"
|
||||
},
|
||||
{
|
||||
desc: "should NOT autolink a string in the form of 'version:1.0'",
|
||||
rawText: "version:1.0",
|
||||
markup: "<p>version:1.0</p>"
|
||||
},
|
||||
{
|
||||
desc: "should linkify an ftp URL",
|
||||
rawText: "This is an ftp://example.com test",
|
||||
markup: '<p>This is an <a href="ftp://example.com">ftp://example.com</a> test</p>'
|
||||
},
|
||||
|
||||
// We don't want to include trailing dots in URLs, even though those
|
||||
// are valid DNS names, as that should match user intent more of the
|
||||
// time, as well as avoid this stuff:
|
||||
//
|
||||
// http://saynt2day.blogspot.it/2013/03/danger-of-trailing-dot-in-domain-name.html
|
||||
//
|
||||
{
|
||||
desc: "should linkify 'http://example.com.', w/o a trailing dot",
|
||||
rawText: "Joe went to http://example.com.",
|
||||
markup: '<p>Joe went to <a href="http://example.com">http://example.com</a>.</p>'
|
||||
},
|
||||
// XXX several more tests like this we could port from Autolinkify.js
|
||||
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
|
||||
{
|
||||
desc: "should exclude invalid chars after domain part",
|
||||
rawText: "Joe went to http://www.example.com's today",
|
||||
markup: '<p>Joe went to <a href="http://www.example.com">http://www.example.com</a>'s today</p>'
|
||||
},
|
||||
{
|
||||
desc: "should not linkify protocol-relative URLs",
|
||||
rawText: "//C/Programs",
|
||||
markup: "<p>//C/Programs</p>"
|
||||
},
|
||||
// do a few tests to convince ourselves that, when our code is handled
|
||||
// HTML in the input box, the integration of our code with React should
|
||||
// cause that to rendered to appear as HTML source code, rather than
|
||||
// getting injected into our real HTML DOM
|
||||
{
|
||||
desc: "should linkify simple HTML include an href properly escaped",
|
||||
rawText: '<p>Joe went to <a href="http://www.example.com">example</a></p>',
|
||||
markup: '<p><p>Joe went to <a href="<a href="http://www.example.com">http://www.example.com</a>">example</a></p></p>'
|
||||
},
|
||||
{
|
||||
desc: "should linkify HTML with nested tags and resource path properly escaped",
|
||||
rawText: '<a href="http://example.com"><img src="http://example.com" /></a>',
|
||||
markup: '<p><a href="<a href="http://example.com">http://example.com</a>"><img src="<a href="http://example.com">http://example.com</a>" /></a></p>'
|
||||
}
|
||||
];
|
||||
|
||||
var skippedTests = [
|
||||
|
||||
// XXX lots of tests similar to below we could port:
|
||||
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
|
||||
{
|
||||
desc: "should link localhost URLs with an allowed URL scheme",
|
||||
rawText: "Joe went to http://localhost today",
|
||||
markup: '<p>Joe went to <a href="http://localhost">localhost</a></p> today'
|
||||
},
|
||||
// XXX lots of tests similar to below we could port:
|
||||
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
|
||||
{
|
||||
desc: "should not include a ? if at the end of a URL",
|
||||
rawText: "Did Joe go to http://example.com?",
|
||||
markup: '<p>Did Joe go to <a href="http://example.com">http://example.com</a>?</p>'
|
||||
},
|
||||
{
|
||||
desc: "should linkify 'check out http://example.com/monkey.', w/o trailing dots",
|
||||
rawText: "check out http://example.com/monkey...",
|
||||
markup: '<p>check out <a href="http://example.com/monkey">http://example.com/monkey</a>...</p>'
|
||||
},
|
||||
// another variant of eating too many trailing characters, it includes
|
||||
// the trailing ", which it shouldn't. Makes links inside pasted HTML
|
||||
// source be slightly broken. Not key for our target users, I suspect,
|
||||
// but still...
|
||||
{
|
||||
desc: "should linkify HTML with nested tags and a resource path properly escaped",
|
||||
rawText: '<a href="http://example.com"><img src="http://example.com/someImage.jpg" /></a>',
|
||||
markup: '<p><a href="<a href="http://example.com">http://example.com</a>"><img src="<a href="http://example.com/someImage.jpg"">http://example.com/someImage.jpg"</a> /></a></p>'
|
||||
},
|
||||
// XXX handle domains without schemes (bug 1186245)
|
||||
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
|
||||
{
|
||||
desc: "should linkify a.museum (known TLD), but not abc.qqqq",
|
||||
rawText: "a.museum should be linked, but abc.qqqq should not",
|
||||
markup: '<p><a href="http://a.museum">a.museum</a> should be linked, but abc.qqqq should not</p>'
|
||||
},
|
||||
{
|
||||
desc: "should linkify example.xyz (known TLD), but not example.etc (unknown TLD)",
|
||||
rawText: "example.xyz should be linked, example.etc should not",
|
||||
rawMarkup: '<><a href="http://example.xyz">example.xyz</a> should be linked, example.etc should not</p>'
|
||||
}
|
||||
];
|
||||
|
||||
tests.forEach(testRender);
|
||||
|
||||
// XXX Over time, we'll want to make these pass..
|
||||
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
|
||||
|
||||
skippedTests.forEach(testSkip);
|
||||
|
||||
});
|
||||
});
|
||||
});
|
|
@ -250,6 +250,21 @@ describe("loop.shared.views.TextChatView", function () {
|
|||
|
||||
expect(node.querySelector(".text-chat-entry-timestamp")).to.not.eql(null);
|
||||
});
|
||||
|
||||
// note that this is really an integration test to be sure that we don't
|
||||
// inadvertently regress using LinkifiedTextView.
|
||||
it("should linkify a URL starting with http", function (){
|
||||
view = mountTestComponent({
|
||||
showTimestamp: true,
|
||||
timestamp: "2015-06-23T22:48:39.738Z",
|
||||
type: CHAT_MESSAGE_TYPES.RECEIVED,
|
||||
contentType: CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Check out http://example.com and see what you think..."
|
||||
});
|
||||
var node = view.getDOMNode();
|
||||
|
||||
expect(node.querySelector("a")).to.not.eql(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TextChatView", function() {
|
||||
|
|
|
@ -58,6 +58,8 @@
|
|||
<script src="../content/shared/js/textChatStore.js"></script>
|
||||
<script src="../content/js/feedbackViews.js"></script>
|
||||
<script src="../content/shared/js/textChatView.js"></script>
|
||||
<script src="../content/shared/js/urlRegExps.js"></script>
|
||||
<script src="../content/shared/js/linkifiedTextView.js"></script>
|
||||
<script src="../content/js/roomStore.js"></script>
|
||||
<script src="../content/js/roomViews.js"></script>
|
||||
<script src="../content/js/conversationViews.js"></script>
|
||||
|
|
|
@ -86,9 +86,11 @@
|
|||
// Dummy function to stop warnings.
|
||||
},
|
||||
|
||||
sendTextChatMessage: function(message) {
|
||||
sendTextChatMessage: function(actionData) {
|
||||
dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({
|
||||
message: message.message
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: actionData.message,
|
||||
receivedTimestamp: actionData.sentTimestamp
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
@ -394,16 +396,17 @@
|
|||
message: "Rheet!",
|
||||
sentTimestamp: "2015-06-23T22:21:45.590Z"
|
||||
}));
|
||||
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Hi there",
|
||||
receivedTimestamp: "2015-06-23T22:21:45.590Z"
|
||||
}));
|
||||
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Hello",
|
||||
receivedTimestamp: "2015-06-23T23:24:45.590Z"
|
||||
}));
|
||||
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Nowforareallylongwordwithoutspacesorpunctuationwhichshouldcause" +
|
||||
"linewrappingissuesifthecssiswrong",
|
||||
sentTimestamp: "2015-06-23T22:23:45.590Z"
|
||||
}));
|
||||
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Check out this menu from DNA Pizza:" +
|
||||
|
@ -411,12 +414,6 @@
|
|||
"%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%",
|
||||
sentTimestamp: "2015-06-23T22:23:45.590Z"
|
||||
}));
|
||||
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Nowforareallylongwordwithoutspacesorpunctuationwhichshouldcause" +
|
||||
"linewrappingissuesifthecssiswrong",
|
||||
sentTimestamp: "2015-06-23T22:23:45.590Z"
|
||||
}));
|
||||
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "That avocado monkey-brains pie sounds tasty!",
|
||||
|
@ -427,10 +424,10 @@
|
|||
message: "What time should we meet?",
|
||||
sentTimestamp: "2015-06-23T22:27:45.590Z"
|
||||
}));
|
||||
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
|
||||
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Cool",
|
||||
sentTimestamp: "2015-06-23T22:27:45.590Z"
|
||||
message: "8:00 PM",
|
||||
receivedTimestamp: "2015-06-23T22:27:45.590Z"
|
||||
}));
|
||||
|
||||
loop.store.StoreMixin.register({
|
||||
|
|
|
@ -86,9 +86,11 @@
|
|||
// Dummy function to stop warnings.
|
||||
},
|
||||
|
||||
sendTextChatMessage: function(message) {
|
||||
sendTextChatMessage: function(actionData) {
|
||||
dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({
|
||||
message: message.message
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: actionData.message,
|
||||
receivedTimestamp: actionData.sentTimestamp
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
@ -394,16 +396,17 @@
|
|||
message: "Rheet!",
|
||||
sentTimestamp: "2015-06-23T22:21:45.590Z"
|
||||
}));
|
||||
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Hi there",
|
||||
receivedTimestamp: "2015-06-23T22:21:45.590Z"
|
||||
}));
|
||||
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Hello",
|
||||
receivedTimestamp: "2015-06-23T23:24:45.590Z"
|
||||
}));
|
||||
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Nowforareallylongwordwithoutspacesorpunctuationwhichshouldcause" +
|
||||
"linewrappingissuesifthecssiswrong",
|
||||
sentTimestamp: "2015-06-23T22:23:45.590Z"
|
||||
}));
|
||||
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Check out this menu from DNA Pizza:" +
|
||||
|
@ -411,12 +414,6 @@
|
|||
"%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%",
|
||||
sentTimestamp: "2015-06-23T22:23:45.590Z"
|
||||
}));
|
||||
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Nowforareallylongwordwithoutspacesorpunctuationwhichshouldcause" +
|
||||
"linewrappingissuesifthecssiswrong",
|
||||
sentTimestamp: "2015-06-23T22:23:45.590Z"
|
||||
}));
|
||||
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "That avocado monkey-brains pie sounds tasty!",
|
||||
|
@ -427,10 +424,10 @@
|
|||
message: "What time should we meet?",
|
||||
sentTimestamp: "2015-06-23T22:27:45.590Z"
|
||||
}));
|
||||
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
|
||||
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Cool",
|
||||
sentTimestamp: "2015-06-23T22:27:45.590Z"
|
||||
message: "8:00 PM",
|
||||
receivedTimestamp: "2015-06-23T22:27:45.590Z"
|
||||
}));
|
||||
|
||||
loop.store.StoreMixin.register({
|
||||
|
|
|
@ -135,6 +135,7 @@
|
|||
<li><a href="about:license#superfasthash">SuperFastHash License</a></li>
|
||||
<li><a href="about:license#unicode">Unicode License</a></li>
|
||||
<li><a href="about:license#ucal">University of California License</a></li>
|
||||
<li><a href="about:license#url-validation">URL Validation Regexp License</a></li>
|
||||
<li><a href="about:license#hunspell-en-US">US English Spellchecking Dictionary Licenses</a></li>
|
||||
<li><a href="about:license#v8">V8 License</a></li>
|
||||
<li><a href="about:license#vtune">VTune License</a></li>
|
||||
|
@ -4195,6 +4196,43 @@ SUCH DAMAGE.
|
|||
</pre>
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
<h1><a id="url-validation"></a>URL Validation Regexp License</h1>
|
||||
|
||||
<p>This license applies to the following file:</p>
|
||||
|
||||
<ul>
|
||||
<li class="path">
|
||||
browser/components/loop/content/shared/js/urlRegExps.js
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<pre>
|
||||
Copyright (c) 2010-2013 Diego Perini (http://www.iport.it)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
</pre>
|
||||
|
||||
<hr>
|
||||
|
||||
<h1><a id="hunspell-en-US"></a>US English Spellchecking Dictionary Licenses</h1>
|
||||
|
|
Загрузка…
Ссылка в новой задаче