From 66aca7ad062e2c007f7f228954349604923c3e57 Mon Sep 17 00:00:00 2001 From: "Andrew Chaney (netuoso)" Date: Sun, 12 Nov 2017 21:27:46 -0600 Subject: [PATCH 01/70] Show the image upload helper on all replies, even comments --- src/app/components/elements/ReplyEditor.jsx | 32 +++++++++------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/app/components/elements/ReplyEditor.jsx b/src/app/components/elements/ReplyEditor.jsx index 063f0c4cab..9e36965a35 100644 --- a/src/app/components/elements/ReplyEditor.jsx +++ b/src/app/components/elements/ReplyEditor.jsx @@ -538,24 +538,20 @@ class ReplyEditor extends React.Component { tabIndex={2} /> - {type === 'submit_story' && ( -

- {tt( - 'reply_editor.insert_images_by_dragging_dropping' - )} - {noClipboardData - ? '' - : tt( - 'reply_editor.pasting_from_the_clipboard' - )} - {tt('reply_editor.or_by')}{' '} - - {tt( - 'reply_editor.selecting_them' - )} - . -

- )} +

+ {tt( + 'reply_editor.insert_images_by_dragging_dropping' + )} + {noClipboardData + ? '' + : tt( + 'reply_editor.pasting_from_the_clipboard' + )} + {tt('reply_editor.or_by')}{' '} + + {tt('reply_editor.selecting_them')} + . +

{progress.message && (
{progress.message} From 7fb0465e888975a904e0cfbe1c1dca1de53c7609 Mon Sep 17 00:00:00 2001 From: Iain Maitland Date: Mon, 11 Dec 2017 15:03:06 -0500 Subject: [PATCH 02/70] add a test for the transaction saga --- src/app/redux/TransactionSaga.js | 8 +- src/app/redux/TransactionSaga.test.js | 253 ++++++++++++++++++++++++++ 2 files changed, 257 insertions(+), 4 deletions(-) create mode 100644 src/app/redux/TransactionSaga.test.js diff --git a/src/app/redux/TransactionSaga.js b/src/app/redux/TransactionSaga.js index a062c1b201..f33535dafa 100644 --- a/src/app/redux/TransactionSaga.js +++ b/src/app/redux/TransactionSaga.js @@ -57,7 +57,7 @@ const hook = { accepted_withdraw_vesting, }; -function* preBroadcast_transfer({ operation }) { +export function* preBroadcast_transfer({ operation }) { let memoStr = operation.memo; if (memoStr) { memoStr = toStringUtf8(memoStr); @@ -498,7 +498,7 @@ function* accepted_account_update({ operation }) { // function* preBroadcast_account_witness_vote({operation, username}) { // } -function* preBroadcast_comment({ operation, username }) { +export function* preBroadcast_comment({ operation, username }) { if (!operation.author) operation.author = username; let permlink = operation.permlink; const { @@ -583,7 +583,7 @@ function* preBroadcast_comment({ operation, username }) { return comment_op; } -function* createPermlink(title, author, parent_author, parent_permlink) { +export function* createPermlink(title, author, parent_author, parent_permlink) { let permlink; if (title && title.trim() !== '') { let s = slug(title); @@ -618,7 +618,7 @@ function* createPermlink(title, author, parent_author, parent_permlink) { import diff_match_patch from 'diff-match-patch'; const dmp = new diff_match_patch(); -function createPatch(text1, text2) { +export function createPatch(text1, text2) { if (!text1 && text1 === '') return undefined; const patches = dmp.patch_make(text1, text2); const patch = dmp.patch_toText(patches); diff --git a/src/app/redux/TransactionSaga.test.js b/src/app/redux/TransactionSaga.test.js new file mode 100644 index 0000000000..7cf9f6b536 --- /dev/null +++ b/src/app/redux/TransactionSaga.test.js @@ -0,0 +1,253 @@ +/* global describe, it, before, beforeEach, after, afterEach */ + +import chai, { expect } from 'chai'; +import { call, select } from 'redux-saga/effects'; +import { api } from '@steemit/steem-js'; +import { + preBroadcast_comment, + createPermlink, + createPatch, + watchForBroadcast, + watchForUpdateAuthorities, + watchForUpdateMeta, + watchForRecoverAccount, + preBroadcast_transfer, +} from './TransactionSaga'; +import * as transactionActions from 'app/redux/TransactionReducer'; +import { DEBT_TICKER } from 'app/client_config'; + +let operation = { + type: 'comment', + author: 'Alice', + body: + "The Body is a pretty long chunck of text that represents the user's voice, it seems they have much to say, and this is one place where they can do that.", + category: 'hi', + json_metadata: { + tags: ['hi'], + app: 'steemit/0.1', + format: 'markdown', + }, + parent_author: 'candide', + parent_permlink: 'cool', + title: 'test', + __config: {}, + errorCallback: () => '', + successCallback: () => '', + memo: '#testing', +}; + +const { + author, + category, + errorCallback, + successCallback, + parent_author, + parent_permlink, + type, + __config, + json_metadata, + title, + body, + memo, +} = operation; + +const username = 'Beatrice'; + +describe('TransactionSaga', () => { + describe('createPatch', () => { + it('should return undefined if empty arguments are passed', () => { + const actual = createPatch('', ''); + expect(actual).to.eql(undefined); + }); + it('should return the patch that reconciles two different strings', () => { + const testString = + 'there is something interesting going on here that I do not fully understand it is seemingly complex but it is actually quite simple'; + const actual = createPatch(testString, testString + 'ILU'); + expect(actual).to.eql( + '@@ -120,12 +120,15 @@\n quite simple\n+ILU\n' + ); + }); + }); + + describe('watchForBroadcast', () => { + const gen = watchForBroadcast(); + it('should call takeEvery with BROADCAST_OPERATION', () => { + const actual = gen.next().value; + const expected = { + '@@redux-saga/IO': true, + TAKE: 'transaction/BROADCAST_OPERATION', + }; + expect(actual).to.eql(expected); + }); + }); + + describe('watchForUpdateAuthorities', () => { + const gen = watchForUpdateAuthorities(); + it('should call takeEvery with UPDATE_AUTHORITIES', () => { + const actual = gen.next().value; + const expected = { + '@@redux-saga/IO': true, + TAKE: 'transaction/UPDATE_AUTHORITIES', + }; + expect(actual).to.eql(expected); + }); + }); + + describe('watchForUpdateMeta', () => { + const gen = watchForUpdateMeta(); + it('should call takeEvery with UPDATE_META', () => { + const actual = gen.next().value; + const expected = { + '@@redux-saga/IO': true, + TAKE: 'transaction/UPDATE_META', + }; + expect(actual).to.eql(expected); + }); + }); + + describe('preBroadcast_transfer', () => { + const operationSansMemo = { + ...operation, + memo: undefined, + }; + const arg = { operation: operationSansMemo }; + it('should return select object if it has a memo attribute with string value starting with #', () => { + const genR = preBroadcast_transfer({ operation }); + const actual = genR.next().value; + const expected = select(state => + state.user.getIn(['current', 'private_keys', 'memo_private']) + ); + expect(actual).to.have.all.keys('@@redux-saga/IO', 'SELECT'); + }); + it('should return the operation unchanged if it has no memo attribute', () => { + let gen = preBroadcast_transfer(arg); + const actual = gen.next().value; + expect(actual).to.eql(operationSansMemo); + }); + }); + + describe('createPermlink', () => { + const gen = createPermlink( + title, + author, + parent_author, + parent_permlink + ); + it('should call the api to get a permlink if the title is valid', () => { + const actual = gen.next().value; + const mockCall = call( + [api, api.getContentAsync], + author, + title + ); + expect(actual).to.eql(mockCall); + }); + it('should return a string containing the transformed data from the api', () => { + const permlink = gen.next({ body: 'test' }).value; + expect(permlink).to.contain('test'); // TODO: cannot deep equal due to date stamp at runtime. + }); + it('should generate own permlink, independent of api if title is empty', () => { + const gen2 = createPermlink( + '', + author, + parent_author, + parent_permlink + ); + const actual = gen2.next().value; + expect(actual).to.contain( + `re-${parent_author}-${parent_permlink}-` + ); // TODO: cannot deep equal due to random hash at runtime. + }); + }); + + describe('preBroadcast_comment', () => { + let gen = preBroadcast_comment({ operation, username }); + + it('should call createPermlink', () => { + const permlink = gen.next( + title, + author, + parent_author, + parent_permlink + ).value; + const actual = permlink.next().value; + const expected = call([api, api.getContentAsync], author, title); + expect(expected).to.eql(actual); + }); + it('should return the comment options array.', () => { + let actual = gen.next('mock-permlink-123').value; + const expected = [ + [ + 'comment', + { + author, + category, + errorCallback, + successCallback, + parent_author, + parent_permlink, + type, + __config, + memo, + permlink: 'mock-permlink-123', + json_metadata: JSON.stringify(json_metadata), + title: new Buffer((title || '').trim(), 'utf-8'), + body: new Buffer(body, 'utf-8'), // TODO: new Buffer is deprecated, prefer Buffer.from() + }, + ], + ]; + expect(actual).to.eql(expected); + }); + it('should return a patch as body value if patch is smaller than body.', () => { + const originalBod = body + 'minor difference'; + operation.__config.originalBody = originalBod; + gen = preBroadcast_comment({ operation, username }); + gen.next(title, author, parent_author, parent_permlink); + const actual = gen.next('mock-permlink-123').value; + const expected = Buffer.from( + createPatch(originalBod, body), + 'utf-8' + ); + expect(actual[0][1].body).to.eql(expected); + }); + it('should return body as body value if patch is larger than body.', () => { + const originalBod = 'major difference'; + operation.__config.originalBody = originalBod; + gen = preBroadcast_comment({ operation, username }); + gen.next(title, author, parent_author, parent_permlink); + const actual = gen.next('mock-permlink-123').value; + const expected = Buffer.from(body, 'utf-8'); + expect(actual[0][1].body).to.eql(expected, 'utf-8'); + }); + it('should include comment_options and autoVote if specified.', () => { + operation.__config.comment_options = true; + operation.__config.autoVote = true; + gen = preBroadcast_comment({ operation, username }); + gen.next(title, author, parent_author, parent_permlink); + const actual = gen.next('mock-permlink-123').value; + const expectedCommentOptions = [ + 'comment_options', + { + author, + permlink: 'mock-permlink-123', + max_accepted_payout: ['1000000.000', DEBT_TICKER].join(' '), + percent_steem_dollars: 10000, + allow_votes: true, + allow_curation_rewards: true, + extensions: [], + }, + ]; + const expectedAutoVoteOptions = [ + 'vote', + { + voter: author, + author, + permlink: 'mock-permlink-123', + weight: 10000, + }, + ]; + expect(actual[1]).to.eql(expectedCommentOptions); + expect(actual[2]).to.eql(expectedAutoVoteOptions); + }); + }); +}); From e8f8395f44509af6dc486b232df716fa3be84acb Mon Sep 17 00:00:00 2001 From: Iain Maitland Date: Wed, 27 Dec 2017 15:26:28 -0500 Subject: [PATCH 03/70] respond to ben's feedback for the transaction saga test --- src/app/redux/TransactionSaga.test.js | 90 +++++++++++---------------- 1 file changed, 37 insertions(+), 53 deletions(-) diff --git a/src/app/redux/TransactionSaga.test.js b/src/app/redux/TransactionSaga.test.js index 7cf9f6b536..38deb26bee 100644 --- a/src/app/redux/TransactionSaga.test.js +++ b/src/app/redux/TransactionSaga.test.js @@ -13,10 +13,9 @@ import { watchForRecoverAccount, preBroadcast_transfer, } from './TransactionSaga'; -import * as transactionActions from 'app/redux/TransactionReducer'; import { DEBT_TICKER } from 'app/client_config'; -let operation = { +const operation = { type: 'comment', author: 'Alice', body: @@ -36,21 +35,6 @@ let operation = { memo: '#testing', }; -const { - author, - category, - errorCallback, - successCallback, - parent_author, - parent_permlink, - type, - __config, - json_metadata, - title, - body, - memo, -} = operation; - const username = 'Beatrice'; describe('TransactionSaga', () => { @@ -128,17 +112,17 @@ describe('TransactionSaga', () => { describe('createPermlink', () => { const gen = createPermlink( - title, - author, - parent_author, - parent_permlink + operation.title, + operation.author, + operation.parent_author, + operation.parent_permlink ); it('should call the api to get a permlink if the title is valid', () => { const actual = gen.next().value; const mockCall = call( [api, api.getContentAsync], - author, - title + operation.author, + operation.title ); expect(actual).to.eql(mockCall); }); @@ -149,13 +133,13 @@ describe('TransactionSaga', () => { it('should generate own permlink, independent of api if title is empty', () => { const gen2 = createPermlink( '', - author, - parent_author, - parent_permlink + operation.author, + operation.parent_author, + operation.parent_permlink ); const actual = gen2.next().value; expect(actual).to.contain( - `re-${parent_author}-${parent_permlink}-` + `re-${operation.parent_author}-${operation.parent_permlink}-` ); // TODO: cannot deep equal due to random hash at runtime. }); }); @@ -165,13 +149,13 @@ describe('TransactionSaga', () => { it('should call createPermlink', () => { const permlink = gen.next( - title, - author, - parent_author, - parent_permlink + operation.title, + operation.author, + operation.parent_author, + operation.parent_permlink ).value; const actual = permlink.next().value; - const expected = call([api, api.getContentAsync], author, title); + const expected = call([api, api.getContentAsync], operation.author, operation.title); expect(expected).to.eql(actual); }); it('should return the comment options array.', () => { @@ -180,32 +164,32 @@ describe('TransactionSaga', () => { [ 'comment', { - author, - category, - errorCallback, - successCallback, - parent_author, - parent_permlink, - type, - __config, - memo, + author: operation.author, + category: operation.category, + errorCallback: operation.errorCallback, + successCallback: operation.successCallback, + parent_author: operation.parent_author, + parent_permlink: operation.parent_permlink, + type: operation.type, + __config: operation.__config, + memo: operation.memo, permlink: 'mock-permlink-123', - json_metadata: JSON.stringify(json_metadata), - title: new Buffer((title || '').trim(), 'utf-8'), - body: new Buffer(body, 'utf-8'), // TODO: new Buffer is deprecated, prefer Buffer.from() + json_metadata: JSON.stringify(operation.json_metadata), + title: new Buffer((operation.title || '').trim(), 'utf-8'), + body: new Buffer(operation.body, 'utf-8'), // TODO: new Buffer is deprecated, prefer Buffer.from() }, ], ]; expect(actual).to.eql(expected); }); it('should return a patch as body value if patch is smaller than body.', () => { - const originalBod = body + 'minor difference'; + const originalBod = operation.body + 'minor difference'; operation.__config.originalBody = originalBod; gen = preBroadcast_comment({ operation, username }); - gen.next(title, author, parent_author, parent_permlink); + gen.next(operation.title, operation.author, operation.parent_author, operation.parent_permlink); const actual = gen.next('mock-permlink-123').value; const expected = Buffer.from( - createPatch(originalBod, body), + createPatch(originalBod, operation.body), 'utf-8' ); expect(actual[0][1].body).to.eql(expected); @@ -214,21 +198,21 @@ describe('TransactionSaga', () => { const originalBod = 'major difference'; operation.__config.originalBody = originalBod; gen = preBroadcast_comment({ operation, username }); - gen.next(title, author, parent_author, parent_permlink); + gen.next(operation.title, operation.author, operation.parent_author, operation.parent_permlink); const actual = gen.next('mock-permlink-123').value; - const expected = Buffer.from(body, 'utf-8'); + const expected = Buffer.from(operation.body, 'utf-8'); expect(actual[0][1].body).to.eql(expected, 'utf-8'); }); it('should include comment_options and autoVote if specified.', () => { operation.__config.comment_options = true; operation.__config.autoVote = true; gen = preBroadcast_comment({ operation, username }); - gen.next(title, author, parent_author, parent_permlink); + gen.next(operation.title, operation.author, operation.parent_author, operation.parent_permlink); const actual = gen.next('mock-permlink-123').value; const expectedCommentOptions = [ 'comment_options', { - author, + author: operation.author, permlink: 'mock-permlink-123', max_accepted_payout: ['1000000.000', DEBT_TICKER].join(' '), percent_steem_dollars: 10000, @@ -240,8 +224,8 @@ describe('TransactionSaga', () => { const expectedAutoVoteOptions = [ 'vote', { - voter: author, - author, + voter: operation.author, + author: operation.author, permlink: 'mock-permlink-123', weight: 10000, }, From 773fc1af4f911def13d95ba199e4f97052128837 Mon Sep 17 00:00:00 2001 From: Benjamin Chodoroff Date: Sat, 30 Dec 2017 15:24:39 -0500 Subject: [PATCH 04/70] add create_user endpoint closes #2243 --- src/db/utils/find_user.js | 18 ++++++++++++++-- src/server/api/general.js | 44 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/db/utils/find_user.js b/src/db/utils/find_user.js index d83ef9d9f2..9f288269ef 100644 --- a/src/db/utils/find_user.js +++ b/src/db/utils/find_user.js @@ -18,13 +18,27 @@ function findByProvider(provider_user_id, resolve) { }); } -export default function findUser({ user_id, email, uid, provider_user_id }) { - console.log('-- findUser -->', user_id, email, uid, provider_user_id); +export default function findUser({ + user_id, + email, + uid, + provider_user_id, + name, +}) { + console.log( + '-- findUser -->', + user_id, + email, + uid, + provider_user_id, + name + ); return new Promise(resolve => { let query; const where_or = []; if (user_id) where_or.push({ id: user_id }); if (email) where_or.push({ email }); + if (name) where_or.push({ name }); if (uid) where_or.push({ uid }); if (where_or.length > 0) { query = { diff --git a/src/server/api/general.js b/src/server/api/general.js index 9bf2db192a..7227639887 100644 --- a/src/server/api/general.js +++ b/src/server/api/general.js @@ -269,6 +269,50 @@ export default function useGeneralApi(app) { recordWebEvent(this, 'api/accounts', account ? account.name : 'n/a'); }); + router.post('/create_user', koaBody, function*() { + if (rateLimitReq(this, this.req)) return; + + const { name, email, secret } = + typeof this.request.body === 'string' + ? JSON.parse(this.request.body) + : this.request.body; + + logRequest('create_user', this, { name, email }); + + try { + if (secret !== process.env.CREATE_USER_SECRET) + throw new Error('invalid secret'); + + if (!emailRegex.test(email.toLowerCase())) + throw new Error('not valid email: ' + email); + const existingUser = yield findUser({ + email: esc(email), + name: esc(name), + }); + if (existingUser) { + this.body = JSON.stringify({ + success: false, + error: 'user with this email or name already exists', + }); + this.status = 400; + } else { + const user = yield models.User.create({ + name: esc(name), + email: esc(email), + }); + this.body = JSON.stringify({ + success: true, + user, + }); + } + } catch (error) { + console.error('Error in /create_user api call', error); + this.body = JSON.stringify({ error: error.message }); + this.status = 500; + } + recordWebEvent(this, 'api/create_user', { name, email }); + }); + router.post('/update_email', koaBody, function*() { if (rateLimitReq(this, this.req)) return; const params = this.request.body; From 322e990bb018245ad2906829d9cb740f5f44b45c Mon Sep 17 00:00:00 2001 From: TimCliff Date: Sun, 31 Dec 2017 21:08:31 -0600 Subject: [PATCH 05/70] Closes #2249 - Add BlockTrades as alternate signup method in FAQ --- src/app/help/en/faq.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/help/en/faq.md b/src/app/help/en/faq.md index dbdcc739ab..7f3e0e5a9a 100644 --- a/src/app/help/en/faq.md +++ b/src/app/help/en/faq.md @@ -287,7 +287,9 @@ There is a third-party tool called SteemCr There is a third-party tool called AnonSteem that accepts bitcoin, Litecoin, STEEM, or SBD to anonymously create a Steem account. You do not need to have an existing Steem blockchain account to use the service, but there is a charge on top of the blockchain account creation fee for using the service. -There is also a third-party tool called SteemConnect that allows you to create accounts by paying or delegating the account creation fee. There is no additional fee to use the service, but does require an existing Steem blockchain account to pay the account creation fee to create the account. +There is a third-party tool called SteemConnect that allows you to create accounts by paying or delegating the account creation fee. There is no additional fee to use the service, but does require an existing Steem blockchain account to pay the account creation fee to create the account. + +There is a third-party tool called BlockTrades that accepts bitcoin, Litecoin, STEEM, SBD, BitShares, Dash, Dogecoin, Ethereum, and more to create a Steem account. You can also send extra tokens to pre-load the account with additional Steem Power. You do not need to have an existing Steem blockchain account to use the service, but there is a charge on top of the blockchain account creation fee for using the service. ^ ## It is not letting me create an account with my phone number. What should I do? From ee49359674ab04baae025dd7ed71f5202ce61999 Mon Sep 17 00:00:00 2001 From: Benjamin Chodoroff Date: Tue, 2 Jan 2018 09:18:19 -0500 Subject: [PATCH 06/70] create account and identity also --- src/server/api/general.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/server/api/general.js b/src/server/api/general.js index 7227639887..c98b483fcf 100644 --- a/src/server/api/general.js +++ b/src/server/api/general.js @@ -282,7 +282,6 @@ export default function useGeneralApi(app) { try { if (secret !== process.env.CREATE_USER_SECRET) throw new Error('invalid secret'); - if (!emailRegex.test(email.toLowerCase())) throw new Error('not valid email: ' + email); const existingUser = yield findUser({ @@ -300,9 +299,22 @@ export default function useGeneralApi(app) { name: esc(name), email: esc(email), }); + const account = yield models.Account.create({ + user_id: user.id, + name: esc(name), + }); + const identity = yield models.Identity.create({ + user_id: user.id, + name: esc(name), + provider: 'email', + verified: true, + email: user.email, + }); this.body = JSON.stringify({ success: true, user, + account, + identity, }); } } catch (error) { From ca4732b838956870bf5666738ba37e124ae7401b Mon Sep 17 00:00:00 2001 From: Benjamin Chodoroff Date: Tue, 2 Jan 2018 10:30:57 -0500 Subject: [PATCH 07/70] add owner_key --- src/server/api/general.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/server/api/general.js b/src/server/api/general.js index c98b483fcf..c9b534dc1d 100644 --- a/src/server/api/general.js +++ b/src/server/api/general.js @@ -269,15 +269,25 @@ export default function useGeneralApi(app) { recordWebEvent(this, 'api/accounts', account ? account.name : 'n/a'); }); + /** + * Provides an endpoint to create user, account, and identity records. + * Used by faucet. + * + * HTTP params: + * name + * email + * owner_key + * secret + */ router.post('/create_user', koaBody, function*() { if (rateLimitReq(this, this.req)) return; - const { name, email, secret } = + const { name, email, owner_key, secret } = typeof this.request.body === 'string' ? JSON.parse(this.request.body) : this.request.body; - logRequest('create_user', this, { name, email }); + logRequest('create_user', this, { name, email, owner_key }); try { if (secret !== process.env.CREATE_USER_SECRET) @@ -309,6 +319,7 @@ export default function useGeneralApi(app) { provider: 'email', verified: true, email: user.email, + owner_key: esc(owner_key), }); this.body = JSON.stringify({ success: true, From a08f4b36cccc7036d6cdf51b86d0da94495f3d7f Mon Sep 17 00:00:00 2001 From: Benjamin Chodoroff Date: Tue, 2 Jan 2018 11:19:10 -0500 Subject: [PATCH 08/70] add a wrapping div to any content containing an iframe closes #2260 --- src/app/components/cards/MarkdownViewer.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/components/cards/MarkdownViewer.jsx b/src/app/components/cards/MarkdownViewer.jsx index 9cd4662417..92c186f3d4 100644 --- a/src/app/components/cards/MarkdownViewer.jsx +++ b/src/app/components/cards/MarkdownViewer.jsx @@ -87,9 +87,9 @@ class MarkdownViewer extends Component { let renderedText = html ? text : remarkable.render(text); - // If content is a bare iframe, wrap it in html tags - if (renderedText.match(/^$/)) { - renderedText = '' + renderedText + ''; + // If content contains an iframe, wrap it in html tags + if (renderedText.match(/