diff --git a/package.json b/package.json index 47d6c9aced..906417a953 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.3", "lodash": "^4.13.1", - "marked": "^0.3.5", + "commonmark": "^0.27.0", "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", "optimist": "^0.6.1", "q": "^1.4.1", diff --git a/src/Markdown.js b/src/Markdown.js index a7b267b110..18c888b541 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -14,20 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import marked from 'marked'; - -// marked only applies the default options on the high -// level marked() interface, so we do it here. -const marked_options = Object.assign({}, marked.defaults, { - gfm: true, - tables: true, - breaks: true, - pedantic: false, - sanitize: true, - smartLists: true, - smartypants: false, - xhtml: true, // return self closing tags (ie.
not
) -}); +import commonmark from 'commonmark'; /** * Class that wraps marked, adding the ability to see whether @@ -36,16 +23,7 @@ const marked_options = Object.assign({}, marked.defaults, { */ export default class Markdown { constructor(input) { - const lexer = new marked.Lexer(marked_options); - this.tokens = lexer.lex(input); - } - - _copyTokens() { - // copy tokens (the parser modifies its input arg) - const tokens_copy = this.tokens.slice(); - // it also has a 'links' property, because this is javascript - // and why wouldn't you have an array that also has properties? - return Object.assign(tokens_copy, this.tokens); + this.input = input } isPlainText() { @@ -64,65 +42,41 @@ export default class Markdown { is_plain = false; } - const dummy_renderer = {}; - for (const k of Object.keys(marked.Renderer.prototype)) { + const dummy_renderer = new commonmark.HtmlRenderer(); + for (const k of Object.keys(commonmark.HtmlRenderer.prototype)) { dummy_renderer[k] = setNotPlain; } // text and paragraph are just text - dummy_renderer.text = function(t){return t;} - dummy_renderer.paragraph = function(t){return t;} + dummy_renderer.text = function(t) { return t; } + dummy_renderer.paragraph = function(t) { return t; } - // ignore links where text is just the url: - // this ignores plain URLs that markdown has - // detected whilst preserving markdown syntax links - dummy_renderer.link = function(href, title, text) { - if (text != href) { - is_plain = false; - } - } - - const dummy_options = Object.assign({}, marked_options, { - renderer: dummy_renderer, - }); - const dummy_parser = new marked.Parser(dummy_options); - dummy_parser.parse(this._copyTokens()); + const dummy_parser = new commonmark.Parser(); + dummy_renderer.render(dummy_parser.parse(this.input)); return is_plain; } toHTML() { - const real_renderer = new marked.Renderer(); - real_renderer.link = function(href, title, text) { - // prevent marked from turning plain URLs - // into links, because its algorithm is fairly - // poor. Let's send plain URLs rather than - // badly linkified ones (the linkifier Vector - // uses on message display is way better, eg. - // handles URLs with closing parens at the end). - if (text == href) { - return href; - } - return marked.Renderer.prototype.link.apply(this, arguments); - } + const parser = new commonmark.Parser(); - real_renderer.paragraph = (text) => { - // The tokens at the top level are the 'blocks', so if we - // have more than one, there are multiple 'paragraphs'. - // If there is only one top level token, just return the + const renderer = new commonmark.HtmlRenderer({safe: true}); + const real_paragraph = renderer.paragraph; + renderer.paragraph = function(node, entering) { + // If there is only one top level node, just return the // bare text: it's a single line of text and so should be - // 'inline', rather than necessarily wrapped in its own - // p tag. If, however, we have multiple tokens, each gets + // 'inline', rather than unnecessarily wrapped in its own + // p tag. If, however, we have multiple nodes, each gets // its own p tag to keep them as separate paragraphs. - if (this.tokens.length == 1) { - return text; + var par = node; + while (par.parent) { + par = par.parent + } + if (par.firstChild != par.lastChild) { + real_paragraph.call(this, node, entering); } - return '

' + text + '

'; } - const real_options = Object.assign({}, marked_options, { - renderer: real_renderer, - }); - const real_parser = new marked.Parser(real_options); - return real_parser.parse(this._copyTokens()); + var parsed = parser.parse(this.input); + return renderer.render(parsed); } } diff --git a/src/Signup.js b/src/Signup.js index f148ac2419..d3643bd749 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -203,7 +203,17 @@ class Register extends Signup { } else if (error.errcode == 'M_INVALID_USERNAME') { throw new Error("User names may only contain alphanumeric characters, underscores or dots!"); } else if (error.httpStatus >= 400 && error.httpStatus < 500) { - throw new Error(`Registration failed! (${error.httpStatus})`); + let msg = null; + if (error.message) { + msg = error.message; + } else if (error.errcode) { + msg = error.errcode; + } + if (msg) { + throw new Error(`Registration failed! (${error.httpStatus}) - ${msg}`); + } else { + throw new Error(`Registration failed! (${error.httpStatus}) - That's all we know.`); + } } else if (error.httpStatus >= 500 && error.httpStatus < 600) { throw new Error( `Server error during registration! (${error.httpStatus})` diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 79e93bb990..7d74e2ee02 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1045,6 +1045,7 @@ module.exports = React.createClass({ defaultHsUrl={this.getDefaultHsUrl()} defaultIsUrl={this.getDefaultIsUrl()} brand={this.props.config.brand} + teamsConfig={this.props.config.teamsConfig} customHsUrl={this.getCurrentHsUrl()} customIsUrl={this.getCurrentIsUrl()} registrationUrl={this.props.registrationUrl} diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 269aabed9b..fb24b61504 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -49,6 +49,16 @@ module.exports = React.createClass({ email: React.PropTypes.string, username: React.PropTypes.string, guestAccessToken: React.PropTypes.string, + teamsConfig: React.PropTypes.shape({ + // Email address to request new teams + supportEmail: React.PropTypes.string, + teams: React.PropTypes.arrayOf(React.PropTypes.shape({ + // The displayed name of the team + "name": React.PropTypes.string, + // The suffix with which every team email address ends + "emailSuffix": React.PropTypes.string, + })).required, + }), defaultDeviceDisplayName: React.PropTypes.string, @@ -254,6 +264,7 @@ module.exports = React.createClass({ defaultUsername={this.state.formVals.username} defaultEmail={this.state.formVals.email} defaultPassword={this.state.formVals.password} + teamsConfig={this.props.teamsConfig} guestUsername={this.props.username} minPasswordLength={MIN_PASSWORD_LENGTH} onError={this.onFormValidationFailed} diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index 33809fbfd6..3e07302a91 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -38,6 +38,16 @@ module.exports = React.createClass({ defaultEmail: React.PropTypes.string, defaultUsername: React.PropTypes.string, defaultPassword: React.PropTypes.string, + teamsConfig: React.PropTypes.shape({ + // Email address to request new teams + supportEmail: React.PropTypes.string, + teams: React.PropTypes.arrayOf(React.PropTypes.shape({ + // The displayed name of the team + "name": React.PropTypes.string, + // The suffix with which every team email address ends + "emailSuffix": React.PropTypes.string, + })).required, + }), // A username that will be used if no username is entered. // Specifying this param will also warn the user that entering @@ -62,7 +72,8 @@ module.exports = React.createClass({ getInitialState: function() { return { - fieldValid: {} + fieldValid: {}, + selectedTeam: null, }; }, @@ -119,6 +130,25 @@ module.exports = React.createClass({ } }, + onSelectTeam: function(teamIndex) { + let team = this._getSelectedTeam(teamIndex); + if (team) { + this.refs.email.value = this.refs.email.value.split("@")[0]; + } + this.setState({ + selectedTeam: team, + showSupportEmail: teamIndex === "other", + }); + }, + + _getSelectedTeam: function(teamIndex) { + if (this.props.teamsConfig && + this.props.teamsConfig.teams[teamIndex]) { + return this.props.teamsConfig.teams[teamIndex]; + } + return null; + }, + /** * Returns true if all fields were valid last time * they were validated. @@ -139,11 +169,15 @@ module.exports = React.createClass({ switch (field_id) { case FIELD_EMAIL: - this.markFieldValid( - field_id, - this.refs.email.value == '' || Email.looksValid(this.refs.email.value), - "RegistrationForm.ERR_EMAIL_INVALID" - ); + let email = this.refs.email.value; + if (this.props.teamsConfig) { + let team = this.state.selectedTeam; + if (team) { + email = email + "@" + team.emailSuffix; + } + } + let valid = email === '' || Email.looksValid(email); + this.markFieldValid(field_id, valid, "RegistrationForm.ERR_EMAIL_INVALID"); break; case FIELD_USERNAME: // XXX: SPEC-1 @@ -222,17 +256,64 @@ module.exports = React.createClass({ return cls; }, + _renderEmailInputSuffix: function() { + let suffix = null; + if (!this.state.selectedTeam) { + return suffix; + } + let team = this.state.selectedTeam; + if (team) { + suffix = "@" + team.emailSuffix; + } + return suffix; + }, + render: function() { var self = this; - var emailSection, registerButton; + var emailSection, teamSection, teamAdditionSupport, registerButton; if (this.props.showEmail) { + let emailSuffix = this._renderEmailInputSuffix(); emailSection = ( - +
+ + {emailSuffix ? : null } +
); + if (this.props.teamsConfig) { + teamSection = ( + + ); + if (this.props.teamsConfig.supportEmail && this.state.showSupportEmail) { + teamAdditionSupport = ( + + If your team is not listed, email  + + {this.props.teamsConfig.supportEmail} + + + ); + } + } } if (this.props.onRegisterClick) { registerButton = ( @@ -248,6 +329,9 @@ module.exports = React.createClass({ return (
+ {teamSection} + {teamAdditionSupport} +
{emailSection}